Repository navigation
Build winapp run projects with Platform instead of a global RID - #1032
Nikola Metulev (nmetulev) wants to merge 16 commits into
Conversation
winapp run passed a global -r win-<arch>. The Windows App SDK MSIX/MRT packaging targets query project references without removing that RID, so RID-agnostic libraries that enable MSIX tooling were built twice and packaging failed with APPX1101/PRI175/PRI252/MSB3030 (for example, FluentStore). winapp run builds now convey the architecture with -p:Platform alone, as Visual Studio does. Projects that set their own RuntimeIdentifier, enable dynamic platform resolution, or pass a Platform that doesn't match --arch keep the RID. winapp pack and --aot are unchanged. Adds tests/project-arch, a generated-project matrix that runs the real CLI and launches each app, as a sharded CI job. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
The RID-required fallback can still suppress the RID for a split reference graph, reproducibly causing NETSDK1032.
1 open finding
What changed in this PR
Fixes multi-project winapp run builds by using MSBuild Platform instead of a global RID, with compatibility fallbacks and expanded regression coverage.
Changes:
- Adds Platform-only architecture resolution with RID fallbacks.
- Adds a generated 44-fixture architecture matrix and CI gate.
- Updates tests, samples, documentation, and plugin guidance.
| File | Description |
|---|---|
tests/project-arch/README.md |
Documents the architecture matrix. |
tests/project-arch/ProjectArch.Tests.ps1 |
Runs and validates generated fixtures. |
tests/project-arch/ProjectArch.psm1 |
Generates fixture projects and executes the CLI. |
src/winapp-CLI/WinApp.Cli/Services/ProjectRunService.Platform.cs |
Resolves Platform-only versus RID builds. |
src/winapp-CLI/WinApp.Cli/Services/ProjectRunService.cs |
Integrates resolution and output fallbacks. |
src/winapp-CLI/WinApp.Cli/Models/ProjectRunModels.cs |
Updates architecture option contracts. |
src/winapp-CLI/WinApp.Cli/Commands/RunCommand.cs |
Updates architecture option help. |
src/winapp-CLI/WinApp.Cli.Tests/ProjectRunServiceTests.cs |
Tests architecture resolution behavior. |
src/winapp-CLI/WinApp.Cli.Tests/ProjectRunServicePublishProfileTests.cs |
Tests profile selection under Platform builds. |
scripts/tests/artifact-workflow.Tests.ps1 |
Validates the new CI dependency. |
samples/winui-solution/README.md |
Updates multi-project behavior guidance. |
samples/winui-solution/App.Core/App.Core.csproj |
Updates the regression explanation. |
samples/winui-app/test.Tests.ps1 |
Tests Platform-based profile selection. |
plugins/winapp/skills/winapp-setup/SKILL.md |
Updates shipped setup guidance. |
plugins/winapp/com.github.copilot/agents/winapp.agent.md |
Updates agent command guidance. |
docs/usage.md |
Documents revised option semantics. |
docs/guides/dotnet.md |
Updates multi-project build guidance. |
AGENTS.md |
Documents the architecture test workflow. |
.github/workflows/build-package.yml |
Adds the four-shard CI matrix. |
🧠 Review effort: Balanced
Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.
Build Metrics ReportValidation passed. All required build and validation jobs succeeded. Binary Sizes
.NET Test Results (TRX reports)Other suites are reflected in the overall validation status above. ✅ 8356 passed, 37 skipped out of 8393 tests in 1473.8s (+25 tests, +272.5s vs. baseline) Test Coverage✅ 87.5% line coverage, 82.3% branch coverage · ✅ no change vs. baseline CLI Startup Time66ms median (x64, Try This BuildInstalls the MSIX for your architecture, replacing any previously installed build. Needs the GitHub CLI — the command offers to install it and sign you in if it is missing. & ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) 1032Switching between builds often?Put the tool on your PATH once: & ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) -AddToPathThen this build is just: winapp-pr 1032Run Updated 2026-10-09 22:17:58 UTC · commit |
…winapp-run-debug # Conflicts: # plugins/winapp/com.github.copilot/agents/winapp.agent.md
A project that sets its own RuntimeIdentifier falls back to the RID path, but the legacy resolution drops the RID for a reference graph that removes it, so the project's RID won and the build failed with NETSDK1032. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Zach Teutsch (zateutsch)
left a comment
There was a problem hiding this comment.
🤖 AI-generated review (winappcli pr-review skill) — verify before acting.
Changes requested. The main fix works. I copied samples\winui-solution and ran winapp run WinUISolution.slnx with a published CLI from this branch on arm64. It built with -p:Platform=ARM64 and no -r, wrote output to bin\ARM64\Debug\<tfm>\, and registered and launched the app. The targeted unit tests (RunBuild, RunNoBuild, PublishProfile) pass 46/46.
I reproduced one regression against the released v0.7.1. Details are in the inline comment on ProjectRunService.Platform.cs; the fix is about two lines.
Must fix
- Projects that enable
EnableDynamicPlatformResolutionget-r win-<arch>added back, even where v0.7.1 had dropped it. A graph that reaches the same reference both with and without the RID then fails with MSB3030. v0.7.1 builds that graph successfully.
Non-blocking
- The new
project-archjob runs on every PR and blocksbuild-and-package. See the inline comment on the workflow.
Not exercised: the full tests/project-arch suite and x64 or cross-arch runs. CI covers x64.
Only a project's own conflicting RuntimeIdentifier needs the RID forced back. Doing it for EnableDynamicPlatformResolution re-added -r to graphs that remove RuntimeIdentifier on one edge, which builds the shared reference twice and fails with MSB3030, while v0.7.1 built them. Run the architecture matrix on PRs only when they touch project-mode build/run code; main pushes and manual runs still always run it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Zach Teutsch (zateutsch)
left a comment
There was a problem hiding this comment.
🤖 AI-generated review (winappcli pr-review skill) — verify before acting.
Changes requested. Thanks for the quick turnaround. Both earlier comments are fixed, and I checked them with a CLI published from 4e1984e5:
R17-edpr-rid-splitbuilds with-p:Platform=ARM64only and succeeds.R16still gets-r win-arm64, andR08passes.- The
project-arch-changesgate is correct: the required check still fails if the filter job fails or is cancelled. - Targeted unit tests pass (45/45), and
artifact-workflow.Tests.ps1passes (26/26).
Must fix: one new regression, which I reproduced against v0.7.1. Building for a different architecture than the machine now breaks apps that reference a source generator project. Details are in the inline comment.
Not exercised: an x64 host and the full tests/project-arch suite.
…alyzers A global Platform also reaches analyzer and source-generator project references, so a cross-architecture build compiled the generator for the target architecture and the compiler (running on the SDK's architecture) couldn't load it: CS8034, then CS0103. v0.7.1 built these with the RID. When --arch differs from the .NET SDK's architecture and the reference graph has a build-only project reference, use the existing RID-based resolution. Native builds keep the Platform-only path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…winapp-run-debug # Conflicts: # docs/usage.md
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The Platform-only check evaluated only the app project and found analyzer references by reading project XML. A referenced project that sets its own RuntimeIdentifier or enables EnableDynamicPlatformResolution, or an analyzer reference added by an import or a $(...) path, still got a Platform-only build and failed (APPX1101, NETSDK1032, NETSDK1083, CS0103) where v0.7.1 built. Evaluate the app and every managed project it references with MSBuild under the Platform-only globals and decide from those results. Any failed evaluation keeps the RID-based resolution. Adds matrix fixtures for each case found (R18-R21, X02, X03) and runs 12 existing shapes cross-architecture (--arch x86). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Install x86 .NET runtimes on the runner and set DOTNET_ROOT_X86 so unpackaged x86 apphosts don't inherit the x64 DOTNET_ROOT and exit. - R20: declare X86;X64;ARM64 on the app so the referenced win-$(Platform) RID mismatches on every host, not only arm64. - Fail when the launched window is an error dialog (#32770). - On a run timeout, record the winapp process tree for diagnosis. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Filter with Where instead of an in-loop if, and use Path.Join so a rooted name can't silently drop the temp directory. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A08 (unpackaged console app, --detach) intermittently leaves winapp running after it prints the launched PID, on CI only. With -DumpDirectory, a timed-out run is dumped (full memory) before it is stopped, and CI uploads the dumps on failure for diagnosis. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Zach Teutsch (zateutsch)
left a comment
There was a problem hiding this comment.
🤖 AI-generated review (winappcli pr-review skill) — verify before acting.
Thanks for the cross-architecture generator fix. With the X01 layout, --arch x86 now keeps -r win-x86, builds an AnyCPU generator and compiles. The 283 ProjectRunService unit tests pass.
Two regressions against v0.7.1 remain, both shown at runtime with a published CLI from ac9e8790:
- A property-path generator reference bypasses the new fallback. It still fails with CS0103 when the build targets another architecture.
- The safety check only looks at the entry project. A referenced project that enables dynamic platform resolution fails with APPX1101, and one with a hard-coded conflicting RuntimeIdentifier fails with NETSDK1032. v0.7.1 builds both.
Details and smallest fixes are inline.
- X04: generator referenced as ..\$(GenFolder)\$(GenFolder).csproj with a project-defined property, built for x86 (review request). - R22: packaged app in a solution with GenerateAppxPackageOnBuild and AppxBundle=Always. Bundling rebuilds the solution for the other platform, which v0.7.1's global RID breaks with NETSDK1134 (reported on FluentStore's update/0.4.2 branch). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Zach Teutsch (zateutsch)
left a comment
There was a problem hiding this comment.
🤖 AI-generated review (winappcli pr-review skill) — verify before acting.
Thanks, evaluating the reference graph through MSBuild fixes the issues from the last review. I re-checked those shapes against npm v0.7.1 using a published CLI from 97f9f665:
- A referenced library with dynamic platform resolution builds.
- A referenced library with a hard-coded RID builds.
- R08 builds (it fails on v0.7.1).
- The property-path generator with
--arch x86builds an x86 exe.
The filtered ProjectRunService and MsBuildPropertyReader unit tests pass (432/432).
One regression against v0.7.1 remains. A reference marked ReferenceOutputAssembly="false" is skipped by the probe even though it is still built, so a conflicting RuntimeIdentifier in that project causes NETSDK1032. Details and the smallest fix are inline.
| if (IsBuildOnlyReference(reference)) | ||
| { | ||
| buildOnlyReference = true; | ||
| continue; | ||
| } |
There was a problem hiding this comment.
A build-order-only reference that sets its own RID still fails
- What is wrong: This branch treats every
ReferenceOutputAssembly="false"reference as an analyzer and skips it withcontinue, so that project is never evaluated. But MSBuild still builds it, with the globalPlatform. If it sets its ownRuntimeIdentifier, the probe never sees it and winapp picks a Platform-only build. - Show me: Ran on arm64 with a CLI published from
97f9f665and with npm v0.7.1:Helper\Helper.csproj:Exe,net10.0,<RuntimeIdentifier>win-x64</RuntimeIdentifier>App\App.csproj:Exe,net10.0,<ProjectReference Include="..\Helper\Helper.csproj" ReferenceOutputAssembly="false" />winapp run . --arch arm64- This PR: NETSDK1032,
The RuntimeIdentifier platform 'win-x64' and the PlatformTarget 'arm64' must be compatible.The build fails. - v0.7.1: builds with
-r win-arm64and printsok.
- This PR: NETSDK1032,
- Why it matters: Referencing a helper exe only so it builds first is a common pattern, and it isn't limited to analyzers. The cross-architecture analyzer fallback doesn't apply to a native-architecture run, so nothing catches this.
- Smallest fix: Keep
buildOnlyReference = truefor the analyzer check, but remove thecontinueso these projects are evaluated like any other reference. An analyzer project has no RID or dynamic platform resolution, so this only adds one evaluation. Please add a project-arch fixture and a unit-test case for a conflicting RID behind aReferenceOutputAssembly="false"reference.

Description
winapp runon a multi-project .NET app could fail to build even though the same solution builds in Visual Studio. FluentStore is a real example:Cause: winapp passed a global
-r win-<arch>, which Visual Studio never does.GetPriOutputs/GetPackagingOutputs) query project references without removing that RID.<RuntimeIdentifiers>that uses MSIX tooling is built twice, once with the RID and once without.Fix:
winapp runnow passes the architecture as-p:Platform=<arch>only, as Visual Studio does. A project's ownwin-$(Platform).pubxmlprofile then applies without winapp choosing one. winapp first evaluates the app and every project it references with MSBuild, and keeps the previous RID-based behavior when:RuntimeIdentifierthat isn'twin-<arch>(a hard-coded RID, orwin-$(Platform)with upper-case platform names). Such a project always gets-r win-<arch>; otherwise its own RID would win.EnableDynamicPlatformResolution.--archdiffers from the .NET SDK's architecture and the reference graph includes an analyzer or source-generator project. With a global Platform, the generator would be built for the target architecture, and the compiler couldn't load it.-p:Platformthat doesn't match--arch, or an exact-p RuntimeIdentifier.winapp packandwinapp run --aotare unchanged.--no-buildstill finds output from earlier RID-based builds.Usage Example
Before:
After (observed):
The app window ("Fluent Store") opened and was responding.
Related Issue
Related to #1035 (a separate launch hang found while building the test suite)
Type of Change
Checklist
plugins/winapp/skills/(if CLI commands/workflows changed)Screenshots / Demo
N/A (CLI behavior; see the before/after output above).
Additional Notes
New automated test:
tests/project-arch/generates 66 small .NET projects and runs the real CLI on each. Each run checks that the build succeeds, that the expected architecture argument was used (Platform only, or the RID for the projects that need it), that the app launches and shows its own window (not an error dialog), and that the running executable matches--arch.The projects cover:
EnableDynamicPlatformResolutiongraph whose reference removesRuntimeIdentifierDirectory.Build.targets,$(…)paths)--arch x86): source generators plus 12 common shapesAnyCPUlibrary,PublishAot, a hard-coded RIDIt runs in CI as the 4-shard
project-archjob (which installs the x86 .NET runtimes and setsDOTNET_ROOT_X86so unpackaged x86 apps can start):project-arch-changesjob finds changes to project-mode build/run code, so docs-only PRs skip it.build-and-packageaccepts the matrix as skipped only when that check decided it wasn't needed.Validation (arm64 dev machine):
.\scripts\build-cli.ps1 -SkipTests -SkipDocsInvoke-Pester .\tests\project-arch\ProjectArch.Tests.ps1(NativeAOTartifacts\cli)R22,X04) pass separatelyR17–R20,X01–X03) against the CLI before each fix and against v0.7.1winapp runon WinUI Gallery, AI Dev Gallery, FluentStore and 9 local test appsR22against v0.7.1update/0.4.2branch; passes on this PRdotnet run --project src\winapp-CLI\WinApp.Cli.Tests -c Release -- --filter "...ProjectRun|RunCommand|PackageCommand".\scripts\build-cli.ps1 -OnlyTests -UseExistingArtifacts -TestSuite CoreInvoke-Pester scripts\tests\artifact-workflow.Tests.ps1.\scripts\test-samples.ps1 -Samples winui-app,winui-solution,winui-unpackaged-app,wpf-app,dotnet-app -WinappPath .\artifacts.\scripts\validate-llm-docs.ps1About the 12 Core-suite failures:
NugetServiceTests, which can't reach nuget.org from this corporate machine (documented in AGENTS.md). They pass when pointed at the mirror viaWINAPP_TEST_NUGET_SOURCE.AnalyzeDumpAsync_ManagedDumpWithDeepStack_DetectsStackOverflowShape) failed: it read the dump's .NET runtime as x64, which doesn't match this arm64 host. The test is unrelated to this change.Limitations:
<Platforms>, built for another architecture than the .NET SDK's. It fails the same way on v0.7.1 (CS0103, verified), because the RID-based path also passes a Platform there.PATH, because this machine also has an older winapp MSIX installed.