+<#
+ setup-windows-steps.ps1 - every step the Windows setup can take, in one list.
+
+ Dot-sourced, never run: it defines functions and nothing else, so loading it
+ changes no state. It deliberately has no param block either - dot-sourcing
+ binds one in the CALLER's scope, which would reset the caller's own -Only and
+ -Skip to their defaults.
+
+ Who reads it:
+ setup-windows-plan.ps1 shows it as the plan before anything happens,
+ and lets you choose; setup-windows.bat runs it first
+ setup-windows.bat takes the Runner 'bat' steps, as the planner hands
+ them over (it never reads this file itself)
+ setup-windows-with-uac.ps1 takes the 'uac' steps - the elevated half
+ setup-windows-no-uac.ps1 takes the 'user' steps - per-user configuration
+
+ THIS IS THE ONE PLACE a step's availability is decided. -X64Only, with its
+ reason, keeps a step off ARM64; -Admin keeps it off an account that cannot
+ elevate. The planner shows both as "[-]" rows with the reason, the batch file
+ prints the same reason in the log where the step would have run, and the two
+ halves apply them again when run on their own. This replaces the :x64only
+ subroutine that used to sit at the bottom of setup-windows.bat and the
+ $Arm64Dropped table in setup-windows-no-uac.ps1.
+
+ Adding a step: add a row below, in the order it runs, and gate the code that
+ does it on the same Id in the script its Runner names. The scripts check that
+ the two agree: an Id the catalog does not know is refused, and a catalog row
+ that nothing implements is reported rather than silently dropped.
+#>
+
+function Get-SetupGroups {
+ # Display order is run order. The letter is what you type in the planner, or
+ # pass to -Only / -Skip, to take a whole group at once.
+ [ordered]@{
+ U = 'Per-user installs (winget, except Claude Code)'
+ M = 'Machine-wide installs (winget), then the WiX .NET tool'
+ E = 'Elevated setup (setup-windows-with-uac.ps1)'
+ C = 'Per-user configuration (setup-windows-no-uac.ps1), then Pillow'
+ }
+}
+
+function New-SetupStep {
+ param(
+ [Parameter(Mandatory, Position = 0)] [string] $Id,
+ [Parameter(Mandatory, Position = 1)] [ValidateSet('U', 'M', 'E', 'C')] [string] $Group,
+ # Which script takes the step.
+ [Parameter(Mandatory, Position = 2)] [ValidateSet('bat', 'uac', 'user')] [string] $Runner,
+ # What the plan says the step does. For a winget step it follows the
+ # package id, so it only needs to add what the id does not say.
+ [string] $Text,
+ [string] $Package,
+ # Not on ARM64, because <this>. Shown in the plan and in the run log.
+ [string] $X64Only,
+ # Needs an account that can elevate: a machine-wide installer, or part
+ # of the elevated half.
+ [switch] $Admin,
+ # Steps this one builds on. Advisory only - the planner notes it when
+ # one of these is switched off, since it may already be installed.
+ [string[]] $Needs = @()
+ )
+ $shown = if (-not $Package) { $Text } elseif ($Text) { "$Package - $Text" } else { $Package }
+ [pscustomobject]@{
+ Id = $Id
+ Group = $Group
+ Runner = $Runner
+ Text = $shown
+ Package = $Package
+ X64Only = $X64Only
+ Admin = [bool]$Admin
+ Needs = $Needs
+ }
+}
+
+function Get-SetupCatalog {
+ # Every step, in the order setup-windows.bat takes them. The long-form
+ # reasoning for each one stays in a comment beside the code that does it;
+ # the text here is what a person deciding needs to see.
+ param([bool] $IsArm64)
+
+ $vs = if ($IsArm64) {
+ 'Visual Studio 2026 Community, C++: MSVC for ARM64 + x86/x64, SDK 26100 (several GB)'
+ } else {
+ 'Visual Studio 2022 Community, C++: MSVC for x86/x64 + ARM64, SDK 26100 (several GB)'
+ }
+ $rust = if ($IsArm64) {
+ 'Rust stable-aarch64-pc-windows-msvc (native), installed and made the default'
+ } else {
+ 'Rust stable-x86_64-pc-windows-msvc, installed and made the default'
+ }
+
+ @(
+ # --- setup-windows.bat: per-user winget installs ---
+ # Not winget: recent Anthropic.ClaudeCode releases are broken through it.
+ New-SetupStep ClaudeCode U bat -Text 'Claude Code, native installer: irm https://claude.ai/install.ps1 | iex'
+ New-SetupStep Brave U bat -Package Brave.Brave `
+ -X64Only 'no ARM64 build, and Edge is in-box and native'
+ New-SetupStep Git U bat -Package Git.Git
+ New-SetupStep PowerShell U bat -Package Microsoft.PowerShell
+ New-SetupStep ProcessExplorer U bat -Package Microsoft.Sysinternals.ProcessExplorer
+ New-SetupStep ProcessMonitor U bat -Package Microsoft.Sysinternals.ProcessMonitor
+ New-SetupStep SDelete U bat -Package Microsoft.Sysinternals.SDelete
+ New-SetupStep VSCode U bat -Package Microsoft.VisualStudioCode
+ New-SetupStep WindowsTerminal U bat -Package Microsoft.WindowsTerminal
+ New-SetupStep Python U bat -Package Python.Python.3.13
+ New-SetupStep WinMerge U bat -Package WinMerge.WinMerge `
+ -X64Only 'its ARM64 build is machine-scope only; unelevated winget gets the x64 one'
+ New-SetupStep Ninja U bat -Package Ninja-build.Ninja -Text 'native; Visual Studio bundles an x64 one'
+ New-SetupStep LLVM U bat -Package LLVM.LLVM -Text 'upstream Clang, a second compiler beside clang-cl' `
+ -X64Only 'this box carries MSVC alone; no second Clang to cross-check'
+ New-SetupStep Rustup U bat -Package Rustlang.Rustup -Text 'the Rust toolchain manager, per-user; installs stable too'
+ New-SetupStep iPerf3 U bat -Package ar51an.iPerf3
+ # On ARM64 too, like iperf3: an x86-only build, but bound by the link to
+ # the device, not the CPU. See the comment beside it in setup-windows.bat.
+ New-SetupStep PlatformTools U bat -Package Google.PlatformTools -Text 'Android SDK Platform-Tools: adb, fastboot (an x86 build)'
+ New-SetupStep NASM U bat -Package NASM.NASM `
+ -X64Only 'x86/x64-only assembler; MSVC armasm64 is the native one'
+ New-SetupStep VirtualBox U bat -Package Oracle.VirtualBox -Text 'for the Windows 7 test VM' `
+ -X64Only 'no ARM64 host build, so no Windows 7 x86 test VM either'
+
+ # --- setup-windows.bat: machine-wide installers, despite running unelevated ---
+ New-SetupStep DotNetSdk M bat -Admin -Package Microsoft.DotNet.SDK.10 -Text 'for the WiX tooling'
+ New-SetupStep CMake M bat -Admin -Package Kitware.CMake -Text 'standalone, so a plain cmake works in any shell'
+ New-SetupStep AndroidGPUInspector M bat -Admin -Package Google.AndroidGPUInspector `
+ -X64Only 'x64-only build, and it profiles Android devices, not this box'
+ New-SetupStep OpenCppCoverage M bat -Admin -Package OpenCppCoverage.OpenCppCoverage -Text 'native line coverage' `
+ -X64Only 'it steps x86/x64 instructions, so it cannot cover an ARM64 binary'
+ New-SetupStep WiX M bat -Needs DotNetSdk `
+ -Text 'WiX 5.0.2 as a .NET global tool, pinned: 6.0 and later carry a maintenance-fee EULA'
+
+ # --- setup-windows-with-uac.ps1: the elevated half ---
+ New-SetupStep OpenSshClient E uac -Admin -Text 'OpenSSH Client capability (ssh.exe, and the libcrypto.dll rsync''s ssh.exe needs)'
+ New-SetupStep SshAgent E uac -Admin -Needs OpenSshClient -Text 'ssh-agent service: start automatically, and start it now'
+ New-SetupStep OpenSshServer E uac -Admin -Text 'OpenSSH Server: sshd automatic and running, inbound TCP 22 on all profiles'
+ New-SetupStep Rsync E uac -Admin -Needs OpenSshClient -Text 'rsync and its ssh.exe into C:\Tools\rsync, on the machine PATH'
+ New-SetupStep VisualStudio E uac -Admin -Text $vs
+ New-SetupStep VsClang E uac -Admin -Needs VisualStudio -Text 'Visual Studio Clang/LLVM toolset (clang-cl, for the ClangCL presets)' `
+ -X64Only 'by choice: this box carries MSVC alone'
+ New-SetupStep VsWinXP E uac -Admin -Needs VisualStudio -Text 'Visual Studio v141 + Windows XP targeting toolset' `
+ -X64Only 'v141 has no ARM64-hosted compiler; no XP targeting from Arm'
+ New-SetupStep Wdk E uac -Admin -Needs VisualStudio -Text 'Windows Driver Kit 10.0.26100 (pairs with SDK 26100)' `
+ -X64Only 'user-mode box; this WDK will not install beside VS 2026''s SDK'
+ New-SetupStep Wpt E uac -Admin -Text 'Windows Performance Toolkit (xperf, wpr, WPA); the Windows ADK if it is missing'
+ New-SetupStep VTune E uac -Admin -Text 'Intel VTune Profiler: report whether it is installed (installs nothing)'
+
+ # --- setup-windows-no-uac.ps1: per-user configuration ---
+ New-SetupStep WinMergePath C user -Needs WinMerge -Text 'WinMerge on the user PATH' `
+ -X64Only 'WinMerge is not installed on ARM64'
+ New-SetupStep VsWhere C user -Needs VisualStudio -Text 'vswhere.exe on the user PATH'
+ New-SetupStep BinSkim C user -Text 'BinSkim into %LOCALAPPDATA%\Programs\BinSkim, on the user PATH (100+ MB)' `
+ -X64Only 'the NuGet package publishes win-x64 only'
+ New-SetupStep GitConfig C user -Needs Git -Text 'global git: identity if filled in, core.sshCommand to a Win32-OpenSSH client'
+ New-SetupStep NinjaPath C user -Needs Ninja -Text 'the native ninja first on the user PATH'
+ New-SetupStep LlvmPath C user -Needs LLVM -Text 'upstream LLVM on the user PATH' `
+ -X64Only 'LLVM is not installed on ARM64'
+ New-SetupStep RustToolchain C user -Needs Rustup, VisualStudio -Text $rust
+ New-SetupStep ArchAudit C user -Text 'architecture audit: native or emulated, for every tool (read-only)'
+
+ # --- setup-windows.bat, after both halves ---
+ New-SetupStep Pillow C bat -Needs Python -Text 'pip install Pillow into the newest Python'
+ )
+}
+
+function Get-SetupStepBlocker {
+ # Why this step cannot run on this machine at all, or $null if it can.
+ # Architecture first: an x64-only step is off ARM64 before elevation is even
+ # consulted, which is the more useful reason to show.
+ param($Step, [bool] $IsArm64, [string] $Elevation)
+ if ($IsArm64 -and $Step.X64Only) { return "not on ARM64 - $($Step.X64Only)" }
+ if ($Elevation -eq 'NOLUA' -and $Step.Admin) {
+ return 'needs an administrator - this account cannot elevate'
+ }
+ $null
+}
+
+function Split-SetupNames {
+ # `powershell -File` hands a whole "A,B,C" to an array parameter as ONE
+ # string, so split here and accept either spelling.
+ param([string[]] $Value)
+ @($Value | ForEach-Object { $_ -split '[,\s]+' } | Where-Object { $_ })
+}
+
+function Resolve-SetupStepNames {
+ # Step ids (case-insensitive) or group letters, as typed, to step ids.
+ # Anything else is an error: a mistyped -Skip that was quietly ignored would
+ # install exactly what you asked it not to.
+ param([string[]] $Names, [object[]] $Steps, [hashtable] $Aliases = @{})
+ $groups = @((Get-SetupGroups).Keys)
+ foreach ($name in (Split-SetupNames $Names)) {
+ if ($Aliases.ContainsKey($name)) { $name = $Aliases[$name] }
+ $hit = @($Steps | Where-Object { $_.Id -eq $name })
+ if (-not $hit) { $hit = @($Steps | Where-Object { $_.Group -eq $name }) }
+ if ($hit) { $hit.Id; continue }
+ # A real group with no steps in this script's share of the catalog.
+ if ($groups -contains $name) { continue }
+ throw "Unknown step '$name'. The steps are: $($Steps.Id -join ', '). Groups: $($groups -join ', ')."
+ }
+}
+
+function Resolve-SetupSelection {
+ # The plan: one row per step, in run order, saying whether it is selected
+ # and - when it cannot run on this machine at all - why not.
+ #
+ # -Only $null means "everything that can run"; an empty -Only means nothing.
+ param(
+ [object[]] $Steps,
+ [AllowNull()] [string[]] $Only,
+ [string[]] $Skip = @(),
+ [hashtable] $Aliases = @{},
+ [bool] $IsArm64,
+ [string] $Elevation
+ )
+ $want = if ($null -eq $Only) { @($Steps.Id) } else { @(Resolve-SetupStepNames $Only $Steps $Aliases) }
+ $drop = @(Resolve-SetupStepNames $Skip $Steps $Aliases)
+ $named = @(Split-SetupNames $Only | ForEach-Object { if ($Aliases.ContainsKey($_)) { $Aliases[$_] } else { $_ } })
+ foreach ($s in $Steps) {
+ $why = Get-SetupStepBlocker $s $IsArm64 $Elevation
+ if ($why -and $named -contains $s.Id) {
+ Write-Warning "$($s.Id) was asked for but cannot run here: $why"
+ }
+ [pscustomobject]@{
+ Id = $s.Id
+ Step = $s
+ Why = $why
+ Selected = (-not $why) -and ($want -contains $s.Id) -and ($drop -notcontains $s.Id)
+ }
+ }
+}
+
+function Write-SetupPlan {
+ # The short form of the plan, for the two halves: what this script will do
+ # and what it will not, before it does any of it.
+ param([object[]] $Plan, [string] $Title)
+ Write-Host "`n==> $Title" -ForegroundColor Cyan
+ foreach ($p in $Plan) {
+ if ($p.Selected) {
+ Write-Host (' run {0,-14} {1}' -f $p.Id, $p.Step.Text)
+ } else {
+ $why = if ($p.Why) { $p.Why } else { 'not selected' }
+ Write-Host (' skip {0,-14} {1}' -f $p.Id, $why) -ForegroundColor DarkGray
+ }
+ }
+}
+
+function Confirm-SetupPlan {
+ # $true to go ahead, $false if declined, $null if there is no one to ask.
+ #
+ # Never assumes yes on its own. A run with no console behind it - piped,
+ # scheduled, a non-interactive ssh command - stops after printing the plan,
+ # and says how to run it unattended (-Yes) on purpose.
+ param([switch] $Yes, [int] $Count)
+ if ($Yes) { return $true }
+ if ([Console]::IsInputRedirected) {
+ Write-Host ' No console to ask on, so nothing has been done. Re-run with -Yes to take' -ForegroundColor Yellow
+ Write-Host ' the steps marked "run" unattended.' -ForegroundColor Yellow
+ return $null
+ }
+ $answer = Read-Host " Take the $Count step(s) marked run? [Y/n]"
+ if ($answer -match '^\s*(y|yes)?\s*$') { return $true }
+ Write-Host ' Nothing done.'
+ $false
+}