-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathsourceprep.go
More file actions
610 lines (512 loc) · 22.3 KB
/
sourceprep.go
File metadata and controls
610 lines (512 loc) · 22.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package sources
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
"slices"
"strings"
"time"
"unicode"
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/components"
"github.com/microsoft/azure-linux-dev-tools/internal/global/opctx"
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
"github.com/microsoft/azure-linux-dev-tools/internal/providers/sourceproviders"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/dirdiff"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileperms"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
"github.com/samber/lo"
"go.szostok.io/version"
)
// MacrosFileExtension is the file extension used for azldev-generated macros files.
const MacrosFileExtension = ".azl.macros"
// MacrosFileHeader is the comment header included at the top of generated macros files.
const MacrosFileHeader = `# Macros file automatically generated by azldev.
# Do not edit manually; changes will be overwritten.`
// SourcePreparer is a utility for acquiring and preparing all input files required to build a component.
// This may include the component's spec file, any loose files that must accompany it, any payload source
// archives and patches required by the spec, etc. Preparation may also include any post-processing of these
// inputs to match the component's configuration within the containing project.
type SourcePreparer interface {
// PrepareSources prepares the input sources for the given component, writing them to the given output directory.
// After this function completes successfully, the output directory should contain all files required to build
// the component, with any post-processing applied as necessary. The only remaining dependencies not contained
// within the output directory will be build-time dependencies on external packages (RPMs), including those
// relied on to be present implicitly within the build root, or expressed via BuildRequires or DynamicBuildRequires
// in the component's spec file and any defaults from the macros used to interpret the spec file.
PrepareSources(ctx context.Context, component components.Component, outputDir string, applyOverlays bool) error
// DiffSources computes a unified diff showing the changes that overlays apply to a component's sources.
// The component's sources are fetched once into a subdirectory of baseDir, then copied to a second
// subdirectory where overlays are applied in-place. The diff between the two subdirectories is returned.
DiffSources(ctx context.Context, component components.Component, baseDir string) (*dirdiff.DiffResult, error)
}
// PreparerOption is a functional option for configuring a [SourcePreparer].
type PreparerOption func(*sourcePreparerImpl)
// WithGitRepo returns a [PreparerOption] that enables dist-git repository
// creation during source preparation. When set, the upstream .git directory
// is preserved and synthetic commit history is generated on top of it. This
// requires the project configuration to reside inside a git repository.
// Without this option, no dist-git is created and synthetic history is skipped.
func WithGitRepo() PreparerOption {
return func(p *sourcePreparerImpl) {
p.withGitRepo = true
}
}
// Standard implementation of the [SourcePreparer] interface.
type sourcePreparerImpl struct {
sourceManager sourceproviders.SourceManager
fs opctx.FS
eventListener opctx.EventListener
dryRunnable opctx.DryRunnable
// withGitRepo, when true, enables dist-git creation by preserving the
// upstream .git directory and generating synthetic commit history.
withGitRepo bool
}
// NewPreparer creates a new [SourcePreparer] instance. All positional arguments
// are required. Optional behavior can be configured via [PreparerOption] values.
func NewPreparer(
sourceManager sourceproviders.SourceManager,
fs opctx.FS,
eventListener opctx.EventListener,
dryRunnable opctx.DryRunnable,
opts ...PreparerOption,
) (SourcePreparer, error) {
if sourceManager == nil {
return nil, errors.New("source manager cannot be nil")
}
if fs == nil {
return nil, errors.New("filesystem interface cannot be nil")
}
if eventListener == nil {
return nil, errors.New("event listener interface cannot be nil")
}
if dryRunnable == nil {
return nil, errors.New("dry runnable interface cannot be nil")
}
impl := &sourcePreparerImpl{
sourceManager: sourceManager,
fs: fs,
eventListener: eventListener,
dryRunnable: dryRunnable,
}
for _, opt := range opts {
if opt != nil {
opt(impl)
}
}
return impl, nil
}
// PrepareSources implements the [SourcePreparer] interface.
func (p *sourcePreparerImpl) PrepareSources(
ctx context.Context, component components.Component, outputDir string, applyOverlays bool,
) error {
// Use the source manager to fetch source files (archives, patches, etc.)
err := p.sourceManager.FetchFiles(ctx, component, outputDir)
if err != nil {
return fmt.Errorf("failed to fetch source files for component %#q:\n%w",
component.GetName(), err)
}
// Preserve the upstream .git directory only when dist-git creation is
// requested via --with-git. This is required so that overlay commits can be
// appended on top of the upstream commit log during synthetic history generation.
var fetchOpts []sourceproviders.FetchComponentOption
if applyOverlays && p.withGitRepo {
fetchOpts = append(fetchOpts, sourceproviders.WithPreserveGitDir())
}
// Use the source manager to fetch the component (spec file and sidecar files).
err = p.sourceManager.FetchComponent(ctx, component, outputDir, fetchOpts...)
if err != nil {
return fmt.Errorf("failed to fetch sources for component %#q:\n%w",
component.GetName(), err)
}
if applyOverlays {
err := p.applyOverlaysToSources(ctx, component, outputDir)
if err != nil {
return err
}
// Record the changes as synthetic git history when dist-git creation is enabled.
if p.withGitRepo {
if err := p.trySyntheticHistory(component, outputDir); err != nil {
return fmt.Errorf("failed to generate synthetic history for component %#q:\n%w",
component.GetName(), err)
}
}
}
return nil
}
// applyOverlaysToSources writes the macros file and then applies all overlays.
func (p *sourcePreparerImpl) applyOverlaysToSources(
ctx context.Context, component components.Component, outputDir string,
) error {
// Emit computed macros to a macros file in the output directory.
// If the build configuration produces no macros, no file is written and
// macrosFileName will be empty.
var macrosFileName string
macrosFilePath, err := p.writeMacrosFile(component, outputDir)
if err != nil {
return fmt.Errorf("failed to write macros file for component %#q:\n%w",
component.GetName(), err)
}
if macrosFilePath != "" {
macrosFileName = filepath.Base(macrosFilePath)
}
// Apply all overlays to prepared sources.
if err := p.applyOverlays(ctx, component, outputDir, macrosFileName); err != nil {
return fmt.Errorf("failed to apply overlays for component %#q:\n%w", component.GetName(), err)
}
return nil
}
// applyOverlays applies all overlays (user-defined and system-generated) to the
// component sources. Overlay application is decoupled from git history generation:
// overlays modify the working tree; synthetic history is recorded separately by
// [trySyntheticHistory].
func (p *sourcePreparerImpl) applyOverlays(
_ context.Context, component components.Component, sourcesDirPath, macrosFileName string,
) error {
event := p.eventListener.StartEvent("Applying overlays", "component", component.GetName())
defer event.End()
// Resolve the spec path once for all overlay operations in this call.
absSpecPath, err := p.resolveSpecPath(component, sourcesDirPath)
if err != nil {
return err
}
// Collect all overlays in application order. This ensures every change is
// captured in the synthetic history, including build configuration changes.
allOverlays, err := p.collectOverlays(component, macrosFileName)
if err != nil {
return fmt.Errorf("failed to collect overlays for component %#q:\n%w", component.GetName(), err)
}
if len(allOverlays) == 0 {
return nil
}
// Apply all overlays to the working tree.
if err := p.applyOverlayList(allOverlays, sourcesDirPath, absSpecPath); err != nil {
return fmt.Errorf("failed to apply overlays for component %#q:\n%w", component.GetName(), err)
}
return nil
}
// collectOverlays gathers all overlays for a component into a single ordered slice:
// macros-load first, then user overlays, followed by check-skip and file-header overlays.
func (p *sourcePreparerImpl) collectOverlays(
component components.Component, macrosFileName string,
) ([]projectconfig.ComponentOverlay, error) {
config := component.GetConfig()
var allOverlays []projectconfig.ComponentOverlay
if macrosFileName != "" {
macroOverlays, err := synthesizeMacroLoadOverlays(macrosFileName)
if err != nil {
return nil, fmt.Errorf("failed to compute macros load overlays:\n%w", err)
}
allOverlays = append(allOverlays, macroOverlays...)
}
allOverlays = append(allOverlays, config.Overlays...)
allOverlays = append(allOverlays, synthesizeCheckSkipOverlays(config.Build.Check)...)
allOverlays = append(allOverlays, generateFileHeaderOverlay()...)
return allOverlays, nil
}
// initSourcesRepo initializes a new git repository in sourcesDirPath, stages all files,
// and creates an initial commit. This is used for components that don't have an upstream
// dist-git so that Affects commits can still be layered on top.
func initSourcesRepo(sourcesDirPath string) (*gogit.Repository, error) {
slog.Info("Initializing git repository for sources", "path", sourcesDirPath)
repo, err := gogit.PlainInit(sourcesDirPath, false)
if err != nil {
return nil, fmt.Errorf("failed to initialize git repository at %#q:\n%w", sourcesDirPath, err)
}
worktree, err := repo.Worktree()
if err != nil {
return nil, fmt.Errorf("failed to get worktree:\n%w", err)
}
if err := worktree.AddWithOptions(&gogit.AddOptions{All: true}); err != nil {
return nil, fmt.Errorf("failed to stage files:\n%w", err)
}
_, err = worktree.Commit("Initial sources", &gogit.CommitOptions{
Author: &object.Signature{
Name: "azldev",
When: time.Unix(0, 0).UTC(),
},
})
if err != nil {
return nil, fmt.Errorf("failed to create initial commit:\n%w", err)
}
return repo, nil
}
// trySyntheticHistory attempts to create synthetic git commits on top of the
// component's sources directory. If no .git directory exists, one is initialized
// with an initial commit so Affects commits can be layered on uniformly for all
// component types.
//
// Returns a non-nil error if history generation fails.
func (p *sourcePreparerImpl) trySyntheticHistory(
component components.Component,
sourcesDirPath string,
) error {
config := component.GetConfig()
// Build commit metadata from Affects commits.
commits, err := buildSyntheticCommits(config, component.GetName())
if err != nil {
return fmt.Errorf("failed to build synthetic commits:\n%w", err)
}
if len(commits) == 0 {
slog.Debug("No synthetic commits to create; skipping history generation",
"component", component.GetName())
return nil
}
// Adjust the Release tag before staging changes. See [tryBumpStaticRelease]
// for the handling of %autorelease, static integers, and non-standard values.
if err := p.tryBumpStaticRelease(component, sourcesDirPath, len(commits)); err != nil {
return fmt.Errorf("failed to apply release bump:\n%w", err)
}
// Use os.Stat (not p.fs) because go-git always operates on the real filesystem.
gitDirPath := filepath.Join(sourcesDirPath, ".git")
_, statErr := os.Stat(gitDirPath)
if statErr != nil && !os.IsNotExist(statErr) {
return fmt.Errorf("failed to check for .git directory at %#q:\n%w", gitDirPath, statErr)
}
if os.IsNotExist(statErr) {
slog.Info("No .git directory in sources; initializing repository",
"component", component.GetName())
if _, err := initSourcesRepo(sourcesDirPath); err != nil {
return fmt.Errorf("failed to initialize sources repository:\n%w", err)
}
}
// Open the git repository where synthetic commits will be recorded.
sourcesRepo, err := gogit.PlainOpen(sourcesDirPath)
if err != nil {
return fmt.Errorf("failed to open sources repository at %#q:\n%w", sourcesDirPath, err)
}
if err := CommitSyntheticHistory(sourcesRepo, commits); err != nil {
return fmt.Errorf("failed to commit synthetic history:\n%w", err)
}
return nil
}
// DiffSources implements the [SourcePreparer] interface.
// It fetches the component's sources once, copies them to a second directory, applies overlays
// to the copy, then diffs the two trees. This avoids fetching the sources twice.
func (p *sourcePreparerImpl) DiffSources(
ctx context.Context, component components.Component, baseDir string,
) (result *dirdiff.DiffResult, err error) {
event := p.eventListener.StartEvent("Computing overlay diff", "component", component.GetName())
defer event.End()
// Create temp dirs for sources prepared without and with overlays.
originalDir, err := fileutils.MkdirTemp(p.fs, baseDir, "original-")
if err != nil {
return nil, fmt.Errorf("failed to create temp dir for original sources:\n%w", err)
}
defer fileutils.RemoveAllAndUpdateErrorIfNil(p.fs, originalDir, &err)
// Prepare sources without applying overlays, to get the original tree.
if err := p.PrepareSources(ctx, component, originalDir, false /* applyOverlays */); err != nil {
return nil, err
}
// Copy the fetched sources to a separate directory for in-place overlay application.
// This avoids a second network fetch.
overlaidDir, err := fileutils.MkdirTemp(p.fs, baseDir, "overlaid-")
if err != nil {
return nil, fmt.Errorf("failed to create temp dir for overlaid sources:\n%w", err)
}
defer fileutils.RemoveAllAndUpdateErrorIfNil(p.fs, overlaidDir, &err)
if err := fileutils.CopyDirRecursive(
p.dryRunnable, p.fs, originalDir, overlaidDir,
fileutils.CopyDirOptions{CopyFileOptions: fileutils.CopyFileOptions{PreserveFileMode: true}},
); err != nil {
return nil, fmt.Errorf("failed to copy sources for component %#q:\n%w", component.GetName(), err)
}
// Apply overlays in-place to the copied directory only.
if err := p.applyOverlaysToSources(ctx, component, overlaidDir); err != nil {
return nil, fmt.Errorf("failed to apply overlays for component %#q:\n%w", component.GetName(), err)
}
// Diff the original tree against the overlaid tree.
result, err = dirdiff.DiffDirs(p.fs, originalDir, overlaidDir)
if err != nil {
return nil, fmt.Errorf("failed to diff source trees for component %#q:\n%w",
component.GetName(), err)
}
return result, nil
}
// writeMacrosFile writes a macros file containing the resolved macros for a component.
// This includes with/without flags converted to macro format, and any explicit defines.
// If the build configuration produces no macros, no file is written and an empty path is
// returned. Otherwise, the path to the written macros file is returned.
func (p *sourcePreparerImpl) writeMacrosFile(component components.Component, outputDir string) (string, error) {
contents := GenerateMacrosFileContents(component.GetConfig().Build)
if contents == "" {
return "", nil
}
macrosFilePath := filepath.Join(outputDir, component.GetName()+MacrosFileExtension)
err := fileutils.WriteFile(p.fs, macrosFilePath, []byte(contents), fileperms.PublicFile)
if err != nil {
return "", fmt.Errorf("failed to write macros file %#q:\n%w", macrosFilePath, err)
}
return macrosFilePath, nil
}
// GenerateMacrosFileContents generates the contents of an RPM macros file from the given
// build configuration. The output uses standard RPM macro file format (%name value) and
// includes a header comment identifying the file as auto-generated.
//
// With flags are converted to %_with_FLAG 1 macros.
// Without flags are converted to %_without_FLAG 1 macros.
// Defines are emitted as %name value macros.
//
// All macros are collected into a single map and sorted alphabetically by name for
// deterministic output. If the same macro is defined multiple times (e.g., via both
// a with flag and an explicit define), the later definition wins. The order of processing
// is: with flags, then without flags, then explicit defines, then undefines.
//
// Undefines remove entries from the generated macros map. This only affects macros that
// originate from the merged TOML configuration (with, without, and defines fields); it
// does not undefine arbitrary system-level RPM macros. The primary use case is allowing
// a component-level TOML file to override a distro-level or project-level default by
// removing a macro that would otherwise be emitted into the macros file.
//
// Note: RPM macro values can contain spaces without special escaping; everything
// after the macro name (and separating whitespace) is treated as the macro body.
//
// If no macros remain after processing (empty config, or all macros removed via
// undefines), an empty string is returned to signal that no macros file is needed.
func GenerateMacrosFileContents(buildConfig projectconfig.ComponentBuildConfig) string {
// Build a unified map of all macros. Later definitions override earlier ones.
// Processing order: with flags -> without flags -> explicit defines.
macros := make(map[string]string)
// Convert 'with' flags to macros: FLAG -> _with_FLAG = 1
for _, with := range buildConfig.With {
macros["_with_"+with] = "1"
}
// Convert 'without' flags to macros: FLAG -> _without_FLAG = 1
for _, without := range buildConfig.Without {
macros["_without_"+without] = "1"
}
// Add explicit macro definitions (these override any conflicting with/without flags).
for name, value := range buildConfig.Defines {
macros[name] = value
}
// Remove any macros listed in undefines. This allows component-level config to
// override distro-level default macro definitions by removing them entirely.
for _, undef := range buildConfig.Undefines {
delete(macros, undef)
}
if len(macros) == 0 {
return ""
}
lines := []string{
MacrosFileHeader,
}
// Sort macro names for deterministic output.
macroNames := lo.Keys(macros)
slices.Sort(macroNames)
for _, name := range macroNames {
lines = append(lines, fmt.Sprintf("%%%s %s", name, macros[name]))
}
return strings.Join(lines, "\n") + "\n"
}
func synthesizeMacroLoadOverlays(macrosFileName string) ([]projectconfig.ComponentOverlay, error) {
// Basic check that the macros file name is valid and doesn't require escaping.
if strings.ContainsFunc(macrosFileName, func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '.' && r != '-' && r != '_' && r != '+'
}) {
return nil, fmt.Errorf(
"macros file name %#q contains invalid characters; does the component name contain invalid characters?",
macrosFileName,
)
}
// We inject an overlay to prepend a line to the spec to load the macros file.
return []projectconfig.ComponentOverlay{
{
// Prepend the %{load:...} directive to the spec.
Type: projectconfig.ComponentOverlayPrependSpecLines,
Lines: []string{
"# All Azure Linux specs with overlays include this macro file, irrespective of" +
" whether new macros have been added.",
fmt.Sprintf("%%{load:%%{_sourcedir}/%s}", macrosFileName),
"",
},
},
{
// Ensure that the macros file is manifested as a source in the spec so that
// mock and other tools know it needs to be present in the build root.
// Use InsertSpecTag to place it after the last existing Source* tag, avoiding
// misplacement after macros like %fontpkg or inside %if conditionals.
Type: projectconfig.ComponentOverlayInsertSpecTag,
Tag: "Source9999", // Use a high number to avoid conflicts with existing sources.
Value: macrosFileName,
},
}, nil
}
// generateFileHeaderOverlay generates an overlay that prepends a header to the spec.
// It should be applied after all other overlays, as it should be the first thing in the spec file.
func generateFileHeaderOverlay() []projectconfig.ComponentOverlay {
ver := version.Get()
return []projectconfig.ComponentOverlay{
{
Type: projectconfig.ComponentOverlayPrependSpecLines,
Lines: []string{
"# This spec file has been modified by azldev to include build configuration overlays. Version: " + ver.Version,
"# Do not edit manually; changes may be overwritten.",
"",
},
},
}
}
// 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.
func synthesizeCheckSkipOverlays(checkConfig projectconfig.CheckConfig) []projectconfig.ComponentOverlay {
if !checkConfig.Skip {
return nil
}
return []projectconfig.ComponentOverlay{
{
Type: projectconfig.ComponentOverlayPrependSpecLines,
SectionName: "%check",
Lines: []string{
"# Check section disabled: " + checkConfig.SkipReason,
"exit 0",
"",
},
},
}
}
// resolveSpecPath locates and returns the absolute path to the component's spec file
// within the given sources directory.
func (p *sourcePreparerImpl) resolveSpecPath(
component components.Component, sourcesDirPath string,
) (string, error) {
specPath, err := findSpecInDir(p.fs, component, sourcesDirPath)
if err != nil {
return "", fmt.Errorf("failed to find spec in sources dir %#q:\n%w", sourcesDirPath, err)
}
absSpecPath, err := filepath.Abs(specPath)
if err != nil {
return "", fmt.Errorf("failed to get absolute path for spec %#q:\n%w", specPath, err)
}
return absSpecPath, nil
}
// applyOverlayList applies a list of overlays to the component sources sequentially.
func (p *sourcePreparerImpl) applyOverlayList(
overlays []projectconfig.ComponentOverlay, sourcesDirPath, absSpecPath string,
) error {
for _, overlay := range overlays {
if err := ApplyOverlayToSources(
p.dryRunnable, p.fs, overlay, sourcesDirPath, absSpecPath,
); err != nil {
return fmt.Errorf("failed to apply %#q overlay:\n%w", overlay.Type, err)
}
}
return nil
}
func findSpecInDir(
fs opctx.FS, component components.Component, dirPath string,
) (string, error) {
specPath := filepath.Join(dirPath, component.GetName()+".spec")
if _, statErr := fs.Stat(specPath); statErr != nil {
return specPath, fmt.Errorf("failed to find spec for component %#q in %#q:\n%w",
component.GetName(), dirPath, statErr,
)
}
return specPath, nil
}