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
56 changes: 33 additions & 23 deletions src/Aspire.Hosting.Dotnet/DotnetProjectHostingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,17 +127,14 @@ public static IResourceBuilder<DotnetProjectResource> AddDotnetProject(this IDis
.WithIconName("CodeCsRectangle")
.WithProjectDefaults(options);

// Build the `dotnet run` command line for a non-debug launch of a DotnetProjectResource:
// Declare the default `dotnet run` invocation separately from the program arguments so a later
// WithLaunchToolArgs call replaces it instead of being prepended to it:
// dotnet run --project <proj> [--no-build] [--configuration <cfg>] --no-launch-profile OR
// dotnet run --file <app.cs> --no-cache [--no-build] [--configuration <cfg>] --no-launch-profile
resource.WithArgs(ctx =>
resource.WithLaunchToolArgs(ctx =>
{
// 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.
// 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)
{
return;
}
Expand Down Expand Up @@ -169,26 +166,39 @@ public static IResourceBuilder<DotnetProjectResource> AddDotnetProject(this IDis
// and must take priority. WithProjectDefaults materializes the profile's environment manually.
ctx.Args.Add("--no-launch-profile");

// The launch profile's command line args are still applied here (run mode), after a `--`
// separator so they're passed to the app, matching the ProjectResource launch behavior.
if (builder.ExecutionContext.IsRunMode && !options.ExcludeLaunchProfile)
if (GetLaunchProfileArguments(ctx.Resource).Count > 0)
{
ctx.Args.Add("--");
}
}, ownedByLaunchConfigurationType: KnownLaunchConfigurationTypes.Project);

// Launch-profile command-line arguments belong to the program, not the replaceable tool invocation.
// Keeping them in the ordinary segment preserves them when a caller supplies a custom launch tool.
resource.WithArgs(ctx =>
{
foreach (var arg in GetLaunchProfileArguments(ctx.Resource))
{
var launchProfile = ctx.Resource.GetEffectiveLaunchProfile()?.LaunchProfile;
if (launchProfile is not null && !string.IsNullOrWhiteSpace(launchProfile.CommandLineArgs))
{
var launchProfileArgs = CommandLineArgsParser.Parse(launchProfile.CommandLineArgs);
if (launchProfileArgs.Count > 0)
{
ctx.Args.Add("--");
foreach (var arg in launchProfileArgs)
{
ctx.Args.Add(arg);
}
}
}
ctx.Args.Add(arg);
}
});

List<string> GetLaunchProfileArguments(IResource resource)
{
// Project launch configurations carry the selected launch profile, so the IDE applies its command-line arguments.
if (!builder.ExecutionContext.IsRunMode
|| options.ExcludeLaunchProfile
|| (resource.SupportsDebugging(builder.Configuration, out var debugAnnotation)
&& debugAnnotation.LaunchConfigurationType is KnownLaunchConfigurationTypes.Project))
{
return [];
}

var launchProfile = resource.GetEffectiveLaunchProfile()?.LaunchProfile;
return launchProfile is not null && !string.IsNullOrWhiteSpace(launchProfile.CommandLineArgs)
? CommandLineArgsParser.Parse(launchProfile.CommandLineArgs)
: [];
}

resource.OnBeforeResourceStarted((r, e, ct) =>
{
var projectPath = projectMetadata.ProjectPath;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,31 +453,14 @@ await notificationService.PublishUpdateAsync(toolResource, s => s with
CreateNoWindow = true
};

// Build command-line arguments by directly invoking each annotation's Callback.
// We intentionally bypass ExecutionConfigurationBuilder.WithArgumentsConfig() here
// because it uses EvaluateOnceAsync which caches callback results. When the tool
// resource is reused across sequential EF commands (e.g., script then bundle),
// the cached BuildToolExecArguments callback does not re-populate the shared
// callbackContext.Args list, so later annotations (the per-command EF args) run
// against an empty list.
if (toolResource.TryGetAnnotationsOfType<CommandLineArgsCallbackAnnotation>(out var cmdLineAnnotations))
var toolArguments = await GatherToolArgumentsAsync(
toolResource,
executionContext,
context.Logger,
context.CancellationToken).ConfigureAwait(false);
foreach (var argument in toolArguments)
{
IList<object> args = [];
var callbackContext = new CommandLineArgsCallbackContext(args, toolResource, context.CancellationToken)
{
Logger = context.Logger,
ExecutionContext = executionContext
};

foreach (var ann in cmdLineAnnotations)
{
await ann.Callback(callbackContext).ConfigureAwait(false);
}

foreach (var arg in callbackContext.Args)
{
startInfo.ArgumentList.Add(arg.ToString()!);
}
startInfo.ArgumentList.Add(argument.ToString()!);
}

foreach (var kvp in GetToolEnvironmentVariables(executionConfiguration, executionContext.IsPublishMode))
Expand Down Expand Up @@ -608,6 +591,21 @@ await notificationService.PublishUpdateAsync(toolResource, s => s with
}
}

/// <summary>
/// Composes the EF tool arguments without cached callback results so sequential commands remain independent.
/// </summary>
internal static ValueTask<List<object>> GatherToolArgumentsAsync(
DotnetToolResource toolResource,
DistributedApplicationExecutionContext executionContext,
ILogger logger,
CancellationToken cancellationToken)
{
return toolResource.GatherArgumentValuesWithoutCachingAsync(
executionContext,
logger,
cancellationToken);
}

// Selects the environment variables to apply to the EF tool process. In publish mode connection
// string references resolve to manifest placeholder expressions (e.g. "{postgres.connectionString}")
// rather than real values because the target resources aren't provisioned yet. Passing such a
Expand Down
155 changes: 72 additions & 83 deletions src/Aspire.Hosting.Go/GoHostingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,86 +95,64 @@ public static IResourceBuilder<GoAppResource> AddGoApp(
? argsAnnotation.Args
: [];

var hasDelve = ctx.Resource.TryGetLastAnnotation<GoDelveServerAnnotation>(out var delveAnnotation);
var pkg = ctx.Resource.TryGetLastAnnotation<GoPackagePathAnnotation>(out var pkgAnnotation)
? pkgAnnotation.PackagePath
: ".";

if (hasDelve)
if (!ctx.Resource.TryGetLastAnnotation<GoDelveServerAnnotation>(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=...] <pkg> [-- 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] <pkg>` 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=...] <pkg> [-- 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<GoPackagePathAnnotation>(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=...] <pkg> [args]
ctx.Args.Add("run");

if (ctx.Resource.TryGetLastAnnotation<GoRaceDetectorAnnotation>(out _))
{
ctx.Args.Add("-race");
}

if (ctx.Resource.TryGetLastAnnotation<GoBuildTagsAnnotation>(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<GoLdFlagsAnnotation>(out var ldFlagsAnnotation))
{
ctx.Args.Add($"-ldflags={ldFlagsAnnotation.Flags}");
}
ctx.Args.Add("debug");
if (delveAnnotation.ContinueOnStart)
{
ctx.Args.Add("--continue");
}

if (ctx.Resource.TryGetLastAnnotation<GoGcFlagsAnnotation>(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);
Expand Down Expand Up @@ -775,37 +753,48 @@ internal static IResourceBuilder<T> WithVSCodeDebugging<T>(this IResourceBuilder
BuildFlags = buildFlags.Length > 0 ? buildFlags : null
};
},
"go",
static ctx =>
"go")
.WithLaunchToolArgs(static ctx =>
{
// The executable resource normally starts as:
// go run [-race] [-tags=...] [-ldflags=...] [-gcflags=...] <pkg> [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 <pkg> is the tool invocation: in IDE mode VS Code's Go debugger
// performs it via program/buildFlags, so it is not passed to the launched program.
if (ctx.Resource.HasAnnotationOfType<GoDelveServerAnnotation>())
{
// 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");

while (ctx.Args is [string arg, ..] && IsGoRunBuildFlag(arg))
if (ctx.Resource.TryGetLastAnnotation<GoRaceDetectorAnnotation>(out _))
{
ctx.Args.RemoveAt(0);
ctx.Args.Add("-race");
}

if (ctx.Args.Count > 0)
if (ctx.Resource.TryGetLastAnnotation<GoBuildTagsAnnotation>(out var tagsAnnotation))
{
ctx.Args.RemoveAt(0);
ctx.Args.Add($"-tags={string.Join(",", tagsAnnotation.Tags)}");
}

if (ctx.Resource.TryGetLastAnnotation<GoLdFlagsAnnotation>(out var ldFlagsAnnotation))
{
ctx.Args.Add($"-ldflags={ldFlagsAnnotation.Flags}");
}
});
}

private static bool IsGoRunBuildFlag(string arg) =>
arg == "-race" ||
arg.StartsWith("-tags=", StringComparison.Ordinal) ||
arg.StartsWith("-ldflags=", StringComparison.Ordinal) ||
arg.StartsWith("-gcflags=", StringComparison.Ordinal);
if (ctx.Resource.TryGetLastAnnotation<GoGcFlagsAnnotation>(out var gcFlagsAnnotation))
{
ctx.Args.Add($"-gcflags={gcFlagsAnnotation.Flags}");
}

ctx.Args.Add(ctx.Resource.TryGetLastAnnotation<GoPackagePathAnnotation>(out var pkgAnnotation)
? pkgAnnotation.PackagePath
: ".");
},
ownedByLaunchConfigurationType: "go");
}

/// <summary>
/// Builds the <c>go build</c> command for the generated Dockerfile, propagating any
Expand Down
Loading
Loading