]> vilimpoc.org git repositories - dotfiles/blame - setup-windows-with-uac.ps1
dotfiles: add iperf3 and nasm to the per-user winget installs
[dotfiles] / setup-windows-with-uac.ps1
CommitLineData
3c096107
MV
1#Requires -RunAsAdministrator\r
2<#\r
3 setup-windows-with-uac.ps1\r
452f525c 4 Elevated portion of the Windows provisioning. Invoked by setup-windows.bat\r
3c096107
MV
5 via Start-Process -Verb RunAs, or run manually from an Administrator prompt.\r
6\r
7 What this installs / configures:\r
b8144088
MV
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
3c096107 10 - ssh-agent set to automatic + started\r
452f525c 11 - OpenSSH Server (sshd) capability: automatic + started + inbound TCP 22\r
b8144088
MV
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\r
3c096107
MV
15 - Visual Studio 2022 Community (C++ desktop workload, Spectre libs, WDK VSIX,\r
16 Win11 SDK 26100, Clang/LLVM, and the v141 + Windows XP targeting toolset)\r
17 - Windows Driver Kit 10.0.26100\r
99725a30
MV
18 - Windows Performance Toolkit - xperf, wpr and Windows Performance Analyzer\r
19 (wpa.exe) - on the machine PATH\r
593189b7
MV
20 - Performance Log Users membership for one ordinary account, so it can run\r
21 user-mode ETW sessions (xperf -start ... -on <provider>) without elevation.\r
22 Kernel traces are NOT covered - the NT Kernel Logger is admin-only; see the\r
23 step for what was measured.\r
3c096107
MV
24\r
25 Change $VsInstallerUrl below to the Professional or Enterprise bootstrapper if needed:\r
26 Professional : https://aka.ms/vs/17/release/vs_professional.exe\r
27 Enterprise : https://aka.ms/vs/17/release/vs_enterprise.exe\r
28#>\r
29\r
99725a30 30param(\r
593189b7
MV
31 # Account to put in Performance Log Users (see the "ETW session control"\r
32 # step, which runs first). Defaults to the interactive console user, but\r
33 # setup-windows.bat passes it explicitly: with over-the-shoulder elevation\r
34 # THIS script runs as the administrator whose credentials went into the UAC\r
35 # prompt, not as the user who started the batch file, so $env:USERNAME here\r
36 # is the wrong answer.\r
99725a30 37 #\r
593189b7
MV
38 # Pass an empty string to skip it; adding an account later is one\r
39 # `net localgroup` away.\r
0aec6243
MV
40 [string] $TraceUser = '',\r
41\r
593189b7
MV
42 # Do the ETW step and nothing else. It is a group membership and no\r
43 # downloads, where a full run is dominated by the three Visual Studio\r
44 # passes, which take minutes even when they have nothing to do. It is why\r
45 # that step runs FIRST: -EtwRightsOnly is then just an early exit rather\r
46 # than a set of guards down the rest of the script.\r
0aec6243 47 [switch] $EtwRightsOnly\r
99725a30
MV
48)\r
49\r
3c096107
MV
50$ErrorActionPreference = 'Stop'\r
51\r
52function Write-Step([string]$Msg) {\r
53 Write-Host "`n==> $Msg" -ForegroundColor Cyan\r
54}\r
55\r
56function Assert-ExitCode([int]$Code, [string]$Step) {\r
57 # 0 = success, 3010 = success + reboot required\r
58 if ($Code -notin @(0, 3010)) {\r
59 throw "$Step failed with exit code $Code"\r
60 }\r
61 if ($Code -eq 3010) {\r
62 Write-Host " [reboot required after $Step]" -ForegroundColor Yellow\r
63 }\r
64}\r
65\r
66function Show-VsSetupLogs {\r
67 # The VS Installer writes dd_*.log to the invoking user's %TEMP%. Because\r
68 # this script runs elevated, that %TEMP% belongs to the elevated user and is\r
69 # readable here even when it is NOT readable by the non-elevated caller. Fold\r
70 # only the NEWEST installer + bootstrapper log into the transcript (the setup\r
71 # engine log is where per-component / product errors actually appear) and\r
72 # keep it short so the transcript stays readable.\r
73 Write-Host "`n==> Collecting VS Installer logs from $env:TEMP" -ForegroundColor Cyan\r
74 $recent = Get-ChildItem $env:TEMP -Filter 'dd_*.log' -ErrorAction SilentlyContinue |\r
75 Where-Object { $_.LastWriteTime -gt (Get-Date).AddMinutes(-15) }\r
76 $picks = @()\r
77 $picks += $recent | Where-Object { $_.Name -like 'dd_installer_*' } | Sort-Object LastWriteTime | Select-Object -Last 1\r
78 $picks += $recent | Where-Object { $_.Name -like 'dd_bootstrapper_*' } | Sort-Object LastWriteTime | Select-Object -Last 1\r
79 $picks = $picks | Where-Object { $_ }\r
80 if (-not $picks) {\r
81 Write-Host ' (no VS Installer logs modified in the last 15 minutes)' -ForegroundColor Yellow\r
82 return\r
83 }\r
84 foreach ($l in $picks) {\r
85 Write-Host "`n----- $($l.Name) (tail) -----" -ForegroundColor Yellow\r
86 Get-Content $l.FullName -Tail 40\r
87 }\r
88}\r
89\r
90function Invoke-VsModify {\r
91 # Run one VS install/modify pass for a named group of components. Splitting\r
92 # the install into separate passes makes it obvious WHICH group fails: each\r
93 # call prints its label and exit code before Assert-ExitCode throws.\r
94 param(\r
95 [string] $Label,\r
96 [string[]] $Ids\r
97 )\r
98 Write-Step "VS2022: $Label"\r
99 $addStr = ($Ids | ForEach-Object { "--add $_" }) -join ' '\r
100 # --installPath must be quoted: it contains spaces ("C:\Program Files\...").\r
101 # Windows PowerShell 5.1's Start-Process does not quote array elements, so we\r
102 # hand-build a single string. Component IDs / flags have no spaces.\r
103 $common = '--includeRecommended --quiet --norestart --wait'\r
104 if ($script:InstallPath) {\r
105 $argString = "modify --installPath `"$script:InstallPath`" $addStr $common --force"\r
106 } else {\r
107 # No existing install yet -> this first pass performs the base install.\r
108 $argString = "$addStr $common"\r
109 }\r
110 Write-Host " > $script:VsBootstrapper $argString" -ForegroundColor DarkGray\r
111 $p = Start-Process -FilePath $script:VsBootstrapper -ArgumentList $argString -Wait -PassThru -NoNewWindow\r
112 Write-Host " exit code: $($p.ExitCode)"\r
113 Assert-ExitCode $p.ExitCode "VS2022 ($Label)"\r
114\r
115 # After the first (fresh) install, re-detect the install path so subsequent\r
116 # passes use `modify`.\r
117 if (-not $script:InstallPath -and (Test-Path $script:VsWhere)) {\r
118 $script:InstallPath = & $script:VsWhere -products '*' -property installationPath -format value |\r
119 Select-Object -First 1\r
120 }\r
121}\r
122\r
123# ---------------------------------------------------------------------------\r
124# This runs in a separate elevated window that closes the moment it exits, so\r
125# the non-elevated caller (setup-windows.bat) can't see what happened. Mirror\r
126# all output to a log next to the script and exit with a real code so the\r
127# caller can detect success/failure and show the log.\r
128# ---------------------------------------------------------------------------\r
129$LogFile = Join-Path $PSScriptRoot 'setup-windows-uac.log'\r
130$ExitCode = 0\r
131try { Start-Transcript -Path $LogFile -Force | Out-Null } catch {}\r
132\r
133try {\r
134\r
0aec6243 135# ---------------------------------------------------------------------------\r
593189b7 136# ETW session control for an ordinary account\r
0aec6243 137#\r
593189b7
MV
138# Creating or controlling an event tracing session - even a user-mode one naming\r
139# a single provider - is checked against the security descriptor ETW keeps per\r
140# provider GUID under HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security. The\r
141# default grants the session-control rights (TRACELOG_CREATE_ONDISK,\r
142# TRACELOG_CREATE_REALTIME, TRACELOG_GUID_ENABLE, TRACELOG_LOG_EVENT) to SYSTEM,\r
143# Administrators, the service accounts and BUILTIN\Performance Log Users, and to\r
144# nobody else. Its own description says members "may ... enable trace providers,\r
145# and collect event traces", and that is what membership buys:\r
0aec6243 146#\r
593189b7
MV
147# xperf -start MySession -on Microsoft-Windows-Kernel-Process -f trace.etl\r
148# xperf -stop MySession\r
0aec6243 149#\r
593189b7
MV
150# runs unelevated for a member and is "Access is denied. (0x5)" for everyone\r
151# else. Enough to trace your own application's providers without a UAC prompt.\r
0aec6243 152#\r
593189b7
MV
153# Membership is read into the access token at LOGON, so the account has to sign\r
154# out and back in. Any NEW logon does it - an ssh login into this box is one,\r
155# which is the quick way to check without dropping the desktop.\r
0aec6243 156#\r
593189b7
MV
157# WHAT THIS DOES NOT BUY: system-wide kernel traces. `xperf -on base` and\r
158# `wpr -start` drive the NT Kernel Logger, which is reserved for Administrators\r
159# and LocalSystem - Microsoft documents Performance Log Users access as\r
160# explicitly NOT extending to it. Measured here, so that nobody repeats it: with\r
161# the account in the group, SeSystemProfilePrivilege ("Profile system\r
162# performance") granted to that group, and an explicit ACE giving the group\r
163# TRACELOG_ACCESS_KERNEL_LOGGER on SystemTraceControlGuid - all three in place,\r
164# across a reboot - xperf still answered\r
0aec6243 165#\r
593189b7 166# xperf: error: NT Kernel Logger: Access is denied. (0x5).\r
0aec6243 167#\r
593189b7
MV
168# It is not a check an ACE overrides. Those two grants were dropped again rather\r
169# than left on the box earning nothing, and CPU sampling and whole-system traces\r
170# are elevated work: run xperf, wpr or VTune from an Administrator prompt.\r
0aec6243 171#\r
593189b7
MV
172# Analysis needs none of this either way - wpa.exe opens an existing .etl as a\r
173# plain user.\r
0aec6243 174# ---------------------------------------------------------------------------\r
593189b7 175Write-Step 'ETW session control (non-elevated user-mode tracing)'\r
0aec6243 176$PerfLogUsersSid = 'S-1-5-32-559' # BUILTIN\Performance Log Users\r
0aec6243 177try {\r
0aec6243
MV
178 # Fall back to the console user when the caller did not name one: with\r
179 # over-the-shoulder elevation that is the person who started\r
180 # setup-windows.bat, which is who wants to trace.\r
181 $target = $TraceUser\r
182 if (-not $target) {\r
183 $target = (Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue).UserName\r
184 if ($target) { Write-Host " No -TraceUser given; using the console user $target" }\r
185 }\r
186\r
187 if (-not $target) {\r
188 Write-Warning 'No account to add to Performance Log Users (pass -TraceUser DOMAIN\user).'\r
593189b7 189 Write-Warning 'To do it later:'\r
0aec6243
MV
190 Write-Warning ' net localgroup "Performance Log Users" DOMAIN\user /add'\r
191 } else {\r
192 # Resolve to a SID first: it validates the name, and it is what the\r
193 # membership check compares, so a member spelled ".\claude" in one place\r
194 # and "LATISLAB\claude" in another is still recognised as the same account.\r
195 $targetSid = (New-Object System.Security.Principal.NTAccount($target)).Translate(\r
196 [System.Security.Principal.SecurityIdentifier])\r
197\r
198 # By SID, never by name: "Performance Log Users" is localised, and\r
199 # Get-LocalGroup -SID is how this stays correct on a non-English box.\r
200 $group = Get-LocalGroup -SID $PerfLogUsersSid\r
201\r
202 # Get-LocalGroupMember throws on a group holding a SID that no longer\r
203 # resolves (a known Windows 10 bug), so a failure to READ the membership\r
204 # must not stop us from writing it - fall through and let the add report.\r
205 $already = $false\r
206 try {\r
207 $already = @(Get-LocalGroupMember -SID $PerfLogUsersSid |\r
208 Where-Object { $_.SID.Value -eq $targetSid.Value }).Count -gt 0\r
209 } catch {\r
210 Write-Host " (could not enumerate $($group.Name) members: $($_.Exception.Message))" -ForegroundColor DarkGray\r
211 }\r
212\r
213 if ($already) {\r
214 Write-Host " OK: $target is already in $($group.Name)"\r
215 } else {\r
216 try {\r
217 Add-LocalGroupMember -SID $PerfLogUsersSid -Member $targetSid.Value\r
218 } catch {\r
219 # "already a member" is only reachable when the enumeration above\r
220 # failed, and is not an error. Matched on the type NAME rather\r
221 # than in a typed catch clause: catch types are resolved when the\r
222 # script is PARSED, before the LocalAccounts module has been\r
223 # autoloaded, so naming the type there is a parse error that\r
224 # would take the whole script down.\r
225 if ($_.Exception.GetType().Name -ne 'MemberExistsException') { throw }\r
226 }\r
227 Write-Host " Added $target to $($group.Name)"\r
228 }\r
229\r
230 Write-Host ''\r
593189b7 231 Write-Host " $target must sign out and back in before this takes effect." -ForegroundColor Yellow\r
0aec6243 232 Write-Host ' Then, from that account (NOT elevated):' -ForegroundColor Yellow\r
593189b7
MV
233 Write-Host ' xperf -start T -on Microsoft-Windows-Kernel-Process -f trace.etl' -ForegroundColor Yellow\r
234 Write-Host ' xperf -stop T' -ForegroundColor Yellow\r
0aec6243
MV
235 }\r
236} catch {\r
593189b7
MV
237 Write-Warning "Performance Log Users membership failed: $($_.Exception.Message)"\r
238 Write-Warning 'Do it by hand with:'\r
0aec6243
MV
239 Write-Warning ' net localgroup "Performance Log Users" <user> /add'\r
240}\r
241\r
242if ($EtwRightsOnly) {\r
243 # `exit` inside the try still runs the finally below, so the transcript is\r
244 # stopped and the log is left readable by the non-elevated caller.\r
245 Write-Host "`n-EtwRightsOnly: skipping the installs." -ForegroundColor Green\r
246 exit 0\r
247}\r
248\r
249\r
3c096107
MV
250# ---------------------------------------------------------------------------\r
251# Base tools via winget\r
252# ---------------------------------------------------------------------------\r
b8144088
MV
253\r
254# ---------------------------------------------------------------------------\r
255# OpenSSH Client\r
256#\r
257# Present by default on Windows 10 1809+ / Windows 11, but removable, and absent\r
258# from some Server images. Two things below want it: rsync does not speak ssh\r
259# itself, it execs an ssh binary, and the release's own ssh.exe links against the\r
260# libcrypto.dll this capability puts in System32. It also owns the ssh-agent\r
261# service configured next, so a missing client is why that step would fail.\r
262#\r
263# Non-fatal, like the server half below: a box that cannot have it should still\r
264# finish provisioning.\r
265# ---------------------------------------------------------------------------\r
266Write-Step 'OpenSSH Client'\r
267try {\r
268 $sshc = Get-WindowsCapability -Online -Name 'OpenSSH.Client*' |\r
269 Select-Object -First 1\r
270 if (-not $sshc) {\r
271 Write-Warning 'OpenSSH.Client capability not offered by this Windows image - skipping.'\r
272 } elseif ($sshc.State -eq 'Installed') {\r
273 Write-Host " OK: $($sshc.Name) already installed"\r
274 } else {\r
275 Write-Host " Installing $($sshc.Name) ..."\r
276 $r = Add-WindowsCapability -Online -Name $sshc.Name\r
277 if ($r.RestartNeeded) { Write-Host ' [reboot required after OpenSSH Client]' -ForegroundColor Yellow }\r
278 }\r
279} catch {\r
280 Write-Warning "OpenSSH Client setup failed: $($_.Exception.Message)"\r
281}\r
282\r
3c096107
MV
283# ---------------------------------------------------------------------------\r
284# SSH agent\r
285# ---------------------------------------------------------------------------\r
286Write-Step 'Enabling ssh-agent'\r
287Set-Service -Name ssh-agent -StartupType Automatic\r
288if ((Get-Service ssh-agent).Status -ne 'Running') { Start-Service ssh-agent }\r
289\r
452f525c
MV
290# ---------------------------------------------------------------------------\r
291# OpenSSH Server (sshd)\r
292#\r
293# Used to reach the test VMs (VirtualBox) from the host: remote shell plus the\r
294# transport rsync rides on when seeding test data in. Ships with Windows 10\r
295# 1809+ / Windows 11 as an on-demand capability, so no third-party install.\r
296#\r
297# The capability normally adds the "OpenSSH Server (sshd)" inbound firewall\r
298# rule; we verify and create it if missing (it is absent on some images).\r
299#\r
300# Non-fatal: a box that can't run sshd should still finish provisioning.\r
301# ---------------------------------------------------------------------------\r
302Write-Step 'OpenSSH Server (sshd)'\r
303try {\r
304 $sshd = Get-WindowsCapability -Online -Name 'OpenSSH.Server*' |\r
305 Select-Object -First 1\r
306 if (-not $sshd) {\r
307 Write-Warning 'OpenSSH.Server capability not offered by this Windows image - skipping.'\r
308 } else {\r
309 if ($sshd.State -ne 'Installed') {\r
310 Write-Host " Installing $($sshd.Name) ..."\r
311 $r = Add-WindowsCapability -Online -Name $sshd.Name\r
312 if ($r.RestartNeeded) { Write-Host ' [reboot required after OpenSSH Server]' -ForegroundColor Yellow }\r
313 } else {\r
314 Write-Host " OK: $($sshd.Name) already installed"\r
315 }\r
316\r
317 Set-Service -Name sshd -StartupType Automatic\r
318 if ((Get-Service sshd).Status -ne 'Running') { Start-Service sshd }\r
319 Write-Host ' sshd: Automatic + running'\r
320\r
321 # Firewall: allow inbound 22 on all profiles. VirtualBox host-only and\r
322 # bridged adapters are frequently classified Public, and the capability's\r
323 # own rule is Private-only on some images, which is what leaves a plainly\r
324 # running sshd plainly unreachable.\r
325 #\r
326 # OpenSSH-Server-In-TCP is the name the capability itself uses, so this\r
327 # WIDENS that rule rather than adding a second one next to it. Creating\r
328 # our own under a different name would leave the narrow rule in place and\r
329 # the box still unreachable on a Public-classified adapter; creating one\r
330 # under the same name would collide. Adopt it if present, create it if not.\r
331 $ruleName = 'OpenSSH-Server-In-TCP'\r
332 if (Get-NetFirewallRule -Name $ruleName -ErrorAction SilentlyContinue) {\r
333 Set-NetFirewallRule -Name $ruleName -Enabled True -Profile Any\r
334 Write-Host " Widened firewall rule $ruleName to all profiles"\r
335 } else {\r
336 New-NetFirewallRule -Name $ruleName -DisplayName 'OpenSSH SSH Server (sshd)' `\r
337 -Enabled True -Direction Inbound -Protocol TCP -Action Allow `\r
338 -LocalPort 22 -Profile Any | Out-Null\r
339 Write-Host " Added firewall rule $ruleName (TCP 22, all profiles)"\r
340 }\r
341 }\r
342} catch {\r
343 Write-Warning "OpenSSH Server setup failed: $($_.Exception.Message)"\r
344}\r
345\r
346# ---------------------------------------------------------------------------\r
347# rsync for Windows (github.com/nuket/rsync-windows)\r
348#\r
349# Windows' OpenSSH ships the transport only - no rsync - so pushing test data\r
350# from a Linux box needs an rsync.exe on the Windows side.\r
351#\r
b8144088
MV
352# The release is one zip per architecture - rsync-windows-x64.zip and\r
353# rsync-windows-x86.zip - each holding rsync.exe, the ssh.exe it runs, and the\r
354# licence texts under exactly those names. Both exes are installed, together:\r
355# rsync.exe prefers an ssh.exe in its own directory, and the release's build is\r
356# what makes a push FROM this box run at line rate. The ssh.exe Windows ships\r
357# reads its stdin 3KB at a time, which holds a send at ~17MB/s however fast the\r
358# link is. Nothing else about it differs - same ~/.ssh, same ssh-agent, same\r
359# known_hosts - and a bare `ssh` still resolves to the in-box client, which sits\r
360# ahead of C:\Tools\rsync on the machine PATH.\r
361#\r
362# That ssh.exe links against the libcrypto.dll the OpenSSH Client capability\r
363# above puts in System32: Windows' own LibreSSL, and the fast one, since it uses\r
364# AES-NI. No copy of it ships in the zip, so where the capability is missing we\r
365# unpack rsync alone rather than an ssh.exe that will not start.\r
366#\r
452f525c
MV
367# Installed to C:\Tools\rsync (NOT under "Program Files"): the remote end is\r
368# invoked as `rsync --server ...` through cmd.exe, and a path with spaces makes\r
369# the client-side --rsync-path escape hatch painful to quote. Added to the\r
370# MACHINE PATH so it resolves for every account, including the non-interactive\r
371# sshd session, which builds its environment from the machine + user registry\r
372# PATH rather than from a login shell.\r
373#\r
374# Non-fatal: a download failure only warns.\r
375# ---------------------------------------------------------------------------\r
376Write-Step 'rsync for Windows'\r
b8144088
MV
377$RsyncRepo = 'nuket/rsync-windows'\r
378$RsyncAsset = if ([Environment]::Is64BitOperatingSystem) { 'rsync-windows-x64.zip' } else { 'rsync-windows-x86.zip' }\r
379# The /releases/latest/download/ redirect rather than the API: unauthenticated\r
380# API calls are rate-limited to 60/hour per IP, which a provisioning run behind a\r
381# shared NAT can genuinely exhaust, and the redirect costs none of that budget.\r
382# To hold a box on a known build, pin the tag instead:\r
383# .../releases/download/v3.5.0-gABCDEF0/$RsyncAsset\r
384$RsyncUrl = "https://github.com/$RsyncRepo/releases/latest/download/$RsyncAsset"\r
385$RsyncDir = 'C:\Tools\rsync'\r
452f525c
MV
386try {\r
387 New-Item -ItemType Directory -Force -Path $RsyncDir | Out-Null\r
388 $RsyncExe = Join-Path $RsyncDir 'rsync.exe'\r
b8144088
MV
389\r
390 # Does the release's ssh.exe have the libcrypto it needs? Decided before the\r
391 # download so the answer can also gate what comes out of the zip.\r
392 $SysCrypto = Join-Path $env:WINDIR 'System32\libcrypto.dll'\r
393 $WantSsh = Test-Path $SysCrypto\r
394 if (-not $WantSsh) {\r
395 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
396 } else {\r
397 $v = (Get-Item $SysCrypto).VersionInfo.FileVersion\r
398 if ($v -and ([version]($v -replace '[^0-9.]', '')) -lt [version]'3.8.2') {\r
399 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
400 }\r
401 }\r
402\r
403 # Download and unpack beside the targets, not over them, so an interrupted\r
404 # transfer can't leave a truncated rsync.exe sitting on the PATH. The scratch\r
405 # name still has to END in .zip: Windows PowerShell 5.1's Expand-Archive\r
406 # refuses any other extension outright ("*.download is not a supported\r
407 # archive file format"), where PowerShell 7 just reads the file.\r
452f525c 408 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\r
b8144088
MV
409 $tmpZip = Join-Path $RsyncDir "download-$RsyncAsset"\r
410 Invoke-WebRequest -Uri $RsyncUrl -OutFile $tmpZip -UseBasicParsing\r
411 Write-Host " Downloaded $RsyncAsset ($([math]::Round((Get-Item $tmpZip).Length / 1MB, 2)) MB)"\r
412\r
413 # Verify against the .sha256 published beside it. Same origin, so this is an\r
414 # integrity check on the transfer rather than a defence against a hostile\r
415 # release - but a truncated or proxy-mangled download is the failure that\r
416 # actually happens, and it fails here instead of mid-transfer later.\r
417 #\r
418 # -OutFile, not .Content: GitHub serves the .sha256 as\r
419 # application/octet-stream, and Invoke-WebRequest hands back a byte[] rather\r
420 # than a string for any non-text content type, so .Content would compare the\r
421 # first BYTE against the hash and fail on every correct download.\r
422 $tmpSha = "$tmpZip.sha256"\r
423 Invoke-WebRequest -Uri "$RsyncUrl.sha256" -OutFile $tmpSha -UseBasicParsing\r
424 $want = (((Get-Content $tmpSha -Raw) -split '\s+')[0]).Trim().ToLower()\r
425 Remove-Item $tmpSha -Force -ErrorAction SilentlyContinue\r
426 $got = (Get-FileHash $tmpZip -Algorithm SHA256).Hash.ToLower()\r
427 if ($want -and $want -ne $got) {\r
428 Remove-Item $tmpZip -Force\r
429 throw "SHA-256 mismatch for ${RsyncAsset}: expected $want, got $got"\r
430 }\r
431 Write-Host " SHA-256 verified: $got"\r
432\r
433 # Unpack to a scratch directory and move out the files we asked for, rather\r
434 # than expanding straight over the install directory: the zip is the unit\r
435 # that was checksummed, and this way a future release adding something to it\r
436 # cannot quietly drop that something onto the machine PATH.\r
437 $unpack = Join-Path $RsyncDir '.unpack'\r
438 if (Test-Path $unpack) { Remove-Item -Recurse -Force $unpack }\r
439 Expand-Archive -Path $tmpZip -DestinationPath $unpack -Force\r
440 Remove-Item $tmpZip -Force\r
441 foreach ($f in 'rsync.exe', 'ssh.exe', 'COPYING.txt', 'NOTICE-ssh.txt') {\r
442 $src = Join-Path $unpack $f\r
443 if (-not (Test-Path $src)) { continue }\r
444 if ($f -eq 'ssh.exe' -and -not $WantSsh) { continue }\r
445 Move-Item -Path $src -Destination (Join-Path $RsyncDir $f) -Force\r
446 }\r
447 Remove-Item -Recurse -Force $unpack\r
448 Write-Host " Installed $RsyncExe$(if ($WantSsh) { ' and the ssh.exe it runs' })"\r
452f525c
MV
449\r
450 # Machine PATH (HKLM environment). Idempotent: only appends if absent.\r
451 $m = [Environment]::GetEnvironmentVariable('Path', 'Machine')\r
452 if (-not $m) { $m = '' }\r
453 if (($m -split ';') -notcontains $RsyncDir) {\r
454 $new = if ($m.Trim()) { $m.TrimEnd(';') + ';' + $RsyncDir } else { $RsyncDir }\r
455 [Environment]::SetEnvironmentVariable('Path', $new, 'Machine')\r
456 Write-Host " Added $RsyncDir to the machine PATH (restart shells / sshd to pick it up)."\r
457 # sshd caches the environment it was started with, so an already-running\r
458 # service would not see the new PATH until restarted.\r
459 if ((Get-Service sshd -ErrorAction SilentlyContinue).Status -eq 'Running') {\r
460 Restart-Service sshd\r
461 Write-Host ' Restarted sshd so it inherits the updated machine PATH.'\r
462 }\r
463 } else {\r
464 Write-Host " OK: $RsyncDir already in the machine PATH"\r
465 }\r
466\r
467 & $RsyncExe --version | Select-Object -First 1\r
468} catch {\r
469 Write-Warning "rsync install failed: $($_.Exception.Message)"\r
b8144088
MV
470 Write-Warning "Download $RsyncAsset from https://github.com/$RsyncRepo/releases manually"\r
471 Write-Warning "and unpack it into $RsyncDir, keeping rsync.exe and ssh.exe together."\r
452f525c
MV
472}\r
473\r
3c096107
MV
474# ---------------------------------------------------------------------------\r
475# Visual Studio 2022 Community\r
476# ---------------------------------------------------------------------------\r
477$TempDir = Join-Path $env:TEMP 'dev_install'\r
478New-Item -ItemType Directory -Force -Path $TempDir | Out-Null\r
479\r
480# Component IDs split into independent groups so each can be installed in its\r
481# own pass. The base group is the known-good set; Clang and the Windows XP\r
482# toolset are layered on afterwards so a failure clearly identifies the culprit.\r
483# Component reference: https://learn.microsoft.com/visualstudio/install/workload-component-id-vs-community\r
484$BaseComponents = @(\r
485 # Core C++ desktop workload\r
486 'Microsoft.VisualStudio.Workload.NativeDesktop'\r
487\r
488 # Spectre-mitigated MSVC runtime libs\r
489 'Microsoft.VisualStudio.Component.VC.Runtimes.x86.x64.Spectre'\r
490 'Microsoft.VisualStudio.Component.VC.Runtimes.ARM64.Spectre'\r
491\r
492 # Spectre-mitigated ATL (needed for many driver/COM projects)\r
493 'Microsoft.VisualStudio.Component.VC.ATL.Spectre'\r
494\r
495 # Windows 11 SDK — build number must match the WDK below\r
496 'Microsoft.VisualStudio.Component.Windows11SDK.26100'\r
497\r
498 # WDK Visual Studio extension (VSIX). The silent wdksetup.exe /quiet does NOT\r
499 # install this (it only prompts interactively), so it must be added here.\r
500 'Component.Microsoft.Windows.DriverKit'\r
501)\r
502\r
503# Clang/LLVM toolset (ClangCL, used in CMakePresets.json). Two parts: the Clang\r
504# compiler itself, plus the MSBuild integration providing the "ClangCL" toolset.\r
505$ClangComponents = @(\r
506 'Microsoft.VisualStudio.Component.VC.Llvm.Clang'\r
507 'Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset'\r
508)\r
509\r
510# Windows XP targeting (v141_xp toolset, used in CMakePresets.json). The v141\r
511# (VS2017) build tools provide the 14.16 compiler that the XP toolset wraps;\r
512# WinXP layers the XP-compatible CRT/SDK on top of it.\r
513$XpComponents = @(\r
514 'Microsoft.VisualStudio.Component.VC.v141.x86.x64'\r
515 'Microsoft.VisualStudio.Component.WinXP'\r
516)\r
517\r
518# Detect an existing VS install via vswhere (ships with the VS Installer).\r
519# These are referenced by Invoke-VsModify via $script: scope.\r
520$VsWhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'\r
521$InstallPath = $null\r
522if (Test-Path $VsWhere) {\r
523 $InstallPath = & $VsWhere -products '*' -property installationPath -format value |\r
524 Select-Object -First 1\r
525}\r
526\r
527Write-Step 'Downloading VS2022 Community bootstrapper'\r
528$VsInstallerUrl = 'https://aka.ms/vs/17/release/vs_community.exe'\r
529$VsBootstrapper = Join-Path $TempDir 'vs_community.exe'\r
530Invoke-WebRequest -Uri $VsInstallerUrl -OutFile $VsBootstrapper -UseBasicParsing\r
531\r
532# Install in three sequential passes. The base set is installed first (this is\r
533# the configuration that previously worked); Clang and the XP toolset are added\r
534# afterwards. If one fails, its label pinpoints which group is responsible.\r
535Invoke-VsModify -Label 'base toolset + workload' -Ids $BaseComponents\r
536Invoke-VsModify -Label 'Clang / LLVM' -Ids $ClangComponents\r
537Invoke-VsModify -Label 'Windows XP (v141 + WinXP)' -Ids $XpComponents\r
538\r
539# ---------------------------------------------------------------------------\r
540# Verify the v141 / XP toolset actually landed. Earlier runs silently skipped\r
541# it and the failure only surfaced at build time, so check on disk and fail\r
542# loudly here instead.\r
543# ---------------------------------------------------------------------------\r
544Write-Step 'Verifying v141 / XP toolset'\r
545$InstallPath = & $VsWhere -products '*' -property installationPath -format value |\r
546 Select-Object -First 1\r
547$V141 = if ($InstallPath) {\r
548 Get-ChildItem (Join-Path $InstallPath 'VC\Tools\MSVC') -Directory -ErrorAction SilentlyContinue |\r
549 Where-Object { $_.Name -like '14.16.*' } | Select-Object -First 1\r
550}\r
551if ($V141) {\r
552 Write-Host " OK: v141 toolset present ($($V141.Name))" -ForegroundColor Green\r
553} else {\r
554 Write-Warning 'v141 (14.16.x) toolset NOT found - the XP build presets will fail.'\r
555 Write-Warning 'Add it via Visual Studio Installer > Modify > Individual components:'\r
556 Write-Warning ' - MSVC v141 - VS 2017 C++ x64/x86 build tools (v14.16)'\r
557 Write-Warning ' - C++ Windows XP Support for VS 2017 (v141) tools'\r
558}\r
559\r
560# ---------------------------------------------------------------------------\r
561# Windows Driver Kit (WDK 10.0.26100)\r
562# Build 26100 matches the Windows 11 SDK installed above.\r
563# Provides IddCx (iddcx.h / iddcx.lib) and UMDF 2.x for Indirect Display Drivers.\r
564# linkid=2335869 -> WDK 26100.6584 (per Microsoft "Other WDK Downloads").\r
565# ---------------------------------------------------------------------------\r
566$WdkVersion = '10.0.26100'\r
567$WdkInstalledRoot = (Get-ItemProperty 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows Kits\Installed Roots' `\r
568 -ErrorAction SilentlyContinue).WdkBinRootVersioned\r
569\r
570if ($WdkInstalledRoot -and $WdkInstalledRoot -match [regex]::Escape($WdkVersion)) {\r
571 # Re-running wdksetup.exe for an already-present version returns exit code\r
572 # 2008 (maintenance mode / nothing to do), which is not a real failure.\r
573 Write-Step "WDK $WdkVersion already installed - skipping ($WdkInstalledRoot)"\r
574} else {\r
575 Write-Step 'Downloading WDK installer'\r
576 $WdkUrl = 'https://go.microsoft.com/fwlink/?linkid=2335869'\r
577 $WdkInstaller = Join-Path $TempDir 'wdksetup.exe'\r
578 Invoke-WebRequest -Uri $WdkUrl -OutFile $WdkInstaller -UseBasicParsing\r
579\r
580 Write-Step 'Installing WDK'\r
581 $proc = Start-Process -FilePath $WdkInstaller -ArgumentList '/quiet /norestart' -Wait -PassThru -NoNewWindow\r
582 Write-Host " WDK installer exit code: $($proc.ExitCode)"\r
583 if ($proc.ExitCode -eq 2008) {\r
584 # 2008 = the WDK is already present; the installer has nothing to do.\r
585 Write-Host ' [WDK already installed (exit 2008) - treating as success]' -ForegroundColor Yellow\r
586 } else {\r
587 Assert-ExitCode $proc.ExitCode 'WDK'\r
588 }\r
589}\r
590\r
591# ---------------------------------------------------------------------------\r
99725a30
MV
592# Windows Performance Toolkit: xperf, wpr, and Windows Performance Analyzer\r
593# (wpa.exe) -- ETW CPU + loader profiling and the GUI that reads the traces.\r
594#\r
595# WPA is NOT a Visual Studio component and has no relationship to VS's own\r
596# Performance Profiler (a separate, .diagsession-based tool that cannot open an\r
597# .etl). It ships in exactly two places: as an optional FEATURE of the Windows\r
598# SDK ("Windows Performance Toolkit", OptionId.WindowsPerformanceToolkit), and\r
599# in the Windows ADK, which bundles the same toolkit. Whether the SDK install\r
600# that Visual Studio performs happens to select that feature varies with the VS\r
601# and SDK version - when it does, WPT lands in\r
602# %ProgramFiles(x86)%\Windows Kits\10\Windows Performance Toolkit and the SDK\r
603# puts that directory on the machine PATH itself - so this step DETECTS first\r
604# and only falls back to installing the ADK (winget owns the versioned download\r
605# URL, which makes it the reliable source) when nothing is there. That fallback\r
606# is a large download; to install just the toolkit instead, run the standalone\r
607# SDK setup with\r
608# winsdksetup.exe /features OptionId.WindowsPerformanceToolkit /q\r
609#\r
610# There is also a newer WPA in the Microsoft Store (`winget install --id\r
611# 9N0W1B2BXGNZ --source msstore`), which updates independently of the SDK. It is\r
612# not installed here: the Store package needs an interactive, signed-in session,\r
613# which is exactly what this elevated, unattended half does not have.\r
614#\r
615# Idempotent and non-fatal - it never aborts provisioning.\r
3c096107 616# ---------------------------------------------------------------------------\r
99725a30
MV
617Write-Step 'Windows Performance Toolkit (xperf / wpr / WPA)'\r
618$WptDirs = @(\r
619 (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Windows Performance Toolkit'),\r
620 (Join-Path $env:ProgramFiles 'Windows Kits\10\Windows Performance Toolkit'),\r
621 (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Assessment and Deployment Kit\Windows Performance Toolkit')\r
3c096107 622)\r
99725a30
MV
623function Find-WptDir { $script:WptDirs | Where-Object { Test-Path (Join-Path $_ 'xperf.exe') } | Select-Object -First 1 }\r
624\r
625$WptDir = Find-WptDir\r
626if ($WptDir) {\r
627 Write-Host " OK: WPT already present ($WptDir)" -ForegroundColor Green\r
3c096107
MV
628} else {\r
629 try {\r
630 winget install --id Microsoft.WindowsADK --exact --silent --disable-interactivity `\r
631 --accept-source-agreements --accept-package-agreements\r
632 Write-Host ' Windows ADK (includes Windows Performance Toolkit) installed.'\r
99725a30 633 $WptDir = Find-WptDir\r
3c096107
MV
634 } catch {\r
635 Write-Warning "WPT install failed: $($_.Exception.Message)"\r
636 Write-Warning 'Install manually: winget install Microsoft.WindowsADK, or add the'\r
637 Write-Warning 'Windows SDK "Windows Performance Toolkit" optional feature.'\r
638 }\r
639}\r
640\r
99725a30
MV
641if ($WptDir) {\r
642 # Report what actually landed. wpa.exe is the piece people come looking for\r
643 # and it is the one that is absent if a trimmed toolkit ever shows up.\r
644 foreach ($tool in 'xperf.exe', 'wpr.exe', 'wpa.exe', 'wpaexporter.exe') {\r
645 $p = Join-Path $WptDir $tool\r
646 if (Test-Path $p) {\r
647 Write-Host " $tool $((Get-Item $p).VersionInfo.ProductVersion)"\r
648 } else {\r
649 Write-Warning "$tool is missing from $WptDir"\r
650 }\r
651 }\r
652\r
653 # The WPT installer normally adds this to the machine PATH itself (and the\r
654 # Start Menu gets "Windows Kits > Windows Performance Toolkit" shortcuts for\r
655 # WPA and WPR). Re-assert it anyway: on the machine PATH rather than a user\r
656 # one so it also resolves for the non-interactive sshd sessions this box is\r
657 # driven through, which build their environment from the registry PATH.\r
658 # Compared trailing-backslash-insensitively - the installer's own entry has\r
659 # one, and adding a second spelling of the same directory is just noise.\r
660 $m = [Environment]::GetEnvironmentVariable('Path', 'Machine')\r
661 if (-not $m) { $m = '' }\r
662 $have = ($m -split ';') | Where-Object { $_.TrimEnd('\') -eq $WptDir.TrimEnd('\') }\r
663 if ($have) {\r
664 Write-Host " OK: $WptDir already in the machine PATH"\r
665 } else {\r
666 $new = if ($m.Trim()) { $m.TrimEnd(';') + ';' + $WptDir } else { $WptDir }\r
667 [Environment]::SetEnvironmentVariable('Path', $new, 'Machine')\r
668 Write-Host " Added $WptDir to the machine PATH (restart shells to pick it up)."\r
669 }\r
670}\r
671\r
99725a30 672\r
1e91b38f
MV
673# ---------------------------------------------------------------------------\r
674# Intel VTune Profiler - reported, not installed\r
675#\r
676# Deliberately NOT automated, unlike everything above. The offline installer is\r
677# a ~750 MB download from a URL carrying a per-release GUID\r
678# (registrationcenter-download.intel.com/akdlm/IRC_NAS/<guid>/intel-vtune-<ver>_offline.exe)\r
679# with no "latest" redirect behind it, so every new build means editing a\r
680# hard-coded link in here - and it is only worth having on Intel silicon, since\r
681# hardware event-based sampling reads Intel PMU counters. Not a good trade for a\r
682# script that has to keep working unattended on any box.\r
683#\r
684# So this step only reports. To install it, take the Windows offline installer\r
685# from\r
686# https://www.intel.com/content/www/us/en/developer/tools/oneapi/vtune-profiler-download.html\r
687# and run it elevated; it installs unattended with\r
688# intel-vtune-<version>_offline.exe -a --silent --cli --eula accept\r
689# ---------------------------------------------------------------------------\r
690Write-Step 'Intel VTune Profiler (status only)'\r
691$UninstallKeys = @(\r
692 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'\r
693 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'\r
694)\r
695$vtune = Get-ItemProperty $UninstallKeys -ErrorAction SilentlyContinue |\r
696 Where-Object { $_.DisplayName -match 'VTune' } |\r
697 Select-Object -First 1\r
698if ($vtune) {\r
699 Write-Host " Installed: $($vtune.DisplayName.Trim()) $($vtune.DisplayVersion)" -ForegroundColor Green\r
700 # The oneAPI layout keeps a `latest` junction beside the versioned directory,\r
701 # so this path stays right across upgrades.\r
702 $VTuneCli = Join-Path $vtune.InstallLocation 'vtune\latest\bin64\vtune.exe'\r
703 if (Test-Path $VTuneCli) { Write-Host " CLI: $VTuneCli" }\r
704} else {\r
705 Write-Host ' Not installed.' -ForegroundColor Yellow\r
706 Write-Host ' https://www.intel.com/content/www/us/en/developer/tools/oneapi/vtune-profiler-download.html' -ForegroundColor Yellow\r
707 $cpu = (Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1).Manufacturer\r
708 if ($cpu -and $cpu -notmatch 'Intel') {\r
709 Write-Host " (This CPU reports itself as '$cpu' - VTune's hardware event-based sampling wants Intel silicon.)" -ForegroundColor Yellow\r
710 }\r
711}\r
712\r
3c096107
MV
713# ---------------------------------------------------------------------------\r
714Write-Host "`nAll done." -ForegroundColor Green\r
715Write-Host 'If a reboot was flagged above, restart before opening VS or building drivers.'\r
716\r
717}\r
718catch {\r
719 $ExitCode = 1\r
720 Write-Host "`n==> SETUP FAILED: $($_.Exception.Message)" -ForegroundColor Red\r
721 if ($_.ScriptStackTrace) { Write-Host $_.ScriptStackTrace -ForegroundColor DarkGray }\r
722 # Only fold in the VS Installer logs when a VS step actually failed; for other\r
723 # steps (e.g. WDK) those logs are stale and misleading, so the message above\r
724 # is what matters.\r
725 if ($_.Exception.Message -match 'VS2022') {\r
726 try { Show-VsSetupLogs } catch {}\r
727 }\r
728}\r
729finally {\r
730 try { Stop-Transcript | Out-Null } catch {}\r
731\r
732 # This log was created by the elevated (admin) process, so by default the\r
733 # non-elevated caller can't delete it (their token has Administrators marked\r
734 # deny-only). Grant BUILTIN\Users Modify rights so the user account that runs\r
735 # setup-windows.bat can remove the log later. S-1-5-32-545 is the well-known\r
736 # Users SID, used here so this is locale-independent.\r
737 try {\r
738 if (Test-Path $LogFile) {\r
739 $usersSid = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-545')\r
740 $acl = Get-Acl -Path $LogFile\r
741 $rule = New-Object System.Security.AccessControl.FileSystemAccessRule(\r
742 $usersSid, 'Modify', 'Allow')\r
743 $acl.AddAccessRule($rule)\r
744 Set-Acl -Path $LogFile -AclObject $acl\r
745 }\r
746 } catch {\r
747 Write-Host " [warning] could not relax ACL on $LogFile : $($_.Exception.Message)" -ForegroundColor Yellow\r
748 }\r
749}\r
750\r
751exit $ExitCode\r