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
17 changes: 4 additions & 13 deletions artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,12 @@ type Artifacts struct {
// Groups is a list of groups to add to the system when the package is installed.
Groups []AddGroupConfig `yaml:"groups,omitempty" json:"groups,omitempty"`

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

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

Expand Down
82 changes: 80 additions & 2 deletions docs/spec.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -307,13 +307,13 @@
"type": [
"boolean"
],
"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."
"description": "DisableAutoRequires disables automatic dependency discovery for package\nartifacts. Required dependencies must be declared explicitly when it is enabled."
},
"disable_strip": {
"type": [
"boolean"
],
"description": "DisableStrip is used to disable stripping of artifacts."
"description": "DisableStrip disables stripping of root package artifacts.\nIt is not supported in subpackages."
},
"docs": {
"additionalProperties": {
Expand Down Expand Up @@ -2209,6 +2209,74 @@
],
"description": "Spec is the specification for a package build."
},
"SubPackage": {
"required": [
"description"
],
"properties": {
"artifacts": {
"$ref": "#/$defs/Artifacts",
"description": "Artifacts specifies which build outputs go into this supplemental package.\nThis is self-contained — no artifacts are inherited from the primary package."
},
"conflicts": {
"$ref": "#/$defs/PackageDependencyList",
"description": "Conflicts is the list of packages that conflict with this supplemental package."
},
"dependencies": {
"$ref": "#/$defs/SubPackageDependencies",
"description": "Dependencies specifies runtime dependencies for this supplemental package.\nOnly runtime and recommends dependencies are allowed; build dependencies\nare shared with the primary package."
},
"description": {
"type": [
"string"
],
"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."
},
"name": {
"type": [
"string",
"null"
],
"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."
},
"provides": {
"$ref": "#/$defs/PackageDependencyList",
"description": "Provides is the list of things this supplemental package provides."
},
"replaces": {
"$ref": "#/$defs/PackageDependencyList",
"description": "Replaces is the list of packages that this supplemental package replaces/obsoletes."
}
},
"additionalProperties": {
"not": {}
},
"type": [
"object",
"null"
],
"description": "SubPackage defines a supplemental package produced from the same build output as the primary package."
},
"SubPackageDependencies": {
"properties": {
"recommends": {
"$ref": "#/$defs/PackageDependencyList",
"description": "Recommends is the list of packages recommended to install with the supplemental package.\nNote: Not all package managers support this (e.g. rpm)"
},
"runtime": {
"$ref": "#/$defs/PackageDependencyList",
"description": "Runtime is the list of packages required to install/run the supplemental package."
}
},
"additionalProperties": {
"not": {}
},
"type": [
"object",
"null"
],
"description": "SubPackageDependencies contains only the dependency fields valid for supplemental packages."
},
"SymlinkTarget": {
"required": [
"path",
Expand Down Expand Up @@ -2386,6 +2454,16 @@
"$ref": "#/$defs/PackageConfig",
"description": "PackageConfig is the configuration to use for artifact targets, such as\nrpms, debs, or zip files containing Windows binaries"
},
"packages": {
"additionalProperties": {
"$ref": "#/$defs/SubPackage"
},
"type": [
"object",
"null"
],
"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\")."
},
"provides": {
"$ref": "#/$defs/PackageDependencyList",
"description": "Provides is the list of packages that this target provides."
Expand Down
3 changes: 3 additions & 0 deletions load.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,9 @@ func (s Spec) Validate() error {
if err := t.validate(); err != nil {
errs = append(errs, errors.Wrapf(err, "target %s", k))
}
if err := validateSubPackageNames(s.Name, k, t.Packages); err != nil {
errs = append(errs, err)
}
}

if err := s.Image.validate(); err != nil {
Expand Down
215 changes: 215 additions & 0 deletions subpackage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package dalec

import (
goerrors "errors"
"fmt"
"iter"

"github.com/moby/buildkit/frontend/dockerfile/shell"
"github.com/pkg/errors"
)

// SubPackage defines a supplemental package produced from the same build
// output as the primary package. Each supplemental package has its own
// artifact selection, runtime dependencies, and package metadata.
//
// Supplemental packages share the primary package's build steps, sources,
// version, revision, license, vendor, and website. They cannot override
// these fields.
type SubPackage struct {
// Name overrides the default package name.
// By default, the package name is "<parent>-<key>" where <key> is the map
// key under which this SubPackage is defined. Set this to use a fully custom
// package name instead. Build argument references are not substituted in this field.
Name string `yaml:"name,omitempty" json:"name,omitempty"`

// Description is the package description. This is required — both RPM and
// Debian require a description/summary for every subpackage.
// Build argument references are not substituted in this field.
Description string `yaml:"description" json:"description" jsonschema:"required"`

// Artifacts specifies which build outputs go into this supplemental package.
// This is self-contained — no artifacts are inherited from the primary package.
Artifacts *Artifacts `yaml:"artifacts,omitempty" json:"artifacts,omitempty"`

// Dependencies specifies runtime dependencies for this supplemental package.
// Only runtime and recommends dependencies are allowed; build dependencies
// are shared with the primary package.
Dependencies *SubPackageDependencies `yaml:"dependencies,omitempty" json:"dependencies,omitempty"`

// Conflicts is the list of packages that conflict with this supplemental package.
Conflicts PackageDependencyList `yaml:"conflicts,omitempty" json:"conflicts,omitempty"`

// Provides is the list of things this supplemental package provides.
Provides PackageDependencyList `yaml:"provides,omitempty" json:"provides,omitempty"`

// Replaces is the list of packages that this supplemental package replaces/obsoletes.
Replaces PackageDependencyList `yaml:"replaces,omitempty" json:"replaces,omitempty"`
}

// ResolvedName returns the package name that this SubPackage will produce.
// If [SubPackage.Name] is set, it is returned as-is.
// Otherwise, the name is "<parentName>-<mapKey>".
func (s *SubPackage) ResolvedName(parentName, mapKey string) string {
if s.Name != "" {
return s.Name
}
return parentName + "-" + mapKey
}

// SubPackageDependencies contains only the dependency fields valid for
// supplemental packages. Build dependencies are shared with the primary
// package and cannot be overridden.
type SubPackageDependencies struct {
// Runtime is the list of packages required to install/run the supplemental package.
Runtime PackageDependencyList `yaml:"runtime,omitempty" json:"runtime,omitempty"`
// Recommends is the list of packages recommended to install with the supplemental package.
// Note: Not all package managers support this (e.g. rpm)
Recommends PackageDependencyList `yaml:"recommends,omitempty" json:"recommends,omitempty"`
}

func (d *SubPackageDependencies) GetRuntime() PackageDependencyList {
if d == nil {
return nil
}
return d.Runtime
}

func (d *SubPackageDependencies) GetRecommends() PackageDependencyList {
if d == nil {
return nil
}
return d.Recommends
}

func (d *SubPackageDependencies) processBuildArgs(lex *shell.Lex, args map[string]string, allowArg func(string) bool) error {
if d == nil {
return nil
}

var errs []error
for k, v := range d.Runtime {
for i, ver := range v.Version {
updated, err := expandArgs(lex, ver, args, allowArg)
if err != nil {
errs = append(errs, errors.Wrapf(err, "runtime version %s", ver))
continue
}
v.Version[i] = updated
}
d.Runtime[k] = v
}

for k, v := range d.Recommends {
for i, ver := range v.Version {
updated, err := expandArgs(lex, ver, args, allowArg)
if err != nil {
errs = append(errs, errors.Wrapf(err, "recommends version %s", ver))
continue
}
v.Version[i] = updated
}
d.Recommends[k] = v
}

return goerrors.Join(errs...)
}

// GetSubPackagesForTarget returns the supplemental packages defined for the
// given target in map-key order. A missing target or a target without
// supplemental packages yields no values.
func GetSubPackagesForTarget(spec *Spec, target string) iter.Seq2[string, SubPackage] {
return SortedMapIter(spec.Targets[target].Packages)
}

func (s *SubPackage) validate() error {
var errs []error

if s.Description == "" {
errs = append(errs, fmt.Errorf("description is required"))
}

if s.Artifacts != nil {
// Subpackages deliberately reuse the root Artifacts public model. DisableStrip
// is currently its sole unsupported field, so accepting one representable
// invalid state avoids maintaining nearly identical public types.
Comment thread
cpuguy83 marked this conversation as resolved.
if s.Artifacts.DisableStrip {
Comment thread
cpuguy83 marked this conversation as resolved.
errs = append(errs, fmt.Errorf("artifacts: disable_strip is only valid for root package artifacts"))
}
if err := s.Artifacts.validate(); err != nil {
errs = append(errs, errors.Wrap(err, "artifacts"))
}
}

return goerrors.Join(errs...)
}

func (s *SubPackage) processBuildArgs(lex *shell.Lex, args map[string]string, allowArg func(string) bool) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have some documentation on build args? Just looking at the code, build args seems to be related to commands which one runs to build the package, but I'm not sure that's the right thing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build-args are a docker-ism.
I imagine its just assumed in our code documentation as to what it is. I suppose it could use a formal definition.

var errs []error

if err := s.Dependencies.processBuildArgs(lex, args, allowArg); err != nil {
errs = append(errs, errors.Wrap(err, "dependencies"))
}

for k, v := range s.Conflicts {
for i, ver := range v.Version {
updated, err := expandArgs(lex, ver, args, allowArg)
if err != nil {
errs = append(errs, errors.Wrapf(err, "conflicts %s version %d", k, i))
continue
}
s.Conflicts[k].Version[i] = updated
}
}

for k, v := range s.Provides {
for i, ver := range v.Version {
updated, err := expandArgs(lex, ver, args, allowArg)
if err != nil {
errs = append(errs, errors.Wrapf(err, "provides %s version %d", k, i))
continue
}
s.Provides[k].Version[i] = updated
}
}

for k, v := range s.Replaces {
for i, ver := range v.Version {
updated, err := expandArgs(lex, ver, args, allowArg)
if err != nil {
errs = append(errs, errors.Wrapf(err, "replaces %s version %d", k, i))
continue
}
s.Replaces[k].Version[i] = updated
}
}

return goerrors.Join(errs...)
}

// validateSubPackageNames checks that no two supplemental packages in the same
// target resolve to the same name, and that no supplemental package name
// conflicts with the primary package name.
func validateSubPackageNames(specName, targetName string, packages map[string]SubPackage) error {
if len(packages) == 0 {
return nil
}

var errs []error
seen := make(map[string]string, len(packages)) // resolved name → map key

for key, pkg := range packages {
resolved := pkg.ResolvedName(specName, key)

if resolved == specName {
errs = append(errs, fmt.Errorf("target %s: package %q resolves to name %q which conflicts with the primary package name", targetName, key, resolved))
}

if prevKey, exists := seen[resolved]; exists {
errs = append(errs, fmt.Errorf("target %s: packages %q and %q both resolve to the same name %q", targetName, prevKey, key, resolved))
}
seen[resolved] = key
}

return goerrors.Join(errs...)
}
Loading