Skip to content

Resolve Windows metadata from .NET SDK ref pack on hosts with no registered Windows SDK#2486

Open
nmetulev wants to merge 3 commits into
microsoft:masterfrom
nmetulev:nmetulev-sdk-less-metadata-resolution
Open

Resolve Windows metadata from .NET SDK ref pack on hosts with no registered Windows SDK#2486
nmetulev wants to merge 3 commits into
microsoft:masterfrom
nmetulev:nmetulev-sdk-less-metadata-resolution

Conversation

@nmetulev

@nmetulev nmetulev commented Jul 18, 2026

Copy link
Copy Markdown
Member

Fixes #2485

Problem

On hosts with no registered Windows SDK (clean CI agents, containers, SDK-less/VS-less developer machines), C#/WinRT projection and authoring fail early with:

Could not find the Windows SDK in the registry

which cascades into downstream failures (e.g. WinUI/WMC XAML compilation).

Root cause

nuget/Microsoft.Windows.CsWinRT.targets defaults CsWinRTWindowsMetadata to a bare Windows SDK version, sourced from $(WindowsSDKVersion) then $(TargetPlatformVersion) (e.g. 10.0.19041.0). That bare version is passed to cswinrt.exe as -input <version>. In src/cswinrt/cmd_reader.h, the files() resolver treats a bare N.N.N.N version by calling get_sdk_path() -> open_sdk(), which reads HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots\KitsRoot10 from the registry and throws when no SDK is installed.

The tool is not at fault: it already accepts -input <folder>, -input <file>, and -input local. The fragility is that the targets pick a default that only the registry can resolve.

Fix

Add a gated target CsWinRTResolveWindowsMetadataFromSdkRefPack. When the bare-version default would otherwise be used and no Windows SDK is registered, it repoints CsWinRTWindowsMetadata at the \winmd folder of the Microsoft.Windows.SDK.NET.Ref projection ref pack that the .NET SDK already auto-restores for net*-windows10.0.x TFMs. Those winmds are versioned to $(TargetPlatformVersion) (more correct than local, which would yield the running OS's winmds).

The ref-pack path is resolved robustly via @(ResolvedTargetingPack) (filtered by NuGetPackageId == Microsoft.Windows.SDK.NET.Ref, then %(Path)\winmd) - not by globbing the NuGet cache. Setting the single CsWinRTWindowsMetadata property covers both the projection response file (-input $(CsWinRTWindowsMetadata)) and the authoring source generator (which reads the CsWinRTWindowsMetadata compiler-visible property).

Why item-based resolution (not a version scan)

The ref-pack folder is resolved from the actually-restored targeting pack via @(ResolvedTargetingPack), not by scanning for a pack whose version equals $(TargetPlatformVersion). Confirmed live on an SDK-less ARM64 host: the restored pack was Microsoft.Windows.SDK.NET.Ref 10.0.26100.67-preview while the project's TargetPlatformVersion was 10.0.26100.0 — they do not string-match. Because @(ResolvedTargetingPack) goes through the SDK's own targeting-pack resolution, it picks up preview/suffix-versioned packs correctly, whereas an exact-version lookup would miss them.

Gating (conservative)

The override takes effect only when all of the following hold:

  1. CsWinRTWindowsMetadata is still the bare-version default: non-empty, not one of the tool's own sentinels (local/sdk/sdk+), and does not point at an existing path (explicit user file/folder values are left untouched).
  2. No Windows SDK is registered - the KitsRoot10 registry value is empty (read via [MSBuild]::GetRegistryValueFromView(...), mirroring the 32-bit-then-default view order cswinrt.exe uses).
  3. The Microsoft.Windows.SDK.NET.Ref ref pack was resolved and its \winmd folder exists.

As a result, machines with a registered Windows SDK (Visual Studio, SDK-installed CI) keep their existing behavior byte-for-byte; only SDK-less hosts change.

Design tradeoff considered

An alternative is to always prefer the ref-pack winmds for modern TFMs (not just when the SDK is unregistered). That is arguably more consistent (build metadata always matches the restored, versioned ref pack rather than whatever SDK happens to be installed), but it changes behavior for the large population of registered-SDK/VS builds and risks subtle regressions in existing pipelines. I chose the gated approach to fix the broken scenario with zero impact on working ones; the always-prefer option can be revisited separately if desired.

Validation

Validated on a genuine SDK-less host (Windows ARM64, .NET SDK 10.0.302, KitsRoot10 registry value empty, no Windows Kits installed).

Tool level (shipped cswinrt.exe from the Microsoft.Windows.CsWinRT 2.2.0 package):

  • -input 10.0.26100.0 (bare version) -> error: Could not find the Windows SDK in the registry (exit 1) - reproduces the root cause.
  • -input <Microsoft.Windows.SDK.NET.Ref>\winmd (the folder this fix resolves) -> exit 0, projection sources generated. Confirms the tool consumes the ref-pack folder with no registered SDK.

End-to-end through the real package targets (a net8.0-windows10.0.26100.0 authoring component referencing Microsoft.Windows.CsWinRT 2.2.0, whose shipped default CsWinRTWindowsMetadata logic is identical to this PR's):

  • Baseline: dotnet build fails with error MSB3073 ... cswinrt.exe ... exited with code 1 / Could not find the Windows SDK in the registry (the targets invoke the tool with the bare version).
  • With this PR's exact new target injected (verbatim, via Directory.Build.targets): the target fires, emits no Windows SDK is registered; using Windows metadata from the .NET SDK projection ref pack: '...\microsoft.windows.sdk.net.ref\10.0.26100.57\winmd', the registry error is gone, projection runs, Build succeeded, and the authored .winmd is produced.

Isolated MSBuild logic (three gate cases): SDK-less + bare default -> rewritten to the ref-pack \winmd; simulated registered SDK (non-empty KitsRoot10) -> bare version preserved (no regression); explicit user path -> preserved unchanged. Edited targets file XML confirmed well-formed.

Not run: a full native cswinrt.exe / master-package build (the native tool is unchanged and its -input folder handling in src/cswinrt/cmd_reader.h is the same in 2.x and master, so the injected-target test on the 2.2.0 package is representative), and builds on a registered-SDK machine (the gate is specifically a no-op there - only SDK-less hosts are affected).

Related

A complementary short-term shim is being added in the winapp CLI (auto-injecting CsWinRTWindowsMetadata on SDK-less hosts). That shim is temporary - this upstream targets fix is the durable solution.

The shipped MSBuild targets default `CsWinRTWindowsMetadata` to a bare Windows
SDK version (`$(WindowsSDKVersion)`/`$(TargetPlatformVersion)`), which
`cswinrt.exe` can only resolve via the registry
(`HKLM\...\Windows Kits\Installed Roots\KitsRoot10`). On hosts with no
registered Windows SDK (clean CI agents, containers, SDK-less dev machines)
that lookup throws "Could not find the Windows SDK in the registry", cascading
into downstream (e.g. XAML) build failures.

Add `CsWinRTResolveWindowsMetadataFromSdkRefPack`, which - only when no Windows
SDK is registered and the value is still the bare-version default - repoints
`CsWinRTWindowsMetadata` at the `\winmd` folder of the
`Microsoft.Windows.SDK.NET.Ref` projection ref pack the .NET SDK already
restores for `net*-windows10.0.x` TFMs (resolved robustly via
`@(ResolvedTargetingPack)`, versioned to `$(TargetPlatformVersion)`). Registered
-SDK builds (Visual Studio, SDK-installed CI) and explicit `CsWinRTWindowsMetadata`
values are unaffected.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4f8b6648-c42b-4fca-98a7-2025142b37bd
Comment thread nuget/Microsoft.Windows.CsWinRT.targets Outdated
Document (in the targets comment) that @(ResolvedTargetingPack) resolves the
actually-restored Microsoft.Windows.SDK.NET.Ref pack via the SDK's own
targeting-pack resolution, so it tolerates ref-pack versions that do not
string-match $(TargetPlatformVersion) (e.g. a 10.0.26100.67-preview pack for a
project targeting 10.0.26100.0) - which an exact-version lookup would miss.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4f8b6648-c42b-4fca-98a7-2025142b37bd
@manodasanW

Copy link
Copy Markdown
Member

Thanks for the change, kicked off CI.

Co-authored-by: Manodasan Wignarajah <manodasan@hotmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Projection/authoring fails on hosts with no registered Windows SDK ("Could not find the Windows SDK in the registry")

2 participants