]> vilimpoc.org git repositories - dotfiles/blob - README.md
dotfiles: pin the native ninja ahead of the one VS bundles
[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). See
14 [Running on ARM64](#running-on-arm64) for what differs.
15
16 ## Files
17
18 | File | Purpose |
19 | --- | --- |
20 | `setup-windows.bat` | Entry point. Runs the winget installs, then launches the elevated half and prints its log, then runs the non-elevated script. |
21 | `setup-windows-no-uac.ps1` | The non-elevated, per-user half: WinMerge and BinSkim on the user `PATH`, 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. |
22 | `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. |
23 | `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. |
24
25 ## Usage
26
27 1. **Edit `setup-windows-no-uac.ps1` first.** The global git identity near the
28    top of the file is empty:
29
30    ```powershell
31    $GitUserName  = ''   # e.g. 'Ada Lovelace'
32    $GitUserEmail = ''   # e.g. 'ada@example.com'
33    ```
34
35    Fill in your own name and email, or leave them empty to keep your identity
36    per-repository - the script skips `user.name` / `user.email` rather than
37    writing a placeholder, and says so. Everything else in that script is set
38    either way.
39
40 2. Run it from a normal (non-elevated) prompt:
41
42    ```bat
43    setup-windows.bat
44    ```
45
46    It will raise a single UAC prompt for the elevated half. Accept it — declining
47    leaves Visual Studio and the WDK uninstalled, and the script says so.
48
49 3. Restart your shell afterwards so the updated user `PATH` is picked up, and
50    reboot if a step reported that a restart was required.
51
52 ## Notes
53
54 - The elevated half writes a transcript to `setup-windows-uac.log` next to the
55   script; the batch file prints it when the elevated window closes. The log is
56   gitignored, as it contains local paths.
57 - **git uses a Win32-OpenSSH client.** `setup-windows-no-uac.ps1` sets
58   `core.sshCommand`. Git for Windows otherwise prefers its own bundled MSYS2
59   `ssh.exe`, which cannot reach the Windows `ssh-agent` service that the elevated
60   half enables - Win32-OpenSSH publishes the agent on a named pipe the MSYS2
61   build does not speak. Without this, keys loaded with `ssh-add` from PowerShell
62   are invisible to `git`, and a push falls back to hunting for a key file and
63   prompting for its passphrase. The value uses forward slashes on purpose: git
64   parses `core.sshCommand` with shell quoting rules, in which a backslash is an
65   escape character.
66 - **Which `ssh.exe` git gets.** `C:\Tools\rsync\ssh.exe` — the fast build the
67   elevated half unpacks — if it is there, `%WINDIR%/System32/OpenSSH/ssh.exe`
68   otherwise. It is the same client with the same `~/.ssh`, agent and
69   `known_hosts`; the difference is the stdin pump, and without it anything git
70   *pushes* is capped at ~17 MB/s. Candidates are tried by **running** them
71   (`ssh -V`), not by `Test-Path`: the fast build needs a `libcrypto.dll` that an
72   image without the OpenSSH Client capability does not have, and a client that
73   will not start should be found here rather than on the next `git push`.
74   This is also why `setup-windows.bat` now runs the elevated half **first** — the
75   fast `ssh.exe` has to exist before the git config step can prefer it. The
76   non-elevated half still runs even when the elevated one failed; it just falls
77   back. Re-run `setup-windows-no-uac.ps1` on its own at any time to re-pick.
78 - All three scripts are idempotent — re-running skips anything already installed.
79   BinSkim in particular checks NuGet for the newest stable version *before*
80   downloading: the package is a self-contained .NET build well over 100 MB, and
81   re-provisioning an up-to-date box should not pay for it. The installed version
82   is tracked in `nupkg-version.txt` beside the tool.
83 - `setup-windows-no-uac.ps1` runs its steps independently: one failing warns and
84   the rest still run, and it exits 1 if any did. The `.bat` reports that and
85   carries on to the elevated half, which is the part worth the UAC prompt. Use
86   `-Skip` to re-run a subset, e.g. `.\setup-windows-no-uac.ps1 -Skip BinSkim`.
87   Run it **non-elevated**: it writes per-user state (the `HKCU` `PATH`, the
88   `.gitconfig` under `%USERPROFILE%`), so an elevated run would configure the
89   administrator's profile instead. It warns if you do.
90 - **Remote access.** OpenSSH Server is installed from the Windows on-demand
91   capability (10/1809+), set to start automatically, and given an inbound TCP 22
92   firewall rule on *all* profiles — a VM's host-only or bridged adapter is
93   routinely classified Public, which is the usual reason a running `sshd` is
94   unreachable. Windows ships no `rsync`, so a build of it
95   ([nuket/rsync-windows](https://github.com/nuket/rsync-windows)) is installed to
96   `C:\Tools\rsync` and added to the **machine** `PATH`. That last detail matters:
97   the remote end of an `rsync` runs non-interactively, with no login shell, and
98   Win32-OpenSSH builds that environment from the registry `PATH` rather than from
99   a profile. Key auth needs `~/.ssh/authorized_keys` ACL'd to just you and
100   `SYSTEM`; accounts in the Administrators group use
101   `C:\ProgramData\ssh\administrators_authorized_keys` instead.
102 - **rsync brings its own `ssh.exe`.** That release ships as one zip per
103   architecture — `rsync-windows-x64.zip` / `rsync-windows-x86.zip`, each holding
104   `rsync.exe`, an `ssh.exe`, `COPYING.txt` and `NOTICE-ssh.txt` — and the
105   elevated half picks the zip for the host architecture (ARM64 takes the x64 one;
106   there is no ARM64 asset), verifies it against the published `.sha256`, unpacks
107   the pair together, then **runs** the unpacked `ssh.exe` and removes it if it
108   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. The
126   last two are optional passes: a failure there warns and provisioning continues,
127   because neither is needed to build with MSVC and losing the whole toolchain
128   over a component you can add later from the installer UI is the worse outcome.
129 - **Which Visual Studio generation.** `$VsChannel` near the VS step picks it —
130   `17` for VS 2022, `18` for VS 2026 — and `$VsEdition` picks Community /
131   Professional / Enterprise. It is **architecture-split by default**: 17 on x64,
132   where the `v141`/XP toolset in the component list is the reason to pin a
133   generation, and 18 on ARM64, where that toolset cannot exist anyway (no
134   ARM64-hosted 14.16 compiler), so nothing holds Arm back to the older one.
135 - **The aka.ms path differs per generation** — VS 2022 is published under
136   `/release/`, VS 2026 under `/stable/`, which `$VsChannelPath` derives. This is
137   not cosmetic: `https://aka.ms/vs/18/release/vs_community.exe` is *not* a 404,
138   it silently redirects to Bing and returns **200 with an HTML body**. A wrong
139   guess there downloads a web page, names it `vs_community.exe`, and fails later
140   at `Start-Process` with something that looks nothing like a bad URL. The step
141   therefore checks the downloaded file starts with an `MZ` header and throws with
142   the URL if it does not.
143 - `Get-VsInstallPath` scopes its `vswhere` query to that generation
144   (`-version "[17.0,18.0)"`). That scoping is load-bearing: a bare
145   `vswhere -products *` returns **every** Visual Studio on the box, newest
146   first, and on a machine that already has VS 2026 the old unscoped lookup handed
147   an 18.x install path to a 17.x bootstrapper as `modify --installPath`, so every
148   pass targeted the wrong product. The step now also lists any other generations
149   it found and states that it left them alone — when none of them matches
150   `$VsChannel` the script is about to fetch a second, largely redundant
151   toolchain, and that should be a visible decision. On ARM64, where `$VsChannel`
152   is 18, an existing VS 2026 **is** the match and gets modified in place rather
153   than duplicated.
154 - **Windows Performance Analyzer is not part of Visual Studio.** VS has its own
155   Performance Profiler, which is a different, `.diagsession`-based tool and
156   cannot open an `.etl`. WPA ships with `xperf` and `wpr` in the Windows
157   Performance Toolkit, which exists in exactly two places: as an optional
158   *feature* of the Windows SDK (`OptionId.WindowsPerformanceToolkit`) and inside
159   the Windows ADK, which bundles the same toolkit. Whether the SDK install that
160   Visual Studio performs selects that feature varies by version, so the elevated
161   half **detects first** — `%ProgramFiles(x86)%\Windows Kits\10\Windows
162   Performance Toolkit`, its 64-bit twin, and the ADK location — and only falls
163   back to `winget install Microsoft.WindowsADK` when nothing is there. It then
164   re-asserts that directory on the machine `PATH` (the toolkit's own installer
165   usually does this, and the Start Menu gets *Windows Kits > Windows Performance
166   Toolkit* shortcuts for WPA and WPR). To install just the toolkit instead of the
167   whole ADK, run the standalone SDK setup with
168   `winsdksetup.exe /features OptionId.WindowsPerformanceToolkit /q`. A newer WPA
169   also exists in the Microsoft Store (`winget install --id 9N0W1B2BXGNZ --source
170   msstore`); it is not installed here because the Store source needs an
171   interactive, signed-in session, which the unattended elevated half does not
172   have.
173 - **Intel VTune Profiler is reported, not installed.** The elevated half prints
174   whether it is on the box, its version, and the path to `vtune.exe`; if it is
175   missing it prints the download page instead (and says so if the CPU is not
176   Intel). Automating the install is not worth it here: the offline installer is a
177   ~750 MB download from a URL carrying a per-release GUID with no "latest"
178   redirect behind it, so every new build would mean editing a hard-coded link,
179   and it is only worth having on Intel silicon since hardware event-based
180   sampling reads Intel PMU counters. It does install unattended if you want it
181   scripted elsewhere:
182
183   ```powershell
184   intel-vtune-<version>_offline.exe -a --silent --cli --eula accept
185   ```
186
187   **Run it from an administrator account, elevated.** Hardware event-based
188   sampling (`uarch-exploration`, `memory-access`, `hotspots -knob
189   sampling-mode=hw`) requires it, and VTune warns about that at the top of every
190   unelevated run. Worth knowing that the failure it gives there is *"cannot
191   recognize the processor"*, which reads like a hardware problem and is not one:
192   the drivers (`sepdrv5`, `sepdal`, `vtss`) are installed and running, and VTune
193   identifies the PMU through them. There is no group to join to get around it —
194   the Linux driver can be handed to a `vtune` group, but on Windows the
195   documented answer is to run as administrator.
196
197 - **Collect traces from an elevated Administrator session. Non-elevated
198   collection was tried here and abandoned.** The attempt was to put one ordinary
199   account into `BUILTIN\Performance Log Users`, which appears in the default
200   security descriptors ETW keeps per provider GUID under
201   `HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security`, and collect without a UAC
202   prompt. It does not survive contact with the real workflow: `xperf -on base`
203   and `wpr -start` drive the *NT Kernel Logger*, reserved for Administrators and
204   LocalSystem, and granting the group `SeSystemProfilePrivilege` ("Profile system
205   performance") plus an explicit ACE for `TRACELOG_ACCESS_KERNEL_LOGGER` on
206   `SystemTraceControlGuid` — all three in place, across a reboot — still answered
207
208   ```text
209   xperf: error: NT Kernel Logger: Access is denied. (0x5).
210   ```
211
212   It is not a check an ACE overrides, and the same wall turned up often enough
213   elsewhere that the whole approach was dropped rather than carried as a
214   half-working path. **Sign in to an administrator account and run `xperf`, `wpr`
215   and VTune from an elevated prompt.** Analysis is the exception and never needed
216   any of this: `wpa.exe` opens an existing `.etl` as a plain user.
217
218   If an earlier revision of these scripts ran on a box, it left that account in
219   the group. Take it back out with:
220
221   ```powershell
222   net localgroup "Performance Log Users" DOMAIN\user /delete
223   ```
224
225   Two revisions also granted the privilege and the ACE. Undo the privilege in
226   `secpol.msc` > Local Policies > User Rights Assignment > "Profile system
227   performance" by removing Performance Log Users. The ACEs sit in the
228   `{9e814aad-3204-11d2-9a82-006008a86939}` value under
229   `HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security`: strip the `LU` entries
230   from that descriptor rather than deleting the value, which also carries entries
231   for SYSTEM, Administrators and two service accounts.
232
233 - The scripts were extracted from a native Windows project, so the component
234   selection is tuned for that: Spectre-mitigated runtimes, the v141/XP toolset,
235   and driver-kit headers. Trim the component lists in the `.ps1` if you don't
236   need them — each group is a plain array near the top.
237
238 ## Running on ARM64
239
240 All three scripts detect the host architecture and adapt. Nothing needs a flag —
241 run `setup-windows.bat` exactly as on x64. Both PowerShell halves use
242 `RuntimeInformation.OSArchitecture` rather than `%PROCESSOR_ARCHITECTURE%`,
243 because an emulated PowerShell reports the *emulated* architecture in the
244 environment variable and the real one through the API.
245
246 **The build toolchain is fully native.** Visual Studio's ARM64 installer, MSVC
247 and clang-cl are all ARM64 binaries, and MSVC cross-compiles every target from
248 an ARM64 host with no emulation in the compiler:
249
250 ```bat
251 cmake -B build -A ARM64   && cmake --build build --config Release   :: native
252 cmake -B build -A x64     && cmake --build build --config Release   :: cross
253 cmake -B build -A Win32   && cmake --build build --config Release   :: cross
254 ```
255
256 **ARM64 uses Visual Studio 2026** (`$VsChannel = 18`) while x64 stays on VS 2022.
257 Nothing forces that split — the Arm box simply has no reason to stay on the older
258 generation, since the `v141`/XP toolset that pins x64 there cannot run on Arm at
259 all — so Arm takes the newer MSVC. Note this is a *default*, not a limitation:
260 VS 2026 does still offer `v141` and `WinXP` as components on x64.
261
262 The base component group now names `VC.Tools.x86.x64` **and** `VC.Tools.ARM64`
263 explicitly instead of relying on `--includeRecommended`, which resolves
264 differently per host: on an ARM64 machine the workload's recommended set is the
265 ARM64-targeting toolchain, and the x64 cross-compiler is *not* implied. Spectre
266 runtimes and ATL are likewise requested for both target families
267 (`VC.ATL.ARM64.Spectre` is a separate component from `VC.ATL.Spectre`).
268
269 **Three native compilers, on purpose.** MSVC and `clang-cl` from Visual Studio,
270 plus **upstream LLVM** (`winget install LLVM.LLVM`, which resolves to
271 `LLVM-<ver>-woa64.exe` on ARM64 — "Windows on Arm 64"). Upstream runs on its own
272 schedule and is usually several major versions ahead of the Clang that VS
273 bundles, and it installs to `C:\Program Files\LLVM` rather than inside the VS
274 tree, so the two are genuinely independent. Two Clang majors plus MSVC over the
275 same sources is what catches the bugs a single toolchain agrees with itself
276 about. Watch the `PATH` if you use it: a bare `clang-cl` may resolve to upstream
277 while CMake's `-T ClangCL` keeps using VS's, so pass a full
278 `-DCMAKE_CXX_COMPILER` when it matters.
279
280 **Clang/LLVM is installed on ARM64 too, and it is native there** — MSVC and
281 `clang-cl` over the same sources is the compiler diversity this box is after.
282 Two things establish that it is genuinely native rather than an emulated x64
283 compiler: the `VC.Llvm.Clang` VSIX is `productArch=neutral` with no `chip` or
284 `machineArch` restriction, so the Arm installer offers it; and MSVC's
285 `VC\Tools\Llvm` tree is partitioned by **host** architecture, with real ARM64
286 binaries already in `ARM64\bin`, which is where `clang-cl.exe` lands:
287
288 | directory | host |
289 | --- | --- |
290 | `VC\Tools\Llvm\bin` | x86 |
291 | `VC\Tools\Llvm\x64\bin` | x64 |
292 | `VC\Tools\Llvm\ARM64\bin` | ARM64 |
293
294 Unlike the `v141`/XP group, nothing technical is in the way. Upstream LLVM ships
295 a Windows-on-Arm build independently too — `winget install LLVM.LLVM` resolves to
296 `LLVM-<ver>-woa64.exe` on ARM64 — if you want a Clang outside Visual Studio.
297
298 > Watch for a false positive when checking by hand: `VC\Tools\Llvm\*\bin` holds
299 > `clang-format.exe` and `clang-tidy.exe` on **any** host, installed or not —
300 > those ship with the NativeDesktop workload. Their presence does not mean the
301 > compiler is there; look for `clang-cl.exe`. The verification step does exactly
302 > that, and warns separately if only a non-native `clang-cl` landed on Arm.
303
304 What changes, and why:
305
306 | | On ARM64 |
307 | --- | --- |
308 | **Native ARM64** | Git, Python, .NET SDK, PowerShell, VS Code, Windows Terminal, WinMerge, Brave, Sysinternals, Claude Code, CMake, Ninja, the VS installer, **MSVC, VS clang-cl and upstream LLVM**, MSBuild, the SDK tools (`rc`, `signtool`), the in-box OpenSSH client, and the Windows Performance Toolkit. |
309 | **Emulated x64** | `iperf3`, NASM, OpenCppCoverage, BinSkim, `rsync.exe`, Android GPU Inspector, and the WDK/ADK *installers* (the kits they lay down are native). |
310 | **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). |
311 | **Unavailable** | VirtualBox, the Windows 7 x86 test VM, Intel VTune, and the `v141` / Windows XP targeting toolset. |
312
313 ### The architecture audit
314
315 `setup-windows-no-uac.ps1` ends with an **`ArchAudit`** step that reads the PE
316 COFF header of every tool the box provisions and reports what you would actually
317 run, resolved PATH-first. It reads the file itself rather than trusting package
318 metadata — a multi-architecture package (Sysinternals) ships every build in one
319 zip under different names, and an installer's metadata says nothing about which
320 binary landed. Purely informational; it never fails the run. Skip it with
321 `-Skip ArchAudit`.
322
323 Anything emulated with a listed reason prints as expected. Anything emulated
324 *without* one is called out, with a remedy where a native build exists — which is
325 how the `ninja` problem below was found.
326
327 ### Two gotchas the audit surfaces
328
329 - **Visual Studio bundles an x64 `ninja.exe` even on ARM64.** It matters more
330   than a one-off tool would, because ninja is re-invoked for every edge in the
331   build graph — it is the one place emulation is paid over and over rather than
332   once. `winget install Ninja-build.Ninja` gets the native `ninja-winarm64`
333   build, and the `NinjaPath` step pins its directory to the **front** of the user
334   `PATH`. VS's bundled `cmake.exe` *is* ARM64, so only ninja has this problem.
335
336   On which copy wins inside a Developer Command Prompt: the native one. VS adds
337   its ninja from `Common7\Tools\vsdevcmd\ext\cmake.bat`, which does
338   `set "PATH=%PATH%;...\CMake\bin;...\CMake\Ninja"` — an **append**, so the VS
339   directories land at the very end of the composed `PATH`, behind every machine
340   and user entry. A native ninja anywhere on the user `PATH` already beats it,
341   measured on an ARM64 box. The `NinjaPath` step is therefore insurance rather
342   than the load-bearing fix: winget *appends* its package directory to the user
343   `PATH`, so without it the margin rests on two append orders staying as they
344   are, one of them in a file Microsoft owns and revises.
345 - **Sysinternals ships every architecture in one zip**, and the ARM64 build is
346   not the default-named exe. `ProcessExplorer.zip` contains `procexp.exe` (x86),
347   `procexp64.exe` (x64) and `procexp64a.exe` (**ARM64**) — so run the `*64a.exe`
348   variants; plain `procexp.exe` runs emulated. (`Microsoft.Sysinternals.Suite`
349   has a dedicated `SysinternalsSuite-ARM64.zip` if you prefer the whole set.)
350
351 - **x64 emulation is checked, not assumed.** Everything in the "emulated" row
352   needs Prism, which is present on Windows 11 on Arm but absent on Windows 10 on
353   Arm (x86-only there) and on some Server images. The elevated half tests for
354   `System32\xtajit64.dll` up front and warns, rather than letting the failure
355   surface much later as a binary that will not start.
356 - **VirtualBox is skipped.** There is no ARM64 Windows host build, and the x64
357   one cannot be rescued by emulation: a hypervisor is kernel-mode, and its x64
358   driver will not load on an ARM64 kernel. This is also what makes
359   `setup-windows-7-test-env.bat` unreachable from an ARM64 host — it prepares a
360   Windows 7 *x86* guest, which neither VirtualBox nor Hyper-V on ARM64 can run.
361   (That script itself is unchanged; it runs inside the guest, so it never sees
362   the host architecture.)
363 - **The `v141` / Windows XP toolset is skipped**, and the verification step says
364   `n/a` instead of warning about it. The components are still *listed* in the
365   catalog on Arm, so this is a deliberate skip rather than a hard unavailability
366   — but the 14.16 toolset predates Windows on Arm as a host and ships
367   `HostX86`/`HostX64` compilers only, so the best you could get is an
368   x86-emulated compiler, and XP targeting is not supported from an Arm host.
369   Nothing is lost that this box could have used:
370   Windows XP never ran on ARM64, so the only point of an XP-targeting build here
371   would be producing x86 binaries, which the current toolset does natively via
372   `-A Win32`.
373 - **OpenCppCoverage is installed but limited.** Unlike the rest of the emulated
374   row, this is a real ceiling rather than a slowdown: it collects coverage by
375   debugging the process under test and stepping x86/x64 instructions, so it
376   covers the x86/x64 binaries this box cross-compiles but **not** an ARM64 one.
377   Run coverage against the x64 build. The script prints this rather than leaving
378   you to discover it.
379 - **BinSkim is emulated and that is fine.** The NuGet package publishes `win-x64`
380   only (its other RIDs are `linux-x64`, `linux-arm64`, `osx-x64`). BinSkim *reads*
381   PE headers and load configs, so its own architecture is independent of the
382   binaries it analyses — an emulated x64 BinSkim checks ARM64 binaries perfectly
383   well. The RID list is ordered preference, so a future `win-arm64` build is
384   picked up with no further change.
385 - **`rsync` gets the x64 zip, and its `ssh.exe` is verified by running it.** The
386   release publishes x64 and x86 assets and no ARM64 one. x64 is still the right
387   pick — a transfer is bounded by the socket, not by emulated CPU, so lifting the
388   in-box client's 3 KB stdin cap wins by far more than emulation costs. But that
389   `ssh.exe` links against a `System32\libcrypto.dll` which on ARM64 is an **ARM64**
390   binary, so it may not load at all. That matters more than it sounds:
391   `rsync.exe` *prefers* an `ssh.exe` in its own directory, so a present-but-broken
392   one does not degrade to the in-box client — it breaks rsync outright, with an
393   error that names a dead remote shell rather than `ssh.exe`. The elevated half
394   therefore runs `ssh -V` after unpacking and **deletes** the binary if it will
395   not start, so `rsync` falls back to the native in-box client. The
396   `core.sshCommand` step in the non-elevated half already picked its candidate by
397   running it, and is unchanged.
398 - **Intel VTune reports `n/a`** rather than printing a download link: there is no
399   Windows-on-Arm build, and its value is reading Intel PMU counters. Use the
400   Windows Performance Toolkit (`wpr`/`xperf` to collect, `wpa` to analyse), or
401   Arm Performance Studio / Streamline for Arm PMU sampling.
402 - **NASM is still installed** (as emulated x64) because this box cross-compiles
403   x86/x64, where NASM is the assembler that builds them. If you only target
404   ARM64, nothing uses it — the ARM64 assembler is `armasm64.exe`, which ships
405   with MSVC.
406 - **`vswhere` stays under the 32-bit Program Files** on ARM64 too
407   (`%ProgramFiles(x86)%\Microsoft Visual Studio\Installer`), so that path is
408   unchanged. The VS Installer is x86-registered there by contract even though the
409   installer binaries themselves are native.
410 - **The WDK registry probe reads both hives.** `wdksetup.exe` is 32-bit and
411   writes under `WOW6432Node` on x64, but which hive a kit lands in has varied
412   across versions and architectures, and reading only one made an installed WDK
413   look absent — costing a needless multi-GB reinstall on every run.