]> vilimpoc.org git repositories - dotfiles/blobdiff - setup-windows.bat
dotfiles: split provisioning between x64 and ARM64
[dotfiles] / setup-windows.bat
index b94922d37cd23ba8bbf053bf95d13e497602e8a1..0a69956da6768ce5b481324bd2dad576a0bf3366 100644 (file)
 @echo off\r
 \r
 @rem ---------------------------------------------------------------------------\r
 @echo off\r
 \r
 @rem ---------------------------------------------------------------------------\r
-@rem setup-windows.bat - provision a fresh Windows box for BlockBox development\r
+@rem setup-windows.bat - provision a fresh Windows box for native development\r
+@rem\r
+@rem Runs on x64 and on ARM64 (Windows 11 on Arm). See the ARM64 block below for\r
+@rem what changes on Arm silicon.\r
+@rem ---------------------------------------------------------------------------\r
+\r
+@rem ---------------------------------------------------------------------------\r
+@rem Host architecture\r
+@rem\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 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   - 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 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
+\r
 @rem ---------------------------------------------------------------------------\r
 @rem ---------------------------------------------------------------------------\r
+@rem Can this account elevate? Worked out ONCE, here, because it gates two very\r
+@rem different things: the elevated half far below, and a handful of the "per-user"\r
+@rem winget installs just after this, which are per-user in name only.\r
+@rem\r
+@rem   ALREADY - already elevated. IsInRole(Administrator) is false for an admin\r
+@rem             running unelevated under UAC, so this means actually elevated,\r
+@rem             not merely capable of it.\r
+@rem   PROMPT  - not elevated, UAC on, so elevation can be requested.\r
+@rem   NOLUA   - UAC is off machine-wide (EnableLUA = 0) AND this is not an\r
+@rem             administrator. Elevation is impossible, not merely declined:\r
+@rem             Windows has no prompt to offer. Note that with UAC off,\r
+@rem             `Start-Process -Verb RunAs` does not fail - it is silently\r
+@rem             ignored, runs the child with the caller's own token, and reports\r
+@rem             success, which is why this needs detecting rather than trying.\r
+@rem ---------------------------------------------------------------------------\r
+set "ELEV=PROMPT"\r
+for /f "usebackq tokens=*" %%A in (`powershell -NoProfile -ExecutionPolicy Bypass -Command "$id=[Security.Principal.WindowsIdentity]::GetCurrent(); if (([Security.Principal.WindowsPrincipal]$id).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 'ALREADY' } elseif ((Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -ErrorAction SilentlyContinue).EnableLUA -eq 0) { 'NOLUA' } else { 'PROMPT' }"`) do set "ELEV=%%A"\r
+echo [setup-windows] Elevation: %ELEV%\r
+if "%ELEV%"=="NOLUA" (\r
+    echo [setup-windows] This account cannot elevate ^(UAC off, not an administrator^). Packages that\r
+    echo [setup-windows] need a machine-wide install will be SKIPPED rather than left to fail; details\r
+    echo [setup-windows] at each one, and a summary before the elevated half.\r
+)\r
 \r
 \r
-@rem --- Non-admin (per-user) installs + git config ---\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 installer the manifest offers for this machine - arm64, else x64, else x86 -\r
+@rem and pinning it would turn "no native build, use the emulated one" into a\r
+@rem hard failure.\r
+@rem\r
+@rem These all publish real ARM64 installers, so on an ARM64 box every one of\r
+@rem them lands as a native ARM64 binary with no emulation involved.\r
+@rem\r
+@rem The Sysinternals packages are a multi-architecture ZIP rather than an\r
+@rem installer, so the ARM64 build is IN there but is not the default-named exe:\r
+@rem the zip holds procexp.exe (x86), procexp64.exe (x64) and procexp64a.exe\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 Anthropic.ClaudeCode\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
-winget install WinMerge.WinMerge\r
-winget install WiXToolset.WiXCLI\r
+\r
+@rem WinMerge is the one package here where winget's own preference order picks\r
+@rem the WRONG architecture, and it is worth knowing why: upstream publishes the\r
+@rem ARM64 build as a MACHINE-scope installer (WinMerge-<ver>-ARM64-Setup.exe) and\r
+@rem the x64 build as a PER-USER one (WinMerge-<ver>-x64-PerUser-Setup.exe). Run\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 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
+@rem CMake and Ninja BOTH ship with Visual Studio, so this is not about making\r
+@rem them exist - it is about which binary you get and where it is on the PATH:\r
+@rem\r
+@rem   - VS's cmake.exe is already ARM64, but it only lands on the PATH inside a\r
+@rem     Developer Command Prompt. The build line further down assumes a plain\r
+@rem     `cmake`, so install the standalone one to make that true in any shell.\r
+@rem   - VS's bundled ninja.exe is x64 EVEN ON ARM64 (verified on this box), so it\r
+@rem     runs under emulation. Ninja is re-invoked for every edge in the build\r
+@rem     graph, which makes it the one tool here where emulation is actually paid\r
+@rem     over and over, and ninja-winarm64.zip is a native build. Worth having.\r
+@rem\r
+@rem Which ninja wins inside a VS Developer Command Prompt: the native one. VS\r
+@rem adds its copy from Common7\Tools\vsdevcmd\ext\cmake.bat with\r
+@rem     set "PATH=%PATH%;...\CMake\bin;...\CMake\Ninja"\r
+@rem which APPENDS, so the VS directories sit at the very end of the composed\r
+@rem PATH, behind every machine and user entry. setup-windows-no-uac.ps1 then\r
+@rem pins the native ninja's directory to the FRONT of the user PATH so the\r
+@rem margin does not depend on two append orders staying as they are.\r
+@rem\r
+@rem Ninja here, CMake in the machine-wide group below: Ninja is a portable zip\r
+@rem winget unpacks into the user profile, CMake is an MSI that installs for the\r
+@rem whole machine.\r
+winget install Ninja-build.Ninja\r
+\r
+@rem --- Second, independent Clang: compiler diversity, on x64 only ---\r
+@rem\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 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 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
+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
+@rem benchmark bounded by the link, not by CPU.\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. 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
+@rem load on an ARM64 kernel. Installing it on ARM64 gets you a broken product and\r
+@rem a failed driver install, so skip it and say so.\r
+@rem\r
+@rem This is also what makes setup-windows-7-test-env.bat unreachable from an\r
+@rem ARM64 host: it prepares a Windows 7 *x86* guest, and neither VirtualBox nor\r
+@rem Hyper-V on ARM64 can run one.\r
+if "%IS_ARM64%"=="1" (\r
+    echo [setup-windows] Skipping Oracle.VirtualBox: no ARM64 Windows host build ^(x64 hypervisor drivers cannot load on an ARM64 kernel^).\r
+    echo [setup-windows]   The Windows 7 x86 test-VM workflow ^(setup-windows-7-test-env.bat^) is therefore unavailable on this box.\r
+) else (\r
+    winget install Oracle.VirtualBox\r
+)\r
+\r
+@rem ---------------------------------------------------------------------------\r
+@rem Machine-wide installers, despite sitting in the "per-user" section\r
+@rem\r
+@rem These four are not per-user at all. On an account that cannot elevate they\r
+@rem fail on every run, with installer exit codes that say nothing useful:\r
+@rem   Microsoft.DotNet.SDK.10      exit 5     (ERROR_ACCESS_DENIED)\r
+@rem   Kitware.CMake                exit 1603  (generic MSI failure)\r
+@rem   Google.AndroidGPUInspector   exit 1603\r
+@rem   OpenCppCoverage              exit 1     (its installer self-elevates)\r
+@rem\r
+@rem Grouped and skipped outright where elevation is impossible. Attempting a\r
+@rem guaranteed failure four times per run - and paying the download for it -\r
+@rem teaches nobody anything, and the four opaque error codes bury the one line\r
+@rem that matters. On an account that can elevate they run exactly as before.\r
+@rem\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
+    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
+)\r
+\r
+winget install Microsoft.DotNet.SDK.10\r
+winget install Kitware.CMake\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
 \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 (BlockBox + the sandbox DLLs build with PDBs,\r
+@rem pytest suite under it to produce an HTML report (the binaries under test build with PDBs,\r
 @rem which it reads). The installer elevates via UAC.\r
 @rem which it reads). The installer elevates via UAC.\r
-winget install OpenCppCoverage.OpenCppCoverage\r
-\r
-@rem --- Add WinMerge to the user PATH (persists to the HKCU environment) ---\r
-@rem Runs non-elevated, so it updates THIS user's PATH (the elevated script runs\r
-@rem as a different account). Idempotent: only appends if not already present.\r
-powershell -NoProfile -Command "$c = @((Join-Path $env:ProgramFiles 'WinMerge'), (Join-Path ${env:ProgramFiles(x86)} 'WinMerge'), (Join-Path $env:LOCALAPPDATA 'Programs\WinMerge')); $d = $c | Where-Object { Test-Path (Join-Path $_ 'WinMergeU.exe') } | Select-Object -First 1; if (-not $d) { Write-Warning 'WinMerge not found; user PATH unchanged.'; exit 0 }; $u = [Environment]::GetEnvironmentVariable('Path','User'); if (-not $u) { $u = '' }; if (($u -split ';') -notcontains $d) { $new = if ($u.Trim()) { $u.TrimEnd(';') + ';' + $d } else { $d }; [Environment]::SetEnvironmentVariable('Path', $new, 'User'); Write-Host ('Added ' + $d + ' to user PATH (restart your shell to pick it up).') } else { Write-Host ($d + ' already in user PATH.') }"\r
-\r
-@rem --- BinSkim (binary hardening analyzer) - per-user install, no admin needed ---\r
-@rem BinSkim checks the exact mitigations we enable in CMakeLists.txt (CFG/XFG, CET,\r
-@rem ASLR/HighEntropyVA, DEP, /GS, stack cookies, DEPENDENTLOADFLAG, etc.). The\r
-@rem Microsoft.CodeAnalysis.BinSkim NuGet package ships a self-contained win-x64\r
-@rem build, so this needs no .NET SDK/runtime: download the .nupkg (a zip), extract\r
-@rem the win-x64 tool folder to %LOCALAPPDATA%\Programs\BinSkim, and add it to the\r
-@rem user PATH. After restarting the shell:  binskim analyze path\to\BlockBox.exe\r
-@rem A failure here only warns (exit 0) so it never aborts the rest of provisioning.\r
-powershell -NoProfile -Command "try { $ErrorActionPreference='Stop'; [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; $dest=Join-Path $env:LOCALAPPDATA 'Programs\BinSkim'; $tmp=Join-Path $env:TEMP ('binskim_'+[guid]::NewGuid().ToString('N')); New-Item -ItemType Directory -Force -Path $tmp | Out-Null; $zip=Join-Path $tmp 'binskim.zip'; Invoke-WebRequest -Uri 'https://www.nuget.org/api/v2/package/Microsoft.CodeAnalysis.BinSkim' -OutFile $zip; Expand-Archive -Path $zip -DestinationPath $tmp -Force; $exe=Get-ChildItem -Path $tmp -Recurse -Filter 'BinSkim.exe' | Where-Object { $_.FullName -match 'win-x64' } | Sort-Object FullName | Select-Object -Last 1; if (-not $exe) { throw 'BinSkim.exe (win-x64) not found in package.' }; if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }; New-Item -ItemType Directory -Force -Path $dest | Out-Null; Copy-Item -Path (Join-Path $exe.Directory.FullName '*') -Destination $dest -Recurse -Force; Remove-Item -Recurse -Force $tmp; $u=[Environment]::GetEnvironmentVariable('Path','User'); if (-not $u) { $u='' }; if (($u -split ';') -notcontains $dest) { $new = if ($u.Trim()) { $u.TrimEnd(';')+';'+$dest } else { $dest }; [Environment]::SetEnvironmentVariable('Path',$new,'User'); Write-Host ('Added '+$dest+' to user PATH (restart your shell to pick it up).') } else { Write-Host ($dest+' already in user PATH.') }; Write-Host ('BinSkim installed to '+$dest) } catch { Write-Warning ('BinSkim install failed: '+$_.Exception.Message); exit 0 }"\r
-\r
-@rem --- Global git identity: EDIT THESE BEFORE RUNNING ---\r
-@rem Replace the placeholders with your own name and email, or comment the two\r
-@rem lines out and set your identity per-repository instead.\r
-git config --global user.name "PLACEHOLDER_NAME"\r
-git config --global user.email "PLACEHOLDER_EMAIL"\r
-git config --global core.sshcommand C:/Windows/System32/OpenSSH/ssh.exe\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 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
+@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
+@rem free of a fee. 5.0.2 is the last release distributed under the Microsoft Reciprocal\r
+@rem License alone. From 6.0 onward the package also carries OSMFEULA.txt, an Open Source\r
+@rem Maintenance Fee agreement: a monthly fee owed by anyone who uses the PREBUILT BINARIES\r
+@rem as part of revenue-generating activity and has annual gross revenue >= US$10,000.\r
+@rem\r
+@rem It is a fee for the binaries, not a restriction on what we ship - MS-RL is file-scoped\r
+@rem and never reached the MSIs WiX builds, under any version - but 5.0.2 owes nothing.\r
+@rem The 6.x/7.x SOURCE is still MS-RL too, so self-compiling is another way out; a pin is\r
+@rem the cheaper one. This replaces `winget install WiXToolset.WiXCLI`, which has no version\r
+@rem selector and so installs the latest (7.0.0 today, EULA and all).\r
+@rem\r
+@rem PIN THE MSBUILD SIDE TOO. A .wixproj referencing WixToolset.Sdk without a version\r
+@rem resolves to the latest - 7.x, same EULA - and nothing here constrains it. Pin it in the\r
+@rem project: <Project Sdk="WixToolset.Sdk/5.0.2">.\r
+@rem\r
+@rem dotnet.exe is called by full path: winget put the SDK on the machine PATH a few lines\r
+@rem ago, but this cmd session inherited its environment before that and cannot see it.\r
+@rem install-then-update is for re-runs - install fails once the tool is there, and update\r
+@rem then holds it at exactly 5.0.2 - which keeps this script idempotent like the rest.\r
+@rem\r
+@rem %ProgramFiles%\dotnet is the NATIVE SDK's root on every architecture, ARM64\r
+@rem included - what winget just installed. An x64 SDK installed alongside it on\r
+@rem an ARM64 box goes to %ProgramFiles%\dotnet\x64 instead, so fall back there\r
+@rem for a machine that only ever got the x64 one. Prefer the native root: the\r
+@rem ARM64 SDK runs the wix tool natively, and building an MSI is not something\r
+@rem worth doing through emulation.\r
+set "DOTNET_EXE=%ProgramFiles%\dotnet\dotnet.exe"\r
+if not exist "%DOTNET_EXE%" set "DOTNET_EXE=%ProgramFiles%\dotnet\x64\dotnet.exe"\r
+if not exist "%DOTNET_EXE%" echo [setup-windows] WARNING: dotnet.exe not found; skipping the WiX 5.0.2 pin.\r
+if not exist "%DOTNET_EXE%" goto :after_wix\r
+\r
+"%DOTNET_EXE%" tool install --global wix --version 5.0.2 || "%DOTNET_EXE%" tool update --global wix --version 5.0.2\r
+\r
+@rem Report what the pin actually produced. By full path again, and because the shim lands in\r
+@rem a directory this session's PATH predates: expect "5.0.2+<commit>", not 7.x.\r
+"%USERPROFILE%\.dotnet\tools\wix.exe" --version\r
+\r
+:after_wix\r
 \r
 @rem ---------------------------------------------------------------------------\r
 @rem No package manager needed for the Windows build\r
 @rem\r
 @rem Just:\r
 @rem   cd windows && cmake -B build -G "Visual Studio 17 2022" -A x64 && cmake --build build --config Release\r
 \r
 @rem ---------------------------------------------------------------------------\r
 @rem No package manager needed for the Windows build\r
 @rem\r
 @rem Just:\r
 @rem   cd windows && cmake -B build -G "Visual Studio 17 2022" -A x64 && cmake --build build --config Release\r
+@rem\r
+@rem -A selects the TARGET, independently of the host. On an ARM64 box the same\r
+@rem generator cross-compiles all three, and MSVC's ARM64-hosted toolchain is used\r
+@rem for each - nothing is emulated:\r
+@rem   -A ARM64    native ARM64 binaries\r
+@rem   -A x64      x64 binaries (run here under emulation)\r
+@rem   -A Win32    x86 binaries\r
+@rem Use the generator matching the Visual Studio the elevated half installed -\r
+@rem "Visual Studio 17 2022" on x64, "Visual Studio 18 2026" on ARM64 - or just\r
+@rem let CMake pick the newest it finds.\r
 @rem ---------------------------------------------------------------------------\r
 \r
 @rem ---------------------------------------------------------------------------\r
 \r
-@rem --- Elevated installs (VS2022, WDK, system tools) ---\r
+@rem --- Elevated installs (Visual Studio, WDK, system tools) ---\r
 @rem The elevated script runs in its own window and logs to setup-windows-uac.log.\r
 @rem -PassThru + $p.ExitCode propagates its real exit code back through to ERRORLEVEL.\r
 set "UAC_LOG=%~dp0setup-windows-uac.log"\r
 if exist "%UAC_LOG%" del "%UAC_LOG%"\r
 \r
 @rem The elevated script runs in its own window and logs to setup-windows-uac.log.\r
 @rem -PassThru + $p.ExitCode propagates its real exit code back through to ERRORLEVEL.\r
 set "UAC_LOG=%~dp0setup-windows-uac.log"\r
 if exist "%UAC_LOG%" del "%UAC_LOG%"\r
 \r
+@rem %ELEV% was worked out at the top of this script - see the comment there for\r
+@rem why `Start-Process -Verb RunAs` cannot be trusted to report this itself.\r
+if "%ELEV%"=="NOLUA" goto :elev_impossible\r
+if "%ELEV%"=="ALREADY" goto :elev_direct\r
+\r
+@rem Not elevated, UAC is on: request it. Expect a prompt.\r
+echo [setup-windows] Requesting elevation ^(expect a UAC prompt^)...\r
 powershell -NoProfile -Command "$p = Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','""%~dp0setup-windows-with-uac.ps1""' -Wait -PassThru; exit $p.ExitCode"\r
 set "UAC_RC=%ERRORLEVEL%"\r
 powershell -NoProfile -Command "$p = Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','""%~dp0setup-windows-with-uac.ps1""' -Wait -PassThru; exit $p.ExitCode"\r
 set "UAC_RC=%ERRORLEVEL%"\r
+goto :elev_done\r
+\r
+:elev_direct\r
+echo [setup-windows] Already running elevated; running the elevated half directly.\r
+powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-with-uac.ps1"\r
+set "UAC_RC=%ERRORLEVEL%"\r
+goto :elev_done\r
+\r
+:elev_impossible\r
+echo.\r
+echo [setup-windows] SKIPPING the elevated half: this account cannot elevate.\r
+echo [setup-windows]   UAC is disabled machine-wide ^(EnableLUA = 0^) and you are not an administrator,\r
+echo [setup-windows]   so Windows offers no way to elevate - there is no prompt to accept. With UAC off,\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 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]   re-run setup-windows-no-uac.ps1 as yourself, so the per-user PATH and .gitconfig\r
+echo [setup-windows]   land in YOUR profile rather than the administrator's.\r
+echo.\r
+set "UAC_RC=SKIPPED"\r
+\r
+:elev_done\r
 \r
 @rem --- Surface the elevated session's output (its window has already closed) ---\r
 \r
 @rem --- Surface the elevated session's output (its window has already closed) ---\r
+@rem\r
+@rem A missing log is NOT self-explanatory, so do not guess at one cause. The\r
+@rem elevated script writes its transcript as almost its first act, so no log\r
+@rem means it never got as far as running: either the prompt was declined, or it\r
+@rem started unelevated and stopped on its own #Requires line. Which of those it\r
+@rem was is already known from %ELEV%, so report that instead of speculating.\r
+if "%UAC_RC%"=="SKIPPED" goto :after_uac_log\r
 if exist "%UAC_LOG%" (\r
     echo.\r
     echo ===== elevated setup log ^(%UAC_LOG%^) =====\r
 if exist "%UAC_LOG%" (\r
     echo.\r
     echo ===== elevated setup log ^(%UAC_LOG%^) =====\r
@@ -63,9 +369,37 @@ if exist "%UAC_LOG%" (
     echo ===== end of elevated setup log =====\r
 ) else (\r
     echo [setup-windows] WARNING: no elevated log found at "%UAC_LOG%".\r
     echo ===== end of elevated setup log =====\r
 ) else (\r
     echo [setup-windows] WARNING: no elevated log found at "%UAC_LOG%".\r
-    echo [setup-windows] The elevated window may have been cancelled at the UAC prompt.\r
+    if "%ELEV%"=="PROMPT" echo [setup-windows] The elevated window never started - the UAC prompt was most likely declined.\r
+    if "%ELEV%"=="ALREADY" echo [setup-windows] The elevated half exited before writing its transcript; see its output above.\r
 )\r
 )\r
+:after_uac_log\r
 \r
 \r
+@rem --- Non-elevated PowerShell half ---\r
+@rem WinMerge on the user PATH, BinSkim, and the global git config (identity +\r
+@rem core.sshCommand -> a Win32-OpenSSH client, so git shares the Windows\r
+@rem ssh-agent). EDIT THE GIT IDENTITY at the top of setup-windows-no-uac.ps1\r
+@rem before the first run.\r
+@rem\r
+@rem Deliberately NOT elevated: every step writes per-user state (the HKCU PATH,\r
+@rem the .gitconfig under %USERPROFILE%), which the elevated half would write to\r
+@rem the administrator profile instead.\r
+@rem\r
+@rem AFTER the elevated half on purpose: core.sshCommand prefers the ssh.exe that\r
+@rem half unpacks beside rsync.exe - a push through the in-box client is capped\r
+@rem at ~17MB/s - and it can only prefer it once it is on disk. Run either way,\r
+@rem including when the elevated half failed above: nothing here depends on it,\r
+@rem and the fallback is the in-box client that Windows already has.\r
+@rem\r
+@rem Non-fatal: these are conveniences, and the elevated half is the part worth\r
+@rem the UAC prompt. A failure warns and provisioning continues.\r
+powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-no-uac.ps1"\r
+if not "%ERRORLEVEL%"=="0" echo [setup-windows] WARNING: setup-windows-no-uac.ps1 reported a failure ^(see above^); continuing.\r
+\r
+@rem Skipping the elevated half is a reported, understood outcome on a box where\r
+@rem elevation is impossible - not a failure to exit non-zero over. The per-user\r
+@rem provisioning above did run, and re-running from an administrator account is\r
+@rem the documented next step.\r
+if "%UAC_RC%"=="SKIPPED" goto :uac_reported\r
 if not "%UAC_RC%"=="0" (\r
     echo.\r
     echo [setup-windows] ELEVATED SETUP FAILED ^(exit code %UAC_RC%^). See log above.\r
 if not "%UAC_RC%"=="0" (\r
     echo.\r
     echo [setup-windows] ELEVATED SETUP FAILED ^(exit code %UAC_RC%^). See log above.\r
@@ -73,10 +407,96 @@ if not "%UAC_RC%"=="0" (
 )\r
 echo.\r
 echo [setup-windows] Elevated setup completed successfully.\r
 )\r
 echo.\r
 echo [setup-windows] Elevated setup completed successfully.\r
+goto :uac_reported\r
+\r
+:uac_reported\r
+if "%UAC_RC%"=="SKIPPED" echo.\r
+if "%UAC_RC%"=="SKIPPED" echo [setup-windows] Per-user setup complete; the elevated half was skipped ^(see above^).\r
 \r
 @rem Removed: this doesn't work as well as I hoped, maybe try again later\r
 @rem -- Install Headroom ---\r
 @rem py -m pip install "headroom-ai[all]"\r
 @rem npm install headroom-ai\r
 \r
 \r
 @rem Removed: this doesn't work as well as I hoped, maybe try again later\r
 @rem -- Install Headroom ---\r
 @rem py -m pip install "headroom-ai[all]"\r
 @rem npm install headroom-ai\r
 \r
-py -m pip install Pillow
\ No newline at end of file
+@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
+    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 and rsync.exe, both network-bound.\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
+@rem The list above is what this script PROVIDES on ARM64, not necessarily what\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 MSVC, the .NET SDK and CMake. The\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