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