Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
4 changes: 4 additions & 0 deletions docs/user/reference/cli/azldev_component_prepare-sources.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 33 additions & 12 deletions internal/app/azldev/cmds/component/preparesources.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,22 @@ The result is a directory containing the spec file and all sources, ready
for inspection or manual building. This is useful for verifying that
overlays apply cleanly before running a full build.

The command output reports downloaded source provenance entries with
Comment thread
Tonisal-byte marked this conversation as resolved.
filename, origin type, URL, hash type, and hash. Only files actually
downloaded during this run are included.

Example JSON output (azldev component prep-sources -p curl -o /tmp/curl --force -O json):

[
{
"filename": "curl-8.12.1.tar.xz",
"originType": "lookaside-url",
"url": "https://src.fedoraproject.org/repo/pkgs/rpms/curl/sha512/.../curl-8.12.1.tar.xz",
"hashType": "sha512",
"hash": "a1b2c3..."
}
]

Only one component may be selected at a time.`,
Example: ` # Prepare sources for a component
azldev component prep-sources -p curl -o ./build/work/scratch/curl --force
Expand All @@ -53,7 +69,12 @@ Only one component may be selected at a time.`,
RunE: azldev.RunFuncWithExtraArgs(func(env *azldev.Env, args []string) (interface{}, error) {
options.ComponentFilter.ComponentNamePatterns = append(args, options.ComponentFilter.ComponentNamePatterns...)

return nil, PrepareComponentSources(env, &options)
report, err := PrepareComponentSources(env, &options)
if err != nil {
return nil, err
}

return report.Sources, nil
Comment thread
Tonisal-byte marked this conversation as resolved.
Comment thread
Tonisal-byte marked this conversation as resolved.
Comment thread
Tonisal-byte marked this conversation as resolved.
}),
ValidArgsFunction: components.GenerateComponentNameCompletions,
Annotations: map[string]string{
Expand All @@ -77,24 +98,24 @@ Only one component may be selected at a time.`,
return cmd
}

func PrepareComponentSources(env *azldev.Env, options *PrepareSourcesOptions) error {
func PrepareComponentSources(env *azldev.Env, options *PrepareSourcesOptions) (*sources.ProvenanceReport, error) {
var comps *components.ComponentSet

resolver := components.NewResolver(env)

comps, err := resolver.FindComponents(&options.ComponentFilter)
if err != nil {
return fmt.Errorf("failed to resolve components:\n%w", err)
return nil, fmt.Errorf("failed to resolve components:\n%w", err)
}

if comps.Len() == 0 {
return errors.New("no components were selected; " +
return nil, errors.New("no components were selected; " +
"please use command-line options to indicate which components you would like to build",
)
}

if comps.Len() != 1 {
return fmt.Errorf("expected exactly one component, got %d", comps.Len())
return nil, fmt.Errorf("expected exactly one component, got %d", comps.Len())
}

component := comps.Components()[0]
Expand All @@ -107,18 +128,18 @@ func PrepareComponentSources(env *azldev.Env, options *PrepareSourcesOptions) er
// Resolve the effective distro for this component before creating the source manager.
distro, err := sourceproviders.ResolveDistro(env, component)
if err != nil {
return fmt.Errorf("failed to resolve distro for component %#q:\n%w", component.GetName(), err)
return nil, fmt.Errorf("failed to resolve distro for component %#q:\n%w", component.GetName(), err)
}

// Create source manager to handle all source fetching, both local and upstream.
sourceManager, err = sourceproviders.NewSourceManager(env, distro)
if err != nil {
return fmt.Errorf("failed to create source manager:\n%w", err)
return nil, fmt.Errorf("failed to create source manager:\n%w", err)
}

// Pre-flight check: detect non-empty output directory before any work.
if err := CheckOutputDir(env, options); err != nil {
return err
return nil, err
}

if options.SkipOverlays && !options.WithoutGitRepo {
Expand All @@ -140,15 +161,15 @@ func PrepareComponentSources(env *azldev.Env, options *PrepareSourcesOptions) er

preparer, err := sources.NewPreparer(sourceManager, env.FS(), env, env, preparerOpts...)
if err != nil {
return fmt.Errorf("failed to create source preparer:\n%w", err)
return nil, fmt.Errorf("failed to create source preparer:\n%w", err)
}

err = preparer.PrepareSources(env, component, options.OutputDir, !options.SkipOverlays)
report, err := preparer.PrepareSources(env, component, options.OutputDir, !options.SkipOverlays)
if err != nil {
return fmt.Errorf("failed to prepare sources for component %q:\n%w", component.GetName(), err)
return nil, fmt.Errorf("failed to prepare sources for component %q:\n%w", component.GetName(), err)
}

return nil
return report, nil
}

// CheckOutputDir verifies the output directory state before source preparation.
Expand Down
2 changes: 1 addition & 1 deletion internal/app/azldev/cmds/component/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ func prepareComponentSources(
return nil, fmt.Errorf("creating source preparer for %#q:\n%w", componentName, err)
}

if prepErr := preparer.PrepareSources(env, comp, componentDir, true /*applyOverlays*/); prepErr != nil {
if _, prepErr := preparer.PrepareSources(env, comp, componentDir, true /*applyOverlays*/); prepErr != nil {
return nil, fmt.Errorf("preparing sources for %#q:\n%w", componentName, prepErr)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func DownloadSources(env *azldev.Env, options *DownloadSourcesOptions) error {
for _, uri := range lookasideBaseURIs {
slog.Info("Trying lookaside base URI", "uri", uri)

uriErr := lookasideDownloader.ExtractSourcesFromRepo(
_, uriErr := lookasideDownloader.ExtractSourcesFromRepo(
env, options.Directory, packageName, uri, nil, extractOpts...,
)
if uriErr == nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func TestDownloadSources_StandaloneMode(t *testing.T) {
mockDownloader := fedorasource_test.NewMockFedoraSourceDownloader(ctrl)
mockDownloader.EXPECT().
ExtractSourcesFromRepo(gomock.Any(), testPkgDir, "curl", testLookasideURI, gomock.Any()).
Return(nil)
Return(nil, nil)

options := &downloadsources.DownloadSourcesOptions{
Directory: testPkgDir,
Expand All @@ -94,7 +94,7 @@ func TestDownloadSources_StandaloneMode_NoSourcesFile(t *testing.T) {
mockDownloader := fedorasource_test.NewMockFedoraSourceDownloader(ctrl)
mockDownloader.EXPECT().
ExtractSourcesFromRepo(gomock.Any(), testPkgDir, "curl", testLookasideURI, gomock.Any()).
Return(nil)
Return(nil, nil)

options := &downloadsources.DownloadSourcesOptions{
Directory: testPkgDir,
Expand Down Expand Up @@ -129,7 +129,7 @@ func TestDownloadSources_ComponentMode(t *testing.T) {
ExtractSourcesFromRepo(
gomock.Any(), testPkgDir, "curl", expectedURI, gomock.Any(),
).
Return(nil)
Return(nil, nil)

options := &downloadsources.DownloadSourcesOptions{
Directory: testPkgDir,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func (b *Builder) prepSourcesForSRPM(
return "", fmt.Errorf("failed to create work dir for source preparation:\n%w", err)
}

err = b.sourcePreparer.PrepareSources(ctx, component, preparedSourcesDir, true /*applyOverlays?*/)
_, err = b.sourcePreparer.PrepareSources(ctx, component, preparedSourcesDir, true /*applyOverlays?*/)
if err != nil {
return "", fmt.Errorf("failed to prepare sources:\n%w", err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ func setupBuilder(t *testing.T) *componentBuilderTestParams {
func(
_ context.Context, component components.Component,
outputDir string, _ ...sourceproviders.FetchComponentOption,
) error {
) ([]sourceproviders.SourceProvenance, error) {
// Create the expected spec file.
specPath := filepath.Join(outputDir, component.GetName()+".spec")

return fileutils.WriteFile(testEnv.Env.FS(), specPath, []byte("# test spec"), fileperms.PublicFile)
return nil, fileutils.WriteFile(testEnv.Env.FS(), specPath, []byte("# test spec"), fileperms.PublicFile)
},
)

sourceManager.EXPECT().FetchFiles(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
sourceManager.EXPECT().FetchFiles(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil, nil)

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

Expand Down
16 changes: 16 additions & 0 deletions internal/app/azldev/core/sources/provenance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package sources

import "github.com/microsoft/azure-linux-dev-tools/internal/providers/sourceproviders"

// ProvenanceReport is the output of a source preparation run, listing
// every file that was downloaded and where it came from.
type ProvenanceReport struct {
// ComponentName is the name of the component whose sources were prepared.
ComponentName string `json:"componentName" table:"Component"`

// Sources lists the provenance of each downloaded source file.
Sources []sourceproviders.SourceProvenance `json:"sources" table:"-"`
}
90 changes: 80 additions & 10 deletions internal/app/azldev/core/sources/sourceprep.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,13 @@ type SourcePreparer interface {
// 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
//
// Returns a [ProvenanceReport] listing each file that was downloaded and where it came from.
// The report only includes files that were actually downloaded; pre-existing files are omitted.
PrepareSources(
ctx context.Context, component components.Component,
outputDir string, applyOverlays bool,
) (*ProvenanceReport, 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
Expand Down Expand Up @@ -214,16 +220,20 @@ func NewPreparer(
// PrepareSources implements the [SourcePreparer] interface.
func (p *sourcePreparerImpl) PrepareSources(
ctx context.Context, component components.Component, outputDir string, applyOverlays bool,
) error {
) (*ProvenanceReport, error) {
allProvenance := []sourceproviders.SourceProvenance{}

// Use the source manager to fetch source files (archives, patches, etc.)
// Skip this step when skipLookaside is set — source tarballs are not needed
// for rendering and are the most expensive download.
if !p.skipLookaside {
err := p.sourceManager.FetchFiles(ctx, component, outputDir)
fileProv, err := p.sourceManager.FetchFiles(ctx, component, outputDir)
if err != nil {
return fmt.Errorf("failed to fetch source files for component %#q:\n%w",
return nil, fmt.Errorf("failed to fetch source files for component %#q:\n%w",
component.GetName(), err)
}

allProvenance = append(allProvenance, fileProv...)
Comment thread
Tonisal-byte marked this conversation as resolved.
}

// Preserve the upstream .git directory only when dist-git creation is
Expand All @@ -239,20 +249,22 @@ func (p *sourcePreparerImpl) PrepareSources(
}

// Use the source manager to fetch the component (spec file and sidecar files).
err := p.sourceManager.FetchComponent(ctx, component, outputDir, fetchOpts...)
compProv, err := p.sourceManager.FetchComponent(ctx, component, outputDir, fetchOpts...)
if err != nil {
return fmt.Errorf("failed to fetch sources for component %#q:\n%w",
return nil, fmt.Errorf("failed to fetch sources for component %#q:\n%w",
component.GetName(), err)
}

allProvenance = append(allProvenance, compProv...)
Comment thread
Tonisal-byte marked this conversation as resolved.

if applyOverlays {
err := p.applyOverlaysToSources(ctx, component, outputDir)
if err != nil {
return err
return nil, err
}

if err := p.updateSourcesFile(component, outputDir); err != nil {
return fmt.Errorf("failed to update 'sources' file for component %#q:\n%w",
return nil, fmt.Errorf("failed to update 'sources' file for component %#q:\n%w",
component.GetName(), err)
}
} else {
Comment thread
Tonisal-byte marked this conversation as resolved.
Expand All @@ -261,14 +273,72 @@ func (p *sourcePreparerImpl) PrepareSources(
"component", component.GetName())
}

// Back-fill missing hash fields in provenance entries from the 'sources' file.
// Entries that already carry hashes (the common case) are skipped.
if err := p.enrichProvenanceWithResolvedHashes(allProvenance, outputDir); err != nil {
return nil, fmt.Errorf("failed to resolve provenance hashes for component %#q:\n%w",
component.GetName(), err)
}
Comment thread
Tonisal-byte marked this conversation as resolved.
Outdated

// Record the changes as synthetic git history when dist-git creation is enabled.
if p.withGitRepo {
if err := p.trySyntheticHistory(ctx, component, outputDir); err != nil {
return fmt.Errorf("failed to generate synthetic history for component %#q:\n%w",
return nil, fmt.Errorf("failed to generate synthetic history for component %#q:\n%w",
component.GetName(), err)
}
}

return &ProvenanceReport{
ComponentName: component.GetName(),
Sources: allProvenance,
}, nil
Comment thread
Tonisal-byte marked this conversation as resolved.
}

Comment thread
Tonisal-byte marked this conversation as resolved.
// enrichProvenanceWithResolvedHashes updates hash fields in provenance entries
// to match the finalized 'sources' file. This ensures the provenance report
// reflects the actual state of files after overlays have been applied and
// [updateSourcesFile] has computed the current hashes.
func (p *sourcePreparerImpl) enrichProvenanceWithResolvedHashes(
provenance []sourceproviders.SourceProvenance,
outputDir string,
) error {
if len(provenance) == 0 {
return nil
}

sourcesFilePath := filepath.Join(outputDir, fedorasource.SourcesFileName)

content, err := p.readSourcesFileIfExists(sourcesFilePath)
if err != nil {
return err
}

if content == "" {
return nil
}

parsedLines, err := fedorasource.ReadSourcesFile(content)
if err != nil {
return fmt.Errorf("failed to parse finalized 'sources' file %#q:\n%w", sourcesFilePath, err)
}

hashByFilename := make(map[string]fedorasource.SourcesFileEntry, len(parsedLines))
for _, line := range parsedLines {
if line.Entry != nil {
hashByFilename[line.Entry.Filename] = *line.Entry
}
}

for provenanceIndex := range provenance {
entry, found := hashByFilename[provenance[provenanceIndex].Filename]
if !found {
continue
}

Comment thread
Tonisal-byte marked this conversation as resolved.
provenance[provenanceIndex].HashType = entry.HashType
provenance[provenanceIndex].Hash = entry.Hash
}

return nil
}

Expand Down Expand Up @@ -584,7 +654,7 @@ func (p *sourcePreparerImpl) DiffSources(
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 {
if _, err := p.PrepareSources(ctx, component, originalDir, false /* applyOverlays */); err != nil {
return nil, err
}

Expand Down
Loading
Loading