@echo off @rem --------------------------------------------------------------------------- @rem setup-windows.bat - provision a fresh Windows box for native development @rem @rem Runs on x64 and on ARM64 (Windows 11 on Arm). See the ARM64 block below for @rem what changes on Arm silicon. @rem --------------------------------------------------------------------------- @rem --------------------------------------------------------------------------- @rem Host architecture @rem @rem PROCESSOR_ARCHITECTURE alone is not enough: a 32-bit cmd.exe under emulation @rem reports x86 and stashes the real one in PROCESSOR_ARCHITEW6432. Check both, @rem so this is right however the script was launched. @rem @rem The value drives two things below, and nothing else - winget resolves the @rem installer architecture on its own (native first, then whatever the box can @rem emulate), so the ordinary installs need no help: @rem - VirtualBox is skipped on ARM64 (see below) @rem - the summary tells you which tools you got as emulated x64 @rem --------------------------------------------------------------------------- set "HOST_ARCH=%PROCESSOR_ARCHITECTURE%" if defined PROCESSOR_ARCHITEW6432 set "HOST_ARCH=%PROCESSOR_ARCHITEW6432%" set "IS_ARM64=0" if /i "%HOST_ARCH%"=="ARM64" set "IS_ARM64=1" echo [setup-windows] Host architecture: %HOST_ARCH% @rem --------------------------------------------------------------------------- @rem Can this account elevate? Worked out ONCE, here, because it gates two very @rem different things: the elevated half far below, and a handful of the "per-user" @rem winget installs just after this, which are per-user in name only. @rem @rem ALREADY - already elevated. IsInRole(Administrator) is false for an admin @rem running unelevated under UAC, so this means actually elevated, @rem not merely capable of it. @rem PROMPT - not elevated, UAC on, so elevation can be requested. @rem NOLUA - UAC is off machine-wide (EnableLUA = 0) AND this is not an @rem administrator. Elevation is impossible, not merely declined: @rem Windows has no prompt to offer. Note that with UAC off, @rem `Start-Process -Verb RunAs` does not fail - it is silently @rem ignored, runs the child with the caller's own token, and reports @rem success, which is why this needs detecting rather than trying. @rem --------------------------------------------------------------------------- set "ELEV=PROMPT" 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" echo [setup-windows] Elevation: %ELEV% if "%ELEV%"=="NOLUA" ( echo [setup-windows] This account cannot elevate ^(UAC off, not an administrator^). Packages that echo [setup-windows] need a machine-wide install will be SKIPPED rather than left to fail; details echo [setup-windows] at each one, and a summary before the elevated half. ) @rem --- Non-admin (per-user) winget installs --- @rem @rem No --architecture anywhere on purpose. winget already picks the best @rem installer the manifest offers for this machine - arm64, else x64, else x86 - @rem and pinning it would turn "no native build, use the emulated one" into a @rem hard failure. @rem @rem These all publish real ARM64 installers, so on an ARM64 box every one of @rem them lands as a native ARM64 binary with no emulation involved. @rem @rem The Sysinternals packages are a multi-architecture ZIP rather than an @rem installer, so the ARM64 build is IN there but is not the default-named exe: @rem the zip holds procexp.exe (x86), procexp64.exe (x64) and procexp64a.exe @rem (ARM64). On ARM64, run the *64a.exe variants - plain procexp.exe is the x86 @rem one and will run emulated. winget install Anthropic.ClaudeCode winget install Brave.Brave winget install Git.Git winget install Microsoft.PowerShell Microsoft.Sysinternals.ProcessExplorer Microsoft.Sysinternals.ProcessMonitor Microsoft.Sysinternals.SDelete Microsoft.VisualStudioCode Microsoft.WindowsTerminal winget install Python.Python.3.13 @rem WinMerge is the one package here where winget's own preference order picks @rem the WRONG architecture, and it is worth knowing why: upstream publishes the @rem ARM64 build as a MACHINE-scope installer (WinMerge--ARM64-Setup.exe) and @rem the x64 build as a PER-USER one (WinMerge--x64-PerUser-Setup.exe). Run @rem unelevated, winget's scope preference beats its architecture preference and @rem you silently get the emulated x64 build. @rem @rem So ask for arm64 explicitly, and fall back to the default when that cannot be @rem installed - which is exactly the standard-user case, where the machine-scope @rem installer has nowhere to go. The fallback is the x64 build under emulation: @rem a diff tool is not a hot path, and a working WinMerge beats none. The @rem architecture audit at the end of setup-windows-no-uac.ps1 reports which one @rem you ended up with. if "%IS_ARM64%"=="1" ( winget install WinMerge.WinMerge --architecture arm64 || winget install WinMerge.WinMerge ) else ( winget install WinMerge.WinMerge ) @rem --- Build drivers, standalone and native --- @rem @rem CMake and Ninja BOTH ship with Visual Studio, so this is not about making @rem them exist - it is about which binary you get and where it is on the PATH: @rem @rem - VS's cmake.exe is already ARM64, but it only lands on the PATH inside a @rem Developer Command Prompt. The build line further down assumes a plain @rem `cmake`, so install the standalone one to make that true in any shell. @rem - VS's bundled ninja.exe is x64 EVEN ON ARM64 (verified on this box), so it @rem runs under emulation. Ninja is re-invoked for every edge in the build @rem graph, which makes it the one tool here where emulation is actually paid @rem over and over, and ninja-winarm64.zip is a native build. Worth having. @rem @rem Which ninja wins inside a VS Developer Command Prompt: the native one. VS @rem adds its copy from Common7\Tools\vsdevcmd\ext\cmake.bat with @rem set "PATH=%PATH%;...\CMake\bin;...\CMake\Ninja" @rem which APPENDS, so the VS directories sit at the very end of the composed @rem PATH, behind every machine and user entry. setup-windows-no-uac.ps1 then @rem pins the native ninja's directory to the FRONT of the user PATH so the @rem margin does not depend on two append orders staying as they are. @rem @rem Ninja here, CMake in the machine-wide group below: Ninja is a portable zip @rem winget unpacks into the user profile, CMake is an MSI that installs for the @rem whole machine. winget install Ninja-build.Ninja @rem --- Second, independent Clang: compiler diversity --- @rem @rem The elevated half installs Visual Studio's Clang component (clang-cl, native @rem ARM64). This is a DIFFERENT LLVM: the upstream release, on its own schedule @rem and usually several major versions ahead of the one VS bundles, installed to @rem C:\Program Files\LLVM rather than inside the VS tree. Two Clang majors plus @rem MSVC over the same sources is the point - it is what catches the bugs a @rem single toolchain agrees with itself about. @rem @rem Native on ARM64: the manifest resolves to LLVM--woa64.exe ("Windows on @rem Arm 64"), not the x64 build. @rem @rem PATH ORDER MATTERS if you use it. Should this installer put its bin on the @rem PATH, a bare `clang-cl` resolves to the upstream one while CMake's @rem `-T ClangCL` toolset keeps using the VS copy - so the two can be selected @rem independently, but only if you know which one you are asking for. Prefer @rem -DCMAKE_CXX_COMPILER with a full path when you care. winget install LLVM.LLVM @rem x64-only manifest: winget falls back to the x64 installer on ARM64 and it runs @rem under Prism emulation. Harmless for what it does here - iperf3 is a network @rem benchmark bounded by the link, not by CPU. winget install ar51an.iPerf3 @rem NASM assembles x86/x86-64 only - there is no ARM64 target in it and no ARM64 @rem build of it. Installed on ARM64 anyway (as emulated x64) because this box @rem still cross-compiles x86/x64 binaries, where NASM is the assembler that @rem builds them. If you only ever target ARM64, nothing here uses it: the ARM64 @rem assembler is armasm64.exe, which ships with MSVC. winget install NASM.NASM @rem VirtualBox has no ARM64 Windows host build, and the x64 one cannot be made to @rem work by emulation: a hypervisor is kernel-mode, and its x64 driver will not @rem load on an ARM64 kernel. Installing it on ARM64 gets you a broken product and @rem a failed driver install, so skip it and say so. @rem @rem This is also what makes setup-windows-7-test-env.bat unreachable from an @rem ARM64 host: it prepares a Windows 7 *x86* guest, and neither VirtualBox nor @rem Hyper-V on ARM64 can run one. if "%IS_ARM64%"=="1" ( echo [setup-windows] Skipping Oracle.VirtualBox: no ARM64 Windows host build ^(x64 hypervisor drivers cannot load on an ARM64 kernel^). echo [setup-windows] The Windows 7 x86 test-VM workflow ^(setup-windows-7-test-env.bat^) is therefore unavailable on this box. ) else ( winget install Oracle.VirtualBox ) @rem --------------------------------------------------------------------------- @rem Machine-wide installers, despite sitting in the "per-user" section @rem @rem These four are not per-user at all. On an account that cannot elevate they @rem fail on every run, with installer exit codes that say nothing useful: @rem Microsoft.DotNet.SDK.10 exit 5 (ERROR_ACCESS_DENIED) @rem Kitware.CMake exit 1603 (generic MSI failure) @rem Google.AndroidGPUInspector exit 1603 @rem OpenCppCoverage exit 1 (its installer self-elevates) @rem @rem Grouped and skipped outright where elevation is impossible. Attempting a @rem guaranteed failure four times per run - and paying the download for it - @rem teaches nobody anything, and the four opaque error codes bury the one line @rem that matters. On an account that can elevate they run exactly as before. @rem @rem NOTE none of these is a build-blocker: the .NET SDK is only here for the WiX @rem MSI tooling, CMake also ships inside Visual Studio, AGI profiles Android @rem devices, and OpenCppCoverage cannot instrument ARM64 binaries anyway. @rem --------------------------------------------------------------------------- if "%ELEV%"=="NOLUA" ( echo. echo [setup-windows] Skipping the machine-wide installers - this account cannot elevate: echo [setup-windows] Microsoft.DotNet.SDK.10, Kitware.CMake, Google.AndroidGPUInspector, OpenCppCoverage. echo [setup-windows] None blocks a build. Re-run from an administrator account to get them. echo. goto :after_admin_pkgs ) winget install Microsoft.DotNet.SDK.10 winget install Kitware.CMake winget install Google.AndroidGPUInspector @rem OpenCppCoverage: native (PE) line coverage for the C++ binaries. run-coverage-occ.py drives the @rem pytest suite under it to produce an HTML report (the binaries under test build with PDBs, @rem which it reads). The installer elevates via UAC. @rem @rem x86/x64 only, and unlike the other emulated tools that is a real limit rather @rem than a slowdown: it collects coverage by debugging the process under test and @rem stepping x86/x64 instructions, so it can cover the x86/x64 binaries this box @rem cross-compiles but NOT an ARM64 one. For ARM64 coverage, build the ARM64 @rem binaries with /fsanitize-coverage or use the x64 build for the coverage run. winget install OpenCppCoverage.OpenCppCoverage :after_admin_pkgs if "%IS_ARM64%"=="1" echo [setup-windows] NOTE: OpenCppCoverage is x86/x64-only - it cannot instrument ARM64 binaries. Run coverage against the x64 build. @rem --- WiX 5.0.2, pinned on purpose --- @rem WiX packages our proprietary software into MSIs, and the version is pinned to keep that @rem free of a fee. 5.0.2 is the last release distributed under the Microsoft Reciprocal @rem License alone. From 6.0 onward the package also carries OSMFEULA.txt, an Open Source @rem Maintenance Fee agreement: a monthly fee owed by anyone who uses the PREBUILT BINARIES @rem as part of revenue-generating activity and has annual gross revenue >= US$10,000. @rem @rem It is a fee for the binaries, not a restriction on what we ship - MS-RL is file-scoped @rem and never reached the MSIs WiX builds, under any version - but 5.0.2 owes nothing. @rem The 6.x/7.x SOURCE is still MS-RL too, so self-compiling is another way out; a pin is @rem the cheaper one. This replaces `winget install WiXToolset.WiXCLI`, which has no version @rem selector and so installs the latest (7.0.0 today, EULA and all). @rem @rem PIN THE MSBUILD SIDE TOO. A .wixproj referencing WixToolset.Sdk without a version @rem resolves to the latest - 7.x, same EULA - and nothing here constrains it. Pin it in the @rem project: . @rem @rem dotnet.exe is called by full path: winget put the SDK on the machine PATH a few lines @rem ago, but this cmd session inherited its environment before that and cannot see it. @rem install-then-update is for re-runs - install fails once the tool is there, and update @rem then holds it at exactly 5.0.2 - which keeps this script idempotent like the rest. @rem @rem %ProgramFiles%\dotnet is the NATIVE SDK's root on every architecture, ARM64 @rem included - what winget just installed. An x64 SDK installed alongside it on @rem an ARM64 box goes to %ProgramFiles%\dotnet\x64 instead, so fall back there @rem for a machine that only ever got the x64 one. Prefer the native root: the @rem ARM64 SDK runs the wix tool natively, and building an MSI is not something @rem worth doing through emulation. set "DOTNET_EXE=%ProgramFiles%\dotnet\dotnet.exe" if not exist "%DOTNET_EXE%" set "DOTNET_EXE=%ProgramFiles%\dotnet\x64\dotnet.exe" if not exist "%DOTNET_EXE%" echo [setup-windows] WARNING: dotnet.exe not found; skipping the WiX 5.0.2 pin. if not exist "%DOTNET_EXE%" goto :after_wix "%DOTNET_EXE%" tool install --global wix --version 5.0.2 || "%DOTNET_EXE%" tool update --global wix --version 5.0.2 @rem Report what the pin actually produced. By full path again, and because the shim lands in @rem a directory this session's PATH predates: expect "5.0.2+", not 7.x. "%USERPROFILE%\.dotnet\tools\wix.exe" --version :after_wix @rem --------------------------------------------------------------------------- @rem No package manager needed for the Windows build @rem @rem Just: @rem cd windows && cmake -B build -G "Visual Studio 17 2022" -A x64 && cmake --build build --config Release @rem @rem -A selects the TARGET, independently of the host. On an ARM64 box the same @rem generator cross-compiles all three, and MSVC's ARM64-hosted toolchain is used @rem for each - nothing is emulated: @rem -A ARM64 native ARM64 binaries @rem -A x64 x64 binaries (run here under emulation) @rem -A Win32 x86 binaries @rem Use the generator matching the Visual Studio the elevated half installed - @rem "Visual Studio 17 2022" on x64, "Visual Studio 18 2026" on ARM64 - or just @rem let CMake pick the newest it finds. @rem --------------------------------------------------------------------------- @rem --- Elevated installs (Visual Studio, WDK, system tools) --- @rem The elevated script runs in its own window and logs to setup-windows-uac.log. @rem -PassThru + $p.ExitCode propagates its real exit code back through to ERRORLEVEL. set "UAC_LOG=%~dp0setup-windows-uac.log" if exist "%UAC_LOG%" del "%UAC_LOG%" @rem %ELEV% was worked out at the top of this script - see the comment there for @rem why `Start-Process -Verb RunAs` cannot be trusted to report this itself. if "%ELEV%"=="NOLUA" goto :elev_impossible if "%ELEV%"=="ALREADY" goto :elev_direct @rem Not elevated, UAC is on: request it. Expect a prompt. echo [setup-windows] Requesting elevation ^(expect a UAC prompt^)... powershell -NoProfile -Command "$p = Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','""%~dp0setup-windows-with-uac.ps1""' -Wait -PassThru; exit $p.ExitCode" set "UAC_RC=%ERRORLEVEL%" goto :elev_done :elev_direct echo [setup-windows] Already running elevated; running the elevated half directly. powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-with-uac.ps1" set "UAC_RC=%ERRORLEVEL%" goto :elev_done :elev_impossible echo. echo [setup-windows] SKIPPING the elevated half: this account cannot elevate. echo [setup-windows] UAC is disabled machine-wide ^(EnableLUA = 0^) and you are not an administrator, echo [setup-windows] so Windows offers no way to elevate - there is no prompt to accept. With UAC off, echo [setup-windows] 'Start-Process -Verb RunAs' is silently ignored and reports success, which is why echo [setup-windows] this used to look like a cancelled prompt. echo [setup-windows] echo [setup-windows] Not installed: Visual Studio components ^(including clang-cl^), the WDK, echo [setup-windows] rsync, and the OpenSSH server. Everything per-user above is unaffected. echo [setup-windows] echo [setup-windows] To finish the box, either sign in to an administrator account and re-run this echo [setup-windows] script, or have an administrator run setup-windows-with-uac.ps1 there. Then echo [setup-windows] re-run setup-windows-no-uac.ps1 as yourself, so the per-user PATH and .gitconfig echo [setup-windows] land in YOUR profile rather than the administrator's. echo. set "UAC_RC=SKIPPED" :elev_done @rem --- Surface the elevated session's output (its window has already closed) --- @rem @rem A missing log is NOT self-explanatory, so do not guess at one cause. The @rem elevated script writes its transcript as almost its first act, so no log @rem means it never got as far as running: either the prompt was declined, or it @rem started unelevated and stopped on its own #Requires line. Which of those it @rem was is already known from %ELEV%, so report that instead of speculating. if "%UAC_RC%"=="SKIPPED" goto :after_uac_log if exist "%UAC_LOG%" ( echo. echo ===== elevated setup log ^(%UAC_LOG%^) ===== type "%UAC_LOG%" echo ===== end of elevated setup log ===== ) else ( echo [setup-windows] WARNING: no elevated log found at "%UAC_LOG%". if "%ELEV%"=="PROMPT" echo [setup-windows] The elevated window never started - the UAC prompt was most likely declined. if "%ELEV%"=="ALREADY" echo [setup-windows] The elevated half exited before writing its transcript; see its output above. ) :after_uac_log @rem --- Non-elevated PowerShell half --- @rem WinMerge on the user PATH, BinSkim, and the global git config (identity + @rem core.sshCommand -> a Win32-OpenSSH client, so git shares the Windows @rem ssh-agent). EDIT THE GIT IDENTITY at the top of setup-windows-no-uac.ps1 @rem before the first run. @rem @rem Deliberately NOT elevated: every step writes per-user state (the HKCU PATH, @rem the .gitconfig under %USERPROFILE%), which the elevated half would write to @rem the administrator profile instead. @rem @rem AFTER the elevated half on purpose: core.sshCommand prefers the ssh.exe that @rem half unpacks beside rsync.exe - a push through the in-box client is capped @rem at ~17MB/s - and it can only prefer it once it is on disk. Run either way, @rem including when the elevated half failed above: nothing here depends on it, @rem and the fallback is the in-box client that Windows already has. @rem @rem Non-fatal: these are conveniences, and the elevated half is the part worth @rem the UAC prompt. A failure warns and provisioning continues. powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-no-uac.ps1" if not "%ERRORLEVEL%"=="0" echo [setup-windows] WARNING: setup-windows-no-uac.ps1 reported a failure ^(see above^); continuing. @rem Skipping the elevated half is a reported, understood outcome on a box where @rem elevation is impossible - not a failure to exit non-zero over. The per-user @rem provisioning above did run, and re-running from an administrator account is @rem the documented next step. if "%UAC_RC%"=="SKIPPED" goto :uac_reported if not "%UAC_RC%"=="0" ( echo. echo [setup-windows] ELEVATED SETUP FAILED ^(exit code %UAC_RC%^). See log above. exit /b %UAC_RC% ) echo. echo [setup-windows] Elevated setup completed successfully. goto :uac_reported :uac_reported if "%UAC_RC%"=="SKIPPED" echo. if "%UAC_RC%"=="SKIPPED" echo [setup-windows] Per-user setup complete; the elevated half was skipped ^(see above^). @rem Removed: this doesn't work as well as I hoped, maybe try again later @rem -- Install Headroom --- @rem py -m pip install "headroom-ai[all]" @rem npm install headroom-ai @rem Pillow publishes win_arm64 wheels alongside win_amd64, so `py` - the ARM64 @rem Python winget just installed - gets a native wheel and never falls back to @rem building from source. Any package WITHOUT an ARM64 wheel does fall back, and @rem that source build needs the MSVC toolchain the elevated half installs. py -m pip install Pillow if "%IS_ARM64%"=="1" ( echo. echo [setup-windows] ARM64 notes: echo [setup-windows] native ARM64: Git, Python, .NET SDK, PowerShell, VS Code, Windows Terminal, echo [setup-windows] WinMerge, Brave, Sysinternals ^(the *64a.exe variants^), echo [setup-windows] Claude Code, CMake, Ninja, and the SDK/WPT tools. echo [setup-windows] compilers : MSVC and clang-cl from Visual Studio 2026, plus upstream echo [setup-windows] LLVM - all three native ARM64, two different Clang majors. echo [setup-windows] emulated x64: iperf3, NASM, OpenCppCoverage, BinSkim, rsync.exe, AGI. echo [setup-windows] unavailable : VirtualBox, the Windows 7 x86 test VM, Intel VTune, echo [setup-windows] and the v141 / Windows XP targeting toolset. ) @rem The list above is what this script PROVIDES on ARM64, not necessarily what @rem landed on this run - so point at the audit, which reports the actual state. if "%ELEV%"=="NOLUA" ( echo [setup-windows] NOT on this box: everything needing elevation was skipped, including echo [setup-windows] Visual Studio's clang-cl, the .NET SDK and CMake. The echo [setup-windows] architecture audit above lists what is really installed. )