Skip to content
Open
559 changes: 559 additions & 0 deletions docs/developer/rfc/001-conditional-aware-spec-editing.md

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions docs/user/reference/config/overlays.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,42 @@ the overlay always removes every section associated with the sub-package.
> an error is returned; use a `spec-search-replace` overlay to adjust the conditionals
> before removing the sub-package.

## Known Limitations

### Section-scoped operations and straddling conditionals

Spec overlays that target a specific section and package (e.g., `spec-remove-tag` with `package = "headless"`) rely on the structural tree parser to identify which lines belong to that section. In rare cases, a section header may live inside a `%if` wrapper while its content continues past the `%endif`:

```spec
%if 0%{!?scl:1}
%package headless
Requires: binutils
%endif
# ← content below is still part of %package headless in RPM's view,
# but the tree parser cannot associate it with the headless section
# because the section header is structurally inside the wrapper.
Recommends: default-yama-scope
```

In this pattern, section-scoped overlays (`spec-remove-tag`, `spec-add-tag`, `spec-update-tag`, `spec-insert-tag`) cannot find or modify tags that fall outside the `%endif`. Use `spec-search-replace` with a precise anchored regex instead:

```toml
# Instead of:
# type = "spec-remove-tag"
# package = "headless"
# tag = "Recommends"
# value = "default-yama-scope"

# Use:
type = "spec-search-replace"
regex = "^Recommends: default-yama-scope$"
replacement = "# Recommends: default-yama-scope (disabled)"
```

### Macro-generated sections

Specs that use macros like `%ghc_lib_subpackage`, `%pyproject_extras_subpkg`, or `%fontpkg` generate sections at build time that are invisible to the static parser. Section-scoped overlays cannot target these generated sections. Use `spec-search-replace` for modifications that need to reach macro-generated content.

## Validation

Overlay configurations are validated when the config file is loaded. Validation checks:
Expand Down
5 changes: 5 additions & 0 deletions internal/app/azldev/core/sources/overlays.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ func ApplySpecOverlay(overlay projectconfig.ComponentOverlay, openedSpec *spec.S
if err != nil {
return fmt.Errorf("failed to prepend lines to spec:\n%w", err)
}
case projectconfig.ComponentOverlayPrependAllSpecLines:
err := openedSpec.PrependLinesToAllSections(overlay.SectionName, overlay.PackageName, overlay.Lines)
if err != nil {
return fmt.Errorf("failed to prepend lines to all matching sections in spec:\n%w", err)
}
case projectconfig.ComponentOverlayAppendSpecLines:
err := openedSpec.AppendLinesToSection(overlay.SectionName, overlay.PackageName, overlay.Lines)
if err != nil {
Expand Down
17 changes: 2 additions & 15 deletions internal/app/azldev/core/sources/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"log/slog"
"regexp"
"strconv"
"strings"

"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/components"
"github.com/microsoft/azure-linux-dev-tools/internal/global/opctx"
Expand Down Expand Up @@ -46,21 +45,9 @@ func GetReleaseTagValue(fs opctx.FS, specPath string) (string, error) {
return "", fmt.Errorf("failed to parse spec %#q:\n%w", specPath, err)
}

var releaseValue string

err = openedSpec.VisitTagsPackage("", func(tagLine *spec.TagLine, _ *spec.Context) error {
if strings.EqualFold(tagLine.Tag, "Release") {
releaseValue = tagLine.Value
}

return nil
})
releaseValue, err := openedSpec.GetTag("", "Release")
if err != nil {
return "", fmt.Errorf("failed to visit tags in spec %#q:\n%w", specPath, err)
}

if releaseValue == "" {
return "", fmt.Errorf("release tag not found in spec %#q:\n%w", specPath, spec.ErrNoSuchTag)
return "", fmt.Errorf("failed to get Release tag from spec %#q:\n%w", specPath, err)
}

return releaseValue, nil
Expand Down
7 changes: 4 additions & 3 deletions internal/app/azldev/core/sources/sourceprep.go
Original file line number Diff line number Diff line change
Expand Up @@ -1046,16 +1046,17 @@ func generateFileHeaderOverlay() []projectconfig.ComponentOverlay {
}

// synthesizeCheckSkipOverlays generates overlays to disable the %check section if configured.
// When check.skip is true, it prepends an 'exit 0' to the %check section with a comment
// explaining why the section was disabled.
// When check.skip is true, it prepends an 'exit 0' to every %check section in the spec with
// a comment explaining why the section was disabled. Uses [ComponentOverlayPrependAllSpecLines]
// to handle specs that contain multiple %check sections gated by different conditionals.
func synthesizeCheckSkipOverlays(checkConfig projectconfig.CheckConfig) []projectconfig.ComponentOverlay {
if !checkConfig.Skip {
return nil
}

return []projectconfig.ComponentOverlay{
{
Type: projectconfig.ComponentOverlayPrependSpecLines,
Type: projectconfig.ComponentOverlayPrependAllSpecLines,
SectionName: "%check",
Lines: []string{
"# Check section disabled: " + checkConfig.SkipReason,
Expand Down
8 changes: 7 additions & 1 deletion internal/projectconfig/overlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ func (c *ComponentOverlay) ModifiesSpec() bool {
c.Type == ComponentOverlayUpdateSpecTag ||
c.Type == ComponentOverlayRemoveSpecTag ||
c.Type == ComponentOverlayPrependSpecLines ||
c.Type == ComponentOverlayPrependAllSpecLines ||
c.Type == ComponentOverlayAppendSpecLines ||
c.Type == ComponentOverlaySearchAndReplaceInSpec ||
c.Type == ComponentOverlayRemoveSection ||
Expand Down Expand Up @@ -152,6 +153,11 @@ const (
// 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"
// ComponentOverlayPrependAllSpecLines is an overlay that prepends lines to every section
// matching the given name and package in a spec. This is useful when a spec contains multiple
// sections with the same name (e.g., two %check sections gated by different conditionals)
// and all of them need the same modification. Fails if no matching section exists.
ComponentOverlayPrependAllSpecLines ComponentOverlayType = "spec-prepend-all-lines"
// ComponentOverlayAppendSpecLines is an overlay that appends lines to a section in a spec; fails if the section
// doesn't exist.
ComponentOverlayAppendSpecLines ComponentOverlayType = "spec-append-lines"
Expand Down Expand Up @@ -246,7 +252,7 @@ func (c *ComponentOverlay) Validate() error {
if c.Tag == "" {
return missingField("tag")
}
case ComponentOverlayPrependSpecLines, ComponentOverlayAppendSpecLines:
case ComponentOverlayPrependSpecLines, ComponentOverlayPrependAllSpecLines, ComponentOverlayAppendSpecLines:
if len(c.Lines) == 0 {
return missingField("lines")
}
Expand Down
Loading
Loading