PwshSetup.psm1
|
#Requires -Version 5.1 # PwshSetup 0.0.3, built by Publish-SetupModule.ps1. Do not edit this file. # On pwsh 7.2+ it loads the whole module from PwshSetupCore.ps1. # On Windows PowerShell it exports the bootstrap only: Assert-PwshSetup, Assert-SetupAdmin and Test-SetupAdmin. <# .SYNOPSIS Public: Assert-SetupAdmin and Test-SetupAdmin. .DESCRIPTION Stays Windows PowerShell 5.1 syntax and plain ASCII throughout: the build makes this file the module's loader, and the loader is what Windows PowerShell imports. #> $script:RerunVariable = 'PWSHSETUP_RERUN' function Test-SetupAdmin { <# .SYNOPSIS $true when the session runs as administrator. .EXAMPLE if (-not (Test-SetupAdmin)) { 'Not elevated' } #> [CmdletBinding()] [OutputType([bool])] param() $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object -TypeName 'Security.Principal.WindowsPrincipal' -ArgumentList $identity return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Write-SetupBootstrapLog { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Runs before PwshAnsi can draw, possibly on Windows PowerShell.')] [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Message ) Write-Host -Object ('[PwshSetup] {0}' -f $Message) -ForegroundColor Yellow } function ConvertTo-SetupPortableArgument { # Switches become booleans: a SwitchParameter does not come back from Import-Clixml as one. [CmdletBinding()] [OutputType([hashtable])] param( [hashtable] $Arguments = @{} ) $bound = @{} foreach ($key in $Arguments.Keys) { $value = $Arguments[$key] if ($value -is [System.Management.Automation.SwitchParameter]) { $value = $value.IsPresent } $bound[$key] = $value } return $bound } function Find-SetupTerminal { # wt.exe on the PATH, or at the per-user WindowsApps alias, or $null. [CmdletBinding()] [OutputType([string])] param() $terminal = Get-Command -Name 'wt.exe' -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($terminal) { return $terminal.Source } if ($env:LOCALAPPDATA) { $alias = Join-Path -Path $env:LOCALAPPDATA -ChildPath 'Microsoft\WindowsApps\wt.exe' if (Test-Path -LiteralPath $alias) { return $alias } } return $null } function Get-SetupTerminalArgument { # Windows Terminal splits its commands at semicolons, and a trailing backslash would escape the closing quote. [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [string] $Pwsh, [Parameter(Mandatory)] [string] $EncodedCommand ) $arguments = @() if ($PWD.Provider.Name -eq 'FileSystem') { $directory = $PWD.ProviderPath.Replace(';', '\;') -replace '\\$', '\\' $arguments += @('-d', ('"{0}"' -f $directory)) } $arguments += @(('"{0}"' -f $Pwsh), '-NoProfile', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', $EncodedCommand) return ($arguments -join ' ') } function Get-SetupRerunCommand { # The command the elevated window runs: marks the rerun, restores the folder and the parameters, and keeps the # window open on a failure that ends the script early. [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [string] $ScriptPath, [Parameter(Mandatory)] [string] $ArgumentFile, [Parameter(Mandatory)] [string] $Marker ) $quote = { param($Text) "'" + ($Text -replace "'", "''") + "'" } $lines = @(('$env:{0} = {1}' -f $script:RerunVariable, (& $quote $Marker))) if ($PWD.Provider.Name -eq 'FileSystem') { $lines += ('Set-Location -LiteralPath {0}' -f (& $quote $PWD.ProviderPath)) } $lines += @( ('$p = Import-Clixml -LiteralPath {0}' -f (& $quote $ArgumentFile)) ('Remove-Item -LiteralPath {0} -Force' -f (& $quote $ArgumentFile)) 'try' '{' (' & {0} @p' -f (& $quote $ScriptPath)) '}' 'catch' '{' ' Write-Host -Object ($_ | Out-String) -ForegroundColor Red' ' $null = Read-Host -Prompt ''Press Enter to close''' ' exit 1' '}' 'exit $LASTEXITCODE' ) return ($lines -join "`n") } function Assert-SetupAdmin { <# .SYNOPSIS Ensures the calling script runs as administrator. .DESCRIPTION Already elevated: returns at once. Otherwise reruns the calling script elevated, with its parameters, after one UAC prompt: in a new Windows Terminal window when wt.exe is found, in a new console window when not. This window ends there. Call it after Assert-PwshAnsi, so the elevated run starts on pwsh. .PARAMETER Arguments Parameters to pass to the rerun. Defaults to the bound parameters of the calling script. .PARAMETER ScriptPath Script to rerun. Defaults to the calling script. .EXAMPLE Assert-SetupAdmin #> [CmdletBinding()] param( [hashtable] $Arguments, [string] $ScriptPath = $MyInvocation.PSCommandPath ) if (Test-SetupAdmin) { return } if (-not $ScriptPath) { throw 'Assert-SetupAdmin: call it from a script file, or pass -ScriptPath.' } $done = (Get-Item -Path ('env:{0}' -f $script:RerunVariable) -ErrorAction SilentlyContinue).Value if ($done -and (($done -split ',') -contains 'admin')) { throw 'Assert-SetupAdmin: the elevated rerun is still not running as administrator.' } if ($null -eq $Arguments) { $Arguments = @{} $caller = Get-PSCallStack | Select-Object -Skip 1 -First 1 if ($caller) { $bound = $caller.InvocationInfo.BoundParameters foreach ($key in $bound.Keys) { $Arguments[$key] = $bound[$key] } } } $argumentFile = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath ('PwshSetup-{0}.clixml' -f [guid]::NewGuid()) ConvertTo-SetupPortableArgument -Arguments $Arguments | Export-Clixml -LiteralPath $argumentFile $marker = (@($done, 'admin') | Where-Object -FilterScript { $_ }) -join ',' $command = Get-SetupRerunCommand -ScriptPath $ScriptPath -ArgumentFile $argumentFile -Marker $marker $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command)) $pwsh = (Get-Process -Id $PID).Path Write-SetupBootstrapLog -Message 'Restarting the script as administrator (UAC)...' try { # Not $arguments: variable names ignore case, so it would be the typed [hashtable] $Arguments parameter. $terminal = Find-SetupTerminal if ($terminal) { $terminalArguments = Get-SetupTerminalArgument -Pwsh $pwsh -EncodedCommand $encoded Start-Process -FilePath $terminal -ArgumentList $terminalArguments -Verb RunAs -ErrorAction Stop } else { $pwshArguments = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', $encoded) Start-Process -FilePath $pwsh -ArgumentList $pwshArguments -Verb RunAs -ErrorAction Stop } } catch { Remove-Item -LiteralPath $argumentFile -Force -ErrorAction SilentlyContinue throw ('Assert-SetupAdmin: setup needs administrator rights, and elevation was not granted. {0}' -f $_.Exception.Message) } Write-SetupBootstrapLog -Message 'Carrying on in the elevated window.' exit 0 } <# .SYNOPSIS Public: Assert-PwshSetup, which runs the calling script on pwsh 7.2+ with the latest PwshSetup. .DESCRIPTION Stays Windows PowerShell 5.1 syntax and plain ASCII throughout: the build merges this file into the module's loader, and the loader is what Windows PowerShell imports. Internal helpers carry a SetupBoot prefix, so they stay clear of every other PwshSetup function. #> $script:SetupBootMinPwsh = [version]'7.2' $script:SetupBootRerunVariable = 'PWSHSETUP_RERUN' $script:SetupBootBuildInfoUri = 'https://aka.ms/pwsh-buildinfo-stable' $script:SetupBootReleaseUri = 'https://github.com/PowerShell/PowerShell/releases/download/v{0}/{1}' $script:SetupBootTimeoutSeconds = 10 $script:SetupBootMsiSuccessCodes = @(0, 3010) $script:SetupBootMsiRestartCode = 3010 function Write-SetupBootLog { # Yellow: an action that changes the system. DarkYellow: its outcome, or a skip. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Runs before PwshAnsi can draw, possibly on Windows PowerShell.')] [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Message, [switch] $Outcome ) $color = 'Yellow' if ($Outcome) { $color = 'DarkYellow' } Write-Host -Object ('[PwshSetup] {0}' -f $Message) -ForegroundColor $color } function ConvertTo-SetupBootEncodedCommand { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [string] $Command ) return [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($Command)) } function ConvertTo-SetupBootVersion { # The first three numbers of a version string, or $null. [CmdletBinding()] [OutputType([version])] param( [AllowNull()] [AllowEmptyString()] [string] $Value ) if (-not $Value) { return $null } $match = [regex]::Match($Value, '^v?(\d+)\.(\d+)\.(\d+)') if (-not $match.Success) { return $null } return [version]('{0}.{1}.{2}' -f $match.Groups[1].Value, $match.Groups[2].Value, $match.Groups[3].Value) } function Enable-SetupBootTls12 { [CmdletBinding()] param() [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } function Test-SetupBootElevated { [CmdletBinding()] [OutputType([bool])] param() $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object -TypeName 'Security.Principal.WindowsPrincipal' -ArgumentList $identity return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Get-SetupBootPwshVersion { # The file version first, which starts no process; a probe run only when that is missing. [CmdletBinding()] [OutputType([version])] param( [Parameter(Mandatory)] [string] $Path ) if (-not (Test-Path -LiteralPath $Path)) { return $null } $version = $null try { $version = ConvertTo-SetupBootVersion -Value (Get-Item -LiteralPath $Path).VersionInfo.FileVersion } catch { $version = $null } if ($version) { return $version } $probe = ConvertTo-SetupBootEncodedCommand -Command '[string]$PSVersionTable.PSVersion' try { $output = & $Path -NoProfile -NonInteractive -EncodedCommand $probe 2>$null | Select-Object -Last 1 return (ConvertTo-SetupBootVersion -Value $output) } catch { return $null } } function Find-SetupBootPwsh { # The newest pwsh on this machine at or above the minimum, as Path and Version, or $null. [CmdletBinding()] [OutputType([pscustomobject])] param() $candidates = @(Get-Command -Name 'pwsh' -CommandType Application -All -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source) if ($env:ProgramFiles) { $candidates += Join-Path -Path $env:ProgramFiles -ChildPath 'PowerShell\7\pwsh.exe' } if ($env:LOCALAPPDATA) { $candidates += Join-Path -Path $env:LOCALAPPDATA -ChildPath 'Microsoft\WindowsApps\pwsh.exe' } $best = $null foreach ($path in ($candidates | Select-Object -Unique)) { $version = Get-SetupBootPwshVersion -Path $path if (-not $version -or $version -lt $script:SetupBootMinPwsh) { continue } if ($best -and $version -le $best.Version) { continue } $best = [pscustomobject]@{ Path = $path Version = $version } } return $best } function Get-SetupBootLatestPwshVersion { # The latest stable pwsh release. Throws when it cannot be checked. [CmdletBinding()] [OutputType([version])] param() Enable-SetupBootTls12 $requestArguments = @{ Uri = $script:SetupBootBuildInfoUri UseBasicParsing = $true TimeoutSec = $script:SetupBootTimeoutSeconds ErrorAction = 'Stop' } $info = Invoke-RestMethod @requestArguments return [version]([string]$info.ReleaseTag).TrimStart('v') } function Get-SetupBootArchitecture { [CmdletBinding()] [OutputType([string])] param() $architecture = $env:PROCESSOR_ARCHITEW6432 if (-not $architecture) { $architecture = $env:PROCESSOR_ARCHITECTURE } switch ($architecture) { 'ARM64' { return 'arm64' } 'x86' { return 'x86' } default { return 'x64' } } } function Install-SetupBootPwsh { # The official MSI: Program Files, PATH, Start menu and Microsoft Update. Only msiexec is elevated, one UAC prompt, # never the script. Restart Manager stays on: other open pwsh windows close gracefully, the PC never restarts. [CmdletBinding()] param( [Parameter(Mandatory)] [version] $Version ) $name = 'PowerShell-{0}-win-{1}.msi' -f $Version, (Get-SetupBootArchitecture) $uri = $script:SetupBootReleaseUri -f $Version, $name $msi = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath $name $msiArguments = @( '/i' ('"{0}"' -f $msi) '/quiet' '/norestart' 'ADD_PATH=1' 'USE_MU=1' 'ENABLE_MU=1' ) Enable-SetupBootTls12 try { Write-SetupBootLog -Message ('Downloading {0}...' -f $name) Invoke-WebRequest -Uri $uri -OutFile $msi -UseBasicParsing -ErrorAction Stop $startArguments = @{ FilePath = 'msiexec.exe' ArgumentList = $msiArguments Wait = $true PassThru = $true ErrorAction = 'Stop' } if (Test-SetupBootElevated) { Write-SetupBootLog -Message ('Running the PowerShell {0} installer...' -f $Version) } else { Write-SetupBootLog -Message ('Requesting administrator rights to install PowerShell {0} (UAC)...' -f $Version) $startArguments.Verb = 'RunAs' } $process = Start-Process @startArguments if ($process.ExitCode -notin $script:SetupBootMsiSuccessCodes) { throw ('msiexec exited with {0}.' -f $process.ExitCode) } if ($process.ExitCode -eq $script:SetupBootMsiRestartCode) { Write-SetupBootLog -Message ('PowerShell {0} installed; a restart finishes it.' -f $Version) -Outcome } else { Write-SetupBootLog -Message ('PowerShell {0} installed.' -f $Version) -Outcome } } finally { Remove-Item -LiteralPath $msi -Force -ErrorAction SilentlyContinue } } function Test-SetupBootRerunStage { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [string] $Stage ) $done = (Get-Item -Path ('env:{0}' -f $script:SetupBootRerunVariable) -ErrorAction SilentlyContinue).Value if (-not $done) { return $false } return (($done -split ',') -contains $Stage) } function Get-SetupBootModuleStage { # Windows PowerShell and pwsh each load their own copy of PwshSetup, so the update stage is marked per edition: # an update in one must not stop the other from updating its own copy. [CmdletBinding()] [OutputType([string])] param() return ('module-{0}' -f $PSVersionTable.PSEdition) } function ConvertTo-SetupBootPortableArgument { # Switches become booleans: a SwitchParameter does not come back from Import-Clixml as one. [CmdletBinding()] [OutputType([hashtable])] param( [hashtable] $Arguments = @{} ) $bound = @{} foreach ($key in $Arguments.Keys) { $value = $Arguments[$key] if ($value -is [System.Management.Automation.SwitchParameter]) { $value = $value.IsPresent } $bound[$key] = $value } return $bound } function Test-SetupBootClassicConsole { # An interactive window that is neither Windows Terminal nor an editor's terminal. A rerun inside it keeps its # font, which lacks the glyphs PwshAnsi draws with. [CmdletBinding()] [OutputType([bool])] param() if ($env:WT_SESSION -or $env:TERM_PROGRAM) { return $false } try { return (-not ([Console]::IsInputRedirected -or [Console]::IsOutputRedirected)) } catch { return $false } } function Find-SetupBootTerminal { # wt.exe on the PATH, or at the per-user WindowsApps alias, or $null. The alias is checked by path because a # Windows Terminal installed by this session is not on the PATH this session read at its start. [CmdletBinding()] [OutputType([string])] param() $terminal = Get-Command -Name 'wt.exe' -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($terminal) { return $terminal.Source } if ($env:LOCALAPPDATA) { $alias = Join-Path -Path $env:LOCALAPPDATA -ChildPath 'Microsoft\WindowsApps\wt.exe' if (Test-Path -LiteralPath $alias) { return $alias } } return $null } function Install-SetupBootWindowsTerminal { # Per user through winget: app packages install without administrator rights, so no UAC prompt. Throws when # winget is missing or fails. [CmdletBinding()] param() $winget = Get-Command -Name 'winget.exe' -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $winget) { throw 'winget is not available on this machine.' } $wingetArguments = @( 'install' '--id' 'Microsoft.WindowsTerminal' '--silent' '--accept-source-agreements' '--accept-package-agreements' '--scope' 'user' ) Write-SetupBootLog -Message 'Installing Windows Terminal through winget...' $process = Start-Process -FilePath $winget.Source -ArgumentList $wingetArguments -Wait -PassThru -NoNewWindow -ErrorAction Stop if ($process.ExitCode -ne 0) { throw ('winget exited with {0}.' -f $process.ExitCode) } Write-SetupBootLog -Message 'Windows Terminal installed.' -Outcome } function Get-SetupBootTerminalTabArgument { # A new tab in the most recent Windows Terminal window, started in the current folder. Windows Terminal splits its # commands at semicolons, and a trailing backslash would escape the closing quote, so both are escaped. [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [string] $Pwsh, [Parameter(Mandatory)] [string] $EncodedCommand ) $tab = @('-w', '0', 'new-tab') if ($PWD.Provider.Name -eq 'FileSystem') { $directory = $PWD.ProviderPath.Replace(';', '\;') -replace '\\$', '\\' $tab += @('-d', ('"{0}"' -f $directory)) } $tab += @(('"{0}"' -f $Pwsh), '-NoProfile', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', $EncodedCommand) return ($tab -join ' ') } function Get-SetupBootRerunCommand { # The stage marker travels inside the command: a Windows Terminal tab is started by the terminal, not by this # process, so it does not inherit this environment. [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [string] $ScriptPath, [Parameter(Mandatory)] [string] $ArgumentFile, [Parameter(Mandatory)] [string] $Marker ) $quote = { param($Text) "'" + ($Text -replace "'", "''") + "'" } $lines = @( ('$env:{0} = {1}' -f $script:SetupBootRerunVariable, (& $quote $Marker)) ('$p = Import-Clixml -LiteralPath {0}' -f (& $quote $ArgumentFile)) ('Remove-Item -LiteralPath {0} -Force' -f (& $quote $ArgumentFile)) ('& {0} @p' -f (& $quote $ScriptPath)) 'exit $LASTEXITCODE' ) return ($lines -join "`n") } function Invoke-SetupBootRerun { # Reruns the script and ends this process. Moving to pwsh from a classic console opens a Windows Terminal tab # instead, installing Windows Terminal on first use, so the script draws with a font that has the glyphs. [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Pwsh, [Parameter(Mandatory)] [string] $ScriptPath, [Parameter(Mandatory)] [string] $Stage, [hashtable] $Arguments = @{} ) $argumentFile = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath ('PwshSetup-{0}.clixml' -f [guid]::NewGuid()) ConvertTo-SetupBootPortableArgument -Arguments $Arguments | Export-Clixml -LiteralPath $argumentFile $done = (Get-Item -Path ('env:{0}' -f $script:SetupBootRerunVariable) -ErrorAction SilentlyContinue).Value $marker = (@($done, $Stage) | Where-Object -FilterScript { $_ }) -join ',' $command = Get-SetupBootRerunCommand -ScriptPath $ScriptPath -ArgumentFile $argumentFile -Marker $marker $encoded = ConvertTo-SetupBootEncodedCommand -Command $command $terminal = $null if ($Stage -eq 'pwsh' -and (Test-SetupBootClassicConsole)) { $terminal = Find-SetupBootTerminal if (-not $terminal) { try { Install-SetupBootWindowsTerminal $terminal = Find-SetupBootTerminal } catch { Write-SetupBootLog -Message ('Windows Terminal unavailable, staying in this window: {0}' -f $_.Exception.Message) } } } if ($terminal) { Write-SetupBootLog -Message ('Restarting the script in a Windows Terminal tab with {0}...' -f $Pwsh) -Outcome Start-Process -FilePath $terminal -ArgumentList (Get-SetupBootTerminalTabArgument -Pwsh $Pwsh -EncodedCommand $encoded) exit 0 } $reason = 'to load the new PwshSetup' if ($Stage -eq 'pwsh') { $reason = 'in {0}' -f $Pwsh } Write-SetupBootLog -Message ('Restarting the script {0}...' -f $reason) -Outcome Set-Item -Path ('env:{0}' -f $script:SetupBootRerunVariable) -Value $marker & $Pwsh -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded exit $LASTEXITCODE } function Update-SetupBootPwsh { # The pwsh 7.2+ to rerun under, or $null when this host already qualifies. Installs only when no pwsh 7.2+ is # found at all; never updates an existing one. [CmdletBinding()] [OutputType([string])] param() if ($PSVersionTable.PSVersion -ge $script:SetupBootMinPwsh) { return $null } $current = Find-SetupBootPwsh if ($current) { return $current.Path } try { $latest = Get-SetupBootLatestPwshVersion Write-SetupBootLog -Message ('PowerShell {0}+ not found. Installing PowerShell {1}...' -f $script:SetupBootMinPwsh, $latest) Install-SetupBootPwsh -Version $latest $installed = Find-SetupBootPwsh if (-not $installed) { throw ('PowerShell {0} was installed but cannot be found.' -f $latest) } return $installed.Path } catch { throw ('PowerShell {0}+ is missing and could not be installed: {1}' -f $script:SetupBootMinPwsh, $_.Exception.Message) } } function Update-SetupBootModule { # $true when a newer PwshSetup was installed and the script must rerun to load it. A failed check or install logs # a warning and returns $false; it never throws. [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [version] $Current ) if (Test-SetupBootRerunStage -Stage (Get-SetupBootModuleStage)) { return $false } $latest = $null try { $latest = [version](Find-Module -Name 'PwshSetup' -ErrorAction Stop).Version if ($latest -le $Current) { return $false } Write-SetupBootLog -Message ('Updating PwshSetup {0} -> {1}...' -f $Current, $latest) Install-Module -Name 'PwshSetup' -RequiredVersion $latest -Scope CurrentUser -Force -ErrorAction Stop Write-SetupBootLog -Message ('PwshSetup {0} installed.' -f $latest) -Outcome return $true } catch { if ($latest -and $latest -gt $Current) { Write-SetupBootLog -Message ('Failed to update PwshSetup: {0} Carrying on with {1}.' -f $_.Exception.Message, $Current) } return $false } } function Assert-PwshSetup { <# .SYNOPSIS Ensures the calling script runs on pwsh 7.2+ with the latest PwshSetup. .DESCRIPTION 1. Unless -SkipUpdate, checks the gallery for a newer PwshSetup and reruns the script to load it when one is found. Windows PowerShell and pwsh keep separate copies, so each edition updates its own. 2. On Windows PowerShell, reruns the script on pwsh 7.2+, installing pwsh first when none is present. Already on pwsh 7.2+ with the latest PwshSetup, or -SkipUpdate: returns at once. A failed gallery check or update logs a warning and carries on. .PARAMETER Arguments Parameters to pass to the rerun. Defaults to the bound parameters of the calling script. .PARAMETER ScriptPath Script to rerun. Defaults to the calling script. .PARAMETER SkipUpdate Skip the gallery check. .EXAMPLE Assert-PwshSetup .EXAMPLE Assert-PwshSetup -SkipUpdate #> [CmdletBinding()] param( [hashtable] $Arguments, [string] $ScriptPath = $MyInvocation.PSCommandPath, [switch] $SkipUpdate ) if (-not $ScriptPath) { throw 'Assert-PwshSetup: call it from a script file, or pass -ScriptPath.' } if ($null -eq $Arguments) { $Arguments = @{} $caller = Get-PSCallStack | Select-Object -Skip 1 -First 1 if ($caller) { $bound = $caller.InvocationInfo.BoundParameters foreach ($key in $bound.Keys) { $Arguments[$key] = $bound[$key] } } } # The update comes first: on Windows PowerShell the move to pwsh ends this process, and the pwsh run only ever # sees its own copy, so this is the one chance to update this edition's. if (-not $SkipUpdate -and (Update-SetupBootModule -Current $MyInvocation.MyCommand.Module.Version)) { $rerunArguments = @{ Pwsh = (Get-Process -Id $PID).Path ScriptPath = $ScriptPath Stage = Get-SetupBootModuleStage Arguments = $Arguments } Invoke-SetupBootRerun @rerunArguments } $pwsh = Update-SetupBootPwsh if ($pwsh) { Invoke-SetupBootRerun -Pwsh $pwsh -ScriptPath $ScriptPath -Stage 'pwsh' -Arguments $Arguments } } if ($PSVersionTable.PSVersion -ge [version]'7.2') { . (Join-Path -Path $PSScriptRoot -ChildPath 'PwshSetupCore.ps1') } # The manifest's FunctionsToExport decides what is public; helpers stay hidden. Export-ModuleMember -Function * |