Skip to content

Commit cbb7393

Browse files
authored
feat(overlays): add spec-remove-section (#40)
1 parent d8c6a4b commit cbb7393

File tree

11 files changed

+340
-2
lines changed

11 files changed

+340
-2
lines changed

docs/user/reference/config/overlays.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ These overlays modify `.spec` files using the structured spec parser, allowing p
2222
| `spec-prepend-lines` | Prepends lines to the start of a section; **fails if section doesn't exist** | `lines` |
2323
| `spec-append-lines` | Appends lines to the end of a section; **fails if section doesn't exist** | `lines` |
2424
| `spec-search-replace` | Regex-based search and replace on spec content | `regex` |
25+
| `spec-remove-section` | Removes an entire section from the spec; **fails if section doesn't exist** | `section` |
2526
| `patch-add` | Adds a patch file and registers it in the spec (PatchN tag or %patchlist) | `source` |
2627
| `patch-remove` | Removes patch files and their spec references matching a glob pattern | `file` |
2728

@@ -53,7 +54,7 @@ successfully makes a replacement to at least one matching file.
5354
| Description | `description` | Human-readable explanation documenting the need for the change; helps identify overlays in error messages | All (optional) |
5455
| 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` |
5556
| 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) |
56-
| Section | `section` | The spec section to target (e.g., `%build`, `%install`, `%files`, `%description`) | `spec-prepend-lines`, `spec-append-lines`, `spec-search-replace` (optional) |
57+
| 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` |
5758
| Package | `package` | The sub-package name for multi-package specs; omit to target the main package | All spec overlays (optional) |
5859
| Regex | `regex` | Regular expression pattern to match | `spec-search-replace`, `file-search-replace` |
5960
| 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` |
@@ -272,6 +273,29 @@ description = "Remove CVE patches that are now upstream"
272273
> `PatchN` tags. Macro-based tag numbering (e.g., `Patch%{n}`) is not expanded and may
273274
> conflict with auto-assigned numbers.
274275
276+
### Removing a Section
277+
278+
The `spec-remove-section` overlay removes an entire section from the spec, including its
279+
header and all body lines. The section is identified by `section` name and optionally
280+
scoped to a specific sub-package with `package`.
281+
282+
```toml
283+
[[components.mypackage.overlays]]
284+
type = "spec-remove-section"
285+
section = "%generate_buildrequires"
286+
description = "Remove dynamic build requirements generation"
287+
```
288+
289+
To remove a section from a specific sub-package:
290+
291+
```toml
292+
[[components.mypackage.overlays]]
293+
type = "spec-remove-section"
294+
section = "%files"
295+
package = "devel"
296+
description = "Remove devel sub-package files section"
297+
```
298+
275299
## Validation
276300

277301
Overlay configurations are validated when the config file is loaded. Validation checks:

internal/app/azldev/core/sources/overlays.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ func ApplySpecOverlay(overlay projectconfig.ComponentOverlay, openedSpec *spec.S
162162
if err != nil {
163163
return fmt.Errorf("failed to search and replace in spec:\n%w", err)
164164
}
165+
case projectconfig.ComponentOverlayRemoveSection:
166+
err := openedSpec.RemoveSection(overlay.SectionName, overlay.PackageName)
167+
if err != nil {
168+
return fmt.Errorf("failed to remove section from spec:\n%w", err)
169+
}
165170
case projectconfig.ComponentOverlayAddPatch:
166171
destFilename := overlay.Filename
167172
if destFilename == "" {

internal/app/azldev/core/sources/overlays_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,3 +1177,72 @@ func TestApplyRemovePatchOverlay(t *testing.T) {
11771177
require.Error(t, err)
11781178
})
11791179
}
1180+
1181+
func TestApplyRemoveSectionOverlay(t *testing.T) {
1182+
t.Run("removes section from spec", func(t *testing.T) {
1183+
specContent := `Name: test
1184+
Version: 1.0
1185+
1186+
%generate_buildrequires
1187+
%cargo_generate_buildrequires
1188+
1189+
%build
1190+
make
1191+
`
1192+
overlay := projectconfig.ComponentOverlay{
1193+
Type: projectconfig.ComponentOverlayRemoveSection,
1194+
SectionName: "%generate_buildrequires",
1195+
}
1196+
1197+
result, err := applyOverlayToSpecContents(t, overlay, specContent)
1198+
require.NoError(t, err)
1199+
1200+
assert.NotContains(t, result, "%generate_buildrequires")
1201+
assert.NotContains(t, result, "%cargo_generate_buildrequires")
1202+
assert.Contains(t, result, "%build")
1203+
assert.Contains(t, result, "make")
1204+
})
1205+
1206+
t.Run("removes section scoped by package", func(t *testing.T) {
1207+
specContent := `Name: test
1208+
1209+
%files
1210+
/usr/bin/test
1211+
1212+
%files devel
1213+
/usr/include/test.h
1214+
1215+
%files libs
1216+
/usr/lib/libtest.so
1217+
`
1218+
overlay := projectconfig.ComponentOverlay{
1219+
Type: projectconfig.ComponentOverlayRemoveSection,
1220+
SectionName: "%files",
1221+
PackageName: "devel",
1222+
}
1223+
1224+
result, err := applyOverlayToSpecContents(t, overlay, specContent)
1225+
require.NoError(t, err)
1226+
1227+
assert.Contains(t, result, "/usr/bin/test")
1228+
assert.NotContains(t, result, "/usr/include/test.h")
1229+
assert.Contains(t, result, "/usr/lib/libtest.so")
1230+
})
1231+
1232+
t.Run("fails when section does not exist", func(t *testing.T) {
1233+
specContent := `Name: test
1234+
Version: 1.0
1235+
1236+
%build
1237+
make
1238+
`
1239+
overlay := projectconfig.ComponentOverlay{
1240+
Type: projectconfig.ComponentOverlayRemoveSection,
1241+
SectionName: "%check",
1242+
}
1243+
1244+
_, err := applyOverlayToSpecContents(t, overlay, specContent)
1245+
require.Error(t, err)
1246+
assert.Contains(t, err.Error(), "not found")
1247+
})
1248+
}

internal/projectconfig/overlay.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
// ComponentOverlay represents an overlay that may be applied to a component's spec and/or its sources.
1616
type ComponentOverlay struct {
1717
// The type of overlay to apply.
18-
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=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"`
18+
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"`
1919
// Human readable description of overlay; primarily present to document the need for the change.
2020
Description string `toml:"description,omitempty" json:"description,omitempty" jsonschema:"title=Description,description=Human readable description of overlay"`
2121

@@ -71,6 +71,7 @@ func (c *ComponentOverlay) ModifiesSpec() bool {
7171
c.Type == ComponentOverlayPrependSpecLines ||
7272
c.Type == ComponentOverlayAppendSpecLines ||
7373
c.Type == ComponentOverlaySearchAndReplaceInSpec ||
74+
c.Type == ComponentOverlayRemoveSection ||
7475
c.Type == ComponentOverlayAddPatch ||
7576
c.Type == ComponentOverlayRemovePatch
7677
}
@@ -113,6 +114,9 @@ const (
113114
ComponentOverlayAppendSpecLines ComponentOverlayType = "spec-append-lines"
114115
// ComponentOverlaySearchAndReplaceInSpec is an overlay that replaces text in a spec with other text.
115116
ComponentOverlaySearchAndReplaceInSpec ComponentOverlayType = "spec-search-replace"
117+
// ComponentOverlayRemoveSection is an overlay that removes an entire section from the spec;
118+
// fails if the section doesn't exist.
119+
ComponentOverlayRemoveSection ComponentOverlayType = "spec-remove-section"
116120
// ComponentOverlayAddPatch is an overlay that adds a patch file and registers it in the spec.
117121
// It copies the source file into the component sources and adds a PatchN tag (or appends to
118122
// %%patchlist if one exists).
@@ -242,6 +246,10 @@ func (c *ComponentOverlay) Validate() error {
242246
if err := requireFileBasename("replacement", c.Replacement); err != nil {
243247
return err
244248
}
249+
case ComponentOverlayRemoveSection:
250+
if c.SectionName == "" {
251+
return missingField("section")
252+
}
245253
case ComponentOverlayAddPatch:
246254
if c.Source == "" {
247255
return missingField("source")

internal/projectconfig/overlay_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,32 @@ func TestComponentOverlay_Validate(t *testing.T) {
359359
errorExpected: true,
360360
errorContains: "invalid glob pattern",
361361
},
362+
// spec-remove-section tests
363+
{
364+
name: "spec-remove-section valid with section only",
365+
overlay: projectconfig.ComponentOverlay{
366+
Type: projectconfig.ComponentOverlayRemoveSection,
367+
SectionName: "%generate_buildrequires",
368+
},
369+
errorExpected: false,
370+
},
371+
{
372+
name: "spec-remove-section valid with section and package",
373+
overlay: projectconfig.ComponentOverlay{
374+
Type: projectconfig.ComponentOverlayRemoveSection,
375+
SectionName: "%files",
376+
PackageName: "devel",
377+
},
378+
errorExpected: false,
379+
},
380+
{
381+
name: "spec-remove-section missing section",
382+
overlay: projectconfig.ComponentOverlay{
383+
Type: projectconfig.ComponentOverlayRemoveSection,
384+
},
385+
errorExpected: true,
386+
errorContains: "section",
387+
},
362388
}
363389

364390
for _, testCase := range testCases {
@@ -390,6 +416,7 @@ func TestComponentOverlay_ModifiesSpec(t *testing.T) {
390416
projectconfig.ComponentOverlayPrependSpecLines,
391417
projectconfig.ComponentOverlayAppendSpecLines,
392418
projectconfig.ComponentOverlaySearchAndReplaceInSpec,
419+
projectconfig.ComponentOverlayRemoveSection,
393420
projectconfig.ComponentOverlayAddPatch,
394421
projectconfig.ComponentOverlayRemovePatch,
395422
}

internal/rpm/spec/edit.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,3 +733,56 @@ func (s *Spec) GetHighestPatchTagNumber() (int, error) {
733733

734734
return highest, err
735735
}
736+
737+
// RemoveSection removes an entire section from the spec, including its header line and all
738+
// body lines. The section is identified by name and optional package qualifier. Returns
739+
// [ErrSectionNotFound] if the section doesn't exist.
740+
func (s *Spec) RemoveSection(sectionName, packageName string) error {
741+
slog.Debug("Removing section from spec", "section", sectionName, "package", packageName)
742+
743+
if sectionName == "" {
744+
return errors.New("cannot remove the global/preamble section")
745+
}
746+
747+
// Find the start and end line numbers for the section.
748+
startLine := -1
749+
endLine := -1
750+
751+
err := s.Visit(func(ctx *Context) error {
752+
if startLine >= 0 && endLine >= 0 {
753+
// Already found the section boundaries.
754+
return nil
755+
}
756+
757+
if ctx.Target.TargetType == SectionStartTarget {
758+
if ctx.CurrentSection.SectName == sectionName && ctx.CurrentSection.Package == packageName {
759+
startLine = ctx.CurrentLineNum
760+
}
761+
}
762+
763+
if ctx.Target.TargetType == SectionEndTarget {
764+
if ctx.CurrentSection.SectName == sectionName && ctx.CurrentSection.Package == packageName {
765+
endLine = ctx.CurrentLineNum
766+
}
767+
}
768+
769+
return nil
770+
})
771+
if err != nil {
772+
return fmt.Errorf("failed to scan spec for section %#q (package=%#q):\n%w", sectionName, packageName, err)
773+
}
774+
775+
if startLine < 0 {
776+
return fmt.Errorf("section %#q (package=%#q) not found:\n%w", sectionName, packageName, ErrSectionNotFound)
777+
}
778+
779+
// endLine is the virtual end marker at the start of the next section (or EOF).
780+
// We remove rawLines[startLine..endLine-1] inclusive.
781+
if endLine < 0 {
782+
endLine = len(s.rawLines)
783+
}
784+
785+
s.RemoveLines(startLine, endLine)
786+
787+
return nil
788+
}

0 commit comments

Comments
 (0)