Files
libretro/install.ps1
T
Abdessamad Derraz 47ed945b4a feat: write installer tables from install.py
scripts/generate_installer_doc.py rewrites the marked tables of
wiki/installer.md: options from build_parser(), events from EVENT_KEYS,
exit codes from the sys.exit calls, RETROBIOS_PAD values and the keys of
each route from TUI_ROUTES, the one table the bar, the help and the stop
line now read. Prose cells are kept, a row the code gains stops the
script until its text is written, and --check runs in the tests. The
site drops the markers.

The full screen falls back to the line questions where the terminal's
terminfo moves no cursor the ANSI way (vt52, glass terminals), and --tui
names why. The line mode draws no escape code with TERM unset, dumb or
such a terminal. Ctrl-Z typed on the screen stops the run as SIGTSTP
does, the terminal handed back and redrawn on fg. A window too small
names 40x12 and its own size.

Pty tests check the modes, the cursor and the main screen after Ctrl-C,
Ctrl-D, SIGTERM, SIGHUP and Ctrl-Z then fg. Tests from a mutmut run over
the device filter, the card bounds and the WSL paths raise its score
from 62/71 to 71/71. The parser lists its options in the page's order,
with NAME, PATH, NAMES and N as arguments.
2026-10-27 09:47:44 +01:00

146 lines
7.9 KiB
PowerShell

# One-line bootstrap and local wrapper. The downloaded installer is accepted
# only when it matches the SHA-256 embedded in this wrapper.
# Everything runs in a script block of its own: piped into iex, the script
# would otherwise leave its variables and $ErrorActionPreference = "Stop"
# in the user's session, where every later error would stop their commands.
# Neither the script nor the block declares a parameter: one [Parameter()]
# attribute makes it an advanced one, whose common parameters took -v
# (-Verbose), -vb, -d (-Debug) and -ea before install.py could see them.
& {
$InstallerArguments = @($args)
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "af5087172bc1909ad70f81c0e10c944db0d2326a3c9c1b4965c53faf1944d37c"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
$temporary = $null
try {
if (-not $installer -or -not (Test-Path -LiteralPath $installer -PathType Leaf)) {
$url = if ($env:RETROBIOS_INSTALL_URL) { $env:RETROBIOS_INSTALL_URL } else { $defaultInstallUrl }
$expected = if ($env:RETROBIOS_INSTALL_SHA256) { $env:RETROBIOS_INSTALL_SHA256 } else { $defaultInstallSha256 }
$uri = [Uri]$url
if ($uri.Scheme -ne "https") {
throw "RETROBIOS_INSTALL_URL must use HTTPS."
}
if ($expected -notmatch '^[0-9a-fA-F]{64}$') {
throw "Installer SHA-256 must contain exactly 64 hexadecimal characters."
}
# Windows PowerShell 5.1 still negotiates SSL 3.0 / TLS 1.0 by default
# and GitHub refuses both, so the download fails before any hash is
# checked. install.sh pins the same floor with curl --tlsv1.2.
if ($PSVersionTable.PSEdition -ne "Core") {
[Net.ServicePointManager]::SecurityProtocol = `
[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
}
$temporary = Join-Path ([IO.Path]::GetTempPath()) ("retrobios-install-{0}.py" -f [Guid]::NewGuid())
try {
Invoke-WebRequest -Uri $uri -OutFile $temporary -UseBasicParsing
} catch {
# The raw WebException read as a crash and named no next step.
throw ("The installer could not be downloaded from {0}: {1} " +
"Check the network connection and any proxy, then run the same command again. " +
"Nothing was run and nothing was written.") -f $url, $_.Exception.Message
}
if ((Get-Item -LiteralPath $temporary).Length -gt $maximumInstallerBytes) {
throw "Downloaded installer exceeds the size limit."
}
$actual = (Get-FileHash -LiteralPath $temporary -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actual -ne $expected.ToLowerInvariant()) {
$mismatch = "The downloaded installer does not match its expected fingerprint. Nothing was run and nothing was written. "
$overrides = @("RETROBIOS_INSTALL_URL", "RETROBIOS_INSTALL_SHA256") |
Where-Object { [Environment]::GetEnvironmentVariable($_) }
if ($overrides) {
# A pin or a URL of the user's own refuses the installer:
# retrying, as the advice below says, would never help.
throw ($mismatch + "{0} is set in this session and decides which installer is accepted: " +
"remove it to use the installer this script names, or set it to match.") -f ($overrides -join " and ")
}
throw ($mismatch + "The download was most likely cut short: try again. Within minutes of an update " +
"of this project the two files can come from caches of different ages. Waiting five minutes settles that.")
}
$installer = $temporary
}
# The py launcher first, then python3 and python. The Microsoft Store
# places a python.exe that only points at the Store: it fails the
# version check like a missing one.
$findPython = {
$found = Get-Command py -ErrorAction SilentlyContinue
if ($found) { return ,@($found, @("-3")) }
foreach ($name in @("python3", "python")) {
$found = Get-Command $name -ErrorAction SilentlyContinue
if ($found) { return ,@($found, @()) }
}
return $null
}
$usable = {
param($candidate)
if (-not $candidate) { return $false }
$arguments = $candidate[1]
& $candidate[0].Source @arguments -c "import sys; raise SystemExit(sys.version_info < (3, 8))" | Out-Null
return ($LASTEXITCODE -eq 0)
}
$needPython = "Python 3.8 or newer is required. Install it with " +
"'winget install Python.Python.3.12' or from https://www.python.org/downloads/ " +
"(tick 'Add python.exe to PATH'), then open a new PowerShell window and run this command again."
$candidate = & $findPython
if (-not (& $usable $candidate)) {
# winget ships with Windows 10 and 11: at a console the bootstrap
# offers to run the install the message names, then carries on.
$winget = Get-Command winget -ErrorAction SilentlyContinue
if ($winget -and -not [Console]::IsInputRedirected) {
$answer = Read-Host "Python 3.8 or newer is required. Install Python 3.12 now with winget? [Y/n]"
if ($answer -notmatch '^\s*[nNqQ]') {
& $winget.Source install --id Python.Python.3.12 --exact --source winget `
--accept-package-agreements --accept-source-agreements
# The installer adds Python to the PATH of new sessions:
# this one reads it again from the registry.
if ($env:OS -eq "Windows_NT") {
$env:Path = [Environment]::GetEnvironmentVariable("Path", "Machine") + ";" +
[Environment]::GetEnvironmentVariable("Path", "User")
}
$candidate = & $findPython
}
}
if (-not (& $usable $candidate)) { throw $needPython }
}
$python = $candidate[0]
$launcher = $candidate[1]
# The installer rebuilds this command in its hints, and the user has
# no install.py of their own to run.
$previousBootstrap = $env:RETROBIOS_BOOTSTRAP
$env:RETROBIOS_BOOTSTRAP = if ($temporary) { "ps1" } else { $null }
try {
& $python.Source @launcher $installer @InstallerArguments
} finally {
$env:RETROBIOS_BOOTSTRAP = $previousBootstrap
}
# The installer has already said what happened: its status is passed on
# as it is (1 for files left to fetch under --check, 2 when nothing was
# installed, 130 on Ctrl+C), not turned into a red exception that reads
# as a crash and flattens every code to one.
$installerExit = $LASTEXITCODE
}
catch {
# One line saying what failed and what to do, rather than the red
# exception block PowerShell prints for an uncaught throw.
[Console]::Error.WriteLine("Error: " + $_.Exception.Message)
$installerExit = 1
}
finally {
if ($temporary -and (Test-Path -LiteralPath $temporary)) {
Remove-Item -LiteralPath $temporary -Force
}
}
if ($installerExit) {
# Run from a file, the script ends with that status. Piped into iex it
# runs in the user's own session, which exit would close.
if ($PSScriptRoot) { exit $installerExit }
$global:LASTEXITCODE = $installerExit
}
} @args