]>
| Commit | Line | Data |
|---|---|---|
| 1 | <# | |
| 2 | setup-windows-steps.ps1 - every step the Windows setup can take, in one list. | |
| 3 | ||
| 4 | Dot-sourced, never run: it defines functions and nothing else, so loading it | |
| 5 | changes no state. It deliberately has no param block either - dot-sourcing | |
| 6 | binds one in the CALLER's scope, which would reset the caller's own -Only and | |
| 7 | -Skip to their defaults. | |
| 8 | ||
| 9 | Who reads it: | |
| 10 | setup-windows-plan.ps1 shows it as the plan before anything happens, | |
| 11 | and lets you choose; setup-windows.bat runs it first | |
| 12 | setup-windows.bat takes the Runner 'bat' steps, as the planner hands | |
| 13 | them over (it never reads this file itself) | |
| 14 | setup-windows-with-uac.ps1 takes the 'uac' steps - the elevated half | |
| 15 | setup-windows-no-uac.ps1 takes the 'user' steps - per-user configuration | |
| 16 | ||
| 17 | THIS IS THE ONE PLACE a step's availability is decided. -X64Only, with its | |
| 18 | reason, keeps a step off ARM64; -NotInVM, with its reason, keeps it off a | |
| 19 | Windows that is itself a virtual machine; -Admin keeps it off an account that | |
| 20 | cannot elevate. The planner shows each as a "[-]" row with the reason, the batch file | |
| 21 | prints the same reason in the log where the step would have run, and the two | |
| 22 | halves apply them again when run on their own. This replaces the :x64only | |
| 23 | subroutine that used to sit at the bottom of setup-windows.bat and the | |
| 24 | $Arm64Dropped table in setup-windows-no-uac.ps1. | |
| 25 | ||
| 26 | Adding a step: add a row below, in the order it runs, and gate the code that | |
| 27 | does it on the same Id in the script its Runner names. The scripts check that | |
| 28 | the two agree: an Id the catalog does not know is refused, and a catalog row | |
| 29 | that nothing implements is reported rather than silently dropped. | |
| 30 | #> | |
| 31 | ||
| 32 | function Get-SetupGroups { | |
| 33 | # Display order is run order. The letter is what you type in the planner, or | |
| 34 | # pass to -Only / -Skip, to take a whole group at once. | |
| 35 | [ordered]@{ | |
| 36 | U = 'Per-user installs (winget, except Claude Code)' | |
| 37 | M = 'Machine-wide installs (winget), then the WiX .NET tool' | |
| 38 | E = 'Elevated setup (setup-windows-with-uac.ps1)' | |
| 39 | C = 'Per-user configuration (setup-windows-no-uac.ps1), then Pillow' | |
| 40 | } | |
| 41 | } | |
| 42 | ||
| 43 | function New-SetupStep { | |
| 44 | param( | |
| 45 | [Parameter(Mandatory, Position = 0)] [string] $Id, | |
| 46 | [Parameter(Mandatory, Position = 1)] [ValidateSet('U', 'M', 'E', 'C')] [string] $Group, | |
| 47 | # Which script takes the step. | |
| 48 | [Parameter(Mandatory, Position = 2)] [ValidateSet('bat', 'uac', 'user')] [string] $Runner, | |
| 49 | # What the plan says the step does. For a winget step it follows the | |
| 50 | # package id, so it only needs to add what the id does not say. | |
| 51 | [string] $Text, | |
| 52 | [string] $Package, | |
| 53 | # Not on ARM64, because <this>. Shown in the plan and in the run log. | |
| 54 | [string] $X64Only, | |
| 55 | # Not when this Windows is a guest in a virtual machine, because <this>. | |
| 56 | # Shown in the plan and in the run log, like -X64Only. | |
| 57 | [string] $NotInVM, | |
| 58 | # Needs an account that can elevate: a machine-wide installer, or part | |
| 59 | # of the elevated half. | |
| 60 | [switch] $Admin, | |
| 61 | # Steps this one builds on. Advisory only - the planner notes it when | |
| 62 | # one of these is switched off, since it may already be installed. | |
| 63 | [string[]] $Needs = @() | |
| 64 | ) | |
| 65 | $shown = if (-not $Package) { $Text } elseif ($Text) { "$Package - $Text" } else { $Package } | |
| 66 | [pscustomobject]@{ | |
| 67 | Id = $Id | |
| 68 | Group = $Group | |
| 69 | Runner = $Runner | |
| 70 | Text = $shown | |
| 71 | Package = $Package | |
| 72 | X64Only = $X64Only | |
| 73 | NotInVM = $NotInVM | |
| 74 | Admin = [bool]$Admin | |
| 75 | Needs = $Needs | |
| 76 | } | |
| 77 | } | |
| 78 | ||
| 79 | function Get-SetupCatalog { | |
| 80 | # Every step, in the order setup-windows.bat takes them. The long-form | |
| 81 | # reasoning for each one stays in a comment beside the code that does it; | |
| 82 | # the text here is what a person deciding needs to see. | |
| 83 | param([bool] $IsArm64) | |
| 84 | ||
| 85 | $vs = if ($IsArm64) { | |
| 86 | 'Visual Studio 2026 Community, C++: MSVC for ARM64 + x86/x64, SDK 26100 (several GB)' | |
| 87 | } else { | |
| 88 | 'Visual Studio 2022 Community, C++: MSVC for x86/x64 + ARM64, SDK 26100 (several GB)' | |
| 89 | } | |
| 90 | $rust = if ($IsArm64) { | |
| 91 | 'Rust stable-aarch64-pc-windows-msvc (native), installed and made the default' | |
| 92 | } else { | |
| 93 | 'Rust stable-x86_64-pc-windows-msvc, installed and made the default' | |
| 94 | } | |
| 95 | ||
| 96 | @( | |
| 97 | # --- setup-windows.bat: per-user winget installs --- | |
| 98 | # Not winget: recent Anthropic.ClaudeCode releases are broken through it. | |
| 99 | New-SetupStep ClaudeCode U bat -Text 'Claude Code, native installer: irm https://claude.ai/install.ps1 | iex' | |
| 100 | New-SetupStep Brave U bat -Package Brave.Brave ` | |
| 101 | -X64Only 'no ARM64 build, and Edge is in-box and native' | |
| 102 | New-SetupStep Git U bat -Package Git.Git | |
| 103 | New-SetupStep PowerShell U bat -Package Microsoft.PowerShell | |
| 104 | New-SetupStep ProcessExplorer U bat -Package Microsoft.Sysinternals.ProcessExplorer | |
| 105 | New-SetupStep ProcessMonitor U bat -Package Microsoft.Sysinternals.ProcessMonitor | |
| 106 | New-SetupStep SDelete U bat -Package Microsoft.Sysinternals.SDelete | |
| 107 | New-SetupStep VSCode U bat -Package Microsoft.VisualStudioCode | |
| 108 | New-SetupStep WindowsTerminal U bat -Package Microsoft.WindowsTerminal | |
| 109 | New-SetupStep Python U bat -Package Python.Python.3.13 | |
| 110 | New-SetupStep WinMerge U bat -Package WinMerge.WinMerge ` | |
| 111 | -X64Only 'its ARM64 build is machine-scope only; unelevated winget gets the x64 one' | |
| 112 | New-SetupStep Ninja U bat -Package Ninja-build.Ninja -Text 'native; Visual Studio bundles an x64 one' | |
| 113 | New-SetupStep LLVM U bat -Package LLVM.LLVM -Text 'upstream Clang, a second compiler beside clang-cl' ` | |
| 114 | -X64Only 'this box carries MSVC alone; no second Clang to cross-check' | |
| 115 | New-SetupStep Rustup U bat -Package Rustlang.Rustup -Text 'the Rust toolchain manager, per-user; installs stable too' | |
| 116 | New-SetupStep iPerf3 U bat -Package ar51an.iPerf3 | |
| 117 | # On ARM64 too, like iperf3: an x86-only build, but bound by the link to | |
| 118 | # the device, not the CPU. See the comment beside it in setup-windows.bat. | |
| 119 | New-SetupStep PlatformTools U bat -Package Google.PlatformTools -Text 'Android SDK Platform-Tools: adb, fastboot (an x86 build)' | |
| 120 | New-SetupStep NASM U bat -Package NASM.NASM ` | |
| 121 | -X64Only 'x86/x64-only assembler; MSVC armasm64 is the native one' | |
| 122 | New-SetupStep VirtualBox U bat -Package Oracle.VirtualBox -Text 'for the Windows 7 test VM' ` | |
| 123 | -X64Only 'no ARM64 host build, so no Windows 7 x86 test VM either' ` | |
| 124 | -NotInVM 'the test VMs belong on the physical host, not nested in a guest' | |
| 125 | ||
| 126 | # --- setup-windows.bat: machine-wide installers, despite running unelevated --- | |
| 127 | New-SetupStep DotNetSdk M bat -Admin -Package Microsoft.DotNet.SDK.10 -Text 'for the WiX tooling' | |
| 128 | New-SetupStep CMake M bat -Admin -Package Kitware.CMake -Text 'standalone, so a plain cmake works in any shell' | |
| 129 | New-SetupStep AndroidGPUInspector M bat -Admin -Package Google.AndroidGPUInspector ` | |
| 130 | -X64Only 'x64-only build, and it profiles Android devices, not this box' | |
| 131 | New-SetupStep AndroidStudio M bat -Admin -Package Google.AndroidStudio -Text 'only if missing; it updates itself' ` | |
| 132 | -X64Only 'Google publishes no Windows ARM64 build of Android Studio' | |
| 133 | New-SetupStep OpenCppCoverage M bat -Admin -Package OpenCppCoverage.OpenCppCoverage -Text 'native line coverage' ` | |
| 134 | -X64Only 'it steps x86/x64 instructions, so it cannot cover an ARM64 binary' | |
| 135 | New-SetupStep WiX M bat -Needs DotNetSdk ` | |
| 136 | -Text 'WiX 5.0.2 as a .NET global tool, pinned: 6.0 and later carry a maintenance-fee EULA' | |
| 137 | ||
| 138 | # --- setup-windows-with-uac.ps1: the elevated half --- | |
| 139 | New-SetupStep OpenSshClient E uac -Admin -Text 'OpenSSH Client capability (ssh.exe, and the libcrypto.dll rsync''s ssh.exe needs)' | |
| 140 | New-SetupStep SshAgent E uac -Admin -Needs OpenSshClient -Text 'ssh-agent service: start automatically, and start it now' | |
| 141 | New-SetupStep OpenSshServer E uac -Admin -Text 'OpenSSH Server: sshd automatic and running, inbound TCP 22 on all profiles' | |
| 142 | New-SetupStep Rsync E uac -Admin -Needs OpenSshClient -Text 'rsync and its ssh.exe into C:\Tools\rsync, on the machine PATH' | |
| 143 | New-SetupStep VisualStudio E uac -Admin -Text $vs | |
| 144 | New-SetupStep VsClang E uac -Admin -Needs VisualStudio -Text 'Visual Studio Clang/LLVM toolset (clang-cl, for the ClangCL presets)' ` | |
| 145 | -X64Only 'by choice: this box carries MSVC alone' | |
| 146 | New-SetupStep VsWinXP E uac -Admin -Needs VisualStudio -Text 'Visual Studio v141 + Windows XP targeting toolset' ` | |
| 147 | -X64Only 'v141 has no ARM64-hosted compiler; no XP targeting from Arm' | |
| 148 | New-SetupStep Wdk E uac -Admin -Needs VisualStudio -Text 'Windows Driver Kit 10.0.26100 (pairs with SDK 26100)' ` | |
| 149 | -X64Only 'user-mode box; this WDK will not install beside VS 2026''s SDK' | |
| 150 | New-SetupStep Wpt E uac -Admin -Text 'Windows Performance Toolkit (xperf, wpr, WPA); the Windows ADK if it is missing' | |
| 151 | New-SetupStep VTune E uac -Admin -Text 'Intel VTune Profiler: report whether it is installed (installs nothing)' | |
| 152 | ||
| 153 | # --- setup-windows-no-uac.ps1: per-user configuration --- | |
| 154 | # First, so the PATH it fixes is in place before anything reports on it. | |
| 155 | New-SetupStep LocalBinPath C user -Text '%USERPROFILE%\.local\bin on the user PATH (where Claude Code''s launcher lives)' | |
| 156 | New-SetupStep WinMergePath C user -Needs WinMerge -Text 'WinMerge on the user PATH' ` | |
| 157 | -X64Only 'WinMerge is not installed on ARM64' | |
| 158 | New-SetupStep VsWhere C user -Needs VisualStudio -Text 'vswhere.exe on the user PATH' | |
| 159 | New-SetupStep BinSkim C user -Text 'BinSkim into %LOCALAPPDATA%\Programs\BinSkim, on the user PATH (100+ MB)' ` | |
| 160 | -X64Only 'the NuGet package publishes win-x64 only' | |
| 161 | New-SetupStep GitConfig C user -Needs Git -Text 'global git: identity if filled in, core.sshCommand to a Win32-OpenSSH client' | |
| 162 | New-SetupStep NinjaPath C user -Needs Ninja -Text 'the native ninja first on the user PATH' | |
| 163 | New-SetupStep LlvmPath C user -Needs LLVM -Text 'upstream LLVM on the user PATH' ` | |
| 164 | -X64Only 'LLVM is not installed on ARM64' | |
| 165 | New-SetupStep RustToolchain C user -Needs Rustup, VisualStudio -Text $rust | |
| 166 | New-SetupStep AndroidSdk C user -Needs AndroidStudio ` | |
| 167 | -Text 'Android SDK, per-user: latest platform + build-tools; accepts the SDK licences' ` | |
| 168 | -X64Only 'its Windows build tools are x86/x64 only, as is Android Studio' | |
| 169 | New-SetupStep ArchAudit C user -Text 'architecture audit: native or emulated, for every tool (read-only)' | |
| 170 | ||
| 171 | # --- setup-windows.bat, after both halves --- | |
| 172 | New-SetupStep Pillow C bat -Needs Python -Text 'pip install Pillow into the newest Python' | |
| 173 | ) | |
| 174 | } | |
| 175 | ||
| 176 | function Get-SetupStepBlocker { | |
| 177 | # Why this step cannot run on this machine at all, or $null if it can. | |
| 178 | # Architecture first: an x64-only step is off ARM64 before elevation is even | |
| 179 | # consulted, which is the more useful reason to show. Then the machine | |
| 180 | # itself, then the account. | |
| 181 | param($Step, [bool] $IsArm64, [string] $Elevation, [string] $VirtualMachine) | |
| 182 | if ($IsArm64 -and $Step.X64Only) { return "not on ARM64 - $($Step.X64Only)" } | |
| 183 | if ($VirtualMachine -and $Step.NotInVM) { | |
| 184 | return "not in a virtual machine ($VirtualMachine) - $($Step.NotInVM)" | |
| 185 | } | |
| 186 | if ($Elevation -eq 'NOLUA' -and $Step.Admin) { | |
| 187 | return 'needs an administrator - this account cannot elevate' | |
| 188 | } | |
| 189 | $null | |
| 190 | } | |
| 191 | ||
| 192 | function Get-SetupVirtualMachine { | |
| 193 | # The hypervisor this Windows is running under as a guest, by name, or $null | |
| 194 | # on physical hardware. | |
| 195 | # | |
| 196 | # Read from the SMBIOS strings the firmware reports - Win32_ComputerSystem's | |
| 197 | # Manufacturer and Model - which every common hypervisor fills in with its own | |
| 198 | # name. NOT from HypervisorPresent: that is true on a PHYSICAL box too as soon | |
| 199 | # as Hyper-V, WSL2, Windows Sandbox or virtualization-based security is on, | |
| 200 | # which is most Windows 11 machines - the host OS then runs in the root | |
| 201 | # partition, above a hypervisor, without being anyone's guest. | |
| 202 | # | |
| 203 | # A lookup that fails says "physical": this only ever takes steps away, and a | |
| 204 | # step wrongly offered on a VM is one you can switch off in the plan. | |
| 205 | $cs = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue | |
| 206 | if (-not $cs) { return $null } | |
| 207 | $smbios = "$($cs.Manufacturer) | $($cs.Model)" | |
| 208 | # Hyper-V's pair is matched whole: Microsoft is also the manufacturer of | |
| 209 | # every Surface, and "Virtual Machine" is the model only in a guest. Azure | |
| 210 | # guests report the same pair. | |
| 211 | $known = [ordered]@{ | |
| 212 | 'innotek|VirtualBox' = 'VirtualBox' | |
| 213 | 'VMware' = 'VMware' | |
| 214 | '^Microsoft Corporation \| Virtual Machine$' = 'Hyper-V' | |
| 215 | 'QEMU|KVM|Bochs' = 'QEMU/KVM' | |
| 216 | 'Parallels' = 'Parallels' | |
| 217 | '\bXen\b|HVM domU' = 'Xen' | |
| 218 | 'Amazon EC2' = 'Amazon EC2' | |
| 219 | 'Google Compute Engine' = 'Google Compute Engine' | |
| 220 | } | |
| 221 | foreach ($pattern in $known.Keys) { | |
| 222 | if ($smbios -match $pattern) { return $known[$pattern] } | |
| 223 | } | |
| 224 | $null | |
| 225 | } | |
| 226 | ||
| 227 | function Split-SetupNames { | |
| 228 | # `powershell -File` hands a whole "A,B,C" to an array parameter as ONE | |
| 229 | # string, so split here and accept either spelling. | |
| 230 | param([string[]] $Value) | |
| 231 | @($Value | ForEach-Object { $_ -split '[,\s]+' } | Where-Object { $_ }) | |
| 232 | } | |
| 233 | ||
| 234 | function Resolve-SetupStepNames { | |
| 235 | # Step ids (case-insensitive) or group letters, as typed, to step ids. | |
| 236 | # Anything else is an error: a mistyped -Skip that was quietly ignored would | |
| 237 | # install exactly what you asked it not to. | |
| 238 | param([string[]] $Names, [object[]] $Steps, [hashtable] $Aliases = @{}) | |
| 239 | $groups = @((Get-SetupGroups).Keys) | |
| 240 | foreach ($name in (Split-SetupNames $Names)) { | |
| 241 | if ($Aliases.ContainsKey($name)) { $name = $Aliases[$name] } | |
| 242 | $hit = @($Steps | Where-Object { $_.Id -eq $name }) | |
| 243 | if (-not $hit) { $hit = @($Steps | Where-Object { $_.Group -eq $name }) } | |
| 244 | if ($hit) { $hit.Id; continue } | |
| 245 | # A real group with no steps in this script's share of the catalog. | |
| 246 | if ($groups -contains $name) { continue } | |
| 247 | throw "Unknown step '$name'. The steps are: $($Steps.Id -join ', '). Groups: $($groups -join ', ')." | |
| 248 | } | |
| 249 | } | |
| 250 | ||
| 251 | function Resolve-SetupSelection { | |
| 252 | # The plan: one row per step, in run order, saying whether it is selected | |
| 253 | # and - when it cannot run on this machine at all - why not. | |
| 254 | # | |
| 255 | # -Only $null means "everything that can run"; an empty -Only means nothing. | |
| 256 | param( | |
| 257 | [object[]] $Steps, | |
| 258 | [AllowNull()] [string[]] $Only, | |
| 259 | [string[]] $Skip = @(), | |
| 260 | [hashtable] $Aliases = @{}, | |
| 261 | [bool] $IsArm64, | |
| 262 | [string] $Elevation, | |
| 263 | # The hypervisor's name when this Windows is a guest (see | |
| 264 | # Get-SetupVirtualMachine), $null or empty on physical hardware. | |
| 265 | [string] $VirtualMachine | |
| 266 | ) | |
| 267 | $want = if ($null -eq $Only) { @($Steps.Id) } else { @(Resolve-SetupStepNames $Only $Steps $Aliases) } | |
| 268 | $drop = @(Resolve-SetupStepNames $Skip $Steps $Aliases) | |
| 269 | $named = @(Split-SetupNames $Only | ForEach-Object { if ($Aliases.ContainsKey($_)) { $Aliases[$_] } else { $_ } }) | |
| 270 | foreach ($s in $Steps) { | |
| 271 | $why = Get-SetupStepBlocker $s $IsArm64 $Elevation $VirtualMachine | |
| 272 | if ($why -and $named -contains $s.Id) { | |
| 273 | Write-Warning "$($s.Id) was asked for but cannot run here: $why" | |
| 274 | } | |
| 275 | [pscustomobject]@{ | |
| 276 | Id = $s.Id | |
| 277 | Step = $s | |
| 278 | Why = $why | |
| 279 | Selected = (-not $why) -and ($want -contains $s.Id) -and ($drop -notcontains $s.Id) | |
| 280 | } | |
| 281 | } | |
| 282 | } | |
| 283 | ||
| 284 | function Write-SetupPlan { | |
| 285 | # The short form of the plan, for the two halves: what this script will do | |
| 286 | # and what it will not, before it does any of it. | |
| 287 | param([object[]] $Plan, [string] $Title) | |
| 288 | Write-Host "`n==> $Title" -ForegroundColor Cyan | |
| 289 | foreach ($p in $Plan) { | |
| 290 | if ($p.Selected) { | |
| 291 | Write-Host (' run {0,-14} {1}' -f $p.Id, $p.Step.Text) | |
| 292 | } else { | |
| 293 | $why = if ($p.Why) { $p.Why } else { 'not selected' } | |
| 294 | Write-Host (' skip {0,-14} {1}' -f $p.Id, $why) -ForegroundColor DarkGray | |
| 295 | } | |
| 296 | } | |
| 297 | } | |
| 298 | ||
| 299 | function Confirm-SetupPlan { | |
| 300 | # $true to go ahead, $false if declined, $null if there is no one to ask. | |
| 301 | # | |
| 302 | # Never assumes yes on its own. A run with no console behind it - piped, | |
| 303 | # scheduled, a non-interactive ssh command - stops after printing the plan, | |
| 304 | # and says how to run it unattended (-Yes) on purpose. | |
| 305 | param([switch] $Yes, [int] $Count) | |
| 306 | if ($Yes) { return $true } | |
| 307 | if ([Console]::IsInputRedirected) { | |
| 308 | Write-Host ' No console to ask on, so nothing has been done. Re-run with -Yes to take' -ForegroundColor Yellow | |
| 309 | Write-Host ' the steps marked "run" unattended.' -ForegroundColor Yellow | |
| 310 | return $null | |
| 311 | } | |
| 312 | $answer = Read-Host " Take the $Count step(s) marked run? [Y/n]" | |
| 313 | if ($answer -match '^\s*(y|yes)?\s*$') { return $true } | |
| 314 | Write-Host ' Nothing done.' | |
| 315 | $false | |
| 316 | } |