1 #Requires -RunAsAdministrator
\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
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
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
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
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
40 $ErrorActionPreference = 'Stop'
\r
42 # ---------------------------------------------------------------------------
\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
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
70 function Write-Step([string]$Msg) {
\r
71 Write-Host "`n==> $Msg" -ForegroundColor Cyan
\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
79 if ($Code -eq 3010) {
\r
80 Write-Host " [reboot required after $Step]" -ForegroundColor Yellow
\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
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
99 Write-Host ' (no VS Installer logs modified in the last 15 minutes)' -ForegroundColor Yellow
\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
108 function Get-VsInstallPath {
\r
109 # The install path of a Visual Studio matching $script:VsChannel, or $null.
\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
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
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
138 Write-Step "Visual Studio: $Label"
\r
140 Write-Host ' (no components in this group for this architecture - skipping)' -ForegroundColor DarkGray
\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
151 # No existing install yet -> this first pass performs the base install.
\r
152 $argString = "$addStr $common"
\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
160 Assert-ExitCode $p.ExitCode "Visual Studio ($Label)"
\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
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
176 try { Start-Transcript -Path $LogFile -Force | Out-Null } catch {}
\r
180 Write-Step "Host architecture: $HostArch"
\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
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
196 # ---------------------------------------------------------------------------
\r
197 # Base tools via winget
\r
198 # ---------------------------------------------------------------------------
\r
200 # ---------------------------------------------------------------------------
\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
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
214 $sshc = Get-WindowsCapability -Online -Name 'OpenSSH.Client*' |
\r
215 Select-Object -First 1
\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
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
226 Write-Warning "OpenSSH Client setup failed: $($_.Exception.Message)"
\r
229 # ---------------------------------------------------------------------------
\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
236 # ---------------------------------------------------------------------------
\r
237 # OpenSSH Server (sshd)
\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
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
246 # Non-fatal: a box that can't run sshd should still finish provisioning.
\r
247 # ---------------------------------------------------------------------------
\r
248 Write-Step 'OpenSSH Server (sshd)'
\r
250 $sshd = Get-WindowsCapability -Online -Name 'OpenSSH.Server*' |
\r
251 Select-Object -First 1
\r
253 Write-Warning 'OpenSSH.Server capability not offered by this Windows image - skipping.'
\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
260 Write-Host " OK: $($sshd.Name) already installed"
\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
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
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
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
289 Write-Warning "OpenSSH Server setup failed: $($_.Exception.Message)"
\r
292 # ---------------------------------------------------------------------------
\r
293 # rsync for Windows (github.com/nuket/rsync-windows)
\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
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
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
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
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
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
340 Write-Host ' ARM64: using the native arm64 asset (rsync.exe and ssh.exe both ARM64).' -ForegroundColor Green
\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
350 New-Item -ItemType Directory -Force -Path $RsyncDir | Out-Null
\r
351 $RsyncExe = Join-Path $RsyncDir 'rsync.exe'
\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
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
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
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
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
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
401 Write-Host " SHA-256 verified: $got"
\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
417 Remove-Item -Recurse -Force $unpack
\r
419 # Prove the unpacked ssh.exe actually starts, and DELETE it if it does not.
\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
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
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
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
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
456 Write-Host " Installed $RsyncExe$(if ($WantSsh) { ' and the ssh.exe it runs' })"
\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
472 Write-Host " OK: $RsyncDir already in the machine PATH"
\r
475 & $RsyncExe --version | Select-Object -First 1
\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
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
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
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
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
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
517 # Windows 11 SDK — build number must match the WDK below
\r
518 'Microsoft.VisualStudio.Component.Windows11SDK.26100'
\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
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
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
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
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
548 'Microsoft.VisualStudio.Component.VC.Llvm.Clang'
\r
549 'Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset'
\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
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
567 'Microsoft.VisualStudio.Component.VC.v141.x86.x64'
\r
568 'Microsoft.VisualStudio.Component.WinXP'
\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
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
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
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
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
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
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
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
636 Write-Host " OK: bootstrapper is a PE ($([math]::Round((Get-Item $VsBootstrapper).Length / 1MB, 2)) MB)"
\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
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
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
653 Invoke-VsModify -Label 'Clang / LLVM' -Ids $ClangComponents -Optional
\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
661 Invoke-VsModify -Label 'Windows XP (v141 + WinXP)' -Ids $XpComponents -Optional
\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
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
682 Write-Host " Install path: $InstallPath"
\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
694 $armHost = Test-Path (Join-Path $msvcRoot '*\bin\HostARM64\ARM64\cl.exe')
\r
696 Write-Host ' OK: native ARM64-hosted cl.exe present.' -ForegroundColor Green
\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
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
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
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
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
726 Write-Host ' clang-cl: n/a on ARM64 (MSVC only by design; use the x64 box for ClangCL).' -ForegroundColor DarkGray
\r
728 } elseif ($clangAny) {
\r
729 Write-Host " clang-cl: $(@(if ($clangArm) {'ARM64'}; if ($clangX64) {'x64'}; if ($clangX86) {'x86'}) -join ', ')" -ForegroundColor Green
\r
731 Write-Warning 'clang-cl not found - the ClangCL presets will fail. Add the "C++ Clang tools for Windows" component.'
\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
738 Write-Host ' v141 / Windows XP toolset: n/a on ARM64 (not offered for Arm hosts).' -ForegroundColor DarkGray
\r
740 $V141 = Get-ChildItem $msvcRoot -Directory -ErrorAction SilentlyContinue |
\r
741 Where-Object { $_.Name -like '14.16.*' } | Select-Object -First 1
\r
743 Write-Host " OK: v141 toolset present ($($V141.Name))" -ForegroundColor Green
\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
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
759 # x64 ONLY. Two reasons, and the second is the one that decided it:
\r
761 # 1. The ARM64 box builds user-mode software; no driver is developed on it.
\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
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
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
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
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
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
818 Assert-ExitCode $proc.ExitCode 'WDK'
\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
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
839 # winsdksetup.exe /features OptionId.WindowsPerformanceToolkit /q
\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
846 # Idempotent and non-fatal - it never aborts provisioning.
\r
847 # ---------------------------------------------------------------------------
\r
848 Write-Step 'Windows Performance Toolkit (xperf / wpr / WPA)'
\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
854 function Find-WptDir { $script:WptDirs | Where-Object { Test-Path (Join-Path $_ 'xperf.exe') } | Select-Object -First 1 }
\r
856 $WptDir = Find-WptDir
\r
858 Write-Host " OK: WPT already present ($WptDir)" -ForegroundColor Green
\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
867 Write-Host ' ARM64: the ADK installer is x64 (emulated); the toolkit it installs is ARM64.' -ForegroundColor Yellow
\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
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
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
888 Write-Warning "$tool is missing from $WptDir"
\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
903 Write-Host " OK: $WptDir already in the machine PATH"
\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
912 # ---------------------------------------------------------------------------
\r
913 # Intel VTune Profiler - reported, not installed
\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
923 # So this step only reports. To install it, take the Windows offline installer
\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
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
940 $UninstallKeys = @(
\r
941 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
\r
942 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
\r
944 $vtune = Get-ItemProperty $UninstallKeys -ErrorAction SilentlyContinue |
\r
945 Where-Object { $_.DisplayName -match 'VTune' } |
\r
946 Select-Object -First 1
\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
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
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
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
976 if ($_.Exception.Message -match 'Visual Studio') {
\r
977 try { Show-VsSetupLogs } catch {}
\r
981 try { Stop-Transcript | Out-Null } catch {}
\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
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
998 Write-Host " [warning] could not relax ACL on $LogFile : $($_.Exception.Message)" -ForegroundColor Yellow
\r