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
8 changes: 7 additions & 1 deletion docs/user/reference/config/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,12 +333,15 @@ origin = { type = "download", uri = "https://example.com/repo/.../shimx64.efi

Use `origin.type = "custom"` when a source archive must be assembled or modified (e.g. stripping sensitive test fixtures from an upstream tarball). azldev runs the script inside a fresh mock chroot — the script **must write all output to `/azldev-gen/output/`**, which azldev packages into the archive named by `filename`. Network access is always enabled; the mock config comes from the project's default distro.

The `script` and `mock-packages` fields are nested under `[origin]`:
Custom sources are regenerated on every source preparation rather than restored from lookaside. The generated archive is validated against its configured hash, so changes to the script or its inputs fail with a hash mismatch until the hash is intentionally refreshed.

The `script`, `mock-packages`, and `inputs` fields are nested under `[origin]`:

| Field | TOML Key | Type | Required | Description |
|-------|----------|------|----------|-------------|
| Script | `origin.script` | string | **Yes** | Script filename (relative to the component's spec dir) to run in mock. Required for `origin.type = "custom"`. |
| Mock packages | `origin.mock-packages` | array of string | No | Extra RPM packages to install in the mock chroot before the script runs. |
| Inputs | `origin.inputs` | array of string | No | Unique filenames to make available in the mock chroot before the script runs. Each file must already be present in the fetched source output directory — upstream source tarballs, sidecar files (patches, scripts), and any earlier `source-files` entries are all placed there by the upstream fetch before custom scripts run. |

On first use, omit `hash` and run `prep-sources --allow-no-hashes` to generate the archive and print its hash, then copy it into the TOML.

Expand All @@ -350,8 +353,11 @@ hash = "abc123..." # from: prep-sources --allow-no-hashes
origin.type = "custom"
origin.script = "gen-yara-stripped.sh" # relative to the component's spec directory
origin.mock-packages = ["cmake"] # omit if not needed
origin.inputs = ["yara-4.5.4.tar.gz"] # available to the script as ./yara-4.5.4.tar.gz
```

> **Note:** Upstream source tarballs are automatically available as inputs before running custom generation scripts. There is no need to re-declare an upstream file in `source-files` to use it as an input.

### Replacing an upstream `sources` entry

A `source-files` entry whose `filename` collides with an upstream `sources` entry is an error by default. Set `replace-upstream = true` (with a non-empty `replace-reason`) to intentionally substitute it:
Expand Down
7 changes: 7 additions & 0 deletions internal/app/azldev/cmds/component/history_customizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,13 @@ func appendSourceFileItems(
Value: pkg,
})
}

for _, input := range sourceFile.Origin.Inputs {
items = append(items, CustomizationItem{
Kind: "source-files.inputs",
Value: input,
})
}
}

return items
Expand Down
3 changes: 2 additions & 1 deletion internal/app/azldev/cmds/component/history_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,11 @@ func TestCustomizationCollectorsCoverEveryFingerprintableField(t *testing.T) {
"SourceFileReference.ReplaceUpstream": "source-files.replace-upstream",
"SourceFileReference.Origin": "delegates to Origin walk",

// Origin -- Script and MockPackages are fingerprint-relevant; Type and Uri
// Origin -- Script, MockPackages, and Inputs are fingerprint-relevant; Type and Uri
// are tagged fingerprint:"-" (download location, not build input).
"Origin.Script": "source-files.script",
"Origin.MockPackages": "source-files.mock-packages",
"Origin.Inputs": "source-files.inputs",
}

actualFields := make(map[string]bool)
Expand Down
21 changes: 9 additions & 12 deletions internal/app/azldev/core/sources/sourceprep.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,17 +215,6 @@ func NewPreparer(
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.)
// 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)
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.
Expand All @@ -238,13 +227,21 @@ func (p *sourcePreparerImpl) PrepareSources(
fetchOpts = append(fetchOpts, sourceproviders.WithSkipLookaside())
}

// Use the source manager to fetch the component (spec file and sidecar files).
// Fetch the component first (spec, sidecar files, and upstream source tarballs).
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)
}

// Fetch custom and downloaded source files.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is an ordering issue here, FetchComponent can grab a dist-git file first, and if it happens to have the same name as something in source-files, we skip further checks. We should probably just not allow things like combining replace-upstream with this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the latest iteration I make sure source preparation never downloads any file already defined in source-files. Of course we continue to support replace-upstream to ensure the dev understands they are replacing a source already present in the upstream

if !p.skipLookaside {
if err := p.sourceManager.FetchFiles(ctx, component, outputDir); err != nil {
return fmt.Errorf("failed to fetch source files for component %#q:\n%w",
component.GetName(), err)
}
}

if applyOverlays {
err := p.applyOverlaysToSources(ctx, component, outputDir)
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions internal/app/azldev/core/sources/sourceprep_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ func TestPrepareSources_SourceManagerError(t *testing.T) {
expectedErr := errors.New("failed to fetch files")

component.EXPECT().GetName().AnyTimes().Return("test-component")
component.EXPECT().GetConfig().AnyTimes().Return(&projectconfig.ComponentConfig{})
sourceManager.EXPECT().FetchComponent(gomock.Any(), component, testOutputDir, gomock.Any()).Return(nil)
sourceManager.EXPECT().FetchFiles(gomock.Any(), component, testOutputDir).Return(expectedErr)

preparer, err := sources.NewPreparer(sourceManager, ctx.FS(), ctx, ctx)
Expand Down Expand Up @@ -633,8 +635,11 @@ func TestDiffSources_FetchError(t *testing.T) {
require.NoError(t, fileutils.MkdirAll(ctx.FS(), baseDir))

component.EXPECT().GetName().AnyTimes().Return("test-component")
component.EXPECT().GetConfig().AnyTimes().Return(&projectconfig.ComponentConfig{})

expectedErr := errors.New("network failure")

sourceManager.EXPECT().FetchComponent(gomock.Any(), component, gomock.Any(), gomock.Any()).Return(nil)
sourceManager.EXPECT().FetchFiles(gomock.Any(), component, gomock.Any()).Return(expectedErr)

preparer, err := sources.NewPreparer(sourceManager, ctx.FS(), ctx, ctx)
Expand Down
15 changes: 11 additions & 4 deletions internal/projectconfig/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,24 @@ type Origin struct {
// MockPackages is a list of RPM package names to install in the mock chroot before
// running [Origin.Script]. Only valid when [Origin.Type] is 'custom'.
MockPackages []string `toml:"mock-packages,omitempty" json:"mockPackages,omitempty" jsonschema:"title=Mock packages,description=RPM packages to install in the mock chroot before running the generation script. Only valid when origin type is 'custom'."`

// Inputs is a list of source-output filenames to copy next to [Origin.Script]
// before it runs. Each entry must be a plain filename. Only valid when
// [Origin.Type] is 'custom'.
Inputs []string `toml:"inputs,omitempty" json:"inputs,omitempty" jsonschema:"title=Inputs,description=Source-output filenames to make available next to the generation script before it runs. Only valid when origin type is 'custom'."`
}

// HashInclude implements the hashstructure [Includable] interface so that
// [Origin.Script] and [Origin.MockPackages] are omitted from the component
// fingerprint when they hold their zero values.
// [Origin.Script], [Origin.MockPackages], and [Origin.Inputs] are omitted from
// the component fingerprint when they hold their zero values.
func (o Origin) HashInclude(field string, _ any) (bool, error) {
switch field {
case "Script":
return o.Script != "", nil
case "MockPackages":
return len(o.MockPackages) > 0, nil
case "Inputs":
return len(o.Inputs) > 0, nil
}

return true, nil
Expand Down Expand Up @@ -114,10 +121,10 @@ type SourceFileReference struct {

// HashInclude implements the hashstructure [Includable] interface so that
// [SourceFileReference.Origin] is omitted from the component fingerprint when
// neither [Origin.Script] nor [Origin.MockPackages] are set.
// none of [Origin.Script], [Origin.MockPackages], or [Origin.Inputs] are set.
func (r SourceFileReference) HashInclude(field string, _ any) (bool, error) {
if field == "Origin" {
return r.Origin.Script != "" || len(r.Origin.MockPackages) > 0, nil
return r.Origin.Script != "" || len(r.Origin.MockPackages) > 0 || len(r.Origin.Inputs) > 0, nil
}

return true, nil
Expand Down
46 changes: 44 additions & 2 deletions internal/projectconfig/configfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,14 @@ func validateReplaceUpstream(ref SourceFileReference, componentName string) erro
return nil
}

// validateCustomSourceRef enforces the pairing rules for the 'script' and 'mock-packages'
// fields on a [SourceFileReference]:
// validateCustomSourceRef enforces the pairing rules for the 'script', 'mock-packages',
// and 'inputs' fields on a [SourceFileReference]:
// - 'script' is required when 'origin.type' is 'custom'.
// - 'script' must be empty when 'origin.type' is not 'custom'.
// - 'mock-packages' must be empty when 'origin.type' is not 'custom'.
// - 'inputs' must be empty when 'origin.type' is not 'custom'.
// - each 'inputs' entry must be a valid filename (no path separators).
// - each 'inputs' entry must be unique.
func validateCustomSourceRef(ref SourceFileReference, componentName string) error {
if ref.Origin.Type == OriginTypeCustom {
if ref.Origin.Script == "" {
Expand All @@ -268,6 +271,10 @@ func validateCustomSourceRef(ref SourceFileReference, componentName string) erro
ref.Origin.Script, ref.Filename, componentName, err)
}

if err := validateCustomSourceInputs(ref, componentName); err != nil {
return err
}
Comment thread
Tonisal-byte marked this conversation as resolved.

return nil
}

Expand All @@ -285,6 +292,41 @@ func validateCustomSourceRef(ref SourceFileReference, componentName string) erro
ref.Filename, componentName, string(ref.Origin.Type))
}

if len(ref.Origin.Inputs) > 0 {
return fmt.Errorf(
"source file %#q in component %#q has 'inputs' set but origin type is %#q; "+
"'inputs' is only valid when origin type is 'custom'",
ref.Filename, componentName, string(ref.Origin.Type))
}

return nil
}

func validateCustomSourceInputs(ref SourceFileReference, componentName string) error {
seen := make(map[string]bool, len(ref.Origin.Inputs))

for _, input := range ref.Origin.Inputs {
if err := fileutils.ValidateFilename(input); err != nil {
return fmt.Errorf(
"invalid 'inputs' entry %#q for source file %#q in component %#q:\n%w",
input, ref.Filename, componentName, err)
}

if seen[input] {
return fmt.Errorf(
"duplicate 'inputs' entry %#q for source file %#q in component %#q; each input filename must be unique",
input, ref.Filename, componentName)
}

seen[input] = true

if input == ref.Origin.Script {
return fmt.Errorf(
"'inputs' entry %#q for source file %#q in component %#q conflicts with 'script' filename",
input, ref.Filename, componentName)
}
}

return nil
}

Expand Down
92 changes: 92 additions & 0 deletions internal/projectconfig/configfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -584,3 +584,95 @@ func TestValidateCustomSourceRef_InvalidScriptFilename(t *testing.T) {
require.Error(t, err)
assert.Contains(t, err.Error(), "script")
}

func TestValidateCustomSourceRef_ValidInputs(t *testing.T) {
file := projectconfig.ConfigFile{
Components: map[string]projectconfig.ComponentConfig{
"comp": {
SourceFiles: []projectconfig.SourceFileReference{
{
Filename: "gen.tar.gz",
Origin: projectconfig.Origin{
Type: projectconfig.OriginTypeCustom,
Script: "gen.sh",
Inputs: []string{"upstream.tar.gz", "fix.patch"},
},
},
},
},
},
}
assert.NoError(t, file.Validate())
}

func TestValidateCustomSourceRef_DuplicateInputs(t *testing.T) {
file := projectconfig.ConfigFile{
Components: map[string]projectconfig.ComponentConfig{
"comp": {
SourceFiles: []projectconfig.SourceFileReference{
{
Filename: "gen.tar.gz",
Origin: projectconfig.Origin{
Type: projectconfig.OriginTypeCustom,
Script: "gen.sh",
Inputs: []string{"upstream.tar.gz", "upstream.tar.gz"},
},
},
},
},
},
}

err := file.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "duplicate 'inputs' entry")
assert.Contains(t, err.Error(), "upstream.tar.gz")
assert.Contains(t, err.Error(), "gen.tar.gz")
assert.Contains(t, err.Error(), "comp")
}

func TestValidateCustomSourceRef_InputsOnDownloadOrigin(t *testing.T) {
file := projectconfig.ConfigFile{
Components: map[string]projectconfig.ComponentConfig{
"comp": {
SourceFiles: []projectconfig.SourceFileReference{
{
Filename: "src.tar.gz",
Origin: projectconfig.Origin{
Type: projectconfig.OriginTypeURI,
Uri: "https://example.com/src.tar.gz",
Inputs: []string{"other.tar.gz"},
},
},
},
},
},
}
err := file.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "'inputs'")
assert.Contains(t, err.Error(), "custom")
}

func TestValidateCustomSourceRef_InvalidInputFilename(t *testing.T) {
file := projectconfig.ConfigFile{
Components: map[string]projectconfig.ComponentConfig{
"comp": {
SourceFiles: []projectconfig.SourceFileReference{
{
Filename: "gen.tar.gz",
Origin: projectconfig.Origin{
Type: projectconfig.OriginTypeCustom,
Script: "gen.sh",
Inputs: []string{"../escape.tar.gz"}, // path traversal attempt
},
},
},
},
},
}
err := file.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "'inputs'")
assert.Contains(t, err.Error(), "escape.tar.gz")
}
Loading
Loading