]> vilimpoc.org git repositories - dotfiles/blobdiff - README.md
dotfiles: stop the ssh.exe check from deleting the ssh.exe it verifies
[dotfiles] / README.md
index 9615defe39c8ea04d48933b99551275328671610..933c53478030a3a03bc3856f42d0dae3c3b155b3 100644 (file)
--- a/README.md
+++ b/README.md
@@ -3,20 +3,24 @@
 Windows development-box provisioning scripts.
 
 `setup-windows.bat` takes a fresh Windows install to a working C++ / native
 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 2022
+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.
 
 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.
 
+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
+[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. |
 ## 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 and BinSkim on the user `PATH`, and the global git config (identity, plus `core.sshCommand`). 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 2022 Community with the required components, the WDK, and the Windows Performance Toolkit, and finally grants one ordinary account the rights to collect ETW traces without elevation. Can also be run directly from an Administrator prompt — pass `-TraceUser DOMAIN\user` to say who gets those rights, and `-EtwRightsOnly` to do that step alone. |
+| `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-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-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
@@ -97,10 +101,11 @@ throwaway VM reachable from a Linux host.
   `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
   `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 OS bitness, verifies it against the
-  published `.sha256`, and unpacks the pair together. 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.
   `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.
@@ -117,7 +122,36 @@ throwaway VM reachable from a Linux host.
   a box on a known build, pin the tag in `$RsyncUrl`
   (`.../releases/download/<tag>/<asset>`) instead.
 - Visual Studio is installed in three labelled passes (base workload, Clang/LLVM,
   a box on a known build, pin the tag in `$RsyncUrl`
   (`.../releases/download/<tag>/<asset>`) instead.
 - Visual Studio is installed in three labelled passes (base workload, Clang/LLVM,
-  XP toolset) so a failure identifies which component group is responsible.
+  XP toolset) so a failure identifies which component group is responsible. On
+  ARM64 only the first runs — the other two are skipped there, each printing why.
+  The last two are optional passes: a failure there warns and provisioning continues,
+  because neither is needed to build with MSVC and losing the whole toolchain
+  over a component you can add later from the installer UI is the worse outcome.
+- **Which Visual Studio generation.** `$VsChannel` near the VS step picks it —
+  `17` for VS 2022, `18` for VS 2026 — and `$VsEdition` picks Community /
+  Professional / Enterprise. It is **architecture-split by default**: 17 on x64,
+  where the `v141`/XP toolset in the component list is the reason to pin a
+  generation, and 18 on ARM64, where that toolset cannot exist anyway (no
+  ARM64-hosted 14.16 compiler), so nothing holds Arm back to the older one.
+- **The aka.ms path differs per generation** — VS 2022 is published under
+  `/release/`, VS 2026 under `/stable/`, which `$VsChannelPath` derives. This is
+  not cosmetic: `https://aka.ms/vs/18/release/vs_community.exe` is *not* a 404,
+  it silently redirects to Bing and returns **200 with an HTML body**. A wrong
+  guess there downloads a web page, names it `vs_community.exe`, and fails later
+  at `Start-Process` with something that looks nothing like a bad URL. The step
+  therefore checks the downloaded file starts with an `MZ` header and throws with
+  the URL if it does not.
+- `Get-VsInstallPath` scopes its `vswhere` query to that generation
+  (`-version "[17.0,18.0)"`). That scoping is load-bearing: a bare
+  `vswhere -products *` returns **every** Visual Studio on the box, newest
+  first, and on a machine that already has VS 2026 the old unscoped lookup handed
+  an 18.x install path to a 17.x bootstrapper as `modify --installPath`, so every
+  pass targeted the wrong product. The step now also lists any other generations
+  it found and states that it left them alone — when none of them matches
+  `$VsChannel` the script is about to fetch a second, largely redundant
+  toolchain, and that should be a visible decision. On ARM64, where `$VsChannel`
+  is 18, an existing VS 2026 **is** the match and gets modified in place rather
+  than duplicated.
 - **Windows Performance Analyzer is not part of Visual Studio.** VS has its own
   Performance Profiler, which is a different, `.diagsession`-based tool and
   cannot open an `.etl`. WPA ships with `xperf` and `wpr` in the Windows
 - **Windows Performance Analyzer is not part of Visual Studio.** VS has its own
   Performance Profiler, which is a different, `.diagsession`-based tool and
   cannot open an `.etl`. WPA ships with `xperf` and `wpr` in the Windows
@@ -137,75 +171,382 @@ throwaway VM reachable from a Linux host.
   msstore`); it is not installed here because the Store source needs an
   interactive, signed-in session, which the unattended elevated half does not
   have.
   msstore`); it is not installed here because the Store source needs an
   interactive, signed-in session, which the unattended elevated half does not
   have.
-- **Tracing without a UAC prompt.** `xperf` and `wpr` fail for a standard user in
-  two different ways, because two different things are missing:
+- **Intel VTune Profiler is reported, not installed.** The elevated half prints
+  whether it is on the box, its version, and the path to `vtune.exe`; if it is
+  missing it prints the download page instead (and says so if the CPU is not
+  Intel). Automating the install is not worth it here: the offline installer is a
+  ~750 MB download from a URL carrying a per-release GUID with no "latest"
+  redirect behind it, so every new build would mean editing a hard-coded link,
+  and it is only worth having on Intel silicon since hardware event-based
+  sampling reads Intel PMU counters. It does install unattended if you want it
+  scripted elsewhere:
 
 
-  ```text
-  xperf -on base             -> NT Kernel Logger: Access is denied. (0x5)
-  wpr -start GeneralProfile  -> Failed to enable the policy to profile system performance.
+  ```powershell
+  intel-vtune-<version>_offline.exe -a --silent --cli --eula accept
   ```
 
   ```
 
-  Three separate things are in the way, and all three have to be dealt with:
-
-  1. Creating or controlling *any* ETW session — even a user-mode one naming a
-     single provider — is checked against the security descriptor ETW keeps per
-     provider GUID under `HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security`,
-     whose **default** grants the session-control rights to SYSTEM,
-     Administrators, the service accounts and `BUILTIN\Performance Log Users`,
-     and to nobody else.
-  2. Switching on the *kernel* provider needs the `SeSystemProfilePrivilege` user
-     right ("Profile system performance"), held by default only by
-     Administrators and `NT SERVICE\WdiServiceHost` — that is the one `wpr`
-     names.
-  3. The kernel logger is not covered by that default descriptor.
-     `SystemTraceControlGuid` — the session both `xperf -on` and `wpr` drive —
-     carries an explicit one that does not mention Performance Log Users. With
-     1 and 2 in place a user-mode session starts and the privilege is held, and
-     `xperf -on base` *still* answers "NT Kernel Logger: Access is denied" while
-     `wpr`'s error changes to a bare `0x80070005`; even reading that descriptor
-     comes back access-denied, which is the tell. So an ACE for the group is
-     added with `EventAccessControl`, `TRACELOG_ACCESS_KERNEL_LOGGER` included.
-
-  The privilege and the ACE both go to the **group**, and the account then goes
-  into the group, so membership alone is the switch and enabling another account
-  afterwards is just `net localgroup "Performance Log Users" <user> /add`.
-  `SeDebugPrivilege` is deliberately *not* granted: CPU sampling and stack walks
-  of your own processes do not need it, and it is equivalent to handing out
-  administrator. Worth being clear about the cost: a member of that group can
-  capture system-wide kernel traces — process, image, file and registry activity
-  across every account on the box, paths and command lines included.
-
-  Two consequences worth knowing. A privilege and a group membership are both
-  read into the access token **at logon**, so the account must sign out and back
-  in for 1 and 2 — any new logon does it, and an `ssh` login into the box is the
-  quick way to check without dropping the desktop. The ACE in 3 is machine state
-  instead, and a logon does nothing for it: ETW reads these descriptors into a
-  cache, so it takes a **reboot** — with the ACE written and readable, `xperf -on
-  base` was still denied from a fresh shell on the running system. Plan on both
-  on a first run. And this only
-  helps a **non-admin** account: UAC hands an administrator a filtered token
-  carrying just five harmless privileges, so an admin's ordinary shell still
-  cannot trace however the policy reads. Verify from the target account,
-  unelevated:
+  **Run it from an administrator account, elevated.** Hardware event-based
+  sampling (`uarch-exploration`, `memory-access`, `hotspots -knob
+  sampling-mode=hw`) requires it, and VTune warns about that at the top of every
+  unelevated run. Worth knowing that the failure it gives there is *"cannot
+  recognize the processor"*, which reads like a hardware problem and is not one:
+  the drivers (`sepdrv5`, `sepdal`, `vtss`) are installed and running, and VTune
+  identifies the PMU through them. There is no group to join to get around it —
+  the Linux driver can be handed to a `vtune` group, but on Windows the
+  documented answer is to run as administrator.
 
 
-  ```powershell
-  whoami /priv | findstr SeSystemProfilePrivilege
-  xperf -on base ; xperf -stop C:\Temp\trace.etl
+- **Collect traces from an elevated Administrator session. Non-elevated
+  collection was tried here and abandoned.** The attempt was to put one ordinary
+  account into `BUILTIN\Performance Log Users`, which appears in the default
+  security descriptors ETW keeps per provider GUID under
+  `HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security`, and collect without a UAC
+  prompt. It does not survive contact with the real workflow: `xperf -on base`
+  and `wpr -start` drive the *NT Kernel Logger*, reserved for Administrators and
+  LocalSystem, and granting the group `SeSystemProfilePrivilege` ("Profile system
+  performance") plus an explicit ACE for `TRACELOG_ACCESS_KERNEL_LOGGER` on
+  `SystemTraceControlGuid` — all three in place, across a reboot — still answered
+
+  ```text
+  xperf: error: NT Kernel Logger: Access is denied. (0x5).
   ```
 
   ```
 
-  Analysis never needed any of this — `wpa.exe` opens an existing `.etl` as a
-  plain user. This is only about collection.
+  It is not a check an ACE overrides, and the same wall turned up often enough
+  elsewhere that the whole approach was dropped rather than carried as a
+  half-working path. **Sign in to an administrator account and run `xperf`, `wpr`
+  and VTune from an elevated prompt.** Analysis is the exception and never needed
+  any of this: `wpa.exe` opens an existing `.etl` as a plain user.
 
 
-  The rights step runs **first** in the elevated half, and `-EtwRightsOnly` runs
-  it and nothing else. It is seconds of LSA and registry work where a full run is
-  dominated by the three Visual Studio passes, which take minutes even with
-  nothing to do:
+  If an earlier revision of these scripts ran on a box, it left that account in
+  the group. Take it back out with:
 
   ```powershell
 
   ```powershell
-  Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass',
-      '-File','<repo>\setup-windows-with-uac.ps1','-TraceUser','DOMAIN\user','-EtwRightsOnly'
+  net localgroup "Performance Log Users" DOMAIN\user /delete
   ```
   ```
+
+  Two revisions also granted the privilege and the ACE. Undo the privilege in
+  `secpol.msc` > Local Policies > User Rights Assignment > "Profile system
+  performance" by removing Performance Log Users. The ACEs sit in the
+  `{9e814aad-3204-11d2-9a82-006008a86939}` value under
+  `HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security`: strip the `LU` entries
+  from that descriptor rather than deleting the value, which also carries entries
+  for SYSTEM, Administrators and two service accounts.
+
 - 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
   need them — each group is a plain array near the top.
 - 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
   need them — each group is a plain array near the top.
+
+## Running on ARM64
+
+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
+[ARM64 installs less, on purpose](#arm64-installs-less-on-purpose) below for the
+list and the reasoning.
+
+### Detecting the architecture
+
+All three scripts read the **machine-level registry value**, not the environment
+and not `RuntimeInformation.OSArchitecture`. Both of those report the *emulated*
+architecture inside an emulated x64 process — which is what you get by launching
+`powershell` or `cmd` from an x64 shell (Git Bash, for one) on an ARM64 box.
+Measured on this machine, from that emulated shell:
+
+| source | reports |
+| --- | --- |
+| `[RuntimeInformation]::OSArchitecture` | `X64` ✗ |
+| `%PROCESSOR_ARCHITECTURE%` | `AMD64` ✗ |
+| `HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment` | `ARM64` ✓ |
+
+`OSArchitecture` is documented as the *OS's* architecture, and on .NET Core it
+is; under .NET Framework on Prism it is not. It is kept only as a fallback. This
+used to be a cosmetic mislabel in a log line — now that the architecture decides
+which packages are installed at all, reading it wrong provisions the wrong
+machine.
+
+> The batch file calls `reg.exe` by full path and parses it **without** piping to
+> `find`/`findstr`. Both are shadowable by anything earlier on the `PATH`, and a
+> POSIX `find` turns the whole detection into a silent no-op that leaves the
+> emulated answer standing. That is not hypothetical; it is how this was found.
+
+**The build toolchain is fully native.** Visual Studio's ARM64 installer and MSVC
+are ARM64 binaries, and MSVC cross-compiles every target from an ARM64 host with
+no emulation in the compiler:
+
+```bat
+cmake -B build -A ARM64   && cmake --build build --config Release   :: native
+cmake -B build -A x64     && cmake --build build --config Release   :: cross
+cmake -B build -A Win32   && cmake --build build --config Release   :: cross
+```
+
+**ARM64 uses Visual Studio 2026** (`$VsChannel = 18`) while x64 stays on VS 2022.
+Nothing forces that split — the Arm box simply has no reason to stay on the older
+generation, since the `v141`/XP toolset that pins x64 there cannot run on Arm at
+all — so Arm takes the newer MSVC. Note this is a *default*, not a limitation:
+VS 2026 does still offer `v141` and `WinXP` as components on x64.
+
+The base component group now names `VC.Tools.x86.x64` **and** `VC.Tools.ARM64`
+explicitly instead of relying on `--includeRecommended`, which resolves
+differently per host: on an ARM64 machine the workload's recommended set is the
+ARM64-targeting toolchain, and the x64 cross-compiler is *not* implied. Spectre
+runtimes and ATL are likewise requested for both target families
+(`VC.ATL.ARM64.Spectre` is a separate component from `VC.ATL.Spectre`).
+
+### ARM64 installs less, on purpose
+
+The ARM64 machine here is a constrained VM, and nearly everything it was
+installing without a native build was going to run under Prism emulation anyway.
+The exceptions were individually defensible and collectively a VM full of x64
+binaries. So the split is now explicit: **ARM64 gets MSVC and nothing that
+duplicates it or that exists only as an x64 binary.**
+
+| dropped on ARM64 | where | why |
+| --- | --- | --- |
+| `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 |
+| `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 |
+| `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 |
+
+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.
+
+**`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
+that: the `VC.Llvm.Clang` VSIX is `productArch=neutral` with no `chip` or
+`machineArch` restriction, so the Arm installer offers it; and MSVC's
+`VC\Tools\Llvm` tree is partitioned by **host** architecture, with real ARM64
+binaries in `ARM64\bin`, which is where `clang-cl.exe` lands:
+
+| directory | host |
+| --- | --- |
+| `VC\Tools\Llvm\bin` | x86 |
+| `VC\Tools\Llvm\x64\bin` | x64 |
+| `VC\Tools\Llvm\ARM64\bin` | ARM64 |
+
+> Watch for a false positive when checking by hand: `VC\Tools\Llvm\*\bin` holds
+> `clang-format.exe` and `clang-tidy.exe` on **any** host, installed or not —
+> those ship with the NativeDesktop workload. Their presence does not mean the
+> compiler is there; look for `clang-cl.exe`.
+
+Note that the VS installer never uninstalls a component you simply stopped
+asking for, so a box provisioned before this split keeps its `clang-cl` until it
+is explicitly removed. The verification step says so and prints the
+`--remove` line.
+
+**The WDK is dropped for two reasons, and the second one bites on x64 too.** The
+ARM64 box builds user-mode software, so it does not need a driver kit — but this
+particular WDK *cannot* install here regardless. The kit requires the **matching
+Windows SDK revision**, and revisions are not side-by-side: `Include\10.0.26100.0`
+is one directory, whichever revision wrote it. VS 2026 installs SDK
+**26100.7705**; `linkid=2335869` is WDK **26100.6584**, the kit paired with VS
+2022. `wdksetup.exe /quiet` therefore aborts:
+
+```
+WDK installer exit code: 15605
+WER: WDK / 10.1.26100.6584 / Install / 0x80070642
+```
+
+`0x642` is `1602`, `ERROR_INSTALL_USEREXIT` — a silent install cancelling itself
+on a failed prerequisite check. Nothing to do with Arm or emulation; the same
+pairing fails on x64. If a driver ever does need building against VS 2026, use
+the kit Microsoft's [supported WDK downloads](https://learn.microsoft.com/en-us/windows-hardware/drivers/other-wdk-downloads)
+table pairs with it — the 28000.x one — not this link. x64 with VS 2022 has the
+matched pair, so it installs there.
+
+**Compiler diversity now lives on the x64 box**, which has room for three
+toolchains: MSVC, VS's `clang-cl`, and **upstream LLVM** (`winget install
+LLVM.LLVM`). Upstream runs on its own schedule, is usually several major versions
+ahead of the Clang VS bundles, and installs to `C:\Program Files\LLVM` rather
+than inside the VS tree, so the two are genuinely independent. Two Clang majors
+plus MSVC over the same sources is what catches the bugs a single toolchain
+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.
+
+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). |
+| **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. |
+
+### The architecture audit
+
+`setup-windows-no-uac.ps1` ends with an **`ArchAudit`** step that reads the PE
+COFF header of every tool the box provisions and reports what you would actually
+run, resolved PATH-first. It reads the file itself rather than trusting package
+metadata — a multi-architecture package (Sysinternals) ships every build in one
+zip under different names, and an installer's metadata says nothing about which
+binary landed. Purely informational; it never fails the run. Skip it with
+`-Skip ArchAudit`.
+
+Anything emulated with a listed reason prints as expected. Anything emulated
+*without* one is called out, with a remedy where a native build exists — which is
+how the `ninja` problem below was found.
+
+### Elevation, and the case where there is no prompt
+
+`setup-windows.bat` works out **once, at the top**, whether this account can
+elevate, and reports it as `Elevation: ALREADY | PROMPT | NOLUA`:
+
+| | |
+| --- | --- |
+| `ALREADY` | Already elevated. The elevated half runs **directly** — no point asking to elevate what already is. |
+| `PROMPT` | Not elevated, UAC on. Elevation is requested: an administrator gets a consent prompt, a standard user gets an over-the-shoulder **credential** prompt. |
+| `NOLUA` | UAC is off machine-wide (`EnableLUA = 0`) **and** this is not an administrator. Elevation is *impossible*, not merely declined. |
+
+`IsInRole(Administrator)` is false for an admin running unelevated under UAC,
+which is what makes `ALREADY` mean *actually elevated* rather than *could be*.
+
+**Why `NOLUA` needs detecting rather than attempting.** `Start-Process -Verb
+RunAs` is not honest about it. With UAC disabled the `RunAs` verb is **silently
+ignored**: no prompt appears, the child starts with the caller's own unelevated
+token, and `Start-Process` reports **exit code 0** as though it had worked. The
+elevated script then refuses itself on its `#Requires -RunAsAdministrator` line,
+exits 1, and never reaches its `Start-Transcript` — so there is no log either.
+Read naively that is indistinguishable from a cancelled UAC prompt, which is
+exactly what this script used to report, sending you to look for a prompt that
+could never have appeared.
+
+Measured on such a box: `Start-Process -Verb RunAs` returned exit 0 while the
+child reported itself as the *unelevated* calling user.
+
+**What is skipped when elevation is impossible.** The elevated half, plus four
+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
+only there for the WiX MSI tooling, CMake also ships inside Visual Studio, AGI
+profiles Android devices, and OpenCppCoverage cannot instrument ARM64 binaries
+anyway.
+
+A `NOLUA` run is a **success** (exit 0), not a failure — the per-user half did
+its job. To finish the box, run it from an administrator account, then re-run
+`setup-windows-no-uac.ps1` as yourself so the per-user `PATH` and `.gitconfig`
+land in *your* profile rather than the administrator's.
+
+Two consequences worth knowing rather than rediscovering:
+
+- **LLVM does not fail — it relocates.** Its NSIS installer targets
+  `%ProgramFiles%\LLVM`, and when it cannot write there it silently falls back to
+  a per-user directory (observed: `%USERPROFILE%\Documents\LLVM`) while winget
+  still reports *Successfully installed*. The compiler is genuinely there and
+  native; nothing can find it. The `LlvmPath` step looks in that fallback, puts
+  the directory on the user `PATH`, and says so.
+- **WinMerge silently downgrades to x64.** Upstream publishes ARM64 as a
+  **machine-scope** installer and x64 as a **per-user** one, so an unelevated
+  winget lets its scope preference beat its architecture preference. The batch
+  now asks for `--architecture arm64` first and falls back, so an admin run gets
+  the native build and a standard-user run still gets a working one.
+
+### Two gotchas the audit surfaces
+
+- **Visual Studio bundles an x64 `ninja.exe` even on ARM64.** It matters more
+  than a one-off tool would, because ninja is re-invoked for every edge in the
+  build graph — it is the one place emulation is paid over and over rather than
+  once. `winget install Ninja-build.Ninja` gets the native `ninja-winarm64`
+  build, and the `NinjaPath` step pins its directory to the **front** of the user
+  `PATH`. VS's bundled `cmake.exe` *is* ARM64, so only ninja has this problem.
+
+  On which copy wins inside a Developer Command Prompt: the native one. VS adds
+  its ninja from `Common7\Tools\vsdevcmd\ext\cmake.bat`, which does
+  `set "PATH=%PATH%;...\CMake\bin;...\CMake\Ninja"` — an **append**, so the VS
+  directories land at the very end of the composed `PATH`, behind every machine
+  and user entry. A native ninja anywhere on the user `PATH` already beats it,
+  measured on an ARM64 box. The `NinjaPath` step is therefore insurance rather
+  than the load-bearing fix: winget *appends* its package directory to the user
+  `PATH`, so without it the margin rests on two append orders staying as they
+  are, one of them in a file Microsoft owns and revises.
+- **Sysinternals ships every architecture in one zip**, and the ARM64 build is
+  not the default-named exe. `ProcessExplorer.zip` contains `procexp.exe` (x86),
+  `procexp64.exe` (x64) and `procexp64a.exe` (**ARM64**) — so run the `*64a.exe`
+  variants; plain `procexp.exe` runs emulated. (`Microsoft.Sysinternals.Suite`
+  has a dedicated `SysinternalsSuite-ARM64.zip` if you prefer the whole set.)
+
+- **x64 emulation is checked, not assumed.** Everything in the "emulated" row
+  needs Prism, which is present on Windows 11 on Arm but absent on Windows 10 on
+  Arm (x86-only there) and on some Server images. The elevated half tests for
+  `System32\xtajit64.dll` up front and warns, rather than letting the failure
+  surface much later as a binary that will not start.
+- **VirtualBox is skipped.** There is no ARM64 Windows host build, and the x64
+  one cannot be rescued by emulation: a hypervisor is kernel-mode, and its x64
+  driver will not load on an ARM64 kernel. This is also what makes
+  `setup-windows-7-test-env.bat` unreachable from an ARM64 host — it prepares a
+  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.)
+- **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
+  — but the 14.16 toolset predates Windows on Arm as a host and ships
+  `HostX86`/`HostX64` compilers only, so the best you could get is an
+  x86-emulated compiler, and XP targeting is not supported from an Arm host.
+  Nothing is lost that this box could have used:
+  Windows XP never ran on ARM64, so the only point of an XP-targeting build here
+  would be producing x86 binaries, which the current toolset does natively via
+  `-A Win32`.
+- **OpenCppCoverage is no longer installed on ARM64.** Unlike the rest of the
+  emulated row this was a real ceiling rather than a slowdown: it collects
+  coverage by debugging the process under test and stepping x86/x64
+  instructions, so it can cover cross-compiled x86/x64 binaries but **not** an
+  ARM64 one. A coverage tool that cannot cover this machine's own output belongs
+  on the x64 box, which is where it still installs. Run coverage there.
+- **BinSkim is no longer installed on ARM64 either**, though the reason is
+  weaker: the NuGet package publishes `win-x64` only (its other RIDs are
+  `linux-x64`, `linux-arm64`, `osx-x64`), but BinSkim *reads* PE headers and load
+  configs, so its own architecture is independent of the binaries it analyses —
+  an emulated x64 BinSkim checks ARM64 binaries perfectly well. It went with the
+  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` 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 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
+  installer binaries themselves are native.
+- **The WDK registry probe reads both hives.** `wdksetup.exe` is 32-bit and
+  writes under `WOW6432Node` on x64, but which hive a kit lands in has varied
+  across versions and architectures, and reading only one made an installed WDK
+  look absent — costing a needless multi-GB reinstall on every run.