Skip to content

Commit 5dc25e1

Browse files
authored
fix: Source9999 could be added in the wrong place, causing build breaks (#463)
1 parent b72e3a3 commit 5dc25e1

11 files changed

Lines changed: 416 additions & 11 deletions

File tree

docs/reference/overlays.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ These overlays modify `.spec` files using the structured spec parser, allowing p
1515
| Type | Description | Required Fields |
1616
|------|-------------|-----------------|
1717
| `spec-add-tag` | Adds a tag to the spec; **fails if the tag already exists** | `tag`, `value` |
18+
| `spec-insert-tag` | Inserts a tag after the last tag of the same family (e.g., `Source9999` after the last `Source*`); falls back to after the last tag of any kind, then appends to the section end. **Fails if targeting a sub-package that doesn't exist.** | `tag`, `value` |
1819
| `spec-set-tag` | Sets a tag value; replaces if exists, adds if not | `tag`, `value` |
1920
| `spec-update-tag` | Updates an existing tag; **fails if the tag doesn't exist** | `tag`, `value` |
2021
| `spec-remove-tag` | Removes a tag from the spec; **fails if the tag doesn't exist** | `tag` |
@@ -50,8 +51,8 @@ successfully makes a replacement to at least one matching file.
5051
|-------|----------|-------------|---------|
5152
| Type | `type` | **Required.** The overlay type to apply | All overlays |
5253
| Description | `description` | Human-readable explanation documenting the need for the change; helps identify overlays in error messages | All (optional) |
53-
| Tag | `tag` | The spec tag name (e.g., `BuildRequires`, `Requires`, `Version`) | `spec-add-tag`, `spec-set-tag`, `spec-update-tag`, `spec-remove-tag` |
54-
| Value | `value` | The tag value to set, or value to match for removal | `spec-add-tag`, `spec-set-tag`, `spec-update-tag`, `spec-remove-tag` (optional for matching) |
54+
| 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` |
55+
| 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) |
5556
| Section | `section` | The spec section to target (e.g., `%build`, `%install`, `%files`, `%description`) | `spec-prepend-lines`, `spec-append-lines`, `spec-search-replace` (optional) |
5657
| Package | `package` | The sub-package name for multi-package specs; omit to target the main package | All spec overlays (optional) |
5758
| Regex | `regex` | Regular expression pattern to match | `spec-search-replace`, `file-search-replace` |
@@ -74,6 +75,22 @@ tag = "BuildRequires"
7475
value = "some-devel-package"
7576
```
7677

78+
### Inserting a Tag by Family
79+
80+
Use `spec-insert-tag` to place a tag after the last existing tag of the same family rather than appending it to the end of the section. The "family" is determined by stripping trailing digits from the tag name (case-insensitive), so `Source0`, `Source1`, and `Source` all belong to the `Source` family.
81+
82+
This is useful when tag placement matters — for example, ensuring a new `Source` tag doesn't end up after macros like `%fontpkg` or inside `%if` conditionals:
83+
84+
```toml
85+
[[components.mypackage.overlays]]
86+
type = "spec-insert-tag"
87+
description = "Add macros file as a source"
88+
tag = "Source9999"
89+
value = "macros.azl.macros"
90+
```
91+
92+
If no tags from the same family exist, the tag is placed after the last tag of any kind. If there are no tags at all, it falls back to `spec-add-tag` behavior (appending to the section end).
93+
7794
### Setting a Version
7895

7996
Use `spec-set-tag` when you want to set a value regardless of whether the tag exists:

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ func ApplySpecOverlay(overlay projectconfig.ComponentOverlay, openedSpec *spec.S
125125
if err != nil {
126126
return fmt.Errorf("failed to add tag %#q to spec:\n%w", overlay.Tag, err)
127127
}
128+
case projectconfig.ComponentOverlayInsertSpecTag:
129+
err := openedSpec.InsertTag(overlay.PackageName, overlay.Tag, overlay.Value)
130+
if err != nil {
131+
return fmt.Errorf("failed to insert tag %#q into spec:\n%w", overlay.Tag, err)
132+
}
128133
case projectconfig.ComponentOverlayUpdateSpecTag:
129134
err := openedSpec.UpdateExistingTag(overlay.PackageName, overlay.Tag, overlay.Value)
130135
if err != nil {

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,35 @@ BuildRequires: added-package
116116
PackageName: "i-do-not-exist",
117117
},
118118
spec: `Name: name
119+
`,
120+
errorExpected: true,
121+
},
122+
{
123+
name: "insert tag after same family",
124+
overlay: projectconfig.ComponentOverlay{
125+
Type: projectconfig.ComponentOverlayInsertSpecTag,
126+
Tag: "Source9999",
127+
Value: "macros.azl.macros",
128+
},
129+
spec: `Name: name
130+
Source0: test.tar.gz
131+
BuildRequires: gcc
132+
`,
133+
result: `Name: name
134+
Source0: test.tar.gz
135+
Source9999: macros.azl.macros
136+
BuildRequires: gcc
137+
`,
138+
},
139+
{
140+
name: "insert tag to non-existent package",
141+
overlay: projectconfig.ComponentOverlay{
142+
Type: projectconfig.ComponentOverlayInsertSpecTag,
143+
Tag: "Source9999",
144+
Value: "macros.azl.macros",
145+
PackageName: "i-do-not-exist",
146+
},
147+
spec: `Name: name
119148
`,
120149
errorExpected: true,
121150
},

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,14 @@ func NewPreparer(
8686
func (p *sourcePreparerImpl) PrepareSources(
8787
ctx context.Context, component components.Component, outputDir string, applyOverlays bool,
8888
) error {
89-
// Download any explicitly configured source files first.
90-
// Files defined here take precedence over any sources implicitly defined by the fedora upstream.
89+
// Use the source manager to fetch source files (archives, patches, etc.)
9190
err := p.sourceManager.FetchFiles(ctx, component, outputDir)
9291
if err != nil {
9392
return fmt.Errorf("failed to fetch source files for component %#q:\n%w",
9493
component.GetName(), err)
9594
}
9695

97-
// Fetch the component sources.
96+
// Use the source manager to fetch the component (spec file and sidecar files).
9897
err = p.sourceManager.FetchComponent(ctx, component, outputDir)
9998
if err != nil {
10099
return fmt.Errorf("failed to fetch sources for component %#q:\n%w",
@@ -290,10 +289,9 @@ func synthesizeMacroLoadOverlays(macrosFileName string) ([]projectconfig.Compone
290289
{
291290
// Ensure that the macros file is manifested as a source in the spec so that
292291
// mock and other tools know it needs to be present in the build root.
293-
// Ideally we'd dynamically compute a unique source tag here, but for now
294-
// we just use a high number to stay simple. If a conflict is found, this
295-
// overlay application will fail.
296-
Type: projectconfig.ComponentOverlayAddSpecTag,
292+
// Use InsertSpecTag to place it after the last existing Source* tag, avoiding
293+
// misplacement after macros like %fontpkg or inside %if conditionals.
294+
Type: projectconfig.ComponentOverlayInsertSpecTag,
297295
Tag: "Source9999", // Use a high number to avoid conflicts with existing sources.
298296
Value: macrosFileName,
299297
},

internal/projectconfig/overlay.go

Lines changed: 8 additions & 2 deletions
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-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=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

@@ -64,6 +64,7 @@ func (c *ComponentOverlay) WithAbsolutePaths(referenceDir string) (result *Compo
6464
// also require spec modifications.
6565
func (c *ComponentOverlay) ModifiesSpec() bool {
6666
return c.Type == ComponentOverlayAddSpecTag ||
67+
c.Type == ComponentOverlayInsertSpecTag ||
6768
c.Type == ComponentOverlaySetSpecTag ||
6869
c.Type == ComponentOverlayUpdateSpecTag ||
6970
c.Type == ComponentOverlayRemoveSpecTag ||
@@ -93,6 +94,10 @@ type ComponentOverlayType string
9394
const (
9495
// ComponentOverlayAddSpecTag is an overlay that adds a tag to the spec; fails if the tag already exists.
9596
ComponentOverlayAddSpecTag ComponentOverlayType = "spec-add-tag"
97+
// ComponentOverlayInsertSpecTag is an overlay that inserts a tag into the spec, placing it
98+
// after the last existing tag from the same family (e.g., Source9999 after the last Source* tag).
99+
// Falls back to after the last tag of any kind, then to appending at the section end.
100+
ComponentOverlayInsertSpecTag ComponentOverlayType = "spec-insert-tag"
96101
// ComponentOverlaySetSpecTag is an overlay that sets a tag to the spec. If the tag already exists, replaces
97102
// its existing value; otherwise, adds the tag.
98103
ComponentOverlaySetSpecTag ComponentOverlayType = "spec-set-tag"
@@ -172,7 +177,8 @@ func (c *ComponentOverlay) Validate() error {
172177
}
173178

174179
switch c.Type {
175-
case ComponentOverlayAddSpecTag, ComponentOverlaySetSpecTag, ComponentOverlayUpdateSpecTag:
180+
case ComponentOverlayAddSpecTag, ComponentOverlayInsertSpecTag,
181+
ComponentOverlaySetSpecTag, ComponentOverlayUpdateSpecTag:
176182
if c.Tag == "" {
177183
return missingField("tag")
178184
}

internal/projectconfig/overlay_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,34 @@ func TestComponentOverlay_Validate(t *testing.T) {
4747
errorExpected: true,
4848
errorContains: "value",
4949
},
50+
// spec-insert-tag tests
51+
{
52+
name: "spec-insert-tag valid",
53+
overlay: projectconfig.ComponentOverlay{
54+
Type: projectconfig.ComponentOverlayInsertSpecTag,
55+
Tag: "Source9999",
56+
Value: "macros.azl.macros",
57+
},
58+
errorExpected: false,
59+
},
60+
{
61+
name: "spec-insert-tag missing tag",
62+
overlay: projectconfig.ComponentOverlay{
63+
Type: projectconfig.ComponentOverlayInsertSpecTag,
64+
Value: "some-value",
65+
},
66+
errorExpected: true,
67+
errorContains: "tag",
68+
},
69+
{
70+
name: "spec-insert-tag missing value",
71+
overlay: projectconfig.ComponentOverlay{
72+
Type: projectconfig.ComponentOverlayInsertSpecTag,
73+
Tag: "Source9999",
74+
},
75+
errorExpected: true,
76+
errorContains: "value",
77+
},
5078
// spec-set-tag tests
5179
{
5280
name: "spec-set-tag valid",
@@ -355,6 +383,7 @@ func TestComponentOverlay_Validate(t *testing.T) {
355383
func TestComponentOverlay_ModifiesSpec(t *testing.T) {
356384
specOverlayTypes := []projectconfig.ComponentOverlayType{
357385
projectconfig.ComponentOverlayAddSpecTag,
386+
projectconfig.ComponentOverlayInsertSpecTag,
358387
projectconfig.ComponentOverlaySetSpecTag,
359388
projectconfig.ComponentOverlayUpdateSpecTag,
360389
projectconfig.ComponentOverlayRemoveSpecTag,

internal/rpm/spec/edit.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,114 @@ func (s *Spec) AddTag(packageName string, tag string, value string) (err error)
167167
return s.AppendLinesToSection(sectionName, packageName, []string{fmt.Sprintf("%s: %s", tag, value)})
168168
}
169169

170+
// tagFamily returns the "family" prefix of a tag name by stripping any trailing digits.
171+
// For example, "Source9999" returns "source", "Patch100" returns "patch", and
172+
// "BuildRequires" returns "buildrequires". The result is always lowercased.
173+
func tagFamily(tag string) string {
174+
lower := strings.ToLower(tag)
175+
176+
// Strip trailing digits.
177+
end := len(lower)
178+
for end > 0 && lower[end-1] >= '0' && lower[end-1] <= '9' {
179+
end--
180+
}
181+
182+
// If the entire tag is digits, return the full lowered tag.
183+
if end == 0 {
184+
return lower
185+
}
186+
187+
return lower[:end]
188+
}
189+
190+
// InsertTag inserts a tag into the spec, placing it after the last existing tag from the
191+
// same "family" (e.g., Source9999 is placed after the last Source* tag). If no tags from
192+
// the same family exist, the tag is placed after the last tag of any kind. If there are no
193+
// tags at all, it falls back to [AddTag] behavior (appending to the section end).
194+
//
195+
// The tag family is determined by stripping trailing digits from the tag name
196+
// (case-insensitive). For example, "Source0", "Source1", and "Source" all belong to the
197+
// "source" family.
198+
//
199+
// Note: When inserting into a sub-package (non-empty packageName), the corresponding
200+
// %package section must already exist in the spec; otherwise, an [ErrSectionNotFound]
201+
// error is returned.
202+
func (s *Spec) InsertTag(packageName string, tag string, value string) error {
203+
slog.Debug("Inserting tag to spec", "package", packageName, "tag", tag, "value", value)
204+
205+
family := tagFamily(tag)
206+
newLine := fmt.Sprintf("%s: %s", tag, value)
207+
208+
// First pass: find the best insertion point.
209+
// Track both the last same-family tag line and the last tag of any kind.
210+
lastFamilyTagLineNum := -1
211+
lastAnyTagLineNum := -1
212+
sectionFound := false
213+
214+
sectionName := ""
215+
if packageName != "" {
216+
sectionName = "%package"
217+
}
218+
219+
err := s.Visit(func(ctx *Context) error {
220+
// Track whether the target section exists.
221+
if ctx.Target.TargetType == SectionStartTarget {
222+
if ctx.CurrentSection.SectName == sectionName && ctx.CurrentSection.Package == packageName {
223+
sectionFound = true
224+
}
225+
}
226+
227+
if ctx.Target.TargetType != SectionLineTarget {
228+
return nil
229+
}
230+
231+
// Only consider tags in the target section/package.
232+
if ctx.CurrentSection.SectName != sectionName || ctx.CurrentSection.Package != packageName {
233+
return nil
234+
}
235+
236+
if ctx.Target.Line.Parsed.GetType() != Tag {
237+
return nil
238+
}
239+
240+
tagLine, ok := ctx.Target.Line.Parsed.(*TagLine)
241+
if !ok {
242+
return nil
243+
}
244+
245+
lastAnyTagLineNum = ctx.CurrentLineNum
246+
247+
if tagFamily(tagLine.Tag) == family {
248+
lastFamilyTagLineNum = ctx.CurrentLineNum
249+
}
250+
251+
return nil
252+
})
253+
if err != nil {
254+
return fmt.Errorf("failed to scan spec for tag insertion point:\n%w", err)
255+
}
256+
257+
if !sectionFound {
258+
return fmt.Errorf("section %#q (package=%#q) not found:\n%w", sectionName, packageName, ErrSectionNotFound)
259+
}
260+
261+
// Determine insertion point: prefer same-family, then any tag, then fall back to AddTag.
262+
insertAfterLine := lastFamilyTagLineNum
263+
if insertAfterLine < 0 {
264+
insertAfterLine = lastAnyTagLineNum
265+
}
266+
267+
if insertAfterLine < 0 {
268+
// No tags at all — fall back to AddTag behavior.
269+
return s.AddTag(packageName, tag, value)
270+
}
271+
272+
// Insert after the found line (0-indexed, so insertAfterLine+1).
273+
s.InsertLinesAt([]string{newLine}, insertAfterLine+1)
274+
275+
return nil
276+
}
277+
170278
// PrependLinesToSection prepends the given lines to the start of the specified section, placing
171279
// them just after the section header (or at the top of the file in the global section). An error
172280
// is returned if the identified section cannot be found in the spec.

0 commit comments

Comments
 (0)