]> vilimpoc.org git repositories - dotfiles/blobdiff - setup-windows-with-uac.ps1
dotfiles: stop the ssh.exe check from deleting the ssh.exe it verifies
[dotfiles] / setup-windows-with-uac.ps1
index c935e2d7c58eb911d35bdb10770b28acb7a96581..4b7ea948dccce4a7c6b51b0b256499c10fd77716 100644 (file)
     - OpenSSH Server (sshd) capability: automatic + started + inbound TCP 22\r
     - rsync for Windows (nuket/rsync-windows) in C:\Tools\rsync, on the machine\r
       PATH: rsync.exe plus the ssh.exe it runs, out of the release zip for this\r
     - OpenSSH Server (sshd) capability: automatic + started + inbound TCP 22\r
     - rsync for Windows (nuket/rsync-windows) in C:\Tools\rsync, on the machine\r
       PATH: rsync.exe plus the ssh.exe it runs, out of the release zip for this\r
-      architecture (there is no ARM64 zip; ARM64 gets the x64 one, and the\r
-      ssh.exe is kept only if it actually starts)\r
+      architecture - x86, x64 and arm64 are all published, so both binaries are\r
+      native everywhere. The ssh.exe is kept only if it actually starts.\r
     - Visual Studio Community (C++ desktop workload, x86/x64 AND ARM64 build\r
     - Visual Studio Community (C++ desktop workload, x86/x64 AND ARM64 build\r
-      tools, Spectre libs, WDK VSIX, Win11 SDK 26100, Clang/LLVM, and - on x64\r
-      only - the v141 + Windows XP targeting toolset). VS 2022 on x64, VS 2026\r
-      on ARM64; see $VsChannel.\r
-    - Windows Driver Kit 10.0.26100\r
+      tools, Spectre libs, Win11 SDK 26100, and - on x64 only - the WDK VSIX,\r
+      Clang/LLVM, and the v141 + Windows XP targeting toolset). VS 2022 on x64,\r
+      VS 2026 on ARM64; see $VsChannel.\r
+    - Windows Driver Kit 10.0.26100 (x64 only)\r
     - Windows Performance Toolkit - xperf, wpr and Windows Performance Analyzer\r
       (wpa.exe) - on the machine PATH\r
 \r
     - Windows Performance Toolkit - xperf, wpr and Windows Performance Analyzer\r
       (wpa.exe) - on the machine PATH\r
 \r
   18 (VS 2026) on ARM64, where that toolset cannot exist anyway.\r
 \r
   ARCHITECTURE. Runs on x64 and on ARM64 (Windows 11 on Arm). On ARM64 the\r
   18 (VS 2026) on ARM64, where that toolset cannot exist anyway.\r
 \r
   ARCHITECTURE. Runs on x64 and on ARM64 (Windows 11 on Arm). On ARM64 the\r
-  Visual Studio installer, MSVC and clang-cl are all native ARM64 and\r
-  cross-compile ARM64/x64/x86 targets; what changes is called out at each step,\r
-  and $HostArch below is what drives it.\r
+  Visual Studio installer and MSVC are native ARM64 and cross-compile\r
+  ARM64/x64/x86 targets.\r
+\r
+  The two architectures deliberately provision DIFFERENT things. x64 is the full\r
+  workstation; ARM64 is a constrained VM set up as a single-compiler build box -\r
+  MSVC only, with clang-cl and the WDK dropped there. Neither is dropped for lack\r
+  of an ARM64 build; see the comments on $ClangComponents and the WDK step.\r
+  What changes is called out at each step, and $HostArch below is what drives it.\r
 #>\r
 \r
 $ErrorActionPreference = 'Stop'\r
 #>\r
 \r
 $ErrorActionPreference = 'Stop'\r
@@ -37,22 +42,30 @@ $ErrorActionPreference = 'Stop'
 # ---------------------------------------------------------------------------\r
 # Host architecture\r
 #\r
 # ---------------------------------------------------------------------------\r
 # Host architecture\r
 #\r
-# RuntimeInformation.OSArchitecture rather than PROCESSOR_ARCHITECTURE: this\r
-# script can be launched by a 32-bit or an emulated x64 PowerShell, either of\r
-# which reports the emulated architecture in the environment variable while this\r
-# API still reports the real one. Values seen here: X64, Arm64, X86.\r
+# Read from the machine environment in the registry, because this script can be\r
+# launched by a 32-bit or an emulated x64 PowerShell and BOTH of the obvious\r
+# sources report the emulated architecture in that case. Measured on this box,\r
+# from the x64 PowerShell an x64 shell resolves to:\r
+#     [RuntimeInformation]::OSArchitecture      X64     <- wrong\r
+#     $env:PROCESSOR_ARCHITECTURE               AMD64   <- wrong\r
+#     HKLM\...\Session Manager\Environment      ARM64   <- right\r
+# OSArchitecture is documented as the OS's architecture, and on .NET Core it is;\r
+# under .NET Framework on Prism it is not, so it is kept only as a fallback.\r
 #\r
 # $IsArm64 gates:\r
 #\r
 # $IsArm64 gates:\r
-#   - which rsync-windows release zip is fetched (there is no ARM64 one)\r
+#   - which rsync-windows release zip is fetched (x86 / x64 / arm64, all native)\r
 #   - which Visual Studio generation is driven ($VsChannel: 18 on Arm, 17 on x64)\r
 #   - which Visual Studio generation is driven ($VsChannel: 18 on Arm, 17 on x64)\r
-#   - the Visual Studio component groups (ARM64 build tools in; the v141/XP\r
-#     toolset out, because it has no ARM64-hosted compiler and Microsoft does not\r
-#     ship Windows XP targeting for Arm hosts. Clang/LLVM is installed on every\r
-#     architecture and is native on Arm, so it is NOT gated here)\r
+#   - the Visual Studio component groups: ARM64 build tools in; the v141/XP\r
+#     toolset out because it has no ARM64-hosted compiler and Microsoft does not\r
+#     ship Windows XP targeting for Arm hosts; Clang/LLVM and the WDK VSIX out by\r
+#     policy rather than by limitation - both are available and native on Arm\r
+#   - whether the Windows Driver Kit is installed at all\r
 #   - what the VTune and WPT steps report\r
 # ---------------------------------------------------------------------------\r
 #   - what the VTune and WPT steps report\r
 # ---------------------------------------------------------------------------\r
-$HostArch = [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()\r
-$IsArm64  = ($HostArch -eq 'Arm64')\r
+$rawArch  = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -ErrorAction SilentlyContinue).PROCESSOR_ARCHITECTURE\r
+if (-not $rawArch) { $rawArch = [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() }\r
+$IsArm64  = ($rawArch -eq 'ARM64')\r
+$HostArch = if ($IsArm64) { 'Arm64' } elseif ($rawArch -eq 'AMD64') { 'X64' } else { $rawArch }\r
 \r
 function Write-Step([string]$Msg) {\r
     Write-Host "`n==> $Msg" -ForegroundColor Cyan\r
 \r
 function Write-Step([string]$Msg) {\r
     Write-Host "`n==> $Msg" -ForegroundColor Cyan\r
@@ -308,20 +321,23 @@ try {
 # ---------------------------------------------------------------------------\r
 Write-Step 'rsync for Windows'\r
 $RsyncRepo  = 'nuket/rsync-windows'\r
 # ---------------------------------------------------------------------------\r
 Write-Step 'rsync for Windows'\r
 $RsyncRepo  = 'nuket/rsync-windows'\r
-# The release publishes exactly two assets - x64 and x86 - and no ARM64 one, so\r
-# on ARM64 the x64 build is the right pick: it runs under Prism, and an emulated\r
-# x64 rsync still moves data far faster than the ~17MB/s stdin cap that the whole\r
-# reason for using this build is to avoid. (The transfer is I/O-bound on the\r
-# socket, not on emulated CPU.) Selected off $HostArch rather than\r
-# Is64BitOperatingSystem, which answers "true" on ARM64 and so cannot tell the\r
-# two 64-bit cases apart.\r
+# The release publishes x86, x64 and - since v3.5.0-gdeeda96f - arm64, so every\r
+# architecture this script runs on gets a native build. ARM64 used to take the\r
+# x64 zip under emulation, which was defensible (a transfer is bound by the\r
+# socket, not by emulated CPU) but cost the release's ssh.exe: that x64 binary\r
+# could not load the ARM64 libcrypto.dll in System32 and had to be deleted on\r
+# every run. The arm64 asset ships an ARM64 rsync.exe AND an ARM64 ssh.exe, so\r
+# both halves are now native and the fast client survives.\r
+#\r
+# Selected off $HostArch rather than Is64BitOperatingSystem, which answers "true"\r
+# on ARM64 and so cannot tell the two 64-bit cases apart.\r
 $RsyncAsset = switch ($HostArch) {\r
     'X64'   { 'rsync-windows-x64.zip' }\r
 $RsyncAsset = switch ($HostArch) {\r
     'X64'   { 'rsync-windows-x64.zip' }\r
-    'Arm64' { 'rsync-windows-x64.zip' }\r
+    'Arm64' { 'rsync-windows-arm64.zip' }\r
     default { 'rsync-windows-x86.zip' }\r
 }\r
 if ($IsArm64) {\r
     default { 'rsync-windows-x86.zip' }\r
 }\r
 if ($IsArm64) {\r
-    Write-Host '    ARM64: no native asset is published, using the x64 build under emulation.' -ForegroundColor Yellow\r
+    Write-Host '    ARM64: using the native arm64 asset (rsync.exe and ssh.exe both ARM64).' -ForegroundColor Green\r
 }\r
 # The /releases/latest/download/ redirect rather than the API: unauthenticated\r
 # API calls are rate-limited to 60/hour per IP, which a provisioning run behind a\r
 }\r
 # The /releases/latest/download/ redirect rather than the API: unauthenticated\r
 # API calls are rate-limited to 60/hour per IP, which a provisioning run behind a\r
@@ -347,15 +363,11 @@ try {
         if ($v -and ([version]($v -replace '[^0-9.]', '')) -lt [version]'3.8.2') {\r
             Write-Warning "$SysCrypto is LibreSSL $v; the release's ssh.exe is built against 3.8.2 (Windows OpenSSH Client 9.5). Update Windows, or expect ssh.exe not to start."\r
         }\r
         if ($v -and ([version]($v -replace '[^0-9.]', '')) -lt [version]'3.8.2') {\r
             Write-Warning "$SysCrypto is LibreSSL $v; the release's ssh.exe is built against 3.8.2 (Windows OpenSSH Client 9.5). Update Windows, or expect ssh.exe not to start."\r
         }\r
-        if ($IsArm64) {\r
-            # On ARM64 the presence of libcrypto.dll proves less than it does on\r
-            # x64: System32 holds the ARM64 build of it, and the ssh.exe in the\r
-            # zip is x64. Whether an emulated x64 process can load that DLL comes\r
-            # down to whether it is a plain ARM64 binary or an ARM64X one - not\r
-            # something worth deciding by parsing the PE header, when running the\r
-            # binary answers it outright. Unpack it, then run it (below).\r
-            Write-Host '    ARM64: the x64 ssh.exe will be verified by running it, not by assuming.' -ForegroundColor Yellow\r
-        }\r
+        # No architecture special-case here any more: the asset chosen above\r
+        # matches the host, so the release's ssh.exe and System32's libcrypto.dll\r
+        # are the same architecture on every box. The run-it check below still\r
+        # happens on all of them - it is cheap, and it is what caught the ARM64\r
+        # mismatch back when this took the x64 zip.\r
     }\r
 \r
     # Download and unpack beside the targets, not over them, so an interrupted\r
     }\r
 \r
     # Download and unpack beside the targets, not over them, so an interrupted\r
@@ -409,11 +421,14 @@ try {
     # This matters more than it looks. rsync.exe prefers an ssh.exe sitting in its\r
     # own directory, so a present-but-unstartable one does not degrade to the\r
     # in-box client - it breaks rsync outright, and the error you get is a remote\r
     # This matters more than it looks. rsync.exe prefers an ssh.exe sitting in its\r
     # own directory, so a present-but-unstartable one does not degrade to the\r
     # in-box client - it breaks rsync outright, and the error you get is a remote\r
-    # shell that died rather than anything naming ssh.exe. The two ways to land\r
-    # there are a missing/old System32 libcrypto.dll (x64 boxes) and an ARM64 box\r
-    # whose ARM64 libcrypto cannot be loaded by this x64 binary. Removing it is\r
-    # the repair in both cases: rsync then falls back to the ssh on the PATH,\r
-    # which on ARM64 is the native in-box client.\r
+    # shell that died rather than anything naming ssh.exe. The way to land there\r
+    # is a missing or too-old System32 libcrypto.dll. Removing it is the repair:\r
+    # rsync then falls back to the ssh on the PATH, which is the in-box client.\r
+    #\r
+    # This also used to fire on every ARM64 run, when the x64 asset was the only\r
+    # 64-bit one published and its ssh.exe could not load the ARM64 libcrypto\r
+    # (exit 0xC0000135, STATUS_DLL_NOT_FOUND). The arm64 asset fixed that at the\r
+    # source; the check stays because it is how that was found in the first place.\r
     #\r
     # EAP back to Continue for the call: ssh -V writes its version to STDERR, and\r
     # under $ErrorActionPreference = 'Stop' a native command's stderr becomes a\r
     #\r
     # EAP back to Continue for the call: ssh -V writes its version to STDERR, and\r
     # under $ErrorActionPreference = 'Stop' a native command's stderr becomes a\r
@@ -421,13 +436,26 @@ try {
     # $global:LASTEXITCODE is cleared first because an exe that cannot start at\r
     # all throws without setting one, and the stale 0 from the previous native\r
     # command would otherwise read as success.\r
     # $global:LASTEXITCODE is cleared first because an exe that cannot start at\r
     # all throws without setting one, and the stale 0 from the previous native\r
     # command would otherwise read as success.\r
+    #\r
+    # DRAIN THE OUTPUT BEFORE READING $LASTEXITCODE. `... | Select-Object -First 1`\r
+    # was the original spelling here, and it is a trap: Select-Object stops the\r
+    # pipeline the instant it has its one object, and that is when ssh.exe has\r
+    # WRITTEN its banner - not when it has exited. PowerShell then tears the\r
+    # pipeline down, kills the still-running process, and reports -1. Measured on a\r
+    # healthy binary on an x64 box: 24 of 25 runs reported exit -1 through\r
+    # Select-Object, and 25 of 25 reported 0 when the output was collected first.\r
+    #\r
+    # So this check was deleting a perfectly good ssh.exe on almost every run, on\r
+    # every architecture, and the resulting fallback to the in-box client - the\r
+    # ~17MB/s one this whole step exists to avoid - looked like a property of the\r
+    # release rather than a bug in the test. Collect, THEN take the first line.\r
     $SshExe = Join-Path $RsyncDir 'ssh.exe'\r
     if ($WantSsh -and (Test-Path $SshExe)) {\r
         $prevEap = $ErrorActionPreference\r
         $ErrorActionPreference = 'Continue'\r
         $global:LASTEXITCODE = $null\r
         $sshVer = $null\r
     $SshExe = Join-Path $RsyncDir 'ssh.exe'\r
     if ($WantSsh -and (Test-Path $SshExe)) {\r
         $prevEap = $ErrorActionPreference\r
         $ErrorActionPreference = 'Continue'\r
         $global:LASTEXITCODE = $null\r
         $sshVer = $null\r
-        try { $sshVer = (& $SshExe -V 2>&1 | Select-Object -First 1) } catch { }\r
+        try { $sshOut = (& $SshExe -V 2>&1); $sshVer = $sshOut | Select-Object -First 1 } catch { }\r
         finally { $ErrorActionPreference = $prevEap }\r
         if ($LASTEXITCODE -eq 0) {\r
             Write-Host "    ssh.exe runs: $sshVer"\r
         finally { $ErrorActionPreference = $prevEap }\r
         if ($LASTEXITCODE -eq 0) {\r
             Write-Host "    ssh.exe runs: $sshVer"\r
@@ -501,29 +529,39 @@ $BaseComponents = @(
 \r
     # Windows 11 SDK — build number must match the WDK below\r
     'Microsoft.VisualStudio.Component.Windows11SDK.26100'\r
 \r
     # Windows 11 SDK — build number must match the WDK below\r
     'Microsoft.VisualStudio.Component.Windows11SDK.26100'\r
-\r
-    # WDK Visual Studio extension (VSIX). The silent wdksetup.exe /quiet does NOT\r
-    # install this (it only prompts interactively), so it must be added here.\r
-    'Component.Microsoft.Windows.DriverKit'\r
 )\r
 \r
 )\r
 \r
+# WDK Visual Studio extension (VSIX). The silent wdksetup.exe /quiet does NOT\r
+# install this (it only prompts interactively), so it has to be asked for here.\r
+#\r
+# Follows the WDK itself: x64 only. On ARM64 the kit is not installed (see the\r
+# WDK step for why), and this VSIX on its own is just the driver project\r
+# templates and property pages with no headers, libs or tools behind them.\r
+if (-not $IsArm64) {\r
+    $BaseComponents += 'Component.Microsoft.Windows.DriverKit'\r
+}\r
+\r
 # Clang/LLVM toolset (ClangCL, used in CMakePresets.json). Two parts: the Clang\r
 # compiler itself, plus the MSBuild integration providing the "ClangCL" toolset.\r
 #\r
 # Clang/LLVM toolset (ClangCL, used in CMakePresets.json). Two parts: the Clang\r
 # compiler itself, plus the MSBuild integration providing the "ClangCL" toolset.\r
 #\r
-# Installed on every architecture, ARM64 included, and native there - not an\r
-# emulated x64 compiler. Two things establish that: the VSIX is\r
+# x64 ONLY, and NOT because ARM64 cannot have it - it can, natively. The VSIX is\r
 # productArch=neutral with no chip/machineArch restriction, so the Arm installer\r
 # productArch=neutral with no chip/machineArch restriction, so the Arm installer\r
-# offers it; and MSVC's VC\Tools\Llvm tree is partitioned by HOST architecture\r
-# (bin = x86, x64\bin = x64, ARM64\bin = ARM64) with genuine ARM64 binaries\r
-# already in ARM64\bin, which is where clang-cl.exe lands. Unlike the v141/XP\r
-# group below, nothing technical is in the way.\r
+# offers it, and MSVC's VC\Tools\Llvm tree is partitioned by HOST architecture\r
+# (bin = x86, x64\bin = x64, ARM64\bin = ARM64) with genuine ARM64 binaries in\r
+# ARM64\bin, which is where clang-cl.exe lands. It was installed and working here\r
+# before this split.\r
 #\r
 #\r
-# This is the compiler diversity the box is after: MSVC and clang-cl over the\r
-# same sources, both native.\r
-$ClangComponents = @(\r
-    'Microsoft.VisualStudio.Component.VC.Llvm.Clang'\r
-    'Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset'\r
-)\r
+# It is dropped on ARM64 because that machine is a constrained VM provisioned as\r
+# a single-compiler build box: MSVC and nothing else. Compiler diversity - MSVC,\r
+# VS clang-cl and upstream LLVM over the same sources - now lives entirely on the\r
+# x64 machine, which is the one with room for three toolchains. Empty here means\r
+# Invoke-VsModify is never called for this group.\r
+$ClangComponents = if ($IsArm64) { @() } else {\r
+    @(\r
+        'Microsoft.VisualStudio.Component.VC.Llvm.Clang'\r
+        'Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset'\r
+    )\r
+}\r
 \r
 # Windows XP targeting (v141_xp toolset, used in CMakePresets.json). The v141\r
 # (VS2017) build tools provide the 14.16 compiler that the XP toolset wraps;\r
 \r
 # Windows XP targeting (v141_xp toolset, used in CMakePresets.json). The v141\r
 # (VS2017) build tools provide the 14.16 compiler that the XP toolset wraps;\r
@@ -619,7 +657,14 @@ Write-Host "    OK: bootstrapper is a PE ($([math]::Round((Get-Item $VsBootstrap
 # offered a hard failure here would cost you the whole toolchain over a component\r
 # you can add later from the installer UI.\r
 Invoke-VsModify -Label 'base toolset + workload' -Ids $BaseComponents\r
 # offered a hard failure here would cost you the whole toolchain over a component\r
 # you can add later from the installer UI.\r
 Invoke-VsModify -Label 'base toolset + workload' -Ids $BaseComponents\r
-Invoke-VsModify -Label 'Clang / LLVM'            -Ids $ClangComponents -Optional\r
+if ($IsArm64) {\r
+    Write-Step 'Visual Studio: Clang / LLVM'\r
+    Write-Host '    Skipped on ARM64 by choice, not by limitation: a native ARM64 clang-cl is' -ForegroundColor Yellow\r
+    Write-Host '    offered here and works. This box is provisioned with MSVC alone; the' -ForegroundColor Yellow\r
+    Write-Host '    multi-compiler builds belong on the x64 machine.' -ForegroundColor Yellow\r
+} else {\r
+    Invoke-VsModify -Label 'Clang / LLVM' -Ids $ClangComponents -Optional\r
+}\r
 if ($IsArm64) {\r
     Write-Step 'Visual Studio: Windows XP (v141 + WinXP)'\r
     Write-Host '    Skipped on ARM64: the v141 (14.16) toolset ships x86/x64-hosted compilers only,' -ForegroundColor Yellow\r
 if ($IsArm64) {\r
     Write-Step 'Visual Studio: Windows XP (v141 + WinXP)'\r
     Write-Host '    Skipped on ARM64: the v141 (14.16) toolset ships x86/x64-hosted compilers only,' -ForegroundColor Yellow\r
@@ -680,13 +725,21 @@ if (-not $InstallPath) {
     $clangArm = Test-Path (Join-Path $llvmRoot 'ARM64\bin\clang-cl.exe')\r
     $clangX64 = Test-Path (Join-Path $llvmRoot 'x64\bin\clang-cl.exe')\r
     $clangX86 = Test-Path (Join-Path $llvmRoot 'bin\clang-cl.exe')\r
     $clangArm = Test-Path (Join-Path $llvmRoot 'ARM64\bin\clang-cl.exe')\r
     $clangX64 = Test-Path (Join-Path $llvmRoot 'x64\bin\clang-cl.exe')\r
     $clangX86 = Test-Path (Join-Path $llvmRoot 'bin\clang-cl.exe')\r
-    if ($clangArm -or $clangX64 -or $clangX86) {\r
-        Write-Host "    clang-cl: $(@(if ($clangArm) {'ARM64'}; if ($clangX64) {'x64'}; if ($clangX86) {'x86'}) -join ', ')" -ForegroundColor Green\r
-        # On ARM64 the x64 build would still run, under emulation - so say plainly\r
-        # whether the NATIVE one is the one that landed.\r
-        if ($IsArm64 -and -not $clangArm) {\r
-            Write-Warning 'No ARM64-hosted clang-cl - the x64 one would run under emulation. Re-run the Clang/LLVM pass, or add "C++ Clang tools for Windows" in the installer.'\r
+    $clangAny = $clangArm -or $clangX64 -or $clangX86\r
+    if ($IsArm64) {\r
+        # Not installed here by design, so its absence is the expected result and\r
+        # must not read as a fault. Its PRESENCE is the thing worth a line: a box\r
+        # provisioned before this split still carries it, and the component stays\r
+        # until it is explicitly removed - the installer never uninstalls what you\r
+        # simply stopped asking for.\r
+        if ($clangAny) {\r
+            Write-Host '    clang-cl: present but no longer provisioned on ARM64 - left over from an earlier run.' -ForegroundColor Yellow\r
+            Write-Host '    Remove it with: vs_installer.exe modify --installPath "<path>" --remove Microsoft.VisualStudio.Component.VC.Llvm.Clang --remove Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset --quiet' -ForegroundColor Yellow\r
+        } else {\r
+            Write-Host '    clang-cl: n/a on ARM64 (MSVC only by design; use the x64 box for ClangCL).' -ForegroundColor DarkGray\r
         }\r
         }\r
+    } elseif ($clangAny) {\r
+        Write-Host "    clang-cl: $(@(if ($clangArm) {'ARM64'}; if ($clangX64) {'x64'}; if ($clangX86) {'x86'}) -join ', ')" -ForegroundColor Green\r
     } else {\r
         Write-Warning 'clang-cl not found - the ClangCL presets will fail. Add the "C++ Clang tools for Windows" component.'\r
     }\r
     } else {\r
         Write-Warning 'clang-cl not found - the ClangCL presets will fail. Add the "C++ Clang tools for Windows" component.'\r
     }\r
@@ -715,6 +768,26 @@ if (-not $InstallPath) {
 # Build 26100 matches the Windows 11 SDK installed above.\r
 # Provides IddCx (iddcx.h / iddcx.lib) and UMDF 2.x for Indirect Display Drivers.\r
 # linkid=2335869 -> WDK 26100.6584 (per Microsoft "Other WDK Downloads").\r
 # Build 26100 matches the Windows 11 SDK installed above.\r
 # Provides IddCx (iddcx.h / iddcx.lib) and UMDF 2.x for Indirect Display Drivers.\r
 # linkid=2335869 -> WDK 26100.6584 (per Microsoft "Other WDK Downloads").\r
+#\r
+# x64 ONLY. Two reasons, and the second is the one that decided it:\r
+#\r
+#   1. The ARM64 box builds user-mode software; no driver is developed on it.\r
+#\r
+#   2. This exact WDK cannot install next to the SDK that VS 2026 brings. The\r
+#      WDK requires the MATCHING Windows SDK revision, and revisions are not\r
+#      side-by-side - Include\10.0.26100.0 is one directory whichever revision\r
+#      wrote it. VS 2026 installs SDK 26100.7705; linkid=2335869 is WDK\r
+#      26100.6584, which is the kit paired with VS 2022. So wdksetup.exe aborts:\r
+#      exit 15605, WER signature "WDK / 10.1.26100.6584 / Install / 0x80070642"\r
+#      (0x642 = 1602, ERROR_INSTALL_USEREXIT - a silent install cancelling\r
+#      itself on a failed prerequisite check). It is not an ARM64 or emulation\r
+#      problem; the same pairing fails on x64.\r
+#\r
+#      If a driver ever does need building here, the fix is the WDK that matches\r
+#      this generation - Microsoft's "Supported and other WDK downloads" table\r
+#      pairs VS 2026 with the 28000.x kit - not this link.\r
+#\r
+# On x64 with VS 2022 the SDK and this WDK are the matched pair, so it installs.\r
 # ---------------------------------------------------------------------------\r
 $WdkVersion = '10.0.26100'\r
 # Both hives. The WDK installer is a 32-bit program, so on x64 it writes under\r
 # ---------------------------------------------------------------------------\r
 $WdkVersion = '10.0.26100'\r
 # Both hives. The WDK installer is a 32-bit program, so on x64 it writes under\r
@@ -728,7 +801,13 @@ $WdkInstalledRoot = @(
 ) | ForEach-Object { (Get-ItemProperty $_ -ErrorAction SilentlyContinue).WdkBinRootVersioned } |\r
     Where-Object { $_ } | Select-Object -First 1\r
 \r
 ) | ForEach-Object { (Get-ItemProperty $_ -ErrorAction SilentlyContinue).WdkBinRootVersioned } |\r
     Where-Object { $_ } | Select-Object -First 1\r
 \r
-if ($WdkInstalledRoot -and $WdkInstalledRoot -match [regex]::Escape($WdkVersion)) {\r
+if ($IsArm64) {\r
+    Write-Step 'Windows Driver Kit'\r
+    Write-Host '    Skipped on ARM64: this box builds user-mode software only, and WDK 26100.6584' -ForegroundColor Yellow\r
+    Write-Host '    cannot install beside the 26100.7705 SDK that Visual Studio 2026 brings - the' -ForegroundColor Yellow\r
+    Write-Host '    kit needs the matching SDK revision and they are not side-by-side. See the' -ForegroundColor Yellow\r
+    Write-Host '    comment above this step for the exit code and what to use instead.' -ForegroundColor Yellow\r
+} elseif ($WdkInstalledRoot -and $WdkInstalledRoot -match [regex]::Escape($WdkVersion)) {\r
     # Re-running wdksetup.exe for an already-present version returns exit code\r
     # 2008 (maintenance mode / nothing to do), which is not a real failure.\r
     Write-Step "WDK $WdkVersion already installed - skipping ($WdkInstalledRoot)"\r
     # Re-running wdksetup.exe for an already-present version returns exit code\r
     # 2008 (maintenance mode / nothing to do), which is not a real failure.\r
     Write-Step "WDK $WdkVersion already installed - skipping ($WdkInstalledRoot)"\r