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.
+Runs on **x64 and on ARM64** (Windows 11 on Arm). See
+[Running on ARM64](#running-on-arm64) for what differs.
+
## 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 reports whether Intel VTune Profiler is present. Can also be run directly from an Administrator 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
- **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:
+ elevated half picks the zip for the host architecture (ARM64 takes the x64 one;
+ there is no ARM64 asset), verifies it against the published `.sha256`, unpacks
+ the pair together, then **runs** the unpacked `ssh.exe` and removes it if it
+ will not start. Together is the point:
`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.
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. 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
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. Both PowerShell halves use
+`RuntimeInformation.OSArchitecture` rather than `%PROCESSOR_ARCHITECTURE%`,
+because an emulated PowerShell reports the *emulated* architecture in the
+environment variable and the real one through the API.
+
+**The build toolchain is fully native.** Visual Studio's ARM64 installer, MSVC
+and clang-cl are all 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`).
+
+**Three native compilers, on purpose.** MSVC and `clang-cl` from Visual Studio,
+plus **upstream LLVM** (`winget install LLVM.LLVM`, which resolves to
+`LLVM-<ver>-woa64.exe` on ARM64 — "Windows on Arm 64"). Upstream runs on its own
+schedule and is usually several major versions ahead of the Clang that VS
+bundles, and it 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` if you use it: 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.
+
+**Clang/LLVM is installed on ARM64 too, and it is native there** — MSVC and
+`clang-cl` over the same sources is the compiler diversity this box is after.
+Two things establish that it is genuinely native rather than an emulated x64
+compiler: 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 already 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 |
+
+Unlike the `v141`/XP group, nothing technical is in the way. Upstream LLVM ships
+a Windows-on-Arm build independently too — `winget install LLVM.LLVM` resolves to
+`LLVM-<ver>-woa64.exe` on ARM64 — if you want a Clang outside Visual Studio.
+
+> 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`. The verification step does exactly
+> that, and warns separately if only a non-native `clang-cl` landed on Arm.
+
+What changes, and why:
+
+| | On ARM64 |
+| --- | --- |
+| **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. |
+| **Emulated x64** | `iperf3`, NASM, OpenCppCoverage, BinSkim, `rsync.exe`, Android GPU Inspector, and the WDK/ADK *installers* (the kits they lay down are 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). |
+| **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.
+
+### Two gotchas the audit surfaces
+
+- **Visual Studio bundles an x64 `ninja.exe` even on ARM64**, and `VsDevCmd.bat`
+ **prepends** the VS directories to `PATH`. So inside a Developer Command Prompt
+ — exactly where C++ builds happen — the emulated ninja wins over the native one
+ `winget install Ninja-build.Ninja` provides. It matters more than a one-off
+ tool would, because ninja is re-invoked for every edge in the build graph. Pass
+ `-DCMAKE_MAKE_PROGRAM=` the native one, or put its directory ahead of the VS
+ one. VS's bundled `cmake.exe` *is* ARM64, so only ninja has this problem.
+- **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 installed but limited.** Unlike the rest of the emulated
+ row, this is a real ceiling rather than a slowdown: it collects coverage by
+ debugging the process under test and stepping x86/x64 instructions, so it
+ covers the x86/x64 binaries this box cross-compiles but **not** an ARM64 one.
+ Run coverage against the x64 build. The script prints this rather than leaving
+ you to discover it.
+- **BinSkim is emulated and that is fine.** The NuGet package publishes `win-x64`
+ only (its other RIDs are `linux-x64`, `linux-arm64`, `osx-x64`). 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. The RID list is ordered preference, so a future `win-arm64` build is
+ picked up with no further change.
+- **`rsync` gets the x64 zip, and its `ssh.exe` is verified by running it.** The
+ release publishes x64 and x86 assets and no ARM64 one. x64 is still the right
+ pick — a transfer is bounded by the socket, not by emulated CPU, so lifting the
+ in-box client's 3 KB stdin cap wins by far more than emulation costs. But that
+ `ssh.exe` links against a `System32\libcrypto.dll` which on ARM64 is an **ARM64**
+ binary, so it may not load at all. That matters more than it sounds:
+ `rsync.exe` *prefers* an `ssh.exe` in its own directory, so a present-but-broken
+ one does not degrade to the in-box client — it breaks rsync outright, with an
+ error that names a dead remote shell rather than `ssh.exe`. The elevated half
+ therefore runs `ssh -V` after unpacking and **deletes** the binary if it will
+ not start, so `rsync` falls back to the native in-box client. The
+ `core.sshCommand` step in the non-elevated half already picked its candidate by
+ running it, and is unchanged.
+- **Intel VTune reports `n/a`** rather than printing a download link: there is no
+ Windows-on-Arm build, and its value is reading Intel PMU counters. Use the
+ Windows Performance Toolkit (`wpr`/`xperf` to collect, `wpa` to analyse), or
+ Arm Performance Studio / Streamline for Arm PMU sampling.
+- **NASM is still installed** (as emulated x64) because this box cross-compiles
+ x86/x64, where NASM is the assembler that builds them. If you only target
+ ARM64, nothing uses it — the ARM64 assembler is `armasm64.exe`, which ships
+ with MSVC.
+- **`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.
to the PATH, so build scripts (and VsDevCmd.bat itself) complain that
'vswhere.exe' is not recognized
- BinSkim (binary hardening analyzer) in %LOCALAPPDATA%\Programs\BinSkim,
- on the user PATH
+ on the user PATH. The package has no win-arm64 build, so on ARM64 this is
+ the x64 tool under emulation - which analyses ARM64 binaries fine, since it
+ only reads their headers.
- Global git identity, and core.sshCommand pointed at a Win32-OpenSSH
client so git shares the Windows ssh-agent: the fast ssh.exe the elevated
half unpacks beside rsync.exe if it is there, the in-box one otherwise
+ - An architecture audit: the real PE machine type of every tool this box
+ provisions, resolved the way a shell would. Informational, never fatal.
+ Anything running emulated without a listed reason is called out.
FILL IN $GitUserName / $GitUserEmail below before the first run.
# for several, dot-call the script or use -Command:
# .\setup-windows-no-uac.ps1 -Skip BinSkim,GitConfig
# powershell -File .\setup-windows-no-uac.ps1 -Skip BinSkim
- [ValidateSet('WinMerge', 'VsWhere', 'BinSkim', 'GitConfig')]
+ [ValidateSet('WinMerge', 'VsWhere', 'BinSkim', 'GitConfig', 'ArchAudit')]
[string[]] $Skip = @()
)
$ErrorActionPreference = 'Stop'
+# Host architecture. RuntimeInformation rather than PROCESSOR_ARCHITECTURE: an
+# emulated PowerShell reports the emulated architecture in the environment
+# variable while this API reports the real one. Values: X64, Arm64, X86.
+$HostArch = [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
+$IsArm64 = ($HostArch -eq 'Arm64')
+
# --- Global git identity: FILL THESE IN BEFORE RUNNING ---
# Left empty, Set-GlobalGitConfig skips the identity and says so, rather than
# stamping a placeholder onto your commits. Leaving them empty is a legitimate
function Install-BinSkim {
# BinSkim checks the exact mitigations the native project enables in
# CMakeLists.txt (CFG/XFG, CET, ASLR/HighEntropyVA, DEP, /GS, stack cookies,
- # DEPENDENTLOADFLAG, ...). The NuGet package ships a self-contained win-x64
- # build, so this needs no .NET SDK/runtime: the .nupkg is a zip - extract the
- # win-x64 tool folder and put it on the PATH. After restarting the shell:
+ # DEPENDENTLOADFLAG, ...). The NuGet package ships a self-contained build, so
+ # this needs no .NET SDK/runtime: the .nupkg is a zip - extract the tool
+ # folder for this architecture and put it on the PATH. After restarting the
+ # shell:
# binskim analyze path\to\your.exe
#
+ # ARCHITECTURE. The package currently publishes win-x64 only (its other RIDs
+ # are linux-x64, linux-arm64 and osx-x64) - there is no win-arm64 build. So on
+ # ARM64 this installs the x64 tool and it runs under emulation. That is a
+ # slowdown and nothing more: BinSkim READS PE headers and load configs, so the
+ # architecture of the binaries it analyses is independent of its own - an
+ # emulated x64 BinSkim checks ARM64 binaries perfectly well. $BinSkimRids is
+ # ordered preference, so if a win-arm64 build ever ships, an ARM64 box picks
+ # it up with no further change here.
+ #
# VERSION CHECK FIRST. The .nupkg is a large download (well over 100 MB), so
# ask NuGet what the newest stable version is BEFORE fetching anything, and
# skip the download entirely when the installed copy already matches.
-Uri "https://api.nuget.org/v3-flatcontainer/$BinSkimPackage/$latest/$BinSkimPackage.$latest.nupkg"
Expand-Archive -Path $zip -DestinationPath $tmp -Force
- $src = Get-ChildItem -Path $tmp -Recurse -Filter 'BinSkim.exe' |
- Where-Object { $_.FullName -match 'win-x64' } |
- Sort-Object FullName | Select-Object -Last 1
- if (-not $src) { throw 'BinSkim.exe (win-x64) not found in package.' }
+ # Native RID first, emulatable one second. Matched against the path
+ # so the newest matching tools\<tfm>\<rid>\ folder wins, as before.
+ $BinSkimRids = if ($IsArm64) { @('win-arm64', 'win-x64') } else { @('win-x64') }
+ $src = $null
+ $allExes = Get-ChildItem -Path $tmp -Recurse -Filter 'BinSkim.exe'
+ foreach ($rid in $BinSkimRids) {
+ $src = $allExes | Where-Object { $_.FullName -match [regex]::Escape($rid) } |
+ Sort-Object FullName | Select-Object -Last 1
+ if ($src) {
+ if ($rid -ne $BinSkimRids[0]) {
+ Write-Host " No $($BinSkimRids[0]) build in the package; using $rid (runs under emulation)." -ForegroundColor Yellow
+ }
+ break
+ }
+ }
+ if (-not $src) { throw "BinSkim.exe ($($BinSkimRids -join ' / ')) not found in package." }
# Replace wholesale rather than merging over the old tree, so files
# dropped between releases don't linger.
# in-box client stays the fallback - and on a first provisioning run from
# setup-windows.bat it is the elevated half that runs first, so the fast one
# is normally already there.
+ #
+ # ON ARM64 the preference is the same but the trade is different: that build
+ # is published for x64 only, so it is the EMULATED client being preferred over
+ # a native ARM64 one. It is still the right pick when it runs - a push is
+ # bounded by the socket, not by emulated CPU, so lifting the 3KB stdin cap
+ # wins by far more than emulation costs - but it may well not run at all,
+ # because it links against a System32 libcrypto.dll that is an ARM64 binary
+ # here. That is exactly why candidates are tried by RUNNING them below rather
+ # than by Test-Path, and why the elevated half deletes that ssh.exe outright
+ # when it will not start. Either way this lands on a working client.
$sshCandidates = @(
(Join-Path $RsyncDir 'ssh.exe'),
(Join-Path $env:WINDIR 'System32\OpenSSH\ssh.exe')
Write-Host " $version"
}
+function Get-PEMachine {
+ # Architecture of a PE, read straight from the COFF header: the 2 bytes at
+ # the e_lfanew offset + 4. Cheap, and it answers the only question that
+ # matters here - would this exe run natively, or through emulation?
+ #
+ # Deliberately NOT Get-Command's .FileVersionInfo or the package metadata:
+ # a multi-architecture package (Sysinternals) ships every build in one zip
+ # under different names, and an installer's own metadata says nothing about
+ # which binary got laid down. The file itself cannot be wrong.
+ param([string]$Path)
+ if (-not (Test-Path $Path)) { return $null }
+ try {
+ $fs = [IO.File]::OpenRead($Path)
+ $br = New-Object IO.BinaryReader($fs)
+ try {
+ $fs.Seek(0x3c, 'Begin') | Out-Null
+ $pe = $br.ReadInt32()
+ if ($pe -le 0 -or $pe -gt ($fs.Length - 6)) { return 'not-PE' }
+ $fs.Seek($pe + 4, 'Begin') | Out-Null
+ switch ($br.ReadUInt16()) {
+ 0x8664 { 'x64' }
+ 0xAA64 { 'ARM64' }
+ 0x014c { 'x86' }
+ 0x01c4 { 'ARM32' }
+ default { 'unknown' }
+ }
+ } finally { $br.Close(); $fs.Close() }
+ } catch { $null }
+}
+
+function Invoke-ArchAudit {
+ # Report the actual architecture of the tools this box provisions, resolved
+ # the way a shell would (PATH first, then the usual install roots), so what
+ # is printed is what you would really run.
+ #
+ # This exists because "winget installed it" does not mean "you got the native
+ # build", and the gap is not always where you would guess - Visual Studio's
+ # own bundled ninja.exe is x64 even on an ARM64 host. A per-run audit turns
+ # that from something you trip over into something the log tells you.
+ #
+ # Purely informational: it never fails the run. On x64 everything is expected
+ # to be x64 and the output is dull; the value is on ARM64, where each line is
+ # either native or a known, listed exception.
+ Write-Step "Architecture audit (host: $HostArch)"
+
+ # Tools with no native ARM64 build available anywhere, with the reason. These
+ # print as expected rather than as problems - see the README's ARM64 section.
+ $KnownEmulated = @{
+ 'BinSkim.exe' = 'NuGet package publishes win-x64 only; it reads PE headers, so it still analyses ARM64 binaries'
+ 'rsync.exe' = 'no ARM64 asset published; transfer is socket-bound, not CPU-bound'
+ 'OpenCppCoverage.exe' = 'x86/x64 only, and cannot instrument ARM64 binaries - run coverage against the x64 build'
+ 'nasm.exe' = 'x86/x86-64 assembler by definition; ARM64 uses armasm64.exe from MSVC'
+ 'iperf3.exe' = 'no ARM64 build published; network-bound anyway'
+ 'py.exe' = 'python.org ships the launcher shim as x86; it execs the native python.exe'
+ 'vswhere.exe' = 'Microsoft ships x86 only; runs once per script'
+ }
+
+ # Fixable cases: a native build DOES exist, something just resolved ahead of
+ # it. Printed with the finding so the log carries the remedy, not just the
+ # complaint.
+ #
+ # ninja is the one that actually bites. Visual Studio bundles an x64 ninja.exe
+ # even on ARM64, and VsDevCmd.bat PREPENDS the VS directories to PATH - so
+ # inside a Developer Command Prompt the emulated one wins over the native
+ # winget copy, and that is exactly the shell C++ builds happen in. It matters
+ # more than a one-off tool because ninja is re-invoked for every edge in the
+ # build graph.
+ $Remedies = @{
+ 'ninja.exe' = 'Visual Studio bundles an x64 ninja and VsDevCmd prepends its directory. Pass -DCMAKE_MAKE_PROGRAM to the native one (winget install Ninja-build.Ninja), or put its directory ahead of the VS one.'
+ 'cmake.exe' = 'Install the native build with: winget install Kitware.CMake'
+ 'clang-cl.exe' = 'Install the upstream native LLVM with: winget install LLVM.LLVM'
+ }
+
+ $vs = $null
+ $vsWhereExe = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
+ if (Test-Path $vsWhereExe) {
+ $vs = & $vsWhereExe -products '*' -property installationPath -format value | Select-Object -First 1
+ }
+
+ # name -> extra candidate paths searched when the name is not on the PATH.
+ $targets = [ordered]@{
+ 'git.exe' = @("$env:LOCALAPPDATA\Programs\Git\cmd\git.exe", (Join-Path $env:ProgramFiles 'Git\cmd\git.exe'))
+ 'python.exe' = @()
+ 'py.exe' = @("$env:WINDIR\py.exe")
+ 'pwsh.exe' = @()
+ 'cmake.exe' = @((Join-Path $env:ProgramFiles 'CMake\bin\cmake.exe'))
+ 'ninja.exe' = @()
+ 'clang-cl.exe' = @((Join-Path $env:ProgramFiles 'LLVM\bin\clang-cl.exe'))
+ 'dotnet.exe' = @((Join-Path $env:ProgramFiles 'dotnet\dotnet.exe'))
+ 'WinMergeU.exe' = @((Join-Path $env:ProgramFiles 'WinMerge\WinMergeU.exe'), (Join-Path ${env:ProgramFiles(x86)} 'WinMerge\WinMergeU.exe'))
+ 'BinSkim.exe' = @((Join-Path $BinSkimDir 'BinSkim.exe'))
+ 'rsync.exe' = @((Join-Path $RsyncDir 'rsync.exe'))
+ 'ssh.exe' = @((Join-Path $env:WINDIR 'System32\OpenSSH\ssh.exe'))
+ 'nasm.exe' = @()
+ 'iperf3.exe' = @()
+ 'OpenCppCoverage.exe' = @((Join-Path $env:ProgramFiles 'OpenCppCoverage\OpenCppCoverage.exe'), (Join-Path ${env:ProgramFiles(x86)} 'OpenCppCoverage\OpenCppCoverage.exe'))
+ 'vswhere.exe' = @($vsWhereExe)
+ 'xperf.exe' = @((Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Windows Performance Toolkit\xperf.exe'))
+ }
+ if ($vs) {
+ $targets['MSBuild.exe'] = @((Join-Path $vs 'MSBuild\Current\Bin\arm64\MSBuild.exe'), (Join-Path $vs 'MSBuild\Current\Bin\MSBuild.exe'))
+ # The MSVC and VS-Clang compilers, under whichever MSVC version is present.
+ $msvc = Get-ChildItem (Join-Path $vs 'VC\Tools\MSVC') -Directory -ErrorAction SilentlyContinue |
+ Sort-Object Name | Select-Object -Last 1
+ if ($msvc) {
+ $hostDir = if ($IsArm64) { 'Hostarm64\arm64' } else { 'Hostx64\x64' }
+ $targets['cl.exe (MSVC)'] = @((Join-Path $msvc.FullName "bin\$hostDir\cl.exe"))
+ }
+ $llvmHost = if ($IsArm64) { 'ARM64\bin' } else { 'x64\bin' }
+ $targets['clang-cl.exe (VS)'] = @((Join-Path $vs "VC\Tools\Llvm\$llvmHost\clang-cl.exe"))
+ }
+
+ $native = if ($IsArm64) { 'ARM64' } else { 'x64' }
+ $unexpected = @()
+ foreach ($name in $targets.Keys) {
+ # PATH first - that is the binary a build would actually invoke - then the
+ # explicit candidates. The bare exe name is stripped of any " (label)".
+ $exe = ($name -split ' ')[0]
+ $path = (Get-Command $exe -ErrorAction SilentlyContinue |
+ Select-Object -First 1).Source
+ if (-not $path) { $path = $targets[$name] | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1 }
+ if (-not $path) { Write-Host (" {0,-22} {1}" -f $name, '- not installed') -ForegroundColor DarkGray; continue }
+
+ $mach = Get-PEMachine $path
+ if (-not $mach) { continue }
+ if ($mach -eq $native) {
+ Write-Host (" {0,-22} {1,-6} native" -f $name, $mach) -ForegroundColor Green
+ } elseif ($KnownEmulated.ContainsKey($exe)) {
+ Write-Host (" {0,-22} {1,-6} expected: {2}" -f $name, $mach, $KnownEmulated[$exe]) -ForegroundColor DarkGray
+ } else {
+ Write-Host (" {0,-22} {1,-6} NOT NATIVE - $path" -f $name, $mach) -ForegroundColor Yellow
+ if ($Remedies.ContainsKey($exe)) {
+ Write-Host (" {0,-22} {1,-6} -> {2}" -f '', '', $Remedies[$exe]) -ForegroundColor Yellow
+ }
+ $unexpected += "$name ($mach)"
+ }
+ }
+
+ if ($unexpected) {
+ Write-Warning "Running under emulation with no listed reason: $($unexpected -join ', ')."
+ Write-Warning 'If a native build exists, prefer it (see the -> lines above); otherwise add it to $KnownEmulated with the reason.'
+ } else {
+ Write-Host ' Everything resolved to a native build, or to a listed exception.' -ForegroundColor Green
+ }
+}
+
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
+Write-Host "Host architecture: $HostArch" -ForegroundColor Cyan
+
if (([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Warning 'Running elevated. The user PATH and .gitconfig changes below will apply to the administrator profile, not yours.'
VsWhere = { Add-VsWhereToUserPath }
BinSkim = { Install-BinSkim }
GitConfig = { Set-GlobalGitConfig }
+ # Last on purpose: it reports on what the steps above (and the winget installs
+ # in setup-windows.bat) actually put on the box.
+ ArchAudit = { Invoke-ArchAudit }
}
$failed = @()
- OpenSSH Server (sshd) capability: automatic + started + inbound TCP 22\r
- rsync for Windows (nuket/rsync-windows) in C:\Tools\rsync, on the machine\r
PATH: rsync.exe plus the ssh.exe it runs, out of the release zip for this\r
- architecture\r
- - Visual Studio 2022 Community (C++ desktop workload, Spectre libs, WDK VSIX,\r
- Win11 SDK 26100, Clang/LLVM, and the v141 + Windows XP targeting toolset)\r
+ architecture (there is no ARM64 zip; ARM64 gets the x64 one, and the\r
+ ssh.exe is kept only if it actually starts)\r
+ - Visual Studio Community (C++ desktop workload, x86/x64 AND ARM64 build\r
+ tools, Spectre libs, WDK VSIX, Win11 SDK 26100, Clang/LLVM, and - on x64\r
+ only - the v141 + Windows XP targeting toolset). VS 2022 on x64, VS 2026\r
+ on ARM64; see $VsChannel.\r
- Windows Driver Kit 10.0.26100\r
- Windows Performance Toolkit - xperf, wpr and Windows Performance Analyzer\r
(wpa.exe) - on the machine PATH\r
\r
- Change $VsInstallerUrl below to the Professional or Enterprise bootstrapper if needed:\r
- Professional : https://aka.ms/vs/17/release/vs_professional.exe\r
- Enterprise : https://aka.ms/vs/17/release/vs_enterprise.exe\r
+ Change $VsEdition below to Professional or Enterprise if needed. $VsChannel\r
+ picks the Visual Studio generation and is architecture-split by default:\r
+ 17 (VS 2022) on x64, where the v141 / Windows XP toolset is wanted, and\r
+ 18 (VS 2026) on ARM64, where that toolset cannot exist anyway.\r
+\r
+ ARCHITECTURE. Runs on x64 and on ARM64 (Windows 11 on Arm). On ARM64 the\r
+ Visual Studio installer, MSVC and clang-cl are all native ARM64 and\r
+ cross-compile ARM64/x64/x86 targets; what changes is called out at each step,\r
+ and $HostArch below is what drives it.\r
#>\r
\r
$ErrorActionPreference = 'Stop'\r
\r
+# ---------------------------------------------------------------------------\r
+# Host architecture\r
+#\r
+# RuntimeInformation.OSArchitecture rather than PROCESSOR_ARCHITECTURE: this\r
+# script can be launched by a 32-bit or an emulated x64 PowerShell, either of\r
+# which reports the emulated architecture in the environment variable while this\r
+# API still reports the real one. Values seen here: X64, Arm64, X86.\r
+#\r
+# $IsArm64 gates:\r
+# - which rsync-windows release zip is fetched (there is no ARM64 one)\r
+# - which Visual Studio generation is driven ($VsChannel: 18 on Arm, 17 on x64)\r
+# - the Visual Studio component groups (ARM64 build tools in; the v141/XP\r
+# toolset out, because it has no ARM64-hosted compiler and Microsoft does not\r
+# ship Windows XP targeting for Arm hosts. Clang/LLVM is installed on every\r
+# architecture and is native on Arm, so it is NOT gated here)\r
+# - what the VTune and WPT steps report\r
+# ---------------------------------------------------------------------------\r
+$HostArch = [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()\r
+$IsArm64 = ($HostArch -eq 'Arm64')\r
+\r
function Write-Step([string]$Msg) {\r
Write-Host "`n==> $Msg" -ForegroundColor Cyan\r
}\r
}\r
}\r
\r
+function Get-VsInstallPath {\r
+ # The install path of a Visual Studio matching $script:VsChannel, or $null.\r
+ #\r
+ # -version is the point. A bare `vswhere -products *` returns EVERY Visual\r
+ # Studio on the box, newest first, and handing that path to a bootstrapper of\r
+ # a different generation is not a no-op: `vs_community.exe` (17.x) told to\r
+ # `modify --installPath <a VS 2026 install>` is a 17.x engine pointed at an\r
+ # 18.x product. On a box that already has VS 2026 - increasingly the default -\r
+ # every pass below would target the wrong install. Scope the query to the\r
+ # generation this script is actually driving.\r
+ if (-not (Test-Path $script:VsWhere)) { return $null }\r
+ $range = "[$script:VsChannel.0,$($script:VsChannel + 1).0)"\r
+ & $script:VsWhere -products '*' -version $range -property installationPath -format value |\r
+ Select-Object -First 1\r
+}\r
+\r
function Invoke-VsModify {\r
# Run one VS install/modify pass for a named group of components. Splitting\r
# the install into separate passes makes it obvious WHICH group fails: each\r
# call prints its label and exit code before Assert-ExitCode throws.\r
+ #\r
+ # -Optional downgrades a failure to a warning. Used for groups that are not\r
+ # available on every host architecture (the v141/XP toolset on ARM64) or that\r
+ # the rest of the box does not depend on, so one unavailable component cannot\r
+ # cost you the toolchain.\r
param(\r
[string] $Label,\r
- [string[]] $Ids\r
+ [string[]] $Ids,\r
+ [switch] $Optional\r
)\r
- Write-Step "VS2022: $Label"\r
+ Write-Step "Visual Studio: $Label"\r
+ if (-not $Ids) {\r
+ Write-Host ' (no components in this group for this architecture - skipping)' -ForegroundColor DarkGray\r
+ return\r
+ }\r
$addStr = ($Ids | ForEach-Object { "--add $_" }) -join ' '\r
# --installPath must be quoted: it contains spaces ("C:\Program Files\...").\r
# Windows PowerShell 5.1's Start-Process does not quote array elements, so we\r
Write-Host " > $script:VsBootstrapper $argString" -ForegroundColor DarkGray\r
$p = Start-Process -FilePath $script:VsBootstrapper -ArgumentList $argString -Wait -PassThru -NoNewWindow\r
Write-Host " exit code: $($p.ExitCode)"\r
- Assert-ExitCode $p.ExitCode "VS2022 ($Label)"\r
+ if ($Optional -and $p.ExitCode -notin @(0, 3010)) {\r
+ Write-Warning "Visual Studio ($Label) failed with exit code $($p.ExitCode); continuing (this group is optional)."\r
+ } else {\r
+ Assert-ExitCode $p.ExitCode "Visual Studio ($Label)"\r
+ }\r
\r
# After the first (fresh) install, re-detect the install path so subsequent\r
# passes use `modify`.\r
- if (-not $script:InstallPath -and (Test-Path $script:VsWhere)) {\r
- $script:InstallPath = & $script:VsWhere -products '*' -property installationPath -format value |\r
- Select-Object -First 1\r
- }\r
+ if (-not $script:InstallPath) { $script:InstallPath = Get-VsInstallPath }\r
}\r
\r
# ---------------------------------------------------------------------------\r
\r
try {\r
\r
+Write-Step "Host architecture: $HostArch"\r
+if ($IsArm64) {\r
+ # x64 emulation ("Prism") is what carries every x64-only tool this script\r
+ # installs - rsync.exe, the WDK and ADK installers, BinSkim in the\r
+ # non-elevated half. It is present on Windows 11 on Arm and absent on\r
+ # Windows 10 on Arm (x86-only there) and on some Server images, so check\r
+ # rather than assume: without it those steps install binaries that cannot\r
+ # start, and the failure would otherwise surface much later.\r
+ $Prism = Join-Path $env:WINDIR 'System32\xtajit64.dll'\r
+ if (Test-Path $Prism) {\r
+ Write-Host ' x64 emulation (Prism) present - x64-only tools will run.' -ForegroundColor Green\r
+ } else {\r
+ Write-Warning "x64 emulation not found ($Prism is missing). rsync.exe, the WDK/ADK installers and BinSkim have no ARM64 build and will not run on this box."\r
+ }\r
+}\r
+\r
# ---------------------------------------------------------------------------\r
# Base tools via winget\r
# ---------------------------------------------------------------------------\r
# ---------------------------------------------------------------------------\r
Write-Step 'rsync for Windows'\r
$RsyncRepo = 'nuket/rsync-windows'\r
-$RsyncAsset = if ([Environment]::Is64BitOperatingSystem) { 'rsync-windows-x64.zip' } else { 'rsync-windows-x86.zip' }\r
+# The release publishes exactly two assets - x64 and x86 - and no ARM64 one, so\r
+# on ARM64 the x64 build is the right pick: it runs under Prism, and an emulated\r
+# x64 rsync still moves data far faster than the ~17MB/s stdin cap that the whole\r
+# reason for using this build is to avoid. (The transfer is I/O-bound on the\r
+# socket, not on emulated CPU.) Selected off $HostArch rather than\r
+# Is64BitOperatingSystem, which answers "true" on ARM64 and so cannot tell the\r
+# two 64-bit cases apart.\r
+$RsyncAsset = switch ($HostArch) {\r
+ 'X64' { 'rsync-windows-x64.zip' }\r
+ 'Arm64' { 'rsync-windows-x64.zip' }\r
+ default { 'rsync-windows-x86.zip' }\r
+}\r
+if ($IsArm64) {\r
+ Write-Host ' ARM64: no native asset is published, using the x64 build under emulation.' -ForegroundColor Yellow\r
+}\r
# The /releases/latest/download/ redirect rather than the API: unauthenticated\r
# API calls are rate-limited to 60/hour per IP, which a provisioning run behind a\r
# shared NAT can genuinely exhaust, and the redirect costs none of that budget.\r
$RsyncExe = Join-Path $RsyncDir 'rsync.exe'\r
\r
# Does the release's ssh.exe have the libcrypto it needs? Decided before the\r
- # download so the answer can also gate what comes out of the zip.\r
+ # download so the answer can also gate what comes out of the zip. This is a\r
+ # cheap pre-filter only - the authoritative check is running the thing, which\r
+ # happens after the unpack below.\r
$SysCrypto = Join-Path $env:WINDIR 'System32\libcrypto.dll'\r
$WantSsh = Test-Path $SysCrypto\r
if (-not $WantSsh) {\r
if ($v -and ([version]($v -replace '[^0-9.]', '')) -lt [version]'3.8.2') {\r
Write-Warning "$SysCrypto is LibreSSL $v; the release's ssh.exe is built against 3.8.2 (Windows OpenSSH Client 9.5). Update Windows, or expect ssh.exe not to start."\r
}\r
+ if ($IsArm64) {\r
+ # On ARM64 the presence of libcrypto.dll proves less than it does on\r
+ # x64: System32 holds the ARM64 build of it, and the ssh.exe in the\r
+ # zip is x64. Whether an emulated x64 process can load that DLL comes\r
+ # down to whether it is a plain ARM64 binary or an ARM64X one - not\r
+ # something worth deciding by parsing the PE header, when running the\r
+ # binary answers it outright. Unpack it, then run it (below).\r
+ Write-Host ' ARM64: the x64 ssh.exe will be verified by running it, not by assuming.' -ForegroundColor Yellow\r
+ }\r
}\r
\r
# Download and unpack beside the targets, not over them, so an interrupted\r
Move-Item -Path $src -Destination (Join-Path $RsyncDir $f) -Force\r
}\r
Remove-Item -Recurse -Force $unpack\r
+\r
+ # Prove the unpacked ssh.exe actually starts, and DELETE it if it does not.\r
+ #\r
+ # This matters more than it looks. rsync.exe prefers an ssh.exe sitting in its\r
+ # own directory, so a present-but-unstartable one does not degrade to the\r
+ # in-box client - it breaks rsync outright, and the error you get is a remote\r
+ # shell that died rather than anything naming ssh.exe. The two ways to land\r
+ # there are a missing/old System32 libcrypto.dll (x64 boxes) and an ARM64 box\r
+ # whose ARM64 libcrypto cannot be loaded by this x64 binary. Removing it is\r
+ # the repair in both cases: rsync then falls back to the ssh on the PATH,\r
+ # which on ARM64 is the native in-box client.\r
+ #\r
+ # EAP back to Continue for the call: ssh -V writes its version to STDERR, and\r
+ # under $ErrorActionPreference = 'Stop' a native command's stderr becomes a\r
+ # terminating error, so a WORKING client would look like a broken one.\r
+ # $global:LASTEXITCODE is cleared first because an exe that cannot start at\r
+ # all throws without setting one, and the stale 0 from the previous native\r
+ # command would otherwise read as success.\r
+ $SshExe = Join-Path $RsyncDir 'ssh.exe'\r
+ if ($WantSsh -and (Test-Path $SshExe)) {\r
+ $prevEap = $ErrorActionPreference\r
+ $ErrorActionPreference = 'Continue'\r
+ $global:LASTEXITCODE = $null\r
+ $sshVer = $null\r
+ try { $sshVer = (& $SshExe -V 2>&1 | Select-Object -First 1) } catch { }\r
+ finally { $ErrorActionPreference = $prevEap }\r
+ if ($LASTEXITCODE -eq 0) {\r
+ Write-Host " ssh.exe runs: $sshVer"\r
+ } else {\r
+ $why = if ($null -eq $LASTEXITCODE) { 'it would not start' } else { "exit $LASTEXITCODE" }\r
+ Write-Warning "The release's ssh.exe does not run here ($why)$(if ($sshVer) { ": $sshVer" }). Removing it so rsync.exe falls back to the ssh on the PATH instead of failing on it."\r
+ Remove-Item $SshExe -Force -ErrorAction SilentlyContinue\r
+ $WantSsh = $false\r
+ }\r
+ }\r
Write-Host " Installed $RsyncExe$(if ($WantSsh) { ' and the ssh.exe it runs' })"\r
\r
# Machine PATH (HKLM environment). Idempotent: only appends if absent.\r
# Core C++ desktop workload\r
'Microsoft.VisualStudio.Workload.NativeDesktop'\r
\r
- # Spectre-mitigated MSVC runtime libs\r
+ # MSVC build tools. Named explicitly rather than left to --includeRecommended,\r
+ # because what that pulls in depends on the host: on an ARM64 machine the\r
+ # workload's recommended set is the ARM64-hosted toolchain targeting ARM64,\r
+ # and the x64 cross-compiler is NOT implied. Ask for both and the box builds\r
+ # every target it can, whichever architecture it is:\r
+ # on x64 -> x64-hosted, targeting x86/x64 and ARM64\r
+ # on ARM64 -> ARM64-hosted, targeting ARM64 and x86/x64\r
+ # Both are native toolchains; neither cross-compile runs under emulation.\r
+ 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64'\r
+ 'Microsoft.VisualStudio.Component.VC.Tools.ARM64'\r
+\r
+ # Spectre-mitigated MSVC runtime libs, for each target above\r
'Microsoft.VisualStudio.Component.VC.Runtimes.x86.x64.Spectre'\r
'Microsoft.VisualStudio.Component.VC.Runtimes.ARM64.Spectre'\r
\r
- # Spectre-mitigated ATL (needed for many driver/COM projects)\r
+ # Spectre-mitigated ATL (needed for many driver/COM projects). The x86/x64 and\r
+ # ARM64 ATL libraries are separate components; a driver or COM project built\r
+ # for ARM64 wants the second one, and it is not implied by the first.\r
'Microsoft.VisualStudio.Component.VC.ATL.Spectre'\r
+ 'Microsoft.VisualStudio.Component.VC.ATL.ARM64.Spectre'\r
\r
# Windows 11 SDK — build number must match the WDK below\r
'Microsoft.VisualStudio.Component.Windows11SDK.26100'\r
\r
# Clang/LLVM toolset (ClangCL, used in CMakePresets.json). Two parts: the Clang\r
# compiler itself, plus the MSBuild integration providing the "ClangCL" toolset.\r
+#\r
+# Installed on every architecture, ARM64 included, and native there - not an\r
+# emulated x64 compiler. Two things establish that: the VSIX is\r
+# productArch=neutral with no chip/machineArch restriction, so the Arm installer\r
+# offers it; and MSVC's VC\Tools\Llvm tree is partitioned by HOST architecture\r
+# (bin = x86, x64\bin = x64, ARM64\bin = ARM64) with genuine ARM64 binaries\r
+# already in ARM64\bin, which is where clang-cl.exe lands. Unlike the v141/XP\r
+# group below, nothing technical is in the way.\r
+#\r
+# This is the compiler diversity the box is after: MSVC and clang-cl over the\r
+# same sources, both native.\r
$ClangComponents = @(\r
'Microsoft.VisualStudio.Component.VC.Llvm.Clang'\r
'Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset'\r
# Windows XP targeting (v141_xp toolset, used in CMakePresets.json). The v141\r
# (VS2017) build tools provide the 14.16 compiler that the XP toolset wraps;\r
# WinXP layers the XP-compatible CRT/SDK on top of it.\r
-$XpComponents = @(\r
- 'Microsoft.VisualStudio.Component.VC.v141.x86.x64'\r
- 'Microsoft.VisualStudio.Component.WinXP'\r
-)\r
+#\r
+# NOT USED ON ARM64, and left empty there. The components are still listed in the\r
+# catalog on Arm, so this is not strictly "unavailable" - but the 14.16 toolset\r
+# predates Windows on Arm as a host and ships HostX86/HostX64 compilers only, so\r
+# the best you could get is an x86-emulated compiler, and Microsoft does not\r
+# support XP targeting from an Arm host. Nothing is lost that this box could have\r
+# used: Windows XP never ran on ARM64, so an XP-targeting build from an ARM64\r
+# host has no purpose beyond producing x86 binaries, which the current toolset\r
+# does natively via -A Win32. On x64 the group is installed exactly as before.\r
+$XpComponents = if ($IsArm64) { @() } else {\r
+ @(\r
+ 'Microsoft.VisualStudio.Component.VC.v141.x86.x64'\r
+ 'Microsoft.VisualStudio.Component.WinXP'\r
+ )\r
+}\r
\r
-# Detect an existing VS install via vswhere (ships with the VS Installer).\r
+# Which Visual Studio generation to drive: 17 = VS 2022, 18 = VS 2026. Both have\r
+# native ARM64 installers and ARM64-hosted MSVC. This picks the bootstrapper URL,\r
+# and - just as importantly - scopes the vswhere lookup below, so a box that\r
+# already has a DIFFERENT generation installed is not mistaken for this one.\r
+#\r
+# Split by architecture on purpose:\r
+# x64 -> 17. The v141 / Windows XP targeting toolset in $XpComponents is the\r
+# reason; that group is the whole point of pinning a generation here.\r
+# ARM64 -> 18. The XP group is skipped on Arm regardless (no ARM64-hosted 14.16\r
+# compiler), so nothing holds this back to 17, and VS 2026 brings the\r
+# newer MSVC. Together with the native ARM64 clang-cl from\r
+# $ClangComponents above, that is the compiler diversity on this box.\r
+$VsChannel = if ($IsArm64) { 18 } else { 17 }\r
+$VsEdition = 'community' # community | professional | enterprise\r
+\r
+# aka.ms path segment per generation. NOT the same word for both: VS 2022 is\r
+# published under /release/, VS 2026 under /stable/. This is not cosmetic -\r
+# https://aka.ms/vs/18/release/vs_community.exe is not a 404, it silently\r
+# redirects to Bing and returns 200 with an HTML body, so a wrong guess here\r
+# downloads a web page, names it vs_community.exe, and fails at Start-Process\r
+# with something that looks nothing like a bad URL.\r
+$VsChannelPath = if ($VsChannel -ge 18) { 'stable' } else { 'release' }\r
+\r
+# Detect an existing VS install via vswhere (ships with the VS Installer). Note\r
+# that vswhere itself lives under the 32-bit Program Files on every architecture,\r
+# ARM64 included - the VS Installer is x86-registered there by contract even\r
+# though the installer binaries themselves are native.\r
# These are referenced by Invoke-VsModify via $script: scope.\r
$VsWhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'\r
-$InstallPath = $null\r
+$InstallPath = Get-VsInstallPath\r
+\r
+# Report any OTHER Visual Studio generations on the box. They are left alone -\r
+# the passes below only ever touch $InstallPath - but when none of them matches\r
+# $VsChannel this script is about to download and install a second, largely\r
+# redundant toolchain, and that should be a visible decision rather than a\r
+# surprise 10GB. (On ARM64, where $VsChannel is 18, an existing VS 2026 IS the\r
+# match and gets modified in place rather than duplicated.)\r
if (Test-Path $VsWhere) {\r
- $InstallPath = & $VsWhere -products '*' -property installationPath -format value |\r
- Select-Object -First 1\r
+ $others = & $VsWhere -products '*' -format value -property installationPath |\r
+ Where-Object { $_ -and $_ -ne $InstallPath }\r
+ if ($others) {\r
+ Write-Step 'Other Visual Studio installations detected'\r
+ foreach ($o in $others) { Write-Host " $o" -ForegroundColor Yellow }\r
+ Write-Host " Not modified. This script drives VS generation $VsChannel only." -ForegroundColor Yellow\r
+ Write-Host " To use one of the above instead, set `$VsChannel at the top of this step." -ForegroundColor Yellow\r
+ }\r
}\r
\r
-Write-Step 'Downloading VS2022 Community bootstrapper'\r
-$VsInstallerUrl = 'https://aka.ms/vs/17/release/vs_community.exe'\r
-$VsBootstrapper = Join-Path $TempDir 'vs_community.exe'\r
+Write-Step "Downloading VS $VsChannel $VsEdition bootstrapper (host: $HostArch)"\r
+# aka.ms serves the bootstrapper for the requesting machine's architecture, so on\r
+# ARM64 this is the native ARM64 installer - no --arch flag needed or offered.\r
+$VsInstallerUrl = "https://aka.ms/vs/$VsChannel/$VsChannelPath/vs_$VsEdition.exe"\r
+$VsBootstrapper = Join-Path $TempDir "vs_$VsEdition.exe"\r
+Write-Host " $VsInstallerUrl" -ForegroundColor DarkGray\r
Invoke-WebRequest -Uri $VsInstallerUrl -OutFile $VsBootstrapper -UseBasicParsing\r
\r
+# Prove we got an installer and not a web page. The Bing redirect described above\r
+# returns 200 with HTML, and every other aka.ms typo behaves the same way, so a\r
+# bad channel/edition combination is otherwise only discovered when the "exe"\r
+# fails to start. 'MZ' is the DOS header every PE begins with.\r
+$vsHead = [IO.File]::ReadAllBytes($VsBootstrapper) | Select-Object -First 2\r
+if (-not ($vsHead.Count -eq 2 -and $vsHead[0] -eq 0x4D -and $vsHead[1] -eq 0x5A)) {\r
+ throw "Visual Studio: $VsInstallerUrl did not return an executable (no MZ header; $((Get-Item $VsBootstrapper).Length) bytes). Check `$VsChannel / `$VsChannelPath / `$VsEdition."\r
+}\r
+Write-Host " OK: bootstrapper is a PE ($([math]::Round((Get-Item $VsBootstrapper).Length / 1MB, 2)) MB)"\r
+\r
# Install in three sequential passes. The base set is installed first (this is\r
# the configuration that previously worked); Clang and the XP toolset are added\r
# afterwards. If one fails, its label pinpoints which group is responsible.\r
+#\r
+# The last two are -Optional: neither the Clang toolset nor XP targeting is\r
+# needed to build with MSVC, and on a host where one of them is simply not\r
+# offered a hard failure here would cost you the whole toolchain over a component\r
+# you can add later from the installer UI.\r
Invoke-VsModify -Label 'base toolset + workload' -Ids $BaseComponents\r
-Invoke-VsModify -Label 'Clang / LLVM' -Ids $ClangComponents\r
-Invoke-VsModify -Label 'Windows XP (v141 + WinXP)' -Ids $XpComponents\r
+Invoke-VsModify -Label 'Clang / LLVM' -Ids $ClangComponents -Optional\r
+if ($IsArm64) {\r
+ Write-Step 'Visual Studio: Windows XP (v141 + WinXP)'\r
+ Write-Host ' Skipped on ARM64: the v141 (14.16) toolset ships x86/x64-hosted compilers only,' -ForegroundColor Yellow\r
+ Write-Host ' and Windows XP targeting is not offered for Arm hosts. Build x86 with the' -ForegroundColor Yellow\r
+ Write-Host ' current toolset instead (cmake -A Win32), which is native here.' -ForegroundColor Yellow\r
+} else {\r
+ Invoke-VsModify -Label 'Windows XP (v141 + WinXP)' -Ids $XpComponents -Optional\r
+}\r
\r
# ---------------------------------------------------------------------------\r
-# Verify the v141 / XP toolset actually landed. Earlier runs silently skipped\r
-# it and the failure only surfaced at build time, so check on disk and fail\r
-# loudly here instead.\r
+# Verify what actually landed, on disk. Earlier runs silently skipped the v141\r
+# toolset and the failure only surfaced at build time, so check here and say so\r
+# loudly instead. Widened from that one check to every toolset worth naming,\r
+# because the same "installed something, but not the thing you needed" failure is\r
+# now possible per host architecture: this reports which MSVC host toolchains are\r
+# present (HostARM64 is what proves the compiler is native rather than emulated),\r
+# whether clang-cl is there, and - on x64 only - whether v141 is.\r
+#\r
+# Reporting, not throwing. A missing optional component is something to fix from\r
+# the installer UI, not a reason to fail a provisioning run that installed a\r
+# working compiler.\r
# ---------------------------------------------------------------------------\r
-Write-Step 'Verifying v141 / XP toolset'\r
-$InstallPath = & $VsWhere -products '*' -property installationPath -format value |\r
- Select-Object -First 1\r
-$V141 = if ($InstallPath) {\r
- Get-ChildItem (Join-Path $InstallPath 'VC\Tools\MSVC') -Directory -ErrorAction SilentlyContinue |\r
- Where-Object { $_.Name -like '14.16.*' } | Select-Object -First 1\r
-}\r
-if ($V141) {\r
- Write-Host " OK: v141 toolset present ($($V141.Name))" -ForegroundColor Green\r
+Write-Step 'Verifying the installed toolsets'\r
+$InstallPath = Get-VsInstallPath\r
+if (-not $InstallPath) {\r
+ Write-Warning "No Visual Studio $VsChannel installation found after the passes above; cannot verify toolsets."\r
} else {\r
- Write-Warning 'v141 (14.16.x) toolset NOT found - the XP build presets will fail.'\r
- Write-Warning 'Add it via Visual Studio Installer > Modify > Individual components:'\r
- Write-Warning ' - MSVC v141 - VS 2017 C++ x64/x86 build tools (v14.16)'\r
- Write-Warning ' - C++ Windows XP Support for VS 2017 (v141) tools'\r
+ Write-Host " Install path: $InstallPath"\r
+\r
+ # What MSVC versions landed, and which host toolchains each one carries.\r
+ # HostARM64 is the directory that proves the native ARM64 compiler is here\r
+ # rather than an x64 one that would run under emulation.\r
+ $msvcRoot = Join-Path $InstallPath 'VC\Tools\MSVC'\r
+ foreach ($v in (Get-ChildItem $msvcRoot -Directory -ErrorAction SilentlyContinue | Sort-Object Name)) {\r
+ $hosts = Get-ChildItem (Join-Path $v.FullName 'bin') -Directory -ErrorAction SilentlyContinue |\r
+ ForEach-Object { $_.Name }\r
+ Write-Host " MSVC $($v.Name): $(if ($hosts) { $hosts -join ', ' } else { '(no bin dir)' })"\r
+ }\r
+ if ($IsArm64) {\r
+ $armHost = Test-Path (Join-Path $msvcRoot '*\bin\HostARM64\ARM64\cl.exe')\r
+ if ($armHost) {\r
+ Write-Host ' OK: native ARM64-hosted cl.exe present.' -ForegroundColor Green\r
+ } else {\r
+ Write-Warning 'No HostARM64 cl.exe found - MSVC would run under x64 emulation. Add "MSVC v14x - VS 2022 C++ ARM64/ARM64EC build tools" in the installer.'\r
+ }\r
+ }\r
+\r
+ # clang-cl, for the ClangCL toolset in CMakePresets.json. Checked per host\r
+ # directory, because the Llvm tree is partitioned by HOST architecture and\r
+ # only the matching one is a native compiler.\r
+ #\r
+ # Look for clang-cl.exe specifically, NOT for the directory. VC\Tools\Llvm\*\bin\r
+ # holds clang-format.exe and clang-tidy.exe on every host whether or not the\r
+ # Clang component was ever installed - those ship with the NativeDesktop\r
+ # workload - so a present ARM64\bin proves nothing on its own. That is the\r
+ # false positive to avoid when checking this by hand.\r
+ $llvmRoot = Join-Path $InstallPath 'VC\Tools\Llvm'\r
+ $clangArm = Test-Path (Join-Path $llvmRoot 'ARM64\bin\clang-cl.exe')\r
+ $clangX64 = Test-Path (Join-Path $llvmRoot 'x64\bin\clang-cl.exe')\r
+ $clangX86 = Test-Path (Join-Path $llvmRoot 'bin\clang-cl.exe')\r
+ if ($clangArm -or $clangX64 -or $clangX86) {\r
+ Write-Host " clang-cl: $(@(if ($clangArm) {'ARM64'}; if ($clangX64) {'x64'}; if ($clangX86) {'x86'}) -join ', ')" -ForegroundColor Green\r
+ # On ARM64 the x64 build would still run, under emulation - so say plainly\r
+ # whether the NATIVE one is the one that landed.\r
+ if ($IsArm64 -and -not $clangArm) {\r
+ Write-Warning 'No ARM64-hosted clang-cl - the x64 one would run under emulation. Re-run the Clang/LLVM pass, or add "C++ Clang tools for Windows" in the installer.'\r
+ }\r
+ } else {\r
+ Write-Warning 'clang-cl not found - the ClangCL presets will fail. Add the "C++ Clang tools for Windows" component.'\r
+ }\r
+\r
+ # v141 / XP. Only meaningful where the toolset can exist at all; on ARM64 the\r
+ # group above was deliberately skipped, so warning here would be noise about\r
+ # a decision this script made on purpose two steps ago.\r
+ if ($IsArm64) {\r
+ Write-Host ' v141 / Windows XP toolset: n/a on ARM64 (not offered for Arm hosts).' -ForegroundColor DarkGray\r
+ } else {\r
+ $V141 = Get-ChildItem $msvcRoot -Directory -ErrorAction SilentlyContinue |\r
+ Where-Object { $_.Name -like '14.16.*' } | Select-Object -First 1\r
+ if ($V141) {\r
+ Write-Host " OK: v141 toolset present ($($V141.Name))" -ForegroundColor Green\r
+ } else {\r
+ Write-Warning 'v141 (14.16.x) toolset NOT found - the XP build presets will fail.'\r
+ Write-Warning 'Add it via Visual Studio Installer > Modify > Individual components:'\r
+ Write-Warning ' - MSVC v141 - VS 2017 C++ x64/x86 build tools (v14.16)'\r
+ Write-Warning ' - C++ Windows XP Support for VS 2017 (v141) tools'\r
+ }\r
+ }\r
}\r
\r
# ---------------------------------------------------------------------------\r
# linkid=2335869 -> WDK 26100.6584 (per Microsoft "Other WDK Downloads").\r
# ---------------------------------------------------------------------------\r
$WdkVersion = '10.0.26100'\r
-$WdkInstalledRoot = (Get-ItemProperty 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows Kits\Installed Roots' `\r
- -ErrorAction SilentlyContinue).WdkBinRootVersioned\r
+# Both hives. The WDK installer is a 32-bit program, so on x64 it writes under\r
+# WOW6432Node - but which hive a given kit lands in has varied across kit\r
+# versions and architectures, and reading only one of them makes an installed WDK\r
+# look absent, which costs a needless multi-GB reinstall on every run. Check the\r
+# native hive too and take whichever answers.\r
+$WdkInstalledRoot = @(\r
+ 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows Kits\Installed Roots'\r
+ 'HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots'\r
+) | ForEach-Object { (Get-ItemProperty $_ -ErrorAction SilentlyContinue).WdkBinRootVersioned } |\r
+ Where-Object { $_ } | Select-Object -First 1\r
\r
if ($WdkInstalledRoot -and $WdkInstalledRoot -match [regex]::Escape($WdkVersion)) {\r
# Re-running wdksetup.exe for an already-present version returns exit code\r
Invoke-WebRequest -Uri $WdkUrl -OutFile $WdkInstaller -UseBasicParsing\r
\r
Write-Step 'Installing WDK'\r
+ # wdksetup.exe is a 32-bit binary and runs under emulation on ARM64; the kit\r
+ # it lays down does include the ARM64 target headers, libs and tools (the\r
+ # signing/deployment tools under bin\arm64), so an ARM64 driver builds from\r
+ # an ARM64 host. Only the installer is emulated, not the toolchain.\r
$proc = Start-Process -FilePath $WdkInstaller -ArgumentList '/quiet /norestart' -Wait -PassThru -NoNewWindow\r
Write-Host " WDK installer exit code: $($proc.ExitCode)"\r
if ($proc.ExitCode -eq 2008) {\r
Write-Host " OK: WPT already present ($WptDir)" -ForegroundColor Green\r
} else {\r
try {\r
+ # The ADK manifest offers no ARM64 installer, so on ARM64 winget fetches\r
+ # the x64 one; it runs under emulation and lays down a toolkit that does\r
+ # include the ARM64 binaries. The SDK feature is the lighter route on any\r
+ # architecture and is worth preferring if this fallback ever gives\r
+ # trouble - see the winsdksetup.exe line in the comment above.\r
+ if ($IsArm64) {\r
+ Write-Host ' ARM64: the ADK installer is x64 (emulated); the toolkit it installs is ARM64.' -ForegroundColor Yellow\r
+ }\r
winget install --id Microsoft.WindowsADK --exact --silent --disable-interactivity `\r
--accept-source-agreements --accept-package-agreements\r
Write-Host ' Windows ADK (includes Windows Performance Toolkit) installed.'\r
# intel-vtune-<version>_offline.exe -a --silent --cli --eula accept\r
# ---------------------------------------------------------------------------\r
Write-Step 'Intel VTune Profiler (status only)'\r
-$UninstallKeys = @(\r
- 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'\r
- 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'\r
-)\r
-$vtune = Get-ItemProperty $UninstallKeys -ErrorAction SilentlyContinue |\r
- Where-Object { $_.DisplayName -match 'VTune' } |\r
- Select-Object -First 1\r
-if ($vtune) {\r
- Write-Host " Installed: $($vtune.DisplayName.Trim()) $($vtune.DisplayVersion)" -ForegroundColor Green\r
- # The oneAPI layout keeps a `latest` junction beside the versioned directory,\r
- # so this path stays right across upgrades.\r
- $VTuneCli = Join-Path $vtune.InstallLocation 'vtune\latest\bin64\vtune.exe'\r
- if (Test-Path $VTuneCli) { Write-Host " CLI: $VTuneCli" }\r
+if ($IsArm64) {\r
+ # Not a "not installed yet" case - there is no Windows-on-Arm build of VTune,\r
+ # and there is nothing for it to sample: its whole value is reading Intel PMU\r
+ # counters. Say so plainly and point at what does work here, rather than\r
+ # printing a download link for a product this box cannot run.\r
+ Write-Host ' n/a on ARM64: Intel ships no Windows-on-Arm build, and hardware event-based' -ForegroundColor DarkGray\r
+ Write-Host ' sampling reads Intel PMU counters. Use the Windows Performance Toolkit above' -ForegroundColor DarkGray\r
+ Write-Host ' (wpr / xperf to collect, wpa to analyse) for profiling on this box.' -ForegroundColor DarkGray\r
+ Write-Host ' Arm also publishes Arm Performance Studio / Streamline for Arm PMU sampling.' -ForegroundColor DarkGray\r
} else {\r
- Write-Host ' Not installed.' -ForegroundColor Yellow\r
- Write-Host ' https://www.intel.com/content/www/us/en/developer/tools/oneapi/vtune-profiler-download.html' -ForegroundColor Yellow\r
- $cpu = (Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1).Manufacturer\r
- if ($cpu -and $cpu -notmatch 'Intel') {\r
- Write-Host " (This CPU reports itself as '$cpu' - VTune's hardware event-based sampling wants Intel silicon.)" -ForegroundColor Yellow\r
+ $UninstallKeys = @(\r
+ 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'\r
+ 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'\r
+ )\r
+ $vtune = Get-ItemProperty $UninstallKeys -ErrorAction SilentlyContinue |\r
+ Where-Object { $_.DisplayName -match 'VTune' } |\r
+ Select-Object -First 1\r
+ if ($vtune) {\r
+ Write-Host " Installed: $($vtune.DisplayName.Trim()) $($vtune.DisplayVersion)" -ForegroundColor Green\r
+ # The oneAPI layout keeps a `latest` junction beside the versioned directory,\r
+ # so this path stays right across upgrades.\r
+ $VTuneCli = Join-Path $vtune.InstallLocation 'vtune\latest\bin64\vtune.exe'\r
+ if (Test-Path $VTuneCli) { Write-Host " CLI: $VTuneCli" }\r
+ } else {\r
+ Write-Host ' Not installed.' -ForegroundColor Yellow\r
+ Write-Host ' https://www.intel.com/content/www/us/en/developer/tools/oneapi/vtune-profiler-download.html' -ForegroundColor Yellow\r
+ $cpu = (Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1).Manufacturer\r
+ if ($cpu -and $cpu -notmatch 'Intel') {\r
+ Write-Host " (This CPU reports itself as '$cpu' - VTune's hardware event-based sampling wants Intel silicon.)" -ForegroundColor Yellow\r
+ }\r
}\r
}\r
\r
# ---------------------------------------------------------------------------\r
Write-Host "`nAll done." -ForegroundColor Green\r
+Write-Host "Host architecture was $HostArch."\r
Write-Host 'If a reboot was flagged above, restart before opening VS or building drivers.'\r
\r
}\r
# Only fold in the VS Installer logs when a VS step actually failed; for other\r
# steps (e.g. WDK) those logs are stale and misleading, so the message above\r
# is what matters.\r
- if ($_.Exception.Message -match 'VS2022') {\r
+ if ($_.Exception.Message -match 'Visual Studio') {\r
try { Show-VsSetupLogs } catch {}\r
}\r
}\r
\r
@rem ---------------------------------------------------------------------------\r
@rem setup-windows.bat - provision a fresh Windows box for native development\r
+@rem\r
+@rem Runs on x64 and on ARM64 (Windows 11 on Arm). See the ARM64 block below for\r
+@rem what changes on Arm silicon.\r
+@rem ---------------------------------------------------------------------------\r
+\r
+@rem ---------------------------------------------------------------------------\r
+@rem Host architecture\r
+@rem\r
+@rem PROCESSOR_ARCHITECTURE alone is not enough: a 32-bit cmd.exe under emulation\r
+@rem reports x86 and stashes the real one in PROCESSOR_ARCHITEW6432. Check both,\r
+@rem so this is right however the script was launched.\r
+@rem\r
+@rem The value drives two things below, and nothing else - winget resolves the\r
+@rem installer architecture on its own (native first, then whatever the box can\r
+@rem emulate), so the ordinary installs need no help:\r
+@rem - VirtualBox is skipped on ARM64 (see below)\r
+@rem - the summary tells you which tools you got as emulated x64\r
@rem ---------------------------------------------------------------------------\r
+set "HOST_ARCH=%PROCESSOR_ARCHITECTURE%"\r
+if defined PROCESSOR_ARCHITEW6432 set "HOST_ARCH=%PROCESSOR_ARCHITEW6432%"\r
+set "IS_ARM64=0"\r
+if /i "%HOST_ARCH%"=="ARM64" set "IS_ARM64=1"\r
+echo [setup-windows] Host architecture: %HOST_ARCH%\r
\r
@rem --- Non-admin (per-user) winget installs ---\r
+@rem\r
+@rem No --architecture anywhere on purpose. winget already picks the best\r
+@rem installer the manifest offers for this machine - arm64, else x64, else x86 -\r
+@rem and pinning it would turn "no native build, use the emulated one" into a\r
+@rem hard failure.\r
+@rem\r
+@rem These all publish real ARM64 installers, so on an ARM64 box every one of\r
+@rem them lands as a native ARM64 binary with no emulation involved.\r
+@rem\r
+@rem The Sysinternals packages are a multi-architecture ZIP rather than an\r
+@rem installer, so the ARM64 build is IN there but is not the default-named exe:\r
+@rem the zip holds procexp.exe (x86), procexp64.exe (x64) and procexp64a.exe\r
+@rem (ARM64). On ARM64, run the *64a.exe variants - plain procexp.exe is the x86\r
+@rem one and will run emulated.\r
winget install Anthropic.ClaudeCode\r
-winget install ar51an.iPerf3\r
winget install Brave.Brave\r
winget install Git.Git\r
-winget install Google.AndroidGPUInspector\r
winget install Microsoft.DotNet.SDK.10\r
winget install Microsoft.PowerShell Microsoft.Sysinternals.ProcessExplorer Microsoft.Sysinternals.ProcessMonitor Microsoft.Sysinternals.SDelete Microsoft.VisualStudioCode Microsoft.WindowsTerminal\r
-winget install NASM.NASM\r
-winget install Oracle.VirtualBox\r
winget install Python.Python.3.13\r
winget install WinMerge.WinMerge\r
\r
+@rem --- Build drivers, standalone and native ---\r
+@rem\r
+@rem CMake and Ninja BOTH ship with Visual Studio, so this is not about making\r
+@rem them exist - it is about which binary you get and where it is on the PATH:\r
+@rem\r
+@rem - VS's cmake.exe is already ARM64, but it only lands on the PATH inside a\r
+@rem Developer Command Prompt. The build line further down assumes a plain\r
+@rem `cmake`, so install the standalone one to make that true in any shell.\r
+@rem - VS's bundled ninja.exe is x64 EVEN ON ARM64 (verified on this box), so it\r
+@rem runs under emulation. Ninja is re-invoked for every edge in the build\r
+@rem graph, which makes it the one tool here where emulation is actually paid\r
+@rem over and over, and ninja-winarm64.zip is a native build. Worth having.\r
+@rem\r
+@rem Both are on the machine/user PATH after install, so a bare `cmake -G Ninja`\r
+@rem picks these up. Inside a VS Developer prompt the VS copies may come first;\r
+@rem that is fine for cmake (same architecture) and is the case to watch for ninja.\r
+winget install Kitware.CMake\r
+winget install Ninja-build.Ninja\r
+\r
+@rem --- Second, independent Clang: compiler diversity ---\r
+@rem\r
+@rem The elevated half installs Visual Studio's Clang component (clang-cl, native\r
+@rem ARM64). This is a DIFFERENT LLVM: the upstream release, on its own schedule\r
+@rem and usually several major versions ahead of the one VS bundles, installed to\r
+@rem C:\Program Files\LLVM rather than inside the VS tree. Two Clang majors plus\r
+@rem MSVC over the same sources is the point - it is what catches the bugs a\r
+@rem single toolchain agrees with itself about.\r
+@rem\r
+@rem Native on ARM64: the manifest resolves to LLVM-<ver>-woa64.exe ("Windows on\r
+@rem Arm 64"), not the x64 build.\r
+@rem\r
+@rem PATH ORDER MATTERS if you use it. Should this installer put its bin on the\r
+@rem PATH, a bare `clang-cl` resolves to the upstream one while CMake's\r
+@rem `-T ClangCL` toolset keeps using the VS copy - so the two can be selected\r
+@rem independently, but only if you know which one you are asking for. Prefer\r
+@rem -DCMAKE_CXX_COMPILER with a full path when you care.\r
+winget install LLVM.LLVM\r
+\r
+@rem x64-only manifests: winget falls back to the x64 installer on ARM64 and these\r
+@rem run under Prism emulation. Harmless for what they do here - iperf3 is a\r
+@rem network benchmark bounded by the link, and AGI is an Android-side GPU\r
+@rem profiler whose work happens on the phone. Neither is CPU-bound on this box.\r
+winget install ar51an.iPerf3\r
+winget install Google.AndroidGPUInspector\r
+\r
+@rem NASM assembles x86/x86-64 only - there is no ARM64 target in it and no ARM64\r
+@rem build of it. Installed on ARM64 anyway (as emulated x64) because this box\r
+@rem still cross-compiles x86/x64 binaries, where NASM is the assembler that\r
+@rem builds them. If you only ever target ARM64, nothing here uses it: the ARM64\r
+@rem assembler is armasm64.exe, which ships with MSVC.\r
+winget install NASM.NASM\r
+\r
+@rem VirtualBox has no ARM64 Windows host build, and the x64 one cannot be made to\r
+@rem work by emulation: a hypervisor is kernel-mode, and its x64 driver will not\r
+@rem load on an ARM64 kernel. Installing it on ARM64 gets you a broken product and\r
+@rem a failed driver install, so skip it and say so.\r
+@rem\r
+@rem This is also what makes setup-windows-7-test-env.bat unreachable from an\r
+@rem ARM64 host: it prepares a Windows 7 *x86* guest, and neither VirtualBox nor\r
+@rem Hyper-V on ARM64 can run one.\r
+if "%IS_ARM64%"=="1" (\r
+ echo [setup-windows] Skipping Oracle.VirtualBox: no ARM64 Windows host build ^(x64 hypervisor drivers cannot load on an ARM64 kernel^).\r
+ echo [setup-windows] The Windows 7 x86 test-VM workflow ^(setup-windows-7-test-env.bat^) is therefore unavailable on this box.\r
+) else (\r
+ winget install Oracle.VirtualBox\r
+)\r
+\r
@rem OpenCppCoverage: native (PE) line coverage for the C++ binaries. run-coverage-occ.py drives the\r
@rem pytest suite under it to produce an HTML report (the binaries under test build with PDBs,\r
@rem which it reads). The installer elevates via UAC.\r
+@rem\r
+@rem x86/x64 only, and unlike the other emulated tools that is a real limit rather\r
+@rem than a slowdown: it collects coverage by debugging the process under test and\r
+@rem stepping x86/x64 instructions, so it can cover the x86/x64 binaries this box\r
+@rem cross-compiles but NOT an ARM64 one. For ARM64 coverage, build the ARM64\r
+@rem binaries with /fsanitize-coverage or use the x64 build for the coverage run.\r
winget install OpenCppCoverage.OpenCppCoverage\r
+if "%IS_ARM64%"=="1" echo [setup-windows] NOTE: OpenCppCoverage is x86/x64-only - it cannot instrument ARM64 binaries. Run coverage against the x64 build.\r
\r
@rem --- WiX 5.0.2, pinned on purpose ---\r
@rem WiX packages our proprietary software into MSIs, and the version is pinned to keep that\r
@rem ago, but this cmd session inherited its environment before that and cannot see it.\r
@rem install-then-update is for re-runs - install fails once the tool is there, and update\r
@rem then holds it at exactly 5.0.2 - which keeps this script idempotent like the rest.\r
+@rem\r
+@rem %ProgramFiles%\dotnet is the NATIVE SDK's root on every architecture, ARM64\r
+@rem included - what winget just installed. An x64 SDK installed alongside it on\r
+@rem an ARM64 box goes to %ProgramFiles%\dotnet\x64 instead, so fall back there\r
+@rem for a machine that only ever got the x64 one. Prefer the native root: the\r
+@rem ARM64 SDK runs the wix tool natively, and building an MSI is not something\r
+@rem worth doing through emulation.\r
set "DOTNET_EXE=%ProgramFiles%\dotnet\dotnet.exe"\r
+if not exist "%DOTNET_EXE%" set "DOTNET_EXE=%ProgramFiles%\dotnet\x64\dotnet.exe"\r
+if not exist "%DOTNET_EXE%" echo [setup-windows] WARNING: dotnet.exe not found; skipping the WiX 5.0.2 pin.\r
+if not exist "%DOTNET_EXE%" goto :after_wix\r
+\r
"%DOTNET_EXE%" tool install --global wix --version 5.0.2 || "%DOTNET_EXE%" tool update --global wix --version 5.0.2\r
\r
@rem Report what the pin actually produced. By full path again, and because the shim lands in\r
@rem a directory this session's PATH predates: expect "5.0.2+<commit>", not 7.x.\r
"%USERPROFILE%\.dotnet\tools\wix.exe" --version\r
\r
+:after_wix\r
+\r
@rem ---------------------------------------------------------------------------\r
@rem No package manager needed for the Windows build\r
@rem\r
@rem Just:\r
@rem cd windows && cmake -B build -G "Visual Studio 17 2022" -A x64 && cmake --build build --config Release\r
+@rem\r
+@rem -A selects the TARGET, independently of the host. On an ARM64 box the same\r
+@rem generator cross-compiles all three, and MSVC's ARM64-hosted toolchain is used\r
+@rem for each - nothing is emulated:\r
+@rem -A ARM64 native ARM64 binaries\r
+@rem -A x64 x64 binaries (run here under emulation)\r
+@rem -A Win32 x86 binaries\r
+@rem Use the generator matching the Visual Studio the elevated half installed -\r
+@rem "Visual Studio 17 2022" on x64, "Visual Studio 18 2026" on ARM64 - or just\r
+@rem let CMake pick the newest it finds.\r
@rem ---------------------------------------------------------------------------\r
\r
-@rem --- Elevated installs (VS2022, WDK, system tools) ---\r
+@rem --- Elevated installs (Visual Studio, WDK, system tools) ---\r
@rem The elevated script runs in its own window and logs to setup-windows-uac.log.\r
@rem -PassThru + $p.ExitCode propagates its real exit code back through to ERRORLEVEL.\r
set "UAC_LOG=%~dp0setup-windows-uac.log"\r
@rem py -m pip install "headroom-ai[all]"\r
@rem npm install headroom-ai\r
\r
-py -m pip install Pillow
\ No newline at end of file
+@rem Pillow publishes win_arm64 wheels alongside win_amd64, so `py` - the ARM64\r
+@rem Python winget just installed - gets a native wheel and never falls back to\r
+@rem building from source. Any package WITHOUT an ARM64 wheel does fall back, and\r
+@rem that source build needs the MSVC toolchain the elevated half installs.\r
+py -m pip install Pillow\r
+\r
+if "%IS_ARM64%"=="1" (\r
+ echo.\r
+ echo [setup-windows] ARM64 notes:\r
+ echo [setup-windows] native ARM64: Git, Python, .NET SDK, PowerShell, VS Code, Windows Terminal,\r
+ echo [setup-windows] WinMerge, Brave, Sysinternals ^(the *64a.exe variants^),\r
+ echo [setup-windows] Claude Code, CMake, Ninja, and the SDK/WPT tools.\r
+ echo [setup-windows] compilers : MSVC and clang-cl from Visual Studio 2026, plus upstream\r
+ echo [setup-windows] LLVM - all three native ARM64, two different Clang majors.\r
+ echo [setup-windows] emulated x64: iperf3, NASM, OpenCppCoverage, BinSkim, rsync.exe, AGI.\r
+ echo [setup-windows] unavailable : VirtualBox, the Windows 7 x86 test VM, Intel VTune,\r
+ echo [setup-windows] and the v141 / Windows XP targeting toolset.\r
+)\r