Add MAUI build queue to serialize platform builds#14945
Open
jfversluis wants to merge 14 commits intomainfrom
Open
Add MAUI build queue to serialize platform builds#14945jfversluis wants to merge 14 commits intomainfrom
jfversluis wants to merge 14 commits intomainfrom
Conversation
- Add ProjectLaunchArgsOverrideAnnotation for DCP launch arg override - Modify DcpExecutor to check for override before default run/watch args - Change MauiPlatformHelper to use dotnet build /t:Run instead of dotnet run - Fix NuGet RC package versions in MauiClient.csproj - Fix service discovery URI scheme (https -> https+http) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The MAUI SDK's Run target does not depend on Build, so using just /t:Run skipped the build step entirely. Mac Catalyst failed because the .app bundle didn't exist when 'open' tried to launch it, and iOS failed with 'app must be built before launch args can be computed'. Using /t:Build;Run ensures the Build target runs first, then the Run target launches the app. This also has the benefit of keeping the process alive (Mac Catalyst uses 'open -W', iOS uses 'mlaunch --wait-for-exit:true'), so resources show as Running instead of immediately going to Finished. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
MAUI uses single-project architecture where multiple platform resources (Android, iOS, Mac Catalyst, Windows) can reference the same .csproj. MSBuild cannot handle concurrent builds of the same project file. This adds a SemaphoreSlim-based build queue that: - Serializes builds per-project using MauiBuildQueueAnnotation - Shows 'Queued' state in the dashboard while waiting - Releases the semaphore when a resource reaches a terminal state - Handles cancellation and failure gracefully The annotation is added eagerly in AddMauiProject() to avoid race conditions when multiple platform resources start concurrently. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two improvements to the MAUI build queue: 1. Show 'Building' state in dashboard when a resource acquires the build lock, so users see Building → Running instead of immediately Running while MSBuild is still compiling. 2. Release the semaphore when a resource reaches 'Running' state (not just terminal states). iOS and Mac Catalyst stay Running indefinitely after launch, so waiting for terminal states would block the queue until the user manually stops the app. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous approach released the semaphore when the resource reached
'Running' state, but DCP sets Running immediately when the process
starts — while MSBuild is still compiling. This caused the queue to
release too early, allowing concurrent builds.
Now the subscriber watches the resource's log stream for MSBuild
completion output ('Build succeeded' or 'Build FAILED') to detect
when the build phase actually finishes. A terminal state fallback
ensures the semaphore is always released if the process exits before
producing build output.
Also shows 'Building' state in the dashboard while MSBuild compiles,
giving users clear visibility into: Queued → Building → Running.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of relying on log output parsing to detect when MSBuild finishes, run 'dotnet build' as a separate subprocess during BeforeResourceStartedEvent. This provides: - Reliable exit-code-based build completion detection (0 = success) - 'Building' state persists in the dashboard for the entire build (the event handler blocks DCP from starting the process) - Clean separation: Build runs in event handler, DCP only runs /t:Run The MauiBuildQueueEventSubscriber now spawns a 'dotnet build' process, pipes its stdout/stderr to the resource logger (visible in dashboard), and uses the exit code to determine success or failure. The semaphore is released in a finally block, ensuring the queue always progresses. MauiPlatformHelper now sets /t:Run (not /t:Build;Run) since the Build target is handled by the event subscriber. A MauiBuildInfoAnnotation carries the project path, TFM, and working directory to the subscriber. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fix #1: Drain stdout/stderr tasks in a finally block so they are always awaited, even when WaitForExitAsync throws on cancellation. PipeOutputAsync now catches OperationCanceledException internally. Fix #2: Resolve build configuration from the AppHost's AssemblyConfigurationAttribute (same as DcpExecutor) and pass it to MauiBuildInfoAnnotation, so the pre-build and DCP's Run target use the same configuration and MSBuild's incremental build is effective. Fix #3: Add 10-minute build timeout via CancellationTokenSource so a hung dotnet build process cannot block the semaphore forever. BuildTimeout is an internal settable property for testability. Fix #4: Add tests for MauiBuildInfoAnnotation (properties, nullable), missing MauiBuildQueueAnnotation (skip), resource restart (build same resource twice), and unexpected exception types (semaphore release). Total: 90 tests passing (18 build queue + 72 existing). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fix #1: Move _buildCompletions.TryRemove into finally block in TestableBuildQueueSubscriber so cancelled/failed builds clean up their TCS entries, preventing stale entries on resource restart. Fix #3: Add MissingBuildInfoAnnotation_SkipsBuildAndReleasesQueue test that uses the real MauiBuildQueueEventSubscriber to verify the RunBuildAsync warning-and-skip path when MauiBuildInfoAnnotation is absent, and confirms the semaphore is properly released. Fix #5: Remove IDisposable from MauiBuildQueueAnnotation — no other annotation in Aspire implements IDisposable and the semaphore is garbage-collected on app shutdown. Remove the Dispose test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fix test issues found in third review pass: - ThreeResources_ExecuteInSequence now asserts actual execution order (android → maccatalyst → ios), not just completion counts - SecondResource_ShowsQueuedState and SingleResource_ShowsBuildingState now properly await all event tasks before test cleanup - ResourcesFromDifferentProjects_RunConcurrently now verifies both tasks are actively building simultaneously (not pre-completed) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The root cause was timing: WithCommand in MauiPlatformHelper ran at app model build time, but the default lifecycle commands (start/stop/restart) are added later by DcpExecutor.EnsureRequiredAnnotations. This caused duplicate stop command annotations, making SingleOrDefault return null and the stop button to fail with 'command not available'. Fix: Move the stop command replacement into BeforeResourceStartedEvent handler in MauiBuildQueueEventSubscriber, which fires AFTER DcpExecutor has added the lifecycle commands. Key changes: - EnsureStopCommandReplaced() in subscriber replaces the default stop command annotation with one that calls CancelResource for Queued/Building and delegates to the original for Running state - MauiStopCommandReplacedAnnotation marker prevents double replacement - CancelResource() on MauiBuildQueueAnnotation with ConcurrentDictionary for per-resource CTS tracking - Graceful cancellation: catches OCE from our CTS, sets Exited state - semaphoreAcquired flag for correct semaphore release - Removed WithCommand from MauiPlatformHelper (was running too early) - 3 tests: cancel queued, cancel building, next proceeds after cancel 93 tests passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fix two bugs in the MAUI build queue stop command: 1. Resource name mismatch: The stop command used context.ResourceName (DCP-resolved name like 'mauiapp-maccatalyst-vqfdyejk') but the CTS dictionary is keyed by resource.Name (model name 'mauiapp-maccatalyst'). 2. OCE swallowing: The catch block swallowed OperationCanceledException instead of re-throwing, causing DCP to proceed with CreateExecutableAsync and start the resource despite cancellation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add stateAtCallTime parameter to ReleaseSemaphoreAfterLaunchAsync to skip the replayed snapshot from WatchAsync on resource restart, preventing premature semaphore release from stale state matches. - Implement IDisposable on MauiBuildQueueAnnotation to properly dispose the SemaphoreSlim when the annotation is cleaned up. - Document why 'Running' in the predicate is acceptable (pre-build already compiled; dotnet build /t:Run only does fast incremental check). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 14945Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 14945" |
Contributor
🎬 CLI E2E Test RecordingsThe following terminal recordings are available for commit
📹 Recordings uploaded automatically from CI run #22685752770 |
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a per-project build serialization mechanism for MAUI platform resources to avoid concurrent MSBuild invocations on the same .csproj, integrating with DCP launch argument generation and exposing queue/building states in the dashboard.
Changes:
- Introduces a MAUI build-queue event subscriber (semaphore-based) plus supporting annotations to serialize builds and enable queued/building UX + cancellation.
- Adds a new
ProjectLaunchArgsOverrideAnnotationand updates DCP to honor it (enablingdotnet build /t:Runfor MAUI). - Adds a dedicated MAUI build-queue test suite and documents the behavior in the MAUI hosting README.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/Aspire.Hosting.Maui/Lifecycle/MauiBuildQueueEventSubscriber.cs |
Core queue/semaphore logic, dashboard state updates, pre-build subprocess, queue-aware Stop command |
src/Aspire.Hosting.Maui/Annotations/MauiBuildQueueAnnotation.cs |
Per-project semaphore + per-resource cancellation CTS storage |
src/Aspire.Hosting.Maui/Annotations/MauiBuildInfoAnnotation.cs |
Build metadata used by the pre-build subprocess |
src/Aspire.Hosting.Maui/MauiProjectResourceExtensions.cs |
Ensures queue annotation is added up-front when creating MAUI project resources |
src/Aspire.Hosting.Maui/MauiPlatformHelper.cs |
Attaches build metadata + DCP launch override annotation for MAUI platform resources |
src/Aspire.Hosting.Maui/MauiHostingExtensions.cs |
Registers the new build-queue subscriber |
src/Aspire.Hosting/ApplicationModel/ProjectLaunchArgsOverrideAnnotation.cs |
New core annotation for overriding DCP project launch args |
src/Aspire.Hosting/Dcp/DcpExecutor.cs |
Implements the override annotation when constructing dotnet args for project executables |
tests/Aspire.Hosting.Maui.Tests/MauiBuildQueueTests.cs |
Unit tests for queue serialization, cancellation, failure/timeout, and concurrency behavior |
src/Aspire.Hosting.Maui/README.md |
Documents build queue behavior and architecture |
playground/AspireWithMaui/AspireWithMaui.MauiClient/MauiProgram.cs |
Updates sample HttpClient base address scheme |
playground/AspireWithMaui/AspireWithMaui.MauiClient/AspireWithMaui.MauiClient.csproj |
Updates sample package versions |
Comments suppressed due to low confidence (1)
src/Aspire.Hosting.Maui/Lifecycle/MauiBuildQueueEventSubscriber.cs:292
ReleaseSemaphoreAfterLaunchAsynccompares resource states using string literals ("Running", "FailedToStart", "Exited", "Finished"). Since the rest of the file already usesKnownResourceStates, using the constants here would avoid drift/typos and keep refactors safe.
var text = e.Snapshot.State?.Text;
// Skip the replayed snapshot that matches the state when we were called.
if (string.Equals(text, stateAtCallTime, StringComparison.Ordinal))
{
return false;
}
return text is "Running" or "FailedToStart" or "Exited" or "Finished";
},
src/Aspire.Hosting.Maui/Lifecycle/MauiBuildQueueEventSubscriber.cs
Outdated
Show resolved
Hide resolved
src/Aspire.Hosting/ApplicationModel/ProjectLaunchArgsOverrideAnnotation.cs
Show resolved
Hide resolved
- Use non-blocking semaphore.Wait(0) instead of CurrentCount check to determine if a resource should show 'Queued' state, eliminating TOCTOU race where a resource could block without showing queued. - Move MauiStopCommandReplacedAnnotation marker to after confirming the stop command exists, so retry on restart works if it was missing initially. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds build queue serialization for MAUI platform resources (Android, iOS, Mac Catalyst, Windows) that share the same project file. MSBuild cannot handle concurrent builds of the same project, causing intermittent file locking and XamlC assembly resolution failures. This PR serializes builds per-project using a semaphore.
Key Changes
Build Queue (
MauiBuildQueueEventSubscriber)BeforeResourceStartedEventfor MAUI platform resourcesSemaphoreSlim(1,1)to serializedotnet buildcommandsdotnet buildas a subprocess with output piped to the resource loggerDCP Launch Override (
ProjectLaunchArgsOverrideAnnotation)dotnet runwithdotnet build /t:Runbuild /t:Runbecausedotnet rundoesn't support the Run target needed for device deployment--configurationautomaticallyStop-While-Queued Support
Post-Build Semaphore Hold
dotnet build /t:Runreaches a stable stateWatchAsyncon resource restartTesting
Files Changed
src/Aspire.Hosting.Maui/Lifecycle/MauiBuildQueueEventSubscriber.cs— Core queue logicsrc/Aspire.Hosting.Maui/Annotations/MauiBuildQueueAnnotation.cs— Per-project queue statesrc/Aspire.Hosting.Maui/Annotations/MauiBuildInfoAnnotation.cs— Build metadata per resourcesrc/Aspire.Hosting/ApplicationModel/ProjectLaunchArgsOverrideAnnotation.cs— DCP launch overridesrc/Aspire.Hosting/Dcp/DcpExecutor.cs— Reads override annotationtests/Aspire.Hosting.Maui.Tests/MauiBuildQueueTests.cs— 93 tests