|
| 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