From 9dea096cf0a866b7e04f93337324806d1b111a58 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Wed, 23 Sep 2026 16:16:38 -0700 Subject: [PATCH 01/20] Stop shipping .pri in lib/; fix publish + lib-folder packaging defects Reactor's three packages each shipped their generated .pri beside the assembly in lib//. RAR relates a same-base-name .pri to its reference (AllowedReferenceRelatedFileExtensions defaults to ".pdb;.xml;.pri;..."), so the Windows App SDK ran makepri.exe Dump on all three into $(IntermediateOutputPath). Those paths break past MAX_PATH, failing consumer builds with PRI175/PRI222/APPX0002 at depths where an equivalent XAML app builds fine. Nothing was lost: the .Advanced and .Devtools indexes were entirely empty, and the core one held a single embedded copy of ReactorApplication.xbf that also ships loose. Relocating the file would not have worked - a .pri that reaches the consumer's layout by any route is also expanded via _ExpandPriFiles - so it is excluded from pack entirely. Generation is untouched, leaving ProjectReference consumers unaffected. Measured on a 261-character output path: build goes from failing to clean, and Reactor makepri Dump calls drop from six to zero, leaving only the Windows App SDK's own framework indexes. Two further defects found while verifying this: - dotnet pack from a clean tree silently omitted ReactorApplication.xbf. It was packed by a guarded with Exists(), which MSBuild evaluates before the build produces the file. Now resolved at pack time, and its absence is a hard error. - The package exposed two lib/ folders for the same framework: the .xbf used a literal lib\$(TargetFramework)\... path (net10.0-windows10.0.22621.0) while NuGet shortens build output to net10.0-windows10.0.22621. The sidecar is now emitted as build output with TargetPath so NuGet places it. Also fixes WindowsAppSDK#6394 for every consumer: an unpackaged app's own .pri and .xbf never reached the publish output, so published apps started with an empty resource dictionary and crashed with 0xC000027B. The new _ReactorCopyWinUIResourcesToPublish target in the shipped targets replaces the per-project _CopyWinUIResourcesForAot snippet in 11 projects. That snippet was gated on PublishAot=true, but the gap is not AOT-specific - a plain dotnet publish -c Release loses the .pri too, so apps that never opted into AOT were broken with no workaround available. Fixes: #1271 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- CHANGELOG.md | 48 +++ docs/aot-support.md | 28 +- docs/specs/022-packaging-and-distribution.md | 62 +++- .../CommandPaletteWindow.csproj | 11 - .../HelloWorldAotDevtoolsOn.csproj | 11 - .../apps/hello-world-aot/HelloWorldAot.csproj | 11 - .../apps/particle-storm/ParticleStorm.csproj | 11 - .../apps/widget-creator/widget-creator.csproj | 15 - .../apps/window-styles/WindowStyles.csproj | 11 - src/Reactor.Advanced/Reactor.Advanced.csproj | 8 + src/Reactor.Devtools/Reactor.Devtools.csproj | 7 + src/Reactor/Reactor.csproj | 69 +++- src/Reactor/build/Reactor.targets | 68 +++- .../Reactor.AppTests.Host.csproj | 23 -- .../Packaging/PriPackagingTests.cs | 328 ++++++++++++++++++ ...tor.AotHelloWorld.Advanced.Positive.csproj | 11 - .../Reactor.AotHelloWorld.Advanced.csproj | 11 - .../Reactor.AotHelloWorld.csproj | 17 - .../PerfBench.ControlModel.csproj | 20 -- 19 files changed, 584 insertions(+), 186 deletions(-) create mode 100644 tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 42c660613..9473883a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,54 @@ Conventions for contributors: ### Fixed +- **Reactor packages no longer ship a `.pri`, so a consuming app builds at path depths + where it previously failed (issue #1271).** `Microsoft.UI.Reactor`, + `.Advanced` and `.Devtools` each packed their generated resource index beside the + assembly in `lib//`. `ResolveAssemblyReference` relates a same-base-name `.pri` + to its reference, so the Windows App SDK's `AddPriPayloadFilesToCopyToOutputDirectoryItems` + ran `makepri.exe Dump` on all three, writing into `$(IntermediateOutputPath)`. Those + output paths break past `MAX_PATH`, and `Microsoft.UI.Reactor.Devtools.pri.xml` — the + longest of the names — was what tipped a borderline project over, failing the build with + `PRI175` / `PRI222` / `APPX0002` at a depth where the equivalent XAML app built fine. + + Nothing was lost: the `.Advanced` and `.Devtools` indexes were entirely empty, and the + core one held a single embedded copy of `ReactorApplication.xbf` that also ships loose. + Measured on a 261-character output path, the build goes from failing to clean, and a + consumer build's `makepri.exe Dump` invocations for Reactor packages drop from six to + zero — leaving only the Windows App SDK's own framework indexes, exactly as for a plain + WinUI app. `.pri` *generation* is unchanged, so `ProjectReference` consumers are + unaffected. + +- **`Microsoft.UI.Reactor` no longer ships two `lib/` folders for the same target framework.** + `ReactorApplication.xbf` was packed with a literal `lib\$(TargetFramework)\…` path, but + `$(TargetFramework)` is `net10.0-windows10.0.22621.0` while NuGet shortens the build-output + folder to `net10.0-windows10.0.22621`. The package therefore carried two sibling `lib` groups + for one framework — one with the assemblies, one holding nothing but the stray sidecar — and + NuGet selected between them by ordering rather than by rule. The sidecar is now emitted as + build output with a `TargetPath`, so NuGet places it beside `Reactor.dll` in the single + normalized folder. Visible in 0.1.0-preview.16 and earlier. + +- **`dotnet pack` from a clean tree no longer silently omits `ReactorApplication.xbf`.** + It was packed by a `` item guarded with `Exists()`, which MSBuild evaluates before + the build that produces the file — so the sidecar appeared only when a previous build had + already left one in `bin/`. It is now resolved at pack time, and its absence is a hard + error rather than a silently incomplete package. + +- **Published apps no longer start with an empty resource dictionary + ([WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394)).** The Windows + App SDK generates an unpackaged app's `.pri` (and the XAML `.xbf` files) into the build output + but never copies them to the publish directory, because the MakePRI publish stage is gated on + `AppxPackage=true`. The published app then resolves no theme resources and dies at startup with + `0xC000027B` inside native XAML. Reactor now ships + `_ReactorCopyWinUIResourcesToPublish` in `build/Microsoft.UI.Reactor.targets`, so every consumer + gets the fix automatically — opt out with + `false`. + + This replaces the `_CopyWinUIResourcesForAot` snippet that sample and test projects each carried + (now removed from all 11 of them). That snippet was gated on `PublishAot=true`, but the gap is + **not** AOT-specific — a plain `dotnet publish -c Release` loses the `.pri` too, which is why + apps that never opted into AOT were affected as well. + ### Security ## [0.1.0-preview.16] — 2026-09-22 diff --git a/docs/aot-support.md b/docs/aot-support.md index 0180d6f7a..e6b26db97 100644 --- a/docs/aot-support.md +++ b/docs/aot-support.md @@ -16,8 +16,8 @@ This page enumerates what works, what doesn't, and what's planned. **If you publ - **Markdown** — md4c-backed parser and renderer. - **Commanding** — command records, focus-scoped accelerators. - **Animation** — compositor-layer transitions, keyframes. -- **Theming** — `ThemeRef` tokens, style caching. Brushes resolved through `XamlControlsResources` work under AOT once the WindowsAppSDK#6394 publish workaround is in place (see [Required publish-time workarounds](#required-publish-time-workarounds) below). -- **Built-in WinUI controls** — `NavigationView`, `TabView`, `Pivot`, `TreeView`, `Expander`, `InfoBar`, `MenuBar`, `BreadcrumbBar`, `RefreshContainer`, `TeachingTip`, `ColorPicker`, `NumberBox`, `RatingControl`, `SplitButton`, etc. all mount and update under AOT once WindowsAppSDK#6394 is mitigated. `{TemplateBinding}` against built-in DPs works; private DPs declared in a separate third-party assembly without an `IXamlMetadataProvider` are still broken (Issue #142). +- **Theming** — `ThemeRef` tokens, style caching. Brushes resolved through `XamlControlsResources` work under AOT; Reactor ships the WindowsAppSDK#6394 publish workaround in its own targets, so no app-side setup is needed (see [Required publish-time workarounds](#required-publish-time-workarounds) below). +- **Built-in WinUI controls** — `NavigationView`, `TabView`, `Pivot`, `TreeView`, `Expander`, `InfoBar`, `MenuBar`, `BreadcrumbBar`, `RefreshContainer`, `TeachingTip`, `ColorPicker`, `NumberBox`, `RatingControl`, `SplitButton`, etc. all mount and update under AOT, because Reactor's shipped targets mitigate WindowsAppSDK#6394 automatically. `{TemplateBinding}` against built-in DPs works; private DPs declared in a separate third-party assembly without an `IXamlMetadataProvider` are still broken (Issue #142). - **Markdown / Localization (read path)** — `IStringLoc`/`Loc.X.Y` lookups generated by `Reactor.Localization.Generator` are AOT-clean. Source-generated, no runtime reflection. ## What does *not* work under AOT (today) @@ -33,7 +33,7 @@ These subsystems compile cleanly with `IsAotCompatible=true` (the warnings are s | **Form validation** | `FormField`'s default editor resolution goes through `TypeRegistry`. Same caveat as PropertyGrid. | Issue #70 | | **Component discovery (`ReactorApp.Run` reflection paths)** | The instantiation of `TApp` itself is annotated and works. Devtools component enumeration is available only when the app opts into `Reactor.DevtoolsSupport` at build time and launches with `--devtools`; leave the switch off for retail/AOT builds. | Issue #70 | | **Devtools `properties` / `setProperty` DP discovery** | The tools locate `DependencyProperty` statics by reflection, and the trimmer keeps no reflection metadata for them unless something roots the members — so under AOT the lookups find nothing. Not specific to WinUI's CsWinRT-projected static properties: a C#-authored DP *field* on a custom control is equally undiscoverable. Works under JIT, which is where the devtools inner loop lives. The tool reports the likely cause rather than an empty result. See [Devtools DependencyProperty discovery](#devtools-dependencyproperty-discovery) for the measured cost of fixing it. Affected fixture: `Devtools_PropertyToolsDpDiscovery`. | Issue #1109 | -| **Theme resource lookup (`Theme.X`, `ThemeRef.Resolve`)** | Works once the WindowsAppSDK#6394 workaround target ships the project `.pri` into the publish output (see [Required publish-time workarounds](#required-publish-time-workarounds)). Reactor's library itself is AOT-clean here. | WindowsAppSDK#6394 | +| **Theme resource lookup (`Theme.X`, `ThemeRef.Resolve`)** | Works — Reactor's shipped `_ReactorCopyWinUIResourcesToPublish` target puts the project `.pri` in the publish output (see [Required publish-time workarounds](#required-publish-time-workarounds)). Reactor's library itself is AOT-clean here. | WindowsAppSDK#6394 | | **Third-party-assembly XAML metadata (Issue #142 fixtures)** | Built-in WinUI controls work under AOT now (see [Required publish-time workarounds](#required-publish-time-workarounds)). What remains is `{TemplateBinding}` against private DPs in a third-party assembly that ships *no* `.xaml` file: the XAML compiler only emits an `IXamlMetadataProvider` for projects that have at least one `.xaml`, and AOT trims any implicit metadata path. Affected fixtures: `Issue142_CustomControlPrivateDp_Renders`, `Issue142_ThirdPartyControlPrivateDp_Renders`. The library-author workaround is to register a hand-written `IXamlMetadataProvider` via `RegisterControlAssembly`. | Issue #142 | ## Devtools DependencyProperty discovery @@ -98,7 +98,9 @@ Devtools are a two-layer opt-in so release and NativeAOT binaries stay trimmed b ### WindowsAppSDK#6394 — project `.pri` (and `.xbf`) missing from publish output -When publishing an unpackaged WinUI 3 app (`WindowsPackageType=None`) with `PublishAot=true`, the WindowsAppSDK build pipeline generates `$(AssemblyName).pri` into the intermediate output but does *not* copy it into the publish directory. The MakePRI step is conditioned on `AppxPackage=true`, so unpackaged apps fall through. Same applies to `.xbf` files generated by the XAML compiler. +When publishing an unpackaged WinUI 3 app (`WindowsPackageType=None`), the WindowsAppSDK build pipeline generates `$(AssemblyName).pri` into the build output but does *not* copy it into the publish directory. The MakePRI step is conditioned on `AppxPackage=true`, so unpackaged apps fall through. Same applies to `.xbf` files generated by the XAML compiler. + +**This is not AOT-specific.** It was originally found via `PublishAot=true`, but a plain `dotnet publish -c Release` loses the `.pri` just the same — measured on the stock `reactorapp` template, where the published app dies at startup with `0xC000027B` (`STATUS_APPLICATION_INTERNAL_EXCEPTION`) inside native XAML. The missing `.pri` has cascading runtime symptoms that look like separate bugs but all trace back to the same cause: @@ -109,23 +111,15 @@ The missing `.pri` has cascading runtime symptoms that look like separate bugs b Upstream tracking: [microsoft/WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394) — still OPEN against 1.8 and 2.0. -**Workaround** (in `tests/Reactor.AppTests.Host/Reactor.AppTests.Host.csproj`): +**Nothing to do — Reactor ships the workaround.** `build/Microsoft.UI.Reactor.targets` defines `_ReactorCopyWinUIResourcesToPublish`, which runs after `Publish` for any unpackaged Reactor executable and copies the project `.pri` plus every `.xbf` into `$(PublishDir)`. It applies to AOT and non-AOT publishes alike, so app projects no longer carry a per-project copy of this target (they used to; it was gated on `PublishAot=true` and therefore missed the plain-publish case). + +Opt out — for example if you drive the copy yourself — with: ```xml - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" - Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - +false ``` -Any Reactor app that publishes with `PublishAot=true` and `WindowsPackageType=None` should copy this target verbatim until WindowsAppSDK ships a fix. Remove the target once #6394 closes. +Remove the shipped target once #6394 closes. ## Debugging an AOT selftest hang diff --git a/docs/specs/022-packaging-and-distribution.md b/docs/specs/022-packaging-and-distribution.md index e6958a8ce..06b1240d9 100644 --- a/docs/specs/022-packaging-and-distribution.md +++ b/docs/specs/022-packaging-and-distribution.md @@ -141,9 +141,12 @@ The `` line stays — `mur --skill` s ``` Microsoft.UI.Reactor.1.0.0-preview.42.nupkg -├── lib/net10.0-windows10.0.22621.0/ +├── lib/net10.0-windows10.0.22621/ # ONE lib group — see "Exactly one lib folder" │ ├── Reactor.dll -│ └── Reactor.xml # XML doc comments +│ ├── Reactor.xml # XML doc comments +│ └── Reactor/Hosting/ +│ ├── ReactorApplication.xaml # Application-level XamlControlsResources +│ └── ReactorApplication.xbf # compiled form; see "No .pri in lib/" below ├── analyzers/dotnet/cs/ │ ├── Reactor.Analyzers.dll │ └── Reactor.Localization.Generator.dll @@ -154,6 +157,61 @@ Microsoft.UI.Reactor.1.0.0-preview.42.nupkg └── Microsoft.UI.Reactor.nuspec ``` +Note the folder is `net10.0-windows10.0.22621`, **not** `…22621.0`: NuGet shortens the +build-output folder name, while `$(TargetFramework)` keeps the trailing `.0`. + +### No `.pri` in `lib/` (issue #1271) + +None of `Microsoft.UI.Reactor`, `.Advanced` or `.Devtools` ships its generated `.pri`, +and none ever should. NuGet packs a project's resource index into the build-output +folder by default, which puts it beside the assembly — and `ResolveAssemblyReference` +treats a same-base-name `.pri` as a *reference-related file* +(`AllowedReferenceRelatedFileExtensions` defaults to `.pdb;.xml;.pri;…`). Once it is in +`_ReferenceRelatedPaths` / `ReferenceCopyLocalPaths`, the Windows App SDK's +`AddPriPayloadFilesToCopyToOutputDirectoryItems` runs `makepri.exe Dump` on it into +`$(IntermediateOutputPath)`, and that output path breaks past `MAX_PATH` — failing +consumer builds with `PRI175` / `PRI222` / `APPX0002` at depths where an equivalent XAML +app builds fine. + +Relocating the file is *not* a fix: a `.pri` that reaches the consumer's layout by any +route — including a `CopyToOutputDirectory` item from a non-reference folder — also lands +in `@(PackagingOutputs)`, where `_ExpandPriFiles` hands it to the same task. It has to be +absent. + +Each of the three csproj files therefore narrows the allow-list that decides what may sit +in the build-output folder: + +```xml + +.dll; .exe; .winmd; .json; .xml; .xbf +``` + +This is deliberately the **pack**-side knob. `.pri` *generation* is untouched, so +`ProjectReference` consumers (in-repo samples, selftests, AOT proofs) are unaffected. + +What consumers need instead is the loose `ReactorApplication.xbf`, which +`ReactorApplication.InitializeComponent()` resolves from the app directory. It is packed +by the `_PackReactorApplicationXbf` target rather than a `` item, because a `` +guarded with `Exists()` is evaluated before the build produces the file and silently +shipped a package without it. + +### Exactly one `lib/` folder + +`_PackReactorApplicationXbf` emits the sidecar as **build output** with a `TargetPath` +(the mechanism satellite assemblies use for `lib///`), never as a +`TfmSpecificPackageFile` with a literal `lib\$(TargetFramework)\…` path. + +That literal form is a trap. `$(TargetFramework)` is `net10.0-windows10.0.22621.0` while +NuGet shortens the build-output folder to `net10.0-windows10.0.22621`, so the package +grew **two sibling `lib` groups for the same framework** — one with the assemblies, one +holding nothing but the stray sidecar. NuGet picked the right one by ordering rather than +by rule. Shipped in 0.1.0-preview.16 and earlier; fixed by letting NuGet place the file. + +`tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs` asserts all of it — no +`.pri`, the sidecars present, exactly one `lib/` folder per package, a real consumer build +logging zero `makepri.exe Dump` calls for Reactor packages, and the app `.pri` reaching +the consumer's publish output. + ### Packaging configuration Enable packing on `src/Reactor/Reactor.csproj`: diff --git a/samples/apps/command-palette-window/CommandPaletteWindow.csproj b/samples/apps/command-palette-window/CommandPaletteWindow.csproj index bebd3327e..181322caa 100644 --- a/samples/apps/command-palette-window/CommandPaletteWindow.csproj +++ b/samples/apps/command-palette-window/CommandPaletteWindow.csproj @@ -22,17 +22,6 @@ true - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - diff --git a/samples/apps/hello-world-aot-devtools-on/HelloWorldAotDevtoolsOn.csproj b/samples/apps/hello-world-aot-devtools-on/HelloWorldAotDevtoolsOn.csproj index e302205f1..d24954de0 100644 --- a/samples/apps/hello-world-aot-devtools-on/HelloWorldAotDevtoolsOn.csproj +++ b/samples/apps/hello-world-aot-devtools-on/HelloWorldAotDevtoolsOn.csproj @@ -44,17 +44,6 @@ - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - diff --git a/samples/apps/hello-world-aot/HelloWorldAot.csproj b/samples/apps/hello-world-aot/HelloWorldAot.csproj index 299e2564a..ca8e8ceee 100644 --- a/samples/apps/hello-world-aot/HelloWorldAot.csproj +++ b/samples/apps/hello-world-aot/HelloWorldAot.csproj @@ -40,17 +40,6 @@ true - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - diff --git a/samples/apps/particle-storm/ParticleStorm.csproj b/samples/apps/particle-storm/ParticleStorm.csproj index 077e65a47..de0e68599 100644 --- a/samples/apps/particle-storm/ParticleStorm.csproj +++ b/samples/apps/particle-storm/ParticleStorm.csproj @@ -40,17 +40,6 @@ true - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - diff --git a/samples/apps/widget-creator/widget-creator.csproj b/samples/apps/widget-creator/widget-creator.csproj index 2c8342c10..ea1a5800f 100644 --- a/samples/apps/widget-creator/widget-creator.csproj +++ b/samples/apps/widget-creator/widget-creator.csproj @@ -57,21 +57,6 @@ 10.0.17763.0 - - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - + .dll; .exe; .winmd; .json; .xml diff --git a/src/Reactor.Devtools/Reactor.Devtools.csproj b/src/Reactor.Devtools/Reactor.Devtools.csproj index 500118bba..f1236a01c 100644 --- a/src/Reactor.Devtools/Reactor.Devtools.csproj +++ b/src/Reactor.Devtools/Reactor.Devtools.csproj @@ -34,6 +34,13 @@ snupkg true $(NoWarn);CS1591;CA2255;REACTOR_DOC_001 + + .dll; .exe; .winmd; .json; .xml diff --git a/src/Reactor/Reactor.csproj b/src/Reactor/Reactor.csproj index 5f0c16b94..9db0dfe14 100644 --- a/src/Reactor/Reactor.csproj +++ b/src/Reactor/Reactor.csproj @@ -55,6 +55,26 @@ enforcement can re-enable per-rule. Unresolved crefs are covered by the built-in CS1574 (severity configured in src/Reactor/.editorconfig). --> $(NoWarn);CS1591;REACTOR_DOC_001 + + .dll; .exe; .winmd; .json; .xml; .xbf @@ -177,6 +197,37 @@ + + + $(TargetsForTfmSpecificBuildOutput);_PackReactorApplicationXbf + + + + + + + - - + - + + + + + <_ReactorWinUIPublishResource Include="$(OutputPath)**\*.xbf" + Exclude="$(PublishDir)**\*.xbf" /> + <_ReactorWinUIPublishResource Include="$(ProjectPriFullPath)" + Condition="'$(ProjectPriFullPath)' != '' and Exists('$(ProjectPriFullPath)')" /> + <_ReactorWinUIPublishResource Include="$(OutputPath)$(AssemblyName).pri" + Condition="'$(ProjectPriFullPath)' == '' and Exists('$(OutputPath)$(AssemblyName).pri')" /> + + + diff --git a/tests/Reactor.AppTests.Host/Reactor.AppTests.Host.csproj b/tests/Reactor.AppTests.Host/Reactor.AppTests.Host.csproj index aec9c83e4..c5c0205b4 100644 --- a/tests/Reactor.AppTests.Host/Reactor.AppTests.Host.csproj +++ b/tests/Reactor.AppTests.Host/Reactor.AppTests.Host.csproj @@ -24,29 +24,6 @@ true - - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - +/// Issue #1271 — the Reactor packages must not ship a .pri, and the compiled +/// ReactorApplication.xbf that replaced it must actually be in the package. +/// +/// Why a .pri beside the assembly is harmful: ResolveAssemblyReference +/// relates same-base-name sidecars to a reference (AllowedReferenceRelatedFileExtensions +/// defaults to .pdb;.xml;.pri;…), so a lib/<tfm>/Reactor.pri lands in +/// _ReferenceRelatedPaths / ReferenceCopyLocalPaths. The Windows App SDK's +/// AddPriPayloadFilesToCopyToOutputDirectoryItems then runs makepri.exe Dump on +/// every one of them, writing into $(IntermediateOutputPath). Those output paths break +/// past MAX_PATH, so consumers failed to build at a depth where an equivalent XAML app built +/// fine — and Microsoft.UI.Reactor.Devtools.pri.xml, the longest of the three names, is +/// what tipped a borderline project over. +/// +/// Measured when this was fixed: Reactor.Advanced.pri and +/// Microsoft.UI.Reactor.Devtools.pri were entirely empty (a resource map with no +/// NamedResource), and Reactor.pri held exactly one entry — an EmbeddedData +/// copy of ReactorApplication.xbf, which also ships loose. So nothing was lost. +/// +/// Like its siblings in this collection, this needs network access to restore the +/// Windows App SDK, so it only runs where the feeds are reachable (CI's "Integration Tests" job). +/// +[Collection(LocalPackageFeedCollection.Name)] +public sealed class PriPackagingTests : IDisposable +{ + private readonly TemplatePackageTestFixture _fixture; + private readonly string _tempRoot = Path.Join(Path.GetTempPath(), $"reactor-pri-pkg-{Guid.NewGuid():N}"); + + private static readonly string[] PackageIds = + [ + "Microsoft.UI.Reactor", + "Microsoft.UI.Reactor.Advanced", + "Microsoft.UI.Reactor.Devtools", + ]; + + public PriPackagingTests(TemplatePackageTestFixture fixture) + { + _fixture = fixture; + Directory.CreateDirectory(_tempRoot); + } + + /// + /// Structural half: no .pri anywhere in any of the three packages. + /// + /// The mechanism that keeps it out is a single property + /// (DefaultAllowedOutputExtensionsInPackageBuildOutputFolder minus .pri) in + /// each csproj. Nothing fails if that property stops being honoured — NuGet would simply + /// resume packing the file and consumers would silently regress — so this assertion is the + /// only thing standing between a rename upstream and a re-broken package. + /// + [Fact] + public void PackagesShipNoPriFile() + { + foreach (var packageId in PackageIds) + { + var entries = ReadPackageEntries(packageId); + + // Positive control: the package was really opened and really has content. Without + // this, an empty or unreadable archive would satisfy the "no .pri" assertion below. + Assert.Contains(entries, e => e.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)); + + var pris = entries.Where(e => e.EndsWith(".pri", StringComparison.OrdinalIgnoreCase)).ToArray(); + Assert.True( + pris.Length == 0, + $"{packageId} ships {pris.Length} .pri file(s): {string.Join(", ", pris)}. " + + "A .pri in lib/ is picked up as a reference-related path and fed to the Windows App SDK's " + + "makepri.exe Dump expansion, whose output path breaks past MAX_PATH (issue #1271)."); + } + } + + /// + /// The other half, and the reason the first half is safe: the loose sidecars are still there. + /// + /// ReactorApplication.xbf is what InitializeComponent() resolves now that + /// the .pri's embedded copy is gone, and it reaches the package through a pack-time + /// target rather than a <None> item — because a <None> guarded by + /// Exists() is evaluated BEFORE the build produces the file, which silently shipped a + /// package without it on a clean tree. Asserting "no .pri" without also asserting this would + /// happily pass for a package that delivers neither copy. + /// + [Fact] + public void CorePackageStillShipsTheApplicationXamlSidecars() + { + var entries = ReadPackageEntries("Microsoft.UI.Reactor"); + + Assert.Contains(entries, e => e.EndsWith("Reactor/Hosting/ReactorApplication.xbf", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(entries, e => e.EndsWith("Reactor/Hosting/ReactorApplication.xaml", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Every package must expose exactly ONE lib/<tfm> folder. + /// + /// Packing a file with a literal lib\$(TargetFramework)\… path is the easy way to + /// get this wrong: $(TargetFramework) is net10.0-windows10.0.22621.0, but NuGet + /// shortens the build-output folder to net10.0-windows10.0.22621. Both parse to the + /// same framework, so the package ends up with two sibling lib groups for one target — + /// one holding the assemblies, one holding only a stray sidecar. It resolved correctly by + /// luck of ordering rather than by rule, which is not a property worth relying on. + /// + /// Asserting the count (rather than a specific folder name) keeps this from breaking + /// the next time the TFM or NuGet's shortening changes. + /// + [Fact] + public void PackagesExposeExactlyOneLibFolder() + { + foreach (var packageId in PackageIds) + { + var libFolders = ReadPackageEntries(packageId) + .Where(e => e.StartsWith("lib/", StringComparison.OrdinalIgnoreCase)) + .Select(e => e.Split('/')[1]) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + // Positive control: a package with no lib/ at all would otherwise satisfy "not > 1". + Assert.True(libFolders.Length == 1, $"{packageId} has {libFolders.Length} lib/ folder(s): {string.Join(", ", libFolders)}."); + } + } + + /// + /// Behavioural half: a real consumer build no longer invokes makepri.exe Dump for any + /// Reactor package. + /// + /// This is what the structural test cannot show — that the expansion step is actually + /// gone from the consumer's build rather than merely that one file moved. Before the fix this + /// build logged six Reactor dumps (three packages, reached through both the reference path and + /// the payload path); after it, zero. + /// + /// The assertion carries its own positive control: the Windows App SDK's own framework + /// PRIs are still dumped and the app still runs makepri.exe New for its own resources. + /// If the probe were simply failing to match — a changed log format, a quieter verbosity, a + /// build that died early — those counts would be zero too, and the test fails instead of + /// reporting a false success. + /// + [Fact] + public void ConsumerBuildRunsNoMakepriDumpForReactorPackages() + { + var appDir = Path.Join(_tempRoot, "consumer"); + Directory.CreateDirectory(appDir); + + WriteConsumerProject(appDir); + WriteConsumerProgram(appDir); + CreateNuGetConfig(appDir); + + var build = RunHelpers.RunProcess( + "dotnet", + $"build -c Debug -a {_fixture.RunArchitecture} -v:normal", + appDir, + _fixture.CommandEnvironment, + timeoutMs: 600_000, + throwOnFailure: true).Stdout; + + var dumpLines = build + .Split('\n') + .Where(l => l.Contains("makepri.exe Dump", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + // Positive controls FIRST: prove the probe can match in this log before trusting a zero. + Assert.True( + dumpLines.Length > 0, + "No 'makepri.exe Dump' line at all. The Windows App SDK dumps its own framework PRIs, " + + "so zero means the probe stopped matching (log format or verbosity changed) rather " + + "than that Reactor stopped contributing PRIs."); + Assert.Contains("makepri.exe New", build, StringComparison.OrdinalIgnoreCase); + + var reactorDumps = dumpLines + .Where(l => l.Contains("microsoft.ui.reactor", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + Assert.True( + reactorDumps.Length == 0, + $"Consumer build ran makepri.exe Dump on {reactorDumps.Length} Reactor package PRI(s) " + + $"(issue #1271):{Environment.NewLine}{string.Join(Environment.NewLine, reactorDumps)}"); + } + + /// + /// WindowsAppSDK#6394 — the package's targets must put the CONSUMING app's own `.pri` + /// (and the `.xbf` sidecars) into its publish output. + /// + /// The Windows App SDK generates the app's resource index into the build output but + /// never copies it to publish for an unpackaged app, and the published app then dies at + /// startup with 0xC000027B inside native XAML. Reactor ships + /// _ReactorCopyWinUIResourcesToPublish to close that gap for every consumer; this + /// test is what keeps it working, since nothing else fails if the target stops running — + /// the build stays green and only the published app breaks. + /// + /// The opt-out property doubles as the control: it must reproduce the missing file. + /// Without that half, a publish that happened to include the .pri for some unrelated + /// reason would let this pass while the target did nothing. + /// + [Fact] + public void ConsumerPublishIncludesTheAppPriAndXbfSidecars() + { + var appDir = Path.Join(_tempRoot, "publish-consumer"); + Directory.CreateDirectory(appDir); + + WriteConsumerProject(appDir); + WriteConsumerProgram(appDir); + CreateNuGetConfig(appDir); + + var publishDir = Path.Join(appDir, "out-default"); + RunHelpers.RunDotnet( + $"publish -c Release -a {_fixture.RunArchitecture} -o \"{publishDir}\"", + appDir, + _fixture.CommandEnvironment, + timeoutMs: 600_000); + + Assert.True( + File.Exists(Path.Join(publishDir, "Consumer.pri")), + "The app's own .pri is missing from the publish output, so the published app would " + + "start with an empty resource dictionary and crash in native XAML (WindowsAppSDK#6394). " + + "Reactor's _ReactorCopyWinUIResourcesToPublish target is supposed to copy it."); + + Assert.True( + File.Exists(Path.Join(publishDir, "Reactor", "Hosting", "ReactorApplication.xbf")), + "ReactorApplication.xbf is missing from the publish output, and its relative " + + "Reactor/Hosting/ path must be preserved for ms-appx:/// to resolve it."); + + // Control: with the target disabled the file must disappear again. If it does not, + // something else is supplying it and the assertions above prove nothing about the target. + var optOutDir = Path.Join(appDir, "out-optout"); + RunHelpers.RunDotnet( + $"publish -c Release -a {_fixture.RunArchitecture} -o \"{optOutDir}\" -p:ReactorCopyWinUIResourcesToPublish=false", + appDir, + _fixture.CommandEnvironment, + timeoutMs: 600_000); + + Assert.False( + File.Exists(Path.Join(optOutDir, "Consumer.pri")), + "Opting out of _ReactorCopyWinUIResourcesToPublish still produced the app .pri, so the " + + "passing assertion above does not actually attribute the copy to Reactor's target."); + } + + private string[] ReadPackageEntries(string packageId) + { + var packagePath = Path.Join(_fixture.PackageSourceDir, $"{packageId}.{_fixture.PackageVersion}.nupkg"); + Assert.True(File.Exists(packagePath), $"Expected package '{packagePath}' to exist."); + + using var archive = ZipFile.OpenRead(packagePath); + return archive.Entries.Select(e => e.FullName).ToArray(); + } + + /// + /// References all three packages explicitly. Before the fix each contributed its own + /// makepri.exe Dump, so covering only the core package would leave the two whose PRIs + /// were empty — including the Devtools one with the longest, build-breaking file name — + /// untested. + /// + private void WriteConsumerProject(string appDir) + { + var csproj = $""" + + + Exe + net10.0-windows10.0.22621.0 + enable + true + true + {_fixture.RunArchitecture} + {_fixture.RunArchitecture} + win-{_fixture.RunArchitecture} + + + + + + + + """; + + File.WriteAllText(Path.Join(appDir, "Consumer.csproj"), csproj); + } + + private static void WriteConsumerProgram(string appDir) + { + // OutputType=Exe is what makes the Windows App SDK compute input PRIs at all + // (ShouldComputeInputPris), so the app must be an executable. It is never run here — + // this test is about the build, and CreateTemplateTests already covers running. + File.WriteAllText(Path.Join(appDir, "Program.cs"), """ + using static Microsoft.UI.Reactor.Factories; + + System.Console.WriteLine(TextBlock("hello") is not null); + + """); + } + + private void CreateNuGetConfig(string appDir) + { + RunHelpers.RunDotnet( + $"new nugetconfig --output \"{appDir}\" --force", + appDir, + _fixture.CommandEnvironment, + timeoutMs: 30_000); + + var configPath = Path.Join(appDir, "nuget.config"); + + RunHelpers.RunDotnet( + $"nuget config set globalPackagesFolder \"{_fixture.NugetPackagesDir}\" --configfile \"{configPath}\"", + appDir, + _fixture.CommandEnvironment, + timeoutMs: 30_000); + + RunHelpers.RunDotnet( + $"nuget add source \"{_fixture.PackageSourceDir}\" --name reactor-local --configfile \"{configPath}\"", + appDir, + _fixture.CommandEnvironment, + timeoutMs: 30_000); + } + + public void Dispose() + { + try + { + if (Directory.Exists(_tempRoot)) + { + Directory.Delete(_tempRoot, recursive: true); + } + } + catch + { + // Best-effort cleanup for temporary test artifacts. + } + } +} diff --git a/tests/aot_trim_proof/Reactor.AotHelloWorld.Advanced.Positive/Reactor.AotHelloWorld.Advanced.Positive.csproj b/tests/aot_trim_proof/Reactor.AotHelloWorld.Advanced.Positive/Reactor.AotHelloWorld.Advanced.Positive.csproj index 8f3ef70c0..feeb241db 100644 --- a/tests/aot_trim_proof/Reactor.AotHelloWorld.Advanced.Positive/Reactor.AotHelloWorld.Advanced.Positive.csproj +++ b/tests/aot_trim_proof/Reactor.AotHelloWorld.Advanced.Positive/Reactor.AotHelloWorld.Advanced.Positive.csproj @@ -27,17 +27,6 @@ true - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - diff --git a/tests/aot_trim_proof/Reactor.AotHelloWorld.Advanced/Reactor.AotHelloWorld.Advanced.csproj b/tests/aot_trim_proof/Reactor.AotHelloWorld.Advanced/Reactor.AotHelloWorld.Advanced.csproj index a2b252323..3302c3e3f 100644 --- a/tests/aot_trim_proof/Reactor.AotHelloWorld.Advanced/Reactor.AotHelloWorld.Advanced.csproj +++ b/tests/aot_trim_proof/Reactor.AotHelloWorld.Advanced/Reactor.AotHelloWorld.Advanced.csproj @@ -34,17 +34,6 @@ true - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - diff --git a/tests/aot_trim_proof/Reactor.AotHelloWorld/Reactor.AotHelloWorld.csproj b/tests/aot_trim_proof/Reactor.AotHelloWorld/Reactor.AotHelloWorld.csproj index e78a341e1..e17aeaa43 100644 --- a/tests/aot_trim_proof/Reactor.AotHelloWorld/Reactor.AotHelloWorld.csproj +++ b/tests/aot_trim_proof/Reactor.AotHelloWorld/Reactor.AotHelloWorld.csproj @@ -61,23 +61,6 @@ true - - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - - - - <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" /> - <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" - Condition="Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - - From 9784eea4a72ba71bd38dc53411da14d0e8ef224f Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Wed, 23 Sep 2026 16:33:36 -0700 Subject: [PATCH 02/20] docs: record EnableMsixTooling as an alternative, and its limits The upstream issue (WindowsAppSDK#6394) carries a community suggestion to set EnableMsixTooling=true instead of copying the resources by hand. Measured both claims rather than repeating them: - It DOES fix the publish gap: the app .pri lands in the publish output and the published app runs. It works as a side effect of importing the MSIX packaging targets, which also drops the reference-path arm of the PRI expansion (makepri.exe Dump calls halve on a normal build). - It does NOT fix issue #1271. Built against pre-fix packages at the failing 177-character geometry with and without the switch: identical failure, PRI175/PRI222 on Microsoft.UI.Reactor.Devtools.pri.xml. Library .pri files are still expanded, so keeping them out of lib/ remains necessary. Reactor does not set the property for consumers: whether an app wants MSIX packaging tooling is the app's decision, and this is a community workaround on an open issue rather than a documented contract. The shipped target is narrower and works whichever way an app sets it. Verified the two compose cleanly - no errors, no duplicated files, app runs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- docs/aot-support.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/aot-support.md b/docs/aot-support.md index e6b26db97..cba6e8f54 100644 --- a/docs/aot-support.md +++ b/docs/aot-support.md @@ -119,6 +119,12 @@ Opt out — for example if you drive the copy yourself — with: false ``` +**Alternative: `true`.** Suggested on the upstream issue, and it does work — measured: the app `.pri` lands in the publish output and the published app runs. It fixes the symptom as a side effect of importing the MSIX packaging targets (which also stops the reference-path arm of the PRI expansion from running, halving the `makepri.exe Dump` count on a normal build). + +Reactor does **not** set it for you. Whether an app wants MSIX packaging tooling is the app's decision, not a framework's, and this is a community workaround on an open issue rather than a documented contract. The shipped target is narrower, explicit, and works whichever way an app sets `EnableMsixTooling`. The two compose cleanly if you do enable it — verified: no errors, no duplicated files, app runs. + +**It does not fix issue #1271.** Measured against pre-fix packages at the failing geometry: with and without `EnableMsixTooling=true` the build fails identically with `PRI175` / `PRI222` on `Microsoft.UI.Reactor.Devtools.pri.xml`. Library `.pri` files still get expanded; keeping them out of `lib/` is a separate and still-necessary fix. + Remove the shipped target once #6394 closes. ## Debugging an AOT selftest hang From 9dd521f126db8686a4fee84e964877fe7d492b98 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Wed, 23 Sep 2026 20:11:22 -0700 Subject: [PATCH 03/20] test: isolate the publish cells and narrow the cleanup catch CI failure: ConsumerPublishIncludesTheAppPriAndXbfSidecars published the same project twice from one directory, once with an extra global property. That forces a full re-evaluation over the same obj/, and the second XAML compile failed with: WMC1006: Cannot resolve Assembly or Windows Metadata file '...\nuget-global-packages\microsoft.windowsappsdk.foundation\2.1.0\lib\...' That reference path is ~258 characters under the fixture's package folder and the XAML compiler is not long-path aware, so the first publish squeaked under the limit and the second did not. Give each cell its own project directory, shorten the temp-root and cell names, and reference only the core package for this test - the target under test ships there, and dropping Advanced (Win2D) and Devtools removes references the XAML compiler would otherwise resolve. Also address the code-quality finding: Dispose() caught every exception while cleaning up its temp directory. Narrow it to IOException and UnauthorizedAccessException, which is what Directory.Delete realistically throws here; anything else is a real bug and should surface. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- .../Packaging/PriPackagingTests.cs | 65 +++++++++++++------ 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs b/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs index 54c8cf36b..4a5ebb6cd 100644 --- a/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs +++ b/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs @@ -29,7 +29,7 @@ namespace Microsoft.UI.Reactor.IntegrationTests.Packaging; public sealed class PriPackagingTests : IDisposable { private readonly TemplatePackageTestFixture _fixture; - private readonly string _tempRoot = Path.Join(Path.GetTempPath(), $"reactor-pri-pkg-{Guid.NewGuid():N}"); + private readonly string _tempRoot = Path.Join(Path.GetTempPath(), $"rpri-{Guid.NewGuid():N}"); private static readonly string[] PackageIds = [ @@ -191,21 +191,38 @@ public void ConsumerBuildRunsNoMakepriDumpForReactorPackages() /// The opt-out property doubles as the control: it must reproduce the missing file. /// Without that half, a publish that happened to include the .pri for some unrelated /// reason would let this pass while the target did nothing. + /// + /// The two publishes deliberately run from SEPARATE project directories. Publishing the + /// same project twice with a different global property forces a full re-evaluation that reuses + /// the same obj/, and the second XAML compile then failed on CI with + /// WMC1006: Cannot resolve Assembly or Windows Metadata file — the reference path under + /// the fixture's package folder is ~258 characters, and the XAML compiler is not long-path + /// aware. One publish per directory stays clear of both. /// [Fact] public void ConsumerPublishIncludesTheAppPriAndXbfSidecars() { - var appDir = Path.Join(_tempRoot, "publish-consumer"); - Directory.CreateDirectory(appDir); - - WriteConsumerProject(appDir); - WriteConsumerProgram(appDir); - CreateNuGetConfig(appDir); + // Short directory names on purpose: these paths feed the XAML compiler, which fails + // around MAX_PATH, and the fixture's package folder already consumes most of the budget. + var onDir = Path.Join(_tempRoot, "on"); + var offDir = Path.Join(_tempRoot, "off"); + Directory.CreateDirectory(onDir); + Directory.CreateDirectory(offDir); + + foreach (var dir in new[] { onDir, offDir }) + { + // Core package only. The publish path is the one that runs the XAML compiler against + // the full reference closure, and dragging in Advanced (Win2D) and Devtools adds + // references without adding coverage — the target under test is in the core package. + WriteConsumerProject(dir, coreOnly: true); + WriteConsumerProgram(dir); + CreateNuGetConfig(dir); + } - var publishDir = Path.Join(appDir, "out-default"); + var publishDir = Path.Join(onDir, "pub"); RunHelpers.RunDotnet( $"publish -c Release -a {_fixture.RunArchitecture} -o \"{publishDir}\"", - appDir, + onDir, _fixture.CommandEnvironment, timeoutMs: 600_000); @@ -222,10 +239,10 @@ public void ConsumerPublishIncludesTheAppPriAndXbfSidecars() // Control: with the target disabled the file must disappear again. If it does not, // something else is supplying it and the assertions above prove nothing about the target. - var optOutDir = Path.Join(appDir, "out-optout"); + var optOutDir = Path.Join(offDir, "pub"); RunHelpers.RunDotnet( $"publish -c Release -a {_fixture.RunArchitecture} -o \"{optOutDir}\" -p:ReactorCopyWinUIResourcesToPublish=false", - appDir, + offDir, _fixture.CommandEnvironment, timeoutMs: 600_000); @@ -245,13 +262,22 @@ private string[] ReadPackageEntries(string packageId) } /// - /// References all three packages explicitly. Before the fix each contributed its own + /// References all three packages by default. Before the fix each contributed its own /// makepri.exe Dump, so covering only the core package would leave the two whose PRIs /// were empty — including the Devtools one with the longest, build-breaking file name — - /// untested. + /// untested. trims that down for the publish test, which does not + /// need them and is sensitive to how many references the XAML compiler has to resolve. /// - private void WriteConsumerProject(string appDir) + private void WriteConsumerProject(string appDir, bool coreOnly = false) { + var extraPackages = coreOnly + ? string.Empty + : $""" + + + + """; + var csproj = $""" @@ -265,9 +291,7 @@ private void WriteConsumerProject(string appDir) win-{_fixture.RunArchitecture} - - - + {extraPackages} """; @@ -320,9 +344,12 @@ public void Dispose() Directory.Delete(_tempRoot, recursive: true); } } - catch + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { - // Best-effort cleanup for temporary test artifacts. + // Best-effort cleanup for temporary test artifacts. These two cover what + // Directory.Delete realistically throws here (a file still held by a just-exited + // build process, or a read-only artifact); anything else is a real bug and + // should not be swallowed. } } } From 1e7f2de2272808b02a508f38c972bbf60d4f53ab Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Thu, 24 Sep 2026 13:14:13 -0700 Subject: [PATCH 04/20] docs: #6394's fix exists but is gated on EnableMsixTooling, not a version Checked the claim that WindowsAppSDK#6394 is fixed and that a newer SDK would let us drop the workaround. Both halves of that are worth recording. A first-party fix DOES exist: Microsoft.Windows.SDK.BuildTools.MSIX.Pri.targets defines AddProjectPriToResolvedFileToPublish and AddXbfFilesToResolvedFileToPublish, which add the project .pri and the .xbf files to @(ResolvedFileToPublish) - exactly the gap. But it is reachable only through EnableMsixTooling -> MsixPackageSupport -> ShouldImportMsixCommonTargets -> MsixCommonTargets -> Packaging.targets -> Pri.targets. With the flag off the build takes the MrtCore branch, which has no publish step at all. That is why the community EnableMsixTooling=true workaround works. So it is not version-gated and an SDK bump will not remove the need. Measured: - the fix targets are present in BOTH the MSIX build tools a current app resolves (1.7.251221100) and the newest published (1.7.260903100); - MsixPackageSupport is assigned in exactly one place, conditioned solely on EnableMsixTooling, and no Microsoft.WindowsAppSDK* package sets it (verified with a positive control so the empty result is a measurement, not a broken grep); - publishing the stock template on the newest stable SDK (metapackage 2.5.1, WinUI 2.3.9, Foundation 2.3.12) with Reactor's target disabled still produces no app .pri and still crashes 0xC000027B. Note the template already pins the newest stable SDK via its dotnet-new post-action, so earlier runs were on it too. Replace the vague "remove once #6394 closes" guidance in both the doc and the target's comment with the real signal: delete this when that fix runs WITHOUT EnableMsixTooling. PriPackagingTests' opt-out control starts failing when the platform begins doing the copy itself, so CI reports the moment it lands. Re-verified the shipped target after the edit: publish with it ON copies the .pri and the app runs; OFF reproduces the crash. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- docs/aot-support.md | 12 +++++++++--- src/Reactor/build/Reactor.targets | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/aot-support.md b/docs/aot-support.md index cba6e8f54..c14130209 100644 --- a/docs/aot-support.md +++ b/docs/aot-support.md @@ -119,13 +119,19 @@ Opt out — for example if you drive the copy yourself — with: false ``` -**Alternative: `true`.** Suggested on the upstream issue, and it does work — measured: the app `.pri` lands in the publish output and the published app runs. It fixes the symptom as a side effect of importing the MSIX packaging targets (which also stops the reference-path arm of the PRI expansion from running, halving the `makepri.exe Dump` count on a normal build). +**Alternative: `true`.** Suggested on the upstream issue, and it does work — measured: the app `.pri` lands in the publish output and the published app runs. -Reactor does **not** set it for you. Whether an app wants MSIX packaging tooling is the app's decision, not a framework's, and this is a community workaround on an open issue rather than a documented contract. The shipped target is narrower, explicit, and works whichever way an app sets `EnableMsixTooling`. The two compose cleanly if you do enable it — verified: no errors, no duplicated files, app runs. +It is worth knowing exactly *why*, because it tells you when this workaround can finally be deleted. **A first-party fix already exists**: `Microsoft.Windows.SDK.BuildTools.MSIX.Pri.targets` defines `AddProjectPriToResolvedFileToPublish` and `AddXbfFilesToResolvedFileToPublish`, which add the project `.pri` and the `.xbf` files to `@(ResolvedFileToPublish)` — precisely the gap. But that file is reached only through +`EnableMsixTooling` → `MsixPackageSupport` → `ShouldImportMsixCommonTargets` → `MsixCommonTargets` → `Packaging.targets` → `Pri.targets`. +With the flag off, the build takes the `MrtCore.targets` branch instead, which has no publish step at all. + +**So this is not version-gated, and upgrading the SDK will not remove the need for a workaround.** Measured across the whole chain: the fix targets are present in *both* the MSIX build tools a current app resolves (1.7.251221100) and the newest published (1.7.260903100); `MsixPackageSupport` is assigned in exactly one place, conditioned solely on `EnableMsixTooling`; and no `Microsoft.WindowsAppSDK*` package sets that property. Publishing the stock template on the newest stable SDK (metapackage 2.5.1, WinUI 2.3.9, Foundation 2.3.12) with Reactor's target disabled still produces no app `.pri` and still crashes with `0xC000027B`. + +Reactor does **not** set the flag for you. Whether an app wants MSIX packaging tooling is the app's decision, not a framework's, and it pulls in the whole single-project MSIX pipeline to fix one missing copy. The shipped target is narrower, explicit, and works whichever way an app sets `EnableMsixTooling`. The two compose cleanly if you do enable it — verified: no errors, no duplicated files, app runs. **It does not fix issue #1271.** Measured against pre-fix packages at the failing geometry: with and without `EnableMsixTooling=true` the build fails identically with `PRI175` / `PRI222` on `Microsoft.UI.Reactor.Devtools.pri.xml`. Library `.pri` files still get expanded; keeping them out of `lib/` is a separate and still-necessary fix. -Remove the shipped target once #6394 closes. +**When to delete the shipped target.** Not "when #6394 closes" — the issue can close on the grounds that the fix already exists. Delete it when `AddProjectPriToResolvedFileToPublish` runs for an unpackaged app that has *not* set `EnableMsixTooling`, i.e. when the MSIX build tools import that target outside the `MsixPackageSupport` gate. The `ConsumerPublishIncludesTheAppPriAndXbfSidecars` test in `tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs` will tell you: its opt-out control asserts the `.pri` is *absent* when Reactor's target is disabled, so it starts failing the moment the platform begins doing this itself. ## Debugging an AOT selftest hang diff --git a/src/Reactor/build/Reactor.targets b/src/Reactor/build/Reactor.targets index 15e504591..f33b8bc8e 100644 --- a/src/Reactor/build/Reactor.targets +++ b/src/Reactor/build/Reactor.targets @@ -193,7 +193,20 @@ loses the .pri too. Opt out with false. - Remove this target once WindowsAppSDK#6394 ships a fix. + + NOT version-gated, so do not wait for an SDK bump to delete this. A first-party fix + already exists — MSIX.Pri.targets' AddProjectPriToResolvedFileToPublish / + AddXbfFilesToResolvedFileToPublish — but it is reachable only via + EnableMsixTooling -> MsixPackageSupport -> ShouldImportMsixCommonTargets -> + Packaging.targets -> Pri.targets. With the flag off the build takes the MrtCore + branch, which has no publish step. Measured: those targets are present in both the + MSIX build tools a current app resolves (1.7.251221100) and the newest published + (1.7.260903100); MsixPackageSupport is assigned in exactly one place, conditioned + solely on EnableMsixTooling; and no Microsoft.WindowsAppSDK* package sets it. The + stock template on the newest stable SDK (2.5.1 / WinUI 2.3.9 / Foundation 2.3.12) + still publishes without its .pri and still crashes 0xC000027B when this target is + disabled. Delete this only once that fix runs WITHOUT EnableMsixTooling; the + opt-out control in PriPackagingTests starts failing when that happens. Note the Exclude: $(PublishDir) is nested INSIDE $(OutputPath) by default (bin\\\\publish\), so globbing $(OutputPath)**\*.xbf without it would From 832178f83bca7cbe67159b3d3ce353f13956e7ae Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Thu, 24 Sep 2026 13:18:50 -0700 Subject: [PATCH 05/20] docs: correct which build-tools version adds which half of the #6394 fix Follow-up after checking the claim that Microsoft.Windows.SDK.BuildTools.MSIX 1.7.260903100 specifically carries the fix. It does carry a real one, and the previous commit undersold that: AddXbfFilesToResolvedFileToPublish is genuinely NEW in 1.7.260903100. Only the .pri half (AddProjectPriToResolvedFileToPublish) predates it in 1.7.251221100. The conclusion is unchanged, because both targets live in Pri.targets, which is imported only behind the EnableMsixTooling gate. Measured directly rather than inferred from the import chain this time - pinned 1.7.260903100 and published with -v:detailed, counting how many times each target actually executes: no EnableMsixTooling : AddProjectPri=0, AddXbf=0, app .pri NOT published EnableMsixTooling=true: AddProjectPri=2, AddXbf=2, app .pri published The second cell is the control that proves the probe can match, so the zeroes in the first are a measurement rather than a broken grep. So upgrading the build tools alone still does not remove the need for the workaround; the fix has to leave the EnableMsixTooling gate first. Record the per-version detail so nobody has to re-derive which half landed when. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- docs/aot-support.md | 13 ++++++++++--- src/Reactor/build/Reactor.targets | 21 ++++++++++++--------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/docs/aot-support.md b/docs/aot-support.md index c14130209..496a5d7bf 100644 --- a/docs/aot-support.md +++ b/docs/aot-support.md @@ -121,11 +121,18 @@ Opt out — for example if you drive the copy yourself — with: **Alternative: `true`.** Suggested on the upstream issue, and it does work — measured: the app `.pri` lands in the publish output and the published app runs. -It is worth knowing exactly *why*, because it tells you when this workaround can finally be deleted. **A first-party fix already exists**: `Microsoft.Windows.SDK.BuildTools.MSIX.Pri.targets` defines `AddProjectPriToResolvedFileToPublish` and `AddXbfFilesToResolvedFileToPublish`, which add the project `.pri` and the `.xbf` files to `@(ResolvedFileToPublish)` — precisely the gap. But that file is reached only through +It is worth knowing exactly *why*, because it tells you when this workaround can finally be deleted. **A first-party fix already exists**: `Microsoft.Windows.SDK.BuildTools.MSIX.Pri.targets` defines `AddProjectPriToResolvedFileToPublish` and `AddXbfFilesToResolvedFileToPublish`, which add the project `.pri` and the `.xbf` files to `@(ResolvedFileToPublish)` — precisely the gap. The XBF half is genuinely new in build tools **1.7.260903100**; the `.pri` half was already there in 1.7.251221100. But both live in a file reached only through `EnableMsixTooling` → `MsixPackageSupport` → `ShouldImportMsixCommonTargets` → `MsixCommonTargets` → `Packaging.targets` → `Pri.targets`. -With the flag off, the build takes the `MrtCore.targets` branch instead, which has no publish step at all. +With the flag off the build takes the `MrtCore.targets` branch instead, which has no publish step at all. -**So this is not version-gated, and upgrading the SDK will not remove the need for a workaround.** Measured across the whole chain: the fix targets are present in *both* the MSIX build tools a current app resolves (1.7.251221100) and the newest published (1.7.260903100); `MsixPackageSupport` is assigned in exactly one place, conditioned solely on `EnableMsixTooling`; and no `Microsoft.WindowsAppSDK*` package sets that property. Publishing the stock template on the newest stable SDK (metapackage 2.5.1, WinUI 2.3.9, Foundation 2.3.12) with Reactor's target disabled still produces no app `.pri` and still crashes with `0xC000027B`. +**So this is not version-gated, and upgrading the SDK will not remove the need for a workaround.** Measured on the newest build tools (1.7.260903100) with a `-v:detailed` publish, counting how many times each fix target actually executes: + +| cell | `AddProjectPriToResolvedFileToPublish` | `AddXbfFilesToResolvedFileToPublish` | app `.pri` published | +|---|---|---|---| +| no `EnableMsixTooling` | 0 | 0 | **no** | +| `EnableMsixTooling=true` | 2 | 2 | yes | + +The second row is the control: it proves the probe can match, so the zeroes in the first row are a measurement rather than a broken grep. Corroborating the same conclusion structurally: `MsixPackageSupport` is assigned in exactly one place, conditioned solely on `EnableMsixTooling`, and no `Microsoft.WindowsAppSDK*` package sets that property. Publishing the stock template on the newest stable SDK (metapackage 2.5.1, WinUI 2.3.9, Foundation 2.3.12) with Reactor's target disabled still produces no app `.pri` and still crashes with `0xC000027B`. Reactor does **not** set the flag for you. Whether an app wants MSIX packaging tooling is the app's decision, not a framework's, and it pulls in the whole single-project MSIX pipeline to fix one missing copy. The shipped target is narrower, explicit, and works whichever way an app sets `EnableMsixTooling`. The two compose cleanly if you do enable it — verified: no errors, no duplicated files, app runs. diff --git a/src/Reactor/build/Reactor.targets b/src/Reactor/build/Reactor.targets index f33b8bc8e..0a48180ce 100644 --- a/src/Reactor/build/Reactor.targets +++ b/src/Reactor/build/Reactor.targets @@ -196,17 +196,20 @@ NOT version-gated, so do not wait for an SDK bump to delete this. A first-party fix already exists — MSIX.Pri.targets' AddProjectPriToResolvedFileToPublish / - AddXbfFilesToResolvedFileToPublish — but it is reachable only via + AddXbfFilesToResolvedFileToPublish (the XBF half is new in build tools + 1.7.260903100; the .pri half predates it) — but both are reachable only via EnableMsixTooling -> MsixPackageSupport -> ShouldImportMsixCommonTargets -> Packaging.targets -> Pri.targets. With the flag off the build takes the MrtCore - branch, which has no publish step. Measured: those targets are present in both the - MSIX build tools a current app resolves (1.7.251221100) and the newest published - (1.7.260903100); MsixPackageSupport is assigned in exactly one place, conditioned - solely on EnableMsixTooling; and no Microsoft.WindowsAppSDK* package sets it. The - stock template on the newest stable SDK (2.5.1 / WinUI 2.3.9 / Foundation 2.3.12) - still publishes without its .pri and still crashes 0xC000027B when this target is - disabled. Delete this only once that fix runs WITHOUT EnableMsixTooling; the - opt-out control in PriPackagingTests starts failing when that happens. + branch, which has no publish step. Measured on 1.7.260903100 with a -v:detailed + publish: without the flag each fix target runs 0 times and no app .pri is + published; with EnableMsixTooling=true each runs twice and the .pri appears — the + second cell being the control that proves the probe can match. MsixPackageSupport + is also assigned in exactly one place, conditioned solely on EnableMsixTooling, + and no Microsoft.WindowsAppSDK* package sets it. The stock template on the newest + stable SDK (2.5.1 / WinUI 2.3.9 / Foundation 2.3.12) still publishes without its + .pri and still crashes 0xC000027B when this target is disabled. Delete this only + once that fix runs WITHOUT EnableMsixTooling; the opt-out control in + PriPackagingTests starts failing when that happens. Note the Exclude: $(PublishDir) is nested INSIDE $(OutputPath) by default (bin\\\\publish\), so globbing $(OutputPath)**\*.xbf without it would From cd353b759ab29a8bac41d430d286b4ac49957793 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Thu, 24 Sep 2026 13:50:02 -0700 Subject: [PATCH 06/20] docs: packaged apps DO set EnableMsixTooling; the two paths are complementary Correcting an overstatement. Saying "apps do not set EnableMsixTooling" was too broad: packaged (MSIX) WinUI apps normally DO - Visual Studio's packaged template sets it, and so does every MSIX project in this repo. That does not change the conclusion, it explains the target's scoping. The two populations do not overlap: unpackaged (WindowsPackageType=None) -> flag off -> platform fix does NOT run -> Reactor's target runs packaged (WindowsPackageType=MSIX) -> flag on -> platform fix runs -> Reactor's target excluded by AppxPackage/WindowsPackageType Verified on samples/ReactorGallery, which builds both ways. Evaluated with dotnet msbuild -getProperty rather than read off the file: default mode reports EnableMsixTooling=false with MsixPackageSupport empty; -p:GalleryPackaged=true reports true/true with AppxPackage=true. Publishing it unpackaged, the platform's AddProjectPriToResolvedFileToPublish and AddXbfFilesToResolvedFileToPublish run 0 times, Reactor's target runs, the app .pri is published, and the build reports 0 errors. Worth recording because I got this wrong first by grepping the csproj: the WindowsPackageType=None and EnableMsixTooling=true lines sit in different Condition'ed PropertyGroups, so a naive read makes it look like an unpackaged app with the flag on. They never co-exist. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- docs/aot-support.md | 9 +++++++++ src/Reactor/build/Reactor.targets | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/aot-support.md b/docs/aot-support.md index 496a5d7bf..021b54a0e 100644 --- a/docs/aot-support.md +++ b/docs/aot-support.md @@ -136,6 +136,15 @@ The second row is the control: it proves the probe can match, so the zeroes in t Reactor does **not** set the flag for you. Whether an app wants MSIX packaging tooling is the app's decision, not a framework's, and it pulls in the whole single-project MSIX pipeline to fix one missing copy. The shipped target is narrower, explicit, and works whichever way an app sets `EnableMsixTooling`. The two compose cleanly if you do enable it — verified: no errors, no duplicated files, app runs. +**Packaged apps are a different population, and they are already covered.** A *packaged* (MSIX) WinUI app normally does set `EnableMsixTooling=true` — Visual Studio's packaged WinUI template does, and so does every MSIX project in this repo. Those apps therefore get the platform's own fix, and Reactor's target deliberately excludes them (`AppxPackage != true`, `WindowsPackageType != MSIX`). The two paths are complementary rather than overlapping: + +| app shape | `EnableMsixTooling` | platform fix runs | Reactor's target runs | +|---|---|---|---| +| unpackaged (`WindowsPackageType=None`) | off | no | **yes** | +| packaged (`WindowsPackageType=MSIX`) | on | yes | no (excluded) | + +`samples/ReactorGallery` demonstrates both halves, because it builds either way: its default is unpackaged, and `-p:GalleryPackaged=true` flips it to MSIX **and** sets `EnableMsixTooling=true` in the same conditional block. Evaluated with `dotnet msbuild -getProperty`, the unpackaged mode reports `EnableMsixTooling=false` with an empty `MsixPackageSupport`, and the packaged mode reports `true` / `true` / `AppxPackage=true`. Publishing it unpackaged, the platform's fix targets run 0 times, Reactor's target runs, and the app `.pri` is published. Note the two settings never co-exist — reading the raw csproj can suggest otherwise, since the `WindowsPackageType=None` and `EnableMsixTooling=true` lines live in different `Condition`ed `PropertyGroup`s. + **It does not fix issue #1271.** Measured against pre-fix packages at the failing geometry: with and without `EnableMsixTooling=true` the build fails identically with `PRI175` / `PRI222` on `Microsoft.UI.Reactor.Devtools.pri.xml`. Library `.pri` files still get expanded; keeping them out of `lib/` is a separate and still-necessary fix. **When to delete the shipped target.** Not "when #6394 closes" — the issue can close on the grounds that the fix already exists. Delete it when `AddProjectPriToResolvedFileToPublish` runs for an unpackaged app that has *not* set `EnableMsixTooling`, i.e. when the MSIX build tools import that target outside the `MsixPackageSupport` gate. The `ConsumerPublishIncludesTheAppPriAndXbfSidecars` test in `tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs` will tell you: its opt-out control asserts the `.pri` is *absent* when Reactor's target is disabled, so it starts failing the moment the platform begins doing this itself. diff --git a/src/Reactor/build/Reactor.targets b/src/Reactor/build/Reactor.targets index 0a48180ce..37ec46134 100644 --- a/src/Reactor/build/Reactor.targets +++ b/src/Reactor/build/Reactor.targets @@ -194,6 +194,15 @@ Opt out with false. + Scoped to UNPACKAGED executables on purpose. A packaged (MSIX) app normally sets + EnableMsixTooling=true - VS's packaged WinUI template does, as does every MSIX + project in this repo - so it already gets the platform's own fix and is excluded + here by AppxPackage/WindowsPackageType. The two populations are complementary, not + overlapping. samples/ReactorGallery exercises both: unpackaged by default, + -p:GalleryPackaged=true flips it to MSIX and sets EnableMsixTooling in the same + conditional block (the two settings never co-exist, though reading the raw csproj + can suggest they do). + NOT version-gated, so do not wait for an SDK bump to delete this. A first-party fix already exists — MSIX.Pri.targets' AddProjectPriToResolvedFileToPublish / AddXbfFilesToResolvedFileToPublish (the XBF half is new in build tools From f6fa64c4273d478ff8e9677f25c442f931c59e4b Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Thu, 24 Sep 2026 14:22:47 -0700 Subject: [PATCH 07/20] docs: trim the CHANGELOG to the two user-facing changes The Fixed entries for this PR had grown to four entries over ~47 lines, against a repo median of 6 lines per entry. Most of that was intermediate work on the way to the fix rather than anything a consumer experiences. Cut to the two changes a consumer actually sees: - packages no longer ship a .pri, so builds stop failing on deep paths (#1271) - published unpackaged apps no longer start with an empty resource dictionary Folded the "two lib/ folders for one target framework" entry into the first as a single clause - it is a packaging-shape detail that never produced a failure, since NuGet happened to select the right group. Dropped the "dotnet pack silently omits ReactorApplication.xbf" entry entirely. It was a real bug in our own pack wiring, but no consumer ever received a broken package from it: the .pri shipped an embedded copy of the same file, so the gap only became load-bearing once this PR removed the .pri. It is a contributor-facing robustness fix, already documented in the csproj comment and spec 022. Net: 47 lines -> 17, with the two remaining entries at 9 and 8 lines. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- CHANGELOG.md | 65 +++++++++++++++------------------------------------- 1 file changed, 18 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf079f9b5..2fc6fc714 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,53 +79,24 @@ Conventions for contributors: is what actually rejects them. Found by requiring a compiled gallery card for every API the skills demonstrate. -- **Reactor packages no longer ship a `.pri`, so a consuming app builds at path depths - where it previously failed (issue #1271).** `Microsoft.UI.Reactor`, - `.Advanced` and `.Devtools` each packed their generated resource index beside the - assembly in `lib//`. `ResolveAssemblyReference` relates a same-base-name `.pri` - to its reference, so the Windows App SDK's `AddPriPayloadFilesToCopyToOutputDirectoryItems` - ran `makepri.exe Dump` on all three, writing into `$(IntermediateOutputPath)`. Those - output paths break past `MAX_PATH`, and `Microsoft.UI.Reactor.Devtools.pri.xml` — the - longest of the names — was what tipped a borderline project over, failing the build with - `PRI175` / `PRI222` / `APPX0002` at a depth where the equivalent XAML app built fine. - - Nothing was lost: the `.Advanced` and `.Devtools` indexes were entirely empty, and the - core one held a single embedded copy of `ReactorApplication.xbf` that also ships loose. - Measured on a 261-character output path, the build goes from failing to clean, and a - consumer build's `makepri.exe Dump` invocations for Reactor packages drop from six to - zero — leaving only the Windows App SDK's own framework indexes, exactly as for a plain - WinUI app. `.pri` *generation* is unchanged, so `ProjectReference` consumers are - unaffected. - -- **`Microsoft.UI.Reactor` no longer ships two `lib/` folders for the same target framework.** - `ReactorApplication.xbf` was packed with a literal `lib\$(TargetFramework)\…` path, but - `$(TargetFramework)` is `net10.0-windows10.0.22621.0` while NuGet shortens the build-output - folder to `net10.0-windows10.0.22621`. The package therefore carried two sibling `lib` groups - for one framework — one with the assemblies, one holding nothing but the stray sidecar — and - NuGet selected between them by ordering rather than by rule. The sidecar is now emitted as - build output with a `TargetPath`, so NuGet places it beside `Reactor.dll` in the single - normalized folder. Visible in 0.1.0-preview.16 and earlier. - -- **`dotnet pack` from a clean tree no longer silently omits `ReactorApplication.xbf`.** - It was packed by a `` item guarded with `Exists()`, which MSBuild evaluates before - the build that produces the file — so the sidecar appeared only when a previous build had - already left one in `bin/`. It is now resolved at pack time, and its absence is a hard - error rather than a silently incomplete package. - -- **Published apps no longer start with an empty resource dictionary - ([WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394)).** The Windows - App SDK generates an unpackaged app's `.pri` (and the XAML `.xbf` files) into the build output - but never copies them to the publish directory, because the MakePRI publish stage is gated on - `AppxPackage=true`. The published app then resolves no theme resources and dies at startup with - `0xC000027B` inside native XAML. Reactor now ships - `_ReactorCopyWinUIResourcesToPublish` in `build/Microsoft.UI.Reactor.targets`, so every consumer - gets the fix automatically — opt out with - `false`. - - This replaces the `_CopyWinUIResourcesForAot` snippet that sample and test projects each carried - (now removed from all 11 of them). That snippet was gated on `PublishAot=true`, but the gap is - **not** AOT-specific — a plain `dotnet publish -c Release` loses the `.pri` too, which is why - apps that never opted into AOT were affected as well. +- **Reactor packages no longer ship a `.pri`, so consuming apps build at path depths where + they previously failed (issue #1271).** The index sat beside the assembly in `lib/`, where + `ResolveAssemblyReference` treats it as a reference-related file and the Windows App SDK + expands it with `makepri.exe Dump` into `$(IntermediateOutputPath)` — a path that breaks + past `MAX_PATH` and fails the build with `PRI175` / `PRI222` / `APPX0002`. Nothing is lost: + the `.Advanced` and `.Devtools` indexes were empty and the core one only embedded a second + copy of `ReactorApplication.xbf`, which still ships loose. `.pri` *generation* is unchanged, + so `ProjectReference` consumers are unaffected. The core package now also exposes one + `lib/` folder instead of two for the same target framework. + +- **Published unpackaged apps no longer start with an empty resource dictionary + ([WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394)).** The + Windows App SDK leaves an unpackaged app's own `.pri` and `.xbf` out of the publish + output, so the app resolved no theme resources and died at startup with `0xC000027B` + inside native XAML. Reactor's targets now copy them, for AOT and non-AOT publishes alike + — opt out with `false`. + Replaces the per-project `_CopyWinUIResourcesForAot` snippet (removed from 11 projects), + which only covered `PublishAot=true` builds. ### Security From bc11bf41c0f27425b1783187299dd204ca396ebd Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Thu, 24 Sep 2026 14:38:33 -0700 Subject: [PATCH 08/20] test: assert a consumer-owned .xbf; docs: reconcile the closed #6394 Two findings from the Copilot review, both valid. 1. The publish test's XBF assertion was vacuous. It checked for ReactorApplication.xbf, which the package already delivers through a item, so deleting the target's $(OutputPath)**\*.xbf glob would have left it green. Confirmed against my own earlier data, where the opt-out cell still reported that file present. The consumer now compiles its own Themes/ConsumerStyles.xaml, and the test asserts on the resulting Themes/ConsumerStyles.xbf. Measured both cells: target ON -> Consumer.pri True, Themes/ConsumerStyles.xbf True target OFF -> Consumer.pri False, Themes/ConsumerStyles.xbf False (ReactorApplication.xbf is True in BOTH, which is exactly why it was the wrong file to assert on.) The subfolder also covers the copy's RecursiveDir handling. 2. WindowsAppSDK#6394 was closed as fixed 19 minutes after I last checked it, in MSIX build tools 1.7.260903100 - the version flagged for review. The docs still said OPEN and claimed categorically that no upgrade could remove the workaround. Both are now corrected, and the apparent contradiction is resolved rather than restated: the fix is real, but lives behind the EnableMsixTooling gate, and the official WinUI templates set that flag by default. So the mainstream WinUI app IS fixed by upgrading; Reactor's template is the case outside it, shipping WindowsPackageType=None without the flag. That also corrects my earlier overstatement that apps do not set the flag - the official template sets it unconditionally. The discriminator is the flag, not packaged vs unpackaged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- docs/aot-support.md | 22 ++++++---- src/Reactor/build/Reactor.targets | 27 ++++++------ .../Packaging/PriPackagingTests.cs | 41 ++++++++++++++++--- 3 files changed, 61 insertions(+), 29 deletions(-) diff --git a/docs/aot-support.md b/docs/aot-support.md index 021b54a0e..b315a8986 100644 --- a/docs/aot-support.md +++ b/docs/aot-support.md @@ -109,7 +109,7 @@ The missing `.pri` has cascading runtime symptoms that look like separate bugs b - `Application.Current.Resources` loads as an empty dictionary (`0` merged, `0` themed, `0` keys) because `Application.LoadComponent` cascades through MRT for type-info lookups. - Every `ThemeRef.Resolve(...)` returns `null`; `.Foreground(Theme.X)` falls back to control defaults. -Upstream tracking: [microsoft/WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394) — still OPEN against 1.8 and 2.0. +Upstream tracking: [microsoft/WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394) — **CLOSED as fixed** in `Microsoft.Windows.SDK.BuildTools.MSIX` **1.7.260903100** (2026-09-24). Read the next section before concluding this no longer affects you: the fix is real, but it only runs for apps that enable the MSIX tooling, which Reactor's unpackaged template deliberately does not. **Nothing to do — Reactor ships the workaround.** `build/Microsoft.UI.Reactor.targets` defines `_ReactorCopyWinUIResourcesToPublish`, which runs after `Publish` for any unpackaged Reactor executable and copies the project `.pri` plus every `.xbf` into `$(PublishDir)`. It applies to AOT and non-AOT publishes alike, so app projects no longer carry a per-project copy of this target (they used to; it was gated on `PublishAot=true` and therefore missed the plain-publish case). @@ -121,33 +121,37 @@ Opt out — for example if you drive the copy yourself — with: **Alternative: `true`.** Suggested on the upstream issue, and it does work — measured: the app `.pri` lands in the publish output and the published app runs. -It is worth knowing exactly *why*, because it tells you when this workaround can finally be deleted. **A first-party fix already exists**: `Microsoft.Windows.SDK.BuildTools.MSIX.Pri.targets` defines `AddProjectPriToResolvedFileToPublish` and `AddXbfFilesToResolvedFileToPublish`, which add the project `.pri` and the `.xbf` files to `@(ResolvedFileToPublish)` — precisely the gap. The XBF half is genuinely new in build tools **1.7.260903100**; the `.pri` half was already there in 1.7.251221100. But both live in a file reached only through +**The upstream fix, and why it does not cover us.** `Microsoft.Windows.SDK.BuildTools.MSIX.Pri.targets` defines `AddProjectPriToResolvedFileToPublish` and `AddXbfFilesToResolvedFileToPublish`, which add the project `.pri` and the `.xbf` files to `@(ResolvedFileToPublish)` — precisely the gap. The XBF half is what landed in **1.7.260903100** and closed the issue; the `.pri` half was already in 1.7.251221100. Both live in a file reached only through `EnableMsixTooling` → `MsixPackageSupport` → `ShouldImportMsixCommonTargets` → `MsixCommonTargets` → `Packaging.targets` → `Pri.targets`. -With the flag off the build takes the `MrtCore.targets` branch instead, which has no publish step at all. +With that flag off the build takes the `MrtCore.targets` branch instead, which has no publish step at all. -**So this is not version-gated, and upgrading the SDK will not remove the need for a workaround.** Measured on the newest build tools (1.7.260903100) with a `-v:detailed` publish, counting how many times each fix target actually executes: +That gate is not an oversight, and the upstream "fixed" is not wrong: **the official WinUI templates set `true` by default**, so the mainstream WinUI app is genuinely fixed by upgrading. Reactor's template is the case that falls outside it — it ships `WindowsPackageType=None` for xcopy deployment and does not enable the MSIX tooling, so the platform's fix never runs. + +Measured on 1.7.260903100 with a `-v:detailed` publish, counting how many times each fix target actually executes: | cell | `AddProjectPriToResolvedFileToPublish` | `AddXbfFilesToResolvedFileToPublish` | app `.pri` published | |---|---|---|---| | no `EnableMsixTooling` | 0 | 0 | **no** | | `EnableMsixTooling=true` | 2 | 2 | yes | -The second row is the control: it proves the probe can match, so the zeroes in the first row are a measurement rather than a broken grep. Corroborating the same conclusion structurally: `MsixPackageSupport` is assigned in exactly one place, conditioned solely on `EnableMsixTooling`, and no `Microsoft.WindowsAppSDK*` package sets that property. Publishing the stock template on the newest stable SDK (metapackage 2.5.1, WinUI 2.3.9, Foundation 2.3.12) with Reactor's target disabled still produces no app `.pri` and still crashes with `0xC000027B`. +The second row is the control: it proves the probe can match, so the zeroes in the first row are a measurement rather than a broken grep. Publishing the stock `reactorapp` template on the newest stable SDK (metapackage 2.5.1, WinUI 2.3.9, Foundation 2.3.12) with Reactor's target disabled still produces no app `.pri` and still crashes with `0xC000027B`. + +**So upgrading the build tools does not by itself remove the need for this workaround** — for an app in Reactor's default shape. Two things would: enabling `EnableMsixTooling` in your own project, or the platform moving those targets outside the gate. Reactor does **not** set the flag for you. Whether an app wants MSIX packaging tooling is the app's decision, not a framework's, and it pulls in the whole single-project MSIX pipeline to fix one missing copy. The shipped target is narrower, explicit, and works whichever way an app sets `EnableMsixTooling`. The two compose cleanly if you do enable it — verified: no errors, no duplicated files, app runs. -**Packaged apps are a different population, and they are already covered.** A *packaged* (MSIX) WinUI app normally does set `EnableMsixTooling=true` — Visual Studio's packaged WinUI template does, and so does every MSIX project in this repo. Those apps therefore get the platform's own fix, and Reactor's target deliberately excludes them (`AppxPackage != true`, `WindowsPackageType != MSIX`). The two paths are complementary rather than overlapping: +**The discriminator is the flag, not the packaging kind.** Visual Studio's WinUI templates set `EnableMsixTooling=true` and every MSIX project in this repo does too, so those apps get the platform's own fix; Reactor's target deliberately excludes packaged ones (`AppxPackage != true`, `WindowsPackageType != MSIX`) and is a harmless no-op for an unpackaged app that opts the flag on. The two paths cover complementary populations: | app shape | `EnableMsixTooling` | platform fix runs | Reactor's target runs | |---|---|---|---| -| unpackaged (`WindowsPackageType=None`) | off | no | **yes** | +| unpackaged, flag off (Reactor's template default) | off | no | **yes** | | packaged (`WindowsPackageType=MSIX`) | on | yes | no (excluded) | -`samples/ReactorGallery` demonstrates both halves, because it builds either way: its default is unpackaged, and `-p:GalleryPackaged=true` flips it to MSIX **and** sets `EnableMsixTooling=true` in the same conditional block. Evaluated with `dotnet msbuild -getProperty`, the unpackaged mode reports `EnableMsixTooling=false` with an empty `MsixPackageSupport`, and the packaged mode reports `true` / `true` / `AppxPackage=true`. Publishing it unpackaged, the platform's fix targets run 0 times, Reactor's target runs, and the app `.pri` is published. Note the two settings never co-exist — reading the raw csproj can suggest otherwise, since the `WindowsPackageType=None` and `EnableMsixTooling=true` lines live in different `Condition`ed `PropertyGroup`s. +`samples/ReactorGallery` demonstrates both, because it builds either way: its default is unpackaged, and `-p:GalleryPackaged=true` flips it to MSIX **and** sets `EnableMsixTooling=true` in the same conditional block. Evaluated with `dotnet msbuild -getProperty`, the unpackaged mode reports `EnableMsixTooling=false` with an empty `MsixPackageSupport`, and the packaged mode reports `true` / `true` / `AppxPackage=true`. Publishing it unpackaged, the platform's fix targets run 0 times, Reactor's target runs, and the app `.pri` is published. Note the two settings never co-exist — reading the raw csproj can suggest otherwise, since the `WindowsPackageType=None` and `EnableMsixTooling=true` lines live in different `Condition`ed `PropertyGroup`s. **It does not fix issue #1271.** Measured against pre-fix packages at the failing geometry: with and without `EnableMsixTooling=true` the build fails identically with `PRI175` / `PRI222` on `Microsoft.UI.Reactor.Devtools.pri.xml`. Library `.pri` files still get expanded; keeping them out of `lib/` is a separate and still-necessary fix. -**When to delete the shipped target.** Not "when #6394 closes" — the issue can close on the grounds that the fix already exists. Delete it when `AddProjectPriToResolvedFileToPublish` runs for an unpackaged app that has *not* set `EnableMsixTooling`, i.e. when the MSIX build tools import that target outside the `MsixPackageSupport` gate. The `ConsumerPublishIncludesTheAppPriAndXbfSidecars` test in `tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs` will tell you: its opt-out control asserts the `.pri` is *absent* when Reactor's target is disabled, so it starts failing the moment the platform begins doing this itself. +**When to delete the shipped target.** #6394 is already closed, so "wait for the issue" is not the signal — it closed while the gate was still in place. Delete this when `AddProjectPriToResolvedFileToPublish` runs for an unpackaged app that has *not* set `EnableMsixTooling`, i.e. when the MSIX build tools import those targets outside the `MsixPackageSupport` gate (or if Reactor's template ever adopts the flag itself). The `ConsumerPublishIncludesTheAppPriAndXbfSidecars` test in `tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs` will tell you: its opt-out control asserts the app `.pri` and the consumer's own `.xbf` are *absent* when Reactor's target is disabled, so it starts failing the moment the platform begins doing this itself. ## Debugging an AOT selftest hang diff --git a/src/Reactor/build/Reactor.targets b/src/Reactor/build/Reactor.targets index 37ec46134..9e7417816 100644 --- a/src/Reactor/build/Reactor.targets +++ b/src/Reactor/build/Reactor.targets @@ -203,22 +203,19 @@ conditional block (the two settings never co-exist, though reading the raw csproj can suggest they do). - NOT version-gated, so do not wait for an SDK bump to delete this. A first-party fix - already exists — MSIX.Pri.targets' AddProjectPriToResolvedFileToPublish / - AddXbfFilesToResolvedFileToPublish (the XBF half is new in build tools - 1.7.260903100; the .pri half predates it) — but both are reachable only via + NOT removed by an SDK upgrade alone. WindowsAppSDK#6394 is CLOSED, fixed in MSIX + build tools 1.7.260903100 by Pri.targets' AddProjectPriToResolvedFileToPublish / + AddXbfFilesToResolvedFileToPublish. That fix is real but reachable only via EnableMsixTooling -> MsixPackageSupport -> ShouldImportMsixCommonTargets -> - Packaging.targets -> Pri.targets. With the flag off the build takes the MrtCore - branch, which has no publish step. Measured on 1.7.260903100 with a -v:detailed - publish: without the flag each fix target runs 0 times and no app .pri is - published; with EnableMsixTooling=true each runs twice and the .pri appears — the - second cell being the control that proves the probe can match. MsixPackageSupport - is also assigned in exactly one place, conditioned solely on EnableMsixTooling, - and no Microsoft.WindowsAppSDK* package sets it. The stock template on the newest - stable SDK (2.5.1 / WinUI 2.3.9 / Foundation 2.3.12) still publishes without its - .pri and still crashes 0xC000027B when this target is disabled. Delete this only - once that fix runs WITHOUT EnableMsixTooling; the opt-out control in - PriPackagingTests starts failing when that happens. + Packaging.targets -> Pri.targets; with the flag off the build takes the MrtCore + branch, which has no publish step. The official WinUI templates set + EnableMsixTooling=true, so the mainstream app IS fixed by upgrading - Reactor's + template is the case outside it, shipping WindowsPackageType=None without the flag. + Measured on 1.7.260903100 with a -v:detailed publish: without the flag each fix + target runs 0 times and no app .pri is published; with it, each runs twice and the + .pri appears (that second cell being the control proving the probe can match). + Delete this once those targets run WITHOUT EnableMsixTooling, or if the template + adopts the flag; the opt-out control in PriPackagingTests fails when that happens. Note the Exclude: $(PublishDir) is nested INSIDE $(OutputPath) by default (bin\\\\publish\), so globbing $(OutputPath)**\*.xbf without it would diff --git a/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs b/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs index 4a5ebb6cd..cbbbe2390 100644 --- a/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs +++ b/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs @@ -216,6 +216,7 @@ public void ConsumerPublishIncludesTheAppPriAndXbfSidecars() // references without adding coverage — the target under test is in the core package. WriteConsumerProject(dir, coreOnly: true); WriteConsumerProgram(dir); + WriteConsumerXaml(dir); CreateNuGetConfig(dir); } @@ -232,13 +233,17 @@ public void ConsumerPublishIncludesTheAppPriAndXbfSidecars() "start with an empty resource dictionary and crash in native XAML (WindowsAppSDK#6394). " + "Reactor's _ReactorCopyWinUIResourcesToPublish target is supposed to copy it."); + // The CONSUMER's own compiled XAML, not Reactor's. ReactorApplication.xbf would be a + // vacuous check here: the package delivers it through a + // item, so it lands in publish whether or not this target's $(OutputPath)**\*.xbf glob + // runs. Only an .xbf the consumer compiles itself exercises that glob. Assert.True( - File.Exists(Path.Join(publishDir, "Reactor", "Hosting", "ReactorApplication.xbf")), - "ReactorApplication.xbf is missing from the publish output, and its relative " + - "Reactor/Hosting/ path must be preserved for ms-appx:/// to resolve it."); + File.Exists(Path.Join(publishDir, "Themes", "ConsumerStyles.xbf")), + "The consumer's own compiled XAML (Themes/ConsumerStyles.xbf) is missing from the " + + "publish output, and its relative folder must be preserved for ms-appx:/// to resolve it."); - // Control: with the target disabled the file must disappear again. If it does not, - // something else is supplying it and the assertions above prove nothing about the target. + // Control: with the target disabled both files must disappear again. If they do not, + // something else is supplying them and the assertions above prove nothing about the target. var optOutDir = Path.Join(offDir, "pub"); RunHelpers.RunDotnet( $"publish -c Release -a {_fixture.RunArchitecture} -o \"{optOutDir}\" -p:ReactorCopyWinUIResourcesToPublish=false", @@ -246,6 +251,11 @@ public void ConsumerPublishIncludesTheAppPriAndXbfSidecars() _fixture.CommandEnvironment, timeoutMs: 600_000); + Assert.False( + File.Exists(Path.Join(optOutDir, "Themes", "ConsumerStyles.xbf")), + "Opting out of _ReactorCopyWinUIResourcesToPublish still produced the consumer's own " + + ".xbf, so the passing assertion above does not attribute the copy to Reactor's target."); + Assert.False( File.Exists(Path.Join(optOutDir, "Consumer.pri")), "Opting out of _ReactorCopyWinUIResourcesToPublish still produced the app .pri, so the " + @@ -312,6 +322,27 @@ private static void WriteConsumerProgram(string appDir) """); } + /// + /// A consumer-owned XAML file, so the publish test can assert on an .xbf that the + /// consumer compiles rather than one the package already delivers by another route. + /// A keyless ResourceDictionary is the smallest thing the XAML compiler will emit an + /// .xbf for; it lives in a subfolder so the copy's RecursiveDir handling is + /// covered too. + /// + private static void WriteConsumerXaml(string appDir) + { + var themes = Path.Join(appDir, "Themes"); + Directory.CreateDirectory(themes); + + File.WriteAllText(Path.Join(themes, "ConsumerStyles.xaml"), """ + + 42 + + """); + } + private void CreateNuGetConfig(string appDir) { RunHelpers.RunDotnet( From e48102170765f7a2a4c3d5059ecbf7d24d255e60 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Thu, 24 Sep 2026 14:57:59 -0700 Subject: [PATCH 09/20] test: shorten the shared package dir so the XAML compiler stays under MAX_PATH ConsumerPublishIncludesTheAppPriAndXbfSidecars failed on CI with "WMC1006: Cannot resolve Assembly or Windows Metadata file" pointing at microsoft.windowsappsdk.foundation's projection DLL. That path measured EXACTLY 260 characters - MAX_PATH - and the WinUI XAML compiler is not long-path aware. It surfaced now because this test gained a consumer-owned .xaml in the previous commit, so the XAML compiler runs on the first publish too. The same root cause already bit once (fixed then by splitting the two publishes into separate directories), which was treating the symptom. The real cause is the shared fixture's naming: it becomes the consumer's globalPackagesFolder, so every resolved reference is rooted under it. reactor-template-packages-{guid}/nuget-global-packages -> rtp-{guid}/gp Saves 41 characters, taking that reference from 260 to 219. Verified against the whole closure rather than the one path that happened to fail first: the longest of 496 reference files under a real restore is 142 characters relative, which with CI's 80-character root is 222 - 38 under the limit. Comment added so the names are not lengthened back for readability. Note the local run cannot catch this class of failure: my temp root is 3 characters shorter than CI's runneradmin one, so the old naming measured 257 locally and squeaked through. The two local failures here are NU1301 (nuget.org is TLS-blocked in this environment), which is the documented limitation these tests already carry - not WMC1006. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- .../Packaging/CreateTemplateTests.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/Reactor.IntegrationTests/Packaging/CreateTemplateTests.cs b/tests/Reactor.IntegrationTests/Packaging/CreateTemplateTests.cs index 9f229b402..cbc883760 100644 --- a/tests/Reactor.IntegrationTests/Packaging/CreateTemplateTests.cs +++ b/tests/Reactor.IntegrationTests/Packaging/CreateTemplateTests.cs @@ -247,7 +247,16 @@ private static void RunDotnetRun( public sealed class TemplatePackageTestFixture : IDisposable { - private readonly string _tempRoot = Path.Combine(Path.GetTempPath(), $"reactor-template-packages-{Guid.NewGuid():N}"); + // Deliberately terse names. These directories become the consumer's + // globalPackagesFolder, so every resolved reference path is rooted here — and the + // WinUI XAML compiler is not long-path aware, failing with + // "WMC1006: Cannot resolve Assembly or Windows Metadata file" once a reference + // crosses MAX_PATH. Measured on CI, the longest reference + // (microsoft.windowsappsdk.foundation's projection DLL under a net6.0-windows... + // lib folder) landed at exactly 260 characters with the previous + // "reactor-template-packages-{guid}/nuget-global-packages" naming. Do not lengthen + // these back for readability; the headroom is load-bearing. + private readonly string _tempRoot = Path.Combine(Path.GetTempPath(), $"rtp-{Guid.NewGuid():N}"); public TemplatePackageTestFixture() { @@ -257,7 +266,7 @@ public TemplatePackageTestFixture() var packageSuffix = Guid.NewGuid().ToString("N")[..12]; PackageVersion = $"0.0.0-template-smoke-{packageSuffix}"; PackageSourceDir = CreateDirectory("packages"); - NugetPackagesDir = CreateDirectory("nuget-global-packages"); + NugetPackagesDir = CreateDirectory("gp"); var nugetHttpCacheDir = CreateDirectory("nuget-http-cache"); var dotnetCliHomeDir = CreateDirectory("dotnet-home"); TemplateHiveDir = CreateDirectory("template-hive"); From 227d9f130b0c4c080bd7f5694c68e3ef01b44491 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Thu, 24 Sep 2026 15:18:27 -0700 Subject: [PATCH 10/20] test: use Path.Join for the fixture temp root Code-quality flagged the Path.Combine call on the line shortened in e4810217: "Call to System.IO.Path.Combine may silently drop its earlier arguments." That is Path.Combine's documented rooted-path behaviour - if a later segment is absolute it discards everything before it. Not reachable here, since the segment is always "rtp-{guid}", but Path.Join has no such behaviour and is already what the sibling PriPackagingTests uses, so there is no reason to keep the sharper tool. Scoped to the flagged line only. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- tests/Reactor.IntegrationTests/Packaging/CreateTemplateTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Reactor.IntegrationTests/Packaging/CreateTemplateTests.cs b/tests/Reactor.IntegrationTests/Packaging/CreateTemplateTests.cs index cbc883760..f8deb4718 100644 --- a/tests/Reactor.IntegrationTests/Packaging/CreateTemplateTests.cs +++ b/tests/Reactor.IntegrationTests/Packaging/CreateTemplateTests.cs @@ -256,7 +256,7 @@ public sealed class TemplatePackageTestFixture : IDisposable // lib folder) landed at exactly 260 characters with the previous // "reactor-template-packages-{guid}/nuget-global-packages" naming. Do not lengthen // these back for readability; the headroom is load-bearing. - private readonly string _tempRoot = Path.Combine(Path.GetTempPath(), $"rtp-{Guid.NewGuid():N}"); + private readonly string _tempRoot = Path.Join(Path.GetTempPath(), $"rtp-{Guid.NewGuid():N}"); public TemplatePackageTestFixture() { From 5455295a0f352918eea27ce7842ee641c872479f Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Thu, 24 Sep 2026 16:31:40 -0700 Subject: [PATCH 11/20] docs: EnableMsixTooling alone does not close the gap yet - measured Answering a fair challenge: if #6394 is fixed upstream, why does _ReactorCopyWinUIResourcesToPublish exist, and shouldn't the template just set EnableMsixTooling=true? Measured rather than argued. The two halves of the upstream fix shipped separately, and only one is reachable today: AddProjectPriToResolvedFileToPublish -> in 1.7.251221100 AddXbfFilesToResolvedFileToPublish -> only in 1.7.260903100 and no Microsoft.WindowsAppSDK release pulls the newer one yet: a restore against the newest SDK (2.5.1) resolves Microsoft.Windows.SDK.BuildTools.MSIX 1.7.251221100. Publishing an unpackaged consumer with EnableMsixTooling=true on that resolved version: app .pri published : True consumer's own .xbf : False So turning the flag on today fixes half the gap while pulling in the entire single-project MSIX pipeline. That is why the template has not adopted it, and why this target is not redundant with the upstream fix. Also drops the previous wording that the template "does not enable the MSIX tooling, so the platform's fix never runs" - true, but it read as a justification when it is really a state with a revisit trigger. The deletion criteria are now concrete: an SDK release pulling build tools >= 1.7.260903100, AND either those targets leaving the MsixPackageSupport gate or the template adopting the flag - at which point the flag alone covers both halves and this target can go. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- docs/aot-support.md | 10 +++++++--- src/Reactor/build/Reactor.targets | 32 ++++++++++++++++++------------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/docs/aot-support.md b/docs/aot-support.md index b315a8986..c24e62af5 100644 --- a/docs/aot-support.md +++ b/docs/aot-support.md @@ -125,9 +125,11 @@ Opt out — for example if you drive the copy yourself — with: `EnableMsixTooling` → `MsixPackageSupport` → `ShouldImportMsixCommonTargets` → `MsixCommonTargets` → `Packaging.targets` → `Pri.targets`. With that flag off the build takes the `MrtCore.targets` branch instead, which has no publish step at all. -That gate is not an oversight, and the upstream "fixed" is not wrong: **the official WinUI templates set `true` by default**, so the mainstream WinUI app is genuinely fixed by upgrading. Reactor's template is the case that falls outside it — it ships `WindowsPackageType=None` for xcopy deployment and does not enable the MSIX tooling, so the platform's fix never runs. +That gate is not an oversight, and the upstream "fixed" is not wrong: **the official WinUI templates set `true` by default**, so the mainstream WinUI app is genuinely fixed by upgrading. Reactor's template ships `WindowsPackageType=None` for xcopy deployment and does not set that flag, so the platform's targets do not run for it. -Measured on 1.7.260903100 with a `-v:detailed` publish, counting how many times each fix target actually executes: +**Turning the flag on is not yet sufficient either, which is the reason this target still exists.** The two halves of the upstream fix shipped separately: `AddProjectPriToResolvedFileToPublish` is in 1.7.251221100, but `AddXbfFilesToResolvedFileToPublish` — the half that closed the issue — is only in **1.7.260903100**, and no `Microsoft.WindowsAppSDK` version pulls that yet. Measured: the newest SDK (2.5.1) resolves `Microsoft.Windows.SDK.BuildTools.MSIX/1.7.251221100`, and an unpackaged consumer with `EnableMsixTooling=true` on that version publishes its app `.pri` but **not** its own compiled `.xbf`. So doing it entirely "the upstream way" today still leaves half the gap open. + +Measured on 1.7.260903100 (explicitly pinned) with a `-v:detailed` publish, counting how many times each fix target actually executes: | cell | `AddProjectPriToResolvedFileToPublish` | `AddXbfFilesToResolvedFileToPublish` | app `.pri` published | |---|---|---|---| @@ -136,7 +138,9 @@ Measured on 1.7.260903100 with a `-v:detailed` publish, counting how many times The second row is the control: it proves the probe can match, so the zeroes in the first row are a measurement rather than a broken grep. Publishing the stock `reactorapp` template on the newest stable SDK (metapackage 2.5.1, WinUI 2.3.9, Foundation 2.3.12) with Reactor's target disabled still produces no app `.pri` and still crashes with `0xC000027B`. -**So upgrading the build tools does not by itself remove the need for this workaround** — for an app in Reactor's default shape. Two things would: enabling `EnableMsixTooling` in your own project, or the platform moving those targets outside the gate. +**So upgrading the build tools does not by itself remove the need for this workaround** — for an app in Reactor's default shape. + +**When to delete the shipped target.** #6394 is already closed, so "wait for the issue" is not the signal — it closed while the gate was still in place, and while no shipping SDK pulled the version carrying the half that closed it. Delete this when both are true: a `Microsoft.WindowsAppSDK` release pulls build tools ≥ 1.7.260903100, **and** either those targets move outside the `MsixPackageSupport` gate or Reactor's template adopts `EnableMsixTooling=true`. Adopting the flag on its own is not enough today — measured above, it publishes the `.pri` but not the `.xbf` — which is why the template has not taken it yet; revisit that once the SDK ships the newer build tools, since at that point the flag alone would cover both halves and this target could go. The `ConsumerPublishIncludesTheAppPriAndXbfSidecars` test will tell you: its opt-out control asserts the app `.pri` and the consumer's own `.xbf` are *absent* when Reactor's target is disabled, so it starts failing the moment the platform begins doing this itself. Reactor does **not** set the flag for you. Whether an app wants MSIX packaging tooling is the app's decision, not a framework's, and it pulls in the whole single-project MSIX pipeline to fix one missing copy. The shipped target is narrower, explicit, and works whichever way an app sets `EnableMsixTooling`. The two compose cleanly if you do enable it — verified: no errors, no duplicated files, app runs. diff --git a/src/Reactor/build/Reactor.targets b/src/Reactor/build/Reactor.targets index 9e7417816..0b8769d88 100644 --- a/src/Reactor/build/Reactor.targets +++ b/src/Reactor/build/Reactor.targets @@ -203,19 +203,25 @@ conditional block (the two settings never co-exist, though reading the raw csproj can suggest they do). - NOT removed by an SDK upgrade alone. WindowsAppSDK#6394 is CLOSED, fixed in MSIX - build tools 1.7.260903100 by Pri.targets' AddProjectPriToResolvedFileToPublish / - AddXbfFilesToResolvedFileToPublish. That fix is real but reachable only via - EnableMsixTooling -> MsixPackageSupport -> ShouldImportMsixCommonTargets -> - Packaging.targets -> Pri.targets; with the flag off the build takes the MrtCore - branch, which has no publish step. The official WinUI templates set - EnableMsixTooling=true, so the mainstream app IS fixed by upgrading - Reactor's - template is the case outside it, shipping WindowsPackageType=None without the flag. - Measured on 1.7.260903100 with a -v:detailed publish: without the flag each fix - target runs 0 times and no app .pri is published; with it, each runs twice and the - .pri appears (that second cell being the control proving the probe can match). - Delete this once those targets run WITHOUT EnableMsixTooling, or if the template - adopts the flag; the opt-out control in PriPackagingTests fails when that happens. + NOT removed by an SDK upgrade alone, and not by EnableMsixTooling alone either. + WindowsAppSDK#6394 is CLOSED, fixed in MSIX build tools 1.7.260903100 by + Pri.targets' AddProjectPriToResolvedFileToPublish / AddXbfFilesToResolvedFileToPublish. + Two things keep that out of reach for a Reactor app today: + + 1. Those targets are imported only via EnableMsixTooling -> MsixPackageSupport -> + ShouldImportMsixCommonTargets -> Packaging.targets -> Pri.targets. With the flag + off the build takes the MrtCore branch, which has no publish step. The official + WinUI templates DO set the flag, which is why upstream considers it fixed; + Reactor's template ships WindowsPackageType=None without it. + 2. The .xbf half landed only in 1.7.260903100, and no Microsoft.WindowsAppSDK + release pulls that yet - 2.5.1 resolves 1.7.251221100. Measured: an unpackaged + consumer with EnableMsixTooling=true on the version the SDK actually resolves + publishes its app .pri but NOT its own compiled .xbf. + + So enabling the flag today would fix half the gap and add the whole single-project + MSIX pipeline. Delete this target once an SDK release pulls build tools >= + 1.7.260903100 AND either those targets leave the gate or the template adopts the + flag; the opt-out control in PriPackagingTests fails when that happens. Note the Exclude: $(PublishDir) is nested INSIDE $(OutputPath) by default (bin\\\\publish\), so globbing $(OutputPath)**\*.xbf without it would From 5ad4ceb8400fa5c53d82aab92d97d29e9083dee4 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Thu, 24 Sep 2026 18:44:00 -0700 Subject: [PATCH 12/20] docs: record the packaged-app and relocation measurements Two claims in this PR were argued but never measured end to end. Both now are, and one of them closed a real gap. 1. Packaged (MSIX) consumers were never tested - every earlier cell was unpackaged. The worry was legitimate: for a packaged app the old lib/ .pri was NOT inert. It reached the app layout, so the app's own makepri New merged it into resources.pri. Measured, a packaged consumer of 0.1.0-preview.16 indexes ms-resource:///Files/Reactor/Hosting/ReactorApplication.xbf; built against this branch it does not. So the .pri really was carrying the sidecar there. It is still safe to drop. Registering and launching the same packaged app: preview.16 runs with theme resources resolving; this branch runs identically; deleting the loose .xaml+.xbf crashes it with brush/style False. That third cell is the control that makes the second mean something. 2. "Just relocate the .pri out of lib/" - previously reasoned from the targets, now measured. Shipping Reactor.pri from a sidecar/ folder via a plain still produces a makepri.exe Dump on it, because it lands in @(PackagingOutputs) and _ExpandPriFiles picks it up. Absence is the only escape. Also re-verified the headline fix at the issue's exact geometry with all three packages referenced, adding a plain-WinUI cell as the reference point: BEFORE (preview.16, has .pri) -> FAILS, 10 PRI/APPX errors AFTER (this branch, no .pri) -> builds clean NO-reactor (plain WinUI) -> builds clean which is the actual claim: parity with a plain WinUI app, not immunity to depth. A deeper path fails for everyone on the SDK's own framework PRIs - at 177 chars those land at 258-259 while Microsoft.UI.Reactor.Devtools.pri.xml landed at 261, matching the arithmetic in the issue. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- docs/specs/022-packaging-and-distribution.md | 27 ++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/specs/022-packaging-and-distribution.md b/docs/specs/022-packaging-and-distribution.md index 06b1240d9..1cf8f2703 100644 --- a/docs/specs/022-packaging-and-distribution.md +++ b/docs/specs/022-packaging-and-distribution.md @@ -175,8 +175,10 @@ app builds fine. Relocating the file is *not* a fix: a `.pri` that reaches the consumer's layout by any route — including a `CopyToOutputDirectory` item from a non-reference folder — also lands -in `@(PackagingOutputs)`, where `_ExpandPriFiles` hands it to the same task. It has to be -absent. +in `@(PackagingOutputs)`, where `_ExpandPriFiles` hands it to the same task. Measured: +shipping `Reactor.pri` from a `sidecar/` folder with a plain +`` item still produces a +`makepri.exe Dump -IndexFile …\sidecar\Reactor.pri` invocation. It has to be absent. Each of the three csproj files therefore narrows the allow-list that decides what may sit in the build-output folder: @@ -195,6 +197,27 @@ by the `_PackReactorApplicationXbf` target rather than a `` item, because guarded with `Exists()` is evaluated before the build produces the file and silently shipped a package without it. +### Why removing the `.pri` is safe for packaged apps too + +Worth stating because the mechanism differs and the obvious worry is real. For a +**packaged** (MSIX) consumer the old `lib/` `.pri` was not inert: it reached the app +layout, so the app's own `makepri New` merged it into `resources.pri`. Measured on a +packaged consumer of 0.1.0-preview.16, that index contains +`ms-resource:///Files/Reactor/Hosting/ReactorApplication.xbf`; built against this +branch, it does not. So the `.pri` genuinely was carrying the sidecar there. + +It is still safe to drop, because the loose files carry it either way. Measured on the +same packaged app, registered and launched: + +| packaged consumer | app starts | theme resources resolve | +|---|---|---| +| 0.1.0-preview.16 (has `.pri`) | yes | yes | +| this branch (no `.pri`) | **yes** | **yes** | +| this branch, loose `.xaml`+`.xbf` deleted | **crashes** | **no** | + +The third row is the control: it is what makes the second row evidence rather than a +coincidence, and it confirms the loose sidecars — not the `.pri` — are load-bearing. + ### Exactly one `lib/` folder `_PackReactorApplicationXbf` emits the sidecar as **build output** with a `TargetPath` From e8957eb3d8f46c19ca9bad13e929b46a12c37760 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 15:03:47 -0700 Subject: [PATCH 13/20] docs: #6394 was reopened; record the #6795 fix and why it does not retire this Re-checked both upstream issues after @azchohfi flagged that upstream movement might make this PR redundant. Two developments, both 2026-09-25, and neither makes it redundant today. WindowsAppSDK#6394 was REOPENED the day after it closed. Upstream's reason is the gate this PR already documents: "Reopening because fix on MRT core version used when not enabling msix tooling is still not released." Our docs and the target's comment said CLOSED, which is now wrong - corrected, and the upstream quote recorded since it independently confirms the measurement rather than merely agreeing with it. WindowsAppSDK#6795 - the MAX_PATH defect behind issue #1271 - is verified fixed in MSIX build tools 1.7.260925103. That is the one that could retire the main fix here, so spec 022 now says so explicitly, along with why it does not yet: build tools reach an app only through Microsoft.WindowsAppSDK.Base, the sole package in the graph referencing them, and the newest Base (2.0.4, pulled by Windows App SDK 2.5.1) still pins 1.7.251221100. Checked against nuget.org directly, with positive and negative controls: 1.7.260925103 returns 404 while 1.7.251221100 and 1.7.260903100 return 200. Also records what happens when it does flow: the .pri removal stops being a build-failure fix, but the layout stays right on its own terms - the .Advanced and .Devtools indexes are entirely empty, and shipping them costs every consumer six wasted makepri Dump invocations per build. Flagged for re-evaluation rather than asserted as permanent. Fixes a duplicated "When to delete the shipped target" paragraph introduced by an earlier edit in this PR - the section had two, with drifting criteria. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- docs/aot-support.md | 6 ++---- docs/specs/022-packaging-and-distribution.md | 15 +++++++++++++++ src/Reactor/build/Reactor.targets | 8 +++++--- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/aot-support.md b/docs/aot-support.md index c24e62af5..0a1da540b 100644 --- a/docs/aot-support.md +++ b/docs/aot-support.md @@ -109,7 +109,7 @@ The missing `.pri` has cascading runtime symptoms that look like separate bugs b - `Application.Current.Resources` loads as an empty dictionary (`0` merged, `0` themed, `0` keys) because `Application.LoadComponent` cascades through MRT for type-info lookups. - Every `ThemeRef.Resolve(...)` returns `null`; `.Foreground(Theme.X)` falls back to control defaults. -Upstream tracking: [microsoft/WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394) — **CLOSED as fixed** in `Microsoft.Windows.SDK.BuildTools.MSIX` **1.7.260903100** (2026-09-24). Read the next section before concluding this no longer affects you: the fix is real, but it only runs for apps that enable the MSIX tooling, which Reactor's unpackaged template deliberately does not. +Upstream tracking: [microsoft/WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394) — closed 2026-09-24 as fixed in `Microsoft.Windows.SDK.BuildTools.MSIX` **1.7.260903100**, then **REOPENED 2026-09-25**: *"Reopening because fix on MRT core version used when not enabling msix tooling is still not released."* That is upstream confirming the gate described below — the fix exists, but only on the MSIX-tooling path. **Nothing to do — Reactor ships the workaround.** `build/Microsoft.UI.Reactor.targets` defines `_ReactorCopyWinUIResourcesToPublish`, which runs after `Publish` for any unpackaged Reactor executable and copies the project `.pri` plus every `.xbf` into `$(PublishDir)`. It applies to AOT and non-AOT publishes alike, so app projects no longer carry a per-project copy of this target (they used to; it was gated on `PublishAot=true` and therefore missed the plain-publish case). @@ -140,8 +140,6 @@ The second row is the control: it proves the probe can match, so the zeroes in t **So upgrading the build tools does not by itself remove the need for this workaround** — for an app in Reactor's default shape. -**When to delete the shipped target.** #6394 is already closed, so "wait for the issue" is not the signal — it closed while the gate was still in place, and while no shipping SDK pulled the version carrying the half that closed it. Delete this when both are true: a `Microsoft.WindowsAppSDK` release pulls build tools ≥ 1.7.260903100, **and** either those targets move outside the `MsixPackageSupport` gate or Reactor's template adopts `EnableMsixTooling=true`. Adopting the flag on its own is not enough today — measured above, it publishes the `.pri` but not the `.xbf` — which is why the template has not taken it yet; revisit that once the SDK ships the newer build tools, since at that point the flag alone would cover both halves and this target could go. The `ConsumerPublishIncludesTheAppPriAndXbfSidecars` test will tell you: its opt-out control asserts the app `.pri` and the consumer's own `.xbf` are *absent* when Reactor's target is disabled, so it starts failing the moment the platform begins doing this itself. - Reactor does **not** set the flag for you. Whether an app wants MSIX packaging tooling is the app's decision, not a framework's, and it pulls in the whole single-project MSIX pipeline to fix one missing copy. The shipped target is narrower, explicit, and works whichever way an app sets `EnableMsixTooling`. The two compose cleanly if you do enable it — verified: no errors, no duplicated files, app runs. **The discriminator is the flag, not the packaging kind.** Visual Studio's WinUI templates set `EnableMsixTooling=true` and every MSIX project in this repo does too, so those apps get the platform's own fix; Reactor's target deliberately excludes packaged ones (`AppxPackage != true`, `WindowsPackageType != MSIX`) and is a harmless no-op for an unpackaged app that opts the flag on. The two paths cover complementary populations: @@ -155,7 +153,7 @@ Reactor does **not** set the flag for you. Whether an app wants MSIX packaging t **It does not fix issue #1271.** Measured against pre-fix packages at the failing geometry: with and without `EnableMsixTooling=true` the build fails identically with `PRI175` / `PRI222` on `Microsoft.UI.Reactor.Devtools.pri.xml`. Library `.pri` files still get expanded; keeping them out of `lib/` is a separate and still-necessary fix. -**When to delete the shipped target.** #6394 is already closed, so "wait for the issue" is not the signal — it closed while the gate was still in place. Delete this when `AddProjectPriToResolvedFileToPublish` runs for an unpackaged app that has *not* set `EnableMsixTooling`, i.e. when the MSIX build tools import those targets outside the `MsixPackageSupport` gate (or if Reactor's template ever adopts the flag itself). The `ConsumerPublishIncludesTheAppPriAndXbfSidecars` test in `tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs` will tell you: its opt-out control asserts the app `.pri` and the consumer's own `.xbf` are *absent* when Reactor's target is disabled, so it starts failing the moment the platform begins doing this itself. +**When to delete the shipped target.** Not "when #6394 closes" — it already closed once, on 2026-09-24, and was reopened the next day precisely because the non-MSIX-tooling path is still unfixed. Delete this when **both** hold: a `Microsoft.WindowsAppSDK` release actually pulls build tools carrying the fix (as of 2026-09-25 the newest `Microsoft.WindowsAppSDK.Base`, 2.0.4, still pins `1.7.251221100`), **and** either those targets move outside the `MsixPackageSupport` gate or Reactor's template adopts `EnableMsixTooling=true`. Adopting the flag alone is not enough today — measured above, it publishes the `.pri` but not the `.xbf`. The `ConsumerPublishIncludesTheAppPriAndXbfSidecars` test in `tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs` is the tripwire: its opt-out control asserts the app `.pri` and the consumer's own `.xbf` are *absent* when Reactor's target is disabled, so it starts failing the moment the platform begins doing this itself. ## Debugging an AOT selftest hang diff --git a/docs/specs/022-packaging-and-distribution.md b/docs/specs/022-packaging-and-distribution.md index 1cf8f2703..b8ddbe192 100644 --- a/docs/specs/022-packaging-and-distribution.md +++ b/docs/specs/022-packaging-and-distribution.md @@ -180,6 +180,21 @@ shipping `Reactor.pri` from a `sidecar/` folder with a plain `` item still produces a `makepri.exe Dump -IndexFile …\sidecar\Reactor.pri` invocation. It has to be absent. +**The upstream long-path fix does not retire this.** The `MAX_PATH` defect itself is +[microsoft/WindowsAppSDK#6795](https://github.com/microsoft/WindowsAppSDK/issues/6795) +(`WinAppSdkExpandPriContent` passes a non-extended-length path to `makepri.exe`), verified +fixed in `Microsoft.Windows.SDK.BuildTools.MSIX` **1.7.260925103**. Consumers do not get +that version yet: build tools reach an app only through `Microsoft.WindowsAppSDK.Base`, +the sole package in the graph that references them, and the newest Base (2.0.4, pulled by +Windows App SDK 2.5.1) pins `1.7.251221100`. Checked 2026-09-25 — `1.7.260925103` was not +yet on nuget.org. + +Once it does flow, this section stops being a build-*failure* fix, but the layout stays +correct on its own terms: the `.Advanced` and `.Devtools` indexes are **entirely empty**, +and shipping them costs every consumer six wasted `makepri.exe Dump` invocations per +build. Re-evaluate then; do not assume the upstream fix alone makes a `.pri` in `lib/` +harmless. + Each of the three csproj files therefore narrows the allow-list that decides what may sit in the build-output folder: diff --git a/src/Reactor/build/Reactor.targets b/src/Reactor/build/Reactor.targets index 0b8769d88..cb247b463 100644 --- a/src/Reactor/build/Reactor.targets +++ b/src/Reactor/build/Reactor.targets @@ -204,9 +204,11 @@ can suggest they do). NOT removed by an SDK upgrade alone, and not by EnableMsixTooling alone either. - WindowsAppSDK#6394 is CLOSED, fixed in MSIX build tools 1.7.260903100 by - Pri.targets' AddProjectPriToResolvedFileToPublish / AddXbfFilesToResolvedFileToPublish. - Two things keep that out of reach for a Reactor app today: + WindowsAppSDK#6394 was closed 2026-09-24 as fixed in MSIX build tools 1.7.260903100 + by Pri.targets' AddProjectPriToResolvedFileToPublish / AddXbfFilesToResolvedFileToPublish, + then REOPENED 2026-09-25 - upstream's own words: "Reopening because fix on MRT core + version used when not enabling msix tooling is still not released." That is the same + gate measured below. Two things keep the fix out of reach for a Reactor app today: 1. Those targets are imported only via EnableMsixTooling -> MsixPackageSupport -> ShouldImportMsixCommonTargets -> Packaging.targets -> Pri.targets. With the flag From 22188e1f385de9671d34bf8e5dd9e5f4712a2890 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 15:25:56 -0700 Subject: [PATCH 14/20] docs: no PUBLISHED build-tools version fixes the long-path defect Strengthens the #6795 note after cross-checking with the session tracking these upstream issues. My previous wording claimed the weaker fact - that the fixed version is not published yet. The stronger one is true: 1.7.251221100 (ships today) -> FAILS, 6 PRI / 4 APPX 1.7.260903100 (latest published) -> FAILS, 6 PRI / 4 APPX 1.7.260925103 (private drop) -> builds clean So no published version fixes it, not merely "not the newest one". That version was A/B'd at a failing depth by the other session, and matches the structure: 1.7.260903100's MrtCore.PriExpansion.targets still passes a bare IntermediateDirectory="$(IntermediateOutputPath)" and its task assembly has no \\?\ handling at all. Independently confirmed the private-drop read from this machine, which can reach nuget.org (the other session's proxy cannot, and they flagged that as their one unverified number): 1.7.260925103 returns 404 while 1.7.251221100 and 1.7.260903100 return 200 and a nonsense version returns 404, so the probe distinguishes absent from broken. Same for Base 2.0.5/2.0.6/2.1.0 and WindowsAppSDK 2.5.2/2.6.0 - all 404. Also records why this section outlives the upstream fix, which the previous wording left as "re-evaluate then": build tools are transitive via Microsoft.WindowsAppSDK.Base - every Base including 2.0.5-experimental2 pins 1.7.251221100 - so a consumer cannot opt into a fixed build-tools release by upgrading Windows App SDK. Removing the .pri keeps the trigger away from every consumer still resolving an older version, which is not a version they control. Deliberately not recording anything about #6570 (MSB6003 MakePri output collision). The 925103 content-addressed cache may address it, but neither session has a repro, and an unverified claim in the spec is one we would have to walk back. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- docs/specs/022-packaging-and-distribution.md | 37 +++++++++++++------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/docs/specs/022-packaging-and-distribution.md b/docs/specs/022-packaging-and-distribution.md index b8ddbe192..5acc7312c 100644 --- a/docs/specs/022-packaging-and-distribution.md +++ b/docs/specs/022-packaging-and-distribution.md @@ -182,18 +182,31 @@ shipping `Reactor.pri` from a `sidecar/` folder with a plain **The upstream long-path fix does not retire this.** The `MAX_PATH` defect itself is [microsoft/WindowsAppSDK#6795](https://github.com/microsoft/WindowsAppSDK/issues/6795) -(`WinAppSdkExpandPriContent` passes a non-extended-length path to `makepri.exe`), verified -fixed in `Microsoft.Windows.SDK.BuildTools.MSIX` **1.7.260925103**. Consumers do not get -that version yet: build tools reach an app only through `Microsoft.WindowsAppSDK.Base`, -the sole package in the graph that references them, and the newest Base (2.0.4, pulled by -Windows App SDK 2.5.1) pins `1.7.251221100`. Checked 2026-09-25 — `1.7.260925103` was not -yet on nuget.org. - -Once it does flow, this section stops being a build-*failure* fix, but the layout stays -correct on its own terms: the `.Advanced` and `.Devtools` indexes are **entirely empty**, -and shipping them costs every consumer six wasted `makepri.exe Dump` invocations per -build. Re-evaluate then; do not assume the upstream fix alone makes a `.pri` in `lib/` -harmless. +(`WinAppSdkExpandPriContent` passes a non-extended-length path to `makepri.exe`), still +open. A fix exists and was verified by A/B in `Microsoft.Windows.SDK.BuildTools.MSIX` +**1.7.260925103** — but that is a private drop, not a feed package: checked against +nuget.org on 2026-09-25 it returns 404, while 1.7.251221100 and 1.7.260903100 return 200 +and a nonsense version returns 404 (i.e. the probe can tell absent from broken). + +**No published version fixes it.** The newest published build tools, 1.7.260903100, were +A/B'd at a failing depth and still fail — 6 `PRI` and 4 `APPX` errors — which matches the +structure: that version's `MrtCore.PriExpansion.targets` still passes a bare +`IntermediateDirectory="$(IntermediateOutputPath)"`, and its task assembly contains no +`\\?\` handling at all. + +Consumers also do not choose this package directly. Build tools reach an app only through +`Microsoft.WindowsAppSDK.Base`, the sole package in the graph that references them, and +every Base — including the newest experimental, 2.0.5-experimental2 — pins +`1.7.251221100`. So the version is transitive: an app cannot opt into a fixed build-tools +release by upgrading Windows App SDK, and pinning the newest published one would not help +either. + +That last point is why this section outlives the upstream fix. Even once 1.7.260925103 (or +its successor) ships and flows, the `.pri` removal keeps the trigger away from every +consumer still resolving an older build-tools version — which, being transitive, is not +something they control. And the layout is right on its own terms regardless: the +`.Advanced` and `.Devtools` indexes are **entirely empty**, and shipping them costs every +consumer six wasted `makepri.exe Dump` invocations per build. Each of the three csproj files therefore narrows the allow-list that decides what may sit in the build-output folder: From c4d8ec4c4fe9aae53dc70d7f2f3d62fc6545c545 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 16:04:12 -0700 Subject: [PATCH 15/20] docs: state the Base finding precisely across stable and prerelease Precision follow-up from the session tracking these upstream issues, so a future reader does not read our two probes as disagreeing. My earlier nuget.org sweep reported Microsoft.WindowsAppSDK.Base 2.0.5 -> 404. Theirs reported a 2.0.5-experimental2 that exists. Both are right: plain 2.0.5 does not exist, only 2.0.5-experimental2. A probe for the former 404s without that meaning there is no newer Base, so the spec now says so explicitly. The substantive finding is stronger with the prerelease included, not weaker. Verified at nuspec level (my own earlier fetch agrees with theirs): Base 2.0.4 -> BuildTools.MSIX 1.7.251221100 Base 2.0.5-experimental2 -> BuildTools.MSIX 1.7.251221100 Base 2.0.250911001-experimental -> BuildTools.MSIX 1.7.20250829.1 So the claim is not "no stable Base carries the fix" but "no Base at all, stable or prerelease, pins even the newest PUBLISHED build tools" - which makes the no-published-route conclusion airtight rather than merely likely. Three independent legs now support it: the fix exists in no published build-tools version; no Base pins even the newest published one; and that newest published one still fails the repro. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- docs/specs/022-packaging-and-distribution.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/specs/022-packaging-and-distribution.md b/docs/specs/022-packaging-and-distribution.md index 5acc7312c..54903c361 100644 --- a/docs/specs/022-packaging-and-distribution.md +++ b/docs/specs/022-packaging-and-distribution.md @@ -195,9 +195,13 @@ structure: that version's `MrtCore.PriExpansion.targets` still passes a bare `\\?\` handling at all. Consumers also do not choose this package directly. Build tools reach an app only through -`Microsoft.WindowsAppSDK.Base`, the sole package in the graph that references them, and -every Base — including the newest experimental, 2.0.5-experimental2 — pins -`1.7.251221100`. So the version is transitive: an app cannot opt into a fixed build-tools +`Microsoft.WindowsAppSDK.Base`, the sole package in the graph that references them — and +**no Base pins even the newest published build tools**, let alone the fixed private drop. +Verified at nuspec level across stable *and* prerelease: 2.0.4 (the newest stable, pulled +by Windows App SDK 2.5.1) and 2.0.5-experimental2 (the newest of any kind) both pin +`1.7.251221100`; 2.0.250911001-experimental pins an older `1.7.20250829.1` still. Note the +version strings: a plain `2.0.5` does not exist, only `2.0.5-experimental2`, so a probe for +the former 404s without that meaning there is no newer Base. So the version is transitive: an app cannot opt into a fixed build-tools release by upgrading Windows App SDK, and pinning the newest published one would not help either. From 621ae7c4b28ab525f1bec7b301564c29d7cbedbb Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Tue, 29 Sep 2026 15:29:08 -0700 Subject: [PATCH 16/20] docs: #1283 narrows the publish fix to self-contained apps; re-measured both #1283 made Reactor depend on Microsoft.WindowsAppSDK.Runtime again, which turns a Reactor-only consumer from silently self-contained into framework-dependent. Every earlier measurement in this PR predates that, so both fixes were re-measured on the merged tree rather than assumed to carry over. 1. The .pri removal (#1271) is unaffected. The fair baseline is main itself - it has #1283 but still ships the .pri, so the only variable is this PR. (preview.16 was the wrong baseline here: it fails earlier with "WindowsAppSDKSelfContained requires a supported Windows architecture", the #822 regression #1283 fixed, and never reaches the PRI step. Counting that as a reproduction would have been a false positive.) Framework-dependent output drops the win-x64 path segment, so the failing geometry shifts. At a 185-char project dir only Microsoft.UI.Reactor.Devtools.pri.xml crosses MAX_PATH (261) while the SDK's own longest lands at 259: main (#1283, ships .pri) -> FAILS, 10 PRI/APPX, on Reactor.Devtools.pri.xml this branch -> builds clean plain WinUI, same depth -> builds clean Build tools still resolve 1.7.251221100 via Base 2.0.4; #1283 does not change that path. 2. The publish target is narrowed, not made redundant. Measured on a published Reactor-only app with the target disabled: framework-dependent (new default) -> runs, theme resources resolve self-contained (opt-in) -> crashes 0xC000027B A framework-dependent app loads WinUI from the installed runtime - its publish output contains no Microsoft.UI.Xaml.dll at all - so its own .pri is not on that path. Self-contained bundles WinUI and resolves it through its own .pri. Before #1283 every Reactor-only app was silently self-contained, which is why the crash was the default experience; now it is the opt-in case. The framework-dependent pass is not vacuous: deleting Reactor's loose sidecars from that same output drops both probes to False. Kept unconditional rather than keyed on WindowsAppSDKSelfContained, and verified that is harmless: for a framework-dependent publish the target adds exactly App.pri and the consumer's .xbf, and the app behaves identically. The CHANGELOG entry, aot-support.md, and the target's comment all claimed the crash was universal; each is now scoped to self-contained apps. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- CHANGELOG.md | 10 +++++---- docs/aot-support.md | 13 ++++++++++- src/Reactor/build/Reactor.targets | 36 ++++++++++++++++++++----------- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 721df8189..d71f8089a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -308,14 +308,16 @@ Conventions for contributors: so `ProjectReference` consumers are unaffected. The core package now also exposes one `lib/` folder instead of two for the same target framework. -- **Published unpackaged apps no longer start with an empty resource dictionary +- **Published self-contained apps no longer start with an empty resource dictionary ([WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394)).** The Windows App SDK leaves an unpackaged app's own `.pri` and `.xbf` out of the publish - output, so the app resolved no theme resources and died at startup with `0xC000027B` + output. A self-contained app (`WindowsAppSDKSelfContained=true`) resolves WinUI through + that `.pri`, so it resolved no theme resources and died at startup with `0xC000027B` inside native XAML. Reactor's targets now copy them, for AOT and non-AOT publishes alike — opt out with `false`. - Replaces the per-project `_CopyWinUIResourcesForAot` snippet (removed from 11 projects), - which only covered `PublishAot=true` builds. + Framework-dependent apps, the default since the Windows App Runtime dependency was + restored, load WinUI from the installed runtime and were not affected. Replaces the + per-project `_CopyWinUIResourcesForAot` snippet (removed from 11 projects). ### Security diff --git a/docs/aot-support.md b/docs/aot-support.md index 0dcf99fff..df332c873 100644 --- a/docs/aot-support.md +++ b/docs/aot-support.md @@ -100,7 +100,18 @@ Devtools are a two-layer opt-in so release and NativeAOT binaries stay trimmed b When publishing an unpackaged WinUI 3 app (`WindowsPackageType=None`), the WindowsAppSDK build pipeline generates `$(AssemblyName).pri` into the build output but does *not* copy it into the publish directory. The MakePRI step is conditioned on `AppxPackage=true`, so unpackaged apps fall through. Same applies to `.xbf` files generated by the XAML compiler. -**This is not AOT-specific.** It was originally found via `PublishAot=true`, but a plain `dotnet publish -c Release` loses the `.pri` just the same — measured on the stock `reactorapp` template, where the published app dies at startup with `0xC000027B` (`STATUS_APPLICATION_INTERNAL_EXCEPTION`) inside native XAML. +**This is not AOT-specific.** It was originally found via `PublishAot=true`, but a plain `dotnet publish -c Release` loses the `.pri` just the same. + +**Whether the missing `.pri` actually breaks the app depends on where WinUI's own resources come from** — which since #1283 depends on how the app is deployed. Measured on a published app that references only Reactor, with Reactor's copy target disabled so the `.pri` is genuinely absent: + +| deployment | app `.pri` in publish | theme resources resolve | app | +|---|---|---|---| +| framework-dependent (the default since #1283) | absent | **yes** | runs | +| self-contained (`WindowsAppSDKSelfContained=true`) | absent | **no** | **crashes `0xC000027B`** | + +A framework-dependent app loads WinUI — `Microsoft.UI.Xaml.dll` and its resources — from the machine-installed Windows App Runtime, so its own `.pri` is not on that path. A self-contained app bundles WinUI into its own folder and resolves it through its own `.pri`, so losing the `.pri` breaks it. The published framework-dependent output confirms the mechanism: it contains no `Microsoft.UI.Xaml.dll` at all. + +Before #1283 every Reactor-only app was *silently* self-contained, which is why the crash was the default experience. #1283 made framework-dependent the default, so most apps no longer hit it — but self-contained remains a supported, documented opt-in (`WindowsAppSDKSelfContained=true`, for deploying to machines without the runtime), and that path still crashes without the `.pri`. The framework-dependent row is not a vacuous pass: deleting Reactor's own loose sidecars from that same output drops both probes to `False`, so the probe can still see a failure there. The missing `.pri` has cascading runtime symptoms that look like separate bugs but all trace back to the same cause: diff --git a/src/Reactor/build/Reactor.targets b/src/Reactor/build/Reactor.targets index cb247b463..b15069f25 100644 --- a/src/Reactor/build/Reactor.targets +++ b/src/Reactor/build/Reactor.targets @@ -178,19 +178,29 @@ An UNPACKAGED (WindowsPackageType=None) WinUI 3 app generates its resource index ($(TargetName).pri) and its compiled XAML (*.xbf) into the BUILD output, but nothing copies them into the PUBLISH output — the MakePRI publish stage is gated on - AppxPackage=true. The published app then starts with no resource index, so - XamlControlsResources loads as an empty dictionary and MRT lookups fail - (TabView/NavigationView throw FileNotFoundException out of - ResourceAccessor::GetLocalizedStringResource). Measured on the stock `reactorapp` - template: the published app dies at startup with 0xC000027B - (STATUS_APPLICATION_INTERNAL_EXCEPTION) inside native XAML, and dropping the app's - own .pri back into the publish folder makes it run. - - This target lives here, in the targets Reactor SHIPS, rather than in each app, - because every Reactor app hits it. It supersedes the per-project - `_CopyWinUIResourcesForAot` snippet that samples used to carry: that one was gated on - PublishAot=true, but the gap is not AOT-specific — a plain `dotnet publish -c Release` - loses the .pri too. + AppxPackage=true. + + Whether that breaks the app depends on where WinUI's resources come from. A + SELF-CONTAINED app (WindowsAppSDKSelfContained=true) bundles Microsoft.UI.Xaml.dll + and resolves it through its own .pri, so without it XamlControlsResources loads + empty and the app dies at startup with 0xC000027B inside native XAML. A + FRAMEWORK-DEPENDENT app loads WinUI from the installed Windows App Runtime, so it + does not need its own .pri for that and runs fine without it. Measured both on a + Reactor-only published app with this target disabled: framework-dependent runs + with theme resources resolving; self-contained crashes. + + Framework-dependent became the default when Reactor's Runtime dependency was + restored; before that every Reactor-only app was silently self-contained, which is + why the crash used to be the default experience. Self-contained remains a supported + opt-in for deploying to machines without the runtime, and that path still needs + this target. It stays unconditional rather than keyed on WindowsAppSDKSelfContained + because copying the .pri into a framework-dependent publish is harmless, and keying + on that property would silently skip apps that set it through a path this + evaluation cannot see. + + This target lives here, in the targets Reactor SHIPS, rather than in each app. It + supersedes the per-project `_CopyWinUIResourcesForAot` snippet that samples used to + carry: that one was gated on PublishAot=true, but the gap is not AOT-specific. Opt out with false. From af5ec009a42f54d866178536863e38eae03a452d Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Tue, 29 Sep 2026 15:30:06 -0700 Subject: [PATCH 17/20] test: record why the publish test's consumer is self-contained After #1283 made framework-dependent the default, the publish test's WindowsAppSDKSelfContained=true looks like an arbitrary leftover that someone might reasonably simplify away. It is load-bearing: self-contained is the one deployment where a missing app .pri actually breaks a published app, since it resolves WinUI through its own .pri. A framework-dependent app loads WinUI from the installed runtime and runs fine without it. So switching the consumer to the framework-dependent default would leave the test passing while it stopped testing the case the target exists for. Documented on the helper, with the measurement that justifies it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- .../Packaging/PriPackagingTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs b/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs index f5f7688b8..07807172d 100644 --- a/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs +++ b/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs @@ -277,6 +277,16 @@ private string[] ReadPackageEntries(string packageId) /// were empty — including the Devtools one with the longest, build-breaking file name — /// untested. trims that down for the publish test, which does not /// need them and is sensitive to how many references the XAML compiler has to resolve. + /// + /// WindowsAppSDKSelfContained=true is deliberate, not incidental. It is the + /// deployment in which a missing app .pri actually breaks a published app: a + /// self-contained app resolves WinUI through its own .pri, whereas a + /// framework-dependent one — the default since Reactor's Windows App Runtime dependency was + /// restored — loads WinUI from the installed runtime and runs without it. Measured: with the + /// copy target disabled, the framework-dependent publish runs with theme resources resolving + /// and the self-contained one crashes with 0xC000027B. Do not "simplify" this to the + /// framework-dependent default; the publish test would still pass, but it would stop testing + /// the case the target exists for. /// private void WriteConsumerProject(string appDir, bool coreOnly = false) { From 9ba2cff3c48f1498ee234f4add549d5a554f8857 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Wed, 14 Oct 2026 11:14:28 -0700 Subject: [PATCH 18/20] Depend on fixed MSIX build tools instead of shipping a publish workaround Microsoft.Windows.SDK.BuildTools.MSIX 1.7.260930100 fixes WindowsAppSDK#6394 (the app .pri missing from publish output, now also on the MrtCore path that apps without EnableMsixTooling take, as Reactor's template does) and #6795 (MAX_PATH in makepri Dump). Every Microsoft.WindowsAppSDK.Base release still resolves 1.7.251221100, so Microsoft.UI.Reactor now declares the fixed version as a dependency floor and the shipped _ReactorCopyWinUIResourcesToPublish target is deleted. Measured with the build-tools version as the only variable: self-contained publish (JIT and NativeAOT) crashes with 0xC000027B without it and runs with it; a framework-dependent app cannot load its own XAML without it; deep-path builds fail with PRI175 without it, including on the SDK's own PRIs, which the .pri removal cannot address. The publish integration test now uses a consumer pinned back to 1.7.251221100 as its control. Also fixes two stale references to removed code (a SelfTestRunner comment and CreateTemplateTests). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- CHANGELOG.md | 38 ++-- Directory.Packages.props | 4 + docs/aot-support.md | 79 ++------ docs/specs/022-packaging-and-distribution.md | 56 +++--- src/Reactor/Reactor.csproj | 19 ++ src/Reactor/build/Reactor.targets | 88 --------- .../SelfTest/SelfTestRunner.cs | 5 +- .../Packaging/PriPackagingTests.cs | 172 +++++++++--------- 8 files changed, 174 insertions(+), 287 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9222a02ea..a05f885fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -647,26 +647,24 @@ Conventions for contributors: `Cancel()` cleanup (PR #1124), that the source had been pooled and reset, was wrong: that crash was this same fault. -- **Reactor packages no longer ship a `.pri`, so consuming apps build at path depths where - they previously failed (issue #1271).** The index sat beside the assembly in `lib/`, where - `ResolveAssemblyReference` treats it as a reference-related file and the Windows App SDK - expands it with `makepri.exe Dump` into `$(IntermediateOutputPath)` — a path that breaks - past `MAX_PATH` and fails the build with `PRI175` / `PRI222` / `APPX0002`. Nothing is lost: - the `.Advanced` and `.Devtools` indexes were empty and the core one only embedded a second - copy of `ReactorApplication.xbf`, which still ships loose. `.pri` *generation* is unchanged, - so `ProjectReference` consumers are unaffected. The core package now also exposes one - `lib/` folder instead of two for the same target framework. - -- **Published self-contained apps no longer start with an empty resource dictionary - ([WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394)).** The - Windows App SDK leaves an unpackaged app's own `.pri` and `.xbf` out of the publish - output. A self-contained app (`WindowsAppSDKSelfContained=true`) resolves WinUI through - that `.pri`, so it resolved no theme resources and died at startup with `0xC000027B` - inside native XAML. Reactor's targets now copy them, for AOT and non-AOT publishes alike - — opt out with `false`. - Framework-dependent apps, the default since the Windows App Runtime dependency was - restored, load WinUI from the installed runtime and were not affected. Replaces the - per-project `_CopyWinUIResourcesForAot` snippet (removed from 11 projects). +- **Reactor packages no longer ship a `.pri` (issue #1271).** The index sat beside the + assembly in `lib/`, where `ResolveAssemblyReference` treats it as a reference-related file, + so every consumer build expanded it with `makepri.exe Dump` into `$(IntermediateOutputPath)` + — and on build tools before 1.7.260930100 that path broke past `MAX_PATH` with `PRI175` / + `PRI222` / `APPX0002`. Nothing is lost: the `.Advanced` and `.Devtools` indexes were empty + and the core one only embedded a second copy of `ReactorApplication.xbf`, which still ships + loose. `.pri` *generation* is unchanged, so `ProjectReference` consumers are unaffected. The + core package now also exposes one `lib/` folder instead of two for the same target framework. + +- **Published apps now include their own `.pri` + ([WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394)).** + `Microsoft.Windows.SDK.BuildTools.MSIX` before 1.7.260930100 leaves an unpackaged app's + `.pri` out of its publish output: self-contained apps (AOT or not) crashed at startup with + `0xC000027B`, and framework-dependent apps could not load their own XAML. Every Windows App + SDK release still resolves an older version, so `Microsoft.UI.Reactor` now depends on + 1.7.260930100 directly, which also fixes the `MAX_PATH` failure above for the SDK's own + `.pri` files. Replaces the per-project `_CopyWinUIResourcesForAot` snippet (removed from 11 + projects). ### Security diff --git a/Directory.Packages.props b/Directory.Packages.props index a27132967..b8e91f8b2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -28,6 +28,10 @@ + + diff --git a/docs/aot-support.md b/docs/aot-support.md index 61880acad..b424b46e1 100644 --- a/docs/aot-support.md +++ b/docs/aot-support.md @@ -16,8 +16,8 @@ This page enumerates what works, what doesn't, and what's planned. **If you publ - **Markdown** — md4c-backed parser and renderer. - **Commanding** — command records, focus-scoped accelerators. - **Animation** — compositor-layer transitions, keyframes. -- **Theming** — `ThemeRef` tokens, style caching. Brushes resolved through `XamlControlsResources` work under AOT; Reactor ships the WindowsAppSDK#6394 publish workaround in its own targets, so no app-side setup is needed (see [Required publish-time workarounds](#required-publish-time-workarounds) below). -- **Built-in WinUI controls** — `NavigationView`, `TabView`, `Pivot`, `TreeView`, `Expander`, `InfoBar`, `MenuBar`, `BreadcrumbBar`, `RefreshContainer`, `TeachingTip`, `ColorPicker`, `NumberBox`, `RatingControl`, `SplitButton`, etc. all mount and update under AOT, because Reactor's shipped targets mitigate WindowsAppSDK#6394 automatically. `{TemplateBinding}` against built-in DPs works; private DPs declared in a separate third-party assembly without an `IXamlMetadataProvider` are still broken (Issue #142). +- **Theming** — `ThemeRef` tokens, style caching. Brushes resolved through `XamlControlsResources` work under AOT; the Reactor package depends on build tools that publish the project `.pri`, so no app-side setup is needed (see [Publish-time resources](#publish-time-resources) below). +- **Built-in WinUI controls** — `NavigationView`, `TabView`, `Pivot`, `TreeView`, `Expander`, `InfoBar`, `MenuBar`, `BreadcrumbBar`, `RefreshContainer`, `TeachingTip`, `ColorPicker`, `NumberBox`, `RatingControl`, `SplitButton`, etc. all mount and update under AOT, because the build tools the Reactor package depends on fix WindowsAppSDK#6394. `{TemplateBinding}` against built-in DPs works; private DPs declared in a separate third-party assembly without an `IXamlMetadataProvider` are still broken (Issue #142). - **Markdown / Localization (read path)** — `IStringLoc`/`Loc.X.Y` lookups generated by `Reactor.Localization.Generator` are AOT-clean. Source-generated, no runtime reflection. ## What does *not* work under AOT (today) @@ -33,8 +33,8 @@ These subsystems compile cleanly with `IsAotCompatible=true` (the warnings are s | **Form validation** | `FormField`'s default editor resolution goes through `TypeRegistry`. Same caveat as PropertyGrid. | Issue #70 | | **Component discovery (`ReactorApp.Run` reflection paths)** | The instantiation of `TApp` itself is annotated and works. Devtools component enumeration is available only when the app opts into `Reactor.DevtoolsSupport` at build time and launches with `--devtools`; leave the switch off for retail/AOT builds. | Issue #70 | | **Devtools `properties` / `setProperty` DP discovery** | The tools locate `DependencyProperty` statics by reflection, and the trimmer keeps no reflection metadata for them unless something roots the members — so under AOT the lookups find nothing. Not specific to WinUI's CsWinRT-projected static properties: a C#-authored DP *field* on a custom control is equally undiscoverable. Works under JIT, which is where the devtools inner loop lives. The tool reports the likely cause rather than an empty result. See [Devtools DependencyProperty discovery](#devtools-dependencyproperty-discovery) for the measured cost of fixing it. Affected fixture: `Devtools_PropertyToolsDpDiscovery`. | Issue #1109 | -| **Theme resource lookup (`Theme.X`, `ThemeRef.Resolve`)** | Works — Reactor's shipped `_ReactorCopyWinUIResourcesToPublish` target puts the project `.pri` in the publish output (see [Required publish-time workarounds](#required-publish-time-workarounds)). Reactor's library itself is AOT-clean here. | WindowsAppSDK#6394 | -| **Third-party-assembly XAML metadata (Issue #142 fixtures)** | Built-in WinUI controls work under AOT now (see [Required publish-time workarounds](#required-publish-time-workarounds)). What remains is `{TemplateBinding}` against private DPs in a third-party assembly that ships *no* `.xaml` file: the XAML compiler only emits an `IXamlMetadataProvider` for projects that have at least one `.xaml`, and AOT trims any implicit metadata path. Affected fixtures: `Issue142_CustomControlPrivateDp_Renders`, `Issue142_ThirdPartyControlPrivateDp_Renders`. The library-author workaround is to register a hand-written `IXamlMetadataProvider` via `RegisterControlAssembly`. The same trim hits a compiled `.xaml` page inside a `ReactorApp.Run` app with no `App.xaml`: `ReactorApplication` finds the app's generated provider only by reflecting over the entry assembly, nothing roots it, and AOT removes it, so the page fails to parse (`Hosting_HostControlXamlComponentType`). A vanilla WinUI app is unaffected: its `App` *is* the provider. | Issue #142 | +| **Theme resource lookup (`Theme.X`, `ThemeRef.Resolve`)** | Works — the build tools the Reactor package depends on put the project `.pri` in the publish output (see [Publish-time resources](#publish-time-resources)). Reactor's library itself is AOT-clean here. | WindowsAppSDK#6394 | +| **Third-party-assembly XAML metadata (Issue #142 fixtures)** | Built-in WinUI controls work under AOT now (see [Publish-time resources](#publish-time-resources)). What remains is `{TemplateBinding}` against private DPs in a third-party assembly that ships *no* `.xaml` file: the XAML compiler only emits an `IXamlMetadataProvider` for projects that have at least one `.xaml`, and AOT trims any implicit metadata path. Affected fixtures: `Issue142_CustomControlPrivateDp_Renders`, `Issue142_ThirdPartyControlPrivateDp_Renders`. The library-author workaround is to register a hand-written `IXamlMetadataProvider` via `RegisterControlAssembly`. The same trim hits a compiled `.xaml` page inside a `ReactorApp.Run` app with no `App.xaml`: `ReactorApplication` finds the app's generated provider only by reflecting over the entry assembly, nothing roots it, and AOT removes it, so the page fails to parse (`Hosting_HostControlXamlComponentType`). A vanilla WinUI app is unaffected: its `App` *is* the provider. | Issue #142 | ## Devtools DependencyProperty discovery @@ -94,77 +94,30 @@ Devtools are a two-layer opt-in so release and NativeAOT binaries stay trimmed b - **The runtime canary.** CI's `AOT Selftests` job (`.github/workflows/ci.yml`) publishes `tests/Reactor.AppTests.Host` with `PublishAotInternal=true` and runs the full selftest suite against the NativeAOT binary on every PR, split across two runners with `--shard 1/2` and `--shard 2/2` (see TESTING.md, "Sharding the corpus across runners"). `SelfTestRunner.DefaultAotSkipPatterns` mutes the 17 fixtures known to fail there (PropertyGrid auto-discovery, the three Devtools fixtures whose reflection targets get trimmed — `fire`'s handler lookup, `state`'s hook read, and DP discovery for `properties`/`setProperty` — the Issue142-style XAML-metadata cases, hot-reload state migration, plus the `UseObservableTree` property walk); any *new* failure fails the job. Note that "uses reflection" is not the same as "fails under AOT": the Devtools fixtures that run here mostly rely on type-name reflection (`GetType().Name`), which trimming always preserves — what breaks is member-level reflection whose target the trimmer drops. If you intentionally need to add a skip, document the bucket in the comment above `DefaultAotSkipPatterns` and re-probe with `tests/Reactor.AppTests.Host/probe-aot-skips.ps1`. Prefer splitting a fixture over skipping it whole — an entry mutes *every* check in the fixture, including ones that do pass under AOT, which is why DP discovery lives in its own `Devtools_PropertyToolsDpDiscovery` fixture. Treat that list as a liability, not a parking lot: a stale entry is AOT coverage silently switched off, so re-probe periodically and delete whatever now passes. `SelfTestRunner.ValidateDefaultSkipPatterns` aborts the run (TAP `Bail out!`, exit 1, no fixtures executed) if an entry stops matching any registered fixture, so a rename can't quietly turn a skip into a no-op. - **Probes can mask the defect they measure.** Reflection metadata under NativeAOT is emitted from what ILCompiler can see statically, so diagnostic code that calls `typeof(T).GetProperties()` / `GetMembers()` on a *constant* type roots that metadata and makes the failure disappear. `DynamicallyAccessedMemberTypes.PublicProperties` covers inherited members too, so probing one type can revive an entire base chain — a probe added to the #1109 fixture turned all 112 of `Button`'s DP statics visible again and made the fixture pass. Always confirm an AOT verdict with the probe deleted. -## Required publish-time workarounds +## Publish-time resources -### WindowsAppSDK#6394 — project `.pri` (and `.xbf`) missing from publish output +### WindowsAppSDK#6394 — project `.pri` missing from publish output -When publishing an unpackaged WinUI 3 app (`WindowsPackageType=None`), the WindowsAppSDK build pipeline generates `$(AssemblyName).pri` into the build output but does *not* copy it into the publish directory. The MakePRI step is conditioned on `AppxPackage=true`, so unpackaged apps fall through. Same applies to `.xbf` files generated by the XAML compiler. +**Nothing to do — the Reactor package carries the fix.** An unpackaged WinUI 3 app (`WindowsPackageType=None`) builds its own `$(AssemblyName).pri`, which also embeds the app's compiled XAML (`.xbf`), but `Microsoft.Windows.SDK.BuildTools.MSIX` before **1.7.260930100** never copies that file into the publish output. This is not AOT-specific: a plain `dotnet publish` loses it too. The fix shipped in 1.7.260930100 ([microsoft/WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394), closed 2026-10-01) and covers apps with or without `EnableMsixTooling`. Apps receive that package only transitively, through `Microsoft.WindowsAppSDK.Base`, and every Base release to date still resolves 1.7.251221100, so `Microsoft.UI.Reactor` declares its own dependency on `Microsoft.Windows.SDK.BuildTools.MSIX` 1.7.260930100. NuGet resolves the higher of the two minimums, so every app that references Reactor gets the fixed targets. -**This is not AOT-specific.** It was originally found via `PublishAot=true`, but a plain `dotnet publish -c Release` loses the `.pri` just the same. +Measured on a published Reactor-only app, with the build-tools version as the only variable: -**Whether the missing `.pri` actually breaks the app depends on where WinUI's own resources come from** — which since #1283 depends on how the app is deployed. Measured on a published app that references only Reactor, with Reactor's copy target disabled so the `.pri` is genuinely absent: - -| deployment | app `.pri` in publish | theme resources resolve | app | -|---|---|---|---| -| framework-dependent (the default since #1283) | absent | **yes** | runs | -| self-contained (`WindowsAppSDKSelfContained=true`) | absent | **no** | **crashes `0xC000027B`** | - -A framework-dependent app loads WinUI — `Microsoft.UI.Xaml.dll` and its resources — from the machine-installed Windows App Runtime, so its own `.pri` is not on that path. A self-contained app bundles WinUI into its own folder and resolves it through its own `.pri`, so losing the `.pri` breaks it. The published framework-dependent output confirms the mechanism: it contains no `Microsoft.UI.Xaml.dll` at all. +| deployment | 1.7.251221100 (what the Windows App SDK resolves alone) | 1.7.260930100 (Reactor's dependency) | +|---|---|---| +| self-contained (`WindowsAppSDKSelfContained=true`) | no app `.pri`; **crashes `0xC000027B`** at startup | `.pri` published; runs | +| self-contained, `PublishAot=true` | no app `.pri`; **crashes `0xC000027B`** at startup | `.pri` published; runs | +| framework-dependent (the default) | no app `.pri`; runs and WinUI resources resolve, but **the app's own XAML fails to load** | `.pri` published; runs; own XAML loads | -Before #1283 every Reactor-only app was *silently* self-contained, which is why the crash was the default experience. #1283 made framework-dependent the default, so most apps no longer hit it — but self-contained remains a supported, documented opt-in (`WindowsAppSDKSelfContained=true`, for deploying to machines without the runtime), and that path still crashes without the `.pri`. The framework-dependent row is not a vacuous pass: deleting Reactor's own loose sidecars from that same output drops both probes to `False`, so the probe can still see a failure there. +The rows differ in what the `.pri` carries. A self-contained app's `.pri` also embeds WinUI's own `generic.xbf` and `themeresources.xbf`, so losing it takes WinUI down at startup. A framework-dependent app loads those from the installed Windows App Runtime, and its `.pri` holds only the app's own XAML. -The missing `.pri` has cascading runtime symptoms that look like separate bugs but all trace back to the same cause: +The missing `.pri` has also surfaced as runtime symptoms that look like separate bugs but trace back to the same cause: - `TabView`'s constructor throws `FileNotFoundException` from `ResourceAccessor::GetLocalizedStringResource(SR_TabViewCloseButtonTooltipWithKA)`. - `NavigationView` mounts but its template-apply throws the same `FileNotFoundException` (Reactor's error boundary catches it and renders an error `TextBlock`). - `Application.Current.Resources` loads as an empty dictionary (`0` merged, `0` themed, `0` keys) because `Application.LoadComponent` cascades through MRT for type-info lookups. - Every `ThemeRef.Resolve(...)` returns `null`; `.Foreground(Theme.X)` falls back to control defaults. -Upstream tracking: [microsoft/WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394) — closed 2026-09-24 as fixed in `Microsoft.Windows.SDK.BuildTools.MSIX` **1.7.260903100**, then **REOPENED 2026-09-25**: *"Reopening because fix on MRT core version used when not enabling msix tooling is still not released."* That is upstream confirming the gate described below — the fix exists, but only on the MSIX-tooling path. - -**Nothing to do — Reactor ships the workaround.** `build/Microsoft.UI.Reactor.targets` defines `_ReactorCopyWinUIResourcesToPublish`, which runs after `Publish` for any unpackaged Reactor executable and copies the project `.pri` plus every `.xbf` into `$(PublishDir)`. It applies to AOT and non-AOT publishes alike, so app projects no longer carry a per-project copy of this target (they used to; it was gated on `PublishAot=true` and therefore missed the plain-publish case). - -Opt out — for example if you drive the copy yourself — with: - -```xml -false -``` - -**Alternative: `true`.** Suggested on the upstream issue, and it does work — measured: the app `.pri` lands in the publish output and the published app runs. - -**The upstream fix, and why it does not cover us.** `Microsoft.Windows.SDK.BuildTools.MSIX.Pri.targets` defines `AddProjectPriToResolvedFileToPublish` and `AddXbfFilesToResolvedFileToPublish`, which add the project `.pri` and the `.xbf` files to `@(ResolvedFileToPublish)` — precisely the gap. The XBF half is what landed in **1.7.260903100** and closed the issue; the `.pri` half was already in 1.7.251221100. Both live in a file reached only through -`EnableMsixTooling` → `MsixPackageSupport` → `ShouldImportMsixCommonTargets` → `MsixCommonTargets` → `Packaging.targets` → `Pri.targets`. -With that flag off the build takes the `MrtCore.targets` branch instead, which has no publish step at all. - -That gate is not an oversight, and the upstream "fixed" is not wrong: **the official WinUI templates set `true` by default**, so the mainstream WinUI app is genuinely fixed by upgrading. Reactor's template ships `WindowsPackageType=None` for xcopy deployment and does not set that flag, so the platform's targets do not run for it. - -**Turning the flag on is not yet sufficient either, which is the reason this target still exists.** The two halves of the upstream fix shipped separately: `AddProjectPriToResolvedFileToPublish` is in 1.7.251221100, but `AddXbfFilesToResolvedFileToPublish` — the half that closed the issue — is only in **1.7.260903100**, and no `Microsoft.WindowsAppSDK` version pulls that yet. Measured: the newest SDK (2.5.1) resolves `Microsoft.Windows.SDK.BuildTools.MSIX/1.7.251221100`, and an unpackaged consumer with `EnableMsixTooling=true` on that version publishes its app `.pri` but **not** its own compiled `.xbf`. So doing it entirely "the upstream way" today still leaves half the gap open. - -Measured on 1.7.260903100 (explicitly pinned) with a `-v:detailed` publish, counting how many times each fix target actually executes: - -| cell | `AddProjectPriToResolvedFileToPublish` | `AddXbfFilesToResolvedFileToPublish` | app `.pri` published | -|---|---|---|---| -| no `EnableMsixTooling` | 0 | 0 | **no** | -| `EnableMsixTooling=true` | 2 | 2 | yes | - -The second row is the control: it proves the probe can match, so the zeroes in the first row are a measurement rather than a broken grep. Publishing the stock `reactorapp` template on the newest stable SDK (metapackage 2.5.1, WinUI 2.3.9, Foundation 2.3.12) with Reactor's target disabled still produces no app `.pri` and still crashes with `0xC000027B`. - -**So upgrading the build tools does not by itself remove the need for this workaround** — for an app in Reactor's default shape. - -Reactor does **not** set the flag for you. Whether an app wants MSIX packaging tooling is the app's decision, not a framework's, and it pulls in the whole single-project MSIX pipeline to fix one missing copy. The shipped target is narrower, explicit, and works whichever way an app sets `EnableMsixTooling`. The two compose cleanly if you do enable it — verified: no errors, no duplicated files, app runs. - -**The discriminator is the flag, not the packaging kind.** Visual Studio's WinUI templates set `EnableMsixTooling=true` and every MSIX project in this repo does too, so those apps get the platform's own fix; Reactor's target deliberately excludes packaged ones (`AppxPackage != true`, `WindowsPackageType != MSIX`) and is a harmless no-op for an unpackaged app that opts the flag on. The two paths cover complementary populations: - -| app shape | `EnableMsixTooling` | platform fix runs | Reactor's target runs | -|---|---|---|---| -| unpackaged, flag off (Reactor's template default) | off | no | **yes** | -| packaged (`WindowsPackageType=MSIX`) | on | yes | no (excluded) | - -`samples/ReactorGallery` demonstrates both, because it builds either way: its default is unpackaged, and `-p:GalleryPackaged=true` flips it to MSIX **and** sets `EnableMsixTooling=true` in the same conditional block. Evaluated with `dotnet msbuild -getProperty`, the unpackaged mode reports `EnableMsixTooling=false` with an empty `MsixPackageSupport`, and the packaged mode reports `true` / `true` / `AppxPackage=true`. Publishing it unpackaged, the platform's fix targets run 0 times, Reactor's target runs, and the app `.pri` is published. Note the two settings never co-exist — reading the raw csproj can suggest otherwise, since the `WindowsPackageType=None` and `EnableMsixTooling=true` lines live in different `Condition`ed `PropertyGroup`s. - -**It does not fix issue #1271.** Measured against pre-fix packages at the failing geometry: with and without `EnableMsixTooling=true` the build fails identically with `PRI175` / `PRI222` on `Microsoft.UI.Reactor.Devtools.pri.xml`. Library `.pri` files still get expanded; keeping them out of `lib/` is a separate and still-necessary fix. - -**When to delete the shipped target.** Not "when #6394 closes" — it already closed once, on 2026-09-24, and was reopened the next day precisely because the non-MSIX-tooling path is still unfixed. Delete this when **both** hold: a `Microsoft.WindowsAppSDK` release actually pulls build tools carrying the fix (as of 2026-09-25 the newest `Microsoft.WindowsAppSDK.Base`, 2.0.4, still pins `1.7.251221100`), **and** either those targets move outside the `MsixPackageSupport` gate or Reactor's template adopts `EnableMsixTooling=true`. Adopting the flag alone is not enough today — measured above, it publishes the `.pri` but not the `.xbf`. The `ConsumerPublishIncludesTheAppPriAndXbfSidecars` test in `tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs` is the tripwire: its opt-out control asserts the app `.pri` and the consumer's own `.xbf` are *absent* when Reactor's target is disabled, so it starts failing the moment the platform begins doing this itself. +An app that pins `Microsoft.Windows.SDK.BuildTools.MSIX` below 1.7.260930100 itself brings the gap back. NuGet fails that restore with an `NU1605` package-downgrade error unless the app suppresses it. ## Debugging an AOT selftest hang diff --git a/docs/specs/022-packaging-and-distribution.md b/docs/specs/022-packaging-and-distribution.md index 53327a77e..88920d4c5 100644 --- a/docs/specs/022-packaging-and-distribution.md +++ b/docs/specs/022-packaging-and-distribution.md @@ -180,37 +180,31 @@ shipping `Reactor.pri` from a `sidecar/` folder with a plain `` item still produces a `makepri.exe Dump -IndexFile …\sidecar\Reactor.pri` invocation. It has to be absent. -**The upstream long-path fix does not retire this.** The `MAX_PATH` defect itself is -[microsoft/WindowsAppSDK#6795](https://github.com/microsoft/WindowsAppSDK/issues/6795) -(`WinAppSdkExpandPriContent` passes a non-extended-length path to `makepri.exe`), still -open. A fix exists and was verified by A/B in `Microsoft.Windows.SDK.BuildTools.MSIX` -**1.7.260925103** — but that is a private drop, not a feed package: checked against -nuget.org on 2026-09-25 it returns 404, while 1.7.251221100 and 1.7.260903100 return 200 -and a nonsense version returns 404 (i.e. the probe can tell absent from broken). - -**No published version fixes it.** The newest published build tools, 1.7.260903100, were -A/B'd at a failing depth and still fail — 6 `PRI` and 4 `APPX` errors — which matches the -structure: that version's `MrtCore.PriExpansion.targets` still passes a bare -`IntermediateDirectory="$(IntermediateOutputPath)"`, and its task assembly contains no -`\\?\` handling at all. - -Consumers also do not choose this package directly. Build tools reach an app only through -`Microsoft.WindowsAppSDK.Base`, the sole package in the graph that references them — and -**no Base pins even the newest published build tools**, let alone the fixed private drop. -Verified at nuspec level across stable *and* prerelease: 2.0.4 (the newest stable, pulled -by Windows App SDK 2.5.1) and 2.0.5-experimental2 (the newest of any kind) both pin -`1.7.251221100`; 2.0.250911001-experimental pins an older `1.7.20250829.1` still. Note the -version strings: a plain `2.0.5` does not exist, only `2.0.5-experimental2`, so a probe for -the former 404s without that meaning there is no newer Base. So the version is transitive: an app cannot opt into a fixed build-tools -release by upgrading Windows App SDK, and pinning the newest published one would not help -either. - -That last point is why this section outlives the upstream fix. Even once 1.7.260925103 (or -its successor) ships and flows, the `.pri` removal keeps the trigger away from every -consumer still resolving an older build-tools version — which, being transitive, is not -something they control. And the layout is right on its own terms regardless: the -`.Advanced` and `.Devtools` indexes are **entirely empty**, and shipping them costs every -consumer six wasted `makepri.exe Dump` invocations per build. +**The upstream long-path fix, and why this section outlives it.** The `MAX_PATH` defect +itself is [microsoft/WindowsAppSDK#6795](https://github.com/microsoft/WindowsAppSDK/issues/6795) +(`WinAppSdkExpandPriContent` passed a non-extended-length path to `makepri.exe`), fixed in +`Microsoft.Windows.SDK.BuildTools.MSIX` **1.7.260930100** (closed 2026-10-01). The fix is a +`\\?\` prefix in the task rather than a shorter path — the dumps still land in +`$(IntermediateOutputPath)` — so it also covers the expansion path a Reactor app takes. +Measured at matched depths, varying only the build-tools version: + +| consumer | 1.7.251221100 | 1.7.260930100 | +|---|---|---| +| pre-fix packages (ship `.pri`), framework-dependent | `PRI175` on `Microsoft.UI.Reactor.Devtools.pri.xml` | builds | +| this layout (no `.pri`), self-contained | `PRI175` on the SDK's own `Microsoft.WindowsAppRuntime.pri.xml` | builds | + +Build tools reach an app only through `Microsoft.WindowsAppSDK.Base`, and every Base release +to date — 2.0.4 stable and 2.0.5-experimental2 alike — pins `1.7.251221100`. So +`Microsoft.UI.Reactor` declares its own dependency on 1.7.260930100 (it also fixes +WindowsAppSDK#6394; see [AOT support](../aot-support.md#publish-time-resources)), and every +consumer resolves the fixed version. The second row is why that matters even with no `.pri` +in `lib/`: in a self-contained app the Windows App SDK's own indexes hit the same limit. + +The `.pri` removal stands on its own terms. The `.Advanced` and `.Devtools` indexes are +**entirely empty** and the core one duplicates a file that ships loose, yet each cost every +consumer a `makepri.exe Dump` per build — six per build with all three referenced, through +both the reference and the payload path. And an app that pins build tools below Reactor's +floor would hit the original failure again. Each of the three csproj files therefore narrows the allow-list that decides what may sit in the build-output folder: diff --git a/src/Reactor/Reactor.csproj b/src/Reactor/Reactor.csproj index e8f5cb761..b0dfc701b 100644 --- a/src/Reactor/Reactor.csproj +++ b/src/Reactor/Reactor.csproj @@ -123,6 +123,25 @@ + + + + + - - - - - <_ReactorWinUIPublishResource Include="$(OutputPath)**\*.xbf" - Exclude="$(PublishDir)**\*.xbf" /> - <_ReactorWinUIPublishResource Include="$(ProjectPriFullPath)" - Condition="'$(ProjectPriFullPath)' != '' and Exists('$(ProjectPriFullPath)')" /> - <_ReactorWinUIPublishResource Include="$(OutputPath)$(AssemblyName).pri" - Condition="'$(ProjectPriFullPath)' == '' and Exists('$(OutputPath)$(AssemblyName).pri')" /> - - - diff --git a/tests/Reactor.AppTests.Host/SelfTest/SelfTestRunner.cs b/tests/Reactor.AppTests.Host/SelfTest/SelfTestRunner.cs index 059ad58fc..0bc837165 100644 --- a/tests/Reactor.AppTests.Host/SelfTest/SelfTestRunner.cs +++ b/tests/Reactor.AppTests.Host/SelfTest/SelfTestRunner.cs @@ -166,8 +166,9 @@ private sealed record FixtureProgress(string Name, long StartTimestamp, TimeSpan // Fixtures known to assert-fail under NativeAOT, captured by running // tests/Reactor.AppTests.Host/probe-aot-skips.ps1 against the AOT-published - // Host. As of WindowsAppSDK#6394 workaround (see Reactor.AppTests.Host.csproj - // _CopyWinUIResourcesForAot target), all NATIVE_CRASH skips are gone. What is + // Host. Since the WindowsAppSDK#6394 fix (the project .pri reaches the AOT + // publish output, via the build tools Reactor depends on), all NATIVE_CRASH + // skips are gone. What is // left is the buckets below: the UseObservableTree property walk, the // Devtools fixtures that reflect over members the trimmer removes, // PropertyGrid auto-discovery, the Issue142 XAML-metadata-provider edge diff --git a/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs b/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs index 07807172d..b8228a544 100644 --- a/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs +++ b/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs @@ -1,4 +1,5 @@ using System.IO.Compression; +using System.Text.Json; using Xunit; namespace Microsoft.UI.Reactor.IntegrationTests.Packaging; @@ -178,19 +179,22 @@ public void ConsumerBuildRunsNoMakepriDumpForReactorPackages() } /// - /// WindowsAppSDK#6394 — the package's targets must put the CONSUMING app's own `.pri` - /// (and the `.xbf` sidecars) into its publish output. + /// WindowsAppSDK#6394 — the CONSUMING app's own .pri must reach its publish output. /// - /// The Windows App SDK generates the app's resource index into the build output but - /// never copies it to publish for an unpackaged app, and the published app then dies at - /// startup with 0xC000027B inside native XAML. Reactor ships - /// _ReactorCopyWinUIResourcesToPublish to close that gap for every consumer; this - /// test is what keeps it working, since nothing else fails if the target stops running — - /// the build stays green and only the published app breaks. + /// An unpackaged app builds a .pri that embeds its compiled XAML and, when + /// self-contained, WinUI's own generic.xbf / themeresources.xbf. Build tools + /// before Microsoft.Windows.SDK.BuildTools.MSIX 1.7.260930100 never copy it to + /// publish, and the published self-contained app dies at startup with 0xC000027B. + /// The Windows App SDK still resolves an older version on its own, so Reactor's package + /// declares the fixed one as a dependency floor. Nothing else fails if that floor is lost — + /// the build stays green and only the published app breaks — so this test is what keeps it. /// - /// The opt-out property doubles as the control: it must reproduce the missing file. - /// Without that half, a publish that happened to include the .pri for some unrelated - /// reason would let this pass while the target did nothing. + /// The control pins the same consumer back to the version the Windows App SDK resolves + /// alone (suppressing the NU1605 downgrade warning that a direct reference below a + /// dependency's minimum raises), so the build-tools version is the only variable between the + /// two publishes. It must reproduce the missing file. Without that half, a publish that + /// produced the .pri for an unrelated reason would let this pass while the floor did + /// nothing. /// /// The two publishes deliberately run from SEPARATE project directories. Publishing the /// same project twice with a different global property forces a full re-evaluation that reuses @@ -200,66 +204,73 @@ public void ConsumerBuildRunsNoMakepriDumpForReactorPackages() /// aware. One publish per directory stays clear of both. /// [Fact] - public void ConsumerPublishIncludesTheAppPriAndXbfSidecars() + public void ConsumerPublishIncludesTheAppPri() { // Short directory names on purpose: these paths feed the XAML compiler, which fails // around MAX_PATH, and the fixture's package folder already consumes most of the budget. - var onDir = Path.Join(_tempRoot, "on"); - var offDir = Path.Join(_tempRoot, "off"); - Directory.CreateDirectory(onDir); - Directory.CreateDirectory(offDir); - - foreach (var dir in new[] { onDir, offDir }) + var floorDir = Path.Join(_tempRoot, "on"); + var downgradedDir = Path.Join(_tempRoot, "off"); + Directory.CreateDirectory(floorDir); + Directory.CreateDirectory(downgradedDir); + + // Core package only. The publish path is the one that runs the XAML compiler against + // the full reference closure, and dragging in Advanced (Win2D) and Devtools adds + // references without adding coverage — the floor under test is in the core package. + WriteConsumerProject(floorDir, coreOnly: true); + WriteConsumerProject(downgradedDir, coreOnly: true, buildToolsMsixVersion: BuildToolsMsixWithoutPublishFix); + foreach (var dir in new[] { floorDir, downgradedDir }) { - // Core package only. The publish path is the one that runs the XAML compiler against - // the full reference closure, and dragging in Advanced (Win2D) and Devtools adds - // references without adding coverage — the target under test is in the core package. - WriteConsumerProject(dir, coreOnly: true); WriteConsumerProgram(dir); - WriteConsumerXaml(dir); CreateNuGetConfig(dir); } - var publishDir = Path.Join(onDir, "pub"); + var publishDir = Path.Join(floorDir, "pub"); RunHelpers.RunDotnet( $"publish -c Release -a {_fixture.RunArchitecture} -o \"{publishDir}\"", - onDir, + floorDir, _fixture.CommandEnvironment, timeoutMs: 600_000); Assert.True( File.Exists(Path.Join(publishDir, "Consumer.pri")), "The app's own .pri is missing from the publish output, so the published app would " + - "start with an empty resource dictionary and crash in native XAML (WindowsAppSDK#6394). " + - "Reactor's _ReactorCopyWinUIResourcesToPublish target is supposed to copy it."); - - // The CONSUMER's own compiled XAML, not Reactor's. ReactorApplication.xbf would be a - // vacuous check here: the package delivers it through a - // item, so it lands in publish whether or not this target's $(OutputPath)**\*.xbf glob - // runs. Only an .xbf the consumer compiles itself exercises that glob. - Assert.True( - File.Exists(Path.Join(publishDir, "Themes", "ConsumerStyles.xbf")), - "The consumer's own compiled XAML (Themes/ConsumerStyles.xbf) is missing from the " + - "publish output, and its relative folder must be preserved for ms-appx:/// to resolve it."); + "crash in native XAML at startup (WindowsAppSDK#6394). Reactor's dependency on " + + $"{BuildToolsMsixId} is supposed to bring the fix; the consumer resolved " + + $"{ResolvedBuildToolsMsix(floorDir)}."); - // Control: with the target disabled both files must disappear again. If they do not, - // something else is supplying them and the assertions above prove nothing about the target. - var optOutDir = Path.Join(offDir, "pub"); + var downgradedPublishDir = Path.Join(downgradedDir, "pub"); RunHelpers.RunDotnet( - $"publish -c Release -a {_fixture.RunArchitecture} -o \"{optOutDir}\" -p:ReactorCopyWinUIResourcesToPublish=false", - offDir, + $"publish -c Release -a {_fixture.RunArchitecture} -o \"{downgradedPublishDir}\"", + downgradedDir, _fixture.CommandEnvironment, timeoutMs: 600_000); Assert.False( - File.Exists(Path.Join(optOutDir, "Themes", "ConsumerStyles.xbf")), - "Opting out of _ReactorCopyWinUIResourcesToPublish still produced the consumer's own " + - ".xbf, so the passing assertion above does not attribute the copy to Reactor's target."); + File.Exists(Path.Join(downgradedPublishDir, "Consumer.pri")), + $"Pinned back to {BuildToolsMsixId} {BuildToolsMsixWithoutPublishFix} (resolved " + + $"{ResolvedBuildToolsMsix(downgradedDir)}), the publish still produced the app .pri, so " + + "the passing assertion above does not attribute it to Reactor's dependency floor."); + } - Assert.False( - File.Exists(Path.Join(optOutDir, "Consumer.pri")), - "Opting out of _ReactorCopyWinUIResourcesToPublish still produced the app .pri, so the " + - "passing assertion above does not actually attribute the copy to Reactor's target."); + private const string BuildToolsMsixId = "Microsoft.Windows.SDK.BuildTools.MSIX"; + + // What Microsoft.WindowsAppSDK.Base still resolves on its own, and the last version + // without the WindowsAppSDK#6394 publish fix. + private const string BuildToolsMsixWithoutPublishFix = "1.7.251221100"; + + private static string ResolvedBuildToolsMsix(string appDir) + { + var assetsPath = Path.Join(appDir, "obj", "project.assets.json"); + if (!File.Exists(assetsPath)) + { + return "nothing (no project.assets.json)"; + } + + using var assets = JsonDocument.Parse(File.ReadAllText(assetsPath)); + return assets.RootElement.GetProperty("libraries").EnumerateObject() + .Select(library => library.Name) + .FirstOrDefault(name => name.StartsWith(BuildToolsMsixId + "/", StringComparison.OrdinalIgnoreCase)) + ?? $"no {BuildToolsMsixId} at all"; } private string[] ReadPackageEntries(string packageId) @@ -277,18 +288,19 @@ private string[] ReadPackageEntries(string packageId) /// were empty — including the Devtools one with the longest, build-breaking file name — /// untested. trims that down for the publish test, which does not /// need them and is sensitive to how many references the XAML compiler has to resolve. + /// pins the build tools below Reactor's floor for + /// the publish test's control. /// - /// WindowsAppSDKSelfContained=true is deliberate, not incidental. It is the - /// deployment in which a missing app .pri actually breaks a published app: a - /// self-contained app resolves WinUI through its own .pri, whereas a - /// framework-dependent one — the default since Reactor's Windows App Runtime dependency was - /// restored — loads WinUI from the installed runtime and runs without it. Measured: with the - /// copy target disabled, the framework-dependent publish runs with theme resources resolving - /// and the self-contained one crashes with 0xC000027B. Do not "simplify" this to the - /// framework-dependent default; the publish test would still pass, but it would stop testing - /// the case the target exists for. + /// WindowsAppSDKSelfContained=true is deliberate, not incidental, for two + /// reasons. It is the deployment in which a missing app .pri crashes the published app + /// (0xC000027B), because the .pri also carries WinUI's own XAML; a + /// framework-dependent app loads that from the installed runtime and loses only its own XAML. + /// And it is what makes the Windows App SDK dump its framework PRIs at all, which is the + /// positive control in : + /// measured, a framework-dependent consumer dumps none, so that control would have nothing to + /// match. Do not "simplify" this to the framework-dependent default. /// - private void WriteConsumerProject(string appDir, bool coreOnly = false) + private void WriteConsumerProject(string appDir, bool coreOnly = false, string? buildToolsMsixVersion = null) { var extraPackages = coreOnly ? string.Empty @@ -298,6 +310,21 @@ private void WriteConsumerProject(string appDir, bool coreOnly = false) """; + // A direct reference below a dependency's minimum is an NU1605 downgrade, which fails the + // restore by default; the control wants exactly that downgrade. + var buildToolsPin = buildToolsMsixVersion is null + ? string.Empty + : $""" + + + """; + var noWarn = buildToolsMsixVersion is null + ? string.Empty + : """ + + $(NoWarn);NU1605 + """; + var csproj = $""" @@ -308,10 +335,10 @@ private void WriteConsumerProject(string appDir, bool coreOnly = false) true {_fixture.RunArchitecture} {_fixture.RunArchitecture} - win-{_fixture.RunArchitecture} + win-{_fixture.RunArchitecture}{noWarn} - {extraPackages} + {extraPackages}{buildToolsPin} """; @@ -322,8 +349,8 @@ private void WriteConsumerProject(string appDir, bool coreOnly = false) private static void WriteConsumerProgram(string appDir) { // OutputType=Exe is what makes the Windows App SDK compute input PRIs at all - // (ShouldComputeInputPris), so the app must be an executable. It is never run here — - // this test is about the build, and CreateTemplateTests already covers running. + // (ShouldComputeInputPris), so the app must be an executable. It is never run here: + // these tests assert on the build and publish layout. File.WriteAllText(Path.Join(appDir, "Program.cs"), """ using static Microsoft.UI.Reactor.Factories; @@ -332,27 +359,6 @@ private static void WriteConsumerProgram(string appDir) """); } - /// - /// A consumer-owned XAML file, so the publish test can assert on an .xbf that the - /// consumer compiles rather than one the package already delivers by another route. - /// A keyless ResourceDictionary is the smallest thing the XAML compiler will emit an - /// .xbf for; it lives in a subfolder so the copy's RecursiveDir handling is - /// covered too. - /// - private static void WriteConsumerXaml(string appDir) - { - var themes = Path.Join(appDir, "Themes"); - Directory.CreateDirectory(themes); - - File.WriteAllText(Path.Join(themes, "ConsumerStyles.xaml"), """ - - 42 - - """); - } - private void CreateNuGetConfig(string appDir) { RunHelpers.RunDotnet( From 0f2c13e046b2e7ae3c4ba023fcf06ed3bd6910cc Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Wed, 14 Oct 2026 14:22:21 -0700 Subject: [PATCH 19/20] Keep shipping .pri files; trim docs and comments The MSIX build-tools dependency (1.7.260930100) fixes the MAX_PATH failure behind issue #1271 by itself. Measured with only the build-tools version changed, consumers of all three packages with their .pri files restored fail with PRI175 on 1.7.251221100 and build on 1.7.260930100, at the same depths that failed before (framework-dependent and self-contained). Self-contained, framework-dependent and packaged (MSIX) consumers all run. So the packages return to NuGet's standard layout: Advanced, Devtools and Reactor.targets match main again, and core only appends .xbf to the allow-list for _PackReactorApplicationXbf. The two tests that asserted the exclusion are gone. CHANGELOG, aot-support.md, spec 022 and the remaining code comments are cut down to what a reader needs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- CHANGELOG.md | 28 +- Directory.Packages.props | 4 +- docs/aot-support.md | 32 +- docs/specs/022-packaging-and-distribution.md | 130 ++----- src/Reactor.Advanced/Reactor.Advanced.csproj | 8 - src/Reactor.Devtools/Reactor.Devtools.csproj | 7 - src/Reactor/Reactor.csproj | 76 +--- src/Reactor/build/Reactor.targets | 20 +- .../Packaging/LocalPackageFeedFixture.cs | 13 +- .../Packaging/PriPackagingTests.cs | 341 ++++-------------- 10 files changed, 142 insertions(+), 517 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a05f885fe..bc215c803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -647,24 +647,16 @@ Conventions for contributors: `Cancel()` cleanup (PR #1124), that the source had been pooled and reset, was wrong: that crash was this same fault. -- **Reactor packages no longer ship a `.pri` (issue #1271).** The index sat beside the - assembly in `lib/`, where `ResolveAssemblyReference` treats it as a reference-related file, - so every consumer build expanded it with `makepri.exe Dump` into `$(IntermediateOutputPath)` - — and on build tools before 1.7.260930100 that path broke past `MAX_PATH` with `PRI175` / - `PRI222` / `APPX0002`. Nothing is lost: the `.Advanced` and `.Devtools` indexes were empty - and the core one only embedded a second copy of `ReactorApplication.xbf`, which still ships - loose. `.pri` *generation* is unchanged, so `ProjectReference` consumers are unaffected. The - core package now also exposes one `lib/` folder instead of two for the same target framework. - -- **Published apps now include their own `.pri` - ([WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394)).** - `Microsoft.Windows.SDK.BuildTools.MSIX` before 1.7.260930100 leaves an unpackaged app's - `.pri` out of its publish output: self-contained apps (AOT or not) crashed at startup with - `0xC000027B`, and framework-dependent apps could not load their own XAML. Every Windows App - SDK release still resolves an older version, so `Microsoft.UI.Reactor` now depends on - 1.7.260930100 directly, which also fixes the `MAX_PATH` failure above for the SDK's own - `.pri` files. Replaces the per-project `_CopyWinUIResourcesForAot` snippet (removed from 11 - projects). +- **Apps that reference Reactor no longer fail to build in deeply nested folders, and their + publish output now includes the app's `.pri` (issue #1271, + [WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394)).** + `Microsoft.UI.Reactor` now depends on `Microsoft.Windows.SDK.BuildTools.MSIX` 1.7.260930100, + which fixes both. If you copied the `_CopyWinUIResourcesForAot` target from + `docs/aot-support.md`, you can delete it. + +- **The `Microsoft.UI.Reactor` package has a single `lib/` folder and always contains + `ReactorApplication.xbf`.** A second `lib/` folder used to hold only that file, and packing + from a clean tree left it out. ### Security diff --git a/Directory.Packages.props b/Directory.Packages.props index b8e91f8b2..e3d67cf03 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -28,9 +28,7 @@ - + diff --git a/docs/aot-support.md b/docs/aot-support.md index b424b46e1..bf15d469a 100644 --- a/docs/aot-support.md +++ b/docs/aot-support.md @@ -16,8 +16,8 @@ This page enumerates what works, what doesn't, and what's planned. **If you publ - **Markdown** — md4c-backed parser and renderer. - **Commanding** — command records, focus-scoped accelerators. - **Animation** — compositor-layer transitions, keyframes. -- **Theming** — `ThemeRef` tokens, style caching. Brushes resolved through `XamlControlsResources` work under AOT; the Reactor package depends on build tools that publish the project `.pri`, so no app-side setup is needed (see [Publish-time resources](#publish-time-resources) below). -- **Built-in WinUI controls** — `NavigationView`, `TabView`, `Pivot`, `TreeView`, `Expander`, `InfoBar`, `MenuBar`, `BreadcrumbBar`, `RefreshContainer`, `TeachingTip`, `ColorPicker`, `NumberBox`, `RatingControl`, `SplitButton`, etc. all mount and update under AOT, because the build tools the Reactor package depends on fix WindowsAppSDK#6394. `{TemplateBinding}` against built-in DPs works; private DPs declared in a separate third-party assembly without an `IXamlMetadataProvider` are still broken (Issue #142). +- **Theming** — `ThemeRef` tokens, style caching. Brushes resolved through `XamlControlsResources` work under AOT. +- **Built-in WinUI controls** — `NavigationView`, `TabView`, `Pivot`, `TreeView`, `Expander`, `InfoBar`, `MenuBar`, `BreadcrumbBar`, `RefreshContainer`, `TeachingTip`, `ColorPicker`, `NumberBox`, `RatingControl`, `SplitButton`, etc. all mount and update under AOT. `{TemplateBinding}` against built-in DPs works; private DPs declared in a separate third-party assembly without an `IXamlMetadataProvider` are still broken (Issue #142). - **Markdown / Localization (read path)** — `IStringLoc`/`Loc.X.Y` lookups generated by `Reactor.Localization.Generator` are AOT-clean. Source-generated, no runtime reflection. ## What does *not* work under AOT (today) @@ -33,8 +33,7 @@ These subsystems compile cleanly with `IsAotCompatible=true` (the warnings are s | **Form validation** | `FormField`'s default editor resolution goes through `TypeRegistry`. Same caveat as PropertyGrid. | Issue #70 | | **Component discovery (`ReactorApp.Run` reflection paths)** | The instantiation of `TApp` itself is annotated and works. Devtools component enumeration is available only when the app opts into `Reactor.DevtoolsSupport` at build time and launches with `--devtools`; leave the switch off for retail/AOT builds. | Issue #70 | | **Devtools `properties` / `setProperty` DP discovery** | The tools locate `DependencyProperty` statics by reflection, and the trimmer keeps no reflection metadata for them unless something roots the members — so under AOT the lookups find nothing. Not specific to WinUI's CsWinRT-projected static properties: a C#-authored DP *field* on a custom control is equally undiscoverable. Works under JIT, which is where the devtools inner loop lives. The tool reports the likely cause rather than an empty result. See [Devtools DependencyProperty discovery](#devtools-dependencyproperty-discovery) for the measured cost of fixing it. Affected fixture: `Devtools_PropertyToolsDpDiscovery`. | Issue #1109 | -| **Theme resource lookup (`Theme.X`, `ThemeRef.Resolve`)** | Works — the build tools the Reactor package depends on put the project `.pri` in the publish output (see [Publish-time resources](#publish-time-resources)). Reactor's library itself is AOT-clean here. | WindowsAppSDK#6394 | -| **Third-party-assembly XAML metadata (Issue #142 fixtures)** | Built-in WinUI controls work under AOT now (see [Publish-time resources](#publish-time-resources)). What remains is `{TemplateBinding}` against private DPs in a third-party assembly that ships *no* `.xaml` file: the XAML compiler only emits an `IXamlMetadataProvider` for projects that have at least one `.xaml`, and AOT trims any implicit metadata path. Affected fixtures: `Issue142_CustomControlPrivateDp_Renders`, `Issue142_ThirdPartyControlPrivateDp_Renders`. The library-author workaround is to register a hand-written `IXamlMetadataProvider` via `RegisterControlAssembly`. The same trim hits a compiled `.xaml` page inside a `ReactorApp.Run` app with no `App.xaml`: `ReactorApplication` finds the app's generated provider only by reflecting over the entry assembly, nothing roots it, and AOT removes it, so the page fails to parse (`Hosting_HostControlXamlComponentType`). A vanilla WinUI app is unaffected: its `App` *is* the provider. | Issue #142 | +| **Third-party-assembly XAML metadata (Issue #142 fixtures)** | Built-in WinUI controls work under AOT now. What remains is `{TemplateBinding}` against private DPs in a third-party assembly that ships *no* `.xaml` file: the XAML compiler only emits an `IXamlMetadataProvider` for projects that have at least one `.xaml`, and AOT trims any implicit metadata path. Affected fixtures: `Issue142_CustomControlPrivateDp_Renders`, `Issue142_ThirdPartyControlPrivateDp_Renders`. The library-author workaround is to register a hand-written `IXamlMetadataProvider` via `RegisterControlAssembly`. The same trim hits a compiled `.xaml` page inside a `ReactorApp.Run` app with no `App.xaml`: `ReactorApplication` finds the app's generated provider only by reflecting over the entry assembly, nothing roots it, and AOT removes it, so the page fails to parse (`Hosting_HostControlXamlComponentType`). A vanilla WinUI app is unaffected: its `App` *is* the provider. | Issue #142 | ## Devtools DependencyProperty discovery @@ -94,30 +93,9 @@ Devtools are a two-layer opt-in so release and NativeAOT binaries stay trimmed b - **The runtime canary.** CI's `AOT Selftests` job (`.github/workflows/ci.yml`) publishes `tests/Reactor.AppTests.Host` with `PublishAotInternal=true` and runs the full selftest suite against the NativeAOT binary on every PR, split across two runners with `--shard 1/2` and `--shard 2/2` (see TESTING.md, "Sharding the corpus across runners"). `SelfTestRunner.DefaultAotSkipPatterns` mutes the 17 fixtures known to fail there (PropertyGrid auto-discovery, the three Devtools fixtures whose reflection targets get trimmed — `fire`'s handler lookup, `state`'s hook read, and DP discovery for `properties`/`setProperty` — the Issue142-style XAML-metadata cases, hot-reload state migration, plus the `UseObservableTree` property walk); any *new* failure fails the job. Note that "uses reflection" is not the same as "fails under AOT": the Devtools fixtures that run here mostly rely on type-name reflection (`GetType().Name`), which trimming always preserves — what breaks is member-level reflection whose target the trimmer drops. If you intentionally need to add a skip, document the bucket in the comment above `DefaultAotSkipPatterns` and re-probe with `tests/Reactor.AppTests.Host/probe-aot-skips.ps1`. Prefer splitting a fixture over skipping it whole — an entry mutes *every* check in the fixture, including ones that do pass under AOT, which is why DP discovery lives in its own `Devtools_PropertyToolsDpDiscovery` fixture. Treat that list as a liability, not a parking lot: a stale entry is AOT coverage silently switched off, so re-probe periodically and delete whatever now passes. `SelfTestRunner.ValidateDefaultSkipPatterns` aborts the run (TAP `Bail out!`, exit 1, no fixtures executed) if an entry stops matching any registered fixture, so a rename can't quietly turn a skip into a no-op. - **Probes can mask the defect they measure.** Reflection metadata under NativeAOT is emitted from what ILCompiler can see statically, so diagnostic code that calls `typeof(T).GetProperties()` / `GetMembers()` on a *constant* type roots that metadata and makes the failure disappear. `DynamicallyAccessedMemberTypes.PublicProperties` covers inherited members too, so probing one type can revive an entire base chain — a probe added to the #1109 fixture turned all 112 of `Button`'s DP statics visible again and made the fixture pass. Always confirm an AOT verdict with the probe deleted. -## Publish-time resources +## Publish output -### WindowsAppSDK#6394 — project `.pri` missing from publish output - -**Nothing to do — the Reactor package carries the fix.** An unpackaged WinUI 3 app (`WindowsPackageType=None`) builds its own `$(AssemblyName).pri`, which also embeds the app's compiled XAML (`.xbf`), but `Microsoft.Windows.SDK.BuildTools.MSIX` before **1.7.260930100** never copies that file into the publish output. This is not AOT-specific: a plain `dotnet publish` loses it too. The fix shipped in 1.7.260930100 ([microsoft/WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394), closed 2026-10-01) and covers apps with or without `EnableMsixTooling`. Apps receive that package only transitively, through `Microsoft.WindowsAppSDK.Base`, and every Base release to date still resolves 1.7.251221100, so `Microsoft.UI.Reactor` declares its own dependency on `Microsoft.Windows.SDK.BuildTools.MSIX` 1.7.260930100. NuGet resolves the higher of the two minimums, so every app that references Reactor gets the fixed targets. - -Measured on a published Reactor-only app, with the build-tools version as the only variable: - -| deployment | 1.7.251221100 (what the Windows App SDK resolves alone) | 1.7.260930100 (Reactor's dependency) | -|---|---|---| -| self-contained (`WindowsAppSDKSelfContained=true`) | no app `.pri`; **crashes `0xC000027B`** at startup | `.pri` published; runs | -| self-contained, `PublishAot=true` | no app `.pri`; **crashes `0xC000027B`** at startup | `.pri` published; runs | -| framework-dependent (the default) | no app `.pri`; runs and WinUI resources resolve, but **the app's own XAML fails to load** | `.pri` published; runs; own XAML loads | - -The rows differ in what the `.pri` carries. A self-contained app's `.pri` also embeds WinUI's own `generic.xbf` and `themeresources.xbf`, so losing it takes WinUI down at startup. A framework-dependent app loads those from the installed Windows App Runtime, and its `.pri` holds only the app's own XAML. - -The missing `.pri` has also surfaced as runtime symptoms that look like separate bugs but trace back to the same cause: - -- `TabView`'s constructor throws `FileNotFoundException` from `ResourceAccessor::GetLocalizedStringResource(SR_TabViewCloseButtonTooltipWithKA)`. -- `NavigationView` mounts but its template-apply throws the same `FileNotFoundException` (Reactor's error boundary catches it and renders an error `TextBlock`). -- `Application.Current.Resources` loads as an empty dictionary (`0` merged, `0` themed, `0` keys) because `Application.LoadComponent` cascades through MRT for type-info lookups. -- Every `ThemeRef.Resolve(...)` returns `null`; `.Foreground(Theme.X)` falls back to control defaults. - -An app that pins `Microsoft.Windows.SDK.BuildTools.MSIX` below 1.7.260930100 itself brings the gap back. NuGet fails that restore with an `NU1605` package-downgrade error unless the app suppresses it. +Publishing an unpackaged app needs no extra setup. `Microsoft.UI.Reactor` depends on `Microsoft.Windows.SDK.BuildTools.MSIX` 1.7.260930100, which copies the app's `.pri` into the publish output ([WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394)); older versions leave it out, and the published app then crashes at startup or cannot load its own XAML. If you added the `_CopyWinUIResourcesForAot` target that earlier versions of this page recommended, you can delete it. ## Debugging an AOT selftest hang diff --git a/docs/specs/022-packaging-and-distribution.md b/docs/specs/022-packaging-and-distribution.md index 88920d4c5..946ed2e85 100644 --- a/docs/specs/022-packaging-and-distribution.md +++ b/docs/specs/022-packaging-and-distribution.md @@ -141,12 +141,13 @@ The `` line stays — `mur --skill` s ``` Microsoft.UI.Reactor.1.0.0-preview.42.nupkg -├── lib/net10.0-windows10.0.22621/ # ONE lib group — see "Exactly one lib folder" +├── lib/net10.0-windows10.0.22621/ # exactly one lib folder; see below │ ├── Reactor.dll +│ ├── Reactor.pri │ ├── Reactor.xml # XML doc comments │ └── Reactor/Hosting/ │ ├── ReactorApplication.xaml # Application-level XamlControlsResources -│ └── ReactorApplication.xbf # compiled form; see "No .pri in lib/" below +│ └── ReactorApplication.xbf # compiled form, packed by _PackReactorApplicationXbf ├── analyzers/dotnet/cs/ │ ├── Reactor.Analyzers.dll │ └── Reactor.Localization.Generator.dll @@ -157,109 +158,32 @@ Microsoft.UI.Reactor.1.0.0-preview.42.nupkg └── Microsoft.UI.Reactor.nuspec ``` -Note the folder is `net10.0-windows10.0.22621`, **not** `…22621.0`: NuGet shortens the -build-output folder name, while `$(TargetFramework)` keeps the trailing `.0`. - -### No `.pri` in `lib/` (issue #1271) - -None of `Microsoft.UI.Reactor`, `.Advanced` or `.Devtools` ships its generated `.pri`, -and none ever should. NuGet packs a project's resource index into the build-output -folder by default, which puts it beside the assembly — and `ResolveAssemblyReference` -treats a same-base-name `.pri` as a *reference-related file* -(`AllowedReferenceRelatedFileExtensions` defaults to `.pdb;.xml;.pri;…`). Once it is in -`_ReferenceRelatedPaths` / `ReferenceCopyLocalPaths`, the Windows App SDK's -`AddPriPayloadFilesToCopyToOutputDirectoryItems` runs `makepri.exe Dump` on it into -`$(IntermediateOutputPath)`, and that output path breaks past `MAX_PATH` — failing -consumer builds with `PRI175` / `PRI222` / `APPX0002` at depths where an equivalent XAML -app builds fine. - -Relocating the file is *not* a fix: a `.pri` that reaches the consumer's layout by any -route — including a `CopyToOutputDirectory` item from a non-reference folder — also lands -in `@(PackagingOutputs)`, where `_ExpandPriFiles` hands it to the same task. Measured: -shipping `Reactor.pri` from a `sidecar/` folder with a plain -`` item still produces a -`makepri.exe Dump -IndexFile …\sidecar\Reactor.pri` invocation. It has to be absent. - -**The upstream long-path fix, and why this section outlives it.** The `MAX_PATH` defect -itself is [microsoft/WindowsAppSDK#6795](https://github.com/microsoft/WindowsAppSDK/issues/6795) -(`WinAppSdkExpandPriContent` passed a non-extended-length path to `makepri.exe`), fixed in -`Microsoft.Windows.SDK.BuildTools.MSIX` **1.7.260930100** (closed 2026-10-01). The fix is a -`\\?\` prefix in the task rather than a shorter path — the dumps still land in -`$(IntermediateOutputPath)` — so it also covers the expansion path a Reactor app takes. -Measured at matched depths, varying only the build-tools version: - -| consumer | 1.7.251221100 | 1.7.260930100 | -|---|---|---| -| pre-fix packages (ship `.pri`), framework-dependent | `PRI175` on `Microsoft.UI.Reactor.Devtools.pri.xml` | builds | -| this layout (no `.pri`), self-contained | `PRI175` on the SDK's own `Microsoft.WindowsAppRuntime.pri.xml` | builds | - -Build tools reach an app only through `Microsoft.WindowsAppSDK.Base`, and every Base release -to date — 2.0.4 stable and 2.0.5-experimental2 alike — pins `1.7.251221100`. So -`Microsoft.UI.Reactor` declares its own dependency on 1.7.260930100 (it also fixes -WindowsAppSDK#6394; see [AOT support](../aot-support.md#publish-time-resources)), and every -consumer resolves the fixed version. The second row is why that matters even with no `.pri` -in `lib/`: in a self-contained app the Windows App SDK's own indexes hit the same limit. - -The `.pri` removal stands on its own terms. The `.Advanced` and `.Devtools` indexes are -**entirely empty** and the core one duplicates a file that ships loose, yet each cost every -consumer a `makepri.exe Dump` per build — six per build with all three referenced, through -both the reference and the payload path. And an app that pins build tools below Reactor's -floor would hit the original failure again. - -Each of the three csproj files therefore narrows the allow-list that decides what may sit -in the build-output folder: - -```xml - -.dll; .exe; .winmd; .json; .xml; .xbf -``` - -This is deliberately the **pack**-side knob. `.pri` *generation* is untouched, so -`ProjectReference` consumers (in-repo samples, selftests, AOT proofs) are unaffected. - -What consumers need instead is the loose `ReactorApplication.xbf`, which -`ReactorApplication.InitializeComponent()` resolves from the app directory. It is packed -by the `_PackReactorApplicationXbf` target rather than a `` item, because a `` -guarded with `Exists()` is evaluated before the build produces the file and silently -shipped a package without it. - -### Why removing the `.pri` is safe for packaged apps too - -Worth stating because the mechanism differs and the obvious worry is real. For a -**packaged** (MSIX) consumer the old `lib/` `.pri` was not inert: it reached the app -layout, so the app's own `makepri New` merged it into `resources.pri`. Measured on a -packaged consumer of 0.1.0-preview.16, that index contains -`ms-resource:///Files/Reactor/Hosting/ReactorApplication.xbf`; built against this -branch, it does not. So the `.pri` genuinely was carrying the sidecar there. - -It is still safe to drop, because the loose files carry it either way. Measured on the -same packaged app, registered and launched: - -| packaged consumer | app starts | theme resources resolve | -|---|---|---| -| 0.1.0-preview.16 (has `.pri`) | yes | yes | -| this branch (no `.pri`) | **yes** | **yes** | -| this branch, loose `.xaml`+`.xbf` deleted | **crashes** | **no** | - -The third row is the control: it is what makes the second row evidence rather than a -coincidence, and it confirms the loose sidecars — not the `.pri` — are load-bearing. - ### Exactly one `lib/` folder -`_PackReactorApplicationXbf` emits the sidecar as **build output** with a `TargetPath` -(the mechanism satellite assemblies use for `lib///`), never as a -`TfmSpecificPackageFile` with a literal `lib\$(TargetFramework)\…` path. - -That literal form is a trap. `$(TargetFramework)` is `net10.0-windows10.0.22621.0` while -NuGet shortens the build-output folder to `net10.0-windows10.0.22621`, so the package -grew **two sibling `lib` groups for the same framework** — one with the assemblies, one -holding nothing but the stray sidecar. NuGet picked the right one by ordering rather than -by rule. Shipped in 0.1.0-preview.16 and earlier; fixed by letting NuGet place the file. - -`tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs` asserts all of it — no -`.pri`, the sidecars present, exactly one `lib/` folder per package, a real consumer build -logging zero `makepri.exe Dump` calls for Reactor packages, and the app `.pri` reaching -the consumer's publish output. +NuGet names the build-output folder `net10.0-windows10.0.22621`, while `$(TargetFramework)` is +`net10.0-windows10.0.22621.0`. A file packed to a literal `lib\$(TargetFramework)\` path +therefore lands in a second `lib/` folder for the same framework, which is how 0.1.0-preview.16 +shipped `ReactorApplication.xbf`. `_PackReactorApplicationXbf` adds the file as build output +instead, so NuGet places it beside `Reactor.dll`. It runs at pack time because the `` +item it replaced was evaluated before the build created the file, so a clean pack left it out. + +### Build-tools dependency + +`Microsoft.UI.Reactor` depends on `Microsoft.Windows.SDK.BuildTools.MSIX` 1.7.260930100. Apps +otherwise get that package only through `Microsoft.WindowsAppSDK.Base`, and every Base release +so far (2.0.4, 2.0.5-experimental2) pins 1.7.251221100, which has two bugs that affect Reactor apps: + +- Expanding a package's `.pri` with `makepri.exe Dump` fails past `MAX_PATH` (`PRI175`), so apps + failed to build in deeply nested folders (issue #1271, + [WindowsAppSDK#6795](https://github.com/microsoft/WindowsAppSDK/issues/6795)). Measured with + only the build-tools version changed, a consumer of all three Reactor packages at the same + depth fails on 1.7.251221100 and builds on 1.7.260930100. +- An unpackaged app's own `.pri` is left out of its publish output + ([WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394)), so the + published app crashes at startup or cannot load its own XAML. + +Remove the dependency once the Windows App SDK version Reactor depends on resolves +1.7.260930100 or later by itself. ### Packaging configuration diff --git a/src/Reactor.Advanced/Reactor.Advanced.csproj b/src/Reactor.Advanced/Reactor.Advanced.csproj index c4497d8d3..070df8e9b 100644 --- a/src/Reactor.Advanced/Reactor.Advanced.csproj +++ b/src/Reactor.Advanced/Reactor.Advanced.csproj @@ -41,14 +41,6 @@ snupkg true $(NoWarn);CS1591;CS8305;REACTOR_DOC_001 - - .dll; .exe; .winmd; .json; .xml diff --git a/src/Reactor.Devtools/Reactor.Devtools.csproj b/src/Reactor.Devtools/Reactor.Devtools.csproj index f1236a01c..500118bba 100644 --- a/src/Reactor.Devtools/Reactor.Devtools.csproj +++ b/src/Reactor.Devtools/Reactor.Devtools.csproj @@ -34,13 +34,6 @@ snupkg true $(NoWarn);CS1591;CA2255;REACTOR_DOC_001 - - .dll; .exe; .winmd; .json; .xml diff --git a/src/Reactor/Reactor.csproj b/src/Reactor/Reactor.csproj index b0dfc701b..06ac97376 100644 --- a/src/Reactor/Reactor.csproj +++ b/src/Reactor/Reactor.csproj @@ -55,26 +55,6 @@ enforcement can re-enable per-rule. Unresolved crefs are covered by the built-in CS1574 (severity configured in src/Reactor/.editorconfig). --> $(NoWarn);CS1591;REACTOR_DOC_001 - - .dll; .exe; .winmd; .json; .xml; .xbf @@ -123,21 +103,11 @@ - + @@ -220,32 +190,17 @@ - + $(TargetsForTfmSpecificBuildOutput);_PackReactorApplicationXbf + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.xbf + Text="ReactorApplication.xbf was not found at '$(TargetDir)Reactor\Hosting\ReactorApplication.xbf'. Build src/Reactor before packing." /> @@ -306,15 +261,6 @@ CallSite is silently null. The file carries an idempotence guard so being imported from both folders in one evaluation is harmless. --> - + -/// Issue #1271 — the Reactor packages must not ship a .pri, and the compiled -/// ReactorApplication.xbf that replaced it must actually be in the package. +/// The Reactor packages' layout, and the build-tools version Microsoft.UI.Reactor +/// guarantees its consumers (issue #1271, WindowsAppSDK#6394). /// -/// Why a .pri beside the assembly is harmful: ResolveAssemblyReference -/// relates same-base-name sidecars to a reference (AllowedReferenceRelatedFileExtensions -/// defaults to .pdb;.xml;.pri;…), so a lib/<tfm>/Reactor.pri lands in -/// _ReferenceRelatedPaths / ReferenceCopyLocalPaths. The Windows App SDK's -/// AddPriPayloadFilesToCopyToOutputDirectoryItems then runs makepri.exe Dump on -/// every one of them, writing into $(IntermediateOutputPath). Those output paths break -/// past MAX_PATH, so consumers failed to build at a depth where an equivalent XAML app built -/// fine — and Microsoft.UI.Reactor.Devtools.pri.xml, the longest of the three names, is -/// what tipped a borderline project over. -/// -/// Measured when this was fixed: Reactor.Advanced.pri and -/// Microsoft.UI.Reactor.Devtools.pri were entirely empty (a resource map with no -/// NamedResource), and Reactor.pri held exactly one entry — an EmbeddedData -/// copy of ReactorApplication.xbf, which also ships loose. So nothing was lost. -/// -/// Like its siblings in this collection, this needs network access to restore the -/// Windows App SDK, so it only runs where the feeds are reachable (CI's "Integration Tests" job). +/// Needs network access to restore the Windows App SDK, so it only runs where the feeds +/// are reachable (CI's "Integration Tests" job). /// [Collection(LocalPackageFeedCollection.Name)] public sealed class PriPackagingTests : IDisposable { - private readonly LocalPackageFeedFixture _fixture; - private readonly string _tempRoot = Path.Join(Path.GetTempPath(), $"rpri-{Guid.NewGuid():N}"); + private const string BuildToolsMsixId = "Microsoft.Windows.SDK.BuildTools.MSIX"; + + // What Microsoft.WindowsAppSDK.Base resolves on its own; it predates the fixes. + private const string BuildToolsMsixWithoutFixes = "1.7.251221100"; private static readonly string[] PackageIds = [ @@ -39,6 +26,9 @@ public sealed class PriPackagingTests : IDisposable "Microsoft.UI.Reactor.Devtools", ]; + private readonly LocalPackageFeedFixture _fixture; + private readonly string _tempRoot = Path.Join(Path.GetTempPath(), $"rpri-{Guid.NewGuid():N}"); + public PriPackagingTests(LocalPackageFeedFixture fixture) { _fixture = fixture; @@ -46,46 +36,12 @@ public PriPackagingTests(LocalPackageFeedFixture fixture) } /// - /// Structural half: no .pri anywhere in any of the three packages. - /// - /// The mechanism that keeps it out is a single property - /// (DefaultAllowedOutputExtensionsInPackageBuildOutputFolder minus .pri) in - /// each csproj. Nothing fails if that property stops being honoured — NuGet would simply - /// resume packing the file and consumers would silently regress — so this assertion is the - /// only thing standing between a rename upstream and a re-broken package. + /// WinUI loads ReactorApplication.xaml (or its compiled .xbf) at startup. + /// The .xbf is packed by a pack-time target, because the <None> item it + /// replaced was evaluated before the build created the file. /// [Fact] - public void PackagesShipNoPriFile() - { - foreach (var packageId in PackageIds) - { - var entries = ReadPackageEntries(packageId); - - // Positive control: the package was really opened and really has content. Without - // this, an empty or unreadable archive would satisfy the "no .pri" assertion below. - Assert.Contains(entries, e => e.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)); - - var pris = entries.Where(e => e.EndsWith(".pri", StringComparison.OrdinalIgnoreCase)).ToArray(); - Assert.True( - pris.Length == 0, - $"{packageId} ships {pris.Length} .pri file(s): {string.Join(", ", pris)}. " + - "A .pri in lib/ is picked up as a reference-related path and fed to the Windows App SDK's " + - "makepri.exe Dump expansion, whose output path breaks past MAX_PATH (issue #1271)."); - } - } - - /// - /// The other half, and the reason the first half is safe: the loose sidecars are still there. - /// - /// ReactorApplication.xbf is what InitializeComponent() resolves now that - /// the .pri's embedded copy is gone, and it reaches the package through a pack-time - /// target rather than a <None> item — because a <None> guarded by - /// Exists() is evaluated BEFORE the build produces the file, which silently shipped a - /// package without it on a clean tree. Asserting "no .pri" without also asserting this would - /// happily pass for a package that delivers neither copy. - /// - [Fact] - public void CorePackageStillShipsTheApplicationXamlSidecars() + public void CorePackageShipsTheApplicationXamlSidecars() { var entries = ReadPackageEntries("Microsoft.UI.Reactor"); @@ -94,17 +50,9 @@ public void CorePackageStillShipsTheApplicationXamlSidecars() } /// - /// Every package must expose exactly ONE lib/<tfm> folder. - /// - /// Packing a file with a literal lib\$(TargetFramework)\… path is the easy way to - /// get this wrong: $(TargetFramework) is net10.0-windows10.0.22621.0, but NuGet - /// shortens the build-output folder to net10.0-windows10.0.22621. Both parse to the - /// same framework, so the package ends up with two sibling lib groups for one target — - /// one holding the assemblies, one holding only a stray sidecar. It resolved correctly by - /// luck of ordering rather than by rule, which is not a property worth relying on. - /// - /// Asserting the count (rather than a specific folder name) keeps this from breaking - /// the next time the TFM or NuGet's shortening changes. + /// Packing a file to a literal lib\$(TargetFramework)\ path adds a second + /// lib/ folder for the same framework: NuGet names the build-output folder + /// net10.0-windows10.0.22621, while $(TargetFramework) ends in .0. /// [Fact] public void PackagesExposeExactlyOneLibFolder() @@ -117,160 +65,38 @@ public void PackagesExposeExactlyOneLibFolder() .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); - // Positive control: a package with no lib/ at all would otherwise satisfy "not > 1". + // Exactly one, so a package with no lib/ folder fails too. Assert.True(libFolders.Length == 1, $"{packageId} has {libFolders.Length} lib/ folder(s): {string.Join(", ", libFolders)}."); } } /// - /// Behavioural half: a real consumer build no longer invokes makepri.exe Dump for any - /// Reactor package. - /// - /// This is what the structural test cannot show — that the expansion step is actually - /// gone from the consumer's build rather than merely that one file moved. Before the fix this - /// build logged six Reactor dumps (three packages, reached through both the reference path and - /// the payload path); after it, zero. - /// - /// The assertion carries its own positive control: the Windows App SDK's own framework - /// PRIs are still dumped and the app still runs makepri.exe New for its own resources. - /// If the probe were simply failing to match — a changed log format, a quieter verbosity, a - /// build that died early — those counts would be zero too, and the test fails instead of - /// reporting a false success. - /// - [Fact] - public void ConsumerBuildRunsNoMakepriDumpForReactorPackages() - { - var appDir = Path.Join(_tempRoot, "consumer"); - Directory.CreateDirectory(appDir); - - WriteConsumerProject(appDir); - WriteConsumerProgram(appDir); - CreateNuGetConfig(appDir); - - var build = RunHelpers.RunProcess( - "dotnet", - $"build -c Debug -a {_fixture.RunArchitecture} -v:normal", - appDir, - _fixture.CommandEnvironment, - timeoutMs: 600_000, - throwOnFailure: true).Stdout; - - var dumpLines = build - .Split('\n') - .Where(l => l.Contains("makepri.exe Dump", StringComparison.OrdinalIgnoreCase)) - .ToArray(); - - // Positive controls FIRST: prove the probe can match in this log before trusting a zero. - Assert.True( - dumpLines.Length > 0, - "No 'makepri.exe Dump' line at all. The Windows App SDK dumps its own framework PRIs, " + - "so zero means the probe stopped matching (log format or verbosity changed) rather " + - "than that Reactor stopped contributing PRIs."); - Assert.Contains("makepri.exe New", build, StringComparison.OrdinalIgnoreCase); - - var reactorDumps = dumpLines - .Where(l => l.Contains("microsoft.ui.reactor", StringComparison.OrdinalIgnoreCase)) - .ToArray(); - - Assert.True( - reactorDumps.Length == 0, - $"Consumer build ran makepri.exe Dump on {reactorDumps.Length} Reactor package PRI(s) " + - $"(issue #1271):{Environment.NewLine}{string.Join(Environment.NewLine, reactorDumps)}"); - } - - /// - /// WindowsAppSDK#6394 — the CONSUMING app's own .pri must reach its publish output. - /// - /// An unpackaged app builds a .pri that embeds its compiled XAML and, when - /// self-contained, WinUI's own generic.xbf / themeresources.xbf. Build tools - /// before Microsoft.Windows.SDK.BuildTools.MSIX 1.7.260930100 never copy it to - /// publish, and the published self-contained app dies at startup with 0xC000027B. - /// The Windows App SDK still resolves an older version on its own, so Reactor's package - /// declares the fixed one as a dependency floor. Nothing else fails if that floor is lost — - /// the build stays green and only the published app breaks — so this test is what keeps it. - /// - /// The control pins the same consumer back to the version the Windows App SDK resolves - /// alone (suppressing the NU1605 downgrade warning that a direct reference below a - /// dependency's minimum raises), so the build-tools version is the only variable between the - /// two publishes. It must reproduce the missing file. Without that half, a publish that - /// produced the .pri for an unrelated reason would let this pass while the floor did - /// nothing. - /// - /// The two publishes deliberately run from SEPARATE project directories. Publishing the - /// same project twice with a different global property forces a full re-evaluation that reuses - /// the same obj/, and the second XAML compile then failed on CI with - /// WMC1006: Cannot resolve Assembly or Windows Metadata file — the reference path under - /// the fixture's package folder is ~258 characters, and the XAML compiler is not long-path - /// aware. One publish per directory stays clear of both. + /// Build tools before 1.7.260930100 leave an unpackaged app's own .pri out of its + /// publish output, and the published app crashes at startup (WindowsAppSDK#6394). Reactor's + /// dependency on the fixed version must reach its consumers. The control pins the same + /// consumer to the version the Windows App SDK resolves alone, and must lose the .pri. /// [Fact] public void ConsumerPublishIncludesTheAppPri() { - // Short directory names on purpose: these paths feed the XAML compiler, which fails - // around MAX_PATH, and the fixture's package folder already consumes most of the budget. - var floorDir = Path.Join(_tempRoot, "on"); - var downgradedDir = Path.Join(_tempRoot, "off"); - Directory.CreateDirectory(floorDir); - Directory.CreateDirectory(downgradedDir); - - // Core package only. The publish path is the one that runs the XAML compiler against - // the full reference closure, and dragging in Advanced (Win2D) and Devtools adds - // references without adding coverage — the floor under test is in the core package. - WriteConsumerProject(floorDir, coreOnly: true); - WriteConsumerProject(downgradedDir, coreOnly: true, buildToolsMsixVersion: BuildToolsMsixWithoutPublishFix); - foreach (var dir in new[] { floorDir, downgradedDir }) - { - WriteConsumerProgram(dir); - CreateNuGetConfig(dir); - } - - var publishDir = Path.Join(floorDir, "pub"); - RunHelpers.RunDotnet( - $"publish -c Release -a {_fixture.RunArchitecture} -o \"{publishDir}\"", - floorDir, - _fixture.CommandEnvironment, - timeoutMs: 600_000); + // Short names: the XAML compiler is not long-path aware. + var withFloorDir = Path.Join(_tempRoot, "on"); + var pinnedDir = Path.Join(_tempRoot, "off"); + WriteConsumer(withFloorDir); + WriteConsumer(pinnedDir, buildToolsMsixVersion: BuildToolsMsixWithoutFixes); + var withFloor = Publish(withFloorDir); Assert.True( - File.Exists(Path.Join(publishDir, "Consumer.pri")), - "The app's own .pri is missing from the publish output, so the published app would " + - "crash in native XAML at startup (WindowsAppSDK#6394). Reactor's dependency on " + - $"{BuildToolsMsixId} is supposed to bring the fix; the consumer resolved " + - $"{ResolvedBuildToolsMsix(floorDir)}."); - - var downgradedPublishDir = Path.Join(downgradedDir, "pub"); - RunHelpers.RunDotnet( - $"publish -c Release -a {_fixture.RunArchitecture} -o \"{downgradedPublishDir}\"", - downgradedDir, - _fixture.CommandEnvironment, - timeoutMs: 600_000); + File.Exists(Path.Join(withFloor, "Consumer.pri")), + $"The app's own .pri is missing from the publish output (WindowsAppSDK#6394). " + + $"The consumer resolved {ResolvedBuildToolsMsix(withFloorDir)}."); + var pinned = Publish(pinnedDir); Assert.False( - File.Exists(Path.Join(downgradedPublishDir, "Consumer.pri")), - $"Pinned back to {BuildToolsMsixId} {BuildToolsMsixWithoutPublishFix} (resolved " + - $"{ResolvedBuildToolsMsix(downgradedDir)}), the publish still produced the app .pri, so " + - "the passing assertion above does not attribute it to Reactor's dependency floor."); - } - - private const string BuildToolsMsixId = "Microsoft.Windows.SDK.BuildTools.MSIX"; - - // What Microsoft.WindowsAppSDK.Base still resolves on its own, and the last version - // without the WindowsAppSDK#6394 publish fix. - private const string BuildToolsMsixWithoutPublishFix = "1.7.251221100"; - - private static string ResolvedBuildToolsMsix(string appDir) - { - var assetsPath = Path.Join(appDir, "obj", "project.assets.json"); - if (!File.Exists(assetsPath)) - { - return "nothing (no project.assets.json)"; - } - - using var assets = JsonDocument.Parse(File.ReadAllText(assetsPath)); - return assets.RootElement.GetProperty("libraries").EnumerateObject() - .Select(library => library.Name) - .FirstOrDefault(name => name.StartsWith(BuildToolsMsixId + "/", StringComparison.OrdinalIgnoreCase)) - ?? $"no {BuildToolsMsixId} at all"; + File.Exists(Path.Join(pinned, "Consumer.pri")), + $"Pinned to {BuildToolsMsixId} {BuildToolsMsixWithoutFixes} (resolved " + + $"{ResolvedBuildToolsMsix(pinnedDir)}), the publish still produced the app .pri, so the " + + "assertion above does not show that Reactor's dependency is what puts it there."); } private string[] ReadPackageEntries(string packageId) @@ -283,49 +109,23 @@ private string[] ReadPackageEntries(string packageId) } /// - /// References all three packages by default. Before the fix each contributed its own - /// makepri.exe Dump, so covering only the core package would leave the two whose PRIs - /// were empty — including the Devtools one with the longest, build-breaking file name — - /// untested. trims that down for the publish test, which does not - /// need them and is sensitive to how many references the XAML compiler has to resolve. - /// pins the build tools below Reactor's floor for - /// the publish test's control. - /// - /// WindowsAppSDKSelfContained=true is deliberate, not incidental, for two - /// reasons. It is the deployment in which a missing app .pri crashes the published app - /// (0xC000027B), because the .pri also carries WinUI's own XAML; a - /// framework-dependent app loads that from the installed runtime and loses only its own XAML. - /// And it is what makes the Windows App SDK dump its framework PRIs at all, which is the - /// positive control in : - /// measured, a framework-dependent consumer dumps none, so that control would have nothing to - /// match. Do not "simplify" this to the framework-dependent default. + /// A self-contained consumer of the core package: the deployment where a missing app + /// .pri crashes the published app, because it also carries WinUI's own XAML. + /// pins the build tools below Reactor's floor, which + /// NuGet reports as an NU1605 downgrade. /// - private void WriteConsumerProject(string appDir, bool coreOnly = false, string? buildToolsMsixVersion = null) + private void WriteConsumer(string appDir, string? buildToolsMsixVersion = null) { - var extraPackages = coreOnly - ? string.Empty - : $""" - - - - """; + Directory.CreateDirectory(appDir); - // A direct reference below a dependency's minimum is an NU1605 downgrade, which fails the - // restore by default; the control wants exactly that downgrade. - var buildToolsPin = buildToolsMsixVersion is null + var pin = buildToolsMsixVersion is null ? string.Empty : $""" """; - var noWarn = buildToolsMsixVersion is null - ? string.Empty - : """ - - $(NoWarn);NU1605 - """; - var csproj = $""" + File.WriteAllText(Path.Join(appDir, "Consumer.csproj"), $""" Exe @@ -335,28 +135,50 @@ private void WriteConsumerProject(string appDir, bool coreOnly = false, string? true {_fixture.RunArchitecture} {_fixture.RunArchitecture} - win-{_fixture.RunArchitecture}{noWarn} + win-{_fixture.RunArchitecture} + $(NoWarn);NU1605 - {extraPackages}{buildToolsPin} + {pin} - """; - - File.WriteAllText(Path.Join(appDir, "Consumer.csproj"), csproj); - } + """); - private static void WriteConsumerProgram(string appDir) - { - // OutputType=Exe is what makes the Windows App SDK compute input PRIs at all - // (ShouldComputeInputPris), so the app must be an executable. It is never run here: - // these tests assert on the build and publish layout. + // Never run: the test asserts on the publish output. File.WriteAllText(Path.Join(appDir, "Program.cs"), """ using static Microsoft.UI.Reactor.Factories; System.Console.WriteLine(TextBlock("hello") is not null); """); + + CreateNuGetConfig(appDir); + } + + private string Publish(string appDir) + { + var publishDir = Path.Join(appDir, "pub"); + RunHelpers.RunDotnet( + $"publish -c Release -a {_fixture.RunArchitecture} -o \"{publishDir}\"", + appDir, + _fixture.CommandEnvironment, + timeoutMs: 600_000); + return publishDir; + } + + private static string ResolvedBuildToolsMsix(string appDir) + { + var assetsPath = Path.Join(appDir, "obj", "project.assets.json"); + if (!File.Exists(assetsPath)) + { + return "nothing (no project.assets.json)"; + } + + using var assets = JsonDocument.Parse(File.ReadAllText(assetsPath)); + return assets.RootElement.GetProperty("libraries").EnumerateObject() + .Select(library => library.Name) + .FirstOrDefault(name => name.StartsWith(BuildToolsMsixId + "/", StringComparison.OrdinalIgnoreCase)) + ?? $"no {BuildToolsMsixId} at all"; } private void CreateNuGetConfig(string appDir) @@ -393,10 +215,7 @@ public void Dispose() } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { - // Best-effort cleanup for temporary test artifacts. These two cover what - // Directory.Delete realistically throws here (a file still held by a just-exited - // build process, or a read-only artifact); anything else is a real bug and - // should not be swallowed. + // Best effort: a just-exited build can still hold a file. } } -} +} \ No newline at end of file From a36841a2852a1e25fce85668b6416dc4dd8833ed Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Wed, 14 Oct 2026 15:43:49 -0700 Subject: [PATCH 20/20] Address pr-review findings - docs/aot-support.md: the no-setup statement now names the first Reactor version it applies to, with a one-line fallback for 0.1.0-preview.16 and earlier (verified on the published preview.16: without it the published app crashes with 0xC000027B; with it the .pri is published and the app runs). - release.yml: check that each packed .nupkg has one lib/ folder with ReactorApplication.xbf beside Reactor.dll. Only a pack of a built tree, as in that job, shows the second-folder defect, so the clean-tree integration test for it is removed. Verified against this head's artifacts (pass), 0.1.0-preview.16 (fails: two lib/ folders) and a core package without the .xbf (fails). - Keep the bump bots off Microsoft.Windows.SDK.BuildTools.MSIX: it moves by hand with the Windows App SDK. - threat-model.md: six NuGet dependencies, not five. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c093b4a-b747-4495-9317-0ecf8597d087 --- .github/dependabot.yml | 4 +++ .github/workflows/release.yml | 23 +++++++++++++++ .github/workflows/windows-nuget-updates.yml | 4 ++- docs/aot-support.md | 2 +- docs/security/threat-model.md | 2 +- .../Packaging/PriPackagingTests.cs | 28 ------------------- 6 files changed, 32 insertions(+), 31 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8b66545ed..053ce5566 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -74,6 +74,10 @@ updates: - dependency-name: "Microsoft.WindowsAppSDK.WinUI" - dependency-name: "Microsoft.WindowsAppSDK.Runtime" - dependency-name: "Microsoft.Graphics.Win2D" + # A floor Microsoft.UI.Reactor ships to its consumers; it moves by hand with the + # Windows App SDK (src/Reactor/Reactor.csproj). Also held back via IGNORE_PACKAGES + # in .github/workflows/windows-nuget-updates.yml. + - dependency-name: "Microsoft.Windows.SDK.BuildTools.MSIX" # MessagePack is a transitive dependency (Microsoft.VisualStudio.SDK -> # StreamJsonRpc) pinned in Directory.Packages.props to the 2.x line for # StreamJsonRpc compatibility, at >= 2.5.302 for the GHSA-hv8m-jj95-wg3x diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a1293e8d2..3e4cd0c58 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -263,6 +263,29 @@ jobs: Write-Host "All $($checked.Count) packed assemblies are AnyCPU:" $checked | ForEach-Object { Write-Host " $_" } + # One lib/ folder per package, with ReactorApplication.xbf beside Reactor.dll. A file packed + # to a literal lib\$(TargetFramework)\ path lands in a second folder (…22621.0 vs NuGet's + # …22621). Checked here because only a pack of an already-built tree, like this one, shows it. + - name: Check package lib/ layout + shell: pwsh + run: | + Add-Type -AssemblyName System.IO.Compression.FileSystem + $packages = @(Get-ChildItem artifacts/nupkg -Filter *.nupkg -File) + if ($packages.Count -eq 0) { throw 'No .nupkg under artifacts/nupkg.' } + $problems = [System.Collections.Generic.List[string]]::new() + foreach ($nupkg in $packages) { + $zip = [System.IO.Compression.ZipFile]::OpenRead($nupkg.FullName) + try { $entries = @($zip.Entries | ForEach-Object FullName) } finally { $zip.Dispose() } + $libs = @($entries | Where-Object { $_ -like 'lib/*/*' } | ForEach-Object { $_.Split('/')[1] } | Sort-Object -Unique) + if ($libs.Count -ne 1) { + $problems.Add("$($nupkg.Name) has $($libs.Count) lib/ folders: $($libs -join ', ')") + } elseif ($entries -contains "lib/$($libs[0])/Reactor.dll" -and $entries -notcontains "lib/$($libs[0])/Reactor/Hosting/ReactorApplication.xbf") { + $problems.Add("$($nupkg.Name) has no lib/$($libs[0])/Reactor/Hosting/ReactorApplication.xbf beside Reactor.dll") + } + } + if ($problems.Count -gt 0) { throw "Package layout problems:`n$($problems -join "`n")" } + Write-Host "One lib/ folder in each of $($packages.Count) packages." + - name: Build VSIX shell: pwsh run: | diff --git a/.github/workflows/windows-nuget-updates.yml b/.github/workflows/windows-nuget-updates.yml index 65348a31b..0bfe80fbd 100644 --- a/.github/workflows/windows-nuget-updates.yml +++ b/.github/workflows/windows-nuget-updates.yml @@ -78,7 +78,9 @@ env: # Package ids to hold back (exact, case-insensitive; comma separated). Seeded # with MessagePack because its 3.x major was intentionally declined in #580 — # remove it here to let this workflow propose that major again. - IGNORE_PACKAGES: 'MessagePack' + # Microsoft.Windows.SDK.BuildTools.MSIX is a floor Microsoft.UI.Reactor ships to + # its consumers; it moves by hand with the Windows App SDK (src/Reactor/Reactor.csproj). + IGNORE_PACKAGES: 'MessagePack,Microsoft.Windows.SDK.BuildTools.MSIX' jobs: # ───────────────────────────────────────────────────────────────────────────── diff --git a/docs/aot-support.md b/docs/aot-support.md index bf15d469a..dd22c8392 100644 --- a/docs/aot-support.md +++ b/docs/aot-support.md @@ -95,7 +95,7 @@ Devtools are a two-layer opt-in so release and NativeAOT binaries stay trimmed b ## Publish output -Publishing an unpackaged app needs no extra setup. `Microsoft.UI.Reactor` depends on `Microsoft.Windows.SDK.BuildTools.MSIX` 1.7.260930100, which copies the app's `.pri` into the publish output ([WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394)); older versions leave it out, and the published app then crashes at startup or cannot load its own XAML. If you added the `_CopyWinUIResourcesForAot` target that earlier versions of this page recommended, you can delete it. +Publishing an unpackaged app needs no extra setup. `Microsoft.UI.Reactor` versions after 0.1.0-preview.16 depend on `Microsoft.Windows.SDK.BuildTools.MSIX` 1.7.260930100, which copies the app's `.pri` into the publish output ([WindowsAppSDK#6394](https://github.com/microsoft/WindowsAppSDK/issues/6394)); older versions leave it out, and the published app then crashes at startup or cannot load its own XAML. On 0.1.0-preview.16 or earlier, add `` to your app, and remove it when you upgrade Reactor. If you added the `_CopyWinUIResourcesForAot` target that earlier versions of this page recommended, you can delete it. ## Debugging an AOT selftest hang diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index f393424d3..214592e10 100644 --- a/docs/security/threat-model.md +++ b/docs/security/threat-model.md @@ -261,7 +261,7 @@ The Microsoft-internal security-review intake form gave succinct answers about R | "CLI/debug tooling runs as developer-invoked user-mode process" | Manifest review of `mur.exe`, install script behavior | No elevation, no scheduled-task install. | | "CLI can talk to app as a debugging tool using MCP protocol over HTTP, domain-validated for localhost, with token auth" | `PreviewCaptureServer.cs`, `DevtoolsMcpServer.cs` | Listener prefix bound to `http://127.0.0.1:/`; Host header allow-list at `IsAllowedHost`; bearer constant-time compare at `BearerMatches`. | | "No outbound network communication beyond the above" | Repo-wide grep | Only `System.Net.*` usage in `src/Reactor/` is the two `HttpListener` servers above and an `HttpClient` *in CLI Docs subsystem* that targets `127.0.0.1:` of a preview server `mur` itself spawned. | -| "No unsupported or third-party components" | `ThirdPartyNoticeText.txt`, `Directory.Build.props`, `nuget.config` | Five Microsoft.* / MIT NuGet deps; three source-ported MIT/ISC OSS components (Yoga, md4c, D3); no checked-in `.nupkg` binaries; `nuget.config` clears inherited feeds. | +| "No unsupported or third-party components" | `ThirdPartyNoticeText.txt`, `Directory.Build.props`, `nuget.config` | Six Microsoft.* / MIT NuGet deps; three source-ported MIT/ISC OSS components (Yoga, md4c, D3); no checked-in `.nupkg` binaries; `nuget.config` clears inherited feeds. | | "No prior threat models / security reviews" | Repo audit | This document is the first formal one. README references a 275-finding internal code review — engineering review, not a threat model. | ## 9. Out-of-scope non-surfaces (with rationale) diff --git a/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs b/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs index c92a41c3e..11e1f6cca 100644 --- a/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs +++ b/tests/Reactor.IntegrationTests/Packaging/PriPackagingTests.cs @@ -19,13 +19,6 @@ public sealed class PriPackagingTests : IDisposable // What Microsoft.WindowsAppSDK.Base resolves on its own; it predates the fixes. private const string BuildToolsMsixWithoutFixes = "1.7.251221100"; - private static readonly string[] PackageIds = - [ - "Microsoft.UI.Reactor", - "Microsoft.UI.Reactor.Advanced", - "Microsoft.UI.Reactor.Devtools", - ]; - private readonly LocalPackageFeedFixture _fixture; private readonly string _tempRoot = Path.Join(Path.GetTempPath(), $"rpri-{Guid.NewGuid():N}"); @@ -49,27 +42,6 @@ public void CorePackageShipsTheApplicationXamlSidecars() Assert.Contains(entries, e => e.EndsWith("Reactor/Hosting/ReactorApplication.xaml", StringComparison.OrdinalIgnoreCase)); } - /// - /// Packing a file to a literal lib\$(TargetFramework)\ path adds a second - /// lib/ folder for the same framework: NuGet names the build-output folder - /// net10.0-windows10.0.22621, while $(TargetFramework) ends in .0. - /// - [Fact] - public void PackagesExposeExactlyOneLibFolder() - { - foreach (var packageId in PackageIds) - { - var libFolders = ReadPackageEntries(packageId) - .Where(e => e.StartsWith("lib/", StringComparison.OrdinalIgnoreCase)) - .Select(e => e.Split('/')[1]) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - - // Exactly one, so a package with no lib/ folder fails too. - Assert.True(libFolders.Length == 1, $"{packageId} has {libFolders.Length} lib/ folder(s): {string.Join(", ", libFolders)}."); - } - } - /// /// Build tools before 1.7.260930100 leave an unpackaged app's own .pri out of its /// publish output, and the published app crashes at startup (WindowsAppSDK#6394). Reactor's