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
23 changes: 23 additions & 0 deletions docs/user/reference/config/overlays.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ These overlays modify `.spec` files using the structured spec parser, allowing p
| `spec-set-tag` | Sets a tag value; replaces if exists, adds if not | `tag`, `value` |
| `spec-update-tag` | Updates an existing tag; **fails if the tag doesn't exist** | `tag`, `value` |
| `spec-remove-tag` | Removes a tag from the spec; **fails if the tag doesn't exist** | `tag` |
| `spec-set-macros` | Sets the value of one or more `%global` / `%define` macros; auto-detects which directive form is in the spec, or use `kind` to force one. **Fails if a target macro is not present.** | `macros` |
| `spec-prepend-lines` | Prepends lines to the start of a section; **fails if section doesn't exist** | `lines` |
| `spec-append-lines` | Appends lines to the end of a section; **fails if section doesn't exist** | `lines` |
| `spec-search-replace` | Regex-based search and replace on spec content | `regex` |
Expand Down Expand Up @@ -59,6 +60,7 @@ successfully makes a replacement to at least one matching file.
| Regex | `regex` | Regular expression pattern to match | `spec-search-replace`, `file-search-replace` |
| Replacement | `replacement` | Literal replacement text; capture group references like `$1` are **not** expanded. Omit or leave empty to delete matched text. | `spec-search-replace`, `file-search-replace`, `file-rename` |
| Lines | `lines` | Array of text lines to insert | `spec-prepend-lines`, `spec-append-lines`, `file-prepend-lines` |
| Macros | `macros` | Map of macro name to settings (`{value = "...", kind = "global" \| "define"}`); `kind` is optional and forces the directive form | `spec-set-macros` |
| File | `file` | The name of the non-spec file to modify or add | `file-prepend-lines`, `file-search-replace`, `file-add`, `file-remove`, `file-rename`, `patch-add` (optional), `patch-remove` |
| Source | `source` | Path to source file for `file-add` and `patch-add`; relative paths are relative to the config file | `file-add`, `patch-add` |

Expand Down Expand Up @@ -149,6 +151,27 @@ regex = "--enable-deprecated-feature\\s*"
replacement = ""
```

### Setting Spec Macros

Use `spec-set-macros` to declaratively change the value of one or more `%global` / `%define` macros. The directive form (`%global` vs `%define`) is auto-detected from the existing spec, so you don't need to know which one upstream uses. The overlay **fails** if any named macro is not present in the spec — this catches typos and upstream removals.

```toml
[[components.gcc.overlays]]
description = "Disable language frontends not supported on AZL toolchain"
type = "spec-set-macros"
macros = { build_ada = { value = "0" }, build_objc = { value = "0" }, build_go = { value = "0" } }
```

To force a specific directive form, use `kind`:

```toml
[[components.mypackage.overlays]]
type = "spec-set-macros"
macros = { with_doc = { value = "1", kind = "global" } }
```

> **Note:** Macro names must be plain identifiers (no whitespace, `%`, or parentheses). Function-like macros (`%define name() ...`) are not supported. Multi-line macro definitions (using `\` line continuation) are also not supported.

### Targeting a Sub-Package

For multi-package specs, use the `package` field to target a specific sub-package:
Expand Down
19 changes: 18 additions & 1 deletion internal/app/azldev/core/sources/overlays.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"os"
"path/filepath"
"regexp"
"sort"
"strings"

"github.com/bmatcuk/doublestar/v4"
Expand Down Expand Up @@ -116,7 +117,7 @@ func ApplySpecOverlayToFileInPlace(fs opctx.FS, overlay projectconfig.ComponentO
// ApplySpecOverlay applies a spec-based overlay to an opened spec. An error is returned if a non-spec
// overlay is provided.
//
//nolint:cyclop,funlen // This function's complexity is inflated by the big switch over overlay types.
//nolint:cyclop,funlen,gocognit // This function's complexity is inherent to the big switch over overlay types.
func ApplySpecOverlay(overlay projectconfig.ComponentOverlay, openedSpec *spec.Spec) error {
//nolint:exhaustive // We intentionally ignore non-spec overlay types.
switch overlay.Type {
Expand Down Expand Up @@ -145,6 +146,22 @@ func ApplySpecOverlay(overlay projectconfig.ComponentOverlay, openedSpec *spec.S
if err != nil {
return fmt.Errorf("failed to remove tag %#q from spec:\n%w", overlay.Tag, err)
}
case projectconfig.ComponentOverlaySetSpecMacros:
// Apply macros in stable (alphabetical) order so the resulting spec
// rendering is deterministic regardless of map iteration order.
names := make([]string, 0, len(overlay.Macros))
for name := range overlay.Macros {
names = append(names, name)
}

sort.Strings(names)

for _, name := range names {
macroSpec := overlay.Macros[name]
if err := openedSpec.SetMacro(name, macroSpec.Value, macroSpec.Kind); err != nil {
return fmt.Errorf("failed to set macro %#q in spec:\n%w", name, err)
}
}
case projectconfig.ComponentOverlayPrependSpecLines:
err := openedSpec.PrependLinesToSection(overlay.SectionName, overlay.PackageName, overlay.Lines)
if err != nil {
Expand Down
100 changes: 100 additions & 0 deletions internal/app/azldev/core/sources/overlays_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1287,3 +1287,103 @@ make
assert.Contains(t, err.Error(), "not found")
})
}

func TestApplySpecOverlay_SetMacros(t *testing.T) {
t.Run("sets a single macro", func(t *testing.T) {
specContent := `Name: test
%global build_ada 1
`
overlay := projectconfig.ComponentOverlay{
Type: projectconfig.ComponentOverlaySetSpecMacros,
Macros: map[string]projectconfig.MacroSetSpec{
"build_ada": {Value: "0"},
},
}

result, err := applyOverlayToSpecContents(t, overlay, specContent)
require.NoError(t, err)
assert.Equal(t, `Name: test
%global build_ada 0
`, result)
})

t.Run("sets multiple macros at once", func(t *testing.T) {
specContent := `Name: gcc
%global build_ada 1
%global build_objc 1
%global build_go 1
`
overlay := projectconfig.ComponentOverlay{
Type: projectconfig.ComponentOverlaySetSpecMacros,
Macros: map[string]projectconfig.MacroSetSpec{
"build_ada": {Value: "0"},
"build_objc": {Value: "0"},
"build_go": {Value: "0"},
},
}

result, err := applyOverlayToSpecContents(t, overlay, specContent)
require.NoError(t, err)
assert.Equal(t, `Name: gcc
%global build_ada 0
%global build_objc 0
%global build_go 0
`, result)
})

t.Run("kind override rewrites directive", func(t *testing.T) {
specContent := `Name: test
%define with_doc 0
`
overlay := projectconfig.ComponentOverlay{
Type: projectconfig.ComponentOverlaySetSpecMacros,
Macros: map[string]projectconfig.MacroSetSpec{
"with_doc": {Value: "1", Kind: projectconfig.MacroKindGlobal},
},
}

result, err := applyOverlayToSpecContents(t, overlay, specContent)
require.NoError(t, err)
assert.Equal(t, `Name: test
%global with_doc 1
`, result)
})

t.Run("fails when macro is missing", func(t *testing.T) {
specContent := `Name: test
%global other 1
`
overlay := projectconfig.ComponentOverlay{
Type: projectconfig.ComponentOverlaySetSpecMacros,
Macros: map[string]projectconfig.MacroSetSpec{
"build_ada": {Value: "0"},
},
}

_, err := applyOverlayToSpecContents(t, overlay, specContent)
require.Error(t, err)
require.ErrorIs(t, err, spec.ErrNoSuchMacro)
assert.Contains(t, err.Error(), "build_ada")
})

t.Run("missing-macro stops at first error in deterministic order", func(t *testing.T) {
// Macros are applied in alphabetical order. Even though `build_objc`
// is present, the missing `build_ada` is processed first and triggers
// the failure.
specContent := `Name: test
%global build_objc 1
`
overlay := projectconfig.ComponentOverlay{
Type: projectconfig.ComponentOverlaySetSpecMacros,
Macros: map[string]projectconfig.MacroSetSpec{
"build_ada": {Value: "0"},
"build_objc": {Value: "0"},
},
}

_, err := applyOverlayToSpecContents(t, overlay, specContent)
require.Error(t, err)
require.ErrorIs(t, err, spec.ErrNoSuchMacro)
assert.Contains(t, err.Error(), "build_ada")
})
}
109 changes: 108 additions & 1 deletion internal/projectconfig/overlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"path/filepath"
"regexp"
"strings"

"github.com/bmatcuk/doublestar/v4"
"github.com/brunoga/deep"
Expand All @@ -17,7 +18,7 @@ import (
// ComponentOverlay represents an overlay that may be applied to a component's spec and/or its sources.
type ComponentOverlay struct {
// The type of overlay to apply.
Type ComponentOverlayType `toml:"type" json:"type" validate:"required" jsonschema:"enum=spec-add-tag,enum=spec-insert-tag,enum=spec-set-tag,enum=spec-update-tag,enum=spec-remove-tag,enum=spec-prepend-lines,enum=spec-append-lines,enum=spec-search-replace,enum=spec-remove-section,enum=patch-add,enum=patch-remove,enum=file-prepend-lines,enum=file-search-replace,enum=file-add,enum=file-remove,enum=file-rename,title=Overlay type,description=The type of overlay to apply"`
Type ComponentOverlayType `toml:"type" json:"type" validate:"required" jsonschema:"enum=spec-add-tag,enum=spec-insert-tag,enum=spec-set-tag,enum=spec-update-tag,enum=spec-remove-tag,enum=spec-set-macros,enum=spec-prepend-lines,enum=spec-append-lines,enum=spec-search-replace,enum=spec-remove-section,enum=patch-add,enum=patch-remove,enum=file-prepend-lines,enum=file-search-replace,enum=file-add,enum=file-remove,enum=file-rename,title=Overlay type,description=The type of overlay to apply"`
// Human readable description of overlay; primarily present to document the need for the change.
Description string `toml:"description,omitempty" json:"description,omitempty" jsonschema:"title=Description,description=Human readable description of overlay" fingerprint:"-"`

Expand All @@ -38,6 +39,11 @@ type ComponentOverlay struct {
Replacement string `toml:"replacement,omitempty" json:"replacement,omitempty" jsonschema:"title=Replacement text,description=The replacement text to use in the spec"`
// For overlays that reference lines of text, the lines of text to use.
Lines []string `toml:"lines,omitempty" json:"lines,omitempty" jsonschema:"title=Lines,description=The lines of text to use"`
// For overlays that set one or more spec macros (spec-set-macros). Keys are
// macro names; values describe how each macro should be set. Object form
// (no scalar shorthand) keeps room for future per-entry flags without
// breaking existing configs.
Macros map[string]MacroSetSpec `toml:"macros,omitempty" json:"macros,omitempty" jsonschema:"title=Macros,description=Map of macro names to settings; used by spec-set-macros"`
// For overlays that require a source file as input, indicates a path to that file; relative paths are relative to
// the config file that defines the overlay.
// Excluded from fingerprint because it contains an absolute path that varies by checkout
Expand Down Expand Up @@ -110,6 +116,7 @@ func (c *ComponentOverlay) ModifiesSpec() bool {
c.Type == ComponentOverlaySetSpecTag ||
c.Type == ComponentOverlayUpdateSpecTag ||
c.Type == ComponentOverlayRemoveSpecTag ||
c.Type == ComponentOverlaySetSpecMacros ||
c.Type == ComponentOverlayPrependSpecLines ||
c.Type == ComponentOverlayAppendSpecLines ||
c.Type == ComponentOverlaySearchAndReplaceInSpec ||
Expand All @@ -134,6 +141,27 @@ func (c *ComponentOverlay) ModifiesNonSpecFiles() bool {
// ComponentOverlayType is the type of a component overlay.
type ComponentOverlayType string

// MacroKindGlobal forces a macro to be expressed as `%global` when set on a
// [MacroSetSpec.Kind] field.
const MacroKindGlobal = "global"

// MacroKindDefine forces a macro to be expressed as `%define` when set on a
// [MacroSetSpec.Kind] field.
const MacroKindDefine = "define"

// MacroSetSpec describes how to set a single spec macro for the
// `spec-set-macros` overlay. Object form is mandatory (no scalar shorthand)
// so additional fields can be added without breaking existing configs.
type MacroSetSpec struct {
// Value is the new value to assign to the macro. Required, non-empty,
// no newlines.
Value string `toml:"value" json:"value" validate:"required" jsonschema:"title=Value,description=The new value to assign to the macro"`

// Kind optionally forces the macro to be expressed as `%global` or
// `%define`. If empty, the existing form in the spec is preserved.
Kind string `toml:"kind,omitempty" json:"kind,omitempty" jsonschema:"enum=global,enum=define,title=Kind,description=Force '%global' or '%define' form (defaults to whichever is already in the spec)"`
}

const (
// ComponentOverlayAddSpecTag is an overlay that adds a tag to the spec; fails if the tag already exists.
ComponentOverlayAddSpecTag ComponentOverlayType = "spec-add-tag"
Expand All @@ -148,6 +176,12 @@ const (
ComponentOverlayUpdateSpecTag ComponentOverlayType = "spec-update-tag"
// ComponentOverlayRemoveSpecTag is an overlay that removes a tag from the spec; fails if the tag doesn't exist.
ComponentOverlayRemoveSpecTag ComponentOverlayType = "spec-remove-tag"
// ComponentOverlaySetSpecMacros is an overlay that sets the value of one or more
// `%global` / `%define` macros in the spec. Auto-detects which form (global vs
// define) is currently used in the spec for each macro; an explicit `kind` may
// be provided to force a specific form. Fails if a target macro is not present
// in the spec.
ComponentOverlaySetSpecMacros ComponentOverlayType = "spec-set-macros"
// ComponentOverlayPrependSpecLines is an overlay that prepends lines to a section in a spec; fails if the section
// doesn't exist.
ComponentOverlayPrependSpecLines ComponentOverlayType = "spec-prepend-lines"
Expand Down Expand Up @@ -236,6 +270,20 @@ func (c *ComponentOverlay) Validate() error {
if c.Tag == "" {
return missingField("tag")
}
case ComponentOverlaySetSpecMacros:
if len(c.Macros) == 0 {
return missingField("macros")
}

for name, macroSpec := range c.Macros {
if err := validateMacroName(name, c.Type, desc); err != nil {
return err
}

if err := validateMacroSetSpec(name, macroSpec, c.Type, desc); err != nil {
return err
}
}
case ComponentOverlayPrependSpecLines, ComponentOverlayAppendSpecLines:
if len(c.Lines) == 0 {
return missingField("lines")
Expand Down Expand Up @@ -328,6 +376,65 @@ func validateRegex(pattern, overlayDesc string) error {
return nil
}

// validateMacroName ensures the provided macro name is well-formed for the
// spec-set-macros overlay. Macro names must be non-empty, must not contain
// whitespace or `%`, and must not be function-like (parentheses).
func validateMacroName(name string, overlayType ComponentOverlayType, overlayDesc string) error {
if name == "" {
return fmt.Errorf("overlay type %#q has empty macro name: %s", overlayType, overlayDesc)
}

if strings.ContainsAny(name, " \t\r\n") {
return fmt.Errorf(
"overlay type %#q has invalid macro name %#q (must not contain whitespace): %s",
overlayType, name, overlayDesc,
)
}

if strings.ContainsAny(name, "%()") {
return fmt.Errorf(
"overlay type %#q has invalid macro name %#q (must not contain '%%' or parentheses; "+
"function-like macros are not supported): %s",
overlayType, name, overlayDesc,
)
}

return nil
}

// validateMacroSetSpec checks per-entry constraints for a spec-set-macros entry.
func validateMacroSetSpec(
name string, macroSpec MacroSetSpec,
overlayType ComponentOverlayType, overlayDesc string,
) error {
if macroSpec.Value == "" {
return fmt.Errorf(
"overlay type %#q requires %#q for macro %#q: %s",
overlayType, "value", name, overlayDesc,
)
}

if strings.ContainsAny(macroSpec.Value, "\r\n") {
return fmt.Errorf(
"overlay type %#q value for macro %#q must not contain newlines "+
"(multi-line macro values are not supported): %s",
overlayType, name, overlayDesc,
)
}

switch macroSpec.Kind {
case "", MacroKindGlobal, MacroKindDefine:
// OK.
default:
return fmt.Errorf(
"overlay type %#q has invalid %#q value %#q for macro %#q (expected %#q or %#q): %s",
overlayType, "kind", macroSpec.Kind, name, MacroKindGlobal, MacroKindDefine, overlayDesc,
)
Comment thread
reubeno marked this conversation as resolved.
}

return nil
}

// validateGlobPattern checks if the provided glob pattern is valid.
func validateGlobPattern(pattern, overlayDesc string) error {
if !doublestar.ValidatePattern(pattern) {
Expand Down
Loading
Loading