`setup-windows.bat` takes a fresh Windows install to a working C++ / native
development environment: editors and shells, Python, the Visual Studio
toolchain (including the Clang and Windows XP targeting toolsets), the Windows
-Driver Kit, and a handful of analysis tools (Sysinternals, OpenCppCoverage,
-BinSkim, the Windows Performance Toolkit). It also sets the box up to be driven
-remotely: OpenSSH Server plus an rsync build for Windows, which is what makes a
-throwaway VM reachable from a Linux host.
+Driver Kit, Rust (rustup, with the stable MSVC toolchain for the host as the
+default), Android's `adb`, and a handful of analysis tools (Sysinternals,
+OpenCppCoverage, BinSkim, the Windows Performance Toolkit). It also sets the
+box up to be driven remotely: OpenSSH Server plus an rsync build for Windows,
+which is what makes a throwaway VM reachable from a Linux host.
+
+**Nothing happens until you have seen the plan.** It first lists every step it
+can take on this machine and lets you choose which ones to run — see
+[Choosing what runs](#choosing-what-runs).
Runs on **x64 and on ARM64** (Windows 11 on Arm), and installs *different things*
-on each: x64 is the full workstation, ARM64 a lean single-compiler build box. See
+on each: x64 is the full workstation, ARM64 a lean build box with a single
+C/C++ compiler. See
[Running on ARM64](#running-on-arm64) for what differs and why.
## Files
| File | Purpose |
| --- | --- |
-| `setup-windows.bat` | Entry point. Runs the winget installs, then launches the elevated half and prints its log, then runs the non-elevated script. |
-| `setup-windows-no-uac.ps1` | The non-elevated, per-user half: WinMerge, BinSkim and upstream LLVM on the user `PATH` (x64 only), the global git config (identity, plus `core.sshCommand`), the native `ninja` pinned ahead of the one Visual Studio bundles, and an architecture audit of everything provisioned. Can also be run directly from an ordinary prompt. |
-| `setup-windows-with-uac.ps1` | The elevated half, started via UAC by the batch file. Enables `ssh-agent`, installs the OpenSSH Client and Server capabilities and starts `sshd`, unpacks the `rsync-windows` release zip for this architecture (`rsync.exe` plus the `ssh.exe` it runs) into `C:\Tools\rsync` on the machine `PATH`, then installs Visual Studio Community with the required components, the WDK, and the Windows Performance Toolkit, and reports whether Intel VTune Profiler is present. Can also be run directly from an Administrator prompt. |
+| `setup-windows.bat` | Entry point. Shows the plan and lets you choose, then runs the winget installs, launches the elevated half and prints its log, then runs the non-elevated script. |
+| `setup-windows-plan.ps1` | The plan. Run by the batch file before anything else: lists every step for this machine, grouped and numbered, and takes your choice. Run it on its own with `-WhatIf` to preview; add `-HostArch ARM64 -Elevation NOLUA` to preview another kind of box. |
+| `setup-windows-steps.ps1` | The catalog the other four read: every step's name, what it does, and where it cannot run and why. Defines functions only; it is dot-sourced, never run. |
+| `setup-windows-current.ps1` | Asked by the batch file before the Claude Code and NASM installs: is what is on the box already current? Those are the two installs that would otherwise run in full every time — see [Re-running](#re-running). |
+| `setup-windows-no-uac.ps1` | The non-elevated, per-user half: WinMerge, BinSkim and upstream LLVM on the user `PATH` (x64 only), the global git config (identity, plus `core.sshCommand`), the native `ninja` pinned ahead of the one Visual Studio bundles, and an architecture audit of everything provisioned. Can also be run directly from an ordinary prompt; it lists its steps and asks first. |
+| `setup-windows-with-uac.ps1` | The elevated half, started via UAC by the batch file. Enables `ssh-agent`, installs the OpenSSH Client and Server capabilities and starts `sshd`, unpacks the `rsync-windows` release zip for this architecture (`rsync.exe` plus the `ssh.exe` it runs) into `C:\Tools\rsync` on the machine `PATH`, then installs Visual Studio Community with the required components, the WDK, and the Windows Performance Toolkit, and reports whether Intel VTune Profiler is present. Can also be run directly from an Administrator prompt; it lists its steps and asks first. |
| `setup-windows-7-test-env.bat` | Prepares a **Windows 7 VM** as a test target driven from the host by `VBoxManage guestcontrol`. Copy it into the guest and run it there; it is idempotent, so re-run it after any snapshot restore. The per-user half needs no UAC (crash-dialog suppression, no screen blanking, a staging directory, the shared folder on `Z:`); the machine-wide half is skipped with a notice unless run elevated inside the guest. It then reports what the box can actually test: DWM composition, printers, audio capture devices. |
## Usage
setup-windows.bat
```
- It will raise a single UAC prompt for the elevated half. Accept it — declining
- leaves Visual Studio and the WDK uninstalled, and the script says so.
+ Before doing anything it prints the plan — every step it can take on this
+ machine, grouped and numbered — and waits:
+
+ ```text
+ Setup plan - nothing has been done yet
+ host: x64 - the full workstation
+ elevation: not elevated; expect a UAC prompt partway through, for the elevated half
+
+ U Per-user installs (winget, except Claude Code)
+ [x] 1 ClaudeCode Claude Code, native installer: irm https://claude.ai/install.ps1 | iex
+ [x] 2 Brave Brave.Brave
+ ...
+ E Elevated setup (setup-windows-with-uac.ps1) - one UAC prompt, runs in its own window
+ [x] 24 OpenSshClient OpenSSH Client capability (ssh.exe, and the libcrypto.dll rsync's ssh.exe needs)
+ ...
+ [x] 28 VisualStudio Visual Studio 2022 Community, C++: MSVC for x86/x64 + ARM64, SDK 26100 (several GB)
+ ...
+
+ 42 of 42 steps selected.
+
+ Enter runs the selected steps. To change them, type step numbers or ranges
+ (3 5-7), step names, or a group letter (U M E C) to switch them on or off;
+ 'all' or 'none' for everything; 'q' to quit without doing anything.
+ ```
+
+ Press Enter to run everything marked `[x]`, or switch steps off and on first;
+ the list is redrawn after each change. Steps that cannot run on this machine
+ are shown as `[-]` with the reason and have no number.
+
+ After Enter it runs unattended apart from a single UAC prompt for the
+ elevated half — none at all if you switched every elevated step off. Accept
+ it; declining leaves Visual Studio and the WDK uninstalled, and the script
+ says so.
3. Restart your shell afterwards so the updated user `PATH` is picked up, and
reboot if a step reported that a restart was required.
+## Choosing what runs
+
+Arguments to `setup-windows.bat` set the starting point of the plan; you can
+still adjust it before pressing Enter unless you also pass `-Yes`.
+
+| | |
+| --- | --- |
+| `setup-windows.bat -WhatIf` | Show the plan for this machine and stop. Nothing is changed. |
+| `setup-windows.bat -Skip VirtualBox,Brave` | Start with those switched off. |
+| `setup-windows.bat -Only E` | Start with only the elevated half. |
+| `setup-windows.bat -Yes` | Do not ask: run the plan as it stands. For unattended runs. |
+| `setup-windows.bat /?` | Usage. |
+
+- Steps are named as the plan shows them, comma-separated. A group letter works
+ anywhere a name does: `U` per-user installs, `M` machine-wide installs and the
+ WiX pin, `E` the elevated half, `C` per-user configuration and Pillow. A name
+ it does not recognise stops the run before anything happens — a mistyped
+ `-Skip` that was quietly ignored would install exactly what you meant to leave
+ out.
+- **With no console to ask on** — piped, scheduled, a non-interactive `ssh`
+ command — it prints the plan and stops with exit code 2 unless given `-Yes`.
+ It never assumes yes.
+- The two halves take `-Only`, `-Skip` and `-Yes` too, and when run on their own
+ print their part of the plan and ask before doing anything, e.g.
+ `.\setup-windows-with-uac.ps1 -Only VisualStudio,Wdk`. From the batch file
+ they are handed exactly what the plan left selected, with `-Yes`, since you
+ were asked already. With nothing selected for a half it is not started — so
+ no UAC prompt is raised for an elevated half with no steps.
+- Every step is declared once, in `setup-windows-steps.ps1`: its name, what it
+ does, and — with the reason — whether it is x64-only (`-X64Only`) or needs an
+ administrator (`-Admin`). The planner, the batch file and both halves read
+ that one list and check each other: a step the list does not know is refused
+ rather than run, and a selected step that nothing implements fails the run
+ rather than quietly not happening. Adding a step means a row there plus the
+ code that does it, gated on the same name.
+- Some steps build on others — the WiX pin needs the .NET SDK, the Clang and XP
+ toolsets need Visual Studio. The plan notes it when you switch the
+ prerequisite off, but does not stop you: it may well be installed already.
+
+## Re-running
+
+Every step checks before it acts, so running `setup-windows.bat` again on a
+provisioned box installs only what is missing or has a newer version out.
+Nothing is reinstalled at the version it already has. Where a check cannot
+decide — no network, a version string it does not recognise — it installs as
+before rather than skipping: a failed check must never be why something is left
+missing or out of date.
+
+| step | how it decides |
+| --- | --- |
+| winget packages | `winget install` on a package winget can see is installed upgrades it when a newer version exists and otherwise does nothing — winget's own default. `--force`, which would install regardless, is never used. |
+| `ClaudeCode` | Anthropic's installer always downloads and reinstalls, so `setup-windows-current.ps1` asks first: is `%USERPROFILE%\.local\bin\claude.exe` the latest release — the version the installer's own endpoint names — and the build for this host? Only both skip it. |
+| `NASM` | Its per-user fallback install registers nothing, so winget cannot see it and would rerun the installer every time. `setup-windows-current.ps1` compares that `nasm.exe` with the version winget offers. A machine-wide NASM is left to winget. |
+| `Rustup` | Needs no help: rustup registers itself under the manifest's product code and rewrites its version when it updates itself, so winget sees what is really there. |
+| `WiX` | Skipped when 5.0.2 is already in the .NET global tool store. |
+| `OpenSshClient`, `OpenSshServer` | Installed only when the capability is not. |
+| `Rsync` | The release's `.sha256` is fetched first and compared with the hash recorded when the files were installed (`rsync-windows.sha256`). The zip is downloaded only when they differ or a file is missing. |
+| `VisualStudio`, `VsClang`, `VsWinXP` | `vswhere -requires` says whether the install already has every component of a pass. A pass with nothing to add does not run the installer, and with nothing to add anywhere the bootstrapper is not even downloaded. The script never updates Visual Studio itself; the Visual Studio Installer does that. |
+| `Wdk`, `Wpt` | Skipped when the kit, or the toolkit, is already there. |
+| `BinSkim` | Asks NuGet for the newest stable version before downloading a package well over 100 MB; `nupkg-version.txt` beside the tool records what is installed. |
+| `RustToolchain` | `rustup toolchain install` updates stable when there is a newer one and otherwise reports it unchanged. |
+| `Pillow` | `pip install` leaves an installed Pillow alone. It does not upgrade it either. |
+| the rest | Configuration: `PATH` entries are added only when missing, and settings are written with the values they already have. |
+
## Notes
- The elevated half writes a transcript to `setup-windows-uac.log` next to the
fast `ssh.exe` has to exist before the git config step can prefer it. The
non-elevated half still runs even when the elevated one failed; it just falls
back. Re-run `setup-windows-no-uac.ps1` on its own at any time to re-pick.
-- All three scripts are idempotent — re-running skips anything already installed.
- BinSkim in particular checks NuGet for the newest stable version *before*
- downloading: the package is a self-contained .NET build well over 100 MB, and
- re-provisioning an up-to-date box should not pay for it. The installed version
- is tracked in `nupkg-version.txt` beside the tool.
+- All the scripts are safe to re-run: nothing is reinstalled at the version it
+ already has. See [Re-running](#re-running) for how each step decides.
- `setup-windows-no-uac.ps1` runs its steps independently: one failing warns and
the rest still run, and it exits 1 if any did. The `.bat` reports that and
- carries on to the elevated half, which is the part worth the UAC prompt. Use
- `-Skip` to re-run a subset, e.g. `.\setup-windows-no-uac.ps1 -Skip BinSkim`.
+ carries on. Use `-Only` / `-Skip` to re-run a subset, e.g.
+ `.\setup-windows-no-uac.ps1 -Skip BinSkim`.
Run it **non-elevated**: it writes per-user state (the `HKCU` `PATH`, the
`.gitconfig` under `%USERPROFILE%`), so an elevated run would configure the
administrator's profile instead. It warns if you do.
from that descriptor rather than deleting the value, which also carries entries
for SYSTEM, Administrators and two service accounts.
+- **Claude Code is not installed through winget.** Recent releases of
+ `Anthropic.ClaudeCode` are broken through that route, so the batch file runs
+ Anthropic's own installer, `irm https://claude.ai/install.ps1 | iex`: it
+ checks the download against the release manifest's SHA-256 and puts `claude`
+ in `%USERPROFILE%\.local\bin`, per-user. Two things are fixed up around it.
+ The script picks its ARM64 or x64 build from `$env:PROCESSOR_ARCHITECTURE`
+ alone, which reads `AMD64` in an emulated x64 shell on ARM64 (see
+ [Detecting the architecture](#detecting-the-architecture)), so the batch file
+ sets that variable from the registry's answer for the installer's process.
+ And the script refuses a 32-bit PowerShell, which is what `powershell` is from a
+ 32-bit `cmd.exe`, so it is run through `%SystemRoot%\Sysnative` when that
+ exists. A copy left over from the winget route can shadow the new one on the
+ `PATH`; the step warns if it finds one (`winget uninstall
+ Anthropic.ClaudeCode` removes it), and the architecture audit shows which
+ `claude.exe` wins.
+- **Rust.** winget installs rustup (`Rustlang.Rustup`, per-user, under
+ `%USERPROFILE%\.cargo` and `.rustup`). Its silent install is
+ `rustup-init -y -q`, which already sets up a stable toolchain for whatever
+ host rustup detects. The `RustToolchain` step in `setup-windows-no-uac.ps1`
+ then pins the answer: `stable-x86_64-pc-windows-msvc` on x64,
+ `stable-aarch64-pc-windows-msvc` on ARM64, installed, made the default, and set
+ as rustup's default host. That matters on ARM64, where an x64 rustup —
+ an older install, or one started from an emulated shell — would otherwise
+ quietly set up an emulated compiler. The `-msvc` toolchains link with the
+ Visual Studio the elevated half installs, but only need it to *build*:
+ installing rustup first is fine, and with `-y` rustup-init just warns that
+ the prerequisites are missing rather than installing a Visual Studio of its
+ own. Re-running the step updates stable. It also resets the default to
+ stable, so leave `RustToolchain` out of the plan if you have chosen another
+ default on purpose. The architecture audit reports the compiler *inside* the
+ toolchain: `.cargo\bin\rustc.exe` is a rustup proxy, so its architecture is
+ rustup's, not the compiler's.
+- **`adb` and `fastboot`** come from `Google.PlatformTools`, a portable zip that
+ winget unpacks under `%LOCALAPPDATA%\Microsoft\WinGet\Packages` and puts on the
+ user `PATH` (`adb.exe` needs the `AdbWinApi` DLLs beside it). The manifest
+ calls it x64, but every binary in it is 32-bit **x86** — checked on r37.0.1 —
+ and there is no ARM64 build. It runs under WOW64 on x64 and under x86
+ emulation on ARM64, which every Windows on Arm has; the audit lists it as
+ expected. It stays on ARM64 for the same reason `iperf3` does: it is bound by
+ the link to the device, not by the CPU.
- The scripts were extracted from a native Windows project, so the component
selection is tuned for that: Spectre-mitigated runtimes, the v141/XP toolset,
and driver-kit headers. Trim the component lists in the `.ps1` if you don't
run `setup-windows.bat` exactly as on x64.
**The two architectures deliberately install different things.** x64 is the full
-workstation; ARM64 is provisioned as a lean, single-compiler build box. See
+workstation; ARM64 is provisioned as a lean build box with a single C/C++
+compiler. See
[ARM64 installs less, on purpose](#arm64-installs-less-on-purpose) below for the
list and the reasoning.
| Windows Driver Kit | `setup-windows-with-uac.ps1` | see below |
| BinSkim | `setup-windows-no-uac.ps1` | the NuGet package publishes `win-x64` only |
-x64 keeps every one of them. `:x64only` at the bottom of `setup-windows.bat` is
-the single place that decision is applied, so each skip prints its reason into
-the run log rather than leaving a silent gap.
+x64 keeps every one of them. The decision is made in one place,
+`setup-windows-steps.ps1` (`-X64Only`, with the reason): the plan shows each as
+`[-]` with that reason, and the run log repeats it where the step would have
+run, rather than leaving a silent gap.
**`clang-cl` is dropped by choice, not by limitation.** A native ARM64 `clang-cl`
*is* offered on Arm and did work here before the split. Two things establish
to upstream while CMake's `-T ClangCL` keeps using VS's, so pass a full
`-DCMAKE_CXX_COMPILER` when it matters.
+**Rust is on both boxes, and native on ARM64.** It is a different language
+rather than a second C/C++ compiler, so it does not bend the one-compiler rule,
+and its `-msvc` toolchain links through MSVC's own `link.exe`. The ARM64 box
+gets `stable-aarch64-pc-windows-msvc`; to build x64 Rust code from there, add
+the x64 *target* (`rustup target add x86_64-pc-windows-msvc`) rather than an x64
+host toolchain, which would run emulated.
+
What you end up with on ARM64:
| | On ARM64 |
| --- | --- |
-| **Native ARM64** | Git, Python, .NET SDK, PowerShell, VS Code, Windows Terminal, Sysinternals, Claude Code, CMake, Ninja, the VS installer, **MSVC**, MSBuild, the SDK tools (`rc`, `signtool`), the in-box OpenSSH client, and the Windows Performance Toolkit. |
-| **Emulated x64** | `iperf3` (network-bound) and the ADK *installer* (the toolkit it lays down is native). |
+| **Native ARM64** | Git, Python, .NET SDK, PowerShell, VS Code, Windows Terminal, Sysinternals, Claude Code, CMake, Ninja, the VS installer, **MSVC**, MSBuild, the SDK tools (`rc`, `signtool`), rustup and **Rust** (`stable-aarch64-pc-windows-msvc`), the in-box OpenSSH client, and the Windows Performance Toolkit. |
+| **Emulated** | `iperf3` (x64; network-bound), `adb` and `fastboot` (x86 — Google ships no other Windows build; bound by the USB/TCP link to the device), and the ADK *installer* (x64; the toolkit it lays down is native). |
| **Emulated, minor** | `py.exe` (python.org ships the launcher shim as x86; it execs the native `python.exe`) and `vswhere.exe` (Microsoft ships x86 only; runs once). |
| **Not installed** | Brave, WinMerge, upstream LLVM, NASM, Android GPU Inspector, OpenCppCoverage, BinSkim, VS `clang-cl`, and the WDK — see the table above. |
| **Unavailable** | VirtualBox, the Windows 7 x86 test VM, Intel VTune, and the `v141` / Windows XP targeting toolset. |
They used to fail on every run with opaque codes — `exit 5`
(`ERROR_ACCESS_DENIED`) for the .NET SDK, MSI `1603` for CMake and Android GPU
Inspector, `exit 1` for OpenCppCoverage, whose installer self-elevates. They are
-now skipped with one clear line instead. None blocks a build: the .NET SDK is
+now shown as unavailable in the plan, with the reason, and skipped with one
+clear line each. None blocks a build: the .NET SDK is
only there for the WiX MSI tooling, CMake also ships inside Visual Studio, AGI
profiles Android devices, and OpenCppCoverage cannot instrument ARM64 binaries
anyway.