diff --git a/docs/user/reference/cli/azldev_package_list.md b/docs/user/reference/cli/azldev_package_list.md index 144e9a27..250d1da3 100644 --- a/docs/user/reference/cli/azldev_package_list.md +++ b/docs/user/reference/cli/azldev_package_list.md @@ -42,9 +42,10 @@ azldev package list [package-name...] [flags] ### Options ``` - -a, --all-packages List all explicitly-configured binary packages - -h, --help help for list - -p, --package stringArray Package name to look up (repeatable) + -a, --all-packages List all explicitly-configured binary packages + -h, --help help for list + -p, --package stringArray Package name to look up (repeatable) + --synthesize-debug-packages Also synthesize '-debuginfo' packages (per reported package) and '-debugsource' packages (per component) ``` ### Options inherited from parent commands diff --git a/internal/app/azldev/cmds/pkg/list.go b/internal/app/azldev/cmds/pkg/list.go index e39b1dc6..43e0db61 100644 --- a/internal/app/azldev/cmds/pkg/list.go +++ b/internal/app/azldev/cmds/pkg/list.go @@ -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" @@ -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) { @@ -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) @@ -186,6 +198,16 @@ 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 @@ -193,3 +215,102 @@ func ListPackages(env *azldev.Env, options *ListPackageOptions) ([]PackageListRe 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, +) ([]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 + } + + 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 '-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. +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") +} diff --git a/internal/app/azldev/cmds/pkg/list_test.go b/internal/app/azldev/cmds/pkg/list_test.go index 62ba6c1d..21ec4fdb 100644 --- a/internal/app/azldev/cmds/pkg/list_test.go +++ b/internal/app/azldev/cmds/pkg/list_test.go @@ -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) +} diff --git a/scenario/__snapshots__/TestMCPServerMode_1.snap.json b/scenario/__snapshots__/TestMCPServerMode_1.snap.json index 47931a41..bd0bd5ea 100755 --- a/scenario/__snapshots__/TestMCPServerMode_1.snap.json +++ b/scenario/__snapshots__/TestMCPServerMode_1.snap.json @@ -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",