├── .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 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Debian package A 14 | 15 | 17 | A_slice1 20 | 22 | A_slice2 25 | 27 | A_slice3 30 | 31 | 32 | 33 | 34 | 35 | Debian package B 38 | 39 | 41 | B_slice1 44 | 45 | 47 | B_slice2 50 | 51 | 53 | 54 | 55 | 57 | 58 | 59 | 61 | 62 | 63 | 64 | 65 | 66 | 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 <