diff --git a/cmd/chisel/cmd_cut.go b/cmd/chisel/cmd_cut.go index 35c81a79..41c799b6 100644 --- a/cmd/chisel/cmd_cut.go +++ b/cmd/chisel/cmd_cut.go @@ -20,6 +20,14 @@ to create a new filesystem tree in the root location. By default it fetches the slices for the same Ubuntu version as the current host, unless the --release flag is used. + +Slices are named _. For packages coming from a store, a +channel may be appended to select which one to fetch, as in +mybin_myslice@2.0/edge. A channel with no risk, such as @2.0, uses the +stable risk. + +Slices of the same package must all agree on the channel. When it is +omitted, the default track of the package is used. ` var cutDescs = map[string]string{ @@ -49,13 +57,13 @@ func (cmd *cmdCut) Execute(args []string) error { return ErrExtraArgs } - sliceKeys := make([]setup.SliceKey, len(cmd.Positional.SliceRefs)) + sliceRefs := make([]setup.SliceRef, len(cmd.Positional.SliceRefs)) for i, sliceRef := range cmd.Positional.SliceRefs { - sliceKey, err := setup.ParseSliceKey(sliceRef) + ref, err := setup.ParseSliceRef(sliceRef) if err != nil { return err } - sliceKeys[i] = sliceKey + sliceRefs[i] = ref } release, err := obtainRelease(cmd.Release) @@ -73,7 +81,7 @@ func (cmd *cmdCut) Execute(args []string) error { } } - selection, err := setup.Select(release, sliceKeys, cmd.Arch) + selection, err := setup.Select(release, sliceRefs, cmd.Arch) if err != nil { return err } diff --git a/cmd/chisel/cmd_find.go b/cmd/chisel/cmd_find.go index 1a208d4b..d7b9dfdb 100644 --- a/cmd/chisel/cmd_find.go +++ b/cmd/chisel/cmd_find.go @@ -89,6 +89,11 @@ func match(slice *setup.Slice, query string) bool { // findSlices returns slices from the provided release that match all of the // query strings (AND). func findSlices(release *setup.Release, query []string) (slices []*setup.Slice, err error) { + for _, term := range query { + if strings.Contains(term, "@") { + return nil, fmt.Errorf("channel is unsupported for find: %q", term) + } + } slices = []*setup.Slice{} for _, pkg := range release.Packages { for _, slice := range pkg.Slices { diff --git a/cmd/chisel/cmd_find_test.go b/cmd/chisel/cmd_find_test.go index a2a68c2a..0d2c117b 100644 --- a/cmd/chisel/cmd_find_test.go +++ b/cmd/chisel/cmd_find_test.go @@ -14,6 +14,7 @@ type findTest struct { release *setup.Release query []string result []*setup.Slice + err string } func makeSamplePackage(pkg string, slices []string) *setup.Package { @@ -123,6 +124,11 @@ var findTests = []findTest{{ release: sampleRelease, query: []string{"python", "slice"}, result: []*setup.Slice{}, +}, { + summary: "Channel is unsupported for find", + release: sampleRelease, + query: []string{"python3.10_bins@3.0"}, + err: `channel is unsupported for find: "python3.10_bins@3.0"`, }} func (s *ChiselSuite) TestFindSlices(c *C) { @@ -131,6 +137,10 @@ func (s *ChiselSuite) TestFindSlices(c *C) { for _, query := range testutil.Permutations(test.query) { slices, err := chisel.FindSlices(test.release, query) + if test.err != "" { + c.Assert(err, ErrorMatches, test.err) + continue + } c.Assert(err, IsNil) c.Assert(slices, DeepEquals, test.result) } diff --git a/cmd/chisel/cmd_info.go b/cmd/chisel/cmd_info.go index 67ede89a..f8f0015c 100644 --- a/cmd/chisel/cmd_info.go +++ b/cmd/chisel/cmd_info.go @@ -50,6 +50,12 @@ func (cmd *infoCmd) Execute(args []string) error { return err } + for _, query := range cmd.Positional.Queries { + if strings.Contains(query, "@") { + return fmt.Errorf("channel is unsupported for info: %q", query) + } + } + packages, notFound := selectPackageSlices(release, cmd.Positional.Queries) for i, pkg := range packages { diff --git a/cmd/chisel/cmd_info_test.go b/cmd/chisel/cmd_info_test.go index fc462487..8c3d13f4 100644 --- a/cmd/chisel/cmd_info_test.go +++ b/cmd/chisel/cmd_info_test.go @@ -140,6 +140,11 @@ var infoTests = []infoTest{{ input: infoRelease, query: []string{"foo_bar_foo", "a_b", "7_c", "a_b c", "a_b x_y"}, err: `no slice definitions found for: "foo_bar_foo", "a_b", "7_c", "a_b c", "a_b x_y"`, +}, { + summary: "Channel is unsupported for info", + input: infoRelease, + query: []string{"mypkg1_myslice1@3.0"}, + err: `channel is unsupported for info: "mypkg1_myslice1@3.0"`, }} var infoRelease = map[string]string{ diff --git a/internal/setup/setup.go b/internal/setup/setup.go index 9d7e4a7b..2c34b67a 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -8,6 +8,7 @@ import ( "slices" "strings" "time" + "unicode" "golang.org/x/crypto/openpgp/packet" @@ -145,6 +146,62 @@ func ParseSliceKey(sliceKey string) (SliceKey, error) { return apacheutil.ParseSliceKey(sliceKey) } +// DefaultRisk is used when a channel does not specify a risk. +const DefaultRisk = "stable" + +// SliceRef is a slice reference with an optional channel for store packages. +// The channel always holds a risk, that is at least "/". +type SliceRef struct { + Key SliceKey + Channel string +} + +// ParseSliceRef parses a "pkg_slice[@channel]" reference. The channel is +// either a track, in which case the default risk is used, or a +// "/" value. Validation is intentionally loose so that longer +// channel forms, such as "//", are accepted without +// changes here. +func ParseSliceRef(ref string) (SliceRef, error) { + keyPart, channel, ok := strings.Cut(ref, "@") + if !ok { + key, err := ParseSliceKey(ref) + if err != nil { + return SliceRef{}, err + } + return SliceRef{Key: key}, nil + } + key, err := ParseSliceKey(keyPart) + if err != nil { + return SliceRef{}, err + } + channel, err = validateChannel(channel) + if err != nil { + return SliceRef{}, fmt.Errorf("invalid slice reference %q: %s", ref, err) + } + return SliceRef{Key: key, Channel: channel}, nil +} + +// validateChannel returns the channel with the default risk appended if it +// holds a track alone. Validation is intentionally loose, any number of +// segments is accepted so that longer forms are not rejected here. +func validateChannel(channel string) (string, error) { + if channel == "" { + return "", fmt.Errorf("missing channel") + } + if strings.ContainsFunc(channel, unicode.IsSpace) { + return "", fmt.Errorf("channel must not contain spaces") + } + segments := strings.Split(channel, "/") + if slices.Contains(segments, "") { + return "", fmt.Errorf("channel must be or /") + } + if len(segments) == 1 { + // A track alone, the risk is implicit. + return channel + "/" + DefaultRisk, nil + } + return channel, nil +} + func (s *Slice) String() string { return s.Package + "_" + s.Name } // Selection holds the required configuration to create a Build for a selection @@ -154,6 +211,8 @@ func (s *Slice) String() string { return s.Package + "_" + s.Name } type Selection struct { Release *Release Slices []*Slice + // Channels holds the resolved channel per store package name. + Channels map[string]string } // Prefers uses the prefer relationships and returns a map from each path to @@ -493,7 +552,7 @@ func stripBase(baseDir, path string) string { return strings.TrimPrefix(path, baseDir+string(filepath.Separator)) } -func Select(release *Release, slices []SliceKey, arch string) (*Selection, error) { +func Select(release *Release, refs []SliceRef, arch string) (*Selection, error) { logf("Selecting slices...") var err error @@ -506,10 +565,33 @@ func Select(release *Release, slices []SliceKey, arch string) (*Selection, error return nil, err } + // Resolve the channel per store package from the references. + channels, err := resolveChannels(release, refs) + if err != nil { + return nil, err + } + // Derive the channel from 'default-track' for the store packages without an + // explicit one. Note the release only defines a track, the risk is + // implicit. This is done for every known package, before ordering, because + // ordering itself depends on the channel of the packages it traverses. + for _, pkg := range release.Packages { + if pkg.Store == "" { + continue + } + if _, ok := channels[pkg.Name]; ok { + continue + } + channels[pkg.Name] = pkg.DefaultTrack + "/" + DefaultRisk + } + selection := &Selection{ Release: release, } + slices := make([]SliceKey, len(refs)) + for i, ref := range refs { + slices[i] = ref.Key + } sorted, err := order(release.Packages, slices, arch) if err != nil { return nil, err @@ -519,6 +601,14 @@ func Select(release *Release, slices []SliceKey, arch string) (*Selection, error selection.Slices[i] = release.Packages[key.Package].Slices[key.Slice] } + // Only report the channels of the selected packages. + selection.Channels = make(map[string]string) + for _, slice := range selection.Slices { + if channel, ok := channels[slice.Package]; ok { + selection.Channels[slice.Package] = channel + } + } + for _, new := range selection.Slices { for newPath, newInfo := range new.Contents { // An invalid "generate" value should only throw an error if that @@ -550,6 +640,36 @@ func Select(release *Release, slices []SliceKey, arch string) (*Selection, error return selection, nil } +// resolveChannels collects the explicit channel per store package from the +// references. It errors if a channel is set on a non-store package or if two +// references to the same package specify different channels. +func resolveChannels(release *Release, refs []SliceRef) (map[string]string, error) { + channels := make(map[string]string) + for _, ref := range refs { + pkg, ok := release.Packages[ref.Key.Package] + if !ok { + // Nothing to validate; the package is unknown. + continue + } + if pkg.Store == "" { + if ref.Channel != "" { + return nil, fmt.Errorf("slice %s has channel but package %q is not in a store", + ref.Key, pkg.Name) + } + continue + } + if ref.Channel == "" { + continue + } + if existing, ok := channels[pkg.Name]; ok && existing != ref.Channel { + return nil, fmt.Errorf("slices of package %q have conflicting channels %q and %q", + pkg.Name, existing, ref.Channel) + } + channels[pkg.Name] = ref.Channel + } + return channels, nil +} + const ( preferSource = 1 preferTarget = 2 diff --git a/internal/setup/setup_test.go b/internal/setup/setup_test.go index 3568b39a..6d3f687f 100644 --- a/internal/setup/setup_test.go +++ b/internal/setup/setup_test.go @@ -26,7 +26,7 @@ type setupTest struct { release *setup.Release relerror string prefers map[string]string - selslices []setup.SliceKey + selrefs []setup.SliceRef selection *setup.Selection selerror string } @@ -426,12 +426,13 @@ var setupTests = []setupTest{{ myslice2: {essential: [mypkg1_myslice1]} `, }, - selslices: []setup.SliceKey{{"mypkg1", "myslice1"}}, + selrefs: []setup.SliceRef{{Key: setup.SliceKey{"mypkg1", "myslice1"}}}, selection: &setup.Selection{ Slices: []*setup.Slice{{ Package: "mypkg1", Name: "myslice1", }}, + Channels: map[string]string{}, }, }, { summary: "Selection with dependencies", @@ -449,7 +450,7 @@ var setupTests = []setupTest{{ myslice2: {essential: [mypkg1_myslice1]} `, }, - selslices: []setup.SliceKey{{"mypkg2", "myslice2"}}, + selrefs: []setup.SliceRef{{Key: setup.SliceKey{"mypkg2", "myslice2"}}}, selection: &setup.Selection{ Slices: []*setup.Slice{{ Package: "mypkg1", @@ -461,6 +462,7 @@ var setupTests = []setupTest{{ {"mypkg1", "myslice1"}: {}, }, }}, + Channels: map[string]string{}, }, }, { summary: "Selection with matching paths don't conflict", @@ -488,7 +490,11 @@ var setupTests = []setupTest{{ /path3: {symlink: /link} `, }, - selslices: []setup.SliceKey{{"mypkg1", "myslice1"}, {"mypkg1", "myslice2"}, {"mypkg2", "myslice1"}}, + selrefs: []setup.SliceRef{ + {Key: setup.SliceKey{"mypkg1", "myslice1"}}, + {Key: setup.SliceKey{"mypkg1", "myslice2"}}, + {Key: setup.SliceKey{"mypkg2", "myslice1"}}, + }, }, { summary: "Conflicting paths across slices", input: map[string]string{ @@ -1756,7 +1762,7 @@ var setupTests = []setupTest{{ EndOfLife: time.Date(2100, time.January, 1, 0, 0, 0, 0, time.UTC), }, }, - selslices: []setup.SliceKey{{"mypkg", "myslice"}}, + selrefs: []setup.SliceRef{{Key: setup.SliceKey{"mypkg", "myslice"}}}, selection: &setup.Selection{ Slices: []*setup.Slice{{ Package: "mypkg", @@ -1765,6 +1771,7 @@ var setupTests = []setupTest{{ "/dir/**": {Kind: "generate", Generate: "manifest"}, }, }}, + Channels: map[string]string{}, }, }, { summary: "Can specify generate with bogus value but cannot select those slices", @@ -1810,8 +1817,8 @@ var setupTests = []setupTest{{ EndOfLife: time.Date(2100, time.January, 1, 0, 0, 0, 0, time.UTC), }, }, - selslices: []setup.SliceKey{{"mypkg", "myslice"}}, - selerror: `slice mypkg_myslice has invalid 'generate' for path /dir/\*\*: "foo"`, + selrefs: []setup.SliceRef{{Key: setup.SliceKey{"mypkg", "myslice"}}}, + selerror: `slice mypkg_myslice has invalid 'generate' for path /dir/\*\*: "foo"`, }, { summary: "Paths with generate: manifest must have trailing /**", input: map[string]string{ @@ -2430,11 +2437,11 @@ var setupTests = []setupTest{{ relerror: "slice mypkg1_myslice1 cannot 'prefer' its own package for path /file", }, { summary: "Path conflicts with 'prefer'", - selslices: []setup.SliceKey{ - {"mypkg1", "myslice1"}, - {"mypkg1", "myslice2"}, - {"mypkg2", "myslice1"}, - {"mypkg3", "myslice1"}, + selrefs: []setup.SliceRef{ + {Key: setup.SliceKey{"mypkg1", "myslice1"}}, + {Key: setup.SliceKey{"mypkg1", "myslice2"}}, + {Key: setup.SliceKey{"mypkg2", "myslice1"}}, + {Key: setup.SliceKey{"mypkg3", "myslice1"}}, }, input: map[string]string{ "slices/mydir/mypkg1.yaml": ` @@ -2540,10 +2547,10 @@ var setupTests = []setupTest{{ }, }, { summary: "Path conflicts with 'prefer' depends on selection", - selslices: []setup.SliceKey{ - {"mypkg1", "myslice1"}, - {"mypkg1", "myslice2"}, - {"mypkg2", "myslice1"}, + selrefs: []setup.SliceRef{ + {Key: setup.SliceKey{"mypkg1", "myslice1"}}, + {Key: setup.SliceKey{"mypkg1", "myslice2"}}, + {Key: setup.SliceKey{"mypkg2", "myslice1"}}, }, input: map[string]string{ "slices/mydir/mypkg1.yaml": ` @@ -4374,8 +4381,8 @@ var setupTests = []setupTest{{ }, }, }, { - summary: "Store unknown kind", - selslices: []setup.SliceKey{{Package: "bin-mypkg", Slice: "myslice"}}, + summary: "Store unknown kind", + selrefs: []setup.SliceRef{{Key: setup.SliceKey{Package: "bin-mypkg", Slice: "myslice"}}}, input: map[string]string{ "chisel.yaml": ` format: v3 @@ -4409,6 +4416,135 @@ var setupTests = []setupTest{{ `, }, selerror: `slice bin-mypkg_myslice refers to store "bin" with unknown kind "unknown"`, +}, { + summary: "Channel on bin slice is derived from default-track when omitted", + selrefs: []setup.SliceRef{{Key: setup.SliceKey{Package: "bin-mypkg", Slice: "myslice"}}}, + input: map[string]string{ + "chisel.yaml": testutil.DefaultChiselYamlWithStores, + "bin-slices/mypkg.yaml": ` + package: mypkg + store: bin + default-track: "3.0" + slices: + myslice: + contents: + /dir/file: {} + `, + }, + selection: &setup.Selection{ + Slices: []*setup.Slice{{ + Package: "bin-mypkg", + Name: "myslice", + Contents: map[string]setup.PathInfo{ + "/dir/file": {Kind: setup.CopyPath}, + }, + }}, + Channels: map[string]string{"bin-mypkg": "3.0/stable"}, + }, +}, { + summary: "Channel on bin slice is set from the reference", + selrefs: []setup.SliceRef{{ + Key: setup.SliceKey{Package: "bin-mypkg", Slice: "myslice"}, + Channel: "2.0/edge", + }}, + input: map[string]string{ + "chisel.yaml": testutil.DefaultChiselYamlWithStores, + "bin-slices/mypkg.yaml": ` + package: mypkg + store: bin + default-track: "3.0" + slices: + myslice: + contents: + /dir/file: {} + `, + }, + selection: &setup.Selection{ + Slices: []*setup.Slice{{ + Package: "bin-mypkg", + Name: "myslice", + Contents: map[string]setup.PathInfo{ + "/dir/file": {Kind: setup.CopyPath}, + }, + }}, + Channels: map[string]string{"bin-mypkg": "2.0/edge"}, + }, +}, { + summary: "Same channel on two slices of same bin package is allowed", + selrefs: []setup.SliceRef{ + {Key: setup.SliceKey{Package: "bin-mypkg", Slice: "myslice"}, Channel: "2.0/stable"}, + {Key: setup.SliceKey{Package: "bin-mypkg", Slice: "myslice2"}, Channel: "2.0/stable"}, + }, + input: map[string]string{ + "chisel.yaml": testutil.DefaultChiselYamlWithStores, + "bin-slices/mypkg.yaml": ` + package: mypkg + store: bin + default-track: "3.0" + slices: + myslice: + contents: + /dir/file1: {} + myslice2: + contents: + /dir/file2: {} + `, + }, + selection: &setup.Selection{ + Slices: []*setup.Slice{{ + Package: "bin-mypkg", + Name: "myslice", + Contents: map[string]setup.PathInfo{ + "/dir/file1": {Kind: setup.CopyPath}, + }, + }, { + Package: "bin-mypkg", + Name: "myslice2", + Contents: map[string]setup.PathInfo{ + "/dir/file2": {Kind: setup.CopyPath}, + }, + }}, + Channels: map[string]string{"bin-mypkg": "2.0/stable"}, + }, +}, { + summary: "Conflicting channels on two slices of same bin package fails", + selrefs: []setup.SliceRef{ + {Key: setup.SliceKey{Package: "bin-mypkg", Slice: "myslice"}, Channel: "2.0/stable"}, + {Key: setup.SliceKey{Package: "bin-mypkg", Slice: "myslice2"}, Channel: "2.0/edge"}, + }, + input: map[string]string{ + "chisel.yaml": testutil.DefaultChiselYamlWithStores, + "bin-slices/mypkg.yaml": ` + package: mypkg + store: bin + default-track: "3.0" + slices: + myslice: + contents: + /dir/file1: {} + myslice2: + contents: + /dir/file2: {} + `, + }, + selerror: `slices of package "bin-mypkg" have conflicting channels "2.0/stable" and "2.0/edge"`, +}, { + summary: "Channel on a non-store (deb) package fails", + selrefs: []setup.SliceRef{{ + Key: setup.SliceKey{Package: "mypkg", Slice: "myslice"}, + Channel: "2.0/stable", + }}, + input: map[string]string{ + "chisel.yaml": testutil.DefaultChiselYaml, + "slices/mypkg.yaml": ` + package: mypkg + slices: + myslice: + contents: + /dir/file: {} + `, + }, + selerror: `slice mypkg_myslice has channel but package "mypkg" is not in a store`, }} func (s *S) TestParseRelease(c *C) { @@ -4540,8 +4676,8 @@ func runParseReleaseTests(c *C, tests []setupTest) { c.Assert(release, DeepEquals, test.release) } - if test.selslices != nil { - selection, err := setup.Select(release, test.selslices, "amd64") + if test.selrefs != nil { + selection, err := setup.Select(release, test.selrefs, "amd64") if test.selerror != "" { c.Assert(err, ErrorMatches, test.selerror) continue @@ -4907,8 +5043,8 @@ func (s *S) TestSelectEmptyArch(c *C) { release, err := setup.ReadRelease(dir) c.Assert(err, IsNil) - selslice := []setup.SliceKey{{"mypkg", "myslice"}} - selection, err := setup.Select(release, selslice, "") + refs := []setup.SliceRef{{Key: setup.SliceKey{"mypkg", "myslice"}}} + selection, err := setup.Select(release, refs, "") c.Assert(err, IsNil) var sliceNames []string @@ -4919,6 +5055,102 @@ func (s *S) TestSelectEmptyArch(c *C) { c.Assert(sliceNames, DeepEquals, expected) } +var parseSliceRefTests = []struct { + input string + expected setup.SliceRef + err string +}{{ + input: "foo_bar", + expected: setup.SliceRef{Key: setup.SliceKey{Package: "foo", Slice: "bar"}}, +}, { + // A track alone gets the default risk. + input: "foo_bar@3.0", + expected: setup.SliceRef{ + Key: setup.SliceKey{Package: "foo", Slice: "bar"}, + Channel: "3.0/stable", + }, +}, { + input: "foo_bar@latest", + expected: setup.SliceRef{ + Key: setup.SliceKey{Package: "foo", Slice: "bar"}, + Channel: "latest/stable", + }, +}, { + input: "foo_bar@3.0/edge", + expected: setup.SliceRef{ + Key: setup.SliceKey{Package: "foo", Slice: "bar"}, + Channel: "3.0/edge", + }, +}, { + // An explicit default risk is kept as is. + input: "foo_bar@3.0/stable", + expected: setup.SliceRef{ + Key: setup.SliceKey{Package: "foo", Slice: "bar"}, + Channel: "3.0/stable", + }, +}, { + // Validation is loose, unknown risks are accepted. + input: "foo_bar@3.0/whatever", + expected: setup.SliceRef{ + Key: setup.SliceKey{Package: "foo", Slice: "bar"}, + Channel: "3.0/whatever", + }, +}, { + input: "foo-pkg_dashed-slice@3.0/beta", + expected: setup.SliceRef{ + Key: setup.SliceKey{Package: "foo-pkg", Slice: "dashed-slice"}, + Channel: "3.0/beta", + }, +}, { + // Split on the first '@'; the channel may itself contain '@'. + input: "foo_bar@3.0@x", + expected: setup.SliceRef{ + Key: setup.SliceKey{Package: "foo", Slice: "bar"}, + Channel: "3.0@x/stable", + }, +}, { + // Longer forms, such as a branch, are not rejected. + input: "foo_bar@3.0/stable/mybranch", + expected: setup.SliceRef{ + Key: setup.SliceKey{Package: "foo", Slice: "bar"}, + Channel: "3.0/stable/mybranch", + }, +}, { + input: "foo_bar@", + err: `invalid slice reference "foo_bar@": missing channel`, +}, { + input: "foo_bar@/stable", + err: `invalid slice reference "foo_bar@/stable": channel must be or /`, +}, { + input: "foo_bar@3.0/", + err: `invalid slice reference "foo_bar@3.0/": channel must be or /`, +}, { + input: "foo_bar@3.0//stable", + err: `invalid slice reference "foo_bar@3.0//stable": channel must be or /`, +}, { + input: "foo_bar@3.0 stable", + err: `invalid slice reference "foo_bar@3.0 stable": channel must not contain spaces`, +}, { + // Identity part is still validated by ParseSliceKey. + input: "foo_ba@3.0", + err: `invalid slice reference: "foo_ba"`, +}, { + input: "foo_bar_baz@3.0", + err: `invalid slice reference: "foo_bar_baz"`, +}} + +func (s *S) TestParseSliceRef(c *C) { + for _, test := range parseSliceRefTests { + ref, err := setup.ParseSliceRef(test.input) + if test.err != "" { + c.Assert(err, ErrorMatches, test.err) + continue + } + c.Assert(err, IsNil) + c.Assert(ref, DeepEquals, test.expected) + } +} + // oldEssentialToV3 converts the essentials in v1 and v2, both 'essential', and // 'v3-essential' to the shape expected by the v3 format. // skip is set to true when an accurate translation of the test is not diff --git a/internal/slicer/slicer_test.go b/internal/slicer/slicer_test.go index d6ef9ca0..df9d25d8 100644 --- a/internal/slicer/slicer_test.go +++ b/internal/slicer/slicer_test.go @@ -2094,7 +2094,11 @@ func runSlicerTests(s *S, c *C, tests []slicerTest) { Slice: "manifest", }) - selection, err := setup.Select(release, testSlices, test.arch) + refs := make([]setup.SliceRef, len(testSlices)) + for i, key := range testSlices { + refs[i] = setup.SliceRef{Key: key} + } + selection, err := setup.Select(release, refs, test.arch) c.Assert(err, IsNil) archives := map[string]archive.Archive{}