]>
| Commit | Line | Data |
|---|---|---|
| 1 | <# | |
| 2 | setup-windows-plan.ps1 - say what setup-windows.bat is about to do, and let | |
| 3 | you choose, before any of it happens. | |
| 4 | ||
| 5 | setup-windows.bat runs this first and passes its own arguments through: | |
| 6 | ||
| 7 | setup-windows.bat show the plan, ask, then run it | |
| 8 | setup-windows.bat -WhatIf show the plan and stop | |
| 9 | setup-windows.bat -Skip VirtualBox,Brave start with those switched off | |
| 10 | setup-windows.bat -Only E just the elevated half | |
| 11 | setup-windows.bat -Yes no questions, for unattended runs | |
| 12 | setup-windows.bat /? this usage | |
| 13 | ||
| 14 | Interactively, the plan is a numbered list: switch steps on and off, then press | |
| 15 | Enter to run what is left. Nothing is installed or changed until you do, and | |
| 16 | with no console to ask on it stops after printing the plan unless given -Yes. | |
| 17 | ||
| 18 | Run it on its own to preview the plan for another kind of box: | |
| 19 | ||
| 20 | powershell -File .\setup-windows-plan.ps1 -WhatIf -HostArch ARM64 -Elevation NOLUA | |
| 21 | powershell -File .\setup-windows-plan.ps1 -WhatIf -VirtualMachine Hyper-V | |
| 22 | ||
| 23 | The steps, and where each one is unavailable and why, come from | |
| 24 | setup-windows-steps.ps1; this script only presents them and records the | |
| 25 | choice. It writes that to -OutFile as `set` lines for setup-windows.bat to | |
| 26 | call: | |
| 27 | PLAN_RUN, PLAN_RC 1 to go on; otherwise stop and exit with PLAN_RC | |
| 28 | STEP_<Id>, WHY_<Id> 1 or 0 for each step the batch file takes, and why not | |
| 29 | PKG_<Id> the winget package the plan showed for that step | |
| 30 | UAC_STEPS, USER_STEPS comma lists for the two halves' -Only | |
| 31 | No -OutFile (run on its own) means nothing is written. | |
| 32 | #> | |
| 33 | [CmdletBinding()] | |
| 34 | param( | |
| 35 | # Step names or group letters, comma-separated. -Only starts from nothing, | |
| 36 | # -Skip from everything; both can be adjusted interactively afterwards. | |
| 37 | [string[]] $Only, | |
| 38 | [string[]] $Skip = @(), | |
| 39 | # Take the plan as it stands, without asking. | |
| 40 | [switch] $Yes, | |
| 41 | # Show the plan and stop. | |
| 42 | [switch] $WhatIf, | |
| 43 | [switch] $Help, | |
| 44 | # Supplied by setup-windows.bat, which has already worked both out. Detected | |
| 45 | # here the same way when this script is run on its own. | |
| 46 | [string] $HostArch, | |
| 47 | [ValidateSet('ALREADY', 'PROMPT', 'NOLUA')] [string] $Elevation, | |
| 48 | # The hypervisor this Windows is a guest of, or 'none' for physical | |
| 49 | # hardware. Detected when not given; setup-windows.bat leaves it to that. | |
| 50 | [string] $VirtualMachine, | |
| 51 | [string] $OutFile | |
| 52 | ) | |
| 53 | ||
| 54 | $ErrorActionPreference = 'Stop' | |
| 55 | . (Join-Path $PSScriptRoot 'setup-windows-steps.ps1') | |
| 56 | ||
| 57 | function Show-Usage { | |
| 58 | Write-Host @' | |
| 59 | ||
| 60 | setup-windows.bat [-WhatIf] [-Yes] [-Only <steps>] [-Skip <steps>] | |
| 61 | ||
| 62 | Shows everything it is about to do and lets you choose before doing any of it. | |
| 63 | ||
| 64 | -WhatIf show the plan for this machine, then stop | |
| 65 | -Only <steps> start from just these steps | |
| 66 | -Skip <steps> start from everything except these | |
| 67 | -Yes do not ask: run the plan as it stands (for unattended runs) | |
| 68 | ||
| 69 | <steps> is a comma-separated list of step names and group letters, as the | |
| 70 | plan shows them: U per-user installs, M machine-wide installs, | |
| 71 | E the elevated half, C per-user configuration. | |
| 72 | ||
| 73 | setup-windows.bat -Skip VirtualBox,Brave | |
| 74 | setup-windows.bat -Only E -Yes | |
| 75 | '@ | |
| 76 | } | |
| 77 | ||
| 78 | function ConvertTo-CmdValue([string] $Value) { | |
| 79 | # Safe inside `set "NAME=..."` in a called .cmd: a quote would end the | |
| 80 | # assignment early and % would expand. cmd's other special characters are | |
| 81 | # inert inside the quotes, and :want prints the value with delayed expansion, | |
| 82 | # which never re-parses it. ASCII only, as cmd reads the file in the OEM | |
| 83 | # code page. | |
| 84 | $Value -replace '"', "'" -replace '%', '%%' -replace '[^\x20-\x7E]', '?' | |
| 85 | } | |
| 86 | ||
| 87 | function Write-Handoff { | |
| 88 | param([object[]] $Plan, [bool] $Run, [int] $Rc = 0) | |
| 89 | if (-not $OutFile) { return } | |
| 90 | $lines = @( | |
| 91 | '@rem Written by setup-windows-plan.ps1 for setup-windows.bat, which deletes it.' | |
| 92 | "set `"PLAN_RUN=$([int]$Run)`"" | |
| 93 | "set `"PLAN_RC=$Rc`"" | |
| 94 | ) | |
| 95 | # Every 'bat' step gets a line whether or not it runs: :want treats a step | |
| 96 | # with no STEP_ line at all as a catalog/batch-file mismatch. WHY_ is empty | |
| 97 | # for a step you switched off - the plan already said so, and repeating it | |
| 98 | # at every call site just buries the skips that have a reason. | |
| 99 | foreach ($p in ($Plan | Where-Object { $_.Step.Runner -eq 'bat' })) { | |
| 100 | $lines += "set `"STEP_$($p.Id)=$([int]$p.Selected)`"" | |
| 101 | $lines += "set `"WHY_$($p.Id)=$(ConvertTo-CmdValue $p.Why)`"" | |
| 102 | if ($p.Step.Package) { $lines += "set `"PKG_$($p.Id)=$($p.Step.Package)`"" } | |
| 103 | } | |
| 104 | $uac = @($Plan | Where-Object { $_.Step.Runner -eq 'uac' -and $_.Selected } | ForEach-Object { $_.Id }) | |
| 105 | $user = @($Plan | Where-Object { $_.Step.Runner -eq 'user' -and $_.Selected } | ForEach-Object { $_.Id }) | |
| 106 | $lines += "set `"UAC_STEPS=$($uac -join ',')`"" | |
| 107 | $lines += "set `"USER_STEPS=$($user -join ',')`"" | |
| 108 | Set-Content -Path $OutFile -Value $lines -Encoding ascii | |
| 109 | } | |
| 110 | ||
| 111 | function Show-Plan { | |
| 112 | param([object[]] $Plan, [string[]] $Messages = @()) | |
| 113 | $groups = Get-SetupGroups | |
| 114 | Write-Host '' | |
| 115 | Write-Host 'Setup plan - nothing has been done yet' -ForegroundColor Cyan | |
| 116 | Write-Host " host: $ArchLabel" | |
| 117 | Write-Host " machine: $VmLabel" | |
| 118 | Write-Host " elevation: $ElevLabel" | |
| 119 | foreach ($g in $groups.Keys) { | |
| 120 | $head = " $g $($groups[$g])" | |
| 121 | if ($GroupNotes[$g]) { $head += " - $($GroupNotes[$g])" } | |
| 122 | Write-Host '' | |
| 123 | Write-Host $head -ForegroundColor Cyan | |
| 124 | foreach ($p in ($Plan | Where-Object { $_.Step.Group -eq $g })) { | |
| 125 | if ($p.Why) { | |
| 126 | Write-Host (' [-] {0,-19} {1}' -f $p.Id, $p.Why) -ForegroundColor DarkGray | |
| 127 | } elseif ($p.Selected) { | |
| 128 | Write-Host (' [x] {0,2} {1,-19} {2}' -f $p.Number, $p.Id, $p.Step.Text) | |
| 129 | } else { | |
| 130 | Write-Host (' [ ] {0,2} {1,-19} {2}' -f $p.Number, $p.Id, $p.Step.Text) -ForegroundColor DarkGray | |
| 131 | } | |
| 132 | } | |
| 133 | } | |
| 134 | ||
| 135 | # Switched-off prerequisites. Advisory: the thing may well be installed | |
| 136 | # already, which is the usual reason for switching its install off. | |
| 137 | Write-Host '' | |
| 138 | foreach ($p in ($Plan | Where-Object Selected)) { | |
| 139 | foreach ($need in $p.Step.Needs) { | |
| 140 | $dep = $Plan | Where-Object { $_.Id -eq $need } | Select-Object -First 1 | |
| 141 | if ($dep -and -not $dep.Selected -and -not $dep.Why) { | |
| 142 | Write-Host " note: $($p.Id) builds on $need, which is switched off - fine if it is already installed." -ForegroundColor Yellow | |
| 143 | } | |
| 144 | } | |
| 145 | } | |
| 146 | $on = @($Plan | Where-Object Selected).Count | |
| 147 | $can = @($Plan | Where-Object { -not $_.Why }).Count | |
| 148 | Write-Host " $on of $can steps selected." | |
| 149 | foreach ($m in $Messages) { Write-Host " $m" -ForegroundColor Yellow } | |
| 150 | } | |
| 151 | ||
| 152 | function Update-Selection { | |
| 153 | # Apply one line of input to the plan. Returns a message for each piece of it | |
| 154 | # that could not be used, to show under the redrawn plan. | |
| 155 | param([object[]] $Plan, [string] $Line) | |
| 156 | $avail = @($Plan | Where-Object { -not $_.Why }) | |
| 157 | $groups = @((Get-SetupGroups).Keys) | |
| 158 | foreach ($tok in ($Line -split '[,\s]+' | Where-Object { $_ })) { | |
| 159 | if ($tok -eq 'all' -or $tok -eq 'none') { | |
| 160 | foreach ($p in $avail) { $p.Selected = ($tok -eq 'all') } | |
| 161 | continue | |
| 162 | } | |
| 163 | if ($tok -match '^(\d+)(?:-(\d+))?$') { | |
| 164 | $lo = [int]$Matches[1] | |
| 165 | $hi = if ($Matches[2]) { [int]$Matches[2] } else { $lo } | |
| 166 | if ($hi -lt $lo) { $lo, $hi = $hi, $lo } | |
| 167 | $rows = @($avail | Where-Object { $_.Number -ge $lo -and $_.Number -le $hi }) | |
| 168 | if (-not $rows) { "There is no step $tok."; continue } | |
| 169 | foreach ($p in $rows) { $p.Selected = -not $p.Selected } | |
| 170 | continue | |
| 171 | } | |
| 172 | $step = $Plan | Where-Object { $_.Id -eq $tok } | Select-Object -First 1 | |
| 173 | if ($step) { | |
| 174 | if ($step.Why) { "$($step.Id) cannot run here: $($step.Why)" } | |
| 175 | else { $step.Selected = -not $step.Selected } | |
| 176 | continue | |
| 177 | } | |
| 178 | if ($groups -contains $tok) { | |
| 179 | # A group switches as a unit: anything on turns it all off, otherwise | |
| 180 | # it all comes on. Toggling each member would just scramble it. | |
| 181 | $members = @($avail | Where-Object { $_.Step.Group -eq $tok }) | |
| 182 | if (-not $members) { "Nothing in group $($tok.ToUpper()) can run here."; continue } | |
| 183 | $anyOn = @($members | Where-Object Selected).Count -gt 0 | |
| 184 | foreach ($p in $members) { $p.Selected = -not $anyOn } | |
| 185 | continue | |
| 186 | } | |
| 187 | "Not a step number, step name or group letter: $tok" | |
| 188 | } | |
| 189 | } | |
| 190 | ||
| 191 | # --------------------------------------------------------------------------- | |
| 192 | # Main | |
| 193 | # --------------------------------------------------------------------------- | |
| 194 | if ($Help) { | |
| 195 | Show-Usage | |
| 196 | Write-Handoff -Plan @() -Run $false | |
| 197 | exit 0 | |
| 198 | } | |
| 199 | ||
| 200 | # Same sources, in the same order, as setup-windows.bat - see the comments there | |
| 201 | # for why the registry wins over the environment. | |
| 202 | if (-not $HostArch) { | |
| 203 | $HostArch = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -ErrorAction SilentlyContinue).PROCESSOR_ARCHITECTURE | |
| 204 | if (-not $HostArch) { $HostArch = $env:PROCESSOR_ARCHITECTURE } | |
| 205 | } | |
| 206 | $IsArm64 = $HostArch -eq 'ARM64' | |
| 207 | if (-not $Elevation) { | |
| 208 | $id = [Security.Principal.WindowsIdentity]::GetCurrent() | |
| 209 | $Elevation = if (([Security.Principal.WindowsPrincipal]$id).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { | |
| 210 | 'ALREADY' | |
| 211 | } elseif ((Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -ErrorAction SilentlyContinue).EnableLUA -eq 0) { | |
| 212 | 'NOLUA' | |
| 213 | } else { | |
| 214 | 'PROMPT' | |
| 215 | } | |
| 216 | } | |
| 217 | ||
| 218 | # A guest VM is not somewhere to put a hypervisor of its own: VirtualBox, for | |
| 219 | # one, is for the test VMs, which belong on the physical host. Detected once | |
| 220 | # here; the steps that care are -NotInVM in setup-windows-steps.ps1. | |
| 221 | if (-not $VirtualMachine) { $VirtualMachine = Get-SetupVirtualMachine } | |
| 222 | if ($VirtualMachine -eq 'none') { $VirtualMachine = $null } | |
| 223 | $VmLabel = if ($VirtualMachine) { | |
| 224 | "virtual machine ($VirtualMachine) - steps meant for the physical host are marked [-]" | |
| 225 | } else { | |
| 226 | 'physical hardware' | |
| 227 | } | |
| 228 | ||
| 229 | $ArchLabel = switch ($HostArch) { | |
| 230 | 'ARM64' { 'ARM64 - the lean build box, one C/C++ compiler; x64-only steps are marked [-]' } | |
| 231 | 'AMD64' { 'x64 - the full workstation' } | |
| 232 | default { $HostArch } | |
| 233 | } | |
| 234 | switch ($Elevation) { | |
| 235 | 'ALREADY' { | |
| 236 | $ElevLabel = 'already elevated, so the elevated half runs directly with no prompt' | |
| 237 | $GroupNotes = @{ E = 'runs in this window' } | |
| 238 | } | |
| 239 | 'PROMPT' { | |
| 240 | $ElevLabel = 'not elevated; expect a UAC prompt partway through, for the elevated half' | |
| 241 | $GroupNotes = @{ M = 'each installer may ask for elevation itself'; E = 'one UAC prompt, runs in its own window' } | |
| 242 | } | |
| 243 | 'NOLUA' { | |
| 244 | $ElevLabel = 'cannot elevate: UAC is off and this is not an administrator' | |
| 245 | $GroupNotes = @{ M = 'these installers need an administrator'; E = 'not possible from this account' } | |
| 246 | } | |
| 247 | } | |
| 248 | ||
| 249 | $onlyGiven = $PSBoundParameters.ContainsKey('Only') | |
| 250 | try { | |
| 251 | $plan = @(Resolve-SetupSelection -Steps (Get-SetupCatalog -IsArm64 $IsArm64) ` | |
| 252 | -Only $(if ($onlyGiven) { $Only } else { $null }) -Skip $Skip ` | |
| 253 | -IsArm64 $IsArm64 -Elevation $Elevation -VirtualMachine $VirtualMachine) | |
| 254 | } catch { | |
| 255 | # A mistyped -Only / -Skip. Said plainly, and no hand-off file, so the batch | |
| 256 | # file stops before doing anything. | |
| 257 | Write-Host "`n $($_.Exception.Message)" -ForegroundColor Red | |
| 258 | Write-Host ' Run setup-windows.bat -WhatIf to see the plan and its step names.' | |
| 259 | exit 1 | |
| 260 | } | |
| 261 | ||
| 262 | # Numbers for the steps that can run, in plan order. The ones that cannot get | |
| 263 | # none - there is nothing to switch. | |
| 264 | $n = 0 | |
| 265 | foreach ($p in $plan) { | |
| 266 | $num = $null | |
| 267 | if (-not $p.Why) { $n++; $num = $n } | |
| 268 | $p | Add-Member -NotePropertyName Number -NotePropertyValue $num | |
| 269 | } | |
| 270 | ||
| 271 | Show-Plan $plan | |
| 272 | ||
| 273 | if ($WhatIf) { | |
| 274 | Write-Host ' -WhatIf: stopping here. Nothing has been done.' | |
| 275 | Write-Handoff -Plan $plan -Run $false | |
| 276 | exit 0 | |
| 277 | } | |
| 278 | ||
| 279 | if (-not $Yes) { | |
| 280 | if ([Console]::IsInputRedirected) { | |
| 281 | Write-Host ' No console to ask on, so nothing has been done. Re-run with -Yes to take' -ForegroundColor Yellow | |
| 282 | Write-Host ' the selected steps unattended.' -ForegroundColor Yellow | |
| 283 | Write-Handoff -Plan $plan -Run $false -Rc 2 | |
| 284 | exit 0 | |
| 285 | } | |
| 286 | while ($true) { | |
| 287 | Write-Host '' | |
| 288 | Write-Host ' Enter runs the selected steps. To change them, type step numbers or ranges' | |
| 289 | Write-Host ' (3 5-7), step names, or a group letter (U M E C) to switch them on or off;' | |
| 290 | Write-Host " 'all' or 'none' for everything; 'q' to quit without doing anything." | |
| 291 | $line = "$(Read-Host ' Enter to run, or change')".Trim() | |
| 292 | if (-not $line) { break } | |
| 293 | if ($line -match '^(q|quit|exit)$') { | |
| 294 | Write-Host ' Nothing done.' | |
| 295 | Write-Handoff -Plan $plan -Run $false | |
| 296 | exit 0 | |
| 297 | } | |
| 298 | $messages = @(Update-Selection $plan $line) | |
| 299 | Show-Plan $plan $messages | |
| 300 | } | |
| 301 | } | |
| 302 | ||
| 303 | $count = @($plan | Where-Object Selected).Count | |
| 304 | if ($count -eq 0) { | |
| 305 | Write-Host ' Nothing selected, so there is nothing to do.' | |
| 306 | Write-Handoff -Plan $plan -Run $false | |
| 307 | exit 0 | |
| 308 | } | |
| 309 | Write-Host '' | |
| 310 | Write-Host " Going ahead with $count step(s). Anything not marked [x] will not be done." -ForegroundColor Green | |
| 311 | Write-Handoff -Plan $plan -Run $true | |
| 312 | exit 0 |