├── .github
├── PULL_REQUEST_TEMPLATE.md
├── scripts
│ └── external-packages-license-check.go
└── workflows
│ ├── build.yml
│ ├── cla-check.yml
│ ├── license.yaml
│ ├── lint.yaml
│ ├── performance.yaml
│ ├── pro_tests.yaml
│ ├── security.yaml
│ ├── snap.yml
│ ├── spread.yml
│ └── tests.yaml
├── .gitignore
├── .golangci.yaml
├── LICENSE
├── README.md
├── SECURITY.md
├── cmd
├── chisel
│ ├── cmd_cut.go
│ ├── cmd_debug.go
│ ├── cmd_find.go
│ ├── cmd_find_test.go
│ ├── cmd_help.go
│ ├── cmd_info.go
│ ├── cmd_info_test.go
│ ├── cmd_version.go
│ ├── cmd_version_test.go
│ ├── export_test.go
│ ├── helpers.go
│ ├── log.go
│ ├── main.go
│ └── main_test.go
├── mkversion.sh
└── version.go
├── docs
└── _static
│ ├── package-slices.svg
│ └── slice-of-ubuntu.png
├── go.mod
├── go.sum
├── internal
├── apachetestutil
│ └── manifest.go
├── apacheutil
│ ├── log.go
│ ├── suite_test.go
│ ├── util.go
│ └── util_test.go
├── archive
│ ├── archive.go
│ ├── archive_test.go
│ ├── credentials.go
│ ├── credentials_test.go
│ ├── export_test.go
│ ├── log.go
│ ├── suite_test.go
│ └── testarchive
│ │ └── testarchive.go
├── cache
│ ├── cache.go
│ ├── cache_test.go
│ └── suite_test.go
├── control
│ ├── control.go
│ ├── control_test.go
│ ├── helpers.go
│ ├── helpers_test.go
│ └── suite_test.go
├── deb
│ ├── chrorder.go
│ ├── chrorder
│ │ └── main.go
│ ├── export_test.go
│ ├── extract.go
│ ├── extract_test.go
│ ├── helpers.go
│ ├── helpers_test.go
│ ├── log.go
│ ├── suite_test.go
│ ├── version.go
│ └── version_test.go
├── fsutil
│ ├── create.go
│ ├── create_test.go
│ ├── log.go
│ └── suite_test.go
├── manifestutil
│ ├── log.go
│ ├── manifestutil.go
│ ├── manifestutil_test.go
│ ├── report.go
│ ├── report_test.go
│ └── suite_test.go
├── pgputil
│ ├── log.go
│ ├── openpgp.go
│ ├── openpgp_test.go
│ └── suite_test.go
├── scripts
│ ├── log.go
│ ├── scripts.go
│ ├── scripts_test.go
│ └── suite_test.go
├── setup
│ ├── export_test.go
│ ├── fetch.go
│ ├── fetch_test.go
│ ├── log.go
│ ├── setup.go
│ ├── setup_test.go
│ ├── suite_test.go
│ ├── tarjan.go
│ ├── tarjan_test.go
│ └── yaml.go
├── slicer
│ ├── log.go
│ ├── slicer.go
│ ├── slicer_test.go
│ └── suite_test.go
├── strdist
│ ├── export_test.go
│ ├── log.go
│ ├── strdist.go
│ ├── strdist_test.go
│ └── suite_test.go
└── testutil
│ ├── archive.go
│ ├── base.go
│ ├── containschecker.go
│ ├── containschecker_test.go
│ ├── exec.go
│ ├── exec_test.go
│ ├── export_test.go
│ ├── filecontentchecker.go
│ ├── filecontentchecker_test.go
│ ├── filepresencechecker.go
│ ├── filepresencechecker_test.go
│ ├── intcheckers.go
│ ├── intcheckers_test.go
│ ├── nopcloser.go
│ ├── permutation.go
│ ├── permutation_test.go
│ ├── pgpkeys.go
│ ├── pkgdata.go
│ ├── pkgdata_test.go
│ ├── reindent.go
│ ├── reindent_test.go
│ ├── testutil_test.go
│ └── treedump.go
├── public
├── jsonwall
│ ├── jsonwall.go
│ ├── jsonwall_test.go
│ ├── log.go
│ └── suite_test.go
└── manifest
│ ├── log.go
│ ├── manifest.go
│ ├── manifest_test.go
│ └── suite_test.go
├── snap
└── snapcraft.yaml
├── spread.yaml
└── tests
├── basic
└── task.yaml
├── find
└── task.yaml
├── info
└── task.yaml
├── pro-archives
├── chisel-releases
│ ├── chisel.yaml
│ └── slices
│ │ └── hello.yaml
└── task.yaml
└── use-a-custom-chisel-release
└── task.yaml
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | - [ ] Have you signed the [CLA](http://www.ubuntu.com/legal/contributors/)?
2 |
3 | -----
4 |
--------------------------------------------------------------------------------
/.github/scripts/external-packages-license-check.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "io/fs"
7 | "os"
8 | "os/exec"
9 | "path/filepath"
10 | "regexp"
11 | "strings"
12 | )
13 |
14 | var licenseRegexp = regexp.MustCompile("// SPDX-License-Identifier: ([^\\s]*)$")
15 |
16 | func fileLicense(path string) (string, error) {
17 | file, err := os.Open(path)
18 | if err != nil {
19 | return "", err
20 | }
21 | defer file.Close()
22 | scanner := bufio.NewScanner(file)
23 |
24 | for scanner.Scan() {
25 | line := scanner.Text()
26 | matches := licenseRegexp.FindStringSubmatch(line)
27 | if len(matches) > 0 {
28 | return matches[1], nil
29 | }
30 | }
31 |
32 | return "", nil
33 | }
34 |
35 | func checkDirLicense(path string, valid string) error {
36 | return filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
37 | if !strings.HasSuffix(path, ".go") {
38 | return nil
39 | }
40 | license, err := fileLicense(path)
41 | if err != nil {
42 | return err
43 | }
44 | if license == "" {
45 | return fmt.Errorf("cannot find a valid license in %q", path)
46 | }
47 | if license != valid {
48 | return fmt.Errorf("expected %q to be %q, got %q", path, valid, license)
49 | }
50 | return nil
51 | })
52 | }
53 |
54 | func run() error {
55 | // Check external packages licenses.
56 | err := checkDirLicense("public", "Apache-2.0")
57 | if err != nil {
58 | return fmt.Errorf("invalid license in exported package: %s", err)
59 | }
60 |
61 | // Check the internal dependencies of the external packages.
62 | output, err := exec.Command("sh", "-c", "go list -deps -test ./public/*").Output()
63 | if err != nil {
64 | return err
65 | }
66 | lines := strings.Split(string(output), "\n")
67 | var internalPkgs []string
68 | for _, line := range lines {
69 | if strings.Contains(line, "github.com/canonical/chisel/internal") {
70 | internalPkgs = append(internalPkgs, strings.TrimPrefix(line, "github.com/canonical/chisel/"))
71 | }
72 | }
73 | for _, pkg := range internalPkgs {
74 | err := checkDirLicense(pkg, "Apache-2.0")
75 | if err != nil {
76 | return fmt.Errorf("invalid license in depedency %q: %s", pkg, err)
77 | }
78 | }
79 |
80 | return nil
81 | }
82 |
83 | func main() {
84 | err := run()
85 | if err != nil {
86 | fmt.Fprintf(os.Stderr, "%s\n", err)
87 | os.Exit(1)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | tags: [v*]
7 | branches: [main]
8 | paths-ignore:
9 | - '**.md'
10 | pull_request:
11 | branches: [main]
12 | release:
13 | types: [published]
14 |
15 | jobs:
16 | build-chisel:
17 | name: Build Chisel
18 | runs-on: ubuntu-22.04
19 | strategy:
20 | fail-fast: false
21 | matrix:
22 | include:
23 | - arch: 'amd64'
24 | machine_arch: 'X86-64'
25 | - arch: 'arm'
26 | machine_arch: 'ARM'
27 | - arch: 'arm64'
28 | machine_arch: 'AArch64'
29 | - arch: 'ppc64le'
30 | machine_arch: 'PowerPC64'
31 | - arch: 'riscv64'
32 | machine_arch: 'RISC-V'
33 | - arch: 's390x'
34 | machine_arch: 'S/390'
35 | steps:
36 | - uses: actions/checkout@v3
37 | with:
38 | fetch-depth: 0
39 |
40 | # The checkout action in previous step overwrites annonated tag
41 | # with lightweight tags breaking ``git describe``
42 | # See https://github.com/actions/checkout/issues/882
43 | # and https://github.com/actions/checkout/issues/290
44 | # The following step is a workaround to restore the latest tag
45 | # to it's original state.
46 | - name: Restore (annonated) tag
47 | run: git fetch --force origin ${GITHUB_REF}:${GITHUB_REF}
48 | if: ${{ github.ref_type == 'tag' }}
49 |
50 | - uses: actions/setup-go@v3
51 | with:
52 | go-version-file: 'go.mod'
53 |
54 | - name: Build Chisel for linux/${{ matrix.arch }}
55 | id: build
56 | env:
57 | GOOS: "linux"
58 | GOARCH: ${{ matrix.arch }}
59 | CGO_ENABLED: "0"
60 | run: |
61 | echo "Generating version file"
62 | go generate ./cmd/
63 |
64 | echo "Building for $GOOS $GOARCH"
65 | go build -trimpath -ldflags='-s -w' ./cmd/chisel
66 |
67 | # Get version via "chisel version" to ensure it matches that exactly
68 | CHISEL_VERSION=$(GOOS=linux GOARCH=amd64 go run ./cmd/chisel version)
69 | echo "Version: $CHISEL_VERSION"
70 |
71 | # Version should not be "unknown"
72 | [ "$CHISEL_VERSION" != "unknown" ] || exit 1
73 |
74 | # Share variables with subsequent steps
75 | echo "CHISEL_VERSION=${CHISEL_VERSION}" >>$GITHUB_OUTPUT
76 |
77 | - name: Test if is executable
78 | run: test -x ./chisel
79 |
80 | - name: Test if binary has the right machine architecture
81 | run: |
82 | [ "$(readelf -h chisel | grep 'Machine:' | awk -F' ' '{print $NF}')" == "${{ matrix.machine_arch }}" ]
83 |
84 | - name: Create archive
85 | id: archive
86 | env:
87 | GOOS: "linux"
88 | GOARCH: ${{ matrix.arch }}
89 | CHISEL_VERSION: ${{ steps.build.outputs.CHISEL_VERSION }}
90 | run: |
91 | ARCHIVE_FILE=chisel_${CHISEL_VERSION}_${GOOS}_${GOARCH}.tar.gz
92 | ARCHIVE_FILE_SHA384="${ARCHIVE_FILE}.sha384"
93 | echo "Creating archive $ARCHIVE_FILE"
94 |
95 | mkdir -p dist/
96 | cp chisel LICENSE README.md dist/
97 | find dist -printf "%P\n" | tar -czf $ARCHIVE_FILE --no-recursion -C dist -T -
98 | sha384sum "${ARCHIVE_FILE}" > "${ARCHIVE_FILE_SHA384}"
99 |
100 | # Share variables with subsequent steps
101 | echo "ARCHIVE_FILE=${ARCHIVE_FILE}" >>$GITHUB_OUTPUT
102 | echo "ARCHIVE_FILE_SHA384=${ARCHIVE_FILE_SHA384}" >>$GITHUB_OUTPUT
103 |
104 | - name: Upload archive as Actions artifact
105 | uses: actions/upload-artifact@v4
106 | with:
107 | name: ${{ steps.archive.outputs.ARCHIVE_FILE }}
108 | path: |
109 | ${{ steps.archive.outputs.ARCHIVE_FILE }}
110 | ${{ steps.archive.outputs.ARCHIVE_FILE_SHA384 }}
111 |
112 | - name: Upload archive to release
113 | env:
114 | CHISEL_VERSION: ${{ steps.build.outputs.CHISEL_VERSION }}
115 | ARCHIVE_FILE: ${{ steps.archive.outputs.ARCHIVE_FILE }}
116 | ARCHIVE_FILE_SHA384: ${{ steps.archive.outputs.ARCHIVE_FILE_SHA384 }}
117 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
118 | if: ${{ github.event_name == 'release' }}
119 | run: |
120 | echo "Uploading $ARCHIVE_FILE to release $CHISEL_VERSION"
121 | gh release upload $CHISEL_VERSION $ARCHIVE_FILE
122 | gh release upload $CHISEL_VERSION $ARCHIVE_FILE_SHA384
123 |
--------------------------------------------------------------------------------
/.github/workflows/cla-check.yml:
--------------------------------------------------------------------------------
1 | name: CLA check
2 |
3 | on:
4 | pull_request:
5 | branches: [main]
6 |
7 | jobs:
8 | cla-check:
9 | runs-on: ubuntu-22.04
10 | steps:
11 | - name: Check if Canonical's Contributor License Agreement has been signed
12 | uses: canonical/has-signed-canonical-cla@v2
13 |
--------------------------------------------------------------------------------
/.github/workflows/license.yaml:
--------------------------------------------------------------------------------
1 | name: License check
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | external-packages:
11 | runs-on: ubuntu-22.04
12 | name: External packages license check
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - uses: actions/setup-go@v3
17 | with:
18 | go-version-file: 'go.mod'
19 |
20 | - name: Run license check
21 | run: |
22 | go run .github/scripts/external-packages-license-check.go
23 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yaml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - '**.md'
7 | pull_request:
8 | branches: [main]
9 |
10 | jobs:
11 | lint:
12 | name: Lint
13 | runs-on: ubuntu-22.04
14 | steps:
15 | - uses: actions/checkout@v3
16 |
17 | - uses: actions/setup-go@v3
18 | with:
19 | go-version-file: 'go.mod'
20 |
21 | - name: Ensure no formatting changes
22 | run: |
23 | go fmt ./...
24 | git diff --exit-code
25 |
26 | - name: Check bugs and unused code
27 | uses: golangci/golangci-lint-action@v3
28 | with:
29 | version: v1.63.4
30 |
--------------------------------------------------------------------------------
/.github/workflows/performance.yaml:
--------------------------------------------------------------------------------
1 | name: Performance
2 |
3 | on:
4 | pull_request_target:
5 | branches: [main]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-22.04
10 | strategy:
11 | matrix:
12 | version:
13 | - name: base
14 | sha: ${{ github.event.pull_request.base.sha }}
15 | - name: head
16 | sha: ${{ github.event.pull_request.head.sha }}
17 | steps:
18 | - uses: actions/checkout@v4
19 | with:
20 | ref: ${{ matrix.version.sha }}
21 |
22 | - uses: actions/setup-go@v4
23 | with:
24 | go-version-file: 'go.mod'
25 |
26 | - name: Build
27 | run: |
28 | go build -o ${{ matrix.version.name }} ./cmd/chisel
29 |
30 | - uses: actions/upload-artifact@v4
31 | with:
32 | name: ${{ matrix.version.name }}
33 | path: ${{ matrix.version.name }}
34 |
35 |
36 | benchmark-info:
37 | runs-on: ubuntu-22.04
38 | needs: build
39 | name: Benchmark chisel info (chisel-releases 24.04)
40 | permissions:
41 | pull-requests: write
42 | steps:
43 | - name: Download base
44 | uses: actions/download-artifact@v4
45 | with:
46 | name: base
47 |
48 | - name: Download head
49 | uses: actions/download-artifact@v4
50 | with:
51 | name: head
52 |
53 | - name: Download chisel-releases
54 | uses: actions/checkout@v4
55 | with:
56 | repository: canonical/chisel-releases
57 | ref: ubuntu-24.04
58 | path: chisel-releases
59 |
60 | - name: Install hyperfine
61 | run: sudo apt-get install -y hyperfine
62 |
63 | - name: Run benchmark
64 | id: benchmark
65 | run: |
66 | msg_file="$(mktemp)"
67 | echo "msg_file=$msg_file" >> $GITHUB_OUTPUT
68 | chmod +x base head
69 | hyperfine --export-markdown "$msg_file" "./base info --release ./chisel-releases 'python3.12_core'" -n "BASE" "./head info --release ./chisel-releases 'python3.12_core'" -n "HEAD"
70 |
71 | - name: Post message to PR
72 | uses: mshick/add-pr-comment@v2
73 | with:
74 | message-path: ${{ steps.benchmark.outputs.msg_file }}
75 |
--------------------------------------------------------------------------------
/.github/workflows/pro_tests.yaml:
--------------------------------------------------------------------------------
1 | name: Pro Tests
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | paths-ignore:
7 | - '**.md'
8 | schedule:
9 | - cron: "0 0 */2 * *"
10 | workflow_run:
11 | workflows: ["CLA check"]
12 | types:
13 | - completed
14 |
15 | jobs:
16 | real-archive-tests:
17 | name: Real Archive Tests
18 | if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
19 | runs-on: ubuntu-22.04
20 | container:
21 | # Do not change to newer releases as "fips" may not be available there.
22 | image: ubuntu:20.04
23 | steps:
24 | - name: Install dependencies
25 | run: |
26 | set -x
27 | # git is needed for Go setup.
28 | apt-get update && apt-get install -y git sudo ubuntu-advantage-tools acl
29 |
30 | - uses: actions/checkout@v3
31 |
32 | - uses: actions/setup-go@v3
33 | with:
34 | go-version-file: 'go.mod'
35 |
36 | - name: Run real archive tests
37 | env:
38 | PRO_TOKEN: ${{ secrets.PRO_TOKEN }}
39 | run: |
40 | set -ex
41 |
42 | detach() {
43 | sudo pro detach --assume-yes || true
44 | sudo rm -f /etc/apt/auth.conf.d/90ubuntu-advantage
45 | }
46 | trap detach EXIT
47 |
48 | # Attach pro token and enable services
49 | sudo pro attach ${PRO_TOKEN} --no-auto-enable
50 |
51 | # Cannot enable fips and fips-updates at the same time.
52 | # Hack: enable fips, copy the credentials and then after enabling
53 | # other services, add the credentials back.
54 | sudo pro enable fips --assume-yes
55 | sudo cp /etc/apt/auth.conf.d/90ubuntu-advantage /etc/apt/auth.conf.d/90ubuntu-advantage.fips-creds
56 | # This will disable the fips service.
57 | sudo pro enable fips-updates esm-apps esm-infra --assume-yes
58 | # Add the fips credentials back.
59 | sudo sh -c 'cat /etc/apt/auth.conf.d/90ubuntu-advantage.fips-creds >> /etc/apt/auth.conf.d/90ubuntu-advantage'
60 | sudo rm /etc/apt/auth.conf.d/90ubuntu-advantage.fips-creds
61 |
62 | # Make apt credentials accessible to USER.
63 | sudo setfacl -m u:$USER:r /etc/apt/auth.conf.d/90ubuntu-advantage
64 |
65 | # Run tests on Pro real archives.
66 | go test ./internal/archive/ --real-pro-archive
67 |
68 | spread-tests:
69 | name: Spread tests
70 | if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
71 | runs-on: ubuntu-22.04
72 | steps:
73 | - uses: actions/checkout@v3
74 |
75 | - uses: actions/checkout@v3
76 | with:
77 | repository: snapcore/spread
78 | path: _spread
79 |
80 | - uses: actions/setup-go@v3
81 | with:
82 | go-version: '>=1.17.0'
83 |
84 | - name: Build and run spread
85 | env:
86 | PRO_TOKEN: ${{ secrets.PRO_TOKEN }}
87 | run: |
88 | (cd _spread/cmd/spread && go build)
89 | _spread/cmd/spread/spread -v tests/pro-archives
90 |
--------------------------------------------------------------------------------
/.github/workflows/security.yaml:
--------------------------------------------------------------------------------
1 | name: Security
2 |
3 | on:
4 | schedule:
5 | - cron: "0 1 * * *"
6 |
7 | jobs:
8 | scan:
9 | name: Scan for known vulnerabilities
10 | runs-on: ubuntu-latest
11 | env:
12 | TRIVY_RESULTS: 'trivy-results.sarif'
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - name: Run Trivy vulnerability scanner in fs mode
17 | uses: aquasecurity/trivy-action@master
18 | with:
19 | scan-type: 'fs'
20 | scan-ref: '.'
21 | format: 'sarif'
22 | output: ${{ env.TRIVY_RESULTS }}
23 |
24 | - name: Upload Trivy scan results to GitHub Security tab
25 | uses: github/codeql-action/upload-sarif@v3
26 | with:
27 | sarif_file: ${{ env.TRIVY_RESULTS }}
28 |
29 | - uses: actions/upload-artifact@v4
30 | with:
31 | name: ${{ env.TRIVY_RESULTS }}
32 | path: ${{ env.TRIVY_RESULTS }}
33 |
34 | - name: Raise error on HIGH,CRITICAL vulnerabilities
35 | uses: aquasecurity/trivy-action@master
36 | with:
37 | scan-type: 'fs'
38 | scan-ref: '.'
39 | severity: 'CRITICAL,HIGH'
40 | exit-code: '1'
41 |
--------------------------------------------------------------------------------
/.github/workflows/snap.yml:
--------------------------------------------------------------------------------
1 | name: Snap
2 |
3 | on:
4 | pull_request:
5 | branches: [main]
6 | release:
7 | types: [published]
8 |
9 | env:
10 | SNAP_NAME: chisel
11 |
12 | jobs:
13 | build:
14 | name: Build
15 | runs-on: ubuntu-latest
16 | outputs:
17 | chisel-snap: ${{ steps.build-chisel-snap.outputs.snap }}
18 |
19 | steps:
20 | - name: Checkout chisel repo
21 | uses: actions/checkout@v3
22 | with:
23 | fetch-depth: 0
24 |
25 | - name: Build chisel Snap
26 | id: build-chisel-snap
27 | uses: snapcore/action-build@v1
28 |
29 | - name: Attach chisel snap to GH workflow execution
30 | uses: actions/upload-artifact@v4
31 | with:
32 | name: ${{ steps.build-chisel-snap.outputs.snap }}
33 | path: ${{ steps.build-chisel-snap.outputs.snap }}
34 |
35 | test:
36 | name: Test
37 | runs-on: ubuntu-latest
38 | needs: [build]
39 | outputs:
40 | chisel-version: ${{ steps.install-chisel-snap.outputs.version }}
41 |
42 | steps:
43 | - uses: actions/download-artifact@v4
44 | with:
45 | name: ${{ needs.build.outputs.chisel-snap }}
46 |
47 | - name: Install the chisel snap
48 | id: install-chisel-snap
49 | run: |
50 | set -ex
51 | # Install the chisel snap from the artifact built in the previous job
52 | sudo snap install --dangerous ${{ needs.build.outputs.chisel-snap }}
53 |
54 | # Make sure chisel is installed
55 | echo "version=$(chisel version)" | tee -a "$GITHUB_OUTPUT"
56 |
57 | - name: Run smoke test
58 | run: |
59 | set -ex
60 | mkdir chisel-rootfs
61 | chisel cut --root chisel-rootfs/ libc6_libs
62 | find chisel-rootfs/ | grep libc
63 |
64 | promote:
65 | if: ${{ github.event_name == 'release' }}
66 | name: Promote
67 | runs-on: ubuntu-latest
68 | needs: test
69 | strategy:
70 | fail-fast: false
71 | matrix:
72 | arch: [amd64, arm64, armhf, ppc64el, s390x, riscv64]
73 | env:
74 | TRACK: latest
75 | DEFAULT_RISK: edge
76 | TO_RISK: candidate
77 |
78 | steps:
79 | - name: Install snapcraft
80 | run: sudo snap install snapcraft --classic
81 |
82 | - name: Wait for ${{ needs.test.outputs.chisel-version }} to be released
83 | env:
84 | SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
85 | run: |
86 | while ! `snapcraft status ${{ env.SNAP_NAME }} --track ${{ env.TRACK }} --arch ${{ matrix.arch }} \
87 | | grep "${{ env.DEFAULT_RISK }}" \
88 | | awk -F' ' '{print $2}' \
89 | | grep -Fxq "${{ needs.test.outputs.chisel-version }}"`; do
90 | echo "[${{ matrix.arch }}] Waiting for ${{ needs.test.outputs.chisel-version }} \
91 | to be released to ${{ env.TRACK }}/${{ env.DEFAULT_RISK }}..."
92 | sleep 10
93 | done
94 |
95 | # It would be easier to use `snapcraft promote`, but there's an error when trying
96 | # to avoid the prompt with the "--yes" option:
97 | # > 'latest/edge' is not a valid set value for --from-channel when using --yes.
98 | - name: Promote ${{ needs.test.outputs.chisel-version }} (${{ matrix.arch }}) to ${{ env.TO_RISK }}
99 | env:
100 | SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
101 | run: |
102 | revision="$(snapcraft status ${{ env.SNAP_NAME }} --track ${{ env.TRACK }} --arch ${{ matrix.arch }} \
103 | | grep "${{ env.DEFAULT_RISK }}" \
104 | | awk -F' ' '{print $3}')"
105 |
106 | snapcraft release ${{ env.SNAP_NAME }} \
107 | $revision \
108 | ${{ env.TRACK }}/${{ env.TO_RISK }}
109 |
--------------------------------------------------------------------------------
/.github/workflows/spread.yml:
--------------------------------------------------------------------------------
1 | name: Run spread tests
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | paths-ignore:
7 | - '**.md'
8 | pull_request:
9 | branches: [main]
10 | schedule:
11 | - cron: "0 0 */2 * *"
12 |
13 | jobs:
14 | spread-tests:
15 | name: Spread tests
16 | runs-on: ubuntu-22.04
17 | steps:
18 | - uses: actions/checkout@v3
19 |
20 | - uses: actions/checkout@v3
21 | with:
22 | repository: snapcore/spread
23 | path: _spread
24 |
25 | - uses: actions/setup-go@v3
26 | with:
27 | go-version: '>=1.17.0'
28 |
29 | - name: Build and run spread
30 | run: |
31 | (cd _spread/cmd/spread && go build)
32 | _spread/cmd/spread/spread -v
33 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yaml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | paths-ignore:
7 | - '**.md'
8 | pull_request:
9 | branches: [main]
10 |
11 | jobs:
12 | unit-tests:
13 | runs-on: ubuntu-22.04
14 | name: Unit Tests
15 | env:
16 | TEST_COVERAGE_FILE: test-coverage.out
17 | TEST_COVERAGE_HTML_FILE: test-coverage.html
18 | steps:
19 | - uses: actions/checkout@v3
20 |
21 | - uses: actions/setup-go@v3
22 | with:
23 | go-version-file: 'go.mod'
24 |
25 | - name: Run unit tests
26 | run: go test -v -cover -coverprofile=${TEST_COVERAGE_FILE} ./...
27 |
28 | - name: Convert test coverage to HTML
29 | if: always()
30 | continue-on-error: true
31 | run: |
32 | set -eu
33 | if [ -f ${TEST_COVERAGE_FILE} ]
34 | then
35 | go tool cover -html=${TEST_COVERAGE_FILE} \
36 | -o=${TEST_COVERAGE_HTML_FILE}
37 | fi
38 |
39 | - name: Upload HTML test coverage
40 | uses: actions/upload-artifact@v4
41 | if: always()
42 | continue-on-error: true
43 | with:
44 | name: chisel-test-coverage.html
45 | path: ./*.html
46 |
47 | real-archive-tests:
48 | name: Real Archive Tests
49 | runs-on: ubuntu-22.04
50 | container:
51 | # Do not change to newer releases as "fips" may not be available there.
52 | image: ubuntu:20.04
53 | steps:
54 | - name: Install git (needed for Go setup)
55 | run: |
56 | set -x
57 | apt-get update && apt-get install -y git
58 |
59 | - uses: actions/checkout@v3
60 |
61 | - uses: actions/setup-go@v3
62 | with:
63 | go-version-file: 'go.mod'
64 |
65 | - name: Run real archive tests
66 | run: |
67 | go test ./internal/archive/ --real-archive
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .spread*
2 |
--------------------------------------------------------------------------------
/.golangci.yaml:
--------------------------------------------------------------------------------
1 | linters:
2 | # Disable all linters.
3 | # Default: false
4 | disable-all: true
5 | # Enable specific linter
6 | # https://golangci-lint.run/usage/linters/#enabled-by-default
7 | enable:
8 | - errcheck
9 | - staticcheck
10 | - unused
11 |
12 | issues:
13 | exclude-rules:
14 | # exclusions for errcheck
15 | - path: "^.*/log.go$"
16 | text: "globalLogger.Output.*not checked"
17 | linters:
18 | - errcheck
19 | - path: "^.*_test.go$"
20 | text: "release.Render.*not checked"
21 | linters:
22 | - errcheck
23 | - path: "^.*_test.go$"
24 | text: "release.Walk.*not checked"
25 | linters:
26 | - errcheck
27 | - path: "internal/setup/fetch.go"
28 | text: "lockFile.Unlock.*not checked"
29 | linters:
30 | - errcheck
31 | # exclusions for unused
32 | # addDebugCommand is an useful function that may be used later
33 | - path: "cmd/chisel/main.go"
34 | text: "addDebugCommand.*unused"
35 | linters:
36 | - unused
37 | # exclude common (unused) issues from log.go files
38 | - path: "^.*/log.go$"
39 | text: "logf.*unused"
40 | linters:
41 | - unused
42 | - path: "^.*/log.go$"
43 | text: "debugf.*unused"
44 | linters:
45 | - unused
46 | - path: "^.*/log.go$"
47 | text: "globalDebug.*unused"
48 | linters:
49 | - unused
50 | - path: "^.*/log.go$"
51 | text: "globalLogger.*unused"
52 | linters:
53 | - unused
54 | - path: "^.*.go$"
55 | text: "\"golang.org/x/crypto/openpgp/\\w+\" is deprecated"
56 | linters:
57 | - staticcheck
58 | max-issues-per-linter: 0
59 | max-same-issues: 0
60 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | To report a security issue, file a [Private Security Report](https://github.com/Canonical/chisel/security/advisories/new) with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue.
6 |
7 | The [Ubuntu Security disclosure and embargo policy](https://ubuntu.com/security/disclosure-policy) contains more information about what you can expect when you contact us and what we expect from you.
--------------------------------------------------------------------------------
/cmd/chisel/cmd_cut.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/jessevdk/go-flags"
5 |
6 | "github.com/canonical/chisel/internal/archive"
7 | "github.com/canonical/chisel/internal/cache"
8 | "github.com/canonical/chisel/internal/setup"
9 | "github.com/canonical/chisel/internal/slicer"
10 | )
11 |
12 | var shortCutHelp = "Cut a tree with selected slices"
13 | var longCutHelp = `
14 | The cut command uses the provided selection of package slices
15 | to create a new filesystem tree in the root location.
16 |
17 | By default it fetches the slices for the same Ubuntu version as the
18 | current host, unless the --release flag is used.
19 | `
20 |
21 | var cutDescs = map[string]string{
22 | "release": "Chisel release name or directory (e.g. ubuntu-22.04)",
23 | "root": "Root for generated content",
24 | "arch": "Package architecture",
25 | }
26 |
27 | type cmdCut struct {
28 | Release string `long:"release" value-name:"
"`
29 | RootDir string `long:"root" value-name:"" required:"yes"`
30 | Arch string `long:"arch" value-name:""`
31 |
32 | Positional struct {
33 | SliceRefs []string `positional-arg-name:"" required:"yes"`
34 | } `positional-args:"yes"`
35 | }
36 |
37 | func init() {
38 | addCommand("cut", shortCutHelp, longCutHelp, func() flags.Commander { return &cmdCut{} }, cutDescs, nil)
39 | }
40 |
41 | func (cmd *cmdCut) Execute(args []string) error {
42 | if len(args) > 0 {
43 | return ErrExtraArgs
44 | }
45 |
46 | sliceKeys := make([]setup.SliceKey, len(cmd.Positional.SliceRefs))
47 | for i, sliceRef := range cmd.Positional.SliceRefs {
48 | sliceKey, err := setup.ParseSliceKey(sliceRef)
49 | if err != nil {
50 | return err
51 | }
52 | sliceKeys[i] = sliceKey
53 | }
54 |
55 | release, err := obtainRelease(cmd.Release)
56 | if err != nil {
57 | return err
58 | }
59 |
60 | selection, err := setup.Select(release, sliceKeys)
61 | if err != nil {
62 | return err
63 | }
64 |
65 | archives := make(map[string]archive.Archive)
66 | for archiveName, archiveInfo := range release.Archives {
67 | openArchive, err := archive.Open(&archive.Options{
68 | Label: archiveName,
69 | Version: archiveInfo.Version,
70 | Arch: cmd.Arch,
71 | Suites: archiveInfo.Suites,
72 | Components: archiveInfo.Components,
73 | Pro: archiveInfo.Pro,
74 | CacheDir: cache.DefaultDir("chisel"),
75 | PubKeys: archiveInfo.PubKeys,
76 | })
77 | if err != nil {
78 | if err == archive.ErrCredentialsNotFound {
79 | logf("Archive %q ignored: credentials not found", archiveName)
80 | continue
81 | }
82 | return err
83 | }
84 | archives[archiveName] = openArchive
85 | }
86 |
87 | err = slicer.Run(&slicer.RunOptions{
88 | Selection: selection,
89 | Archives: archives,
90 | TargetDir: cmd.RootDir,
91 | })
92 | return err
93 | }
94 |
--------------------------------------------------------------------------------
/cmd/chisel/cmd_debug.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type cmdDebug struct{}
4 |
5 | var shortDebugHelp = "Run debug commands"
6 | var longDebugHelp = `
7 | The debug command contains a selection of additional sub-commands.
8 |
9 | Debug commands can be removed without notice and may not work on
10 | non-development systems.
11 | `
12 |
--------------------------------------------------------------------------------
/cmd/chisel/cmd_find.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 | "strings"
7 | "text/tabwriter"
8 |
9 | "github.com/jessevdk/go-flags"
10 |
11 | "github.com/canonical/chisel/internal/setup"
12 | "github.com/canonical/chisel/internal/strdist"
13 | )
14 |
15 | var shortFindHelp = "Find existing slices"
16 | var longFindHelp = `
17 | The find command queries the slice definitions for matching slices.
18 | Globs (* and ?) are allowed in the query.
19 |
20 | By default it fetches the slices for the same Ubuntu version as the
21 | current host, unless the --release flag is used.
22 | `
23 |
24 | var findDescs = map[string]string{
25 | "release": "Chisel release name or directory (e.g. ubuntu-22.04)",
26 | }
27 |
28 | type cmdFind struct {
29 | Release string `long:"release" value-name:""`
30 |
31 | Positional struct {
32 | Query []string `positional-arg-name:"" required:"yes"`
33 | } `positional-args:"yes"`
34 | }
35 |
36 | func init() {
37 | addCommand("find", shortFindHelp, longFindHelp, func() flags.Commander { return &cmdFind{} }, findDescs, nil)
38 | }
39 |
40 | func (cmd *cmdFind) Execute(args []string) error {
41 | if len(args) > 0 {
42 | return ErrExtraArgs
43 | }
44 |
45 | release, err := obtainRelease(cmd.Release)
46 | if err != nil {
47 | return err
48 | }
49 |
50 | slices, err := findSlices(release, cmd.Positional.Query)
51 | if err != nil {
52 | return err
53 | }
54 | if len(slices) == 0 {
55 | fmt.Fprintf(Stderr, "No matching slices for \"%s\"\n", strings.Join(cmd.Positional.Query, " "))
56 | return nil
57 | }
58 |
59 | w := tabWriter()
60 | fmt.Fprintf(w, "Slice\tSummary\n")
61 | for _, s := range slices {
62 | fmt.Fprintf(w, "%s\t%s\n", s, "-")
63 | }
64 | w.Flush()
65 |
66 | return nil
67 | }
68 |
69 | // match reports whether a slice (partially) matches the query.
70 | func match(slice *setup.Slice, query string) bool {
71 | var term string
72 | switch {
73 | case strings.HasPrefix(query, "_"):
74 | query = strings.TrimPrefix(query, "_")
75 | term = slice.Name
76 | case strings.Contains(query, "_"):
77 | term = slice.String()
78 | default:
79 | term = slice.Package
80 | }
81 | query = strings.ReplaceAll(query, "**", "⁑")
82 | return strdist.Distance(term, query, distWithGlobs, 0) <= 1
83 | }
84 |
85 | // findSlices returns slices from the provided release that match all of the
86 | // query strings (AND).
87 | func findSlices(release *setup.Release, query []string) (slices []*setup.Slice, err error) {
88 | slices = []*setup.Slice{}
89 | for _, pkg := range release.Packages {
90 | for _, slice := range pkg.Slices {
91 | if slice == nil {
92 | continue
93 | }
94 | allMatch := true
95 | for _, term := range query {
96 | if !match(slice, term) {
97 | allMatch = false
98 | break
99 | }
100 | }
101 | if allMatch {
102 | slices = append(slices, slice)
103 | }
104 | }
105 | }
106 | sort.Slice(slices, func(i, j int) bool {
107 | return slices[i].String() < slices[j].String()
108 | })
109 | return slices, nil
110 | }
111 |
112 | func tabWriter() *tabwriter.Writer {
113 | return tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0)
114 | }
115 |
116 | // distWithGlobs encodes the standard Levenshtein distance with support for
117 | // "*", "?" and "**". However, because it works on runes "**" has to be encoded
118 | // as "⁑" in the strings.
119 | //
120 | // Supported wildcards:
121 | //
122 | // ? - Any one character
123 | // * - Any zero or more characters
124 | // ⁑ - Any zero or more characters
125 | func distWithGlobs(ar, br rune) strdist.Cost {
126 | if ar == '⁑' || br == '⁑' {
127 | return strdist.Cost{SwapAB: 0, DeleteA: 0, InsertB: 0}
128 | }
129 | if ar == '*' || br == '*' {
130 | return strdist.Cost{SwapAB: 0, DeleteA: 0, InsertB: 0}
131 | }
132 | if ar == '?' || br == '?' {
133 | return strdist.Cost{SwapAB: 0, DeleteA: 1, InsertB: 1}
134 | }
135 | return strdist.StandardCost(ar, br)
136 | }
137 |
--------------------------------------------------------------------------------
/cmd/chisel/cmd_find_test.go:
--------------------------------------------------------------------------------
1 | package main_test
2 |
3 | import (
4 | . "gopkg.in/check.v1"
5 |
6 | "github.com/canonical/chisel/internal/setup"
7 | "github.com/canonical/chisel/internal/testutil"
8 |
9 | chisel "github.com/canonical/chisel/cmd/chisel"
10 | )
11 |
12 | type findTest struct {
13 | summary string
14 | release *setup.Release
15 | query []string
16 | result []*setup.Slice
17 | }
18 |
19 | func makeSamplePackage(pkg string, slices []string) *setup.Package {
20 | slicesMap := map[string]*setup.Slice{}
21 | for _, slice := range slices {
22 | slicesMap[slice] = &setup.Slice{
23 | Package: pkg,
24 | Name: slice,
25 | }
26 | }
27 | return &setup.Package{
28 | Name: pkg,
29 | Path: "slices/" + pkg,
30 | Archive: "ubuntu",
31 | Slices: slicesMap,
32 | }
33 | }
34 |
35 | var sampleRelease = &setup.Release{
36 | Archives: map[string]*setup.Archive{
37 | "ubuntu": {
38 | Name: "ubuntu",
39 | Version: "22.04",
40 | Suites: []string{"jammy", "jammy-security"},
41 | Components: []string{"main", "other"},
42 | },
43 | },
44 | Packages: map[string]*setup.Package{
45 | "openjdk-8-jdk": makeSamplePackage("openjdk-8-jdk", []string{"bins", "config", "core", "libs", "utils"}),
46 | "python3.10": makeSamplePackage("python3.10", []string{"bins", "config", "core", "libs", "utils"}),
47 | },
48 | }
49 |
50 | var findTests = []findTest{{
51 | summary: "Search by package name",
52 | release: sampleRelease,
53 | query: []string{"python3.10"},
54 | result: []*setup.Slice{
55 | sampleRelease.Packages["python3.10"].Slices["bins"],
56 | sampleRelease.Packages["python3.10"].Slices["config"],
57 | sampleRelease.Packages["python3.10"].Slices["core"],
58 | sampleRelease.Packages["python3.10"].Slices["libs"],
59 | sampleRelease.Packages["python3.10"].Slices["utils"],
60 | },
61 | }, {
62 | summary: "Search by slice name",
63 | release: sampleRelease,
64 | query: []string{"_config"},
65 | result: []*setup.Slice{
66 | sampleRelease.Packages["openjdk-8-jdk"].Slices["config"],
67 | sampleRelease.Packages["python3.10"].Slices["config"],
68 | },
69 | }, {
70 | summary: "Slice search without leading underscore",
71 | release: sampleRelease,
72 | query: []string{"config"},
73 | result: []*setup.Slice{},
74 | }, {
75 | summary: "Check distance greater than one",
76 | release: sampleRelease,
77 | query: []string{"python3."},
78 | result: []*setup.Slice{},
79 | }, {
80 | summary: "Check glob matching (*)",
81 | release: sampleRelease,
82 | query: []string{"python3.*_bins"},
83 | result: []*setup.Slice{
84 | sampleRelease.Packages["python3.10"].Slices["bins"],
85 | },
86 | }, {
87 | summary: "Check glob matching (?)",
88 | release: sampleRelease,
89 | query: []string{"python3.1?_co*"},
90 | result: []*setup.Slice{
91 | sampleRelease.Packages["python3.10"].Slices["config"],
92 | sampleRelease.Packages["python3.10"].Slices["core"],
93 | },
94 | }, {
95 | summary: "Check no matching slice",
96 | release: sampleRelease,
97 | query: []string{"foo_bar"},
98 | result: []*setup.Slice{},
99 | }, {
100 | summary: "Several terms all match",
101 | release: sampleRelease,
102 | query: []string{"python*", "_co*"},
103 | result: []*setup.Slice{
104 | sampleRelease.Packages["python3.10"].Slices["config"],
105 | sampleRelease.Packages["python3.10"].Slices["core"],
106 | },
107 | }, {
108 | summary: "Distance of one in each term",
109 | release: sampleRelease,
110 | query: []string{"python3.1", "_lib"},
111 | result: []*setup.Slice{
112 | sampleRelease.Packages["python3.10"].Slices["libs"],
113 | },
114 | }, {
115 | summary: "Query with underscore is matched against full name",
116 | release: sampleRelease,
117 | query: []string{"python3.1_libs"},
118 | result: []*setup.Slice{
119 | sampleRelease.Packages["python3.10"].Slices["libs"],
120 | },
121 | }, {
122 | summary: "Several terms, one does not match",
123 | release: sampleRelease,
124 | query: []string{"python", "slice"},
125 | result: []*setup.Slice{},
126 | }}
127 |
128 | func (s *ChiselSuite) TestFindSlices(c *C) {
129 | for _, test := range findTests {
130 | c.Logf("Summary: %s", test.summary)
131 |
132 | for _, query := range testutil.Permutations(test.query) {
133 | slices, err := chisel.FindSlices(test.release, query)
134 | c.Assert(err, IsNil)
135 | c.Assert(slices, DeepEquals, test.result)
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/cmd/chisel/cmd_info.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 |
8 | "github.com/jessevdk/go-flags"
9 | "gopkg.in/yaml.v3"
10 |
11 | "github.com/canonical/chisel/internal/setup"
12 | )
13 |
14 | var shortInfoHelp = "Show information about package slices"
15 | var longInfoHelp = `
16 | The info command shows detailed information about package slices.
17 |
18 | It accepts a whitespace-separated list of strings. The list can be
19 | composed of package names, slice names, or a combination of both. The
20 | default output format is YAML. When multiple arguments are provided,
21 | the output is a list of YAML documents separated by a "---" line.
22 |
23 | Slice definitions are shown verbatim according to their definition in
24 | the selected release. For example, globs are not expanded.
25 | `
26 |
27 | var infoDescs = map[string]string{
28 | "release": "Chisel release name or directory (e.g. ubuntu-22.04)",
29 | }
30 |
31 | type infoCmd struct {
32 | Release string `long:"release" value-name:""`
33 |
34 | Positional struct {
35 | Queries []string `positional-arg-name:"" required:"yes"`
36 | } `positional-args:"yes"`
37 | }
38 |
39 | func init() {
40 | addCommand("info", shortInfoHelp, longInfoHelp, func() flags.Commander { return &infoCmd{} }, infoDescs, nil)
41 | }
42 |
43 | func (cmd *infoCmd) Execute(args []string) error {
44 | if len(args) > 0 {
45 | return ErrExtraArgs
46 | }
47 |
48 | release, err := obtainRelease(cmd.Release)
49 | if err != nil {
50 | return err
51 | }
52 |
53 | packages, notFound := selectPackageSlices(release, cmd.Positional.Queries)
54 |
55 | for i, pkg := range packages {
56 | data, err := yaml.Marshal(pkg)
57 | if err != nil {
58 | return err
59 | }
60 | if i > 0 {
61 | fmt.Fprintln(Stdout, "---")
62 | }
63 | fmt.Fprint(Stdout, string(data))
64 | }
65 |
66 | if len(notFound) > 0 {
67 | for i := range notFound {
68 | notFound[i] = strconv.Quote(notFound[i])
69 | }
70 | return fmt.Errorf("no slice definitions found for: " + strings.Join(notFound, ", "))
71 | }
72 |
73 | return nil
74 | }
75 |
76 | // selectPackageSlices takes in a release and a list of query strings
77 | // of package names and/or slice names, and returns a list of packages
78 | // containing the found slices. It also returns a list of query
79 | // strings that were not found.
80 | func selectPackageSlices(release *setup.Release, queries []string) (packages []*setup.Package, notFound []string) {
81 | var pkgOrder []string
82 | pkgSlices := make(map[string][]string)
83 | allPkgSlices := make(map[string]bool)
84 |
85 | sliceExists := func(key setup.SliceKey) bool {
86 | pkg, ok := release.Packages[key.Package]
87 | if !ok {
88 | return false
89 | }
90 | _, ok = pkg.Slices[key.Slice]
91 | return ok
92 | }
93 | for _, query := range queries {
94 | var pkg, slice string
95 | if strings.Contains(query, "_") {
96 | key, err := setup.ParseSliceKey(query)
97 | if err != nil || !sliceExists(key) {
98 | notFound = append(notFound, query)
99 | continue
100 | }
101 | pkg, slice = key.Package, key.Slice
102 | } else {
103 | if _, ok := release.Packages[query]; !ok {
104 | notFound = append(notFound, query)
105 | continue
106 | }
107 | pkg = query
108 | }
109 | if len(pkgSlices[pkg]) == 0 && !allPkgSlices[pkg] {
110 | pkgOrder = append(pkgOrder, pkg)
111 | }
112 | if slice == "" {
113 | allPkgSlices[pkg] = true
114 | } else {
115 | pkgSlices[pkg] = append(pkgSlices[pkg], slice)
116 | }
117 | }
118 |
119 | for _, pkgName := range pkgOrder {
120 | var pkg *setup.Package
121 | if allPkgSlices[pkgName] {
122 | pkg = release.Packages[pkgName]
123 | } else {
124 | releasePkg := release.Packages[pkgName]
125 | pkg = &setup.Package{
126 | Name: releasePkg.Name,
127 | Archive: releasePkg.Archive,
128 | Slices: make(map[string]*setup.Slice),
129 | }
130 | for _, sliceName := range pkgSlices[pkgName] {
131 | pkg.Slices[sliceName] = releasePkg.Slices[sliceName]
132 | }
133 | }
134 | packages = append(packages, pkg)
135 | }
136 | return packages, notFound
137 | }
138 |
--------------------------------------------------------------------------------
/cmd/chisel/cmd_version.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/jessevdk/go-flags"
7 |
8 | "github.com/canonical/chisel/cmd"
9 | )
10 |
11 | var shortVersionHelp = "Show version details"
12 | var longVersionHelp = `
13 | Show the tool version and exit.
14 | `
15 |
16 | type cmdVersion struct{}
17 |
18 | func init() {
19 | addCommand("version", shortVersionHelp, longVersionHelp, func() flags.Commander { return &cmdVersion{} }, nil, nil)
20 | }
21 |
22 | func (cmd cmdVersion) Execute(args []string) error {
23 | if len(args) > 0 {
24 | return ErrExtraArgs
25 | }
26 |
27 | return printVersions()
28 | }
29 |
30 | func printVersions() error {
31 | fmt.Fprintf(Stdout, "%s\n", cmd.Version)
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------
/cmd/chisel/cmd_version_test.go:
--------------------------------------------------------------------------------
1 | package main_test
2 |
3 | import (
4 | . "gopkg.in/check.v1"
5 |
6 | chisel "github.com/canonical/chisel/cmd/chisel"
7 | )
8 |
9 | func (s *ChiselSuite) TestVersionCommand(c *C) {
10 | restore := fakeVersion("4.56")
11 | defer restore()
12 |
13 | _, err := chisel.Parser().ParseArgs([]string{"version"})
14 | c.Assert(err, IsNil)
15 | c.Assert(s.Stdout(), Equals, "4.56\n")
16 | c.Assert(s.Stderr(), Equals, "")
17 | }
18 |
--------------------------------------------------------------------------------
/cmd/chisel/export_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | var RunMain = run
4 |
5 | func FakeIsStdoutTTY(t bool) (restore func()) {
6 | oldIsStdoutTTY := isStdoutTTY
7 | isStdoutTTY = t
8 | return func() {
9 | isStdoutTTY = oldIsStdoutTTY
10 | }
11 | }
12 |
13 | func FakeIsStdinTTY(t bool) (restore func()) {
14 | oldIsStdinTTY := isStdinTTY
15 | isStdinTTY = t
16 | return func() {
17 | isStdinTTY = oldIsStdinTTY
18 | }
19 | }
20 |
21 | var FindSlices = findSlices
22 |
--------------------------------------------------------------------------------
/cmd/chisel/helpers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "regexp"
7 | "strings"
8 |
9 | "github.com/canonical/chisel/internal/setup"
10 | )
11 |
12 | // TODO These need testing
13 |
14 | var releaseExp = regexp.MustCompile(`^([a-z](?:-?[a-z0-9]){2,})-([0-9]+(?:\.?[0-9])+)$`)
15 |
16 | func parseReleaseInfo(release string) (label, version string, err error) {
17 | match := releaseExp.FindStringSubmatch(release)
18 | if match == nil {
19 | return "", "", fmt.Errorf("invalid release reference: %q", release)
20 | }
21 | return match[1], match[2], nil
22 | }
23 |
24 | func readReleaseInfo() (label, version string, err error) {
25 | data, err := os.ReadFile("/etc/lsb-release")
26 | if err == nil {
27 | const labelPrefix = "DISTRIB_ID="
28 | const versionPrefix = "DISTRIB_RELEASE="
29 | for _, line := range strings.Split(string(data), "\n") {
30 | switch {
31 | case strings.HasPrefix(line, labelPrefix):
32 | label = strings.ToLower(line[len(labelPrefix):])
33 | case strings.HasPrefix(line, versionPrefix):
34 | version = line[len(versionPrefix):]
35 | }
36 | if label != "" && version != "" {
37 | return label, version, nil
38 | }
39 | }
40 | }
41 | return "", "", fmt.Errorf("cannot infer release via /etc/lsb-release, see the --release option")
42 | }
43 |
44 | // obtainRelease returns the Chisel release information matching the provided string,
45 | // fetching it if necessary. The provided string should be either:
46 | // * "-",
47 | // * the path to a directory containing a previously fetched release,
48 | // * "" and Chisel will attempt to read the release label from the host.
49 | func obtainRelease(releaseStr string) (release *setup.Release, err error) {
50 | if strings.Contains(releaseStr, "/") {
51 | release, err = setup.ReadRelease(releaseStr)
52 | } else {
53 | var label, version string
54 | if releaseStr == "" {
55 | label, version, err = readReleaseInfo()
56 | } else {
57 | label, version, err = parseReleaseInfo(releaseStr)
58 | }
59 | if err != nil {
60 | return nil, err
61 | }
62 | release, err = setup.FetchRelease(&setup.FetchOptions{
63 | Label: label,
64 | Version: version,
65 | })
66 | }
67 | if err != nil {
68 | return nil, err
69 | }
70 | return release, nil
71 | }
72 |
--------------------------------------------------------------------------------
/cmd/chisel/log.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | )
7 |
8 | // Avoid importing the log type information unnecessarily. There's a small cost
9 | // associated with using an interface rather than the type. Depending on how
10 | // often the logger is plugged in, it would be worth using the type instead.
11 | type log_Logger interface {
12 | Output(calldepth int, s string) error
13 | }
14 |
15 | var globalLoggerLock sync.Mutex
16 | var globalLogger log_Logger
17 | var globalDebug bool
18 |
19 | // Specify the *log.Logger object where log messages should be sent to.
20 | func SetLogger(logger log_Logger) {
21 | globalLoggerLock.Lock()
22 | globalLogger = logger
23 | globalLoggerLock.Unlock()
24 | }
25 |
26 | // Enable the delivery of debug messages to the logger. Only meaningful
27 | // if a logger is also set.
28 | func SetDebug(debug bool) {
29 | globalLoggerLock.Lock()
30 | globalDebug = debug
31 | globalLoggerLock.Unlock()
32 | }
33 |
34 | // logf sends to the logger registered via SetLogger the string resulting
35 | // from running format and args through Sprintf.
36 | func logf(format string, args ...interface{}) {
37 | globalLoggerLock.Lock()
38 | defer globalLoggerLock.Unlock()
39 | if globalLogger != nil {
40 | globalLogger.Output(2, fmt.Sprintf(format, args...))
41 | }
42 | }
43 |
44 | // debugf sends to the logger registered via SetLogger the string resulting
45 | // from running format and args through Sprintf, but only if debugging was
46 | // enabled via SetDebug.
47 | func debugf(format string, args ...interface{}) {
48 | globalLoggerLock.Lock()
49 | defer globalLoggerLock.Unlock()
50 | if globalDebug && globalLogger != nil {
51 | globalLogger.Output(2, fmt.Sprintf(format, args...))
52 | }
53 | }
54 |
55 | // panicf sends to the logger registered via SetLogger the string resulting
56 | // from running format and args through Sprintf, and then panics with the
57 | // same message.
58 | func panicf(format string, args ...interface{}) {
59 | globalLoggerLock.Lock()
60 | defer globalLoggerLock.Unlock()
61 | if globalDebug && globalLogger != nil {
62 | msg := fmt.Sprintf(format, args...)
63 | globalLogger.Output(2, msg)
64 | panic(msg)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/cmd/chisel/main_test.go:
--------------------------------------------------------------------------------
1 | package main_test
2 |
3 | import (
4 | "bytes"
5 | "os"
6 | "testing"
7 |
8 | "golang.org/x/term"
9 | . "gopkg.in/check.v1"
10 |
11 | "github.com/canonical/chisel/cmd"
12 | "github.com/canonical/chisel/internal/testutil"
13 |
14 | chisel "github.com/canonical/chisel/cmd/chisel"
15 | )
16 |
17 | // Hook up check.v1 into the "go test" runner
18 | func Test(t *testing.T) { TestingT(t) }
19 |
20 | type BaseChiselSuite struct {
21 | testutil.BaseTest
22 | stdin *bytes.Buffer
23 | stdout *bytes.Buffer
24 | stderr *bytes.Buffer
25 | password string
26 | }
27 |
28 | func (s *BaseChiselSuite) readPassword(fd int) ([]byte, error) {
29 | return []byte(s.password), nil
30 | }
31 |
32 | func (s *BaseChiselSuite) SetUpTest(c *C) {
33 | s.BaseTest.SetUpTest(c)
34 |
35 | s.stdin = bytes.NewBuffer(nil)
36 | s.stdout = bytes.NewBuffer(nil)
37 | s.stderr = bytes.NewBuffer(nil)
38 | s.password = ""
39 |
40 | chisel.Stdin = s.stdin
41 | chisel.Stdout = s.stdout
42 | chisel.Stderr = s.stderr
43 | chisel.ReadPassword = s.readPassword
44 |
45 | s.AddCleanup(chisel.FakeIsStdoutTTY(false))
46 | s.AddCleanup(chisel.FakeIsStdinTTY(false))
47 | }
48 |
49 | func (s *BaseChiselSuite) TearDownTest(c *C) {
50 | chisel.Stdin = os.Stdin
51 | chisel.Stdout = os.Stdout
52 | chisel.Stderr = os.Stderr
53 | chisel.ReadPassword = term.ReadPassword
54 |
55 | s.BaseTest.TearDownTest(c)
56 | }
57 |
58 | func (s *BaseChiselSuite) Stdout() string {
59 | return s.stdout.String()
60 | }
61 |
62 | func (s *BaseChiselSuite) Stderr() string {
63 | return s.stderr.String()
64 | }
65 |
66 | func (s *BaseChiselSuite) ResetStdStreams() {
67 | s.stdin.Reset()
68 | s.stdout.Reset()
69 | s.stderr.Reset()
70 | }
71 |
72 | func fakeVersion(v string) (restore func()) {
73 | old := cmd.Version
74 | cmd.Version = v
75 | return func() { cmd.Version = old }
76 | }
77 |
78 | type ChiselSuite struct {
79 | BaseChiselSuite
80 | }
81 |
82 | var _ = Suite(&ChiselSuite{})
83 |
--------------------------------------------------------------------------------
/cmd/mkversion.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | # debugging if anything fails is tricky as dh-golang eats up all output
5 | # uncomment the lines below to get a useful trace if you have to touch
6 | # this again (my advice is: DON'T)
7 | #set -x
8 | #logfile=/tmp/mkversions.log
9 | #exec >> $logfile 2>&1
10 | #echo "env: $(set)"
11 | #echo "mkversion.sh run from: $0"
12 | #echo "pwd: $(pwd)"
13 |
14 | # we have two directories we need to care about:
15 | # - our toplevel pkg builddir which is where "mkversion.sh" is located
16 | # and where "snap-confine" expects its cmd/VERSION file
17 | # - the GO_GENERATE_BUILDDIR which may be the toplevel pkg dir. but
18 | # during "dpkg-buildpackage" it will become a different _build/ dir
19 | # that dh-golang creates and that only contains a subset of the
20 | # files of the toplevel buildir.
21 | PKG_BUILDDIR=$(dirname "$0")/..
22 | GO_GENERATE_BUILDDIR="$(pwd)"
23 |
24 | # run from "go generate" adjust path
25 | if [ "$GOPACKAGE" = "cmd" ]; then
26 | GO_GENERATE_BUILDDIR="$(pwd)/.."
27 | fi
28 |
29 | OUTPUT_ONLY=false
30 | if [ "$1" = "--output-only" ]; then
31 | OUTPUT_ONLY=true
32 | shift
33 | fi
34 |
35 | # If the version is passed in as an argument to mkversion.sh, let's use that.
36 | if [ -n "$1" ]; then
37 | v="$1"
38 | o=shell
39 | fi
40 |
41 | if [ -z "$v" ]; then
42 | # Let's try to derive the version from git..
43 | if command -v git >/dev/null; then
44 | # not using "--dirty" here until the following bug is fixed:
45 | # https://bugs.launchpad.net/snapcraft/+bug/1662388
46 | v="$(git describe --tags --always | sed -e 's/-/+git/;y/-/./' )"
47 | o=git
48 | fi
49 | fi
50 |
51 | if [ -z "$v" ]; then
52 | # at this point we maybe in _build/src/github etc where we have no
53 | # debian/changelog (dh-golang only exports the sources here)
54 | # switch to the real source dir for the changelog parsing
55 | v="$(cd "$PKG_BUILDDIR"; dpkg-parsechangelog --show-field Version)";
56 | o=debian/changelog
57 | fi
58 |
59 | if [ -z "$v" ]; then
60 | exit 1
61 | fi
62 |
63 | if [ "$OUTPUT_ONLY" = true ]; then
64 | echo "$v"
65 | exit 0
66 | fi
67 |
68 | echo "*** Setting version to '$v' from $o." >&2
69 |
70 | cat < "$GO_GENERATE_BUILDDIR/cmd/version_generated.go"
71 | package cmd
72 |
73 | // generated by mkversion.sh; do not edit
74 |
75 | func init() {
76 | Version = "$v"
77 | }
78 | EOF
79 |
80 | cat < "$PKG_BUILDDIR/cmd/VERSION"
81 | $v
82 | EOF
83 |
84 | #cat < "$PKG_BUILDDIR/data/info"
85 | #VERSION=$v
86 | #EOF
87 |
--------------------------------------------------------------------------------
/cmd/version.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2020 Canonical Ltd
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License version 3 as
5 | // published by the Free Software Foundation.
6 | //
7 | // This program is distributed in the hope that it will be useful,
8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 | // GNU General Public License for more details.
11 | //
12 | // You should have received a copy of the GNU General Public License
13 | // along with this program. If not, see .
14 |
15 | package cmd
16 |
17 | //go:generate ./mkversion.sh
18 |
19 | // Version will be overwritten at build-time via mkversion.sh
20 | var Version = "unknown"
21 |
22 | func MockVersion(version string) (restore func()) {
23 | old := Version
24 | Version = version
25 | return func() { Version = old }
26 | }
27 |
--------------------------------------------------------------------------------
/docs/_static/package-slices.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
67 |
--------------------------------------------------------------------------------
/docs/_static/slice-of-ubuntu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/chisel/c243d6e247c0cff8c1449d69a42f6be04c58d78b/docs/_static/slice-of-ubuntu.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/canonical/chisel
2 |
3 | go 1.22.12
4 |
5 | require (
6 | github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
7 | github.com/jessevdk/go-flags v1.6.1
8 | github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b
9 | github.com/klauspost/compress v1.18.0
10 | github.com/ulikunitz/xz v0.5.12
11 | go.starlark.net v0.0.0-20250318223901-d9371fef63fe
12 | golang.org/x/crypto v0.33.0
13 | golang.org/x/term v0.29.0
14 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
15 | gopkg.in/yaml.v3 v3.0.1
16 | )
17 |
18 | require (
19 | github.com/google/go-cmp v0.6.0 // indirect
20 | github.com/kr/pretty v0.3.1 // indirect
21 | github.com/kr/text v0.2.0 // indirect
22 | github.com/rogpeppe/go-internal v1.13.1 // indirect
23 | golang.org/x/sys v0.30.0 // indirect
24 | )
25 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
2 | github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
4 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
5 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
6 | github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
7 | github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
8 | github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b h1:FQ7+9fxhyp82ks9vAuyPzG0/vVbWwMwLJ+P6yJI5FN8=
9 | github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b/go.mod h1:HMcgvsgd0Fjj4XXDkbjdmlbI505rUPBs6WBMYg2pXks=
10 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
11 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
12 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
13 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
14 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
15 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
16 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
17 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
18 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
19 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
20 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
21 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
22 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
23 | github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
24 | github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
25 | go.starlark.net v0.0.0-20250318223901-d9371fef63fe h1:Wf00k2WTLCW/L1/+gA1gxfTcU4yI+nK4YRTjumYezD8=
26 | go.starlark.net v0.0.0-20250318223901-d9371fef63fe/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
27 | golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
28 | golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
29 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
30 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
31 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
32 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
33 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
34 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
35 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
36 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
37 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
38 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
39 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
40 |
--------------------------------------------------------------------------------
/internal/apachetestutil/manifest.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package apachetestutil
4 |
5 | import (
6 | "gopkg.in/check.v1"
7 |
8 | "github.com/canonical/chisel/public/manifest"
9 | )
10 |
11 | type ManifestContents struct {
12 | Paths []*manifest.Path
13 | Packages []*manifest.Package
14 | Slices []*manifest.Slice
15 | Contents []*manifest.Content
16 | }
17 |
18 | func DumpManifestContents(c *check.C, mfest *manifest.Manifest) *ManifestContents {
19 | var slices []*manifest.Slice
20 | err := mfest.IterateSlices("", func(slice *manifest.Slice) error {
21 | slices = append(slices, slice)
22 | return nil
23 | })
24 | c.Assert(err, check.IsNil)
25 |
26 | var pkgs []*manifest.Package
27 | err = mfest.IteratePackages(func(pkg *manifest.Package) error {
28 | pkgs = append(pkgs, pkg)
29 | return nil
30 | })
31 | c.Assert(err, check.IsNil)
32 |
33 | var paths []*manifest.Path
34 | err = mfest.IteratePaths("", func(path *manifest.Path) error {
35 | paths = append(paths, path)
36 | return nil
37 | })
38 | c.Assert(err, check.IsNil)
39 |
40 | var contents []*manifest.Content
41 | err = mfest.IterateContents("", func(content *manifest.Content) error {
42 | contents = append(contents, content)
43 | return nil
44 | })
45 | c.Assert(err, check.IsNil)
46 |
47 | mc := ManifestContents{
48 | Paths: paths,
49 | Packages: pkgs,
50 | Slices: slices,
51 | Contents: contents,
52 | }
53 | return &mc
54 | }
55 |
--------------------------------------------------------------------------------
/internal/apacheutil/log.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package apacheutil
4 |
5 | import (
6 | "fmt"
7 | "sync"
8 | )
9 |
10 | // Avoid importing the log type information unnecessarily. There's a small cost
11 | // associated with using an interface rather than the type. Depending on how
12 | // often the logger is plugged in, it would be worth using the type instead.
13 | type log_Logger interface {
14 | Output(calldepth int, s string) error
15 | }
16 |
17 | var globalLoggerLock sync.Mutex
18 | var globalLogger log_Logger
19 | var globalDebug bool
20 |
21 | // Specify the *log.Logger object where log messages should be sent to.
22 | func SetLogger(logger log_Logger) {
23 | globalLoggerLock.Lock()
24 | globalLogger = logger
25 | globalLoggerLock.Unlock()
26 | }
27 |
28 | // Enable the delivery of debug messages to the logger. Only meaningful
29 | // if a logger is also set.
30 | func SetDebug(debug bool) {
31 | globalLoggerLock.Lock()
32 | globalDebug = debug
33 | globalLoggerLock.Unlock()
34 | }
35 |
36 | // logf sends to the logger registered via SetLogger the string resulting
37 | // from running format and args through Sprintf.
38 | func logf(format string, args ...interface{}) {
39 | globalLoggerLock.Lock()
40 | defer globalLoggerLock.Unlock()
41 | if globalLogger != nil {
42 | globalLogger.Output(2, fmt.Sprintf(format, args...))
43 | }
44 | }
45 |
46 | // debugf sends to the logger registered via SetLogger the string resulting
47 | // from running format and args through Sprintf, but only if debugging was
48 | // enabled via SetDebug.
49 | func debugf(format string, args ...interface{}) {
50 | globalLoggerLock.Lock()
51 | defer globalLoggerLock.Unlock()
52 | if globalDebug && globalLogger != nil {
53 | globalLogger.Output(2, fmt.Sprintf(format, args...))
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/internal/apacheutil/suite_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package apacheutil_test
4 |
5 | import (
6 | "testing"
7 |
8 | . "gopkg.in/check.v1"
9 |
10 | "github.com/canonical/chisel/internal/apacheutil"
11 | )
12 |
13 | func Test(t *testing.T) { TestingT(t) }
14 |
15 | type S struct{}
16 |
17 | var _ = Suite(&S{})
18 |
19 | func (s *S) SetUpTest(c *C) {
20 | apacheutil.SetDebug(true)
21 | apacheutil.SetLogger(c)
22 | }
23 |
24 | func (s *S) TearDownTest(c *C) {
25 | apacheutil.SetDebug(false)
26 | apacheutil.SetLogger(nil)
27 | }
28 |
--------------------------------------------------------------------------------
/internal/apacheutil/util.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package apacheutil
4 |
5 | import (
6 | "fmt"
7 | "regexp"
8 | )
9 |
10 | type SliceKey struct {
11 | Package string
12 | Slice string
13 | }
14 |
15 | func (s SliceKey) String() string { return s.Package + "_" + s.Slice }
16 |
17 | // FnameExp matches the slice definition file basename.
18 | var FnameExp = regexp.MustCompile(`^([a-z0-9](?:-?[.a-z0-9+]){1,})\.yaml$`)
19 |
20 | // SnameExp matches only the slice name, without the leading package name.
21 | var SnameExp = regexp.MustCompile(`^([a-z](?:-?[a-z0-9]){2,})$`)
22 |
23 | // knameExp matches the slice full name in pkg_slice format.
24 | var knameExp = regexp.MustCompile(`^([a-z0-9](?:-?[.a-z0-9+]){1,})_([a-z](?:-?[a-z0-9]){2,})$`)
25 |
26 | func ParseSliceKey(sliceKey string) (SliceKey, error) {
27 | match := knameExp.FindStringSubmatch(sliceKey)
28 | if match == nil {
29 | return SliceKey{}, fmt.Errorf("invalid slice reference: %q", sliceKey)
30 | }
31 | return SliceKey{match[1], match[2]}, nil
32 | }
33 |
--------------------------------------------------------------------------------
/internal/apacheutil/util_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package apacheutil_test
4 |
5 | import (
6 | . "gopkg.in/check.v1"
7 |
8 | "github.com/canonical/chisel/internal/apacheutil"
9 | )
10 |
11 | var sliceKeyTests = []struct {
12 | input string
13 | expected apacheutil.SliceKey
14 | err string
15 | }{{
16 | input: "foo_bar",
17 | expected: apacheutil.SliceKey{Package: "foo", Slice: "bar"},
18 | }, {
19 | input: "fo_bar",
20 | expected: apacheutil.SliceKey{Package: "fo", Slice: "bar"},
21 | }, {
22 | input: "1234_bar",
23 | expected: apacheutil.SliceKey{Package: "1234", Slice: "bar"},
24 | }, {
25 | input: "foo1.1-2-3_bar",
26 | expected: apacheutil.SliceKey{Package: "foo1.1-2-3", Slice: "bar"},
27 | }, {
28 | input: "foo-pkg_dashed-slice-name",
29 | expected: apacheutil.SliceKey{Package: "foo-pkg", Slice: "dashed-slice-name"},
30 | }, {
31 | input: "foo+_bar",
32 | expected: apacheutil.SliceKey{Package: "foo+", Slice: "bar"},
33 | }, {
34 | input: "foo_slice123",
35 | expected: apacheutil.SliceKey{Package: "foo", Slice: "slice123"},
36 | }, {
37 | input: "g++_bins",
38 | expected: apacheutil.SliceKey{Package: "g++", Slice: "bins"},
39 | }, {
40 | input: "a+_bar",
41 | expected: apacheutil.SliceKey{Package: "a+", Slice: "bar"},
42 | }, {
43 | input: "a._bar",
44 | expected: apacheutil.SliceKey{Package: "a.", Slice: "bar"},
45 | }, {
46 | input: "foo_ba",
47 | err: `invalid slice reference: "foo_ba"`,
48 | }, {
49 | input: "f_bar",
50 | err: `invalid slice reference: "f_bar"`,
51 | }, {
52 | input: "1234_789",
53 | err: `invalid slice reference: "1234_789"`,
54 | }, {
55 | input: "foo_bar.x.y",
56 | err: `invalid slice reference: "foo_bar.x.y"`,
57 | }, {
58 | input: "foo-_-bar",
59 | err: `invalid slice reference: "foo-_-bar"`,
60 | }, {
61 | input: "foo_bar-",
62 | err: `invalid slice reference: "foo_bar-"`,
63 | }, {
64 | input: "foo-_bar",
65 | err: `invalid slice reference: "foo-_bar"`,
66 | }, {
67 | input: "-foo_bar",
68 | err: `invalid slice reference: "-foo_bar"`,
69 | }, {
70 | input: "foo_bar_baz",
71 | err: `invalid slice reference: "foo_bar_baz"`,
72 | }, {
73 | input: "a-_bar",
74 | err: `invalid slice reference: "a-_bar"`,
75 | }, {
76 | input: "+++_bar",
77 | err: `invalid slice reference: "\+\+\+_bar"`,
78 | }, {
79 | input: "..._bar",
80 | err: `invalid slice reference: "\.\.\._bar"`,
81 | }, {
82 | input: "white space_no-whitespace",
83 | err: `invalid slice reference: "white space_no-whitespace"`,
84 | }}
85 |
86 | func (s *S) TestParseSliceKey(c *C) {
87 | for _, test := range sliceKeyTests {
88 | key, err := apacheutil.ParseSliceKey(test.input)
89 | if test.err != "" {
90 | c.Assert(err, ErrorMatches, test.err)
91 | continue
92 | }
93 | c.Assert(err, IsNil)
94 | c.Assert(key, DeepEquals, test.expected)
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/internal/archive/export_test.go:
--------------------------------------------------------------------------------
1 | package archive
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | func FakeDo(do func(req *http.Request) (*http.Response, error)) (restore func()) {
8 | _httpDo := httpDo
9 | _bulkDo := bulkDo
10 | httpDo = do
11 | bulkDo = do
12 | return func() {
13 | httpDo = _httpDo
14 | bulkDo = _bulkDo
15 | }
16 | }
17 |
18 | type Credentials = credentials
19 |
20 | var FindCredentials = findCredentials
21 | var FindCredentialsInDir = findCredentialsInDir
22 |
23 | var ProArchiveInfo = proArchiveInfo
24 |
--------------------------------------------------------------------------------
/internal/archive/log.go:
--------------------------------------------------------------------------------
1 | package archive
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | )
7 |
8 | // Avoid importing the log type information unnecessarily. There's a small cost
9 | // associated with using an interface rather than the type. Depending on how
10 | // often the logger is plugged in, it would be worth using the type instead.
11 | type log_Logger interface {
12 | Output(calldepth int, s string) error
13 | }
14 |
15 | var globalLoggerLock sync.Mutex
16 | var globalLogger log_Logger
17 | var globalDebug bool
18 |
19 | // Specify the *log.Logger object where log messages should be sent to.
20 | func SetLogger(logger log_Logger) {
21 | globalLoggerLock.Lock()
22 | globalLogger = logger
23 | globalLoggerLock.Unlock()
24 | }
25 |
26 | // Enable the delivery of debug messages to the logger. Only meaningful
27 | // if a logger is also set.
28 | func SetDebug(debug bool) {
29 | globalLoggerLock.Lock()
30 | globalDebug = debug
31 | globalLoggerLock.Unlock()
32 | }
33 |
34 | // logf sends to the logger registered via SetLogger the string resulting
35 | // from running format and args through Sprintf.
36 | func logf(format string, args ...interface{}) {
37 | globalLoggerLock.Lock()
38 | defer globalLoggerLock.Unlock()
39 | if globalLogger != nil {
40 | globalLogger.Output(2, fmt.Sprintf(format, args...))
41 | }
42 | }
43 |
44 | // debugf sends to the logger registered via SetLogger the string resulting
45 | // from running format and args through Sprintf, but only if debugging was
46 | // enabled via SetDebug.
47 | func debugf(format string, args ...interface{}) {
48 | globalLoggerLock.Lock()
49 | defer globalLoggerLock.Unlock()
50 | if globalDebug && globalLogger != nil {
51 | globalLogger.Output(2, fmt.Sprintf(format, args...))
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/internal/archive/suite_test.go:
--------------------------------------------------------------------------------
1 | package archive_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "gopkg.in/check.v1"
7 |
8 | "github.com/canonical/chisel/internal/archive"
9 | )
10 |
11 | func Test(t *testing.T) { TestingT(t) }
12 |
13 | type S struct{}
14 |
15 | var _ = Suite(&S{})
16 |
17 | func (s *S) SetUpTest(c *C) {
18 | archive.SetLogger(c)
19 | }
20 |
21 | func (s *S) TearDownTest(c *C) {
22 | archive.SetLogger(nil)
23 | }
24 |
--------------------------------------------------------------------------------
/internal/archive/testarchive/testarchive.go:
--------------------------------------------------------------------------------
1 | package testarchive
2 |
3 | import (
4 | "bytes"
5 | "compress/gzip"
6 | "crypto/sha256"
7 | "fmt"
8 | "path"
9 | "strings"
10 |
11 | "golang.org/x/crypto/openpgp/clearsign"
12 | "golang.org/x/crypto/openpgp/packet"
13 |
14 | "github.com/canonical/chisel/internal/testutil"
15 | )
16 |
17 | type Item interface {
18 | Path() string
19 | Walk(f func(Item) error) error
20 | Section() []byte
21 | Content() []byte
22 | }
23 |
24 | func CallWalkFunc(this Item, f func(Item) error, items ...Item) error {
25 | if this != nil {
26 | err := f(this)
27 | if err != nil {
28 | return err
29 | }
30 | }
31 | for _, item := range items {
32 | err := item.Walk(f)
33 | if err != nil {
34 | return err
35 | }
36 | }
37 | return nil
38 | }
39 |
40 | type Gzip struct {
41 | Item Item
42 | }
43 |
44 | func (gz *Gzip) Path() string {
45 | return gz.Item.Path() + ".gz"
46 | }
47 |
48 | func (gz *Gzip) Walk(f func(Item) error) error {
49 | return CallWalkFunc(gz, f, gz.Item)
50 | }
51 |
52 | func (gz *Gzip) Section() []byte {
53 | return gz.Item.Section()
54 | }
55 |
56 | func (gz *Gzip) Content() []byte {
57 | return makeGzip(gz.Item.Content())
58 | }
59 |
60 | type Package struct {
61 | Name string
62 | Version string
63 | Arch string
64 | Component string
65 | Data []byte
66 | }
67 |
68 | func (p *Package) Path() string {
69 | return fmt.Sprintf("pool/%s/%c/%s/%s_%subuntu1_%s.deb", p.Component, p.Name[0], p.Name, p.Name, p.Version, p.Arch)
70 | }
71 |
72 | func (p *Package) Walk(f func(Item) error) error {
73 | return CallWalkFunc(p, f)
74 | }
75 |
76 | func (p *Package) Section() []byte {
77 | content := p.Content()
78 | section := fmt.Sprintf(string(testutil.Reindent(`
79 | Package: %s
80 | Architecture: %s
81 | Version: %s
82 | Priority: required
83 | Essential: yes
84 | Section: admin
85 | Origin: Ubuntu
86 | Installed-Size: 10
87 | Filename: %s
88 | Size: %d
89 | SHA256: %s
90 | Description: Description of %s
91 | Task: minimal
92 |
93 | `)), p.Name, p.Arch, p.Version, p.Path(), len(content), makeSha256(content), p.Name)
94 | return []byte(section)
95 | }
96 |
97 | func (p *Package) Content() []byte {
98 | if len(p.Data) == 0 {
99 | return []byte(p.Name + " " + p.Version + " data")
100 | }
101 | return p.Data
102 | }
103 |
104 | type Release struct {
105 | Suite string
106 | Version string
107 | Label string
108 | Items []Item
109 | PrivKey *packet.PrivateKey
110 | }
111 |
112 | func (r *Release) Walk(f func(Item) error) error {
113 | return CallWalkFunc(r, f, r.Items...)
114 | }
115 |
116 | func (r *Release) Path() string {
117 | return "InRelease"
118 | }
119 |
120 | func (r *Release) Section() []byte {
121 | return nil
122 | }
123 |
124 | func (r *Release) Content() []byte {
125 | digests := bytes.Buffer{}
126 | for _, item := range r.Items {
127 | content := item.Content()
128 | digests.WriteString(fmt.Sprintf(" %s %d %s\n", makeSha256(content), len(content), item.Path()))
129 | }
130 | content := fmt.Sprintf(string(testutil.Reindent(`
131 | Origin: Ubuntu
132 | Label: %s
133 | Suite: %s
134 | Version: %s
135 | Codename: codename
136 | Date: Thu, 21 Apr 2022 17:16:08 UTC
137 | Architectures: amd64 arm64 armhf i386 ppc64el riscv64 s390x
138 | Components: main restricted universe multiverse
139 | Description: Ubuntu %s
140 | SHA256:
141 | %s
142 | `)), r.Label, r.Suite, r.Version, r.Version, digests.String())
143 |
144 | var buf bytes.Buffer
145 | writer, err := clearsign.Encode(&buf, r.PrivKey, nil)
146 | if err != nil {
147 | panic(err)
148 | }
149 | _, err = writer.Write([]byte(content))
150 | if err != nil {
151 | panic(err)
152 | }
153 | err = writer.Close()
154 | if err != nil {
155 | panic(err)
156 | }
157 | return buf.Bytes()
158 | }
159 |
160 | func (r *Release) Render(prefix string, content map[string][]byte) error {
161 | return r.Walk(func(item Item) error {
162 | itemPath := item.Path()
163 | if strings.HasPrefix(itemPath, "pool/") {
164 | itemPath = path.Join(prefix, itemPath)
165 | } else {
166 | itemPath = path.Join(prefix, "dists", r.Suite, itemPath)
167 | }
168 | content[itemPath] = item.Content()
169 | return nil
170 | })
171 | }
172 |
173 | func MergeSections(items []Item) []byte {
174 | buf := bytes.Buffer{}
175 | for _, item := range items {
176 | buf.Write(item.Section())
177 | }
178 | return buf.Bytes()
179 | }
180 |
181 | type PackageIndex struct {
182 | Component string
183 | Arch string
184 | Packages []Item
185 | }
186 |
187 | func (pi *PackageIndex) Path() string {
188 | return fmt.Sprintf("%s/binary-%s/Packages", pi.Component, pi.Arch)
189 | }
190 |
191 | func (pi *PackageIndex) Walk(f func(Item) error) error {
192 | return CallWalkFunc(pi, f, pi.Packages...)
193 | }
194 |
195 | func (pi *PackageIndex) Section() []byte {
196 | return nil
197 | }
198 |
199 | func (pi *PackageIndex) Content() []byte {
200 | return MergeSections(pi.Packages)
201 | }
202 |
203 | func makeSha256(b []byte) string {
204 | return fmt.Sprintf("%x", sha256.Sum256(b))
205 | }
206 |
207 | func makeGzip(b []byte) []byte {
208 | var buf bytes.Buffer
209 | gz := gzip.NewWriter(&buf)
210 | _, err := gz.Write(b)
211 | if err != nil {
212 | panic(err)
213 | }
214 | err = gz.Close()
215 | if err != nil {
216 | panic(err)
217 | }
218 | return buf.Bytes()
219 | }
220 |
--------------------------------------------------------------------------------
/internal/cache/cache.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "crypto/sha256"
5 | "encoding/hex"
6 | "fmt"
7 | "hash"
8 | "io"
9 | "os"
10 | "path/filepath"
11 | "time"
12 | )
13 |
14 | func DefaultDir(suffix string) string {
15 | cacheDir := os.Getenv("XDG_CACHE_HOME")
16 | if cacheDir == "" {
17 | homeDir := os.Getenv("HOME")
18 | if homeDir != "" {
19 | cacheDir = filepath.Join(homeDir, ".cache")
20 | } else {
21 | var err error
22 | cacheDir, err = os.MkdirTemp("", "cache-*")
23 | if err != nil {
24 | panic("no proper location for cache: " + err.Error())
25 | }
26 | }
27 | }
28 | return filepath.Join(cacheDir, suffix)
29 | }
30 |
31 | type Cache struct {
32 | Dir string
33 | }
34 |
35 | type Writer struct {
36 | dir string
37 | digest string
38 | hash hash.Hash
39 | file *os.File
40 | err error
41 | }
42 |
43 | func (cw *Writer) fail(err error) error {
44 | if cw.err == nil {
45 | cw.err = err
46 | cw.file.Close()
47 | os.Remove(cw.file.Name())
48 | }
49 | return err
50 | }
51 |
52 | func (cw *Writer) Write(data []byte) (n int, err error) {
53 | if cw.err != nil {
54 | return 0, cw.err
55 | }
56 | n, err = cw.file.Write(data)
57 | if err != nil {
58 | return n, cw.fail(err)
59 | }
60 | cw.hash.Write(data)
61 | return n, nil
62 | }
63 |
64 | func (cw *Writer) Close() error {
65 | if cw.err != nil {
66 | return cw.err
67 | }
68 | err := cw.file.Close()
69 | if err != nil {
70 | return cw.fail(err)
71 | }
72 | sum := cw.hash.Sum(nil)
73 | digest := hex.EncodeToString(sum[:])
74 | if cw.digest == "" {
75 | cw.digest = digest
76 | } else if digest != cw.digest {
77 | return cw.fail(fmt.Errorf("expected digest %s, got %s", cw.digest, digest))
78 | }
79 | fname := cw.file.Name()
80 | err = os.Rename(fname, filepath.Join(filepath.Dir(fname), cw.digest))
81 | if err != nil {
82 | return cw.fail(err)
83 | }
84 | cw.err = io.EOF
85 | return nil
86 | }
87 |
88 | func (cw *Writer) Digest() string {
89 | return cw.digest
90 | }
91 |
92 | const digestKind = "sha256"
93 |
94 | var MissErr = fmt.Errorf("not cached")
95 |
96 | func (c *Cache) filePath(digest string) string {
97 | return filepath.Join(c.Dir, digestKind, digest)
98 | }
99 |
100 | func (c *Cache) Create(digest string) *Writer {
101 | if c.Dir == "" {
102 | return &Writer{err: fmt.Errorf("internal error: cache directory is unset")}
103 | }
104 | err := os.MkdirAll(filepath.Join(c.Dir, digestKind), 0755)
105 | if err != nil {
106 | return &Writer{err: fmt.Errorf("cannot create cache directory: %v", err)}
107 | }
108 | var file *os.File
109 | if digest == "" {
110 | file, err = os.CreateTemp(c.filePath(""), "tmp.*")
111 | } else {
112 | file, err = os.Create(c.filePath(digest + ".tmp"))
113 | }
114 | if err != nil {
115 | return &Writer{err: fmt.Errorf("cannot create cache file: %v", err)}
116 | }
117 | return &Writer{
118 | dir: c.Dir,
119 | digest: digest,
120 | hash: sha256.New(),
121 | file: file,
122 | }
123 | }
124 |
125 | func (c *Cache) Write(digest string, data []byte) error {
126 | f := c.Create(digest)
127 | _, err1 := f.Write(data)
128 | err2 := f.Close()
129 | if err1 != nil {
130 | return err1
131 | }
132 | return err2
133 | }
134 |
135 | func (c *Cache) Open(digest string) (io.ReadSeekCloser, error) {
136 | if c.Dir == "" || digest == "" {
137 | return nil, MissErr
138 | }
139 | filePath := c.filePath(digest)
140 | file, err := os.Open(filePath)
141 | if os.IsNotExist(err) {
142 | return nil, MissErr
143 | } else if err != nil {
144 | return nil, fmt.Errorf("cannot open cache file: %v", err)
145 | }
146 | // Use mtime as last reuse time.
147 | now := time.Now()
148 | if err := os.Chtimes(filePath, now, now); err != nil {
149 | return nil, fmt.Errorf("cannot update cached file timestamp: %v", err)
150 | }
151 | return file, nil
152 | }
153 |
154 | func (c *Cache) Read(digest string) ([]byte, error) {
155 | file, err := c.Open(digest)
156 | if err != nil {
157 | return nil, err
158 | }
159 | defer file.Close()
160 | data, err := io.ReadAll(file)
161 | if err != nil {
162 | return nil, fmt.Errorf("cannot read file from cache: %v", err)
163 | }
164 | return data, nil
165 | }
166 |
167 | func (c *Cache) Expire(timeout time.Duration) error {
168 | entries, err := os.ReadDir(filepath.Join(c.Dir, digestKind))
169 | if err != nil {
170 | return fmt.Errorf("cannot list cache directory: %v", err)
171 | }
172 | expired := time.Now().Add(-timeout)
173 | for _, entry := range entries {
174 | finfo, err := entry.Info()
175 | if err != nil {
176 | return err
177 | }
178 | if finfo.ModTime().After(expired) {
179 | continue
180 | }
181 | err = os.Remove(filepath.Join(c.Dir, digestKind, finfo.Name()))
182 | if err != nil {
183 | return fmt.Errorf("cannot expire cache entry: %v", err)
184 | }
185 | }
186 | return nil
187 | }
188 |
--------------------------------------------------------------------------------
/internal/cache/cache_test.go:
--------------------------------------------------------------------------------
1 | package cache_test
2 |
3 | import (
4 | . "gopkg.in/check.v1"
5 |
6 | "io"
7 | "os"
8 | "path/filepath"
9 | "strings"
10 | "time"
11 |
12 | "github.com/canonical/chisel/internal/cache"
13 | )
14 |
15 | const (
16 | data1Digest = "5b41362bc82b7f3d56edc5a306db22105707d01ff4819e26faef9724a2d406c9"
17 | data2Digest = "d98cf53e0c8b77c14a96358d5b69584225b4bb9026423cbc2f7b0161894c402c"
18 | data3Digest = "f60f2d65da046fcaaf8a10bd96b5630104b629e111aff46ce89792e1caa11b18"
19 | )
20 |
21 | func (s *S) TestDefaultDir(c *C) {
22 | oldA := os.Getenv("HOME")
23 | oldB := os.Getenv("XDG_CACHE_HOME")
24 | defer func() {
25 | os.Setenv("HOME", oldA)
26 | os.Setenv("XDG_CACHE_HOME", oldB)
27 | }()
28 |
29 | os.Setenv("HOME", "/home/user")
30 | os.Setenv("XDG_CACHE_HOME", "")
31 | c.Assert(cache.DefaultDir("foo/bar"), Equals, "/home/user/.cache/foo/bar")
32 |
33 | os.Setenv("HOME", "/home/user")
34 | os.Setenv("XDG_CACHE_HOME", "/xdg/cache")
35 | c.Assert(cache.DefaultDir("foo/bar"), Equals, "/xdg/cache/foo/bar")
36 |
37 | os.Setenv("HOME", "")
38 | os.Setenv("XDG_CACHE_HOME", "")
39 | defaultDir := cache.DefaultDir("foo/bar")
40 | c.Assert(strings.HasPrefix(defaultDir, os.TempDir()), Equals, true)
41 | c.Assert(strings.Contains(defaultDir, "/cache-"), Equals, true)
42 | c.Assert(strings.HasSuffix(defaultDir, "/foo/bar"), Equals, true)
43 | }
44 |
45 | func (s *S) TestCacheEmpty(c *C) {
46 | cc := cache.Cache{c.MkDir()}
47 |
48 | _, err := cc.Open(data1Digest)
49 | c.Assert(err, Equals, cache.MissErr)
50 | _, err = cc.Read(data1Digest)
51 | c.Assert(err, Equals, cache.MissErr)
52 | _, err = cc.Read("")
53 | c.Assert(err, Equals, cache.MissErr)
54 | }
55 |
56 | func (s *S) TestCacheReadWrite(c *C) {
57 | cc := cache.Cache{Dir: c.MkDir()}
58 |
59 | data1Path := filepath.Join(cc.Dir, "sha256", data1Digest)
60 | data2Path := filepath.Join(cc.Dir, "sha256", data2Digest)
61 | data3Path := filepath.Join(cc.Dir, "sha256", data3Digest)
62 |
63 | err := cc.Write(data1Digest, []byte("data1"))
64 | c.Assert(err, IsNil)
65 | data1, err := cc.Read(data1Digest)
66 | c.Assert(err, IsNil)
67 | c.Assert(string(data1), Equals, "data1")
68 |
69 | err = cc.Write("", []byte("data2"))
70 | c.Assert(err, IsNil)
71 | data2, err := cc.Read(data2Digest)
72 | c.Assert(err, IsNil)
73 | c.Assert(string(data2), Equals, "data2")
74 |
75 | _, err = cc.Read(data3Digest)
76 | c.Assert(err, Equals, cache.MissErr)
77 | _, err = cc.Read("")
78 | c.Assert(err, Equals, cache.MissErr)
79 |
80 | _, err = os.Stat(data1Path)
81 | c.Assert(err, IsNil)
82 | _, err = os.Stat(data2Path)
83 | c.Assert(err, IsNil)
84 | _, err = os.Stat(data3Path)
85 | c.Assert(os.IsNotExist(err), Equals, true)
86 |
87 | now := time.Now()
88 | expired := now.Add(-time.Hour - time.Second)
89 | err = os.Chtimes(data1Path, now, expired)
90 | c.Assert(err, IsNil)
91 |
92 | err = cc.Expire(time.Hour)
93 | c.Assert(err, IsNil)
94 | _, err = os.Stat(data1Path)
95 | c.Assert(os.IsNotExist(err), Equals, true)
96 | }
97 |
98 | func (s *S) TestCacheCreate(c *C) {
99 | cc := cache.Cache{Dir: c.MkDir()}
100 |
101 | w := cc.Create("")
102 |
103 | c.Assert(w.Digest(), Equals, "")
104 |
105 | _, err := w.Write([]byte("da"))
106 | c.Assert(err, IsNil)
107 | _, err = w.Write([]byte("ta"))
108 | c.Assert(err, IsNil)
109 | _, err = w.Write([]byte("1"))
110 | c.Assert(err, IsNil)
111 | err = w.Close()
112 | c.Assert(err, IsNil)
113 |
114 | c.Assert(w.Digest(), Equals, data1Digest)
115 |
116 | data1, err := cc.Read(data1Digest)
117 | c.Assert(err, IsNil)
118 | c.Assert(string(data1), Equals, "data1")
119 | }
120 |
121 | func (s *S) TestCacheWrongDigest(c *C) {
122 | cc := cache.Cache{Dir: c.MkDir()}
123 |
124 | w := cc.Create(data1Digest)
125 |
126 | c.Assert(w.Digest(), Equals, data1Digest)
127 |
128 | _, err := w.Write([]byte("data2"))
129 | errClose := w.Close()
130 | c.Assert(err, IsNil)
131 | c.Assert(errClose, ErrorMatches, "expected digest "+data1Digest+", got "+data2Digest)
132 |
133 | _, err = cc.Read(data1Digest)
134 | c.Assert(err, Equals, cache.MissErr)
135 | _, err = cc.Read(data2Digest)
136 | c.Assert(err, Equals, cache.MissErr)
137 | }
138 |
139 | func (s *S) TestCacheOpen(c *C) {
140 | cc := cache.Cache{Dir: c.MkDir()}
141 |
142 | err := cc.Write(data1Digest, []byte("data1"))
143 | c.Assert(err, IsNil)
144 |
145 | f, err := cc.Open(data1Digest)
146 | c.Assert(err, IsNil)
147 | data1, err := io.ReadAll(f)
148 | closeErr := f.Close()
149 | c.Assert(err, IsNil)
150 | c.Assert(closeErr, IsNil)
151 |
152 | c.Assert(string(data1), Equals, "data1")
153 | }
154 |
--------------------------------------------------------------------------------
/internal/cache/suite_test.go:
--------------------------------------------------------------------------------
1 | package cache_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "gopkg.in/check.v1"
7 | )
8 |
9 | func Test(t *testing.T) { TestingT(t) }
10 |
11 | type S struct{}
12 |
13 | var _ = Suite(&S{})
14 |
--------------------------------------------------------------------------------
/internal/control/control.go:
--------------------------------------------------------------------------------
1 | package control
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "strings"
7 | )
8 |
9 | // The logic in this file is supposed to be fast so that parsing large data
10 | // files feels instantaneous. It does that by performing a fast scan once to
11 | // index the sections, and then rather than parsing the individual sections it
12 | // scans fields directly on retrieval. That means the whole content is loaded
13 | // in memory at once and without impact to the GC. Should be a good enough
14 | // strategy for the sort of files handled, with long documents of sections
15 | // that are relatively few fields long.
16 |
17 | type File interface {
18 | Section(key string) Section
19 | }
20 |
21 | type Section interface {
22 | Get(key string) string
23 | }
24 |
25 | type ctrlFile struct {
26 | // For the moment content is cached as a string internally as it's faster
27 | // to convert it all at once and remaining operations will not involve
28 | // the GC for the individual string data.
29 | content string
30 | sections map[string]ctrlPos
31 | sectionKey string
32 | }
33 |
34 | func (f *ctrlFile) Section(key string) Section {
35 | if pos, ok := f.sections[key]; ok {
36 | return &ctrlSection{f.content[pos.start:pos.end]}
37 | }
38 | return nil
39 | }
40 |
41 | type ctrlSection struct {
42 | content string
43 | }
44 |
45 | func (s *ctrlSection) Get(key string) string {
46 | content := s.content
47 | pos := 0
48 | if len(content) > len(key)+1 && content[:len(key)] == key && content[len(key)] == ':' {
49 | // Key is on the first line.
50 | pos = len(key) + 1
51 | } else {
52 | prefix := "\n" + key + ":"
53 | pos = strings.Index(content, prefix)
54 | if pos < 0 {
55 | return ""
56 | }
57 | pos += len(prefix)
58 | if pos+1 > len(content) {
59 | return ""
60 | }
61 | }
62 | if content[pos] == ' ' {
63 | pos++
64 | }
65 | eol := strings.Index(content[pos:], "\n")
66 | if eol < 0 {
67 | eol = len(content)
68 | } else {
69 | eol += pos
70 | }
71 | value := content[pos:eol]
72 | if eol+1 >= len(content) || content[eol+1] != ' ' && content[eol+1] != '\t' {
73 | // Single line value.
74 | return value
75 | }
76 | // Multi line value so we'll need to allocate.
77 | var multi bytes.Buffer
78 | if len(value) > 0 {
79 | multi.WriteString(value)
80 | multi.WriteByte('\n')
81 | }
82 | for {
83 | pos = eol + 2
84 | eol = strings.Index(content[pos:], "\n")
85 | if eol < 0 {
86 | eol = len(content)
87 | } else {
88 | eol += pos
89 | }
90 | multi.WriteString(content[pos:eol])
91 | if eol+1 >= len(content) || content[eol+1] != ' ' && content[eol+1] != '\t' {
92 | break
93 | }
94 | multi.WriteByte('\n')
95 | }
96 | return multi.String()
97 | }
98 |
99 | type ctrlPos struct {
100 | start, end int
101 | }
102 |
103 | func ParseReader(sectionKey string, content io.Reader) (File, error) {
104 | data, err := io.ReadAll(content)
105 | if err != nil {
106 | return nil, err
107 | }
108 | return ParseString(sectionKey, string(data))
109 | }
110 |
111 | func ParseString(sectionKey, content string) (File, error) {
112 | skey := sectionKey + ": "
113 | skeylen := len(skey)
114 | sections := make(map[string]ctrlPos)
115 | start := 0
116 | pos := start
117 | for pos < len(content) {
118 | eol := strings.Index(content[pos:], "\n")
119 | if eol < 0 {
120 | eol = len(content)
121 | } else {
122 | eol += pos
123 | }
124 | if pos+skeylen < len(content) && content[pos:pos+skeylen] == skey {
125 | pos += skeylen
126 | end := strings.Index(content[pos:], "\n\n")
127 | if end < 0 {
128 | end = len(content)
129 | } else {
130 | end += pos
131 | }
132 | sections[content[pos:eol]] = ctrlPos{start, end}
133 | pos = end + 2
134 | start = pos
135 | } else {
136 | pos = eol + 1
137 | }
138 | }
139 | return &ctrlFile{
140 | content: content,
141 | sections: sections,
142 | sectionKey: sectionKey,
143 | }, nil
144 | }
145 |
--------------------------------------------------------------------------------
/internal/control/control_test.go:
--------------------------------------------------------------------------------
1 | package control_test
2 |
3 | import (
4 | "github.com/canonical/chisel/internal/control"
5 |
6 | "bytes"
7 | "os"
8 | "testing"
9 |
10 | . "gopkg.in/check.v1"
11 | )
12 |
13 | var testFile = `` +
14 | `Section: one
15 | Line: line for one
16 | Multi:
17 | multi line
18 | for one
19 |
20 | Line: line for two
21 | Multi: multi line
22 | for two
23 | Section: two
24 |
25 | Multi:
26 | multi line
27 | for three
28 | Line: line for three
29 | Section: three
30 |
31 | Section: four
32 | Multi:
33 | Space at EOL above
34 | Extra space
35 | One tab
36 | `
37 |
38 | var testFileResults = map[string]map[string]string{
39 | "one": map[string]string{
40 | "Section": "one",
41 | "Line": "line for one",
42 | "Multi": "multi line\nfor one",
43 | },
44 | "two": map[string]string{
45 | "Section": "two",
46 | "Line": "line for two",
47 | "Multi": "multi line\nfor two",
48 | },
49 | "three": map[string]string{
50 | "Section": "three",
51 | "Line": "line for three",
52 | "Multi": "multi line\nfor three",
53 | },
54 | "four": map[string]string{
55 | "Multi": "Space at EOL above\n Extra space\nOne tab",
56 | },
57 | }
58 |
59 | func (s *S) TestParseString(c *C) {
60 | file, err := control.ParseString("Section", testFile)
61 | c.Assert(err, IsNil)
62 |
63 | for skey, svalues := range testFileResults {
64 | section := file.Section(skey)
65 | for key, value := range svalues {
66 | c.Assert(section.Get(key), Equals, value, Commentf("Section %q / Key %q", skey, key))
67 | }
68 | }
69 | }
70 |
71 | func (s *S) TestParseReader(c *C) {
72 | file, err := control.ParseReader("Section", bytes.NewReader([]byte(testFile)))
73 | c.Assert(err, IsNil)
74 |
75 | for skey, svalues := range testFileResults {
76 | section := file.Section(skey)
77 | for key, value := range svalues {
78 | c.Assert(section.Get(key), Equals, value, Commentf("Section %q / Key %q", skey, key))
79 | }
80 | }
81 | }
82 |
83 | func BenchmarkParse(b *testing.B) {
84 | data, err := os.ReadFile("Packages")
85 | if err != nil {
86 | b.Fatalf("cannot open Packages file: %v", err)
87 | }
88 | content := string(data)
89 | b.ResetTimer()
90 | for i := 0; i < b.N; i++ {
91 | _, err := control.ParseString("Package", content)
92 | if err != nil {
93 | panic(err)
94 | }
95 | }
96 | }
97 |
98 | func BenchmarkSectionGet(b *testing.B) {
99 | data, err := os.ReadFile("Packages")
100 | if err != nil {
101 | b.Fatalf("cannot open Packages file: %v", err)
102 | }
103 | content := string(data)
104 | file, err := control.ParseString("Package", content)
105 | if err != nil {
106 | panic(err)
107 | }
108 | b.ResetTimer()
109 | for i := 0; i < b.N; i++ {
110 | section := file.Section("util-linux")
111 | value := section.Get("Description")
112 | if value != "miscellaneous system utilities" {
113 | b.Fatalf("Unexpected package description: %q", value)
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/internal/control/helpers.go:
--------------------------------------------------------------------------------
1 | package control
2 |
3 | import (
4 | "regexp"
5 | "strconv"
6 | "strings"
7 | )
8 |
9 | var pathInfoExp = regexp.MustCompile(`([a-f0-9]{32,}) +([0-9]+) +\S+`)
10 |
11 | func ParsePathInfo(table, path string) (digest string, size int, ok bool) {
12 | pos := strings.Index(table, " "+path+"\n")
13 | if pos == -1 {
14 | if !strings.HasSuffix(table, " "+path) {
15 | return "", -1, false
16 | }
17 | pos = len(table) - len(path)
18 | } else {
19 | pos++
20 | }
21 | eol := pos + len(path)
22 | for pos > 0 && table[pos] != '\n' {
23 | pos--
24 | }
25 | match := pathInfoExp.FindStringSubmatch(table[pos:eol])
26 | if match == nil {
27 | return "", -1, false
28 | }
29 | size, err := strconv.Atoi(match[2])
30 | if err != nil {
31 | panic("internal error: FindPathInfo regexp is wrong")
32 | }
33 | return match[1], size, true
34 | }
35 |
--------------------------------------------------------------------------------
/internal/control/helpers_test.go:
--------------------------------------------------------------------------------
1 | package control_test
2 |
3 | import (
4 | "github.com/canonical/chisel/internal/control"
5 |
6 | . "gopkg.in/check.v1"
7 | )
8 |
9 | type parsePathInfoTest struct {
10 | table string
11 | path string
12 | size int
13 | digest string
14 | }
15 |
16 | var parsePathInfoTests = []parsePathInfoTest{{
17 | table: `
18 | before 1 /one/path
19 | 0123456789abcdef0123456789abcdef 2 /the/path
20 | after 2 /two/path
21 | `,
22 | path: "/the/path",
23 | size: 2,
24 | digest: "0123456789abcdef0123456789abcdef",
25 | }, {
26 | table: `
27 | 0123456789abcdef0123456789abcdef 1 /the/path
28 | after 2 /two/path
29 | `,
30 | path: "/the/path",
31 | size: 1,
32 | digest: "0123456789abcdef0123456789abcdef",
33 | }, {
34 | table: `
35 | before 1 /two/path
36 | 0123456789abcdef0123456789abcdef 2 /the/path
37 | `,
38 | path: "/the/path",
39 | size: 2,
40 | digest: "0123456789abcdef0123456789abcdef",
41 | }, {
42 | table: `0123456789abcdef0123456789abcdef 0 /the/path`,
43 | path: "/the/path",
44 | size: 0,
45 | digest: "0123456789abcdef0123456789abcdef",
46 | }, {
47 | table: `0123456789abcdef0123456789abcdef 555 /the/path`,
48 | path: "/the/path",
49 | size: 555,
50 | digest: "0123456789abcdef0123456789abcdef",
51 | }, {
52 | table: `deadbeef 0 /the/path`,
53 | path: "/the/path",
54 | digest: "",
55 | }, {
56 | table: `bad-data 0 /the/path`,
57 | path: "/the/path",
58 | digest: "",
59 | }}
60 |
61 | func (s *S) TestParsePathInfo(c *C) {
62 | for _, test := range parsePathInfoTests {
63 | c.Logf("Path is %q, expecting digest %q and size %d.", test.path, test.digest, test.size)
64 | digest, size, ok := control.ParsePathInfo(test.table, test.path)
65 | c.Logf("Got digest %q, size %d, ok %v.", digest, size, ok)
66 | if test.digest == "" {
67 | c.Assert(digest, Equals, "")
68 | c.Assert(size, Equals, -1)
69 | c.Assert(ok, Equals, false)
70 | } else {
71 | c.Assert(digest, Equals, test.digest)
72 | c.Assert(size, Equals, test.size)
73 | c.Assert(ok, Equals, true)
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/internal/control/suite_test.go:
--------------------------------------------------------------------------------
1 | package control_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "gopkg.in/check.v1"
7 | )
8 |
9 | func Test(t *testing.T) { TestingT(t) }
10 |
11 | type S struct{}
12 |
13 | var _ = Suite(&S{})
14 |
--------------------------------------------------------------------------------
/internal/deb/chrorder.go:
--------------------------------------------------------------------------------
1 | // auto-generated, DO NOT EDIT!
2 | package deb
3 |
4 | var chOrder = [...]int{
5 | -5, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271,
6 | 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287,
7 | 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303,
8 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 314, 315, 316, 317, 318, 319,
9 | 320, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79,
10 | 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 347, 348, 349, 350, 351,
11 | 352, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111,
12 | 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 379, 380, 381, -10, 383,
13 | 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399,
14 | 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415,
15 | 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431,
16 | 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447,
17 | 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463,
18 | 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479,
19 | 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495,
20 | 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511,
21 | }
22 |
--------------------------------------------------------------------------------
/internal/deb/chrorder/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "os"
7 | "regexp"
8 | )
9 |
10 | var (
11 | matchDigit = regexp.MustCompile("[0-9]").Match
12 | matchAlpha = regexp.MustCompile("[a-zA-Z]").Match
13 | )
14 |
15 | func chOrder(ch uint8) int {
16 | // "~" is lower than everything else
17 | if ch == '~' {
18 | return -10
19 | }
20 | // empty is higher than "~" but lower than everything else
21 | if ch == 0 {
22 | return -5
23 | }
24 | if matchAlpha([]byte{ch}) {
25 | return int(ch)
26 | }
27 |
28 | // can only happen if cmpString sets '0' because there is no fragment
29 | if matchDigit([]byte{ch}) {
30 | return 0
31 | }
32 |
33 | return int(ch) + 256
34 | }
35 |
36 | func main() {
37 | var outFile string
38 | var pkgName string
39 | flag.StringVar(&outFile, "output", "-", "output file")
40 | flag.StringVar(&pkgName, "package", "foo", "package name")
41 | flag.Parse()
42 |
43 | out := os.Stdout
44 | if outFile != "" && outFile != "-" {
45 | var err error
46 | out, err = os.Create(outFile)
47 | if err != nil {
48 | fmt.Fprintf(os.Stderr, "error: %v", err)
49 | os.Exit(1)
50 | }
51 | defer out.Close()
52 | }
53 |
54 | if pkgName == "" {
55 | pkgName = "foo"
56 | }
57 |
58 | fmt.Fprintln(out, "// auto-generated, DO NOT EDIT!")
59 | fmt.Fprintf(out, "package %v\n", pkgName)
60 | fmt.Fprintf(out, "\n")
61 | fmt.Fprintln(out, "var chOrder = [...]int{")
62 | for i := 0; i < 16; i++ {
63 | fmt.Fprintf(out, "\t")
64 | for j := 0; j < 16; j++ {
65 | if j != 0 {
66 | fmt.Fprintf(out, " ")
67 | }
68 | fmt.Fprintf(out, "%d,", chOrder(uint8(i*16+j)))
69 |
70 | }
71 | fmt.Fprintf(out, "\n")
72 | }
73 | fmt.Fprintln(out, "}")
74 | }
75 |
--------------------------------------------------------------------------------
/internal/deb/export_test.go:
--------------------------------------------------------------------------------
1 | package deb
2 |
3 | func FakePlatformGoArch(goArch string) (restore func()) {
4 | saved := platformGoArch
5 | platformGoArch = goArch
6 | return func() { platformGoArch = saved }
7 | }
8 |
--------------------------------------------------------------------------------
/internal/deb/helpers.go:
--------------------------------------------------------------------------------
1 | package deb
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 | )
7 |
8 | type archPair struct {
9 | goArch string
10 | debArch string
11 | }
12 |
13 | var knownArchs = []archPair{
14 | {"386", "i386"},
15 | {"amd64", "amd64"},
16 | {"arm", "armhf"},
17 | {"arm64", "arm64"},
18 | {"ppc64le", "ppc64el"},
19 | {"riscv64", "riscv64"},
20 | {"s390x", "s390x"},
21 | }
22 |
23 | var platformGoArch = runtime.GOARCH
24 |
25 | func InferArch() (string, error) {
26 | for _, arch := range knownArchs {
27 | if arch.goArch == platformGoArch {
28 | return arch.debArch, nil
29 | }
30 | }
31 | return "", fmt.Errorf("cannot infer package architecture from current platform architecture: %s", platformGoArch)
32 | }
33 |
34 | func ValidateArch(debArch string) error {
35 | for _, arch := range knownArchs {
36 | if arch.debArch == debArch {
37 | return nil
38 | }
39 | }
40 | return fmt.Errorf("invalid package architecture: %s", debArch)
41 | }
42 |
--------------------------------------------------------------------------------
/internal/deb/helpers_test.go:
--------------------------------------------------------------------------------
1 | package deb_test
2 |
3 | import (
4 | "github.com/canonical/chisel/internal/deb"
5 | . "gopkg.in/check.v1"
6 | )
7 |
8 | func inferArchFromPlatform(platformArch string) string {
9 | restore := deb.FakePlatformGoArch(platformArch)
10 | defer restore()
11 | goArch, _ := deb.InferArch()
12 | return goArch
13 | }
14 |
15 | func (s *S) TestInferArch(c *C) {
16 | c.Assert(inferArchFromPlatform("386"), Equals, "i386")
17 | c.Assert(inferArchFromPlatform("amd64"), Equals, "amd64")
18 | c.Assert(inferArchFromPlatform("arm"), Equals, "armhf")
19 | c.Assert(inferArchFromPlatform("arm64"), Equals, "arm64")
20 | c.Assert(inferArchFromPlatform("ppc64le"), Equals, "ppc64el")
21 | c.Assert(inferArchFromPlatform("riscv64"), Equals, "riscv64")
22 | c.Assert(inferArchFromPlatform("s390x"), Equals, "s390x")
23 | c.Assert(inferArchFromPlatform("i386"), Equals, "")
24 | c.Assert(inferArchFromPlatform("armhf"), Equals, "")
25 | c.Assert(inferArchFromPlatform("ppc64el"), Equals, "")
26 | c.Assert(inferArchFromPlatform("foo"), Equals, "")
27 | c.Assert(inferArchFromPlatform(""), Equals, "")
28 | }
29 |
30 | func (s *S) TestValidateArch(c *C) {
31 | c.Assert(deb.ValidateArch("i386"), IsNil)
32 | c.Assert(deb.ValidateArch("amd64"), IsNil)
33 | c.Assert(deb.ValidateArch("armhf"), IsNil)
34 | c.Assert(deb.ValidateArch("arm64"), IsNil)
35 | c.Assert(deb.ValidateArch("ppc64el"), IsNil)
36 | c.Assert(deb.ValidateArch("riscv64"), IsNil)
37 | c.Assert(deb.ValidateArch("s390x"), IsNil)
38 | c.Assert(deb.ValidateArch("386"), Not(IsNil))
39 | c.Assert(deb.ValidateArch("arm"), Not(IsNil))
40 | c.Assert(deb.ValidateArch("ppc64le"), Not(IsNil))
41 | c.Assert(deb.ValidateArch("foo"), Not(IsNil))
42 | c.Assert(deb.ValidateArch("i3866"), Not(IsNil))
43 | c.Assert(deb.ValidateArch(""), Not(IsNil))
44 | }
45 |
--------------------------------------------------------------------------------
/internal/deb/log.go:
--------------------------------------------------------------------------------
1 | package deb
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | )
7 |
8 | // Avoid importing the log type information unnecessarily. There's a small cost
9 | // associated with using an interface rather than the type. Depending on how
10 | // often the logger is plugged in, it would be worth using the type instead.
11 | type log_Logger interface {
12 | Output(calldepth int, s string) error
13 | }
14 |
15 | var globalLoggerLock sync.Mutex
16 | var globalLogger log_Logger
17 | var globalDebug bool
18 |
19 | // Specify the *log.Logger object where log messages should be sent to.
20 | func SetLogger(logger log_Logger) {
21 | globalLoggerLock.Lock()
22 | globalLogger = logger
23 | globalLoggerLock.Unlock()
24 | }
25 |
26 | // Enable the delivery of debug messages to the logger. Only meaningful
27 | // if a logger is also set.
28 | func SetDebug(debug bool) {
29 | globalLoggerLock.Lock()
30 | globalDebug = debug
31 | globalLoggerLock.Unlock()
32 | }
33 |
34 | // logf sends to the logger registered via SetLogger the string resulting
35 | // from running format and args through Sprintf.
36 | func logf(format string, args ...interface{}) {
37 | globalLoggerLock.Lock()
38 | defer globalLoggerLock.Unlock()
39 | if globalLogger != nil {
40 | globalLogger.Output(2, fmt.Sprintf(format, args...))
41 | }
42 | }
43 |
44 | // debugf sends to the logger registered via SetLogger the string resulting
45 | // from running format and args through Sprintf, but only if debugging was
46 | // enabled via SetDebug.
47 | func debugf(format string, args ...interface{}) {
48 | globalLoggerLock.Lock()
49 | defer globalLoggerLock.Unlock()
50 | if globalDebug && globalLogger != nil {
51 | globalLogger.Output(2, fmt.Sprintf(format, args...))
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/internal/deb/suite_test.go:
--------------------------------------------------------------------------------
1 | package deb_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "gopkg.in/check.v1"
7 |
8 | "github.com/canonical/chisel/internal/deb"
9 | )
10 |
11 | func Test(t *testing.T) { TestingT(t) }
12 |
13 | type S struct{}
14 |
15 | var _ = Suite(&S{})
16 |
17 | func (s *S) SetUpTest(c *C) {
18 | deb.SetDebug(true)
19 | deb.SetLogger(c)
20 | }
21 |
22 | func (s *S) TearDownTest(c *C) {
23 | deb.SetDebug(false)
24 | deb.SetLogger(nil)
25 | }
26 |
--------------------------------------------------------------------------------
/internal/deb/version.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * Copyright (C) 2014-2017 Canonical Ltd
5 | *
6 | * This program is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License version 3 as
8 | * published by the Free Software Foundation.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | *
18 | */
19 |
20 | package deb
21 |
22 | import (
23 | "strings"
24 | )
25 |
26 | func max(a, b int) int {
27 | if a < b {
28 | return b
29 | }
30 | return a
31 | }
32 |
33 | //go:generate go run ./chrorder/main.go -package=deb -output=chrorder.go
34 |
35 | func cmpString(as, bs string) int {
36 | for i := 0; i < max(len(as), len(bs)); i++ {
37 | var a uint8
38 | var b uint8
39 | if i < len(as) {
40 | a = as[i]
41 | }
42 | if i < len(bs) {
43 | b = bs[i]
44 | }
45 | if chOrder[a] < chOrder[b] {
46 | return -1
47 | }
48 | if chOrder[a] > chOrder[b] {
49 | return +1
50 | }
51 | }
52 | return 0
53 | }
54 |
55 | func trimLeadingZeroes(a string) string {
56 | for i := 0; i < len(a); i++ {
57 | if a[i] != '0' {
58 | return a[i:]
59 | }
60 | }
61 | return ""
62 | }
63 |
64 | // a and b both match /[0-9]+/
65 | func cmpNumeric(a, b string) int {
66 | a = trimLeadingZeroes(a)
67 | b = trimLeadingZeroes(b)
68 |
69 | switch d := len(a) - len(b); {
70 | case d > 0:
71 | return 1
72 | case d < 0:
73 | return -1
74 | }
75 | for i := 0; i < len(a); i++ {
76 | switch {
77 | case a[i] > b[i]:
78 | return 1
79 | case a[i] < b[i]:
80 | return -1
81 | }
82 | }
83 | return 0
84 | }
85 |
86 | func nextFrag(s string) (frag, rest string, numeric bool) {
87 | if len(s) == 0 {
88 | return "", "", false
89 | }
90 |
91 | var i int
92 | if s[0] >= '0' && s[0] <= '9' {
93 | // is digit
94 | for i = 1; i < len(s) && s[i] >= '0' && s[i] <= '9'; i++ {
95 | }
96 | numeric = true
97 | } else {
98 | // not digit
99 | for i = 1; i < len(s) && (s[i] < '0' || s[i] > '9'); i++ {
100 | }
101 | }
102 | return s[:i], s[i:], numeric
103 | }
104 |
105 | func compareSubversion(va, vb string) int {
106 | var a, b string
107 | var anum, bnum bool
108 | var res int
109 | for res == 0 {
110 | a, va, anum = nextFrag(va)
111 | b, vb, bnum = nextFrag(vb)
112 | if a == "" && b == "" {
113 | break
114 | }
115 | if anum && bnum {
116 | res = cmpNumeric(a, b)
117 | } else {
118 | res = cmpString(a, b)
119 | }
120 | }
121 | return res
122 | }
123 |
124 | // CompareVersions compare two version strings that follow the debian
125 | // version policy and
126 | // Returns:
127 | //
128 | // -1 if a is smaller than b
129 | // 0 if a equals b
130 | // +1 if a is bigger than b
131 | func CompareVersions(va, vb string) int {
132 | var sa, sb string
133 | if ia := strings.IndexByte(va, '-'); ia < 0 {
134 | sa = "0"
135 | } else {
136 | va, sa = va[:ia], va[ia+1:]
137 | }
138 | if ib := strings.IndexByte(vb, '-'); ib < 0 {
139 | sb = "0"
140 | } else {
141 | vb, sb = vb[:ib], vb[ib+1:]
142 | }
143 |
144 | // the main version number (before the "-")
145 | res := compareSubversion(va, vb)
146 | if res != 0 {
147 | return res
148 | }
149 |
150 | // the subversion revision behind the "-"
151 | return compareSubversion(sa, sb)
152 | }
153 |
--------------------------------------------------------------------------------
/internal/deb/version_test.go:
--------------------------------------------------------------------------------
1 | // -*- Mode: Go; indent-tabs-mode: t -*-
2 |
3 | /*
4 | * Copyright (C) 2014-2015 Canonical Ltd
5 | *
6 | * This program is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License version 3 as
8 | * published by the Free Software Foundation.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | *
18 | */
19 |
20 | package deb_test
21 |
22 | import (
23 | . "gopkg.in/check.v1"
24 |
25 | "github.com/canonical/chisel/internal/deb"
26 | )
27 |
28 | type VersionTestSuite struct{}
29 |
30 | var _ = Suite(&VersionTestSuite{})
31 |
32 | func (s *VersionTestSuite) TestVersionCompare(c *C) {
33 | for _, t := range []struct {
34 | A, B string
35 | res int
36 | }{
37 | {"20000000000000000000", "020000000000000000000", 0},
38 | {"1.0", "2.0", -1},
39 | {"1.3", "1.2.2.2", 1},
40 | {"1.3", "1.3.1", -1},
41 | {"1.0", "1.0~", 1},
42 | {"7.2p2", "7.2", 1},
43 | {"0.4a6", "0.4", 1},
44 | {"0pre", "0pre", 0},
45 | {"0pree", "0pre", 1},
46 | {"1.18.36:5.4", "1.18.36:5.5", -1},
47 | {"1.18.36:5.4", "1.18.37:1.1", -1},
48 | {"2.0.7pre1", "2.0.7r", -1},
49 | {"0.10.0", "0.8.7", 1},
50 | // subrev
51 | {"1.0-1", "1.0-2", -1},
52 | {"1.0-1.1", "1.0-1", 1},
53 | {"1.0-1.1", "1.0-1.1", 0},
54 | // do we like strange versions? Yes we like strange versions…
55 | {"0", "0", 0},
56 | {"0", "00", 0},
57 | {"", "", 0},
58 | {"", "0", -1},
59 | {"0", "", 1},
60 | {"", "~", 1},
61 | {"~", "", -1},
62 | // from the apt suite
63 | {"0-pre", "0-pre", 0},
64 | {"0-pre", "0-pree", -1},
65 | {"1.1.6r2-2", "1.1.6r-1", 1},
66 | {"2.6b2-1", "2.6b-2", 1},
67 | {"0.4a6-2", "0.4-1", 1},
68 | {"3.0~rc1-1", "3.0-1", -1},
69 | {"1.0", "1.0-0", 0},
70 | {"0.2", "1.0-0", -1},
71 | {"1.0", "1.0-0+b1", -1},
72 | {"1.0", "1.0-0~", 1},
73 | // from the old perl cupt
74 | {"1.2.3", "1.2.3", 0}, // identical
75 | {"4.4.3-2", "4.4.3-2", 0}, // identical
76 | {"1.2.3", "1.2.3-0", 0}, // zero revision
77 | {"009", "9", 0}, // zeroes…
78 | {"009ab5", "9ab5", 0}, // there as well
79 | {"1.2.3", "1.2.3-1", -1}, // added non-zero revision
80 | {"1.2.3", "1.2.4", -1}, // just bigger
81 | {"1.2.4", "1.2.3", 1}, // order doesn't matter
82 | {"1.2.24", "1.2.3", 1}, // bigger, eh?
83 | {"0.10.0", "0.8.7", 1}, // bigger, eh?
84 | {"3.2", "2.3", 1}, // major number rocks
85 | {"1.3.2a", "1.3.2", 1}, // letters rock
86 | {"0.5.0~git", "0.5.0~git2", -1}, // numbers rock
87 | {"2a", "21", -1}, // but not in all places
88 | {"1.2a+~bCd3", "1.2a++", -1}, // tilde doesn't rock
89 | {"1.2a+~bCd3", "1.2a+~", 1}, // but first is longer!
90 | {"5.10.0", "5.005", 1}, // preceding zeroes don't matters
91 | {"3a9.8", "3.10.2", -1}, // letters are before all letter symbols
92 | {"3a9.8", "3~10", 1}, // but after the tilde
93 | {"1.4+OOo3.0.0~", "1.4+OOo3.0.0-4", -1}, // another tilde check
94 | {"2.4.7-1", "2.4.7-z", -1}, // revision comparing
95 | {"1.002-1+b2", "1.00", 1}, // whatever...
96 | {"12-20220319-1ubuntu1", "12-20220319-1ubuntu2", -1}, // libgcc-s1
97 | {"1:13.0.1-2ubuntu2", "1:13.0.1-2ubuntu3", -1},
98 | } {
99 | res := deb.CompareVersions(t.A, t.B)
100 | c.Assert(res, Equals, t.res, Commentf("%#v %#v: %v but got %v", t.A, t.B, res, t.res))
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/internal/fsutil/log.go:
--------------------------------------------------------------------------------
1 | package fsutil
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | )
7 |
8 | // Avoid importing the log type information unnecessarily. There's a small cost
9 | // associated with using an interface rather than the type. Depending on how
10 | // often the logger is plugged in, it would be worth using the type instead.
11 | type log_Logger interface {
12 | Output(calldepth int, s string) error
13 | }
14 |
15 | var globalLoggerLock sync.Mutex
16 | var globalLogger log_Logger
17 | var globalDebug bool
18 |
19 | // Specify the *log.Logger object where log messages should be sent to.
20 | func SetLogger(logger log_Logger) {
21 | globalLoggerLock.Lock()
22 | globalLogger = logger
23 | globalLoggerLock.Unlock()
24 | }
25 |
26 | // Enable the delivery of debug messages to the logger. Only meaningful
27 | // if a logger is also set.
28 | func SetDebug(debug bool) {
29 | globalLoggerLock.Lock()
30 | globalDebug = debug
31 | globalLoggerLock.Unlock()
32 | }
33 |
34 | // logf sends to the logger registered via SetLogger the string resulting
35 | // from running format and args through Sprintf.
36 | func logf(format string, args ...interface{}) {
37 | globalLoggerLock.Lock()
38 | defer globalLoggerLock.Unlock()
39 | if globalLogger != nil {
40 | globalLogger.Output(2, fmt.Sprintf(format, args...))
41 | }
42 | }
43 |
44 | // debugf sends to the logger registered via SetLogger the string resulting
45 | // from running format and args through Sprintf, but only if debugging was
46 | // enabled via SetDebug.
47 | func debugf(format string, args ...interface{}) {
48 | globalLoggerLock.Lock()
49 | defer globalLoggerLock.Unlock()
50 | if globalDebug && globalLogger != nil {
51 | globalLogger.Output(2, fmt.Sprintf(format, args...))
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/internal/fsutil/suite_test.go:
--------------------------------------------------------------------------------
1 | package fsutil_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "gopkg.in/check.v1"
7 |
8 | "github.com/canonical/chisel/internal/fsutil"
9 | )
10 |
11 | func Test(t *testing.T) { TestingT(t) }
12 |
13 | type S struct{}
14 |
15 | var _ = Suite(&S{})
16 |
17 | func (s *S) SetUpTest(c *C) {
18 | fsutil.SetDebug(true)
19 | fsutil.SetLogger(c)
20 | }
21 |
22 | func (s *S) TearDownTest(c *C) {
23 | fsutil.SetDebug(false)
24 | fsutil.SetLogger(nil)
25 | }
26 |
--------------------------------------------------------------------------------
/internal/manifestutil/log.go:
--------------------------------------------------------------------------------
1 | package manifestutil
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | )
7 |
8 | // Avoid importing the log type information unnecessarily. There's a small cost
9 | // associated with using an interface rather than the type. Depending on how
10 | // often the logger is plugged in, it would be worth using the type instead.
11 | type log_Logger interface {
12 | Output(calldepth int, s string) error
13 | }
14 |
15 | var globalLoggerLock sync.Mutex
16 | var globalLogger log_Logger
17 | var globalDebug bool
18 |
19 | // Specify the *log.Logger object where log messages should be sent to.
20 | func SetLogger(logger log_Logger) {
21 | globalLoggerLock.Lock()
22 | globalLogger = logger
23 | globalLoggerLock.Unlock()
24 | }
25 |
26 | // Enable the delivery of debug messages to the logger. Only meaningful
27 | // if a logger is also set.
28 | func SetDebug(debug bool) {
29 | globalLoggerLock.Lock()
30 | globalDebug = debug
31 | globalLoggerLock.Unlock()
32 | }
33 |
34 | // logf sends to the logger registered via SetLogger the string resulting
35 | // from running format and args through Sprintf.
36 | func logf(format string, args ...interface{}) {
37 | globalLoggerLock.Lock()
38 | defer globalLoggerLock.Unlock()
39 | if globalLogger != nil {
40 | globalLogger.Output(2, fmt.Sprintf(format, args...))
41 | }
42 | }
43 |
44 | // debugf sends to the logger registered via SetLogger the string resulting
45 | // from running format and args through Sprintf, but only if debugging was
46 | // enabled via SetDebug.
47 | func debugf(format string, args ...interface{}) {
48 | globalLoggerLock.Lock()
49 | defer globalLoggerLock.Unlock()
50 | if globalDebug && globalLogger != nil {
51 | globalLogger.Output(2, fmt.Sprintf(format, args...))
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/internal/manifestutil/report.go:
--------------------------------------------------------------------------------
1 | package manifestutil
2 |
3 | import (
4 | "fmt"
5 | "io/fs"
6 | "path/filepath"
7 | "strings"
8 |
9 | "github.com/canonical/chisel/internal/fsutil"
10 | "github.com/canonical/chisel/internal/setup"
11 | )
12 |
13 | type ReportEntry struct {
14 | Path string
15 | Mode fs.FileMode
16 | SHA256 string
17 | Size int
18 | Slices map[*setup.Slice]bool
19 | Link string
20 | FinalSHA256 string
21 | // If Inode is greater than 0, all entries represent hard links to the same
22 | // inode.
23 | Inode uint64
24 | }
25 |
26 | // Report holds the information about files and directories created when slicing
27 | // packages.
28 | type Report struct {
29 | // Root is the filesystem path where the all reported content is based.
30 | Root string
31 | // Entries holds all reported content, indexed by their path.
32 | Entries map[string]ReportEntry
33 | // lastInode is used internally to allocate unique Inode for hard
34 | // links.
35 | lastInode uint64
36 | }
37 |
38 | // NewReport returns an empty report for content that will be based at the
39 | // provided root path.
40 | func NewReport(root string) (*Report, error) {
41 | if !filepath.IsAbs(root) {
42 | return nil, fmt.Errorf("cannot use relative path for report root: %q", root)
43 | }
44 | root = filepath.Clean(root)
45 | if root != "/" {
46 | root = filepath.Clean(root) + "/"
47 | }
48 | report := &Report{
49 | Root: root,
50 | Entries: make(map[string]ReportEntry),
51 | }
52 | return report, nil
53 | }
54 |
55 | func (r *Report) Add(slice *setup.Slice, fsEntry *fsutil.Entry) error {
56 | relPath, err := r.sanitizeAbsPath(fsEntry.Path, fsEntry.Mode.IsDir())
57 | if err != nil {
58 | return fmt.Errorf("cannot add path to report: %s", err)
59 | }
60 |
61 | var inode uint64
62 | fsEntryCpy := *fsEntry
63 | if fsEntry.Mode.IsRegular() && fsEntry.Link != "" {
64 | // Hard link.
65 | relLinkPath, _ := r.sanitizeAbsPath(fsEntry.Link, false)
66 | entry, ok := r.Entries[relLinkPath]
67 | if !ok {
68 | return fmt.Errorf("cannot add hard link %s to report: target %s not previously added", relPath, relLinkPath)
69 | }
70 | if entry.Inode == 0 {
71 | r.lastInode += 1
72 | entry.Inode = r.lastInode
73 | r.Entries[relLinkPath] = entry
74 | }
75 | inode = entry.Inode
76 | fsEntryCpy.SHA256 = entry.SHA256
77 | fsEntryCpy.Size = entry.Size
78 | fsEntryCpy.Link = entry.Link
79 | }
80 |
81 | if entry, ok := r.Entries[relPath]; ok {
82 | if fsEntryCpy.Mode != entry.Mode {
83 | return fmt.Errorf("path %s reported twice with diverging mode: 0%03o != 0%03o", relPath, fsEntryCpy.Mode, entry.Mode)
84 | } else if fsEntryCpy.Link != entry.Link {
85 | return fmt.Errorf("path %s reported twice with diverging link: %q != %q", relPath, fsEntryCpy.Link, entry.Link)
86 | } else if fsEntryCpy.Size != entry.Size {
87 | return fmt.Errorf("path %s reported twice with diverging size: %d != %d", relPath, fsEntryCpy.Size, entry.Size)
88 | } else if fsEntryCpy.SHA256 != entry.SHA256 {
89 | return fmt.Errorf("path %s reported twice with diverging hash: %q != %q", relPath, fsEntryCpy.SHA256, entry.SHA256)
90 | }
91 | entry.Slices[slice] = true
92 | r.Entries[relPath] = entry
93 | } else {
94 | r.Entries[relPath] = ReportEntry{
95 | Path: relPath,
96 | Mode: fsEntry.Mode,
97 | SHA256: fsEntryCpy.SHA256,
98 | Size: fsEntryCpy.Size,
99 | Slices: map[*setup.Slice]bool{slice: true},
100 | Link: fsEntryCpy.Link,
101 | Inode: inode,
102 | }
103 | }
104 | return nil
105 | }
106 |
107 | // Mutate updates the FinalSHA256 and Size of an existing path entry.
108 | func (r *Report) Mutate(fsEntry *fsutil.Entry) error {
109 | relPath, err := r.sanitizeAbsPath(fsEntry.Path, fsEntry.Mode.IsDir())
110 | if err != nil {
111 | return fmt.Errorf("cannot mutate path in report: %s", err)
112 | }
113 |
114 | entry, ok := r.Entries[relPath]
115 | if !ok {
116 | return fmt.Errorf("cannot mutate path in report: %s not previously added", relPath)
117 | }
118 | if entry.Mode.IsDir() {
119 | return fmt.Errorf("cannot mutate path in report: %s is a directory", relPath)
120 | }
121 | if entry.SHA256 == fsEntry.SHA256 {
122 | // Content has not changed, nothing to do.
123 | return nil
124 | }
125 | entry.FinalSHA256 = fsEntry.SHA256
126 | entry.Size = fsEntry.Size
127 | r.Entries[relPath] = entry
128 | return nil
129 | }
130 |
131 | func (r *Report) sanitizeAbsPath(path string, isDir bool) (relPath string, err error) {
132 | if !strings.HasPrefix(path, r.Root) {
133 | return "", fmt.Errorf("%s outside of root %s", path, r.Root)
134 | }
135 | relPath = filepath.Clean("/" + strings.TrimPrefix(path, r.Root))
136 | if isDir {
137 | relPath = relPath + "/"
138 | }
139 | return relPath, nil
140 | }
141 |
--------------------------------------------------------------------------------
/internal/manifestutil/suite_test.go:
--------------------------------------------------------------------------------
1 | package manifestutil_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "gopkg.in/check.v1"
7 |
8 | "github.com/canonical/chisel/internal/manifestutil"
9 | )
10 |
11 | func Test(t *testing.T) { TestingT(t) }
12 |
13 | type S struct{}
14 |
15 | var _ = Suite(&S{})
16 |
17 | func (s *S) SetUpTest(c *C) {
18 | manifestutil.SetDebug(true)
19 | manifestutil.SetLogger(c)
20 | }
21 |
22 | func (s *S) TearDownTest(c *C) {
23 | manifestutil.SetDebug(false)
24 | manifestutil.SetLogger(nil)
25 | }
26 |
--------------------------------------------------------------------------------
/internal/pgputil/log.go:
--------------------------------------------------------------------------------
1 | package pgputil
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | )
7 |
8 | // Avoid importing the log type information unnecessarily. There's a small cost
9 | // associated with using an interface rather than the type. Depending on how
10 | // often the logger is plugged in, it would be worth using the type instead.
11 | type log_Logger interface {
12 | Output(calldepth int, s string) error
13 | }
14 |
15 | var globalLoggerLock sync.Mutex
16 | var globalLogger log_Logger
17 | var globalDebug bool
18 |
19 | // Specify the *log.Logger object where log messages should be sent to.
20 | func SetLogger(logger log_Logger) {
21 | globalLoggerLock.Lock()
22 | globalLogger = logger
23 | globalLoggerLock.Unlock()
24 | }
25 |
26 | // Enable the delivery of debug messages to the logger. Only meaningful
27 | // if a logger is also set.
28 | func SetDebug(debug bool) {
29 | globalLoggerLock.Lock()
30 | globalDebug = debug
31 | globalLoggerLock.Unlock()
32 | }
33 |
34 | // logf sends to the logger registered via SetLogger the string resulting
35 | // from running format and args through Sprintf.
36 | func logf(format string, args ...interface{}) {
37 | globalLoggerLock.Lock()
38 | defer globalLoggerLock.Unlock()
39 | if globalLogger != nil {
40 | globalLogger.Output(2, fmt.Sprintf(format, args...))
41 | }
42 | }
43 |
44 | // debugf sends to the logger registered via SetLogger the string resulting
45 | // from running format and args through Sprintf, but only if debugging was
46 | // enabled via SetDebug.
47 | func debugf(format string, args ...interface{}) {
48 | globalLoggerLock.Lock()
49 | defer globalLoggerLock.Unlock()
50 | if globalDebug && globalLogger != nil {
51 | globalLogger.Output(2, fmt.Sprintf(format, args...))
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/internal/pgputil/openpgp.go:
--------------------------------------------------------------------------------
1 | package pgputil
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 |
8 | "golang.org/x/crypto/openpgp/armor"
9 | "golang.org/x/crypto/openpgp/clearsign"
10 | "golang.org/x/crypto/openpgp/packet"
11 | )
12 |
13 | // DecodeKeys decodes public and private key packets from armored data.
14 | func DecodeKeys(armoredData []byte) (pubKeys []*packet.PublicKey, privKeys []*packet.PrivateKey, err error) {
15 | block, err := armor.Decode(bytes.NewReader(armoredData))
16 | if err != nil {
17 | return nil, nil, fmt.Errorf("cannot decode armored data")
18 | }
19 |
20 | reader := packet.NewReader(block.Body)
21 | for {
22 | p, err := reader.Next()
23 | if err != nil {
24 | if err == io.EOF {
25 | break
26 | }
27 | return nil, nil, err
28 | }
29 | if privKey, ok := p.(*packet.PrivateKey); ok {
30 | privKeys = append(privKeys, privKey)
31 | }
32 | if pubKey, ok := p.(*packet.PublicKey); ok {
33 | pubKeys = append(pubKeys, pubKey)
34 | }
35 | }
36 | return pubKeys, privKeys, nil
37 | }
38 |
39 | // DecodePubKey decodes a single public key packet from armored data. The
40 | // data should contain exactly one public key packet and no private key packets.
41 | func DecodePubKey(armoredData []byte) (*packet.PublicKey, error) {
42 | pubKeys, privKeys, err := DecodeKeys(armoredData)
43 | if err != nil {
44 | return nil, err
45 | }
46 | if len(privKeys) > 0 {
47 | return nil, fmt.Errorf("armored data contains private key")
48 | }
49 | if len(pubKeys) > 1 {
50 | return nil, fmt.Errorf("armored data contains more than one public key")
51 | }
52 | if len(pubKeys) == 0 {
53 | return nil, fmt.Errorf("armored data contains no public key")
54 | }
55 | return pubKeys[0], nil
56 | }
57 |
58 | // DecodeClearSigned decodes the first clearsigned message in the data and
59 | // returns the signatures and the message body.
60 | //
61 | // The returned canonicalBody is canonicalized by converting line endings to
62 | // per the openPGP RCF: https://www.rfc-editor.org/rfc/rfc4880#section-5.2.4
63 | func DecodeClearSigned(clearData []byte) (sigs []*packet.Signature, canonicalBody []byte, err error) {
64 | block, _ := clearsign.Decode(clearData)
65 | if block == nil {
66 | return nil, nil, fmt.Errorf("cannot decode clearsign text")
67 | }
68 | reader := packet.NewReader(block.ArmoredSignature.Body)
69 | for {
70 | p, err := reader.Next()
71 | if err != nil {
72 | if err == io.EOF {
73 | break
74 | }
75 | return nil, nil, fmt.Errorf("cannot parse armored data: %w", err)
76 | }
77 | if sig, ok := p.(*packet.Signature); ok {
78 | sigs = append(sigs, sig)
79 | }
80 | }
81 | if len(sigs) == 0 {
82 | return nil, nil, fmt.Errorf("clearsigned data contains no signatures")
83 | }
84 | return sigs, block.Bytes, nil
85 | }
86 |
87 | // VerifySignature returns nil if sig is a valid signature from pubKey.
88 | func VerifySignature(pubKey *packet.PublicKey, sig *packet.Signature, body []byte) error {
89 | hash := sig.Hash.New()
90 | _, err := io.Copy(hash, bytes.NewBuffer(body))
91 | if err != nil {
92 | return err
93 | }
94 | return pubKey.VerifySignature(hash, sig)
95 | }
96 |
97 | // VerifyAnySignature returns nil if any signature in sigs is a valid signature
98 | // mady by any of the public keys in pubKeys.
99 | func VerifyAnySignature(pubKeys []*packet.PublicKey, sigs []*packet.Signature, body []byte) error {
100 | var err error
101 | for _, sig := range sigs {
102 | for _, key := range pubKeys {
103 | err = VerifySignature(key, sig, body)
104 | if err == nil {
105 | return nil
106 | }
107 | }
108 | }
109 | if len(sigs) == 1 && len(pubKeys) == 1 {
110 | return err
111 | }
112 | return fmt.Errorf("cannot verify any signatures")
113 | }
114 |
--------------------------------------------------------------------------------
/internal/pgputil/suite_test.go:
--------------------------------------------------------------------------------
1 | package pgputil_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "gopkg.in/check.v1"
7 |
8 | "github.com/canonical/chisel/internal/pgputil"
9 | )
10 |
11 | func Test(t *testing.T) { TestingT(t) }
12 |
13 | type S struct{}
14 |
15 | var _ = Suite(&S{})
16 |
17 | func (s *S) SetUpTest(c *C) {
18 | pgputil.SetDebug(true)
19 | pgputil.SetLogger(c)
20 | }
21 |
22 | func (s *S) TearDownTest(c *C) {
23 | pgputil.SetDebug(false)
24 | pgputil.SetLogger(nil)
25 | }
26 |
--------------------------------------------------------------------------------
/internal/scripts/log.go:
--------------------------------------------------------------------------------
1 | package scripts
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | )
7 |
8 | // Avoid importing the log type information unnecessarily. There's a small cost
9 | // associated with using an interface rather than the type. Depending on how
10 | // often the logger is plugged in, it would be worth using the type instead.
11 | type log_Logger interface {
12 | Output(calldepth int, s string) error
13 | }
14 |
15 | var globalLoggerLock sync.Mutex
16 | var globalLogger log_Logger
17 | var globalDebug bool
18 |
19 | // Specify the *log.Logger object where log messages should be sent to.
20 | func SetLogger(logger log_Logger) {
21 | globalLoggerLock.Lock()
22 | globalLogger = logger
23 | globalLoggerLock.Unlock()
24 | }
25 |
26 | // Enable the delivery of debug messages to the logger. Only meaningful
27 | // if a logger is also set.
28 | func SetDebug(debug bool) {
29 | globalLoggerLock.Lock()
30 | globalDebug = debug
31 | globalLoggerLock.Unlock()
32 | }
33 |
34 | // logf sends to the logger registered via SetLogger the string resulting
35 | // from running format and args through Sprintf.
36 | func logf(format string, args ...interface{}) {
37 | globalLoggerLock.Lock()
38 | defer globalLoggerLock.Unlock()
39 | if globalLogger != nil {
40 | globalLogger.Output(2, fmt.Sprintf(format, args...))
41 | }
42 | }
43 |
44 | // debugf sends to the logger registered via SetLogger the string resulting
45 | // from running format and args through Sprintf, but only if debugging was
46 | // enabled via SetDebug.
47 | func debugf(format string, args ...interface{}) {
48 | globalLoggerLock.Lock()
49 | defer globalLoggerLock.Unlock()
50 | if globalDebug && globalLogger != nil {
51 | globalLogger.Output(2, fmt.Sprintf(format, args...))
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/internal/scripts/suite_test.go:
--------------------------------------------------------------------------------
1 | package scripts_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "gopkg.in/check.v1"
7 |
8 | "github.com/canonical/chisel/internal/scripts"
9 | )
10 |
11 | func Test(t *testing.T) { TestingT(t) }
12 |
13 | type S struct{}
14 |
15 | var _ = Suite(&S{})
16 |
17 | func (s *S) SetUpTest(c *C) {
18 | scripts.SetDebug(true)
19 | scripts.SetLogger(c)
20 | }
21 |
22 | func (s *S) TearDownTest(c *C) {
23 | scripts.SetDebug(false)
24 | scripts.SetLogger(nil)
25 | }
26 |
--------------------------------------------------------------------------------
/internal/setup/export_test.go:
--------------------------------------------------------------------------------
1 | package setup
2 |
3 | type YAMLPath = yamlPath
4 |
--------------------------------------------------------------------------------
/internal/setup/fetch.go:
--------------------------------------------------------------------------------
1 | package setup
2 |
3 | import (
4 | "archive/tar"
5 | "compress/gzip"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | "os"
10 | "path/filepath"
11 | "strings"
12 | "time"
13 |
14 | "github.com/juju/fslock"
15 |
16 | "github.com/canonical/chisel/internal/cache"
17 | "github.com/canonical/chisel/internal/fsutil"
18 | )
19 |
20 | type FetchOptions struct {
21 | Label string
22 | Version string
23 | CacheDir string
24 | }
25 |
26 | var bulkClient = &http.Client{
27 | Timeout: 5 * time.Minute,
28 | }
29 |
30 | const baseURL = "https://codeload.github.com/canonical/chisel-releases/tar.gz/refs/heads/"
31 |
32 | func FetchRelease(options *FetchOptions) (*Release, error) {
33 | logf("Consulting release repository...")
34 |
35 | cacheDir := options.CacheDir
36 | if cacheDir == "" {
37 | cacheDir = cache.DefaultDir("chisel")
38 | }
39 |
40 | dirName := filepath.Join(cacheDir, "releases", options.Label+"-"+options.Version)
41 | err := os.MkdirAll(dirName, 0755)
42 | if err == nil {
43 | lockFile := fslock.New(filepath.Join(cacheDir, "releases", ".lock"))
44 | err = lockFile.LockWithTimeout(10 * time.Second)
45 | if err == nil {
46 | defer lockFile.Unlock()
47 | }
48 | }
49 | if err != nil {
50 | return nil, fmt.Errorf("cannot create cache directory: %w", err)
51 | }
52 |
53 | tagName := filepath.Join(dirName, ".etag")
54 | tagData, err := os.ReadFile(tagName)
55 | if err != nil && !os.IsNotExist(err) {
56 | return nil, err
57 | }
58 |
59 | req, err := http.NewRequest("GET", baseURL+options.Label+"-"+options.Version, nil)
60 | if err != nil {
61 | return nil, fmt.Errorf("cannot create request for release information: %w", err)
62 | }
63 | req.Header.Add("If-None-Match", string(tagData))
64 |
65 | resp, err := bulkClient.Do(req)
66 | if err != nil {
67 | return nil, fmt.Errorf("cannot talk to release repository: %w", err)
68 | }
69 | defer resp.Body.Close()
70 |
71 | cacheIsValid := false
72 | switch resp.StatusCode {
73 | case 200:
74 | // ok
75 | case 304:
76 | cacheIsValid = true
77 | case 401, 404:
78 | return nil, fmt.Errorf("no information for %s-%s release", options.Label, options.Version)
79 | default:
80 | return nil, fmt.Errorf("error from release repository: %v", resp.Status)
81 | }
82 |
83 | if cacheIsValid {
84 | logf("Cached %s-%s release is still up-to-date.", options.Label, options.Version)
85 | } else {
86 | logf("Fetching current %s-%s release...", options.Label, options.Version)
87 | if !strings.Contains(dirName, "/releases/") {
88 | // Better safe than sorry.
89 | return nil, fmt.Errorf("internal error: will not remove something unexpected: %s", dirName)
90 | }
91 | err = os.RemoveAll(dirName)
92 | if err != nil {
93 | return nil, fmt.Errorf("cannot remove previously cached release: %w", err)
94 | }
95 | err = extractTarGz(resp.Body, dirName)
96 | if err != nil {
97 | return nil, err
98 | }
99 | tag := resp.Header.Get("ETag")
100 | if tag != "" {
101 | err := os.WriteFile(tagName, []byte(tag), 0644)
102 | if err != nil {
103 | return nil, fmt.Errorf("cannot write remote release tag file: %v", err)
104 | }
105 | }
106 | }
107 |
108 | return ReadRelease(dirName)
109 | }
110 |
111 | func extractTarGz(dataReader io.Reader, targetDir string) error {
112 | gzipReader, err := gzip.NewReader(dataReader)
113 | if err != nil {
114 | return err
115 | }
116 | defer gzipReader.Close()
117 | return extractTar(gzipReader, targetDir)
118 | }
119 |
120 | func extractTar(dataReader io.Reader, targetDir string) error {
121 | tarReader := tar.NewReader(dataReader)
122 | for {
123 | tarHeader, err := tarReader.Next()
124 | if err == io.EOF {
125 | break
126 | }
127 | if err != nil {
128 | return err
129 | }
130 |
131 | sourcePath := filepath.Clean(tarHeader.Name)
132 | if pos := strings.IndexByte(sourcePath, '/'); pos <= 0 || pos == len(sourcePath)-1 || sourcePath[0] == '.' {
133 | continue
134 | } else {
135 | sourcePath = sourcePath[pos+1:]
136 | }
137 |
138 | //debugf("Extracting header: %#v", tarHeader)
139 |
140 | _, err = fsutil.Create(&fsutil.CreateOptions{
141 | Root: targetDir,
142 | Path: sourcePath,
143 | Mode: tarHeader.FileInfo().Mode(),
144 | Data: tarReader,
145 | Link: tarHeader.Linkname,
146 | MakeParents: true,
147 | })
148 | if err != nil {
149 | return err
150 | }
151 | }
152 | return nil
153 | }
154 |
--------------------------------------------------------------------------------
/internal/setup/fetch_test.go:
--------------------------------------------------------------------------------
1 | package setup_test
2 |
3 | import (
4 | . "gopkg.in/check.v1"
5 |
6 | "os"
7 | "path/filepath"
8 |
9 | "github.com/canonical/chisel/internal/setup"
10 | )
11 |
12 | // TODO Implement local test server instead of using live repository.
13 |
14 | func (s *S) TestFetch(c *C) {
15 | options := &setup.FetchOptions{
16 | Label: "ubuntu",
17 | Version: "22.04",
18 | CacheDir: c.MkDir(),
19 | }
20 |
21 | for fetch := 0; fetch < 3; fetch++ {
22 | release, err := setup.FetchRelease(options)
23 | c.Assert(err, IsNil)
24 |
25 | c.Assert(release.Path, Equals, filepath.Join(options.CacheDir, "releases", "ubuntu-22.04"))
26 |
27 | archive := release.Archives["ubuntu"]
28 | c.Assert(archive.Name, Equals, "ubuntu")
29 | c.Assert(archive.Version, Equals, "22.04")
30 |
31 | // Fetch multiple times and use a marker file inside
32 | // the release directory to check if caching is both
33 | // preserving and cleaning it when appropriate.
34 | markerPath := filepath.Join(release.Path, "test.marker")
35 | switch fetch {
36 | case 0:
37 | err := os.WriteFile(markerPath, nil, 0644)
38 | c.Assert(err, IsNil)
39 | case 1:
40 | _, err := os.ReadFile(markerPath)
41 | c.Assert(err, IsNil)
42 |
43 | err = os.WriteFile(filepath.Join(release.Path, ".etag"), []byte("wrong"), 0644)
44 | c.Assert(err, IsNil)
45 | case 2:
46 | _, err := os.ReadFile(markerPath)
47 | c.Assert(os.IsNotExist(err), Equals, true)
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/internal/setup/log.go:
--------------------------------------------------------------------------------
1 | package setup
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | )
7 |
8 | // Avoid importing the log type information unnecessarily. There's a small cost
9 | // associated with using an interface rather than the type. Depending on how
10 | // often the logger is plugged in, it would be worth using the type instead.
11 | type log_Logger interface {
12 | Output(calldepth int, s string) error
13 | }
14 |
15 | var globalLoggerLock sync.Mutex
16 | var globalLogger log_Logger
17 | var globalDebug bool
18 |
19 | // Specify the *log.Logger object where log messages should be sent to.
20 | func SetLogger(logger log_Logger) {
21 | globalLoggerLock.Lock()
22 | globalLogger = logger
23 | globalLoggerLock.Unlock()
24 | }
25 |
26 | // Enable the delivery of debug messages to the logger. Only meaningful
27 | // if a logger is also set.
28 | func SetDebug(debug bool) {
29 | globalLoggerLock.Lock()
30 | globalDebug = debug
31 | globalLoggerLock.Unlock()
32 | }
33 |
34 | // logf sends to the logger registered via SetLogger the string resulting
35 | // from running format and args through Sprintf.
36 | func logf(format string, args ...interface{}) {
37 | globalLoggerLock.Lock()
38 | defer globalLoggerLock.Unlock()
39 | if globalLogger != nil {
40 | globalLogger.Output(2, fmt.Sprintf(format, args...))
41 | }
42 | }
43 |
44 | // debugf sends to the logger registered via SetLogger the string resulting
45 | // from running format and args through Sprintf, but only if debugging was
46 | // enabled via SetDebug.
47 | func debugf(format string, args ...interface{}) {
48 | globalLoggerLock.Lock()
49 | defer globalLoggerLock.Unlock()
50 | if globalDebug && globalLogger != nil {
51 | globalLogger.Output(2, fmt.Sprintf(format, args...))
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/internal/setup/suite_test.go:
--------------------------------------------------------------------------------
1 | package setup_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "gopkg.in/check.v1"
7 |
8 | "github.com/canonical/chisel/internal/setup"
9 | )
10 |
11 | func Test(t *testing.T) { TestingT(t) }
12 |
13 | type S struct{}
14 |
15 | var _ = Suite(&S{})
16 |
17 | func (s *S) SetUpTest(c *C) {
18 | setup.SetDebug(true)
19 | setup.SetLogger(c)
20 | }
21 |
22 | func (s *S) TearDownTest(c *C) {
23 | setup.SetDebug(false)
24 | setup.SetLogger(nil)
25 | }
26 |
--------------------------------------------------------------------------------
/internal/setup/tarjan.go:
--------------------------------------------------------------------------------
1 | // This file was copied from mgo, MongoDB driver for Go.
2 | //
3 | // Copyright (c) 2010-2013 - Gustavo Niemeyer
4 | //
5 | // All rights reserved.
6 | //
7 | // Redistribution and use in source and binary forms, with or without
8 | // modification, are permitted provided that the following conditions are met:
9 | //
10 | // 1. Redistributions of source code must retain the above copyright notice, this
11 | // list of conditions and the following disclaimer.
12 | // 2. Redistributions in binary form must reproduce the above copyright notice,
13 | // this list of conditions and the following disclaimer in the documentation
14 | // and/or other materials provided with the distribution.
15 | //
16 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
20 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 | // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 |
27 | package setup
28 |
29 | import (
30 | "sort"
31 | )
32 |
33 | func tarjanSort(successors map[string][]string) [][]string {
34 | // http://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
35 | data := &tarjanData{
36 | successors: successors,
37 | nodes: make([]tarjanNode, 0, len(successors)),
38 | index: make(map[string]int, len(successors)),
39 | }
40 |
41 | // Stabilize iteration through successors map to prevent
42 | // disjointed components producing unstable output due to
43 | // golang map randomized iteration.
44 | stableIDs := make([]string, 0, len(successors))
45 | for id := range successors {
46 | stableIDs = append(stableIDs, id)
47 | }
48 | sort.Strings(stableIDs)
49 | for _, id := range stableIDs {
50 | if _, seen := data.index[id]; !seen {
51 | data.strongConnect(id)
52 | }
53 | }
54 |
55 | // Sort connected components to stabilize the algorithm.
56 | for _, ids := range data.output {
57 | if len(ids) > 1 {
58 | sort.Sort(idList(ids))
59 | }
60 | }
61 | return data.output
62 | }
63 |
64 | type tarjanData struct {
65 | successors map[string][]string
66 | output [][]string
67 |
68 | nodes []tarjanNode
69 | stack []string
70 | index map[string]int
71 | }
72 |
73 | type tarjanNode struct {
74 | lowlink int
75 | stacked bool
76 | }
77 |
78 | type idList []string
79 |
80 | func (l idList) Len() int { return len(l) }
81 | func (l idList) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
82 | func (l idList) Less(i, j int) bool { return l[i] < l[j] }
83 |
84 | func (data *tarjanData) strongConnect(id string) *tarjanNode {
85 | index := len(data.nodes)
86 | data.index[id] = index
87 | data.stack = append(data.stack, id)
88 | data.nodes = append(data.nodes, tarjanNode{index, true})
89 | node := &data.nodes[index]
90 |
91 | for _, succid := range data.successors[id] {
92 | succindex, seen := data.index[succid]
93 | if !seen {
94 | succnode := data.strongConnect(succid)
95 | if succnode.lowlink < node.lowlink {
96 | node.lowlink = succnode.lowlink
97 | }
98 | } else if data.nodes[succindex].stacked {
99 | // Part of the current strongly-connected component.
100 | if succindex < node.lowlink {
101 | node.lowlink = succindex
102 | }
103 | }
104 | }
105 |
106 | if node.lowlink == index {
107 | // Root node; pop stack and output new
108 | // strongly-connected component.
109 | var scc []string
110 | i := len(data.stack) - 1
111 | for {
112 | stackid := data.stack[i]
113 | stackindex := data.index[stackid]
114 | data.nodes[stackindex].stacked = false
115 | scc = append(scc, stackid)
116 | if stackindex == index {
117 | break
118 | }
119 | i--
120 | }
121 | data.stack = data.stack[:i]
122 | data.output = append(data.output, scc)
123 | }
124 |
125 | return node
126 | }
127 |
--------------------------------------------------------------------------------
/internal/setup/tarjan_test.go:
--------------------------------------------------------------------------------
1 | // This file was copied from mgo, MongoDB driver for Go.
2 | //
3 | // Copyright (c) 2010-2013 - Gustavo Niemeyer
4 | //
5 | // All rights reserved.
6 | //
7 | // Redistribution and use in source and binary forms, with or without
8 | // modification, are permitted provided that the following conditions are met:
9 | //
10 | // 1. Redistributions of source code must retain the above copyright notice, this
11 | // list of conditions and the following disclaimer.
12 | // 2. Redistributions in binary form must reproduce the above copyright notice,
13 | // this list of conditions and the following disclaimer in the documentation
14 | // and/or other materials provided with the distribution.
15 | //
16 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
20 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 | // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 |
27 | package setup
28 |
29 | import (
30 | . "gopkg.in/check.v1"
31 | )
32 |
33 | type TarjanSuite struct{}
34 |
35 | var _ = Suite(TarjanSuite{})
36 |
37 | func (TarjanSuite) TestExample(c *C) {
38 | successors := map[string][]string{
39 | "1": {"2", "3"},
40 | "2": {"1", "5"},
41 | "3": {"4"},
42 | "4": {"3", "5"},
43 | "5": {"6"},
44 | "6": {"7"},
45 | "7": {"8"},
46 | "8": {"6", "9"},
47 | "9": {},
48 | }
49 |
50 | c.Assert(tarjanSort(successors), DeepEquals, [][]string{
51 | {"9"},
52 | {"6", "7", "8"},
53 | {"5"},
54 | {"3", "4"},
55 | {"1", "2"},
56 | })
57 | }
58 |
--------------------------------------------------------------------------------
/internal/slicer/log.go:
--------------------------------------------------------------------------------
1 | package slicer
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | )
7 |
8 | // Avoid importing the log type information unnecessarily. There's a small cost
9 | // associated with using an interface rather than the type. Depending on how
10 | // often the logger is plugged in, it would be worth using the type instead.
11 | type log_Logger interface {
12 | Output(calldepth int, s string) error
13 | }
14 |
15 | var globalLoggerLock sync.Mutex
16 | var globalLogger log_Logger
17 | var globalDebug bool
18 |
19 | // Specify the *log.Logger object where log messages should be sent to.
20 | func SetLogger(logger log_Logger) {
21 | globalLoggerLock.Lock()
22 | globalLogger = logger
23 | globalLoggerLock.Unlock()
24 | }
25 |
26 | // Enable the delivery of debug messages to the logger. Only meaningful
27 | // if a logger is also set.
28 | func SetDebug(debug bool) {
29 | globalLoggerLock.Lock()
30 | globalDebug = debug
31 | globalLoggerLock.Unlock()
32 | }
33 |
34 | // logf sends to the logger registered via SetLogger the string resulting
35 | // from running format and args through Sprintf.
36 | func logf(format string, args ...interface{}) {
37 | globalLoggerLock.Lock()
38 | defer globalLoggerLock.Unlock()
39 | if globalLogger != nil {
40 | globalLogger.Output(2, fmt.Sprintf(format, args...))
41 | }
42 | }
43 |
44 | // debugf sends to the logger registered via SetLogger the string resulting
45 | // from running format and args through Sprintf, but only if debugging was
46 | // enabled via SetDebug.
47 | func debugf(format string, args ...interface{}) {
48 | globalLoggerLock.Lock()
49 | defer globalLoggerLock.Unlock()
50 | if globalDebug && globalLogger != nil {
51 | globalLogger.Output(2, fmt.Sprintf(format, args...))
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/internal/slicer/suite_test.go:
--------------------------------------------------------------------------------
1 | package slicer_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "gopkg.in/check.v1"
7 |
8 | "github.com/canonical/chisel/internal/slicer"
9 | )
10 |
11 | func Test(t *testing.T) { TestingT(t) }
12 |
13 | type S struct{}
14 |
15 | var _ = Suite(&S{})
16 |
17 | func (s *S) SetUpTest(c *C) {
18 | slicer.SetDebug(true)
19 | slicer.SetLogger(c)
20 | }
21 |
22 | func (s *S) TearDownTest(c *C) {
23 | slicer.SetDebug(false)
24 | slicer.SetLogger(nil)
25 | }
26 |
--------------------------------------------------------------------------------
/internal/strdist/export_test.go:
--------------------------------------------------------------------------------
1 | package strdist
2 |
3 | var GlobCost = globCost
4 |
--------------------------------------------------------------------------------
/internal/strdist/log.go:
--------------------------------------------------------------------------------
1 | package strdist
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | )
7 |
8 | // Avoid importing the log type information unnecessarily. There's a small cost
9 | // associated with using an interface rather than the type. Depending on how
10 | // often the logger is plugged in, it would be worth using the type instead.
11 | type log_Logger interface {
12 | Output(calldepth int, s string) error
13 | }
14 |
15 | var globalLoggerLock sync.Mutex
16 | var globalLogger log_Logger
17 | var globalDebug bool
18 |
19 | // Specify the *log.Logger object where log messages should be sent to.
20 | func SetLogger(logger log_Logger) {
21 | globalLoggerLock.Lock()
22 | globalLogger = logger
23 | globalLoggerLock.Unlock()
24 | }
25 |
26 | // Enable the delivery of debug messages to the logger. Only meaningful
27 | // if a logger is also set.
28 | func SetDebug(debug bool) {
29 | globalLoggerLock.Lock()
30 | globalDebug = debug
31 | globalLoggerLock.Unlock()
32 | }
33 |
34 | func IsDebugOn() bool {
35 | globalLoggerLock.Lock()
36 | on := globalDebug
37 | globalLoggerLock.Unlock()
38 | return on
39 | }
40 |
41 | // logf sends to the logger registered via SetLogger the string resulting
42 | // from running format and args through Sprintf.
43 | func logf(format string, args ...interface{}) {
44 | globalLoggerLock.Lock()
45 | defer globalLoggerLock.Unlock()
46 | if globalLogger != nil {
47 | globalLogger.Output(2, fmt.Sprintf(format, args...))
48 | }
49 | }
50 |
51 | // debugf sends to the logger registered via SetLogger the string resulting
52 | // from running format and args through Sprintf, but only if debugging was
53 | // enabled via SetDebug.
54 | func debugf(format string, args ...interface{}) {
55 | globalLoggerLock.Lock()
56 | defer globalLoggerLock.Unlock()
57 | if globalDebug && globalLogger != nil {
58 | globalLogger.Output(2, fmt.Sprintf(format, args...))
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/internal/strdist/strdist.go:
--------------------------------------------------------------------------------
1 | package strdist
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | type CostInt int64
9 |
10 | func (cv CostInt) String() string {
11 | if cv == Inhibit {
12 | return "-"
13 | }
14 | return fmt.Sprint(int64(cv))
15 | }
16 |
17 | const Inhibit = 1<<63 - 1
18 |
19 | type Cost struct {
20 | SwapAB CostInt
21 | DeleteA CostInt
22 | InsertB CostInt
23 | }
24 |
25 | type CostFunc func(ar, br rune) Cost
26 |
27 | func StandardCost(ar, br rune) Cost {
28 | return Cost{SwapAB: 1, DeleteA: 1, InsertB: 1}
29 | }
30 |
31 | func Distance(a, b string, f CostFunc, cut int64) int64 {
32 | if a == b {
33 | return 0
34 | }
35 | lst := make([]CostInt, len(b)+1)
36 | bl := 0
37 | for bi, br := range b {
38 | bl++
39 | cost := f(-1, br)
40 | if cost.InsertB == Inhibit || lst[bi] == Inhibit {
41 | lst[bi+1] = Inhibit
42 | } else {
43 | lst[bi+1] = lst[bi] + cost.InsertB
44 | }
45 | }
46 | lst = lst[:bl+1]
47 | // Not required, but caching means preventing the fast path
48 | // below from calling the function and locking every time.
49 | debug := IsDebugOn()
50 | if debug {
51 | debugf(">>> %v", lst)
52 | }
53 | for _, ar := range a {
54 | last := lst[0]
55 | cost := f(ar, -1)
56 | if cost.DeleteA == Inhibit || last == Inhibit {
57 | lst[0] = Inhibit
58 | } else {
59 | lst[0] = last + cost.DeleteA
60 | }
61 | stop := true
62 | i := 0
63 | for _, br := range b {
64 | i++
65 | cost := f(ar, br)
66 | min := CostInt(Inhibit)
67 | if ar == br {
68 | min = last
69 | } else if cost.SwapAB != Inhibit && last != Inhibit {
70 | min = last + cost.SwapAB
71 | }
72 | if cost.InsertB != Inhibit && lst[i-1] != Inhibit {
73 | if n := lst[i-1] + cost.InsertB; n < min {
74 | min = n
75 | }
76 | }
77 | if cost.DeleteA != Inhibit && lst[i] != Inhibit {
78 | if n := lst[i] + cost.DeleteA; n < min {
79 | min = n
80 | }
81 | }
82 | last, lst[i] = lst[i], min
83 | if min < CostInt(cut) {
84 | stop = false
85 | }
86 | }
87 | if debug {
88 | debugf("... %v", lst)
89 | }
90 | _ = stop
91 | if cut != 0 && stop {
92 | break
93 | }
94 | }
95 | return int64(lst[len(lst)-1])
96 | }
97 |
98 | // GlobPath returns true if a and b match using supported wildcards.
99 | // Note that both a and b main contain wildcards, and it's up to the
100 | // call site to constrain the string content if that's not desirable.
101 | //
102 | // Supported wildcards:
103 | //
104 | // ? - Any one character, except for /
105 | // * - Any zero or more characters, except for /
106 | // ** - Any zero or more characters, including /
107 | func GlobPath(a, b string) bool {
108 | if !wildcardPrefixMatch(a, b) {
109 | // Fast path.
110 | return false
111 | }
112 | if !wildcardSuffixMatch(a, b) {
113 | // Fast path.
114 | return false
115 | }
116 |
117 | a = strings.ReplaceAll(a, "**", "⁑")
118 | b = strings.ReplaceAll(b, "**", "⁑")
119 | return Distance(a, b, globCost, 1) == 0
120 | }
121 |
122 | func globCost(ar, br rune) Cost {
123 | if ar == '⁑' || br == '⁑' {
124 | return Cost{SwapAB: 0, DeleteA: 0, InsertB: 0}
125 | }
126 | if ar == '/' || br == '/' {
127 | return Cost{SwapAB: Inhibit, DeleteA: Inhibit, InsertB: Inhibit}
128 | }
129 | if ar == '*' || br == '*' {
130 | return Cost{SwapAB: 0, DeleteA: 0, InsertB: 0}
131 | }
132 | if ar == '?' || br == '?' {
133 | return Cost{SwapAB: 0, DeleteA: 1, InsertB: 1}
134 | }
135 | return Cost{SwapAB: 1, DeleteA: 1, InsertB: 1}
136 | }
137 |
138 | // wildcardPrefixMatch compares whether the prefixes of a and b are equal up
139 | // to the shortest one. The prefix is defined as the longest substring that
140 | // starts at index 0 and does not contain a wildcard.
141 | func wildcardPrefixMatch(a, b string) bool {
142 | ai := strings.IndexAny(a, "*?")
143 | bi := strings.IndexAny(b, "*?")
144 | if ai == -1 {
145 | ai = len(a)
146 | }
147 | if bi == -1 {
148 | bi = len(b)
149 | }
150 | mini := min(ai, bi)
151 | return a[:mini] == b[:mini]
152 | }
153 |
154 | // wildcardSuffixMatch compares whether the suffixes of a and b are equal up
155 | // to the shortest one. The suffix is defined as the longest substring that ends
156 | // at the string length and does not contain a wildcard.
157 | func wildcardSuffixMatch(a, b string) bool {
158 | ai := strings.LastIndexAny(a, "*?")
159 | la := 0
160 | if ai != -1 {
161 | la = len(a) - ai - 1
162 | }
163 | lb := 0
164 | bi := strings.LastIndexAny(b, "*?")
165 | if bi != -1 {
166 | lb = len(b) - bi - 1
167 | }
168 | minl := min(la, lb)
169 | return a[len(a)-minl:] == b[len(b)-minl:]
170 | }
171 |
--------------------------------------------------------------------------------
/internal/strdist/strdist_test.go:
--------------------------------------------------------------------------------
1 | package strdist_test
2 |
3 | import (
4 | . "gopkg.in/check.v1"
5 |
6 | "strings"
7 | "testing"
8 |
9 | "github.com/canonical/chisel/internal/strdist"
10 | )
11 |
12 | type distanceTest struct {
13 | a, b string
14 | f strdist.CostFunc
15 | r int64
16 | cut int64
17 | }
18 |
19 | func uniqueCost(ar, br rune) strdist.Cost {
20 | return strdist.Cost{SwapAB: 1, DeleteA: 3, InsertB: 5}
21 | }
22 |
23 | var distanceTests = []distanceTest{
24 | {f: uniqueCost, r: 0, a: "abc", b: "abc"},
25 | {f: uniqueCost, r: 1, a: "abc", b: "abd"},
26 | {f: uniqueCost, r: 1, a: "abc", b: "adc"},
27 | {f: uniqueCost, r: 1, a: "abc", b: "dbc"},
28 | {f: uniqueCost, r: 2, a: "abc", b: "add"},
29 | {f: uniqueCost, r: 2, a: "abc", b: "ddc"},
30 | {f: uniqueCost, r: 2, a: "abc", b: "dbd"},
31 | {f: uniqueCost, r: 3, a: "abc", b: "ddd"},
32 | {f: uniqueCost, r: 3, a: "abc", b: "ab"},
33 | {f: uniqueCost, r: 3, a: "abc", b: "bc"},
34 | {f: uniqueCost, r: 3, a: "abc", b: "ac"},
35 | {f: uniqueCost, r: 6, a: "abc", b: "a"},
36 | {f: uniqueCost, r: 6, a: "abc", b: "b"},
37 | {f: uniqueCost, r: 6, a: "abc", b: "c"},
38 | {f: uniqueCost, r: 9, a: "abc", b: ""},
39 | {f: uniqueCost, r: 5, a: "abc", b: "abcd"},
40 | {f: uniqueCost, r: 5, a: "abc", b: "dabc"},
41 | {f: uniqueCost, r: 10, a: "abc", b: "adbdc"},
42 | {f: uniqueCost, r: 10, a: "abc", b: "dabcd"},
43 | {f: uniqueCost, r: 40, a: "abc", b: "ddaddbddcdd"},
44 | {f: strdist.StandardCost, r: 3, a: "abcdefg", b: "axcdfgh"},
45 | {f: strdist.StandardCost, r: 2, cut: 2, a: "abcdef", b: "abc"},
46 | {f: strdist.StandardCost, r: 2, cut: 3, a: "abcdef", b: "abcd"},
47 | {f: strdist.GlobCost, r: 0, a: "abc*", b: "abcdef"},
48 | {f: strdist.GlobCost, r: 0, a: "ab*ef", b: "abcdef"},
49 | {f: strdist.GlobCost, r: 0, a: "*def", b: "abcdef"},
50 | {f: strdist.GlobCost, r: 0, a: "a*/def", b: "abc/def"},
51 | {f: strdist.GlobCost, r: 1, a: "a*/def", b: "abc/gef"},
52 | {f: strdist.GlobCost, r: 0, a: "a*/*f", b: "abc/def"},
53 | {f: strdist.GlobCost, r: 1, a: "a*/*f", b: "abc/defh"},
54 | {f: strdist.GlobCost, r: 1, a: "a*/*f", b: "abc/defhi"},
55 | {f: strdist.GlobCost, r: strdist.Inhibit, a: "a*", b: "abc/def"},
56 | {f: strdist.GlobCost, r: strdist.Inhibit, a: "a*/*f", b: "abc/def/hij"},
57 | {f: strdist.GlobCost, r: 0, a: "a**f/hij", b: "abc/def/hij"},
58 | {f: strdist.GlobCost, r: 1, a: "a**f/hij", b: "abc/def/hik"},
59 | {f: strdist.GlobCost, r: 2, a: "a**fg", b: "abc/def/hik"},
60 | {f: strdist.GlobCost, r: 0, a: "a**f/hij/klm", b: "abc/d**m"},
61 | }
62 |
63 | func (s *S) TestDistance(c *C) {
64 | for _, test := range distanceTests {
65 | c.Logf("Test: %v", test)
66 | if strings.Contains(test.a, "*") || strings.Contains(test.b, "*") {
67 | c.Assert(strdist.GlobPath(test.a, test.b), Equals, test.r == 0)
68 | }
69 | f := test.f
70 | if f == nil {
71 | f = strdist.StandardCost
72 | }
73 | test.a = strings.ReplaceAll(test.a, "**", "⁑")
74 | test.b = strings.ReplaceAll(test.b, "**", "⁑")
75 | r := strdist.Distance(test.a, test.b, f, test.cut)
76 | c.Assert(r, Equals, test.r)
77 | }
78 | }
79 |
80 | func BenchmarkDistance(b *testing.B) {
81 | const one = "abdefghijklmnopqrstuvwxyz"
82 | const two = "a.d.f.h.j.l.n.p.r.t.v.x.z"
83 | for i := 0; i < b.N; i++ {
84 | strdist.Distance(one, two, strdist.StandardCost, 0)
85 | }
86 | }
87 |
88 | func BenchmarkDistanceCut(b *testing.B) {
89 | const one = "abdefghijklmnopqrstuvwxyz"
90 | const two = "a.d.f.h.j.l.n.p.r.t.v.x.z"
91 | for i := 0; i < b.N; i++ {
92 | strdist.Distance(one, two, strdist.StandardCost, 1)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/internal/strdist/suite_test.go:
--------------------------------------------------------------------------------
1 | package strdist_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "gopkg.in/check.v1"
7 |
8 | "github.com/canonical/chisel/internal/strdist"
9 | )
10 |
11 | func Test(t *testing.T) { TestingT(t) }
12 |
13 | type S struct{}
14 |
15 | var _ = Suite(&S{})
16 |
17 | func (s *S) SetUpTest(c *C) {
18 | strdist.SetDebug(true)
19 | strdist.SetLogger(c)
20 | }
21 |
22 | func (s *S) TearDownTest(c *C) {
23 | strdist.SetDebug(false)
24 | strdist.SetLogger(nil)
25 | }
26 |
--------------------------------------------------------------------------------
/internal/testutil/archive.go:
--------------------------------------------------------------------------------
1 | package testutil
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 |
8 | "github.com/canonical/chisel/internal/archive"
9 | )
10 |
11 | type TestArchive struct {
12 | Opts archive.Options
13 | Packages map[string]*TestPackage
14 | }
15 |
16 | type TestPackage struct {
17 | Name string
18 | Version string
19 | Hash string
20 | Arch string
21 | Data []byte
22 | Archives []string
23 | }
24 |
25 | func (a *TestArchive) Options() *archive.Options {
26 | return &a.Opts
27 | }
28 |
29 | func (a *TestArchive) Fetch(pkgName string) (io.ReadSeekCloser, *archive.PackageInfo, error) {
30 | pkg, ok := a.Packages[pkgName]
31 | if !ok {
32 | return nil, nil, fmt.Errorf("cannot find package %q in archive", pkgName)
33 | }
34 | info := &archive.PackageInfo{
35 | Name: pkg.Name,
36 | Version: pkg.Version,
37 | SHA256: pkg.Hash,
38 | Arch: pkg.Arch,
39 | }
40 | return ReadSeekNopCloser(bytes.NewReader(pkg.Data)), info, nil
41 | }
42 |
43 | func (a *TestArchive) Exists(pkg string) bool {
44 | _, ok := a.Packages[pkg]
45 | return ok
46 | }
47 |
48 | func (a *TestArchive) Info(pkgName string) (*archive.PackageInfo, error) {
49 | pkg, ok := a.Packages[pkgName]
50 | if !ok {
51 | return nil, fmt.Errorf("cannot find package %q in archive", pkgName)
52 | }
53 | return &archive.PackageInfo{
54 | Name: pkg.Name,
55 | Version: pkg.Version,
56 | SHA256: pkg.Hash,
57 | Arch: pkg.Arch,
58 | }, nil
59 | }
60 |
--------------------------------------------------------------------------------
/internal/testutil/base.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2020 Canonical Ltd
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License version 3 as
5 | // published by the Free Software Foundation.
6 | //
7 | // This program is distributed in the hope that it will be useful,
8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 | // GNU General Public License for more details.
11 | //
12 | // You should have received a copy of the GNU General Public License
13 | // along with this program. If not, see .
14 |
15 | package testutil
16 |
17 | import (
18 | "gopkg.in/check.v1"
19 | )
20 |
21 | // BaseTest is a structure used as a base test suite for many of the pebble
22 | // tests.
23 | type BaseTest struct {
24 | cleanupHandlers []func()
25 | }
26 |
27 | // SetUpTest prepares the cleanup
28 | func (s *BaseTest) SetUpTest(c *check.C) {
29 | s.cleanupHandlers = nil
30 | }
31 |
32 | // TearDownTest cleans up the channel.ini files in case they were changed by
33 | // the test.
34 | // It also runs the cleanup handlers
35 | func (s *BaseTest) TearDownTest(c *check.C) {
36 | // run cleanup handlers and clear the slice
37 | for _, f := range s.cleanupHandlers {
38 | f()
39 | }
40 | s.cleanupHandlers = nil
41 | }
42 |
43 | // AddCleanup adds a new cleanup function to the test
44 | func (s *BaseTest) AddCleanup(f func()) {
45 | s.cleanupHandlers = append(s.cleanupHandlers, f)
46 | }
47 |
--------------------------------------------------------------------------------
/internal/testutil/containschecker.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2020 Canonical Ltd
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License version 3 as
5 | // published by the Free Software Foundation.
6 | //
7 | // This program is distributed in the hope that it will be useful,
8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 | // GNU General Public License for more details.
11 | //
12 | // You should have received a copy of the GNU General Public License
13 | // along with this program. If not, see .
14 |
15 | package testutil
16 |
17 | import (
18 | "fmt"
19 | "reflect"
20 | "strings"
21 |
22 | "gopkg.in/check.v1"
23 | )
24 |
25 | type containsChecker struct {
26 | *check.CheckerInfo
27 | }
28 |
29 | // Contains is a Checker that looks for a elem in a container.
30 | // The elem can be any object. The container can be an array, slice or string.
31 | var Contains check.Checker = &containsChecker{
32 | &check.CheckerInfo{Name: "Contains", Params: []string{"container", "elem"}},
33 | }
34 |
35 | func commonEquals(container, elem interface{}, result *bool, error *string) bool {
36 | containerV := reflect.ValueOf(container)
37 | elemV := reflect.ValueOf(elem)
38 | switch containerV.Kind() {
39 | case reflect.Slice, reflect.Array, reflect.Map:
40 | containerElemType := containerV.Type().Elem()
41 | if containerElemType.Kind() == reflect.Interface {
42 | // Ensure that element implements the type of elements stored in the container.
43 | if !elemV.Type().Implements(containerElemType) {
44 | *result = false
45 | *error = fmt.Sprintf(""+
46 | "container has items of interface type %s but expected"+
47 | " element does not implement it", containerElemType)
48 | return true
49 | }
50 | } else {
51 | // Ensure that type of elements in container is compatible with elem
52 | if containerElemType != elemV.Type() {
53 | *result = false
54 | *error = fmt.Sprintf(
55 | "container has items of type %s but expected element is a %s",
56 | containerElemType, elemV.Type())
57 | return true
58 | }
59 | }
60 | case reflect.String:
61 | // When container is a string, we expect elem to be a string as well
62 | if elemV.Kind() != reflect.String {
63 | *result = false
64 | *error = fmt.Sprintf("element is a %T but expected a string", elem)
65 | } else {
66 | *result = strings.Contains(containerV.String(), elemV.String())
67 | *error = ""
68 | }
69 | return true
70 | }
71 | return false
72 | }
73 |
74 | func (c *containsChecker) Check(params []interface{}, names []string) (result bool, error string) {
75 | defer func() {
76 | if v := recover(); v != nil {
77 | result = false
78 | error = fmt.Sprint(v)
79 | }
80 | }()
81 | var container interface{} = params[0]
82 | var elem interface{} = params[1]
83 | if commonEquals(container, elem, &result, &error) {
84 | return
85 | }
86 | // Do the actual test using ==
87 | switch containerV := reflect.ValueOf(container); containerV.Kind() {
88 | case reflect.Slice, reflect.Array:
89 | for length, i := containerV.Len(), 0; i < length; i++ {
90 | itemV := containerV.Index(i)
91 | if itemV.Interface() == elem {
92 | return true, ""
93 | }
94 | }
95 | return false, ""
96 | case reflect.Map:
97 | for _, keyV := range containerV.MapKeys() {
98 | itemV := containerV.MapIndex(keyV)
99 | if itemV.Interface() == elem {
100 | return true, ""
101 | }
102 | }
103 | return false, ""
104 | default:
105 | return false, fmt.Sprintf("%T is not a supported container", container)
106 | }
107 | }
108 |
109 | type deepContainsChecker struct {
110 | *check.CheckerInfo
111 | }
112 |
113 | // DeepContains is a Checker that looks for a elem in a container using
114 | // DeepEqual. The elem can be any object. The container can be an array, slice
115 | // or string.
116 | var DeepContains check.Checker = &deepContainsChecker{
117 | &check.CheckerInfo{Name: "DeepContains", Params: []string{"container", "elem"}},
118 | }
119 |
120 | func (c *deepContainsChecker) Check(params []interface{}, names []string) (result bool, error string) {
121 | var container interface{} = params[0]
122 | var elem interface{} = params[1]
123 | if commonEquals(container, elem, &result, &error) {
124 | return
125 | }
126 | // Do the actual test using reflect.DeepEqual
127 | switch containerV := reflect.ValueOf(container); containerV.Kind() {
128 | case reflect.Slice, reflect.Array:
129 | for length, i := containerV.Len(), 0; i < length; i++ {
130 | itemV := containerV.Index(i)
131 | if reflect.DeepEqual(itemV.Interface(), elem) {
132 | return true, ""
133 | }
134 | }
135 | return false, ""
136 | case reflect.Map:
137 | for _, keyV := range containerV.MapKeys() {
138 | itemV := containerV.MapIndex(keyV)
139 | if reflect.DeepEqual(itemV.Interface(), elem) {
140 | return true, ""
141 | }
142 | }
143 | return false, ""
144 | default:
145 | return false, fmt.Sprintf("%T is not a supported container", container)
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/internal/testutil/exec_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2020 Canonical Ltd
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License version 3 as
5 | // published by the Free Software Foundation.
6 | //
7 | // This program is distributed in the hope that it will be useful,
8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 | // GNU General Public License for more details.
11 | //
12 | // You should have received a copy of the GNU General Public License
13 | // along with this program. If not, see .
14 |
15 | package testutil
16 |
17 | import (
18 | "fmt"
19 | "os"
20 | "os/exec"
21 | "path/filepath"
22 |
23 | "gopkg.in/check.v1"
24 | )
25 |
26 | type fakeCommandSuite struct{}
27 |
28 | var _ = check.Suite(&fakeCommandSuite{})
29 |
30 | func (s *fakeCommandSuite) TestFakeCommand(c *check.C) {
31 | fake := FakeCommand(c, "cmd", "true")
32 | defer fake.Restore()
33 | err := exec.Command("cmd", "first-run", "--arg1", "arg2", "a space").Run()
34 | c.Assert(err, check.IsNil)
35 | err = exec.Command("cmd", "second-run", "--arg1", "arg2", "a %s").Run()
36 | c.Assert(err, check.IsNil)
37 | c.Assert(fake.Calls(), check.DeepEquals, [][]string{
38 | {"cmd", "first-run", "--arg1", "arg2", "a space"},
39 | {"cmd", "second-run", "--arg1", "arg2", "a %s"},
40 | })
41 | }
42 |
43 | func (s *fakeCommandSuite) TestFakeCommandAlso(c *check.C) {
44 | fake := FakeCommand(c, "fst", "")
45 | also := fake.Also("snd", "")
46 | defer fake.Restore()
47 |
48 | c.Assert(exec.Command("fst").Run(), check.IsNil)
49 | c.Assert(exec.Command("snd").Run(), check.IsNil)
50 | c.Check(fake.Calls(), check.DeepEquals, [][]string{{"fst"}, {"snd"}})
51 | c.Check(fake.Calls(), check.DeepEquals, also.Calls())
52 | }
53 |
54 | func (s *fakeCommandSuite) TestFakeCommandConflictEcho(c *check.C) {
55 | fake := FakeCommand(c, "do-not-swallow-echo-args", "")
56 | defer fake.Restore()
57 |
58 | c.Assert(exec.Command("do-not-swallow-echo-args", "-E", "-n", "-e").Run(), check.IsNil)
59 | c.Assert(fake.Calls(), check.DeepEquals, [][]string{
60 | {"do-not-swallow-echo-args", "-E", "-n", "-e"},
61 | })
62 | }
63 |
64 | func (s *fakeCommandSuite) TestFakeShellchecksWhenAvailable(c *check.C) {
65 | tmpDir := c.MkDir()
66 | fakeShellcheck := FakeCommand(c, "shellcheck", fmt.Sprintf(`cat > %s/input`, tmpDir))
67 | defer fakeShellcheck.Restore()
68 |
69 | restore := FakeShellcheckPath(fakeShellcheck.Exe())
70 | defer restore()
71 |
72 | fake := FakeCommand(c, "some-command", "echo some-command")
73 |
74 | c.Assert(exec.Command("some-command").Run(), check.IsNil)
75 |
76 | c.Assert(fake.Calls(), check.DeepEquals, [][]string{
77 | {"some-command"},
78 | })
79 | c.Assert(fakeShellcheck.Calls(), check.DeepEquals, [][]string{
80 | {"shellcheck", "-s", "bash", "-"},
81 | })
82 |
83 | scriptData, err := os.ReadFile(fake.Exe())
84 | c.Assert(err, check.IsNil)
85 | c.Assert(string(scriptData), Contains, "\necho some-command\n")
86 |
87 | data, err := os.ReadFile(filepath.Join(tmpDir, "input"))
88 | c.Assert(err, check.IsNil)
89 | c.Assert(data, check.DeepEquals, scriptData)
90 | }
91 |
92 | func (s *fakeCommandSuite) TestFakeNoShellchecksWhenNotAvailable(c *check.C) {
93 | fakeShellcheck := FakeCommand(c, "shellcheck", `echo "i am not called"; exit 1`)
94 | defer fakeShellcheck.Restore()
95 |
96 | restore := FakeShellcheckPath("")
97 | defer restore()
98 |
99 | // This would fail with proper shellcheck due to SC2086: Double quote to
100 | // prevent globbing and word splitting.
101 | fake := FakeCommand(c, "some-command", "echo $1")
102 |
103 | c.Assert(exec.Command("some-command").Run(), check.IsNil)
104 |
105 | c.Assert(fake.Calls(), check.DeepEquals, [][]string{
106 | {"some-command"},
107 | })
108 | c.Assert(fakeShellcheck.Calls(), check.HasLen, 0)
109 | }
110 |
--------------------------------------------------------------------------------
/internal/testutil/export_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2020 Canonical Ltd
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License version 3 as
5 | // published by the Free Software Foundation.
6 | //
7 | // This program is distributed in the hope that it will be useful,
8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 | // GNU General Public License for more details.
11 | //
12 | // You should have received a copy of the GNU General Public License
13 | // along with this program. If not, see .
14 |
15 | package testutil
16 |
17 | import (
18 | "gopkg.in/check.v1"
19 | )
20 |
21 | func UnexpectedIntChecker(relation string) *intChecker {
22 | return &intChecker{CheckerInfo: &check.CheckerInfo{Name: "unexpected", Params: []string{"a", "b"}}, rel: relation}
23 | }
24 |
25 | func FakeShellcheckPath(p string) (restore func()) {
26 | old := shellcheckPath
27 | shellcheckPath = p
28 | return func() {
29 | shellcheckPath = old
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/internal/testutil/filecontentchecker.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2020 Canonical Ltd
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License version 3 as
5 | // published by the Free Software Foundation.
6 | //
7 | // This program is distributed in the hope that it will be useful,
8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 | // GNU General Public License for more details.
11 | //
12 | // You should have received a copy of the GNU General Public License
13 | // along with this program. If not, see .
14 |
15 | package testutil
16 |
17 | import (
18 | "bytes"
19 | "fmt"
20 | "os"
21 | "regexp"
22 | "strings"
23 |
24 | "gopkg.in/check.v1"
25 | )
26 |
27 | type fileContentChecker struct {
28 | *check.CheckerInfo
29 | exact bool
30 | }
31 |
32 | // FileEquals verifies that the given file's content is equal
33 | // to the string (or fmt.Stringer) or []byte provided.
34 | var FileEquals check.Checker = &fileContentChecker{
35 | CheckerInfo: &check.CheckerInfo{Name: "FileEquals", Params: []string{"filename", "contents"}},
36 | exact: true,
37 | }
38 |
39 | // FileContains verifies that the given file's content contains
40 | // the string (or fmt.Stringer) or []byte provided.
41 | var FileContains check.Checker = &fileContentChecker{
42 | CheckerInfo: &check.CheckerInfo{Name: "FileContains", Params: []string{"filename", "contents"}},
43 | }
44 |
45 | // FileMatches verifies that the given file's content matches
46 | // the string provided.
47 | var FileMatches check.Checker = &fileContentChecker{
48 | CheckerInfo: &check.CheckerInfo{Name: "FileMatches", Params: []string{"filename", "regex"}},
49 | }
50 |
51 | func (c *fileContentChecker) Check(params []interface{}, names []string) (result bool, error string) {
52 | filename, ok := params[0].(string)
53 | if !ok {
54 | return false, "Filename must be a string"
55 | }
56 | if names[1] == "regex" {
57 | regexpr, ok := params[1].(string)
58 | if !ok {
59 | return false, "Regex must be a string"
60 | }
61 | rx, err := regexp.Compile(regexpr)
62 | if err != nil {
63 | return false, fmt.Sprintf("Cannot compile regexp %q: %v", regexpr, err)
64 | }
65 | params[1] = rx
66 | }
67 | return fileContentCheck(filename, params[1], c.exact)
68 | }
69 |
70 | func fileContentCheck(filename string, content interface{}, exact bool) (result bool, error string) {
71 | buf, err := os.ReadFile(filename)
72 | if err != nil {
73 | return false, fmt.Sprintf("Cannot read file %q: %v", filename, err)
74 | }
75 | presentableBuf := string(buf)
76 | if exact {
77 | switch content := content.(type) {
78 | case string:
79 | result = presentableBuf == content
80 | case []byte:
81 | result = bytes.Equal(buf, content)
82 | presentableBuf = ""
83 | case fmt.Stringer:
84 | result = presentableBuf == content.String()
85 | default:
86 | error = fmt.Sprintf("Cannot compare file contents with something of type %T", content)
87 | }
88 | } else {
89 | switch content := content.(type) {
90 | case string:
91 | result = strings.Contains(presentableBuf, content)
92 | case []byte:
93 | result = bytes.Contains(buf, content)
94 | presentableBuf = ""
95 | case *regexp.Regexp:
96 | result = content.Match(buf)
97 | case fmt.Stringer:
98 | result = strings.Contains(presentableBuf, content.String())
99 | default:
100 | error = fmt.Sprintf("Cannot compare file contents with something of type %T", content)
101 | }
102 | }
103 | if !result {
104 | if error == "" {
105 | error = fmt.Sprintf("Cannot match with file contents:\n%v", presentableBuf)
106 | }
107 | return result, error
108 | }
109 | return result, ""
110 | }
111 |
--------------------------------------------------------------------------------
/internal/testutil/filecontentchecker_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2020 Canonical Ltd
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License version 3 as
5 | // published by the Free Software Foundation.
6 | //
7 | // This program is distributed in the hope that it will be useful,
8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 | // GNU General Public License for more details.
11 | //
12 | // You should have received a copy of the GNU General Public License
13 | // along with this program. If not, see .
14 |
15 | package testutil_test
16 |
17 | import (
18 | "os"
19 | "path/filepath"
20 | "regexp"
21 |
22 | "gopkg.in/check.v1"
23 |
24 | . "github.com/canonical/chisel/internal/testutil"
25 | )
26 |
27 | type fileContentCheckerSuite struct{}
28 |
29 | var _ = check.Suite(&fileContentCheckerSuite{})
30 |
31 | type myStringer struct{ str string }
32 |
33 | func (m myStringer) String() string { return m.str }
34 |
35 | func (s *fileContentCheckerSuite) TestFileEquals(c *check.C) {
36 | d := c.MkDir()
37 | content := "not-so-random-string"
38 | filename := filepath.Join(d, "canary")
39 | c.Assert(os.WriteFile(filename, []byte(content), 0644), check.IsNil)
40 |
41 | testInfo(c, FileEquals, "FileEquals", []string{"filename", "contents"})
42 | testCheck(c, FileEquals, true, "", filename, content)
43 | testCheck(c, FileEquals, true, "", filename, []byte(content))
44 | testCheck(c, FileEquals, true, "", filename, myStringer{content})
45 |
46 | twofer := content + content
47 | testCheck(c, FileEquals, false, "Cannot match with file contents:\nnot-so-random-string", filename, twofer)
48 | testCheck(c, FileEquals, false, "Cannot match with file contents:\n", filename, []byte(twofer))
49 | testCheck(c, FileEquals, false, "Cannot match with file contents:\nnot-so-random-string", filename, myStringer{twofer})
50 |
51 | testCheck(c, FileEquals, false, `Cannot read file "": open : no such file or directory`, "", "")
52 | testCheck(c, FileEquals, false, "Filename must be a string", 42, "")
53 | testCheck(c, FileEquals, false, "Cannot compare file contents with something of type int", filename, 1)
54 | }
55 |
56 | func (s *fileContentCheckerSuite) TestFileContains(c *check.C) {
57 | d := c.MkDir()
58 | content := "not-so-random-string"
59 | filename := filepath.Join(d, "canary")
60 | c.Assert(os.WriteFile(filename, []byte(content), 0644), check.IsNil)
61 |
62 | testInfo(c, FileContains, "FileContains", []string{"filename", "contents"})
63 | testCheck(c, FileContains, true, "", filename, content[1:])
64 | testCheck(c, FileContains, true, "", filename, []byte(content[1:]))
65 | testCheck(c, FileContains, true, "", filename, myStringer{content[1:]})
66 | // undocumented
67 | testCheck(c, FileContains, true, "", filename, regexp.MustCompile(".*"))
68 |
69 | twofer := content + content
70 | testCheck(c, FileContains, false, "Cannot match with file contents:\nnot-so-random-string", filename, twofer)
71 | testCheck(c, FileContains, false, "Cannot match with file contents:\n", filename, []byte(twofer))
72 | testCheck(c, FileContains, false, "Cannot match with file contents:\nnot-so-random-string", filename, myStringer{twofer})
73 | // undocumented
74 | testCheck(c, FileContains, false, "Cannot match with file contents:\nnot-so-random-string", filename, regexp.MustCompile("^$"))
75 |
76 | testCheck(c, FileContains, false, `Cannot read file "": open : no such file or directory`, "", "")
77 | testCheck(c, FileContains, false, "Filename must be a string", 42, "")
78 | testCheck(c, FileContains, false, "Cannot compare file contents with something of type int", filename, 1)
79 | }
80 |
81 | func (s *fileContentCheckerSuite) TestFileMatches(c *check.C) {
82 | d := c.MkDir()
83 | content := "not-so-random-string"
84 | filename := filepath.Join(d, "canary")
85 | c.Assert(os.WriteFile(filename, []byte(content), 0644), check.IsNil)
86 |
87 | testInfo(c, FileMatches, "FileMatches", []string{"filename", "regex"})
88 | testCheck(c, FileMatches, true, "", filename, ".*")
89 | testCheck(c, FileMatches, true, "", filename, "^"+regexp.QuoteMeta(content)+"$")
90 |
91 | testCheck(c, FileMatches, false, "Cannot match with file contents:\nnot-so-random-string", filename, "^$")
92 | testCheck(c, FileMatches, false, "Cannot match with file contents:\nnot-so-random-string", filename, "123"+regexp.QuoteMeta(content))
93 |
94 | testCheck(c, FileMatches, false, `Cannot read file "": open : no such file or directory`, "", "")
95 | testCheck(c, FileMatches, false, "Filename must be a string", 42, ".*")
96 | testCheck(c, FileMatches, false, "Regex must be a string", filename, 1)
97 | }
98 |
--------------------------------------------------------------------------------
/internal/testutil/filepresencechecker.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2020 Canonical Ltd
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License version 3 as
5 | // published by the Free Software Foundation.
6 | //
7 | // This program is distributed in the hope that it will be useful,
8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 | // GNU General Public License for more details.
11 | //
12 | // You should have received a copy of the GNU General Public License
13 | // along with this program. If not, see .
14 |
15 | package testutil
16 |
17 | import (
18 | "fmt"
19 | "os"
20 |
21 | "gopkg.in/check.v1"
22 | )
23 |
24 | type filePresenceChecker struct {
25 | *check.CheckerInfo
26 | present bool
27 | }
28 |
29 | // FilePresent verifies that the given file exists.
30 | var FilePresent check.Checker = &filePresenceChecker{
31 | CheckerInfo: &check.CheckerInfo{Name: "FilePresent", Params: []string{"filename"}},
32 | present: true,
33 | }
34 |
35 | // FileAbsent verifies that the given file does not exist.
36 | var FileAbsent check.Checker = &filePresenceChecker{
37 | CheckerInfo: &check.CheckerInfo{Name: "FileAbsent", Params: []string{"filename"}},
38 | present: false,
39 | }
40 |
41 | func (c *filePresenceChecker) Check(params []interface{}, names []string) (result bool, error string) {
42 | filename, ok := params[0].(string)
43 | if !ok {
44 | return false, "filename must be a string"
45 | }
46 | _, err := os.Stat(filename)
47 | if os.IsNotExist(err) && c.present {
48 | return false, fmt.Sprintf("file %q is absent but should exist", filename)
49 | }
50 | if err == nil && !c.present {
51 | return false, fmt.Sprintf("file %q is present but should not exist", filename)
52 | }
53 | return true, ""
54 | }
55 |
--------------------------------------------------------------------------------
/internal/testutil/filepresencechecker_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2020 Canonical Ltd
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License version 3 as
5 | // published by the Free Software Foundation.
6 | //
7 | // This program is distributed in the hope that it will be useful,
8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 | // GNU General Public License for more details.
11 | //
12 | // You should have received a copy of the GNU General Public License
13 | // along with this program. If not, see .
14 |
15 | package testutil_test
16 |
17 | import (
18 | "fmt"
19 | "os"
20 | "path/filepath"
21 |
22 | "gopkg.in/check.v1"
23 |
24 | . "github.com/canonical/chisel/internal/testutil"
25 | )
26 |
27 | type filePresenceCheckerSuite struct{}
28 |
29 | var _ = check.Suite(&filePresenceCheckerSuite{})
30 |
31 | func (*filePresenceCheckerSuite) TestFilePresent(c *check.C) {
32 | d := c.MkDir()
33 | filename := filepath.Join(d, "foo")
34 | testInfo(c, FilePresent, "FilePresent", []string{"filename"})
35 | testCheck(c, FilePresent, false, `filename must be a string`, 42)
36 | testCheck(c, FilePresent, false, fmt.Sprintf(`file %q is absent but should exist`, filename), filename)
37 | c.Assert(os.WriteFile(filename, nil, 0644), check.IsNil)
38 | testCheck(c, FilePresent, true, "", filename)
39 | }
40 |
41 | func (*filePresenceCheckerSuite) TestFileAbsent(c *check.C) {
42 | d := c.MkDir()
43 | filename := filepath.Join(d, "foo")
44 | testInfo(c, FileAbsent, "FileAbsent", []string{"filename"})
45 | testCheck(c, FileAbsent, false, `filename must be a string`, 42)
46 | testCheck(c, FileAbsent, true, "", filename)
47 | c.Assert(os.WriteFile(filename, nil, 0644), check.IsNil)
48 | testCheck(c, FileAbsent, false, fmt.Sprintf(`file %q is present but should not exist`, filename), filename)
49 | }
50 |
--------------------------------------------------------------------------------
/internal/testutil/intcheckers.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2020 Canonical Ltd
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License version 3 as
5 | // published by the Free Software Foundation.
6 | //
7 | // This program is distributed in the hope that it will be useful,
8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 | // GNU General Public License for more details.
11 | //
12 | // You should have received a copy of the GNU General Public License
13 | // along with this program. If not, see .
14 |
15 | package testutil
16 |
17 | import (
18 | "fmt"
19 |
20 | "gopkg.in/check.v1"
21 | )
22 |
23 | type intChecker struct {
24 | *check.CheckerInfo
25 | rel string
26 | }
27 |
28 | func (checker *intChecker) Check(params []interface{}, names []string) (result bool, error string) {
29 | a, ok := params[0].(int)
30 | if !ok {
31 | return false, "left-hand-side argument must be an int"
32 | }
33 | b, ok := params[1].(int)
34 | if !ok {
35 | return false, "right-hand-side argument must be an int"
36 | }
37 | switch checker.rel {
38 | case "<":
39 | result = a < b
40 | case "<=":
41 | result = a <= b
42 | case "==":
43 | result = a == b
44 | case "!=":
45 | result = a != b
46 | case ">":
47 | result = a > b
48 | case ">=":
49 | result = a >= b
50 | default:
51 | return false, fmt.Sprintf("unexpected relation %q", checker.rel)
52 | }
53 | if !result {
54 | error = fmt.Sprintf("relation %d %s %d is not true", a, checker.rel, b)
55 | }
56 | return result, error
57 | }
58 |
59 | // IntLessThan checker verifies that one integer is less than other integer.
60 | //
61 | // For example:
62 | //
63 | // c.Assert(1, IntLessThan, 2)
64 | var IntLessThan = &intChecker{CheckerInfo: &check.CheckerInfo{Name: "IntLessThan", Params: []string{"a", "b"}}, rel: "<"}
65 |
66 | // IntLessEqual checker verifies that one integer is less than or equal to other integer.
67 | //
68 | // For example:
69 | //
70 | // c.Assert(1, IntLessEqual, 1)
71 | var IntLessEqual = &intChecker{CheckerInfo: &check.CheckerInfo{Name: "IntLessEqual", Params: []string{"a", "b"}}, rel: "<="}
72 |
73 | // IntEqual checker verifies that one integer is equal to other integer.
74 | //
75 | // For example:
76 | //
77 | // c.Assert(1, IntEqual, 1)
78 | var IntEqual = &intChecker{CheckerInfo: &check.CheckerInfo{Name: "IntEqual", Params: []string{"a", "b"}}, rel: "=="}
79 |
80 | // IntNotEqual checker verifies that one integer is not equal to other integer.
81 | //
82 | // For example:
83 | //
84 | // c.Assert(1, IntNotEqual, 2)
85 | var IntNotEqual = &intChecker{CheckerInfo: &check.CheckerInfo{Name: "IntNotEqual", Params: []string{"a", "b"}}, rel: "!="}
86 |
87 | // IntGreaterThan checker verifies that one integer is greater than other integer.
88 | //
89 | // For example:
90 | //
91 | // c.Assert(2, IntGreaterThan, 1)
92 | var IntGreaterThan = &intChecker{CheckerInfo: &check.CheckerInfo{Name: "IntGreaterThan", Params: []string{"a", "b"}}, rel: ">"}
93 |
94 | // IntGreaterEqual checker verifies that one integer is greater than or equal to other integer.
95 | //
96 | // For example:
97 | //
98 | // c.Assert(1, IntGreaterEqual, 2)
99 | var IntGreaterEqual = &intChecker{CheckerInfo: &check.CheckerInfo{Name: "IntGreaterEqual", Params: []string{"a", "b"}}, rel: ">="}
100 |
--------------------------------------------------------------------------------
/internal/testutil/intcheckers_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2020 Canonical Ltd
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License version 3 as
5 | // published by the Free Software Foundation.
6 | //
7 | // This program is distributed in the hope that it will be useful,
8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 | // GNU General Public License for more details.
11 | //
12 | // You should have received a copy of the GNU General Public License
13 | // along with this program. If not, see .
14 |
15 | package testutil_test
16 |
17 | import (
18 | "gopkg.in/check.v1"
19 |
20 | . "github.com/canonical/chisel/internal/testutil"
21 | )
22 |
23 | type intCheckersSuite struct{}
24 |
25 | var _ = check.Suite(&intCheckersSuite{})
26 |
27 | func (*intCheckersSuite) TestIntChecker(c *check.C) {
28 | c.Assert(1, IntLessThan, 2)
29 | c.Assert(1, IntLessEqual, 1)
30 | c.Assert(1, IntEqual, 1)
31 | c.Assert(2, IntNotEqual, 1)
32 | c.Assert(2, IntGreaterThan, 1)
33 | c.Assert(2, IntGreaterEqual, 2)
34 |
35 | // Wrong argument types.
36 | testCheck(c, IntLessThan, false, "left-hand-side argument must be an int", false, 1)
37 | testCheck(c, IntLessThan, false, "right-hand-side argument must be an int", 1, false)
38 |
39 | // Relationship error.
40 | testCheck(c, IntLessThan, false, "relation 2 < 1 is not true", 2, 1)
41 | testCheck(c, IntLessEqual, false, "relation 2 <= 1 is not true", 2, 1)
42 | testCheck(c, IntEqual, false, "relation 2 == 1 is not true", 2, 1)
43 | testCheck(c, IntNotEqual, false, "relation 2 != 2 is not true", 2, 2)
44 | testCheck(c, IntGreaterThan, false, "relation 1 > 2 is not true", 1, 2)
45 | testCheck(c, IntGreaterEqual, false, "relation 1 >= 2 is not true", 1, 2)
46 |
47 | // Unexpected relation.
48 | unexpected := UnexpectedIntChecker("===")
49 | testCheck(c, unexpected, false, `unexpected relation "==="`, 1, 2)
50 | }
51 |
--------------------------------------------------------------------------------
/internal/testutil/nopcloser.go:
--------------------------------------------------------------------------------
1 | package testutil
2 |
3 | import (
4 | "io"
5 | )
6 |
7 | // readSeekNopCloser is an io.ReadSeeker that does nothing on Close.
8 | type readSeekNopCloser struct {
9 | io.ReadSeeker
10 | }
11 |
12 | func (readSeekNopCloser) Close() error { return nil }
13 |
14 | // ReadSeekNopCloser is an extension of io.NopCloser that also implements
15 | // io.Seeker.
16 | func ReadSeekNopCloser(r io.ReadSeeker) io.ReadSeekCloser {
17 | return readSeekNopCloser{r}
18 | }
19 |
--------------------------------------------------------------------------------
/internal/testutil/permutation.go:
--------------------------------------------------------------------------------
1 | package testutil
2 |
3 | func Permutations[S ~[]E, E any](s S) []S {
4 | var output []S
5 | // Heap's algorithm: https://en.wikipedia.org/wiki/Heap%27s_algorithm.
6 | var generate func(k int, s S)
7 | generate = func(k int, s S) {
8 | if k <= 1 {
9 | r := make([]E, len(s))
10 | copy(r, s)
11 | output = append(output, r)
12 | return
13 | }
14 | // Generate permutations with k-th unaltered.
15 | // Initially k = length(A).
16 | generate(k-1, s)
17 |
18 | // Generate permutations for k-th swapped with each k-1 initial.
19 | for i := 0; i < k-1; i += 1 {
20 | // Swap choice dependent on parity of k (even or odd).
21 | if k%2 == 0 {
22 | s[i], s[k-1] = s[k-1], s[i]
23 | } else {
24 | s[0], s[k-1] = s[k-1], s[0]
25 | }
26 | generate(k-1, s)
27 | }
28 | }
29 |
30 | sCpy := make([]E, len(s))
31 | copy(sCpy, s)
32 | generate(len(sCpy), sCpy)
33 | return output
34 | }
35 |
--------------------------------------------------------------------------------
/internal/testutil/permutation_test.go:
--------------------------------------------------------------------------------
1 | package testutil_test
2 |
3 | import (
4 | "sort"
5 |
6 | . "gopkg.in/check.v1"
7 |
8 | "github.com/canonical/chisel/internal/testutil"
9 | )
10 |
11 | type permutationSuite struct{}
12 |
13 | var _ = Suite(&permutationSuite{})
14 |
15 | var permutationTests = []struct {
16 | slice []any
17 | res [][]any
18 | }{
19 | {
20 | slice: []any{},
21 | res: [][]any{{}},
22 | },
23 | {
24 | slice: []any{1},
25 | res: [][]any{{1}},
26 | },
27 | {
28 | slice: []any{1, 2},
29 | res: [][]any{{1, 2}, {2, 1}},
30 | },
31 | {
32 | slice: []any{1, 2, 3},
33 | res: [][]any{{1, 2, 3}, {2, 1, 3}, {3, 1, 2}, {1, 3, 2}, {2, 3, 1}, {3, 2, 1}},
34 | },
35 | }
36 |
37 | func (*permutationSuite) TestPermutations(c *C) {
38 | for _, test := range permutationTests {
39 | c.Assert(testutil.Permutations(test.slice), DeepEquals, test.res)
40 | }
41 | }
42 |
43 | func (*permutationSuite) TestFuzzPermutations(c *C) {
44 | for sLen := 0; sLen <= 10; sLen++ {
45 | s := make([]byte, sLen)
46 | for i := 0; i < sLen; i++ {
47 | s[i] = byte(i)
48 | }
49 | permutations := testutil.Permutations(s)
50 |
51 | // Factorial.
52 | expectedLen := 1
53 | for i := 2; i <= len(s); i++ {
54 | expectedLen *= i
55 | }
56 | c.Assert(len(permutations), Equals, expectedLen)
57 |
58 | duplicated := map[string]bool{}
59 | for _, perm := range permutations {
60 | // []byte is not comparable.
61 | permStr := string(perm)
62 | if _, ok := duplicated[permStr]; ok {
63 | c.Fatalf("duplicated permutation: %v", perm)
64 | }
65 | duplicated[permStr] = true
66 | // Check that the elements are the same.
67 | sort.Slice(perm, func(i, j int) bool {
68 | return perm[i] < perm[j]
69 | })
70 | c.Assert(perm, DeepEquals, s, Commentf("invalid elements in permutation %v of base slice %v", perm, s))
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/internal/testutil/reindent.go:
--------------------------------------------------------------------------------
1 | package testutil
2 |
3 | import (
4 | "bytes"
5 | "strings"
6 | )
7 |
8 | // Reindent deindents the provided string and replaces tabs with spaces
9 | // so yaml inlined into tests works properly when decoded.
10 | func Reindent(in string) []byte {
11 | var buf bytes.Buffer
12 | var trim string
13 | var trimSet bool
14 | for _, line := range strings.Split(in, "\n") {
15 | if !trimSet {
16 | trimmed := strings.TrimLeft(line, "\t")
17 | if trimmed == "" {
18 | continue
19 | }
20 | if trimmed[0] == ' ' {
21 | panic("Space used in indent early in string:\n" + in)
22 | }
23 |
24 | trim = line[:len(line)-len(trimmed)]
25 | trimSet = true
26 |
27 | if trim == "" {
28 | return []uint8(strings.ReplaceAll(in, "\t", " ") + "\n")
29 | }
30 | }
31 | trimmed := strings.TrimPrefix(line, trim)
32 | if len(trimmed) == len(line) && strings.TrimLeft(line, "\t ") != "" {
33 | panic("Line not indented consistently:\n" + line)
34 | }
35 | trimmed = strings.ReplaceAll(trimmed, "\t", " ")
36 | buf.WriteString(trimmed)
37 | buf.WriteByte('\n')
38 | }
39 | return buf.Bytes()
40 | }
41 |
42 | // PrefixEachLine indents each line in the provided string with the prefix.
43 | func PrefixEachLine(text string, prefix string) string {
44 | var result strings.Builder
45 | lines := strings.Split(text, "\n")
46 | lastNewline := false
47 | if strings.HasSuffix(text, "\n") {
48 | // Skip iterating over the empty line.
49 | lines = lines[:len(lines)-1]
50 | lastNewline = true
51 | }
52 | for i, line := range lines {
53 | result.WriteString(prefix)
54 | result.WriteString(line)
55 | if i == len(lines)-1 && !lastNewline {
56 | // Do not add the last newline if the text did not have it to begin with.
57 | continue
58 | } else {
59 | result.WriteString("\n")
60 | }
61 | }
62 | return result.String()
63 | }
64 |
--------------------------------------------------------------------------------
/internal/testutil/reindent_test.go:
--------------------------------------------------------------------------------
1 | package testutil_test
2 |
3 | import (
4 | "strings"
5 |
6 | . "gopkg.in/check.v1"
7 |
8 | "github.com/canonical/chisel/internal/testutil"
9 | )
10 |
11 | type reindentTest struct {
12 | raw, result, error string
13 | }
14 |
15 | var reindentTests = []reindentTest{{
16 | raw: "a\nb",
17 | result: "a\nb",
18 | }, {
19 | raw: "\ta\n\tb",
20 | result: "a\nb",
21 | }, {
22 | raw: "a\n\tb\nc",
23 | result: "a\n b\nc",
24 | }, {
25 | raw: "a\n b\nc",
26 | result: "a\n b\nc",
27 | }, {
28 | raw: "\ta\n\t\tb\n\tc",
29 | result: "a\n b\nc",
30 | }, {
31 | raw: "\t a",
32 | error: "Space used in indent early in string:\n\t a",
33 | }, {
34 | raw: "\t a\n\t b\n\t c",
35 | error: "Space used in indent early in string:\n\t a\n\t b\n\t c",
36 | }, {
37 | raw: " a\nb",
38 | error: "Space used in indent early in string:\n a\nb",
39 | }, {
40 | raw: "\ta\nb",
41 | error: "Line not indented consistently:\nb",
42 | }}
43 |
44 | func (s *S) TestReindent(c *C) {
45 | for _, test := range reindentTests {
46 | s.testReindent(c, test)
47 | }
48 | }
49 |
50 | func (*S) testReindent(c *C, test reindentTest) {
51 | defer func() {
52 | if err := recover(); err != nil {
53 | errMsg, ok := err.(string)
54 | if !ok {
55 | panic(err)
56 | }
57 | c.Assert(errMsg, Equals, test.error)
58 | }
59 | }()
60 |
61 | c.Logf("Test: %#v", test)
62 |
63 | if !strings.HasSuffix(test.result, "\n") {
64 | test.result += "\n"
65 | }
66 |
67 | reindented := testutil.Reindent(test.raw)
68 | if test.error != "" {
69 | c.Errorf("Expected panic with message '%#v'", test.error)
70 | return
71 | }
72 | c.Assert(string(reindented), Equals, test.result)
73 | }
74 |
75 | type prefixEachLineTest struct {
76 | raw, prefix, result string
77 | }
78 |
79 | var prefixEachLineTests = []prefixEachLineTest{{
80 | raw: "a\n\tb\n \t\tc\td\n\t ",
81 | prefix: "foo",
82 | result: "fooa\nfoo\tb\nfoo \t\tc\td\nfoo\t ",
83 | }, {
84 | raw: "foo",
85 | prefix: "pref",
86 | result: "preffoo",
87 | }, {
88 | raw: "",
89 | prefix: "p",
90 | result: "p",
91 | }, {
92 | raw: "\n",
93 | prefix: "\t",
94 | result: "\t\n",
95 | }, {
96 | raw: "\n\n",
97 | prefix: "\t",
98 | result: "\t\n\t\n",
99 | }}
100 |
101 | func (s *S) TestPrefixEachLine(c *C) {
102 | for _, test := range prefixEachLineTests {
103 | c.Logf("Test: %#v", test)
104 |
105 | prefixed := testutil.PrefixEachLine(test.raw, test.prefix)
106 | c.Assert(prefixed, Equals, test.result)
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/internal/testutil/testutil_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014-2020 Canonical Ltd
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License version 3 as
5 | // published by the Free Software Foundation.
6 | //
7 | // This program is distributed in the hope that it will be useful,
8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 | // GNU General Public License for more details.
11 | //
12 | // You should have received a copy of the GNU General Public License
13 | // along with this program. If not, see .
14 |
15 | package testutil_test
16 |
17 | import (
18 | "reflect"
19 | "testing"
20 |
21 | "gopkg.in/check.v1"
22 | )
23 |
24 | func Test(t *testing.T) {
25 | check.TestingT(t)
26 | }
27 |
28 | type S struct{}
29 |
30 | var _ = check.Suite(&S{})
31 |
32 | func testInfo(c *check.C, checker check.Checker, name string, paramNames []string) {
33 | info := checker.Info()
34 | if info.Name != name {
35 | c.Fatalf("Got name %s, expected %s", info.Name, name)
36 | }
37 | if !reflect.DeepEqual(info.Params, paramNames) {
38 | c.Fatalf("Got param names %#v, expected %#v", info.Params, paramNames)
39 | }
40 | }
41 |
42 | func testCheck(c *check.C, checker check.Checker, result bool, error string, params ...interface{}) ([]interface{}, []string) {
43 | info := checker.Info()
44 | if len(params) != len(info.Params) {
45 | c.Fatalf("unexpected param count in test; expected %d got %d", len(info.Params), len(params))
46 | }
47 | names := append([]string{}, info.Params...)
48 | resultActual, errorActual := checker.Check(params, names)
49 | if resultActual != result || errorActual != error {
50 | c.Fatalf("%s.Check(%#v) returned (%#v, %#v) rather than (%#v, %#v)",
51 | info.Name, params, resultActual, errorActual, result, error)
52 | }
53 | return params, names
54 | }
55 |
--------------------------------------------------------------------------------
/internal/testutil/treedump.go:
--------------------------------------------------------------------------------
1 | package testutil
2 |
3 | import (
4 | "crypto/sha256"
5 | "fmt"
6 | "io/fs"
7 | "os"
8 | "path/filepath"
9 | "syscall"
10 |
11 | "github.com/canonical/chisel/internal/fsutil"
12 | )
13 |
14 | func TreeDump(dir string) map[string]string {
15 | var inodes []uint64
16 | pathsByInodes := make(map[uint64][]string)
17 | result := make(map[string]string)
18 | dirfs := os.DirFS(dir)
19 | err := fs.WalkDir(dirfs, ".", func(path string, d fs.DirEntry, err error) error {
20 | if err != nil {
21 | return fmt.Errorf("walk error: %w", err)
22 | }
23 | if path == "." {
24 | return nil
25 | }
26 | fpath := filepath.Join(dir, path)
27 | finfo, err := d.Info()
28 | if err != nil {
29 | return fmt.Errorf("cannot get stat info for %q: %w", fpath, err)
30 | }
31 | fperm := finfo.Mode() & fs.ModePerm
32 | ftype := finfo.Mode() & fs.ModeType
33 | if finfo.Mode()&fs.ModeSticky != 0 {
34 | fperm |= 01000
35 | }
36 | var entry string
37 | switch ftype {
38 | case fs.ModeDir:
39 | path = "/" + path + "/"
40 | entry = fmt.Sprintf("dir %#o", fperm)
41 | case fs.ModeSymlink:
42 | lpath, err := os.Readlink(fpath)
43 | if err != nil {
44 | return err
45 | }
46 | path = "/" + path
47 | entry = fmt.Sprintf("symlink %s", lpath)
48 | case 0: // Regular
49 | data, err := os.ReadFile(fpath)
50 | if err != nil {
51 | return fmt.Errorf("cannot read file: %w", err)
52 | }
53 | if len(data) == 0 {
54 | entry = fmt.Sprintf("file %#o empty", fperm)
55 | } else {
56 | sum := sha256.Sum256(data)
57 | entry = fmt.Sprintf("file %#o %.4x", fperm, sum)
58 | }
59 | path = "/" + path
60 | default:
61 | return fmt.Errorf("unknown file type %d: %s", ftype, fpath)
62 | }
63 | result[path] = entry
64 | if ftype != fs.ModeDir {
65 | stat, ok := finfo.Sys().(*syscall.Stat_t)
66 | if !ok {
67 | return fmt.Errorf("cannot get syscall stat info for %q", fpath)
68 | }
69 | inode := stat.Ino
70 | if len(pathsByInodes[inode]) == 1 {
71 | inodes = append(inodes, inode)
72 | }
73 | pathsByInodes[inode] = append(pathsByInodes[inode], path)
74 | }
75 | return nil
76 | })
77 | if err != nil {
78 | panic(err)
79 | }
80 |
81 | // Append identifiers to paths who share an inode e.g. hard links.
82 | for i := 0; i < len(inodes); i++ {
83 | for _, path := range pathsByInodes[inodes[i]] {
84 | result[path] = fmt.Sprintf("%s <%d>", result[path], i+1)
85 | }
86 | }
87 | return result
88 | }
89 |
90 | // TreeDumpEntry the file information in the same format as [testutil.TreeDump].
91 | func TreeDumpEntry(entry *fsutil.Entry) string {
92 | fperm := entry.Mode.Perm()
93 | if entry.Mode&fs.ModeSticky != 0 {
94 | fperm |= 01000
95 | }
96 | switch entry.Mode.Type() {
97 | case fs.ModeDir:
98 | return fmt.Sprintf("dir %#o", fperm)
99 | case fs.ModeSymlink:
100 | return fmt.Sprintf("symlink %s", entry.Link)
101 | case 0:
102 | // Regular file.
103 | if entry.Size == 0 {
104 | return fmt.Sprintf("file %#o empty", entry.Mode.Perm())
105 | } else {
106 | return fmt.Sprintf("file %#o %s", fperm, entry.SHA256[:8])
107 | }
108 | default:
109 | panic(fmt.Errorf("unknown file type %d: %s", entry.Mode.Type(), entry.Path))
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/public/jsonwall/log.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package jsonwall
4 |
5 | import (
6 | "fmt"
7 | "sync"
8 | )
9 |
10 | // Avoid importing the log type information unnecessarily. There's a small cost
11 | // associated with using an interface rather than the type. Depending on how
12 | // often the logger is plugged in, it would be worth using the type instead.
13 | type log_Logger interface {
14 | Output(calldepth int, s string) error
15 | }
16 |
17 | var globalLoggerLock sync.Mutex
18 | var globalLogger log_Logger
19 | var globalDebug bool
20 |
21 | // Specify the *log.Logger object where log messages should be sent to.
22 | func SetLogger(logger log_Logger) {
23 | globalLoggerLock.Lock()
24 | globalLogger = logger
25 | globalLoggerLock.Unlock()
26 | }
27 |
28 | // Enable the delivery of debug messages to the logger. Only meaningful
29 | // if a logger is also set.
30 | func SetDebug(debug bool) {
31 | globalLoggerLock.Lock()
32 | globalDebug = debug
33 | globalLoggerLock.Unlock()
34 | }
35 |
36 | // logf sends to the logger registered via SetLogger the string resulting
37 | // from running format and args through Sprintf.
38 | func logf(format string, args ...interface{}) {
39 | globalLoggerLock.Lock()
40 | defer globalLoggerLock.Unlock()
41 | if globalLogger != nil {
42 | globalLogger.Output(2, fmt.Sprintf(format, args...))
43 | }
44 | }
45 |
46 | // debugf sends to the logger registered via SetLogger the string resulting
47 | // from running format and args through Sprintf, but only if debugging was
48 | // enabled via SetDebug.
49 | func debugf(format string, args ...interface{}) {
50 | globalLoggerLock.Lock()
51 | defer globalLoggerLock.Unlock()
52 | if globalDebug && globalLogger != nil {
53 | globalLogger.Output(2, fmt.Sprintf(format, args...))
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/public/jsonwall/suite_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package jsonwall_test
4 |
5 | import (
6 | "testing"
7 |
8 | . "gopkg.in/check.v1"
9 |
10 | "github.com/canonical/chisel/public/jsonwall"
11 | )
12 |
13 | func Test(t *testing.T) { TestingT(t) }
14 |
15 | type S struct{}
16 |
17 | var _ = Suite(&S{})
18 |
19 | func (s *S) SetUpTest(c *C) {
20 | jsonwall.SetDebug(true)
21 | jsonwall.SetLogger(c)
22 | }
23 |
24 | func (s *S) TearDownTest(c *C) {
25 | jsonwall.SetDebug(false)
26 | jsonwall.SetLogger(nil)
27 | }
28 |
--------------------------------------------------------------------------------
/public/manifest/log.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package manifest
4 |
5 | import (
6 | "fmt"
7 | "sync"
8 | )
9 |
10 | // Avoid importing the log type information unnecessarily. There's a small cost
11 | // associated with using an interface rather than the type. Depending on how
12 | // often the logger is plugged in, it would be worth using the type instead.
13 | type log_Logger interface {
14 | Output(calldepth int, s string) error
15 | }
16 |
17 | var globalLoggerLock sync.Mutex
18 | var globalLogger log_Logger
19 | var globalDebug bool
20 |
21 | // Specify the *log.Logger object where log messages should be sent to.
22 | func SetLogger(logger log_Logger) {
23 | globalLoggerLock.Lock()
24 | globalLogger = logger
25 | globalLoggerLock.Unlock()
26 | }
27 |
28 | // Enable the delivery of debug messages to the logger. Only meaningful
29 | // if a logger is also set.
30 | func SetDebug(debug bool) {
31 | globalLoggerLock.Lock()
32 | globalDebug = debug
33 | globalLoggerLock.Unlock()
34 | }
35 |
36 | // logf sends to the logger registered via SetLogger the string resulting
37 | // from running format and args through Sprintf.
38 | func logf(format string, args ...interface{}) {
39 | globalLoggerLock.Lock()
40 | defer globalLoggerLock.Unlock()
41 | if globalLogger != nil {
42 | globalLogger.Output(2, fmt.Sprintf(format, args...))
43 | }
44 | }
45 |
46 | // debugf sends to the logger registered via SetLogger the string resulting
47 | // from running format and args through Sprintf, but only if debugging was
48 | // enabled via SetDebug.
49 | func debugf(format string, args ...interface{}) {
50 | globalLoggerLock.Lock()
51 | defer globalLoggerLock.Unlock()
52 | if globalDebug && globalLogger != nil {
53 | globalLogger.Output(2, fmt.Sprintf(format, args...))
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/public/manifest/manifest.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package manifest
4 |
5 | import (
6 | "fmt"
7 | "io"
8 |
9 | "github.com/canonical/chisel/public/jsonwall"
10 | )
11 |
12 | const Schema = "1.0"
13 |
14 | type Package struct {
15 | Kind string `json:"kind"`
16 | Name string `json:"name,omitempty"`
17 | Version string `json:"version,omitempty"`
18 | Digest string `json:"sha256,omitempty"`
19 | Arch string `json:"arch,omitempty"`
20 | }
21 |
22 | type Slice struct {
23 | Kind string `json:"kind"`
24 | Name string `json:"name,omitempty"`
25 | }
26 |
27 | type Path struct {
28 | Kind string `json:"kind"`
29 | Path string `json:"path,omitempty"`
30 | Mode string `json:"mode,omitempty"`
31 | Slices []string `json:"slices,omitempty"`
32 | SHA256 string `json:"sha256,omitempty"`
33 | FinalSHA256 string `json:"final_sha256,omitempty"`
34 | Size uint64 `json:"size,omitempty"`
35 | Link string `json:"link,omitempty"`
36 | Inode uint64 `json:"inode,omitempty"`
37 | }
38 |
39 | type Content struct {
40 | Kind string `json:"kind"`
41 | Slice string `json:"slice,omitempty"`
42 | Path string `json:"path,omitempty"`
43 | }
44 |
45 | type Manifest struct {
46 | db *jsonwall.DB
47 | }
48 |
49 | // Read loads a Manifest without performing any validation. The data is assumed
50 | // to be both valid jsonwall and a valid Manifest (see Validate).
51 | func Read(reader io.Reader) (manifest *Manifest, err error) {
52 | defer func() {
53 | if err != nil {
54 | err = fmt.Errorf("cannot read manifest: %s", err)
55 | }
56 | }()
57 |
58 | db, err := jsonwall.ReadDB(reader)
59 | if err != nil {
60 | return nil, err
61 | }
62 | mfestSchema := db.Schema()
63 | if mfestSchema != Schema {
64 | return nil, fmt.Errorf("unknown schema version %q", mfestSchema)
65 | }
66 |
67 | manifest = &Manifest{db: db}
68 | return manifest, nil
69 | }
70 |
71 | func (manifest *Manifest) IteratePaths(pathPrefix string, onMatch func(*Path) error) (err error) {
72 | return iteratePrefix(manifest, &Path{Kind: "path", Path: pathPrefix}, onMatch)
73 | }
74 |
75 | func (manifest *Manifest) IteratePackages(onMatch func(*Package) error) (err error) {
76 | return iteratePrefix(manifest, &Package{Kind: "package"}, onMatch)
77 | }
78 |
79 | func (manifest *Manifest) IterateSlices(pkgName string, onMatch func(*Slice) error) (err error) {
80 | return iteratePrefix(manifest, &Slice{Kind: "slice", Name: pkgName}, onMatch)
81 | }
82 |
83 | func (manifest *Manifest) IterateContents(slice string, onMatch func(*Content) error) (err error) {
84 | return iteratePrefix(manifest, &Content{Kind: "content", Slice: slice}, onMatch)
85 | }
86 |
87 | type prefixable interface {
88 | Path | Content | Package | Slice
89 | }
90 |
91 | func iteratePrefix[T prefixable](manifest *Manifest, prefix *T, onMatch func(*T) error) error {
92 | iter, err := manifest.db.IteratePrefix(prefix)
93 | if err != nil {
94 | return err
95 | }
96 | for iter.Next() {
97 | var val T
98 | err := iter.Get(&val)
99 | if err != nil {
100 | return fmt.Errorf("cannot read manifest: %s", err)
101 | }
102 | err = onMatch(&val)
103 | if err != nil {
104 | return err
105 | }
106 | }
107 | return nil
108 | }
109 |
--------------------------------------------------------------------------------
/public/manifest/suite_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package manifest_test
4 |
5 | import (
6 | "testing"
7 |
8 | . "gopkg.in/check.v1"
9 |
10 | "github.com/canonical/chisel/public/manifest"
11 | )
12 |
13 | func Test(t *testing.T) { TestingT(t) }
14 |
15 | type S struct{}
16 |
17 | var _ = Suite(&S{})
18 |
19 | func (s *S) SetUpTest(c *C) {
20 | manifest.SetDebug(true)
21 | manifest.SetLogger(c)
22 | }
23 |
24 | func (s *S) TearDownTest(c *C) {
25 | manifest.SetDebug(false)
26 | manifest.SetLogger(nil)
27 | }
28 |
--------------------------------------------------------------------------------
/snap/snapcraft.yaml:
--------------------------------------------------------------------------------
1 | name: chisel
2 | summary: Chisel is a software tool for carving and cutting Debian packages!
3 | description: |
4 | Chisel can derive a minimal Ubuntu-like Linux distribution
5 | using a release database that defines "slices" of existing packages.
6 | Slices enable developers to cherry-pick just the files they need
7 | from the Ubuntu archives, and combine them to create a new
8 | filesystem which can be packaged into an OCI-compliant
9 | container image or similar.
10 |
11 | This snap can only install the slices in a location inside the
12 | user $HOME directory i.e. the --root option in "cut" command
13 | should have a location inside the user $HOME directory.
14 | issues:
15 | - https://github.com/canonical/chisel/issues
16 | - https://github.com/Canonical/chisel/security/advisories
17 | source-code: https://github.com/canonical/chisel
18 | license: AGPL-3.0
19 | adopt-info: chisel-release-data
20 | contact:
21 | - rocks@canonical.com
22 | - security@ubuntu.com
23 |
24 | base: core22
25 | confinement: strict
26 |
27 | parts:
28 | chisel:
29 | plugin: go
30 | source: .
31 | build-snaps:
32 | - go/1.22/stable
33 | build-environment:
34 | - CGO_ENABLED: 0
35 | - GOFLAGS: -trimpath -ldflags=-w -ldflags=-s
36 | override-build: |
37 | go generate ./cmd
38 | craftctl default
39 | stage:
40 | - -bin/chrorder
41 |
42 | chisel-release-data:
43 | plugin: nil
44 | source: .
45 | override-build: |
46 | # set snap version
47 | version="$(${CRAFT_STAGE}/bin/chisel version)"
48 | craftctl set version="$version"
49 |
50 | # chisel releases are semantically versioned and
51 | # have a "v" prefix
52 | [[ "${version}" == *"git"* ]] && grade=devel || grade=stable
53 | craftctl set grade="$grade"
54 | after: [chisel]
55 |
56 | plugs:
57 | etc-apt-auth-conf-d:
58 | interface: system-files
59 | read:
60 | - /etc/apt/auth.conf.d
61 | - /etc/apt/auth.conf.d/90ubuntu-advantage
62 |
63 | apps:
64 | chisel:
65 | command: bin/chisel
66 | plugs:
67 | - network
68 | - home
69 | - etc-apt-auth-conf-d
70 |
--------------------------------------------------------------------------------
/spread.yaml:
--------------------------------------------------------------------------------
1 | project: chisel
2 |
3 | path: /chisel
4 |
5 | environment:
6 | OS: ubuntu
7 | PRO_TOKEN: $(HOST:echo $PRO_TOKEN)
8 |
9 | backends:
10 | # Cannot use LXD backend due to https://github.com/snapcore/spread/issues/154
11 | # lxd:
12 | # systems:
13 | # - ubuntu-bionic
14 | # - ubuntu-focal
15 | # - ubuntu-jammy
16 | # GitHub actions (runners) don't support nested virtualization (https://github.com/community/community/discussions/8305)
17 | # qemu:
18 | # systems:
19 | # - ubuntu-22.04:
20 | # username: ubuntu
21 | # password: ubuntu
22 | # - ubuntu-22.10:
23 | # username: ubuntu
24 | # password: ubuntu
25 | adhoc:
26 | allocate: |
27 | echo "Allocating $SPREAD_SYSTEM..."
28 | image=$(echo $SPREAD_SYSTEM | tr '-' ':')
29 | docker pull $image
30 | docker run -e usr=$SPREAD_SYSTEM_USERNAME -e pass=$SPREAD_SYSTEM_PASSWORD --name $SPREAD_SYSTEM -d $image sh -c '
31 | set -x
32 | apt update
33 | apt install -y openssh-server sudo zstd jq
34 | mkdir /run/sshd
35 | useradd -rm -d /home/ubuntu -s /bin/bash -g root -G sudo -u 1000 ubuntu
36 | echo "$usr:$pass" | chpasswd
37 | echo "$usr ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
38 | /usr/sbin/sshd -D
39 | '
40 | ADDRESS `docker inspect $SPREAD_SYSTEM --format '{{.NetworkSettings.Networks.bridge.IPAddress}}'`
41 | discard:
42 | docker rm -f $SPREAD_SYSTEM
43 | systems:
44 | - ubuntu-24.04:
45 | username: ubuntu
46 | password: ubuntu
47 |
48 | prepare: |
49 | apt install -y golang git
50 | go build -buildvcs=false ./cmd/chisel/
51 | mv chisel /usr/local/bin
52 |
53 | suites:
54 | tests/:
55 | summary: Tests common scenarios
56 | environment:
57 | RELEASE/jammy: 22.04
58 | RELEASE/focal: 20.04
59 | RELEASE/noble: 24.04
60 |
--------------------------------------------------------------------------------
/tests/basic/task.yaml:
--------------------------------------------------------------------------------
1 | summary: Ensure multiple slices (with mutation scripts) are properly installed
2 |
3 | execute: |
4 | rootfs_folder=rootfs_${RELEASE}
5 | mkdir -p $rootfs_folder
6 | chisel cut --release ${OS}-${RELEASE} \
7 | --root $rootfs_folder base-passwd_data openssl_bins
8 |
9 | # make sure $rootfs_folder is not empty
10 | ls ${rootfs_folder}/*
11 |
12 | # with the base-passwd mutation script, we can assert that:
13 | # - etc/{group,passwd} are not empty and not equal to FIXME
14 | # - usr/share/base-passwd/{group.master,passwd.master} do not exist
15 | test -s ${rootfs_folder}/etc/group && test "$(< ${rootfs_folder}/etc/group)" != "FIXME"
16 | test -s ${rootfs_folder}/etc/passwd && test "$(< ${rootfs_folder}/etc/passwd)" != "FIXME"
17 | test ! -e ${rootfs_folder}/usr/share/base-passwd/group.master
18 | test ! -e ${rootfs_folder}/usr/share/base-passwd/passwd.master
19 |
20 | # with the openssl pkg, both internal and external dependencies need to be met, for:
21 | # - libc6_libs
22 | # - libc6_config
23 | # - libssl3_libs
24 | # - openssl_config
25 | test -f ${rootfs_folder}/etc/ssl/openssl.cnf
26 | test -f ${rootfs_folder}/usr/lib/*-linux-*/libssl.so.*
27 | test -f ${rootfs_folder}/etc/ld.so.conf.d/*-linux-*.conf
28 | if [[ "${RELEASE}" == "24.04" ]]
29 | then
30 | libc_base_path="/usr/lib"
31 | else
32 | libc_base_path="/lib"
33 | fi
34 | test -f ${rootfs_folder}${libc_base_path}/*-linux-*/libc.so.*
35 |
--------------------------------------------------------------------------------
/tests/find/task.yaml:
--------------------------------------------------------------------------------
1 | summary: Chisel can find slice by slice name, package name or a combination
2 |
3 | execute: |
4 | find() {
5 | fullname=$1
6 | shift
7 | query=$@
8 | chisel find --release ${OS}-${RELEASE} $query | grep $fullname
9 | }
10 |
11 | find "ca-certificates_data" "ca-certificates_data"
12 | find "ca-certificates_data" "ca-certificates" "_data"
13 | find "ca-certificates_data" "_data" "ca-certificates"
14 | ! find "ca-certificates_data" "ca-certificates" "foo"
15 |
--------------------------------------------------------------------------------
/tests/info/task.yaml:
--------------------------------------------------------------------------------
1 | summary: Chisel can show detailed information about slices
2 |
3 | execute: |
4 | # Install dependencies.
5 | apt update && apt install -y wget
6 | wget -q https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq &&\
7 | chmod +x /usr/bin/yq
8 |
9 | # Single slice.
10 | chisel info --release ${OS}-${RELEASE} base-passwd_data > file.yaml
11 | yq file.yaml
12 | grep -q "/etc/group: {text: FIXME, mutable: true}" file.yaml
13 | ! grep -q "/usr/share/doc/base-passwd/copyright" file.yaml
14 |
15 | # Multiple slices.
16 | chisel info --release ${OS}-${RELEASE} base-passwd_data base-passwd_copyright > file.yaml
17 | yq file.yaml
18 | grep -q "/etc/group: {text: FIXME, mutable: true}" file.yaml
19 | grep -q "/usr/share/doc/base-passwd/copyright" file.yaml
20 |
21 | # Whole package.
22 | chisel info --release ${OS}-${RELEASE} base-passwd > file.yaml
23 | yq file.yaml
24 | grep -q "/etc/group: {text: FIXME, mutable: true}" file.yaml
25 | grep -q "/usr/share/doc/base-passwd/copyright" file.yaml
26 |
27 | # Non-existing.
28 | ! chisel info --release ${OS}-${RELEASE} does-not-exist
29 |
--------------------------------------------------------------------------------
/tests/pro-archives/chisel-releases/chisel.yaml:
--------------------------------------------------------------------------------
1 | format: v1
2 |
3 | v2-archives:
4 | ubuntu:
5 | version: 24.04
6 | pro: esm-infra
7 | components: [main]
8 | suites: [noble-infra-security, noble-infra-updates]
9 | public-keys: [ubuntu-esm-key-v2]
10 |
11 | public-keys:
12 | # Ubuntu Extended Security Maintenance Automatic Signing Key v2
13 | # rsa4096/56f7650a24c9e9ecf87c4d8d4067e40313cb4b13 2019-04-17T02:33:33Z
14 | ubuntu-esm-key-v2:
15 | id: "4067E40313CB4B13"
16 | armor: |
17 | -----BEGIN PGP PUBLIC KEY BLOCK-----
18 |
19 | mQINBFy2kH0BEADl/2e2pULZaSRovd3E1i1cVk3zebzndHZm/hK8/Srx69ivw3pY
20 | 680gFE/N3s3R/C5Jh9ThdD1zpGmxVdqcABSPmW1FczdFZY2E37HMH7Uijs4CsnFs
21 | 8nrNGQaqX/T1g2fQqjia3zkabMeehUEZC5GPYjpeeFW6Wy1O1A1Tzu7/Wjc+uF/t
22 | YYe/ZPXea74QZphu/N+8dy/ts/IzL2VtXuxiegGLfBFqzgZuBmlxXHVhftKvcis9
23 | t2ko65uVyDcLtItMhSJokKBsIYJliqOXjUbQf5dz8vLXkku94arBMgsxDWT4K/xI
24 | OTsaI/GMlSIKQ6Ucd/GKrBEsy5O8RDtD9A2klV7YeEwPEgqL+RhpdxAs/xUeTOZG
25 | JKwuvlBjzIhJF9bIfbyzx7DdcGFqRE+a8eBIUMQjVkt9Yk7jj0eV3oVTE7XNhb53
26 | rHuPL+zJVkiharxiTgYvkow3Nlbg3oURx9Ln67ni9pUtI1HbortGZsAkyOcpep58
27 | K9cYvUePJWzjkY+bjcGKR19CWPl7KaUalIf2Tao5OwtqjrblTsXdtV7eG45ys0MT
28 | Kl/DeqTJ0w6+i4eq4ZUfOCL/DIwS5zUB9j1KMUgEfocjYIdHWI8TSrA8jLYNPbVE
29 | 6+WjekHMB9liNrEQoESWBddS+bglPxuVwy2paGTUYJW1GnRZOTD+CG4ETQARAQAB
30 | tFFVYnVudHUgRXh0ZW5kZWQgU2VjdXJpdHkgTWFpbnRlbmFuY2UgQXV0b21hdGlj
31 | IFNpZ25pbmcgS2V5IHYyIDxlc21AY2Fub25pY2FsLmNvbT6JAjgEEwECACIFAly2
32 | kH0CGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEEBn5AMTy0sTo/8QAJ1C
33 | NhAkZ+Xq/BZ8UzAFCQn6GlIYg/ueY216xcQdDX1uN8hNOlPTNmftroIvohFAfFtB
34 | m5galzY3DBPU8eZr8Y8XgiGD97wkR4zfhfh1EK/6diMG/HG00kdcWquFXMRB7E7S
35 | nDTpyuPfkAzm9n6l69UB3UA53CaEUuVJ7qFfZsWgiQeUJpvqD0MIVsWr+T/paSx7
36 | 1JE9BVatFefq0egErv1sa2uYgcH9TRZMLw6gYxWtXeGA08Cpp0+OEvIzmJOHo5/F
37 | EpJ3hGk87Of77BC7FbqSDpeYkcjnlI2i0QAxxFygKhPOMLuA4XVn3TDuqCgTFIFC
38 | puupzIX/Up51FJmo64V9GZ/uF0jZy4tDxsCRJnEV+4Kv2sU5uMlmNchZMBjXYGiG
39 | tpH9CqJkSZjFvB6bk+Ot98KI6+CuNWn1N0sXFKpEUGdJLuOKfJ9+xI5plo8Bct5C
40 | DM9s4l0IuAPCsyayXrSmlyOAHzxDUeRMCEUnXWfycCUyqdyYIcCMPLV44Ccg9NyS
41 | 89dEauSCPuyCSxm5UYEHQdsSI/+rxRdS9IzoKs4za2L7fhY8PfdPlmghmXc/chz1
42 | RtgjPfAsUHUPRr0h//TzxRm5dbYdUyqMPzZcDO8wYBT/4xrwnFkSHZhnVxpw7PDi
43 | JYK4SVVc4ZO20PE1+RZc5oSbt4hRbFTCSb31Pydc
44 | =KWLs
45 | -----END PGP PUBLIC KEY BLOCK-----
46 |
--------------------------------------------------------------------------------
/tests/pro-archives/chisel-releases/slices/hello.yaml:
--------------------------------------------------------------------------------
1 | package: hello
2 |
3 | essential:
4 | - hello_copyright
5 |
6 | slices:
7 | bins:
8 | contents:
9 | /usr/bin/hello:
10 |
11 | copyright:
12 | contents:
13 | /usr/share/doc/hello/copyright:
14 |
--------------------------------------------------------------------------------
/tests/pro-archives/task.yaml:
--------------------------------------------------------------------------------
1 | summary: Chisel can fetch packages from Ubuntu Pro archives
2 |
3 | manual: true
4 |
5 | variants:
6 | - noble
7 |
8 | environment:
9 | ROOTFS: rootfs
10 |
11 | prepare: |
12 | apt update && apt install -y ubuntu-pro-client
13 | pro attach ${PRO_TOKEN} --no-auto-enable
14 | pro enable esm-infra --assume-yes
15 | mkdir ${ROOTFS}
16 |
17 | restore: |
18 | pro detach --assume-yes
19 | rm -r ${ROOTFS}
20 |
21 | execute: |
22 | chisel cut --release ./chisel-releases/ --root ${ROOTFS} hello_bins
23 | test -f ${ROOTFS}/usr/bin/hello
24 | test -f ${ROOTFS}/usr/share/doc/hello/copyright
25 |
--------------------------------------------------------------------------------
/tests/use-a-custom-chisel-release/task.yaml:
--------------------------------------------------------------------------------
1 | summary: Use a custom Chisel release
2 |
3 | execute: |
4 | rootfs_folder=rootfs_${RELEASE}
5 | mkdir -p $rootfs_folder
6 |
7 | chisel_release="./release_${RELEASE}"
8 | mkdir -p ${chisel_release}/slices
9 |
10 | ref_chisel_release="ref-chisel-release_${RELEASE}"
11 | git clone --depth=1 -b ${OS}-${RELEASE} \
12 | https://github.com/canonical/chisel-releases $ref_chisel_release
13 |
14 | cp ${ref_chisel_release}/chisel.yaml ${chisel_release}/chisel.yaml
15 |
16 | cat >>${chisel_release}/slices/base-files.yaml <