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
55 changes: 55 additions & 0 deletions docs/user/reference/config/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ A component definition tells azldev where to find the spec file, how to customiz
| Overlays | `overlays` | array of [Overlay](overlays.md) | No | Modifications to apply to the spec and/or source files |
| Build config | `build` | [BuildConfig](#build-configuration) | No | Build-time options (macros, conditionals, check config) |
| Source files | `source-files` | array of [SourceFileReference](#source-file-references) | No | Additional source files to download for this component |
| 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 |
| Package overrides | `packages` | map of string → [PackageConfig](package-groups.md#package-config) | No | Exact per-package configuration overrides; highest priority in the resolution order |

### Bare Components

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

## Package Configuration

Components can customize the configuration for the binary packages they produce. There are two fields for this, applied at different levels of specificity.

### Default Package Config

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.

```toml
[components.curl.default-package-config.publish]
channel = "rpm-base"
```

### Per-Package Overrides

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:

```toml
# Override just one subpackage
[components.curl.packages.curl-devel.publish]
channel = "rpm-devel"
```

### Resolution Order

For each binary package produced by a component, the effective config is assembled in this order (later layers win):

1. Project `default-package-config`
2. Package group containing this package name (if any)
3. Component `default-package-config`
4. Component `packages.<exact-name>` (highest priority)

See [Package Groups](package-groups.md) for the full field reference and a complete example.

### Example

```toml
[components.curl]

# Route all curl packages to "base" by default ...
[components.curl.default-package-config.publish]
channel = "rpm-base"

# ... but put curl-devel in the "devel" channel
[components.curl.packages.libcurl-devel.publish]
channel = "rpm-devel"

# Signal to downstream tooling that this package should not be published
[components.curl.packages.libcurl-minimal.publish]
channel = "none"
```

## Source File References

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.
Expand Down Expand Up @@ -313,5 +367,6 @@ lines = ["cp -vf %{shimdirx64}/$(basename %{shimefix64}) %{shimefix64} ||:"]
- [Config File Structure](config-file.md) — top-level config file layout
- [Distros](distros.md) — distro definitions and `default-component-config` inheritance
- [Component Groups](component-groups.md) — grouping components with shared defaults
- [Package Groups](package-groups.md) — project-level package groups and full resolution order
- [Configuration System](../../explanation/config-system.md) — inheritance and merge behavior
- [JSON Schema](../../../../schemas/azldev.schema.json) — machine-readable schema
92 changes: 92 additions & 0 deletions docs/user/reference/config/package-groups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Package Groups

Package groups let you apply shared configuration to named sets of binary packages. They are defined under `[package-groups.<name>]` in the TOML configuration.

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.

## Field Reference

| Field | TOML Key | Type | Required | Description |
|-------|----------|------|----------|-------------|
| Description | `description` | string | No | Human-readable description of this group |
| Packages | `packages` | string array | No | Explicit list of binary package names that belong to this group |
| Default package config | `default-package-config` | [PackageConfig](#package-config) | No | Configuration inherited by all packages listed in this group |

## Packages

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.

```toml
[package-groups.devel-packages]
description = "Development subpackages"
packages = ["libcurl-devel", "curl-static", "wget2-devel"]

[package-groups.debug-packages]
description = "Debug info and source packages"
packages = ["curl-debuginfo", "curl-debugsource", "wget2-debuginfo"]
```

> **Note:** A package name may appear in at most one group. Listing the same name in two groups produces a validation error.

## Package Config

The `[package-groups.<name>.default-package-config]` section defines the configuration applied to all packages matching this group.

### PackageConfig Fields

| Field | TOML Key | Type | Required | Description |
|-------|----------|------|----------|-------------|

| Publish settings | `publish` | [PublishConfig](#publish-config) | No | Publishing settings for matched packages |

### Publish Config

| Field | TOML Key | Type | Required | Description |
|-------|----------|------|----------|-------------|
| Channel | `channel` | string | No | Publish channel for this package. Use `"none"` to signal to downstream tooling that this package should not be published. |

## Resolution Order

When determining the effective config for a binary package, azldev applies config layers in this order — later layers override earlier ones:

1. **Project `default-package-config`** — lowest priority; applies to all packages in the project
2. **Package group** — the group (if any) whose `packages` list contains the package name
3. **Component `default-package-config`** — applies to all packages produced by that component
4. **Component `packages.<name>`** — highest priority; exact per-package override

> **Note:** Each package name may appear in at most one group. Listing the same name in two groups produces a validation error.

## Example

```toml
# Set a project-wide default channel
[default-package-config.publish]
channel = "rpm-base"

[package-groups.devel-packages]
description = "Development subpackages"
packages = ["libcurl-devel", "curl-static", "wget2-devel"]

[package-groups.devel-packages.default-package-config.publish]
channel = "rpm-build-only"

[package-groups.debug-packages]
description = "Debug info and source"
packages = [
"libcurl-debuginfo",
"libcurl-minimal-debuginfo",
"curl-debugsource",
"wget2-debuginfo",
"wget2-debugsource",
"wget2-libs-debuginfo"
]

[package-groups.debug-packages.default-package-config.publish]
channel = "rpm-debug"
```

## Related Resources

- [Project Configuration](project.md) — top-level `default-package-config` and `package-groups` fields
- [Components](components.md) — per-component `default-package-config` and `packages` overrides
- [Configuration System](../../explanation/config-system.md) — inheritance and merge behavior
35 changes: 35 additions & 0 deletions docs/user/reference/config/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ The `[project]` section defines metadata and directory layout for an azldev proj
| Work directory | `work-dir` | string | No | Path to the temporary working directory for build artifacts (relative to this config file) |
| Output directory | `output-dir` | string | No | Path to the directory where final build outputs (RPMs, SRPMs) are placed (relative to this config file) |
| Default distro | `default-distro` | [DistroReference](distros.md#distro-references) | No | The default distro and version to use when building components |
| 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 |
| Package groups | `package-groups` | map of string → [PackageGroupConfig](package-groups.md) | No | Named groups of binary packages with shared configuration |

Comment on lines +14 to 16
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The field reference table is for the [project] section, but default-package-config and package-groups are top-level keys (see ConfigFile struct tags / examples in this same doc). Listing them under [project] is misleading; move them to the appropriate top-level config documentation or clarify they are not nested under [project].

Suggested change
| 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 |
| Package groups | `package-groups` | map of string → [PackageGroupConfig](package-groups.md) | No | Named groups of binary packages with shared configuration |
Additional top-level sections that affect project behavior — `[default-package-config]` and `[package-groups]` — are siblings of `[project]` in the config file, not nested within it. They are documented in the sections below.

Copilot uses AI. Check for mistakes.
## Directory Paths

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

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.

## Default Package Config

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.

The most common use is to set a project-wide default publish channel:

```toml
[default-package-config.publish]
channel = "rpm-base"
```

See [Package Groups](package-groups.md#resolution-order) for the full resolution order.

## Package Groups

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.

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.

See [Package Groups](package-groups.md) for the full field reference.

## Example

```toml
Expand All @@ -42,10 +65,22 @@ log-dir = "build/logs"
work-dir = "build/work"
output-dir = "out"
default-distro = { name = "azurelinux", version = "4.0" }

[default-package-config.publish]
channel = "base"

[package-groups.devel-packages]
description = "Development subpackages"
packages = ["curl-devel", "curl-static", "wget2-devel"]

[package-groups.devel-packages.default-package-config.publish]
channel = "devel"
```

## Related Resources

- [Config File Structure](config-file.md) — top-level config file layout
- [Distros](distros.md) — distro definitions referenced by `default-distro`
- [Package Groups](package-groups.md) — full reference for `package-groups` and package config resolution
- [Components](components.md) — per-component package config overrides
- [Configuration System](../../explanation/config-system.md) — how project config merges with other files
89 changes: 89 additions & 0 deletions internal/app/azldev/cmds/component/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import (
"fmt"
"path/filepath"

rpmlib "github.com/cavaliergopher/rpm"
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev"
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/componentbuilder"
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/components"
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/sources"
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/workdir"
"github.com/microsoft/azure-linux-dev-tools/internal/buildenv"
"github.com/microsoft/azure-linux-dev-tools/internal/global/opctx"
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
"github.com/microsoft/azure-linux-dev-tools/internal/providers/sourceproviders"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/defers"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
Expand All @@ -37,6 +40,20 @@ type ComponentBuildOptions struct {
MockConfigOpts map[string]string
}

// RPMResult encapsulates a single binary RPM produced by a component build,
// together with the resolved publish channel for that package.
type RPMResult struct {
// Path is the absolute path to the RPM file.
Path string `json:"path" table:"Path"`

// PackageName is the binary package name extracted from the RPM header tag (e.g., "libcurl-devel").
PackageName string `json:"packageName" table:"Package"`

// Channel is the resolved publish channel from project config.
// Empty when no channel is configured for this package.
Channel string `json:"channel" table:"Channel"`
}

// ComponentBuildResults summarizes the results of building a single component.
type ComponentBuildResults struct {
// Names of the component that was built.
Expand All @@ -47,6 +64,13 @@ type ComponentBuildResults struct {

// Absolute paths to any RPMs built by the operation.
RPMPaths []string `json:"rpmPaths" table:"RPM Paths"`

// RPMChannels holds the resolved publish channel for each RPM, parallel to [RPMPaths].
// Empty string means no channel was configured for that package.
RPMChannels []string `json:"rpmChannels" table:"Channels"`

// RPMs contains enriched per-RPM information including the resolved publish channel.
RPMs []RPMResult `json:"rpms" table:"-"`
}

func buildOnAppInit(_ *azldev.App, parent *cobra.Command) {
Expand Down Expand Up @@ -288,6 +312,18 @@ func buildComponentUsingBuilder(
return results, fmt.Errorf("failed to build RPM for %q: %w", component.GetName(), err)
}

// Enrich each RPM with its binary package name and resolved publish channel.
results.RPMs, err = resolveRPMResults(env.FS(), results.RPMPaths, env.Config(), component.GetConfig())
if err != nil {
return results, fmt.Errorf("failed to resolve publish channels for %q:\n%w", component.GetName(), err)
}
Comment on lines +315 to +319
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

New behavior enriches build results with RPM package names/channels (RPMs/RPMChannels), but there are no unit tests exercising this path (e.g., that channels are resolved and populated, and that failures to read RPM metadata surface correctly). Adding focused tests here would prevent regressions; use the in-memory test environment FS and avoid spawning external processes.

Copilot generated this review using guidance from repository custom instructions.

// Populate the parallel Channels slice for table display.
results.RPMChannels = make([]string, len(results.RPMs))
for rpmIdx, rpm := range results.RPMs {
results.RPMChannels[rpmIdx] = rpm.Channel
}

// Publish built RPMs to local repo with publish enabled.
if localRepoWithPublishPath != "" && len(results.RPMPaths) > 0 {
publishErr := publishToLocalRepo(env, results.RPMPaths, localRepoWithPublishPath)
Expand Down Expand Up @@ -352,6 +388,59 @@ func checkLocalRepoPathOverlap(localRepoPaths []string, localRepoWithPublishPath
return nil
}

// resolveRPMResults builds an [RPMResult] for each RPM path, extracting the binary package
// name from the RPM headers and resolving its publish channel from the project config (if available).
// When no project config is loaded, the Channel field is left empty.
func resolveRPMResults(
fs opctx.FS, rpmPaths []string, proj *projectconfig.ProjectConfig, compConfig *projectconfig.ComponentConfig,
) ([]RPMResult, error) {
rpmResults := make([]RPMResult, 0, len(rpmPaths))

for _, rpmPath := range rpmPaths {
pkgName, err := packageNameFromRPM(fs, rpmPath)
if err != nil {
return nil, fmt.Errorf("failed to determine package name:\n%w", err)
}

rpmResult := RPMResult{
Path: rpmPath,
PackageName: pkgName,
}

if proj != nil {
pkgConfig, err := projectconfig.ResolvePackageConfig(pkgName, compConfig, proj)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We may have already discussed this, but what do you think about (in a separate PR) exposing a command-line verb to resolve the configuration for a named package?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's a reasonable idea. The infrastructure (ResolvePackageConfig) is already in place and does exactly the right thing. A separate PR will add azldev package list/config to look up the resolved configuration for a given package by using the resolver in this PR.

if err != nil {
return nil, fmt.Errorf("failed to resolve package config for %#q:\n%w", pkgName, err)
}

rpmResult.Channel = pkgConfig.Publish.Channel
}

rpmResults = append(rpmResults, rpmResult)
}

return rpmResults, nil
}

// packageNameFromRPM extracts the binary package name from an RPM file by reading
// its headers. Reading the Name tag directly from the RPM metadata is authoritative and
// handles all valid package names regardless of naming conventions.
func packageNameFromRPM(fs opctx.FS, rpmPath string) (string, error) {
rpmFile, err := fs.Open(rpmPath)
if err != nil {
return "", fmt.Errorf("failed to open RPM %#q:\n%w", rpmPath, err)
}

defer rpmFile.Close()

pkg, err := rpmlib.Read(rpmFile)
if err != nil {
return "", fmt.Errorf("failed to read RPM headers from %#q:\n%w", rpmPath, err)
}

return pkg.Name(), nil
}

// publishToLocalRepo publishes the given RPMs to the specified local repo.
func publishToLocalRepo(env *azldev.Env, rpmPaths []string, repoPath string) error {
publisher, err := localrepo.NewPublisher(env, repoPath, false)
Expand Down
20 changes: 15 additions & 5 deletions internal/projectconfig/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ type ComponentConfig struct {

// Source file references for this component.
SourceFiles []SourceFileReference `toml:"source-files,omitempty" json:"sourceFiles,omitempty" table:"-" jsonschema:"title=Source files,description=Source files to download for this component"`

// Default configuration applied to all binary packages produced by this component.
// Takes precedence over package-group defaults; overridden by explicit Packages entries.
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"`

// Per-package configuration overrides, keyed by exact binary package name.
// Takes precedence over DefaultPackageConfig and package-group defaults.
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"`
}

// Mutates the component config, updating it with overrides present in other.
Expand All @@ -147,11 +155,13 @@ func (c *ComponentConfig) WithAbsolutePaths(referenceDir string) *ComponentConfi
// the SourceConfigFile, as we *do* want to alias that pointer, sharing it across
// all configs that came from that source config file.
result := &ComponentConfig{
Name: c.Name,
SourceConfigFile: c.SourceConfigFile,
Spec: deep.MustCopy(c.Spec),
Build: deep.MustCopy(c.Build),
SourceFiles: deep.MustCopy(c.SourceFiles),
Name: c.Name,
SourceConfigFile: c.SourceConfigFile,
Spec: deep.MustCopy(c.Spec),
Build: deep.MustCopy(c.Build),
SourceFiles: deep.MustCopy(c.SourceFiles),
DefaultPackageConfig: deep.MustCopy(c.DefaultPackageConfig),
Packages: deep.MustCopy(c.Packages),
}

// Fix up paths.
Expand Down
Loading
Loading