diff --git a/cmd/chisel/cmd_info_test.go b/cmd/chisel/cmd_info_test.go index b3fd6eb0..86a04c96 100644 --- a/cmd/chisel/cmd_info_test.go +++ b/cmd/chisel/cmd_info_test.go @@ -52,7 +52,7 @@ var infoTests = []infoTest{{ contents: /dir/file: {} myslice2: - v3-essential: + essential: mypkg1_myslice1: {} mypkg2_myslice: {arch: amd64} `, @@ -67,7 +67,7 @@ var infoTests = []infoTest{{ contents: /dir/file: {} myslice2: - v3-essential: + essential: mypkg1_myslice1: {} mypkg2_myslice: {arch: amd64} --- @@ -88,7 +88,7 @@ var infoTests = []infoTest{{ contents: /dir/file: {} myslice2: - v3-essential: + essential: mypkg1_myslice1: {} mypkg2_myslice: {arch: amd64} `, @@ -138,7 +138,7 @@ var infoTests = []infoTest{{ }} var infoRelease = map[string]string{ - "chisel.yaml": string(testutil.DefaultChiselYaml), + "chisel.yaml": testutil.DefaultChiselYaml, "slices/mypkg1.yaml": ` package: mypkg1 essential: diff --git a/internal/setup/setup.go b/internal/setup/setup.go index b59e30f9..a5c69849 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -23,6 +23,8 @@ type Release struct { Packages map[string]*Package Archives map[string]*Archive Maintenance *Maintenance + // Format is used for parsing and error checking. + Format string } type Maintenance struct { @@ -448,7 +450,7 @@ func readSlices(release *Release, baseDir, dirName string) error { return fmt.Errorf("cannot read slice definition file: %v", err) } - pkg, err := parsePackage(baseDir, pkgName, stripBase(baseDir, pkgPath), data) + pkg, err := parsePackage(release.Format, pkgName, stripBase(baseDir, pkgPath), data) if err != nil { return err } diff --git a/internal/setup/setup_test.go b/internal/setup/setup_test.go index 1b6adaf3..78df3ac0 100644 --- a/internal/setup/setup_test.go +++ b/internal/setup/setup_test.go @@ -79,6 +79,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -125,6 +126,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -189,6 +191,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -452,6 +455,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -669,6 +673,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -711,6 +716,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -754,6 +760,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -816,6 +823,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "foo": { Name: "foo", @@ -1031,6 +1039,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -1095,6 +1104,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "foo": { Name: "foo", @@ -1217,6 +1227,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -1285,6 +1296,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -1357,6 +1369,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -1435,7 +1448,7 @@ var setupTests = []setupTest{{ - mypkg_slice1 `, }, - relerror: `cannot add slice to itself as essential "mypkg_slice1" in slices/mydir/mypkg.yaml`, + relerror: `package "mypkg": cannot add slice to itself as essential "mypkg_slice1"`, }, { summary: "Package essentials clashes with slice essentials", input: map[string]string{ @@ -1464,7 +1477,7 @@ var setupTests = []setupTest{{ slice2: `, }, - relerror: `slice mypkg_slice1 repeats mypkg_slice2 in essential fields`, + relerror: `cannot parse package "mypkg" slice definitions: cannot decode essential: repeats mypkg_slice2`, }, { summary: "Duplicated package essentials", input: map[string]string{ @@ -1478,7 +1491,7 @@ var setupTests = []setupTest{{ slice2: `, }, - relerror: `package "mypkg" repeats mypkg_slice1 in essential fields`, + relerror: `cannot parse package "mypkg" slice definitions: cannot decode essential: repeats mypkg_slice1`, }, { summary: "Bad slice reference in slice essential", input: map[string]string{ @@ -1539,6 +1552,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -1591,6 +1605,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -1685,6 +1700,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -1836,6 +1852,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "default": { Name: "default", @@ -1939,6 +1956,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -2036,6 +2054,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "default": { Name: "default", @@ -2129,6 +2148,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -2265,6 +2285,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -2698,6 +2719,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -2747,6 +2769,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -2894,6 +2917,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -3015,6 +3039,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -3136,6 +3161,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -3257,6 +3283,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -3376,6 +3403,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -3460,6 +3488,7 @@ var setupTests = []setupTest{{ `, }, release: &setup.Release{ + Format: "v1", Archives: map[string]*setup.Archive{ "ubuntu": { Name: "ubuntu", @@ -3587,6 +3616,90 @@ var setupTests = []setupTest{{ `, }, relerror: "essential loop detected: mypkg1_myslice, mypkg2_myslice", +}, { + summary: "Format v3 expects a map in 'essential' (pkg)", + input: map[string]string{ + "chisel.yaml": strings.ReplaceAll(testutil.DefaultChiselYaml, "format: v1", "format: v3"), + "slices/mydir/mypkg.yaml": ` + package: mypkg + essential: + - mypkg_myslice2 + slices: + myslice1: + myslice2: + `, + }, + relerror: `cannot parse package "mypkg": essential expects a map`, +}, { + summary: "Format v3 expects a map in 'essential' (slice)", + input: map[string]string{ + "chisel.yaml": strings.ReplaceAll(testutil.DefaultChiselYaml, "format: v1", "format: v3"), + "slices/mydir/mypkg.yaml": ` + package: mypkg + slices: + myslice1: + essential: + - mypkg_myslice2 + myslice2: + `, + }, + relerror: `cannot parse slice mypkg_myslice1: essential expects a map`, +}, { + summary: "In format v3 'v3-essential' is not supported (pkg)", + input: map[string]string{ + "chisel.yaml": strings.ReplaceAll(testutil.DefaultChiselYaml, "format: v1", "format: v3"), + "slices/mydir/mypkg.yaml": ` + package: mypkg + v3-essential: + mypkg_myslice2: + slices: + myslice1: + myslice2: + `, + }, + relerror: `cannot parse package "mypkg": v3-essential is deprecated since format v3`, +}, { + summary: "In format v3 'v3-essential' is not supported (slice)", + input: map[string]string{ + "chisel.yaml": strings.ReplaceAll(testutil.DefaultChiselYaml, "format: v1", "format: v3"), + "slices/mydir/mypkg.yaml": ` + package: mypkg + slices: + myslice1: + v3-essential: + mypkg_myslice2: + myslice2: + `, + }, + relerror: `cannot parse slice mypkg_myslice1: v3-essential is deprecated since format v3`, +}, { + summary: "Format v1/v2 expect a list in 'essential' (pkg)", + input: map[string]string{ + "chisel.yaml": strings.ReplaceAll(testutil.DefaultChiselYaml, "format: v1", "format: v2"), + "slices/mydir/mypkg.yaml": ` + package: mypkg + essential: + mypkg_myslice2: {} + slices: + myslice1: + myslice2: + `, + }, + relerror: `cannot parse package "mypkg": essential expects a list`, +}, { + summary: "Format v1/v2 expect a list in 'essential' (slice)", + input: map[string]string{ + "chisel.yaml": strings.ReplaceAll(testutil.DefaultChiselYaml, "format: v1", "format: v2"), + "slices/mydir/mypkg.yaml": ` + package: mypkg + slices: + myslice1: + essential: + mypkg_myslice2: {} + myslice2: + `, + }, + relerror: `cannot parse slice mypkg_myslice1: essential expects a list`, }} func (s *S) TestParseRelease(c *C) { @@ -3599,7 +3712,7 @@ func (s *S) TestParseRelease(c *C) { m := make(map[string]string) for k, v := range t.input { if !strings.Contains(v, "v2-archives:") && strings.Contains(v, "format: v1") { - v = strings.Replace(v, "archives:", "v2-archives:", -1) + v = strings.ReplaceAll(v, "archives:", "v2-archives:") } m[k] = v } @@ -3611,19 +3724,64 @@ func (s *S) TestParseRelease(c *C) { // Run tests for "v2" format. v2FormatTests := make([]setupTest, 0, len(setupTests)) for _, t := range setupTests { + t.summary += " (v2)" m := make(map[string]string) + skip := false for k, v := range t.input { - if strings.Contains(v, "format: v1") && - !strings.Contains(v, "v2-archives:") && - !strings.Contains(v, "default: true") { - v = strings.Replace(v, "format: v1", "format: v2", -1) + if strings.Contains(v, "format: v2") || + strings.Contains(v, "format: v3") || + strings.Contains(v, "v2-archives:") || + strings.Contains(v, "default: true") { + skip = true + break } + v = strings.ReplaceAll(v, "format: v1", "format: v2") m[k] = v } + if skip { + // Test was not affected, no need to re-run. + continue + } t.input = m + if t.release != nil { + t.release.Format = "v2" + } v2FormatTests = append(v2FormatTests, t) } runParseReleaseTests(c, v2FormatTests) + + // Run tests for "v3" format. + v3FormatTests := make([]setupTest, 0, len(setupTests)) + for _, t := range setupTests { + t.summary += " (v3)" + m := make(map[string]string) + skip := false + for k, v := range t.input { + if strings.Contains(v, "format: v2") || + strings.Contains(v, "format: v3") || + strings.Contains(v, "v2-archives:") || + strings.Contains(v, "default: true") { + skip = true + break + } + v, skip = oldEssentialToV3(c, testutil.Reindent(v)) + if skip { + break + } + v = strings.ReplaceAll(v, "format: v1", "format: v3") + m[k] = v + } + if skip { + // Test was not affected, or it is not meaningful, no need to re-run. + continue + } + t.input = m + if t.release != nil { + t.release.Format = "v3" + } + v3FormatTests = append(v3FormatTests, t) + } + runParseReleaseTests(c, v3FormatTests) } func runParseReleaseTests(c *C, tests []setupTest) { @@ -3795,13 +3953,13 @@ func (s *S) TestPackageYAMLFormat(c *C) { myslice1: contents: /dir/file1: {} - v3-essential: + essential: mypkg_myslice2: {arch: i386} mypkg_myslice3: {} myslice2: contents: /dir/file2: {} - v3-essential: + essential: mypkg_myslice3: {} myslice3: contents: @@ -3828,6 +3986,39 @@ func (s *S) TestPackageYAMLFormat(c *C) { /dir/prefer: {} `, }, + }, { + summary: "Format v3", + input: map[string]string{ + "chisel.yaml": strings.ReplaceAll(testutil.DefaultChiselYaml, "format: v1", "format: v3"), + "slices/mypkg.yaml": ` + package: mypkg + archive: ubuntu + essential: + mypkg_three: {arch: i386} + slices: + one: + essential: + mypkg_two: {arch: [amd64, aarch64]} + two: + three: + `, + }, + expected: map[string]string{ + "chisel.yaml": strings.ReplaceAll(testutil.DefaultChiselYaml, "format: v1", "format: v3"), + "slices/mypkg.yaml": ` + package: mypkg + archive: ubuntu + slices: + one: + essential: + mypkg_three: {arch: i386} + mypkg_two: {arch: [amd64, aarch64]} + three: {} + two: + essential: + mypkg_three: {arch: i386} + `, + }, }} for _, test := range tests { @@ -3939,3 +4130,69 @@ func (s *S) TestSelectEmptyArch(c *C) { expected := []string{"myotherslice", "myslice"} c.Assert(sliceNames, DeepEquals, 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 +// possible, for example having duplicates in the list. +func oldEssentialToV3(c *C, input []byte) (out string, skip bool) { + var raw map[string]any + err := yaml.Unmarshal(input, &raw) + c.Assert(err, IsNil) + + if slices, ok := raw["slices"].(map[string]any); ok { + for _, rawSlice := range slices { + if slice, ok := rawSlice.(map[string]any); ok { + newEssential := make(map[string]any) + if oldEssential, ok := slice["essential"].([]any); ok { + for _, value := range oldEssential { + s := value.(string) + if _, ok := newEssential[s]; ok { + // Duplicated entries are impossible in v3. + return "", true + } + newEssential[s] = nil + } + } + if oldEssential, ok := slice["v3-essential"].(map[string]any); ok { + for key, value := range oldEssential { + if _, ok := newEssential[key]; ok { + return "", true + } + newEssential[key] = value + } + delete(slice, "v3-essential") + } + slice["essential"] = newEssential + } + } + } + + newEssential := make(map[string]any) + if oldEssential, ok := raw["essential"].([]any); ok { + for _, item := range oldEssential { + s := item.(string) + if _, ok := newEssential[s]; ok { + // Duplicated entries are impossible in v3. + return "", true + } + newEssential[s] = nil + } + } + if oldEssential, ok := raw["v3-essential"].(map[string]any); ok { + for key, value := range oldEssential { + if _, ok := newEssential[key]; ok { + // Duplicated entries are impossible in v3. + return "", true + } + newEssential[key] = value + } + delete(raw, "v3-essential") + } + raw["essential"] = newEssential + + bs, err := yaml.Marshal(raw) + c.Assert(err, IsNil) + // Maintenance dates get marshaled as T00:00:00Z by default. + return strings.ReplaceAll(string(bs), "T00:00:00Z", ""), false +} diff --git a/internal/setup/yaml.go b/internal/setup/yaml.go index b7fc65b4..964fb863 100644 --- a/internal/setup/yaml.go +++ b/internal/setup/yaml.go @@ -52,16 +52,55 @@ type yamlArchive struct { } type yamlPackage struct { - Name string `yaml:"package"` - Archive string `yaml:"archive,omitempty"` - Essential []string `yaml:"essential,omitempty"` - Slices map[string]yamlSlice `yaml:"slices,omitempty"` + Name string `yaml:"package"` + Archive string `yaml:"archive,omitempty"` + Slices map[string]yamlSlice `yaml:"slices,omitempty"` // "v3-essential" is used for backwards porting of arch-specific essential // to releases that use "v1" or "v2". When using older versions of Chisel // the field will be ignored and `essential` is used as a fallback. V3Essential map[string]*yamlEssential `yaml:"v3-essential,omitempty"` + // For backwards-compatibility reasons with v1 and v2, essential needs + // custom logic to be parsed. See [essentialListMap]. + Essential yamlEssentialListMap `yaml:"essential,omitempty"` } +type yamlEssentialListMap struct { + Values map[string]*yamlEssential + // isList is set to true when the marshaler found a isList and false if it + // found a map. The former is only valid in format "v1" and "v2" while the + // latter is valid from "v3" onwards. + isList bool +} + +func (es *yamlEssentialListMap) UnmarshalYAML(value *yaml.Node) error { + m := map[string]*yamlEssential{} + es.isList = false + err := value.Decode(&m) + if err != nil { + l := []string{} + err = value.Decode(&l) + if err != nil { + return errors.New("cannot decode essential") + } + es.isList = true + for _, sliceName := range l { + if _, ok := m[sliceName]; ok { + return fmt.Errorf("cannot decode essential: repeats %s", sliceName) + } + m[sliceName] = &yamlEssential{} + } + } + es.Values = m + return nil +} + +func (es yamlEssentialListMap) MarshalYAML() (any, error) { + return es.Values, nil +} + +var _ yaml.Marshaler = yamlEssentialListMap{} +var _ yaml.Unmarshaler = (*yamlEssentialListMap)(nil) + type yamlPath struct { Dir bool `yaml:"make,omitempty"` Mode yamlMode `yaml:"mode,omitempty"` @@ -146,13 +185,15 @@ func (ym yamlMode) MarshalYAML() (any, error) { var _ yaml.Marshaler = yamlMode(0) type yamlSlice struct { - Essential []string `yaml:"essential,omitempty"` - Contents map[string]*yamlPath `yaml:"contents,omitempty"` - Mutate string `yaml:"mutate,omitempty"` + Contents map[string]*yamlPath `yaml:"contents,omitempty"` + Mutate string `yaml:"mutate,omitempty"` // "v3-essential" is used for backwards porting of arch-specific essential // to releases that use "v1" or "v2". When using older versions of Chisel // the field will be ignored and `essential` is used as a fallback. V3Essential map[string]*yamlEssential `yaml:"v3-essential,omitempty"` + // For backwards-compatibility reasons with v1 and v2, essential needs + // custom logic to be parsed. See [essentialListMap]. + Essential yamlEssentialListMap `yaml:"essential,omitempty"` } type yamlPubKey struct { @@ -193,13 +234,14 @@ func parseRelease(baseDir, filePath string, data []byte) (*Release, error) { if err != nil { return nil, fmt.Errorf("%s: cannot parse release definition: %v", fileName, err) } - if yamlVar.Format != "v1" && yamlVar.Format != "v2" { + if yamlVar.Format != "v1" && yamlVar.Format != "v2" && yamlVar.Format != "v3" { return nil, fmt.Errorf("%s: unknown format %q", fileName, yamlVar.Format) } + release.Format = yamlVar.Format + if yamlVar.Format != "v1" && len(yamlVar.V2Archives) > 0 { return nil, fmt.Errorf("%s: v2-archives is deprecated since format v2", fileName) } - if len(yamlVar.Archives)+len(yamlVar.V2Archives) == 0 { return nil, fmt.Errorf("%s: no archives defined", fileName) } @@ -373,7 +415,7 @@ func parseRelease(baseDir, filePath string, data []byte) (*Release, error) { return release, err } -func parsePackage(baseDir, pkgName, pkgPath string, data []byte) (*Package, error) { +func parsePackage(format, pkgName, pkgPath string, data []byte) (*Package, error) { pkg := Package{ Name: pkgName, Path: pkgPath, @@ -390,16 +432,31 @@ func parsePackage(baseDir, pkgName, pkgPath string, data []byte) (*Package, erro if yamlPkg.Name != pkg.Name { return nil, fmt.Errorf("%s: filename and 'package' field (%q) disagree", pkgPath, yamlPkg.Name) } - if yamlPkg.V3Essential == nil { - yamlPkg.V3Essential = map[string]*yamlEssential{} - } - for _, refName := range yamlPkg.Essential { - if _, ok := yamlPkg.V3Essential[refName]; ok { - // This check is only needed because the list format can contain - // duplicates. It should be removed when format "v2" is deprecated. - return nil, fmt.Errorf("package %q repeats %s in essential fields", pkgName, refName) + + if format == "v1" || format == "v2" { + if len(yamlPkg.Essential.Values) > 0 && !yamlPkg.Essential.isList { + return nil, fmt.Errorf("cannot parse package %q: essential expects a list", pkgName) + } + for sliceName, yamlSlice := range yamlPkg.Slices { + if len(yamlSlice.Essential.Values) > 0 && !yamlSlice.Essential.isList { + return nil, fmt.Errorf("cannot parse slice %s: essential expects a list", SliceKey{pkgName, sliceName}) + } + } + } else { + if yamlPkg.V3Essential != nil { + return nil, fmt.Errorf("cannot parse package %q: v3-essential is deprecated since format v3", pkgName) + } + if len(yamlPkg.Essential.Values) > 0 && yamlPkg.Essential.isList { + return nil, fmt.Errorf("cannot parse package %q: essential expects a map", pkgName) + } + for sliceName, yamlSlice := range yamlPkg.Slices { + if yamlSlice.V3Essential != nil { + return nil, fmt.Errorf("cannot parse slice %s: v3-essential is deprecated since format v3", SliceKey{pkgName, sliceName}) + } + if len(yamlSlice.Essential.Values) > 0 && yamlSlice.Essential.isList { + return nil, fmt.Errorf("cannot parse slice %s: essential expects a map", SliceKey{pkgName, sliceName}) + } } - yamlPkg.V3Essential[refName] = &yamlEssential{} } pkg.Archive = yamlPkg.Archive @@ -416,58 +473,9 @@ func parsePackage(baseDir, pkgName, pkgPath string, data []byte) (*Package, erro Mutate: yamlSlice.Mutate, }, } - - if yamlSlice.V3Essential == nil { - yamlSlice.V3Essential = map[string]*yamlEssential{} - } - for _, refName := range yamlSlice.Essential { - if _, ok := yamlSlice.V3Essential[refName]; ok { - // This check is only needed because the list format can contain - // duplicates. It should be removed when format "v2" is deprecated. - return nil, fmt.Errorf("slice %s repeats %s in essential fields", slice, refName) - } - yamlSlice.V3Essential[refName] = &yamlEssential{} - } - for refName, essentialInfo := range yamlPkg.V3Essential { - sliceKey, err := ParseSliceKey(refName) - if err != nil { - return nil, fmt.Errorf("package %q has invalid essential slice reference: %q", pkgName, refName) - } - if sliceKey.Package == slice.Package && sliceKey.Slice == slice.Name { - // Do not add the slice to its own essentials list. - continue - } - if _, ok := slice.Essential[sliceKey]; ok { - return nil, fmt.Errorf("package %q repeats %s in essential fields", pkgName, refName) - } - if slice.Essential == nil { - slice.Essential = map[SliceKey]EssentialInfo{} - } - var archList []string - if essentialInfo != nil { - archList = essentialInfo.Arch.List - } - slice.Essential[sliceKey] = EssentialInfo{Arch: archList} - } - for refName, essentialInfo := range yamlSlice.V3Essential { - sliceKey, err := ParseSliceKey(refName) - if err != nil { - return nil, fmt.Errorf("package %q has invalid essential slice reference: %q", pkgName, refName) - } - if sliceKey.Package == slice.Package && sliceKey.Slice == slice.Name { - return nil, fmt.Errorf("cannot add slice to itself as essential %q in %s", refName, pkgPath) - } - if _, ok := slice.Essential[sliceKey]; ok { - return nil, fmt.Errorf("slice %s repeats %s in essential fields", slice, refName) - } - if slice.Essential == nil { - slice.Essential = map[SliceKey]EssentialInfo{} - } - var archList []string - if essentialInfo != nil { - archList = essentialInfo.Arch.List - } - slice.Essential[sliceKey] = EssentialInfo{Arch: archList} + err := parseEssentials(&yamlPkg, &yamlSlice, slice) + if err != nil { + return nil, err } if len(yamlSlice.Contents) > 0 { @@ -581,7 +589,7 @@ func parsePackage(baseDir, pkgName, pkgPath string, data []byte) (*Package, erro pkg.Slices[sliceName] = slice } - return &pkg, err + return &pkg, nil } // validateGeneratePath validates that the path follows the following format: @@ -631,12 +639,14 @@ func pathInfoToYAML(pi *PathInfo) (*yamlPath, error) { // sliceToYAML converts a Slice object to a yamlSlice object. func sliceToYAML(s *Slice) (*yamlSlice, error) { slice := &yamlSlice{ - Contents: make(map[string]*yamlPath, len(s.Contents)), - Mutate: s.Scripts.Mutate, - V3Essential: make(map[string]*yamlEssential, len(s.Essential)), + Contents: make(map[string]*yamlPath, len(s.Contents)), + Mutate: s.Scripts.Mutate, + Essential: yamlEssentialListMap{ + Values: make(map[string]*yamlEssential, len(s.Essential)), + }, } for key, info := range s.Essential { - slice.V3Essential[key.String()] = &yamlEssential{Arch: yamlArch{info.Arch}} + slice.Essential.Values[key.String()] = &yamlEssential{Arch: yamlArch{info.Arch}} } for path, info := range s.Contents { yamlPath, err := pathInfoToYAML(&info) @@ -752,3 +762,78 @@ var defaultMaintenance = map[string]Maintenance{ EndOfLife: time.Date(2026, time.January, 15, 0, 0, 0, 0, time.UTC), }, } + +// parseEssentials takes into account package-level and slice-level essentials, +// processes them to check they are valid and not duplicated and, if +// successful, adds them to slice. +func parseEssentials(yamlPkg *yamlPackage, yamlSlice *yamlSlice, slice *Slice) error { + addPackageEssential := func(refName string, essentialInfo *yamlEssential) error { + sliceKey, err := ParseSliceKey(refName) + if err != nil { + return fmt.Errorf("package %q has invalid essential slice reference: %q", yamlPkg.Name, refName) + } + if sliceKey.Package == slice.Package && sliceKey.Slice == slice.Name { + // Do not add the slice to its own essentials list. + return nil + } + if _, ok := slice.Essential[sliceKey]; ok { + return fmt.Errorf("package %q repeats %s in essential fields", yamlPkg.Name, refName) + } + if slice.Essential == nil { + slice.Essential = map[SliceKey]EssentialInfo{} + } + var archList []string + if essentialInfo != nil { + archList = essentialInfo.Arch.List + } + slice.Essential[sliceKey] = EssentialInfo{Arch: archList} + return nil + } + addSliceEssential := func(refName string, essentialInfo *yamlEssential) error { + sliceKey, err := ParseSliceKey(refName) + if err != nil { + return fmt.Errorf("package %q has invalid essential slice reference: %q", yamlPkg.Name, refName) + } + if sliceKey.Package == slice.Package && sliceKey.Slice == slice.Name { + return fmt.Errorf("package %q: cannot add slice to itself as essential %q", yamlPkg.Name, refName) + } + if _, ok := slice.Essential[sliceKey]; ok { + return fmt.Errorf("slice %s repeats %s in essential fields", slice, refName) + } + if slice.Essential == nil { + slice.Essential = map[SliceKey]EssentialInfo{} + } + var archList []string + if essentialInfo != nil { + archList = essentialInfo.Arch.List + } + slice.Essential[sliceKey] = EssentialInfo{Arch: archList} + return nil + } + + for refName, essentialInfo := range yamlPkg.Essential.Values { + err := addPackageEssential(refName, essentialInfo) + if err != nil { + return err + } + } + for refName, essentialInfo := range yamlPkg.V3Essential { + err := addPackageEssential(refName, essentialInfo) + if err != nil { + return err + } + } + for refName, essentialInfo := range yamlSlice.Essential.Values { + err := addSliceEssential(refName, essentialInfo) + if err != nil { + return err + } + } + for refName, essentialInfo := range yamlSlice.V3Essential { + err := addSliceEssential(refName, essentialInfo) + if err != nil { + return err + } + } + return nil +}