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
39 changes: 34 additions & 5 deletions docs/user/reference/config/overlays.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ 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-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` |
| `spec-prepend-lines` | Prepends lines to the start of a section, or to the top of the file if `section` is omitted; **fails if a named section doesn't exist** | `lines` |
| `spec-append-lines` | Appends lines to the end of a section, or to the bottom of the file if `section` is omitted; **fails if a named section doesn't exist** | `lines` |
| `spec-search-replace` | Regex-based search and replace on spec content; targets a single section if `section` is given, otherwise the entire spec | `regex` |
| `spec-remove-section` | Removes an entire section from the spec; **fails if section doesn't exist** | `section` |
| `spec-remove-subpackage` | Removes every section associated with a sub-package (e.g. its `%package`, `%description`, `%files`, `%post`, `%postun`, ...); **fails if no such sections exist** | `package` |
| `patch-add` | Adds a patch file and registers it in the spec (PatchN tag or %patchlist) | `source` |
Expand Down Expand Up @@ -55,8 +55,8 @@ successfully makes a replacement to at least one matching file.
| Description | `description` | Human-readable explanation documenting the need for the change; helps identify overlays in error messages | All (optional) |
| Tag | `tag` | The spec tag name (e.g., `BuildRequires`, `Requires`, `Version`) | `spec-add-tag`, `spec-insert-tag`, `spec-set-tag`, `spec-update-tag`, `spec-remove-tag` |
| Value | `value` | The tag value to set, or value to match for removal | `spec-add-tag`, `spec-insert-tag`, `spec-set-tag`, `spec-update-tag`, `spec-remove-tag` (optional for matching) |
| Section | `section` | The spec section to target (e.g., `%build`, `%install`, `%files`, `%description`) | `spec-prepend-lines`, `spec-append-lines`, `spec-search-replace` (optional), `spec-remove-section` |
| Package | `package` | The sub-package name for multi-package specs; omit to target the main package | All spec overlays (optional, except `spec-remove-subpackage` which **requires** it) |
| Section | `section` | The spec section to target (e.g., `%build`, `%install`, `%files`, `%description`). Optional for `spec-prepend-lines`, `spec-append-lines`, and `spec-search-replace` — omit to target the entire spec file. Required for `spec-remove-section`. | `spec-prepend-lines` (optional), `spec-append-lines` (optional), `spec-search-replace` (optional), `spec-remove-section` |
| Package | `package` | The sub-package name for multi-package specs; omit to target the main package. Cannot be combined with an omitted `section` (a sub-package is always a sub-qualifier of a section). | All spec overlays (optional, except `spec-remove-subpackage` which **requires** it) |
| 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` |
Expand Down Expand Up @@ -150,6 +150,35 @@ regex = "--enable-deprecated-feature\\s*"
replacement = ""
```

### Targeting the Entire Spec File

The `spec-prepend-lines`, `spec-append-lines`, and `spec-search-replace` overlays accept an
empty/omitted `section` field to operate on the whole spec file rather than a single section:
prepend inserts at the very top of the file, append inserts at the very bottom, and search-replace
scans every section. The `package` field cannot be combined with an omitted `section`.

```toml
[[components.mypackage.overlays]]
type = "spec-prepend-lines"
description = "Add a top-of-file banner comment"
lines = ["# This spec is maintained by the Azure Linux team."]
```

```toml
[[components.mypackage.overlays]]
type = "spec-append-lines"
description = "Append a trailing macro definition"
lines = ["%global azl_marker 1"]
```

```toml
[[components.mypackage.overlays]]
type = "spec-search-replace"
description = "Rename the project everywhere it appears"
regex = "oldname"
replacement = "newname"
```

### Targeting a Sub-Package

For multi-package specs, use the `package` field to target a specific sub-package:
Expand Down
22 changes: 15 additions & 7 deletions internal/app/azldev/core/sources/overlays.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,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 inflated by 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 @@ -146,14 +146,22 @@ func ApplySpecOverlay(overlay projectconfig.ComponentOverlay, openedSpec *spec.S
return fmt.Errorf("failed to remove tag %#q from spec:\n%w", overlay.Tag, err)
}
case projectconfig.ComponentOverlayPrependSpecLines:
err := openedSpec.PrependLinesToSection(overlay.SectionName, overlay.PackageName, overlay.Lines)
if err != nil {
return fmt.Errorf("failed to prepend lines to spec:\n%w", err)
if overlay.SectionName == "" {
openedSpec.PrependLines(overlay.Lines)
} else {
err := openedSpec.PrependLinesToSection(overlay.SectionName, overlay.PackageName, overlay.Lines)
if err != nil {
return fmt.Errorf("failed to prepend lines to spec:\n%w", err)
}
}
case projectconfig.ComponentOverlayAppendSpecLines:
err := openedSpec.AppendLinesToSection(overlay.SectionName, overlay.PackageName, overlay.Lines)
if err != nil {
return fmt.Errorf("failed to append lines to spec:\n%w", err)
if overlay.SectionName == "" {
openedSpec.AppendLines(overlay.Lines)
} else {
err := openedSpec.AppendLinesToSection(overlay.SectionName, overlay.PackageName, overlay.Lines)
if err != nil {
return fmt.Errorf("failed to append lines to spec:\n%w", err)
}
}
case projectconfig.ComponentOverlaySearchAndReplaceInSpec:
err := openedSpec.SearchAndReplace(
Expand Down
81 changes: 81 additions & 0 deletions internal/app/azldev/core/sources/overlays_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func applyOverlayToSpecContents(
return outputBuffer.String(), nil
}

//nolint:maintidx // Test table complexity scales with the number of overlay types.
func TestApplySpecOverlay(t *testing.T) {
testCases := []struct {
name string
Expand Down Expand Up @@ -256,6 +257,58 @@ line1
`,
errorExpected: true,
},
{
name: "prepend lines to entire spec (no section)",
overlay: projectconfig.ComponentOverlay{
Type: projectconfig.ComponentOverlayPrependSpecLines,
Lines: []string{"# top of file"},
},
spec: `Name: name

%description
text

%changelog
* Mon Jan 01 2024 User <user@example.com> - 1.0-1
- Initial release
`,
result: `# top of file
Name: name

%description
text

%changelog
* Mon Jan 01 2024 User <user@example.com> - 1.0-1
- Initial release
`,
},
{
name: "append lines to entire spec (no section)",
overlay: projectconfig.ComponentOverlay{
Type: projectconfig.ComponentOverlayAppendSpecLines,
Lines: []string{"# end of file"},
},
spec: `Name: name

%description
text

%changelog
* Mon Jan 01 2024 User <user@example.com> - 1.0-1
- Initial release
`,
result: `Name: name

%description
text

%changelog
* Mon Jan 01 2024 User <user@example.com> - 1.0-1
- Initial release
# end of file
`,
},
{
name: "search and replace",
overlay: projectconfig.ComponentOverlay{
Expand Down Expand Up @@ -296,6 +349,34 @@ line1
`,
errorExpected: true,
},
{
name: "search and replace across entire spec (no section)",
overlay: projectconfig.ComponentOverlay{
Type: projectconfig.ComponentOverlaySearchAndReplaceInSpec,
Regex: `oldname`,
Replacement: "newname",
},
spec: `Name: oldname

%description
oldname package

%build
./configure --prefix=/opt/oldname

%changelog
`,
result: `Name: newname

%description
newname package

%build
./configure --prefix=/opt/newname

%changelog
`,
},
}

for _, testCase := range testCases {
Expand Down
32 changes: 30 additions & 2 deletions internal/projectconfig/overlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@ type ComponentOverlay struct {
// apply to multiple files, supports glob patterns (including globstar).
Filename string `toml:"file,omitempty" json:"file,omitempty" jsonschema:"title=Filename,description=The name of the non-spec file to which this overlay applies, or a glob pattern matching multiple files"`
// For overlays that apply to specs, indicates the name of the section to which it applies.
SectionName string `toml:"section,omitempty" json:"section,omitempty" jsonschema:"title=Section name,description=The name of the section to which this overlay applies"`
// Optional for spec-prepend-lines, spec-append-lines, and spec-search-replace: when omitted,
// the overlay targets the entire spec file (prepend at top, append at end, search-replace
// across all sections).
SectionName string `toml:"section,omitempty" json:"section,omitempty" jsonschema:"title=Section name,description=The name of the section to which this overlay applies. Optional for spec-prepend-lines/spec-append-lines/spec-search-replace; when omitted these overlays target the entire spec file."`
// For overlays that apply to specs, indicates the name of the sub-package to which it applies.
PackageName string `toml:"package,omitempty" json:"package,omitempty" jsonschema:"title=Package name,description=The name of the sub-package to which this overlay applies"`
// A sub-package is always a sub-qualifier of a section, so this field cannot be combined
// with an omitted SectionName on overlays that support whole-file targeting.
PackageName string `toml:"package,omitempty" json:"package,omitempty" jsonschema:"title=Package name,description=The name of the sub-package to which this overlay applies. Cannot be combined with an omitted section on overlays that support whole-file targeting."`
// For overlays that apply to spec tags, indicates the name of the tag.
Tag string `toml:"tag,omitempty" json:"tag,omitempty" jsonschema:"title=Tag,description=For overlays that apply to spec tags, indicates the name of the tag"`
// For overlays that apply to values in specs, an exact string value to match.
Expand Down Expand Up @@ -202,6 +207,21 @@ func (c *ComponentOverlay) Validate() error {
return fmt.Errorf("overlay type %#q does not accept %#q field: %s", c.Type, fieldName, desc)
}

// requireSectionIfPackageSet checks that, for overlays that may target either a single
// section or the entire spec file (indicated by omitting `section`), a `package` is only
// specified when a `section` is also specified. A package is always a sub-qualifier of
// a section, so specifying one without the other is meaningless.
requireSectionIfPackageSet := func() error {
if c.SectionName == "" && c.PackageName != "" {
return fmt.Errorf(
"overlay type %#q requires %#q field when %#q is set: %s",
c.Type, "section", "package", desc,
)
}

return nil
}

requireRelativePath := func(fieldName, value string) error {
if value == "" {
return missingField(fieldName)
Expand Down Expand Up @@ -250,6 +270,10 @@ func (c *ComponentOverlay) Validate() error {
if len(c.Lines) == 0 {
return missingField("lines")
}

if err := requireSectionIfPackageSet(); err != nil {
return err
}
case ComponentOverlaySearchAndReplaceInSpec:
if c.Regex == "" {
return missingField("regex")
Expand All @@ -258,6 +282,10 @@ func (c *ComponentOverlay) Validate() error {
if err := validateRegex(c.Regex, desc); err != nil {
return err
}

if err := requireSectionIfPackageSet(); err != nil {
return err
}
case ComponentOverlayPrependLinesToFile:
if err := requireRelativePath("file", c.Filename); err != nil {
return err
Expand Down
57 changes: 57 additions & 0 deletions internal/projectconfig/overlay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,63 @@ func TestComponentOverlay_Validate(t *testing.T) {
},
errorExpected: false,
},
// whole-file targeting (empty section) for prepend/append/search-replace
{
name: "spec-prepend-lines whole-file (no section) valid",
overlay: projectconfig.ComponentOverlay{
Type: projectconfig.ComponentOverlayPrependSpecLines,
Lines: []string{"# Top of file"},
},
errorExpected: false,
},
{
name: "spec-prepend-lines whole-file with package rejected",
overlay: projectconfig.ComponentOverlay{
Type: projectconfig.ComponentOverlayPrependSpecLines,
PackageName: "foo",
Lines: []string{"# Top of file"},
},
errorExpected: true,
errorContains: "package",
},
{
name: "spec-append-lines whole-file (no section) valid",
overlay: projectconfig.ComponentOverlay{
Type: projectconfig.ComponentOverlayAppendSpecLines,
Lines: []string{"# End of file"},
},
errorExpected: false,
},
{
name: "spec-append-lines whole-file with package rejected",
overlay: projectconfig.ComponentOverlay{
Type: projectconfig.ComponentOverlayAppendSpecLines,
PackageName: "foo",
Lines: []string{"# End of file"},
},
errorExpected: true,
errorContains: "package",
},
{
name: "spec-search-replace whole-file (no section) valid",
overlay: projectconfig.ComponentOverlay{
Type: projectconfig.ComponentOverlaySearchAndReplaceInSpec,
Regex: "pattern",
Replacement: "replacement",
},
errorExpected: false,
},
{
name: "spec-search-replace whole-file with package rejected",
overlay: projectconfig.ComponentOverlay{
Type: projectconfig.ComponentOverlaySearchAndReplaceInSpec,
PackageName: "foo",
Regex: "pattern",
Replacement: "replacement",
},
errorExpected: true,
errorContains: "package",
},
// spec-search-replace tests
{
name: "spec-search-replace valid",
Expand Down
18 changes: 18 additions & 0 deletions internal/rpm/spec/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,24 @@ func (s *Spec) skipPastConditional(lineNum int, sectionEnd int) int {
return lineNum
}

// PrependLines prepends the given lines to the very top of the spec file. Unlike
// [Spec.PrependLinesToSection] with an empty section, this does not interpret the lines as
// belonging to the global section; they are inserted verbatim above all existing content.
func (s *Spec) PrependLines(lines []string) {
slog.Debug("Prepending lines to spec file", "lines", lines)

s.rawLines = append(append([]string{}, lines...), s.rawLines...)
}

// AppendLines appends the given lines at the very bottom of the spec file. Unlike
// [Spec.AppendLinesToSection] with an empty section (which would insert before the first
// section header), this places the lines after the final line of the file.
func (s *Spec) AppendLines(lines []string) {
slog.Debug("Appending lines to spec file", "lines", lines)

s.rawLines = append(s.rawLines, lines...)
}

// PrependLinesToSection prepends the given lines to the start of the specified section, placing
// them just after the section header (or at the top of the file in the global section). An error
// is returned if the identified section cannot be found in the spec.
Expand Down
Loading
Loading