Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
3f6fa9f
Build winapp run projects with Platform instead of a global RID
nmetulev Oct 8, 2026
41c11f6
Merge remote-tracking branch 'origin/main' into nmetulev-fluentstore-…
nmetulev Oct 8, 2026
fd9b319
Keep the RID for projects that need it even when references remove it
nmetulev Oct 8, 2026
b0883aa
Merge remote-tracking branch 'origin/main' into nmetulev-fluentstore-…
nmetulev Oct 8, 2026
4e1984e
Keep the RID-free build for dynamic platform resolution graphs
nmetulev Oct 8, 2026
ebd75ee
Keep the RID for cross-architecture builds with project-referenced an…
nmetulev Oct 9, 2026
4caa28a
Merge remote-tracking branch 'origin/main' into nmetulev-fluentstore-…
nmetulev Oct 9, 2026
e622801
Catch only file and XML errors when walking project references
nmetulev Oct 9, 2026
ac9e879
Merge branch 'main' into nmetulev-fluentstore-winapp-run-debug
nmetulev Oct 9, 2026
5cc4e8a
Decide Platform-only builds from the evaluated reference graph
nmetulev Oct 9, 2026
06c292f
Fix project-arch CI: x86 runtime, R20 host, hang diagnostics
nmetulev Oct 9, 2026
8d086b6
Address code-quality findings in architecture graph test helper
nmetulev Oct 9, 2026
8e5ac81
project-arch: dump winapp when a run times out
nmetulev Oct 9, 2026
59c1fa0
Merge branch 'main' into nmetulev-fluentstore-winapp-run-debug
nmetulev Oct 9, 2026
19fd807
project-arch: add property-path generator and solution bundle fixtures
nmetulev Oct 9, 2026
97f9f66
Merge remote-tracking branch 'origin/main' into nmetulev-fluentstore-…
nmetulev Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion .github/workflows/build-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -344,10 +344,73 @@ jobs:
if-no-files-found: warn
overwrite: true

# Generated .NET projects covering how `winapp run` conveys the target architecture to MSBuild.
# Fixtures run one at a time per shard; see tests/project-arch/README.md.
project-arch:
Comment thread
nmetulev marked this conversation as resolved.
needs: build-artifacts
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
name: project-arch (${{ matrix.shard }}/4)
runs-on: windows-latest
timeout-minutes: 75
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Enable Windows Developer Mode
run: |
reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /v "AllowDevelopmentWithoutDevLicense" /d 1 /f

# Some fixtures target net8.0 and must launch on its runtime.
- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: |
8.0.x
10.0.x

- name: Download CLI binaries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: cli-binaries
path: artifacts/cli

- name: Run architecture matrix
env:
SHARD: ${{ matrix.shard }}
run: |
$pester = Get-Module -Name Pester -ListAvailable | Where-Object { $_.Version.Major -ge 5 } | Sort-Object Version -Descending | Select-Object -First 1
if (-not $pester) { throw 'Pester 5 is required.' }
Import-Module $pester -Force
$container = New-PesterContainer -Path tests/project-arch/ProjectArch.Tests.ps1 -Data @{
WinappPath = 'artifacts/cli/win-x64'
Architecture = 'x64'
Shard = "$env:SHARD/4"
}
$config = New-PesterConfiguration
$config.Run.Container = $container
$config.Run.Exit = $true
$config.Output.Verbosity = 'Detailed'
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = "artifacts/TestResults/project-arch-$env:SHARD.xml"
$config.TestResult.OutputFormat = 'JUnitXml'
Invoke-Pester -Configuration $config

- name: Upload test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: project-arch-results-${{ matrix.shard }}
path: artifacts/TestResults/project-arch-${{ matrix.shard }}.xml
if-no-files-found: error
overwrite: true

# Retain the original required check, but only turn it green after every lane.
build-and-package:
if: always()
needs: [build-artifacts, validate-tests, validate-docs, e2e-test-ui, samples, metrics]
needs: [build-artifacts, validate-tests, validate-docs, e2e-test-ui, samples, metrics, project-arch]
runs-on: ubuntu-latest
steps:
- name: Require all build and validation jobs
Expand Down
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,13 @@ matrix with `npm-package` and `nuget-packages` from the same run. Manual sample
runs still build their own packages and support selecting one sample. Sample
results are uploaded as JUnit XML via Pester's `TestResult` configuration.

The `project-arch` job runs `tests\project-arch\ProjectArch.Tests.ps1` in four
shards with the same-run x64 CLI. It generates .NET projects covering how
`winapp run` conveys the target architecture (Platform vs. RuntimeIdentifier,
reference graphs, packaging, Windows App SDK versions) and launches each app.
Add a fixture there when changing project-mode architecture handling; see
`tests\project-arch\README.md` to run it locally.

Keep `build-and-package` and `test-samples-result` as top-level aggregate check
names for branch protection. They must fail on failures, cancellations, and
unexpected skips, not just test failures. Metrics join package artifacts with
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/dotnet.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ winapp run .\bin\Debug\net10.0-windows10.0.26100.0

Project mode supports both **packaged** and **unpackaged** WinUI apps — it detects which from the project's `WindowsPackageType` and installs the matching-architecture Windows App Runtime automatically. To force an unpackaged run of a packaged project, add `-p WindowsPackageType=None`.

**Multi-project apps** (an app referencing class libraries) build correctly: winapp keeps `AnyCPU`/`netstandard2.0` references on their compatible platform instead of forcing the app's architecture across the graph. RID-only remains the default; when the effective configuration requires a self-contained profile (for example, a trimmed Release build), winapp selects the matching profile without changing referenced libraries' platforms.
**Multi-project apps** (an app referencing class libraries) build correctly: winapp builds with the matching MSBuild `Platform`, as Visual Studio does, so referenced libraries, including `AnyCPU`/`netstandard2.0` ones and WinUI libraries that enable MSIX tooling, are built once and packaged once. A `win-$(Platform)` publish profile in your app (for example, for a trimmed Release build) applies automatically.

The `dotnet build` output streams live, with the exact invocation printed first. Add `--verbose` for winapp's own build decision traces. Requires .NET SDK 8.0.100 or newer. See [`winapp run` in the usage reference](../usage.md#project-mode-net-sdk-projects) for the full option list.

Expand Down
4 changes: 2 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -843,8 +843,8 @@ For apps that use package identity without a generated MSIX layout, include `Pac
**Project-mode options** (ignored in folder mode unless noted):

- `-c, --configuration <name>` - Build configuration. Default: `Debug`. *(Also honored in single-file mode.)*
- `--arch <x64|arm64|x86>` - Target architecture. Default: the current process architecture. Determines the build RID and Windows App Runtime architecture, and selects a matching platform-dependent publish profile when required by the effective build. *(Also honored in single-file mode.)*
- `-r, --runtime <rid>` - Target .NET runtime identifier (e.g. `win-x64`). Project mode uses only the RID's architecture, always builds the canonical `win-<arch>`, and rejects non-Windows RIDs (e.g. `linux-x64`). Its architecture overrides `--arch` and can select the required publish profile. *(Also honored in single-file mode, where it overrides a `#:property RuntimeIdentifier` declared by the file.)*
- `--arch <x64|arm64|x86>` - Target architecture. Default: the current process architecture. Builds with the matching MSBuild `Platform` (for example `-p:Platform=ARM64`), as Visual Studio does, so a `win-$(Platform)` publish profile in your project applies automatically. A project that sets its own `RuntimeIdentifier` or enables `EnableDynamicPlatformResolution` builds for `win-<arch>` instead. Also selects the Windows App Runtime architecture. *(Also honored in single-file mode.)*
- `-r, --runtime <rid>` - Target .NET runtime identifier (e.g. `win-x64`). Project mode uses only the RID's architecture, exactly like `--arch`, and rejects non-Windows RIDs (e.g. `linux-x64`). Its architecture overrides `--arch`. *(Also honored in single-file mode, where it overrides a `#:property RuntimeIdentifier` declared by the file.)*
- `-f, --framework <tfm>` - Target framework moniker for multi-targeted projects (e.g. `net10.0-windows10.0.26100.0`). *(Rejected in single-file mode — use `#:property TargetFramework=...`.)*
- `--project <name-or-path>` - When the input is a solution (`.sln`/`.slnx`) or a directory with multiple runnable app projects, selects which project to launch (by project name or path). *(Rejected in single-file mode — a `.cs` file-based app is itself the project.)*
- `--no-build` - Skip building and run the existing build output (still evaluates output properties). *(Also honored in single-file mode.)*
Expand Down
4 changes: 2 additions & 2 deletions plugins/winapp/com.github.copilot/agents/winapp.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,8 @@ Need to know whether a Windows/WinRT API exists, or what a type/enum actually of
- `--with-alias` — force execution-alias launch for a windowed app (console apps already do this by default, so it is rarely needed)
- `--without-alias` — force AUMID activation for a console app, which then runs without a console and prints nothing to the terminal
- `-c, --configuration <name>` — (project + single-file mode) build configuration; default `Debug`
- `--arch <x64|arm64|x86>` — (project mode) target architecture; default: current process arch. Sets the build RID and Windows App Runtime arch, and selects a matching platform-dependent publish profile when required. Honored in single-file mode too, where it defaults to the current winapp process architecture.
- `-r, --runtime <rid>` — (project mode) target .NET RID (e.g. `win-x64`); **only the RID's architecture is used** — project mode reduces it and always builds the canonical `win-<arch>` RID, so a version-specific or non-Windows RID is not forwarded (a non-Windows RID like `linux-x64` is rejected). Overrides `--arch` and can select the required publish profile. Honored in single-file mode too, where it defaults to the current winapp process architecture.
- `--arch <x64|arm64|x86>` — (project mode) target architecture; default: current process arch. Builds with the matching MSBuild `Platform` (no global RID), as Visual Studio does, and sets the Windows App Runtime arch; a project that sets its own `RuntimeIdentifier` or enables `EnableDynamicPlatformResolution` builds for `win-<arch>` instead. Honored in single-file mode too, where it defaults to the current winapp process architecture.
- `-r, --runtime <rid>` — (project mode) target .NET RID (e.g. `win-x64`); **only the RID's architecture is used**, exactly like `--arch`, so a version-specific or non-Windows RID is not forwarded (a non-Windows RID like `linux-x64` is rejected). Overrides `--arch`. Honored in single-file mode too, where it defaults to the current winapp process architecture.
- `-f, --framework <tfm>` — (project mode) target framework for multi-targeted projects. **Rejected in single-file mode** — declare `#:property TargetFramework=…` instead
- `--project <name-or-path>` — (project mode) select which project to launch when a solution/directory has multiple runnable app projects (errors listing candidates if ambiguous). **Rejected in single-file mode** — the `.cs` file is the project
- `--no-build` / `--no-restore` — (project + single-file mode) skip build / restore
Expand Down
2 changes: 1 addition & 1 deletion plugins/winapp/skills/winapp-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ winapp run . --aot
winapp run . --aot -c Release
```

Project mode supports both **packaged** and **unpackaged** WinUI apps, detected from the project's effective `WindowsPackageType` (`MSIX` ⇒ loose-layout register + AUMID launch; `None` ⇒ launch the built `.exe`), and installs the matching-architecture Windows App Runtime before launching. RID-only remains the default; when the effective configuration requires a self-contained profile, winapp selects the architecture-matching profile without forcing that platform onto referenced `AnyCPU` libraries. Requires .NET SDK 8.0.100+.
Project mode supports both **packaged** and **unpackaged** WinUI apps, detected from the project's effective `WindowsPackageType` (`MSIX` ⇒ loose-layout register + AUMID launch; `None` ⇒ launch the built `.exe`), and installs the matching-architecture Windows App Runtime before launching. `--arch` builds with the matching MSBuild `Platform`, as Visual Studio does, so multi-project graphs and `win-$(Platform)` publish profiles work without extra properties; a project that sets its own `RuntimeIdentifier` or enables `EnableDynamicPlatformResolution` builds for `win-<arch>` instead. Requires .NET SDK 8.0.100+.

- **Build inputs:** `-c/--configuration`, `--arch`, `-r/--runtime`, `-f/--framework`, `--no-build`, `--no-restore`, `--aot`, `-p/--property` (repeat for multiple properties; use `%3B` or `%2C` for a literal semicolon or comma in a value). `--aot` supports x64/ARM64 projects, requires .NET SDK 8.0.300+ and effective `PublishAot=true`, and cannot use `--no-build` or `--manifest`; configure the project manifest before publishing.
- **Packaged-only options:** `--manifest`, `--no-launch`, `--with-alias`, `--clean`, `--unregister-on-exit`, `--output-appx-directory`, `--executable` — rejected for unpackaged apps.
Expand Down
23 changes: 15 additions & 8 deletions samples/winui-app/test.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Describe 'winui-app sample' {
$script:platform = if ($script:rid -eq 'win-arm64') { 'ARM64' } else { 'x64' }
$script:profileName = "debug-$($script:platform).pubxml"

# Exercise the real .NET SDK boundary behind architecture-profile inference. Keep this
# Exercise the real .NET SDK boundary behind architecture-profile selection. Keep this
# test-only configuration out of the sample itself so its normal workflow stays lightweight.
$projectPath = Join-Path $script:tempDir 'winui-app.csproj'
$project = [System.Xml.Linq.XDocument]::Load($projectPath)
Expand Down Expand Up @@ -125,7 +125,7 @@ Describe 'winui-app sample' {
"@ | Set-Content -Path (Join-Path $profileDir $script:profileName)

# A same-named library profile would be imported by a plain global PublishProfile property.
# winapp scopes the inferred profile to the root app, so this file must remain inactive.
# Only the app selects a profile, so this file must remain inactive.
$libraryProfileDir = Join-Path $libraryDir 'Properties\PublishProfiles'
New-Item -ItemType Directory -Path $libraryProfileDir -Force | Out-Null
Copy-Item -Path (Join-Path $profileDir $script:profileName) -Destination $libraryProfileDir
Expand All @@ -150,16 +150,21 @@ Describe 'winui-app sample' {
# --no-launch builds the loose layout and registers a debug identity
# without launching the app (no GUI, deterministic in CI).
$output = Invoke-WinappCommand -Arguments 'run . --no-launch'
"$output" | Should -Match ([regex]::Escape("-p:PublishProfile=$($script:profileName)"))
# winapp conveys the architecture with Platform, so the project's own debug-$(Platform)
# profile applies; RID-only would fail with NETSDK1102 (proven above).
"$output" | Should -Match ([regex]::Escape("-p:Platform=$($script:platform)"))
"$output" | Should -Not -Match '(^|\s)-r\s+win-'
# Scoped to the profile argument rather than the whole console output: winapp prints its
# version banner, which carries the branch name, so a bare 'release-' match fails on any
# branch whose name happens to contain it.
"$output" | Should -Not -Match ([regex]::Escape('PublishProfile=release-'))
Join-Path $script:tempDir "bin\$($script:platform)\Debug\net10.0-windows10.0.26100.0\$($script:rid)\winui-app.exe" |
Should -Exist -Because 'the self-contained profile places the output under its RuntimeIdentifier'
"$output" | Should -Match 'Registering packaged application'
"$output" | Should -Match 'registered'
}

It 'Finds the inferred-profile output with --no-build' -Skip:$script:skip {
It 'Finds the profile output with --no-build' -Skip:$script:skip {
Invoke-WinappCommand -Arguments 'run . --no-build --no-launch'
}

Expand Down Expand Up @@ -218,7 +223,12 @@ Describe 'winui-app sample' {
$stagedExe = Join-Path $layoutDir $executable
$stagedExe | Should -Exist

$recipes = @(Get-ChildItem (Join-Path $script:tempDir 'bin') -Recurse -Filter '*.build.appxrecipe')
# Earlier steps in this folder leave their own build output (bin\<Platform>\...), so locate the
# Native AOT build through its publish folder: the recipe sits in that publish folder's TargetDir.
$published = @(Get-ChildItem (Join-Path $script:tempDir 'bin') -Recurse -Filter $executable |
Where-Object { $_.Directory.Name -eq 'publish' })
$published.Count | Should -Be 1
$recipes = @(Get-ChildItem $published[0].Directory.Parent.FullName -Filter '*.build.appxrecipe')
$recipes.Count | Should -Be 1
$recipe = [System.Xml.Linq.XDocument]::Load($recipes[0].FullName)
$entries = @($recipe.Descendants() | Where-Object {
Expand All @@ -228,9 +238,6 @@ Describe 'winui-app sample' {
$entries.Count | Should -Be 1
$nativeExe = [System.IO.Path]::GetFullPath($entries[0].Attribute('Include').Value, $recipes[0].DirectoryName)
$nativeExe | Should -Exist
$published = @(Get-ChildItem (Join-Path $script:tempDir 'bin') -Recurse -Filter $executable |
Where-Object { $_.Directory.Name -eq 'publish' })
$published.Count | Should -Be 1
(Get-FileHash $stagedExe).Hash | Should -Be (Get-FileHash $nativeExe).Hash
(Get-FileHash $stagedExe).Hash | Should -Be (Get-FileHash $published[0].FullName).Hash

Expand Down
3 changes: 2 additions & 1 deletion samples/winui-solution/App.Core/App.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"PRI file not found" failure when a consumer forces a global -p:Platform=<arch> together with
-p:EnableDynamicPlatformResolution=true: EDPR negotiates this library's compile down to AnyCPU
(bin\Debug\...) while the forced Platform keeps the app's PRI-outputs lookup at bin\<arch>\...
Conveying arch via -r <rid> only (winapp project mode) keeps both sides consistent.
Without EDPR a global Platform reaches this library too, so both sides stay consistent; winapp
project mode conveys the arch with -r <rid> instead for projects that enable EDPR.
-->
<PropertyGroup>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
Expand Down
16 changes: 7 additions & 9 deletions samples/winui-solution/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ This sample is a **multi-project Visual Studio solution** used to exercise
**skipping `App.Core`** (a library) and **`App.Tests`**, with no ambiguity error.
- Explicit project selection via `--project` when you want a specific project.
- A **multi-project arch regression guard**: `App` references `App.Core`, which
declares no `<Platforms>`. A global `-p:Platform=<arch>` would desynchronize the
library's XAML/PRI output path from the app's lookup (`MSB3030`/`PRI252`), so a
green build here proves winapp keeps the two consistent.
declares no `<Platforms>`. A green build here proves the library's XAML/PRI output
and the app's lookup stay on one path (a mismatch fails with `MSB3030`/`PRI252`).

## The projects

Expand All @@ -30,12 +29,11 @@ This sample is a **multi-project Visual Studio solution** used to exercise
| `App.Tests` | Test-shaped (skipped by auto-select) | `OutputType=WinExe`, `<ProjectCapability Include="TestContainer" />`, `MSTest.TestFramework` ref, **`IsTestProject` deliberately NOT set** |

`App.Core` intentionally declares **no `<Platforms>`** (effective Platform `AnyCPU`).
Because winapp only injects an explicit `-p:Platform=<arch>` when the target *and its
whole `ProjectReference` closure* declare that arch, this sample builds with the
architecture conveyed by `-r win-<arch>` alone — so the library's `Generic.xbf`/`.pri`
and the consuming app agree on one output path. The solution maps `App.Core` to
`Any CPU` for every configuration (the VS-canonical mapping for a no-`<Platforms>`
library) so a plain `dotnet build WinUISolution.sln -p:Platform=<arch>` stays consistent too.
`winapp run` builds with `-p:Platform=<arch>`, as Visual Studio does, so the library's
`Generic.xbf`/`.pri` and the consuming app agree on one output path. The solution maps
`App.Core` to `Any CPU` for every configuration (the VS-canonical mapping for a
no-`<Platforms>` library) so a plain `dotnet build WinUISolution.sln -p:Platform=<arch>`
stays consistent too.

`App.Tests` intentionally does **not** set `IsTestProject`, so a naive check on that
property (or on a `.Tests` name suffix) would miss it. `winapp run` classifies it from
Expand Down
Loading
Loading