]> vilimpoc.org git repositories - dotfiles/blobdiff - setup-windows.bat
dotfiles: stop the per-user ssh.exe check killing the ssh.exe it tests
[dotfiles] / setup-windows.bat
index bd248a917a057c349fade212e17ec5ebe26abd0b..c9c7efb009eb218b96288990518de99ce89fa0b9 100644 (file)
 @rem ---------------------------------------------------------------------------\r
 @rem Host architecture\r
 @rem\r
 @rem ---------------------------------------------------------------------------\r
 @rem Host architecture\r
 @rem\r
-@rem PROCESSOR_ARCHITECTURE alone is not enough: a 32-bit cmd.exe under emulation\r
-@rem reports x86 and stashes the real one in PROCESSOR_ARCHITEW6432. Check both,\r
-@rem so this is right however the script was launched.\r
+@rem The environment variables are not enough, and the failure is silent. A 32-bit\r
+@rem cmd.exe under emulation reports x86 and stashes the real one in\r
+@rem PROCESSOR_ARCHITEW6432 - that pair is handled below - but an EMULATED x64\r
+@rem cmd.exe on an ARM64 box reports AMD64 with PROCESSOR_ARCHITEW6432 unset, and\r
+@rem nothing in the environment contradicts it. Launching this script from an x64\r
+@rem shell (Git Bash, for one) is enough to hit that, and measured here it gives:\r
+@rem     PROCESSOR_ARCHITECTURE                  AMD64\r
+@rem     HKLM\...\Session Manager\Environment    ARM64   <- the real one\r
 @rem\r
 @rem\r
-@rem The value drives two things below, and nothing else - winget resolves the\r
+@rem So take the machine-level registry value as the answer and keep the\r
+@rem environment pair only as the fallback. The registry key is per-machine and\r
+@rem not subject to per-process emulation, and it is not one of the redirected\r
+@rem hives. This is load-bearing now: %IS_ARM64% decides which packages are\r
+@rem installed at all, so reading it wrong provisions the wrong machine.\r
+@rem\r
+@rem The value drives three things below, and nothing else - winget resolves the\r
 @rem installer architecture on its own (native first, then whatever the box can\r
 @rem emulate), so the ordinary installs need no help:\r
 @rem   - VirtualBox is skipped on ARM64 (see below)\r
 @rem installer architecture on its own (native first, then whatever the box can\r
 @rem emulate), so the ordinary installs need no help:\r
 @rem   - VirtualBox is skipped on ARM64 (see below)\r
+@rem   - the x64-only package set is skipped on ARM64 (see :x64only)\r
 @rem   - the summary tells you which tools you got as emulated x64\r
 @rem ---------------------------------------------------------------------------\r
 set "HOST_ARCH=%PROCESSOR_ARCHITECTURE%"\r
 if defined PROCESSOR_ARCHITEW6432 set "HOST_ARCH=%PROCESSOR_ARCHITEW6432%"\r
 @rem   - the summary tells you which tools you got as emulated x64\r
 @rem ---------------------------------------------------------------------------\r
 set "HOST_ARCH=%PROCESSOR_ARCHITECTURE%"\r
 if defined PROCESSOR_ARCHITEW6432 set "HOST_ARCH=%PROCESSOR_ARCHITEW6432%"\r
+@rem reg.exe by full path, and NOT through a pipe to find/findstr: both of those\r
+@rem are shadowable by anything earlier on the PATH, and a POSIX `find` on the\r
+@rem PATH turns this into a silent no-op that leaves the emulated answer standing.\r
+for /f "tokens=1,2,*" %%A in ('%SystemRoot%\System32\reg.exe query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v PROCESSOR_ARCHITECTURE 2^>nul') do if /i "%%A"=="PROCESSOR_ARCHITECTURE" set "HOST_ARCH=%%C"\r
 set "IS_ARM64=0"\r
 if /i "%HOST_ARCH%"=="ARM64" set "IS_ARM64=1"\r
 echo [setup-windows] Host architecture: %HOST_ARCH%\r
 set "IS_ARM64=0"\r
 if /i "%HOST_ARCH%"=="ARM64" set "IS_ARM64=1"\r
 echo [setup-windows] Host architecture: %HOST_ARCH%\r
@@ -51,6 +67,30 @@ if "%ELEV%"=="NOLUA" (
     echo [setup-windows] at each one, and a summary before the elevated half.\r
 )\r
 \r
     echo [setup-windows] at each one, and a summary before the elevated half.\r
 )\r
 \r
+@rem ---------------------------------------------------------------------------\r
+@rem ARM64 is a lean build box, x64 is the full workstation\r
+@rem\r
+@rem The ARM64 machine here is a small VM, and everything it was installing\r
+@rem without a native build was going to run under Prism emulation anyway. So the\r
+@rem two architectures now provision different things on purpose: x64 keeps the\r
+@rem full set, ARM64 installs ONE compiler - MSVC from Visual Studio - and\r
+@rem nothing that merely duplicates it or that exists only as an x64 binary.\r
+@rem\r
+@rem Dropped on ARM64, and where each one is dropped:\r
+@rem   Brave.Brave                      here      no ARM64 build; Edge is in-box and native\r
+@rem   WinMerge.WinMerge                here      ARM64 build is machine-scope only\r
+@rem   LLVM.LLVM                        here      second Clang, dropped with VS's clang-cl\r
+@rem   NASM.NASM                        here      x86/x64 assembler; ARM64 uses MSVC's armasm64\r
+@rem   Google.AndroidGPUInspector       here      x64-only, profiles Android devices\r
+@rem   OpenCppCoverage.OpenCppCoverage  here      x86/x64 only; cannot instrument ARM64 binaries\r
+@rem   VS Clang/LLVM component          with-uac  see $ClangComponents\r
+@rem   Windows Driver Kit               with-uac  see the WDK step\r
+@rem   BinSkim                          no-uac    NuGet publishes win-x64 only\r
+@rem\r
+@rem :x64only at the bottom of this file is the one place that decision is\r
+@rem applied, so the skip and its reason land in the log together.\r
+@rem ---------------------------------------------------------------------------\r
+\r
 @rem --- Non-admin (per-user) winget installs ---\r
 @rem\r
 @rem No --architecture anywhere on purpose. winget already picks the best\r
 @rem --- Non-admin (per-user) winget installs ---\r
 @rem\r
 @rem No --architecture anywhere on purpose. winget already picks the best\r
@@ -67,7 +107,7 @@ if "%ELEV%"=="NOLUA" (
 @rem (ARM64). On ARM64, run the *64a.exe variants - plain procexp.exe is the x86\r
 @rem one and will run emulated.\r
 winget install Anthropic.ClaudeCode\r
 @rem (ARM64). On ARM64, run the *64a.exe variants - plain procexp.exe is the x86\r
 @rem one and will run emulated.\r
 winget install Anthropic.ClaudeCode\r
-winget install Brave.Brave\r
+call :x64only Brave.Brave "no ARM64 build - the emulated x64 browser is the slowest thing on the box, and Edge is in-box and native"\r
 winget install Git.Git\r
 winget install Microsoft.PowerShell Microsoft.Sysinternals.ProcessExplorer Microsoft.Sysinternals.ProcessMonitor Microsoft.Sysinternals.SDelete Microsoft.VisualStudioCode Microsoft.WindowsTerminal\r
 winget install Python.Python.3.13\r
 winget install Git.Git\r
 winget install Microsoft.PowerShell Microsoft.Sysinternals.ProcessExplorer Microsoft.Sysinternals.ProcessMonitor Microsoft.Sysinternals.SDelete Microsoft.VisualStudioCode Microsoft.WindowsTerminal\r
 winget install Python.Python.3.13\r
@@ -79,17 +119,11 @@ winget install Python.Python.3.13
 @rem unelevated, winget's scope preference beats its architecture preference and\r
 @rem you silently get the emulated x64 build.\r
 @rem\r
 @rem unelevated, winget's scope preference beats its architecture preference and\r
 @rem you silently get the emulated x64 build.\r
 @rem\r
-@rem So ask for arm64 explicitly, and fall back to the default when that cannot be\r
-@rem installed - which is exactly the standard-user case, where the machine-scope\r
-@rem installer has nowhere to go. The fallback is the x64 build under emulation:\r
-@rem a diff tool is not a hot path, and a working WinMerge beats none. The\r
-@rem architecture audit at the end of setup-windows-no-uac.ps1 reports which one\r
-@rem you ended up with.\r
-if "%IS_ARM64%"=="1" (\r
-    winget install WinMerge.WinMerge --architecture arm64 || winget install WinMerge.WinMerge\r
-) else (\r
-    winget install WinMerge.WinMerge\r
-)\r
+@rem That asymmetry is why ARM64 no longer installs it at all: the only build a\r
+@rem normal user can install there is the emulated one, and asking for --architecture\r
+@rem arm64 just fails unless the run is elevated. VS Code's diff view and\r
+@rem `git difftool` cover the same ground natively.\r
+call :x64only WinMerge.WinMerge "upstream ships ARM64 as a machine-scope installer only, so an unelevated winget can install nothing but the emulated x64 build"\r
 \r
 @rem --- Build drivers, standalone and native ---\r
 @rem\r
 \r
 @rem --- Build drivers, standalone and native ---\r
 @rem\r
@@ -117,24 +151,27 @@ if "%IS_ARM64%"=="1" (
 @rem whole machine.\r
 winget install Ninja-build.Ninja\r
 \r
 @rem whole machine.\r
 winget install Ninja-build.Ninja\r
 \r
-@rem --- Second, independent Clang: compiler diversity ---\r
+@rem --- Second, independent Clang: compiler diversity, on x64 only ---\r
 @rem\r
 @rem\r
-@rem The elevated half installs Visual Studio's Clang component (clang-cl, native\r
-@rem ARM64). This is a DIFFERENT LLVM: the upstream release, on its own schedule\r
-@rem and usually several major versions ahead of the one VS bundles, installed to\r
+@rem On x64 the elevated half installs Visual Studio's Clang component (clang-cl).\r
+@rem This is a DIFFERENT LLVM: the upstream release, on its own schedule and\r
+@rem usually several major versions ahead of the one VS bundles, installed to\r
 @rem C:\Program Files\LLVM rather than inside the VS tree. Two Clang majors plus\r
 @rem MSVC over the same sources is the point - it is what catches the bugs a\r
 @rem single toolchain agrees with itself about.\r
 @rem\r
 @rem C:\Program Files\LLVM rather than inside the VS tree. Two Clang majors plus\r
 @rem MSVC over the same sources is the point - it is what catches the bugs a\r
 @rem single toolchain agrees with itself about.\r
 @rem\r
-@rem Native on ARM64: the manifest resolves to LLVM-<ver>-woa64.exe ("Windows on\r
-@rem Arm 64"), not the x64 build.\r
+@rem NOT ON ARM64, and not for lack of a native build - the manifest does resolve\r
+@rem to LLVM-<ver>-woa64.exe there. It is dropped because the ARM64 box carries\r
+@rem MSVC alone: with VS's clang-cl component gone too, a third compiler with\r
+@rem nothing to disagree with is just disk. Do the multi-compiler build on the\r
+@rem x64 machine, which keeps all three.\r
 @rem\r
 @rem\r
-@rem PATH ORDER MATTERS if you use it. Should this installer put its bin on the\r
-@rem PATH, a bare `clang-cl` resolves to the upstream one while CMake's\r
+@rem PATH ORDER MATTERS on x64 if you use it. Should this installer put its bin on\r
+@rem the PATH, a bare `clang-cl` resolves to the upstream one while CMake's\r
 @rem `-T ClangCL` toolset keeps using the VS copy - so the two can be selected\r
 @rem independently, but only if you know which one you are asking for. Prefer\r
 @rem -DCMAKE_CXX_COMPILER with a full path when you care.\r
 @rem `-T ClangCL` toolset keeps using the VS copy - so the two can be selected\r
 @rem independently, but only if you know which one you are asking for. Prefer\r
 @rem -DCMAKE_CXX_COMPILER with a full path when you care.\r
-winget install LLVM.LLVM\r
+call :x64only LLVM.LLVM "ARM64 carries MSVC alone - VS's clang-cl is dropped there too, so a second Clang has nothing to cross-check against"\r
 \r
 @rem x64-only manifest: winget falls back to the x64 installer on ARM64 and it runs\r
 @rem under Prism emulation. Harmless for what it does here - iperf3 is a network\r
 \r
 @rem x64-only manifest: winget falls back to the x64 installer on ARM64 and it runs\r
 @rem under Prism emulation. Harmless for what it does here - iperf3 is a network\r
@@ -142,11 +179,15 @@ winget install LLVM.LLVM
 winget install ar51an.iPerf3\r
 \r
 @rem NASM assembles x86/x86-64 only - there is no ARM64 target in it and no ARM64\r
 winget install ar51an.iPerf3\r
 \r
 @rem NASM assembles x86/x86-64 only - there is no ARM64 target in it and no ARM64\r
-@rem build of it. Installed on ARM64 anyway (as emulated x64) because this box\r
-@rem still cross-compiles x86/x64 binaries, where NASM is the assembler that\r
-@rem builds them. If you only ever target ARM64, nothing here uses it: the ARM64\r
-@rem assembler is armasm64.exe, which ships with MSVC.\r
-winget install NASM.NASM\r
+@rem build of it. x64 keeps it; ARM64 does not, because an emulated assembler is\r
+@rem only worth carrying if you cross-compile x86/x64 here, and that work belongs\r
+@rem on the x64 box. The ARM64 assembler is armasm64.exe, which ships with MSVC.\r
+@rem\r
+@rem Its installer is also one of the two here that fall back to a per-user\r
+@rem directory when it cannot write to Program Files - C:\Users\<you>\AppData\\r
+@rem Local\bin\NASM, registered nowhere, so `winget uninstall` cannot find it\r
+@rem afterwards. Uninstall.exe in that directory is the only way back out.\r
+call :x64only NASM.NASM "x86/x86-64 assembler with no ARM64 target and no ARM64 build - MSVC's armasm64.exe is the native one"\r
 \r
 @rem VirtualBox has no ARM64 Windows host build, and the x64 one cannot be made to\r
 @rem work by emulation: a hypervisor is kernel-mode, and its x64 driver will not\r
 \r
 @rem VirtualBox has no ARM64 Windows host build, and the x64 one cannot be made to\r
 @rem work by emulation: a hypervisor is kernel-mode, and its x64 driver will not\r
@@ -181,11 +222,15 @@ if "%IS_ARM64%"=="1" (
 @rem NOTE none of these is a build-blocker: the .NET SDK is only here for the WiX\r
 @rem MSI tooling, CMake also ships inside Visual Studio, AGI profiles Android\r
 @rem devices, and OpenCppCoverage cannot instrument ARM64 binaries anyway.\r
 @rem NOTE none of these is a build-blocker: the .NET SDK is only here for the WiX\r
 @rem MSI tooling, CMake also ships inside Visual Studio, AGI profiles Android\r
 @rem devices, and OpenCppCoverage cannot instrument ARM64 binaries anyway.\r
+@rem\r
+@rem On ARM64 the group is just the first two - AGI and OpenCppCoverage are in the\r
+@rem x64-only set, so there they are skipped before elevation is even consulted.\r
 @rem ---------------------------------------------------------------------------\r
 if "%ELEV%"=="NOLUA" (\r
     echo.\r
     echo [setup-windows] Skipping the machine-wide installers - this account cannot elevate:\r
 @rem ---------------------------------------------------------------------------\r
 if "%ELEV%"=="NOLUA" (\r
     echo.\r
     echo [setup-windows] Skipping the machine-wide installers - this account cannot elevate:\r
-    echo [setup-windows]   Microsoft.DotNet.SDK.10, Kitware.CMake, Google.AndroidGPUInspector, OpenCppCoverage.\r
+    echo [setup-windows]   Microsoft.DotNet.SDK.10 and Kitware.CMake, plus - on x64 only -\r
+    echo [setup-windows]   Google.AndroidGPUInspector and OpenCppCoverage.\r
     echo [setup-windows]   None blocks a build. Re-run from an administrator account to get them.\r
     echo.\r
     goto :after_admin_pkgs\r
     echo [setup-windows]   None blocks a build. Re-run from an administrator account to get them.\r
     echo.\r
     goto :after_admin_pkgs\r
@@ -193,7 +238,7 @@ if "%ELEV%"=="NOLUA" (
 \r
 winget install Microsoft.DotNet.SDK.10\r
 winget install Kitware.CMake\r
 \r
 winget install Microsoft.DotNet.SDK.10\r
 winget install Kitware.CMake\r
-winget install Google.AndroidGPUInspector\r
+call :x64only Google.AndroidGPUInspector "x64-only build, and it profiles Android devices rather than anything compiled here"\r
 \r
 @rem OpenCppCoverage: native (PE) line coverage for the C++ binaries. run-coverage-occ.py drives the\r
 @rem pytest suite under it to produce an HTML report (the binaries under test build with PDBs,\r
 \r
 @rem OpenCppCoverage: native (PE) line coverage for the C++ binaries. run-coverage-occ.py drives the\r
 @rem pytest suite under it to produce an HTML report (the binaries under test build with PDBs,\r
@@ -201,13 +246,12 @@ winget install Google.AndroidGPUInspector
 @rem\r
 @rem x86/x64 only, and unlike the other emulated tools that is a real limit rather\r
 @rem than a slowdown: it collects coverage by debugging the process under test and\r
 @rem\r
 @rem x86/x64 only, and unlike the other emulated tools that is a real limit rather\r
 @rem than a slowdown: it collects coverage by debugging the process under test and\r
-@rem stepping x86/x64 instructions, so it can cover the x86/x64 binaries this box\r
-@rem cross-compiles but NOT an ARM64 one. For ARM64 coverage, build the ARM64\r
-@rem binaries with /fsanitize-coverage or use the x64 build for the coverage run.\r
-winget install OpenCppCoverage.OpenCppCoverage\r
+@rem stepping x86/x64 instructions, so it can cover x86/x64 binaries but NOT an\r
+@rem ARM64 one. That is what puts it in the x64-only set: on ARM64 it could only\r
+@rem ever cover the cross-compiled x64 build, which is a job for the x64 box.\r
+call :x64only OpenCppCoverage.OpenCppCoverage "it collects coverage by stepping x86/x64 instructions, so it cannot instrument an ARM64 binary at all"\r
 \r
 :after_admin_pkgs\r
 \r
 :after_admin_pkgs\r
-if "%IS_ARM64%"=="1" echo [setup-windows] NOTE: OpenCppCoverage is x86/x64-only - it cannot instrument ARM64 binaries. Run coverage against the x64 build.\r
 \r
 @rem --- WiX 5.0.2, pinned on purpose ---\r
 @rem WiX packages our proprietary software into MSIs, and the version is pinned to keep that\r
 \r
 @rem --- WiX 5.0.2, pinned on purpose ---\r
 @rem WiX packages our proprietary software into MSIs, and the version is pinned to keep that\r
@@ -298,8 +342,8 @@ echo [setup-windows]   so Windows offers no way to elevate - there is no prompt
 echo [setup-windows]   'Start-Process -Verb RunAs' is silently ignored and reports success, which is why\r
 echo [setup-windows]   this used to look like a cancelled prompt.\r
 echo [setup-windows]\r
 echo [setup-windows]   'Start-Process -Verb RunAs' is silently ignored and reports success, which is why\r
 echo [setup-windows]   this used to look like a cancelled prompt.\r
 echo [setup-windows]\r
-echo [setup-windows]   Not installed: Visual Studio components ^(including clang-cl^), the WDK,\r
-echo [setup-windows]   rsync, and the OpenSSH server. Everything per-user above is unaffected.\r
+echo [setup-windows]   Not installed: Visual Studio and its MSVC toolchain, rsync, and the OpenSSH\r
+echo [setup-windows]   server. Everything per-user above is unaffected.\r
 echo [setup-windows]\r
 echo [setup-windows]   To finish the box, either sign in to an administrator account and re-run this\r
 echo [setup-windows]   script, or have an administrator run setup-windows-with-uac.ps1 there. Then\r
 echo [setup-windows]\r
 echo [setup-windows]   To finish the box, either sign in to an administrator account and re-run this\r
 echo [setup-windows]   script, or have an administrator run setup-windows-with-uac.ps1 there. Then\r
@@ -374,21 +418,57 @@ if "%UAC_RC%"=="SKIPPED" echo [setup-windows] Per-user setup complete; the eleva
 @rem py -m pip install "headroom-ai[all]"\r
 @rem npm install headroom-ai\r
 \r
 @rem py -m pip install "headroom-ai[all]"\r
 @rem npm install headroom-ai\r
 \r
-@rem Pillow publishes win_arm64 wheels alongside win_amd64, so `py` - the ARM64\r
-@rem Python winget just installed - gets a native wheel and never falls back to\r
-@rem building from source. Any package WITHOUT an ARM64 wheel does fall back, and\r
-@rem that source build needs the MSVC toolchain the elevated half installs.\r
-py -m pip install Pillow\r
+@rem --- Find an interpreter by path, rather than trusting `py` ---\r
+@rem\r
+@rem `py -m pip install Pillow` used to be this line, and it failed with\r
+@rem     'py' is not recognized as an internal or external command\r
+@rem for two independent reasons, which look identical at the prompt:\r
+@rem\r
+@rem   1. PATH staleness. winget installed Python a few lines above, but this cmd\r
+@rem      session inherited its environment before that, so nothing winget added\r
+@rem      is visible here. Same problem the DOTNET_EXE lookup solves above, and it\r
+@rem      would bite even where the launcher IS installed.\r
+@rem\r
+@rem   2. The launcher may simply not be there. Measured on the ARM64 box: every\r
+@rem      other Python component registered - Core Interpreter, Executables,\r
+@rem      Standard Library, pip Bootstrap, Tcl/Tk, Add to Path - and there is no\r
+@rem      launcher entry and no py.exe anywhere on disk, while the Launcher\r
+@rem      directory it would occupy is on the user PATH but does not exist.\r
+@rem\r
+@rem So resolve an interpreter by full path and prefer the launcher only when it\r
+@rem is real. python.exe is the thing that actually has to exist; `py` is a\r
+@rem convenience shim that picks between several of them, and there is exactly one\r
+@rem here. Reverse name order so the newest Python wins, and require python.exe\r
+@rem inside the directory - a half-removed version leaves the folder behind with\r
+@rem only Doc and Lib in it, which is exactly what this box has.\r
+set "PY_EXE="\r
+if exist "%LOCALAPPDATA%\Programs\Python\Launcher\py.exe" set "PY_EXE=%LOCALAPPDATA%\Programs\Python\Launcher\py.exe"\r
+if not defined PY_EXE if exist "%WINDIR%\py.exe" set "PY_EXE=%WINDIR%\py.exe"\r
+if not defined PY_EXE for /f "delims=" %%P in ('dir /b /ad /o-n "%LOCALAPPDATA%\Programs\Python\Python3*" 2^>nul') do if not defined PY_EXE if exist "%LOCALAPPDATA%\Programs\Python\%%P\python.exe" set "PY_EXE=%LOCALAPPDATA%\Programs\Python\%%P\python.exe"\r
+\r
+@rem Pillow publishes win_arm64 wheels alongside win_amd64, so the ARM64 Python\r
+@rem winget just installed gets a native wheel and never falls back to building\r
+@rem from source. Any package WITHOUT an ARM64 wheel does fall back, and that\r
+@rem source build needs the MSVC toolchain the elevated half installs.\r
+if not defined PY_EXE (\r
+    echo [setup-windows] WARNING: no Python interpreter found; skipping the Pillow install.\r
+    echo [setup-windows]   Looked for the py launcher, then %LOCALAPPDATA%\Programs\Python\Python3*\python.exe.\r
+) else (\r
+    echo [setup-windows] Python: %PY_EXE%\r
+    "%PY_EXE%" -m pip install Pillow\r
+)\r
 \r
 if "%IS_ARM64%"=="1" (\r
     echo.\r
     echo [setup-windows] ARM64 notes:\r
     echo [setup-windows]   native ARM64: Git, Python, .NET SDK, PowerShell, VS Code, Windows Terminal,\r
 \r
 if "%IS_ARM64%"=="1" (\r
     echo.\r
     echo [setup-windows] ARM64 notes:\r
     echo [setup-windows]   native ARM64: Git, Python, .NET SDK, PowerShell, VS Code, Windows Terminal,\r
-    echo [setup-windows]                 WinMerge, Brave, Sysinternals ^(the *64a.exe variants^),\r
-    echo [setup-windows]                 Claude Code, CMake, Ninja, and the SDK/WPT tools.\r
-    echo [setup-windows]   compilers   : MSVC and clang-cl from Visual Studio 2026, plus upstream\r
-    echo [setup-windows]                 LLVM - all three native ARM64, two different Clang majors.\r
-    echo [setup-windows]   emulated x64: iperf3, NASM, OpenCppCoverage, BinSkim, rsync.exe, AGI.\r
+    echo [setup-windows]                 Sysinternals ^(the *64a.exe variants^), Claude Code, CMake,\r
+    echo [setup-windows]                 Ninja, and the SDK/WPT tools.\r
+    echo [setup-windows]   compiler    : MSVC from Visual Studio 2026, native ARM64. One compiler by\r
+    echo [setup-windows]                 design - clang-cl and upstream LLVM are x64-box only.\r
+    echo [setup-windows]   emulated x64: iperf3, which is network-bound anyway.\r
+    echo [setup-windows]   not here    : Brave, WinMerge, LLVM, NASM, AGI, OpenCppCoverage, BinSkim\r
+    echo [setup-windows]                 and the WDK - dropped on ARM64, see the x64-only set above.\r
     echo [setup-windows]   unavailable : VirtualBox, the Windows 7 x86 test VM, Intel VTune,\r
     echo [setup-windows]                 and the v141 / Windows XP targeting toolset.\r
 )\r
     echo [setup-windows]   unavailable : VirtualBox, the Windows 7 x86 test VM, Intel VTune,\r
     echo [setup-windows]                 and the v141 / Windows XP targeting toolset.\r
 )\r
@@ -396,6 +476,27 @@ if "%IS_ARM64%"=="1" (
 @rem landed on this run - so point at the audit, which reports the actual state.\r
 if "%ELEV%"=="NOLUA" (\r
     echo [setup-windows]   NOT on this box: everything needing elevation was skipped, including\r
 @rem landed on this run - so point at the audit, which reports the actual state.\r
 if "%ELEV%"=="NOLUA" (\r
     echo [setup-windows]   NOT on this box: everything needing elevation was skipped, including\r
-    echo [setup-windows]                 Visual Studio's clang-cl, the .NET SDK and CMake. The\r
+    echo [setup-windows]                 Visual Studio's MSVC, the .NET SDK and CMake. The\r
     echo [setup-windows]                 architecture audit above lists what is really installed.\r
 )\r
     echo [setup-windows]                 architecture audit above lists what is really installed.\r
 )\r
+\r
+exit /b 0\r
+\r
+@rem ---------------------------------------------------------------------------\r
+@rem :x64only <winget-id> "<why not on ARM64>"\r
+@rem\r
+@rem Install a package on x64 and skip it on ARM64, saying why. One subroutine\r
+@rem rather than a gate at each call site so that the reason is never separated\r
+@rem from the skip: the log line a future reader sees is the same text this file\r
+@rem carries, and there is exactly one place to change the policy.\r
+@rem\r
+@rem The reason is a real argument, not a comment, because the interesting case is\r
+@rem reading the run log six months later and wondering where NASM went.\r
+@rem ---------------------------------------------------------------------------\r
+:x64only\r
+if not "%IS_ARM64%"=="1" (\r
+    winget install %1\r
+    goto :eof\r
+)\r
+echo [setup-windows] Skipping %1 on ARM64: %~2\r
+goto :eof\r