]> vilimpoc.org git repositories - dotfiles/blob - setup-windows-plan.ps1
dotfiles: register UI.Xaml 2.8 for the account before installing Terminal
[dotfiles] / setup-windows-plan.ps1
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