From 1b268b6952fa370aba2cf91bb8f8d5697f845472 Mon Sep 17 00:00:00 2001 From: Karol Zadora-Przylecki Date: Tue, 4 Aug 2026 10:52:57 -0700 Subject: [PATCH 01/13] Separate regular arguments from launch tool ("entrypoint") arguments Fixes https://github.com/microsoft/aspire/issues/18929 --- .../DotnetProjectHostingExtensions.cs | 4 +- src/Aspire.Hosting.Go/GoHostingExtensions.cs | 138 ++++++------- .../PythonAppResourceBuilderExtensions.cs | 44 +--- ...ArgumentsExecutionConfigurationGatherer.cs | 46 +++++ .../DebugSupportExtensions.cs | 16 ++ .../EntrypointArgsCallbackAnnotation.cs | 98 +++++++++ src/Aspire.Hosting/Dcp/DcpExecutor.cs | 8 + src/Aspire.Hosting/Dcp/ExecutableCreator.cs | 194 +++++++++++++----- .../ResourceBuilderExtensions.cs | 86 +++++--- .../SupportsDebuggingAnnotation.cs | 31 +-- .../DotnetProjectResourceTests.cs | 17 +- .../Aspire.Hosting.Go.Tests/AddGoAppTests.cs | 39 +++- .../AddPythonAppTests.cs | 30 ++- .../Dcp/DcpExecutorTests.cs | 128 ++++++++---- ...ExecutableResourceBuilderExtensionTests.cs | 156 +++++++++++--- 15 files changed, 720 insertions(+), 315 deletions(-) create mode 100644 src/Aspire.Hosting/ApplicationModel/EntrypointArgsCallbackAnnotation.cs diff --git a/src/Aspire.Hosting.Dotnet/DotnetProjectHostingExtensions.cs b/src/Aspire.Hosting.Dotnet/DotnetProjectHostingExtensions.cs index 412281a10bd..9db14a3de43 100644 --- a/src/Aspire.Hosting.Dotnet/DotnetProjectHostingExtensions.cs +++ b/src/Aspire.Hosting.Dotnet/DotnetProjectHostingExtensions.cs @@ -134,10 +134,10 @@ public static IResourceBuilder AddDotnetProject(this IDis { // Mirrors the fallback rule in Dcp/ExecutableCreator: // a Process fallback is offered for the plain executable UNLESS - // the launch configuration is "project", OR the configuration rewrites the arguments for debugging. + // the launch configuration is "project", OR the launch configuration owns the entrypoint arguments. // For any other active annotation a fallback IS offered and we need to construct the args here. if (ctx.Resource.SupportsDebugging(builder.Configuration, out var debugAnnotation) - && (debugAnnotation.LaunchConfigurationType is KnownLaunchConfigurationTypes.Project || debugAnnotation.RewritesArgumentsForDebugging)) + && (debugAnnotation.LaunchConfigurationType is KnownLaunchConfigurationTypes.Project || ctx.Resource.OwnsEntrypointArguments(debugAnnotation))) { return; } diff --git a/src/Aspire.Hosting.Go/GoHostingExtensions.cs b/src/Aspire.Hosting.Go/GoHostingExtensions.cs index e1d298d7f1e..1429c85112d 100644 --- a/src/Aspire.Hosting.Go/GoHostingExtensions.cs +++ b/src/Aspire.Hosting.Go/GoHostingExtensions.cs @@ -95,60 +95,36 @@ public static IResourceBuilder AddGoApp( ? argsAnnotation.Args : []; - var hasDelve = ctx.Resource.TryGetLastAnnotation(out var delveAnnotation); - var pkg = ctx.Resource.TryGetLastAnnotation(out var pkgAnnotation) - ? pkgAnnotation.PackagePath - : "."; - - if (hasDelve) + if (!ctx.Resource.TryGetLastAnnotation(out var delveAnnotation)) { - // Delve debug mode — global flags MUST precede the subcommand per the Delve CLI: - // dlv --headless=true --listen=127.0.0.1:PORT --api-version=2 debug [--continue] [--build-flags=...] [-- args] - // See: https://www.jetbrains.com/help/go/attach-to-running-go-processes-with-debugger.html - ctx.Args.Add("--headless=true"); - ctx.Args.Add($"--listen=127.0.0.1:{delveAnnotation!.Port}"); - ctx.Args.Add("--api-version=2"); - if (delveAnnotation.AcceptMultiClient) - { - ctx.Args.Add("--accept-multiclient"); - } - if (delveAnnotation.OnlySameUser.HasValue) - { - ctx.Args.Add($"--only-same-user={delveAnnotation.OnlySameUser.Value.ToString().ToLowerInvariant()}"); - } - if (delveAnnotation.Log) - { - ctx.Args.Add("--log"); - if (!string.IsNullOrEmpty(delveAnnotation.LogOutput)) - { - ctx.Args.Add($"--log-output={delveAnnotation.LogOutput}"); - } - } - - ctx.Args.Add("debug"); - if (delveAnnotation.ContinueOnStart) + // Normal run mode. The `go run [build flags] ` prefix is contributed as entrypoint + // arguments by WithVSCodeDebugging(), so only the program's own arguments belong here. + foreach (var arg in programArgs) { - ctx.Args.Add("--continue"); + ctx.Args.Add(arg); } - var buildFlags = BuildFlagsString(ctx.Resource); - if (buildFlags.Length > 0) - { - ctx.Args.Add($"--build-flags={buildFlags}"); - } + return; + } - ctx.Args.Add(pkg); + // Delve debug mode — global flags MUST precede the subcommand per the Delve CLI: + // dlv --headless=true --listen=127.0.0.1:PORT --api-version=2 debug [--continue] [--build-flags=...] [-- args] + // See: https://www.jetbrains.com/help/go/attach-to-running-go-processes-with-debugger.html + // WithDelveServer removes the debug launch annotation, so this whole command line is a plain + // process invocation and stays in the regular argument callback. + var pkg = ctx.Resource.TryGetLastAnnotation(out var pkgAnnotation) + ? pkgAnnotation.PackagePath + : "."; - if (programArgs.Length > 0) - { - ctx.Args.Add("--"); - foreach (var arg in programArgs) - { - ctx.Args.Add(arg); - } - } + ctx.Args.Add("--headless=true"); + ctx.Args.Add($"--listen=127.0.0.1:{delveAnnotation.Port}"); + ctx.Args.Add("--api-version=2"); + if (delveAnnotation.AcceptMultiClient) + { + ctx.Args.Add("--accept-multiclient"); } else + if (delveAnnotation.OnlySameUser.HasValue) { // Normal run mode: go run [-race] [-tags=...] [-ldflags=...] [-gcflags=...] [args] ctx.Args.Add("run"); @@ -159,22 +135,34 @@ public static IResourceBuilder AddGoApp( } if (ctx.Resource.TryGetLastAnnotation(out var tagsAnnotation)) + ctx.Args.Add($"--only-same-user={delveAnnotation.OnlySameUser.Value.ToString().ToLowerInvariant()}"); + } + if (delveAnnotation.Log) + { + ctx.Args.Add("--log"); + if (!string.IsNullOrEmpty(delveAnnotation.LogOutput)) { - ctx.Args.Add($"-tags={string.Join(",", tagsAnnotation.Tags)}"); + ctx.Args.Add($"--log-output={delveAnnotation.LogOutput}"); } + } - if (ctx.Resource.TryGetLastAnnotation(out var ldFlagsAnnotation)) - { - ctx.Args.Add($"-ldflags={ldFlagsAnnotation.Flags}"); - } + ctx.Args.Add("debug"); + if (delveAnnotation.ContinueOnStart) + { + ctx.Args.Add("--continue"); + } - if (ctx.Resource.TryGetLastAnnotation(out var gcFlagsAnnotation)) - { - ctx.Args.Add($"-gcflags={gcFlagsAnnotation.Flags}"); - } + var delveBuildFlags = BuildFlagsString(ctx.Resource); + if (delveBuildFlags.Length > 0) + { + ctx.Args.Add($"--build-flags={delveBuildFlags}"); + } - ctx.Args.Add(pkg); + ctx.Args.Add(pkg); + if (programArgs.Length > 0) + { + ctx.Args.Add("--"); foreach (var arg in programArgs) { ctx.Args.Add(arg); @@ -780,33 +768,43 @@ internal static IResourceBuilder WithVSCodeDebugging(this IResourceBuilder { // The executable resource normally starts as: // go run [-race] [-tags=...] [-ldflags=...] [-gcflags=...] [app args] - // In IDE mode VS Code's Go debugger owns the tool/build/package portion via - // program/buildFlags, so only the user program arguments should remain. - if (ctx.Args is not [string runCommand, ..] || runCommand != "run") + // Everything up to and including is the entrypoint: in IDE mode VS Code's Go debugger owns it + // via program/buildFlags, so it is not passed to the launched program. + if (ctx.Resource.HasAnnotationOfType()) { + // WithDelveServer replaces the whole command line with a headless `dlv debug ...` invocation and + // removes the debug launch annotation, so there is no `go run` prefix to contribute. return; } - ctx.Args.RemoveAt(0); + ctx.Args.Add("run"); + + if (ctx.Resource.TryGetLastAnnotation(out _)) + { + ctx.Args.Add("-race"); + } + + if (ctx.Resource.TryGetLastAnnotation(out var tagsAnnotation)) + { + ctx.Args.Add($"-tags={string.Join(",", tagsAnnotation.Tags)}"); + } - while (ctx.Args is [string arg, ..] && IsGoRunBuildFlag(arg)) + if (ctx.Resource.TryGetLastAnnotation(out var ldFlagsAnnotation)) { - ctx.Args.RemoveAt(0); + ctx.Args.Add($"-ldflags={ldFlagsAnnotation.Flags}"); } - if (ctx.Args.Count > 0) + if (ctx.Resource.TryGetLastAnnotation(out var gcFlagsAnnotation)) { - ctx.Args.RemoveAt(0); + ctx.Args.Add($"-gcflags={gcFlagsAnnotation.Flags}"); } + + ctx.Args.Add(ctx.Resource.TryGetLastAnnotation(out var pkgAnnotation) + ? pkgAnnotation.PackagePath + : "."); }); } - private static bool IsGoRunBuildFlag(string arg) => - arg == "-race" || - arg.StartsWith("-tags=", StringComparison.Ordinal) || - arg.StartsWith("-ldflags=", StringComparison.Ordinal) || - arg.StartsWith("-gcflags=", StringComparison.Ordinal); - /// /// Builds the go build command for the generated Dockerfile, propagating any /// build-time flags that were set on the resource via . diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index 949ea8b8e0e..7fe70758328 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -993,37 +993,10 @@ public static IResourceBuilder WithDebugging( WorkingDirectory = workingDirectory }; }, - "python", - static ctx => - { - // Remove entrypoint-specific arguments that VS Code will handle. - // We need to verify the annotation to ensure we remove the correct args. - if (!ctx.Resource.TryGetLastAnnotation(out var annotation)) - { - return; - } - - // For Module type: remove "-m" and module name (2 args) - if (annotation.Type == EntrypointType.Module) - { - if (ctx.Args is [string arg0, string arg1, ..] && - arg0 == "-m" && - arg1 == annotation.Entrypoint) - { - ctx.Args.RemoveAt(0); // Remove "-m" - ctx.Args.RemoveAt(0); // Remove module name - } - } - // For Script type: remove script path (1 arg) - else if (annotation.Type == EntrypointType.Script) - { - if (ctx.Args is [string arg0, ..] && - arg0 == annotation.Entrypoint) - { - ctx.Args.RemoveAt(0); // Remove script path - } - } - }); + // The entrypoint arguments (`-m ` / `