#Requires -RunAsAdministrator <# setup-windows-with-uac.ps1 Elevated portion of the Windows provisioning. Invoked by setup-windows.bat via Start-Process -Verb RunAs, or run manually from an Administrator prompt. Its steps are the 'uac' rows of setup-windows-steps.ps1. The batch file runs it with -Only -Yes, having already asked. Run by hand, it lists what it will do and asks before doing any of it: .\setup-windows-with-uac.ps1 all of it, after asking .\setup-windows-with-uac.ps1 -Only VisualStudio,Wdk just these .\setup-windows-with-uac.ps1 -Skip VTune -Yes without asking What this installs / configures: - OpenSSH Client capability (the ssh.exe rsync shells out to, and the System32 libcrypto.dll the release's own ssh.exe links against) - ssh-agent set to automatic + started - OpenSSH Server (sshd) capability: automatic + started + inbound TCP 22 - rsync for Windows (nuket/rsync-windows) in C:\Tools\rsync, on the machine PATH: rsync.exe plus the ssh.exe it runs, out of the release zip for this architecture - x86, x64 and arm64 are all published, so both binaries are native everywhere. The ssh.exe is kept only if it actually starts. - Visual Studio Community (C++ desktop workload, x86/x64 AND ARM64 build tools, Spectre libs, Win11 SDK 26100, and - on x64 only - the WDK VSIX, Clang/LLVM, and the v141 + Windows XP targeting toolset). VS 2022 on x64, VS 2026 on ARM64; see $VsChannel. - Windows Driver Kit 10.0.26100 (x64 only) - Windows Performance Toolkit - xperf, wpr and Windows Performance Analyzer (wpa.exe) - on the machine PATH Change $VsEdition below to Professional or Enterprise if needed. $VsChannel picks the Visual Studio generation and is architecture-split by default: 17 (VS 2022) on x64, where the v141 / Windows XP toolset is wanted, and 18 (VS 2026) on ARM64, where that toolset cannot exist anyway. ARCHITECTURE. Runs on x64 and on ARM64 (Windows 11 on Arm). On ARM64 the Visual Studio installer and MSVC are native ARM64 and cross-compile ARM64/x64/x86 targets. The two architectures deliberately provision DIFFERENT things. x64 is the full workstation; ARM64 is a constrained VM set up as a single-compiler build box - MSVC only, with clang-cl and the WDK dropped there. Neither is dropped for lack of an ARM64 build; see the comments on $ClangComponents and the WDK step. What changes is called out at each step, and $HostArch below is what drives it. #> [CmdletBinding()] param( # Step names from setup-windows-steps.ps1, comma-separated. `powershell -File` # hands "A,B" to an array parameter as one string; it is split, so both work. [string[]] $Only, [string[]] $Skip = @(), # Do not ask first. setup-windows.bat passes it, having asked already. [switch] $Yes ) $ErrorActionPreference = 'Stop' # --------------------------------------------------------------------------- # Host architecture # # Read from the machine environment in the registry, because this script can be # launched by a 32-bit or an emulated x64 PowerShell and BOTH of the obvious # sources report the emulated architecture in that case. Measured on this box, # from the x64 PowerShell an x64 shell resolves to: # [RuntimeInformation]::OSArchitecture X64 <- wrong # $env:PROCESSOR_ARCHITECTURE AMD64 <- wrong # HKLM\...\Session Manager\Environment ARM64 <- right # OSArchitecture is documented as the OS's architecture, and on .NET Core it is; # under .NET Framework on Prism it is not, so it is kept only as a fallback. # # $IsArm64 gates: # - which steps run at all. VsClang, VsWinXP and Wdk are x64-only, and that is # decided in setup-windows-steps.ps1 (-X64Only, with the reason the plan # shows); the comments at each step here carry the full reasoning. In short: # the v141/XP toolset has no ARM64-hosted compiler and Microsoft does not # ship Windows XP targeting for Arm hosts; Clang/LLVM and the WDK are out by # policy rather than by limitation - both are available and native on Arm # - which rsync-windows release zip is fetched (x86 / x64 / arm64, all native) # - which Visual Studio generation is driven ($VsChannel: 18 on Arm, 17 on x64) # - the base Visual Studio components: ARM64 build tools in, the WDK VSIX out # - what the verification, VTune and WPT steps report # --------------------------------------------------------------------------- $rawArch = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -ErrorAction SilentlyContinue).PROCESSOR_ARCHITECTURE if (-not $rawArch) { $rawArch = [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() } $IsArm64 = ($rawArch -eq 'ARM64') $HostArch = if ($IsArm64) { 'Arm64' } elseif ($rawArch -eq 'AMD64') { 'X64' } else { $rawArch } function Write-Step([string]$Msg) { Write-Host "`n==> $Msg" -ForegroundColor Cyan } function Assert-ExitCode([int]$Code, [string]$Step) { # 0 = success, 3010 = success + reboot required if ($Code -notin @(0, 3010)) { throw "$Step failed with exit code $Code" } if ($Code -eq 3010) { Write-Host " [reboot required after $Step]" -ForegroundColor Yellow } } function Show-VsSetupLogs { # The VS Installer writes dd_*.log to the invoking user's %TEMP%. Because # this script runs elevated, that %TEMP% belongs to the elevated user and is # readable here even when it is NOT readable by the non-elevated caller. Fold # only the NEWEST installer + bootstrapper log into the transcript (the setup # engine log is where per-component / product errors actually appear) and # keep it short so the transcript stays readable. Write-Host "`n==> Collecting VS Installer logs from $env:TEMP" -ForegroundColor Cyan $recent = Get-ChildItem $env:TEMP -Filter 'dd_*.log' -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -gt (Get-Date).AddMinutes(-15) } $picks = @() $picks += $recent | Where-Object { $_.Name -like 'dd_installer_*' } | Sort-Object LastWriteTime | Select-Object -Last 1 $picks += $recent | Where-Object { $_.Name -like 'dd_bootstrapper_*' } | Sort-Object LastWriteTime | Select-Object -Last 1 $picks = $picks | Where-Object { $_ } if (-not $picks) { Write-Host ' (no VS Installer logs modified in the last 15 minutes)' -ForegroundColor Yellow return } foreach ($l in $picks) { Write-Host "`n----- $($l.Name) (tail) -----" -ForegroundColor Yellow Get-Content $l.FullName -Tail 40 } } function Get-VsInstallPath { # The install path of a Visual Studio matching $script:VsChannel, or $null. # # -version is the point. A bare `vswhere -products *` returns EVERY Visual # Studio on the box, newest first, and handing that path to a bootstrapper of # a different generation is not a no-op: `vs_community.exe` (17.x) told to # `modify --installPath ` is a 17.x engine pointed at an # 18.x product. On a box that already has VS 2026 - increasingly the default - # every pass below would target the wrong install. Scope the query to the # generation this script is actually driving. if (-not (Test-Path $script:VsWhere)) { return $null } $range = "[$script:VsChannel.0,$($script:VsChannel + 1).0)" & $script:VsWhere -products '*' -version $range -property installationPath -format value | Select-Object -First 1 } function Test-VsHasComponents([string[]] $Ids) { # True when the Visual Studio this script drives already has every one of # these workload / component ids. vswhere's -requires matches only an # instance that has ALL of them (checked: one id it does not have, and the # instance is not returned), and it is scoped to $VsChannel the same way # Get-VsInstallPath is, so the answer is about the install the passes target. if (-not $Ids -or -not $script:InstallPath -or -not (Test-Path $script:VsWhere)) { return $false } $range = "[$script:VsChannel.0,$($script:VsChannel + 1).0)" $found = & $script:VsWhere -products '*' -version $range -requires $Ids -property installationPath -format value @($found) -contains $script:InstallPath } function Invoke-VsModify { # Run one VS install/modify pass for a named group of components. Splitting # the install into separate passes makes it obvious WHICH group fails: each # call prints its label and exit code before Assert-ExitCode throws. # # -Optional downgrades a failure to a warning. Used for groups that are not # available on every host architecture (the v141/XP toolset on ARM64) or that # the rest of the box does not depend on, so one unavailable component cannot # cost you the toolchain. param( [string] $Label, [string[]] $Ids, [switch] $Optional ) Write-Step "Visual Studio: $Label" if (-not $Ids) { Write-Host ' (no components in this group for this architecture - skipping)' -ForegroundColor DarkGray return } # Everything already there: running the installer would change nothing and # cost minutes. This script never UPDATES Visual Studio - `modify` adds # components, it does not move the product version - so skipping loses # nothing. Updates come from the Visual Studio Installer, as always. if (Test-VsHasComponents $Ids) { Write-Host " All $($Ids.Count) components already installed in $script:InstallPath; not running the installer." return } $addStr = ($Ids | ForEach-Object { "--add $_" }) -join ' ' # --installPath must be quoted: it contains spaces ("C:\Program Files\..."). # Windows PowerShell 5.1's Start-Process does not quote array elements, so we # hand-build a single string. Component IDs / flags have no spaces. $common = '--includeRecommended --quiet --norestart --wait' if ($script:InstallPath) { $argString = "modify --installPath `"$script:InstallPath`" $addStr $common --force" } else { # No existing install yet -> this first pass performs the base install. $argString = "$addStr $common" } Write-Host " > $script:VsBootstrapper $argString" -ForegroundColor DarkGray $p = Start-Process -FilePath $script:VsBootstrapper -ArgumentList $argString -Wait -PassThru -NoNewWindow Write-Host " exit code: $($p.ExitCode)" if ($Optional -and $p.ExitCode -notin @(0, 3010)) { Write-Warning "Visual Studio ($Label) failed with exit code $($p.ExitCode); continuing (this group is optional)." } else { Assert-ExitCode $p.ExitCode "Visual Studio ($Label)" } # After the first (fresh) install, re-detect the install path so subsequent # passes use `modify`. if (-not $script:InstallPath) { $script:InstallPath = Get-VsInstallPath } } # --------------------------------------------------------------------------- # Which steps to take # # The steps, and which of them cannot run on this machine, come from the 'uac' # rows of setup-windows-steps.ps1 - the same catalog setup-windows.bat builds its # plan from. The plan is printed first, inside the transcript, so the log the # batch file shows afterwards starts with what this half was asked to do. # # Every section below is gated on Test-SetupStep with its catalog Id, which also # records that it was asked. Both kinds of disagreement with the catalog are # loud: an Id the catalog does not have throws where it is used, and a selected # step that no section ever asks about fails the run at the end rather than # letting "All done." cover a step that was never taken. # --------------------------------------------------------------------------- . (Join-Path $PSScriptRoot 'setup-windows-steps.ps1') # Elevation is a given here (#Requires above), so nothing is blocked on it. $Plan = @(Resolve-SetupSelection -Steps @(Get-SetupCatalog -IsArm64 $IsArm64 | Where-Object Runner -eq 'uac') ` -Only $(if ($PSBoundParameters.ContainsKey('Only')) { $Only } else { $null }) -Skip $Skip ` -IsArm64 $IsArm64 -Elevation 'ALREADY') $StepIds = @($Plan | ForEach-Object { $_.Id }) $RunSteps = @($Plan | Where-Object Selected | ForEach-Object { $_.Id }) $AskedSteps = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) function Test-SetupStep([string] $Id) { if ($script:StepIds -notcontains $Id) { throw "Test-SetupStep: '$Id' is not a setup-windows-with-uac.ps1 step in setup-windows-steps.ps1." } [void]$script:AskedSteps.Add($Id) $script:RunSteps -contains $Id } # --------------------------------------------------------------------------- # This runs in a separate elevated window that closes the moment it exits, so # the non-elevated caller (setup-windows.bat) can't see what happened. Mirror # all output to a log next to the script and exit with a real code so the # caller can detect success/failure and show the log. # --------------------------------------------------------------------------- $LogFile = Join-Path $PSScriptRoot 'setup-windows-uac.log' $ExitCode = 0 try { Start-Transcript -Path $LogFile -Force | Out-Null } catch {} try { Write-SetupPlan $Plan "Plan for setup-windows-with-uac.ps1 (host: $HostArch)" if (-not $RunSteps) { Write-Host ' Nothing selected, so there is nothing to do.' exit 0 } # From the batch file, -Yes: the question was asked before anything ran. By # hand, ask now. Either way `exit` here still runs the finally below, so the # transcript is closed and its ACL relaxed. $ok = Confirm-SetupPlan -Yes:$Yes -Count $RunSteps.Count if ($ok -ne $true) { exit $(if ($null -eq $ok) { 2 } else { 0 }) } Write-Step "Host architecture: $HostArch" if ($IsArm64) { # x64 emulation ("Prism") is what carries every x64-only tool this script # installs - rsync.exe, the WDK and ADK installers, BinSkim in the # non-elevated half. It is present on Windows 11 on Arm and absent on # Windows 10 on Arm (x86-only there) and on some Server images, so check # rather than assume: without it those steps install binaries that cannot # start, and the failure would otherwise surface much later. $Prism = Join-Path $env:WINDIR 'System32\xtajit64.dll' if (Test-Path $Prism) { Write-Host ' x64 emulation (Prism) present - x64-only tools will run.' -ForegroundColor Green } else { 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." } } # --------------------------------------------------------------------------- # Base tools via winget # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # OpenSSH Client # # Present by default on Windows 10 1809+ / Windows 11, but removable, and absent # from some Server images. Two things below want it: rsync does not speak ssh # itself, it execs an ssh binary, and the release's own ssh.exe links against the # libcrypto.dll this capability puts in System32. It also owns the ssh-agent # service configured next, so a missing client is why that step would fail. # # Non-fatal, like the server half below: a box that cannot have it should still # finish provisioning. # --------------------------------------------------------------------------- if (Test-SetupStep 'OpenSshClient') { Write-Step 'OpenSSH Client' try { $sshc = Get-WindowsCapability -Online -Name 'OpenSSH.Client*' | Select-Object -First 1 if (-not $sshc) { Write-Warning 'OpenSSH.Client capability not offered by this Windows image - skipping.' } elseif ($sshc.State -eq 'Installed') { Write-Host " OK: $($sshc.Name) already installed" } else { Write-Host " Installing $($sshc.Name) ..." $r = Add-WindowsCapability -Online -Name $sshc.Name if ($r.RestartNeeded) { Write-Host ' [reboot required after OpenSSH Client]' -ForegroundColor Yellow } } } catch { Write-Warning "OpenSSH Client setup failed: $($_.Exception.Message)" } } # --------------------------------------------------------------------------- # SSH agent # --------------------------------------------------------------------------- if (Test-SetupStep 'SshAgent') { Write-Step 'Enabling ssh-agent' Set-Service -Name ssh-agent -StartupType Automatic if ((Get-Service ssh-agent).Status -ne 'Running') { Start-Service ssh-agent } } # --------------------------------------------------------------------------- # OpenSSH Server (sshd) # # Used to reach the test VMs (VirtualBox) from the host: remote shell plus the # transport rsync rides on when seeding test data in. Ships with Windows 10 # 1809+ / Windows 11 as an on-demand capability, so no third-party install. # # The capability normally adds the "OpenSSH Server (sshd)" inbound firewall # rule; we verify and create it if missing (it is absent on some images). # # Non-fatal: a box that can't run sshd should still finish provisioning. # --------------------------------------------------------------------------- if (Test-SetupStep 'OpenSshServer') { Write-Step 'OpenSSH Server (sshd)' try { $sshd = Get-WindowsCapability -Online -Name 'OpenSSH.Server*' | Select-Object -First 1 if (-not $sshd) { Write-Warning 'OpenSSH.Server capability not offered by this Windows image - skipping.' } else { if ($sshd.State -ne 'Installed') { Write-Host " Installing $($sshd.Name) ..." $r = Add-WindowsCapability -Online -Name $sshd.Name if ($r.RestartNeeded) { Write-Host ' [reboot required after OpenSSH Server]' -ForegroundColor Yellow } } else { Write-Host " OK: $($sshd.Name) already installed" } Set-Service -Name sshd -StartupType Automatic if ((Get-Service sshd).Status -ne 'Running') { Start-Service sshd } Write-Host ' sshd: Automatic + running' # Firewall: allow inbound 22 on all profiles. VirtualBox host-only and # bridged adapters are frequently classified Public, and the capability's # own rule is Private-only on some images, which is what leaves a plainly # running sshd plainly unreachable. # # OpenSSH-Server-In-TCP is the name the capability itself uses, so this # WIDENS that rule rather than adding a second one next to it. Creating # our own under a different name would leave the narrow rule in place and # the box still unreachable on a Public-classified adapter; creating one # under the same name would collide. Adopt it if present, create it if not. $ruleName = 'OpenSSH-Server-In-TCP' if (Get-NetFirewallRule -Name $ruleName -ErrorAction SilentlyContinue) { Set-NetFirewallRule -Name $ruleName -Enabled True -Profile Any Write-Host " Widened firewall rule $ruleName to all profiles" } else { New-NetFirewallRule -Name $ruleName -DisplayName 'OpenSSH SSH Server (sshd)' ` -Enabled True -Direction Inbound -Protocol TCP -Action Allow ` -LocalPort 22 -Profile Any | Out-Null Write-Host " Added firewall rule $ruleName (TCP 22, all profiles)" } } } catch { Write-Warning "OpenSSH Server setup failed: $($_.Exception.Message)" } } # --------------------------------------------------------------------------- # rsync for Windows (github.com/nuket/rsync-windows) # # Windows' OpenSSH ships the transport only - no rsync - so pushing test data # from a Linux box needs an rsync.exe on the Windows side. # # The release is one zip per architecture - rsync-windows-x64.zip and # rsync-windows-x86.zip - each holding rsync.exe, the ssh.exe it runs, and the # licence texts under exactly those names. Both exes are installed, together: # rsync.exe prefers an ssh.exe in its own directory, and the release's build is # what makes a push FROM this box run at line rate. The ssh.exe Windows ships # reads its stdin 3KB at a time, which holds a send at ~17MB/s however fast the # link is. Nothing else about it differs - same ~/.ssh, same ssh-agent, same # known_hosts - and a bare `ssh` still resolves to the in-box client, which sits # ahead of C:\Tools\rsync on the machine PATH. # # That ssh.exe links against the libcrypto.dll the OpenSSH Client capability # above puts in System32: Windows' own LibreSSL, and the fast one, since it uses # AES-NI. No copy of it ships in the zip, so where the capability is missing we # unpack rsync alone rather than an ssh.exe that will not start. # # Installed to C:\Tools\rsync (NOT under "Program Files"): the remote end is # invoked as `rsync --server ...` through cmd.exe, and a path with spaces makes # the client-side --rsync-path escape hatch painful to quote. Added to the # MACHINE PATH so it resolves for every account, including the non-interactive # sshd session, which builds its environment from the machine + user registry # PATH rather than from a login shell. # # Non-fatal: a download failure only warns. # --------------------------------------------------------------------------- if (Test-SetupStep 'Rsync') { Write-Step 'rsync for Windows' $RsyncRepo = 'nuket/rsync-windows' # The release publishes x86, x64 and - since v3.5.0-gdeeda96f - arm64, so every # architecture this script runs on gets a native build. ARM64 used to take the # x64 zip under emulation, which was defensible (a transfer is bound by the # socket, not by emulated CPU) but cost the release's ssh.exe: that x64 binary # could not load the ARM64 libcrypto.dll in System32 and had to be deleted on # every run. The arm64 asset ships an ARM64 rsync.exe AND an ARM64 ssh.exe, so # both halves are now native and the fast client survives. # # Selected off $HostArch rather than Is64BitOperatingSystem, which answers "true" # on ARM64 and so cannot tell the two 64-bit cases apart. $RsyncAsset = switch ($HostArch) { 'X64' { 'rsync-windows-x64.zip' } 'Arm64' { 'rsync-windows-arm64.zip' } default { 'rsync-windows-x86.zip' } } if ($IsArm64) { Write-Host ' ARM64: using the native arm64 asset (rsync.exe and ssh.exe both ARM64).' -ForegroundColor Green } # The /releases/latest/download/ redirect rather than the API: unauthenticated # API calls are rate-limited to 60/hour per IP, which a provisioning run behind a # shared NAT can genuinely exhaust, and the redirect costs none of that budget. # To hold a box on a known build, pin the tag instead: # .../releases/download/v3.5.0-gABCDEF0/$RsyncAsset $RsyncUrl = "https://github.com/$RsyncRepo/releases/latest/download/$RsyncAsset" $RsyncDir = 'C:\Tools\rsync' try { New-Item -ItemType Directory -Force -Path $RsyncDir | Out-Null $RsyncExe = Join-Path $RsyncDir 'rsync.exe' # Does the release's ssh.exe have the libcrypto it needs? Decided before the # download so the answer can also gate what comes out of the zip. This is a # cheap pre-filter only - the authoritative check is running the thing, which # happens after the unpack below. $SysCrypto = Join-Path $env:WINDIR 'System32\libcrypto.dll' $WantSsh = Test-Path $SysCrypto if (-not $WantSsh) { 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." } else { $v = (Get-Item $SysCrypto).VersionInfo.FileVersion if ($v -and ([version]($v -replace '[^0-9.]', '')) -lt [version]'3.8.2') { 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." } # No architecture special-case here any more: the asset chosen above # matches the host, so the release's ssh.exe and System32's libcrypto.dll # are the same architecture on every box. The run-it check below still # happens on all of them - it is cheap, and it is what caught the ARM64 # mismatch back when this took the x64 zip. } # Download and unpack beside the targets, not over them, so an interrupted # transfer can't leave a truncated rsync.exe sitting on the PATH. The scratch # name still has to END in .zip: Windows PowerShell 5.1's Expand-Archive # refuses any other extension outright ("*.download is not a supported # archive file format"), where PowerShell 7 just reads the file. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $tmpZip = Join-Path $RsyncDir "download-$RsyncAsset" # The .sha256 published beside the zip is fetched FIRST, for two jobs. It # verifies the download below. Same origin, so that is an integrity check on # the transfer rather than a defence against a hostile release - but a # truncated or proxy-mangled download is the failure that actually happens, # and it fails here instead of mid-transfer later. # # And it says whether there is anything new to fetch at all. The hash of the # zip the installed files came from is recorded beside them in # rsync-windows.sha256, written only once they have all landed (as BinSkim's # nupkg-version.txt is). When the release's hash still matches and the files # are all there, the download and unpack are skipped - a re-run costs one # small request instead of the zip. An ssh.exe that was wanted but is gone # (removed below for not starting) does NOT count as there, so each run # tries it again: whatever stopped it starting may have been fixed since. # # -OutFile, not .Content: GitHub serves the .sha256 as # application/octet-stream, and Invoke-WebRequest hands back a byte[] rather # than a string for any non-text content type, so .Content would compare the # first BYTE against the hash and fail on every correct download. $tmpSha = "$tmpZip.sha256" Invoke-WebRequest -Uri "$RsyncUrl.sha256" -OutFile $tmpSha -UseBasicParsing $want = (((Get-Content $tmpSha -Raw) -split '\s+')[0]).Trim().ToLower() Remove-Item $tmpSha -Force -ErrorAction SilentlyContinue $RsyncMarker = Join-Path $RsyncDir 'rsync-windows.sha256' $InstalledSha = if (Test-Path $RsyncMarker) { (Get-Content $RsyncMarker -Raw).Trim().ToLower() } else { '' } $RsyncCurrent = $want -and ($InstalledSha -eq $want) -and (Test-Path $RsyncExe) -and (-not $WantSsh -or (Test-Path (Join-Path $RsyncDir 'ssh.exe'))) if ($RsyncCurrent) { Write-Host " $RsyncAsset unchanged since it was installed (SHA-256 $want); not downloading it again." } else { Invoke-WebRequest -Uri $RsyncUrl -OutFile $tmpZip -UseBasicParsing Write-Host " Downloaded $RsyncAsset ($([math]::Round((Get-Item $tmpZip).Length / 1MB, 2)) MB)" $got = (Get-FileHash $tmpZip -Algorithm SHA256).Hash.ToLower() if ($want -and $want -ne $got) { Remove-Item $tmpZip -Force throw "SHA-256 mismatch for ${RsyncAsset}: expected $want, got $got" } Write-Host " SHA-256 verified: $got" # Unpack to a scratch directory and move out the files we asked for, rather # than expanding straight over the install directory: the zip is the unit # that was checksummed, and this way a future release adding something to it # cannot quietly drop that something onto the machine PATH. $unpack = Join-Path $RsyncDir '.unpack' if (Test-Path $unpack) { Remove-Item -Recurse -Force $unpack } Expand-Archive -Path $tmpZip -DestinationPath $unpack -Force Remove-Item $tmpZip -Force foreach ($f in 'rsync.exe', 'ssh.exe', 'COPYING.txt', 'NOTICE-ssh.txt') { $src = Join-Path $unpack $f if (-not (Test-Path $src)) { continue } if ($f -eq 'ssh.exe' -and -not $WantSsh) { continue } Move-Item -Path $src -Destination (Join-Path $RsyncDir $f) -Force } Remove-Item -Recurse -Force $unpack # Written last: the marker must only claim a release that fully landed. if ($want) { Set-Content -Path $RsyncMarker -Value $want -Encoding ascii } } # Prove the unpacked ssh.exe actually starts, and DELETE it if it does not. # # This matters more than it looks. rsync.exe prefers an ssh.exe sitting in its # own directory, so a present-but-unstartable one does not degrade to the # in-box client - it breaks rsync outright, and the error you get is a remote # shell that died rather than anything naming ssh.exe. The way to land there # is a missing or too-old System32 libcrypto.dll. Removing it is the repair: # rsync then falls back to the ssh on the PATH, which is the in-box client. # # This also used to fire on every ARM64 run, when the x64 asset was the only # 64-bit one published and its ssh.exe could not load the ARM64 libcrypto # (exit 0xC0000135, STATUS_DLL_NOT_FOUND). The arm64 asset fixed that at the # source; the check stays because it is how that was found in the first place. # # EAP back to Continue for the call: ssh -V writes its version to STDERR, and # under $ErrorActionPreference = 'Stop' a native command's stderr becomes a # terminating error, so a WORKING client would look like a broken one. # $global:LASTEXITCODE is cleared first because an exe that cannot start at # all throws without setting one, and the stale 0 from the previous native # command would otherwise read as success. # # DRAIN THE OUTPUT BEFORE READING $LASTEXITCODE. `... | Select-Object -First 1` # was the original spelling here, and it is a trap: Select-Object stops the # pipeline the instant it has its one object, and that is when ssh.exe has # WRITTEN its banner - not when it has exited. PowerShell then tears the # pipeline down, kills the still-running process, and reports -1. Measured on a # healthy binary on an x64 box: 24 of 25 runs reported exit -1 through # Select-Object, and 25 of 25 reported 0 when the output was collected first. # # So this check was deleting a perfectly good ssh.exe on almost every run, on # every architecture, and the resulting fallback to the in-box client - the # ~17MB/s one this whole step exists to avoid - looked like a property of the # release rather than a bug in the test. Collect, THEN take the first line. $SshExe = Join-Path $RsyncDir 'ssh.exe' if ($WantSsh -and (Test-Path $SshExe)) { $prevEap = $ErrorActionPreference $ErrorActionPreference = 'Continue' $global:LASTEXITCODE = $null $sshVer = $null try { $sshOut = (& $SshExe -V 2>&1); $sshVer = $sshOut | Select-Object -First 1 } catch { } finally { $ErrorActionPreference = $prevEap } if ($LASTEXITCODE -eq 0) { Write-Host " ssh.exe runs: $sshVer" } else { $why = if ($null -eq $LASTEXITCODE) { 'it would not start' } else { "exit $LASTEXITCODE" } 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." Remove-Item $SshExe -Force -ErrorAction SilentlyContinue $WantSsh = $false } } Write-Host " $(if ($RsyncCurrent) { 'Kept' } else { 'Installed' }) $RsyncExe$(if ($WantSsh) { ' and the ssh.exe it runs' })" # Machine PATH (HKLM environment). Idempotent: only appends if absent. $m = [Environment]::GetEnvironmentVariable('Path', 'Machine') if (-not $m) { $m = '' } if (($m -split ';') -notcontains $RsyncDir) { $new = if ($m.Trim()) { $m.TrimEnd(';') + ';' + $RsyncDir } else { $RsyncDir } [Environment]::SetEnvironmentVariable('Path', $new, 'Machine') Write-Host " Added $RsyncDir to the machine PATH (restart shells / sshd to pick it up)." # sshd caches the environment it was started with, so an already-running # service would not see the new PATH until restarted. if ((Get-Service sshd -ErrorAction SilentlyContinue).Status -eq 'Running') { Restart-Service sshd Write-Host ' Restarted sshd so it inherits the updated machine PATH.' } } else { Write-Host " OK: $RsyncDir already in the machine PATH" } & $RsyncExe --version | Select-Object -First 1 } catch { Write-Warning "rsync install failed: $($_.Exception.Message)" Write-Warning "Download $RsyncAsset from https://github.com/$RsyncRepo/releases manually" Write-Warning "and unpack it into $RsyncDir, keeping rsync.exe and ssh.exe together." } } # --------------------------------------------------------------------------- # Visual Studio 2022 Community # --------------------------------------------------------------------------- $TempDir = Join-Path $env:TEMP 'dev_install' New-Item -ItemType Directory -Force -Path $TempDir | Out-Null # Component IDs split into independent groups so each can be installed in its # own pass. The base group is the known-good set; Clang and the Windows XP # toolset are layered on afterwards so a failure clearly identifies the culprit. # Component reference: https://learn.microsoft.com/visualstudio/install/workload-component-id-vs-community $BaseComponents = @( # Core C++ desktop workload 'Microsoft.VisualStudio.Workload.NativeDesktop' # MSVC build tools. Named explicitly rather than left to --includeRecommended, # because what that pulls in depends on the host: on an ARM64 machine the # workload's recommended set is the ARM64-hosted toolchain targeting ARM64, # and the x64 cross-compiler is NOT implied. Ask for both and the box builds # every target it can, whichever architecture it is: # on x64 -> x64-hosted, targeting x86/x64 and ARM64 # on ARM64 -> ARM64-hosted, targeting ARM64 and x86/x64 # Both are native toolchains; neither cross-compile runs under emulation. 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64' 'Microsoft.VisualStudio.Component.VC.Tools.ARM64' # Spectre-mitigated MSVC runtime libs, for each target above 'Microsoft.VisualStudio.Component.VC.Runtimes.x86.x64.Spectre' 'Microsoft.VisualStudio.Component.VC.Runtimes.ARM64.Spectre' # Spectre-mitigated ATL (needed for many driver/COM projects). The x86/x64 and # ARM64 ATL libraries are separate components; a driver or COM project built # for ARM64 wants the second one, and it is not implied by the first. 'Microsoft.VisualStudio.Component.VC.ATL.Spectre' 'Microsoft.VisualStudio.Component.VC.ATL.ARM64.Spectre' # Windows 11 SDK — build number must match the WDK below 'Microsoft.VisualStudio.Component.Windows11SDK.26100' ) # WDK Visual Studio extension (VSIX). The silent wdksetup.exe /quiet does NOT # install this (it only prompts interactively), so it has to be asked for here. # # Follows the WDK itself: x64 only. On ARM64 the kit is not installed (see the # WDK step for why), and this VSIX on its own is just the driver project # templates and property pages with no headers, libs or tools behind them. if (-not $IsArm64) { $BaseComponents += 'Component.Microsoft.Windows.DriverKit' } # Clang/LLVM toolset (ClangCL, used in CMakePresets.json). Two parts: the Clang # compiler itself, plus the MSBuild integration providing the "ClangCL" toolset. # # x64 ONLY, and NOT because ARM64 cannot have it - it can, natively. The VSIX is # productArch=neutral with no chip/machineArch restriction, so the Arm installer # offers it, and MSVC's VC\Tools\Llvm tree is partitioned by HOST architecture # (bin = x86, x64\bin = x64, ARM64\bin = ARM64) with genuine ARM64 binaries in # ARM64\bin, which is where clang-cl.exe lands. It was installed and working here # before this split. # # It is dropped on ARM64 because that machine is a constrained VM provisioned as # a single-compiler build box: MSVC and nothing else. Compiler diversity - MSVC, # VS clang-cl and upstream LLVM over the same sources - now lives entirely on the # x64 machine, which is the one with room for three toolchains. The VsClang step # is -X64Only in setup-windows-steps.ps1, so on ARM64 this group is never asked # for. $ClangComponents = @( 'Microsoft.VisualStudio.Component.VC.Llvm.Clang' 'Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset' ) # Windows XP targeting (v141_xp toolset, used in CMakePresets.json). The v141 # (VS2017) build tools provide the 14.16 compiler that the XP toolset wraps; # WinXP layers the XP-compatible CRT/SDK on top of it. # # NOT USED ON ARM64 - VsWinXP is -X64Only. The components are still listed in the # catalog on Arm, so this is not strictly "unavailable" - but the 14.16 toolset # predates Windows on Arm as a host and ships HostX86/HostX64 compilers only, so # the best you could get is an x86-emulated compiler, and Microsoft does not # support XP targeting from an Arm host. Nothing is lost that this box could have # used: Windows XP never ran on ARM64, so an XP-targeting build from an ARM64 # host has no purpose beyond producing x86 binaries, which the current toolset # does natively via -A Win32. On x64 the group is installed exactly as before. $XpComponents = @( 'Microsoft.VisualStudio.Component.VC.v141.x86.x64' 'Microsoft.VisualStudio.Component.WinXP' ) # Which Visual Studio generation to drive: 17 = VS 2022, 18 = VS 2026. Both have # native ARM64 installers and ARM64-hosted MSVC. This picks the bootstrapper URL, # and - just as importantly - scopes the vswhere lookup below, so a box that # already has a DIFFERENT generation installed is not mistaken for this one. # # Split by architecture on purpose: # x64 -> 17. The v141 / Windows XP targeting toolset in $XpComponents is the # reason; that group is the whole point of pinning a generation here. # ARM64 -> 18. The XP group is skipped on Arm regardless (no ARM64-hosted 14.16 # compiler), so nothing holds this back to 17, and VS 2026 brings the # newer MSVC. Together with the native ARM64 clang-cl from # $ClangComponents above, that is the compiler diversity on this box. $VsChannel = if ($IsArm64) { 18 } else { 17 } $VsEdition = 'community' # community | professional | enterprise # aka.ms path segment per generation. NOT the same word for both: VS 2022 is # published under /release/, VS 2026 under /stable/. This is not cosmetic - # https://aka.ms/vs/18/release/vs_community.exe is not a 404, it silently # redirects to Bing and returns 200 with an HTML body, so a wrong guess here # downloads a web page, names it vs_community.exe, and fails at Start-Process # with something that looks nothing like a bad URL. $VsChannelPath = if ($VsChannel -ge 18) { 'stable' } else { 'release' } # Detect an existing VS install via vswhere (ships with the VS Installer). Note # that vswhere itself lives under the 32-bit Program Files on every architecture, # ARM64 included - the VS Installer is x86-registered there by contract even # though the installer binaries themselves are native. # These are referenced by Invoke-VsModify via $script: scope. $VsWhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' $InstallPath = Get-VsInstallPath # Three steps drive Visual Studio - the base install and the two optional groups # layered on it. They share the bootstrapper download and the verification # below, which happen once if any of them runs. Asked once, here. $DoVsBase = Test-SetupStep 'VisualStudio' $DoVsClang = Test-SetupStep 'VsClang' $DoVsXp = Test-SetupStep 'VsWinXP' $DoVs = $DoVsBase -or $DoVsClang -or $DoVsXp # Whether any selected pass still has something to add. When none does, the # bootstrapper is not even downloaded: each pass would find its components # already installed (Test-VsHasComponents) and return before using it. The Clang # and XP passes only count with an install to add them to - without one they # stand down below, and would not use the bootstrapper either. $VsNeedsInstaller = ($DoVsBase -and -not (Test-VsHasComponents $BaseComponents)) -or ($DoVsClang -and $InstallPath -and -not (Test-VsHasComponents $ClangComponents)) -or ($DoVsXp -and $InstallPath -and -not (Test-VsHasComponents $XpComponents)) # Report any OTHER Visual Studio generations on the box. They are left alone - # the passes below only ever touch $InstallPath - but when none of them matches # $VsChannel this script is about to download and install a second, largely # redundant toolchain, and that should be a visible decision rather than a # surprise 10GB. (On ARM64, where $VsChannel is 18, an existing VS 2026 IS the # match and gets modified in place rather than duplicated.) if ($DoVs -and (Test-Path $VsWhere)) { $others = & $VsWhere -products '*' -format value -property installationPath | Where-Object { $_ -and $_ -ne $InstallPath } if ($others) { Write-Step 'Other Visual Studio installations detected' foreach ($o in $others) { Write-Host " $o" -ForegroundColor Yellow } Write-Host " Not modified. This script drives VS generation $VsChannel only." -ForegroundColor Yellow Write-Host " To use one of the above instead, set `$VsChannel at the top of this step." -ForegroundColor Yellow } } if ($DoVs -and -not $VsNeedsInstaller -and $InstallPath) { Write-Step 'Visual Studio' Write-Host " Every selected component group is already in $InstallPath; not downloading the installer." } if ($VsNeedsInstaller) { Write-Step "Downloading VS $VsChannel $VsEdition bootstrapper (host: $HostArch)" # aka.ms serves the bootstrapper for the requesting machine's architecture, so on # ARM64 this is the native ARM64 installer - no --arch flag needed or offered. $VsInstallerUrl = "https://aka.ms/vs/$VsChannel/$VsChannelPath/vs_$VsEdition.exe" $VsBootstrapper = Join-Path $TempDir "vs_$VsEdition.exe" Write-Host " $VsInstallerUrl" -ForegroundColor DarkGray Invoke-WebRequest -Uri $VsInstallerUrl -OutFile $VsBootstrapper -UseBasicParsing # Prove we got an installer and not a web page. The Bing redirect described above # returns 200 with HTML, and every other aka.ms typo behaves the same way, so a # bad channel/edition combination is otherwise only discovered when the "exe" # fails to start. 'MZ' is the DOS header every PE begins with. $vsHead = [IO.File]::ReadAllBytes($VsBootstrapper) | Select-Object -First 2 if (-not ($vsHead.Count -eq 2 -and $vsHead[0] -eq 0x4D -and $vsHead[1] -eq 0x5A)) { throw "Visual Studio: $VsInstallerUrl did not return an executable (no MZ header; $((Get-Item $VsBootstrapper).Length) bytes). Check `$VsChannel / `$VsChannelPath / `$VsEdition." } Write-Host " OK: bootstrapper is a PE ($([math]::Round((Get-Item $VsBootstrapper).Length / 1MB, 2)) MB)" } # Install in three sequential passes. The base set is installed first (this is # the configuration that previously worked); Clang and the XP toolset are added # afterwards. If one fails, its label pinpoints which group is responsible. # # The last two are -Optional: neither the Clang toolset nor XP targeting is # needed to build with MSVC, and on a host where one of them is simply not # offered a hard failure here would cost you the whole toolchain over a component # you can add later from the installer UI. # # On ARM64 only the first pass is ever selected: VsClang and VsWinXP are # -X64Only in setup-windows-steps.ps1, and the plan printed at the top says why. # # The last two MODIFY an existing install. With the base step switched off and # no Visual Studio of this generation on the box, there is nothing to modify - # and letting the pass run anyway would install a Visual Studio holding that one # component group and no C++ workload. So they say so and stand down instead. if ($DoVsBase) { Invoke-VsModify -Label 'base toolset + workload' -Ids $BaseComponents } foreach ($pass in @( @{ Run = $DoVsClang; Label = 'Clang / LLVM'; Ids = $ClangComponents } @{ Run = $DoVsXp; Label = 'Windows XP (v141 + WinXP)'; Ids = $XpComponents })) { if (-not $pass.Run) { continue } if ($InstallPath) { Invoke-VsModify -Label $pass.Label -Ids $pass.Ids -Optional } else { Write-Step "Visual Studio: $($pass.Label)" Write-Warning "No Visual Studio $VsChannel is installed to add this to. Select the VisualStudio step as well." } } # --------------------------------------------------------------------------- # Verify what actually landed, on disk. Earlier runs silently skipped the v141 # toolset and the failure only surfaced at build time, so check here and say so # loudly instead. Widened from that one check to every toolset worth naming, # because the same "installed something, but not the thing you needed" failure is # now possible per host architecture: this reports which MSVC host toolchains are # present (HostARM64 is what proves the compiler is native rather than emulated), # whether clang-cl is there, and - on x64 only - whether v141 is. # # Reporting, not throwing. A missing optional component is something to fix from # the installer UI, not a reason to fail a provisioning run that installed a # working compiler. # --------------------------------------------------------------------------- if ($DoVs) { Write-Step 'Verifying the installed toolsets' $InstallPath = Get-VsInstallPath if (-not $InstallPath) { Write-Warning "No Visual Studio $VsChannel installation found after the passes above; cannot verify toolsets." } else { Write-Host " Install path: $InstallPath" # What MSVC versions landed, and which host toolchains each one carries. # HostARM64 is the directory that proves the native ARM64 compiler is here # rather than an x64 one that would run under emulation. $msvcRoot = Join-Path $InstallPath 'VC\Tools\MSVC' foreach ($v in (Get-ChildItem $msvcRoot -Directory -ErrorAction SilentlyContinue | Sort-Object Name)) { $hosts = Get-ChildItem (Join-Path $v.FullName 'bin') -Directory -ErrorAction SilentlyContinue | ForEach-Object { $_.Name } Write-Host " MSVC $($v.Name): $(if ($hosts) { $hosts -join ', ' } else { '(no bin dir)' })" } if ($IsArm64) { $armHost = Test-Path (Join-Path $msvcRoot '*\bin\HostARM64\ARM64\cl.exe') if ($armHost) { Write-Host ' OK: native ARM64-hosted cl.exe present.' -ForegroundColor Green } else { 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.' } } # clang-cl, for the ClangCL toolset in CMakePresets.json. Checked per host # directory, because the Llvm tree is partitioned by HOST architecture and # only the matching one is a native compiler. # # Look for clang-cl.exe specifically, NOT for the directory. VC\Tools\Llvm\*\bin # holds clang-format.exe and clang-tidy.exe on every host whether or not the # Clang component was ever installed - those ship with the NativeDesktop # workload - so a present ARM64\bin proves nothing on its own. That is the # false positive to avoid when checking this by hand. $llvmRoot = Join-Path $InstallPath 'VC\Tools\Llvm' $clangArm = Test-Path (Join-Path $llvmRoot 'ARM64\bin\clang-cl.exe') $clangX64 = Test-Path (Join-Path $llvmRoot 'x64\bin\clang-cl.exe') $clangX86 = Test-Path (Join-Path $llvmRoot 'bin\clang-cl.exe') $clangAny = $clangArm -or $clangX64 -or $clangX86 if ($IsArm64) { # Not installed here by design, so its absence is the expected result and # must not read as a fault. Its PRESENCE is the thing worth a line: a box # provisioned before this split still carries it, and the component stays # until it is explicitly removed - the installer never uninstalls what you # simply stopped asking for. if ($clangAny) { Write-Host ' clang-cl: present but no longer provisioned on ARM64 - left over from an earlier run.' -ForegroundColor Yellow Write-Host ' Remove it with: vs_installer.exe modify --installPath "" --remove Microsoft.VisualStudio.Component.VC.Llvm.Clang --remove Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset --quiet' -ForegroundColor Yellow } else { Write-Host ' clang-cl: n/a on ARM64 (MSVC only by design; use the x64 box for ClangCL).' -ForegroundColor DarkGray } } elseif ($clangAny) { Write-Host " clang-cl: $(@(if ($clangArm) {'ARM64'}; if ($clangX64) {'x64'}; if ($clangX86) {'x86'}) -join ', ')" -ForegroundColor Green } elseif (-not $DoVsClang) { # Switched off in the plan: a choice, so not worth a warning. Write-Host ' clang-cl: not installed (the VsClang step was not selected).' -ForegroundColor DarkGray } else { Write-Warning 'clang-cl not found - the ClangCL presets will fail. Add the "C++ Clang tools for Windows" component.' } # v141 / XP. Only meaningful where the toolset can exist at all; on ARM64 the # group above was deliberately skipped, so warning here would be noise about # a decision this script made on purpose two steps ago. if ($IsArm64) { Write-Host ' v141 / Windows XP toolset: n/a on ARM64 (not offered for Arm hosts).' -ForegroundColor DarkGray } else { $V141 = Get-ChildItem $msvcRoot -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -like '14.16.*' } | Select-Object -First 1 if ($V141) { Write-Host " OK: v141 toolset present ($($V141.Name))" -ForegroundColor Green } elseif (-not $DoVsXp) { Write-Host ' v141 / Windows XP toolset: not installed (the VsWinXP step was not selected).' -ForegroundColor DarkGray } else { Write-Warning 'v141 (14.16.x) toolset NOT found - the XP build presets will fail.' Write-Warning 'Add it via Visual Studio Installer > Modify > Individual components:' Write-Warning ' - MSVC v141 - VS 2017 C++ x64/x86 build tools (v14.16)' Write-Warning ' - C++ Windows XP Support for VS 2017 (v141) tools' } } } } # --------------------------------------------------------------------------- # Windows Driver Kit (WDK 10.0.26100) # Build 26100 matches the Windows 11 SDK installed above. # Provides IddCx (iddcx.h / iddcx.lib) and UMDF 2.x for Indirect Display Drivers. # linkid=2335869 -> WDK 26100.6584 (per Microsoft "Other WDK Downloads"). # # x64 ONLY. Two reasons, and the second is the one that decided it: # # 1. The ARM64 box builds user-mode software; no driver is developed on it. # # 2. This exact WDK cannot install next to the SDK that VS 2026 brings. The # WDK requires the MATCHING Windows SDK revision, and revisions are not # side-by-side - Include\10.0.26100.0 is one directory whichever revision # wrote it. VS 2026 installs SDK 26100.7705; linkid=2335869 is WDK # 26100.6584, which is the kit paired with VS 2022. So wdksetup.exe aborts: # exit 15605, WER signature "WDK / 10.1.26100.6584 / Install / 0x80070642" # (0x642 = 1602, ERROR_INSTALL_USEREXIT - a silent install cancelling # itself on a failed prerequisite check). It is not an ARM64 or emulation # problem; the same pairing fails on x64. # # If a driver ever does need building here, the fix is the WDK that matches # this generation - Microsoft's "Supported and other WDK downloads" table # pairs VS 2026 with the 28000.x kit - not this link. # # On x64 with VS 2022 the SDK and this WDK are the matched pair, so it installs. # # The x64-only rule itself is the Wdk step's -X64Only in setup-windows-steps.ps1, # so on ARM64 this step never runs and the plan says why. # --------------------------------------------------------------------------- if (Test-SetupStep 'Wdk') { $WdkVersion = '10.0.26100' # Both hives. The WDK installer is a 32-bit program, so on x64 it writes under # WOW6432Node - but which hive a given kit lands in has varied across kit # versions and architectures, and reading only one of them makes an installed WDK # look absent, which costs a needless multi-GB reinstall on every run. Check the # native hive too and take whichever answers. $WdkInstalledRoot = @( 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows Kits\Installed Roots' 'HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots' ) | ForEach-Object { (Get-ItemProperty $_ -ErrorAction SilentlyContinue).WdkBinRootVersioned } | Where-Object { $_ } | Select-Object -First 1 if ($WdkInstalledRoot -and $WdkInstalledRoot -match [regex]::Escape($WdkVersion)) { # Re-running wdksetup.exe for an already-present version returns exit code # 2008 (maintenance mode / nothing to do), which is not a real failure. Write-Step "WDK $WdkVersion already installed - skipping ($WdkInstalledRoot)" } else { Write-Step 'Downloading WDK installer' $WdkUrl = 'https://go.microsoft.com/fwlink/?linkid=2335869' $WdkInstaller = Join-Path $TempDir 'wdksetup.exe' Invoke-WebRequest -Uri $WdkUrl -OutFile $WdkInstaller -UseBasicParsing Write-Step 'Installing WDK' # wdksetup.exe is a 32-bit binary and runs under emulation on ARM64; the kit # it lays down does include the ARM64 target headers, libs and tools (the # signing/deployment tools under bin\arm64), so an ARM64 driver builds from # an ARM64 host. Only the installer is emulated, not the toolchain. $proc = Start-Process -FilePath $WdkInstaller -ArgumentList '/quiet /norestart' -Wait -PassThru -NoNewWindow Write-Host " WDK installer exit code: $($proc.ExitCode)" if ($proc.ExitCode -eq 2008) { # 2008 = the WDK is already present; the installer has nothing to do. Write-Host ' [WDK already installed (exit 2008) - treating as success]' -ForegroundColor Yellow } else { Assert-ExitCode $proc.ExitCode 'WDK' } } } # --------------------------------------------------------------------------- # Windows Performance Toolkit: xperf, wpr, and Windows Performance Analyzer # (wpa.exe) -- ETW CPU + loader profiling and the GUI that reads the traces. # # WPA is NOT a Visual Studio component and has no relationship to VS's own # Performance Profiler (a separate, .diagsession-based tool that cannot open an # .etl). It ships in exactly two places: as an optional FEATURE of the Windows # SDK ("Windows Performance Toolkit", OptionId.WindowsPerformanceToolkit), and # in the Windows ADK, which bundles the same toolkit. Whether the SDK install # that Visual Studio performs happens to select that feature varies with the VS # and SDK version - when it does, WPT lands in # %ProgramFiles(x86)%\Windows Kits\10\Windows Performance Toolkit and the SDK # puts that directory on the machine PATH itself - so this step DETECTS first # and only falls back to installing the ADK (winget owns the versioned download # URL, which makes it the reliable source) when nothing is there. That fallback # is a large download; to install just the toolkit instead, run the standalone # SDK setup with # winsdksetup.exe /features OptionId.WindowsPerformanceToolkit /q # # There is also a newer WPA in the Microsoft Store (`winget install --id # 9N0W1B2BXGNZ --source msstore`), which updates independently of the SDK. It is # not installed here: the Store package needs an interactive, signed-in session, # which is exactly what this elevated, unattended half does not have. # # Idempotent and non-fatal - it never aborts provisioning. # --------------------------------------------------------------------------- if (Test-SetupStep 'Wpt') { Write-Step 'Windows Performance Toolkit (xperf / wpr / WPA)' $WptDirs = @( (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Windows Performance Toolkit'), (Join-Path $env:ProgramFiles 'Windows Kits\10\Windows Performance Toolkit'), (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Assessment and Deployment Kit\Windows Performance Toolkit') ) function Find-WptDir { $script:WptDirs | Where-Object { Test-Path (Join-Path $_ 'xperf.exe') } | Select-Object -First 1 } $WptDir = Find-WptDir if ($WptDir) { Write-Host " OK: WPT already present ($WptDir)" -ForegroundColor Green } else { try { # The ADK manifest offers no ARM64 installer, so on ARM64 winget fetches # the x64 one; it runs under emulation and lays down a toolkit that does # include the ARM64 binaries. The SDK feature is the lighter route on any # architecture and is worth preferring if this fallback ever gives # trouble - see the winsdksetup.exe line in the comment above. if ($IsArm64) { Write-Host ' ARM64: the ADK installer is x64 (emulated); the toolkit it installs is ARM64.' -ForegroundColor Yellow } winget install --id Microsoft.WindowsADK --exact --silent --disable-interactivity ` --accept-source-agreements --accept-package-agreements Write-Host ' Windows ADK (includes Windows Performance Toolkit) installed.' $WptDir = Find-WptDir } catch { Write-Warning "WPT install failed: $($_.Exception.Message)" Write-Warning 'Install manually: winget install Microsoft.WindowsADK, or add the' Write-Warning 'Windows SDK "Windows Performance Toolkit" optional feature.' } } if ($WptDir) { # Report what actually landed. wpa.exe is the piece people come looking for # and it is the one that is absent if a trimmed toolkit ever shows up. foreach ($tool in 'xperf.exe', 'wpr.exe', 'wpa.exe', 'wpaexporter.exe') { $p = Join-Path $WptDir $tool if (Test-Path $p) { Write-Host " $tool $((Get-Item $p).VersionInfo.ProductVersion)" } else { Write-Warning "$tool is missing from $WptDir" } } # The WPT installer normally adds this to the machine PATH itself (and the # Start Menu gets "Windows Kits > Windows Performance Toolkit" shortcuts for # WPA and WPR). Re-assert it anyway: on the machine PATH rather than a user # one so it also resolves for the non-interactive sshd sessions this box is # driven through, which build their environment from the registry PATH. # Compared trailing-backslash-insensitively - the installer's own entry has # one, and adding a second spelling of the same directory is just noise. $m = [Environment]::GetEnvironmentVariable('Path', 'Machine') if (-not $m) { $m = '' } $have = ($m -split ';') | Where-Object { $_.TrimEnd('\') -eq $WptDir.TrimEnd('\') } if ($have) { Write-Host " OK: $WptDir already in the machine PATH" } else { $new = if ($m.Trim()) { $m.TrimEnd(';') + ';' + $WptDir } else { $WptDir } [Environment]::SetEnvironmentVariable('Path', $new, 'Machine') Write-Host " Added $WptDir to the machine PATH (restart shells to pick it up)." } } } # --------------------------------------------------------------------------- # Intel VTune Profiler - reported, not installed # # Deliberately NOT automated, unlike everything above. The offline installer is # a ~750 MB download from a URL carrying a per-release GUID # (registrationcenter-download.intel.com/akdlm/IRC_NAS//intel-vtune-_offline.exe) # with no "latest" redirect behind it, so every new build means editing a # hard-coded link in here - and it is only worth having on Intel silicon, since # hardware event-based sampling reads Intel PMU counters. Not a good trade for a # script that has to keep working unattended on any box. # # So this step only reports. To install it, take the Windows offline installer # from # https://www.intel.com/content/www/us/en/developer/tools/oneapi/vtune-profiler-download.html # and run it elevated; it installs unattended with # intel-vtune-_offline.exe -a --silent --cli --eula accept # --------------------------------------------------------------------------- if (Test-SetupStep 'VTune') { Write-Step 'Intel VTune Profiler (status only)' if ($IsArm64) { # Not a "not installed yet" case - there is no Windows-on-Arm build of VTune, # and there is nothing for it to sample: its whole value is reading Intel PMU # counters. Say so plainly and point at what does work here, rather than # printing a download link for a product this box cannot run. Write-Host ' n/a on ARM64: Intel ships no Windows-on-Arm build, and hardware event-based' -ForegroundColor DarkGray Write-Host ' sampling reads Intel PMU counters. Use the Windows Performance Toolkit above' -ForegroundColor DarkGray Write-Host ' (wpr / xperf to collect, wpa to analyse) for profiling on this box.' -ForegroundColor DarkGray Write-Host ' Arm also publishes Arm Performance Studio / Streamline for Arm PMU sampling.' -ForegroundColor DarkGray } else { $UninstallKeys = @( 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' ) $vtune = Get-ItemProperty $UninstallKeys -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -match 'VTune' } | Select-Object -First 1 if ($vtune) { Write-Host " Installed: $($vtune.DisplayName.Trim()) $($vtune.DisplayVersion)" -ForegroundColor Green # The oneAPI layout keeps a `latest` junction beside the versioned directory, # so this path stays right across upgrades. $VTuneCli = Join-Path $vtune.InstallLocation 'vtune\latest\bin64\vtune.exe' if (Test-Path $VTuneCli) { Write-Host " CLI: $VTuneCli" } } else { Write-Host ' Not installed.' -ForegroundColor Yellow Write-Host ' https://www.intel.com/content/www/us/en/developer/tools/oneapi/vtune-profiler-download.html' -ForegroundColor Yellow $cpu = (Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1).Manufacturer if ($cpu -and $cpu -notmatch 'Intel') { Write-Host " (This CPU reports itself as '$cpu' - VTune's hardware event-based sampling wants Intel silicon.)" -ForegroundColor Yellow } } } } # --------------------------------------------------------------------------- # A selected step that no section above asked about means this script and the # catalog disagree - an Id renamed in one and not the other, or a row added with # nothing behind it. That is a failure: the plan promised a step that was not # taken. $neverAsked = @($RunSteps | Where-Object { -not $AskedSteps.Contains($_) }) if ($neverAsked) { throw "Selected in the plan but not implemented by setup-windows-with-uac.ps1: $($neverAsked -join ', ')" } Write-Host "`nAll done." -ForegroundColor Green Write-Host "Host architecture was $HostArch." Write-Host 'If a reboot was flagged above, restart before opening VS or building drivers.' } catch { $ExitCode = 1 Write-Host "`n==> SETUP FAILED: $($_.Exception.Message)" -ForegroundColor Red if ($_.ScriptStackTrace) { Write-Host $_.ScriptStackTrace -ForegroundColor DarkGray } # Only fold in the VS Installer logs when a VS step actually failed; for other # steps (e.g. WDK) those logs are stale and misleading, so the message above # is what matters. if ($_.Exception.Message -match 'Visual Studio') { try { Show-VsSetupLogs } catch {} } } finally { try { Stop-Transcript | Out-Null } catch {} # This log was created by the elevated (admin) process, so by default the # non-elevated caller can't delete it (their token has Administrators marked # deny-only). Grant BUILTIN\Users Modify rights so the user account that runs # setup-windows.bat can remove the log later. S-1-5-32-545 is the well-known # Users SID, used here so this is locale-independent. try { if (Test-Path $LogFile) { $usersSid = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-545') $acl = Get-Acl -Path $LogFile $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( $usersSid, 'Modify', 'Allow') $acl.AddAccessRule($rule) Set-Acl -Path $LogFile -AclObject $acl } } catch { Write-Host " [warning] could not relax ACL on $LogFile : $($_.Exception.Message)" -ForegroundColor Yellow } } exit $ExitCode