-
Notifications
You must be signed in to change notification settings - Fork 971
Expose through extension methods maui build and launch arguments #19572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c8962f2
e17dc45
548a443
0775fdd
8963292
0bf8a93
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Aspire.Hosting.ApplicationModel; | ||
|
|
||
| namespace Aspire.Hosting.Maui.Annotations; | ||
|
|
||
| /// <summary> | ||
| /// Annotation that registers a callback used to inspect or modify the arguments passed to | ||
| /// <c>dotnet</c> for a particular <see cref="MauiBuildStep"/> of a MAUI platform resource. | ||
| /// </summary> | ||
| internal sealed class MauiBuildArgumentsCallbackAnnotation( | ||
| MauiBuildStep step, | ||
| Func<MauiBuildArgumentsCallbackContext, Task> callback) : IResourceAnnotation | ||
| { | ||
| /// <summary> | ||
| /// Gets the build step this callback participates in. | ||
| /// </summary> | ||
| public MauiBuildStep Step { get; } = step; | ||
|
|
||
| /// <summary> | ||
| /// Gets the callback invoked with the mutable argument list for <see cref="Step"/>. | ||
| /// </summary> | ||
| public Func<MauiBuildArgumentsCallbackContext, Task> Callback { get; } = callback; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Aspire.Hosting.ApplicationModel; | ||
|
|
||
| namespace Aspire.Hosting.Maui.Annotations; | ||
|
|
||
| /// <summary> | ||
| /// Captures the pristine <see cref="ProjectLaunchArgsOverrideAnnotation"/> arguments before any | ||
| /// launch-argument callbacks run, so re-applying the callbacks stays idempotent across restarts. | ||
| /// </summary> | ||
| internal sealed class MauiLaunchArgsBaselineAnnotation(IReadOnlyList<string> arguments) : IResourceAnnotation | ||
| { | ||
| /// <summary> | ||
| /// Gets the untouched launch arguments captured before the first callback pass. | ||
| /// </summary> | ||
| public IReadOnlyList<string> Arguments { get; } = arguments.ToArray(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,10 +40,24 @@ internal class MauiBuildQueueEventSubscriber( | |
| /// <inheritdoc/> | ||
| public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken) | ||
| { | ||
| // Launch callbacks must run before DCP renders the executables. DCP reads | ||
| // ProjectLaunchArgsOverrideAnnotation during PrepareProjectExecutables and bakes the | ||
| // resulting launch command into the DCP Executable, which happens after BeforeStartEvent | ||
| // but before BeforeResourceStartedEvent. Applying launch edits at BeforeResourceStartedEvent | ||
| // would therefore be ignored, so they are applied here instead. | ||
| eventing.Subscribe<BeforeStartEvent>(OnBeforeStartAsync); | ||
| eventing.Subscribe<BeforeResourceStartedEvent>(OnBeforeResourceStartedAsync); | ||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| private async Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken cancellationToken) | ||
| { | ||
| foreach (var resource in @event.Model.Resources.OfType<IMauiPlatformResource>()) | ||
| { | ||
| await ApplyLaunchArgumentCallbacksAsync(resource, cancellationToken).ConfigureAwait(false); | ||
| } | ||
| } | ||
|
|
||
| private async Task OnBeforeResourceStartedAsync(BeforeResourceStartedEvent @event, CancellationToken cancellationToken) | ||
| { | ||
| if (@event.Resource is not IMauiPlatformResource mauiResource) | ||
|
|
@@ -160,6 +174,9 @@ internal virtual async Task RunBuildAsync(IResource resource, ILogger logger, Ca | |
|
|
||
| args.AddRange(buildInfo.AdditionalBuildArguments); | ||
|
|
||
| // Allow consumers to inspect/modify the compile arguments. | ||
| var callbackContext = await ApplyBuildArgumentCallbacksAsync(resource, args, cancellationToken).ConfigureAwait(false); | ||
|
|
||
| var psi = new ProcessStartInfo("dotnet") | ||
| { | ||
| WorkingDirectory = buildInfo.WorkingDirectory, | ||
|
|
@@ -174,7 +191,10 @@ internal virtual async Task RunBuildAsync(IResource resource, ILogger logger, Ca | |
| psi.ArgumentList.Add(arg); | ||
| } | ||
|
|
||
| logger.LogInformation("Running: dotnet {Arguments}", string.Join(" ", args)); | ||
| // Redact any arguments the callback marked sensitive so signing passwords and similar secrets | ||
| // do not leak into the resource logs. The process itself still receives the real values. | ||
| var displayArgs = callbackContext?.GetRedactedArguments() ?? args; | ||
| logger.LogInformation("Running: dotnet {Arguments}", string.Join(" ", displayArgs)); | ||
|
|
||
| // Apply a timeout so that a hung build does not block the queue forever. | ||
| using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); | ||
|
|
@@ -221,6 +241,88 @@ internal virtual async Task RunBuildAsync(IResource resource, ILogger logger, Ca | |
| logger.LogInformation("Build succeeded for resource '{ResourceName}'.", resource.Name); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Invokes any registered <see cref="MauiBuildStep.Build"/> callbacks, letting consumers | ||
| /// mutate the compile <paramref name="arguments"/> in place. | ||
| /// </summary> | ||
| /// <returns> | ||
| /// The context the callbacks ran against (used to redact sensitive arguments from logs), or | ||
| /// <see langword="null"/> when no build callbacks are registered. | ||
| /// </returns> | ||
| private static async Task<MauiBuildArgumentsCallbackContext?> ApplyBuildArgumentCallbacksAsync( | ||
| IResource resource, IList<string> arguments, CancellationToken cancellationToken) | ||
| { | ||
| var callbacks = resource.Annotations | ||
| .OfType<MauiBuildArgumentsCallbackAnnotation>() | ||
| .Where(a => a.Step == MauiBuildStep.Build) | ||
| .ToArray(); | ||
|
|
||
| if (callbacks.Length == 0) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var context = new MauiBuildArgumentsCallbackContext(MauiBuildStep.Build, arguments, resource, cancellationToken); | ||
|
|
||
| foreach (var callback in callbacks) | ||
| { | ||
| await callback.Callback(context).ConfigureAwait(false); | ||
| } | ||
|
|
||
| return context; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Applies any registered <see cref="MauiBuildStep.Launch"/> callbacks to the resource's | ||
| /// <see cref="ProjectLaunchArgsOverrideAnnotation"/> before DCP renders it into the launch command. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// This runs during <see cref="BeforeStartEvent"/> — before DCP's <c>PrepareProjectExecutables</c> | ||
| /// reads the override annotation and bakes the launch command into the DCP executable. The override | ||
| /// annotation's arguments are immutable, so callbacks operate on a copy and the annotation is swapped | ||
| /// for one carrying the result. | ||
| /// </remarks> | ||
| private static async Task ApplyLaunchArgumentCallbacksAsync(IResource resource, CancellationToken cancellationToken) | ||
| { | ||
| var callbacks = resource.Annotations | ||
| .OfType<MauiBuildArgumentsCallbackAnnotation>() | ||
| .Where(a => a.Step == MauiBuildStep.Launch) | ||
| .ToArray(); | ||
|
|
||
| if (callbacks.Length == 0) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (!resource.TryGetLastAnnotation<ProjectLaunchArgsOverrideAnnotation>(out var launchOverride)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // BeforeStartEvent can run more than once (for example across restarts). Capture the pristine | ||
| // override arguments on the first pass and always start from that baseline, otherwise each pass | ||
| // would re-append the callback edits to the already-edited override and accumulate them. | ||
| if (!resource.TryGetLastAnnotation<MauiLaunchArgsBaselineAnnotation>(out var baseline)) | ||
| { | ||
| baseline = new MauiLaunchArgsBaselineAnnotation(launchOverride.Arguments); | ||
| resource.Annotations.Add(baseline); | ||
| } | ||
|
|
||
| var arguments = new List<string>(baseline.Arguments); | ||
|
|
||
| var context = new MauiBuildArgumentsCallbackContext(MauiBuildStep.Launch, arguments, resource, cancellationToken); | ||
|
|
||
| foreach (var callback in callbacks) | ||
| { | ||
| await callback.Callback(context).ConfigureAwait(false); | ||
| } | ||
|
|
||
| // Swap the immutable annotation for one carrying the updated arguments; DCP reads the last | ||
| // override annotation when it renders the launch command during PrepareProjectExecutables. | ||
| resource.Annotations.Remove(launchOverride); | ||
| resource.Annotations.Add(new ProjectLaunchArgsOverrideAnnotation(arguments, launchOverride.LeadingResourceArgumentToRemove)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This callback edits only |
||
| } | ||
|
|
||
| private static async Task PipeOutputAsync(System.IO.StreamReader reader, ILogger logger, LogLevel level, CancellationToken cancellationToken) | ||
| { | ||
| try | ||
|
|
@@ -272,7 +374,7 @@ private static void TryKillProcess(Process process, ILogger logger) | |
| /// Without this guard a restart could match on a stale snapshot. | ||
| /// </para> | ||
| /// <para> | ||
| /// Including "Running" in the predicate is intentional: the pre-build step already compiled | ||
| /// Including "Running" in the predicate is intentional: the build step already compiled | ||
| /// the project for the same configuration that DCP will pass to the launch command, and DCP's | ||
| /// <c>dotnet build --no-restore /t:Run -p:NoBuild=true</c> launch command is configured not to | ||
| /// restore or build. Waiting for a terminal state would hold the semaphore for the entire app | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Aspire.Hosting.ApplicationModel; | ||
|
|
||
| namespace Aspire.Hosting.Maui; | ||
|
|
||
| /// <summary> | ||
| /// Context passed to build-argument callbacks registered with | ||
| /// <see cref="MauiBuildArgumentsExtensions.WithMauiBuildArguments{T}(IResourceBuilder{T}, Func{MauiBuildArgumentsCallbackContext, System.Threading.Tasks.Task})"/> | ||
| /// and | ||
| /// <see cref="MauiBuildArgumentsExtensions.WithMauiLaunchArguments{T}(IResourceBuilder{T}, Func{MauiBuildArgumentsCallbackContext, System.Threading.Tasks.Task})"/>. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Mutate the arguments in place to add, remove, or replace the arguments that will be | ||
| /// passed to <c>dotnet</c> for the <see cref="Step"/> this callback is registered for. For arguments | ||
| /// that carry secrets (for example an MSBuild property holding a signing password), add them with | ||
| /// <see cref="AddArgument(string, bool)"/> so their values are redacted from the arguments the | ||
| /// build pipeline writes to the resource logs. | ||
| /// </remarks> | ||
| [AspireExport(ExposeProperties = true)] | ||
|
|
||
| public sealed class MauiBuildArgumentsCallbackContext | ||
| { | ||
| // Tracks the exact argument strings added via AddSensitiveArgument so the build pipeline can | ||
| // redact them before logging. Ordinal comparison because these are literal command-line tokens. | ||
| private readonly HashSet<string> _sensitiveArguments = new(StringComparer.Ordinal); | ||
|
|
||
| private readonly IList<string> _arguments; | ||
|
|
||
|
|
||
| internal MauiBuildArgumentsCallbackContext( | ||
| MauiBuildStep step, | ||
| IList<string> arguments, | ||
| IResource resource, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| _arguments = arguments; | ||
| Step = step; | ||
| Resource = resource; | ||
| CancellationToken = cancellationToken; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the build step this callback is participating in. | ||
| /// </summary> | ||
| public MauiBuildStep Step { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets the MAUI platform resource the arguments apply to. | ||
| /// </summary> | ||
| public IResource Resource { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets a token that is cancelled if the resource start is cancelled. | ||
| /// </summary> | ||
| public CancellationToken CancellationToken { get; } | ||
|
|
||
| /// <summary> | ||
| /// Appends an argument whose value is sensitive (for example <c>-p:AndroidSigningKeyPass=…</c>). | ||
| /// </summary> | ||
| /// <param name="argument">The full argument to add.</param> | ||
| /// <param name="isSensitive"></param> | ||
|
Comment on lines
+57
to
+61
|
||
| /// <remarks> | ||
| /// The argument is passed to <c>dotnet</c> verbatim so the build and launch still work, but the MAUI | ||
| /// build pipeline replaces its value with a placeholder in the arguments it logs to the resource | ||
| /// output. Launch-step arguments are additionally masked by the dashboard's command-line display. | ||
| /// </remarks> | ||
| public void AddArgument(string argument, bool isSensitive = false) | ||
|
|
||
| { | ||
| ArgumentNullException.ThrowIfNull(argument); | ||
|
|
||
| _arguments.Add(argument); | ||
| if (isSensitive) | ||
| { | ||
| _sensitiveArguments.Add(argument); | ||
| } | ||
|
Comment on lines
+72
to
+75
|
||
| } | ||
|
|
||
| /// <summary> | ||
| /// Produces a display-safe rendering of the arguments with values added through | ||
| /// <see cref="AddArgument(string, bool)"/> replaced by a redaction placeholder. | ||
| /// </summary> | ||
| internal IEnumerable<string> GetRedactedArguments() | ||
| => _sensitiveArguments.Count == 0 | ||
| ? _arguments | ||
| : _arguments.Select(argument => _sensitiveArguments.Contains(argument) ? "[REDACTED]" : argument); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.