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/CHANGELOG.md b/CHANGELOG.md
index 99ad437fa..2bba8b2e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -648,6 +648,17 @@ Conventions for contributors:
`Cancel()` cleanup (PR #1124), that the source had been pooled and reset, was wrong: that crash
was this same fault.
+- **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
## [0.1.0-preview.16] — 2026-09-22
diff --git a/Directory.Packages.props b/Directory.Packages.props
index a27132967..e3d67cf03 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -28,6 +28,8 @@
+
+
diff --git a/docs/aot-support.md b/docs/aot-support.md
index e651c5efa..dd22c8392 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.
+- **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 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 |
-| **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 |
+| **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,38 +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.
-## Required publish-time workarounds
+## Publish output
-### 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.
-
-The missing `.pri` has cascading runtime symptoms that look like separate bugs but all 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) — still OPEN against 1.8 and 2.0.
-
-**Workaround** (in `tests/Reactor.AppTests.Host/Reactor.AppTests.Host.csproj`):
-
-```xml
-
-
- <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" />
- <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri"
- Condition="Exists('$(OutputPath)$(AssemblyName).pri')" />
-
-
-
-```
-
-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.
+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/docs/specs/022-packaging-and-distribution.md b/docs/specs/022-packaging-and-distribution.md
index 625385f9a..946ed2e85 100644
--- a/docs/specs/022-packaging-and-distribution.md
+++ b/docs/specs/022-packaging-and-distribution.md
@@ -141,9 +141,13 @@ 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/ # exactly one lib folder; see below
│ ├── Reactor.dll
-│ └── Reactor.xml # XML doc comments
+│ ├── Reactor.pri
+│ ├── Reactor.xml # XML doc comments
+│ └── Reactor/Hosting/
+│ ├── ReactorApplication.xaml # Application-level XamlControlsResources
+│ └── ReactorApplication.xbf # compiled form, packed by _PackReactorApplicationXbf
├── analyzers/dotnet/cs/
│ ├── Reactor.Analyzers.dll
│ └── Reactor.Localization.Generator.dll
@@ -154,6 +158,33 @@ Microsoft.UI.Reactor.1.0.0-preview.42.nupkg
└── Microsoft.UI.Reactor.nuspec
```
+### Exactly one `lib/` folder
+
+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
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')" />
-
-
-
-
+
+
+
+
+
+
+ $(TargetsForTfmSpecificBuildOutput);_PackReactorApplicationXbf
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.xbf
+
+
+
+
+
+
+
-
-
-
-
- <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" />
- <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri" Condition="Exists('$(OutputPath)$(AssemblyName).pri')" />
-
-
-
-
+/// The Reactor packages' layout, and the build-tools version Microsoft.UI.Reactor
+/// guarantees its consumers (issue #1271, WindowsAppSDK#6394).
+///
+/// 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 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 readonly LocalPackageFeedFixture _fixture;
+ private readonly string _tempRoot = Path.Join(Path.GetTempPath(), $"rpri-{Guid.NewGuid():N}");
+
+ public PriPackagingTests(LocalPackageFeedFixture fixture)
+ {
+ _fixture = fixture;
+ Directory.CreateDirectory(_tempRoot);
+ }
+
+ ///
+ /// 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 CorePackageShipsTheApplicationXamlSidecars()
+ {
+ 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));
+ }
+
+ ///
+ /// 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 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(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(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)
+ {
+ 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();
+ }
+
+ ///
+ /// 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 WriteConsumer(string appDir, string? buildToolsMsixVersion = null)
+ {
+ Directory.CreateDirectory(appDir);
+
+ var pin = buildToolsMsixVersion is null
+ ? string.Empty
+ : $"""
+
+
+ """;
+
+ File.WriteAllText(Path.Join(appDir, "Consumer.csproj"), $"""
+
+
+ Exe
+ net10.0-windows10.0.22621.0
+ enable
+ true
+ true
+ {_fixture.RunArchitecture}
+ {_fixture.RunArchitecture}
+ win-{_fixture.RunArchitecture}
+ $(NoWarn);NU1605
+
+
+ {pin}
+
+
+ """);
+
+ // 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)
+ {
+ 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 (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ // Best effort: a just-exited build can still hold a file.
+ }
+ }
+}
\ No newline at end of file
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')" />
-
-
-
-