2 setup-windows-plan.ps1 - say what setup-windows.bat is about to do, and let
3 you choose, before any of it happens.
5 setup-windows.bat runs this first and passes its own arguments through:
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
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.
18 Run it on its own to preview the plan for another kind of box:
20 powershell -File .\setup-windows-plan.ps1 -WhatIf -HostArch ARM64 -Elevation NOLUA
21 powershell -File .\setup-windows-plan.ps1 -WhatIf -VirtualMachine Hyper-V
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
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.
35 # Step names or group letters, comma-separated. -Only starts from nothing,
36 # -Skip from everything; both can be adjusted interactively afterwards.
38 [string[]] $Skip = @(),
39 # Take the plan as it stands, without asking.
41 # Show the plan and stop.
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.
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,
54 $ErrorActionPreference = 'Stop'
55 . (Join-Path $PSScriptRoot 'setup-windows-steps.ps1')
60 setup-windows.bat [-WhatIf] [-Yes] [-Only <steps>] [-Skip <steps>]
62 Shows everything it is about to do and lets you choose before doing any of it.
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)
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.
73 setup-windows.bat -Skip VirtualBox,Brave
74 setup-windows.bat -Only E -Yes
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
84 $Value -replace '"', "'" -replace '%', '%%' -replace '[^\x20-\x7E]', '?'
87 function Write-Handoff {
88 param([object[]] $Plan, [bool] $Run, [int] $Rc = 0)
89 if (-not $OutFile) { return }
91 '@rem Written by setup-windows-plan.ps1 for setup-windows.bat, which deletes it.'
92 "set `"PLAN_RUN=$([int]$Run)`""
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)`"" }
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
112 param([object[]] $Plan, [string[]] $Messages = @())
113 $groups = Get-SetupGroups
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])" }
123 Write-Host $head -ForegroundColor Cyan
124 foreach ($p in ($Plan | Where-Object { $_.Step.Group -eq $g })) {
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)
130 Write-Host (' [ ] {0,2} {1,-19} {2}' -f $p.Number, $p.Id, $p.Step.Text) -ForegroundColor DarkGray
135 # Switched-off prerequisites. Advisory: the thing may well be installed
136 # already, which is the usual reason for switching its install off.
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
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 }
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') }
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 }
172 $step = $Plan | Where-Object { $_.Id -eq $tok } | Select-Object -First 1
174 if ($step.Why) { "$($step.Id) cannot run here: $($step.Why)" }
175 else { $step.Selected = -not $step.Selected }
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 }
187 "Not a step number, step name or group letter: $tok"
191 # ---------------------------------------------------------------------------
193 # ---------------------------------------------------------------------------
196 Write-Handoff -Plan @() -Run $false
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 }
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)) {
211 } elseif ((Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -ErrorAction SilentlyContinue).EnableLUA -eq 0) {
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 [-]"
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 }
234 switch ($Elevation) {
236 $ElevLabel = 'already elevated, so the elevated half runs directly with no prompt'
237 $GroupNotes = @{ E = 'runs in this window' }
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' }
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' }
249 $onlyGiven = $PSBoundParameters.ContainsKey('Only')
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)
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.'
262 # Numbers for the steps that can run, in plan order. The ones that cannot get
263 # none - there is nothing to switch.
265 foreach ($p in $plan) {
267 if (-not $p.Why) { $n++; $num = $n }
268 $p | Add-Member -NotePropertyName Number -NotePropertyValue $num
274 Write-Host ' -WhatIf: stopping here. Nothing has been done.'
275 Write-Handoff -Plan $plan -Run $false
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
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
298 $messages = @(Update-Selection $plan $line)
299 Show-Plan $plan $messages
303 $count = @($plan | Where-Object Selected).Count
305 Write-Host ' Nothing selected, so there is nothing to do.'
306 Write-Handoff -Plan $plan -Run $false
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