]> vilimpoc.org git repositories - dotfiles/blame - setup-windows-plan.ps1
dotfiles: register UI.Xaml 2.8 for the account before installing Terminal
[dotfiles] / setup-windows-plan.ps1
CommitLineData
0b412520
MV
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
325b494c 21 powershell -File .\setup-windows-plan.ps1 -WhatIf -VirtualMachine Hyper-V
0b412520
MV
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()]
34param(
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,
325b494c
MV
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,
0b412520
MV
51 [string] $OutFile
52)
53
54$ErrorActionPreference = 'Stop'
55. (Join-Path $PSScriptRoot 'setup-windows-steps.ps1')
56
57function Show-Usage {
58 Write-Host @'
59
60setup-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
78function 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
87function 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
111function 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"
325b494c 117 Write-Host " machine: $VmLabel"
0b412520
MV
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
152function 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# ---------------------------------------------------------------------------
194if ($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.
202if (-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'
207if (-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
325b494c
MV
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.
221if (-not $VirtualMachine) { $VirtualMachine = Get-SetupVirtualMachine }
222if ($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
0b412520
MV
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}
234switch ($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')
250try {
251 $plan = @(Resolve-SetupSelection -Steps (Get-SetupCatalog -IsArm64 $IsArm64) `
252 -Only $(if ($onlyGiven) { $Only } else { $null }) -Skip $Skip `
325b494c 253 -IsArm64 $IsArm64 -Elevation $Elevation -VirtualMachine $VirtualMachine)
0b412520
MV
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
265foreach ($p in $plan) {
266 $num = $null
267 if (-not $p.Why) { $n++; $num = $n }
268 $p | Add-Member -NotePropertyName Number -NotePropertyValue $num
269}
270
271Show-Plan $plan
272
273if ($WhatIf) {
274 Write-Host ' -WhatIf: stopping here. Nothing has been done.'
275 Write-Handoff -Plan $plan -Run $false
276 exit 0
277}
278
279if (-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
304if ($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}
309Write-Host ''
310Write-Host " Going ahead with $count step(s). Anything not marked [x] will not be done." -ForegroundColor Green
311Write-Handoff -Plan $plan -Run $true
312exit 0