]> vilimpoc.org git repositories - dotfiles/blob - setup-windows-with-uac.ps1
dotfiles: detect that elevation is impossible instead of misreporting it
[dotfiles] / setup-windows-with-uac.ps1
1 #Requires -RunAsAdministrator\r
2 <#\r
3   setup-windows-with-uac.ps1\r
4   Elevated portion of the Windows provisioning. Invoked by setup-windows.bat\r
5   via Start-Process -Verb RunAs, or run manually from an Administrator prompt.\r
6 \r
7   What this installs / configures:\r
8     - OpenSSH Client capability (the ssh.exe rsync shells out to, and the\r
9       System32 libcrypto.dll the release's own ssh.exe links against)\r
10     - ssh-agent set to automatic + started\r
11     - OpenSSH Server (sshd) capability: automatic + started + inbound TCP 22\r
12     - rsync for Windows (nuket/rsync-windows) in C:\Tools\rsync, on the machine\r
13       PATH: rsync.exe plus the ssh.exe it runs, out of the release zip for this\r
14       architecture (there is no ARM64 zip; ARM64 gets the x64 one, and the\r
15       ssh.exe is kept only if it actually starts)\r
16     - Visual Studio Community (C++ desktop workload, x86/x64 AND ARM64 build\r
17       tools, Spectre libs, WDK VSIX, Win11 SDK 26100, Clang/LLVM, and - on x64\r
18       only - the v141 + Windows XP targeting toolset). VS 2022 on x64, VS 2026\r
19       on ARM64; see $VsChannel.\r
20     - Windows Driver Kit 10.0.26100\r
21     - Windows Performance Toolkit - xperf, wpr and Windows Performance Analyzer\r
22       (wpa.exe) - on the machine PATH\r
23 \r
24   Change $VsEdition below to Professional or Enterprise if needed. $VsChannel\r
25   picks the Visual Studio generation and is architecture-split by default:\r
26   17 (VS 2022) on x64, where the v141 / Windows XP toolset is wanted, and\r
27   18 (VS 2026) on ARM64, where that toolset cannot exist anyway.\r
28 \r
29   ARCHITECTURE. Runs on x64 and on ARM64 (Windows 11 on Arm). On ARM64 the\r
30   Visual Studio installer, MSVC and clang-cl are all native ARM64 and\r
31   cross-compile ARM64/x64/x86 targets; what changes is called out at each step,\r
32   and $HostArch below is what drives it.\r
33 #>\r
34 \r
35 $ErrorActionPreference = 'Stop'\r
36 \r
37 # ---------------------------------------------------------------------------\r
38 # Host architecture\r
39 #\r
40 # RuntimeInformation.OSArchitecture rather than PROCESSOR_ARCHITECTURE: this\r
41 # script can be launched by a 32-bit or an emulated x64 PowerShell, either of\r
42 # which reports the emulated architecture in the environment variable while this\r
43 # API still reports the real one. Values seen here: X64, Arm64, X86.\r
44 #\r
45 # $IsArm64 gates:\r
46 #   - which rsync-windows release zip is fetched (there is no ARM64 one)\r
47 #   - which Visual Studio generation is driven ($VsChannel: 18 on Arm, 17 on x64)\r
48 #   - the Visual Studio component groups (ARM64 build tools in; the v141/XP\r
49 #     toolset out, because it has no ARM64-hosted compiler and Microsoft does not\r
50 #     ship Windows XP targeting for Arm hosts. Clang/LLVM is installed on every\r
51 #     architecture and is native on Arm, so it is NOT gated here)\r
52 #   - what the VTune and WPT steps report\r
53 # ---------------------------------------------------------------------------\r
54 $HostArch = [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()\r
55 $IsArm64  = ($HostArch -eq 'Arm64')\r
56 \r
57 function Write-Step([string]$Msg) {\r
58     Write-Host "`n==> $Msg" -ForegroundColor Cyan\r
59 }\r
60 \r
61 function Assert-ExitCode([int]$Code, [string]$Step) {\r
62     # 0 = success, 3010 = success + reboot required\r
63     if ($Code -notin @(0, 3010)) {\r
64         throw "$Step failed with exit code $Code"\r
65     }\r
66     if ($Code -eq 3010) {\r
67         Write-Host "    [reboot required after $Step]" -ForegroundColor Yellow\r
68     }\r
69 }\r
70 \r
71 function Show-VsSetupLogs {\r
72     # The VS Installer writes dd_*.log to the invoking user's %TEMP%. Because\r
73     # this script runs elevated, that %TEMP% belongs to the elevated user and is\r
74     # readable here even when it is NOT readable by the non-elevated caller. Fold\r
75     # only the NEWEST installer + bootstrapper log into the transcript (the setup\r
76     # engine log is where per-component / product errors actually appear) and\r
77     # keep it short so the transcript stays readable.\r
78     Write-Host "`n==> Collecting VS Installer logs from $env:TEMP" -ForegroundColor Cyan\r
79     $recent = Get-ChildItem $env:TEMP -Filter 'dd_*.log' -ErrorAction SilentlyContinue |\r
80               Where-Object { $_.LastWriteTime -gt (Get-Date).AddMinutes(-15) }\r
81     $picks = @()\r
82     $picks += $recent | Where-Object { $_.Name -like 'dd_installer_*'    } | Sort-Object LastWriteTime | Select-Object -Last 1\r
83     $picks += $recent | Where-Object { $_.Name -like 'dd_bootstrapper_*' } | Sort-Object LastWriteTime | Select-Object -Last 1\r
84     $picks = $picks | Where-Object { $_ }\r
85     if (-not $picks) {\r
86         Write-Host '    (no VS Installer logs modified in the last 15 minutes)' -ForegroundColor Yellow\r
87         return\r
88     }\r
89     foreach ($l in $picks) {\r
90         Write-Host "`n----- $($l.Name) (tail) -----" -ForegroundColor Yellow\r
91         Get-Content $l.FullName -Tail 40\r
92     }\r
93 }\r
94 \r
95 function Get-VsInstallPath {\r
96     # The install path of a Visual Studio matching $script:VsChannel, or $null.\r
97     #\r
98     # -version is the point. A bare `vswhere -products *` returns EVERY Visual\r
99     # Studio on the box, newest first, and handing that path to a bootstrapper of\r
100     # a different generation is not a no-op: `vs_community.exe` (17.x) told to\r
101     # `modify --installPath <a VS 2026 install>` is a 17.x engine pointed at an\r
102     # 18.x product. On a box that already has VS 2026 - increasingly the default -\r
103     # every pass below would target the wrong install. Scope the query to the\r
104     # generation this script is actually driving.\r
105     if (-not (Test-Path $script:VsWhere)) { return $null }\r
106     $range = "[$script:VsChannel.0,$($script:VsChannel + 1).0)"\r
107     & $script:VsWhere -products '*' -version $range -property installationPath -format value |\r
108         Select-Object -First 1\r
109 }\r
110 \r
111 function Invoke-VsModify {\r
112     # Run one VS install/modify pass for a named group of components. Splitting\r
113     # the install into separate passes makes it obvious WHICH group fails: each\r
114     # call prints its label and exit code before Assert-ExitCode throws.\r
115     #\r
116     # -Optional downgrades a failure to a warning. Used for groups that are not\r
117     # available on every host architecture (the v141/XP toolset on ARM64) or that\r
118     # the rest of the box does not depend on, so one unavailable component cannot\r
119     # cost you the toolchain.\r
120     param(\r
121         [string]   $Label,\r
122         [string[]] $Ids,\r
123         [switch]   $Optional\r
124     )\r
125     Write-Step "Visual Studio: $Label"\r
126     if (-not $Ids) {\r
127         Write-Host '    (no components in this group for this architecture - skipping)' -ForegroundColor DarkGray\r
128         return\r
129     }\r
130     $addStr = ($Ids | ForEach-Object { "--add $_" }) -join ' '\r
131     # --installPath must be quoted: it contains spaces ("C:\Program Files\...").\r
132     # Windows PowerShell 5.1's Start-Process does not quote array elements, so we\r
133     # hand-build a single string. Component IDs / flags have no spaces.\r
134     $common = '--includeRecommended --quiet --norestart --wait'\r
135     if ($script:InstallPath) {\r
136         $argString = "modify --installPath `"$script:InstallPath`" $addStr $common --force"\r
137     } else {\r
138         # No existing install yet -> this first pass performs the base install.\r
139         $argString = "$addStr $common"\r
140     }\r
141     Write-Host "    > $script:VsBootstrapper $argString" -ForegroundColor DarkGray\r
142     $p = Start-Process -FilePath $script:VsBootstrapper -ArgumentList $argString -Wait -PassThru -NoNewWindow\r
143     Write-Host "    exit code: $($p.ExitCode)"\r
144     if ($Optional -and $p.ExitCode -notin @(0, 3010)) {\r
145         Write-Warning "Visual Studio ($Label) failed with exit code $($p.ExitCode); continuing (this group is optional)."\r
146     } else {\r
147         Assert-ExitCode $p.ExitCode "Visual Studio ($Label)"\r
148     }\r
149 \r
150     # After the first (fresh) install, re-detect the install path so subsequent\r
151     # passes use `modify`.\r
152     if (-not $script:InstallPath) { $script:InstallPath = Get-VsInstallPath }\r
153 }\r
154 \r
155 # ---------------------------------------------------------------------------\r
156 # This runs in a separate elevated window that closes the moment it exits, so\r
157 # the non-elevated caller (setup-windows.bat) can't see what happened. Mirror\r
158 # all output to a log next to the script and exit with a real code so the\r
159 # caller can detect success/failure and show the log.\r
160 # ---------------------------------------------------------------------------\r
161 $LogFile  = Join-Path $PSScriptRoot 'setup-windows-uac.log'\r
162 $ExitCode = 0\r
163 try { Start-Transcript -Path $LogFile -Force | Out-Null } catch {}\r
164 \r
165 try {\r
166 \r
167 Write-Step "Host architecture: $HostArch"\r
168 if ($IsArm64) {\r
169     # x64 emulation ("Prism") is what carries every x64-only tool this script\r
170     # installs - rsync.exe, the WDK and ADK installers, BinSkim in the\r
171     # non-elevated half. It is present on Windows 11 on Arm and absent on\r
172     # Windows 10 on Arm (x86-only there) and on some Server images, so check\r
173     # rather than assume: without it those steps install binaries that cannot\r
174     # start, and the failure would otherwise surface much later.\r
175     $Prism = Join-Path $env:WINDIR 'System32\xtajit64.dll'\r
176     if (Test-Path $Prism) {\r
177         Write-Host '    x64 emulation (Prism) present - x64-only tools will run.' -ForegroundColor Green\r
178     } else {\r
179         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
180     }\r
181 }\r
182 \r
183 # ---------------------------------------------------------------------------\r
184 # Base tools via winget\r
185 # ---------------------------------------------------------------------------\r
186 \r
187 # ---------------------------------------------------------------------------\r
188 # OpenSSH Client\r
189 #\r
190 # Present by default on Windows 10 1809+ / Windows 11, but removable, and absent\r
191 # from some Server images. Two things below want it: rsync does not speak ssh\r
192 # itself, it execs an ssh binary, and the release's own ssh.exe links against the\r
193 # libcrypto.dll this capability puts in System32. It also owns the ssh-agent\r
194 # service configured next, so a missing client is why that step would fail.\r
195 #\r
196 # Non-fatal, like the server half below: a box that cannot have it should still\r
197 # finish provisioning.\r
198 # ---------------------------------------------------------------------------\r
199 Write-Step 'OpenSSH Client'\r
200 try {\r
201     $sshc = Get-WindowsCapability -Online -Name 'OpenSSH.Client*' |\r
202             Select-Object -First 1\r
203     if (-not $sshc) {\r
204         Write-Warning 'OpenSSH.Client capability not offered by this Windows image - skipping.'\r
205     } elseif ($sshc.State -eq 'Installed') {\r
206         Write-Host "    OK: $($sshc.Name) already installed"\r
207     } else {\r
208         Write-Host "    Installing $($sshc.Name) ..."\r
209         $r = Add-WindowsCapability -Online -Name $sshc.Name\r
210         if ($r.RestartNeeded) { Write-Host '    [reboot required after OpenSSH Client]' -ForegroundColor Yellow }\r
211     }\r
212 } catch {\r
213     Write-Warning "OpenSSH Client setup failed: $($_.Exception.Message)"\r
214 }\r
215 \r
216 # ---------------------------------------------------------------------------\r
217 # SSH agent\r
218 # ---------------------------------------------------------------------------\r
219 Write-Step 'Enabling ssh-agent'\r
220 Set-Service -Name ssh-agent -StartupType Automatic\r
221 if ((Get-Service ssh-agent).Status -ne 'Running') { Start-Service ssh-agent }\r
222 \r
223 # ---------------------------------------------------------------------------\r
224 # OpenSSH Server (sshd)\r
225 #\r
226 # Used to reach the test VMs (VirtualBox) from the host: remote shell plus the\r
227 # transport rsync rides on when seeding test data in. Ships with Windows 10\r
228 # 1809+ / Windows 11 as an on-demand capability, so no third-party install.\r
229 #\r
230 # The capability normally adds the "OpenSSH Server (sshd)" inbound firewall\r
231 # rule; we verify and create it if missing (it is absent on some images).\r
232 #\r
233 # Non-fatal: a box that can't run sshd should still finish provisioning.\r
234 # ---------------------------------------------------------------------------\r
235 Write-Step 'OpenSSH Server (sshd)'\r
236 try {\r
237     $sshd = Get-WindowsCapability -Online -Name 'OpenSSH.Server*' |\r
238             Select-Object -First 1\r
239     if (-not $sshd) {\r
240         Write-Warning 'OpenSSH.Server capability not offered by this Windows image - skipping.'\r
241     } else {\r
242         if ($sshd.State -ne 'Installed') {\r
243             Write-Host "    Installing $($sshd.Name) ..."\r
244             $r = Add-WindowsCapability -Online -Name $sshd.Name\r
245             if ($r.RestartNeeded) { Write-Host '    [reboot required after OpenSSH Server]' -ForegroundColor Yellow }\r
246         } else {\r
247             Write-Host "    OK: $($sshd.Name) already installed"\r
248         }\r
249 \r
250         Set-Service -Name sshd -StartupType Automatic\r
251         if ((Get-Service sshd).Status -ne 'Running') { Start-Service sshd }\r
252         Write-Host '    sshd: Automatic + running'\r
253 \r
254         # Firewall: allow inbound 22 on all profiles. VirtualBox host-only and\r
255         # bridged adapters are frequently classified Public, and the capability's\r
256         # own rule is Private-only on some images, which is what leaves a plainly\r
257         # running sshd plainly unreachable.\r
258         #\r
259         # OpenSSH-Server-In-TCP is the name the capability itself uses, so this\r
260         # WIDENS that rule rather than adding a second one next to it. Creating\r
261         # our own under a different name would leave the narrow rule in place and\r
262         # the box still unreachable on a Public-classified adapter; creating one\r
263         # under the same name would collide. Adopt it if present, create it if not.\r
264         $ruleName = 'OpenSSH-Server-In-TCP'\r
265         if (Get-NetFirewallRule -Name $ruleName -ErrorAction SilentlyContinue) {\r
266             Set-NetFirewallRule -Name $ruleName -Enabled True -Profile Any\r
267             Write-Host "    Widened firewall rule $ruleName to all profiles"\r
268         } else {\r
269             New-NetFirewallRule -Name $ruleName -DisplayName 'OpenSSH SSH Server (sshd)' `\r
270                 -Enabled True -Direction Inbound -Protocol TCP -Action Allow `\r
271                 -LocalPort 22 -Profile Any | Out-Null\r
272             Write-Host "    Added firewall rule $ruleName (TCP 22, all profiles)"\r
273         }\r
274     }\r
275 } catch {\r
276     Write-Warning "OpenSSH Server setup failed: $($_.Exception.Message)"\r
277 }\r
278 \r
279 # ---------------------------------------------------------------------------\r
280 # rsync for Windows (github.com/nuket/rsync-windows)\r
281 #\r
282 # Windows' OpenSSH ships the transport only - no rsync - so pushing test data\r
283 # from a Linux box needs an rsync.exe on the Windows side.\r
284 #\r
285 # The release is one zip per architecture - rsync-windows-x64.zip and\r
286 # rsync-windows-x86.zip - each holding rsync.exe, the ssh.exe it runs, and the\r
287 # licence texts under exactly those names. Both exes are installed, together:\r
288 # rsync.exe prefers an ssh.exe in its own directory, and the release's build is\r
289 # what makes a push FROM this box run at line rate. The ssh.exe Windows ships\r
290 # reads its stdin 3KB at a time, which holds a send at ~17MB/s however fast the\r
291 # link is. Nothing else about it differs - same ~/.ssh, same ssh-agent, same\r
292 # known_hosts - and a bare `ssh` still resolves to the in-box client, which sits\r
293 # ahead of C:\Tools\rsync on the machine PATH.\r
294 #\r
295 # That ssh.exe links against the libcrypto.dll the OpenSSH Client capability\r
296 # above puts in System32: Windows' own LibreSSL, and the fast one, since it uses\r
297 # AES-NI. No copy of it ships in the zip, so where the capability is missing we\r
298 # unpack rsync alone rather than an ssh.exe that will not start.\r
299 #\r
300 # Installed to C:\Tools\rsync (NOT under "Program Files"): the remote end is\r
301 # invoked as `rsync --server ...` through cmd.exe, and a path with spaces makes\r
302 # the client-side --rsync-path escape hatch painful to quote. Added to the\r
303 # MACHINE PATH so it resolves for every account, including the non-interactive\r
304 # sshd session, which builds its environment from the machine + user registry\r
305 # PATH rather than from a login shell.\r
306 #\r
307 # Non-fatal: a download failure only warns.\r
308 # ---------------------------------------------------------------------------\r
309 Write-Step 'rsync for Windows'\r
310 $RsyncRepo  = 'nuket/rsync-windows'\r
311 # The release publishes exactly two assets - x64 and x86 - and no ARM64 one, so\r
312 # on ARM64 the x64 build is the right pick: it runs under Prism, and an emulated\r
313 # x64 rsync still moves data far faster than the ~17MB/s stdin cap that the whole\r
314 # reason for using this build is to avoid. (The transfer is I/O-bound on the\r
315 # socket, not on emulated CPU.) Selected off $HostArch rather than\r
316 # Is64BitOperatingSystem, which answers "true" on ARM64 and so cannot tell the\r
317 # two 64-bit cases apart.\r
318 $RsyncAsset = switch ($HostArch) {\r
319     'X64'   { 'rsync-windows-x64.zip' }\r
320     'Arm64' { 'rsync-windows-x64.zip' }\r
321     default { 'rsync-windows-x86.zip' }\r
322 }\r
323 if ($IsArm64) {\r
324     Write-Host '    ARM64: no native asset is published, using the x64 build under emulation.' -ForegroundColor Yellow\r
325 }\r
326 # The /releases/latest/download/ redirect rather than the API: unauthenticated\r
327 # API calls are rate-limited to 60/hour per IP, which a provisioning run behind a\r
328 # shared NAT can genuinely exhaust, and the redirect costs none of that budget.\r
329 # To hold a box on a known build, pin the tag instead:\r
330 #     .../releases/download/v3.5.0-gABCDEF0/$RsyncAsset\r
331 $RsyncUrl   = "https://github.com/$RsyncRepo/releases/latest/download/$RsyncAsset"\r
332 $RsyncDir   = 'C:\Tools\rsync'\r
333 try {\r
334     New-Item -ItemType Directory -Force -Path $RsyncDir | Out-Null\r
335     $RsyncExe = Join-Path $RsyncDir 'rsync.exe'\r
336 \r
337     # Does the release's ssh.exe have the libcrypto it needs? Decided before the\r
338     # download so the answer can also gate what comes out of the zip. This is a\r
339     # cheap pre-filter only - the authoritative check is running the thing, which\r
340     # happens after the unpack below.\r
341     $SysCrypto = Join-Path $env:WINDIR 'System32\libcrypto.dll'\r
342     $WantSsh   = Test-Path $SysCrypto\r
343     if (-not $WantSsh) {\r
344         Write-Warning "$SysCrypto is missing - the OpenSSH Client capability is not installed - and the release's ssh.exe needs it. Installing rsync.exe only; rsync will use the ssh on the PATH."\r
345     } else {\r
346         $v = (Get-Item $SysCrypto).VersionInfo.FileVersion\r
347         if ($v -and ([version]($v -replace '[^0-9.]', '')) -lt [version]'3.8.2') {\r
348             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
349         }\r
350         if ($IsArm64) {\r
351             # On ARM64 the presence of libcrypto.dll proves less than it does on\r
352             # x64: System32 holds the ARM64 build of it, and the ssh.exe in the\r
353             # zip is x64. Whether an emulated x64 process can load that DLL comes\r
354             # down to whether it is a plain ARM64 binary or an ARM64X one - not\r
355             # something worth deciding by parsing the PE header, when running the\r
356             # binary answers it outright. Unpack it, then run it (below).\r
357             Write-Host '    ARM64: the x64 ssh.exe will be verified by running it, not by assuming.' -ForegroundColor Yellow\r
358         }\r
359     }\r
360 \r
361     # Download and unpack beside the targets, not over them, so an interrupted\r
362     # transfer can't leave a truncated rsync.exe sitting on the PATH. The scratch\r
363     # name still has to END in .zip: Windows PowerShell 5.1's Expand-Archive\r
364     # refuses any other extension outright ("*.download is not a supported\r
365     # archive file format"), where PowerShell 7 just reads the file.\r
366     [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\r
367     $tmpZip = Join-Path $RsyncDir "download-$RsyncAsset"\r
368     Invoke-WebRequest -Uri $RsyncUrl -OutFile $tmpZip -UseBasicParsing\r
369     Write-Host "    Downloaded $RsyncAsset ($([math]::Round((Get-Item $tmpZip).Length / 1MB, 2)) MB)"\r
370 \r
371     # Verify against the .sha256 published beside it. Same origin, so this is an\r
372     # integrity check on the transfer rather than a defence against a hostile\r
373     # release - but a truncated or proxy-mangled download is the failure that\r
374     # actually happens, and it fails here instead of mid-transfer later.\r
375     #\r
376     # -OutFile, not .Content: GitHub serves the .sha256 as\r
377     # application/octet-stream, and Invoke-WebRequest hands back a byte[] rather\r
378     # than a string for any non-text content type, so .Content would compare the\r
379     # first BYTE against the hash and fail on every correct download.\r
380     $tmpSha = "$tmpZip.sha256"\r
381     Invoke-WebRequest -Uri "$RsyncUrl.sha256" -OutFile $tmpSha -UseBasicParsing\r
382     $want = (((Get-Content $tmpSha -Raw) -split '\s+')[0]).Trim().ToLower()\r
383     Remove-Item $tmpSha -Force -ErrorAction SilentlyContinue\r
384     $got  = (Get-FileHash $tmpZip -Algorithm SHA256).Hash.ToLower()\r
385     if ($want -and $want -ne $got) {\r
386         Remove-Item $tmpZip -Force\r
387         throw "SHA-256 mismatch for ${RsyncAsset}: expected $want, got $got"\r
388     }\r
389     Write-Host "    SHA-256 verified: $got"\r
390 \r
391     # Unpack to a scratch directory and move out the files we asked for, rather\r
392     # than expanding straight over the install directory: the zip is the unit\r
393     # that was checksummed, and this way a future release adding something to it\r
394     # cannot quietly drop that something onto the machine PATH.\r
395     $unpack = Join-Path $RsyncDir '.unpack'\r
396     if (Test-Path $unpack) { Remove-Item -Recurse -Force $unpack }\r
397     Expand-Archive -Path $tmpZip -DestinationPath $unpack -Force\r
398     Remove-Item $tmpZip -Force\r
399     foreach ($f in 'rsync.exe', 'ssh.exe', 'COPYING.txt', 'NOTICE-ssh.txt') {\r
400         $src = Join-Path $unpack $f\r
401         if (-not (Test-Path $src)) { continue }\r
402         if ($f -eq 'ssh.exe' -and -not $WantSsh) { continue }\r
403         Move-Item -Path $src -Destination (Join-Path $RsyncDir $f) -Force\r
404     }\r
405     Remove-Item -Recurse -Force $unpack\r
406 \r
407     # Prove the unpacked ssh.exe actually starts, and DELETE it if it does not.\r
408     #\r
409     # This matters more than it looks. rsync.exe prefers an ssh.exe sitting in its\r
410     # own directory, so a present-but-unstartable one does not degrade to the\r
411     # in-box client - it breaks rsync outright, and the error you get is a remote\r
412     # shell that died rather than anything naming ssh.exe. The two ways to land\r
413     # there are a missing/old System32 libcrypto.dll (x64 boxes) and an ARM64 box\r
414     # whose ARM64 libcrypto cannot be loaded by this x64 binary. Removing it is\r
415     # the repair in both cases: rsync then falls back to the ssh on the PATH,\r
416     # which on ARM64 is the native in-box client.\r
417     #\r
418     # EAP back to Continue for the call: ssh -V writes its version to STDERR, and\r
419     # under $ErrorActionPreference = 'Stop' a native command's stderr becomes a\r
420     # terminating error, so a WORKING client would look like a broken one.\r
421     # $global:LASTEXITCODE is cleared first because an exe that cannot start at\r
422     # all throws without setting one, and the stale 0 from the previous native\r
423     # command would otherwise read as success.\r
424     $SshExe = Join-Path $RsyncDir 'ssh.exe'\r
425     if ($WantSsh -and (Test-Path $SshExe)) {\r
426         $prevEap = $ErrorActionPreference\r
427         $ErrorActionPreference = 'Continue'\r
428         $global:LASTEXITCODE = $null\r
429         $sshVer = $null\r
430         try { $sshVer = (& $SshExe -V 2>&1 | Select-Object -First 1) } catch { }\r
431         finally { $ErrorActionPreference = $prevEap }\r
432         if ($LASTEXITCODE -eq 0) {\r
433             Write-Host "    ssh.exe runs: $sshVer"\r
434         } else {\r
435             $why = if ($null -eq $LASTEXITCODE) { 'it would not start' } else { "exit $LASTEXITCODE" }\r
436             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
437             Remove-Item $SshExe -Force -ErrorAction SilentlyContinue\r
438             $WantSsh = $false\r
439         }\r
440     }\r
441     Write-Host "    Installed $RsyncExe$(if ($WantSsh) { ' and the ssh.exe it runs' })"\r
442 \r
443     # Machine PATH (HKLM environment). Idempotent: only appends if absent.\r
444     $m = [Environment]::GetEnvironmentVariable('Path', 'Machine')\r
445     if (-not $m) { $m = '' }\r
446     if (($m -split ';') -notcontains $RsyncDir) {\r
447         $new = if ($m.Trim()) { $m.TrimEnd(';') + ';' + $RsyncDir } else { $RsyncDir }\r
448         [Environment]::SetEnvironmentVariable('Path', $new, 'Machine')\r
449         Write-Host "    Added $RsyncDir to the machine PATH (restart shells / sshd to pick it up)."\r
450         # sshd caches the environment it was started with, so an already-running\r
451         # service would not see the new PATH until restarted.\r
452         if ((Get-Service sshd -ErrorAction SilentlyContinue).Status -eq 'Running') {\r
453             Restart-Service sshd\r
454             Write-Host '    Restarted sshd so it inherits the updated machine PATH.'\r
455         }\r
456     } else {\r
457         Write-Host "    OK: $RsyncDir already in the machine PATH"\r
458     }\r
459 \r
460     & $RsyncExe --version | Select-Object -First 1\r
461 } catch {\r
462     Write-Warning "rsync install failed: $($_.Exception.Message)"\r
463     Write-Warning "Download $RsyncAsset from https://github.com/$RsyncRepo/releases manually"\r
464     Write-Warning "and unpack it into $RsyncDir, keeping rsync.exe and ssh.exe together."\r
465 }\r
466 \r
467 # ---------------------------------------------------------------------------\r
468 # Visual Studio 2022 Community\r
469 # ---------------------------------------------------------------------------\r
470 $TempDir = Join-Path $env:TEMP 'dev_install'\r
471 New-Item -ItemType Directory -Force -Path $TempDir | Out-Null\r
472 \r
473 # Component IDs split into independent groups so each can be installed in its\r
474 # own pass. The base group is the known-good set; Clang and the Windows XP\r
475 # toolset are layered on afterwards so a failure clearly identifies the culprit.\r
476 # Component reference: https://learn.microsoft.com/visualstudio/install/workload-component-id-vs-community\r
477 $BaseComponents = @(\r
478     # Core C++ desktop workload\r
479     'Microsoft.VisualStudio.Workload.NativeDesktop'\r
480 \r
481     # MSVC build tools. Named explicitly rather than left to --includeRecommended,\r
482     # because what that pulls in depends on the host: on an ARM64 machine the\r
483     # workload's recommended set is the ARM64-hosted toolchain targeting ARM64,\r
484     # and the x64 cross-compiler is NOT implied. Ask for both and the box builds\r
485     # every target it can, whichever architecture it is:\r
486     #   on x64   -> x64-hosted, targeting x86/x64 and ARM64\r
487     #   on ARM64 -> ARM64-hosted, targeting ARM64 and x86/x64\r
488     # Both are native toolchains; neither cross-compile runs under emulation.\r
489     'Microsoft.VisualStudio.Component.VC.Tools.x86.x64'\r
490     'Microsoft.VisualStudio.Component.VC.Tools.ARM64'\r
491 \r
492     # Spectre-mitigated MSVC runtime libs, for each target above\r
493     'Microsoft.VisualStudio.Component.VC.Runtimes.x86.x64.Spectre'\r
494     'Microsoft.VisualStudio.Component.VC.Runtimes.ARM64.Spectre'\r
495 \r
496     # Spectre-mitigated ATL (needed for many driver/COM projects). The x86/x64 and\r
497     # ARM64 ATL libraries are separate components; a driver or COM project built\r
498     # for ARM64 wants the second one, and it is not implied by the first.\r
499     'Microsoft.VisualStudio.Component.VC.ATL.Spectre'\r
500     'Microsoft.VisualStudio.Component.VC.ATL.ARM64.Spectre'\r
501 \r
502     # Windows 11 SDK â€” build number must match the WDK below\r
503     'Microsoft.VisualStudio.Component.Windows11SDK.26100'\r
504 \r
505     # WDK Visual Studio extension (VSIX). The silent wdksetup.exe /quiet does NOT\r
506     # install this (it only prompts interactively), so it must be added here.\r
507     'Component.Microsoft.Windows.DriverKit'\r
508 )\r
509 \r
510 # Clang/LLVM toolset (ClangCL, used in CMakePresets.json). Two parts: the Clang\r
511 # compiler itself, plus the MSBuild integration providing the "ClangCL" toolset.\r
512 #\r
513 # Installed on every architecture, ARM64 included, and native there - not an\r
514 # emulated x64 compiler. Two things establish that: the VSIX is\r
515 # productArch=neutral with no chip/machineArch restriction, so the Arm installer\r
516 # offers it; and MSVC's VC\Tools\Llvm tree is partitioned by HOST architecture\r
517 # (bin = x86, x64\bin = x64, ARM64\bin = ARM64) with genuine ARM64 binaries\r
518 # already in ARM64\bin, which is where clang-cl.exe lands. Unlike the v141/XP\r
519 # group below, nothing technical is in the way.\r
520 #\r
521 # This is the compiler diversity the box is after: MSVC and clang-cl over the\r
522 # same sources, both native.\r
523 $ClangComponents = @(\r
524     'Microsoft.VisualStudio.Component.VC.Llvm.Clang'\r
525     'Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset'\r
526 )\r
527 \r
528 # Windows XP targeting (v141_xp toolset, used in CMakePresets.json). The v141\r
529 # (VS2017) build tools provide the 14.16 compiler that the XP toolset wraps;\r
530 # WinXP layers the XP-compatible CRT/SDK on top of it.\r
531 #\r
532 # NOT USED ON ARM64, and left empty there. The components are still listed in the\r
533 # catalog on Arm, so this is not strictly "unavailable" - but the 14.16 toolset\r
534 # predates Windows on Arm as a host and ships HostX86/HostX64 compilers only, so\r
535 # the best you could get is an x86-emulated compiler, and Microsoft does not\r
536 # support XP targeting from an Arm host. Nothing is lost that this box could have\r
537 # used: Windows XP never ran on ARM64, so an XP-targeting build from an ARM64\r
538 # host has no purpose beyond producing x86 binaries, which the current toolset\r
539 # does natively via -A Win32. On x64 the group is installed exactly as before.\r
540 $XpComponents = if ($IsArm64) { @() } else {\r
541     @(\r
542         'Microsoft.VisualStudio.Component.VC.v141.x86.x64'\r
543         'Microsoft.VisualStudio.Component.WinXP'\r
544     )\r
545 }\r
546 \r
547 # Which Visual Studio generation to drive: 17 = VS 2022, 18 = VS 2026. Both have\r
548 # native ARM64 installers and ARM64-hosted MSVC. This picks the bootstrapper URL,\r
549 # and - just as importantly - scopes the vswhere lookup below, so a box that\r
550 # already has a DIFFERENT generation installed is not mistaken for this one.\r
551 #\r
552 # Split by architecture on purpose:\r
553 #   x64   -> 17. The v141 / Windows XP targeting toolset in $XpComponents is the\r
554 #            reason; that group is the whole point of pinning a generation here.\r
555 #   ARM64 -> 18. The XP group is skipped on Arm regardless (no ARM64-hosted 14.16\r
556 #            compiler), so nothing holds this back to 17, and VS 2026 brings the\r
557 #            newer MSVC. Together with the native ARM64 clang-cl from\r
558 #            $ClangComponents above, that is the compiler diversity on this box.\r
559 $VsChannel = if ($IsArm64) { 18 } else { 17 }\r
560 $VsEdition = 'community'   # community | professional | enterprise\r
561 \r
562 # aka.ms path segment per generation. NOT the same word for both: VS 2022 is\r
563 # published under /release/, VS 2026 under /stable/. This is not cosmetic -\r
564 # https://aka.ms/vs/18/release/vs_community.exe is not a 404, it silently\r
565 # redirects to Bing and returns 200 with an HTML body, so a wrong guess here\r
566 # downloads a web page, names it vs_community.exe, and fails at Start-Process\r
567 # with something that looks nothing like a bad URL.\r
568 $VsChannelPath = if ($VsChannel -ge 18) { 'stable' } else { 'release' }\r
569 \r
570 # Detect an existing VS install via vswhere (ships with the VS Installer). Note\r
571 # that vswhere itself lives under the 32-bit Program Files on every architecture,\r
572 # ARM64 included - the VS Installer is x86-registered there by contract even\r
573 # though the installer binaries themselves are native.\r
574 # These are referenced by Invoke-VsModify via $script: scope.\r
575 $VsWhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'\r
576 $InstallPath = Get-VsInstallPath\r
577 \r
578 # Report any OTHER Visual Studio generations on the box. They are left alone -\r
579 # the passes below only ever touch $InstallPath - but when none of them matches\r
580 # $VsChannel this script is about to download and install a second, largely\r
581 # redundant toolchain, and that should be a visible decision rather than a\r
582 # surprise 10GB. (On ARM64, where $VsChannel is 18, an existing VS 2026 IS the\r
583 # match and gets modified in place rather than duplicated.)\r
584 if (Test-Path $VsWhere) {\r
585     $others = & $VsWhere -products '*' -format value -property installationPath |\r
586               Where-Object { $_ -and $_ -ne $InstallPath }\r
587     if ($others) {\r
588         Write-Step 'Other Visual Studio installations detected'\r
589         foreach ($o in $others) { Write-Host "    $o" -ForegroundColor Yellow }\r
590         Write-Host "    Not modified. This script drives VS generation $VsChannel only." -ForegroundColor Yellow\r
591         Write-Host "    To use one of the above instead, set `$VsChannel at the top of this step." -ForegroundColor Yellow\r
592     }\r
593 }\r
594 \r
595 Write-Step "Downloading VS $VsChannel $VsEdition bootstrapper (host: $HostArch)"\r
596 # aka.ms serves the bootstrapper for the requesting machine's architecture, so on\r
597 # ARM64 this is the native ARM64 installer - no --arch flag needed or offered.\r
598 $VsInstallerUrl = "https://aka.ms/vs/$VsChannel/$VsChannelPath/vs_$VsEdition.exe"\r
599 $VsBootstrapper = Join-Path $TempDir "vs_$VsEdition.exe"\r
600 Write-Host "    $VsInstallerUrl" -ForegroundColor DarkGray\r
601 Invoke-WebRequest -Uri $VsInstallerUrl -OutFile $VsBootstrapper -UseBasicParsing\r
602 \r
603 # Prove we got an installer and not a web page. The Bing redirect described above\r
604 # returns 200 with HTML, and every other aka.ms typo behaves the same way, so a\r
605 # bad channel/edition combination is otherwise only discovered when the "exe"\r
606 # fails to start. 'MZ' is the DOS header every PE begins with.\r
607 $vsHead = [IO.File]::ReadAllBytes($VsBootstrapper) | Select-Object -First 2\r
608 if (-not ($vsHead.Count -eq 2 -and $vsHead[0] -eq 0x4D -and $vsHead[1] -eq 0x5A)) {\r
609     throw "Visual Studio: $VsInstallerUrl did not return an executable (no MZ header; $((Get-Item $VsBootstrapper).Length) bytes). Check `$VsChannel / `$VsChannelPath / `$VsEdition."\r
610 }\r
611 Write-Host "    OK: bootstrapper is a PE ($([math]::Round((Get-Item $VsBootstrapper).Length / 1MB, 2)) MB)"\r
612 \r
613 # Install in three sequential passes. The base set is installed first (this is\r
614 # the configuration that previously worked); Clang and the XP toolset are added\r
615 # afterwards. If one fails, its label pinpoints which group is responsible.\r
616 #\r
617 # The last two are -Optional: neither the Clang toolset nor XP targeting is\r
618 # needed to build with MSVC, and on a host where one of them is simply not\r
619 # offered a hard failure here would cost you the whole toolchain over a component\r
620 # you can add later from the installer UI.\r
621 Invoke-VsModify -Label 'base toolset + workload' -Ids $BaseComponents\r
622 Invoke-VsModify -Label 'Clang / LLVM'            -Ids $ClangComponents -Optional\r
623 if ($IsArm64) {\r
624     Write-Step 'Visual Studio: Windows XP (v141 + WinXP)'\r
625     Write-Host '    Skipped on ARM64: the v141 (14.16) toolset ships x86/x64-hosted compilers only,' -ForegroundColor Yellow\r
626     Write-Host '    and Windows XP targeting is not offered for Arm hosts. Build x86 with the' -ForegroundColor Yellow\r
627     Write-Host '    current toolset instead (cmake -A Win32), which is native here.' -ForegroundColor Yellow\r
628 } else {\r
629     Invoke-VsModify -Label 'Windows XP (v141 + WinXP)' -Ids $XpComponents -Optional\r
630 }\r
631 \r
632 # ---------------------------------------------------------------------------\r
633 # Verify what actually landed, on disk. Earlier runs silently skipped the v141\r
634 # toolset and the failure only surfaced at build time, so check here and say so\r
635 # loudly instead. Widened from that one check to every toolset worth naming,\r
636 # because the same "installed something, but not the thing you needed" failure is\r
637 # now possible per host architecture: this reports which MSVC host toolchains are\r
638 # present (HostARM64 is what proves the compiler is native rather than emulated),\r
639 # whether clang-cl is there, and - on x64 only - whether v141 is.\r
640 #\r
641 # Reporting, not throwing. A missing optional component is something to fix from\r
642 # the installer UI, not a reason to fail a provisioning run that installed a\r
643 # working compiler.\r
644 # ---------------------------------------------------------------------------\r
645 Write-Step 'Verifying the installed toolsets'\r
646 $InstallPath = Get-VsInstallPath\r
647 if (-not $InstallPath) {\r
648     Write-Warning "No Visual Studio $VsChannel installation found after the passes above; cannot verify toolsets."\r
649 } else {\r
650     Write-Host "    Install path: $InstallPath"\r
651 \r
652     # What MSVC versions landed, and which host toolchains each one carries.\r
653     # HostARM64 is the directory that proves the native ARM64 compiler is here\r
654     # rather than an x64 one that would run under emulation.\r
655     $msvcRoot = Join-Path $InstallPath 'VC\Tools\MSVC'\r
656     foreach ($v in (Get-ChildItem $msvcRoot -Directory -ErrorAction SilentlyContinue | Sort-Object Name)) {\r
657         $hosts = Get-ChildItem (Join-Path $v.FullName 'bin') -Directory -ErrorAction SilentlyContinue |\r
658                  ForEach-Object { $_.Name }\r
659         Write-Host "    MSVC $($v.Name): $(if ($hosts) { $hosts -join ', ' } else { '(no bin dir)' })"\r
660     }\r
661     if ($IsArm64) {\r
662         $armHost = Test-Path (Join-Path $msvcRoot '*\bin\HostARM64\ARM64\cl.exe')\r
663         if ($armHost) {\r
664             Write-Host '    OK: native ARM64-hosted cl.exe present.' -ForegroundColor Green\r
665         } else {\r
666             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
667         }\r
668     }\r
669 \r
670     # clang-cl, for the ClangCL toolset in CMakePresets.json. Checked per host\r
671     # directory, because the Llvm tree is partitioned by HOST architecture and\r
672     # only the matching one is a native compiler.\r
673     #\r
674     # Look for clang-cl.exe specifically, NOT for the directory. VC\Tools\Llvm\*\bin\r
675     # holds clang-format.exe and clang-tidy.exe on every host whether or not the\r
676     # Clang component was ever installed - those ship with the NativeDesktop\r
677     # workload - so a present ARM64\bin proves nothing on its own. That is the\r
678     # false positive to avoid when checking this by hand.\r
679     $llvmRoot = Join-Path $InstallPath 'VC\Tools\Llvm'\r
680     $clangArm = Test-Path (Join-Path $llvmRoot 'ARM64\bin\clang-cl.exe')\r
681     $clangX64 = Test-Path (Join-Path $llvmRoot 'x64\bin\clang-cl.exe')\r
682     $clangX86 = Test-Path (Join-Path $llvmRoot 'bin\clang-cl.exe')\r
683     if ($clangArm -or $clangX64 -or $clangX86) {\r
684         Write-Host "    clang-cl: $(@(if ($clangArm) {'ARM64'}; if ($clangX64) {'x64'}; if ($clangX86) {'x86'}) -join ', ')" -ForegroundColor Green\r
685         # On ARM64 the x64 build would still run, under emulation - so say plainly\r
686         # whether the NATIVE one is the one that landed.\r
687         if ($IsArm64 -and -not $clangArm) {\r
688             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
689         }\r
690     } else {\r
691         Write-Warning 'clang-cl not found - the ClangCL presets will fail. Add the "C++ Clang tools for Windows" component.'\r
692     }\r
693 \r
694     # v141 / XP. Only meaningful where the toolset can exist at all; on ARM64 the\r
695     # group above was deliberately skipped, so warning here would be noise about\r
696     # a decision this script made on purpose two steps ago.\r
697     if ($IsArm64) {\r
698         Write-Host '    v141 / Windows XP toolset: n/a on ARM64 (not offered for Arm hosts).' -ForegroundColor DarkGray\r
699     } else {\r
700         $V141 = Get-ChildItem $msvcRoot -Directory -ErrorAction SilentlyContinue |\r
701                 Where-Object { $_.Name -like '14.16.*' } | Select-Object -First 1\r
702         if ($V141) {\r
703             Write-Host "    OK: v141 toolset present ($($V141.Name))" -ForegroundColor Green\r
704         } else {\r
705             Write-Warning 'v141 (14.16.x) toolset NOT found - the XP build presets will fail.'\r
706             Write-Warning 'Add it via Visual Studio Installer > Modify > Individual components:'\r
707             Write-Warning '  - MSVC v141 - VS 2017 C++ x64/x86 build tools (v14.16)'\r
708             Write-Warning '  - C++ Windows XP Support for VS 2017 (v141) tools'\r
709         }\r
710     }\r
711 }\r
712 \r
713 # ---------------------------------------------------------------------------\r
714 # Windows Driver Kit (WDK 10.0.26100)\r
715 # Build 26100 matches the Windows 11 SDK installed above.\r
716 # Provides IddCx (iddcx.h / iddcx.lib) and UMDF 2.x for Indirect Display Drivers.\r
717 # linkid=2335869 -> WDK 26100.6584 (per Microsoft "Other WDK Downloads").\r
718 # ---------------------------------------------------------------------------\r
719 $WdkVersion = '10.0.26100'\r
720 # Both hives. The WDK installer is a 32-bit program, so on x64 it writes under\r
721 # WOW6432Node - but which hive a given kit lands in has varied across kit\r
722 # versions and architectures, and reading only one of them makes an installed WDK\r
723 # look absent, which costs a needless multi-GB reinstall on every run. Check the\r
724 # native hive too and take whichever answers.\r
725 $WdkInstalledRoot = @(\r
726     'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows Kits\Installed Roots'\r
727     'HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots'\r
728 ) | ForEach-Object { (Get-ItemProperty $_ -ErrorAction SilentlyContinue).WdkBinRootVersioned } |\r
729     Where-Object { $_ } | Select-Object -First 1\r
730 \r
731 if ($WdkInstalledRoot -and $WdkInstalledRoot -match [regex]::Escape($WdkVersion)) {\r
732     # Re-running wdksetup.exe for an already-present version returns exit code\r
733     # 2008 (maintenance mode / nothing to do), which is not a real failure.\r
734     Write-Step "WDK $WdkVersion already installed - skipping ($WdkInstalledRoot)"\r
735 } else {\r
736     Write-Step 'Downloading WDK installer'\r
737     $WdkUrl       = 'https://go.microsoft.com/fwlink/?linkid=2335869'\r
738     $WdkInstaller = Join-Path $TempDir 'wdksetup.exe'\r
739     Invoke-WebRequest -Uri $WdkUrl -OutFile $WdkInstaller -UseBasicParsing\r
740 \r
741     Write-Step 'Installing WDK'\r
742     # wdksetup.exe is a 32-bit binary and runs under emulation on ARM64; the kit\r
743     # it lays down does include the ARM64 target headers, libs and tools (the\r
744     # signing/deployment tools under bin\arm64), so an ARM64 driver builds from\r
745     # an ARM64 host. Only the installer is emulated, not the toolchain.\r
746     $proc = Start-Process -FilePath $WdkInstaller -ArgumentList '/quiet /norestart' -Wait -PassThru -NoNewWindow\r
747     Write-Host "    WDK installer exit code: $($proc.ExitCode)"\r
748     if ($proc.ExitCode -eq 2008) {\r
749         # 2008 = the WDK is already present; the installer has nothing to do.\r
750         Write-Host '    [WDK already installed (exit 2008) - treating as success]' -ForegroundColor Yellow\r
751     } else {\r
752         Assert-ExitCode $proc.ExitCode 'WDK'\r
753     }\r
754 }\r
755 \r
756 # ---------------------------------------------------------------------------\r
757 # Windows Performance Toolkit: xperf, wpr, and Windows Performance Analyzer\r
758 # (wpa.exe) -- ETW CPU + loader profiling and the GUI that reads the traces.\r
759 #\r
760 # WPA is NOT a Visual Studio component and has no relationship to VS's own\r
761 # Performance Profiler (a separate, .diagsession-based tool that cannot open an\r
762 # .etl). It ships in exactly two places: as an optional FEATURE of the Windows\r
763 # SDK ("Windows Performance Toolkit", OptionId.WindowsPerformanceToolkit), and\r
764 # in the Windows ADK, which bundles the same toolkit. Whether the SDK install\r
765 # that Visual Studio performs happens to select that feature varies with the VS\r
766 # and SDK version - when it does, WPT lands in\r
767 # %ProgramFiles(x86)%\Windows Kits\10\Windows Performance Toolkit and the SDK\r
768 # puts that directory on the machine PATH itself - so this step DETECTS first\r
769 # and only falls back to installing the ADK (winget owns the versioned download\r
770 # URL, which makes it the reliable source) when nothing is there. That fallback\r
771 # is a large download; to install just the toolkit instead, run the standalone\r
772 # SDK setup with\r
773 #     winsdksetup.exe /features OptionId.WindowsPerformanceToolkit /q\r
774 #\r
775 # There is also a newer WPA in the Microsoft Store (`winget install --id\r
776 # 9N0W1B2BXGNZ --source msstore`), which updates independently of the SDK. It is\r
777 # not installed here: the Store package needs an interactive, signed-in session,\r
778 # which is exactly what this elevated, unattended half does not have.\r
779 #\r
780 # Idempotent and non-fatal - it never aborts provisioning.\r
781 # ---------------------------------------------------------------------------\r
782 Write-Step 'Windows Performance Toolkit (xperf / wpr / WPA)'\r
783 $WptDirs = @(\r
784     (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Windows Performance Toolkit'),\r
785     (Join-Path $env:ProgramFiles        'Windows Kits\10\Windows Performance Toolkit'),\r
786     (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Assessment and Deployment Kit\Windows Performance Toolkit')\r
787 )\r
788 function Find-WptDir { $script:WptDirs | Where-Object { Test-Path (Join-Path $_ 'xperf.exe') } | Select-Object -First 1 }\r
789 \r
790 $WptDir = Find-WptDir\r
791 if ($WptDir) {\r
792     Write-Host "    OK: WPT already present ($WptDir)" -ForegroundColor Green\r
793 } else {\r
794     try {\r
795         # The ADK manifest offers no ARM64 installer, so on ARM64 winget fetches\r
796         # the x64 one; it runs under emulation and lays down a toolkit that does\r
797         # include the ARM64 binaries. The SDK feature is the lighter route on any\r
798         # architecture and is worth preferring if this fallback ever gives\r
799         # trouble - see the winsdksetup.exe line in the comment above.\r
800         if ($IsArm64) {\r
801             Write-Host '    ARM64: the ADK installer is x64 (emulated); the toolkit it installs is ARM64.' -ForegroundColor Yellow\r
802         }\r
803         winget install --id Microsoft.WindowsADK --exact --silent --disable-interactivity `\r
804             --accept-source-agreements --accept-package-agreements\r
805         Write-Host '    Windows ADK (includes Windows Performance Toolkit) installed.'\r
806         $WptDir = Find-WptDir\r
807     } catch {\r
808         Write-Warning "WPT install failed: $($_.Exception.Message)"\r
809         Write-Warning 'Install manually: winget install Microsoft.WindowsADK, or add the'\r
810         Write-Warning 'Windows SDK "Windows Performance Toolkit" optional feature.'\r
811     }\r
812 }\r
813 \r
814 if ($WptDir) {\r
815     # Report what actually landed. wpa.exe is the piece people come looking for\r
816     # and it is the one that is absent if a trimmed toolkit ever shows up.\r
817     foreach ($tool in 'xperf.exe', 'wpr.exe', 'wpa.exe', 'wpaexporter.exe') {\r
818         $p = Join-Path $WptDir $tool\r
819         if (Test-Path $p) {\r
820             Write-Host "    $tool $((Get-Item $p).VersionInfo.ProductVersion)"\r
821         } else {\r
822             Write-Warning "$tool is missing from $WptDir"\r
823         }\r
824     }\r
825 \r
826     # The WPT installer normally adds this to the machine PATH itself (and the\r
827     # Start Menu gets "Windows Kits > Windows Performance Toolkit" shortcuts for\r
828     # WPA and WPR). Re-assert it anyway: on the machine PATH rather than a user\r
829     # one so it also resolves for the non-interactive sshd sessions this box is\r
830     # driven through, which build their environment from the registry PATH.\r
831     # Compared trailing-backslash-insensitively - the installer's own entry has\r
832     # one, and adding a second spelling of the same directory is just noise.\r
833     $m = [Environment]::GetEnvironmentVariable('Path', 'Machine')\r
834     if (-not $m) { $m = '' }\r
835     $have = ($m -split ';') | Where-Object { $_.TrimEnd('\') -eq $WptDir.TrimEnd('\') }\r
836     if ($have) {\r
837         Write-Host "    OK: $WptDir already in the machine PATH"\r
838     } else {\r
839         $new = if ($m.Trim()) { $m.TrimEnd(';') + ';' + $WptDir } else { $WptDir }\r
840         [Environment]::SetEnvironmentVariable('Path', $new, 'Machine')\r
841         Write-Host "    Added $WptDir to the machine PATH (restart shells to pick it up)."\r
842     }\r
843 }\r
844 \r
845 \r
846 # ---------------------------------------------------------------------------\r
847 # Intel VTune Profiler - reported, not installed\r
848 #\r
849 # Deliberately NOT automated, unlike everything above. The offline installer is\r
850 # a ~750 MB download from a URL carrying a per-release GUID\r
851 # (registrationcenter-download.intel.com/akdlm/IRC_NAS/<guid>/intel-vtune-<ver>_offline.exe)\r
852 # with no "latest" redirect behind it, so every new build means editing a\r
853 # hard-coded link in here - and it is only worth having on Intel silicon, since\r
854 # hardware event-based sampling reads Intel PMU counters. Not a good trade for a\r
855 # script that has to keep working unattended on any box.\r
856 #\r
857 # So this step only reports. To install it, take the Windows offline installer\r
858 # from\r
859 #     https://www.intel.com/content/www/us/en/developer/tools/oneapi/vtune-profiler-download.html\r
860 # and run it elevated; it installs unattended with\r
861 #     intel-vtune-<version>_offline.exe -a --silent --cli --eula accept\r
862 # ---------------------------------------------------------------------------\r
863 Write-Step 'Intel VTune Profiler (status only)'\r
864 if ($IsArm64) {\r
865     # Not a "not installed yet" case - there is no Windows-on-Arm build of VTune,\r
866     # and there is nothing for it to sample: its whole value is reading Intel PMU\r
867     # counters. Say so plainly and point at what does work here, rather than\r
868     # printing a download link for a product this box cannot run.\r
869     Write-Host '    n/a on ARM64: Intel ships no Windows-on-Arm build, and hardware event-based' -ForegroundColor DarkGray\r
870     Write-Host '    sampling reads Intel PMU counters. Use the Windows Performance Toolkit above' -ForegroundColor DarkGray\r
871     Write-Host '    (wpr / xperf to collect, wpa to analyse) for profiling on this box.' -ForegroundColor DarkGray\r
872     Write-Host '    Arm also publishes Arm Performance Studio / Streamline for Arm PMU sampling.' -ForegroundColor DarkGray\r
873 } else {\r
874     $UninstallKeys = @(\r
875         'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'\r
876         'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'\r
877     )\r
878     $vtune = Get-ItemProperty $UninstallKeys -ErrorAction SilentlyContinue |\r
879              Where-Object { $_.DisplayName -match 'VTune' } |\r
880              Select-Object -First 1\r
881     if ($vtune) {\r
882         Write-Host "    Installed: $($vtune.DisplayName.Trim()) $($vtune.DisplayVersion)" -ForegroundColor Green\r
883         # The oneAPI layout keeps a `latest` junction beside the versioned directory,\r
884         # so this path stays right across upgrades.\r
885         $VTuneCli = Join-Path $vtune.InstallLocation 'vtune\latest\bin64\vtune.exe'\r
886         if (Test-Path $VTuneCli) { Write-Host "    CLI: $VTuneCli" }\r
887     } else {\r
888         Write-Host '    Not installed.' -ForegroundColor Yellow\r
889         Write-Host '    https://www.intel.com/content/www/us/en/developer/tools/oneapi/vtune-profiler-download.html' -ForegroundColor Yellow\r
890         $cpu = (Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1).Manufacturer\r
891         if ($cpu -and $cpu -notmatch 'Intel') {\r
892             Write-Host "    (This CPU reports itself as '$cpu' - VTune's hardware event-based sampling wants Intel silicon.)" -ForegroundColor Yellow\r
893         }\r
894     }\r
895 }\r
896 \r
897 # ---------------------------------------------------------------------------\r
898 Write-Host "`nAll done." -ForegroundColor Green\r
899 Write-Host "Host architecture was $HostArch."\r
900 Write-Host 'If a reboot was flagged above, restart before opening VS or building drivers.'\r
901 \r
902 }\r
903 catch {\r
904     $ExitCode = 1\r
905     Write-Host "`n==> SETUP FAILED: $($_.Exception.Message)" -ForegroundColor Red\r
906     if ($_.ScriptStackTrace) { Write-Host $_.ScriptStackTrace -ForegroundColor DarkGray }\r
907     # Only fold in the VS Installer logs when a VS step actually failed; for other\r
908     # steps (e.g. WDK) those logs are stale and misleading, so the message above\r
909     # is what matters.\r
910     if ($_.Exception.Message -match 'Visual Studio') {\r
911         try { Show-VsSetupLogs } catch {}\r
912     }\r
913 }\r
914 finally {\r
915     try { Stop-Transcript | Out-Null } catch {}\r
916 \r
917     # This log was created by the elevated (admin) process, so by default the\r
918     # non-elevated caller can't delete it (their token has Administrators marked\r
919     # deny-only). Grant BUILTIN\Users Modify rights so the user account that runs\r
920     # setup-windows.bat can remove the log later. S-1-5-32-545 is the well-known\r
921     # Users SID, used here so this is locale-independent.\r
922     try {\r
923         if (Test-Path $LogFile) {\r
924             $usersSid = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-545')\r
925             $acl  = Get-Acl -Path $LogFile\r
926             $rule = New-Object System.Security.AccessControl.FileSystemAccessRule(\r
927                         $usersSid, 'Modify', 'Allow')\r
928             $acl.AddAccessRule($rule)\r
929             Set-Acl -Path $LogFile -AclObject $acl\r
930         }\r
931     } catch {\r
932         Write-Host "    [warning] could not relax ACL on $LogFile : $($_.Exception.Message)" -ForegroundColor Yellow\r
933     }\r
934 }\r
935 \r
936 exit $ExitCode\r