build: discover VS 2026 Insiders and exclude BuildTools in vswhere lookup (#47462)

## Summary of the Pull Request

Fixes `tools\build\build-common.ps1` so a clean
`tools\build\build-essentials.cmd` (or `build.ps1`) run discovers and
uses **Visual Studio 2026 Insiders** without any manual
`Enter-VsDevShell` preamble. Today the script lands on the first VS 2022
BuildTools instance it finds (which lacks the C++ workload) and every
native `.vcxproj` errors with `MSB4086: $(PlatformToolsetVersion)
evaluates to ""` during NuGet restore.

Two scoped commits, **only `tools\build\build-common.ps1` is touched**
(+58 / −36 net).

## PR Checklist

- [ ] Closes: #xxx
- [x] **Communication:** I''ve discussed this with core contributors
already. If the work hasn''t been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places (n/a — build-script
change only)
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

## Detailed Description of the Pull Request / Additional comments

### What''s broken

`build-common.ps1`''s `vswhere` lookup runs **without `-prerelease`**,
so VS 2026 Insiders / Preview installs are invisible to it. The explicit
fallback path list also only contains VS 2022 entries. On a machine that
has VS 2022 BuildTools (typical for CI hosts and many dev boxes) but
only has VS 2026 in *Insiders* form, the script:

1. Picks `C:\Program Files (x86)\Microsoft Visual
Studio\2022\BuildTools` because it''s a candidate `vswhere` returned and
it''s in the fallback list.
2. Enters its DevShell — but BuildTools has no C++ workload installed,
so `$(VCToolsInstallDir)` and `$(PlatformToolsetVersion)` are unset.
3. NuGet restore over `PowerToys.slnx` fans out to every `.vcxproj` and
they each hit `Microsoft.CodeAnalysis.targets(401,15): error MSB4086: A
numeric comparison was attempted on "$(PlatformToolsetVersion)" that
evaluates to "" instead of a number, in condition
"''$(PlatformToolsetVersion)''<''120''".` The build dies before any
project compiles.

This was reproduced today against `origin/main` — confirming this is a
pre-existing breakage independent of any in-flight feature work.

### What this PR changes

Commit **`30acf72c` — Fix build scripts to discover VS 2026 / Insiders
installations**
- Adds `-prerelease` to `vswhere` calls, tried **before** the stable
lookup so prerelease VS is preferred when it''s the only one with a
working C++ workload.
- Adds VS 2026 year-name and internal-version (`18\Insiders`) paths to
the explicit fallback list.
- Keeps VS 2022 paths as the final fallback so existing setups keep
working.
- Priority order: `prerelease+VC tools` → `prerelease` → `stable+VC
tools` → `stable`.

Commit **`18b27209` — build: simplify VS environment initialization with
VS2022/VS2026 support**
- Adds `-prerelease` to `vswhere` (consolidates with the above; the
prerelease query subsumes the stable one now).
- Restricts the SKU query to `Community` / `Professional` / `Enterprise`
(explicitly excludes `BuildTools`) so the script can no longer
accidentally pick a SKU without the C++ workload.
- Reduces vswhere from **4 calls to 2**.
- Removes the destructive BuildTools env-var cleanup block (no longer
needed once vswhere refuses to return BuildTools in the first place).
- Adds `VsDevCmd.bat` exit-code validation so a partial DevShell init
fails loudly instead of silently producing a half-initialized
environment.
- Adds VS 2022 / VS 2026 Preview entries to the explicit fallback list.

### Why two commits instead of a squash

The first commit is the minimum-viable fix that addresses the reported
MSB4086 failure. The second commit is a follow-up cleanup that''s only
safe **once** prerelease is preferred and BuildTools is excluded.
Splitting them keeps each commit independently revert-able if a
regression shows up on a specific dev environment shape.

## Validation Steps Performed

| Step | Result |
|---|---|
| Reproduce on `main`: cold `tools\build\build-essentials.cmd` in a
non-DevShell PowerShell | **MSB4086** on
`Microsoft.CommandPalette.Extensions.vcxproj`,
`Microsoft.Terminal.UI.vcxproj`, `PowerToys.MeasureToolCore.vcxproj`,
`PowerRenameUI.vcxproj` — confirmed broken |
| With this branch checked out: same cold `build-essentials.cmd`
invocation | `[VS] vswhere found: ... C:\Program Files (x86)\Microsoft
Visual Studio\Installer\vswhere.exe` → `[VS] Checking candidate:
C:\Program Files\Microsoft Visual Studio\18\Insiders` → `[VS] Entered
Visual Studio DevShell at C:\Program Files\Microsoft Visual
Studio\18\Insiders` — VS 2026 Insiders selected directly, restore step
proceeds past the MSB4086 wall |
| Manual workaround pre-init via `Import-Module
Microsoft.VisualStudio.DevShell.dll` + `Enter-VsDevShell` | Now
**unnecessary** — the script self-initializes correctly |

After this fix, the next failure mode the build hits is unrelated NuGet
feed coverage for in-flight WinAppSDK upgrade work, which is the gated
dependency this PR was extracted from to keep this change atomic.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Gordon Lam
2026-04-30 14:55:43 +08:00
committed by GitHub
parent 05cd66c9bc
commit 1744bdecd8

View File

@@ -176,11 +176,22 @@ function Get-DefaultPlatform {
return 'x64'
}
function Test-VsHasNativeTools {
# Returns $true when the current process environment was initialized with a
# usable native (C++) toolchain. Any VS instance (full SKU or Build Tools)
# without the C++ workload leaves these unset, which is the original
# MSB4086 ($(PlatformToolsetVersion) empty) failure mode we are guarding
# against.
if (-not $env:VCToolsInstallDir) { return $false }
if (-not (Test-Path $env:VCToolsInstallDir)) { return $false }
return $true
}
function Ensure-VsDevEnvironment {
$OriginalLocationForVsInit = Get-Location
try {
if ($env:VSINSTALLDIR -or $env:VCINSTALLDIR -or $env:DevEnvDir -or $env:VCToolsInstallDir) {
if ($env:VSINSTALLDIR -or $env:VCINSTALLDIR) {
Write-Host "[VS] VS developer environment already present"
return $true
}
@@ -193,22 +204,44 @@ function Ensure-VsDevEnvironment {
$vswhere = $vswhereCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
if ($vswhere) { Write-Host "[VS] vswhere found: $vswhere" } else { Write-Host "[VS] vswhere not found" }
# Probe for a Visual Studio install with the C++ workload. Selection is
# capability-based (-requires VC.Tools.x86.x64), not SKU-based: full VS
# SKUs and Build Tools are equally valid as long as the C++ workload is
# installed. -prerelease lets vswhere see Preview/Insiders alongside GA.
$vsProducts = @('Microsoft.VisualStudio.Product.Community',
'Microsoft.VisualStudio.Product.Professional',
'Microsoft.VisualStudio.Product.Enterprise',
'Microsoft.VisualStudio.Product.BuildTools')
$instPaths = @()
if ($vswhere) {
# First try with the VC tools requirement (preferred)
try { $p = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2>$null; if ($p) { $instPaths += $p } } catch {}
# Fallback: try without -requires to find any VS installations
# Newest VS instance with the C++ VC tools workload (stable + prerelease).
try { $p = & $vswhere -latest -prerelease -products $vsProducts -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2>$null; if ($p) { $instPaths += $p } } catch {}
# Last-resort fallback if no instance reports the VC workload (still
# validated below by Test-VsHasNativeTools to skip unusable installs).
if (-not $instPaths) {
try { $p2 = & $vswhere -latest -products * -property installationPath 2>$null; if ($p2) { $instPaths += $p2 } } catch {}
try { $p2 = & $vswhere -latest -prerelease -products $vsProducts -property installationPath 2>$null; if ($p2) { $instPaths += $p2 } } catch {}
}
}
# Add explicit common year-based candidates as a last resort
# Add explicit common candidates as a last resort (newest first)
if (-not $instPaths) {
$explicit = @(
"$env:ProgramFiles\Microsoft Visual Studio\2026\Insiders",
"$env:ProgramFiles\Microsoft Visual Studio\2026\Preview",
"$env:ProgramFiles\Microsoft Visual Studio\2026\Community",
"$env:ProgramFiles\Microsoft Visual Studio\2026\Professional",
"$env:ProgramFiles\Microsoft Visual Studio\2026\Enterprise",
"$env:ProgramFiles\Microsoft Visual Studio\18\Insiders",
"$env:ProgramFiles\Microsoft Visual Studio\18\Preview",
"$env:ProgramFiles\Microsoft Visual Studio\18\Community",
"$env:ProgramFiles\Microsoft Visual Studio\18\Professional",
"$env:ProgramFiles\Microsoft Visual Studio\18\Enterprise",
"$env:ProgramFiles (x86)\Microsoft Visual Studio\2022\Preview",
"$env:ProgramFiles (x86)\Microsoft Visual Studio\2022\Community",
"$env:ProgramFiles (x86)\Microsoft Visual Studio\2022\Professional",
"$env:ProgramFiles (x86)\Microsoft Visual Studio\2022\Enterprise",
"$env:ProgramFiles\Microsoft Visual Studio\2022\Preview",
"$env:ProgramFiles\Microsoft Visual Studio\2022\Community",
"$env:ProgramFiles\Microsoft Visual Studio\2022\Professional",
"$env:ProgramFiles\Microsoft Visual Studio\2022\Enterprise"
@@ -221,7 +254,8 @@ function Ensure-VsDevEnvironment {
return $false
}
# Try each candidate installation path until one works
# Try each candidate installation path until one yields a working native
# toolchain (validated post-init via Test-VsHasNativeTools).
foreach ($inst in $instPaths) {
if (-not $inst) { continue }
Write-Host "[VS] Checking candidate: $inst"
@@ -234,6 +268,10 @@ function Ensure-VsDevEnvironment {
# Call Enter-VsDevShell using only the install path to avoid parameter name differences
try {
Enter-VsDevShell -VsInstallPath $inst -ErrorAction Stop
if (-not (Test-VsHasNativeTools)) {
Write-Warning "[VS] DevShell entered $inst but VC tools (VCToolsInstallDir) not present - skipping"
continue
}
Write-Host "[VS] Entered Visual Studio DevShell at $inst"
return $true
} catch {
@@ -249,12 +287,20 @@ function Ensure-VsDevEnvironment {
Write-Host "[VS] Running VsDevCmd.bat and importing environment from $vsDevCmd"
try {
$cmdOut = cmd.exe /c "`"$vsDevCmd`" && set"
if ($LASTEXITCODE -ne 0) {
Write-Warning "[VS] VsDevCmd.bat exited with code $LASTEXITCODE at $inst"
continue
}
foreach ($line in $cmdOut) {
$parts = $line -split('=',2)
if ($parts.Length -eq 2) {
try { [Environment]::SetEnvironmentVariable($parts[0], $parts[1], 'Process') } catch {}
}
}
if (-not (Test-VsHasNativeTools)) {
Write-Warning "[VS] VsDevCmd.bat imported $inst but VC tools (VCToolsInstallDir) not present - skipping"
continue
}
Write-Host "[VS] Imported environment from VsDevCmd.bat at $inst"
return $true
} catch {