Skip to content

Commit d2e2ccf

Browse files
authored
feat: auto-load generated macros file in spec (#392)
1 parent db66300 commit d2e2ccf

3 files changed

Lines changed: 107 additions & 25 deletions

File tree

internal/app/azldev/core/componentbuilder/componentbuilder_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,19 @@ func setupBuilder(t *testing.T) *componentBuilderTestParams {
4747
workDirFactory, err := workdir.NewFactory(testEnv.Env.FS(), testEnv.Env.WorkDir(), testEnv.Env.ConstructionTime())
4848
require.NoError(t, err)
4949

50-
sourceManager := sourceproviders_test.NewNoOpMockSourceManager(ctrl)
50+
sourceManager := sourceproviders_test.NewMockSourceManager(ctrl)
51+
52+
// Configure the source manager to create a spec file when FetchComponent is called.
53+
sourceManager.EXPECT().FetchComponent(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn(
54+
func(ctx context.Context, component components.Component, outputDir string) error {
55+
// Create the expected spec file.
56+
specPath := filepath.Join(outputDir, component.GetName()+".spec")
57+
58+
return fileutils.WriteFile(testEnv.Env.FS(), specPath, []byte("# test spec"), fileperms.PublicFile)
59+
},
60+
)
61+
62+
sourceManager.EXPECT().FetchFile(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
5163

5264
preparer, err := sources.NewPreparer(sourceManager, testEnv.Env.FS(), testEnv.Env, testEnv.Env)
5365

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

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"path/filepath"
1111
"slices"
1212
"strings"
13+
"unicode"
1314

1415
"github.com/gim-home/azldev-preview/internal/app/azldev/core/components"
1516
"github.com/gim-home/azldev-preview/internal/global/opctx"
@@ -91,36 +92,37 @@ func (p *sourcePreparerImpl) PrepareSources(
9192
component.GetName(), err)
9293
}
9394

94-
// Apply any postprocessing to the sources in-place, in the output directory.
95-
err = p.postProcessSources(component, outputDir)
96-
if err != nil {
97-
return fmt.Errorf("failed to post-process sources for component %q:\n%w", component.GetName(), err)
98-
}
99-
10095
// Emit computed macros to a macros file in the output directory.
101-
err = p.writeMacrosFile(component, outputDir)
96+
macrosFilePath, err := p.writeMacrosFile(component, outputDir)
10297
if err != nil {
10398
return fmt.Errorf("failed to write macros file for component %#q:\n%w",
10499
component.GetName(), err)
105100
}
106101

102+
// Apply any postprocessing to the sources in-place, in the output directory.
103+
err = p.postProcessSources(component, outputDir, filepath.Base(macrosFilePath))
104+
if err != nil {
105+
return fmt.Errorf("failed to post-process sources for component %q:\n%w", component.GetName(), err)
106+
}
107+
107108
return nil
108109
}
109110

110111
// writeMacrosFile writes a macros file containing the resolved macros for a component.
111112
// This includes with/without flags converted to macro format, and any explicit defines.
112-
// The file is always created, even if empty (aside from the header comment).
113-
func (p *sourcePreparerImpl) writeMacrosFile(component components.Component, outputDir string) error {
113+
// The file is always created, even if empty (aside from the header comment). Returns
114+
// the path to the written macros file, which is guaranteed to be within the given outputDir.
115+
func (p *sourcePreparerImpl) writeMacrosFile(component components.Component, outputDir string) (string, error) {
114116
contents := GenerateMacrosFileContents(component.GetConfig().Build)
115117

116118
macrosFilePath := filepath.Join(outputDir, component.GetName()+MacrosFileExtension)
117119

118120
err := fileutils.WriteFile(p.fs, macrosFilePath, []byte(contents), fileperms.PublicFile)
119121
if err != nil {
120-
return fmt.Errorf("failed to write macros file %#q:\n%w", macrosFilePath, err)
122+
return "", fmt.Errorf("failed to write macros file %#q:\n%w", macrosFilePath, err)
121123
}
122124

123-
return nil
125+
return macrosFilePath, nil
124126
}
125127

126128
// GenerateMacrosFileContents generates the contents of an RPM macros file from the given
@@ -174,16 +176,12 @@ func GenerateMacrosFileContents(buildConfig projectconfig.ComponentBuildConfig)
174176
}
175177

176178
func (p *sourcePreparerImpl) postProcessSources(
177-
component components.Component, sourcesDirPath string,
179+
component components.Component, sourcesDirPath, macrosFileName string,
178180
) error {
179-
// Short-circuit early if no overlays to apply.
180-
if len(component.GetConfig().Overlays) == 0 {
181-
return nil
182-
}
183-
184181
event := p.eventListener.StartEvent("Applying overlays to sources", "component", component.GetName())
185182
defer event.End()
186183

184+
// Find the spec.
187185
specPath, err := findSpecInDir(p.fs, component, sourcesDirPath)
188186
if err != nil {
189187
return fmt.Errorf("failed to find spec in acquired sources dir %#q:\n%w", sourcesDirPath, err)
@@ -195,6 +193,21 @@ func (p *sourcePreparerImpl) postProcessSources(
195193
return fmt.Errorf("failed to get absolute path for %#q:\n%w", specPath, err)
196194
}
197195

196+
// Compute any synthetic overlays required to load the macros file.
197+
macroOverlays, err := synthesizeMacroLoadOverlays(macrosFileName)
198+
if err != nil {
199+
return fmt.Errorf("failed to compute macros load overlays:\n%w", err)
200+
}
201+
202+
// Apply those overlays *first*, in sequence.
203+
for _, overlay := range macroOverlays {
204+
err = ApplyOverlayToSources(p.dryRunnable, p.fs, overlay, sourcesDirPath, absSpecPath)
205+
if err != nil {
206+
return fmt.Errorf("failed to apply system overlay to sources for component %#q:\n%w", component.GetName(), err)
207+
}
208+
}
209+
210+
// Apply all overlays in sequence.
198211
for _, overlay := range component.GetConfig().Overlays {
199212
err = ApplyOverlayToSources(p.dryRunnable, p.fs, overlay, sourcesDirPath, absSpecPath)
200213
if err != nil {
@@ -205,6 +218,39 @@ func (p *sourcePreparerImpl) postProcessSources(
205218
return nil
206219
}
207220

221+
func synthesizeMacroLoadOverlays(macrosFileName string) ([]projectconfig.ComponentOverlay, error) {
222+
// Basic check that the macros file name is valid and doesn't require escaping.
223+
if strings.ContainsFunc(macrosFileName, func(r rune) bool {
224+
return !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '.' && r != '-' && r != '_'
225+
}) {
226+
return nil, fmt.Errorf(
227+
"macros file name %#q contains invalid characters; does the component name contain invalid characters?",
228+
macrosFileName,
229+
)
230+
}
231+
232+
// We inject an overlay to prepend a line to the spec to load the macros file.
233+
return []projectconfig.ComponentOverlay{
234+
{
235+
// Prepend the %{load:...} directive to the spec.
236+
Type: projectconfig.ComponentOverlayPrependSpecLines,
237+
Lines: []string{
238+
fmt.Sprintf("%%{load:%%{_sourcedir}/%s}", macrosFileName),
239+
},
240+
},
241+
{
242+
// Ensure that the macros file is manifested as a source in the spec so that
243+
// mock and other tools know it needs to be present in the build root.
244+
// Ideally we'd dynamically compute a unique source tag here, but for now
245+
// we just use a high number to stay simple. If a conflict is found, this
246+
// overlay application will fail.
247+
Type: projectconfig.ComponentOverlayAddSpecTag,
248+
Tag: "Source9999", // Use a high number to avoid conflicts with existing sources.
249+
Value: macrosFileName,
250+
},
251+
}, nil
252+
}
253+
208254
func findSpecInDir(
209255
fs opctx.FS, component components.Component, dirPath string,
210256
) (string, error) {

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

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
"github.com/gim-home/azldev-preview/internal/global/testctx"
1515
"github.com/gim-home/azldev-preview/internal/projectconfig"
1616
"github.com/gim-home/azldev-preview/internal/providers/sourceproviders/sourceproviders_test"
17-
"github.com/spf13/afero"
17+
"github.com/gim-home/azldev-preview/internal/utils/fileutils"
1818
"github.com/stretchr/testify/assert"
1919
"github.com/stretchr/testify/require"
2020
"go.uber.org/mock/gomock"
@@ -43,25 +43,42 @@ func TestNewPreparer_NilArgs(t *testing.T) {
4343
}
4444

4545
func TestPrepareSources_Success(t *testing.T) {
46+
const (
47+
testSpecName = "test-component.spec"
48+
outputSpecPath = testOutputDir + "/" + testSpecName
49+
)
50+
4651
ctrl := gomock.NewController(t)
4752
component := components_testutils.NewMockComponent(ctrl)
4853
sourceManager := sourceproviders_test.NewMockSourceManager(ctrl)
4954
ctx := testctx.NewCtx()
5055

5156
component.EXPECT().GetName().AnyTimes().Return("test-component")
5257
component.EXPECT().GetConfig().AnyTimes().Return(&projectconfig.ComponentConfig{})
53-
sourceManager.EXPECT().FetchComponent(gomock.Any(), component, testOutputDir).Return(nil)
58+
sourceManager.EXPECT().FetchComponent(gomock.Any(), component, testOutputDir).DoAndReturn(
59+
func(_ interface{}, _ interface{}, outputDir string) error {
60+
// Create the expected spec file.
61+
return fileutils.WriteFile(ctx.FS(), outputSpecPath, []byte("# test spec"), 0o644)
62+
},
63+
)
5464

5565
preparer, err := sources.NewPreparer(sourceManager, ctx.FS(), ctx, ctx)
5666
require.NoError(t, err)
5767
err = preparer.PrepareSources(ctx, component, testOutputDir)
5868
require.NoError(t, err)
5969

70+
macrosFileName := "test-component" + sources.MacrosFileExtension
71+
macrosFilePath := filepath.Join(testOutputDir, macrosFileName)
72+
6073
// Verify macros file was created.
61-
macrosFilePath := filepath.Join(testOutputDir, "test-component"+sources.MacrosFileExtension)
62-
exists, err := afero.Exists(ctx.FS(), macrosFilePath)
74+
exists, err := fileutils.Exists(ctx.FS(), macrosFilePath)
6375
require.NoError(t, err)
6476
assert.True(t, exists, "macros file should be created")
77+
78+
// Verify spec loads macros.
79+
specContents, err := fileutils.ReadFile(ctx.FS(), outputSpecPath)
80+
require.NoError(t, err)
81+
assert.Contains(t, string(specContents), "%{load:%{_sourcedir}/"+macrosFileName+"}")
6582
}
6683

6784
func TestPrepareSources_SourceManagerError(t *testing.T) {
@@ -95,7 +112,14 @@ func TestPrepareSources_WritesMacrosFile(t *testing.T) {
95112
With: []string{"feature"},
96113
},
97114
})
98-
sourceManager.EXPECT().FetchComponent(gomock.Any(), component, testOutputDir).Return(nil)
115+
sourceManager.EXPECT().FetchComponent(gomock.Any(), component, testOutputDir).DoAndReturn(
116+
func(_ interface{}, _ interface{}, outputDir string) error {
117+
// Create the expected spec file.
118+
specPath := filepath.Join(outputDir, "my-package.spec")
119+
120+
return fileutils.WriteFile(ctx.FS(), specPath, []byte("# test spec"), 0o644)
121+
},
122+
)
99123

100124
preparer, err := sources.NewPreparer(sourceManager, ctx.FS(), ctx, ctx)
101125
require.NoError(t, err)
@@ -104,12 +128,12 @@ func TestPrepareSources_WritesMacrosFile(t *testing.T) {
104128

105129
// Verify file exists with expected name.
106130
macrosFilePath := filepath.Join(testOutputDir, "my-package"+sources.MacrosFileExtension)
107-
exists, err := afero.Exists(ctx.FS(), macrosFilePath)
131+
exists, err := fileutils.Exists(ctx.FS(), macrosFilePath)
108132
require.NoError(t, err)
109133
assert.True(t, exists)
110134

111135
// Verify content is non-empty and has expected macro.
112-
contents, err := afero.ReadFile(ctx.FS(), macrosFilePath)
136+
contents, err := fileutils.ReadFile(ctx.FS(), macrosFilePath)
113137
require.NoError(t, err)
114138
assert.Contains(t, string(contents), "%_with_feature 1")
115139
}

0 commit comments

Comments
 (0)