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
35 changes: 35 additions & 0 deletions docs/user/reference/config/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ The `[components.<name>.build]` section controls build-time options for a compon
| Without options | `without` | string array | No | Build conditionals to disable (`--without <option>` passed to rpmbuild) |
| Macro definitions | `defines` | map of string to string | No | RPM macro definitions (`--define '<name> <value>'` passed to rpmbuild) |
| Undefined macros | `undefines` | string array | No | RPM macro names to undefine (`--undefine '<name>'` passed to rpmbuild) |
| Emit upstream provenance | `emit-upstream-provenance` | boolean | No | Inject `%fedora_upstream_version`/`%fedora_upstream_release` macros for Fedora upstream components (defaults to `false`) |
| Check config | `check` | [CheckConfig](#check-configuration) | No | Configuration for the `%check` section of the spec |
| Failure config | `failure` | [FailureConfig](#failure-configuration) | No | Configuration and policy regarding build failures |
| Build hints | `hints` | [BuildHints](#build-hints) | No | Non-essential hints for how or when to build the component |
Expand Down Expand Up @@ -197,6 +198,40 @@ The `undefines` field removes macros that would otherwise be defined:
undefines = ["fedora"]
```

### Upstream Provenance Macros

For components sourced from a Fedora upstream (`spec.type = "upstream"` with a Fedora `upstream-distro`), azldev can inject two macros into the component's build so the spec can record where it was derived from (useful for SBAT and similar provenance metadata). This is **opt-in** — enable it with `emit-upstream-provenance`:

```toml
[components.grub2.build]
emit-upstream-provenance = true
```

| Macro | Description |
|-------|-------------|
| `%fedora_upstream_version` | The `Version` tag from the pristine upstream Fedora spec |
| `%fedora_upstream_release` | The `Release` tag from the pristine upstream Fedora spec, with `%{?dist}` expanded to the Fedora dist tag (e.g. `.fc43`) |

The values are read from the upstream spec **before** any azldev overlays are applied, so they reflect the true upstream Name-Version-Release, not the Azure Linux–modified spec. The macros are derived fresh at render/build time from the pinned upstream commit and emitted into the component's generated macros file (loaded via `%{load:...}`).

Example: for a component pinned to Fedora 43's `grub2-2.12-5.fc43`, the spec can reference:

```spec
%sbat_generate_metadata ... derived from grub2 %{fedora_upstream_version}-%{fedora_upstream_release}
```

which expands to `grub2 2.12-5.fc43`.

Notes:

- The flag has no effect on local or SRPM components (they have no upstream provenance) or on non-Fedora upstreams; for those it is silently ignored.
- If a component defines a macro of the same name via `build.defines`, the user-defined value wins — the injected value does not overwrite it.

Limitations:

- Tag values are read as plain text, not evaluated by rpm. Only `%{?dist}` is expanded. A literal tag like `Version: 2.12` is captured as-is, but a tag built from other macros like `Version: %{majorver}.%{minorver}` is captured with those macros unexpanded.
- Listing `fedora_upstream_version` or `fedora_upstream_release` in `build.undefines` does **not** suppress the injected macros — they are layered on after `undefines` is applied. To disable them, set `emit-upstream-provenance = false` (or omit it) instead.

### Check Configuration

The `check` field controls the `%check` section of the spec (the package's test suite).
Expand Down
3 changes: 3 additions & 0 deletions internal/app/azldev/cmds/component/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ func BuildComponent(
)
}

preparerOpts = append(preparerOpts,
sources.WithUpstreamProvenance(sources.FedoraDistTag(distro.Ref.Name, distro.Version.ReleaseVer)))

Comment thread
liunan-ms marked this conversation as resolved.
sourcePreparer, err := sources.NewPreparer(sourceManager, env.FS(), env, env, preparerOpts...)
if err != nil {
return ComponentBuildResults{},
Expand Down
3 changes: 2 additions & 1 deletion internal/app/azldev/cmds/component/diffsources.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ func DiffComponentSources(env *azldev.Env, options *DiffSourcesOptions) (interfa
return nil, fmt.Errorf("failed to create source manager:\n%w", err)
}

preparer, err := sources.NewPreparer(sourceManager, env.FS(), env, env)
preparer, err := sources.NewPreparer(sourceManager, env.FS(), env, env,
sources.WithUpstreamProvenance(sources.FedoraDistTag(distro.Ref.Name, distro.Version.ReleaseVer)))
if err != nil {
return nil, fmt.Errorf("failed to create source preparer:\n%w", err)
}
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 @@ -116,6 +116,13 @@ func appendBuildItems(
items = append(items, CustomizationItem{Kind: "build.undefines", Value: macro})
}

if build.EmitUpstreamProvenance {
items = append(items, CustomizationItem{
Kind: "build.emit-upstream-provenance",
Value: strconv.FormatBool(true),
})
}

if build.Check.Skip {
items = append(items, CustomizationItem{
Kind: "build.check.skip",
Expand Down
23 changes: 13 additions & 10 deletions internal/app/azldev/cmds/component/history_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,12 @@ func TestCustomizationCollectorsCoverEveryFingerprintableField(t *testing.T) {
"ComponentConfig.Packages": "appendPackageItems (opaque unit per package override)",

// ComponentBuildConfig.
"ComponentBuildConfig.With": "build.with",
"ComponentBuildConfig.Without": "build.without",
"ComponentBuildConfig.Defines": "build.defines",
"ComponentBuildConfig.Undefines": "build.undefines",
"ComponentBuildConfig.Check": "delegates to CheckConfig walk",
"ComponentBuildConfig.With": "build.with",
"ComponentBuildConfig.Without": "build.without",
"ComponentBuildConfig.Defines": "build.defines",
"ComponentBuildConfig.Undefines": "build.undefines",
"ComponentBuildConfig.EmitUpstreamProvenance": "build.emit-upstream-provenance",
"ComponentBuildConfig.Check": "delegates to CheckConfig walk",

// CheckConfig.
"CheckConfig.Skip": "build.check.skip",
Expand Down Expand Up @@ -197,11 +198,12 @@ func TestCollectCustomizationsEmitsEveryKind(t *testing.T) {
{Type: projectconfig.ComponentOverlayAddSpecTag, Tag: "Release", Value: "1"},
},
Build: projectconfig.ComponentBuildConfig{
With: []string{"feature"},
Without: []string{"docs"},
Defines: map[string]string{"macro": "value"},
Undefines: []string{"othermacro"},
Check: projectconfig.CheckConfig{Skip: true, SkipReason: "flaky"},
With: []string{"feature"},
Without: []string{"docs"},
Defines: map[string]string{"macro": "value"},
Undefines: []string{"othermacro"},
Check: projectconfig.CheckConfig{Skip: true, SkipReason: "flaky"},
EmitUpstreamProvenance: true,
},
Spec: projectconfig.SpecSource{
SourceType: projectconfig.SpecSourceTypeUpstream,
Expand All @@ -228,6 +230,7 @@ func TestCollectCustomizationsEmitsEveryKind(t *testing.T) {
"build.defines",
"build.undefines",
"build.check.skip",
"build.emit-upstream-provenance",
"spec.source-type",
"spec.upstream-commit",
"spec.upstream-name",
Expand Down
3 changes: 3 additions & 0 deletions internal/app/azldev/cmds/component/preparesources.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ func buildPreparerOptions(
)
}

opts = append(opts,
sources.WithUpstreamProvenance(sources.FedoraDistTag(distro.Ref.Name, distro.Version.ReleaseVer)))

if options.AllowNoHashes {
opts = append(opts, sources.WithAllowNoHashes())
}
Expand Down
1 change: 1 addition & 0 deletions internal/app/azldev/cmds/component/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,7 @@ func prepareComponentSources(
sources.WithGitRepo(env, env.LockReader(), distro.Version.ReleaseVer),
sources.WithDirtyDetection(),
sources.WithSkipLookaside(),
sources.WithUpstreamProvenance(sources.FedoraDistTag(distro.Ref.Name, distro.Version.ReleaseVer)),
}
Comment thread
liunan-ms marked this conversation as resolved.
Comment thread
liunan-ms marked this conversation as resolved.

preparer, err := sources.NewPreparer(sourceManager, env.FS(), env, env, preparerOpts...)
Expand Down
44 changes: 41 additions & 3 deletions internal/app/azldev/core/sources/sourceprep.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,17 @@ func WithSkipLookaside() PreparerOption {
}
}

// WithUpstreamProvenance returns a [PreparerOption] that enables emission of
// the %fedora_upstream_version and %fedora_upstream_release macros for Fedora
// upstream components. distTag is the resolved %{?dist} expansion (e.g.
// ".fc43", as produced by [FedoraDistTag]) used to expand the pristine Release
// tag. Pass "" (e.g. for non-Fedora upstreams) to disable the macros.
func WithUpstreamProvenance(distTag string) PreparerOption {
return func(p *sourcePreparerImpl) {
p.upstreamDistTag = distTag
}
}

// WithAllowNoHashes returns a [PreparerOption] that allows source file
// references to omit their [projectconfig.SourceFileReference.Hash] value.
// When set, any source file that lacks a hash will have its hash computed
Expand Down Expand Up @@ -163,6 +174,12 @@ type sourcePreparerImpl struct {
// releaseVer is the per-component resolved distro release version, not the
// project default. Set via [WithGitRepo].
releaseVer string

// upstreamDistTag is the resolved %{?dist} expansion (e.g. ".fc43") used to
// expand a Fedora upstream component's pristine Release tag when emitting
// the %fedora_upstream_* macros. Empty disables provenance macros. Set via
// [WithUpstreamProvenance].
upstreamDistTag string
}

// NewPreparer creates a new [SourcePreparer] instance. All positional arguments
Expand Down Expand Up @@ -908,7 +925,13 @@ func (p *sourcePreparerImpl) resolveSourceHash(
// 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)
macros := buildMacrosMap(component.GetConfig().Build)

// Layer Fedora upstream provenance macros on top, reading the pristine
// upstream spec that is already on disk (before overlays are applied).
p.addUpstreamProvenanceMacros(macros, component, outputDir)

contents := renderMacrosFile(macros)
if contents == "" {
return "", nil
}
Expand Down Expand Up @@ -948,8 +971,15 @@ func (p *sourcePreparerImpl) writeMacrosFile(component components.Component, out
// 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.
return renderMacrosFile(buildMacrosMap(buildConfig))
}

// buildMacrosMap converts a build configuration's with/without/defines/undefines
// into a unified macro name->value map. Later definitions override earlier ones;
// processing order is: with flags -> without flags -> explicit defines ->
// undefines. Upstream provenance macros are layered on separately by
// [sourcePreparerImpl.addUpstreamProvenanceMacros].
func buildMacrosMap(buildConfig projectconfig.ComponentBuildConfig) map[string]string {
macros := make(map[string]string)

// Convert 'with' flags to macros: FLAG -> _with_FLAG = 1
Expand All @@ -973,6 +1003,14 @@ func GenerateMacrosFileContents(buildConfig projectconfig.ComponentBuildConfig)
delete(macros, undef)
}

return macros
}

// renderMacrosFile serializes a fully-resolved macro map to RPM macros-file
// format (%name value), sorted alphabetically for deterministic output and
// prefixed with the standard auto-generated header. Returns "" when the map is
// empty, signaling that no macros file is needed.
func renderMacrosFile(macros map[string]string) string {
if len(macros) == 0 {
return ""
}
Expand Down
165 changes: 165 additions & 0 deletions internal/app/azldev/core/sources/upstream_provenance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package sources

import (
"bytes"
"fmt"
"log/slog"
"strings"

"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/rpm/spec"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
)

// Macro names emitted to carry upstream Fedora provenance into a component's
// build. Specs may reference these (e.g. for SBAT) via %fedora_upstream_version
// and %fedora_upstream_release.
const (
fedoraUpstreamVersionMacro = "fedora_upstream_version"
fedoraUpstreamReleaseMacro = "fedora_upstream_release"
)

// distPlaceholder is the RPM %{?dist} token substituted with the resolved
// Fedora dist tag when expanding a pristine upstream Release value.
const distPlaceholder = "%{?dist}"

// FedoraDistTag returns the RPM %{?dist} expansion for a Fedora distro
// (".fc<releasever>", e.g. ".fc43"), or "" when the distro is not Fedora or the
// release version is not a plain integer. Fedora dist tags are always ".fc<N>";
// a non-numeric release version (e.g. "rawhide") has no well-defined dist tag,
// so provenance is disabled rather than emitting an invalid value like
// ".fcrawhide". Callers pass the result to [WithUpstreamProvenance].
func FedoraDistTag(distroName, releaseVer string) string {
if !strings.EqualFold(distroName, "fedora") || !isNumericReleaseVer(releaseVer) {
return ""
}

return ".fc" + releaseVer
}

// isNumericReleaseVer reports whether s is a non-empty string of ASCII digits,
// i.e. a Fedora numbered release like "43".
func isNumericReleaseVer(s string) bool {
if s == "" {
return false
}

for _, r := range s {
if r < '0' || r > '9' {
return false
}
}

return true
}

// addUpstreamProvenanceMacros injects %fedora_upstream_version and
// %fedora_upstream_release into macros for Fedora upstream components. The
// values are read from the pristine upstream spec on disk (before overlays are
// applied) and the Release tag's %{?dist} token is expanded to the resolved
// Fedora dist tag.
//
// Best-effort: if the component is not a Fedora upstream, or the spec cannot be
// located or parsed, no macros are added. User-defined macros take precedence —
// an existing entry is never overwritten.
func (p *sourcePreparerImpl) addUpstreamProvenanceMacros(
macros map[string]string, component components.Component, sourcesDir string,
) {
// Only Fedora upstream components carry upstream provenance. Local and
// SRPM components have no upstream spec; their own identity is already
// available at build time via %{version}/%{release}.
if p.upstreamDistTag == "" {
return
}

config := component.GetConfig()

// Opt-in: the macros are only useful to specs that reference them, so they
// are emitted only for components that explicitly request them.
if !config.Build.EmitUpstreamProvenance {
return
}

if config.Spec.SourceType != projectconfig.SpecSourceTypeUpstream {
return
}

specPath, err := findSpecInDir(p.fs, component, sourcesDir)
if err != nil {
// The user opted in via emit-upstream-provenance, so surface the
// failure loudly enough to be visible in normal CI logs; still
// non-fatal so the render/build proceeds without the macros.
slog.Warn("Skipping upstream provenance macros; spec not found",
"component", component.GetName(), "error", err)

return
}

version, release, err := parseSpecVersionRelease(p.fs, specPath)
if err != nil {
slog.Warn("Skipping upstream provenance macros; failed to parse spec",
"component", component.GetName(), "error", err)

return
}

if version != "" {
setMacroIfAbsent(macros, fedoraUpstreamVersionMacro, version)
}

if release != "" {
setMacroIfAbsent(macros, fedoraUpstreamReleaseMacro,
strings.ReplaceAll(release, distPlaceholder, p.upstreamDistTag))
}
}

// setMacroIfAbsent sets macros[name]=value only when name is not already
// present, so user-defined macros (e.g. from 'build.defines') win.
func setMacroIfAbsent(macros map[string]string, name, value string) {
if _, exists := macros[name]; !exists {
macros[name] = value
}
}

// parseSpecVersionRelease reads the Version and Release tags from the base
// package of the spec at specPath. Values are captured verbatim (no macro
// expansion beyond the caller's later %{?dist} substitution). Missing tags
// yield empty strings; it is not an error for a tag to be absent.
func parseSpecVersionRelease(fs opctx.FS, specPath string) (version, release string, err error) {
data, err := fileutils.ReadFile(fs, specPath)
if err != nil {
return "", "", fmt.Errorf("failed to read spec %#q:\n%w", specPath, err)
}

parsed, err := spec.OpenSpec(bytes.NewReader(data))
if err != nil {
return "", "", fmt.Errorf("failed to parse spec %#q:\n%w", specPath, err)
}

// VisitTagsPackage("") iterates tags in the base (unnamed) package, where
// Name/Version/Release live for a well-formed spec.
visitErr := parsed.VisitTagsPackage("", func(tagLine *spec.TagLine, _ *spec.Context) error {
switch strings.ToLower(tagLine.Tag) {
case "version":
if version == "" {
version = strings.TrimSpace(tagLine.Value)
}
case "release":
if release == "" {
release = strings.TrimSpace(tagLine.Value)
}
}

return nil
})
if visitErr != nil {
return "", "", fmt.Errorf("failed to scan spec tags in %#q:\n%w", specPath, visitErr)
}

return version, release, nil
}
Loading
Loading