Skip to content

Make Microsoft.Windows.SDK.BuildTools.WinApp safe for transitive consumption#513

Open
nmetulev wants to merge 4 commits into
mainfrom
nm/winapp-nuget-buildtransitive
Open

Make Microsoft.Windows.SDK.BuildTools.WinApp safe for transitive consumption#513
nmetulev wants to merge 4 commits into
mainfrom
nm/winapp-nuget-buildtransitive

Conversation

@nmetulev
Copy link
Copy Markdown
Member

@nmetulev nmetulev commented Apr 30, 2026

Summary

Makes Microsoft.Windows.SDK.BuildTools.WinApp safe for transitive consumption so a library author (e.g., the MAUI Windows wrapper) can take a PackageReference on it and have the targets flow through to consumer head apps — without having to ask every consumer to add their own PackageReference.

What changes

Targets hardening (build/Microsoft.Windows.SDK.BuildTools.WinApp.targets)

Introduces a single computed master gate _WinAppRunSupportActive that all gated targets/items now reference instead of repeating long conditions. The gate requires all of:

  1. EnableWinAppRunSupport != false (existing opt-out preserved)
  2. _WinAppEffectiveTargetPlatformIdentifier == Windows (skips iOS/Android/Mac TFMs in MAUI multi-target)
  3. OutputType != Library (head apps only — Exe, WinExe, etc.)
  4. WindowsPackageType != None (skip unpackaged WinUI apps)
  5. A manifest exists — either WinAppManifestPath is set and the file exists, or auto-detection finds one

_WinAppEffectiveTargetPlatformIdentifier prefers $(TargetPlatformIdentifier) if explicitly set, falls back to [MSBuild]::GetTargetPlatformIdentifier($(TargetFramework)).

The gate is also applied to the <AppxManifest> ItemGroup, the design-time <Compile> inclusion, and the <Content Include="appxmanifest.xml"> ItemGroup so they no longer fire in unrelated projects.

Packaging (Microsoft.Windows.SDK.BuildTools.WinApp.csproj)

  • Added <None Include="build\**\*" PackagePath="buildTransitive" /> so the targets are mirrored into buildTransitive/ (verified in the produced nupkg: 2 files in build/, identical 2 in buildTransitive/).
  • Removed <DevelopmentDependency>true</DevelopmentDependency> — this was forcing consumers to default to PrivateAssets=all, which blocks transitive flow entirely.

Docs

  • src/winapp-NuGet/README.md: split Usage into "Direct consumption (head app)" with PrivateAssets="all" recommended, and "Transitive consumption (library author)" with PrivateAssets="none" IncludeAssets="build;buildTransitive".
  • docs/dotnet-run-support.md: full 5-condition gate explanation in the Detection Logic section.

Tests (src/winapp-NuGet/tests/NuGet.Tests.ps1)

New 14-case Pester suite (12 gate matrix + 2 nupkg layout). Runs dotnet msbuild -getProperty:_WinAppRunSupportActive against synthetic csprojs (Library / Exe / WinExe × manifest yes/no × EnableWinAppRunSupport=false × non-Windows TFM × WindowsPackageType=None × custom manifest path missing). All 14 pass.

Validation

End-to-end smoke tests against the locally produced 0.3.1-nm-winapp-nuget-buildtransitive.10.nupkg:

Scenario OutputType Manifest Consumption Gate Build
samples/dotnet-app Exe yes direct true
samples/wpf-app WinExe (UseWPF) yes direct true
Synthetic MyLib Library no direct (PrivateAssets=none) false
Synthetic HeadExe (no manifest) Exe no transitive via ProjectReference false
Synthetic HeadExe (with manifest) Exe yes transitive via ProjectReference true
Synthetic ConsumerLib Library no transitive via ProjectReference false

Inspected obj/HeadExe.csproj.nuget.g.targets and confirmed NuGet auto-imports buildTransitive/Microsoft.Windows.SDK.BuildTools.WinApp.targets into the consumer head app even though it never directly references the package — proves the MAUI scenario works.

samples/dotnet-app (OutputType=Exe) was the specific reason the gate uses OutputType != Library rather than OutputType == WinExe — gating on WinExe would have silently broken our own packaged-console-app sample.

Risks / behavior changes

  1. DevelopmentDependency removal is a behavior change for direct consumers using default PrivateAssets. They will now see the package flow transitively unless they set PrivateAssets="all" themselves. README updated to recommend PrivateAssets="all" for direct head-app consumption.
  2. Both build/ and buildTransitive/ contain the same files; for direct consumers NuGet imports build/ (with buildTransitive/ as fallback), so no double-import. This matches WindowsAppSDK's pattern at scale.

Not covered by automated tests yet

  • Real MAUI multi-target project with iOS/Android TFMs (T3 in the test matrix). The gating logic is based purely on _WinAppEffectiveTargetPlatformIdentifier != 'Windows', which is straightforward and tested at the property-evaluation level, but a workload-installed MAUI build would be the strongest signal. Recommend coordinating with Matthew for a real-world try.
  • Release-strategy decisions (version bump, changelog wording, single vs. two-phase release) — leaving for maintainers.

…umption

Enables a library (e.g. a MAUI library wrapper) to re-export the package
to its consumers via PrivateAssets="none", matching the dual-pack pattern
used by Microsoft.WindowsAppSDK (build/ + buildTransitive/ mirrored 1:1).

Hardening (build/Microsoft.Windows.SDK.BuildTools.WinApp.targets):
* Introduce single computed gate _WinAppRunSupportActive based on:
  EnableWinAppRunSupport, WindowsPackageType != None, OutputType != Library,
  Windows TargetPlatformIdentifier (TargetPlatformIdentifier preferred over
  GetTargetPlatformIdentifier(TargetFramework) when set), and either a
  manifest in the project directory, an explicit WindowsPackageType=MSIX,
  or a custom WinAppManifestPath that exists.
* OutputType=Exe is intentionally supported (samples/dotnet-app is a
  packaged console app via execution alias) - not gated to WinExe.
* Replace per-target conditions with the single gate. Move the gate from
  the inner PropertyGroup to the outer Target Condition for
  _WinAppPrepareRunArguments so it no longer fires BeforeTargets=
  ComputeRunArguments in unrelated projects.
* Apply the gate to the AppxManifest promotion ItemGroup, the design-time
  Compile inclusion, and the appxmanifest Content inclusion.

Packaging (Microsoft.Windows.SDK.BuildTools.WinApp.csproj):
* Pack build/ contents to both build/ and buildTransitive/ so direct and
  transitive consumers see the same MSBuild logic.
* Remove DevelopmentDependency=true (it forces PrivateAssets=all on
  consumers, blocking transitive flow). Comment explains why.

Tests (src/winapp-NuGet/tests/NuGet.Tests.ps1):
* 12-case gate matrix using dotnet msbuild -getProperty: against
  synthesized csproj files, covering active scenarios (WinUI, packaged
  console, explicit MSIX, custom manifest path, MAUI Windows TFM) and
  inactive scenarios (libraries, no-manifest console, opt-outs, MAUI
  non-Windows TFMs, plain net8.0).
* Package-shape parity test asserting build/ ↔ buildTransitive/ contain
  the same files.
* All 14 cases pass.

Docs:
* README: split usage into direct (PrivateAssets=all recommended) vs
  transitive (PrivateAssets=none + IncludeAssets=build;buildTransitive)
  consumption with explanations of the gating guarantees.
* docs/dotnet-run-support.md: detection logic section updated to describe
  the full _WinAppRunSupportActive gate; pipeline diagram label updated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 30, 2026 21:29
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Enables Microsoft.Windows.SDK.BuildTools.WinApp to flow MSBuild props/targets transitively (via buildTransitive/) while keeping them inert unless the consuming project is a packaged Windows app.

Changes:

  • Introduces a single master gate property (_WinAppRunSupportActive) and applies it across targets/item groups to prevent activation in non-applicable projects.
  • Packs identical build/ and buildTransitive/ contents and removes DevelopmentDependency to allow transitive propagation.
  • Adds Pester coverage for gate behavior and nupkg layout parity; updates docs/README guidance for direct vs transitive consumption.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/winapp-NuGet/build/Microsoft.Windows.SDK.BuildTools.WinApp.targets Adds _WinAppRunSupportActive and re-gates targets/items to be safe under buildTransitive/.
src/winapp-NuGet/Microsoft.Windows.SDK.BuildTools.WinApp.csproj Packs build/ into buildTransitive/ and removes DevelopmentDependency to allow transitive flow.
src/winapp-NuGet/README.md Documents direct vs transitive referencing patterns and expected gating behavior.
docs/dotnet-run-support.md Updates detection logic description to reflect the new master gate.
src/winapp-NuGet/tests/NuGet.Tests.ps1 Adds Pester tests for gate matrix and nupkg layout parity.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/winapp-NuGet/tests/NuGet.Tests.ps1 Outdated
Comment thread src/winapp-NuGet/tests/NuGet.Tests.ps1
Comment thread docs/dotnet-run-support.md Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 30, 2026

Build Metrics Report

Binary Sizes

Artifact Baseline Current Delta
CLI (ARM64) 30.84 MB 30.84 MB ✅ 0.0 KB (0.00%)
CLI (x64) 31.20 MB 31.20 MB ✅ 0.0 KB (0.00%)
MSIX (ARM64) 13.00 MB 13.00 MB 📈 +0.2 KB (+0.00%)
MSIX (x64) 13.81 MB 13.81 MB ✅ 0.0 KB (0.00%)
NPM Package 27.06 MB 27.06 MB 📈 +0.1 KB (+0.00%)
NuGet Package 27.14 MB 27.15 MB 📈 +8.9 KB (+0.03%)
VS Code Extension 19.87 MB 19.87 MB 📈 +0.2 KB (+0.00%)

Test Results

923 passed, 1 skipped out of 924 tests in 427.4s (-34.2s vs. baseline)

Test Coverage

23.2% line coverage, 38.9% branch coverage · ✅ no change vs. baseline

CLI Startup Time

44ms median (x64, winapp --version) · ✅ no change vs. baseline


Updated 2026-05-01 19:47:30 UTC · commit effd650 · workflow run

Code review fixes:

- Tighten gate (Copilot #1, L1): Drop the standalone WindowsPackageType=='MSIX'
  activation clause and capture _WinAppManifestPathUserSet *before* the manifest
  auto-detection runs. The gate now only counts an explicitly user-set
  WinAppManifestPath, never an auto-detected output-dir path. This eliminates
  a class of subtle false-positives where a stale bin/AppxManifest.xml from a
  prior build would activate the gate on rebuild.

- WinAppRunSupportInfo (L2): Print TargetPlatformIdentifier (raw),
  _WinAppEffectiveTargetPlatformIdentifier (used by gate), and
  _WinAppManifestPathUserSet so diagnostics mirror the actual gate inputs.

- Doc/gate semantics alignment (Copilot #5): docs/dotnet-run-support.md now
  says "EnableWinAppRunSupport is true (the default)" instead of "is not set
  to false", matching the actual gate condition (== 'true').

Tests (Copilot #3, #4, M4):

- Skip on non-Windows hosts via $IsWindows.
- Add Get-GateValue parameters: -ProjectDirManifestName (named manifest
  filename), -TargetPlatformIdentifier (explicit TPI override).
- Add new cases: project-dir Package.appxmanifest, project-dir
  AppxManifest.xml, explicit TargetPlatformIdentifier=windows on a non-windows
  TFM, explicit TargetPlatformIdentifier=android overriding a windows TFM.
- Add regression case: WindowsPackageType=MSIX with no manifest must be
  inactive (replaces the previous active case).
- Suite is now 18 tests, all passing.

CI integration (M3):

- scripts/build-cli.ps1 now invokes Invoke-Pester against
  src/winapp-NuGet/tests/NuGet.Tests.ps1 immediately after package-nuget.ps1
  succeeds (skipped if -SkipTests is passed). Honors $FailOnTestFailure.

Docs (M1, L3, M2):

- README: replace stale Version="0.1.10" pins with <latest> placeholder
  + link to nuget.org.
- README: new "Behavior changes" section explaining the
  DevelopmentDependency removal so direct consumers know to add
  PrivateAssets="all" if they don't want the package to flow into their
  own .nupkg output.
- README: new troubleshooting entry "dotnet run is not intercepted" pointing
  users at `dotnet msbuild -t:WinAppRunSupportInfo` and listing the five
  gate discriminators.

CI failures on PR #513 (build-and-package, electron) were pre-existing on
main (E2E_DotNetApp_PackageShouldIncludeRuntimeDependency) or flaky/unrelated
(electron C# native addon build) and are not caused by this PR. The four
.NET-using sample jobs (dotnet-app, wpf-app, cpp-app, packaging-cli) all pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Exists('$(MSBuildProjectDirectory)\Package.appxmanifest')
Or Exists('$(MSBuildProjectDirectory)\AppxManifest.xml')
Or Exists('$(MSBuildProjectDirectory)\appxmanifest.xml')
Or ('$(_WinAppManifestPathUserSet)' == 'true' And Exists('$(WinAppManifestPath)'))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In https://github.com/microsoft/winappCli/pull/513/changes#diff-b0415846b8cb2acc21a22ff180ca6cf3c704abd31b07711731d8eb42b3ad9770R52-R65

there are 6 conditional sets of WinAppManifestPath to fist check $(OutputPath) then $(MSBuildProjectDirectory). However, in the condition in _WinAppRunSupportActive there is just the latter 3.

In a MAUI app, we generate the manifest based on the platform and various msbuild props, so it only exists in the output. This condition is skipped here. If I just check Exists('$(WinAppManifestPath)') then it works. But I am not sure what else I break.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants