Skip to content

Commit d8c6a4b

Browse files
authored
feat(projectconfig): add package publish channel annotations to TOML schema (#38)
1 parent 730d14d commit d8c6a4b

File tree

14 files changed

+1223
-5
lines changed

14 files changed

+1223
-5
lines changed

docs/user/reference/config/components.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ A component definition tells azldev where to find the spec file, how to customiz
1212
| Overlays | `overlays` | array of [Overlay](overlays.md) | No | Modifications to apply to the spec and/or source files |
1313
| Build config | `build` | [BuildConfig](#build-configuration) | No | Build-time options (macros, conditionals, check config) |
1414
| Source files | `source-files` | array of [SourceFileReference](#source-file-references) | No | Additional source files to download for this component |
15+
| Default package config | `default-package-config` | [PackageConfig](package-groups.md#package-config) | No | Default configuration applied to all binary packages produced by this component; overrides project defaults and package-group defaults |
16+
| Package overrides | `packages` | map of string → [PackageConfig](package-groups.md#package-config) | No | Exact per-package configuration overrides; highest priority in the resolution order |
1517

1618
### Bare Components
1719

@@ -190,6 +192,58 @@ The `hints` field provides non-essential metadata about how or when to build a c
190192
hints = { expensive = true }
191193
```
192194

195+
## Package Configuration
196+
197+
Components can customize the configuration for the binary packages they produce. There are two fields for this, applied at different levels of specificity.
198+
199+
### Default Package Config
200+
201+
The `default-package-config` field provides a component-level default that applies to **all** binary packages produced by this component. It overrides any matching [package groups](package-groups.md) but is itself overridden by the `packages` map.
202+
203+
```toml
204+
[components.curl.default-package-config.publish]
205+
channel = "rpm-base"
206+
```
207+
208+
### Per-Package Overrides
209+
210+
The `[components.<name>.packages.<pkgname>]` map lets you override config for a **specific** binary package by its exact name. This is the highest-priority layer and overrides all inherited defaults:
211+
212+
```toml
213+
# Override just one subpackage
214+
[components.curl.packages.curl-devel.publish]
215+
channel = "rpm-devel"
216+
```
217+
218+
### Resolution Order
219+
220+
For each binary package produced by a component, the effective config is assembled in this order (later layers win):
221+
222+
1. Project `default-package-config`
223+
2. Package group containing this package name (if any)
224+
3. Component `default-package-config`
225+
4. Component `packages.<exact-name>` (highest priority)
226+
227+
See [Package Groups](package-groups.md) for the full field reference and a complete example.
228+
229+
### Example
230+
231+
```toml
232+
[components.curl]
233+
234+
# Route all curl packages to "base" by default ...
235+
[components.curl.default-package-config.publish]
236+
channel = "rpm-base"
237+
238+
# ... but put curl-devel in the "devel" channel
239+
[components.curl.packages.libcurl-devel.publish]
240+
channel = "rpm-devel"
241+
242+
# Signal to downstream tooling that this package should not be published
243+
[components.curl.packages.libcurl-minimal.publish]
244+
channel = "none"
245+
```
246+
193247
## Source File References
194248

195249
The `[[components.<name>.source-files]]` array defines additional source files that azldev should download before building. These are files not available in the dist-git repository or lookaside cache — typically binaries, pre-built artifacts, or files from custom hosting.
@@ -313,5 +367,6 @@ lines = ["cp -vf %{shimdirx64}/$(basename %{shimefix64}) %{shimefix64} ||:"]
313367
- [Config File Structure](config-file.md) — top-level config file layout
314368
- [Distros](distros.md) — distro definitions and `default-component-config` inheritance
315369
- [Component Groups](component-groups.md) — grouping components with shared defaults
370+
- [Package Groups](package-groups.md) — project-level package groups and full resolution order
316371
- [Configuration System](../../explanation/config-system.md) — inheritance and merge behavior
317372
- [JSON Schema](../../../../schemas/azldev.schema.json) — machine-readable schema
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Package Groups
2+
3+
Package groups let you apply shared configuration to named sets of binary packages. They are defined under `[package-groups.<name>]` in the TOML configuration.
4+
5+
Package groups are evaluated at build time, after the binary RPMs are produced. They are analogous to [component groups](component-groups.md), which apply shared configuration to sets of components.
6+
7+
## Field Reference
8+
9+
| Field | TOML Key | Type | Required | Description |
10+
|-------|----------|------|----------|-------------|
11+
| Description | `description` | string | No | Human-readable description of this group |
12+
| Packages | `packages` | string array | No | Explicit list of binary package names that belong to this group |
13+
| Default package config | `default-package-config` | [PackageConfig](#package-config) | No | Configuration inherited by all packages listed in this group |
14+
15+
## Packages
16+
17+
The `packages` field is an explicit list of binary package names (as they appear in the RPM `Name` tag) that belong to this group. Membership is determined by exact name match — no glob patterns or wildcards are supported.
18+
19+
```toml
20+
[package-groups.devel-packages]
21+
description = "Development subpackages"
22+
packages = ["libcurl-devel", "curl-static", "wget2-devel"]
23+
24+
[package-groups.debug-packages]
25+
description = "Debug info and source packages"
26+
packages = ["curl-debuginfo", "curl-debugsource", "wget2-debuginfo"]
27+
```
28+
29+
> **Note:** A package name may appear in at most one group. Listing the same name in two groups produces a validation error.
30+
31+
## Package Config
32+
33+
The `[package-groups.<name>.default-package-config]` section defines the configuration applied to all packages matching this group.
34+
35+
### PackageConfig Fields
36+
37+
| Field | TOML Key | Type | Required | Description |
38+
|-------|----------|------|----------|-------------|
39+
40+
| Publish settings | `publish` | [PublishConfig](#publish-config) | No | Publishing settings for matched packages |
41+
42+
### Publish Config
43+
44+
| Field | TOML Key | Type | Required | Description |
45+
|-------|----------|------|----------|-------------|
46+
| Channel | `channel` | string | No | Publish channel for this package. Use `"none"` to signal to downstream tooling that this package should not be published. |
47+
48+
## Resolution Order
49+
50+
When determining the effective config for a binary package, azldev applies config layers in this order — later layers override earlier ones:
51+
52+
1. **Project `default-package-config`** — lowest priority; applies to all packages in the project
53+
2. **Package group** — the group (if any) whose `packages` list contains the package name
54+
3. **Component `default-package-config`** — applies to all packages produced by that component
55+
4. **Component `packages.<name>`** — highest priority; exact per-package override
56+
57+
> **Note:** Each package name may appear in at most one group. Listing the same name in two groups produces a validation error.
58+
59+
## Example
60+
61+
```toml
62+
# Set a project-wide default channel
63+
[default-package-config.publish]
64+
channel = "rpm-base"
65+
66+
[package-groups.devel-packages]
67+
description = "Development subpackages"
68+
packages = ["libcurl-devel", "curl-static", "wget2-devel"]
69+
70+
[package-groups.devel-packages.default-package-config.publish]
71+
channel = "rpm-build-only"
72+
73+
[package-groups.debug-packages]
74+
description = "Debug info and source"
75+
packages = [
76+
"libcurl-debuginfo",
77+
"libcurl-minimal-debuginfo",
78+
"curl-debugsource",
79+
"wget2-debuginfo",
80+
"wget2-debugsource",
81+
"wget2-libs-debuginfo"
82+
]
83+
84+
[package-groups.debug-packages.default-package-config.publish]
85+
channel = "rpm-debug"
86+
```
87+
88+
## Related Resources
89+
90+
- [Project Configuration](project.md) — top-level `default-package-config` and `package-groups` fields
91+
- [Components](components.md) — per-component `default-package-config` and `packages` overrides
92+
- [Configuration System](../../explanation/config-system.md) — inheritance and merge behavior

docs/user/reference/config/project.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ The `[project]` section defines metadata and directory layout for an azldev proj
1111
| Work directory | `work-dir` | string | No | Path to the temporary working directory for build artifacts (relative to this config file) |
1212
| Output directory | `output-dir` | string | No | Path to the directory where final build outputs (RPMs, SRPMs) are placed (relative to this config file) |
1313
| Default distro | `default-distro` | [DistroReference](distros.md#distro-references) | No | The default distro and version to use when building components |
14+
| Default package config | `default-package-config` | [PackageConfig](package-groups.md#package-config) | No | Project-wide default applied to every binary package before group and component overrides |
15+
| Package groups | `package-groups` | map of string → [PackageGroupConfig](package-groups.md) | No | Named groups of binary packages with shared configuration |
1416

1517
## Directory Paths
1618

@@ -33,6 +35,27 @@ default-distro = { name = "azurelinux", version = "4.0" }
3335

3436
Components inherit their spec source and build environment from the default distro's configuration unless they override it explicitly. See [Configuration Inheritance](../../explanation/config-system.md#configuration-inheritance) for details.
3537

38+
## Default Package Config
39+
40+
The `[default-package-config]` section defines the lowest-priority configuration layer applied to every binary package produced by any component in the project. It is overridden by [package groups](package-groups.md), [component-level defaults](components.md#package-configuration), and explicit per-package overrides.
41+
42+
The most common use is to set a project-wide default publish channel:
43+
44+
```toml
45+
[default-package-config.publish]
46+
channel = "rpm-base"
47+
```
48+
49+
See [Package Groups](package-groups.md#resolution-order) for the full resolution order.
50+
51+
## Package Groups
52+
53+
The `[package-groups.<name>]` section defines named groups of binary packages. Each group lists its members explicitly in the `packages` field and provides a `default-package-config` that is applied to all listed packages.
54+
55+
This is currently used to route different types of packages (e.g., `-devel`, `-debuginfo`) to different publish channels, though groups can also carry other future configuration.
56+
57+
See [Package Groups](package-groups.md) for the full field reference.
58+
3659
## Example
3760

3861
```toml
@@ -42,10 +65,22 @@ log-dir = "build/logs"
4265
work-dir = "build/work"
4366
output-dir = "out"
4467
default-distro = { name = "azurelinux", version = "4.0" }
68+
69+
[default-package-config.publish]
70+
channel = "base"
71+
72+
[package-groups.devel-packages]
73+
description = "Development subpackages"
74+
packages = ["curl-devel", "curl-static", "wget2-devel"]
75+
76+
[package-groups.devel-packages.default-package-config.publish]
77+
channel = "devel"
4578
```
4679

4780
## Related Resources
4881

4982
- [Config File Structure](config-file.md) — top-level config file layout
5083
- [Distros](distros.md) — distro definitions referenced by `default-distro`
84+
- [Package Groups](package-groups.md) — full reference for `package-groups` and package config resolution
85+
- [Components](components.md) — per-component package config overrides
5186
- [Configuration System](../../explanation/config-system.md) — how project config merges with other files

internal/app/azldev/cmds/component/build.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import (
88
"fmt"
99
"path/filepath"
1010

11+
rpmlib "github.com/cavaliergopher/rpm"
1112
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev"
1213
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/componentbuilder"
1314
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/components"
1415
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/sources"
1516
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/workdir"
1617
"github.com/microsoft/azure-linux-dev-tools/internal/buildenv"
18+
"github.com/microsoft/azure-linux-dev-tools/internal/global/opctx"
19+
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
1720
"github.com/microsoft/azure-linux-dev-tools/internal/providers/sourceproviders"
1821
"github.com/microsoft/azure-linux-dev-tools/internal/utils/defers"
1922
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
@@ -38,6 +41,20 @@ type ComponentBuildOptions struct {
3841
MockConfigOpts map[string]string
3942
}
4043

44+
// RPMResult encapsulates a single binary RPM produced by a component build,
45+
// together with the resolved publish channel for that package.
46+
type RPMResult struct {
47+
// Path is the absolute path to the RPM file.
48+
Path string `json:"path" table:"Path"`
49+
50+
// PackageName is the binary package name extracted from the RPM header tag (e.g., "libcurl-devel").
51+
PackageName string `json:"packageName" table:"Package"`
52+
53+
// Channel is the resolved publish channel from project config.
54+
// Empty when no channel is configured for this package.
55+
Channel string `json:"channel" table:"Channel"`
56+
}
57+
4158
// ComponentBuildResults summarizes the results of building a single component.
4259
type ComponentBuildResults struct {
4360
// Names of the component that was built.
@@ -48,6 +65,13 @@ type ComponentBuildResults struct {
4865

4966
// Absolute paths to any RPMs built by the operation.
5067
RPMPaths []string `json:"rpmPaths" table:"RPM Paths"`
68+
69+
// RPMChannels holds the resolved publish channel for each RPM, parallel to [RPMPaths].
70+
// Empty string means no channel was configured for that package.
71+
RPMChannels []string `json:"rpmChannels" table:"Channels"`
72+
73+
// RPMs contains enriched per-RPM information including the resolved publish channel.
74+
RPMs []RPMResult `json:"rpms" table:"-"`
5175
}
5276

5377
func buildOnAppInit(_ *azldev.App, parent *cobra.Command) {
@@ -296,6 +320,18 @@ func buildComponentUsingBuilder(
296320
return results, fmt.Errorf("failed to build RPM for %q: %w", component.GetName(), err)
297321
}
298322

323+
// Enrich each RPM with its binary package name and resolved publish channel.
324+
results.RPMs, err = resolveRPMResults(env.FS(), results.RPMPaths, env.Config(), component.GetConfig())
325+
if err != nil {
326+
return results, fmt.Errorf("failed to resolve publish channels for %q:\n%w", component.GetName(), err)
327+
}
328+
329+
// Populate the parallel Channels slice for table display.
330+
results.RPMChannels = make([]string, len(results.RPMs))
331+
for rpmIdx, rpm := range results.RPMs {
332+
results.RPMChannels[rpmIdx] = rpm.Channel
333+
}
334+
299335
// Publish built RPMs to local repo with publish enabled.
300336
if localRepoWithPublishPath != "" && len(results.RPMPaths) > 0 {
301337
publishErr := publishToLocalRepo(env, results.RPMPaths, localRepoWithPublishPath)
@@ -360,6 +396,59 @@ func checkLocalRepoPathOverlap(localRepoPaths []string, localRepoWithPublishPath
360396
return nil
361397
}
362398

399+
// resolveRPMResults builds an [RPMResult] for each RPM path, extracting the binary package
400+
// name from the RPM headers and resolving its publish channel from the project config (if available).
401+
// When no project config is loaded, the Channel field is left empty.
402+
func resolveRPMResults(
403+
fs opctx.FS, rpmPaths []string, proj *projectconfig.ProjectConfig, compConfig *projectconfig.ComponentConfig,
404+
) ([]RPMResult, error) {
405+
rpmResults := make([]RPMResult, 0, len(rpmPaths))
406+
407+
for _, rpmPath := range rpmPaths {
408+
pkgName, err := packageNameFromRPM(fs, rpmPath)
409+
if err != nil {
410+
return nil, fmt.Errorf("failed to determine package name:\n%w", err)
411+
}
412+
413+
rpmResult := RPMResult{
414+
Path: rpmPath,
415+
PackageName: pkgName,
416+
}
417+
418+
if proj != nil {
419+
pkgConfig, err := projectconfig.ResolvePackageConfig(pkgName, compConfig, proj)
420+
if err != nil {
421+
return nil, fmt.Errorf("failed to resolve package config for %#q:\n%w", pkgName, err)
422+
}
423+
424+
rpmResult.Channel = pkgConfig.Publish.Channel
425+
}
426+
427+
rpmResults = append(rpmResults, rpmResult)
428+
}
429+
430+
return rpmResults, nil
431+
}
432+
433+
// packageNameFromRPM extracts the binary package name from an RPM file by reading
434+
// its headers. Reading the Name tag directly from the RPM metadata is authoritative and
435+
// handles all valid package names regardless of naming conventions.
436+
func packageNameFromRPM(fs opctx.FS, rpmPath string) (string, error) {
437+
rpmFile, err := fs.Open(rpmPath)
438+
if err != nil {
439+
return "", fmt.Errorf("failed to open RPM %#q:\n%w", rpmPath, err)
440+
}
441+
442+
defer rpmFile.Close()
443+
444+
pkg, err := rpmlib.Read(rpmFile)
445+
if err != nil {
446+
return "", fmt.Errorf("failed to read RPM headers from %#q:\n%w", rpmPath, err)
447+
}
448+
449+
return pkg.Name(), nil
450+
}
451+
363452
// publishToLocalRepo publishes the given RPMs to the specified local repo.
364453
func publishToLocalRepo(env *azldev.Env, rpmPaths []string, repoPath string) error {
365454
publisher, err := localrepo.NewPublisher(env, repoPath, false)

internal/projectconfig/component.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ type ComponentConfig struct {
128128

129129
// Source file references for this component.
130130
SourceFiles []SourceFileReference `toml:"source-files,omitempty" json:"sourceFiles,omitempty" table:"-" jsonschema:"title=Source files,description=Source files to download for this component"`
131+
132+
// Default configuration applied to all binary packages produced by this component.
133+
// Takes precedence over package-group defaults; overridden by explicit Packages entries.
134+
DefaultPackageConfig PackageConfig `toml:"default-package-config,omitempty" json:"defaultPackageConfig,omitempty" table:"-" jsonschema:"title=Default package config,description=Default configuration applied to all binary packages produced by this component"`
135+
136+
// Per-package configuration overrides, keyed by exact binary package name.
137+
// Takes precedence over DefaultPackageConfig and package-group defaults.
138+
Packages map[string]PackageConfig `toml:"packages,omitempty" json:"packages,omitempty" table:"-" jsonschema:"title=Package overrides,description=Per-package configuration overrides keyed by exact binary package name"`
131139
}
132140

133141
// Mutates the component config, updating it with overrides present in other.
@@ -147,11 +155,13 @@ func (c *ComponentConfig) WithAbsolutePaths(referenceDir string) *ComponentConfi
147155
// the SourceConfigFile, as we *do* want to alias that pointer, sharing it across
148156
// all configs that came from that source config file.
149157
result := &ComponentConfig{
150-
Name: c.Name,
151-
SourceConfigFile: c.SourceConfigFile,
152-
Spec: deep.MustCopy(c.Spec),
153-
Build: deep.MustCopy(c.Build),
154-
SourceFiles: deep.MustCopy(c.SourceFiles),
158+
Name: c.Name,
159+
SourceConfigFile: c.SourceConfigFile,
160+
Spec: deep.MustCopy(c.Spec),
161+
Build: deep.MustCopy(c.Build),
162+
SourceFiles: deep.MustCopy(c.SourceFiles),
163+
DefaultPackageConfig: deep.MustCopy(c.DefaultPackageConfig),
164+
Packages: deep.MustCopy(c.Packages),
155165
}
156166

157167
// Fix up paths.

0 commit comments

Comments
 (0)