@echo off\r
+@rem The plan below hands back a few dozen STEP_/WHY_/PKG_ variables; keep them,\r
+@rem and everything else this script sets, out of the calling cmd session.\r
+setlocal\r
\r
@rem ---------------------------------------------------------------------------\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
+@rem Before it does anything, it shows the plan - every step it and its two\r
+@rem halves can take, marked for this machine - and lets you choose. See "The\r
+@rem plan" below, or run `setup-windows.bat /?`.\r
@rem ---------------------------------------------------------------------------\r
\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 hives. This is load-bearing now: the architecture decides which packages\r
+@rem are 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 The value drives two 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 plan: every x64-only step, VirtualBox included, is marked unavailable\r
+@rem on ARM64 and so never selected (see setup-windows-steps.ps1)\r
@rem - the summary tells you which tools you got as emulated x64\r
@rem ---------------------------------------------------------------------------\r
set "HOST_ARCH=%PROCESSOR_ARCHITECTURE%"\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
+ echo [setup-windows] need a machine-wide install will be SKIPPED rather than left to fail; the\r
+ echo [setup-windows] plan below marks each one, and there is a summary before the elevated half.\r
)\r
\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 full set, ARM64 installs ONE C/C++ compiler - MSVC from Visual Studio - and\r
@rem nothing that merely duplicates it or that exists only as an x64 binary.\r
+@rem Rust does not break that rule: it is a different language, not a second\r
+@rem C/C++ compiler, and its native aarch64 MSVC toolchain links through MSVC.\r
@rem\r
-@rem Dropped on ARM64, and where each one is dropped:\r
+@rem Dropped on ARM64, and which script would otherwise take each one:\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 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 The decision itself is made in ONE place, setup-windows-steps.ps1, which\r
+@rem marks each of these -X64Only with its reason. The plan shows them as [-]\r
+@rem with that reason before anything runs, and :want prints it again in the log\r
+@rem where the step would have run, so the skip and its reason stay together.\r
+@rem ---------------------------------------------------------------------------\r
+\r
@rem ---------------------------------------------------------------------------\r
+@rem The plan: what this run will do, shown BEFORE any of it happens\r
+@rem\r
+@rem setup-windows-plan.ps1 lists every step this file and its two halves can\r
+@rem take - from the catalog in setup-windows-steps.ps1, marked for this host's\r
+@rem architecture and elevation - and lets you switch steps off and on. Nothing\r
+@rem below runs unless it was on that list and still selected when you pressed\r
+@rem Enter; with no console to ask on, it stops unless given -Yes. Arguments to\r
+@rem this script go straight to the planner:\r
+@rem setup-windows.bat -WhatIf show the plan, do nothing\r
+@rem setup-windows.bat -Skip VirtualBox,Brave start with those switched off\r
+@rem setup-windows.bat -Only E -Yes just the elevated half, no questions\r
+@rem\r
+@rem The choice comes back as a scratch .cmd of `set` lines, called here:\r
+@rem STEP_<Id>, WHY_<Id>, PKG_<Id> per step this file takes - see :want\r
+@rem UAC_STEPS, USER_STEPS comma lists passed to the halves as -Only\r
+@rem PLAN_RUN, PLAN_RC go on (1), or stop and exit with PLAN_RC\r
+@rem No file at all means the planner failed - it exits 0 even on a mistyped\r
+@rem parameter - so that stops the run too, before anything has been done.\r
+@rem ---------------------------------------------------------------------------\r
+set "PLAN_ARGS=%*"\r
+if "%~1"=="/?" set "PLAN_ARGS=-Help"\r
+if "%~1"=="-?" set "PLAN_ARGS=-Help"\r
+set "PLAN_CMD=%TEMP%\setup-windows-plan-%RANDOM%%RANDOM%.cmd"\r
+if exist "%PLAN_CMD%" del "%PLAN_CMD%"\r
+powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-plan.ps1" -HostArch "%HOST_ARCH%" -Elevation "%ELEV%" -OutFile "%PLAN_CMD%" %PLAN_ARGS%\r
+if not exist "%PLAN_CMD%" (\r
+ echo [setup-windows] No plan was made ^(see above^), so nothing has been done.\r
+ exit /b 1\r
+)\r
+call "%PLAN_CMD%"\r
+del "%PLAN_CMD%"\r
+if not "%PLAN_RUN%"=="1" exit /b %PLAN_RC%\r
+\r
+@rem --- Claude Code, from Anthropic's own installer rather than winget ---\r
+@rem\r
+@rem NOT `winget install Anthropic.ClaudeCode` any more: recent releases are\r
+@rem broken through that route. This is the documented native install,\r
+@rem `irm https://claude.ai/install.ps1 | iex`. The script downloads the current\r
+@rem release's claude.exe, checks it against the SHA-256 in that release's\r
+@rem manifest, and runs `claude install`, which puts the launcher in\r
+@rem %USERPROFILE%\.local\bin - per-user, no elevation. Re-running it updates to\r
+@rem the latest release.\r
+@rem\r
+@rem Two things are fixed up around it, both about which build it gets:\r
+@rem - It picks win32-arm64 or win32-x64 from $env:PROCESSOR_ARCHITECTURE and\r
+@rem nothing else - the variable the block at the top of this file shows to be\r
+@rem WRONG in an emulated x64 shell on ARM64, where it would hand you the\r
+@rem emulated build. So %HOST_ARCH%, the registry's answer, is written into it\r
+@rem for the installer's process first. That only corrects the value; anything\r
+@rem else in that process that reads it sees the truth too.\r
+@rem - It refuses a 32-bit PowerShell outright ("does not support 32-bit\r
+@rem Windows"), and a bare `powershell` from a 32-bit cmd.exe is exactly that.\r
+@rem %SystemRoot%\Sysnative reaches the 64-bit one from there - it exists only\r
+@rem for 32-bit processes - so it is used when present.\r
+@rem\r
+@rem A copy left behind by the old winget route is a second claude.exe, reached\r
+@rem through WinGet\Links on the user PATH, and it can win over this one. It is\r
+@rem reported, not removed: `winget uninstall Anthropic.ClaudeCode` is yours to run.\r
+@rem\r
+@rem The installer has no "already current" check - it downloads and reinstalls\r
+@rem the whole release every time - so setup-windows-current.ps1 asks first: is\r
+@rem the native claude.exe already the latest release, and the build for this\r
+@rem host? Only a clear yes skips it; a check that cannot decide installs.\r
+set "PS64=powershell"\r
+if exist "%SystemRoot%\Sysnative\WindowsPowerShell\v1.0\powershell.exe" set "PS64=%SystemRoot%\Sysnative\WindowsPowerShell\v1.0\powershell.exe"\r
+call :want ClaudeCode || goto :after_claude\r
+"%PS64%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-current.ps1" -Step ClaudeCode -HostArch "%HOST_ARCH%"\r
+if "%ERRORLEVEL%"=="0" goto :claude_leftover\r
+echo [setup-windows] ClaudeCode: irm https://claude.ai/install.ps1 ^| iex ^(as %HOST_ARCH%^)\r
+"%PS64%" -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $env:PROCESSOR_ARCHITECTURE = '%HOST_ARCH%'; irm https://claude.ai/install.ps1 | iex"\r
+if not "%ERRORLEVEL%"=="0" echo [setup-windows] WARNING: the Claude Code installer failed ^(exit %ERRORLEVEL%^); see above. Continuing.\r
+:claude_leftover\r
+set "CLAUDE_WINGET="\r
+if exist "%LOCALAPPDATA%\Microsoft\WinGet\Links\claude.exe" set "CLAUDE_WINGET=1"\r
+for /d %%D in ("%LOCALAPPDATA%\Microsoft\WinGet\Packages\Anthropic.ClaudeCode_*") do set "CLAUDE_WINGET=1"\r
+if defined CLAUDE_WINGET (\r
+ echo [setup-windows] WARNING: Claude Code is ALSO installed through winget ^(Anthropic.ClaudeCode^), the\r
+ echo [setup-windows] route this script no longer uses. That claude.exe can win over the new one on\r
+ echo [setup-windows] the PATH. Remove it with: winget uninstall Anthropic.ClaudeCode\r
+)\r
+:after_claude\r
\r
@rem --- Non-admin (per-user) winget installs ---\r
@rem\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
-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
+@rem\r
+@rem One :winget call per package, each named by its step in the plan, so any of\r
+@rem them can be switched off. Brave is x64-only: the emulated x64 browser is the\r
+@rem slowest thing on an ARM64 box, and Edge is in-box and native there.\r
+call :winget Brave Brave.Brave\r
+call :winget Git Git.Git\r
+call :winget PowerShell Microsoft.PowerShell\r
+call :winget ProcessExplorer Microsoft.Sysinternals.ProcessExplorer\r
+call :winget ProcessMonitor Microsoft.Sysinternals.ProcessMonitor\r
+call :winget SDelete Microsoft.Sysinternals.SDelete\r
+call :winget VSCode Microsoft.VisualStudioCode\r
+call :winget WindowsTerminal Microsoft.WindowsTerminal\r
+call :winget Python Python.Python.3.13\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 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
+call :winget WinMerge WinMerge.WinMerge\r
\r
@rem --- Build drivers, standalone and native ---\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
+call :winget Ninja Ninja-build.Ninja\r
\r
@rem --- Second, independent Clang: compiler diversity, on x64 only ---\r
@rem\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
+call :winget LLVM LLVM.LLVM\r
+\r
+@rem --- Rust, via rustup ---\r
+@rem\r
+@rem Per-user on both architectures: rustup-init keeps rustup and every\r
+@rem toolchain it manages under %USERPROFILE%\.cargo and \.rustup, and puts\r
+@rem .cargo\bin on the user PATH. The manifest publishes an aarch64 rustup-init,\r
+@rem so ARM64 gets a native rustup.\r
+@rem\r
+@rem winget runs it as `rustup-init -y -q`, which also installs a stable\r
+@rem toolchain for whatever host rustup detects. Which toolchain is the DEFAULT -\r
+@rem stable MSVC for this host, x86_64 or aarch64 - is then pinned explicitly by\r
+@rem the RustToolchain step in setup-windows-no-uac.ps1 rather than left to that\r
+@rem detection; see Set-RustDefaultToolchain there for why.\r
+@rem\r
+@rem The -msvc toolchains link with MSVC's link.exe, so building needs the Visual\r
+@rem Studio the elevated half installs. Installing rustup before it is fine:\r
+@rem with -y, rustup-init only warns "installing msvc toolchain without its\r
+@rem prerequisites" (rustup 1.29.1, src/cli/self_update/windows.rs) - it does not\r
+@rem go and install a Visual Studio of its own.\r
+@rem\r
+@rem Safe to re-run as it stands. rustup registers itself in Add/Remove Programs\r
+@rem under the key "Rustup" - the manifest's ProductCode - and rewrites its\r
+@rem DisplayVersion when it updates itself, so winget always sees the version\r
+@rem that is really there and only runs rustup-init again for a newer one.\r
+call :winget Rustup Rustlang.Rustup\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
+call :winget iPerf3 ar51an.iPerf3\r
+\r
+@rem Android SDK Platform-Tools: adb and fastboot. A portable zip, so per-user:\r
+@rem winget unpacks it under %LOCALAPPDATA%\Microsoft\WinGet\Packages and puts\r
+@rem that directory on the user PATH, rather than linking the exes one by one,\r
+@rem because adb.exe needs AdbWinApi.dll and AdbWinUsbApi.dll beside it.\r
+@rem\r
+@rem The manifest labels it x64, but every binary in the zip is 32-bit x86\r
+@rem (machine type 0x014C, checked on r37.0.1), and there is no ARM64 build. So it\r
+@rem runs under WOW64 on x64 and under x86 emulation on ARM64 - which, unlike x64\r
+@rem emulation, every Windows on Arm has. Kept on ARM64 anyway, for the iperf3\r
+@rem reason: adb is bound by the USB or TCP link to the device, not by the CPU.\r
+call :winget PlatformTools Google.PlatformTools\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 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
+@rem\r
+@rem Registered nowhere also means winget cannot see it is installed, so it\r
+@rem reruns the installer on every run - measured, with the same 3.02 on both\r
+@rem sides. setup-windows-current.ps1 compares that per-user nasm.exe with the\r
+@rem version winget offers first, and only a match skips the install. A\r
+@rem machine-wide NASM is registered, so it is left to winget as usual.\r
+call :want NASM || goto :after_nasm\r
+"%PS64%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-current.ps1" -Step NASM -HostArch "%HOST_ARCH%"\r
+if not "%ERRORLEVEL%"=="0" call :winget NASM NASM.NASM\r
+:after_nasm\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 a failed driver install, so the plan marks it x64-only and says 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
+call :winget VirtualBox Oracle.VirtualBox\r
\r
@rem ---------------------------------------------------------------------------\r
@rem Machine-wide installers, despite sitting in the "per-user" section\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 The four are -Admin in setup-windows-steps.ps1, so on such an account the\r
+@rem plan shows them as unavailable, with the reason, and :want skips each one.\r
+@rem Re-run from an administrator account to get them.\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\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 AGI has no ARM64 build, and it profiles Android devices rather than\r
+@rem anything compiled here, so emulating it is not worth the disk.\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
+call :winget DotNetSdk Microsoft.DotNet.SDK.10\r
+call :winget CMake Kitware.CMake\r
+call :winget AndroidGPUInspector Google.AndroidGPUInspector\r
\r
@rem OpenCppCoverage: native (PE) line coverage for the C++ binaries. run-coverage-occ.py drives the\r
@rem pytest suite under it to produce an HTML report (the binaries under test build with PDBs,\r
@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
+call :winget OpenCppCoverage OpenCppCoverage.OpenCppCoverage\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 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 But update reinstalls even when 5.0.2 is already what is there, so first look in the\r
+@rem global tool store, which keeps one directory per installed version under\r
+@rem %USERPROFILE%\.dotnet\tools\.store\wix. With 5.0.2 there and its shim in place, nothing\r
+@rem runs; install-then-update is left for a first install, or for pulling a tool that has\r
+@rem drifted to another version back to the pin.\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
+call :want WiX || goto :after_wix\r
+set "WIX_VERSION=5.0.2"\r
+set "WIX_SHIM=%USERPROFILE%\.dotnet\tools\wix.exe"\r
+if exist "%USERPROFILE%\.dotnet\tools\.store\wix\%WIX_VERSION%\" if exist "%WIX_SHIM%" (\r
+ echo [setup-windows] WiX: %WIX_VERSION% is already the installed version; not reinstalling.\r
+ goto :wix_report\r
+)\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%" echo [setup-windows] WARNING: dotnet.exe not found; skipping the WiX %WIX_VERSION% 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
+"%DOTNET_EXE%" tool install --global wix --version %WIX_VERSION% || "%DOTNET_EXE%" tool update --global wix --version %WIX_VERSION%\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
+:wix_report\r
+"%WIX_SHIM%" --version\r
\r
:after_wix\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
+@rem\r
+@rem It is handed exactly the elevated steps the plan left selected (-Only, as one\r
+@rem comma list, which it splits) and -Yes, since the question was asked before\r
+@rem anything ran. With none selected there is nothing to elevate FOR, so no\r
+@rem prompt is raised at all.\r
if "%ELEV%"=="NOLUA" goto :elev_impossible\r
+if not defined UAC_STEPS goto :elev_not_selected\r
+\r
+@rem The last run's log goes only now, when a new one is about to replace it: a\r
+@rem run that leaves the elevated half out keeps the log of the one that did it.\r
+if exist "%UAC_LOG%" del "%UAC_LOG%"\r
if "%ELEV%"=="ALREADY" goto :elev_direct\r
\r
-@rem Not elevated, UAC is on: request it. Expect a prompt.\r
+@rem Not elevated, UAC is on: request it. Expect a prompt - a Yes/No for an\r
+@rem administrator, and for a standard account one that asks for an\r
+@rem administrator's name and password, after which the elevated half runs as\r
+@rem THAT account.\r
+@rem\r
+@rem Process.Start with the runas verb, NOT Start-Process -Verb RunAs, which this\r
+@rem line used to be. Two reasons:\r
+@rem - When Start-Process fails - a cancelled prompt, most often - `$p` stays\r
+@rem empty and `exit $p.ExitCode` exited 0: a failed elevation reported as a\r
+@rem successful one.\r
+@rem - It cannot be told apart even when caught. Windows PowerShell's\r
+@rem Start-Process rethrows the failure as a plain InvalidOperationException\r
+@rem and drops the Win32Exception that says why (checked: a missing program\r
+@rem came back as exit 1, not 2). Process.Start keeps it, so the exit code\r
+@rem here is the real Windows error - 1223, ERROR_CANCELLED, for a declined\r
+@rem prompt - and the message is printed where it happens.\r
+@rem If the elevated process does start, its own exit code comes back as before.\r
+@rem Arguments is one string, passed through verbatim - the same hand-built form\r
+@rem Invoke-VsModify uses in the elevated half.\r
+@rem\r
+@rem NO QUOTE CHARACTERS INSIDE THE -Command STRING. This line used to wrap the\r
+@rem script path in "" pairs, and powershell.exe's command-line parser does not\r
+@rem read those the way they look: the first "" yields a quote but leaves the\r
+@rem quoted region, and the second - now outside it - yields nothing at all. The\r
+@rem path's CLOSING quote vanished. That went unnoticed while the path was the\r
+@rem last argument, because an unclosed quote simply runs to the end of the line.\r
+@rem Once -Only and -Yes followed it, they were swallowed into the -File path,\r
+@rem and the elevated PowerShell refused a script that did not exist (exit\r
+@rem -196608) before running a single line: no transcript, and a window that\r
+@rem closed at once. Measured with a stand-in that records its own command line:\r
+@rem -File "C:\...\setup-windows-with-uac.ps1 -Only OpenSshClient,... -Yes\r
+@rem So the path and the step list travel as environment variables, and the one\r
+@rem quote the child needs is [char]34, made inside PowerShell where no parser in\r
+@rem between can eat it.\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_SCRIPT=%~dp0setup-windows-with-uac.ps1"\r
+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 } }"\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
+powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-with-uac.ps1" -Only %UAC_STEPS% -Yes\r
set "UAC_RC=%ERRORLEVEL%"\r
goto :elev_done\r
\r
+:elev_not_selected\r
+echo.\r
+echo [setup-windows] No elevated steps were selected, so no elevation is requested.\r
+set "UAC_RC=NOTSELECTED"\r
+goto :elev_done\r
+\r
:elev_impossible\r
echo.\r
echo [setup-windows] SKIPPING the elevated half: this account cannot elevate.\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
+@rem means it never got as far as running. That has more than one cause: the\r
+@rem prompt was declined; it started unelevated and stopped on its own #Requires\r
+@rem line; or it was elevated and then refused to start in PowerShell's OWN\r
+@rem startup, before the script's first line. The last one is real: the lost\r
+@rem quote described at the launch line above produced it, and it was reported\r
+@rem here as a declined prompt when the prompt had in fact been accepted. The\r
+@rem exit code tells them apart, so report that instead of guessing.\r
if "%UAC_RC%"=="SKIPPED" goto :after_uac_log\r
+if "%UAC_RC%"=="NOTSELECTED" goto :after_uac_log\r
if exist "%UAC_LOG%" (\r
echo.\r
echo ===== elevated setup log ^(%UAC_LOG%^) =====\r
echo ===== end of elevated setup log =====\r
) else (\r
echo [setup-windows] WARNING: no elevated log found at "%UAC_LOG%".\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
+ if "%UAC_RC%"=="1223" echo [setup-windows] The UAC prompt was declined or cancelled, so the elevated half never ran.\r
+ 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.\r
+ 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\r
+ 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:\r
+ if not "%UAC_RC%"=="1223" if "%ELEV%"=="PROMPT" echo [setup-windows] powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-with-uac.ps1"\r
+ 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.\r
)\r
:after_uac_log\r
\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
+@rem\r
+@rem Handed the per-user steps the plan left selected, and -Yes, just as the\r
+@rem elevated half is; with none selected it is not started at all.\r
+if not defined USER_STEPS goto :user_not_selected\r
+powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-no-uac.ps1" -Only %USER_STEPS% -Yes\r
if not "%ERRORLEVEL%"=="0" echo [setup-windows] WARNING: setup-windows-no-uac.ps1 reported a failure ^(see above^); continuing.\r
+goto :after_user_steps\r
+:user_not_selected\r
+echo [setup-windows] No per-user configuration steps were selected; setup-windows-no-uac.ps1 not run.\r
+:after_user_steps\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
+@rem the documented next step. Not selecting any of it is a choice, not a failure.\r
if "%UAC_RC%"=="SKIPPED" goto :uac_reported\r
+if "%UAC_RC%"=="NOTSELECTED" 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
@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
+call :want Pillow || goto :after_pillow\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
echo [setup-windows] Python: %PY_EXE%\r
"%PY_EXE%" -m pip install Pillow\r
)\r
+:after_pillow\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] Ninja, rustup, and the SDK/WPT tools.\r
+ echo [setup-windows] compilers : MSVC from Visual Studio 2026, native ARM64 - the only C/C++\r
+ echo [setup-windows] compiler by design; clang-cl and upstream LLVM are x64-box only.\r
+ echo [setup-windows] Rust stable-aarch64-pc-windows-msvc, native, linking through MSVC.\r
+ echo [setup-windows] emulated : iperf3 ^(x64^) and adb ^(x86^), both bound by their link, not the CPU.\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] and the WDK - dropped on ARM64; the plan above says why.\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
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 :want <StepId>\r
+@rem\r
+@rem Exit 0 if the plan selected this step, so the command after it runs.\r
+@rem Otherwise exit 1, first printing the reason if it is one that cannot run on\r
+@rem this machine at all. A step you simply switched off passes silently: the\r
+@rem plan said so a moment ago, and a line for each would bury the skips that\r
+@rem have a reason. Used as\r
+@rem call :want Id && <command>\r
+@rem call :want Id || goto :after_id\r
+@rem\r
+@rem It replaces :x64only and keeps that subroutine's rule: the reason is never\r
+@rem separated from the skip. The log line a future reader sees is the text the\r
+@rem plan showed, from setup-windows-steps.ps1 - still exactly one place to change\r
+@rem the policy - because the interesting case is reading the run log six months\r
+@rem later and wondering where NASM went.\r
+@rem\r
+@rem A step with no STEP_ line at all is one the catalog does not know, so the\r
+@rem two have drifted apart. It is refused rather than run: it was never on the\r
+@rem plan you agreed to.\r
+@rem\r
+@rem Delayed expansion, kept local, to read STEP_<Id> by a computed name and to\r
+@rem print the reason without cmd re-parsing an & | < > or paren inside it.\r
@rem ---------------------------------------------------------------------------\r
-:x64only\r
-if not "%IS_ARM64%"=="1" (\r
- winget install %1\r
- goto :eof\r
+:want\r
+setlocal EnableDelayedExpansion\r
+set "_state=!STEP_%~1!"\r
+if "!_state!"=="1" (endlocal & exit /b 0)\r
+if not defined STEP_%~1 (\r
+ echo [setup-windows] BUG: step %~1 is not in the plan - setup-windows-steps.ps1 and this file disagree. Not run.\r
+ endlocal & exit /b 1\r
)\r
-echo [setup-windows] Skipping %1 on ARM64: %~2\r
-goto :eof\r
+if defined WHY_%~1 echo [setup-windows] Skipping %~1: !WHY_%~1!\r
+endlocal & exit /b 1\r
+\r
+@rem ---------------------------------------------------------------------------\r
+@rem :winget <StepId> <package-id>\r
+@rem\r
+@rem Re-running is safe and cheap. On a package winget can see is installed,\r
+@rem `winget install` upgrades it when a newer version exists and otherwise does\r
+@rem nothing - its documented default ("check package installed status and try to\r
+@rem perform an upgrade if applicable"; only --force, never used here, installs\r
+@rem regardless). The installs winget cannot see are checked before they get here.\r
+@rem\r
+@rem winget install the package, if the plan selected the step. The package id is\r
+@rem written at the call site as well as in the catalog so that this file still\r
+@rem says what it installs. Should the two ever disagree, the plan showed you\r
+@rem something other than what is about to be installed, so it is not run.\r
+@rem ---------------------------------------------------------------------------\r
+:winget\r
+call :want %1 || exit /b 0\r
+setlocal EnableDelayedExpansion\r
+if /i not "!PKG_%~1!"=="%~2" (\r
+ echo [setup-windows] BUG: the plan showed %~1 as "!PKG_%~1!" but this file installs "%~2". Not run.\r
+ endlocal & exit /b 0\r
+)\r
+endlocal\r
+echo [setup-windows] %~1: winget install %~2\r
+winget install %2\r
+exit /b 0\r