<# 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 . 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 AndroidStudio M bat -Admin -Package Google.AndroidStudio -Text 'only if missing; it updates itself' ` -X64Only 'Google publishes no Windows ARM64 build of Android Studio' 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 AndroidSdk C user -Needs AndroidStudio ` -Text 'Android SDK, per-user: latest platform + build-tools; accepts the SDK licences' ` -X64Only 'its Windows build tools are x86/x64 only, as is Android Studio' 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 }