├── .gitignore
├── internal
├── setup
│ ├── export_test.go
│ ├── suite_test.go
│ ├── fetch_test.go
│ ├── log.go
│ ├── tarjan_test.go
│ ├── tarjan.go
│ └── fetch.go
├── strdist
│ ├── export_test.go
│ ├── suite_test.go
│ ├── log.go
│ ├── strdist_test.go
│ └── strdist.go
├── deb
│ ├── export_test.go
│ ├── suite_test.go
│ ├── helpers.go
│ ├── chrorder.go
│ ├── chrorder
│ │ └── main.go
│ ├── log.go
│ ├── helpers_test.go
│ ├── version.go
│ └── version_test.go
├── cache
│ ├── suite_test.go
│ ├── cache_test.go
│ └── cache.go
├── control
│ ├── suite_test.go
│ ├── helpers.go
│ ├── helpers_test.go
│ ├── control_test.go
│ └── control.go
├── archive
│ ├── suite_test.go
│ ├── export_test.go
│ └── log.go
├── fsutil
│ ├── suite_test.go
│ └── log.go
├── slicer
│ ├── suite_test.go
│ └── log.go
├── testutil
│ ├── nopcloser.go
│ ├── defaults.go
│ ├── permutation.go
│ ├── export_test.go
│ ├── archive.go
│ ├── base.go
│ ├── permutation_test.go
│ ├── reindent.go
│ ├── filepresencechecker.go
│ ├── testutil_test.go
│ ├── filepresencechecker_test.go
│ ├── intcheckers_test.go
│ ├── reindent_test.go
│ ├── treedump.go
│ ├── intcheckers.go
│ ├── filecontentchecker.go
│ ├── exec_test.go
│ └── filecontentchecker_test.go
├── pgputil
│ ├── suite_test.go
│ ├── log.go
│ └── openpgp.go
├── scripts
│ ├── suite_test.go
│ └── log.go
├── manifestutil
│ ├── suite_test.go
│ ├── log.go
│ └── report.go
├── apacheutil
│ ├── suite_test.go
│ ├── util.go
│ ├── log.go
│ └── util_test.go
└── apachetestutil
│ └── manifest.go
├── docs
└── _static
│ ├── slice-of-ubuntu.png
│ └── package-slices.svg
├── .github
├── PULL_REQUEST_TEMPLATE.md
├── workflows
│ ├── cla-check.yml
│ ├── license.yaml
│ ├── lint.yaml
│ ├── spread.yml
│ ├── security.yaml
│ ├── tiobe.yaml
│ ├── tests.yaml
│ ├── performance.yaml
│ ├── pro_tests.yaml
│ ├── snap.yml
│ └── build.yml
└── scripts
│ └── external-packages-license-check.go
├── tests
├── unmaintained
│ ├── release-23.10
│ │ ├── slices
│ │ │ └── hello.yaml
│ │ └── chisel.yaml
│ └── task.yaml
├── pro-archives
│ ├── chisel-releases
│ │ ├── slices
│ │ │ └── hello.yaml
│ │ └── chisel.yaml
│ └── task.yaml
├── debug-check-release-archives
│ └── task.yaml
├── find
│ └── task.yaml
├── info
│ └── task.yaml
├── unstable
│ └── task.yaml
├── use-a-custom-chisel-release
│ └── task.yaml
└── basic
│ └── task.yaml
├── cmd
├── chisel
│ ├── cmd_debug.go
│ ├── cmd_version_test.go
│ ├── cmd_version.go
│ ├── export_test.go
│ ├── main_test.go
│ ├── log.go
│ ├── helpers.go
│ ├── cmd_find.go
│ ├── cmd_info.go
│ ├── cmd_find_test.go
│ └── cmd_cut.go
├── version.go
└── mkversion.sh
├── SECURITY.md
├── public
├── jsonwall
│ ├── suite_test.go
│ └── log.go
└── manifest
│ ├── suite_test.go
│ ├── log.go
│ └── manifest.go
├── go.mod
├── .golangci.yaml
├── spread.yaml
├── snap
└── snapcraft.yaml
└── go.sum
/.gitignore:
--------------------------------------------------------------------------------
1 | .spread*
2 |
--------------------------------------------------------------------------------
/internal/setup/export_test.go:
--------------------------------------------------------------------------------
1 | package setup
2 |
3 | type YAMLPath = yamlPath
4 |
--------------------------------------------------------------------------------
/internal/strdist/export_test.go:
--------------------------------------------------------------------------------
1 | package strdist
2 |
3 | var GlobCost = globCost
4 |
--------------------------------------------------------------------------------
/docs/_static/slice-of-ubuntu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canonical/chisel/HEAD/docs/_static/slice-of-ubuntu.png
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | - [ ] Have you signed the [CLA](http://www.ubuntu.com/legal/contributors/)?
2 |
3 | -----
4 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/tests/unmaintained/release-23.10/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/debug-check-release-archives/task.yaml:
--------------------------------------------------------------------------------
1 | summary: Check cohesion runs correctly
2 |
3 | execute: |
4 | chisel debug check-release-archives --release ${OS}-${RELEASE} 2> stderr.out || true
5 |
6 | # We cannot assert that the command completed successfully as that depends on
7 | # the release itself. We check for progress instead.
8 | grep -q "Fetching" stderr.out
9 | grep -q "Processing archive" stderr.out
10 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/defaults.go:
--------------------------------------------------------------------------------
1 | package testutil
2 |
3 | var testKey = PGPKeys["key1"]
4 |
5 | var DefaultChiselYaml = `
6 | format: v1
7 | maintenance:
8 | standard: 2025-01-01
9 | end-of-life: 2100-01-01
10 | archives:
11 | ubuntu:
12 | version: 22.04
13 | components: [main, universe]
14 | suites: [jammy]
15 | public-keys: [test-key]
16 | public-keys:
17 | test-key:
18 | id: ` + testKey.ID + `
19 | armor: |` + "\n" + PrefixEachLine(testKey.PubKeyArmor, "\t\t\t\t\t\t")
20 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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.64.8
30 |
--------------------------------------------------------------------------------
/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/export_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/canonical/chisel/internal/archive"
4 |
5 | var RunMain = run
6 |
7 | func FakeIsStdoutTTY(t bool) (restore func()) {
8 | oldIsStdoutTTY := isStdoutTTY
9 | isStdoutTTY = t
10 | return func() {
11 | isStdoutTTY = oldIsStdoutTTY
12 | }
13 | }
14 |
15 | func FakeIsStdinTTY(t bool) (restore func()) {
16 | oldIsStdinTTY := isStdinTTY
17 | isStdinTTY = t
18 | return func() {
19 | isStdinTTY = oldIsStdinTTY
20 | }
21 | }
22 |
23 | var FindSlices = findSlices
24 |
25 | func FakeArchiveOpen(f func(_ *archive.Options) (archive.Archive, error)) (restore func()) {
26 | oldArchiveOpen := archiveOpen
27 | archiveOpen = f
28 | return func() {
29 | archiveOpen = oldArchiveOpen
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/canonical/chisel
2 |
3 | go 1.24.6
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.15
11 | go.starlark.net v0.0.0-20250417143717-f57e51f710eb
12 | golang.org/x/crypto v0.37.0
13 | golang.org/x/term v0.31.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.14.1 // indirect
23 | golang.org/x/sys v0.32.0 // indirect
24 | )
25 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/tests/unmaintained/task.yaml:
--------------------------------------------------------------------------------
1 | summary: Chisel can fetch packages from unmaintained releases
2 |
3 | variants:
4 | - mantic
5 |
6 | environment:
7 | ROOTFS: rootfs
8 |
9 | execute: |
10 | mkdir "${ROOTFS}"
11 |
12 | # TODO: change to the upstream release when it has maintenance set.
13 | ! OUTPUT=$(chisel cut --release ./release-${RELEASE} --root ${ROOTFS} hello_bins 2>&1)
14 | echo "$OUTPUT" | grep "no archive has \"maintained\" maintenance status, consider the different Ubuntu Pro subscriptions to be safe, see https://documentation.ubuntu.com/chisel/en/latest/reference/chisel-releases/chisel.yaml/#maintenance for details"
15 |
16 | OUTPUT=$(chisel cut --release ./release-${RELEASE} --root ${ROOTFS} --ignore=unmaintained hello_bins 2>&1)
17 | echo "$OUTPUT" | grep "Warning: No archive has \"maintained\" maintenance status. Consider the different Ubuntu Pro subscriptions to be safe. See https://documentation.ubuntu.com/chisel/en/latest/reference/chisel-releases/chisel.yaml/#maintenance for details."
18 |
19 | test -f ${ROOTFS}/usr/bin/hello
20 | test -f ${ROOTFS}/usr/share/doc/hello/copyright
21 |
22 | # Remove the maintenance block.
23 | sed -e "/maintenance:/,+3d" -i ./release-${RELEASE}/chisel.yaml
24 | ! chisel cut --release ./release-${RELEASE} --root ${ROOTFS} --ignore=unmaintained
25 | ! chisel cut --release ./release-${RELEASE} --root ${ROOTFS} hello_bins
26 |
--------------------------------------------------------------------------------
/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/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/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 := range 3 {
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/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 |
--------------------------------------------------------------------------------
/tests/unstable/task.yaml:
--------------------------------------------------------------------------------
1 | summary: Chisel can fetch packages from unmaintained releases
2 |
3 | variants:
4 | # No reason we should run this test for all releases.
5 | - noble
6 |
7 | environment:
8 | ROOTFS: rootfs
9 |
10 | execute: |
11 | # Install yq.
12 | wget -q https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq &&\
13 | chmod +x /usr/bin/yq
14 |
15 | mkdir "${ROOTFS}"
16 |
17 | git clone --depth=1 -b ${OS}-${RELEASE} \
18 | https://github.com/canonical/chisel-releases release-${RELEASE}
19 |
20 | # Add dates for the release to be unstable.
21 | yq -i '.maintenance.standard = "2100-01-01"' ./release-${RELEASE}/chisel.yaml
22 | yq -i '.maintenance.end-of-life = "2200-01-01"' ./release-${RELEASE}/chisel.yaml
23 |
24 | ! OUTPUT=$(chisel cut --release ./release-${RELEASE} --root ${ROOTFS} hello_bins 2>&1)
25 | echo "$OUTPUT" | grep "this release is in the \"unstable\" maintenance status, see https://documentation.ubuntu.com/chisel/en/latest/reference/chisel-releases/chisel.yaml/#maintenance for details"
26 |
27 | OUTPUT=$(chisel cut --release ./release-${RELEASE} --root ${ROOTFS} --ignore=unstable hello_bins 2>&1)
28 | echo "$OUTPUT" | grep "Warning: This release is in the \"unstable\" maintenance status. See https://documentation.ubuntu.com/chisel/en/latest/reference/chisel-releases/chisel.yaml/#maintenance to be safe"
29 |
30 | test -f ${ROOTFS}/usr/bin/hello
31 | test -f ${ROOTFS}/usr/share/doc/hello/copyright
32 |
--------------------------------------------------------------------------------
/.github/workflows/tiobe.yaml:
--------------------------------------------------------------------------------
1 | name: TIOBE Quality Checks
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: '0 7 1 * *'
7 |
8 | jobs:
9 | TiCS:
10 | runs-on: [self-hosted, reactive, amd64, tiobe, noble]
11 | steps:
12 | - uses: actions/checkout@v4
13 | with:
14 | persist-credentials: false
15 |
16 | - name: Set up Go
17 | uses: actions/setup-go@v5
18 | with:
19 | go-version-file: 'go.mod'
20 |
21 | - name: Install dependencies
22 | run: |
23 | go install honnef.co/go/tools/cmd/staticcheck@v0.6.1
24 | go install github.com/axw/gocov/gocov@v1.1.0
25 | go install github.com/AlekSi/gocov-xml@v1.1.0
26 |
27 | # We could store a report from the regular run, but this is cheap to do and keeps this isolated.
28 | - name: Test and generate coverage report
29 | run: |
30 | go test -coverprofile=coverage.out ./...
31 | gocov convert coverage.out > coverage.json
32 | # Annoyingly, the coverage.xml file needs to be in a .coverage folder.
33 | mkdir .coverage
34 | gocov-xml < coverage.json > .coverage/coverage.xml
35 |
36 | - name: TiCS GitHub Action
37 | uses: tiobe/tics-github-action@v3
38 | with:
39 | mode: qserver
40 | viewerUrl: https://canonical.tiobe.com/tiobeweb/TICS/api/cfg?name=GoProjects
41 | ticsAuthToken: ${{ secrets.TICSAUTHTOKEN }}
42 | project: chisel
43 | installTics: true
44 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 <.
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 []any, 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/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 | "slices"
20 | "testing"
21 |
22 | "gopkg.in/check.v1"
23 | )
24 |
25 | func Test(t *testing.T) {
26 | check.TestingT(t)
27 | }
28 |
29 | type S struct{}
30 |
31 | var _ = check.Suite(&S{})
32 |
33 | func testInfo(c *check.C, checker check.Checker, name string, paramNames []string) {
34 | info := checker.Info()
35 | if info.Name != name {
36 | c.Fatalf("Got name %s, expected %s", info.Name, name)
37 | }
38 | if !reflect.DeepEqual(info.Params, paramNames) {
39 | c.Fatalf("Got param names %#v, expected %#v", info.Params, paramNames)
40 | }
41 | }
42 |
43 | func testCheck(c *check.C, checker check.Checker, result bool, error string, params ...any) ([]any, []string) {
44 | info := checker.Info()
45 | if len(params) != len(info.Params) {
46 | c.Fatalf("unexpected param count in test; expected %d got %d", len(info.Params), len(params))
47 | }
48 | names := slices.Clone(info.Params)
49 | resultActual, errorActual := checker.Check(params, names)
50 | if resultActual != result || errorActual != error {
51 | c.Fatalf("%s.Check(%#v) returned (%#v, %#v) rather than (%#v, %#v)",
52 | info.Name, params, resultActual, errorActual, result, error)
53 | }
54 | return params, names
55 | }
56 |
--------------------------------------------------------------------------------
/spread.yaml:
--------------------------------------------------------------------------------
1 | project: chisel
2 |
3 | path: /chisel
4 |
5 | environment:
6 | OS: ubuntu
7 | PRO_TOKEN: $(HOST:echo $PRO_TOKEN)
8 | RELEASE/jammy: 22.04
9 | RELEASE/focal: 20.04
10 | RELEASE/noble: 24.04
11 | RELEASE/mantic: 23.10
12 |
13 | backends:
14 | # Cannot use LXD backend due to https://github.com/snapcore/spread/issues/154
15 | # lxd:
16 | # systems:
17 | # - ubuntu-bionic
18 | # - ubuntu-focal
19 | # - ubuntu-jammy
20 | # GitHub actions (runners) don't support nested virtualization (https://github.com/community/community/discussions/8305)
21 | # qemu:
22 | # systems:
23 | # - ubuntu-22.04:
24 | # username: ubuntu
25 | # password: ubuntu
26 | # - ubuntu-22.10:
27 | # username: ubuntu
28 | # password: ubuntu
29 | adhoc:
30 | allocate: |
31 | echo "Allocating $SPREAD_SYSTEM..."
32 | image=$(echo $SPREAD_SYSTEM | tr '-' ':')
33 | docker pull $image
34 | docker run -e usr=$SPREAD_SYSTEM_USERNAME -e pass=$SPREAD_SYSTEM_PASSWORD --name $SPREAD_SYSTEM -d $image sh -c '
35 | set -x
36 | apt update
37 | apt install -y openssh-server sudo zstd jq
38 | mkdir /run/sshd
39 | useradd -rm -d /home/ubuntu -s /bin/bash -g root -G sudo -u 1000 ubuntu
40 | echo "$usr:$pass" | chpasswd
41 | echo "$usr ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
42 | /usr/sbin/sshd -D
43 | '
44 | ADDRESS `docker inspect $SPREAD_SYSTEM --format '{{.NetworkSettings.Networks.bridge.IPAddress}}'`
45 | discard:
46 | docker rm -f $SPREAD_SYSTEM
47 | systems:
48 | - ubuntu-24.04:
49 | username: ubuntu
50 | password: ubuntu
51 |
52 | prepare: |
53 | apt install -y golang-1.23 git
54 | export PATH=/usr/lib/go-1.23/bin:$PATH
55 | go build -buildvcs=false ./cmd/chisel/
56 | mv chisel /usr/local/bin
57 |
58 | suites:
59 | tests/:
60 | summary: Tests common scenarios
61 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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_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/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/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 |
--------------------------------------------------------------------------------
/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 ...any) {
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 ...any) {
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 ...any) {
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 |
--------------------------------------------------------------------------------
/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.24/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/unmaintained/release-23.10/chisel.yaml:
--------------------------------------------------------------------------------
1 | format: v2
2 |
3 | maintenance:
4 | standard: 2023-10-01
5 | end-of-life: 2024-04-01
6 |
7 | archives:
8 | ubuntu:
9 | version: 23.10
10 | components: [main, universe]
11 | suites: [mantic, mantic-security, mantic-updates]
12 | public-keys: [ubuntu-archive-key-2018]
13 |
14 | public-keys:
15 | # Ubuntu Archive Automatic Signing Key (2018)
16 | # rsa4096/f6ecb3762474eda9d21b7022871920d1991bc93c 2018-09-17T15:01:46Z
17 | ubuntu-archive-key-2018:
18 | id: "871920D1991BC93C"
19 | armor: |
20 | -----BEGIN PGP PUBLIC KEY BLOCK-----
21 |
22 | mQINBFufwdoBEADv/Gxytx/LcSXYuM0MwKojbBye81s0G1nEx+lz6VAUpIUZnbkq
23 | dXBHC+dwrGS/CeeLuAjPRLU8AoxE/jjvZVp8xFGEWHYdklqXGZ/gJfP5d3fIUBtZ
24 | HZEJl8B8m9pMHf/AQQdsC+YzizSG5t5Mhnotw044LXtdEEkx2t6Jz0OGrh+5Ioxq
25 | X7pZiq6Cv19BohaUioKMdp7ES6RYfN7ol6HSLFlrMXtVfh/ijpN9j3ZhVGVeRC8k
26 | KHQsJ5PkIbmvxBiUh7SJmfZUx0IQhNMaDHXfdZAGNtnhzzNReb1FqNLSVkrS/Pns
27 | AQzMhG1BDm2VOSF64jebKXffFqM5LXRQTeqTLsjUbbrqR6s/GCO8UF7jfUj6I7ta
28 | LygmsHO/JD4jpKRC0gbpUBfaiJyLvuepx3kWoqL3sN0LhlMI80+fA7GTvoOx4tpq
29 | VlzlE6TajYu+jfW3QpOFS5ewEMdL26hzxsZg/geZvTbArcP+OsJKRmhv4kNo6Ayd
30 | yHQ/3ZV/f3X9mT3/SPLbJaumkgp3Yzd6t5PeBu+ZQk/mN5WNNuaihNEV7llb1Zhv
31 | Y0Fxu9BVd/BNl0rzuxp3rIinB2TX2SCg7wE5xXkwXuQ/2eTDE0v0HlGntkuZjGow
32 | DZkxHZQSxZVOzdZCRVaX/WEFLpKa2AQpw5RJrQ4oZ/OfifXyJzP27o03wQARAQAB
33 | tEJVYnVudHUgQXJjaGl2ZSBBdXRvbWF0aWMgU2lnbmluZyBLZXkgKDIwMTgpIDxm
34 | dHBtYXN0ZXJAdWJ1bnR1LmNvbT6JAjgEEwEKACIFAlufwdoCGwMGCwkIBwMCBhUI
35 | AgkKCwQWAgMBAh4BAheAAAoJEIcZINGZG8k8LHMQAKS2cnxz/5WaoCOWArf5g6UH
36 | beOCgc5DBm0hCuFDZWWv427aGei3CPuLw0DGLCXZdyc5dqE8mvjMlOmmAKKlj1uG
37 | g3TYCbQWjWPeMnBPZbkFgkZoXJ7/6CB7bWRht1sHzpt1LTZ+SYDwOwJ68QRp7DRa
38 | Zl9Y6QiUbeuhq2DUcTofVbBxbhrckN4ZteLvm+/nG9m/ciopc66LwRdkxqfJ32Cy
39 | q+1TS5VaIJDG7DWziG+Kbu6qCDM4QNlg3LH7p14CrRxAbc4lvohRgsV4eQqsIcdF
40 | kuVY5HPPj2K8TqpY6STe8Gh0aprG1RV8ZKay3KSMpnyV1fAKn4fM9byiLzQAovC0
41 | LZ9MMMsrAS/45AvC3IEKSShjLFn1X1dRCiO6/7jmZEoZtAp53hkf8SMBsi78hVNr
42 | BumZwfIdBA1v22+LY4xQK8q4XCoRcA9G+pvzU9YVW7cRnDZZGl0uwOw7z9PkQBF5
43 | KFKjWDz4fCk+K6+YtGpovGKekGBb8I7EA6UpvPgqA/QdI0t1IBP0N06RQcs1fUaA
44 | QEtz6DGy5zkRhR4pGSZn+dFET7PdAjEK84y7BdY4t+U1jcSIvBj0F2B7LwRL7xGp
45 | SpIKi/ekAXLs117bvFHaCvmUYN7JVp1GMmVFxhIdx6CFm3fxG8QjNb5tere/YqK+
46 | uOgcXny1UlwtCUzlrSaP
47 | =9AdM
48 | -----END PGP PUBLIC KEY BLOCK-----
49 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/docs/_static/package-slices.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
67 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 := range inodes {
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/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 |
--------------------------------------------------------------------------------
/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 []any, 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 |
--------------------------------------------------------------------------------
/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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
22 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
23 | github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
24 | github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
25 | go.starlark.net v0.0.0-20250417143717-f57e51f710eb h1:zOg9DxxrorEmgGUr5UPdCEwKqiqG0MlZciuCuA3XiDE=
26 | go.starlark.net v0.0.0-20250417143717-f57e51f710eb/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
27 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
28 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
29 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
30 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
31 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
32 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
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/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/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 := range max(len(as), len(bs)) {
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 := range len(a) {
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 := range len(a) {
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/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/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 []any, 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 any, 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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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_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: %s", 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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/cmd/chisel/cmd_cut.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "slices"
6 | "time"
7 |
8 | "github.com/jessevdk/go-flags"
9 |
10 | "github.com/canonical/chisel/internal/archive"
11 | "github.com/canonical/chisel/internal/cache"
12 | "github.com/canonical/chisel/internal/setup"
13 | "github.com/canonical/chisel/internal/slicer"
14 | )
15 |
16 | var shortCutHelp = "Cut a tree with selected slices"
17 | var longCutHelp = `
18 | The cut command uses the provided selection of package slices
19 | to create a new filesystem tree in the root location.
20 |
21 | By default it fetches the slices for the same Ubuntu version as the
22 | current host, unless the --release flag is used.
23 | `
24 |
25 | var cutDescs = map[string]string{
26 | "release": "Chisel release name or directory (e.g. ubuntu-22.04)",
27 | "root": "Root for generated content",
28 | "arch": "Package architecture",
29 | "ignore": "Conditions to ignore (e.g. unmaintained, unstable)",
30 | }
31 |
32 | type cmdCut struct {
33 | Release string `long:"release" value-name:""`
34 | RootDir string `long:"root" value-name:"" required:"yes"`
35 | Arch string `long:"arch" value-name:""`
36 | Ignore []string `long:"ignore" choice:"unmaintained" choice:"unstable" value-name:""`
37 |
38 | Positional struct {
39 | SliceRefs []string `positional-arg-name:"" required:"yes"`
40 | } `positional-args:"yes"`
41 | }
42 |
43 | func init() {
44 | addCommand("cut", shortCutHelp, longCutHelp, func() flags.Commander { return &cmdCut{} }, cutDescs, nil)
45 | }
46 |
47 | func (cmd *cmdCut) Execute(args []string) error {
48 | if len(args) > 0 {
49 | return ErrExtraArgs
50 | }
51 |
52 | sliceKeys := make([]setup.SliceKey, len(cmd.Positional.SliceRefs))
53 | for i, sliceRef := range cmd.Positional.SliceRefs {
54 | sliceKey, err := setup.ParseSliceKey(sliceRef)
55 | if err != nil {
56 | return err
57 | }
58 | sliceKeys[i] = sliceKey
59 | }
60 |
61 | release, err := obtainRelease(cmd.Release)
62 | if err != nil {
63 | return err
64 | }
65 |
66 | if time.Now().Before(release.Maintenance.Standard) {
67 | if slices.Contains(cmd.Ignore, "unstable") {
68 | logf(`Warning: This release is in the "unstable" maintenance status. ` +
69 | `See https://documentation.ubuntu.com/chisel/en/latest/reference/chisel-releases/chisel.yaml/#maintenance to be safe`)
70 | } else {
71 | return fmt.Errorf(`this release is in the "unstable" maintenance status, ` +
72 | `see https://documentation.ubuntu.com/chisel/en/latest/reference/chisel-releases/chisel.yaml/#maintenance for details`)
73 | }
74 | }
75 |
76 | selection, err := setup.Select(release, sliceKeys, cmd.Arch)
77 | if err != nil {
78 | return err
79 | }
80 |
81 | archives := make(map[string]archive.Archive)
82 | for archiveName, archiveInfo := range release.Archives {
83 | openArchive, err := archive.Open(&archive.Options{
84 | Label: archiveName,
85 | Version: archiveInfo.Version,
86 | Arch: cmd.Arch,
87 | Suites: archiveInfo.Suites,
88 | Components: archiveInfo.Components,
89 | Pro: archiveInfo.Pro,
90 | CacheDir: cache.DefaultDir("chisel"),
91 | PubKeys: archiveInfo.PubKeys,
92 | Maintained: archiveInfo.Maintained,
93 | OldRelease: archiveInfo.OldRelease,
94 | })
95 | if err != nil {
96 | if err == archive.ErrCredentialsNotFound {
97 | logf("Archive %q ignored: credentials not found", archiveName)
98 | continue
99 | }
100 | return err
101 | }
102 | archives[archiveName] = openArchive
103 | }
104 |
105 | hasMaintainedArchive := false
106 | for _, archive := range archives {
107 | if archive.Options().Maintained {
108 | hasMaintainedArchive = true
109 | break
110 | }
111 | }
112 | if !hasMaintainedArchive {
113 | if slices.Contains(cmd.Ignore, "unmaintained") {
114 | logf(`Warning: No archive has "maintained" maintenance status. ` +
115 | `Consider the different Ubuntu Pro subscriptions to be safe. ` +
116 | `See https://documentation.ubuntu.com/chisel/en/latest/reference/chisel-releases/chisel.yaml/#maintenance for details.`)
117 | } else {
118 | return fmt.Errorf(`no archive has "maintained" maintenance status, ` +
119 | `consider the different Ubuntu Pro subscriptions to be safe, ` +
120 | `see https://documentation.ubuntu.com/chisel/en/latest/reference/chisel-releases/chisel.yaml/#maintenance for details`)
121 | }
122 | }
123 |
124 | err = slicer.Run(&slicer.RunOptions{
125 | Selection: selection,
126 | Archives: archives,
127 | TargetDir: cmd.RootDir,
128 | })
129 | return err
130 | }
131 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------