]> vilimpoc.org git repositories - dotfiles/blobdiff - README.md
setup-windows: Msys2 step - MSYS2 with MinGW-w64 GCC, per-user, no elevation
[dotfiles] / README.md
index ed5d49d6ae8cfd9a4025ef6e0dcdb42264dfe88a..8c4d631ac8edd4d3e86c95570b374e7a60f3f36b 100644 (file)
--- a/README.md
+++ b/README.md
@@ -5,22 +5,32 @@ Windows development-box provisioning scripts.
 `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 Studio and a per-user Android SDK, 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`, or `-VirtualMachine Hyper-V` / `-VirtualMachine none`, 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, NASM and Android Studio installs: is what is on the box already current? Those are the 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: `%USERPROFILE%\.local\bin` (where `claude.exe` lives), 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
@@ -44,12 +54,111 @@ on each: x64 is the full workstation, ARM64 a lean single-compiler build box. Se
    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] 25 OpenSshClient       OpenSSH Client capability (ssh.exe, and the libcrypto.dll rsync's ssh.exe needs)
+      ...
+      [x] 29 VisualStudio        Visual Studio 2022 Community, C++: MSVC for x86/x64 + ARM64, SDK 26100 (several GB)
+      ...
+
+     44 of 44 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`), kept off a
+  Windows that is itself a virtual machine (`-NotInVM`), 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. |
+| `Msys2` | The base archive is downloaded only when `%LOCALAPPDATA%\msys64\usr\bin\pacman.exe` is missing. Otherwise `pacman -Syuu` updates what is there, and `pacman -S --needed` leaves the GCC packages alone when they are already installed. |
+| `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. |
+| `AndroidStudio` | Installed only when it is missing. Studio updates itself in the app, and winget's package is marked never to upgrade it; the check keeps winget from installing over a Studio it cannot match to its package, as on a box where Studio was installed some other way. |
+| `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. |
+| `AndroidSdk` | The command-line tools are re-downloaded only when Google's package index has a newer revision than the installed one; `android sdk install` updates the platform, build-tools and platform-tools only when they are out of date. A re-run with nothing new takes seconds. |
+| `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
@@ -76,15 +185,12 @@ on each: x64 is the full workstation, ARM64 a lean single-compiler build box. Se
   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.
@@ -101,12 +207,11 @@ on each: x64 is the full workstation, ARM64 a lean single-compiler build box. Se
   `SYSTEM`; accounts in the Administrators group use
   `C:\ProgramData\ssh\administrators_authorized_keys` instead.
 - **rsync brings its own `ssh.exe`.** That release ships as one zip per
-  architecture — `rsync-windows-x64.zip` / `rsync-windows-x86.zip`, each holding
-  `rsync.exe`, an `ssh.exe`, `COPYING.txt` and `NOTICE-ssh.txt` — and the
-  elevated half picks the zip for the host architecture (ARM64 takes the x64 one;
-  there is no ARM64 asset), verifies it against the published `.sha256`, unpacks
-  the pair together, then **runs** the unpacked `ssh.exe` and removes it if it
-  will not start. Together is the point:
+  architecture — `rsync-windows-x86.zip`, `-x64.zip` and `-arm64.zip`, each
+  holding `rsync.exe`, an `ssh.exe`, `COPYING.txt` and `NOTICE-ssh.txt` — and the
+  elevated half picks the zip matching the host, verifies it against the
+  published `.sha256`, unpacks the pair together, then **runs** the unpacked
+  `ssh.exe` and removes it if it will not start. Together is the point:
   `rsync.exe` prefers an `ssh.exe` sitting in its own directory, and the release
   builds one because the client Windows ships reads its stdin 3 KB at a time,
   which holds a transfer *from* the box at ~17 MB/s however fast the link is.
@@ -232,6 +337,97 @@ on each: x64 is the full workstation, ARM64 a lean single-compiler build box. Se
   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. The `LocalBinPath` step in
+  `setup-windows-no-uac.ps1` makes sure that directory is on the user `PATH`
+  (creating it if need be); the installer itself only prints a reminder. 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.
+- **Windows Terminal gets its framework package registered first.** Terminal is
+  an MSIX that depends on `Microsoft.UI.Xaml.2.8`, which winget installs for it
+  from a pinned build (8.2310.30001.0). When a *newer* build is already on the
+  machine but not registered for the account running the setup, Windows
+  declines to stage the older one, winget then tries to register a package
+  that is not there, and what it prints is `Installer failed with exit code:
+  0x80070002 : The system cannot find the file specified` — with Terminal
+  itself never attempted, on every run. So the batch file first registers the
+  build that is already there (`Add-AppxPackage -RegisterByFamilyName`, no
+  elevation needed), and winget then finds the dependency met. With none on
+  the machine it does nothing, and winget installs the framework as usual.
+- **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.
+- **MSYS2 and MinGW-w64 GCC** (`Msys2`) go into `%LOCALAPPDATA%\msys64`, per-user,
+  with no elevation: the MSYS2 base self-extracting archive unpacks wherever it
+  is pointed, whereas the winget package runs the installer machine-wide into
+  `C:\msys64`. Two environments get GCC: `ucrt64` (x86_64, links the UCRT) and
+  `mingw32` (i686, links `msvcrt.dll`, the one that can still target Windows XP).
+  These are the same toolchains GitHub's `windows-latest` runner carries, so a
+  build that works here works there. Nothing is added to the `PATH`: the
+  environments' `bin` directories hold same-named `gcc`, `ld` and `windres`, so a
+  project names the one it wants by full path, e.g.
+  `%LOCALAPPDATA%\msys64\mingw32\bin`.
+- **Android Studio and the Android SDK** are separate steps. Studio
+  (`Google.AndroidStudio`) is machine-wide — its installer elevates itself — and
+  is installed only when missing, since it updates itself. The SDK is per-user,
+  in Studio's default `%LOCALAPPDATA%\Android\Sdk` (or wherever `ANDROID_HOME`
+  already points): the command-line tools, platform-tools, and the newest
+  stable platform and build-tools from Google's package index, read at run
+  time. That is what Studio's first-run wizard would set up, and it is enough
+  to build: Gradle fetches whatever else a project asks for. The step sets
+  `ANDROID_HOME`, puts the command-line tools on the user `PATH`, and points
+  `JAVA_HOME` at Studio's bundled JDK for a command-line `gradlew` when there is
+  no `JAVA_HOME` already. The SDK licences are accepted on your behalf — the
+  plan says so before anything runs. Both are x64-only: Studio has no ARM64
+  build, and the SDK's Windows build tools are x86/x64. Three things about
+  the current command-line tools (23.0), found when this step was written:
+  - `sdkmanager` is deprecated. It is now a shim over the new **Android CLI**
+    (`android.exe`), which on first use installs itself into
+    `%USERPROFILE%\.android` — about 90 MB in `bin\`, and a `cli\` folder with
+    its own Java runtime. It also keeps a spool of **usage metrics** there and
+    uploads them; no opt-out was found. The step calls the CLI directly.
+  - **CLI 1.0.16406183 crashes on the way out of every `sdk` command**
+    (`0xC0000409`) on this Windows 10, after the work is done. So the step does
+    not trust its exit code: each package must be present afterwards, and a
+    crash with everything in place is printed as a note, not a failure.
+  - Windows PowerShell 5.1's progress bar made the 148 MB command-line tools
+    download take about ten minutes; with it off, the whole first install took
+    under a minute. The BinSkim download now skips the progress bar for the
+    same reason.
 - 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
@@ -243,7 +439,8 @@ All three scripts detect the host architecture and adapt. Nothing needs a flag 
 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.
 
@@ -309,15 +506,19 @@ duplicates it or that exists only as an x64 binary.**
 | `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 |
 | `LLVM.LLVM` | `setup-windows.bat` | second Clang, dropped along with VS's `clang-cl` |
 | `NASM.NASM` | `setup-windows.bat` | x86/x86-64 assembler with no ARM64 target; MSVC's `armasm64.exe` is the native one |
+| MSYS2 (`Msys2`) | `setup-windows.bat` | its base system is x86_64 and would run emulated, and the Windows XP target it is installed for is x64-box work |
 | `Google.AndroidGPUInspector` | `setup-windows.bat` | x64-only, and it profiles Android devices rather than anything compiled here |
 | `OpenCppCoverage` | `setup-windows.bat` | collects coverage by stepping x86/x64 instructions, so it cannot instrument an ARM64 binary at all |
 | VS Clang/LLVM component | `setup-windows-with-uac.ps1` | see below |
 | Windows Driver Kit | `setup-windows-with-uac.ps1` | see below |
 | BinSkim | `setup-windows-no-uac.ps1` | the NuGet package publishes `win-x64` only |
+| Android Studio (`Google.AndroidStudio`) | `setup-windows.bat` | Google publishes no Windows ARM64 build |
+| Android SDK | `setup-windows-no-uac.ps1` | its Windows build tools are x86/x64 only, as is Android Studio |
 
-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
@@ -372,15 +573,22 @@ agrees with itself about. Watch the `PATH` there: a bare `clang-cl` may resolve
 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` and `rsync.exe` — both 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. |
+| **Not installed** | Brave, WinMerge, upstream LLVM, NASM, Android GPU Inspector, OpenCppCoverage, BinSkim, VS `clang-cl`, the WDK, and the Android SDK — see the table above. |
+| **Unavailable** | VirtualBox, the Windows 7 x86 test VM, Intel VTune, Android Studio, and the `v141` / Windows XP targeting toolset. |
 
 ### The architecture audit
 
@@ -428,7 +636,8 @@ packages that sit in the "per-user" section but are machine-wide installers.
 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.
@@ -488,6 +697,13 @@ Two consequences worth knowing rather than rediscovering:
   Windows 7 *x86* guest, which neither VirtualBox nor Hyper-V on ARM64 can run.
   (That script itself is unchanged; it runs inside the guest, so it never sees
   the host architecture.)
+- **VirtualBox is also skipped inside a virtual machine**, on any architecture.
+  The planner reads the firmware's SMBIOS manufacturer and model
+  (`Win32_ComputerSystem`) and recognises VirtualBox, VMware, Hyper-V (and
+  Azure), QEMU/KVM, Parallels, Xen, EC2 and GCE; the plan's `machine:` line
+  names what it found. It does not use `HypervisorPresent`, which is also true
+  on physical hardware running Hyper-V, WSL2 or VBS. The test VMs belong on the
+  physical host.
 - **The `v141` / Windows XP toolset is skipped**, and the verification step says
   `n/a` instead of warning about it. The components are still *listed* in the
   catalog on Arm, so this is a deliberate skip rather than a hard unavailability
@@ -512,27 +728,37 @@ Two consequences worth knowing rather than rediscovering:
   rest of the x64-only set to keep the rule simple, not because it was broken.
   The RID list is still ordered preference on x64, so a future `win-arm64` build
   would be picked up with no further change.
-- **`rsync` gets the x64 zip, and its `ssh.exe` is verified by running it.** The
-  release publishes x64 and x86 assets and no ARM64 one. x64 is still the right
-  pick — a transfer is bounded by the socket, not by emulated CPU, so lifting the
-  in-box client's 3 KB stdin cap wins by far more than emulation costs. But that
-  `ssh.exe` links against a `System32\libcrypto.dll` which on ARM64 is an **ARM64**
-  binary, so it may not load at all. That matters more than it sounds:
-  `rsync.exe` *prefers* an `ssh.exe` in its own directory, so a present-but-broken
-  one does not degrade to the in-box client — it breaks rsync outright, with an
-  error that names a dead remote shell rather than `ssh.exe`. The elevated half
-  therefore runs `ssh -V` after unpacking and **deletes** the binary if it will
-  not start, so `rsync` falls back to the native in-box client. The
-  `core.sshCommand` step in the non-elevated half already picked its candidate by
-  running it, and is unchanged.
+- **`rsync` is native on ARM64 now**, and its `ssh.exe` is still verified by
+  running it. The release publishes an `arm64` asset as of `v3.5.0-gdeeda96f`;
+  both binaries in it are `0xAA64`, and the `ssh.exe` reports
+  `OpenSSH_for_Windows_10.0p2 ... LibreSSL 3.8.2` on this box.
+
+  Before that asset existed, ARM64 took the x64 zip. The `rsync.exe` was fine —
+  a transfer is bounded by the socket, not by emulated CPU — but its `ssh.exe`
+  links against `System32\libcrypto.dll`, which on ARM64 is an **ARM64** binary
+  an x64 process cannot load. It died with `0xC0000135`
+  (`STATUS_DLL_NOT_FOUND`) and had to be deleted on every run. That matters more
+  than it sounds: `rsync.exe` *prefers* an `ssh.exe` in its own directory, so a
+  present-but-broken one does not degrade to the in-box client — it breaks rsync
+  outright, with an error naming a dead remote shell rather than `ssh.exe`.
+
+  The elevated half still runs `ssh -V` after unpacking and **deletes** the
+  binary if it will not start. The check is kept because it is what found that
+  problem, and it still covers a missing or too-old `libcrypto.dll` on any
+  architecture. An x64 `rsync.exe` surviving on an ARM64 box now shows up in the
+  audit as emulation *without* a listed reason, with a remedy pointing at a
+  re-run. The `core.sshCommand` step in the non-elevated half already picked its
+  candidate by running it, and is unchanged.
 - **Intel VTune reports `n/a`** rather than printing a download link: there is no
   Windows-on-Arm build, and its value is reading Intel PMU counters. Use the
   Windows Performance Toolkit (`wpr`/`xperf` to collect, `wpa` to analyse), or
   Arm Performance Studio / Streamline for Arm PMU sampling.
-- **NASM is still installed** (as emulated x64) because this box cross-compiles
-  x86/x64, where NASM is the assembler that builds them. If you only target
-  ARM64, nothing uses it — the ARM64 assembler is `armasm64.exe`, which ships
-  with MSVC.
+- **NASM is not installed on ARM64.** It assembles x86/x86-64 only — there is no
+  ARM64 target in it and no ARM64 build of it — so it went into the x64-only set
+  with the rest. The ARM64 assembler is `armasm64.exe`, which ships with MSVC.
+  Its installer is also one of the two here that fall back to a per-user
+  directory it never registers (`%LOCALAPPDATA%\bin\NASM`), so `winget uninstall`
+  cannot find it afterwards; the `Uninstall.exe` in that directory is the way out.
 - **`vswhere` stays under the 32-bit Program Files** on ARM64 too
   (`%ProgramFiles(x86)%\Microsoft Visual Studio\Installer`), so that path is
   unchanged. The VS Installer is x86-registered there by contract even though the