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
7 changes: 4 additions & 3 deletions docs/user/reference/cli/azldev_package_list.md

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

121 changes: 121 additions & 0 deletions internal/app/azldev/cmds/pkg/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"log/slog"
"sort"
"strings"

"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev"
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
Expand All @@ -21,6 +22,15 @@ type ListPackageOptions struct {
// PackageNames contains specific binary package names to look up.
// If a package is not in any explicit config it is still resolved using project defaults.
PackageNames []string

// SynthesizeDebugPackages, when true, augments the result list with synthetic
// '-debuginfo' packages (one per reported package, using a parallel publish
// channel derived from the original package's publish channel by appending
// '-debuginfo', except when the original channel is "" or "none") and synthetic
// '-debugsource' packages (one per component in the project configuration,
// using the component's resolved publish channel after applying the same
// debug-channel derivation logic).
SynthesizeDebugPackages bool
}

func listOnAppInit(_ *azldev.App, parent *cobra.Command) {
Expand Down Expand Up @@ -66,6 +76,8 @@ Resolution order (lowest to highest priority):

cmd.Flags().BoolVarP(&options.All, "all-packages", "a", false, "List all explicitly-configured binary packages")
cmd.Flags().StringArrayVarP(&options.PackageNames, "package", "p", []string{}, "Package name to look up (repeatable)")
cmd.Flags().BoolVar(&options.SynthesizeDebugPackages, "synthesize-debug-packages", false,
"Also synthesize '-debuginfo' packages (per reported package) and '-debugsource' packages (per component)")

azldev.ExportAsMCPTool(cmd)

Expand Down Expand Up @@ -186,10 +198,119 @@ func ListPackages(env *azldev.Env, options *ListPackageOptions) ([]PackageListRe
})
}

if options.SynthesizeDebugPackages {
slog.Warn("'--synthesize-debug-packages' is a transitional flag and may change or be removed " +
"once first-class debug-package configuration is supported.")

results, err = synthesizeDebugPackages(results, proj)
if err != nil {
return nil, err
}
}

// Sort by package name for deterministic, readable output.
sort.Slice(results, func(i, j int) bool {
return results[i].PackageName < results[j].PackageName
})

return results, nil
}

// synthesizeDebugPackages augments results with synthetic '-debuginfo' packages (one per
// already-resolved package, using a parallel publish channel derived from the original
// package's publish channel by appending '-debuginfo', except when the original channel is
// "" or "none") and '-debugsource' packages (one per component in the project, with the
// publish channel resolved through the normal component → project default chain).
//
// Note: '-debugsource' entries are emitted for every component in the project regardless of
// which packages were requested via '-p'. Components own packages via [ComponentConfig.Packages],
// but most listed packages come from package groups with no component association — so scoping
// debugsource emission to the requested package set cannot be done reliably with the current
// configuration model.
//
// Synthetic entries that collide with an already-present (real) package name are skipped so
// real configuration always wins. Source packages whose names already end in '-debuginfo' or
// '-debugsource' do not get a doubled suffix synthesized.
func synthesizeDebugPackages(
results []PackageListResult, proj *projectconfig.ProjectConfig,
Comment thread
reubeno marked this conversation as resolved.
) ([]PackageListResult, error) {
existing := make(map[string]struct{}, len(results))
for _, result := range results {
existing[result.PackageName] = struct{}{}
}

// One '-debuginfo' per originally-reported package, sharing its publish channel and
// group/component attribution.
debugInfoEntries := make([]PackageListResult, 0, len(results))

for _, result := range results {
if isDebugPackageName(result.PackageName) {
continue
}

name := result.PackageName + "-debuginfo"
if _, exists := existing[name]; exists {
continue
}
Comment thread
reubeno marked this conversation as resolved.

existing[name] = struct{}{}
debugInfoEntries = append(debugInfoEntries, PackageListResult{
PackageName: name,
Group: result.Group,
Component: result.Component,
Channel: debugChannelName(result.Channel),
})
}

results = append(results, debugInfoEntries...)

// One '-debugsource' per component. Resolve the synthesized package using the
// standard package-resolution chain for '<component>-debugsource' so the entry
// reflects any explicit package override, package-group defaults, component
// settings, or project defaults instead of an implicit empty channel.
for compName, comp := range proj.Components {
if isDebugPackageName(compName) {
continue
}

name := compName + "-debugsource"
if _, exists := existing[name]; exists {
continue
}

existing[name] = struct{}{}

compCopy := comp

pkgConfig, err := projectconfig.ResolvePackageConfig(name, &compCopy, proj)
if err != nil {
return nil, fmt.Errorf("failed to resolve config for synthesized package %#q:\n%w", name, err)
}

results = append(results, PackageListResult{
PackageName: name,
Component: compName,
Channel: debugChannelName(pkgConfig.Publish.Channel),
})
}

return results, nil
}

// debugChannelName returns the publish-channel name to use for a synthesized debug package.
// Real (non-empty, non-"none") channels are suffixed with '-debuginfo' so debug artifacts are
// published to a parallel channel; "" and 'none' are passed through unchanged because they
// represent "default" and "do not publish" respectively.
Comment thread
reubeno marked this conversation as resolved.
func debugChannelName(channel string) string {
if channel == "" || channel == "none" || strings.HasSuffix(channel, "-debuginfo") {
return channel
}

return channel + "-debuginfo"
}

// isDebugPackageName reports whether name already has a '-debuginfo' or '-debugsource' suffix,
// so the caller can avoid synthesizing doubled-suffix names like 'foo-debuginfo-debuginfo'.
func isDebugPackageName(name string) bool {
return strings.HasSuffix(name, "-debuginfo") || strings.HasSuffix(name, "-debugsource")
}
184 changes: 184 additions & 0 deletions internal/app/azldev/cmds/pkg/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,187 @@ func TestListPackages_DuplicatePackageAcrossComponents_ReturnsError(t *testing.T
assert.Contains(t, err.Error(), "curl")
assert.Contains(t, err.Error(), "other")
}

func TestListPackages_SynthesizeDebugPackages(t *testing.T) {
testEnv := testutils.NewTestEnv(t)
testEnv.Config.DefaultPackageConfig = projectconfig.PackageConfig{
Publish: projectconfig.PackagePublishConfig{Channel: "default-channel"},
}
testEnv.Config.PackageGroups = map[string]projectconfig.PackageGroupConfig{
"devel-packages": {
Packages: []string{"curl-devel"},
DefaultPackageConfig: projectconfig.PackageConfig{
Publish: projectconfig.PackagePublishConfig{Channel: "devel"},
},
},
}
testEnv.Config.Components["curl"] = projectconfig.ComponentConfig{Name: "curl"}
testEnv.Config.Components["wget"] = projectconfig.ComponentConfig{
Name: "wget",
DefaultPackageConfig: projectconfig.PackageConfig{
Publish: projectconfig.PackagePublishConfig{Channel: "wget-default"},
},
}

results, err := pkgcmds.ListPackages(testEnv.Env, &pkgcmds.ListPackageOptions{
All: true,
SynthesizeDebugPackages: true,
})

require.NoError(t, err)

byName := make(map[string]pkgcmds.PackageListResult, len(results))
for _, result := range results {
byName[result.PackageName] = result
}

// Original package present.
require.Contains(t, byName, "curl-devel")
assert.Equal(t, "devel", byName["curl-devel"].Channel)

// '-debuginfo' synthesized for each reported package on the parallel debug channel.
require.Contains(t, byName, "curl-devel-debuginfo")
assert.Equal(t, "devel-debuginfo", byName["curl-devel-debuginfo"].Channel)
assert.Equal(t, "devel-packages", byName["curl-devel-debuginfo"].Group)

// '-debugsource' synthesized for each component, with its channel resolved through the
// component → project default chain and suffixed onto the parallel debug channel.
require.Contains(t, byName, "curl-debugsource")
assert.Equal(t, "default-channel-debuginfo", byName["curl-debugsource"].Channel)
assert.Equal(t, "curl", byName["curl-debugsource"].Component)
assert.Empty(t, byName["curl-debugsource"].Group)

require.Contains(t, byName, "wget-debugsource")
assert.Equal(t, "wget-default-debuginfo", byName["wget-debugsource"].Channel)
assert.Equal(t, "wget", byName["wget-debugsource"].Component)
}

func TestListPackages_SynthesizeDebugPackages_SkipsExisting(t *testing.T) {
testEnv := testutils.NewTestEnv(t)
testEnv.Config.PackageGroups = map[string]projectconfig.PackageGroupConfig{
"g": {
Packages: []string{"curl", "curl-debuginfo"},
DefaultPackageConfig: projectconfig.PackageConfig{
Publish: projectconfig.PackagePublishConfig{Channel: "real"},
},
},
}
testEnv.Config.Components["curl"] = projectconfig.ComponentConfig{
Name: "curl",
Packages: map[string]projectconfig.PackageConfig{
"curl-debugsource": {Publish: projectconfig.PackagePublishConfig{Channel: "explicit"}},
},
}

results, err := pkgcmds.ListPackages(testEnv.Env, &pkgcmds.ListPackageOptions{
All: true,
SynthesizeDebugPackages: true,
})

require.NoError(t, err)

// No duplicate entries — real config wins for both -debuginfo and -debugsource.
seen := make(map[string]int)
for _, result := range results {
seen[result.PackageName]++
}

assert.Equal(t, 1, seen["curl-debuginfo"])
assert.Equal(t, 1, seen["curl-debugsource"])
// The doubled-suffix names must NOT be synthesized — guard against recursive synthesis
// when a real '-debuginfo' / '-debugsource' is already in the listed set.
assert.NotContains(t, seen, "curl-debuginfo-debuginfo")
assert.NotContains(t, seen, "curl-debugsource-debuginfo")
// The pre-existing curl-debuginfo keeps its real channel, not a synthesized override.
for _, result := range results {
if result.PackageName == "curl-debuginfo" {
assert.Equal(t, "real", result.Channel)
}

if result.PackageName == "curl-debugsource" {
assert.Equal(t, "explicit", result.Channel)
}
}
}

func TestListPackages_SynthesizeDebugPackages_ByName(t *testing.T) {
testEnv := testutils.NewTestEnv(t)
testEnv.Config.DefaultPackageConfig = projectconfig.PackageConfig{
Publish: projectconfig.PackagePublishConfig{Channel: "default-channel"},
}
testEnv.Config.PackageGroups = map[string]projectconfig.PackageGroupConfig{
"devel-packages": {
Packages: []string{"curl-devel"},
DefaultPackageConfig: projectconfig.PackageConfig{
Publish: projectconfig.PackagePublishConfig{Channel: "devel"},
},
},
}
testEnv.Config.Components["curl"] = projectconfig.ComponentConfig{Name: "curl"}

// The headline CLI path: -p PKG with the synthesize flag.
results, err := pkgcmds.ListPackages(testEnv.Env, &pkgcmds.ListPackageOptions{
PackageNames: []string{"curl-devel"},
SynthesizeDebugPackages: true,
})

require.NoError(t, err)

byName := make(map[string]pkgcmds.PackageListResult, len(results))
for _, result := range results {
byName[result.PackageName] = result
}

require.Contains(t, byName, "curl-devel")
assert.Equal(t, "devel", byName["curl-devel"].Channel)

// '-debuginfo' synthesized on the parallel debug channel for the requested package.
require.Contains(t, byName, "curl-devel-debuginfo")
assert.Equal(t, "devel-debuginfo", byName["curl-devel-debuginfo"].Channel)

// '-debugsource' synthesized for every component in the project, regardless of which
// packages were requested. Channel resolves to the project default + '-debuginfo'.
require.Contains(t, byName, "curl-debugsource")
assert.Equal(t, "default-channel-debuginfo", byName["curl-debugsource"].Channel)
}

func TestListPackages_SynthesizeDebugPackages_ChannelSuffixRules(t *testing.T) {
testEnv := testutils.NewTestEnv(t)
testEnv.Config.PackageGroups = map[string]projectconfig.PackageGroupConfig{
"none-grp": {
Packages: []string{"pkg-none"},
DefaultPackageConfig: projectconfig.PackageConfig{
Publish: projectconfig.PackagePublishConfig{Channel: "none"},
},
},
"empty-grp": {
// No DefaultPackageConfig → resolves to "" (no channel configured).
Packages: []string{"pkg-empty"},
},
"already-grp": {
Packages: []string{"pkg-already"},
DefaultPackageConfig: projectconfig.PackageConfig{
Publish: projectconfig.PackagePublishConfig{Channel: "ms-debuginfo"},
},
},
}

results, err := pkgcmds.ListPackages(testEnv.Env, &pkgcmds.ListPackageOptions{
All: true,
SynthesizeDebugPackages: true,
})

require.NoError(t, err)

byName := make(map[string]pkgcmds.PackageListResult, len(results))
for _, result := range results {
byName[result.PackageName] = result
}

// "none" passes through unchanged — debug artifacts inherit the do-not-publish intent.
assert.Equal(t, "none", byName["pkg-none-debuginfo"].Channel)
// Empty passes through unchanged — downstream applies the configured default.
assert.Empty(t, byName["pkg-empty-debuginfo"].Channel)
// Already-suffixed channels are not doubled.
assert.Equal(t, "ms-debuginfo", byName["pkg-already-debuginfo"].Channel)
}
5 changes: 5 additions & 0 deletions scenario/__snapshots__/TestMCPServerMode_1.snap.json
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,11 @@
"description": "only enable minimal output",
"type": "boolean"
},
"synthesize-debug-packages": {
"default": false,
"description": "Also synthesize '-debuginfo' packages (per reported package) and '-debugsource' packages (per component)",
"type": "boolean"
},
"verbose": {
"default": false,
"description": "enable verbose output",
Expand Down
Loading