From c5d397c1f476c3c41d2db98fd740d43b5b7bc9ae Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Thu, 9 Jul 2026 14:55:35 +0200 Subject: [PATCH 1/7] feat: prepare interface for store Signed-off-by: Paul Mars --- internal/slicer/slicer.go | 74 +++++++++++++++++++++++++++------- internal/slicer/slicer_test.go | 24 +++++++++++ 2 files changed, 84 insertions(+), 14 deletions(-) diff --git a/internal/slicer/slicer.go b/internal/slicer/slicer.go index 6f4783b8..a2ab6032 100644 --- a/internal/slicer/slicer.go +++ b/internal/slicer/slicer.go @@ -41,6 +41,15 @@ type contentChecker struct { knownPaths map[string]pathData } +// pkgSource holds the resolved source for a package: its architecture and a +// fetch function returning the package reader and metadata. The fetch +// function is bound at resolution time, so callers are agnostic to whether +// the package comes from an archive or a store. +type pkgSource struct { + arch string + fetch func() (io.ReadSeekCloser, *archive.PackageInfo, error) +} + func (cc *contentChecker) checkMutable(path string) error { if !cc.knownPaths[path].mutable { return fmt.Errorf("cannot write file which is not mutable: %s", path) @@ -90,7 +99,7 @@ func Run(options *RunOptions) error { targetDir = filepath.Join(dir, targetDir) } - pkgArchive, err := selectPkgArchives(options.Archives, options.Selection) + pkgSources, err := resolvePkgSources(options.Archives, options.Selection) if err != nil { return err } @@ -108,12 +117,12 @@ func Run(options *RunOptions) error { extractPackage = make(map[string][]tarball.ExtractInfo) extract[slice.Package] = extractPackage } - arch := pkgArchive[slice.Package].Options().Arch + src := pkgSources[slice.Package] for targetPath, pathInfo := range slice.Contents { if targetPath == "" { continue } - if len(pathInfo.Arch) > 0 && !slices.Contains(pathInfo.Arch, arch) { + if len(pathInfo.Arch) > 0 && !slices.Contains(pathInfo.Arch, src.arch) { continue } if preferredPkg, ok := prefers[targetPath]; ok && preferredPkg.Name != slice.Package { @@ -153,7 +162,8 @@ func Run(options *RunOptions) error { continue } pkg := options.Selection.Release.Packages[slice.Package] - reader, info, err := pkgArchive[slice.Package].Fetch(pkg.RealName) + src := pkgSources[pkg] + reader, info, err := src.fetch() if err != nil { return err } @@ -270,9 +280,9 @@ func Run(options *RunOptions) error { // them to the appropriate slices. relPaths := map[string][]*setup.Slice{} for _, slice := range options.Selection.Slices { - arch := pkgArchive[slice.Package].Options().Arch + src := pkgSources[slice.Package] for relPath, pathInfo := range slice.Contents { - if len(pathInfo.Arch) > 0 && !slices.Contains(pathInfo.Arch, arch) { + if len(pathInfo.Arch) > 0 && !slices.Contains(pathInfo.Arch, src.arch) { continue } if pathInfo.Kind == setup.CopyPath || pathInfo.Kind == setup.GlobPath || @@ -490,10 +500,13 @@ func createFile(targetDir, relPath string, pathInfo setup.PathInfo) (*fsutil.Ent }) } -// selectPkgArchives selects the highest priority archive containing the package -// unless a particular archive is pinned within the slice definition file. It -// returns a map of archives indexed by package names. -func selectPkgArchives(archives map[string]archive.Archive, selection *setup.Selection) (map[string]archive.Archive, error) { +// resolvePkgSources determines the source for each package in the selection. +// For archive packages it selects the highest priority archive containing the +// package unless a particular archive is pinned within the slice definition +// file. For store packages it records a fetch function that returns an error +// until store support is implemented. It returns a map of pkgSource indexed by +// package names. +func resolvePkgSources(archives map[string]archive.Archive, selection *setup.Selection) (map[string]pkgSource, error) { sortedArchives := make([]*setup.Archive, 0, len(selection.Release.Archives)) for _, archive := range selection.Release.Archives { if archive.Priority < 0 { @@ -507,12 +520,20 @@ func selectPkgArchives(archives map[string]archive.Archive, selection *setup.Sel return b.Priority - a.Priority }) - pkgArchive := make(map[string]archive.Archive) + pkgSources := make(map[string]pkgSource) for _, s := range selection.Slices { - if _, ok := pkgArchive[s.Package]; ok { + if _, ok := pkgSources[s.Package]; ok { continue } pkg := selection.Release.Packages[s.Package] + if pkg.Store != "" { + pkgSources[pkg.Name] = pkgSource{ + fetch: func() (io.ReadSeekCloser, *archive.PackageInfo, error) { + return nil, nil, fmt.Errorf("cannot fetch package %q from store: store packages are not yet supported", pkg.Name) + }, + } + continue + } if pkg.Store != "" { return nil, fmt.Errorf("cannot fetch package %q from store %q: not implemented", pkg.Name, pkg.Store) @@ -538,7 +559,32 @@ func selectPkgArchives(archives map[string]archive.Archive, selection *setup.Sel if chosen == nil { return nil, fmt.Errorf("cannot find package %q in archive(s)", pkg.RealName) } - pkgArchive[pkg.Name] = chosen + chosenArchive := chosen + pkgSources[pkg.Name] = pkgSource{ + arch: chosen.Options().Arch, + fetch: func() (io.ReadSeekCloser, *archive.PackageInfo, error) { + return chosenArchive.Fetch(pkg.RealName) + }, + } } - return pkgArchive, nil + + // Until a store is implemented as a package source there is no proper way to + // determine the architecture for store packages. + // So relying on the fact that all packages in a selection share the same architecture, + // we can borrow it from any archive package that was already resolved. + var arch string + for _, src := range pkgSources { + if src.arch != "" { + arch = src.arch + break + } + } + for name, src := range pkgSources { + if src.arch == "" { + src.arch = arch + pkgSources[name] = src + } + } + + return pkgSources, nil } diff --git a/internal/slicer/slicer_test.go b/internal/slicer/slicer_test.go index d6ef9ca0..4e1e39d1 100644 --- a/internal/slicer/slicer_test.go +++ b/internal/slicer/slicer_test.go @@ -1994,6 +1994,30 @@ var slicerTests = []slicerTest{{ `, }, error: `cannot fetch package "bin-curl" from store "bin": not implemented`, +}, { + summary: "Store package fails as it is not yet supported", + slices: []setup.SliceKey{{"test-package", "myslice"}, {"bin-store-pkg", "myslice"}}, + arch: "amd64", + release: map[string]string{ + "chisel.yaml": testutil.DefaultChiselYamlWithStores, + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /dir/file: + `, + "slices/mydir/store-pkg.yaml": ` + package: store-pkg + store: bin + default-track: stable + slices: + myslice: + contents: + /dir/store-file: + `, + }, + error: `cannot fetch package "bin-store-pkg" from store: store packages are not yet supported`, }} func (s *S) TestRun(c *C) { From f79521ea21a040baef490ea9987aab08d0d1d795 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Fri, 10 Jul 2026 09:59:59 +0200 Subject: [PATCH 2/7] fix: error fetching from the store Signed-off-by: Paul Mars --- internal/slicer/slicer.go | 8 ++------ internal/slicer/slicer_test.go | 20 ++------------------ 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/internal/slicer/slicer.go b/internal/slicer/slicer.go index a2ab6032..83ff84a7 100644 --- a/internal/slicer/slicer.go +++ b/internal/slicer/slicer.go @@ -162,7 +162,7 @@ func Run(options *RunOptions) error { continue } pkg := options.Selection.Release.Packages[slice.Package] - src := pkgSources[pkg] + src := pkgSources[pkg.Name] reader, info, err := src.fetch() if err != nil { return err @@ -529,16 +529,12 @@ func resolvePkgSources(archives map[string]archive.Archive, selection *setup.Sel if pkg.Store != "" { pkgSources[pkg.Name] = pkgSource{ fetch: func() (io.ReadSeekCloser, *archive.PackageInfo, error) { - return nil, nil, fmt.Errorf("cannot fetch package %q from store: store packages are not yet supported", pkg.Name) + return nil, nil, fmt.Errorf("cannot fetch package %q from store %q: not implemented", pkg.Name, pkg.Store) }, } continue } - if pkg.Store != "" { - return nil, fmt.Errorf("cannot fetch package %q from store %q: not implemented", pkg.Name, pkg.Store) - } - var candidates []*setup.Archive if pkg.Archive == "" { // If the package has not pinned any archive, choose the highest diff --git a/internal/slicer/slicer_test.go b/internal/slicer/slicer_test.go index 4e1e39d1..af107779 100644 --- a/internal/slicer/slicer_test.go +++ b/internal/slicer/slicer_test.go @@ -1978,23 +1978,7 @@ var slicerTests = []slicerTest{{ manifestPaths: map[string]string{ "/dir/file": "file 0644 cc55e2ec {test-package_third}", }, -}, { - summary: "Store package is not yet implemented", - slices: []setup.SliceKey{{"bin-curl", "bin"}}, - release: map[string]string{ - "chisel.yaml": testutil.DefaultChiselYamlWithStores, - "slices/curl.yaml": ` - package: curl - store: bin - default-track: latest - slices: - bin: - contents: - /usr/bin/curl: - `, - }, - error: `cannot fetch package "bin-curl" from store "bin": not implemented`, -}, { +},{ summary: "Store package fails as it is not yet supported", slices: []setup.SliceKey{{"test-package", "myslice"}, {"bin-store-pkg", "myslice"}}, arch: "amd64", @@ -2017,7 +2001,7 @@ var slicerTests = []slicerTest{{ /dir/store-file: `, }, - error: `cannot fetch package "bin-store-pkg" from store: store packages are not yet supported`, + error: `cannot fetch package "bin-store-pkg" from store "bin": not implemented`, }} func (s *S) TestRun(c *C) { From a2e22f27ee82997cf6305825a60b330b68f2612a Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Fri, 10 Jul 2026 10:37:57 +0200 Subject: [PATCH 3/7] fix: cleaning Signed-off-by: Paul Mars --- internal/slicer/slicer.go | 14 ++++++-------- internal/slicer/slicer_test.go | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/internal/slicer/slicer.go b/internal/slicer/slicer.go index 83ff84a7..ab510628 100644 --- a/internal/slicer/slicer.go +++ b/internal/slicer/slicer.go @@ -117,12 +117,12 @@ func Run(options *RunOptions) error { extractPackage = make(map[string][]tarball.ExtractInfo) extract[slice.Package] = extractPackage } - src := pkgSources[slice.Package] + arch := pkgSources[slice.Package].arch for targetPath, pathInfo := range slice.Contents { if targetPath == "" { continue } - if len(pathInfo.Arch) > 0 && !slices.Contains(pathInfo.Arch, src.arch) { + if len(pathInfo.Arch) > 0 && !slices.Contains(pathInfo.Arch, arch) { continue } if preferredPkg, ok := prefers[targetPath]; ok && preferredPkg.Name != slice.Package { @@ -162,8 +162,7 @@ func Run(options *RunOptions) error { continue } pkg := options.Selection.Release.Packages[slice.Package] - src := pkgSources[pkg.Name] - reader, info, err := src.fetch() + reader, info, err := pkgSources[pkg.Name].fetch() if err != nil { return err } @@ -280,9 +279,9 @@ func Run(options *RunOptions) error { // them to the appropriate slices. relPaths := map[string][]*setup.Slice{} for _, slice := range options.Selection.Slices { - src := pkgSources[slice.Package] + arch := pkgSources[slice.Package].arch for relPath, pathInfo := range slice.Contents { - if len(pathInfo.Arch) > 0 && !slices.Contains(pathInfo.Arch, src.arch) { + if len(pathInfo.Arch) > 0 && !slices.Contains(pathInfo.Arch, arch) { continue } if pathInfo.Kind == setup.CopyPath || pathInfo.Kind == setup.GlobPath || @@ -555,11 +554,10 @@ func resolvePkgSources(archives map[string]archive.Archive, selection *setup.Sel if chosen == nil { return nil, fmt.Errorf("cannot find package %q in archive(s)", pkg.RealName) } - chosenArchive := chosen pkgSources[pkg.Name] = pkgSource{ arch: chosen.Options().Arch, fetch: func() (io.ReadSeekCloser, *archive.PackageInfo, error) { - return chosenArchive.Fetch(pkg.RealName) + return chosen.Fetch(pkg.RealName) }, } } diff --git a/internal/slicer/slicer_test.go b/internal/slicer/slicer_test.go index af107779..eaedfa28 100644 --- a/internal/slicer/slicer_test.go +++ b/internal/slicer/slicer_test.go @@ -1978,8 +1978,8 @@ var slicerTests = []slicerTest{{ manifestPaths: map[string]string{ "/dir/file": "file 0644 cc55e2ec {test-package_third}", }, -},{ - summary: "Store package fails as it is not yet supported", +}, { + summary: "Store package fails as not yet implemented", slices: []setup.SliceKey{{"test-package", "myslice"}, {"bin-store-pkg", "myslice"}}, arch: "amd64", release: map[string]string{ From 2eb85d0aecd46e0a7d1fb2d3f8a378f9c4db41eb Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Wed, 15 Jul 2026 15:52:47 +0200 Subject: [PATCH 4/7] feat: remove arch setting until store implementation Signed-off-by: Paul Mars --- internal/slicer/slicer.go | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/internal/slicer/slicer.go b/internal/slicer/slicer.go index ab510628..fcc16d67 100644 --- a/internal/slicer/slicer.go +++ b/internal/slicer/slicer.go @@ -527,6 +527,7 @@ func resolvePkgSources(archives map[string]archive.Archive, selection *setup.Sel pkg := selection.Release.Packages[s.Package] if pkg.Store != "" { pkgSources[pkg.Name] = pkgSource{ + // TODO: set the arch when implementing fetching from the store. fetch: func() (io.ReadSeekCloser, *archive.PackageInfo, error) { return nil, nil, fmt.Errorf("cannot fetch package %q from store %q: not implemented", pkg.Name, pkg.Store) }, @@ -561,24 +562,5 @@ func resolvePkgSources(archives map[string]archive.Archive, selection *setup.Sel }, } } - - // Until a store is implemented as a package source there is no proper way to - // determine the architecture for store packages. - // So relying on the fact that all packages in a selection share the same architecture, - // we can borrow it from any archive package that was already resolved. - var arch string - for _, src := range pkgSources { - if src.arch != "" { - arch = src.arch - break - } - } - for name, src := range pkgSources { - if src.arch == "" { - src.arch = arch - pkgSources[name] = src - } - } - return pkgSources, nil } From 3004a6eff32ba8b8dd5c61177cdf450fc85a024e Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Wed, 15 Jul 2026 15:57:02 +0200 Subject: [PATCH 5/7] test: refine Signed-off-by: Paul Mars --- internal/slicer/slicer_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/slicer/slicer_test.go b/internal/slicer/slicer_test.go index eaedfa28..954602de 100644 --- a/internal/slicer/slicer_test.go +++ b/internal/slicer/slicer_test.go @@ -1979,7 +1979,7 @@ var slicerTests = []slicerTest{{ "/dir/file": "file 0644 cc55e2ec {test-package_third}", }, }, { - summary: "Store package fails as not yet implemented", + summary: "Store package fetching not yet implemented", slices: []setup.SliceKey{{"test-package", "myslice"}, {"bin-store-pkg", "myslice"}}, arch: "amd64", release: map[string]string{ @@ -1994,7 +1994,7 @@ var slicerTests = []slicerTest{{ "slices/mydir/store-pkg.yaml": ` package: store-pkg store: bin - default-track: stable + default-track: 3.1 slices: myslice: contents: From ed0e070951ca544e1a2744b498b487154429ea16 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Tue, 18 Aug 2026 11:54:44 +0200 Subject: [PATCH 6/7] refactor: adopt alternative approach Add a source package with a Source interface to abstract over the archive and store interface. This is needed because fetching from the store requires the architecture, track and risk. --- internal/slicer/slicer.go | 84 +--------- internal/source/source.go | 112 ++++++++++++++ internal/source/source_test.go | 275 +++++++++++++++++++++++++++++++++ internal/source/suite_test.go | 13 ++ internal/testutil/defaults.go | 23 +++ 5 files changed, 428 insertions(+), 79 deletions(-) create mode 100644 internal/source/source.go create mode 100644 internal/source/source_test.go create mode 100644 internal/source/suite_test.go diff --git a/internal/slicer/slicer.go b/internal/slicer/slicer.go index fcc16d67..2f6a18bb 100644 --- a/internal/slicer/slicer.go +++ b/internal/slicer/slicer.go @@ -20,6 +20,7 @@ import ( "github.com/canonical/chisel/internal/manifestutil" "github.com/canonical/chisel/internal/scripts" "github.com/canonical/chisel/internal/setup" + "github.com/canonical/chisel/internal/source" "github.com/canonical/chisel/internal/tarball" ) @@ -41,15 +42,6 @@ type contentChecker struct { knownPaths map[string]pathData } -// pkgSource holds the resolved source for a package: its architecture and a -// fetch function returning the package reader and metadata. The fetch -// function is bound at resolution time, so callers are agnostic to whether -// the package comes from an archive or a store. -type pkgSource struct { - arch string - fetch func() (io.ReadSeekCloser, *archive.PackageInfo, error) -} - func (cc *contentChecker) checkMutable(path string) error { if !cc.knownPaths[path].mutable { return fmt.Errorf("cannot write file which is not mutable: %s", path) @@ -99,7 +91,7 @@ func Run(options *RunOptions) error { targetDir = filepath.Join(dir, targetDir) } - pkgSources, err := resolvePkgSources(options.Archives, options.Selection) + pkgSources, err := source.Resolve(options.Archives, options.Selection) if err != nil { return err } @@ -117,7 +109,7 @@ func Run(options *RunOptions) error { extractPackage = make(map[string][]tarball.ExtractInfo) extract[slice.Package] = extractPackage } - arch := pkgSources[slice.Package].arch + arch := pkgSources[slice.Package].Arch() for targetPath, pathInfo := range slice.Contents { if targetPath == "" { continue @@ -162,7 +154,7 @@ func Run(options *RunOptions) error { continue } pkg := options.Selection.Release.Packages[slice.Package] - reader, info, err := pkgSources[pkg.Name].fetch() + reader, info, err := pkgSources[pkg.Name].Fetch() if err != nil { return err } @@ -279,7 +271,7 @@ func Run(options *RunOptions) error { // them to the appropriate slices. relPaths := map[string][]*setup.Slice{} for _, slice := range options.Selection.Slices { - arch := pkgSources[slice.Package].arch + arch := pkgSources[slice.Package].Arch() for relPath, pathInfo := range slice.Contents { if len(pathInfo.Arch) > 0 && !slices.Contains(pathInfo.Arch, arch) { continue @@ -498,69 +490,3 @@ func createFile(targetDir, relPath string, pathInfo setup.PathInfo) (*fsutil.Ent MakeParents: true, }) } - -// resolvePkgSources determines the source for each package in the selection. -// For archive packages it selects the highest priority archive containing the -// package unless a particular archive is pinned within the slice definition -// file. For store packages it records a fetch function that returns an error -// until store support is implemented. It returns a map of pkgSource indexed by -// package names. -func resolvePkgSources(archives map[string]archive.Archive, selection *setup.Selection) (map[string]pkgSource, error) { - sortedArchives := make([]*setup.Archive, 0, len(selection.Release.Archives)) - for _, archive := range selection.Release.Archives { - if archive.Priority < 0 { - // Ignore negative priority archives unless a package specifically - // asks for it with the "archive" field. - continue - } - sortedArchives = append(sortedArchives, archive) - } - slices.SortFunc(sortedArchives, func(a, b *setup.Archive) int { - return b.Priority - a.Priority - }) - - pkgSources := make(map[string]pkgSource) - for _, s := range selection.Slices { - if _, ok := pkgSources[s.Package]; ok { - continue - } - pkg := selection.Release.Packages[s.Package] - if pkg.Store != "" { - pkgSources[pkg.Name] = pkgSource{ - // TODO: set the arch when implementing fetching from the store. - fetch: func() (io.ReadSeekCloser, *archive.PackageInfo, error) { - return nil, nil, fmt.Errorf("cannot fetch package %q from store %q: not implemented", pkg.Name, pkg.Store) - }, - } - continue - } - - var candidates []*setup.Archive - if pkg.Archive == "" { - // If the package has not pinned any archive, choose the highest - // priority archive in which the package exists. - candidates = sortedArchives - } else { - candidates = []*setup.Archive{selection.Release.Archives[pkg.Archive]} - } - - var chosen archive.Archive - for _, archiveInfo := range candidates { - archive := archives[archiveInfo.Name] - if archive != nil && archive.Exists(pkg.RealName) { - chosen = archive - break - } - } - if chosen == nil { - return nil, fmt.Errorf("cannot find package %q in archive(s)", pkg.RealName) - } - pkgSources[pkg.Name] = pkgSource{ - arch: chosen.Options().Arch, - fetch: func() (io.ReadSeekCloser, *archive.PackageInfo, error) { - return chosen.Fetch(pkg.RealName) - }, - } - } - return pkgSources, nil -} diff --git a/internal/source/source.go b/internal/source/source.go new file mode 100644 index 00000000..46df8e4a --- /dev/null +++ b/internal/source/source.go @@ -0,0 +1,112 @@ +package source + +import ( + "fmt" + "io" + "slices" + + "github.com/canonical/chisel/internal/archive" + "github.com/canonical/chisel/internal/setup" +) + +// Source is a resolved package source, abstracting over archives and stores. +type Source interface { + Arch() string + Fetch() (io.ReadSeekCloser, *archive.PackageInfo, error) +} + +// archiveSource adapts an archive.Archive to the Source interface for a +// specific package. +type archiveSource struct { + archive archive.Archive + name string +} + +func (a *archiveSource) Arch() string { + return a.archive.Options().Arch +} + +func (a *archiveSource) Fetch() (io.ReadSeekCloser, *archive.PackageInfo, error) { + return a.archive.Fetch(a.name) +} + +// storeSource adapts a store to the Source interface for a specific package. +type storeSource struct { + arch string + name string + store string + track string + risk string +} + +func (s *storeSource) Arch() string { + return s.arch +} + +func (s *storeSource) Fetch() (io.ReadSeekCloser, *archive.PackageInfo, error) { + return nil, nil, fmt.Errorf("cannot fetch package %q from store %q: not implemented", s.name, s.store) +} + +// Resolve determines the source for each package in the selection. +// For archive packages it selects the highest priority archive containing the +// package unless a particular archive is pinned within the slice definition +// file. For store packages it records a store source. It returns a map of +// Source indexed by package names. +func Resolve(archives map[string]archive.Archive, selection *setup.Selection) (map[string]Source, error) { + sortedArchives := make([]*setup.Archive, 0, len(selection.Release.Archives)) + for _, archive := range selection.Release.Archives { + if archive.Priority < 0 { + // Ignore negative priority archives unless a package specifically + // asks for it with the "archive" field. + continue + } + sortedArchives = append(sortedArchives, archive) + } + slices.SortFunc(sortedArchives, func(a, b *setup.Archive) int { + return b.Priority - a.Priority + }) + + sources := make(map[string]Source) + for _, s := range selection.Slices { + if _, ok := sources[s.Package]; ok { + continue + } + pkg := selection.Release.Packages[s.Package] + if pkg.Store != "" { + sources[pkg.Name] = &storeSource{ + name: pkg.Name, + store: pkg.Store, + // TODO: populate arch, track and risk when implementing + // fetching from the store. + } + continue + } + + var candidates []*setup.Archive + if pkg.Archive == "" { + // If the package has not pinned any archive, choose the highest + // priority archive in which the package exists. + candidates = sortedArchives + } else { + candidates = []*setup.Archive{selection.Release.Archives[pkg.Archive]} + } + + var chosen archive.Archive + for _, archiveInfo := range candidates { + archive := archives[archiveInfo.Name] + if archive != nil && archive.Exists(pkg.RealName) { + chosen = archive + break + } + } + if chosen == nil { + return nil, fmt.Errorf("cannot find package %q in archive(s)", pkg.RealName) + } + sources[pkg.Name] = &archiveSource{ + archive: chosen, + name: pkg.RealName, + } + } + + return sources, nil +} diff --git a/internal/source/source_test.go b/internal/source/source_test.go new file mode 100644 index 00000000..edf3ed92 --- /dev/null +++ b/internal/source/source_test.go @@ -0,0 +1,275 @@ +package source_test + +import ( + "os" + "path/filepath" + "slices" + + . "gopkg.in/check.v1" + + "github.com/canonical/chisel/internal/archive" + "github.com/canonical/chisel/internal/setup" + "github.com/canonical/chisel/internal/source" + "github.com/canonical/chisel/internal/testutil" +) + +var testKey = testutil.PGPKeys["key1"] + +type sourceTest struct { + summary string + arch string + release map[string]string + pkgs []*testutil.TestPackage + slices []setup.SliceKey + archs map[string]string + fetchErrors map[string]string + error string +} + +var sourceTests = []sourceTest{{ + summary: "Highest priority archive is selected", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Hash: "h1", + Version: "v1", + Arch: "amd64", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Reg(0644, "./file", "from foo"), + }), + Archives: []string{"foo"}, + }}, + arch: "amd64", + release: map[string]string{ + "chisel.yaml": testutil.DefaultChiselYamlTwoArchives, + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /file: + `, + }, + archs: map[string]string{"test-package": "amd64"}, +}, { + summary: "Pinned archive bypasses higher priority", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Hash: "h1", + Version: "v1", + Arch: "amd64", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Reg(0644, "./file", "from foo"), + }), + Archives: []string{"foo"}, + }, { + Name: "test-package", + Hash: "h2", + Version: "v2", + Arch: "amd64", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Reg(0644, "./file", "from bar"), + }), + Archives: []string{"bar"}, + }}, + arch: "amd64", + release: map[string]string{ + "chisel.yaml": testutil.DefaultChiselYamlTwoArchives, + "slices/mydir/test-package.yaml": ` + package: test-package + archive: bar + slices: + myslice: + contents: + /file: + `, + }, + archs: map[string]string{"test-package": "amd64"}, +}, { + summary: "Pinned archive not available fails", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Hash: "h1", + Version: "v1", + Arch: "amd64", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Reg(0644, "./file", "from foo"), + }), + Archives: []string{"foo"}, + }}, + arch: "amd64", + release: map[string]string{ + "chisel.yaml": testutil.DefaultChiselYamlTwoArchives, + "slices/mydir/test-package.yaml": ` + package: test-package + archive: bar + slices: + myslice: + contents: + /file: + `, + }, + error: `cannot find package "test-package" in archive\(s\)`, +}, { + summary: "No archives have the package", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{}, + arch: "amd64", + release: map[string]string{ + "chisel.yaml": testutil.DefaultChiselYamlTwoArchives, + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /file: + `, + }, + error: `cannot find package "test-package" in archive\(s\)`, +}, { + summary: "Negative priority archives are ignored when not explicitly pinned", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Reg(0644, "./file", "from foo"), + }), + Archives: []string{"foo"}, + }}, + arch: "amd64", + release: map[string]string{ + "chisel.yaml": ` + format: v1 + maintenance: + standard: 2025-01-01 + end-of-life: 2100-01-01 + archives: + foo: + version: 22.04 + components: [main, universe] + suites: [jammy] + priority: -20 + public-keys: [test-key] + public-keys: + test-key: + id: ` + testKey.ID + ` + armor: |` + "\n" + testutil.PrefixEachLine(testKey.PubKeyArmor, "\t\t\t\t\t\t") + ` + `, + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /file: + `, + }, + error: `cannot find package "test-package" in archive\(s\)`, +}, { + summary: "Store package fetching not yet implemented", + slices: []setup.SliceKey{{"test-package", "myslice"}, {"bin-store-pkg", "myslice"}}, + arch: "amd64", + release: map[string]string{ + "chisel.yaml": testutil.DefaultChiselYamlWithStores, + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /dir/file: + `, + "slices/mydir/store-pkg.yaml": ` + package: store-pkg + store: bin + default-track: 3.1 + slices: + myslice: + contents: + /dir/store-file: + `, + }, + fetchErrors: map[string]string{ + "bin-store-pkg": `cannot fetch package "bin-store-pkg" from store "bin": not implemented`, + }, +}} + +func (s *S) TestResolve(c *C) { + for _, test := range sourceTests { + c.Logf("Summary: %s", test.summary) + + if _, ok := test.release["chisel.yaml"]; !ok { + test.release["chisel.yaml"] = testutil.DefaultChiselYaml + } + if test.pkgs == nil { + test.pkgs = []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.PackageData["test-package"], + }} + } + for _, pkg := range test.pkgs { + if pkg.Arch == "" { + pkg.Arch = "arch" + } + if pkg.Hash == "" { + pkg.Hash = "hash" + } + if pkg.Version == "" { + pkg.Version = "version" + } + } + + releaseDir := c.MkDir() + for path, data := range test.release { + fpath := filepath.Join(releaseDir, path) + err := os.MkdirAll(filepath.Dir(fpath), 0755) + c.Assert(err, IsNil) + err = os.WriteFile(fpath, testutil.Reindent(data), 0644) + c.Assert(err, IsNil) + } + + release, err := setup.ReadRelease(releaseDir) + c.Assert(err, IsNil) + + selection, err := setup.Select(release, test.slices, test.arch) + c.Assert(err, IsNil) + + archives := map[string]archive.Archive{} + for name, setupArchive := range release.Archives { + pkgs := make(map[string]*testutil.TestPackage) + for _, pkg := range test.pkgs { + if len(pkg.Archives) == 0 || slices.Contains(pkg.Archives, name) { + pkgs[pkg.Name] = pkg + } + } + archive := &testutil.TestArchive{ + Opts: archive.Options{ + Label: setupArchive.Name, + Version: setupArchive.Version, + Suites: setupArchive.Suites, + Components: setupArchive.Components, + Pro: setupArchive.Pro, + Arch: test.arch, + }, + Packages: pkgs, + } + archives[name] = archive + } + + sources, err := source.Resolve(archives, selection) + if test.error != "" { + c.Assert(err, ErrorMatches, test.error) + continue + } + c.Assert(err, IsNil) + + for pkgName, arch := range test.archs { + c.Assert(sources[pkgName].Arch(), Equals, arch) + } + + for pkgName, fetchErr := range test.fetchErrors { + _, _, err := sources[pkgName].Fetch() + c.Assert(err, ErrorMatches, fetchErr) + } + } +} diff --git a/internal/source/suite_test.go b/internal/source/suite_test.go new file mode 100644 index 00000000..f720627f --- /dev/null +++ b/internal/source/suite_test.go @@ -0,0 +1,13 @@ +package source_test + +import ( + "testing" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type S struct{} + +var _ = Suite(&S{}) diff --git a/internal/testutil/defaults.go b/internal/testutil/defaults.go index 046d4636..2b4b318c 100644 --- a/internal/testutil/defaults.go +++ b/internal/testutil/defaults.go @@ -27,3 +27,26 @@ var DefaultChiselYamlWithStores = strings.ReplaceAll(DefaultChiselYaml, "format: version: 26.10 default-prefix: "bin-" ` + +var DefaultChiselYamlTwoArchives = ` + format: v1 + maintenance: + standard: 2025-01-01 + end-of-life: 2100-01-01 + archives: + foo: + version: 22.04 + components: [main, universe] + suites: [jammy] + priority: 20 + public-keys: [test-key] + bar: + version: 22.04 + components: [main] + suites: [jammy] + priority: 10 + public-keys: [test-key] + public-keys: + test-key: + id: ` + testKey.ID + ` + armor: |` + "\n" + PrefixEachLine(testKey.PubKeyArmor, "\t\t\t\t\t\t") From 268501e7e436b1ba2a3dd937c423d3c790bc8548 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Wed, 19 Aug 2026 08:25:33 +0200 Subject: [PATCH 7/7] style: remove unused track and risk --- internal/source/source.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/source/source.go b/internal/source/source.go index 46df8e4a..217b5db6 100644 --- a/internal/source/source.go +++ b/internal/source/source.go @@ -35,8 +35,6 @@ type storeSource struct { arch string name string store string - track string - risk string } func (s *storeSource) Arch() string {