Skip to content

Commit 6d192e0

Browse files
committed
Add SubPackage type and Target.Packages field
Introduce SubPackage and SubPackageDependencies types for supplemental package support (subpackages from shared builds). Add Packages field to Target, validation in Spec.Validate(), and full unit test coverage. Signed-off-by: Brian Goff <cpuguy83@gmail.com>
1 parent 61c48d8 commit 6d192e0

6 files changed

Lines changed: 780 additions & 15 deletions

File tree

artifacts.go

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,21 +51,12 @@ type Artifacts struct {
5151
// Groups is a list of groups to add to the system when the package is installed.
5252
Groups []AddGroupConfig `yaml:"groups,omitempty" json:"groups,omitempty"`
5353

54-
// DisableStrip is used to disable stripping of artifacts.
54+
// DisableStrip disables stripping of root package artifacts.
55+
// It is not supported in subpackages.
5556
DisableStrip bool `yaml:"disable_strip,omitempty" json:"disable_strip,omitempty"`
5657

57-
// DisableAutoRequires is used to disable automatic dependency discovery for
58-
// the produced package.
59-
//
60-
// Some tooling, such as `rpmbuild`, will look at all artifacts and
61-
// automatically inject missing dependencies into the package metadata.
62-
// For instance, if you include a `.sh` script, rpmbuild with automatically
63-
// add `bash` as a dependency for the package.
64-
// It also does this for libraries being linked against.
65-
//
66-
// This is useful if you want to have more control over the dependencies
67-
// that are included in the package.
68-
// However, you must be careful to manually include all dependencies that are required.
58+
// DisableAutoRequires disables automatic dependency discovery for package
59+
// artifacts. Required dependencies must be declared explicitly when it is enabled.
6960
DisableAutoRequires bool `yaml:"disable_auto_requires,omitempty" json:"disable_auto_requires,omitempty"`
7061
}
7162

docs/spec.schema.json

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,13 +307,13 @@
307307
"type": [
308308
"boolean"
309309
],
310-
"description": "DisableAutoRequires is used to disable automatic dependency discovery for\nthe produced package.\n\nSome tooling, such as `rpmbuild`, will look at all artifacts and\nautomatically inject missing dependencies into the package metadata.\nFor instance, if you include a `.sh` script, rpmbuild with automatically\nadd `bash` as a dependency for the package.\nIt also does this for libraries being linked against.\n\nThis is useful if you want to have more control over the dependencies\nthat are included in the package.\nHowever, you must be careful to manually include all dependencies that are required."
310+
"description": "DisableAutoRequires disables automatic dependency discovery for package\nartifacts. Required dependencies must be declared explicitly when it is enabled."
311311
},
312312
"disable_strip": {
313313
"type": [
314314
"boolean"
315315
],
316-
"description": "DisableStrip is used to disable stripping of artifacts."
316+
"description": "DisableStrip disables stripping of root package artifacts.\nIt is not supported in subpackages."
317317
},
318318
"docs": {
319319
"additionalProperties": {
@@ -2209,6 +2209,74 @@
22092209
],
22102210
"description": "Spec is the specification for a package build."
22112211
},
2212+
"SubPackage": {
2213+
"required": [
2214+
"description"
2215+
],
2216+
"properties": {
2217+
"artifacts": {
2218+
"$ref": "#/$defs/Artifacts",
2219+
"description": "Artifacts specifies which build outputs go into this supplemental package.\nThis is self-contained — no artifacts are inherited from the primary package."
2220+
},
2221+
"conflicts": {
2222+
"$ref": "#/$defs/PackageDependencyList",
2223+
"description": "Conflicts is the list of packages that conflict with this supplemental package."
2224+
},
2225+
"dependencies": {
2226+
"$ref": "#/$defs/SubPackageDependencies",
2227+
"description": "Dependencies specifies runtime dependencies for this supplemental package.\nOnly runtime and recommends dependencies are allowed; build dependencies\nare shared with the primary package."
2228+
},
2229+
"description": {
2230+
"type": [
2231+
"string"
2232+
],
2233+
"description": "Description is the package description. This is required — both RPM and\nDebian require a description/summary for every subpackage.\nBuild argument references are not substituted in this field."
2234+
},
2235+
"name": {
2236+
"type": [
2237+
"string",
2238+
"null"
2239+
],
2240+
"description": "Name overrides the default package name.\nBy default, the package name is \"\u003cparent\u003e-\u003ckey\u003e\" where \u003ckey\u003e is the map\nkey under which this SubPackage is defined. Set this to use a fully custom\npackage name instead. Build argument references are not substituted in this field."
2241+
},
2242+
"provides": {
2243+
"$ref": "#/$defs/PackageDependencyList",
2244+
"description": "Provides is the list of things this supplemental package provides."
2245+
},
2246+
"replaces": {
2247+
"$ref": "#/$defs/PackageDependencyList",
2248+
"description": "Replaces is the list of packages that this supplemental package replaces/obsoletes."
2249+
}
2250+
},
2251+
"additionalProperties": {
2252+
"not": {}
2253+
},
2254+
"type": [
2255+
"object",
2256+
"null"
2257+
],
2258+
"description": "SubPackage defines a supplemental package produced from the same build output as the primary package."
2259+
},
2260+
"SubPackageDependencies": {
2261+
"properties": {
2262+
"recommends": {
2263+
"$ref": "#/$defs/PackageDependencyList",
2264+
"description": "Recommends is the list of packages recommended to install with the supplemental package.\nNote: Not all package managers support this (e.g. rpm)"
2265+
},
2266+
"runtime": {
2267+
"$ref": "#/$defs/PackageDependencyList",
2268+
"description": "Runtime is the list of packages required to install/run the supplemental package."
2269+
}
2270+
},
2271+
"additionalProperties": {
2272+
"not": {}
2273+
},
2274+
"type": [
2275+
"object",
2276+
"null"
2277+
],
2278+
"description": "SubPackageDependencies contains only the dependency fields valid for supplemental packages."
2279+
},
22122280
"SymlinkTarget": {
22132281
"required": [
22142282
"path",
@@ -2386,6 +2454,16 @@
23862454
"$ref": "#/$defs/PackageConfig",
23872455
"description": "PackageConfig is the configuration to use for artifact targets, such as\nrpms, debs, or zip files containing Windows binaries"
23882456
},
2457+
"packages": {
2458+
"additionalProperties": {
2459+
"$ref": "#/$defs/SubPackage"
2460+
},
2461+
"type": [
2462+
"object",
2463+
"null"
2464+
],
2465+
"description": "Packages defines supplemental packages produced from the same build output\nas the primary package. Each entry produces an additional package with its\nown artifact selection, runtime dependencies, and metadata.\nThe map key is used as a suffix for the default package name (\"\u003cspecName\u003e-\u003ckey\u003e\")."
2466+
},
23892467
"provides": {
23902468
"$ref": "#/$defs/PackageDependencyList",
23912469
"description": "Provides is the list of packages that this target provides."

load.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,9 @@ func (s Spec) Validate() error {
579579
if err := t.validate(); err != nil {
580580
errs = append(errs, errors.Wrapf(err, "target %s", k))
581581
}
582+
if err := validateSubPackageNames(s.Name, k, t.Packages); err != nil {
583+
errs = append(errs, err)
584+
}
582585
}
583586

584587
if err := s.Image.validate(); err != nil {

subpackage.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package dalec
2+
3+
import (
4+
goerrors "errors"
5+
"fmt"
6+
"iter"
7+
8+
"github.com/moby/buildkit/frontend/dockerfile/shell"
9+
"github.com/pkg/errors"
10+
)
11+
12+
// SubPackage defines a supplemental package produced from the same build
13+
// output as the primary package. Each supplemental package has its own
14+
// artifact selection, runtime dependencies, and package metadata.
15+
//
16+
// Supplemental packages share the primary package's build steps, sources,
17+
// version, revision, license, vendor, and website. They cannot override
18+
// these fields.
19+
type SubPackage struct {
20+
// Name overrides the default package name.
21+
// By default, the package name is "<parent>-<key>" where <key> is the map
22+
// key under which this SubPackage is defined. Set this to use a fully custom
23+
// package name instead. Build argument references are not substituted in this field.
24+
Name string `yaml:"name,omitempty" json:"name,omitempty"`
25+
26+
// Description is the package description. This is required — both RPM and
27+
// Debian require a description/summary for every subpackage.
28+
// Build argument references are not substituted in this field.
29+
Description string `yaml:"description" json:"description" jsonschema:"required"`
30+
31+
// Artifacts specifies which build outputs go into this supplemental package.
32+
// This is self-contained — no artifacts are inherited from the primary package.
33+
Artifacts *Artifacts `yaml:"artifacts,omitempty" json:"artifacts,omitempty"`
34+
35+
// Dependencies specifies runtime dependencies for this supplemental package.
36+
// Only runtime and recommends dependencies are allowed; build dependencies
37+
// are shared with the primary package.
38+
Dependencies *SubPackageDependencies `yaml:"dependencies,omitempty" json:"dependencies,omitempty"`
39+
40+
// Conflicts is the list of packages that conflict with this supplemental package.
41+
Conflicts PackageDependencyList `yaml:"conflicts,omitempty" json:"conflicts,omitempty"`
42+
43+
// Provides is the list of things this supplemental package provides.
44+
Provides PackageDependencyList `yaml:"provides,omitempty" json:"provides,omitempty"`
45+
46+
// Replaces is the list of packages that this supplemental package replaces/obsoletes.
47+
Replaces PackageDependencyList `yaml:"replaces,omitempty" json:"replaces,omitempty"`
48+
}
49+
50+
// ResolvedName returns the package name that this SubPackage will produce.
51+
// If [SubPackage.Name] is set, it is returned as-is.
52+
// Otherwise, the name is "<parentName>-<mapKey>".
53+
func (s *SubPackage) ResolvedName(parentName, mapKey string) string {
54+
if s.Name != "" {
55+
return s.Name
56+
}
57+
return parentName + "-" + mapKey
58+
}
59+
60+
// SubPackageDependencies contains only the dependency fields valid for
61+
// supplemental packages. Build dependencies are shared with the primary
62+
// package and cannot be overridden.
63+
type SubPackageDependencies struct {
64+
// Runtime is the list of packages required to install/run the supplemental package.
65+
Runtime PackageDependencyList `yaml:"runtime,omitempty" json:"runtime,omitempty"`
66+
// Recommends is the list of packages recommended to install with the supplemental package.
67+
// Note: Not all package managers support this (e.g. rpm)
68+
Recommends PackageDependencyList `yaml:"recommends,omitempty" json:"recommends,omitempty"`
69+
}
70+
71+
func (d *SubPackageDependencies) GetRuntime() PackageDependencyList {
72+
if d == nil {
73+
return nil
74+
}
75+
return d.Runtime
76+
}
77+
78+
func (d *SubPackageDependencies) GetRecommends() PackageDependencyList {
79+
if d == nil {
80+
return nil
81+
}
82+
return d.Recommends
83+
}
84+
85+
func (d *SubPackageDependencies) processBuildArgs(lex *shell.Lex, args map[string]string, allowArg func(string) bool) error {
86+
if d == nil {
87+
return nil
88+
}
89+
90+
var errs []error
91+
for k, v := range d.Runtime {
92+
for i, ver := range v.Version {
93+
updated, err := expandArgs(lex, ver, args, allowArg)
94+
if err != nil {
95+
errs = append(errs, errors.Wrapf(err, "runtime version %s", ver))
96+
continue
97+
}
98+
v.Version[i] = updated
99+
}
100+
d.Runtime[k] = v
101+
}
102+
103+
for k, v := range d.Recommends {
104+
for i, ver := range v.Version {
105+
updated, err := expandArgs(lex, ver, args, allowArg)
106+
if err != nil {
107+
errs = append(errs, errors.Wrapf(err, "recommends version %s", ver))
108+
continue
109+
}
110+
v.Version[i] = updated
111+
}
112+
d.Recommends[k] = v
113+
}
114+
115+
return goerrors.Join(errs...)
116+
}
117+
118+
// GetSubPackagesForTarget returns the supplemental packages defined for the
119+
// given target in map-key order. A missing target or a target without
120+
// supplemental packages yields no values.
121+
func GetSubPackagesForTarget(spec *Spec, target string) iter.Seq2[string, SubPackage] {
122+
return SortedMapIter(spec.Targets[target].Packages)
123+
}
124+
125+
func (s *SubPackage) validate() error {
126+
var errs []error
127+
128+
if s.Description == "" {
129+
errs = append(errs, fmt.Errorf("description is required"))
130+
}
131+
132+
if s.Artifacts != nil {
133+
// Subpackages deliberately reuse the root Artifacts public model. DisableStrip
134+
// is currently its sole unsupported field, so accepting one representable
135+
// invalid state avoids maintaining nearly identical public types.
136+
if s.Artifacts.DisableStrip {
137+
errs = append(errs, fmt.Errorf("artifacts: disable_strip is only valid for root package artifacts"))
138+
}
139+
if err := s.Artifacts.validate(); err != nil {
140+
errs = append(errs, errors.Wrap(err, "artifacts"))
141+
}
142+
}
143+
144+
return goerrors.Join(errs...)
145+
}
146+
147+
func (s *SubPackage) processBuildArgs(lex *shell.Lex, args map[string]string, allowArg func(string) bool) error {
148+
var errs []error
149+
150+
if err := s.Dependencies.processBuildArgs(lex, args, allowArg); err != nil {
151+
errs = append(errs, errors.Wrap(err, "dependencies"))
152+
}
153+
154+
for k, v := range s.Conflicts {
155+
for i, ver := range v.Version {
156+
updated, err := expandArgs(lex, ver, args, allowArg)
157+
if err != nil {
158+
errs = append(errs, errors.Wrapf(err, "conflicts %s version %d", k, i))
159+
continue
160+
}
161+
s.Conflicts[k].Version[i] = updated
162+
}
163+
}
164+
165+
for k, v := range s.Provides {
166+
for i, ver := range v.Version {
167+
updated, err := expandArgs(lex, ver, args, allowArg)
168+
if err != nil {
169+
errs = append(errs, errors.Wrapf(err, "provides %s version %d", k, i))
170+
continue
171+
}
172+
s.Provides[k].Version[i] = updated
173+
}
174+
}
175+
176+
for k, v := range s.Replaces {
177+
for i, ver := range v.Version {
178+
updated, err := expandArgs(lex, ver, args, allowArg)
179+
if err != nil {
180+
errs = append(errs, errors.Wrapf(err, "replaces %s version %d", k, i))
181+
continue
182+
}
183+
s.Replaces[k].Version[i] = updated
184+
}
185+
}
186+
187+
return goerrors.Join(errs...)
188+
}
189+
190+
// validateSubPackageNames checks that no two supplemental packages in the same
191+
// target resolve to the same name, and that no supplemental package name
192+
// conflicts with the primary package name.
193+
func validateSubPackageNames(specName, targetName string, packages map[string]SubPackage) error {
194+
if len(packages) == 0 {
195+
return nil
196+
}
197+
198+
var errs []error
199+
seen := make(map[string]string, len(packages)) // resolved name → map key
200+
201+
for key, pkg := range packages {
202+
resolved := pkg.ResolvedName(specName, key)
203+
204+
if resolved == specName {
205+
errs = append(errs, fmt.Errorf("target %s: package %q resolves to name %q which conflicts with the primary package name", targetName, key, resolved))
206+
}
207+
208+
if prevKey, exists := seen[resolved]; exists {
209+
errs = append(errs, fmt.Errorf("target %s: packages %q and %q both resolve to the same name %q", targetName, prevKey, key, resolved))
210+
}
211+
seen[resolved] = key
212+
}
213+
214+
return goerrors.Join(errs...)
215+
}

0 commit comments

Comments
 (0)