]> vilimpoc.org git repositories - dotfiles/blob - README.md
dotfiles: show the plan, and let it be chosen, before anything runs
[dotfiles] / README.md
1 # dotfiles
2
3 Windows development-box provisioning scripts.
4
5 `setup-windows.bat` takes a fresh Windows install to a working C++ / native
6 development environment: editors and shells, Python, the Visual Studio
7 toolchain (including the Clang and Windows XP targeting toolsets), the Windows
8 Driver Kit, Rust (rustup, with the stable MSVC toolchain for the host as the
9 default), Android's `adb`, and a handful of analysis tools (Sysinternals,
10 OpenCppCoverage, BinSkim, the Windows Performance Toolkit). It also sets the
11 box up to be driven remotely: OpenSSH Server plus an rsync build for Windows,
12 which is what makes a throwaway VM reachable from a Linux host.
13
14 **Nothing happens until you have seen the plan.** It first lists every step it
15 can take on this machine and lets you choose which ones to run — see
16 [Choosing what runs](#choosing-what-runs).
17
18 Runs on **x64 and on ARM64** (Windows 11 on Arm), and installs *different things*
19 on each: x64 is the full workstation, ARM64 a lean build box with a single
20 C/C++ compiler. See
21 [Running on ARM64](#running-on-arm64) for what differs and why.
22
23 ## Files
24
25 | File | Purpose |
26 | --- | --- |
27 | `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. |
28 | `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. |
29 | `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. |
30 | `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). |
31 | `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. |
32 | `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. |
33 | `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. |
34
35 ## Usage
36
37 1. **Edit `setup-windows-no-uac.ps1` first.** The global git identity near the
38    top of the file is empty:
39
40    ```powershell
41    $GitUserName  = ''   # e.g. 'Ada Lovelace'
42    $GitUserEmail = ''   # e.g. 'ada@example.com'
43    ```
44
45    Fill in your own name and email, or leave them empty to keep your identity
46    per-repository - the script skips `user.name` / `user.email` rather than
47    writing a placeholder, and says so. Everything else in that script is set
48    either way.
49
50 2. Run it from a normal (non-elevated) prompt:
51
52    ```bat
53    setup-windows.bat
54    ```
55
56    Before doing anything it prints the plan — every step it can take on this
57    machine, grouped and numbered — and waits:
58
59    ```text
60    Setup plan - nothing has been done yet
61      host:      x64 - the full workstation
62      elevation: not elevated; expect a UAC prompt partway through, for the elevated half
63
64     U  Per-user installs (winget, except Claude Code)
65       [x]  1 ClaudeCode          Claude Code, native installer: irm https://claude.ai/install.ps1 | iex
66       [x]  2 Brave               Brave.Brave
67       ...
68     E  Elevated setup (setup-windows-with-uac.ps1) - one UAC prompt, runs in its own window
69       [x] 24 OpenSshClient       OpenSSH Client capability (ssh.exe, and the libcrypto.dll rsync's ssh.exe needs)
70       ...
71       [x] 28 VisualStudio        Visual Studio 2022 Community, C++: MSVC for x86/x64 + ARM64, SDK 26100 (several GB)
72       ...
73
74      42 of 42 steps selected.
75
76      Enter runs the selected steps. To change them, type step numbers or ranges
77      (3 5-7), step names, or a group letter (U M E C) to switch them on or off;
78      'all' or 'none' for everything; 'q' to quit without doing anything.
79    ```
80
81    Press Enter to run everything marked `[x]`, or switch steps off and on first;
82    the list is redrawn after each change. Steps that cannot run on this machine
83    are shown as `[-]` with the reason and have no number.
84
85    After Enter it runs unattended apart from a single UAC prompt for the
86    elevated half — none at all if you switched every elevated step off. Accept
87    it; declining leaves Visual Studio and the WDK uninstalled, and the script
88    says so.
89
90 3. Restart your shell afterwards so the updated user `PATH` is picked up, and
91    reboot if a step reported that a restart was required.
92
93 ## Choosing what runs
94
95 Arguments to `setup-windows.bat` set the starting point of the plan; you can
96 still adjust it before pressing Enter unless you also pass `-Yes`.
97
98 | | |
99 | --- | --- |
100 | `setup-windows.bat -WhatIf` | Show the plan for this machine and stop. Nothing is changed. |
101 | `setup-windows.bat -Skip VirtualBox,Brave` | Start with those switched off. |
102 | `setup-windows.bat -Only E` | Start with only the elevated half. |
103 | `setup-windows.bat -Yes` | Do not ask: run the plan as it stands. For unattended runs. |
104 | `setup-windows.bat /?` | Usage. |
105
106 - Steps are named as the plan shows them, comma-separated. A group letter works
107   anywhere a name does: `U` per-user installs, `M` machine-wide installs and the
108   WiX pin, `E` the elevated half, `C` per-user configuration and Pillow. A name
109   it does not recognise stops the run before anything happens — a mistyped
110   `-Skip` that was quietly ignored would install exactly what you meant to leave
111   out.
112 - **With no console to ask on** — piped, scheduled, a non-interactive `ssh`
113   command — it prints the plan and stops with exit code 2 unless given `-Yes`.
114   It never assumes yes.
115 - The two halves take `-Only`, `-Skip` and `-Yes` too, and when run on their own
116   print their part of the plan and ask before doing anything, e.g.
117   `.\setup-windows-with-uac.ps1 -Only VisualStudio,Wdk`. From the batch file
118   they are handed exactly what the plan left selected, with `-Yes`, since you
119   were asked already. With nothing selected for a half it is not started — so
120   no UAC prompt is raised for an elevated half with no steps.
121 - Every step is declared once, in `setup-windows-steps.ps1`: its name, what it
122   does, and — with the reason — whether it is x64-only (`-X64Only`) or needs an
123   administrator (`-Admin`). The planner, the batch file and both halves read
124   that one list and check each other: a step the list does not know is refused
125   rather than run, and a selected step that nothing implements fails the run
126   rather than quietly not happening. Adding a step means a row there plus the
127   code that does it, gated on the same name.
128 - Some steps build on others — the WiX pin needs the .NET SDK, the Clang and XP
129   toolsets need Visual Studio. The plan notes it when you switch the
130   prerequisite off, but does not stop you: it may well be installed already.
131
132 ## Re-running
133
134 Every step checks before it acts, so running `setup-windows.bat` again on a
135 provisioned box installs only what is missing or has a newer version out.
136 Nothing is reinstalled at the version it already has. Where a check cannot
137 decide — no network, a version string it does not recognise — it installs as
138 before rather than skipping: a failed check must never be why something is left
139 missing or out of date.
140
141 | step | how it decides |
142 | --- | --- |
143 | 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. |
144 | `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. |
145 | `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. |
146 | `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. |
147 | `WiX` | Skipped when 5.0.2 is already in the .NET global tool store. |
148 | `OpenSshClient`, `OpenSshServer` | Installed only when the capability is not. |
149 | `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. |
150 | `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. |
151 | `Wdk`, `Wpt` | Skipped when the kit, or the toolkit, is already there. |
152 | `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. |
153 | `RustToolchain` | `rustup toolchain install` updates stable when there is a newer one and otherwise reports it unchanged. |
154 | `Pillow` | `pip install` leaves an installed Pillow alone. It does not upgrade it either. |
155 | the rest | Configuration: `PATH` entries are added only when missing, and settings are written with the values they already have. |
156
157 ## Notes
158
159 - The elevated half writes a transcript to `setup-windows-uac.log` next to the
160   script; the batch file prints it when the elevated window closes. The log is
161   gitignored, as it contains local paths.
162 - **git uses a Win32-OpenSSH client.** `setup-windows-no-uac.ps1` sets
163   `core.sshCommand`. Git for Windows otherwise prefers its own bundled MSYS2
164   `ssh.exe`, which cannot reach the Windows `ssh-agent` service that the elevated
165   half enables - Win32-OpenSSH publishes the agent on a named pipe the MSYS2
166   build does not speak. Without this, keys loaded with `ssh-add` from PowerShell
167   are invisible to `git`, and a push falls back to hunting for a key file and
168   prompting for its passphrase. The value uses forward slashes on purpose: git
169   parses `core.sshCommand` with shell quoting rules, in which a backslash is an
170   escape character.
171 - **Which `ssh.exe` git gets.** `C:\Tools\rsync\ssh.exe` — the fast build the
172   elevated half unpacks — if it is there, `%WINDIR%/System32/OpenSSH/ssh.exe`
173   otherwise. It is the same client with the same `~/.ssh`, agent and
174   `known_hosts`; the difference is the stdin pump, and without it anything git
175   *pushes* is capped at ~17 MB/s. Candidates are tried by **running** them
176   (`ssh -V`), not by `Test-Path`: the fast build needs a `libcrypto.dll` that an
177   image without the OpenSSH Client capability does not have, and a client that
178   will not start should be found here rather than on the next `git push`.
179   This is also why `setup-windows.bat` now runs the elevated half **first** — the
180   fast `ssh.exe` has to exist before the git config step can prefer it. The
181   non-elevated half still runs even when the elevated one failed; it just falls
182   back. Re-run `setup-windows-no-uac.ps1` on its own at any time to re-pick.
183 - All the scripts are safe to re-run: nothing is reinstalled at the version it
184   already has. See [Re-running](#re-running) for how each step decides.
185 - `setup-windows-no-uac.ps1` runs its steps independently: one failing warns and
186   the rest still run, and it exits 1 if any did. The `.bat` reports that and
187   carries on. Use `-Only` / `-Skip` to re-run a subset, e.g.
188   `.\setup-windows-no-uac.ps1 -Skip BinSkim`.
189   Run it **non-elevated**: it writes per-user state (the `HKCU` `PATH`, the
190   `.gitconfig` under `%USERPROFILE%`), so an elevated run would configure the
191   administrator's profile instead. It warns if you do.
192 - **Remote access.** OpenSSH Server is installed from the Windows on-demand
193   capability (10/1809+), set to start automatically, and given an inbound TCP 22
194   firewall rule on *all* profiles — a VM's host-only or bridged adapter is
195   routinely classified Public, which is the usual reason a running `sshd` is
196   unreachable. Windows ships no `rsync`, so a build of it
197   ([nuket/rsync-windows](https://github.com/nuket/rsync-windows)) is installed to
198   `C:\Tools\rsync` and added to the **machine** `PATH`. That last detail matters:
199   the remote end of an `rsync` runs non-interactively, with no login shell, and
200   Win32-OpenSSH builds that environment from the registry `PATH` rather than from
201   a profile. Key auth needs `~/.ssh/authorized_keys` ACL'd to just you and
202   `SYSTEM`; accounts in the Administrators group use
203   `C:\ProgramData\ssh\administrators_authorized_keys` instead.
204 - **rsync brings its own `ssh.exe`.** That release ships as one zip per
205   architecture — `rsync-windows-x86.zip`, `-x64.zip` and `-arm64.zip`, each
206   holding `rsync.exe`, an `ssh.exe`, `COPYING.txt` and `NOTICE-ssh.txt` — and the
207   elevated half picks the zip matching the host, verifies it against the
208   published `.sha256`, unpacks the pair together, then **runs** the unpacked
209   `ssh.exe` and removes it if it will not start. Together is the point:
210   `rsync.exe` prefers an `ssh.exe` sitting in its own directory, and the release
211   builds one because the client Windows ships reads its stdin 3 KB at a time,
212   which holds a transfer *from* the box at ~17 MB/s however fast the link is.
213   Nothing else about it differs — same `~/.ssh`, same `ssh-agent`, same
214   `known_hosts` — and a bare `ssh` still resolves to the in-box client, which
215   sits ahead of `C:\Tools\rsync` on the `PATH`. It links against the
216   `libcrypto.dll` the **OpenSSH Client** capability puts in `System32` (Windows'
217   own LibreSSL, which uses AES-NI) and ships no copy of its own, so the elevated
218   half installs that capability first and falls back to `rsync.exe` alone,
219   warning, on an image that will not offer it.
220 - The `rsync` download follows the `releases/latest/download/` redirect rather
221   than the GitHub API: unauthenticated API calls are rate-limited to 60/hour per
222   IP, which a provisioning run behind a shared NAT can genuinely exhaust. To hold
223   a box on a known build, pin the tag in `$RsyncUrl`
224   (`.../releases/download/<tag>/<asset>`) instead.
225 - Visual Studio is installed in three labelled passes (base workload, Clang/LLVM,
226   XP toolset) so a failure identifies which component group is responsible. On
227   ARM64 only the first runs — the other two are skipped there, each printing why.
228   The last two are optional passes: a failure there warns and provisioning continues,
229   because neither is needed to build with MSVC and losing the whole toolchain
230   over a component you can add later from the installer UI is the worse outcome.
231 - **Which Visual Studio generation.** `$VsChannel` near the VS step picks it —
232   `17` for VS 2022, `18` for VS 2026 — and `$VsEdition` picks Community /
233   Professional / Enterprise. It is **architecture-split by default**: 17 on x64,
234   where the `v141`/XP toolset in the component list is the reason to pin a
235   generation, and 18 on ARM64, where that toolset cannot exist anyway (no
236   ARM64-hosted 14.16 compiler), so nothing holds Arm back to the older one.
237 - **The aka.ms path differs per generation** — VS 2022 is published under
238   `/release/`, VS 2026 under `/stable/`, which `$VsChannelPath` derives. This is
239   not cosmetic: `https://aka.ms/vs/18/release/vs_community.exe` is *not* a 404,
240   it silently redirects to Bing and returns **200 with an HTML body**. A wrong
241   guess there downloads a web page, names it `vs_community.exe`, and fails later
242   at `Start-Process` with something that looks nothing like a bad URL. The step
243   therefore checks the downloaded file starts with an `MZ` header and throws with
244   the URL if it does not.
245 - `Get-VsInstallPath` scopes its `vswhere` query to that generation
246   (`-version "[17.0,18.0)"`). That scoping is load-bearing: a bare
247   `vswhere -products *` returns **every** Visual Studio on the box, newest
248   first, and on a machine that already has VS 2026 the old unscoped lookup handed
249   an 18.x install path to a 17.x bootstrapper as `modify --installPath`, so every
250   pass targeted the wrong product. The step now also lists any other generations
251   it found and states that it left them alone — when none of them matches
252   `$VsChannel` the script is about to fetch a second, largely redundant
253   toolchain, and that should be a visible decision. On ARM64, where `$VsChannel`
254   is 18, an existing VS 2026 **is** the match and gets modified in place rather
255   than duplicated.
256 - **Windows Performance Analyzer is not part of Visual Studio.** VS has its own
257   Performance Profiler, which is a different, `.diagsession`-based tool and
258   cannot open an `.etl`. WPA ships with `xperf` and `wpr` in the Windows
259   Performance Toolkit, which exists in exactly two places: as an optional
260   *feature* of the Windows SDK (`OptionId.WindowsPerformanceToolkit`) and inside
261   the Windows ADK, which bundles the same toolkit. Whether the SDK install that
262   Visual Studio performs selects that feature varies by version, so the elevated
263   half **detects first** — `%ProgramFiles(x86)%\Windows Kits\10\Windows
264   Performance Toolkit`, its 64-bit twin, and the ADK location — and only falls
265   back to `winget install Microsoft.WindowsADK` when nothing is there. It then
266   re-asserts that directory on the machine `PATH` (the toolkit's own installer
267   usually does this, and the Start Menu gets *Windows Kits > Windows Performance
268   Toolkit* shortcuts for WPA and WPR). To install just the toolkit instead of the
269   whole ADK, run the standalone SDK setup with
270   `winsdksetup.exe /features OptionId.WindowsPerformanceToolkit /q`. A newer WPA
271   also exists in the Microsoft Store (`winget install --id 9N0W1B2BXGNZ --source
272   msstore`); it is not installed here because the Store source needs an
273   interactive, signed-in session, which the unattended elevated half does not
274   have.
275 - **Intel VTune Profiler is reported, not installed.** The elevated half prints
276   whether it is on the box, its version, and the path to `vtune.exe`; if it is
277   missing it prints the download page instead (and says so if the CPU is not
278   Intel). Automating the install is not worth it here: the offline installer is a
279   ~750 MB download from a URL carrying a per-release GUID with no "latest"
280   redirect behind it, so every new build would mean editing a hard-coded link,
281   and it is only worth having on Intel silicon since hardware event-based
282   sampling reads Intel PMU counters. It does install unattended if you want it
283   scripted elsewhere:
284
285   ```powershell
286   intel-vtune-<version>_offline.exe -a --silent --cli --eula accept
287   ```
288
289   **Run it from an administrator account, elevated.** Hardware event-based
290   sampling (`uarch-exploration`, `memory-access`, `hotspots -knob
291   sampling-mode=hw`) requires it, and VTune warns about that at the top of every
292   unelevated run. Worth knowing that the failure it gives there is *"cannot
293   recognize the processor"*, which reads like a hardware problem and is not one:
294   the drivers (`sepdrv5`, `sepdal`, `vtss`) are installed and running, and VTune
295   identifies the PMU through them. There is no group to join to get around it —
296   the Linux driver can be handed to a `vtune` group, but on Windows the
297   documented answer is to run as administrator.
298
299 - **Collect traces from an elevated Administrator session. Non-elevated
300   collection was tried here and abandoned.** The attempt was to put one ordinary
301   account into `BUILTIN\Performance Log Users`, which appears in the default
302   security descriptors ETW keeps per provider GUID under
303   `HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security`, and collect without a UAC
304   prompt. It does not survive contact with the real workflow: `xperf -on base`
305   and `wpr -start` drive the *NT Kernel Logger*, reserved for Administrators and
306   LocalSystem, and granting the group `SeSystemProfilePrivilege` ("Profile system
307   performance") plus an explicit ACE for `TRACELOG_ACCESS_KERNEL_LOGGER` on
308   `SystemTraceControlGuid` — all three in place, across a reboot — still answered
309
310   ```text
311   xperf: error: NT Kernel Logger: Access is denied. (0x5).
312   ```
313
314   It is not a check an ACE overrides, and the same wall turned up often enough
315   elsewhere that the whole approach was dropped rather than carried as a
316   half-working path. **Sign in to an administrator account and run `xperf`, `wpr`
317   and VTune from an elevated prompt.** Analysis is the exception and never needed
318   any of this: `wpa.exe` opens an existing `.etl` as a plain user.
319
320   If an earlier revision of these scripts ran on a box, it left that account in
321   the group. Take it back out with:
322
323   ```powershell
324   net localgroup "Performance Log Users" DOMAIN\user /delete
325   ```
326
327   Two revisions also granted the privilege and the ACE. Undo the privilege in
328   `secpol.msc` > Local Policies > User Rights Assignment > "Profile system
329   performance" by removing Performance Log Users. The ACEs sit in the
330   `{9e814aad-3204-11d2-9a82-006008a86939}` value under
331   `HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security`: strip the `LU` entries
332   from that descriptor rather than deleting the value, which also carries entries
333   for SYSTEM, Administrators and two service accounts.
334
335 - **Claude Code is not installed through winget.** Recent releases of
336   `Anthropic.ClaudeCode` are broken through that route, so the batch file runs
337   Anthropic's own installer, `irm https://claude.ai/install.ps1 | iex`: it
338   checks the download against the release manifest's SHA-256 and puts `claude`
339   in `%USERPROFILE%\.local\bin`, per-user. Two things are fixed up around it.
340   The script picks its ARM64 or x64 build from `$env:PROCESSOR_ARCHITECTURE`
341   alone, which reads `AMD64` in an emulated x64 shell on ARM64 (see
342   [Detecting the architecture](#detecting-the-architecture)), so the batch file
343   sets that variable from the registry's answer for the installer's process.
344   And the script refuses a 32-bit PowerShell, which is what `powershell` is from a
345   32-bit `cmd.exe`, so it is run through `%SystemRoot%\Sysnative` when that
346   exists. A copy left over from the winget route can shadow the new one on the
347   `PATH`; the step warns if it finds one (`winget uninstall
348   Anthropic.ClaudeCode` removes it), and the architecture audit shows which
349   `claude.exe` wins.
350 - **Rust.** winget installs rustup (`Rustlang.Rustup`, per-user, under
351   `%USERPROFILE%\.cargo` and `.rustup`). Its silent install is
352   `rustup-init -y -q`, which already sets up a stable toolchain for whatever
353   host rustup detects. The `RustToolchain` step in `setup-windows-no-uac.ps1`
354   then pins the answer: `stable-x86_64-pc-windows-msvc` on x64,
355   `stable-aarch64-pc-windows-msvc` on ARM64, installed, made the default, and set
356   as rustup's default host. That matters on ARM64, where an x64 rustup —
357   an older install, or one started from an emulated shell — would otherwise
358   quietly set up an emulated compiler. The `-msvc` toolchains link with the
359   Visual Studio the elevated half installs, but only need it to *build*:
360   installing rustup first is fine, and with `-y` rustup-init just warns that
361   the prerequisites are missing rather than installing a Visual Studio of its
362   own. Re-running the step updates stable. It also resets the default to
363   stable, so leave `RustToolchain` out of the plan if you have chosen another
364   default on purpose. The architecture audit reports the compiler *inside* the
365   toolchain: `.cargo\bin\rustc.exe` is a rustup proxy, so its architecture is
366   rustup's, not the compiler's.
367 - **`adb` and `fastboot`** come from `Google.PlatformTools`, a portable zip that
368   winget unpacks under `%LOCALAPPDATA%\Microsoft\WinGet\Packages` and puts on the
369   user `PATH` (`adb.exe` needs the `AdbWinApi` DLLs beside it). The manifest
370   calls it x64, but every binary in it is 32-bit **x86** — checked on r37.0.1 —
371   and there is no ARM64 build. It runs under WOW64 on x64 and under x86
372   emulation on ARM64, which every Windows on Arm has; the audit lists it as
373   expected. It stays on ARM64 for the same reason `iperf3` does: it is bound by
374   the link to the device, not by the CPU.
375 - The scripts were extracted from a native Windows project, so the component
376   selection is tuned for that: Spectre-mitigated runtimes, the v141/XP toolset,
377   and driver-kit headers. Trim the component lists in the `.ps1` if you don't
378   need them — each group is a plain array near the top.
379
380 ## Running on ARM64
381
382 All three scripts detect the host architecture and adapt. Nothing needs a flag —
383 run `setup-windows.bat` exactly as on x64.
384
385 **The two architectures deliberately install different things.** x64 is the full
386 workstation; ARM64 is provisioned as a lean build box with a single C/C++
387 compiler. See
388 [ARM64 installs less, on purpose](#arm64-installs-less-on-purpose) below for the
389 list and the reasoning.
390
391 ### Detecting the architecture
392
393 All three scripts read the **machine-level registry value**, not the environment
394 and not `RuntimeInformation.OSArchitecture`. Both of those report the *emulated*
395 architecture inside an emulated x64 process — which is what you get by launching
396 `powershell` or `cmd` from an x64 shell (Git Bash, for one) on an ARM64 box.
397 Measured on this machine, from that emulated shell:
398
399 | source | reports |
400 | --- | --- |
401 | `[RuntimeInformation]::OSArchitecture` | `X64` ✗ |
402 | `%PROCESSOR_ARCHITECTURE%` | `AMD64` ✗ |
403 | `HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment` | `ARM64` ✓ |
404
405 `OSArchitecture` is documented as the *OS's* architecture, and on .NET Core it
406 is; under .NET Framework on Prism it is not. It is kept only as a fallback. This
407 used to be a cosmetic mislabel in a log line — now that the architecture decides
408 which packages are installed at all, reading it wrong provisions the wrong
409 machine.
410
411 > The batch file calls `reg.exe` by full path and parses it **without** piping to
412 > `find`/`findstr`. Both are shadowable by anything earlier on the `PATH`, and a
413 > POSIX `find` turns the whole detection into a silent no-op that leaves the
414 > emulated answer standing. That is not hypothetical; it is how this was found.
415
416 **The build toolchain is fully native.** Visual Studio's ARM64 installer and MSVC
417 are ARM64 binaries, and MSVC cross-compiles every target from an ARM64 host with
418 no emulation in the compiler:
419
420 ```bat
421 cmake -B build -A ARM64   && cmake --build build --config Release   :: native
422 cmake -B build -A x64     && cmake --build build --config Release   :: cross
423 cmake -B build -A Win32   && cmake --build build --config Release   :: cross
424 ```
425
426 **ARM64 uses Visual Studio 2026** (`$VsChannel = 18`) while x64 stays on VS 2022.
427 Nothing forces that split — the Arm box simply has no reason to stay on the older
428 generation, since the `v141`/XP toolset that pins x64 there cannot run on Arm at
429 all — so Arm takes the newer MSVC. Note this is a *default*, not a limitation:
430 VS 2026 does still offer `v141` and `WinXP` as components on x64.
431
432 The base component group now names `VC.Tools.x86.x64` **and** `VC.Tools.ARM64`
433 explicitly instead of relying on `--includeRecommended`, which resolves
434 differently per host: on an ARM64 machine the workload's recommended set is the
435 ARM64-targeting toolchain, and the x64 cross-compiler is *not* implied. Spectre
436 runtimes and ATL are likewise requested for both target families
437 (`VC.ATL.ARM64.Spectre` is a separate component from `VC.ATL.Spectre`).
438
439 ### ARM64 installs less, on purpose
440
441 The ARM64 machine here is a constrained VM, and nearly everything it was
442 installing without a native build was going to run under Prism emulation anyway.
443 The exceptions were individually defensible and collectively a VM full of x64
444 binaries. So the split is now explicit: **ARM64 gets MSVC and nothing that
445 duplicates it or that exists only as an x64 binary.**
446
447 | dropped on ARM64 | where | why |
448 | --- | --- | --- |
449 | `Brave.Brave` | `setup-windows.bat` | no ARM64 build; an emulated browser is the slowest thing on the box, and Edge is in-box and native |
450 | `WinMerge.WinMerge` | `setup-windows.bat` | ARM64 ships as a **machine-scope** installer only, so an unelevated `winget` can install nothing but the emulated x64 build |
451 | `LLVM.LLVM` | `setup-windows.bat` | second Clang, dropped along with VS's `clang-cl` |
452 | `NASM.NASM` | `setup-windows.bat` | x86/x86-64 assembler with no ARM64 target; MSVC's `armasm64.exe` is the native one |
453 | `Google.AndroidGPUInspector` | `setup-windows.bat` | x64-only, and it profiles Android devices rather than anything compiled here |
454 | `OpenCppCoverage` | `setup-windows.bat` | collects coverage by stepping x86/x64 instructions, so it cannot instrument an ARM64 binary at all |
455 | VS Clang/LLVM component | `setup-windows-with-uac.ps1` | see below |
456 | Windows Driver Kit | `setup-windows-with-uac.ps1` | see below |
457 | BinSkim | `setup-windows-no-uac.ps1` | the NuGet package publishes `win-x64` only |
458
459 x64 keeps every one of them. The decision is made in one place,
460 `setup-windows-steps.ps1` (`-X64Only`, with the reason): the plan shows each as
461 `[-]` with that reason, and the run log repeats it where the step would have
462 run, rather than leaving a silent gap.
463
464 **`clang-cl` is dropped by choice, not by limitation.** A native ARM64 `clang-cl`
465 *is* offered on Arm and did work here before the split. Two things establish
466 that: the `VC.Llvm.Clang` VSIX is `productArch=neutral` with no `chip` or
467 `machineArch` restriction, so the Arm installer offers it; and MSVC's
468 `VC\Tools\Llvm` tree is partitioned by **host** architecture, with real ARM64
469 binaries in `ARM64\bin`, which is where `clang-cl.exe` lands:
470
471 | directory | host |
472 | --- | --- |
473 | `VC\Tools\Llvm\bin` | x86 |
474 | `VC\Tools\Llvm\x64\bin` | x64 |
475 | `VC\Tools\Llvm\ARM64\bin` | ARM64 |
476
477 > Watch for a false positive when checking by hand: `VC\Tools\Llvm\*\bin` holds
478 > `clang-format.exe` and `clang-tidy.exe` on **any** host, installed or not —
479 > those ship with the NativeDesktop workload. Their presence does not mean the
480 > compiler is there; look for `clang-cl.exe`.
481
482 Note that the VS installer never uninstalls a component you simply stopped
483 asking for, so a box provisioned before this split keeps its `clang-cl` until it
484 is explicitly removed. The verification step says so and prints the
485 `--remove` line.
486
487 **The WDK is dropped for two reasons, and the second one bites on x64 too.** The
488 ARM64 box builds user-mode software, so it does not need a driver kit — but this
489 particular WDK *cannot* install here regardless. The kit requires the **matching
490 Windows SDK revision**, and revisions are not side-by-side: `Include\10.0.26100.0`
491 is one directory, whichever revision wrote it. VS 2026 installs SDK
492 **26100.7705**; `linkid=2335869` is WDK **26100.6584**, the kit paired with VS
493 2022. `wdksetup.exe /quiet` therefore aborts:
494
495 ```
496 WDK installer exit code: 15605
497 WER: WDK / 10.1.26100.6584 / Install / 0x80070642
498 ```
499
500 `0x642` is `1602`, `ERROR_INSTALL_USEREXIT` — a silent install cancelling itself
501 on a failed prerequisite check. Nothing to do with Arm or emulation; the same
502 pairing fails on x64. If a driver ever does need building against VS 2026, use
503 the kit Microsoft's [supported WDK downloads](https://learn.microsoft.com/en-us/windows-hardware/drivers/other-wdk-downloads)
504 table pairs with it — the 28000.x one — not this link. x64 with VS 2022 has the
505 matched pair, so it installs there.
506
507 **Compiler diversity now lives on the x64 box**, which has room for three
508 toolchains: MSVC, VS's `clang-cl`, and **upstream LLVM** (`winget install
509 LLVM.LLVM`). Upstream runs on its own schedule, is usually several major versions
510 ahead of the Clang VS bundles, and installs to `C:\Program Files\LLVM` rather
511 than inside the VS tree, so the two are genuinely independent. Two Clang majors
512 plus MSVC over the same sources is what catches the bugs a single toolchain
513 agrees with itself about. Watch the `PATH` there: a bare `clang-cl` may resolve
514 to upstream while CMake's `-T ClangCL` keeps using VS's, so pass a full
515 `-DCMAKE_CXX_COMPILER` when it matters.
516
517 **Rust is on both boxes, and native on ARM64.** It is a different language
518 rather than a second C/C++ compiler, so it does not bend the one-compiler rule,
519 and its `-msvc` toolchain links through MSVC's own `link.exe`. The ARM64 box
520 gets `stable-aarch64-pc-windows-msvc`; to build x64 Rust code from there, add
521 the x64 *target* (`rustup target add x86_64-pc-windows-msvc`) rather than an x64
522 host toolchain, which would run emulated.
523
524 What you end up with on ARM64:
525
526 | | On ARM64 |
527 | --- | --- |
528 | **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. |
529 | **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). |
530 | **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). |
531 | **Not installed** | Brave, WinMerge, upstream LLVM, NASM, Android GPU Inspector, OpenCppCoverage, BinSkim, VS `clang-cl`, and the WDK — see the table above. |
532 | **Unavailable** | VirtualBox, the Windows 7 x86 test VM, Intel VTune, and the `v141` / Windows XP targeting toolset. |
533
534 ### The architecture audit
535
536 `setup-windows-no-uac.ps1` ends with an **`ArchAudit`** step that reads the PE
537 COFF header of every tool the box provisions and reports what you would actually
538 run, resolved PATH-first. It reads the file itself rather than trusting package
539 metadata — a multi-architecture package (Sysinternals) ships every build in one
540 zip under different names, and an installer's metadata says nothing about which
541 binary landed. Purely informational; it never fails the run. Skip it with
542 `-Skip ArchAudit`.
543
544 Anything emulated with a listed reason prints as expected. Anything emulated
545 *without* one is called out, with a remedy where a native build exists — which is
546 how the `ninja` problem below was found.
547
548 ### Elevation, and the case where there is no prompt
549
550 `setup-windows.bat` works out **once, at the top**, whether this account can
551 elevate, and reports it as `Elevation: ALREADY | PROMPT | NOLUA`:
552
553 | | |
554 | --- | --- |
555 | `ALREADY` | Already elevated. The elevated half runs **directly** — no point asking to elevate what already is. |
556 | `PROMPT` | Not elevated, UAC on. Elevation is requested: an administrator gets a consent prompt, a standard user gets an over-the-shoulder **credential** prompt. |
557 | `NOLUA` | UAC is off machine-wide (`EnableLUA = 0`) **and** this is not an administrator. Elevation is *impossible*, not merely declined. |
558
559 `IsInRole(Administrator)` is false for an admin running unelevated under UAC,
560 which is what makes `ALREADY` mean *actually elevated* rather than *could be*.
561
562 **Why `NOLUA` needs detecting rather than attempting.** `Start-Process -Verb
563 RunAs` is not honest about it. With UAC disabled the `RunAs` verb is **silently
564 ignored**: no prompt appears, the child starts with the caller's own unelevated
565 token, and `Start-Process` reports **exit code 0** as though it had worked. The
566 elevated script then refuses itself on its `#Requires -RunAsAdministrator` line,
567 exits 1, and never reaches its `Start-Transcript` — so there is no log either.
568 Read naively that is indistinguishable from a cancelled UAC prompt, which is
569 exactly what this script used to report, sending you to look for a prompt that
570 could never have appeared.
571
572 Measured on such a box: `Start-Process -Verb RunAs` returned exit 0 while the
573 child reported itself as the *unelevated* calling user.
574
575 **What is skipped when elevation is impossible.** The elevated half, plus four
576 packages that sit in the "per-user" section but are machine-wide installers.
577 They used to fail on every run with opaque codes — `exit 5`
578 (`ERROR_ACCESS_DENIED`) for the .NET SDK, MSI `1603` for CMake and Android GPU
579 Inspector, `exit 1` for OpenCppCoverage, whose installer self-elevates. They are
580 now shown as unavailable in the plan, with the reason, and skipped with one
581 clear line each. None blocks a build: the .NET SDK is
582 only there for the WiX MSI tooling, CMake also ships inside Visual Studio, AGI
583 profiles Android devices, and OpenCppCoverage cannot instrument ARM64 binaries
584 anyway.
585
586 A `NOLUA` run is a **success** (exit 0), not a failure — the per-user half did
587 its job. To finish the box, run it from an administrator account, then re-run
588 `setup-windows-no-uac.ps1` as yourself so the per-user `PATH` and `.gitconfig`
589 land in *your* profile rather than the administrator's.
590
591 Two consequences worth knowing rather than rediscovering:
592
593 - **LLVM does not fail — it relocates.** Its NSIS installer targets
594   `%ProgramFiles%\LLVM`, and when it cannot write there it silently falls back to
595   a per-user directory (observed: `%USERPROFILE%\Documents\LLVM`) while winget
596   still reports *Successfully installed*. The compiler is genuinely there and
597   native; nothing can find it. The `LlvmPath` step looks in that fallback, puts
598   the directory on the user `PATH`, and says so.
599 - **WinMerge silently downgrades to x64.** Upstream publishes ARM64 as a
600   **machine-scope** installer and x64 as a **per-user** one, so an unelevated
601   winget lets its scope preference beat its architecture preference. The batch
602   now asks for `--architecture arm64` first and falls back, so an admin run gets
603   the native build and a standard-user run still gets a working one.
604
605 ### Two gotchas the audit surfaces
606
607 - **Visual Studio bundles an x64 `ninja.exe` even on ARM64.** It matters more
608   than a one-off tool would, because ninja is re-invoked for every edge in the
609   build graph — it is the one place emulation is paid over and over rather than
610   once. `winget install Ninja-build.Ninja` gets the native `ninja-winarm64`
611   build, and the `NinjaPath` step pins its directory to the **front** of the user
612   `PATH`. VS's bundled `cmake.exe` *is* ARM64, so only ninja has this problem.
613
614   On which copy wins inside a Developer Command Prompt: the native one. VS adds
615   its ninja from `Common7\Tools\vsdevcmd\ext\cmake.bat`, which does
616   `set "PATH=%PATH%;...\CMake\bin;...\CMake\Ninja"` — an **append**, so the VS
617   directories land at the very end of the composed `PATH`, behind every machine
618   and user entry. A native ninja anywhere on the user `PATH` already beats it,
619   measured on an ARM64 box. The `NinjaPath` step is therefore insurance rather
620   than the load-bearing fix: winget *appends* its package directory to the user
621   `PATH`, so without it the margin rests on two append orders staying as they
622   are, one of them in a file Microsoft owns and revises.
623 - **Sysinternals ships every architecture in one zip**, and the ARM64 build is
624   not the default-named exe. `ProcessExplorer.zip` contains `procexp.exe` (x86),
625   `procexp64.exe` (x64) and `procexp64a.exe` (**ARM64**) — so run the `*64a.exe`
626   variants; plain `procexp.exe` runs emulated. (`Microsoft.Sysinternals.Suite`
627   has a dedicated `SysinternalsSuite-ARM64.zip` if you prefer the whole set.)
628
629 - **x64 emulation is checked, not assumed.** Everything in the "emulated" row
630   needs Prism, which is present on Windows 11 on Arm but absent on Windows 10 on
631   Arm (x86-only there) and on some Server images. The elevated half tests for
632   `System32\xtajit64.dll` up front and warns, rather than letting the failure
633   surface much later as a binary that will not start.
634 - **VirtualBox is skipped.** There is no ARM64 Windows host build, and the x64
635   one cannot be rescued by emulation: a hypervisor is kernel-mode, and its x64
636   driver will not load on an ARM64 kernel. This is also what makes
637   `setup-windows-7-test-env.bat` unreachable from an ARM64 host — it prepares a
638   Windows 7 *x86* guest, which neither VirtualBox nor Hyper-V on ARM64 can run.
639   (That script itself is unchanged; it runs inside the guest, so it never sees
640   the host architecture.)
641 - **The `v141` / Windows XP toolset is skipped**, and the verification step says
642   `n/a` instead of warning about it. The components are still *listed* in the
643   catalog on Arm, so this is a deliberate skip rather than a hard unavailability
644   — but the 14.16 toolset predates Windows on Arm as a host and ships
645   `HostX86`/`HostX64` compilers only, so the best you could get is an
646   x86-emulated compiler, and XP targeting is not supported from an Arm host.
647   Nothing is lost that this box could have used:
648   Windows XP never ran on ARM64, so the only point of an XP-targeting build here
649   would be producing x86 binaries, which the current toolset does natively via
650   `-A Win32`.
651 - **OpenCppCoverage is no longer installed on ARM64.** Unlike the rest of the
652   emulated row this was a real ceiling rather than a slowdown: it collects
653   coverage by debugging the process under test and stepping x86/x64
654   instructions, so it can cover cross-compiled x86/x64 binaries but **not** an
655   ARM64 one. A coverage tool that cannot cover this machine's own output belongs
656   on the x64 box, which is where it still installs. Run coverage there.
657 - **BinSkim is no longer installed on ARM64 either**, though the reason is
658   weaker: the NuGet package publishes `win-x64` only (its other RIDs are
659   `linux-x64`, `linux-arm64`, `osx-x64`), but BinSkim *reads* PE headers and load
660   configs, so its own architecture is independent of the binaries it analyses —
661   an emulated x64 BinSkim checks ARM64 binaries perfectly well. It went with the
662   rest of the x64-only set to keep the rule simple, not because it was broken.
663   The RID list is still ordered preference on x64, so a future `win-arm64` build
664   would be picked up with no further change.
665 - **`rsync` is native on ARM64 now**, and its `ssh.exe` is still verified by
666   running it. The release publishes an `arm64` asset as of `v3.5.0-gdeeda96f`;
667   both binaries in it are `0xAA64`, and the `ssh.exe` reports
668   `OpenSSH_for_Windows_10.0p2 ... LibreSSL 3.8.2` on this box.
669
670   Before that asset existed, ARM64 took the x64 zip. The `rsync.exe` was fine —
671   a transfer is bounded by the socket, not by emulated CPU — but its `ssh.exe`
672   links against `System32\libcrypto.dll`, which on ARM64 is an **ARM64** binary
673   an x64 process cannot load. It died with `0xC0000135`
674   (`STATUS_DLL_NOT_FOUND`) and had to be deleted on every run. That matters more
675   than it sounds: `rsync.exe` *prefers* an `ssh.exe` in its own directory, so a
676   present-but-broken one does not degrade to the in-box client — it breaks rsync
677   outright, with an error naming a dead remote shell rather than `ssh.exe`.
678
679   The elevated half still runs `ssh -V` after unpacking and **deletes** the
680   binary if it will not start. The check is kept because it is what found that
681   problem, and it still covers a missing or too-old `libcrypto.dll` on any
682   architecture. An x64 `rsync.exe` surviving on an ARM64 box now shows up in the
683   audit as emulation *without* a listed reason, with a remedy pointing at a
684   re-run. The `core.sshCommand` step in the non-elevated half already picked its
685   candidate by running it, and is unchanged.
686 - **Intel VTune reports `n/a`** rather than printing a download link: there is no
687   Windows-on-Arm build, and its value is reading Intel PMU counters. Use the
688   Windows Performance Toolkit (`wpr`/`xperf` to collect, `wpa` to analyse), or
689   Arm Performance Studio / Streamline for Arm PMU sampling.
690 - **NASM is not installed on ARM64.** It assembles x86/x86-64 only — there is no
691   ARM64 target in it and no ARM64 build of it — so it went into the x64-only set
692   with the rest. The ARM64 assembler is `armasm64.exe`, which ships with MSVC.
693   Its installer is also one of the two here that fall back to a per-user
694   directory it never registers (`%LOCALAPPDATA%\bin\NASM`), so `winget uninstall`
695   cannot find it afterwards; the `Uninstall.exe` in that directory is the way out.
696 - **`vswhere` stays under the 32-bit Program Files** on ARM64 too
697   (`%ProgramFiles(x86)%\Microsoft Visual Studio\Installer`), so that path is
698   unchanged. The VS Installer is x86-registered there by contract even though the
699   installer binaries themselves are native.
700 - **The WDK registry probe reads both hives.** `wdksetup.exe` is 32-bit and
701   writes under `WOW6432Node` on x64, but which hive a kit lands in has varied
702   across versions and architectures, and reading only one made an installed WDK
703   look absent — costing a needless multi-GB reinstall on every run.