Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 5 additions & 57 deletions internal/slicer/slicer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -90,7 +91,7 @@ func Run(options *RunOptions) error {
targetDir = filepath.Join(dir, targetDir)
}

pkgArchive, err := selectPkgArchives(options.Archives, options.Selection)
pkgSources, fetchOpts, err := source.Resolve(options.Archives, options.Selection)
if err != nil {
return err
}
Expand All @@ -108,7 +109,7 @@ func Run(options *RunOptions) error {
extractPackage = make(map[string][]tarball.ExtractInfo)
extract[slice.Package] = extractPackage
}
arch := pkgArchive[slice.Package].Options().Arch
arch := pkgSources[slice.Package].Arch()
for targetPath, pathInfo := range slice.Contents {
if targetPath == "" {
continue
Expand Down Expand Up @@ -153,7 +154,7 @@ func Run(options *RunOptions) error {
continue
}
pkg := options.Selection.Release.Packages[slice.Package]
reader, info, err := pkgArchive[slice.Package].Fetch(pkg.RealName)
reader, info, err := pkgSources[pkg.Name].Fetch(fetchOpts[pkg.Name])
if err != nil {
return err
}
Expand Down Expand Up @@ -270,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 := pkgArchive[slice.Package].Options().Arch
arch := pkgSources[slice.Package].Arch()
for relPath, pathInfo := range slice.Contents {
if len(pathInfo.Arch) > 0 && !slices.Contains(pathInfo.Arch, arch) {
continue
Expand Down Expand Up @@ -489,56 +490,3 @@ func createFile(targetDir, relPath string, pathInfo setup.PathInfo) (*fsutil.Ent
MakeParents: true,
})
}

// 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) {
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
})

pkgArchive := make(map[string]archive.Archive)
for _, s := range selection.Slices {
if _, ok := pkgArchive[s.Package]; ok {
continue
}
pkg := selection.Release.Packages[s.Package]

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
// 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)
}
pkgArchive[pkg.Name] = chosen
}
return pkgArchive, nil
}
24 changes: 16 additions & 8 deletions internal/slicer/slicer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1979,21 +1979,29 @@ var slicerTests = []slicerTest{{
"/dir/file": "file 0644 cc55e2ec {test-package_third}",
},
}, {
summary: "Store package is not yet implemented",
slices: []setup.SliceKey{{"bin-curl", "bin"}},
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/curl.yaml": `
package: curl
"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: latest
default-track: 3.1
slices:
bin:
myslice:
contents:
/usr/bin/curl:
/dir/store-file:
`,
},
error: `cannot fetch package "bin-curl" from store "bin": not implemented`,
error: `cannot fetch package "bin-store-pkg" from store "bin": not implemented`,
}}

func (s *S) TestRun(c *C) {
Expand Down
155 changes: 155 additions & 0 deletions internal/source/source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package source

import (
"fmt"
"io"
"slices"

"github.com/canonical/chisel/internal/archive"
"github.com/canonical/chisel/internal/setup"
)

// Source is a package source that can be fetched. It is implemented by
// archives and stores.
type Source interface {
Arch() string
Fetch(opts FetchOption) (io.ReadSeekCloser, *archive.PackageInfo, error)
}

// FetchOption carries source-specific arguments for fetching a package.
// Concrete types are defined in this package to keep the interface sealed.
type FetchOption interface {
isFetchOption()
}

// ArchiveFetch carries the arguments for fetching a package from an archive.
type ArchiveFetch struct {
Name string
}

func (ArchiveFetch) isFetchOption() {}

// StoreFetch carries the arguments for fetching a package from a store.
type StoreFetch struct {
Name string
Store string
Track string
}

func (StoreFetch) isFetchOption() {}

// archiveSource adapts an archive.Archive to the Source interface.
type archiveSource struct {
archive archive.Archive
}

func (a *archiveSource) Arch() string {
return a.archive.Options().Arch
}

func (a *archiveSource) Fetch(opts FetchOption) (io.ReadSeekCloser, *archive.PackageInfo, error) {
archiveOpts, ok := opts.(ArchiveFetch)
if !ok {
return nil, nil, fmt.Errorf("internal error: invalid fetch option %T for archive source", opts)
}
return a.archive.Fetch(archiveOpts.Name)
}

// storeSource is a Source backed by a store. Store support is not yet
// implemented, so Fetch always returns an error.
type storeSource struct {
arch string
}

func (s *storeSource) Arch() string {
return s.arch
}

func (s *storeSource) Fetch(opts FetchOption) (io.ReadSeekCloser, *archive.PackageInfo, error) {
storeOpts, ok := opts.(StoreFetch)
if !ok {
return nil, nil, fmt.Errorf("internal error: invalid fetch option %T for store source", opts)
}
return nil, nil, fmt.Errorf("cannot fetch package %q from store %q: not implemented", storeOpts.Name, storeOpts.Store)
}

// Resolve determines the source and fetch options 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. It returns a map of Source and a map of FetchOption,
// both indexed by package name.
func Resolve(archives map[string]archive.Archive, selection *setup.Selection) (map[string]Source, map[string]FetchOption, 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)
fetchOpts := make(map[string]FetchOption)
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{}
fetchOpts[pkg.Name] = StoreFetch{
Name: pkg.Name,
Store: pkg.Store,
Track: pkg.DefaultTrack,
}
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, nil, fmt.Errorf("cannot find package %q in archive(s)", pkg.RealName)
}
sources[pkg.Name] = &archiveSource{archive: chosen}
fetchOpts[pkg.Name] = ArchiveFetch{Name: pkg.RealName}
}

// 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 sources {
if _, ok := src.(*storeSource); !ok {
arch = src.Arch()
break
}
}
for name, src := range sources {
if ss, ok := src.(*storeSource); ok {
ss.arch = arch
sources[name] = src
}
}

return sources, fetchOpts, nil
}
Loading