Skip to content
Merged
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
16 changes: 16 additions & 0 deletions docs/user/reference/config/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ A component definition tells azldev where to find the spec file, how to customiz
| Field | TOML Key | Type | Required | Description |
|-------|----------|------|----------|-------------|
| Spec source | `spec` | [SpecSource](#spec-source) | No | Where to find the spec file for this component. Inherited from distro defaults if not specified. |
| Release config | `release` | [ReleaseConfig](#release-configuration) | No | Controls how the Release tag is managed during rendering |
| Overlays | `overlays` | array of [Overlay](overlays.md) | No | Modifications to apply to the spec and/or source files |
| Build config | `build` | [BuildConfig](#build-configuration) | No | Build-time options (macros, conditionals, check config) |
| Source files | `source-files` | array of [SourceFileReference](#source-file-references) | No | Additional source files to download for this component |
Expand Down Expand Up @@ -97,6 +98,21 @@ spec = { type = "local", path = "azurelinux-release.spec" }

The `path` is relative to the config file that defines the component. Local spec files and any associated source files should be placed alongside the component's `.comp.toml` file.

## Release Configuration

The `[components.<name>.release]` section controls how azldev manages the Release tag during rendering.

| Field | TOML Key | Type | Required | Description |
|-------|----------|------|----------|-------------|
| Calculation | `calculation` | string | No | `"auto"` (default) = auto-bump; `"manual"` = skip all automatic Release manipulation |

Most components use auto mode (the default) and need no release configuration. Set `calculation = "manual"` for components that manage their own release numbering, such as kernel:

```toml
[components.kernel.release]
calculation = "manual"
```

## Build Configuration

The `[components.<name>.build]` section controls build-time options for a component.
Expand Down
4 changes: 4 additions & 0 deletions internal/app/azldev/core/components/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,10 @@ func (r *Resolver) createComponentFromConfig(componentConfig *projectconfig.Comp
componentConfig.Name, err)
}

if componentConfig.Release.Calculation == "" {
componentConfig.Release.Calculation = projectconfig.ReleaseCalculationAuto
}

return &resolvedComponent{
env: r.env,
config: *componentConfig,
Expand Down
13 changes: 12 additions & 1 deletion internal/app/azldev/core/components/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,22 @@ func setupTestSpec(t *testing.T, testEnv *testutils.TestEnv, path string) projec
SourceType: projectconfig.SpecSourceTypeLocal,
Path: path,
},
Release: projectconfig.ReleaseConfig{
Calculation: projectconfig.ReleaseCalculationAuto,
},
}
}

// Constructs a test component's config, adds it to the test environment, and returns a copy of it.
func addTestComponentToConfig(t *testing.T, env *testutils.TestEnv) projectconfig.ComponentConfig {
t.Helper()

component := projectconfig.ComponentConfig{Name: "test-component"}
component := projectconfig.ComponentConfig{
Name: "test-component",
Release: projectconfig.ReleaseConfig{
Calculation: projectconfig.ReleaseCalculationAuto,
},
}

env.Config.Components[component.Name] = component

Expand Down Expand Up @@ -242,6 +250,9 @@ func TestFindAllComponents_MergesComponentPresentBySpecAndConfig(t *testing.T) {
SourceType: projectconfig.SpecSourceTypeLocal,
Path: testSpecPath,
},
Release: projectconfig.ReleaseConfig{
Calculation: projectconfig.ReleaseCalculationAuto,
},
}

// Find!
Expand Down
47 changes: 16 additions & 31 deletions internal/app/azldev/core/sources/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,42 +84,34 @@ func BumpStaticRelease(releaseValue string, commitCount int) (string, error) {
return fmt.Sprintf("%d%s", newRelease, suffix), nil
}

// HasUserReleaseOverlay reports whether the given overlay list contains an overlay
// that explicitly sets or updates the Release tag. This is used to determine whether
// a user has configured the component to handle a non-standard Release value
// (e.g. one using a custom macro like %{pkg_release}).
func HasUserReleaseOverlay(overlays []projectconfig.ComponentOverlay) bool {
for _, overlay := range overlays {
if !strings.EqualFold(overlay.Tag, "Release") {
continue
}

if overlay.Type == projectconfig.ComponentOverlaySetSpecTag ||
overlay.Type == projectconfig.ComponentOverlayUpdateSpecTag {
return true
}
}

return false
}

// tryBumpStaticRelease checks whether the component's spec uses %autorelease.
// If not, it bumps the static Release tag by commitCount and applies the change
// as an overlay to the spec file in-place. This ensures that components with static
// release numbers get deterministic version bumps matching the number of synthetic
// commits applied from the project repository.
//
// When the component's release calculation is "manual", this function is a no-op.
//
// When the spec uses %autorelease, this function is a no-op because rpmautospec
// already resolves the release number from git history.
//
// When the Release tag uses a non-standard value (not %autorelease and not a leading
// integer, e.g. %{pkg_release}), the component must define an explicit overlay that
// sets the Release tag. If no such overlay exists, an error is returned.
// integer, e.g. %{pkg_release}), the component must set release.calculation to
// "manual", and likely define an explicit overlay that sets the Release tag.
// If a non-standard Release is found and release.calculation is not "manual",
// an error is returned.
func (p *sourcePreparerImpl) tryBumpStaticRelease(
component components.Component,
sourcesDirPath string,
commitCount int,
) error {
if component.GetConfig().Release.Calculation == projectconfig.ReleaseCalculationManual {
slog.Debug("Component uses manual release calculation; skipping static release bump",
"component", component.GetName())

return nil
}

specPath, err := p.resolveSpecPath(component, sourcesDirPath)
if err != nil {
return err
Expand All @@ -138,21 +130,14 @@ func (p *sourcePreparerImpl) tryBumpStaticRelease(
return nil
}

// Skip static release bump if the user has defined an explicit overlay for the Release tag.
if HasUserReleaseOverlay(component.GetConfig().Overlays) {
slog.Debug("Component has an explicit Release overlay; skipping static release bump",
"component", component.GetName())

return nil
}

newRelease, err := BumpStaticRelease(releaseValue, commitCount)
if err != nil {
// The Release tag does not start with an integer (e.g. %{pkg_release})
// and the user did not provide an explicit overlay to set it.
// and the user did not set release.calculation to "manual".
return fmt.Errorf(
"component %#q has a non-standard Release tag value %#q that cannot be auto-bumped; "+
"add a \"spec-set-tag\" overlay for the Release tag in the component configuration:\n%w",
"set 'release.calculation = \"manual\"' in the component configuration "+
"and add a \"spec-set-tag\" overlay for the Release tag if needed:\n%w",
component.GetName(), releaseValue, err)
}
Comment thread
dmcilvaney marked this conversation as resolved.

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

package sources

import (
"path/filepath"
"testing"

"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/components/components_testutils"
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileperms"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)

const testSourcesDir = "/sources"

func newTestPreparer(memFS afero.Fs) *sourcePreparerImpl {
return &sourcePreparerImpl{
fs: memFS,
}
}

func writeTestSpec(t *testing.T, memFS afero.Fs, name, release string) {
t.Helper()

specDir := filepath.Join(testSourcesDir, name)
require.NoError(t, fileutils.MkdirAll(memFS, specDir))

specPath := filepath.Join(specDir, name+".spec")
content := []byte("Name: " + name + "\nVersion: 1.0.0\nRelease: " + release + "\nSummary: Test\nLicense: MIT\n")

require.NoError(t, fileutils.WriteFile(memFS, specPath, content, fileperms.PublicFile))
}

func mockComponent(
ctrl *gomock.Controller, name string, config *projectconfig.ComponentConfig,
) *components_testutils.MockComponent {
comp := components_testutils.NewMockComponent(ctrl)
comp.EXPECT().GetName().AnyTimes().Return(name)
comp.EXPECT().GetConfig().AnyTimes().Return(config)

return comp
}

func TestTryBumpStaticRelease_ManualSkips(t *testing.T) {
ctrl := gomock.NewController(t)
memFS := afero.NewMemMapFs()
preparer := newTestPreparer(memFS)

comp := mockComponent(ctrl, "kernel", &projectconfig.ComponentConfig{
Release: projectconfig.ReleaseConfig{
Calculation: projectconfig.ReleaseCalculationManual,
},
})

// No spec file needed — should skip before reading anything.
err := preparer.tryBumpStaticRelease(comp, testSourcesDir, 3)
require.NoError(t, err)
}

func TestTryBumpStaticRelease_AutoreleaseSkips(t *testing.T) {
ctrl := gomock.NewController(t)
memFS := afero.NewMemMapFs()
preparer := newTestPreparer(memFS)

writeTestSpec(t, memFS, "test-pkg", "%autorelease")

comp := mockComponent(ctrl, "test-pkg", &projectconfig.ComponentConfig{
Release: projectconfig.ReleaseConfig{
Calculation: projectconfig.ReleaseCalculationAuto,
},
})

err := preparer.tryBumpStaticRelease(comp, filepath.Join(testSourcesDir, "test-pkg"), 3)
require.NoError(t, err)
}

func TestTryBumpStaticRelease_StaticBumps(t *testing.T) {
ctrl := gomock.NewController(t)
memFS := afero.NewMemMapFs()
preparer := newTestPreparer(memFS)

writeTestSpec(t, memFS, "test-pkg", "1%{?dist}")

comp := mockComponent(ctrl, "test-pkg", &projectconfig.ComponentConfig{
Release: projectconfig.ReleaseConfig{
Calculation: projectconfig.ReleaseCalculationAuto,
},
})

err := preparer.tryBumpStaticRelease(comp, filepath.Join(testSourcesDir, "test-pkg"), 3)
require.NoError(t, err)

// Verify the spec was updated.
specPath := filepath.Join(testSourcesDir, "test-pkg", "test-pkg.spec")
content, err := fileutils.ReadFile(memFS, specPath)
require.NoError(t, err)
assert.Contains(t, string(content), "Release: 4%{?dist}")
}

func TestTryBumpStaticRelease_NonStandardErrorsWithoutManual(t *testing.T) {
ctrl := gomock.NewController(t)
memFS := afero.NewMemMapFs()
preparer := newTestPreparer(memFS)

writeTestSpec(t, memFS, "kernel", "%{pkg_release}")

comp := mockComponent(ctrl, "kernel", &projectconfig.ComponentConfig{
Release: projectconfig.ReleaseConfig{
Calculation: projectconfig.ReleaseCalculationAuto,
},
})

err := preparer.tryBumpStaticRelease(comp, filepath.Join(testSourcesDir, "kernel"), 3)
require.Error(t, err)
assert.Contains(t, err.Error(), "cannot be auto-bumped")
assert.Contains(t, err.Error(), "release.calculation")
}

func TestTryBumpStaticRelease_NonStandardSucceedsWithManual(t *testing.T) {
ctrl := gomock.NewController(t)
memFS := afero.NewMemMapFs()
preparer := newTestPreparer(memFS)

writeTestSpec(t, memFS, "kernel", "%{pkg_release}")

comp := mockComponent(ctrl, "kernel", &projectconfig.ComponentConfig{
Release: projectconfig.ReleaseConfig{
Calculation: projectconfig.ReleaseCalculationManual,
},
})

err := preparer.tryBumpStaticRelease(comp, filepath.Join(testSourcesDir, "kernel"), 3)
require.NoError(t, err)
}
34 changes: 0 additions & 34 deletions internal/app/azldev/core/sources/release_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (

"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/sources"
"github.com/microsoft/azure-linux-dev-tools/internal/global/testctx"
"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"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -96,36 +95,3 @@ func TestGetReleaseTagValue_FileNotFound(t *testing.T) {
_, err := sources.GetReleaseTagValue(ctx.FS(), "/nonexistent.spec")
require.Error(t, err)
}

func TestHasUserReleaseOverlay(t *testing.T) {
for _, testCase := range []struct {
name string
overlays []projectconfig.ComponentOverlay
expected bool
}{
{"no overlays", nil, false},
{"unrelated tag", []projectconfig.ComponentOverlay{
{Type: projectconfig.ComponentOverlaySetSpecTag, Tag: "Version", Value: "1.0"},
}, false},
{"unsupported overlay type", []projectconfig.ComponentOverlay{
{Type: projectconfig.ComponentOverlayAddSpecTag, Tag: "Release", Value: "1%{?dist}"},
}, false},
{"spec-set-tag", []projectconfig.ComponentOverlay{
{Type: projectconfig.ComponentOverlaySetSpecTag, Tag: "Release", Value: "1%{?dist}"},
}, true},
{"spec-update-tag", []projectconfig.ComponentOverlay{
{Type: projectconfig.ComponentOverlayUpdateSpecTag, Tag: "Release", Value: "2%{?dist}"},
}, true},
{"case insensitive", []projectconfig.ComponentOverlay{
{Type: projectconfig.ComponentOverlaySetSpecTag, Tag: "release", Value: "1%{?dist}"},
}, true},
{"mixed overlays", []projectconfig.ComponentOverlay{
{Type: projectconfig.ComponentOverlaySetSpecTag, Tag: "BuildRequires", Value: "gcc"},
{Type: projectconfig.ComponentOverlaySetSpecTag, Tag: "Release", Value: "5%{?dist}"},
}, true},
} {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.expected, sources.HasUserReleaseOverlay(testCase.overlays))
})
}
}
24 changes: 24 additions & 0 deletions internal/projectconfig/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,26 @@ func (g ComponentGroupConfig) WithAbsolutePaths(referenceDir string) ComponentGr
return result
}

// ReleaseCalculation controls how the Release tag is managed during rendering.
type ReleaseCalculation string

const (
// ReleaseCalculationAuto is the default. azldev auto-bumps the Release tag based on
// synthetic commit history. Static integer releases are incremented; %autorelease
// is handled by rpmautospec.
ReleaseCalculationAuto ReleaseCalculation = "auto"

// ReleaseCalculationManual skips all automatic Release tag manipulation. Use this for
// components that manage their own release numbering (e.g. kernel).
ReleaseCalculationManual ReleaseCalculation = "manual"
)

// ReleaseConfig holds release-related configuration for a component.
type ReleaseConfig struct {
// Calculation controls how the Release tag is managed during rendering.
Calculation ReleaseCalculation `toml:"calculation,omitempty" json:"calculation,omitempty" validate:"omitempty,oneof=auto manual" jsonschema:"enum=auto,enum=manual,default=auto,title=Release calculation,description=Controls how the Release tag is managed during rendering. Empty or omitted means auto."`
}

// Defines a component.
type ComponentConfig struct {
// The component's name; not actually present in serialized files.
Expand All @@ -125,6 +145,9 @@ type ComponentConfig struct {
// Where to get its spec and adjacent files from.
Spec SpecSource `toml:"spec,omitempty" json:"spec,omitempty" jsonschema:"title=Spec,description=Identifies where to find the spec for this component"`

// Release configuration for this component.
Release ReleaseConfig `toml:"release,omitempty" json:"release,omitempty" table:"-" jsonschema:"title=Release configuration,description=Configuration for how the Release tag is managed during rendering."`

// Overlays to apply to sources after they've been acquired. May mutate the spec as well as sources.
Overlays []ComponentOverlay `toml:"overlays,omitempty" json:"overlays,omitempty" table:"-" jsonschema:"title=Overlays,description=Overlays to apply to this component's spec and/or sources"`

Expand Down Expand Up @@ -173,6 +196,7 @@ func (c *ComponentConfig) WithAbsolutePaths(referenceDir string) *ComponentConfi
Name: c.Name,
SourceConfigFile: c.SourceConfigFile,
RenderedSpecDir: c.RenderedSpecDir,
Release: c.Release,
Spec: deep.MustCopy(c.Spec),
Build: deep.MustCopy(c.Build),
SourceFiles: deep.MustCopy(c.SourceFiles),
Expand Down
Loading
Loading