<# setup-windows-plan.ps1 - say what setup-windows.bat is about to do, and let you choose, before any of it happens. setup-windows.bat runs this first and passes its own arguments through: setup-windows.bat show the plan, ask, then run it setup-windows.bat -WhatIf show the plan and stop setup-windows.bat -Skip VirtualBox,Brave start with those switched off setup-windows.bat -Only E just the elevated half setup-windows.bat -Yes no questions, for unattended runs setup-windows.bat /? this usage Interactively, the plan is a numbered list: switch steps on and off, then press Enter to run what is left. Nothing is installed or changed until you do, and with no console to ask on it stops after printing the plan unless given -Yes. Run it on its own to preview the plan for another kind of box: powershell -File .\setup-windows-plan.ps1 -WhatIf -HostArch ARM64 -Elevation NOLUA The steps, and where each one is unavailable and why, come from setup-windows-steps.ps1; this script only presents them and records the choice. It writes that to -OutFile as `set` lines for setup-windows.bat to call: PLAN_RUN, PLAN_RC 1 to go on; otherwise stop and exit with PLAN_RC STEP_, WHY_ 1 or 0 for each step the batch file takes, and why not PKG_ the winget package the plan showed for that step UAC_STEPS, USER_STEPS comma lists for the two halves' -Only No -OutFile (run on its own) means nothing is written. #> [CmdletBinding()] param( # Step names or group letters, comma-separated. -Only starts from nothing, # -Skip from everything; both can be adjusted interactively afterwards. [string[]] $Only, [string[]] $Skip = @(), # Take the plan as it stands, without asking. [switch] $Yes, # Show the plan and stop. [switch] $WhatIf, [switch] $Help, # Supplied by setup-windows.bat, which has already worked both out. Detected # here the same way when this script is run on its own. [string] $HostArch, [ValidateSet('ALREADY', 'PROMPT', 'NOLUA')] [string] $Elevation, [string] $OutFile ) $ErrorActionPreference = 'Stop' . (Join-Path $PSScriptRoot 'setup-windows-steps.ps1') function Show-Usage { Write-Host @' setup-windows.bat [-WhatIf] [-Yes] [-Only ] [-Skip ] Shows everything it is about to do and lets you choose before doing any of it. -WhatIf show the plan for this machine, then stop -Only start from just these steps -Skip start from everything except these -Yes do not ask: run the plan as it stands (for unattended runs) is a comma-separated list of step names and group letters, as the plan shows them: U per-user installs, M machine-wide installs, E the elevated half, C per-user configuration. setup-windows.bat -Skip VirtualBox,Brave setup-windows.bat -Only E -Yes '@ } function ConvertTo-CmdValue([string] $Value) { # Safe inside `set "NAME=..."` in a called .cmd: a quote would end the # assignment early and % would expand. cmd's other special characters are # inert inside the quotes, and :want prints the value with delayed expansion, # which never re-parses it. ASCII only, as cmd reads the file in the OEM # code page. $Value -replace '"', "'" -replace '%', '%%' -replace '[^\x20-\x7E]', '?' } function Write-Handoff { param([object[]] $Plan, [bool] $Run, [int] $Rc = 0) if (-not $OutFile) { return } $lines = @( '@rem Written by setup-windows-plan.ps1 for setup-windows.bat, which deletes it.' "set `"PLAN_RUN=$([int]$Run)`"" "set `"PLAN_RC=$Rc`"" ) # Every 'bat' step gets a line whether or not it runs: :want treats a step # with no STEP_ line at all as a catalog/batch-file mismatch. WHY_ is empty # for a step you switched off - the plan already said so, and repeating it # at every call site just buries the skips that have a reason. foreach ($p in ($Plan | Where-Object { $_.Step.Runner -eq 'bat' })) { $lines += "set `"STEP_$($p.Id)=$([int]$p.Selected)`"" $lines += "set `"WHY_$($p.Id)=$(ConvertTo-CmdValue $p.Why)`"" if ($p.Step.Package) { $lines += "set `"PKG_$($p.Id)=$($p.Step.Package)`"" } } $uac = @($Plan | Where-Object { $_.Step.Runner -eq 'uac' -and $_.Selected } | ForEach-Object { $_.Id }) $user = @($Plan | Where-Object { $_.Step.Runner -eq 'user' -and $_.Selected } | ForEach-Object { $_.Id }) $lines += "set `"UAC_STEPS=$($uac -join ',')`"" $lines += "set `"USER_STEPS=$($user -join ',')`"" Set-Content -Path $OutFile -Value $lines -Encoding ascii } function Show-Plan { param([object[]] $Plan, [string[]] $Messages = @()) $groups = Get-SetupGroups Write-Host '' Write-Host 'Setup plan - nothing has been done yet' -ForegroundColor Cyan Write-Host " host: $ArchLabel" Write-Host " elevation: $ElevLabel" foreach ($g in $groups.Keys) { $head = " $g $($groups[$g])" if ($GroupNotes[$g]) { $head += " - $($GroupNotes[$g])" } Write-Host '' Write-Host $head -ForegroundColor Cyan foreach ($p in ($Plan | Where-Object { $_.Step.Group -eq $g })) { if ($p.Why) { Write-Host (' [-] {0,-19} {1}' -f $p.Id, $p.Why) -ForegroundColor DarkGray } elseif ($p.Selected) { Write-Host (' [x] {0,2} {1,-19} {2}' -f $p.Number, $p.Id, $p.Step.Text) } else { Write-Host (' [ ] {0,2} {1,-19} {2}' -f $p.Number, $p.Id, $p.Step.Text) -ForegroundColor DarkGray } } } # Switched-off prerequisites. Advisory: the thing may well be installed # already, which is the usual reason for switching its install off. Write-Host '' foreach ($p in ($Plan | Where-Object Selected)) { foreach ($need in $p.Step.Needs) { $dep = $Plan | Where-Object { $_.Id -eq $need } | Select-Object -First 1 if ($dep -and -not $dep.Selected -and -not $dep.Why) { Write-Host " note: $($p.Id) builds on $need, which is switched off - fine if it is already installed." -ForegroundColor Yellow } } } $on = @($Plan | Where-Object Selected).Count $can = @($Plan | Where-Object { -not $_.Why }).Count Write-Host " $on of $can steps selected." foreach ($m in $Messages) { Write-Host " $m" -ForegroundColor Yellow } } function Update-Selection { # Apply one line of input to the plan. Returns a message for each piece of it # that could not be used, to show under the redrawn plan. param([object[]] $Plan, [string] $Line) $avail = @($Plan | Where-Object { -not $_.Why }) $groups = @((Get-SetupGroups).Keys) foreach ($tok in ($Line -split '[,\s]+' | Where-Object { $_ })) { if ($tok -eq 'all' -or $tok -eq 'none') { foreach ($p in $avail) { $p.Selected = ($tok -eq 'all') } continue } if ($tok -match '^(\d+)(?:-(\d+))?$') { $lo = [int]$Matches[1] $hi = if ($Matches[2]) { [int]$Matches[2] } else { $lo } if ($hi -lt $lo) { $lo, $hi = $hi, $lo } $rows = @($avail | Where-Object { $_.Number -ge $lo -and $_.Number -le $hi }) if (-not $rows) { "There is no step $tok."; continue } foreach ($p in $rows) { $p.Selected = -not $p.Selected } continue } $step = $Plan | Where-Object { $_.Id -eq $tok } | Select-Object -First 1 if ($step) { if ($step.Why) { "$($step.Id) cannot run here: $($step.Why)" } else { $step.Selected = -not $step.Selected } continue } if ($groups -contains $tok) { # A group switches as a unit: anything on turns it all off, otherwise # it all comes on. Toggling each member would just scramble it. $members = @($avail | Where-Object { $_.Step.Group -eq $tok }) if (-not $members) { "Nothing in group $($tok.ToUpper()) can run here."; continue } $anyOn = @($members | Where-Object Selected).Count -gt 0 foreach ($p in $members) { $p.Selected = -not $anyOn } continue } "Not a step number, step name or group letter: $tok" } } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- if ($Help) { Show-Usage Write-Handoff -Plan @() -Run $false exit 0 } # Same sources, in the same order, as setup-windows.bat - see the comments there # for why the registry wins over the environment. if (-not $HostArch) { $HostArch = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -ErrorAction SilentlyContinue).PROCESSOR_ARCHITECTURE if (-not $HostArch) { $HostArch = $env:PROCESSOR_ARCHITECTURE } } $IsArm64 = $HostArch -eq 'ARM64' if (-not $Elevation) { $id = [Security.Principal.WindowsIdentity]::GetCurrent() $Elevation = if (([Security.Principal.WindowsPrincipal]$id).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 'ALREADY' } elseif ((Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -ErrorAction SilentlyContinue).EnableLUA -eq 0) { 'NOLUA' } else { 'PROMPT' } } $ArchLabel = switch ($HostArch) { 'ARM64' { 'ARM64 - the lean build box, one C/C++ compiler; x64-only steps are marked [-]' } 'AMD64' { 'x64 - the full workstation' } default { $HostArch } } switch ($Elevation) { 'ALREADY' { $ElevLabel = 'already elevated, so the elevated half runs directly with no prompt' $GroupNotes = @{ E = 'runs in this window' } } 'PROMPT' { $ElevLabel = 'not elevated; expect a UAC prompt partway through, for the elevated half' $GroupNotes = @{ M = 'each installer may ask for elevation itself'; E = 'one UAC prompt, runs in its own window' } } 'NOLUA' { $ElevLabel = 'cannot elevate: UAC is off and this is not an administrator' $GroupNotes = @{ M = 'these installers need an administrator'; E = 'not possible from this account' } } } $onlyGiven = $PSBoundParameters.ContainsKey('Only') try { $plan = @(Resolve-SetupSelection -Steps (Get-SetupCatalog -IsArm64 $IsArm64) ` -Only $(if ($onlyGiven) { $Only } else { $null }) -Skip $Skip ` -IsArm64 $IsArm64 -Elevation $Elevation) } catch { # A mistyped -Only / -Skip. Said plainly, and no hand-off file, so the batch # file stops before doing anything. Write-Host "`n $($_.Exception.Message)" -ForegroundColor Red Write-Host ' Run setup-windows.bat -WhatIf to see the plan and its step names.' exit 1 } # Numbers for the steps that can run, in plan order. The ones that cannot get # none - there is nothing to switch. $n = 0 foreach ($p in $plan) { $num = $null if (-not $p.Why) { $n++; $num = $n } $p | Add-Member -NotePropertyName Number -NotePropertyValue $num } Show-Plan $plan if ($WhatIf) { Write-Host ' -WhatIf: stopping here. Nothing has been done.' Write-Handoff -Plan $plan -Run $false exit 0 } if (-not $Yes) { if ([Console]::IsInputRedirected) { Write-Host ' No console to ask on, so nothing has been done. Re-run with -Yes to take' -ForegroundColor Yellow Write-Host ' the selected steps unattended.' -ForegroundColor Yellow Write-Handoff -Plan $plan -Run $false -Rc 2 exit 0 } while ($true) { Write-Host '' Write-Host ' Enter runs the selected steps. To change them, type step numbers or ranges' Write-Host ' (3 5-7), step names, or a group letter (U M E C) to switch them on or off;' Write-Host " 'all' or 'none' for everything; 'q' to quit without doing anything." $line = "$(Read-Host ' Enter to run, or change')".Trim() if (-not $line) { break } if ($line -match '^(q|quit|exit)$') { Write-Host ' Nothing done.' Write-Handoff -Plan $plan -Run $false exit 0 } $messages = @(Update-Selection $plan $line) Show-Plan $plan $messages } } $count = @($plan | Where-Object Selected).Count if ($count -eq 0) { Write-Host ' Nothing selected, so there is nothing to do.' Write-Handoff -Plan $plan -Run $false exit 0 } Write-Host '' Write-Host " Going ahead with $count step(s). Anything not marked [x] will not be done." -ForegroundColor Green Write-Handoff -Plan $plan -Run $true exit 0