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