X-Git-Url: https://vilimpoc.org/repos/dotfiles/blobdiff_plain/fbe899b67d263a1ae5404d3ed25f5f091c3d2c1e..0b4125206461c1a63a971ac9cf503319bcb05798:/setup-windows.bat?ds=sidebyside diff --git a/setup-windows.bat b/setup-windows.bat index b9e4752..7445de5 100644 --- a/setup-windows.bat +++ b/setup-windows.bat @@ -1,31 +1,190 @@ @echo off +@rem The plan below hands back a few dozen STEP_/WHY_/PKG_ variables; keep them, +@rem and everything else this script sets, out of the calling cmd session. +setlocal @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 Before it does anything, it shows the plan - every step it and its two +@rem halves can take, marked for this machine - and lets you choose. See "The +@rem plan" below, or run `setup-windows.bat /?`. @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 The environment variables are not enough, and the failure is silent. A 32-bit +@rem cmd.exe under emulation reports x86 and stashes the real one in +@rem PROCESSOR_ARCHITEW6432 - that pair is handled below - but an EMULATED x64 +@rem cmd.exe on an ARM64 box reports AMD64 with PROCESSOR_ARCHITEW6432 unset, and +@rem nothing in the environment contradicts it. Launching this script from an x64 +@rem shell (Git Bash, for one) is enough to hit that, and measured here it gives: +@rem PROCESSOR_ARCHITECTURE AMD64 +@rem HKLM\...\Session Manager\Environment ARM64 <- the real one +@rem +@rem So take the machine-level registry value as the answer and keep the +@rem environment pair only as the fallback. The registry key is per-machine and +@rem not subject to per-process emulation, and it is not one of the redirected +@rem hives. This is load-bearing now: the architecture decides which packages +@rem are installed at all, so reading it wrong provisions the wrong machine. @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 plan: every x64-only step, VirtualBox included, is marked unavailable +@rem on ARM64 and so never selected (see setup-windows-steps.ps1) @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%" +@rem reg.exe by full path, and NOT through a pipe to find/findstr: both of those +@rem are shadowable by anything earlier on the PATH, and a POSIX `find` on the +@rem PATH turns this into a silent no-op that leaves the emulated answer standing. +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" 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; the + echo [setup-windows] plan below marks each one, and there is a summary before the elevated half. +) + +@rem --------------------------------------------------------------------------- +@rem ARM64 is a lean build box, x64 is the full workstation +@rem +@rem The ARM64 machine here is a small VM, and everything it was installing +@rem without a native build was going to run under Prism emulation anyway. So the +@rem two architectures now provision different things on purpose: x64 keeps the +@rem full set, ARM64 installs ONE C/C++ compiler - MSVC from Visual Studio - and +@rem nothing that merely duplicates it or that exists only as an x64 binary. +@rem Rust does not break that rule: it is a different language, not a second +@rem C/C++ compiler, and its native aarch64 MSVC toolchain links through MSVC. +@rem +@rem Dropped on ARM64, and which script would otherwise take each one: +@rem Brave.Brave here no ARM64 build; Edge is in-box and native +@rem WinMerge.WinMerge here ARM64 build is machine-scope only +@rem LLVM.LLVM here second Clang, dropped with VS's clang-cl +@rem NASM.NASM here x86/x64 assembler; ARM64 uses MSVC's armasm64 +@rem Google.AndroidGPUInspector here x64-only, profiles Android devices +@rem OpenCppCoverage.OpenCppCoverage here x86/x64 only; cannot instrument ARM64 binaries +@rem VS Clang/LLVM component with-uac see $ClangComponents +@rem Windows Driver Kit with-uac see the WDK step +@rem BinSkim no-uac NuGet publishes win-x64 only +@rem +@rem The decision itself is made in ONE place, setup-windows-steps.ps1, which +@rem marks each of these -X64Only with its reason. The plan shows them as [-] +@rem with that reason before anything runs, and :want prints it again in the log +@rem where the step would have run, so the skip and its reason stay together. +@rem --------------------------------------------------------------------------- + +@rem --------------------------------------------------------------------------- +@rem The plan: what this run will do, shown BEFORE any of it happens +@rem +@rem setup-windows-plan.ps1 lists every step this file and its two halves can +@rem take - from the catalog in setup-windows-steps.ps1, marked for this host's +@rem architecture and elevation - and lets you switch steps off and on. Nothing +@rem below runs unless it was on that list and still selected when you pressed +@rem Enter; with no console to ask on, it stops unless given -Yes. Arguments to +@rem this script go straight to the planner: +@rem setup-windows.bat -WhatIf show the plan, do nothing +@rem setup-windows.bat -Skip VirtualBox,Brave start with those switched off +@rem setup-windows.bat -Only E -Yes just the elevated half, no questions +@rem +@rem The choice comes back as a scratch .cmd of `set` lines, called here: +@rem STEP_, WHY_, PKG_ per step this file takes - see :want +@rem UAC_STEPS, USER_STEPS comma lists passed to the halves as -Only +@rem PLAN_RUN, PLAN_RC go on (1), or stop and exit with PLAN_RC +@rem No file at all means the planner failed - it exits 0 even on a mistyped +@rem parameter - so that stops the run too, before anything has been done. +@rem --------------------------------------------------------------------------- +set "PLAN_ARGS=%*" +if "%~1"=="/?" set "PLAN_ARGS=-Help" +if "%~1"=="-?" set "PLAN_ARGS=-Help" +set "PLAN_CMD=%TEMP%\setup-windows-plan-%RANDOM%%RANDOM%.cmd" +if exist "%PLAN_CMD%" del "%PLAN_CMD%" +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-plan.ps1" -HostArch "%HOST_ARCH%" -Elevation "%ELEV%" -OutFile "%PLAN_CMD%" %PLAN_ARGS% +if not exist "%PLAN_CMD%" ( + echo [setup-windows] No plan was made ^(see above^), so nothing has been done. + exit /b 1 +) +call "%PLAN_CMD%" +del "%PLAN_CMD%" +if not "%PLAN_RUN%"=="1" exit /b %PLAN_RC% + +@rem --- Claude Code, from Anthropic's own installer rather than winget --- +@rem +@rem NOT `winget install Anthropic.ClaudeCode` any more: recent releases are +@rem broken through that route. This is the documented native install, +@rem `irm https://claude.ai/install.ps1 | iex`. The script downloads the current +@rem release's claude.exe, checks it against the SHA-256 in that release's +@rem manifest, and runs `claude install`, which puts the launcher in +@rem %USERPROFILE%\.local\bin - per-user, no elevation. Re-running it updates to +@rem the latest release. +@rem +@rem Two things are fixed up around it, both about which build it gets: +@rem - It picks win32-arm64 or win32-x64 from $env:PROCESSOR_ARCHITECTURE and +@rem nothing else - the variable the block at the top of this file shows to be +@rem WRONG in an emulated x64 shell on ARM64, where it would hand you the +@rem emulated build. So %HOST_ARCH%, the registry's answer, is written into it +@rem for the installer's process first. That only corrects the value; anything +@rem else in that process that reads it sees the truth too. +@rem - It refuses a 32-bit PowerShell outright ("does not support 32-bit +@rem Windows"), and a bare `powershell` from a 32-bit cmd.exe is exactly that. +@rem %SystemRoot%\Sysnative reaches the 64-bit one from there - it exists only +@rem for 32-bit processes - so it is used when present. +@rem +@rem A copy left behind by the old winget route is a second claude.exe, reached +@rem through WinGet\Links on the user PATH, and it can win over this one. It is +@rem reported, not removed: `winget uninstall Anthropic.ClaudeCode` is yours to run. +@rem +@rem The installer has no "already current" check - it downloads and reinstalls +@rem the whole release every time - so setup-windows-current.ps1 asks first: is +@rem the native claude.exe already the latest release, and the build for this +@rem host? Only a clear yes skips it; a check that cannot decide installs. +set "PS64=powershell" +if exist "%SystemRoot%\Sysnative\WindowsPowerShell\v1.0\powershell.exe" set "PS64=%SystemRoot%\Sysnative\WindowsPowerShell\v1.0\powershell.exe" +call :want ClaudeCode || goto :after_claude +"%PS64%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-current.ps1" -Step ClaudeCode -HostArch "%HOST_ARCH%" +if "%ERRORLEVEL%"=="0" goto :claude_leftover +echo [setup-windows] ClaudeCode: irm https://claude.ai/install.ps1 ^| iex ^(as %HOST_ARCH%^) +"%PS64%" -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $env:PROCESSOR_ARCHITECTURE = '%HOST_ARCH%'; irm https://claude.ai/install.ps1 | iex" +if not "%ERRORLEVEL%"=="0" echo [setup-windows] WARNING: the Claude Code installer failed ^(exit %ERRORLEVEL%^); see above. Continuing. +:claude_leftover +set "CLAUDE_WINGET=" +if exist "%LOCALAPPDATA%\Microsoft\WinGet\Links\claude.exe" set "CLAUDE_WINGET=1" +for /d %%D in ("%LOCALAPPDATA%\Microsoft\WinGet\Packages\Anthropic.ClaudeCode_*") do set "CLAUDE_WINGET=1" +if defined CLAUDE_WINGET ( + echo [setup-windows] WARNING: Claude Code is ALSO installed through winget ^(Anthropic.ClaudeCode^), the + echo [setup-windows] route this script no longer uses. That claude.exe can win over the new one on + echo [setup-windows] the PATH. Remove it with: winget uninstall Anthropic.ClaudeCode +) +:after_claude + @rem --- Non-admin (per-user) winget installs --- @rem @rem No --architecture anywhere on purpose. winget already picks the best @@ -41,12 +200,19 @@ echo [setup-windows] Host architecture: %HOST_ARCH% @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.DotNet.SDK.10 -winget install Microsoft.PowerShell Microsoft.Sysinternals.ProcessExplorer Microsoft.Sysinternals.ProcessMonitor Microsoft.Sysinternals.SDelete Microsoft.VisualStudioCode Microsoft.WindowsTerminal -winget install Python.Python.3.13 +@rem +@rem One :winget call per package, each named by its step in the plan, so any of +@rem them can be switched off. Brave is x64-only: the emulated x64 browser is the +@rem slowest thing on an ARM64 box, and Edge is in-box and native there. +call :winget Brave Brave.Brave +call :winget Git Git.Git +call :winget PowerShell Microsoft.PowerShell +call :winget ProcessExplorer Microsoft.Sysinternals.ProcessExplorer +call :winget ProcessMonitor Microsoft.Sysinternals.ProcessMonitor +call :winget SDelete Microsoft.Sysinternals.SDelete +call :winget VSCode Microsoft.VisualStudioCode +call :winget WindowsTerminal Microsoft.WindowsTerminal +call :winget Python 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 @@ -55,17 +221,11 @@ winget install Python.Python.3.13 @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 That asymmetry is why ARM64 no longer installs it at all: the only build a +@rem normal user can install there is the emulated one, and asking for --architecture +@rem arm64 just fails unless the run is elevated. VS Code's diff view and +@rem `git difftool` cover the same ground natively. +call :winget WinMerge WinMerge.WinMerge @rem --- Build drivers, standalone and native --- @rem @@ -87,56 +247,136 @@ if "%IS_ARM64%"=="1" ( @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. -winget install Kitware.CMake -winget install Ninja-build.Ninja +@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. +call :winget Ninja Ninja-build.Ninja -@rem --- Second, independent Clang: compiler diversity --- +@rem --- Second, independent Clang: compiler diversity, on x64 only --- @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 On x64 the elevated half installs Visual Studio's Clang component (clang-cl). +@rem This is a DIFFERENT LLVM: the upstream release, on its own schedule and +@rem 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 NOT ON ARM64, and not for lack of a native build - the manifest does resolve +@rem to LLVM--woa64.exe there. It is dropped because the ARM64 box carries +@rem MSVC alone: with VS's clang-cl component gone too, a third compiler with +@rem nothing to disagree with is just disk. Do the multi-compiler build on the +@rem x64 machine, which keeps all three. @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 PATH ORDER MATTERS on x64 if you use it. Should this installer put its bin on +@rem the 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 +call :winget LLVM LLVM.LLVM + +@rem --- Rust, via rustup --- +@rem +@rem Per-user on both architectures: rustup-init keeps rustup and every +@rem toolchain it manages under %USERPROFILE%\.cargo and \.rustup, and puts +@rem .cargo\bin on the user PATH. The manifest publishes an aarch64 rustup-init, +@rem so ARM64 gets a native rustup. +@rem +@rem winget runs it as `rustup-init -y -q`, which also installs a stable +@rem toolchain for whatever host rustup detects. Which toolchain is the DEFAULT - +@rem stable MSVC for this host, x86_64 or aarch64 - is then pinned explicitly by +@rem the RustToolchain step in setup-windows-no-uac.ps1 rather than left to that +@rem detection; see Set-RustDefaultToolchain there for why. +@rem +@rem The -msvc toolchains link with MSVC's link.exe, so building needs the Visual +@rem Studio the elevated half installs. Installing rustup before it is fine: +@rem with -y, rustup-init only warns "installing msvc toolchain without its +@rem prerequisites" (rustup 1.29.1, src/cli/self_update/windows.rs) - it does not +@rem go and install a Visual Studio of its own. +@rem +@rem Safe to re-run as it stands. rustup registers itself in Add/Remove Programs +@rem under the key "Rustup" - the manifest's ProductCode - and rewrites its +@rem DisplayVersion when it updates itself, so winget always sees the version +@rem that is really there and only runs rustup-init again for a newer one. +call :winget Rustup Rustlang.Rustup + +@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. +call :winget iPerf3 ar51an.iPerf3 -@rem x64-only manifests: winget falls back to the x64 installer on ARM64 and these -@rem run under Prism emulation. Harmless for what they do here - iperf3 is a -@rem network benchmark bounded by the link, and AGI is an Android-side GPU -@rem profiler whose work happens on the phone. Neither is CPU-bound on this box. -winget install ar51an.iPerf3 -winget install Google.AndroidGPUInspector +@rem Android SDK Platform-Tools: adb and fastboot. A portable zip, so per-user: +@rem winget unpacks it under %LOCALAPPDATA%\Microsoft\WinGet\Packages and puts +@rem that directory on the user PATH, rather than linking the exes one by one, +@rem because adb.exe needs AdbWinApi.dll and AdbWinUsbApi.dll beside it. +@rem +@rem The manifest labels it x64, but every binary in the zip is 32-bit x86 +@rem (machine type 0x014C, checked on r37.0.1), and there is no ARM64 build. So it +@rem runs under WOW64 on x64 and under x86 emulation on ARM64 - which, unlike x64 +@rem emulation, every Windows on Arm has. Kept on ARM64 anyway, for the iperf3 +@rem reason: adb is bound by the USB or TCP link to the device, not by the CPU. +call :winget PlatformTools Google.PlatformTools @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 build of it. x64 keeps it; ARM64 does not, because an emulated assembler is +@rem only worth carrying if you cross-compile x86/x64 here, and that work belongs +@rem on the x64 box. The ARM64 assembler is armasm64.exe, which ships with MSVC. +@rem +@rem Its installer is also one of the two here that fall back to a per-user +@rem directory when it cannot write to Program Files - C:\Users\\AppData\ +@rem Local\bin\NASM, registered nowhere, so `winget uninstall` cannot find it +@rem afterwards. Uninstall.exe in that directory is the only way back out. +@rem +@rem Registered nowhere also means winget cannot see it is installed, so it +@rem reruns the installer on every run - measured, with the same 3.02 on both +@rem sides. setup-windows-current.ps1 compares that per-user nasm.exe with the +@rem version winget offers first, and only a match skips the install. A +@rem machine-wide NASM is registered, so it is left to winget as usual. +call :want NASM || goto :after_nasm +"%PS64%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-current.ps1" -Step NASM -HostArch "%HOST_ARCH%" +if not "%ERRORLEVEL%"=="0" call :winget NASM NASM.NASM +:after_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 a failed driver install, so the plan marks it x64-only and says 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 -) +call :winget VirtualBox 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 The four are -Admin in setup-windows-steps.ps1, so on such an account the +@rem plan shows them as unavailable, with the reason, and :want skips each one. +@rem Re-run from an administrator account to get them. +@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 +@rem On ARM64 the group is just the first two - AGI and OpenCppCoverage are in the +@rem x64-only set, so there they are skipped before elevation is even consulted. +@rem AGI has no ARM64 build, and it profiles Android devices rather than +@rem anything compiled here, so emulating it is not worth the disk. +@rem --------------------------------------------------------------------------- +call :winget DotNetSdk Microsoft.DotNet.SDK.10 +call :winget CMake Kitware.CMake +call :winget AndroidGPUInspector 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, @@ -144,11 +384,10 @@ if "%IS_ARM64%"=="1" ( @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 -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 stepping x86/x64 instructions, so it can cover x86/x64 binaries but NOT an +@rem ARM64 one. That is what puts it in the x64-only set: on ARM64 it could only +@rem ever cover the cross-compiled x64 build, which is a job for the x64 box. +call :winget OpenCppCoverage OpenCppCoverage.OpenCppCoverage @rem --- WiX 5.0.2, pinned on purpose --- @rem WiX packages our proprietary software into MSIs, and the version is pinned to keep that @@ -172,22 +411,36 @@ if "%IS_ARM64%"=="1" echo [setup-windows] NOTE: OpenCppCoverage is x86/x64-only @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 But update reinstalls even when 5.0.2 is already what is there, so first look in the +@rem global tool store, which keeps one directory per installed version under +@rem %USERPROFILE%\.dotnet\tools\.store\wix. With 5.0.2 there and its shim in place, nothing +@rem runs; install-then-update is left for a first install, or for pulling a tool that has +@rem drifted to another version back to the pin. +@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. +call :want WiX || goto :after_wix +set "WIX_VERSION=5.0.2" +set "WIX_SHIM=%USERPROFILE%\.dotnet\tools\wix.exe" +if exist "%USERPROFILE%\.dotnet\tools\.store\wix\%WIX_VERSION%\" if exist "%WIX_SHIM%" ( + echo [setup-windows] WiX: %WIX_VERSION% is already the installed version; not reinstalling. + goto :wix_report +) 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%" echo [setup-windows] WARNING: dotnet.exe not found; skipping the WiX %WIX_VERSION% 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 +"%DOTNET_EXE%" tool install --global wix --version %WIX_VERSION% || "%DOTNET_EXE%" tool update --global wix --version %WIX_VERSION% @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 +:wix_report +"%WIX_SHIM%" --version :after_wix @@ -212,12 +465,107 @@ if not exist "%DOTNET_EXE%" goto :after_wix @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" + +@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. +@rem +@rem It is handed exactly the elevated steps the plan left selected (-Only, as one +@rem comma list, which it splits) and -Yes, since the question was asked before +@rem anything ran. With none selected there is nothing to elevate FOR, so no +@rem prompt is raised at all. +if "%ELEV%"=="NOLUA" goto :elev_impossible +if not defined UAC_STEPS goto :elev_not_selected + +@rem The last run's log goes only now, when a new one is about to replace it: a +@rem run that leaves the elevated half out keeps the log of the one that did it. if exist "%UAC_LOG%" del "%UAC_LOG%" +if "%ELEV%"=="ALREADY" goto :elev_direct -powershell -NoProfile -Command "$p = Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','""%~dp0setup-windows-with-uac.ps1""' -Wait -PassThru; exit $p.ExitCode" +@rem Not elevated, UAC is on: request it. Expect a prompt - a Yes/No for an +@rem administrator, and for a standard account one that asks for an +@rem administrator's name and password, after which the elevated half runs as +@rem THAT account. +@rem +@rem Process.Start with the runas verb, NOT Start-Process -Verb RunAs, which this +@rem line used to be. Two reasons: +@rem - When Start-Process fails - a cancelled prompt, most often - `$p` stays +@rem empty and `exit $p.ExitCode` exited 0: a failed elevation reported as a +@rem successful one. +@rem - It cannot be told apart even when caught. Windows PowerShell's +@rem Start-Process rethrows the failure as a plain InvalidOperationException +@rem and drops the Win32Exception that says why (checked: a missing program +@rem came back as exit 1, not 2). Process.Start keeps it, so the exit code +@rem here is the real Windows error - 1223, ERROR_CANCELLED, for a declined +@rem prompt - and the message is printed where it happens. +@rem If the elevated process does start, its own exit code comes back as before. +@rem Arguments is one string, passed through verbatim - the same hand-built form +@rem Invoke-VsModify uses in the elevated half. +@rem +@rem NO QUOTE CHARACTERS INSIDE THE -Command STRING. This line used to wrap the +@rem script path in "" pairs, and powershell.exe's command-line parser does not +@rem read those the way they look: the first "" yields a quote but leaves the +@rem quoted region, and the second - now outside it - yields nothing at all. The +@rem path's CLOSING quote vanished. That went unnoticed while the path was the +@rem last argument, because an unclosed quote simply runs to the end of the line. +@rem Once -Only and -Yes followed it, they were swallowed into the -File path, +@rem and the elevated PowerShell refused a script that did not exist (exit +@rem -196608) before running a single line: no transcript, and a window that +@rem closed at once. Measured with a stand-in that records its own command line: +@rem -File "C:\...\setup-windows-with-uac.ps1 -Only OpenSshClient,... -Yes +@rem So the path and the step list travel as environment variables, and the one +@rem quote the child needs is [char]34, made inside PowerShell where no parser in +@rem between can eat it. +echo [setup-windows] Requesting elevation ^(expect a UAC prompt^)... +set "UAC_SCRIPT=%~dp0setup-windows-with-uac.ps1" +powershell -NoProfile -Command "try { $q = [char]34; $i = New-Object Diagnostics.ProcessStartInfo 'powershell'; $i.Verb = 'runas'; $i.UseShellExecute = $true; $i.Arguments = '-NoProfile -ExecutionPolicy Bypass -File ' + $q + $env:UAC_SCRIPT + $q + ' -Only ' + $env:UAC_STEPS + ' -Yes'; $p = [Diagnostics.Process]::Start($i); $p.WaitForExit(); exit $p.ExitCode } catch { $e = $_.Exception; while ($e -and $e -isnot [ComponentModel.Win32Exception]) { $e = $e.InnerException }; Write-Host ('[setup-windows] Could not start the elevated half: ' + $(if ($e) { $e.Message } else { $_.Exception.Message })); if ($e) { exit $e.NativeErrorCode } else { exit 1 } }" 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" -Only %UAC_STEPS% -Yes +set "UAC_RC=%ERRORLEVEL%" +goto :elev_done + +:elev_not_selected +echo. +echo [setup-windows] No elevated steps were selected, so no elevation is requested. +set "UAC_RC=NOTSELECTED" +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 and its MSVC toolchain, rsync, and the OpenSSH +echo [setup-windows] 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. That has more than one cause: the +@rem prompt was declined; it started unelevated and stopped on its own #Requires +@rem line; or it was elevated and then refused to start in PowerShell's OWN +@rem startup, before the script's first line. The last one is real: the lost +@rem quote described at the launch line above produced it, and it was reported +@rem here as a declined prompt when the prompt had in fact been accepted. The +@rem exit code tells them apart, so report that instead of guessing. +if "%UAC_RC%"=="SKIPPED" goto :after_uac_log +if "%UAC_RC%"=="NOTSELECTED" goto :after_uac_log if exist "%UAC_LOG%" ( echo. echo ===== elevated setup log ^(%UAC_LOG%^) ===== @@ -225,8 +573,14 @@ if exist "%UAC_LOG%" ( echo ===== end of elevated setup log ===== ) else ( echo [setup-windows] WARNING: no elevated log found at "%UAC_LOG%". - echo [setup-windows] The elevated window may have been cancelled at the UAC prompt. + if "%UAC_RC%"=="1223" echo [setup-windows] The UAC prompt was declined or cancelled, so the elevated half never ran. + if not "%UAC_RC%"=="1223" if "%ELEV%"=="ALREADY" echo [setup-windows] The elevated half exited ^(code %UAC_RC%^) before writing its log; see its output above. + if not "%UAC_RC%"=="1223" if "%ELEV%"=="PROMPT" echo [setup-windows] The elevated half exited ^(code %UAC_RC%^) before it could write its log. Its window + if not "%UAC_RC%"=="1223" if "%ELEV%"=="PROMPT" echo [setup-windows] closes as it exits, so to read why, run it by hand from an Administrator prompt: + if not "%UAC_RC%"=="1223" if "%ELEV%"=="PROMPT" echo [setup-windows] powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-with-uac.ps1" + if "%UAC_RC%"=="-196608" echo [setup-windows] -196608 is PowerShell rejecting its own command line, e.g. a -File path that does not exist as given. ) +:after_uac_log @rem --- Non-elevated PowerShell half --- @rem WinMerge on the user PATH, BinSkim, and the global git config (identity + @@ -246,9 +600,23 @@ if exist "%UAC_LOG%" ( @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" +@rem +@rem Handed the per-user steps the plan left selected, and -Yes, just as the +@rem elevated half is; with none selected it is not started at all. +if not defined USER_STEPS goto :user_not_selected +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-no-uac.ps1" -Only %USER_STEPS% -Yes if not "%ERRORLEVEL%"=="0" echo [setup-windows] WARNING: setup-windows-no-uac.ps1 reported a failure ^(see above^); continuing. +goto :after_user_steps +:user_not_selected +echo [setup-windows] No per-user configuration steps were selected; setup-windows-no-uac.ps1 not run. +:after_user_steps +@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. Not selecting any of it is a choice, not a failure. +if "%UAC_RC%"=="SKIPPED" goto :uac_reported +if "%UAC_RC%"=="NOTSELECTED" goto :uac_reported if not "%UAC_RC%"=="0" ( echo. echo [setup-windows] ELEVATED SETUP FAILED ^(exit code %UAC_RC%^). See log above. @@ -256,27 +624,141 @@ if not "%UAC_RC%"=="0" ( ) 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 +@rem --- Find an interpreter by path, rather than trusting `py` --- +@rem +@rem `py -m pip install Pillow` used to be this line, and it failed with +@rem 'py' is not recognized as an internal or external command +@rem for two independent reasons, which look identical at the prompt: +@rem +@rem 1. PATH staleness. winget installed Python a few lines above, but this cmd +@rem session inherited its environment before that, so nothing winget added +@rem is visible here. Same problem the DOTNET_EXE lookup solves above, and it +@rem would bite even where the launcher IS installed. +@rem +@rem 2. The launcher may simply not be there. Measured on the ARM64 box: every +@rem other Python component registered - Core Interpreter, Executables, +@rem Standard Library, pip Bootstrap, Tcl/Tk, Add to Path - and there is no +@rem launcher entry and no py.exe anywhere on disk, while the Launcher +@rem directory it would occupy is on the user PATH but does not exist. +@rem +@rem So resolve an interpreter by full path and prefer the launcher only when it +@rem is real. python.exe is the thing that actually has to exist; `py` is a +@rem convenience shim that picks between several of them, and there is exactly one +@rem here. Reverse name order so the newest Python wins, and require python.exe +@rem inside the directory - a half-removed version leaves the folder behind with +@rem only Doc and Lib in it, which is exactly what this box has. +call :want Pillow || goto :after_pillow +set "PY_EXE=" +if exist "%LOCALAPPDATA%\Programs\Python\Launcher\py.exe" set "PY_EXE=%LOCALAPPDATA%\Programs\Python\Launcher\py.exe" +if not defined PY_EXE if exist "%WINDIR%\py.exe" set "PY_EXE=%WINDIR%\py.exe" +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" + +@rem Pillow publishes win_arm64 wheels alongside win_amd64, so the ARM64 Python +@rem winget just installed gets a native wheel and never falls back to building +@rem from source. Any package WITHOUT an ARM64 wheel does fall back, and that +@rem source build needs the MSVC toolchain the elevated half installs. +if not defined PY_EXE ( + echo [setup-windows] WARNING: no Python interpreter found; skipping the Pillow install. + echo [setup-windows] Looked for the py launcher, then %LOCALAPPDATA%\Programs\Python\Python3*\python.exe. +) else ( + echo [setup-windows] Python: %PY_EXE% + "%PY_EXE%" -m pip install Pillow +) +:after_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] Sysinternals ^(the *64a.exe variants^), Claude Code, CMake, + echo [setup-windows] Ninja, rustup, and the SDK/WPT tools. + echo [setup-windows] compilers : MSVC from Visual Studio 2026, native ARM64 - the only C/C++ + echo [setup-windows] compiler by design; clang-cl and upstream LLVM are x64-box only. + echo [setup-windows] Rust stable-aarch64-pc-windows-msvc, native, linking through MSVC. + echo [setup-windows] emulated : iperf3 ^(x64^) and adb ^(x86^), both bound by their link, not the CPU. + echo [setup-windows] not here : Brave, WinMerge, LLVM, NASM, AGI, OpenCppCoverage, BinSkim + echo [setup-windows] and the WDK - dropped on ARM64; the plan above says why. 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 MSVC, the .NET SDK and CMake. The + echo [setup-windows] architecture audit above lists what is really installed. +) + +exit /b 0 + +@rem --------------------------------------------------------------------------- +@rem :want +@rem +@rem Exit 0 if the plan selected this step, so the command after it runs. +@rem Otherwise exit 1, first printing the reason if it is one that cannot run on +@rem this machine at all. A step you simply switched off passes silently: the +@rem plan said so a moment ago, and a line for each would bury the skips that +@rem have a reason. Used as +@rem call :want Id && +@rem call :want Id || goto :after_id +@rem +@rem It replaces :x64only and keeps that subroutine's rule: the reason is never +@rem separated from the skip. The log line a future reader sees is the text the +@rem plan showed, from setup-windows-steps.ps1 - still exactly one place to change +@rem the policy - because the interesting case is reading the run log six months +@rem later and wondering where NASM went. +@rem +@rem A step with no STEP_ line at all is one the catalog does not know, so the +@rem two have drifted apart. It is refused rather than run: it was never on the +@rem plan you agreed to. +@rem +@rem Delayed expansion, kept local, to read STEP_ by a computed name and to +@rem print the reason without cmd re-parsing an & | < > or paren inside it. +@rem --------------------------------------------------------------------------- +:want +setlocal EnableDelayedExpansion +set "_state=!STEP_%~1!" +if "!_state!"=="1" (endlocal & exit /b 0) +if not defined STEP_%~1 ( + echo [setup-windows] BUG: step %~1 is not in the plan - setup-windows-steps.ps1 and this file disagree. Not run. + endlocal & exit /b 1 +) +if defined WHY_%~1 echo [setup-windows] Skipping %~1: !WHY_%~1! +endlocal & exit /b 1 + +@rem --------------------------------------------------------------------------- +@rem :winget +@rem +@rem Re-running is safe and cheap. On a package winget can see is installed, +@rem `winget install` upgrades it when a newer version exists and otherwise does +@rem nothing - its documented default ("check package installed status and try to +@rem perform an upgrade if applicable"; only --force, never used here, installs +@rem regardless). The installs winget cannot see are checked before they get here. +@rem +@rem winget install the package, if the plan selected the step. The package id is +@rem written at the call site as well as in the catalog so that this file still +@rem says what it installs. Should the two ever disagree, the plan showed you +@rem something other than what is about to be installed, so it is not run. +@rem --------------------------------------------------------------------------- +:winget +call :want %1 || exit /b 0 +setlocal EnableDelayedExpansion +if /i not "!PKG_%~1!"=="%~2" ( + echo [setup-windows] BUG: the plan showed %~1 as "!PKG_%~1!" but this file installs "%~2". Not run. + endlocal & exit /b 0 +) +endlocal +echo [setup-windows] %~1: winget install %~2 +winget install %2 +exit /b 0