diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e4e64169..dc92ff7d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,14 @@ Conventions for contributors: ### Added +- **Getting Started documents the single-file path.** A Reactor app does not need + a `.csproj`: .NET 10 runs a lone `.cs` file whose `#:package` / `#:property` + header supplies what a project file otherwise would. The header also references + `Microsoft.Windows.SDK.BuildTools.WinApp`, so `dotnet run counter.cs` launches + the app packaged, just like `winapp run counter.cs`; `WindowsPackageType=None` + runs it unpackaged (winappCli#794 / #874, shipped in winapp 0.7.0). The + agent-kit skills use the same header. + - **Validation now works on any control, not only inside `FormField`** — the Forms guide's "Validation Context" example needed new surface to work as written (issue #1262): @@ -149,6 +157,16 @@ Conventions for contributors: ### Fixed +- **Apps that reference only `Microsoft.UI.Reactor` are framework-dependent + again** (regression from #822). The package now depends on + `Microsoft.WindowsAppSDK.Runtime`; without it the Windows App SDK silently + built such apps self-contained, so a plain `dotnet run` failed with + *"WindowsAppSDKSelfContained requires a supported Windows architecture"*. To + keep bundling the Windows App Runtime, set `WindowsAppSDKSelfContained=true`. + To move to a newer WinUI, bump the full `Microsoft.WindowsAppSDK` package; + bumping `Microsoft.WindowsAppSDK.WinUI` on its own now fails the Windows App + SDK's version check. + - **The Forms guide's "Validation Context" example now works as written** (issue #1262). Clicking **Register** on an empty form submitted successfully with no errors, because five defects compounded: `.Validate()` was inert diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d0e8e50e0..47935e61f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope `Directory.Build.props`) - Visual Studio 2022 (17.8+) or VS Code with C# Dev Kit -> **Windows App SDK packages:** The right package is injected centrally by `Directory.Build.targets` based on each project's shape: framework-dependent **libraries** (including the shipped `Microsoft.UI.Reactor`) reference the lean `Microsoft.WindowsAppSDK.WinUI` sub-package; framework-dependent **apps** additionally reference `Microsoft.WindowsAppSDK.Runtime`; **self-contained / MSIX** projects keep the full `Microsoft.WindowsAppSDK` metapackage (it bundles the runtime + DWriteCore redist). The metapackage's AI/ML/Widgets/DWrite slices are otherwise unused. Versions are pinned in `Directory.Build.props` (`WindowsAppSDKWinUIVersion` and `WindowsAppSDKVersion`) — update them there to change the version for every project at once. +> **Windows App SDK packages:** The right package is injected centrally by `Directory.Build.targets` based on each project's shape: framework-dependent projects — **libraries and apps alike**, including the shipped `Microsoft.UI.Reactor` — reference the lean `Microsoft.WindowsAppSDK.WinUI` sub-package plus `Microsoft.WindowsAppSDK.Runtime` (a library's dependencies become its consumers', and without Runtime the SDK silently builds a consumer app self-contained); **self-contained / MSIX** projects keep the full `Microsoft.WindowsAppSDK` metapackage (it bundles the runtime + DWriteCore redist). The metapackage's AI/ML/Widgets/DWrite slices are otherwise unused. Versions are pinned in `Directory.Build.props` (`WindowsAppSDKWinUIVersion` and `WindowsAppSDKVersion`) — update them there to change the version for every project at once. > **Clone to a short path (Windows):** Clone into a short root directory such as > `C:\src\` (for example `C:\src\microsoft-ui-reactor`) rather than a deeply diff --git a/Directory.Build.targets b/Directory.Build.targets index 5ec9801ce..61eb474e0 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -52,17 +52,24 @@ which transitively brings Base + Foundation + InteractiveExperiences + WebView2 (the bootstrapper, deployment manager, MRT Core and app-lifecycle MSBuild targets). The metapackage also aggregates AI / ML / Widgets / DWrite, which the - repo never uses — so framework-dependent projects reference only - Microsoft.WindowsAppSDK.WinUI and let the machine-wide installed - Microsoft.WindowsAppRuntime.2 supply native bits (DWriteCore, etc.) at runtime. + repo never uses — so framework-dependent projects reference + Microsoft.WindowsAppSDK.WinUI + Microsoft.WindowsAppSDK.Runtime rather than the + metapackage, and let the machine-wide installed Microsoft.WindowsAppRuntime.2 + supply native bits (DWriteCore, etc.) at runtime. + + Runtime is what makes an app framework-dependent: it sets + MicrosoftWindowsAppSDKPackageDir, and without it the SDK's + Microsoft.WindowsAppSDK.Base.targets silently defaults WindowsAppSDKSelfContained + to true (it only errors when a project sets that property to something other + than true explicitly). Libraries take it too, even though they don't produce an + app, because a library's dependencies become its consumers' dependencies: when + the published Microsoft.UI.Reactor carried WinUI alone, every app that + referenced only Reactor was silently built self-contained, which also demands a + RuntimeIdentifier. Projects that BUNDLE the native runtime keep the full metapackage, which carries the Runtime redist + DWriteCore: WindowsAppSDKSelfContained=true, or - framework-dependent MSIX (WindowsPackageType=MSIX). A framework-dependent WinUI - *executable* additionally needs Microsoft.WindowsAppSDK.Runtime — the SDK's - Microsoft.WindowsAppSDK.Base.targets rejects a framework-dependent app that - references neither Runtime nor WindowsAppSDKSelfContained. Libraries don't produce - an app, so they take the lean WinUI reference alone. + framework-dependent MSIX (WindowsPackageType=MSIX). Evaluating the conditions here in .targets (a late import) means per-project OutputType / WindowsAppSDKSelfContained / WindowsPackageType values are already @@ -80,14 +87,12 @@ <_ReactorWinAppSDKMetapackage>false <_ReactorWinAppSDKMetapackage Condition="'$(WindowsAppSDKSelfContained)' == 'true' or '$(WindowsPackageType)' == 'MSIX'">true - <_ReactorWinAppSDKFrameworkDependentApp>false - <_ReactorWinAppSDKFrameworkDependentApp Condition="'$(_ReactorWinAppSDKMetapackage)' != 'true' and ('$(OutputType)' == 'Exe' or '$(OutputType)' == 'WinExe')">true - + diff --git a/SKILL.md b/SKILL.md index 86ae6220d..217fb33e4 100644 --- a/SKILL.md +++ b/SKILL.md @@ -541,11 +541,10 @@ For lightweight demos, skip the `.csproj` entirely. Add a file-level header: ```csharp #:package Microsoft.UI.Reactor@0.0.0-local -#:package Microsoft.WindowsAppSDK@2.2.0 +#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0 #:property OutputType=WinExe #:property TargetFramework=net10.0-windows10.0.22621.0 #:property UseWinUI=true -#:property WindowsPackageType=None using Microsoft.UI.Reactor; using static Microsoft.UI.Reactor.Factories; @@ -557,9 +556,19 @@ ReactorApp.Run("Hello", ctx => }); ``` -Run with `dotnet run MyApp.cs -p:Platform=ARM64` (or `x64`). In selfhost -the version is `0.0.0-local` — run `mur pack-local` first if the package -isn't found. Outside the clone, replace the version with the published +Run with `dotnet run MyApp.cs` — no architecture flag. This is the header the +[Getting Started guide](https://microsoft.github.io/microsoft-ui-reactor/getting-started/#one-file-no-project) +documents: `Microsoft.Windows.SDK.BuildTools.WinApp` makes `dotnet run` launch +the app packaged, with package identity, which needs Developer Mode. If +Developer Mode is off, or you don't need identity, add +`#:property WindowsPackageType=None` to run it as a plain unpackaged `.exe`. +With neither, the app builds, then dies at startup with `REGDB_E_CLASSNOTREG`. +Either way the app uses the Windows App Runtime installed on the machine. To +bundle it instead, add `#:property WindowsAppSDKSelfContained=true`; that mode +also needs an architecture (`-r win-x64` or `-r win-arm64`), and `-r` alone +bundles nothing. +In selfhost the version is `0.0.0-local` — run `mur pack-local` first if the +package isn't found. Outside the clone, replace the version with the published release you depend on. > **Always capture `dotnet run` output.** Build errors exit with code 1. diff --git a/docs/_pipeline/templates/getting-started.md.dt b/docs/_pipeline/templates/getting-started.md.dt index 41b354b8d..253cfa5a8 100644 --- a/docs/_pipeline/templates/getting-started.md.dt +++ b/docs/_pipeline/templates/getting-started.md.dt @@ -82,6 +82,86 @@ identity — the same thing F5 does in Visual Studio. That requires **Developer Mode** (Settings → System → For developers). See [Packaging](packaging.md) for the unpackaged alternative. +## One file, no project + +A Reactor app does not need a `.csproj`. .NET 10 runs a single `.cs` file +directly, and file-level `#:` directives supply the configuration a project file +otherwise would: + +```csharp +#:package Microsoft.UI.Reactor@{{reactorVersion}} +#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0 +#:property OutputType=WinExe +#:property TargetFramework=net10.0-windows10.0.22621.0 +#:property UseWinUI=true + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Hello Reactor"); + +class MyApp : Component +{ + public override Element Render() + { + var (count, setCount) = UseState(0); + + return VStack( + Heading($"Count: {count}"), + HStack(8, + Button("-", () => setCount(count - 1)), + Button("+", () => setCount(count + 1)) + ) + ); + } +} +``` + +Save it as `counter.cs` and run it: + +```powershell +dotnet run counter.cs +``` + +That is the whole app — one file, no scaffolding, no build output to manage. +It is the fastest way to try an idea, reproduce a bug, or paste a complete +runnable example into an issue. Like a scaffolded app, it runs packaged: +`Microsoft.Windows.SDK.BuildTools.WinApp` makes `dotnet run` synthesize an +appxmanifest from the `#:property` values, register the app, and launch it with +package identity, which needs **Developer Mode**. If you have the +[winapp CLI](https://github.com/microsoft/winappCli) 0.7.0 or later, +`winapp run counter.cs` does the same. + +`Microsoft.UI.Reactor` brings the Windows App SDK in transitively, exactly as in +the [minimal `.csproj`](index.md#minimal-setup). The rest of the header: + +| Directive | Why it is required | +|---|---| +| `Microsoft.Windows.SDK.BuildTools.WinApp` | Lets `dotnet run` package the app. It carries its own copy of the winapp CLI, so that does not need installing. Without it, plain `dotnet run` cannot package the app, and it dies at startup with `COMException … REGDB_E_CLASSNOTREG` — an error that says nothing about the missing package — unless you run it unpackaged (below). | +| `OutputType=WinExe` | Links the app for the Windows GUI subsystem. Without it the build still succeeds, but the `.exe` is a console binary, so Windows opens a console window alongside your UI for as long as the app runs. | +| `TargetFramework=net10.0-windows10.0.22621.0` | Reactor ships `net10.0-windows10.0.22621`. A lower Windows version compiles against nothing and you get `CS0234: the namespace 'Reactor' does not exist`, as if the package were missing. | +| `UseWinUI=true` | Brings in the WinUI 3 targets. | + +### Running it unpackaged + +`WindowsPackageType` decides whether the app runs packaged, and `dotnet run` and +`winapp run` both obey it. Declare the app unpackaged to run it as a plain `.exe`: + +```csharp +#:property WindowsPackageType=None +``` + +An unpackaged app has no package identity, so `Package.Current` and `ms-appx:` +URIs are unavailable. + +Either way the app is framework-dependent: it uses the Windows App Runtime +installed on the machine. To carry it with the app instead, add +`#:property WindowsAppSDKSelfContained=true`. That mode also needs the +architecture named with `-r win-x64` or `-r win-arm64`, because the runtime it +copies is native code built separately for each CPU — `-r` on its own does not +bundle anything. + ## Contributor setup (one-time) Working *on* Reactor rather than with it? Clone and bootstrap: diff --git a/docs/_pipeline/templates/index.md.dt b/docs/_pipeline/templates/index.md.dt index 771d6a682..2a13b7538 100644 --- a/docs/_pipeline/templates/index.md.dt +++ b/docs/_pipeline/templates/index.md.dt @@ -214,7 +214,9 @@ project and edit the `.csproj`: One package reference is enough — `Microsoft.UI.Reactor` brings the Windows App SDK in transitively, so you only add `Microsoft.WindowsAppSDK` yourself when you want to pin a specific WinUI patch or bundle the runtime with -`WindowsAppSDKSelfContained`. See [Packaging](packaging.md) for the +`WindowsAppSDKSelfContained`. Pin WinUI through that full package, not +`Microsoft.WindowsAppSDK.WinUI` on its own, which fails the Windows App SDK's +version check. See [Packaging](packaging.md) for the ``, ARM64, and self-contained shapes the project template uses. Replace `App.cs` with a component and a `ReactorApp.Run()` call, and run with diff --git a/docs/_pipeline/templates/packaging.md.dt b/docs/_pipeline/templates/packaging.md.dt index 5fac5b182..ff40bd60b 100644 --- a/docs/_pipeline/templates/packaging.md.dt +++ b/docs/_pipeline/templates/packaging.md.dt @@ -71,9 +71,10 @@ machines, with ARM64 second for Snapdragon X). The sub-package brings the WinUI 3 SDK — reference assemblies plus the MSBuild build/props/targets — while the native WinUI runtime is supplied by the machine-wide Windows App Runtime install (or bundled into the publish output -when `WindowsAppSDKSelfContained=true`). That sub-package is what a consumer -gets transitively from `Microsoft.UI.Reactor`; the scaffolded template adds the -full +when `WindowsAppSDKSelfContained=true`). That sub-package, together with +`Microsoft.WindowsAppSDK.Runtime` (which keeps the app framework-dependent), is +what a consumer gets transitively from `Microsoft.UI.Reactor`; the scaffolded +template adds the full [`Microsoft.WindowsAppSDK`](https://www.nuget.org/packages/Microsoft.WindowsAppSDK) metapackage on top at scaffold time (see below), because the self-contained shape needs the Runtime redist the metapackage carries. Inside this repo the @@ -295,9 +296,15 @@ frozen when the template shipped. The two references then sit side-by-side in your CSPROJ, so the SDK version stays an obvious knob — edit it there when you need a specific WinUI patch. If you skip the template and reference `Microsoft.UI.Reactor` by hand, the SDK still -arrives transitively as `Microsoft.WindowsAppSDK.WinUI`; add the full +arrives transitively as `Microsoft.WindowsAppSDK.WinUI` plus +`Microsoft.WindowsAppSDK.Runtime`, so the app builds framework-dependent; add the full metapackage yourself when you want `WindowsAppSDKSelfContained=true` or -an MSIX, both of which need the Runtime redist. The repo-internal +an MSIX, both of which need the Runtime redist. The metapackage is also +how you move to a newer WinUI, because it moves Runtime with it: a newer +`Microsoft.WindowsAppSDK.WinUI` on its own leaves Runtime behind, and the +build fails with *"One or more referenced Windows App SDK components are +newer than the versions expected by the Microsoft.WindowsAppSDK.Runtime +package"*. The repo-internal `WindowsAppSDKVersion` MSBuild property only governs projects under this clone (`Directory.Build.props`); consumer projects pick their version directly. diff --git a/docs/guide/README.md b/docs/guide/README.md index e0bd7f939..17769014d 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -240,7 +240,9 @@ project and edit the `.csproj`: One package reference is enough — `Microsoft.UI.Reactor` brings the Windows App SDK in transitively, so you only add `Microsoft.WindowsAppSDK` yourself when you want to pin a specific WinUI patch or bundle the runtime with -`WindowsAppSDKSelfContained`. See [Packaging](packaging.md) for the +`WindowsAppSDKSelfContained`. Pin WinUI through that full package, not +`Microsoft.WindowsAppSDK.WinUI` on its own, which fails the Windows App SDK's +version check. See [Packaging](packaging.md) for the ``, ARM64, and self-contained shapes the project template uses. Replace `App.cs` with a component and a `ReactorApp.Run()` call, and run with diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 4cc6ee722..6858a45f1 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -70,6 +70,86 @@ identity — the same thing F5 does in Visual Studio. That requires **Developer Mode** (Settings → System → For developers). See [Packaging](packaging.md) for the unpackaged alternative. +## One file, no project + +A Reactor app does not need a `.csproj`. .NET 10 runs a single `.cs` file +directly, and file-level `#:` directives supply the configuration a project file +otherwise would: + +```csharp +#:package Microsoft.UI.Reactor@0.1.0-preview.16 +#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0 +#:property OutputType=WinExe +#:property TargetFramework=net10.0-windows10.0.22621.0 +#:property UseWinUI=true + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using static Microsoft.UI.Reactor.Factories; + +ReactorApp.Run("Hello Reactor"); + +class MyApp : Component +{ + public override Element Render() + { + var (count, setCount) = UseState(0); + + return VStack( + Heading($"Count: {count}"), + HStack(8, + Button("-", () => setCount(count - 1)), + Button("+", () => setCount(count + 1)) + ) + ); + } +} +``` + +Save it as `counter.cs` and run it: + +```powershell +dotnet run counter.cs +``` + +That is the whole app — one file, no scaffolding, no build output to manage. +It is the fastest way to try an idea, reproduce a bug, or paste a complete +runnable example into an issue. Like a scaffolded app, it runs packaged: +`Microsoft.Windows.SDK.BuildTools.WinApp` makes `dotnet run` synthesize an +appxmanifest from the `#:property` values, register the app, and launch it with +package identity, which needs **Developer Mode**. If you have the +[winapp CLI](https://github.com/microsoft/winappCli) 0.7.0 or later, +`winapp run counter.cs` does the same. + +`Microsoft.UI.Reactor` brings the Windows App SDK in transitively, exactly as in +the [minimal `.csproj`](index.md#minimal-setup). The rest of the header: + +| Directive | Why it is required | +|---|---| +| `Microsoft.Windows.SDK.BuildTools.WinApp` | Lets `dotnet run` package the app. It carries its own copy of the winapp CLI, so that does not need installing. Without it, plain `dotnet run` cannot package the app, and it dies at startup with `COMException … REGDB_E_CLASSNOTREG` — an error that says nothing about the missing package — unless you run it unpackaged (below). | +| `OutputType=WinExe` | Links the app for the Windows GUI subsystem. Without it the build still succeeds, but the `.exe` is a console binary, so Windows opens a console window alongside your UI for as long as the app runs. | +| `TargetFramework=net10.0-windows10.0.22621.0` | Reactor ships `net10.0-windows10.0.22621`. A lower Windows version compiles against nothing and you get `CS0234: the namespace 'Reactor' does not exist`, as if the package were missing. | +| `UseWinUI=true` | Brings in the WinUI 3 targets. | + +### Running it unpackaged + +`WindowsPackageType` decides whether the app runs packaged, and `dotnet run` and +`winapp run` both obey it. Declare the app unpackaged to run it as a plain `.exe`: + +```csharp +#:property WindowsPackageType=None +``` + +An unpackaged app has no package identity, so `Package.Current` and `ms-appx:` +URIs are unavailable. + +Either way the app is framework-dependent: it uses the Windows App Runtime +installed on the machine. To carry it with the app instead, add +`#:property WindowsAppSDKSelfContained=true`. That mode also needs the +architecture named with `-r win-x64` or `-r win-arm64`, because the runtime it +copies is native code built separately for each CPU — `-r` on its own does not +bundle anything. + ## Contributor setup (one-time) Working *on* Reactor rather than with it? Clone and bootstrap: diff --git a/docs/guide/index.md b/docs/guide/index.md index e0bd7f939..17769014d 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -240,7 +240,9 @@ project and edit the `.csproj`: One package reference is enough — `Microsoft.UI.Reactor` brings the Windows App SDK in transitively, so you only add `Microsoft.WindowsAppSDK` yourself when you want to pin a specific WinUI patch or bundle the runtime with -`WindowsAppSDKSelfContained`. See [Packaging](packaging.md) for the +`WindowsAppSDKSelfContained`. Pin WinUI through that full package, not +`Microsoft.WindowsAppSDK.WinUI` on its own, which fails the Windows App SDK's +version check. See [Packaging](packaging.md) for the ``, ARM64, and self-contained shapes the project template uses. Replace `App.cs` with a component and a `ReactorApp.Run()` call, and run with diff --git a/docs/guide/packaging.md b/docs/guide/packaging.md index d3d89de3d..8543af81d 100644 --- a/docs/guide/packaging.md +++ b/docs/guide/packaging.md @@ -65,9 +65,10 @@ machines, with ARM64 second for Snapdragon X). The sub-package brings the WinUI 3 SDK — reference assemblies plus the MSBuild build/props/targets — while the native WinUI runtime is supplied by the machine-wide Windows App Runtime install (or bundled into the publish output -when `WindowsAppSDKSelfContained=true`). That sub-package is what a consumer -gets transitively from `Microsoft.UI.Reactor`; the scaffolded template adds the -full +when `WindowsAppSDKSelfContained=true`). That sub-package, together with +`Microsoft.WindowsAppSDK.Runtime` (which keeps the app framework-dependent), is +what a consumer gets transitively from `Microsoft.UI.Reactor`; the scaffolded +template adds the full [`Microsoft.WindowsAppSDK`](https://www.nuget.org/packages/Microsoft.WindowsAppSDK) metapackage on top at scaffold time (see below), because the self-contained shape needs the Runtime redist the metapackage carries. Inside this repo the @@ -313,9 +314,15 @@ frozen when the template shipped. The two references then sit side-by-side in your CSPROJ, so the SDK version stays an obvious knob — edit it there when you need a specific WinUI patch. If you skip the template and reference `Microsoft.UI.Reactor` by hand, the SDK still -arrives transitively as `Microsoft.WindowsAppSDK.WinUI`; add the full +arrives transitively as `Microsoft.WindowsAppSDK.WinUI` plus +`Microsoft.WindowsAppSDK.Runtime`, so the app builds framework-dependent; add the full metapackage yourself when you want `WindowsAppSDKSelfContained=true` or -an MSIX, both of which need the Runtime redist. The repo-internal +an MSIX, both of which need the Runtime redist. The metapackage is also +how you move to a newer WinUI, because it moves Runtime with it: a newer +`Microsoft.WindowsAppSDK.WinUI` on its own leaves Runtime behind, and the +build fails with *"One or more referenced Windows App SDK components are +newer than the versions expected by the Microsoft.WindowsAppSDK.Runtime +package"*. The repo-internal `WindowsAppSDKVersion` MSBuild property only governs projects under this clone (`Directory.Build.props`); consumer projects pick their version directly. diff --git a/docs/specs/022-packaging-and-distribution.md b/docs/specs/022-packaging-and-distribution.md index e6958a8ce..625385f9a 100644 --- a/docs/specs/022-packaging-and-distribution.md +++ b/docs/specs/022-packaging-and-distribution.md @@ -430,7 +430,7 @@ Consumer gets: framework, analyzers, source generator, and WinUI SDK (transitive - **Internal feed ownership.** Which Azure Artifacts organization hosts the P1 feed? Creating one takes ~a day plus approvals. - **Signing prerequisite for P1.** Does the chosen internal feed enforce signed packages? If yes, P1 needs ESRP too, not just P2. - **Package ID.** `Microsoft.UI.Reactor` assumes we stay in the `Microsoft.UI.*` namespace (see spec 018 for the namespace rename). If that namespace decision changes, the package ID follows. -- **WinUI SDK version.** The right Windows App SDK package is injected centrally per project shape (`Directory.Build.targets`): framework-dependent libraries — including the shipped `Microsoft.UI.Reactor` — reference `Microsoft.WindowsAppSDK.WinUI` (pinned via `WindowsAppSDKWinUIVersion`), so the published package flows only WinUI to consumers rather than the full metapackage (AI/ML/Widgets/DWrite); framework-dependent apps additionally reference `Microsoft.WindowsAppSDK.Runtime`; self-contained/MSIX projects keep the `Microsoft.WindowsAppSDK` metapackage (`WindowsAppSDKVersion`). Consumers who want a different WinUI version can reference their own `Microsoft.WindowsAppSDK*` version — NuGet unifies it transitively with the library's. +- **WinUI SDK version.** The right Windows App SDK package is injected centrally per project shape (`Directory.Build.targets`): framework-dependent projects — libraries and apps alike, including the shipped `Microsoft.UI.Reactor` — reference `Microsoft.WindowsAppSDK.WinUI` (pinned via `WindowsAppSDKWinUIVersion`) plus `Microsoft.WindowsAppSDK.Runtime` rather than the full metapackage (AI/ML/Widgets/DWrite), so the published package flows WinUI + Runtime to consumers — without Runtime the SDK silently builds a consumer app self-contained; self-contained/MSIX projects keep the `Microsoft.WindowsAppSDK` metapackage (`WindowsAppSDKVersion`). Consumers who want a newer WinUI reference the `Microsoft.WindowsAppSDK` metapackage at that version, which moves Runtime and the components together. Referencing `Microsoft.WindowsAppSDK.WinUI` alone leaves Runtime at the library's version, and Runtime's component-version check fails the build. - **`mur` install-script trust boundary.** `iwr | iex` from GitHub Releases works for P1 but will concern P3 users. Document the signed-binary fallback (direct download + verify signature) before public launch. ## 14. Implementation Phases diff --git a/plugins/reactor/skills/reactor-build-and-check/SKILL.md b/plugins/reactor/skills/reactor-build-and-check/SKILL.md index 80c1e3f51..17c05b52c 100644 --- a/plugins/reactor/skills/reactor-build-and-check/SKILL.md +++ b/plugins/reactor/skills/reactor-build-and-check/SKILL.md @@ -10,9 +10,18 @@ Run after every non-trivial edit. **Read the output** — `dotnet run` exits wit ### Single-file `.cs` (default for new apps) ```powershell -dotnet run App.cs -p:Platform=ARM64 # or -p:Platform=x64 +dotnet run App.cs ``` +The file header needs `#:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0`, +which launches the app packaged (needs Developer Mode), or +`#:property WindowsPackageType=None`, which runs it unpackaged. With neither, +the app builds, then dies at startup with `REGDB_E_CLASSNOTREG`. No +`-p:Platform` is needed — only a self-contained app +(`#:property WindowsAppSDKSelfContained=true`) needs an architecture. The +[Getting Started guide](https://microsoft.github.io/microsoft-ui-reactor/getting-started/#one-file-no-project) +documents the packaged header. + Single-file builds **do not load analyzers**. You'll catch CS errors but not the Reactor-specific `REACTOR_*` warnings. ### `.csproj` (multi-file, analyzer coverage) @@ -136,8 +145,9 @@ Additional flags: | `CS1061` | error | "'X' does not contain a definition for 'Y'" | A type-specific modifier called on an element type that does not declare it — `.Bold()` / `.FontSize()` / `.TextWrapping()` are `TextBlockElement`-only, so `Button("Go").Bold()` fails. This is **not** an ordering problem: modifiers are generic (` where T : Element`) and preserve the concrete type, so `.Margin(16).Bold()` and `.Bold().Margin(16)` compile alike. Check the receiver's element type against `reactor-dsl/references/reactor.api.txt`. | | `CS0117` | error | "'Element' does not contain a definition for X" | You're calling a factory or static member that doesn't exist — confirm the name against `reactor-dsl/references/reactor.api.txt`. (For a missing *instance* modifier see `CS1061` above.) | | `CS1955` | error | "Non-invocable member 'Element.Margin' cannot be used like a method" | Missing `using Microsoft.UI.Reactor;`. The element records expose same-named *properties* (`Element.Margin`, `Element.Padding`, plus `CornerRadius`/`BorderThickness` on derived records), and C# only falls back to extension methods when none is in scope — so an absent import reports the property as non-invocable rather than saying the fluent modifier isn't imported. It reads like the API doesn't exist; it does. Add the `using`. (Distinct from `REACTOR_DYM_001` above, which is a genuine property invoked with parens, e.g. `GridSize.Auto()`.) | -| `MSB4025` | error | "The project file could not be loaded" | Single-file `.cs` build attempted without `-p:Platform=...` on a WinUI project. Add `-p:Platform=ARM64` (or x64). | +| `MSB4025` | error | "The project file could not be loaded" | Self-contained (`WindowsAppSDKSelfContained=true`) single-file `.cs` build attempted without an architecture. Add `-p:Platform=ARM64` (or x64), or `-r win-arm64` / `-r win-x64`. | | `NETSDK1136` | error | "platform required" | Same fix — pass `-p:Platform=ARM64` or `x64`. | +| — | error | "One or more referenced Windows App SDK components are newer than the versions expected by the Microsoft.WindowsAppSDK.Runtime package" | A newer `Microsoft.WindowsAppSDK.WinUI` (or another Windows App SDK component) was referenced on its own, leaving `Microsoft.WindowsAppSDK.Runtime` at Reactor's version. Reference the full `Microsoft.WindowsAppSDK` package at the version you want instead; it moves Runtime with the components. Don't switch to `WindowsAppSDKSelfContained=true` just to silence it. | If a `REACTOR_*` ID isn't in this table, the bundled analyzer DLL has more docs. The descriptions ship in the warnings themselves. diff --git a/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs b/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs index 5a19dbe24..dc65c78de 100644 --- a/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs +++ b/tests/Reactor.DocPipeline.Tests/InlineSnippetLedgerTests.cs @@ -183,6 +183,68 @@ private static bool IsParameterDeclaration(string parameter) private static readonly Dictionary> AllowedInlineExamples = new(StringComparer.OrdinalIgnoreCase) { + ["getting-started"] = new(StringComparer.Ordinal) + { + // A .NET file-based app is *defined* by having no .csproj, so it cannot be a doc + // app — the snippet mechanism extracts from projects. It stays one contiguous + // block rather than a ledgered header plus a snippet-backed body because the + // section's whole claim is "this is the entire file"; splitting it would show the + // reader two fragments to reassemble. It is also kept verbatim in step with the + // published blog's quick-start, so the two cannot drift. + // + // What that costs is smaller than it looks. Every construct in the body is + // exercised by a compiled snippet on this same page — ReactorApp.Run(string), + // Component/Render, UseState, VStack, Heading and HStack(spacing, …) all appear + // in getting-started/hello-world and getting-started/layout-basics, and + // UseState + Button(string, Action) in getting-started/usestate-counter. An API + // rename that broke this block would redden those first. + // + // The genuinely unverified part is the #: directives, and they are covered + // separately: the Reactor version is the {{reactorVersion}} token the doc + // compiler substitutes, and SingleFileGuideHeaderTests pins the TargetFramework + // to src/Reactor/Reactor.csproj and asserts each remaining directive sits in the + // block that needs it. That TFM check matters because a stale Windows version + // surfaces as CS0234 "the namespace 'Reactor' does not exist", which reads like + // a missing package rather than a wrong TFM. + [""" + #:package Microsoft.UI.Reactor@{{reactorVersion}} + #:package Microsoft.Windows.SDK.BuildTools.WinApp@0.7.0 + #:property OutputType=WinExe + #:property TargetFramework=net10.0-windows10.0.22621.0 + #:property UseWinUI=true + + using Microsoft.UI.Reactor; + using Microsoft.UI.Reactor.Core; + using static Microsoft.UI.Reactor.Factories; + + ReactorApp.Run("Hello Reactor"); + + class MyApp : Component + { + public override Element Render() + { + var (count, setCount) = UseState(0); + + return VStack( + Heading($"Count: {count}"), + HStack(8, + Button("-", () => setCount(count - 1)), + Button("+", () => setCount(count + 1)) + ) + ); + } + } + """] = + "File-based app: no .csproj by definition, so it cannot be a doc app. The published blog's quick-start app, plus the BuildTools package that lets dotnet run launch it packaged. Its DSL is covered by the compiled hello-world/usestate-counter/layout-basics snippets on the same page; its header by SingleFileGuideHeaderTests.", + + // The one directive that switches the same file to an unpackaged launch. A + // fragment by design: the point is the delta, not a second copy of the app. + [""" + #:property WindowsPackageType=None + """] = + "One-line delta that runs the same file unpackaged; placement is guarded by SingleFileGuideHeaderTests.", + }, + ["source-mapping"] = new(StringComparer.Ordinal) { // The "before" half of the [ReactorSourceTransparent] explanation: a helper diff --git a/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs b/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs new file mode 100644 index 000000000..df6afd9ed --- /dev/null +++ b/tests/Reactor.DocPipeline.Tests/SingleFileGuideHeaderTests.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Xunit; + +namespace Microsoft.UI.Reactor.Cli.Docs.Tests; + +/// +/// Ties the file-based-app header documented in the Getting Started guide to the framework's +/// own target framework. +/// +/// +/// +/// Why this needs a test at all. A .NET file-based app is defined by having no +/// .csproj, so the guide's #: header cannot be a compiled doc-app snippet the way +/// every other C# block on that page is — it is ledgered in +/// InlineSnippetLedgerTests.AllowedInlineExamples instead. That buys a hole: nothing +/// compiles those six lines, and the page renders them identically to the verified blocks around +/// them. +/// +/// +/// The values that actually rot. The Reactor version is the {{reactorVersion}} +/// token the doc compiler substitutes, so it cannot go stale. Every other directive is asserted +/// here, because nothing else looks at them: the target framework against the framework project, +/// and the rest for presence — plus, for WindowsPackageType, placement, since which block +/// carries it is what makes one example packaged and the other not. The target framework is the +/// worst to get wrong: a reader who copies a stale Windows version compiles against nothing and +/// gets CS0234: the type or namespace name 'Reactor' does not exist, which reads like a +/// missing package rather than a wrong TFM. Measured, not assumed — that is the exact error +/// net10.0-windows10.0.19041.0 produces against this package. +/// +/// +/// The Microsoft.Windows.SDK.BuildTools.WinApp pin in the header is deliberately +/// not version-guarded. WinAppSDKReferenceGuardTests does sweep #:package +/// headers, but only for Microsoft.WindowsAppSDK…, and this repo has no central pin for +/// the build-tools package to check a doc against — it is a consumer-side tool rather than a +/// framework dependency. Its presence is asserted below; its version is not. +/// +/// +public class SingleFileGuideHeaderTests +{ + private const string GuideTemplate = "docs/_pipeline/templates/getting-started.md.dt"; + private const string FrameworkProject = "src/Reactor/Reactor.csproj"; + + /// + /// Every #:property TargetFramework=… in the guide must name the TFM + /// src/Reactor/Reactor.csproj builds, because that is what the published package's + /// lib/ folder is keyed on. + /// + [Fact] + public void DocumentedTargetFramework_MatchesTheFrameworkProject() + { + var root = RepoRoot(); + var expected = FrameworkTfm(root); + + var guide = File.ReadAllText(Path.Join(root, GuideTemplate)); + var documented = Regex.Matches(guide, @"^#:property\s+TargetFramework=(?\S+)\s*$", RegexOptions.Multiline) + .Select(m => m.Groups["tfm"].Value) + .ToArray(); + + // A rename or a restructure that drops the header entirely would otherwise leave this + // test passing over an empty set — the classic vacuous green. + Assert.NotEmpty(documented); + + foreach (var tfm in documented) + { + Assert.True( + string.Equals(tfm, expected, StringComparison.Ordinal), + $"{GuideTemplate} documents '#:property TargetFramework={tfm}' but {FrameworkProject} " + + $"builds '{expected}'. A reader copying the stale value compiles against no Reactor " + + "assembly and sees CS0234 \"the namespace 'Reactor' does not exist\", which looks " + + "like a missing package. Update the guide header (both the unpackaged and packaged " + + "blocks) to match."); + } + } + + /// + /// The primary example carries the full header; the second block is the unpackaged delta and + /// must be exactly the one directive that delta exists to add. + /// + /// + /// + /// The header runs packaged under both runners — Microsoft.Windows.SDK.BuildTools.WinApp + /// makes dotnet run package the app the way winapp run does — and + /// WindowsPackageType=None is the switch both obey to run it unpackaged instead. For a + /// Reactor-only app it is also what lets that plain exe start: the app is framework-dependent, + /// because the package flows Microsoft.WindowsAppSDK.Runtime (guarded by + /// WinAppSDKReferenceGuardTests.Reactor_library_flows_WinUI_and_Runtime_not_the_metapackage), + /// and the directive turns on the Windows App SDK bootstrapper. No architecture is needed: only + /// a self-contained app needs one, and the section offers that as an opt-in rather than a step. + /// Equality rather than Contains, so a RuntimeIdentifier line creeping back into + /// the delta fails here. + /// + /// + /// Placement matters as much as presence. Both runners obey the directive — measured: each + /// launched the app with no package identity — so in the primary block it would make every + /// launch unpackaged and silently cost the reader the identity that block exists to give them. + /// + /// + [Fact] + public void DotnetDeltaDirective_SitsInTheSecondBlockOnly() + { + var guide = File.ReadAllText(Path.Join(RepoRoot(), GuideTemplate)); + var blocks = HeaderBlocks(guide); + + // Two header blocks: the complete file, then the unpackaged delta. If the section is + // restructured into a different number, this needs rereading rather than silently + // passing over whichever blocks happen to remain. + Assert.Equal(2, blocks.Count); + + const string directive = "#:property WindowsPackageType=None"; + Assert.Equal(directive, blocks[1]); + + Assert.False( + blocks[0].Contains(directive, StringComparison.Ordinal), + $"The first #: block is the complete single-file app, packaged by default, and must NOT " + + $"declare '{directive}': dotnet run and winapp run both obey it and launch the app " + + "unpackaged, so the reader would lose the package identity that block is there to give them."); + } + + /// + /// The primary header is exactly the five directives the page documents. + /// + /// + /// Compared as an order-insensitive set of directive names: the values are pinned by the other + /// tests, and this one is about which directives the reader has to write at all. It is what + /// catches an architecture or packaging directive being added to the file everyone copies — + /// every such directive is either unnecessary or, like WindowsPackageType, changes how + /// the app runs. + /// + [Fact] + public void PrimaryHeader_DeclaresExactlyTheDocumentedDirectives() + { + var guide = File.ReadAllText(Path.Join(RepoRoot(), GuideTemplate)); + var blocks = HeaderBlocks(guide); + + Assert.Equal(2, blocks.Count); + + var names = blocks[0].Split('\n') + .Select(d => Regex.Match(d, @"^#:(?:package\s+(?[^@\s]+)|property\s+(?[^=\s]+))").Groups["name"].Value) + .Order(StringComparer.Ordinal) + .ToArray(); + + Assert.Equal( + new[] { "Microsoft.UI.Reactor", "Microsoft.Windows.SDK.BuildTools.WinApp", "OutputType", "TargetFramework", "UseWinUI" }, + names); + } + + /// + /// Every directive the primary header's table calls load-bearing must appear in it. + /// + /// + /// These share a failure mode: the block is exempt from compilation, so dropping any one of + /// them leaves the whole suite green while a reader's run fails. Each row of the table is a + /// claim, and each claim is checked. + /// + /// Microsoft.Windows.SDK.BuildTools.WinApp — without it plain dotnet run + /// cannot package the app, and it dies at startup with REGDB_E_CLASSNOTREG. Measured: + /// the same header with WindowsPackageType unset, or even set to MSIX, crashes + /// that way under plain dotnet run. + /// OutputType=WinExe — does not fail the build at all; it links the app for the + /// console subsystem instead. Measured on this tree: the PE subsystem byte reads 3 (console) + /// without it and 2 (Windows GUI) with it, so a console window sits behind the UI. + /// TargetFramework — also checked against the framework project by + /// ; this pins its presence + /// in the primary block specifically. + /// UseWinUI=true — brings in the WinUI 3 targets. + /// + /// + [Theory] + [InlineData("#:package Microsoft.Windows.SDK.BuildTools.WinApp@")] + [InlineData("#:property OutputType=WinExe")] + [InlineData("#:property TargetFramework=")] + [InlineData("#:property UseWinUI=true")] + public void PrimaryHeader_DeclaresTheLoadBearingDirectives(string directive) + { + var guide = File.ReadAllText(Path.Join(RepoRoot(), GuideTemplate)); + var blocks = HeaderBlocks(guide); + + Assert.Equal(2, blocks.Count); + Assert.True( + blocks[0].Contains(directive, StringComparison.Ordinal), + $"The primary single-file header in {GuideTemplate} is missing '{directive}'. The " + + "guide's directive table documents it as required, and this block is exempt from " + + "compilation, so nothing else would catch its removal."); + } + + /// + /// The contiguous runs of #: directives in the guide, in document order. + /// + private static List HeaderBlocks(string guide) + { + var blocks = new List(); + var current = new List(); + + foreach (var line in guide.Split('\n').Select(l => l.TrimEnd('\r'))) + { + if (line.StartsWith("#:", StringComparison.Ordinal)) + { + current.Add(line); + } + else if (current.Count > 0) + { + blocks.Add(string.Join('\n', current)); + current.Clear(); + } + } + + if (current.Count > 0) + blocks.Add(string.Join('\n', current)); + + return blocks; + } + + private static string FrameworkTfm(string root) + { + var csproj = File.ReadAllText(Path.Join(root, FrameworkProject)); + var match = Regex.Match(csproj, @"(?[^<]+)"); + + Assert.True(match.Success, $"No found in {FrameworkProject}."); + return match.Groups["tfm"].Value.Trim(); + } + + private static string RepoRoot() + { + // Anchor on the solution file, not on .git: in a git worktree .git is a *file*, so a + // Directory.Exists probe walks past the root and returns null. Mirrors FindRepoRoot in + // InlineSnippetLedgerTests. + var dir = AppContext.BaseDirectory; + while (dir is not null + && !File.Exists(Path.Join(dir, "Reactor.slnx")) + && !Directory.Exists(Path.Join(dir, ".git"))) + { + dir = Path.GetDirectoryName(dir); + } + + Assert.NotNull(dir); + return dir!; + } +} diff --git a/tests/Reactor.Tests/WinAppSDKReferenceGuardTests.cs b/tests/Reactor.Tests/WinAppSDKReferenceGuardTests.cs index 2dddf07df..e5ac3968b 100644 --- a/tests/Reactor.Tests/WinAppSDKReferenceGuardTests.cs +++ b/tests/Reactor.Tests/WinAppSDKReferenceGuardTests.cs @@ -14,8 +14,9 @@ namespace Microsoft.UI.Reactor.Tests; /// Guards the Windows App SDK dependency policy (see Directory.Build.targets). /// Repo projects must not reference the full Microsoft.WindowsAppSDK /// metapackage directly: the correct package is injected centrally — -/// Microsoft.WindowsAppSDK.WinUI for framework-dependent libraries, WinUI + -/// Microsoft.WindowsAppSDK.Runtime for framework-dependent apps, and the full +/// Microsoft.WindowsAppSDK.WinUI + Microsoft.WindowsAppSDK.Runtime for +/// framework-dependent projects (libraries included, so Runtime flows to consumer +/// apps), and the full /// metapackage only for self-contained / MSIX projects. A stray direct reference /// bypasses that rule and re-drags the unused AI / ML / Widgets / DWrite slices back /// into the dependency graph. @@ -86,21 +87,31 @@ public void No_repo_project_references_the_WindowsAppSDK_metapackage_directly() offenders.Count == 0, "These projects reference the Microsoft.WindowsAppSDK metapackage directly. " + "Remove the reference — Directory.Build.targets injects " - + "Microsoft.WindowsAppSDK.WinUI (+ .Runtime for apps) or the metapackage " + + "Microsoft.WindowsAppSDK.WinUI + .Runtime or the metapackage " + "(self-contained / MSIX) centrally. Offenders:\n " + string.Join("\n ", offenders.OrderBy(x => x, StringComparer.Ordinal))); } /// /// Outcome-level guard: the shipped Microsoft.UI.Reactor library must - /// resolve the lean WinUI sub-package and must NOT flow the full metapackage to - /// consumers. Reads Reactor's restore graph (present because this test project - /// references Reactor), so it catches a central-rule regression wherever it is - /// introduced — including in Directory.Build.*, which the source scan above - /// does not cover. + /// resolve the lean WinUI sub-package plus Microsoft.WindowsAppSDK.Runtime, + /// and must NOT flow the full metapackage to consumers. Reads Reactor's restore + /// graph (present because this test project references Reactor), so it catches a + /// central-rule regression wherever it is introduced — including in + /// Directory.Build.*, which the source scan above does not cover. /// + /// + /// Runtime is what keeps a consumer app framework-dependent: it sets + /// MicrosoftWindowsAppSDKPackageDir, and an app whose graph lacks it is + /// silently built self-contained by the SDK's Base.targets. That is what + /// happened to every app referencing only Reactor between #822 and this guard, + /// surfacing as "WindowsAppSDKSelfContained requires a supported Windows + /// architecture". Being in Reactor's own graph is not enough, so the edge is also + /// checked for flowing: a PrivateAssets="all" reference would pass the graph + /// check while never reaching the nuspec. + /// [Fact] - public void Reactor_library_resolves_WinUI_subpackage_not_the_metapackage() + public void Reactor_library_flows_WinUI_and_Runtime_not_the_metapackage() { var root = RepoRootFinder.FindRepoRoot(); Assert.NotNull(root); @@ -116,7 +127,24 @@ public void Reactor_library_resolves_WinUI_subpackage_not_the_metapackage() .ToList(); Assert.Contains(winAppSdk, k => k.StartsWith("Microsoft.WindowsAppSDK.WinUI/", StringComparison.Ordinal)); + Assert.Contains(winAppSdk, k => k.StartsWith("Microsoft.WindowsAppSDK.Runtime/", StringComparison.Ordinal)); Assert.DoesNotContain(winAppSdk, k => k.StartsWith("Microsoft.WindowsAppSDK/", StringComparison.Ordinal)); + + foreach (var framework in doc.RootElement.GetProperty("project").GetProperty("frameworks").EnumerateObject()) + { + Assert.True( + framework.Value.GetProperty("dependencies").TryGetProperty("Microsoft.WindowsAppSDK.Runtime", out var runtime), + $"Microsoft.WindowsAppSDK.Runtime is not a direct dependency of Reactor for {framework.Name}, so " + + "nothing guarantees it reaches the nuspec. The Windows App SDK injection in " + + "Directory.Build.targets must give framework-dependent libraries Runtime as well as WinUI."); + + var suppressParent = runtime.TryGetProperty("suppressParent", out var value) ? value.GetString() : null; + Assert.False( + string.Equals(suppressParent, "All", StringComparison.OrdinalIgnoreCase), + $"Microsoft.WindowsAppSDK.Runtime is a private asset of Reactor for {framework.Name} " + + "(suppressParent=All), so it never reaches consumer apps and they fall back to " + + "self-contained. Drop PrivateAssets from that reference."); + } } ///