github.com/wolfi-dev/wolfictl@v0.16.11/pkg/dag/packages.go (about) 1 package dag 2 3 import ( 4 "context" 5 "fmt" 6 "io/fs" 7 "path/filepath" 8 "sort" 9 "strconv" 10 "strings" 11 12 "chainguard.dev/melange/pkg/build" 13 "chainguard.dev/melange/pkg/config" 14 "github.com/chainguard-dev/clog" 15 apk "github.com/chainguard-dev/go-apk/pkg/apk" 16 ) 17 18 const ( 19 Local = "local" 20 ) 21 22 // Configuration represents a configuration along with the file that sourced it. 23 // It can be for an origin package, a subpackage, or something that is provided by a package. 24 // The Configuration field is a pointer to the actual configuration as parsed from a file. The Path field is the 25 // path to the file from which the configuration was parsed. The Name and Version fields are the name and version 26 // of the package, subpackage, or provided item. In the case of an origin package, the Name field 27 // is the same as the Configuration.Package.Name field, and the Version field is the same as 28 // the Configuration.Package.Version field with the epoch added as `-r<epoch>`. In the case of a 29 // subpackage or provided item, the Name and Version fields may be different. 30 type Configuration struct { 31 *config.Configuration 32 Path string 33 name string 34 version string 35 36 // the actual package or subpackage name providing this configuration 37 // this allows us to distinguish between a subpackge that is providing a virtual and providing itself 38 pkg string 39 } 40 41 func (c Configuration) String() string { 42 return fmt.Sprintf("%s-%s", c.name, c.version) 43 } 44 45 func (c Configuration) Name() string { 46 return c.name 47 } 48 49 func (c Configuration) Version() string { 50 return c.version 51 } 52 53 func (c Configuration) Source() string { 54 return Local 55 } 56 57 func (c Configuration) FullName() string { 58 return fmt.Sprintf("%s-%s-r%d", c.name, c.version, c.Package.Epoch) 59 } 60 61 func (c Configuration) Resolved() bool { 62 return true 63 } 64 65 // Packages represents a set of package configurations, including 66 // the parent, or origin, package, its subpackages, and whatever else it 'provides'. 67 // It contains references from each such origin package, subpackage and provides 68 // to the origin config. 69 // 70 // It also maintains a list of the origin packages. 71 // 72 // It does not try to determine relationships and dependencies between packages. For that, 73 // pass a Packages to NewGraph. 74 type Packages struct { 75 configs map[string][]*Configuration 76 packages map[string][]*Configuration 77 index map[string]*Configuration 78 } 79 80 var ErrMultipleConfigurations = fmt.Errorf("multiple configurations using the same package name") 81 82 func (p *Packages) addPackage(name string, configuration *Configuration) error { 83 if _, exists := p.packages[name]; exists { 84 return fmt.Errorf("%s: %w", name, ErrMultipleConfigurations) 85 } 86 87 p.packages[name] = append(p.packages[name], configuration) 88 89 return nil 90 } 91 92 func (p *Packages) addConfiguration(name string, configuration *Configuration) error { 93 p.configs[name] = append(p.configs[name], configuration) 94 p.index[configuration.String()] = configuration 95 96 return nil 97 } 98 99 func (p *Packages) addProvides(c *Configuration, provides []string) error { 100 for _, prov := range provides { 101 pctx := &build.PipelineBuild{ 102 Build: &build.Build{ 103 Configuration: *c.Configuration, 104 }, 105 Package: &c.Package, 106 } 107 template, err := build.MutateWith(pctx, nil) 108 if err != nil { 109 return err 110 } 111 for tmpl, val := range template { 112 prov = strings.ReplaceAll(prov, tmpl, val) 113 } 114 name, version := packageNameFromProvides(prov) 115 if version == "" { 116 version = c.version 117 } 118 providesc := &Configuration{ 119 Configuration: c.Configuration, 120 Path: c.Path, 121 name: name, 122 version: version, // provides can have own version or inherit package's version 123 pkg: c.pkg, 124 } 125 if err := p.addConfiguration(name, providesc); err != nil { 126 return err 127 } 128 } 129 return nil 130 } 131 132 // NewPackages reads an fs.FS to get all of the Melange configuration yamls in 133 // the given directory, and then parses them, including their subpackages and 134 // 'provides' parameters, to create a Packages struct with all of the 135 // information, as well as the list of original packages, and, for each such 136 // package, the source path (yaml) from which it came. The result is a Packages 137 // struct. 138 // 139 // The input is any fs.FS filesystem implementation. Given a directory path, you 140 // can call NewPackages like this: 141 // 142 // NewPackages(ctx, os.DirFS("/path/to/dir"), "/path/to/dir", "./pipelines") 143 // 144 // The repetition of the path is necessary because of how the upstream parser in 145 // melange requires the full path to the directory to be passed in. 146 func NewPackages(ctx context.Context, fsys fs.FS, dirPath, pipelineDir string) (*Packages, error) { 147 log := clog.FromContext(ctx) 148 149 pkgs := &Packages{ 150 configs: make(map[string][]*Configuration), 151 packages: make(map[string][]*Configuration), 152 index: make(map[string]*Configuration), 153 } 154 err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { 155 if err != nil { 156 return err 157 } 158 159 // Skip anything in .github/ and .git/ 160 if path == ".github" { 161 return fs.SkipDir 162 } 163 if path == ".git" { 164 return fs.SkipDir 165 } 166 167 // Skip .yam.yaml and .melange.k8s.yaml 168 if d.Type().IsRegular() && path == ".yam.yaml" { 169 return nil 170 } 171 if d.Type().IsRegular() && path == ".melange.k8s.yaml" { 172 return nil 173 } 174 175 // Skip any file that isn't a yaml file 176 if !d.Type().IsRegular() || !strings.HasSuffix(path, ".yaml") { 177 return nil 178 } 179 180 if filepath.Dir(path) != "." && !strings.HasSuffix(path, ".melange.yaml") { 181 log.With("path", path).Debug("skipping non-melange YAML file") 182 return nil 183 } 184 185 p := filepath.Join(dirPath, path) 186 buildc, err := config.ParseConfiguration(ctx, p) 187 if err != nil { 188 return err 189 } 190 c := &Configuration{ 191 Configuration: buildc, 192 Path: p, 193 name: buildc.Package.Name, 194 version: fullVersion(&buildc.Package), 195 pkg: buildc.Package.Name, 196 } 197 198 name := c.name 199 if name == "" { 200 return fmt.Errorf("no package name in %q", path) 201 } 202 if err := pkgs.addConfiguration(name, c); err != nil { 203 return err 204 } 205 if err := pkgs.addPackage(name, c); err != nil { 206 return err 207 } 208 if err := pkgs.addProvides(c, c.Package.Dependencies.Provides); err != nil { 209 return err 210 } 211 212 for i := range c.Subpackages { 213 subpkg := c.Subpackages[i] 214 name := subpkg.Name 215 if name == "" { 216 return fmt.Errorf("empty subpackage name at index %d for package %q", i, c.Package.Name) 217 } 218 c := &Configuration{ 219 Configuration: buildc, 220 Path: p, 221 name: name, 222 version: fullVersion(&buildc.Package), // subpackages have same version as origin 223 pkg: name, 224 } 225 if err := pkgs.addConfiguration(name, c); err != nil { 226 return err 227 } 228 if err := pkgs.addProvides(c, subpkg.Dependencies.Provides); err != nil { 229 return err 230 } 231 232 // TODO: resolve deps via `uses` for subpackage pipelines. 233 } 234 // Resolve all `uses` used by the pipeline. This updates the set of 235 // .environment.contents.packages so the next block can include those as build deps. 236 pctx := &build.PipelineBuild{ 237 Build: &build.Build{ 238 PipelineDirs: []string{pipelineDir}, 239 Configuration: *c.Configuration, 240 }, 241 Package: &c.Package, 242 } 243 for i := range c.Pipeline { 244 s := &build.PipelineContext{Environment: &pctx.Build.Configuration.Environment, PipelineDirs: []string{pipelineDir}, Pipeline: &c.Pipeline[i]} 245 if err := s.ApplyNeeds(ctx, pctx); err != nil { 246 return fmt.Errorf("unable to resolve needs for package %s: %w", name, err) 247 } 248 c.Environment.Contents.Packages = pctx.Build.Configuration.Environment.Contents.Packages 249 } 250 251 return nil 252 }) 253 if err != nil { 254 return nil, err 255 } 256 257 return pkgs, nil 258 } 259 260 // Config returns the Melange configuration for the package, provides or 261 // subpackage with the given name, if the package is present in the Graph. If 262 // it's not present, Config returns an empty list. 263 // 264 // Pass packageOnly=true to restruct it just to origin package names. 265 func (p Packages) Config(name string, packageOnly bool) []*Configuration { 266 if p.configs == nil { 267 // this would be unexpected 268 return nil 269 } 270 var ( 271 c []*Configuration 272 ok bool 273 ) 274 if packageOnly { 275 c, ok = p.packages[name] 276 } else { 277 c, ok = p.configs[name] 278 } 279 if !ok { 280 return nil 281 } 282 list := make([]*Configuration, 0, len(c)) 283 list = append(list, c...) 284 285 // sort the list by increasing version 286 // this should be better about this, perhaps we will use the apko version sorting library in a future revision 287 sort.Slice(list, func(i, j int) bool { 288 return fullVersion(&list[i].Package) < fullVersion(&list[j].Package) 289 }) 290 return list 291 } 292 293 func (p Packages) ConfigByKey(key string) *Configuration { 294 if len(p.index) == 0 { 295 return nil 296 } 297 c, ok := p.index[key] 298 if !ok { 299 return nil 300 } 301 return c 302 } 303 304 // PkgConfig returns the melange Configuration for a given package name. 305 func (p Packages) PkgConfig(pkgName string) *Configuration { 306 for _, cfg := range p.packages[pkgName] { 307 if pkgName == cfg.Package.Name { 308 return cfg 309 } 310 } 311 return nil 312 } 313 314 // PkgInfo returns the build.Package struct for a given package name. 315 // If no such package name is found in the packages, return nil package and nil error. 316 func (p Packages) PkgInfo(pkgName string) *config.Package { 317 if cfg := p.PkgConfig(pkgName); cfg != nil { 318 return &cfg.Package 319 } 320 return nil 321 } 322 323 // Packages returns a slice of every package and subpackage available in the Packages struct, 324 // sorted alphabetically and then by version, with each package converted to a *apk.RepositoryPackage. 325 func (p Packages) Packages() []*Configuration { 326 allPackages := make([]*Configuration, 0, len(p.packages)) 327 for _, byVersion := range p.packages { 328 allPackages = append(allPackages, byVersion...) 329 } 330 331 // sort for deterministic output 332 sort.Slice(allPackages, func(i, j int) bool { 333 if allPackages[i].name == allPackages[j].name { 334 return allPackages[i].version < allPackages[j].version 335 } 336 return allPackages[i].name < allPackages[j].name 337 }) 338 return allPackages 339 } 340 341 // PackageNames returns a slice of the names of all packages, sorted alphabetically. 342 func (p Packages) PackageNames() []string { 343 allPackages := make([]string, 0, len(p.packages)) 344 for name := range p.packages { 345 allPackages = append(allPackages, name) 346 } 347 348 // sort for deterministic output 349 sort.Strings(allPackages) 350 return allPackages 351 } 352 353 // Sub returns a new Packages whose members are the named packages or provides that are listed. 354 // If a listed element is a provides, automatically includes the origin package that provides it. 355 // If a listed element is a subpackage, automatically includes the origin package that contains it. 356 // If a listed element does not exist, returns an error. 357 func (p Packages) Sub(names ...string) (*Packages, error) { 358 pkgs := &Packages{ 359 configs: make(map[string][]*Configuration), 360 index: make(map[string]*Configuration), 361 packages: make(map[string][]*Configuration), 362 } 363 for _, name := range names { 364 if c, ok := p.configs[name]; ok { 365 for _, config := range c { 366 if err := pkgs.addConfiguration(name, config); err != nil { 367 return nil, err 368 } 369 if err := pkgs.addPackage(name, config); err != nil { 370 return nil, err 371 } 372 } 373 } else { 374 return nil, fmt.Errorf("package %q not found", name) 375 } 376 } 377 return pkgs, nil 378 } 379 380 func wantArch(have string, want []string) bool { 381 if len(want) == 0 { 382 return true 383 } 384 385 for _, a := range want { 386 if a == have { 387 return true 388 } 389 } 390 391 return false 392 } 393 394 // WithArch returns a new Packages whose members are valid for the given arch. 395 func (p Packages) WithArch(arch string) (*Packages, error) { 396 pkgs := &Packages{ 397 configs: make(map[string][]*Configuration), 398 index: p.index, 399 packages: make(map[string][]*Configuration), 400 } 401 402 for name, c := range p.configs { 403 for _, config := range c { 404 if !wantArch(arch, config.Package.TargetArchitecture) { 405 continue 406 } 407 if err := pkgs.addConfiguration(name, config); err != nil { 408 return nil, err 409 } 410 } 411 } 412 413 for name, c := range p.packages { 414 for _, config := range c { 415 if !wantArch(arch, config.Package.TargetArchitecture) { 416 continue 417 } 418 if err := pkgs.addPackage(name, config); err != nil { 419 return nil, err 420 } 421 } 422 } 423 return pkgs, nil 424 } 425 426 // Repository provide the Packages as a apk.RepositoryWithIndex. To be used in other places that require 427 // using alpine/go structs instead of ours. 428 func (p Packages) Repository(arch string) apk.NamedIndex { 429 repo := apk.NewRepositoryFromComponents(Local, "latest", "", arch) 430 431 // Precompute the number of packages to avoid growslice. 432 size := 0 433 for _, byVersion := range p.packages { 434 for _, config := range byVersion { 435 size++ // top-level package 436 size += len(config.Subpackages) 437 } 438 } 439 440 packages := make([]*apk.Package, 0, size) 441 for _, byVersion := range p.packages { 442 for _, cfg := range byVersion { 443 cfg := cfg 444 packages = append(packages, &apk.Package{ 445 Arch: arch, 446 Name: cfg.Package.Name, 447 Version: fullVersion(&cfg.Package), 448 Description: cfg.Package.Description, 449 License: cfg.Package.LicenseExpression(), 450 Origin: cfg.Package.Name, 451 URL: cfg.Package.URL, 452 Dependencies: cfg.Environment.Contents.Packages, 453 Provides: cfg.Package.Dependencies.Provides, 454 RepoCommit: cfg.Package.Commit, 455 }) 456 for i := range cfg.Subpackages { 457 sub := cfg.Subpackages[i] 458 packages = append(packages, &apk.Package{ 459 Arch: arch, 460 Name: sub.Name, 461 Version: fullVersion(&cfg.Package), 462 Description: sub.Description, 463 License: cfg.Package.LicenseExpression(), 464 Origin: cfg.Package.Name, 465 URL: cfg.Package.URL, 466 Dependencies: cfg.Environment.Contents.Packages, 467 Provides: sub.Dependencies.Provides, 468 RepoCommit: sub.Commit, 469 }) 470 } 471 } 472 } 473 index := &apk.APKIndex{ 474 Description: "local repository", 475 Packages: packages, 476 } 477 478 return apk.NewNamedRepositoryWithIndex("", repo.WithIndex(index)) 479 } 480 481 func packageNameFromProvides(prov string) (name, version string) { 482 var ok bool 483 if name, version, ok = strings.Cut(prov, "~="); ok { 484 return 485 } 486 if name, version, ok = strings.Cut(prov, "="); ok { 487 return 488 } 489 name = prov 490 return 491 } 492 493 func fullVersion(pkg *config.Package) string { 494 return pkg.Version + "-r" + strconv.FormatUint(pkg.Epoch, 10) 495 }