Skip to content
Open
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
16 changes: 12 additions & 4 deletions cmd/chisel/cmd_cut.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <package>_<slice>. 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{
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/chisel/cmd_find.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions cmd/chisel/cmd_find_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}
Expand Down
6 changes: 6 additions & 0 deletions cmd/chisel/cmd_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions cmd/chisel/cmd_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
122 changes: 121 additions & 1 deletion internal/setup/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"slices"
"strings"
"time"
"unicode"

"golang.org/x/crypto/openpgp/packet"

Expand Down Expand Up @@ -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 "<track>/<risk>".
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
// "<track>/<risk>" value. Validation is intentionally loose so that longer
// channel forms, such as "<track>/<risk>/<branch>", 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 <track> or <track>/<risk>")
}
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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading