Skip to content
Draft
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
151 changes: 151 additions & 0 deletions docs/user/reference/config/component-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Component Templates

Component templates define a matrix of axes whose cartesian product expands into multiple [component](components.md) definitions. This is useful when you need to build several variants of the same source project — for example, an out-of-tree kernel module compiled against different kernel versions and toolchains.

Templates are defined under `[component-templates.<name>]` in the TOML configuration. During config loading, each template is expanded into regular components that behave identically to explicitly defined ones.

## Template Config

| Field | TOML Key | Type | Required | Description |
|-------|----------|------|----------|-------------|
| Description | `description` | string | No | Human-friendly description of this template |
| Default component config | `default-component-config` | [ComponentConfig](components.md#component-config) | No | Base configuration applied to every expanded variant before axis overrides |
| Matrix | `matrix` | array of [MatrixAxis](#matrix-axis) | **Yes** | Ordered list of axes whose cartesian product defines the expanded variants |

## Matrix Axis

Each matrix axis defines one dimension of the expansion. The cartesian product of all axes determines the set of expanded components.

| Field | TOML Key | Type | Required | Description |
|-------|----------|------|----------|-------------|
| Axis name | `axis` | string | **Yes** | Name of this axis (e.g., `"kernel"`, `"toolchain"`) |
| Values | `values` | map of string → [ComponentConfig](components.md#component-config) | **Yes** | Named values for this axis; each value is a partial component config merged into the expanded component |

### Constraints

- At least one axis is required per template.
- Each axis must have at least one value.
- Axis names must be unique within a template.
- Value names must be non-empty.

## Expansion Rules

### Name Synthesis

Expanded component names are formed by joining the template name with each selected value name, separated by `-`, in the order the axes appear in the `matrix` array:

```
<template-name>-<axis1-value>-<axis2-value>-...
```

For example, a template `my-driver` with axes `kernel` (values `6-6`, `6-12`) and `toolchain` (values `gcc13`, `gcc14`) produces:

- `my-driver-6-6-gcc13`
- `my-driver-6-6-gcc14`
- `my-driver-6-12-gcc13`
- `my-driver-6-12-gcc14`

### Config Layering

For each expanded component, configurations are layered in the following order (later layers override earlier ones):

1. Template's `default-component-config`
2. First axis's selected value config
3. Second axis's selected value config
4. ... (additional axes in definition order)

This uses the same `MergeUpdatesFrom` mechanism as [component group defaults](component-groups.md), so all standard merge rules apply.

### Collision Handling

If an expanded component name collides with an explicitly defined component (or another template's expansion), a validation error is produced at load time. Expanded components are merged **after** regular components, so the error message will identify the template as the source of the collision.

## Example

### Basic Two-Axis Template

```toml
[component-templates.my-driver]
description = "Out-of-tree driver built against multiple kernel versions and toolchains"

[component-templates.my-driver.default-component-config]
spec = { type = "local", path = "my-driver.spec" }

[component-templates.my-driver.default-component-config.build]
defines = { base_config = "true" }

[[component-templates.my-driver.matrix]]
axis = "kernel"
[component-templates.my-driver.matrix.values.6-6.build]
defines = { kernel_version = "6.6.72" }
[component-templates.my-driver.matrix.values.6-12.build]
defines = { kernel_version = "6.12.8" }

[[component-templates.my-driver.matrix]]
axis = "toolchain"
[component-templates.my-driver.matrix.values.gcc13.build]
defines = { gcc_version = "13" }
[component-templates.my-driver.matrix.values.gcc14.build]
defines = { gcc_version = "14" }
```

This produces 4 components. For example, `my-driver-6-6-gcc14` will have:
- `spec` from the default: `{ type = "local", path = "my-driver.spec" }`
- `build.defines` merged from all layers: `{ base_config = "true", kernel_version = "6.6.72", gcc_version = "14" }`

### Template with Overlays

Axis values can include any [ComponentConfig](components.md#component-config) fields, including overlays:

```toml
[component-templates.my-driver]

[component-templates.my-driver.default-component-config]
spec = { type = "local", path = "my-driver.spec" }

[[component-templates.my-driver.matrix]]
axis = "kernel"

[component-templates.my-driver.matrix.values.6-6]

[[component-templates.my-driver.matrix.values.6-6.overlays]]
type = "spec-set-tag"
description = "Set kernel version to 6.6"
tag = "kernel_version"
value = "6.6.72"

[component-templates.my-driver.matrix.values.6-12]

[[component-templates.my-driver.matrix.values.6-12.overlays]]
type = "spec-set-tag"
description = "Set kernel version to 6.12"
tag = "kernel_version"
value = "6.12.8"
```

### Mixing Templates with Regular Components

Templates and regular components coexist in the same config files:

```toml
# Regular component
[components.curl]

# Template-expanded components
[component-templates.my-driver]

[[component-templates.my-driver.matrix]]
axis = "kernel"
[component-templates.my-driver.matrix.values.6-6]
[component-templates.my-driver.matrix.values.6-12]
```

This produces 3 components: `curl`, `my-driver-6-6`, and `my-driver-6-12`.

## Related Resources

- [Components](components.md) — component configuration reference
- [Component Groups](component-groups.md) — grouping components with shared defaults
- [Config File Structure](config-file.md) — top-level config file layout
- [Configuration System](../../explanation/config-system.md) — inheritance and merge behavior
- [JSON Schema](../../../../schemas/azldev.schema.json) — machine-readable schema
1 change: 1 addition & 0 deletions docs/user/reference/config/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ 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
- [Component Templates](component-templates.md) — generating multiple component variants from a matrix
- [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
1 change: 1 addition & 0 deletions docs/user/reference/config/config-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ All config files share the same schema — there is no distinction between a "ro
| `distros` | map of objects | Distro definitions (build environments, upstream sources) | [Distros](distros.md) |
| `components` | map of objects | Component (package) definitions | [Components](components.md) |
| `component-groups` | map of objects | Named groups of components with shared defaults | [Component Groups](component-groups.md) |
| `component-templates` | map of objects | Component templates that expand into multiple components via matrix axes | [Component Templates](component-templates.md) |
| `images` | map of objects | Image definitions (VMs, containers) | [Images](images.md) |
| `tools` | object | Configuration for external tools used by azldev | [Tools](tools.md) |

Expand Down
95 changes: 95 additions & 0 deletions internal/projectconfig/componenttemplate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package projectconfig

import (
"errors"
"fmt"

"github.com/brunoga/deep"
)

// ComponentTemplateConfig defines a component template that produces multiple component variants
// via a matrix of axes. Each axis contributes named values; the template expands into the
// cartesian product of all axis values, yielding one [ComponentConfig] per combination.
type ComponentTemplateConfig struct {
// A human-friendly description of this component template.
Description string `toml:"description,omitempty" json:"description,omitempty" jsonschema:"title=Description,description=Description of this component template"`

// Default configuration applied to every expanded component before axis-specific overrides.
DefaultComponentConfig ComponentConfig `toml:"default-component-config,omitempty" json:"defaultComponentConfig,omitempty" jsonschema:"title=Default component configuration,description=Default component config applied to every expanded variant before axis overrides"`

// Ordered list of matrix axes. Each axis defines a dimension with named values;
// the cartesian product of all axes determines the set of expanded components.
// Axis configs are applied in array order, so later axes override earlier ones.
Matrix []MatrixAxis `toml:"matrix" json:"matrix" validate:"required,min=1,dive" jsonschema:"required,minItems=1,title=Matrix axes,description=Ordered list of matrix axes whose cartesian product defines the expanded component variants"`

// Internal: name assigned during loading (not serialized).
name string `toml:"-" json:"-"`

// Internal: reference to the source config file (not serialized).
sourceConfigFile *ConfigFile `toml:"-" json:"-"`
}

// MatrixAxis defines a single dimension of a component template's matrix.
// It has a name (the axis identifier) and a map of named values, each containing
// a partial [ComponentConfig] that is merged into the expanded component.
type MatrixAxis struct {
// Name of this axis (e.g., "kernel", "toolchain").
Axis string `toml:"axis" json:"axis" validate:"required" jsonschema:"required,title=Axis name,description=Name of this matrix axis (e.g. kernel or toolchain)"`

// Named values for this axis. Each key is a value name that appears in the
// synthesized component name; each value is a partial [ComponentConfig] merged
// into the expanded component.
Values map[string]ComponentConfig `toml:"values" json:"values" validate:"required,min=1,dive" jsonschema:"required,minProperties=1,title=Axis values,description=Named values for this axis; each value is a partial ComponentConfig merged into the expanded component"`
}

// Validate checks that the component template configuration is internally consistent.
func (t ComponentTemplateConfig) Validate() error {
if len(t.Matrix) == 0 {
return errors.New("component template must have at least one matrix axis")
}

seenAxes := make(map[string]struct{}, len(t.Matrix))

for i, axis := range t.Matrix {
if axis.Axis == "" {
return fmt.Errorf("matrix axis %d has an empty axis name", i+1)
}

if _, seen := seenAxes[axis.Axis]; seen {
return fmt.Errorf("duplicate matrix axis name %#q", axis.Axis)
}

seenAxes[axis.Axis] = struct{}{}

if len(axis.Values) == 0 {
return fmt.Errorf("matrix axis %#q must have at least one value", axis.Axis)
}

for valueName := range axis.Values {
if valueName == "" {
return fmt.Errorf("matrix axis %#q has an empty value name", axis.Axis)
}
}
}

return nil
}

// WithAbsolutePaths returns a copy of the component template config with relative file paths
// converted to absolute file paths (relative to referenceDir).
func (t ComponentTemplateConfig) WithAbsolutePaths(referenceDir string) ComponentTemplateConfig {
result := deep.MustCopy(t)

result.DefaultComponentConfig = *(result.DefaultComponentConfig.WithAbsolutePaths(referenceDir))

for i, axis := range result.Matrix {
for valueName, valueCfg := range axis.Values {
result.Matrix[i].Values[valueName] = *(valueCfg.WithAbsolutePaths(referenceDir))
}
}

return result
}
Loading
Loading