Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions docs/user/reference/cli/azldev_component_build.md

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

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

1 change: 1 addition & 0 deletions docs/user/reference/cli/azldev_component_render.md

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

16 changes: 15 additions & 1 deletion internal/app/azldev/cmds/component/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ type ComponentBuildOptions struct {
// MockConfigOpts is an optional set of key-value config options that will be passed through
// to mock as --config-opts key=value arguments.
MockConfigOpts map[string]string

AllowDirty bool
}

// RPMResult encapsulates a single binary RPM produced by a component build,
Expand Down Expand Up @@ -146,6 +148,8 @@ builds can consume.`,
"Path to local repository to include during build and publish built RPMs to")
cmd.Flags().StringToStringVar(&options.MockConfigOpts, "mock-config-opt", nil,
"Pass a configuration option through to mock (key=value, can be specified multiple times)")
cmd.Flags().BoolVarP(&options.AllowDirty, "allow-dirty", "d", false,
"include uncommitted changes in synthetic history")

// Mark flags as mutually exclusive.
cmd.MarkFlagsMutuallyExclusive("srpm-only", "local-repo-with-publish")
Expand Down Expand Up @@ -255,8 +259,11 @@ func BuildComponent(
if !options.WithoutGitRepo {
preparerOpts = append(preparerOpts,
sources.WithGitRepo(env, env.LockReader(), distro.Version.ReleaseVer),
sources.WithDirtyDetection(),
)

if options.AllowDirty {
preparerOpts = append(preparerOpts, sources.WithDirtyDetection())
}
}

Comment thread
dmcilvaney marked this conversation as resolved.
sourcePreparer, err := sources.NewPreparer(sourceManager, env.FS(), env, env, preparerOpts...)
Expand All @@ -278,6 +285,13 @@ func BuildComponent(
options.SourcePackageOnly, options.NoCheck,
options.LocalRepoWithPublishPath,
)

// Drain hints from the preparer — the builder calls PrepareSources
// internally, so hints are populated after buildComponentUsingBuilder returns.
for _, hint := range sourcePreparer.Hints() {
env.AddFixSuggestion(hint)
}

if err != nil {
return results, err
}
Expand Down
20 changes: 16 additions & 4 deletions internal/app/azldev/cmds/component/preparesources.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type PrepareSourcesOptions struct {
WithoutGitRepo bool
Force bool
AllowNoHashes bool
AllowDirty bool
}

func prepareOnAppInit(_ *azldev.App, sourceCmd *cobra.Command) {
Expand Down Expand Up @@ -73,10 +74,13 @@ Only one component may be selected at a time.`,
cmd.Flags().BoolVar(&options.Force, "force", false, "delete and recreate the output directory if it already exists")
cmd.Flags().BoolVar(&options.AllowNoHashes, "allow-no-hashes", false,
"compute missing hashes by downloading source files from their origin")
cmd.Flags().BoolVarP(&options.AllowDirty, "allow-dirty", "d", false,
"include uncommitted changes in synthetic history")

return cmd
}

//nolint:cyclop // slightly over due to flag/option setup
func PrepareComponentSources(env *azldev.Env, options *PrepareSourcesOptions) error {
var comps *components.ComponentSet

Expand Down Expand Up @@ -130,8 +134,11 @@ func PrepareComponentSources(env *azldev.Env, options *PrepareSourcesOptions) er
if !options.WithoutGitRepo && !options.SkipOverlays {
preparerOpts = append(preparerOpts,
sources.WithGitRepo(env, env.LockReader(), distro.Version.ReleaseVer),
sources.WithDirtyDetection(),
)

if options.AllowDirty {
preparerOpts = append(preparerOpts, sources.WithDirtyDetection())
}
}

Comment thread
dmcilvaney marked this conversation as resolved.
if options.AllowNoHashes {
Expand All @@ -143,9 +150,14 @@ func PrepareComponentSources(env *azldev.Env, options *PrepareSourcesOptions) er
return fmt.Errorf("failed to create source preparer:\n%w", err)
}

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)
prepErr := preparer.PrepareSources(env, component, options.OutputDir, !options.SkipOverlays)

for _, hint := range preparer.Hints() {
env.AddFixSuggestion(hint)
}

if prepErr != nil {
return fmt.Errorf("failed to prepare sources for component %q:\n%w", component.GetName(), prepErr)
}

return nil
Expand Down
34 changes: 28 additions & 6 deletions internal/app/azldev/cmds/component/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,15 @@ type RenderOptions struct {
Force bool
CleanStale bool
CheckOnly bool
AllowDirty bool
}

func renderOnAppInit(_ *azldev.App, parentCmd *cobra.Command) {
parentCmd.AddCommand(NewRenderCmd())
}

// NewRenderCmd constructs a [cobra.Command] for the "component render" CLI subcommand.
func NewRenderCmd() *cobra.Command {
func NewRenderCmd() *cobra.Command { //nolint:funlen // flag registrations push count slightly over
var options RenderOptions

var cmd *cobra.Command
Expand Down Expand Up @@ -120,12 +121,20 @@ valid with -a.`,
"and 1 when any component would drift. With -a + --clean-stale, also fails "+
"on orphan rendered-spec directories. Intended for CI gates.")

cmd.Flags().BoolVarP(&options.AllowDirty, "allow-dirty", "d", false,
"include uncommitted changes in synthetic history")

// --check-only is a read-only diff against on-disk state; --fail-on-error
// is the loud-failure-per-run knob. Combining them is semantically
// muddled (CI would fail on stale failures even when on-disk markers
// already record them) and forcing a choice keeps the contract crisp.
cmd.MarkFlagsMutuallyExclusive("fail-on-error", "check-only")

// --check-only validates committed state for CI gates; --allow-dirty
// injects uncommitted state. Combining them would validate against
// working-tree state instead of committed state, defeating the purpose.
cmd.MarkFlagsMutuallyExclusive("check-only", "allow-dirty")

return cmd
}

Expand Down Expand Up @@ -204,7 +213,7 @@ func RenderComponents(env *azldev.Env, options *RenderOptions) ([]*RenderResult,
results := make([]*RenderResult, len(componentList))

// ── Phase 1: Parallel source preparation ──
prepared := parallelPrepare(env, componentList, stagingDir, options.OutputDir, results)
prepared := parallelPrepare(env, componentList, stagingDir, options.OutputDir, results, options.AllowDirty)

// ── Phase 2: Batch mock processing ──
mockResultMap := batchMockProcess(env, mockProcessor, stagingDir, prepared)
Expand Down Expand Up @@ -391,6 +400,7 @@ func parallelPrepare(
stagingDir string,
outputDir string,
results []*RenderResult,
allowDirty bool,
) []*preparedComponent {
progressEvent := env.StartEvent("Preparing component sources", "count", len(comps))
defer progressEvent.End()
Expand All @@ -408,7 +418,8 @@ func parallelPrepare(
func(_ context.Context, comp components.Component) prepResult {
// workerEnv (captured) is the effective context for this call chain;
// the parmap-supplied ctx is identical and unused here.
return prepareOneComponent(workerEnv, comp, stagingDir, outputDir) //nolint:contextcheck // env carries the ctx
//nolint:contextcheck // env carries the ctx
return prepareOneComponent(workerEnv, comp, stagingDir, outputDir, allowDirty)
},
)

Expand Down Expand Up @@ -454,6 +465,7 @@ func prepareOneComponent(
comp components.Component,
stagingDir string,
outputDir string,
allowDirty bool,
) prepResult {
componentName := comp.GetName()

Expand All @@ -468,7 +480,7 @@ func prepareOneComponent(
}}
}

prep, err := prepareComponentSources(env, comp, stagingDir)
prep, err := prepareComponentSources(env, comp, stagingDir, allowDirty)
if err != nil {
slog.Error("Failed to prepare component sources",
"component", componentName, "error", err)
Expand All @@ -493,6 +505,7 @@ func prepareComponentSources(
env *azldev.Env,
comp components.Component,
stagingDir string,
allowDirty bool,
) (*preparedComponent, error) {
componentName := comp.GetName()

Expand Down Expand Up @@ -525,16 +538,25 @@ func prepareComponentSources(
// sidecar files are needed for rendering.
preparerOpts := []sources.PreparerOption{
sources.WithGitRepo(env, env.LockReader(), distro.Version.ReleaseVer),
sources.WithDirtyDetection(),
sources.WithSkipLookaside(),
}

if allowDirty {
preparerOpts = append(preparerOpts, sources.WithDirtyDetection())
}

preparer, err := sources.NewPreparer(sourceManager, env.FS(), env, env, preparerOpts...)
if err != nil {
return nil, fmt.Errorf("creating source preparer for %#q:\n%w", componentName, err)
}

if prepErr := preparer.PrepareSources(env, comp, componentDir, true /*applyOverlays*/); prepErr != nil {
prepErr := preparer.PrepareSources(env, comp, componentDir, true /*applyOverlays*/)

for _, hint := range preparer.Hints() {
env.AddFixSuggestion(hint)
}
Comment thread
dmcilvaney marked this conversation as resolved.
Comment thread
dmcilvaney marked this conversation as resolved.
Comment thread
dmcilvaney marked this conversation as resolved.
Comment thread
dmcilvaney marked this conversation as resolved.

if prepErr != nil {
return nil, fmt.Errorf("preparing sources for %#q:\n%w", componentName, prepErr)
}

Expand Down
53 changes: 33 additions & 20 deletions internal/app/azldev/core/sources/sourceprep.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ type SourcePreparer interface {
// 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)

// Hints returns user-facing suggestions collected during the most recent
// [PrepareSources] call (e.g., "use --allow-dirty"). Callers should pass
// these to [azldev.Env.AddFixSuggestion] for display.
Hints() []string
}

// PreparerOption is a functional option for configuring a [SourcePreparer].
Expand Down Expand Up @@ -83,15 +88,11 @@ func WithGitRepo(
}
}

// WithDirtyDetection returns a [PreparerOption] that enables uncommitted-change
// detection during synthetic history generation. When set, the current input
// fingerprint is compared against the committed lock file; if they differ, a
// "dirty" synthetic commit is appended to represent the uncommitted changes.
// Without this option, only committed fingerprint changes produce synthetic commits.
//
// This should be enabled for commands that operate on the working tree state
// (build, render, prepare-sources) and left disabled for commands that should
// only reflect committed state.
// WithDirtyDetection returns a [PreparerOption] that allows uncommitted lock
// state to be included in synthetic history generation. When set, uncommitted
// lock files and fingerprint mismatches produce dirty synthetic commits.
// Without this option, uncommitted changes cause an error with a hint to use
// '--allow-dirty'. Gated by the '--allow-dirty' ('-d') CLI flag.
func WithDirtyDetection() PreparerOption {
return func(p *sourcePreparerImpl) {
p.dirtyDetection = true
Expand Down Expand Up @@ -160,6 +161,10 @@ type sourcePreparerImpl struct {
// synthetic history generation. Set via [WithDirtyDetection].
dirtyDetection bool

// hints collects user-facing suggestions generated during [PrepareSources].
// Callers can retrieve them via [Hints] after PrepareSources returns.
hints []string

// releaseVer is the per-component resolved distro release version, not the
// project default. Set via [WithGitRepo].
releaseVer string
Expand Down Expand Up @@ -211,10 +216,18 @@ func NewPreparer(
return impl, nil
}

// Hints returns user-facing suggestions collected during PrepareSources.
func (p *sourcePreparerImpl) Hints() []string {
return slices.Clone(p.hints)
}

// PrepareSources implements the [SourcePreparer] interface.
func (p *sourcePreparerImpl) PrepareSources(
ctx context.Context, component components.Component, outputDir string, applyOverlays bool,
) error {
// Reset hints from any prior call (preparer may be reused).
p.hints = p.hints[:0]

// 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.
Expand Down Expand Up @@ -407,23 +420,23 @@ func (p *sourcePreparerImpl) trySyntheticHistory(
config := component.GetConfig()
componentName := component.GetName()

// Compute the current fingerprint for uncommitted-change detection.
// Only computed when dirty detection is enabled (e.g., build, render).
// An empty fingerprint skips dirty detection in buildSyntheticCommits.
var currentFingerprint string

if p.dirtyDetection {
var fpErr error

currentFingerprint, fpErr = computeCurrentFingerprint(p.fs, config, p.releaseVer)
if fpErr != nil {
// Always compute the current fingerprint — it's local I/O only (no
// network). When dirty detection is enabled (--allow-dirty), mismatches
// produce synthetic dirty entries. When disabled, mismatches generate
// hints suggesting --allow-dirty.
currentFingerprint, fpErr := computeCurrentFingerprint(p.fs, config, p.releaseVer)
if fpErr != nil {
if p.dirtyDetection {
return fmt.Errorf("dirty detection failed for component %#q:\n%w", componentName, fpErr)
}

slog.Debug("Cannot compute current fingerprint for dirty detection",
"component", componentName, "error", fpErr)
Comment thread
dmcilvaney marked this conversation as resolved.
}

changes, importCommit, err := buildSyntheticCommits(
ctx, p.cmdFactory, config, componentName, p.lockReader.LockDir(),
currentFingerprint,
currentFingerprint, p.dirtyDetection, &p.hints,
)
Comment thread
dmcilvaney marked this conversation as resolved.
if err != nil {
return fmt.Errorf("failed to build synthetic commits:\n%w", err)
Expand Down
Loading
Loading