mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 12:49:03 -05:00
chore: merge main into the engine round
This commit is contained in:
commit
b34a6058f7
47 files changed
+2731
-442
No files matched your search
@@ -1,5 +1,5 @@
|
||||
emulator: "00_example"
|
||||
type: libretro
|
||||
type: test
|
||||
core_classification: pure_libretro
|
||||
source: "https://github.com/libretro/libretro-core-info"
|
||||
profiled_date: "2026-03-22"
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
$defaultInstallSha256 = "7bc94c44f3696a0b777e5c280754633105a06756932ee52547f0a9c775a895b9"
|
||||
$defaultInstallSha256 = "240faa427aaa602deca1a3180f770c6f335095b1cf1d5bb28fc060eb55f54917"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+535
-80
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -18,7 +18,7 @@ esac
|
||||
TEMP_INSTALLER=""
|
||||
TEMP_DIRECTORY=""
|
||||
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
DEFAULT_INSTALL_SHA256="7bc94c44f3696a0b777e5c280754633105a06756932ee52547f0a9c775a895b9"
|
||||
DEFAULT_INSTALL_SHA256="240faa427aaa602deca1a3180f770c6f335095b1cf1d5bb28fc060eb55f54917"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -28,8 +28,10 @@ GAMEDIR="/$directory/ports/retrobios"
|
||||
cd "$GAMEDIR" || exit 1
|
||||
|
||||
# Appended to, as the launcher and install.py also write this file: a tee
|
||||
# writing from its own offset would overwrite their lines.
|
||||
: > "$GAMEDIR/log.txt" && exec > >(tee -a "$GAMEDIR/log.txt") 2>&1
|
||||
# writing from its own offset would overwrite their lines. The previous run's
|
||||
# is kept, for a player who starts again before reading it.
|
||||
[ -s "$GAMEDIR/log.txt" ] && mv -f "$GAMEDIR/log.txt" "$GAMEDIR/log.prev.txt" 2>/dev/null
|
||||
{ : > "$GAMEDIR/log.txt"; } 2>/dev/null && exec > >(tee -a "$GAMEDIR/log.txt") 2>&1
|
||||
|
||||
# PortMasterDialog.txt has shipped with PortMaster since before 2024.02.26,
|
||||
# and the 8.5.23 copy JELOS and KOLOS preinstall has no funcs.txt to source
|
||||
@@ -48,17 +50,33 @@ RB_CONSOLE="${CUR_TTY:-/dev/tty0}"
|
||||
RB_MAPPER="$GPTOKEYB"
|
||||
RB_FALLBACK=1
|
||||
RB_PACK_PATH="$GAMEDIR:/$directory/ports:/$directory"
|
||||
# ArkOS and dArkOS, as control.txt tells them apart (its plymouth theme): the
|
||||
# games card is /$directory, roms2 when fstab mounts a second card, and its
|
||||
# bios folder is the one RetroArch reads. The other card's is offered.
|
||||
# ArkOS and dArkOS, as control.txt tells them apart (its plymouth theme):
|
||||
# their two card folders are offered to each other, and the destination is
|
||||
# the one RetroArch reads. control.txt names the card PortMaster runs from
|
||||
# roms2 whenever fstab mounts a second card, even when RetroArch still reads
|
||||
# /roms/bios, so RetroArch's own setting decides, as in the Tools entry.
|
||||
if [ -e "${RB_ARK_MARKER:-/usr/share/plymouth/themes/text.plymouth}" ]; then
|
||||
RB_OTHER_CARDS="${RB_OTHER_CARDS:-/roms/bios:/roms2/bios}"
|
||||
RB_BIOS_DEST="/$directory/bios"
|
||||
RB_BIOS_DEST=$(awk -F'"' '/^system_directory *=/ { print $2; exit }' \
|
||||
"${RB_RETROARCH_CFG:-/home/ark/.config/retroarch/retroarch.cfg}" 2>/dev/null)
|
||||
RB_BIOS_DEST="${RB_BIOS_DEST:-/$directory/bios}"
|
||||
fi
|
||||
# shellcheck source=../shared/tui.txt
|
||||
source "$GAMEDIR/tui.txt"
|
||||
# shellcheck source=retrobios/screens.txt
|
||||
source "$GAMEDIR/screens.txt"
|
||||
rb_open_log keep
|
||||
|
||||
# muOS: PortMaster's mod_muOS.txt points CUR_TTY at /dev/null, so there is no
|
||||
# console to draw on. muxterm, the terminal muOS's Terminal core runs, shows
|
||||
# the interface instead. It is told to leave the pad alone (an SDL hint) so
|
||||
# that gptokeyb sends every button once, with this port's mapping: muxterm
|
||||
# sends Y as Space, where the interface reads Y as Options. The hint allows
|
||||
# one vendor and product id, 0001:0001, which no pad carries.
|
||||
RB_MUXTERM="${RB_MUXTERM:-/opt/muos/frontend/muxterm}"
|
||||
if [ -x "$RB_MUXTERM" ] && [ -n "$RB_MAPPER" ]; then
|
||||
RB_TERMAPP=(env SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT=0x0001/0x0001 "$RB_MUXTERM" --gl --)
|
||||
fi
|
||||
|
||||
export SDL_GAMECONTROLLERCONFIG="$sdl_controllerconfig"
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ options, L1 and R1 to page. B on the first screen closes it.
|
||||
EmulationStation covers the text console on Batocera and Knulli, and no
|
||||
terminal for a script is documented there. RetroBIOS opens xterm (the X11
|
||||
builds) or foot when either is installed, else switches to console 4, runs
|
||||
there, and switches back. Which of the two shows up on a given build is not
|
||||
verified. If the screen stays black, `retrobios/log.txt` says which route
|
||||
there, and switches back. When console 4 does not come to the front within
|
||||
3 s, it switches back and stops. Which of the two shows up on a given build
|
||||
is not verified. If nothing shows, `retrobios/log.txt` says which route
|
||||
was taken, and the computer route always works: with the card mounted, run
|
||||
the one-line command from abdess.github.io/retrobios/wiki/installer.
|
||||
@@ -16,16 +16,23 @@ RB_ROUTES="app vt"
|
||||
RB_VT_SWITCH="${RB_VT_SWITCH:-4}"
|
||||
RB_PACK_PATH="$RB_DIR:$HERE:/userdata"
|
||||
RB_LOG="$RB_DIR/log.txt"
|
||||
RB_PAD="evmapy"
|
||||
|
||||
# The X11 builds start EmulationStation on display :0, which a script run
|
||||
# from it does not always inherit.
|
||||
if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ] && [ -S /tmp/.X11-unix/X0 ]; then
|
||||
# from it does not always inherit, with the cookie S31emulationstation
|
||||
# exports: xterm is refused the display without it.
|
||||
if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ] \
|
||||
&& [ -S "${RB_X_SOCKET:-/tmp/.X11-unix/X0}" ]; then
|
||||
export DISPLAY=:0
|
||||
fi
|
||||
RB_XAUTH="${RB_XAUTH:-/var/lib/.Xauthority}"
|
||||
if [ -n "${DISPLAY:-}" ] && [ -z "${XAUTHORITY:-}" ] && [ -f "$RB_XAUTH" ]; then
|
||||
export XAUTHORITY="$RB_XAUTH"
|
||||
fi
|
||||
|
||||
# shellcheck source=../shared/tui.txt
|
||||
source "$RB_DIR/tui.txt"
|
||||
: > "$RB_LOG"
|
||||
rb_open_log fresh
|
||||
rb_select_python
|
||||
# A launcher's own arguments are not install.py's (muOS passes the app folder).
|
||||
# shellcheck disable=SC2119
|
||||
|
||||
@@ -7,10 +7,12 @@ It checks the BIOS folder (`/run/muos/storage/bios`) against the files its
|
||||
emulators read and installs the missing ones: downloaded with a network
|
||||
connection, or from a pack copied to the root of the card without one.
|
||||
|
||||
It runs in muxterm, muOS's terminal. muxterm already sends the D-pad as
|
||||
arrows, Start as Enter, B as Backspace (back) and Y as Space (tick). With
|
||||
PortMaster installed, gptokeyb2 adds A (choose), X (tick) and L1/R1 (page).
|
||||
Menu closes it.
|
||||
It runs in muxterm, muOS's terminal. With PortMaster installed, gptokeyb2
|
||||
turns every button into a key and muxterm is told to leave the pad alone, so
|
||||
each button is sent once: D-pad to move, A or Start to choose, B or Select to
|
||||
go back, X to tick a system, Y for the options, L1 and R1 to page. Hold
|
||||
Select, then press Start to quit. Without PortMaster, muxterm reads the pad
|
||||
itself (Y then ticks a system) and Menu closes it.
|
||||
|
||||
Not tried on a device yet: the icon, and whether muxterm and gptokeyb2 share
|
||||
the pad as described.
|
||||
Not tried on a device yet: the icon, and whether muxterm honours SDL's
|
||||
controller allow-list as SDL 2.30 does.
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
# RetroBIOS for muOS, from Applications: the installer's full-screen
|
||||
# interface in muxterm, the terminal muOS's own Terminal core runs, with
|
||||
# PortMaster's gptokeyb2 adding the keys muxterm leaves unmapped. Without
|
||||
# muxterm it runs on the console with the full mapping.
|
||||
# PortMaster's gptokeyb2 turning the buttons into keys. Without muxterm it
|
||||
# runs on the console.
|
||||
|
||||
MUOS="${RB_MUOS:-/opt/muos}"
|
||||
# shellcheck source=/dev/null
|
||||
@@ -19,12 +19,12 @@ PM_DIR="$ROM_MOUNT/MUOS/PortMaster"
|
||||
MUXTERM="$MUOS/frontend/muxterm"
|
||||
|
||||
RB_DIR="$APP_DIR"
|
||||
RB_PACK_PATH="$APP_DIR:$ROM_MOUNT"
|
||||
RB_LOG="$APP_DIR/log.txt"
|
||||
# muOS binds MUOS/bios from SD2 when that card has one, else from SD1, onto
|
||||
# /run/muos/storage/bios (script/device/bind.sh:10-11,30-52): the other card's
|
||||
# MUOS/bios is the one offered to copy from.
|
||||
SD2_MOUNT="$(GET_VAR "device" "storage/sdcard/mount")"
|
||||
RB_PACK_PATH="$APP_DIR:$ROM_MOUNT:$SD2_MOUNT"
|
||||
RB_OTHER_CARDS="$ROM_MOUNT/MUOS/bios:$SD2_MOUNT/MUOS/bios"
|
||||
RB_BIOS_DEST="${RB_BIOS_DEST:-/run/muos/storage/bios}"
|
||||
# PortMaster keeps its runtimes here: the Python runtime is used when
|
||||
@@ -33,21 +33,33 @@ controlfolder="$PM_DIR"
|
||||
|
||||
# shellcheck source=../shared/tui.txt
|
||||
source "$APP_DIR/tui.txt"
|
||||
: > "$RB_LOG"
|
||||
rb_open_log fresh
|
||||
|
||||
RB_GPTK="$APP_DIR/retrobios.gptk"
|
||||
if [ -x "$PM_DIR/gptokeyb2" ]; then
|
||||
# As muOS's own launchers start it (script/launch/ext-xroar.sh).
|
||||
# As muOS's own launchers start it (script/launch/ext-xroar.sh). gptokeyb2
|
||||
# kills only with SIGKILL: by process name by default, which never names
|
||||
# this run, and quits all the same, or with -X by command line, which is
|
||||
# what reaches the Start + Select sentinel (gptokeyb2 src/util.c).
|
||||
ln -sf "$PM_DIR/libinterpose.aarch64.so" "/usr/lib/libinterpose.aarch64.so" >/dev/null 2>&1
|
||||
RB_MAPPER="$PM_DIR/gptokeyb2"
|
||||
RB_MAPPER="$PM_DIR/gptokeyb2 -X"
|
||||
fi
|
||||
if [ -x "$MUXTERM" ]; then
|
||||
RB_ROUTES="app"
|
||||
RB_TERMAPP=("$MUXTERM" --gl --)
|
||||
RB_GPTK="$APP_DIR/muxterm.gptk"
|
||||
if [ -n "${RB_MAPPER:-}" ]; then
|
||||
# muxterm reads the pad itself and sends Y as Space (MustardOS/terminal
|
||||
# input.c), where the interface reads Y as Options. The SDL hint allows
|
||||
# only vendor and product 0001:0001, which no pad carries, so muxterm
|
||||
# leaves the pad to gptokeyb2 and every button is sent once.
|
||||
RB_TERMAPP=(env SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT=0x0001/0x0001 "$MUXTERM" --gl --)
|
||||
else
|
||||
# No gptokeyb2: muxterm's own buttons are all there is.
|
||||
RB_TERMAPP=("$MUXTERM" --gl --)
|
||||
RB_PAD="muxterm"
|
||||
fi
|
||||
else
|
||||
RB_ROUTES="vt"
|
||||
RB_CONSOLE="${RB_CONSOLE:-/dev/tty0}"
|
||||
RB_GPTK="$APP_DIR/retrobios.gptk"
|
||||
fi
|
||||
|
||||
rb_select_python
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# RetroBIOS inside muxterm (gptokeyb2). muxterm already sends the D-pad as
|
||||
# arrows, Start as Enter, B as Backspace and Y as Space to the program it
|
||||
# runs (MustardOS/terminal input.c and osk.c), so mapping those here too
|
||||
# would press each key twice. Only what muxterm leaves unmapped is added.
|
||||
[controls]
|
||||
overlay = clear
|
||||
a = enter
|
||||
x = space
|
||||
l1 = pageup
|
||||
r1 = pagedown
|
||||
+479
-101
@@ -5,18 +5,23 @@
|
||||
#
|
||||
# Sourced by every launcher (the PortMaster port, the Batocera and Knulli
|
||||
# port, the muOS app, the Tools entry), each of which sets the inputs below
|
||||
# and calls rb_select_python, then rb_launch. Run as `bash tui.txt here`, it
|
||||
# is the part that runs inside a terminal app the launcher opened.
|
||||
# and calls rb_open_log, rb_select_python, then rb_launch. Run as
|
||||
# `bash tui.txt here`, it is the part that runs inside a terminal app the
|
||||
# launcher opened.
|
||||
#
|
||||
# Inputs, all optional:
|
||||
# RB_ROUTES where to look for a terminal, in order: here (this
|
||||
# script's own stdin and stdout), app (foot or xterm in the
|
||||
# graphical session), vt (a Linux console). Default "app vt".
|
||||
# A route that fails to show the interface hands over to the
|
||||
# next one.
|
||||
# RB_CONSOLE the console of the vt route (default /dev/tty0)
|
||||
# RB_VT_SWITCH a console number to switch to when a graphical session
|
||||
# covers the current one (Batocera, Knulli)
|
||||
# RB_MAPPER the command that starts gptokeyb, kill flag included
|
||||
# ("$GPTOKEYB" in a PortMaster port), empty for none
|
||||
# RB_MAPPER_NEEDED 0 when the terminal reads the pad itself, so the
|
||||
# interface still has buttons without the mapper
|
||||
# RB_GPTK its mapping (default retrobios.gptk next to this file)
|
||||
# RB_PACK_PATH folders searched for a pack when there is no network,
|
||||
# separated by ":", the first one named to the player
|
||||
@@ -25,6 +30,10 @@
|
||||
# RB_BIOS_DEST the one of them the firmware reads, never offered as the
|
||||
# other card
|
||||
# RB_FALLBACK 1 when the launcher has screens of its own to fall back to
|
||||
# RB_PAD the input route, exported to the interface as
|
||||
# RETROBIOS_PAD: gptokeyb (retrobios.gptk, the default when
|
||||
# RB_MAPPER is set), muxterm (muxterm's own keys), evmapy
|
||||
# (RetroBIOS.sh.keys), miyoo (buttons already keys)
|
||||
# ESUDO sudo where the launcher needs it, as PortMaster sets it
|
||||
|
||||
RB_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
||||
@@ -44,33 +53,141 @@ RB_FB="${RB_FB:-/sys/class/graphics/fb0/virtual_size}"
|
||||
RB_ROUTE_TABLE="${RB_ROUTE_TABLE:-/proc/net/route}"
|
||||
RB_VT_ACTIVE="${RB_VT_ACTIVE:-/sys/class/tty/tty0/active}"
|
||||
RB_PROBE_URL="${RETROBIOS_BASE_URL:-https://raw.githubusercontent.com/Abdess/retrobios/}"
|
||||
# Seconds: the interface's time to write its result once asked to stop, the
|
||||
# wait for a button when the mapper is gone, the offline detection run.
|
||||
RB_STOP_GRACE="${RB_STOP_GRACE:-10}"
|
||||
RB_DEAD_WAIT="${RB_DEAD_WAIT:-10}"
|
||||
RB_DETECT_LIMIT="${RB_DETECT_LIMIT:-20}"
|
||||
RB_MAPPER_NEEDED="${RB_MAPPER_NEEDED:-1}"
|
||||
RB_PAD="${RB_PAD:-}"
|
||||
RB_RUNTIME="python_3.11"
|
||||
RB_RELEASES="github.com/Abdess/retrobios/releases/latest"
|
||||
RB_PC_ROUTE="On a computer, with this SD card mounted, the same installer fills the card: install.py --platform NAME --dest BIOS-FOLDER-ON-THE-CARD. Instructions: abdess.github.io/retrobios/wiki/installer"
|
||||
RB_PYTHON="${RB_PYTHON:-}"
|
||||
RB_PYTHONHOME="${RB_PYTHONHOME:-}"
|
||||
RB_PYTHON_PROBLEM="${RB_PYTHON_PROBLEM:-}"
|
||||
RB_MAPPER_PID="${RB_MAPPER_PID:-}"
|
||||
RB_STOP_PID="${RB_STOP_PID:-}"
|
||||
RB_STOP_TOKEN="${RB_STOP_TOKEN:-}"
|
||||
RB_STATE_PROBLEM=""
|
||||
RB_MOUNTED=""
|
||||
RB_MAPPER_PID=""
|
||||
RB_UI_PID=""
|
||||
RB_APP_PID=""
|
||||
RB_STOPPED=""
|
||||
RB_TTY=""
|
||||
RB_STTY=""
|
||||
RB_FONT_BACK=""
|
||||
RB_VT_BACK=""
|
||||
RB_ROUTE=""
|
||||
RB_ROUTE_FAILED=""
|
||||
RB_NAP_FD=""
|
||||
RB_KEY=""
|
||||
RB_TERMAPP=()
|
||||
RB_PYCMD=()
|
||||
|
||||
rb_arch() {
|
||||
uname -m
|
||||
}
|
||||
|
||||
rb_py() {
|
||||
rb_say() {
|
||||
printf '%s\n' "RetroBIOS: $*" >> "$RB_LOG" 2>/dev/null
|
||||
}
|
||||
|
||||
# The interpreter as one command, so it can be started in the background and
|
||||
# signalled itself: a function run with & is a subshell whose python child a
|
||||
# kill does not reach.
|
||||
rb_pycmd() {
|
||||
if [ -n "$RB_PYTHONHOME" ]; then
|
||||
PYTHONHOME="$RB_PYTHONHOME" "$RB_PYTHON" "$@"
|
||||
RB_PYCMD=(env "PYTHONHOME=$RB_PYTHONHOME" "$RB_PYTHON")
|
||||
else
|
||||
"$RB_PYTHON" "$@"
|
||||
RB_PYCMD=("$RB_PYTHON")
|
||||
fi
|
||||
}
|
||||
|
||||
rb_py() {
|
||||
rb_pycmd
|
||||
"${RB_PYCMD[@]}" "$@"
|
||||
}
|
||||
|
||||
# A pause without a child process, in fractions of a second, which busybox
|
||||
# sleep does not always take: a read that times out on a pipe this shell
|
||||
# holds both ends of.
|
||||
rb_nap() {
|
||||
if [ -z "$RB_NAP_FD" ]; then
|
||||
exec {RB_NAP_FD}<> <(:) || RB_NAP_FD="none"
|
||||
fi
|
||||
if [ "$RB_NAP_FD" = "none" ]; then
|
||||
sleep 1
|
||||
else
|
||||
read -r -t "$1" -u "$RB_NAP_FD" _
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Whether a process runs. /proc, not kill -0: the mapper may run as root
|
||||
# under sudo, and kill -0 on it fails for a user. A zombie is not running.
|
||||
rb_alive() {
|
||||
local stat
|
||||
[ -n "${1:-}" ] || return 1
|
||||
{ read -r stat < "/proc/$1/stat"; } 2>/dev/null || return 1
|
||||
stat="${stat##*) }"
|
||||
[ "${stat%% *}" != "Z" ]
|
||||
}
|
||||
|
||||
# Runs a command for at most $1 seconds: status 124 when it was stopped.
|
||||
rb_bounded() {
|
||||
local limit="$1" pid waited=0
|
||||
shift
|
||||
"$@" &
|
||||
pid=$!
|
||||
while rb_alive "$pid"; do
|
||||
if [ "$waited" -ge $((limit * 4)) ]; then
|
||||
kill "$pid" 2>/dev/null
|
||||
wait "$pid" 2>/dev/null
|
||||
return 124
|
||||
fi
|
||||
rb_nap 0.25
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
wait "$pid"
|
||||
}
|
||||
|
||||
# --- State and log --------------------------------------------------------
|
||||
|
||||
rb_can_write() {
|
||||
mkdir -p "$1" 2>/dev/null || return 1
|
||||
{ printf 'x' > "$1/.probe"; } 2>/dev/null || return 1
|
||||
rm -f "$1/.probe"
|
||||
}
|
||||
|
||||
# The folder the run keeps its state in, and its log, started afresh with the
|
||||
# previous one kept as log.prev.txt. A read-only or full card moves both to
|
||||
# XDG_RUNTIME_DIR or /tmp: the interface then runs and says itself what it
|
||||
# cannot write. "fresh" starts the log, "keep" leaves it as the launcher
|
||||
# opened it.
|
||||
rb_open_log() {
|
||||
local spare="${XDG_RUNTIME_DIR:-/tmp}/retrobios" moved=""
|
||||
if ! rb_can_write "$RB_STATE" || ! { : >> "$RB_LOG"; } 2>/dev/null; then
|
||||
moved="$RB_DIR"
|
||||
RB_STATE="$spare/run"
|
||||
RB_LOG="$spare/log.txt"
|
||||
if ! rb_can_write "$RB_STATE"; then
|
||||
RB_STATE_PROBLEM="RetroBIOS cannot write to $moved or to $spare: the SD card may be read-only or full."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
if [ "${1:-}" != "keep" ] || [ -n "$moved" ]; then
|
||||
if [ -s "$RB_LOG" ]; then
|
||||
mv -f "$RB_LOG" "${RB_LOG%.txt}.prev.txt" 2>/dev/null
|
||||
fi
|
||||
: > "$RB_LOG"
|
||||
fi
|
||||
if [ -n "$moved" ]; then
|
||||
rb_say "$moved cannot be written (read-only or full card): the log and the run's files are in $spare"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- Python ---------------------------------------------------------------
|
||||
|
||||
rb_python_ok() {
|
||||
@@ -144,20 +261,22 @@ rb_python_text() {
|
||||
|
||||
# Whether the file list can be reached. Only a failure to resolve or to
|
||||
# connect counts as offline: a certificate refused by an old curl still means
|
||||
# a network, and install.py names that problem itself.
|
||||
# a network, and install.py names that problem itself. Bounded as a whole:
|
||||
# GNU wget retries a timeout 20 times.
|
||||
rb_online() {
|
||||
local code
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -sSI --max-time 8 "$RB_PROBE_URL" >/dev/null 2>&1
|
||||
rb_bounded 8 curl -sSI --connect-timeout 5 --max-time 7 "$RB_PROBE_URL" >/dev/null 2>&1
|
||||
code=$?
|
||||
case "$code" in
|
||||
5|6|7|28) return 1 ;;
|
||||
5|6|7|28|124) return 1 ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
fi
|
||||
if command -v wget >/dev/null 2>&1; then
|
||||
wget -q --spider -T 8 "$RB_PROBE_URL" >/dev/null 2>&1
|
||||
[ $? -ne 4 ]
|
||||
rb_bounded 8 wget -q --spider -T 7 "$RB_PROBE_URL" >/dev/null 2>&1
|
||||
code=$?
|
||||
[ "$code" -ne 4 ] && [ "$code" -ne 124 ]
|
||||
return
|
||||
fi
|
||||
# Neither tool: a default route is the best guess there is.
|
||||
@@ -166,12 +285,18 @@ rb_online() {
|
||||
|
||||
# The system and BIOS folder install.py finds here, read without a network:
|
||||
# detection is local, and the run stops where the file list would download.
|
||||
# That download is sent to a loopback port nothing listens on, so it fails
|
||||
# at once instead of retrying three times 30 s on a network already found
|
||||
# missing.
|
||||
rb_detect() {
|
||||
RB_PLATFORM=""
|
||||
RB_PLATFORM_NAME=""
|
||||
RB_BIOS_DIR=""
|
||||
mkdir -p "$RB_STATE"
|
||||
rb_py "$RB_INSTALL" --check --events > "$RB_STATE/detect.jsonl" 2>> "$RB_LOG"
|
||||
rb_pycmd
|
||||
RETROBIOS_BASE_URL="http://127.0.0.1:9" rb_bounded "$RB_DETECT_LIMIT" \
|
||||
"${RB_PYCMD[@]}" "$RB_INSTALL" --check --events > "$RB_STATE/detect.jsonl" 2>> "$RB_LOG"
|
||||
[ $? = 124 ] && rb_say "the detection run took over $RB_DETECT_LIMIT s and was stopped"
|
||||
local kind a b c
|
||||
while IFS=$'\t' read -r kind a b c; do
|
||||
if [ "$kind" = "platform" ]; then
|
||||
@@ -184,9 +309,10 @@ rb_detect() {
|
||||
done < <(rb_py "$RB_EVENTS" status "$RB_STATE/detect.jsonl" 2>> "$RB_LOG")
|
||||
}
|
||||
|
||||
# The first pack in RB_PACK_PATH, one whose name spells this system first:
|
||||
# install.py reads the platform from the same spelling (Name_With_Underscores
|
||||
# between two underscores).
|
||||
# The first pack in RB_PACK_PATH whose name spells this system: install.py
|
||||
# reads the platform from the same spelling (Name_With_Underscores between
|
||||
# two underscores) and refuses a pack named for another. Any pack only when
|
||||
# the system is not known, since install.py then reads it from the name.
|
||||
rb_find_pack() {
|
||||
local dir file base stem first="" want=""
|
||||
if [ -n "${RB_PLATFORM_NAME:-}" ]; then
|
||||
@@ -203,13 +329,27 @@ rb_find_pack() {
|
||||
printf '%s\n' "$file"
|
||||
return 0
|
||||
fi
|
||||
[ -z "$first" ] && first="$file"
|
||||
[ -z "$want" ] && [ -z "$first" ] && first="$file"
|
||||
done
|
||||
done
|
||||
[ -n "$first" ] && printf '%s\n' "$first"
|
||||
return 0
|
||||
}
|
||||
|
||||
# How the release pack that serves this system is named: the platforms of its
|
||||
# group, as packs.txt lists them. build_port.py writes packs.txt from the
|
||||
# registry's release_pack platforms, so a system with no line has no pack.
|
||||
rb_pack_label() {
|
||||
[ -n "${RB_PLATFORM:-}" ] && [ -f "$RB_DIR/packs.txt" ] || return 1
|
||||
awk -F'\t' -v p="$RB_PLATFORM" '$1 == p { print $2; found = 1; exit } END { exit !found }' \
|
||||
"$RB_DIR/packs.txt"
|
||||
}
|
||||
|
||||
# Whether the system is known to have no release pack.
|
||||
rb_no_pack_serves() {
|
||||
[ -n "${RB_PLATFORM:-}" ] && [ -f "$RB_DIR/packs.txt" ] && ! rb_pack_label > /dev/null
|
||||
}
|
||||
|
||||
rb_same_dir() {
|
||||
[ -n "$1" ] && [ -n "$2" ] || return 1
|
||||
[ "$(stat -L -c '%d:%i' "$1" 2>/dev/null)" = "$(stat -L -c '%d:%i' "$2" 2>/dev/null)" ]
|
||||
@@ -235,8 +375,14 @@ rb_other_card() {
|
||||
}
|
||||
|
||||
rb_pack_text() {
|
||||
local dir="${RB_PACK_PATH%%:*}" pack
|
||||
if [ -n "${RB_PLATFORM_NAME:-}" ]; then
|
||||
local dir="${RB_PACK_PATH%%:*}" pack label
|
||||
if rb_no_pack_serves; then
|
||||
printf '%s\n' "No network connection, and the releases carry no BIOS pack for ${RB_PLATFORM_NAME:-$RB_PLATFORM}: connect to Wi-Fi and start RetroBIOS again. $RB_PC_ROUTE"
|
||||
return 0
|
||||
fi
|
||||
if label=$(rb_pack_label); then
|
||||
pack="the $label BIOS pack (a file whose name starts with ${label}_ and ends in _BIOS_Pack.zip)"
|
||||
elif [ -n "${RB_PLATFORM_NAME:-}" ]; then
|
||||
pack="the BIOS pack whose name contains ${RB_PLATFORM_NAME// /_}"
|
||||
else
|
||||
pack="the BIOS pack for this system (a file whose name ends in _BIOS_Pack.zip)"
|
||||
@@ -269,44 +415,50 @@ rb_terminal_app() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Sets RB_ROUTE to the first of RB_ROUTES that can show text: here, app,
|
||||
# vt, or none.
|
||||
rb_route() {
|
||||
local route
|
||||
RB_ROUTE="none"
|
||||
for route in $RB_ROUTES; do
|
||||
case "$route" in
|
||||
here)
|
||||
if [ -t 0 ] && [ -t 1 ]; then
|
||||
RB_ROUTE="here"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
app)
|
||||
if rb_terminal_app; then
|
||||
RB_ROUTE="app"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
vt)
|
||||
# A compositor or X server holds the console it runs on: text
|
||||
# written there is never seen, so the console needs a switch.
|
||||
if rb_graphical; then
|
||||
if [ -n "${RB_VT_SWITCH:-}" ] && command -v chvt >/dev/null 2>&1 \
|
||||
&& [ -n "$(rb_active_vt)" ]; then
|
||||
RB_ROUTE="vt"
|
||||
return 0
|
||||
fi
|
||||
elif [ -c "$RB_CONSOLE" ]; then
|
||||
RB_ROUTE="vt"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
# A console a terminal can be read from. PortMaster's mod_muOS.txt sets
|
||||
# CUR_TTY to /dev/null, a character device that is no console at all. A
|
||||
# console this user cannot open yet (rb_vt_enter opens it up) is judged by
|
||||
# its name.
|
||||
rb_is_console() {
|
||||
[ -c "$1" ] || return 1
|
||||
if [ -r "$1" ] && [ -w "$1" ]; then
|
||||
{ [ -t 0 ]; } < "$1" 2>/dev/null
|
||||
return
|
||||
fi
|
||||
case "$1" in
|
||||
/dev/tty[0-9]*|/dev/console|/dev/pts/*) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
# Whether a route can show text here. A compositor or X server holds the
|
||||
# console it runs on: text written there is never seen, so the console needs
|
||||
# a switch.
|
||||
rb_route_ready() {
|
||||
case "$1" in
|
||||
here)
|
||||
[ -t 0 ] && [ -t 1 ]
|
||||
;;
|
||||
app)
|
||||
rb_terminal_app
|
||||
;;
|
||||
vt)
|
||||
if rb_graphical; then
|
||||
[ -n "${RB_VT_SWITCH:-}" ] && command -v chvt >/dev/null 2>&1 \
|
||||
&& [ -n "$(rb_active_vt)" ]
|
||||
elif rb_is_console "$RB_CONSOLE"; then
|
||||
return 0
|
||||
else
|
||||
rb_say "$RB_CONSOLE is not a console: no route on it"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# The largest Terminus font that keeps 56 columns and 20 rows, the way
|
||||
# dArkOS's own tools pick one per screen (Lat7-Terminus16 on 480x320,
|
||||
# TerminusBold22x11 on 640x480, 32x16 on 1024x768). Only changed when the
|
||||
@@ -368,12 +520,34 @@ rb_active_vt() {
|
||||
printf '%s\n' "$active"
|
||||
}
|
||||
|
||||
# chvt, and whether the console really came to the front. chvt waits for the
|
||||
# switch (VT_WAITACTIVE): a compositor that keeps the display would hold it
|
||||
# forever, so it gets 3 s.
|
||||
rb_chvt() {
|
||||
local pid waited=0
|
||||
$ESUDO chvt "$1" >> "$RB_LOG" 2>&1 &
|
||||
pid=$!
|
||||
while [ "$waited" -lt 12 ]; do
|
||||
[ "$(rb_active_vt)" = "$1" ] && return 0
|
||||
rb_nap 0.25
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
$ESUDO kill "$pid" >/dev/null 2>&1
|
||||
return 1
|
||||
}
|
||||
|
||||
rb_vt_enter() {
|
||||
RB_TTY="$RB_CONSOLE"
|
||||
if rb_graphical && [ -n "${RB_VT_SWITCH:-}" ]; then
|
||||
RB_VT_BACK=$(rb_active_vt)
|
||||
if ! rb_chvt "$RB_VT_SWITCH"; then
|
||||
rb_say "console $RB_VT_SWITCH did not come to the front (console ${RB_VT_BACK:-?} still is)"
|
||||
[ -n "$RB_VT_BACK" ] && rb_chvt "$RB_VT_BACK"
|
||||
RB_TTY=""
|
||||
RB_VT_BACK=""
|
||||
return 1
|
||||
fi
|
||||
RB_TTY="${RB_VT_PREFIX:-/dev/tty}$RB_VT_SWITCH"
|
||||
$ESUDO chvt "$RB_VT_SWITCH" >> "$RB_LOG" 2>&1
|
||||
fi
|
||||
if [ ! -r "$RB_TTY" ] || [ ! -w "$RB_TTY" ]; then
|
||||
$ESUDO chmod 666 "$RB_TTY" >> "$RB_LOG" 2>&1
|
||||
@@ -391,32 +565,98 @@ rb_vt_leave() {
|
||||
stty "$RB_STTY" < "$RB_TTY" 2>/dev/null
|
||||
fi
|
||||
if [ -n "$RB_VT_BACK" ]; then
|
||||
$ESUDO chvt "$RB_VT_BACK" >> "$RB_LOG" 2>&1
|
||||
rb_chvt "$RB_VT_BACK"
|
||||
fi
|
||||
RB_TTY=""
|
||||
RB_STTY=""
|
||||
RB_VT_BACK=""
|
||||
}
|
||||
|
||||
# gptokeyb on a name only this installer's processes carry: kill mode runs
|
||||
# pkill -f on it, and the path of install.py is in the command line of the
|
||||
# interface and of every run it starts, never in python3's alone.
|
||||
# --- Controller and Start + Select ----------------------------------------
|
||||
|
||||
# Start + Select is the mapper's kill mode: gptokeyb runs pkill -f on its
|
||||
# target, then pkill -9 3 s later, and gptokeyb2 -X runs kill -9 at once.
|
||||
# Either would reach the interface's engine too, whose command line carries
|
||||
# the installer's path, and SIGKILL leaves no time to say what an install
|
||||
# wrote. So the target is a sentinel, a process this launcher owns whose
|
||||
# command line alone carries the token: when it dies, the launcher asks the
|
||||
# interface to stop, and the interface gives a running install its 2 s.
|
||||
rb_stop_start() {
|
||||
RB_STOP_TOKEN="retrobios-stop-${$}x"
|
||||
bash -c 'exec {f}<> <(:); while :; do read -r -t 3600 -u "$f" _; done' \
|
||||
"$RB_STOP_TOKEN" > /dev/null 2>&1 &
|
||||
RB_STOP_PID=$!
|
||||
}
|
||||
|
||||
rb_stop_requested() {
|
||||
[ -n "$RB_STOP_PID" ] && ! rb_alive "$RB_STOP_PID"
|
||||
}
|
||||
|
||||
rb_stop_end() {
|
||||
[ -n "$RB_STOP_PID" ] || return 0
|
||||
kill "$RB_STOP_PID" >/dev/null 2>&1
|
||||
RB_STOP_PID=""
|
||||
}
|
||||
|
||||
# The mapper, checked half a second after it starts: where it is the only
|
||||
# input, a mapper that exits at once leaves an interface nothing can move.
|
||||
rb_mapper_start() {
|
||||
[ -n "${RB_MAPPER:-}" ] || return 0
|
||||
[ -n "$RB_MAPPER_PID" ] && return 0
|
||||
rb_stop_start
|
||||
# shellcheck disable=SC2086 # RB_MAPPER is a command and its flags, as $GPTOKEYB is
|
||||
$RB_MAPPER "$RB_INSTALL" -c "$RB_GPTK" >> "$RB_LOG" 2>&1 &
|
||||
$RB_MAPPER "$RB_STOP_TOKEN" -c "$RB_GPTK" >> "$RB_LOG" 2>&1 &
|
||||
RB_MAPPER_PID=$!
|
||||
rb_nap 0.5
|
||||
if ! rb_alive "$RB_MAPPER_PID"; then
|
||||
rb_say "the controller mapper ($RB_MAPPER) stopped as soon as it started"
|
||||
fi
|
||||
}
|
||||
|
||||
# The mapper was started, is needed, and is no longer running.
|
||||
rb_mapper_gone() {
|
||||
[ -n "${RB_MAPPER:-}" ] && [ -n "$RB_MAPPER_PID" ] && [ "$RB_MAPPER_NEEDED" != "0" ] \
|
||||
&& ! rb_alive "$RB_MAPPER_PID"
|
||||
}
|
||||
|
||||
rb_mapper_stop() {
|
||||
[ -n "$RB_MAPPER_PID" ] || return 0
|
||||
$ESUDO kill "$RB_MAPPER_PID" >/dev/null 2>&1
|
||||
wait "$RB_MAPPER_PID" 2>/dev/null
|
||||
if [ -n "$RB_MAPPER_PID" ]; then
|
||||
$ESUDO kill "$RB_MAPPER_PID" >/dev/null 2>&1
|
||||
wait "$RB_MAPPER_PID" 2>/dev/null
|
||||
fi
|
||||
RB_MAPPER_PID=""
|
||||
rb_stop_end
|
||||
}
|
||||
|
||||
# A screen of text on the current terminal, closed by any button.
|
||||
rb_message() {
|
||||
rb_mapper_text() {
|
||||
printf '%s\n' "The controller cannot be read: gptokeyb, which turns the buttons into keys, stopped as soon as it started. What it said is in $RB_LOG. $RB_PC_ROUTE"
|
||||
}
|
||||
|
||||
# --- Messages -------------------------------------------------------------
|
||||
|
||||
# One key into RB_KEY. False when none came: the wait ran out, Start + Select
|
||||
# was pressed, or the mapper is gone and only a keyboard is left, which gets
|
||||
# RB_DEAD_WAIT seconds.
|
||||
rb_read_key() {
|
||||
local limit="${RB_MESSAGE_WAIT:-300}" waited=0 code
|
||||
RB_KEY=""
|
||||
while [ "$waited" -lt "$limit" ]; do
|
||||
rb_stop_requested && return 1
|
||||
if rb_mapper_gone && [ "$limit" -gt "$RB_DEAD_WAIT" ]; then
|
||||
limit="$RB_DEAD_WAIT"
|
||||
[ "$waited" -lt "$limit" ] || return 1
|
||||
fi
|
||||
IFS= read -r -s -n 1 -t 1 RB_KEY
|
||||
code=$?
|
||||
[ "$code" = 0 ] && return 0
|
||||
# The end of the input, not a timeout.
|
||||
[ "$code" -gt 128 ] || return 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
rb_screen() {
|
||||
local cols
|
||||
cols=$(stty size 2>/dev/null | awk '{print $2}')
|
||||
case "$cols" in
|
||||
@@ -426,60 +666,133 @@ rb_message() {
|
||||
printf '\033[2J\033[H'
|
||||
printf 'RetroBIOS\n\n'
|
||||
printf '%s\n' "$1" | fold -s -w $((cols - 1))
|
||||
printf '\n%s\n' "Press A or Enter to close."
|
||||
read -r -s -n 1 -t "${RB_MESSAGE_WAIT:-300}" _
|
||||
printf '\n%s\n' "$2"
|
||||
}
|
||||
|
||||
# A screen of text on the current terminal, closed by any button.
|
||||
rb_message() {
|
||||
rb_screen "$1" "Press A or Enter to close."
|
||||
rb_read_key
|
||||
printf '\033[2J\033[H'
|
||||
}
|
||||
|
||||
# A question on the current terminal: true for A or Enter, false for B, Esc,
|
||||
# any other button or no answer.
|
||||
rb_ask() {
|
||||
local cols key
|
||||
cols=$(stty size 2>/dev/null | awk '{print $2}')
|
||||
case "$cols" in
|
||||
''|*[!0-9]*) cols=40 ;;
|
||||
esac
|
||||
[ "$cols" -ge 20 ] || cols=40
|
||||
printf '\033[2J\033[H'
|
||||
printf 'RetroBIOS\n\n'
|
||||
printf '%s\n' "$1" | fold -s -w $((cols - 1))
|
||||
printf '\n%s\n' "A or Enter: yes. B or Esc: no."
|
||||
if ! IFS= read -r -s -n 1 -t "${RB_MESSAGE_WAIT:-300}" key; then
|
||||
rb_screen "$1" "A or Enter: yes. B or Esc: no."
|
||||
if ! rb_read_key; then
|
||||
printf '\033[2J\033[H'
|
||||
return 1
|
||||
fi
|
||||
# The rest of an escape sequence (an arrow) is not a second answer.
|
||||
read -r -s -t 0.05 -n 8 _ 2>/dev/null
|
||||
printf '\033[2J\033[H'
|
||||
[ -z "$key" ] || [ "$key" = " " ]
|
||||
[ -z "$RB_KEY" ] || [ "$RB_KEY" = " " ]
|
||||
}
|
||||
|
||||
# --- The run --------------------------------------------------------------
|
||||
|
||||
# The interface itself, watched: Start + Select or a mapper gone (nothing
|
||||
# moves it any more) asks it to stop with SIGTERM, then SIGKILL after
|
||||
# RB_STOP_GRACE seconds. Its stdin is named, since a background command in a
|
||||
# script reads /dev/null otherwise.
|
||||
rb_interface() {
|
||||
local status naps=0
|
||||
rb_pycmd
|
||||
# A Ctrl-C typed while the terminal still sends signals reaches the
|
||||
# interface on its own: this shell keeps watching.
|
||||
trap ':' INT
|
||||
"${RB_PYCMD[@]}" "$RB_INSTALL" --tui "$@" <&0 2> "$RB_STATE/tui.err" &
|
||||
RB_UI_PID=$!
|
||||
while rb_alive "$RB_UI_PID"; do
|
||||
if [ -z "$RB_STOPPED" ]; then
|
||||
if rb_stop_requested; then
|
||||
RB_STOPPED="start_select"
|
||||
rb_say "Start + Select: the interface is asked to stop"
|
||||
elif rb_mapper_gone; then
|
||||
RB_STOPPED="mapper"
|
||||
rb_say "the controller mapper stopped: the interface is asked to stop"
|
||||
fi
|
||||
[ -n "$RB_STOPPED" ] && kill -TERM "$RB_UI_PID" 2>/dev/null
|
||||
else
|
||||
naps=$((naps + 1))
|
||||
if [ "$naps" -ge $((RB_STOP_GRACE * 4)) ]; then
|
||||
rb_say "the interface did not stop within $RB_STOP_GRACE s"
|
||||
kill -KILL "$RB_UI_PID" 2>/dev/null
|
||||
rb_kill_engines
|
||||
fi
|
||||
fi
|
||||
rb_nap 0.25
|
||||
done
|
||||
wait "$RB_UI_PID"
|
||||
status=$?
|
||||
trap - INT
|
||||
RB_UI_PID=""
|
||||
return "$status"
|
||||
}
|
||||
|
||||
# An engine the interface started survives it when the interface is killed:
|
||||
# it runs in its own session.
|
||||
rb_kill_engines() {
|
||||
command -v pkill >/dev/null 2>&1 || return 0
|
||||
pkill -KILL -f "$RB_INSTALL" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
rb_ui_stop() {
|
||||
local naps=0
|
||||
rb_alive "$RB_UI_PID" || return 0
|
||||
kill -TERM "$RB_UI_PID" 2>/dev/null
|
||||
while rb_alive "$RB_UI_PID" && [ "$naps" -lt $((RB_STOP_GRACE * 4)) ]; do
|
||||
rb_nap 0.25
|
||||
naps=$((naps + 1))
|
||||
done
|
||||
if rb_alive "$RB_UI_PID"; then
|
||||
kill -KILL "$RB_UI_PID" 2>/dev/null
|
||||
rb_kill_engines
|
||||
fi
|
||||
RB_UI_PID=""
|
||||
}
|
||||
|
||||
# The interface on the current terminal: install.py --tui, from a pack on the
|
||||
# card when there is no network. The status also goes to RB_STATE/status,
|
||||
# since a terminal app does not always pass its child's on.
|
||||
rb_here() {
|
||||
local status pack args=()
|
||||
local status pack args=() other
|
||||
mkdir -p "$RB_STATE"
|
||||
rm -f "$RB_STATE/status"
|
||||
case "${TERM:-}" in
|
||||
''|dumb|unknown) export TERM=xterm ;;
|
||||
esac
|
||||
if rb_mapper_gone; then
|
||||
rb_message "$(rb_mapper_text)"
|
||||
printf '1\n' > "$RB_STATE/status"
|
||||
return 1
|
||||
fi
|
||||
if [ -z "$RB_PYTHON" ]; then
|
||||
rb_message "$(rb_python_text)"
|
||||
printf '1\n' > "$RB_STATE/status"
|
||||
return 1
|
||||
fi
|
||||
local other
|
||||
printf '\033[2J\033[HRetroBIOS\n\nChecking the connection...\n'
|
||||
if ! rb_online; then
|
||||
printf 'No connection. Looking for the BIOS folder and a pack on the card...\n'
|
||||
# Detection also names the BIOS folder the other card must not be.
|
||||
rb_detect
|
||||
if rb_no_pack_serves; then
|
||||
# Copying from the other card needs a file list too: nothing to ask.
|
||||
rb_message "$(rb_pack_text)"
|
||||
printf '1\n' > "$RB_STATE/status"
|
||||
return 1
|
||||
fi
|
||||
other=$(rb_other_card)
|
||||
if [ -n "$other" ] && rb_ask "Copy the BIOS files from your other card? $other holds files. RetroBIOS copies the ones this system needs, each checked by its hash, and leaves that card as it is."; then
|
||||
args=(--from "$other")
|
||||
fi
|
||||
pack=$(rb_find_pack)
|
||||
if rb_stop_requested; then
|
||||
printf '143\n' > "$RB_STATE/status"
|
||||
return 143
|
||||
fi
|
||||
if [ -z "$pack" ]; then
|
||||
if [ "${#args[@]}" -gt 0 ]; then
|
||||
# --from needs the list of files, which comes from the network or a pack.
|
||||
@@ -495,7 +808,7 @@ rb_here() {
|
||||
args+=(--platform "$RB_PLATFORM")
|
||||
fi
|
||||
fi
|
||||
rb_py "$RB_INSTALL" --tui "${args[@]}" "$@" 2> "$RB_STATE/tui.err"
|
||||
rb_interface "${args[@]}" "$@"
|
||||
status=$?
|
||||
cat "$RB_STATE/tui.err" >> "$RB_LOG" 2>/dev/null
|
||||
if [ "$status" = "2" ] && grep -q -- "--tui cannot open" "$RB_STATE/tui.err"; then
|
||||
@@ -503,59 +816,124 @@ rb_here() {
|
||||
if [ "${RB_FALLBACK:-0}" != "1" ]; then
|
||||
rb_message "$(tail -n 1 "$RB_STATE/tui.err") The messages are in $RB_LOG."
|
||||
fi
|
||||
elif [ -n "$RB_STOPPED" ] || [ "$status" -ge 128 ]; then
|
||||
# Ended by Start + Select or a signal: the player asked to leave, and no
|
||||
# button may be left to close a message.
|
||||
:
|
||||
elif [ "$status" != "0" ] && [ -s "$RB_STATE/tui.err" ]; then
|
||||
rb_message "$(tail -n 1 "$RB_STATE/tui.err") The messages are in $RB_LOG."
|
||||
# The first line of the farewell: the error screen it closes was read.
|
||||
rb_message "$(head -n 1 "$RB_STATE/tui.err") The messages are in $RB_LOG."
|
||||
fi
|
||||
printf '%s\n' "$status" > "$RB_STATE/status"
|
||||
return "$status"
|
||||
}
|
||||
|
||||
rb_cleanup() {
|
||||
rb_ui_stop
|
||||
if rb_alive "$RB_APP_PID"; then
|
||||
kill -TERM "$RB_APP_PID" 2>/dev/null
|
||||
wait "$RB_APP_PID" 2>/dev/null
|
||||
fi
|
||||
RB_APP_PID=""
|
||||
rb_vt_leave
|
||||
rb_mapper_stop
|
||||
}
|
||||
|
||||
# The whole session: the terminal, the mapper, the interface, then the
|
||||
# console, its font and the mapper put back. Returns the interface's status,
|
||||
# 125 when no terminal could show it.
|
||||
rb_launch() {
|
||||
local status
|
||||
mkdir -p "$RB_STATE"
|
||||
if ! rb_route; then
|
||||
printf '%s\n' "RetroBIOS: no terminal to show the interface on (routes: $RB_ROUTES)" >> "$RB_LOG"
|
||||
return 125
|
||||
fi
|
||||
export RB_DIR RB_INSTALL RB_EVENTS RB_STATE RB_LOG RB_PACK_PATH RB_FALLBACK
|
||||
export RB_OTHER_CARDS RB_BIOS_DEST
|
||||
export RB_PYTHON RB_PYTHONHOME RB_PYTHON_PROBLEM RB_MESSAGE_WAIT RB_ROUTE_TABLE
|
||||
trap 'rb_cleanup; exit 143' TERM HUP
|
||||
rb_mapper_start
|
||||
# One route: sets RB_ROUTE_FAILED when it could not show the interface, so
|
||||
# the next route is tried.
|
||||
rb_route_run() {
|
||||
local status code
|
||||
RB_ROUTE_FAILED=""
|
||||
case "$RB_ROUTE" in
|
||||
here)
|
||||
rb_here "$@"
|
||||
status=$?
|
||||
;;
|
||||
app)
|
||||
"${RB_TERMAPP[@]}" bash "$RB_LIB" here "$@" 2>> "$RB_LOG"
|
||||
status=$(cat "$RB_STATE/status" 2>/dev/null)
|
||||
rm -f "$RB_STATE/status"
|
||||
"${RB_TERMAPP[@]}" bash "$RB_LIB" here "$@" 2>> "$RB_LOG" &
|
||||
RB_APP_PID=$!
|
||||
wait "$RB_APP_PID"
|
||||
code=$?
|
||||
RB_APP_PID=""
|
||||
if [ -f "$RB_STATE/status" ]; then
|
||||
status=$(cat "$RB_STATE/status")
|
||||
else
|
||||
# foot or xterm refused to open (no display, no XAUTHORITY): the
|
||||
# interface never ran.
|
||||
rb_say "${RB_TERMAPP[0]} ended with status $code before the interface ran"
|
||||
RB_ROUTE_FAILED=1
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
vt)
|
||||
rb_vt_enter
|
||||
if ! rb_vt_enter; then
|
||||
RB_ROUTE_FAILED=1
|
||||
return 1
|
||||
fi
|
||||
export TERM=linux
|
||||
# shellcheck disable=SC2094 # a console is read and written at once by design
|
||||
rb_here "$@" < "$RB_TTY" > "$RB_TTY" 2>> "$RB_LOG"
|
||||
status=$?
|
||||
rb_vt_leave
|
||||
;;
|
||||
esac
|
||||
return "${status:-1}"
|
||||
}
|
||||
|
||||
# The whole session: the terminal, the mapper, the interface, then the
|
||||
# console, its font and the mapper put back. Returns the interface's status,
|
||||
# 125 when no terminal could show it.
|
||||
rb_launch() {
|
||||
local status="" route
|
||||
RB_STOPPED=""
|
||||
if [ -n "$RB_STATE_PROBLEM" ]; then
|
||||
printf '%s\n' "$RB_STATE_PROBLEM" >&2
|
||||
if rb_is_console "$RB_CONSOLE"; then
|
||||
printf '\r\n%s\r\n' "$RB_STATE_PROBLEM" > "$RB_CONSOLE" 2>/dev/null
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
mkdir -p "$RB_STATE"
|
||||
export RB_DIR RB_INSTALL RB_EVENTS RB_STATE RB_LOG RB_PACK_PATH RB_FALLBACK
|
||||
export RB_OTHER_CARDS RB_BIOS_DEST RB_STOP_GRACE RB_DEAD_WAIT RB_DETECT_LIMIT
|
||||
export RB_PYTHON RB_PYTHONHOME RB_PYTHON_PROBLEM RB_MESSAGE_WAIT RB_ROUTE_TABLE
|
||||
# The interface names the buttons of this route in its hints.
|
||||
if [ -z "$RB_PAD" ] && [ -n "${RB_MAPPER:-}" ]; then
|
||||
RB_PAD="gptokeyb"
|
||||
fi
|
||||
if [ -n "$RB_PAD" ]; then
|
||||
export RETROBIOS_PAD="$RB_PAD"
|
||||
fi
|
||||
trap 'rb_cleanup; exit 143' TERM HUP
|
||||
for route in $RB_ROUTES; do
|
||||
rb_route_ready "$route" || continue
|
||||
rb_mapper_start
|
||||
export RB_MAPPER RB_MAPPER_PID RB_MAPPER_NEEDED RB_STOP_PID
|
||||
RB_ROUTE="$route"
|
||||
rb_say "showing the interface by the $route route"
|
||||
rb_route_run "$@"
|
||||
status=$?
|
||||
[ -z "$RB_ROUTE_FAILED" ] && break
|
||||
status=""
|
||||
done
|
||||
rb_cleanup
|
||||
trap - TERM HUP
|
||||
return "${status:-1}"
|
||||
if [ -z "$status" ]; then
|
||||
RB_ROUTE="none"
|
||||
rb_say "no terminal to show the interface on (routes: $RB_ROUTES)"
|
||||
return 125
|
||||
fi
|
||||
return "$status"
|
||||
}
|
||||
|
||||
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||
case "${1:-}" in
|
||||
here)
|
||||
shift
|
||||
# The terminal app closed or the launcher signalled: the interface
|
||||
# gets its time to stop.
|
||||
trap 'rb_ui_stop; exit 143' TERM HUP
|
||||
rb_here "$@"
|
||||
exit $?
|
||||
;;
|
||||
|
||||
@@ -13,7 +13,8 @@ the missing ones: downloaded with a network connection, or from a pack copied
|
||||
into the `retrobios` folder without one.
|
||||
|
||||
Controls: D-pad to move, A or Start to choose, B or Select to go back, X to
|
||||
tick a system, Y for the options, L1 and R1 to page, Start + Select to quit.
|
||||
tick a system, Y for the options, L1 and R1 to page. To quit, hold Select, then press Start: Start pressed
|
||||
first would also choose the line in focus.
|
||||
|
||||
ArkOS has Python 3.7, older than the installer needs: RetroBIOS uses the
|
||||
Python runtime the RetroBIOS PortMaster port downloads, so start that port
|
||||
|
||||
@@ -15,6 +15,8 @@ ROOT="${RB_ROOT:-}"
|
||||
RB_DIR="$HERE/retrobios"
|
||||
RB_LOG="$RB_DIR/log.txt"
|
||||
ESUDO=""
|
||||
MAPPER_SUDO=""
|
||||
kill_flag="-1"
|
||||
OS_NAME=""
|
||||
if [ -f "$ROOT/etc/os-release" ]; then
|
||||
OS_NAME=$(awk -F= '$1 == "NAME" { gsub(/"/, "", $2); print $2 }' "$ROOT/etc/os-release")
|
||||
@@ -49,6 +51,10 @@ if [ -d "$ROOT/opt/system" ] && [ -d "$ROOT/opt/inttools" ]; then
|
||||
mapper=$(rb_gptokeyb "$ROOT/opt/inttools/gptokeyb" "$controlfolder/gptokeyb")
|
||||
[ -f "$ROOT/opt/inttools/gamecontrollerdb.txt" ] \
|
||||
&& export SDL_GAMECONTROLLERCONFIG_FILE="$ROOT/opt/inttools/gamecontrollerdb.txt"
|
||||
# sudo's env_reset drops the controller list exported above, and a pad
|
||||
# SDL does not know then sends nothing: PortMaster keeps it the same way
|
||||
# on ArkOS (control.txt, ESUDO).
|
||||
[ -n "$ESUDO" ] && MAPPER_SUDO="sudo --preserve-env=SDL_GAMECONTROLLERCONFIG_FILE"
|
||||
elif [ "$OS_NAME" = "AmberELEC" ]; then
|
||||
RB_ROUTES="vt"
|
||||
RB_CONSOLE="${RB_CONSOLE:-/dev/console}"
|
||||
@@ -65,15 +71,29 @@ else
|
||||
RB_PACK_PATH="$RB_DIR:$HERE:/storage/roms"
|
||||
controlfolder="/storage/roms/ports/PortMaster"
|
||||
mapper=$(rb_gptokeyb "$ROOT/usr/bin/gptokeyb" "$controlfolder/gptokeyb")
|
||||
# ROCKNIX's own tools load the pad's controller list first (fex-emu's
|
||||
# Start FEXConfig.sh: control-gen_init.sh, control.ini, get_controls), and
|
||||
# start the GPTOKEYB it names. control.ini also sets ESUDO and
|
||||
# controlfolder, which this launcher keeps as they were.
|
||||
control="$ROOT/storage/.config/gptokeyb/control.ini"
|
||||
command -v control-gen_init.sh >/dev/null 2>&1 && control-gen_init.sh >/dev/null 2>&1
|
||||
if [ -f "$control" ]; then
|
||||
saved_esudo="$ESUDO" saved_folder="$controlfolder"
|
||||
# shellcheck source=/dev/null
|
||||
source "$control"
|
||||
declare -F get_controls >/dev/null && get_controls
|
||||
# It carries its own kill flag (-sudokill).
|
||||
[ -n "${GPTOKEYB:-}" ] && mapper="$GPTOKEYB" kill_flag=""
|
||||
ESUDO="$saved_esudo" controlfolder="$saved_folder"
|
||||
fi
|
||||
fi
|
||||
# Kill mode on the installer's own path: Start + Select closes the
|
||||
# interface, never another program.
|
||||
# Kill mode: Start + Select closes the interface, never another program.
|
||||
RB_MAPPER=""
|
||||
[ -n "$mapper" ] && RB_MAPPER="$ESUDO $mapper -1"
|
||||
[ -n "$mapper" ] && RB_MAPPER="${MAPPER_SUDO:-$ESUDO} $mapper $kill_flag"
|
||||
|
||||
# shellcheck source=../shared/tui.txt
|
||||
source "$RB_DIR/tui.txt"
|
||||
: > "$RB_LOG"
|
||||
rb_open_log fresh
|
||||
rb_select_python
|
||||
# A launcher's own arguments are not install.py's (muOS passes the app folder).
|
||||
# shellcheck disable=SC2119
|
||||
|
||||
@@ -912,6 +912,22 @@
|
||||
"type": "string",
|
||||
"description": "Why source_commit is held on purpose rather than followed. The profile is then judged against its own revision, like one pinned to a superseded tag: comparing it to HEAD says nothing and recaling would repoint its refs at code the build it documents never had."
|
||||
},
|
||||
"data_root": {
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"propertyNames": {"enum": ["android", "ios", "windows", "macos", "linux"]},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["path", "source_ref"],
|
||||
"properties": {
|
||||
"path": {"type": "string", "minLength": 1, "description": "The folder as the user finds it on that OS, with the environment variable or home prefix the code expands (%APPDATA%, ~, Android/data/<package>)."},
|
||||
"source_ref": {"type": "string", "minLength": 1},
|
||||
"note": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"description": "Per operating system, the folder the app reads its user data from, under which standalone_path destinations sit. Only the systems the app runs on are keyed."
|
||||
},
|
||||
"upstream_frozen": {
|
||||
"type": "string",
|
||||
"description": "Why upstream_commit is held on purpose rather than followed: the port vendors that upstream tree, synced to that release, or merged that commit. The upstream view is judged against its own revision and a bump leaves it, the way pin_frozen holds source_commit."
|
||||
|
||||
+50
-7
@@ -46,6 +46,8 @@ MUOS_APP = "RetroBIOS.muxapp"
|
||||
TOOLS_ZIP = "RetroBIOS-tools.zip"
|
||||
# Every archive a release attaches, in the order the release notes list them.
|
||||
ARCHIVES = (PORT_ZIP, BATOCERA_ZIP, MUOS_APP, TOOLS_ZIP)
|
||||
# Which release pack serves each platform, read by tui.txt offline.
|
||||
PACKS_TABLE = "packs.txt"
|
||||
_PIN = re.compile(r'^DEFAULT_INSTALL_SHA256="([0-9a-f]{64})"$', re.MULTILINE)
|
||||
|
||||
|
||||
@@ -57,7 +59,7 @@ def pinned_installer_sha256(root: Path = REPO_ROOT) -> str:
|
||||
return found.group(1)
|
||||
|
||||
|
||||
def _engine(folder: str, root: Path, mapping: bool = True) -> list[tuple[str, Path]]:
|
||||
def _engine(folder: str, root: Path, mapping: bool = True) -> list[tuple[str, Path | None]]:
|
||||
"""What every launcher runs: the installer, the interface launcher, the
|
||||
event reader, the licenses, and the gptokeyb mapping where one is read."""
|
||||
shared = root / "ports" / "shared"
|
||||
@@ -67,14 +69,17 @@ def _engine(folder: str, root: Path, mapping: bool = True) -> list[tuple[str, Pa
|
||||
(f"{folder}/rbevents.py", shared / "rbevents.py"),
|
||||
(f"{folder}/licenses/LICENSE-retrobios.txt", root / "LICENSE"),
|
||||
(f"{folder}/licenses/NOTICE-retrobios.txt", root / "NOTICE"),
|
||||
# Written by build_archive from the registry.
|
||||
(f"{folder}/{PACKS_TABLE}", None),
|
||||
]
|
||||
if mapping:
|
||||
members.append((f"{folder}/retrobios.gptk", shared / "retrobios.gptk"))
|
||||
return members
|
||||
|
||||
|
||||
def archive_members(name: str, root: Path = REPO_ROOT) -> list[tuple[str, Path]]:
|
||||
"""(name in the archive, source file) for every member of *name*, sorted."""
|
||||
def archive_members(name: str, root: Path = REPO_ROOT) -> list[tuple[str, Path | None]]:
|
||||
"""(name in the archive, source file) for every member of *name*, sorted.
|
||||
The source is None for the packs table build_archive writes."""
|
||||
ports = root / "ports"
|
||||
if name == PORT_ZIP:
|
||||
source = ports / "RetroBIOS"
|
||||
@@ -103,7 +108,6 @@ def archive_members(name: str, root: Path = REPO_ROOT) -> list[tuple[str, Path]]
|
||||
source = ports / "muos"
|
||||
members = [
|
||||
("RetroBIOS/mux_launch.sh", source / "mux_launch.sh"),
|
||||
("RetroBIOS/muxterm.gptk", source / "muxterm.gptk"),
|
||||
("RetroBIOS/README.md", source / "README.md"),
|
||||
*_engine("RetroBIOS", root),
|
||||
]
|
||||
@@ -122,6 +126,40 @@ def archive_members(name: str, root: Path = REPO_ROOT) -> list[tuple[str, Path]]
|
||||
return sorted(members)
|
||||
|
||||
|
||||
def release_pack_labels(root: Path = REPO_ROOT) -> dict[str, str]:
|
||||
"""Platform -> the platform part of the release pack that serves it.
|
||||
|
||||
Only the platforms the registry marks release_pack have a pack, grouped
|
||||
as the release builds them (pipeline.py --include-archived). install.py
|
||||
refuses a pack whose name does not spell the platform, so a platform
|
||||
outside these groups has no pack to copy, whatever its file list.
|
||||
"""
|
||||
from common import ( # noqa: PLC0415
|
||||
group_identical_platforms,
|
||||
list_release_pack_platforms,
|
||||
load_platform_config,
|
||||
)
|
||||
from generate_pack import _name_part, _platform_pack_stem # noqa: PLC0415
|
||||
|
||||
platforms_dir = str(root / "platforms")
|
||||
pool = list_release_pack_platforms(platforms_dir, include_archived=True)
|
||||
labels: dict[str, str] = {}
|
||||
for group, representative in group_identical_platforms(pool, platforms_dir):
|
||||
stem = _platform_pack_stem(group, representative, platforms_dir)
|
||||
config = load_platform_config(representative, platforms_dir)
|
||||
version = config.get("version", config.get("dat_version", ""))
|
||||
tag = f"_{_name_part(str(version))}" if version else ""
|
||||
label = stem[: -len(tag)] if tag and stem.endswith(tag) else stem
|
||||
for platform in group:
|
||||
labels[platform] = label
|
||||
return dict(sorted(labels.items()))
|
||||
|
||||
|
||||
def _packs_table(root: Path) -> bytes:
|
||||
return "".join(f"{platform}\t{label}\n"
|
||||
for platform, label in release_pack_labels(root).items()).encode()
|
||||
|
||||
|
||||
def build_archive(name: str, output: Path, root: Path = REPO_ROOT) -> Path:
|
||||
"""Write the launcher archive *name* to *output* and return it."""
|
||||
pinned = pinned_installer_sha256(root)
|
||||
@@ -138,9 +176,14 @@ def build_archive(name: str, output: Path, root: Path = REPO_ROOT) -> Path:
|
||||
members = archive_members(name, root)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
scratch = output.with_name(f".{output.name}.tmp")
|
||||
with zipfile.ZipFile(scratch, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
|
||||
for member, path in members:
|
||||
_add_pack_member(zf, str(path), member)
|
||||
table = output.with_name(f".{output.name}.{PACKS_TABLE}")
|
||||
table.write_bytes(_packs_table(root))
|
||||
try:
|
||||
with zipfile.ZipFile(scratch, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
|
||||
for member, path in members:
|
||||
_add_pack_member(zf, str(path or table), member)
|
||||
finally:
|
||||
table.unlink()
|
||||
scratch.replace(output)
|
||||
return output
|
||||
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
RetroBIOS: Confirm
|
||||
|
||||
1 file to download, 16.0 KB
|
||||
0 in place, 1 missing, 0 wrong copies
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
0 in place, 1 missing, 0 wrong copies
|
||||
Free space: 50.0 GB
|
||||
|
||||
> Install
|
||||
|
||||
Vendored
+2
-2
@@ -1,12 +1,12 @@
|
||||
RetroBIOS: Confirm
|
||||
|
||||
1 file to download, 16.0 KB
|
||||
BIOS folder: ~/.co.../retroarch/system
|
||||
0 in place, 1 missing, 0 wrong copies
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
Free space: 50.0 GB
|
||||
|
||||
> Install
|
||||
Back
|
||||
|
||||
|
||||
A/Enter Install B/Esc Back ? Help
|
||||
Vendored
+2
-2
@@ -1,9 +1,8 @@
|
||||
RetroBIOS: Confirm
|
||||
|
||||
1 file to download, 16.0 KB
|
||||
BIOS folder: ~/.co.../retroarch/system
|
||||
0 in place, 1 missing, 0 wrong copies
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
Free space: 50.0 GB
|
||||
|
||||
> Install
|
||||
@@ -12,4 +11,5 @@
|
||||
|
||||
|
||||
|
||||
|
||||
A/Enter Install B/Esc Back ? Help
|
||||
Vendored
+1
-1
@@ -1,8 +1,8 @@
|
||||
RetroBIOS: Confirm
|
||||
|
||||
1 file to download, 16.0 KB
|
||||
0 in place, 1 missing, 0 wrong copies
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
0 in place, 1 missing, 0 wrong copies
|
||||
Free space: 50.0 GB
|
||||
|
||||
> Install
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
Each row is an option of the command line, named on the right.
|
||||
|
||||
> Preferred regions: North America, Japan --region
|
||||
Leave out game data: no --no-game-data
|
||||
Game data: kept --no-game-data
|
||||
Installed cores only: no (1 found) --installed-cores
|
||||
Cores: all --core
|
||||
Parallel downloads: 8 --jobs
|
||||
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
RetroBIOS: Options
|
||||
|
||||
> Preferred regions: North America, Ja
|
||||
Leave out game data: no
|
||||
Game data: kept
|
||||
Installed cores only: no (1 found)
|
||||
Cores: all
|
||||
Parallel downloads: 8
|
||||
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
RetroBIOS: Options
|
||||
|
||||
> Preferred regions: North America, Ja
|
||||
Leave out game data: no
|
||||
Game data: kept
|
||||
Installed cores only: no (1 found)
|
||||
Cores: all
|
||||
Parallel downloads: 8
|
||||
|
||||
Vendored
+1
-1
@@ -3,7 +3,7 @@
|
||||
Each row is an option of the command line, named on the right.
|
||||
|
||||
> Preferred regions: North America, Japan --region
|
||||
Leave out game data: no --no-game-data
|
||||
Game data: kept --no-game-data
|
||||
Installed cores only: no (1 found) --installed-cores
|
||||
Cores: all --core
|
||||
Parallel downloads: 8 --jobs
|
||||
|
||||
Vendored
+2
-2
@@ -1,11 +1,10 @@
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
1 in place, 3 missing, 1 wrong copy
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
1 in place, 3 missing, 1 wrong copy
|
||||
Free space: 50.0 GB
|
||||
They are replaced, and the old copies are kept in a backup folder.
|
||||
2 files of the list are not in the collection.
|
||||
|
||||
> Install
|
||||
Choose systems
|
||||
@@ -35,6 +34,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A/Enter Install B/Esc Back Y/o Options ? Help
|
||||
Vendored
+2
-2
@@ -1,9 +1,9 @@
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder: ~/.co.../retroarch/system
|
||||
1 in place, 3 missing, 1 wrong copy
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
Free space: 50.0 GB
|
||||
> Install
|
||||
Choose systems
|
||||
Copy from your old card or a folder
|
||||
|
||||
Vendored
+2
-2
@@ -1,12 +1,12 @@
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder: ~/.co.../retroarch/system
|
||||
1 in place, 3 missing, 1 wrong copy
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
Free space: 50.0 GB
|
||||
They are replaced, and the old copies
|
||||
are kept in a backup folder.
|
||||
|
||||
> Install
|
||||
Choose systems
|
||||
Copy from your old card or a folder
|
||||
|
||||
Vendored
+2
-2
@@ -1,11 +1,10 @@
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
1 in place, 3 missing, 1 wrong copy
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
1 in place, 3 missing, 1 wrong copy
|
||||
Free space: 50.0 GB
|
||||
They are replaced, and the old copies are kept in a backup folder.
|
||||
2 files of the list are not in the collection.
|
||||
|
||||
> Install
|
||||
Choose systems
|
||||
@@ -21,4 +20,5 @@
|
||||
|
||||
|
||||
|
||||
|
||||
A/Enter Install B/Esc Back Y/o Options ? Help
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
Free space: 50.0 GB
|
||||
|
||||
> Close
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
BIOS folder: ~/.co.../retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
Free space: 50.0 GB
|
||||
|
||||
> Close
|
||||
Options
|
||||
|
||||
|
||||
A Close B Back Y Options ? Help
|
||||
+2
-2
@@ -1,9 +1,8 @@
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
BIOS folder: ~/.co.../retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
Free space: 50.0 GB
|
||||
|
||||
> Close
|
||||
@@ -12,4 +11,5 @@
|
||||
|
||||
|
||||
|
||||
|
||||
A Close B Back Y Options ? Help
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
Free space: 50.0 GB
|
||||
|
||||
> Close
|
||||
|
||||
Vendored
+1
-1
@@ -6,7 +6,6 @@
|
||||
The files it replaced are kept in /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
To put them back: python3 install.py --restore
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
2 files of the list are not in the collection, 1 of them needed to start an emulator.
|
||||
Same choice without questions:
|
||||
python3 install.py --platform retroarch --dest /home/user/.config/retroarch/system
|
||||
Details: /tmp/retrobios-x/install.log
|
||||
@@ -35,6 +34,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A/Enter Exit Up/Down Scroll
|
||||
Vendored
+1
-1
@@ -8,5 +8,5 @@
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
To put them back: python3 install.py
|
||||
1-4 of 9
|
||||
1-4 of 8
|
||||
A/Enter Exit Up/Down Scroll
|
||||
Vendored
+1
-1
@@ -11,5 +11,5 @@
|
||||
--restore
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
1-5 of 9
|
||||
1-5 of 8
|
||||
A/Enter Exit Up/Down Scroll
|
||||
Vendored
+2
-2
@@ -7,8 +7,6 @@
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
To put them back: python3 install.py --restore
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
2 files of the list are not in the collection, 1 of them needed to start an
|
||||
emulator.
|
||||
Same choice without questions:
|
||||
python3 install.py --platform retroarch --dest
|
||||
/home/user/.config/retroarch/system
|
||||
@@ -21,4 +19,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A/Enter Exit Up/Down Scroll
|
||||
+16
-13
@@ -699,13 +699,14 @@ class FirstInstallWalkthrough(unittest.TestCase):
|
||||
step3 = _section(self.run, "The file index and the download")
|
||||
step4 = _section(self.run, "The end of the run")
|
||||
step5 = _section(self.run, "A later check")
|
||||
# First install names the three endings a run prints, and no count:
|
||||
# a figure from one setup read as the size of every install.
|
||||
# First install names the three endings the full screen shows as
|
||||
# its title, and no count: a figure from one setup read as the size
|
||||
# of every install.
|
||||
source = (ROOT / "install.py").read_text(encoding="utf-8")
|
||||
for ending in ("Done.", "Finished with errors.", "Nothing was installed."):
|
||||
for ending in ("Done", "Finished with errors", "Nothing installed"):
|
||||
with self.subTest(ending=ending):
|
||||
self.assertIn(f"`{ending}`", _section(self.text, "5. Read the last line"))
|
||||
self.assertIn(f'"\\n{ending} ', source)
|
||||
self.assertIn(f"**{ending}**", _section(self.text, "5. Read the result"))
|
||||
self.assertIn(f'title = "{ending}"', source)
|
||||
self.assertNotRegex(self.text, r"Done\. \d+ files")
|
||||
count, size = re.search(r"\n (\d+) files \(([\d.]+ [KMG]B)\)", step3).groups()
|
||||
self.assertIn(f"0/{count} present", step3)
|
||||
@@ -726,15 +727,17 @@ class FirstInstallWalkthrough(unittest.TestCase):
|
||||
self.assertIn(f"all {required} belong", step4)
|
||||
self.assertIn(f"other {total - required} are optional", step4)
|
||||
|
||||
def test_no_platform_detected_reads_like_the_installer(self):
|
||||
source = (ROOT / "install.py").read_text(encoding="utf-8")
|
||||
branch = source.split('print(" No supported platform detected.")', 1)[1][:1600]
|
||||
self.assertIn("if not interactive or listing:", branch)
|
||||
self.assertIn("_prompt_manual_platform(os_type)", branch)
|
||||
def test_nothing_found_reads_like_the_installer(self):
|
||||
# The one-liner hands the terminal to the installer, which opens the
|
||||
# full screen: its first screen says the heading and offers the row.
|
||||
import install
|
||||
|
||||
self.assertIn('"$INSTALLER" "$@" </dev/tty', (ROOT / "install.sh").read_text(encoding="utf-8"))
|
||||
section = _section(self.text, "No supported platform detected.")
|
||||
self.assertIn("a menu follows", section)
|
||||
self.assertIn("--no-input", section)
|
||||
state = install.TuiState()
|
||||
screen = "\n".join(install.tui_view(state, 80, 24))
|
||||
self.assertIn("Nothing was found here.", screen)
|
||||
section = _section(self.text, "Nothing was found here.")
|
||||
self.assertIn(f"**{install.tui_rows(state)[-1].label}**", section)
|
||||
self.assertNotIn("cannot stop and ask", section)
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -353,7 +353,8 @@ class TheInterfaceReadsAFoundEvent(unittest.TestCase):
|
||||
state, _before = self.receive("check")
|
||||
self.assertEqual(state.found, [self.FOUND])
|
||||
state, _before = self.receive("install")
|
||||
self.assertEqual(state.local_dests, {"scph5501.bin"})
|
||||
self.assertEqual(list(state.local_dests), ["scph5501.bin"])
|
||||
self.assertEqual(state.local_dests["scph5501.bin"][0], self.FOUND["folder"])
|
||||
|
||||
def test_a_dry_run_ignores_it(self):
|
||||
for run in ("confirm", "preview"):
|
||||
|
||||
+535
-17
@@ -28,7 +28,7 @@ import threading
|
||||
import time
|
||||
import unittest
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PureWindowsPath
|
||||
from unittest import mock
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -656,9 +656,10 @@ class Flow(unittest.TestCase):
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]):
|
||||
state, effects = install.tui_start(_args(), "linux")
|
||||
self.assertEqual((state.screen, effects), ("setup", []))
|
||||
self.assertEqual(state.setups, [("minui", card, "MinUI SD card")])
|
||||
label = f"MinUI SD card ({card.name})"
|
||||
self.assertEqual(state.setups, [("minui", card, label)])
|
||||
self.assertEqual([row.label for row in install.tui_rows(state)],
|
||||
["MinUI SD card", "Another setup or folder"])
|
||||
[label, "Another setup or folder"])
|
||||
|
||||
def test_an_error_offers_another_try(self):
|
||||
state = fresh_state()
|
||||
@@ -717,9 +718,11 @@ class PlanWarnings(unittest.TestCase):
|
||||
def test_a_folder_that_cannot_take_the_files_says_so_first(self):
|
||||
state = checked_state({"free": 1024, "writable": False, "cores": []})
|
||||
text = " ".join(install.tui_view(state, 120, 40))
|
||||
self.assertIn("Not enough free space for this download", text)
|
||||
self.assertIn("You cannot write to this folder: choose another one, or run the "
|
||||
"installer with sudo.", text)
|
||||
self.assertIn("Not enough free space: 1.0 MB needed.", text)
|
||||
self.assertIn("You cannot write to this folder: choose another one.", text)
|
||||
# sudo changes the home folder: the advice names this one.
|
||||
self.assertIn("Or run it as root, this folder named: sudo python3 install.py "
|
||||
f"--platform retroarch --dest {SYSTEM_DIR}", " ".join(text.split()))
|
||||
rows = install.tui_rows(state)
|
||||
self.assertEqual([row.action for row in rows[:2]], ["folder", "install"])
|
||||
self.assertEqual(state.focus.get("plan", 0), 0)
|
||||
@@ -880,6 +883,7 @@ def golden_states() -> dict:
|
||||
installing.run, installing.running = "install", True
|
||||
installing.download = {"event": "download", "files": 4, "bytes": 1064964}
|
||||
installing.samples, installing.now = [(100.0, 12672), (104.0, 540672)], 104.0
|
||||
installing.network = 540672
|
||||
installing.progress = {"files_done": 2, "files_total": 4, "bytes_done": 540672,
|
||||
"bytes_total": 1064964}
|
||||
installing.finished = [("gba_bios.bin", True, ""), ("scph5500.bin", False, "HTTP 404")]
|
||||
@@ -1100,6 +1104,10 @@ class SmallFixes(unittest.TestCase):
|
||||
install.tui_receive(state, ("event", state.run_id, {
|
||||
"event": "progress", "files_done": 0, "files_total": 2,
|
||||
"bytes_done": done, "bytes_total": 1000 * mega}))
|
||||
# The partial copy resumed is counted local by the engine.
|
||||
install.tui_receive(state, ("event", state.run_id, {
|
||||
"event": "transfer", "bytes_local": 500 * mega,
|
||||
"bytes_network": done - 500 * mega}))
|
||||
text = "\n".join(install.tui_view(state, 80, 24))
|
||||
self.assertIn("1.0 MB/s, 8:18 left", text)
|
||||
|
||||
@@ -1248,8 +1256,9 @@ class CleanupSurvivesABrokenTerminal(unittest.TestCase):
|
||||
code = install.run_tui(_args())
|
||||
self.assertIs(sys.stdout, tty)
|
||||
self.assertEqual(code, 1)
|
||||
# The crash keeps its log, and nothing else of the run.
|
||||
self.assertEqual(sorted(path.name for path in work.iterdir()), ["error.log"])
|
||||
# The crash keeps its log, marked kept, and nothing else of the run.
|
||||
self.assertEqual(sorted(path.name for path in work.iterdir()),
|
||||
["error.log", "kept", "owner"])
|
||||
self.assertEqual(termios.tcgetattr(slave), before)
|
||||
for number, handler in handlers.items():
|
||||
self.assertEqual(signal.getsignal(number), handler)
|
||||
@@ -1384,7 +1393,7 @@ class AuditRoundTwo(unittest.TestCase):
|
||||
_title, lines = install.tui_outcome(state)
|
||||
self.assertEqual([line for line in lines if "failed" in line],
|
||||
["Nothing was installed: 50 files failed.",
|
||||
" 50 files failed (folder not writable), the first f0.bin"])
|
||||
" 50 files failed, all for one reason: folder not writable."])
|
||||
|
||||
def test_p2_6_a_folder_inside_a_card_offers_its_root(self):
|
||||
card = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
||||
@@ -1444,7 +1453,12 @@ class AuditRoundTwo(unittest.TestCase):
|
||||
("retroarch", SYSTEM_DIR, "RetroArch"),
|
||||
("emudeck", HOME / "Emulation/bios", "EmuDeck")])
|
||||
self.assertEqual([label for _p, _f, label in setups],
|
||||
["RetroArch (Flatpak)", "RetroArch (retroarch/system)", "EmuDeck"])
|
||||
["RetroArch (Flatpak)", "RetroArch (installed)", "EmuDeck"])
|
||||
# Two that no package names keep the end of their folder.
|
||||
twice = install.tell_apart([("retroarch", SYSTEM_DIR, "RetroArch"),
|
||||
("retroarch", HOME / "ra2/system", "RetroArch")])
|
||||
self.assertEqual([label for _p, _f, label in twice],
|
||||
["RetroArch (retroarch/system)", "RetroArch (ra2/system)"])
|
||||
state = fresh_state()
|
||||
state.setups = setups
|
||||
self.assertIn("RetroArch (Flatpak)", "\n".join(install.tui_view(state, 40, 15)))
|
||||
@@ -1634,7 +1648,8 @@ class CopyFromAnOldCard(unittest.TestCase):
|
||||
self.recheck(state, [found("scph5500.bin", str(old)), found("gba_bios.bin", str(old))])
|
||||
self.assertEqual(state.screen, "plan")
|
||||
text = "".join("".join(install.tui_view(state, 120, 40)).split())
|
||||
self.assertIn("".join(f"2 found in {old}, 2 to download (512.0 KB)".split()), text)
|
||||
# The folder by its name: the whole path pushed the BIOS folder off.
|
||||
self.assertIn("".join(f"2 found in {old.name}, 2 to download (512.0 KB)".split()), text)
|
||||
# A second folder from the options: --from twice.
|
||||
press(state, "o")
|
||||
state.focus["options"] = next(i for i, r in enumerate(install.tui_rows(state))
|
||||
@@ -1655,11 +1670,15 @@ class CopyFromAnOldCard(unittest.TestCase):
|
||||
def test_left_takes_the_last_folder_back(self):
|
||||
state = checked_state()
|
||||
state.from_folders = [Path("/media/old")]
|
||||
state.checked_flags = install.tui_flags(state)
|
||||
state.stack.append("options")
|
||||
state.focus["options"] = next(i for i, r in enumerate(install.tui_rows(state))
|
||||
if r.action == "from")
|
||||
self.assertEqual(press(state, "left"), [("run", "check")])
|
||||
self.assertEqual(state.from_folders, [])
|
||||
# Said, and the screen stays: Back to the plan counts again.
|
||||
self.assertEqual(press(state, "left"), [])
|
||||
self.assertEqual((state.screen, state.from_folders), ("options", []))
|
||||
self.assertIn("No longer copying from old.", "\n".join(install.tui_view(state, 80, 24)))
|
||||
self.assertEqual(press(state, "esc"), [("run", "check")])
|
||||
|
||||
def test_the_command_line_folders_are_kept(self):
|
||||
with mock.patch.object(install, "detect_platforms", return_value=[]), \
|
||||
@@ -1699,10 +1718,17 @@ class CopyFromAnOldCard(unittest.TestCase):
|
||||
(10.0, {"event": "downloaded", "dest": "big.bin", "ok": True, "reason": "",
|
||||
"size": 200 * mega, "files_done": 1, "files_total": 2,
|
||||
"bytes_done": 200 * mega, "bytes_total": 300 * mega}),
|
||||
# The engine says which bytes came from this machine.
|
||||
(10.0, {"event": "transfer", "bytes_local": 200 * mega,
|
||||
"bytes_network": 0}),
|
||||
(11.0, {"event": "progress", "files_done": 1, "files_total": 2,
|
||||
"bytes_done": 201 * mega, "bytes_total": 300 * mega}),
|
||||
(11.0, {"event": "transfer", "bytes_local": 200 * mega,
|
||||
"bytes_network": mega}),
|
||||
(12.0, {"event": "progress", "files_done": 1, "files_total": 2,
|
||||
"bytes_done": 202 * mega, "bytes_total": 300 * mega})]
|
||||
"bytes_done": 202 * mega, "bytes_total": 300 * mega}),
|
||||
(12.0, {"event": "transfer", "bytes_local": 200 * mega,
|
||||
"bytes_network": 2 * mega})]
|
||||
for when, event in events:
|
||||
state.now = when
|
||||
install.tui_receive(state, ("event", state.run_id, event))
|
||||
@@ -1710,6 +1736,495 @@ class CopyFromAnOldCard(unittest.TestCase):
|
||||
self.assertIn("1.0 MB/s, 1:38 left", text)
|
||||
|
||||
|
||||
class AuditRoundThree(unittest.TestCase):
|
||||
"""tui_audit_2.md: its P1 and P2 findings, each held by a test."""
|
||||
|
||||
CARDS = {
|
||||
"muOS": ({"MUOS/": ""}, "retroarch", "MUOS/bios"),
|
||||
"Onion": ({".tmp_update/onionVersion/version.txt": "4.3.1\n"}, "retroarch", "BIOS"),
|
||||
"spruceOS": ({"spruce/": ""}, "retroarch", "BIOS"),
|
||||
"KNULLI": ({"system/batocera.conf": "a=1\nsystem.hostname=KNULLI\n"}, "knulli",
|
||||
"bios"),
|
||||
"ROCKNIX": ({".config/system/configs/system.cfg": "root.password=rocknix\n"},
|
||||
"rocknix", "roms/bios"),
|
||||
}
|
||||
|
||||
def card(self, files: dict) -> Path:
|
||||
root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
||||
self.addCleanup(shutil.rmtree, root, True)
|
||||
for name, text in files.items():
|
||||
path = root / name
|
||||
if name.endswith("/"):
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
return root
|
||||
|
||||
# P1-1: a handheld's card mounted on a computer.
|
||||
|
||||
def test_p1_1_each_card_by_its_own_files(self):
|
||||
for name, (files, plat, folder) in self.CARDS.items():
|
||||
with self.subTest(card=name):
|
||||
root = self.card(files)
|
||||
self.assertEqual(install.card_system(root), (plat, root / folder, name))
|
||||
|
||||
def test_p1_1_a_card_nothing_names_is_left_alone(self):
|
||||
self.assertIsNone(install.card_system(self.card({"roms/": "", "BIOS/": ""})))
|
||||
# Batocera's share and a KNULLI board seeded with its name read alike.
|
||||
self.assertIsNone(install.card_system(self.card({
|
||||
"system/batocera.conf": "system.hostname=BATOCERA\n"})))
|
||||
|
||||
def test_p1_1_the_card_rules_are_the_device_rules(self):
|
||||
# The folder and the marker come from the rule the device reads, so
|
||||
# they are never written twice.
|
||||
rules = {rule.name: rule for rule in install._card_rules()}
|
||||
self.assertLessEqual({"muOS", "Onion", "spruceOS", "CrossMix-OS", "Allium", "Koriki",
|
||||
"KNULLI", "ROCKNIX"}, set(rules))
|
||||
device = {rule.name: rule for rule in install._EMBEDDED_RULES
|
||||
if rule.values and rule.values[0].startswith(install._CARD_MOUNTS)}
|
||||
for name, rule in device.items():
|
||||
if rule.bios and rule.manifest:
|
||||
with self.subTest(system=name):
|
||||
self.assertTrue(rule.bios.endswith("/" + rules[name].bios))
|
||||
self.assertEqual(install.DEFAULT_DESTS["knulli"], Path("/userdata") / "bios")
|
||||
self.assertEqual(install.DEFAULT_DESTS["rocknix"], Path("/storage") / "roms/bios")
|
||||
|
||||
def test_p1_1_the_first_screen_offers_the_card_folder(self):
|
||||
root = self.card(self.CARDS["muOS"][0])
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[root]), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=[]), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]):
|
||||
state, effects = install.tui_start(_args(), "linux")
|
||||
label = f"muOS SD card ({root.name})"
|
||||
self.assertEqual(state.setups, [("retroarch", root / "MUOS" / "bios", label)])
|
||||
text = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
|
||||
self.assertIn(f"> {label}", text)
|
||||
self.assertIn(f"BIOS folder: {root / 'MUOS' / 'bios'}", text)
|
||||
(root / "MUOS" / "bios").mkdir()
|
||||
self.assertEqual(press(state, "enter"), [("fetch", "retroarch")])
|
||||
self.assertEqual(state.dest, root / "MUOS" / "bios")
|
||||
|
||||
def test_p1_1_a_drive_letter_names_the_card(self):
|
||||
self.assertEqual(install._mount_name(PureWindowsPath("E:\\")), "E:\\")
|
||||
self.assertEqual(install._mount_name(Path("/media/me/MUOS")), "MUOS")
|
||||
|
||||
def test_p1_1_on_the_device_its_card_is_not_offered_twice(self):
|
||||
root = self.card(self.CARDS["muOS"][0])
|
||||
bound = Path("/run/muos/storage/bios")
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[root]), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=[("retroarch",
|
||||
bound)]), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]), \
|
||||
mock.patch.dict(install._DETECTED_OS, {("retroarch", bound): "muOS"}):
|
||||
state, _effects = install.tui_start(_args(), "linux")
|
||||
self.assertEqual([setup[1] for setup in state.setups], [bound])
|
||||
|
||||
def test_p1_1_browsing_to_the_card_root_uses_its_folder(self):
|
||||
root = self.card(self.CARDS["KNULLI"][0])
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[root]):
|
||||
rows, _error = install._browse_rows(REPO_ROOT / "tests")
|
||||
self.assertEqual(rows[2].label, f"{root} (KNULLI card)")
|
||||
state = fresh_state()
|
||||
install._choose(state, "knulli", root)
|
||||
self.assertEqual(state.dest, root / "bios")
|
||||
# Another platform's list keeps the folder it was given.
|
||||
install._choose(state, "batocera", root)
|
||||
self.assertEqual(state.dest, root)
|
||||
|
||||
def test_p1_1_the_command_line_writes_where_muos_reads(self):
|
||||
root = self.card(self.CARDS["muOS"][0])
|
||||
with FakeRepository() as repo:
|
||||
proc = install_run("--platform", "muos", "--dest", str(root), "--yes",
|
||||
env=repo.env())
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertIn(f"muOS card: RetroArch files, in {root / 'MUOS' / 'bios'}", proc.stdout)
|
||||
self.assertEqual(sorted(path.name for path in root.iterdir()), ["MUOS"])
|
||||
self.assertTrue((root / "MUOS" / "bios" / "scph5501.bin").is_file())
|
||||
|
||||
def test_p1_1_a_folder_that_is_no_muos_card_is_refused(self):
|
||||
root = self.card({"roms/": ""})
|
||||
proc = install_run("--platform", "muos", "--dest", str(root), "--yes")
|
||||
self.assertEqual(proc.returncode, 1)
|
||||
self.assertIn(f"{root} is not a muOS card: it holds no MUOS.", proc.stderr)
|
||||
self.assertIn("--dest <card>/MUOS/bios", proc.stderr)
|
||||
self.assertEqual(list(root.iterdir()), [root / "roms"])
|
||||
# Its BIOS folder named outright is taken as given.
|
||||
self.assertEqual(install.card_dest("muos", "retroarch", root / "MUOS" / "bios"),
|
||||
("retroarch", root / "MUOS" / "bios"))
|
||||
# On the device, the folder muOS binds it to.
|
||||
bound = Path("/run/muos/storage/bios")
|
||||
self.assertEqual(install.card_dest("muos", "retroarch", bound), ("retroarch", bound))
|
||||
# A list named by its own name says which list, not which card.
|
||||
self.assertEqual(install.card_dest("retroarch", "retroarch", root),
|
||||
("retroarch", root))
|
||||
|
||||
# P1-2: Space is X on PortMaster and Y on muxterm.
|
||||
|
||||
def test_p1_2_space_on_install_opens_the_confirm_screen(self):
|
||||
state = checked_state()
|
||||
self.assertEqual(install.tui_rows(state)[0].action, "install")
|
||||
self.assertEqual(press(state, " "), [("run", "confirm")])
|
||||
self.assertEqual(state.screen, "confirm")
|
||||
# The same plan comes back: the screen still waits for A.
|
||||
install.tui_receive(state, ("event", state.run_id, dict(CHECK)))
|
||||
self.assertEqual(install.tui_receive(state, ("ended", state.run_id, 0, [])), [])
|
||||
self.assertEqual(state.screen, "confirm")
|
||||
self.assertEqual(install.tui_rows(state)[0].action, "install")
|
||||
self.assertEqual(press(state, " "), [])
|
||||
self.assertEqual(state.screen, "confirm")
|
||||
self.assertEqual(press(state, "enter"), [("run", "install")])
|
||||
|
||||
def test_p1_2_space_never_closes_or_quits(self):
|
||||
for action in ("quit", "close"):
|
||||
with self.subTest(row=action):
|
||||
state = checked_state()
|
||||
if action == "close":
|
||||
state.check = {**CHECK, "missing": 0, "wrong_hash": 0, "bytes_needed": 0}
|
||||
rows = install.tui_rows(state)
|
||||
state.focus["plan"] = next(i for i, r in enumerate(rows) if r.action == action)
|
||||
self.assertEqual(press(state, " "), [])
|
||||
self.assertIsNone(state.exit_code)
|
||||
state = checked_state()
|
||||
state.focus["plan"] = next(i for i, r in enumerate(install.tui_rows(state))
|
||||
if r.action == "options")
|
||||
press(state, " ")
|
||||
self.assertEqual(state.screen, "options")
|
||||
|
||||
def test_p1_2_enter_still_installs_in_three_presses(self):
|
||||
state = checked_state()
|
||||
self.assertEqual(press(state, "enter"), [("run", "install")])
|
||||
self.assertEqual(state.screen, "installing")
|
||||
|
||||
# P2-1: a card too small for the download.
|
||||
|
||||
def test_p2_1_another_folder_comes_first_when_space_is_short(self):
|
||||
state = checked_state({"free": 1024, "writable": True, "cores": []})
|
||||
self.assertEqual([row.label for row in install.tui_rows(state)[:2]],
|
||||
["Choose another folder", "Install anyway"])
|
||||
self.assertEqual(press(state, "enter"), [])
|
||||
self.assertEqual(state.screen, "folder")
|
||||
|
||||
FULL_TAIL = [
|
||||
" Checking existing files...",
|
||||
" 0/50 present (0 verified, 0 wrong hash)",
|
||||
" Downloading 50 files (845.7 KB)...",
|
||||
"Not enough space on the drive holding /media/me/MUOS/MUOS/bios.",
|
||||
" Needed: 1.2 GB. Free: 300.0 MB.",
|
||||
" Free up 920.0 MB, or install to another drive:",
|
||||
" python3 install.py --platform retroarch --dest '<folder on another drive>'",
|
||||
]
|
||||
|
||||
def test_p2_1_a_full_card_is_said_in_words(self):
|
||||
root = self.card(self.CARDS["muOS"][0])
|
||||
state = checked_state()
|
||||
state.dest, state.log = root / "MUOS" / "bios", "/tmp/retrobios-tui-x/install.log"
|
||||
state.stack.append("result")
|
||||
state.code, state.tail = 2, list(self.FULL_TAIL)
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[root]):
|
||||
title, lines = install.tui_outcome(state)
|
||||
self.assertEqual(title, "Not enough space")
|
||||
self.assertEqual(lines[0], "The card is full: 1.2 GB needed, 300.0 MB free.")
|
||||
self.assertIn("Free some space, or run again and choose another folder.", lines)
|
||||
text = "\n".join(lines)
|
||||
for raw in ("Checking existing files", "Downloading", "stopped with code", "Details:"):
|
||||
self.assertNotIn(raw, text)
|
||||
self.assertFalse(install.tui_keeps_log(state))
|
||||
|
||||
def test_p2_1_advice_never_names_the_interface_list(self):
|
||||
if hasattr(os, "geteuid") and os.geteuid() == 0:
|
||||
self.skipTest("root writes to a read-only folder")
|
||||
folder = self.card({})
|
||||
folder.chmod(0o555)
|
||||
self.addCleanup(folder.chmod, 0o755)
|
||||
with FakeRepository() as repo:
|
||||
listed = repo.root / "install" / "retroarch.json"
|
||||
proc = install_run("--platform", "retroarch", "--dest", str(folder),
|
||||
f"--manifest={listed}", "--yes",
|
||||
env={**repo.env(), install.TUI_LIST_ENV: "1"})
|
||||
self.assertIn("sudo", proc.stdout + proc.stderr)
|
||||
self.assertNotIn("--manifest", proc.stdout + proc.stderr)
|
||||
|
||||
# P2-2: the BIOS folder line on a handheld screen.
|
||||
|
||||
def test_p2_2_the_plan_keeps_the_folder_at_handheld_sizes(self):
|
||||
zipped = "/home/me/a/long/path/to/the/Downloads/PSX BIOS.zip"
|
||||
for cols, rows in ((40, 12), (40, 15)):
|
||||
with self.subTest(size=f"{cols}x{rows}"):
|
||||
state = checked_state()
|
||||
state.dest = Path("/home/someone/with/a/long/path/.config/retroarch/system")
|
||||
state.found = [found("scph5500.bin", zipped)]
|
||||
state.cols, state.rows = cols, rows
|
||||
lines = [line.strip() for line in install.tui_view(state, cols, rows)]
|
||||
folder = next(line for line in lines if line.startswith("BIOS folder: "))
|
||||
self.assertTrue(folder.endswith("retroarch/system"), folder)
|
||||
self.assertIn("...", folder)
|
||||
self.assertIn("1 found in PSX BIOS.zip,", " ".join(lines))
|
||||
self.assertNotIn("/home/me/a/long", " ".join(lines))
|
||||
|
||||
# P2-3: where the files came from.
|
||||
|
||||
def test_p2_3_the_result_says_what_came_from_the_card(self):
|
||||
state = checked_state()
|
||||
state.stack.append("result")
|
||||
state.code, state.download = 0, {"files": 5, "bytes": 10}
|
||||
state.local_dests = {"scph5500.bin": ("/media/me/OLD", ""),
|
||||
"scph5501.bin": ("/media/me/OLD", ""),
|
||||
"gba_bios.bin": (str(SYSTEM_DIR), "one folder too deep")}
|
||||
state.finished = [("scph5500.bin", True, ""), ("scph5501.bin", True, ""),
|
||||
("gba_bios.bin", True, ""), ("pcsx/extra.bin", True, ""),
|
||||
("fc/disksys.rom", False, "HTTP 404")]
|
||||
state.summary = {"installed": 4, "up_to_date": 0, "failed": [
|
||||
{"dest": "fc/disksys.rom", "reason": "HTTP 404"}]}
|
||||
_title, lines = install.tui_outcome(state)
|
||||
self.assertEqual(lines[1], "2 files copied from OLD, 1 fixed from copies "
|
||||
"already in the BIOS folder, 1 downloaded, 1 still missing.")
|
||||
# The line mode's own sentence, from the same counts.
|
||||
self.assertEqual(lines[1], install.sources_line([("OLD", 2)], 1, 1, 1))
|
||||
|
||||
def test_p2_3_a_run_that_copied_nothing_adds_no_line(self):
|
||||
state = install.TuiState()
|
||||
state.summary, state.finished = {"installed": 2, "failed": []}, [("a", True, "")]
|
||||
self.assertEqual(install._tui_sources(state, 2, 0), "")
|
||||
|
||||
# P2-4 and P2-5: the systems list.
|
||||
|
||||
def systems_state(self) -> install.TuiState:
|
||||
state = checked_state()
|
||||
state.systems = [*state.systems,
|
||||
{"system": "other", "name": "other", "files": 2, "present": 0,
|
||||
"missing": 2, "wrong_hash": 0, "bytes_needed": 300},
|
||||
{"system": "3do", "name": "3do", "files": 3, "present": 0,
|
||||
"missing": 3, "wrong_hash": 0, "bytes_needed": 600}]
|
||||
state.files = [*state.files,
|
||||
{"event": "file", "dest": "panafz1.bin", "systems": ["3do"],
|
||||
"size": 100, "state": "missing"},
|
||||
{"event": "file", "dest": "mame/hash/a.xml", "systems": ["3do", "mac"],
|
||||
"size": 200, "state": "missing"},
|
||||
{"event": "file", "dest": "mame/hash/b.xml", "systems": ["3do", "mac"],
|
||||
"size": 300, "state": "missing"}]
|
||||
state.ticked |= {"other", "3do"}
|
||||
state.stack.append("systems")
|
||||
return state
|
||||
|
||||
def test_p2_4_the_other_row_is_named_and_explained(self):
|
||||
state = self.systems_state()
|
||||
rows = install.tui_rows(state)
|
||||
other = next(i for i, row in enumerate(rows) if row.value == "other")
|
||||
self.assertEqual(rows[other].label, "[x] Other systems and game engines")
|
||||
state.focus["systems"] = other
|
||||
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
|
||||
self.assertIn("Other systems and game engines: what cores load for systems this "
|
||||
"setup does not list", text)
|
||||
|
||||
def test_p2_5_shared_files_are_counted_apart(self):
|
||||
state = self.systems_state()
|
||||
rows = install.tui_rows(state)
|
||||
three = next(i for i, row in enumerate(rows) if row.value == "3do")
|
||||
self.assertEqual(rows[three].right, "1 file, 100 B + 2 shared")
|
||||
state.focus["systems"] = three
|
||||
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
|
||||
self.assertIn("2 of these files are shared with other systems, counted once", text)
|
||||
# The line mode counts the same files apart.
|
||||
self.assertIn("and 2 files, 500 B shared with other systems", install._system_sized([
|
||||
{"dest": "panafz1.bin", "size": 100, "systems": ["3do"]},
|
||||
{"dest": "a.xml", "size": 200, "systems": ["3do", "mac"]},
|
||||
{"dest": "b.xml", "size": 300, "systems": ["3do", "mac"]}]))
|
||||
narrow = install.tui_view(state, 40, 15)
|
||||
self.assertTrue(any("100 B + 2 shared" in line for line in narrow), narrow)
|
||||
|
||||
# P2-6: a file that cannot be supplied, by name, where it matters.
|
||||
|
||||
def unavailable_state(self) -> install.TuiState:
|
||||
state = checked_state()
|
||||
state.manifest = {**MANIFEST, "system_titles": {"nintendo-gba": "Game Boy Advance"},
|
||||
"omitted_files": [
|
||||
{"dest": "gba_extra.bin", "system": "nintendo-gba",
|
||||
"required": True, "reason": "not_found"},
|
||||
{"dest": "optional.bin", "system": "nintendo-gba",
|
||||
"required": False, "reason": "not_found"}]}
|
||||
state.stack.append("result")
|
||||
state.code, state.download = 0, {"files": 1, "bytes": 1}
|
||||
state.summary = {"installed": 1, "up_to_date": 0, "failed": []}
|
||||
return state
|
||||
|
||||
def test_p2_6_the_missing_file_is_named_with_its_system(self):
|
||||
state = self.unavailable_state()
|
||||
_title, lines = install.tui_outcome(state)
|
||||
self.assertIn("Not available here, and needed to start its emulator: gba_extra.bin "
|
||||
"(Game Boy Advance). Your own copy goes in the BIOS folder.", lines)
|
||||
self.assertNotIn("collection", " ".join(lines))
|
||||
|
||||
def test_p2_6_it_is_left_out_when_it_does_not_concern_the_run(self):
|
||||
state = self.unavailable_state()
|
||||
state.ticked = {"sony-playstation"}
|
||||
self.assertEqual(install._unavailable_lines(state), [])
|
||||
state = self.unavailable_state()
|
||||
state.summary = {"installed": 1, "failed": [], "interrupted": True}
|
||||
self.assertFalse(any("Not available" in line for line in install.tui_outcome(state)[1]))
|
||||
|
||||
# P3: small fixes the audit listed.
|
||||
|
||||
def fetch_failure(self, error: BaseException, env: dict | None = None) -> tuple:
|
||||
inbox = __import__("queue").Queue()
|
||||
with mock.patch.object(install, "_fetch_json", side_effect=error), \
|
||||
mock.patch.dict(os.environ, env or {}):
|
||||
for name in ("HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy"):
|
||||
if name not in (env or {}):
|
||||
os.environ.pop(name, None)
|
||||
install._tui_fetch("retroarch", inbox, io.StringIO())
|
||||
_kind, _plat, title, lines, _code = inbox.get_nowait()
|
||||
return title, lines
|
||||
|
||||
def test_p3_the_clock_names_the_row_to_choose(self):
|
||||
import ssl
|
||||
|
||||
error = urllib_error(ssl.SSLCertVerificationError("certificate is not yet valid"))
|
||||
words = install.network_words(error, today=install.datetime.date(1980, 1, 1))
|
||||
with mock.patch.object(install, "network_words", return_value=words):
|
||||
_title, lines = self.fetch_failure(error)
|
||||
self.assertTrue(lines[0].endswith("then choose Try again."), lines[0])
|
||||
|
||||
def test_p3_a_proxy_in_the_way_is_named_not_shown(self):
|
||||
error = urllib_error(ConnectionRefusedError(111, "Connection refused"))
|
||||
_title, lines = self.fetch_failure(
|
||||
error, {"HTTPS_PROXY": "http://me:secret@10.0.0.1:3128"})
|
||||
self.assertIn("HTTPS_PROXY is set, so the connection goes through a proxy: check "
|
||||
"that it answers.", lines)
|
||||
self.assertNotIn("secret", " ".join(lines))
|
||||
_title, lines = self.fetch_failure(error)
|
||||
self.assertFalse(any("PROXY" in line for line in lines))
|
||||
|
||||
def test_p3_a_list_that_is_no_json_is_said_in_words(self):
|
||||
try:
|
||||
install.json.loads("<html>")
|
||||
except install.json.JSONDecodeError as cause:
|
||||
error = ValueError(f"invalid file list: {cause}")
|
||||
error.__cause__ = cause
|
||||
title, lines = self.fetch_failure(error)
|
||||
self.assertEqual(title, "The file list is not valid")
|
||||
self.assertNotIn("Expecting value", " ".join(lines))
|
||||
self.assertIn("a Wi-Fi sign-in page or a cut download looks like this.", lines[0])
|
||||
|
||||
def test_p3_the_preview_counts_its_files_only(self):
|
||||
state = checked_state()
|
||||
state.stack += ["options", "preview"]
|
||||
state.dry = dict(CHECK)
|
||||
state.dry_files = [{**FILES[0], "dest": f"f{n}.bin"} for n in range(40)]
|
||||
state.rows = 24
|
||||
lines = [line.strip() for line in install.tui_view(state, 80, 24)]
|
||||
self.assertIn("1-18 of 40", lines)
|
||||
|
||||
def test_p3_game_data_reads_without_a_double_negative(self):
|
||||
state = checked_state()
|
||||
label = next(row.label for row in install._option_rows(state)
|
||||
if row.action == "game_data")
|
||||
self.assertEqual(label, "Game data: kept")
|
||||
state.no_game_data = True
|
||||
label = next(row.label for row in install._option_rows(state)
|
||||
if row.action == "game_data")
|
||||
self.assertEqual(label, "Game data: left out, firmware only")
|
||||
|
||||
def test_p3_the_plan_says_which_config_named_the_folder(self):
|
||||
config = HOME / ".config/retroarch/retroarch.cfg"
|
||||
with mock.patch.dict(install._RETROARCH_CONFIGS, {SYSTEM_DIR: (config, config.parent)}):
|
||||
text = "\n".join(install.tui_view(checked_state(), 80, 24))
|
||||
self.assertIn("Named by ~/.config/retroarch/retroarch.cfg", text)
|
||||
|
||||
# The engine's own events (main, after the second audit).
|
||||
|
||||
def test_the_speed_is_the_network_bytes(self):
|
||||
state = checked_state()
|
||||
state.stack.append("installing")
|
||||
state.run, state.running = "install", True
|
||||
install.tui_receive(state, ("event", state.run_id,
|
||||
{"event": "download", "files": 4, "bytes": 4 << 20}))
|
||||
for now, done, network in ((100.0, 1 << 20, 0), (102.0, 3 << 20, 1 << 20)):
|
||||
state.now = now
|
||||
install.tui_receive(state, ("event", state.run_id, {
|
||||
"event": "progress", "files_done": 1, "files_total": 4, "bytes_done": done,
|
||||
"bytes_total": 4 << 20}))
|
||||
install.tui_receive(state, ("event", state.run_id, {
|
||||
"event": "transfer", "bytes_local": done - network, "bytes_network": network}))
|
||||
text = "\n".join(install.tui_view(state, 80, 24))
|
||||
# 1 MB from the network in 2 s, whatever came from the old card.
|
||||
self.assertIn("512.0 KB/s, 0:02 left", text)
|
||||
|
||||
def test_the_plan_says_what_the_scan_left_out(self):
|
||||
state = checked_state()
|
||||
state.from_folders = [Path("E:\\")]
|
||||
install._start_check(state)
|
||||
events = [{"event": "scanned", "folder": "E:\\", "files": 200000, "truncated": True,
|
||||
"other_drives": ["E:\\D"]},
|
||||
{"event": "unreadable", "platform": "retroarch", "folder": "E:\\",
|
||||
"path": "E:\\bios\\scph5501.bin", "member": "", "reason": "Permission denied"},
|
||||
{"event": "unreadable", "platform": "retroarch", "folder": "E:\\",
|
||||
"path": "E:\\backup.zip", "member": "gba_bios.bin",
|
||||
"reason": "it is encrypted, a password is needed"}]
|
||||
for event in [*FILES, *SYSTEMS, *events, CHECK]:
|
||||
install.tui_receive(state, ("event", state.run_id, event))
|
||||
with mock.patch.object(install, "_tui_folder_facts", return_value={
|
||||
"free": 50 * 1024 ** 3, "writable": True, "cores": []}):
|
||||
install.tui_receive(state, ("ended", state.run_id, 1, []))
|
||||
text = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
|
||||
self.assertIn("Read the first 200,000 files of E:\\.", text)
|
||||
self.assertIn("E:\\D is another drive: add it to copy from it too.", text)
|
||||
self.assertIn("2 files on your old card could not be read.", text)
|
||||
rows = install.tui_rows(state)
|
||||
state.focus["plan"] = next(i for i, row in enumerate(rows) if row.action == "unreadable")
|
||||
press(state, "enter")
|
||||
self.assertEqual(state.screen, "unreadable")
|
||||
self.assertEqual(install._pager_items(state), [
|
||||
"E:\\bios\\scph5501.bin: Permission denied",
|
||||
"E:\\backup.zip, gba_bios.bin: it is encrypted, a password is needed"])
|
||||
|
||||
def test_the_interface_does_not_index_the_folders_itself(self):
|
||||
missing = REPO_ROOT / "tmp" / "no-such-old-card"
|
||||
err = io.StringIO()
|
||||
with mock.patch.object(install, "FolderIndex", side_effect=AssertionError("indexed")), \
|
||||
mock.patch.object(install, "detect_os", return_value="linux"), \
|
||||
mock.patch.object(install, "tui_start", side_effect=AssertionError("opened")), \
|
||||
mock.patch.object(sys, "stderr", err):
|
||||
self.assertEqual(install.run_tui(_args(**{"from": [missing]})), 1)
|
||||
self.assertIn(f"Error: {missing} does not exist: --from names the folder", err.getvalue())
|
||||
|
||||
# Work folders: only a kill no handler sees leaves one, and the next
|
||||
# run takes it away.
|
||||
|
||||
def test_work_folders_left_behind_are_pruned(self):
|
||||
import subprocess
|
||||
|
||||
root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
||||
self.addCleanup(shutil.rmtree, root, True)
|
||||
gone = subprocess.Popen([sys.executable, "-c", "pass"])
|
||||
gone.wait()
|
||||
now = time.time()
|
||||
week = install.TUI_KEEP_SECONDS
|
||||
|
||||
def folder(name: str, pid: int | None, kept: bool, age: float) -> Path:
|
||||
path = root / f"{install.TUI_WORK_PREFIX}{name}"
|
||||
path.mkdir()
|
||||
if pid is not None:
|
||||
(path / install.TUI_OWNER).write_text(str(pid))
|
||||
if kept:
|
||||
(path / install.TUI_KEPT).write_text("")
|
||||
os.utime(path, (now - age, now - age))
|
||||
return path
|
||||
|
||||
killed = folder("killed", gone.pid, False, 60)
|
||||
running = folder("running", os.getpid(), False, 2 * week)
|
||||
failed = folder("failed", gone.pid, True, 60)
|
||||
failed_old = folder("failed-old", gone.pid, True, 2 * week)
|
||||
unknown = folder("unknown", None, False, 60)
|
||||
unknown_old = folder("unknown-old", None, False, 2 * week)
|
||||
other = root / "something-else"
|
||||
other.mkdir()
|
||||
removed = install._prune_work_folders(root, now)
|
||||
self.assertEqual(sorted(removed), sorted([killed, failed_old, unknown_old]))
|
||||
self.assertEqual(sorted(path.name for path in root.iterdir()),
|
||||
sorted(path.name for path in (running, failed, unknown, other)))
|
||||
|
||||
|
||||
class Crash(unittest.TestCase):
|
||||
"""An unexpected error ends on one sentence and a log, never a traceback."""
|
||||
|
||||
@@ -2030,11 +2545,14 @@ class EndToEnd(unittest.TestCase):
|
||||
term.send("\r")
|
||||
screen = term.wait("found in")
|
||||
# The folder wraps at a slash: compared without the spaces.
|
||||
self.assertIn("".join(f"3 found in {old}, 2 to download (8 B)".split()),
|
||||
"".join(screen.split()))
|
||||
self.assertIn("3 found in oldcard, 2 to download (8 B)", screen)
|
||||
# Checked again, the plan opens on Install.
|
||||
term.send("\r")
|
||||
term.wait("files installed.")
|
||||
screen = term.wait("files installed.")
|
||||
# Where each came from, as the line mode says it.
|
||||
self.assertIn("5 files installed.", screen)
|
||||
self.assertIn("3 files copied from", screen)
|
||||
self.assertIn("2 downloaded.", screen)
|
||||
term.send("\r")
|
||||
self.assertEqual(term.close(), 0)
|
||||
self.assertEqual(len(repo.installed()), 5)
|
||||
|
||||
@@ -96,3 +96,29 @@ class SemanticManifestValidation(unittest.TestCase):
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
try:
|
||||
import jsonschema
|
||||
except ImportError:
|
||||
jsonschema = None
|
||||
|
||||
|
||||
@unittest.skipIf(jsonschema is None, "jsonschema unavailable")
|
||||
class DataRootIsKeyedBySystem(unittest.TestCase):
|
||||
"""AetherSX2 gave Android/data/xyz.aethersx2.android only in its notes:
|
||||
the site could not print the folder, nor which OS tabs apply."""
|
||||
|
||||
def _errors(self, value):
|
||||
schema = json.loads((REPO_ROOT / "schemas/emulator.schema.json").read_text(encoding="utf-8"))
|
||||
validator = jsonschema.Draft202012Validator(schema["properties"]["data_root"])
|
||||
return list(validator.iter_errors(value))
|
||||
|
||||
def test_a_system_with_its_path_and_reference(self):
|
||||
self.assertEqual(self._errors({"android": {
|
||||
"path": "Android/data/xyz.aethersx2.android/files", "source_ref": "a.kt:1"}}), [])
|
||||
|
||||
def test_an_unknown_system_or_a_path_without_reference(self):
|
||||
self.assertTrue(self._errors({"amiga": {"path": "x", "source_ref": "y"}}))
|
||||
self.assertTrue(self._errors({"windows": {"path": "%APPDATA%/X"}}))
|
||||
self.assertTrue(self._errors({}))
|
||||
@@ -125,15 +125,21 @@ class FirstInstallEnds(unittest.TestCase):
|
||||
"""C7, B15: the page said `Done.` may list failures, and sent a wrong
|
||||
folder to a section with no answer for it."""
|
||||
|
||||
def test_the_wrong_folder_answer_is_dest(self):
|
||||
section = _section(_wiki("first-install.md"), "2. Check the folder it found")
|
||||
self.assertIn("--dest", section)
|
||||
def test_the_wrong_folder_answer_is_on_the_screen(self):
|
||||
# The one-liner opens the full screen: its first screen has the
|
||||
# answer, a row the page names as the interface spells it.
|
||||
import install
|
||||
|
||||
section = _section(_wiki("first-install.md"), "2. Check the setup it found")
|
||||
state = install.TuiState()
|
||||
row = install.tui_rows(state)[-1].label
|
||||
self.assertIn(f"**{row}**", section)
|
||||
self.assertIn("<your setup>", section)
|
||||
self.assertNotIn("Batocera", section)
|
||||
|
||||
def test_the_diagram_follows_the_steps(self):
|
||||
text = _wiki("first-install.md")
|
||||
self.assertLess(text.index("## 5. Read the last line"), text.index("<!-- image: install-flow -->"))
|
||||
self.assertLess(text.index("## 5. Read the result"), text.index("<!-- image: install-flow -->"))
|
||||
|
||||
|
||||
class OneNamePerPage(unittest.TestCase):
|
||||
|
||||
@@ -251,11 +251,21 @@ class SmallCopyFixes(unittest.TestCase):
|
||||
self.assertIn("`retroarch/cores/system/` on the SD card", guides)
|
||||
self.assertIn("--dest E:\\retroarch\\cores\\system", section)
|
||||
|
||||
def test_the_end_line_is_the_printed_one(self):
|
||||
section = _section(_wiki("first-install.md"), "5. Read the last line")
|
||||
self.assertIn("`N files installed, M already up to date.`", section)
|
||||
source = (REPO_ROOT / "install.py").read_text(encoding="utf-8")
|
||||
self.assertIn("installed, \"\n f\"{_plural(up_to_date, 'file')} already up to date.", source)
|
||||
def test_the_end_is_the_shown_one(self):
|
||||
# The one-liner opens the full screen: the page names the titles
|
||||
# its result screen shows, as tui_outcome writes them.
|
||||
import install
|
||||
|
||||
section = _section(_wiki("first-install.md"), "5. Read the result")
|
||||
state = install.TuiState()
|
||||
state.stack, state.code = ["result"], 0
|
||||
for summary in ({"installed": 3, "up_to_date": 0, "failed": []},
|
||||
{"installed": 2, "failed": [{"dest": "a.bin", "reason": "HTTP 404"}]},
|
||||
{"installed": 0, "failed": [{"dest": "a.bin", "reason": "HTTP 404"}]}):
|
||||
state.summary = summary
|
||||
title, _lines = install.tui_outcome(state)
|
||||
with self.subTest(title=title):
|
||||
self.assertIn(f"**{title}**", section)
|
||||
|
||||
|
||||
class CommunityAnswers(unittest.TestCase):
|
||||
|
||||
+30
-34
@@ -67,44 +67,44 @@ curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh |
|
||||
On Batocera, Recalbox and similar setups, see
|
||||
[Get your files](../which-pack.md).
|
||||
|
||||
## 2. Check the folder it found
|
||||
## 2. Check the setup it found
|
||||
|
||||
It shows your setup and its BIOS folder:
|
||||
It opens a menu:
|
||||
|
||||
```
|
||||
Found <your setup> at <its folder>
|
||||
RetroBIOS: Where do you play?
|
||||
Found on this machine:
|
||||
> <your setup>
|
||||
Another setup or folder
|
||||
BIOS folder: <its folder>
|
||||
```
|
||||
|
||||
- **Right folder?** Go on.
|
||||
- **Wrong folder?** Press Ctrl+C. Run it again with your folder:
|
||||
- **Right folder?** Press Enter.
|
||||
- **Wrong folder?** Choose **Another setup or folder**, then browse to yours.
|
||||
- **A handheld's SD card in a reader?** It is listed, as **muOS SD card**.
|
||||
|
||||
<!-- os-tabs windows: --dest D:\path\to\bios -->
|
||||
```sh
|
||||
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --dest ~/path/to/bios
|
||||
```
|
||||
Arrows move, Enter chooses, Esc goes back. A controller's A and B do the same.
|
||||
|
||||
## 3. Choose what to install
|
||||
## 3. Install
|
||||
|
||||
```
|
||||
Enter) install everything
|
||||
i) only what the installed cores read
|
||||
c) choose systems, cores and regions
|
||||
q) quit
|
||||
```
|
||||
The next screen counts the missing files and the free space.
|
||||
Press Enter on **Install**. **Choose systems** keeps the consoles you
|
||||
play.
|
||||
|
||||
**Enter** installs everything. **c** lets you pick consoles.
|
||||
**Old card?** In a card reader, choose **Copy the BIOS files from your old
|
||||
card or a folder**, then the card. It stays unchanged.
|
||||
|
||||
## 4. Wait
|
||||
|
||||
Ctrl+C stops it. Run the command again to carry on.
|
||||
B or Ctrl+C stops it. Run it again to carry on.
|
||||
|
||||
## 5. Read the last line
|
||||
## 5. Read the result
|
||||
|
||||
- **`Done.`** `N files installed, M already up to date.` All files are in place.
|
||||
- **`Finished with errors.`** Some files failed. Run the same command again.
|
||||
It retries only those.
|
||||
- **`Nothing was installed.`** The lines above it say why. See
|
||||
[If it stops](#if-it-stops).
|
||||
- **Done**: all files are in place.
|
||||
- **Finished with errors**: run the command again. It retries only those.
|
||||
- **Nothing installed**: the screen says why. See [If it stops](#if-it-stops).
|
||||
|
||||
For questions line by line (screen readers), add `--plain`.
|
||||
|
||||
## What the command does
|
||||
|
||||
@@ -127,18 +127,14 @@ All good:
|
||||
|
||||
## Undo
|
||||
|
||||
Every file it replaces goes to a `retrobios-backup` folder next to your BIOS
|
||||
folder. The last lines say how to put them back.
|
||||
Files it replaces go to a `retrobios-backup` folder beside your BIOS folder.
|
||||
The result says how to restore them.
|
||||
|
||||
## If it stops
|
||||
|
||||
### No supported platform detected.
|
||||
### Nothing was found here.
|
||||
|
||||
At a terminal, a menu follows. Pick your setup by its number, then type its
|
||||
BIOS folder, or press Enter for the usual one.
|
||||
|
||||
With `--no-input`, or without a terminal, it stops and prints the command to
|
||||
use.
|
||||
Choose **Another setup or folder**, then your setup and its folder.
|
||||
|
||||
### Error: Python 3.8 or newer is required.
|
||||
|
||||
@@ -157,9 +153,9 @@ The download was cut short. Nothing was changed. Run the command again.
|
||||
|
||||
You cannot write there. Do what the message says.
|
||||
|
||||
### Not enough space on the drive holding ...
|
||||
### The card is full
|
||||
|
||||
Nothing was written. Free some space, or pick another drive with `--dest`.
|
||||
Nothing was written. Free some space, or choose another folder.
|
||||
|
||||
### The game still does not start
|
||||
|
||||
|
||||
+80
-29
@@ -67,7 +67,10 @@ curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh \
|
||||
```
|
||||
|
||||
`--dest` is the BIOS folder itself: on a card, `retroarch/cores/system` for
|
||||
RetroArch on Switch, `RetroArch/system` on Android.
|
||||
RetroArch on Switch, `RetroArch/system` on Android. The root of a handheld's
|
||||
card works too when the installer knows that card (muOS, Onion, spruceOS,
|
||||
KNULLI, ROCKNIX and the MinUI family, see
|
||||
[Platform detection](#platform-detection)): the files go where its system reads.
|
||||
|
||||
PowerShell needs another form. `iex` takes the script text as its own argument,
|
||||
so anything appended to `irm ... | iex` is read as an argument to `iex` and
|
||||
@@ -454,6 +457,9 @@ python install.py --from ~/Downloads --from /media/me/backup
|
||||
python install.py --check --from /media/me/STOCK
|
||||
```
|
||||
|
||||
The first line names the root of the muOS card, the one holding `MUOS`: the
|
||||
files go to `MUOS/bios` on it, where muOS reads them, and the run says so.
|
||||
|
||||
Every file is recognised by its contents, never by its name:
|
||||
`PSX BIOS (USA).bin` in a subfolder lands as `scph5501.bin` when it holds those
|
||||
bytes, and a file of the right name with other bytes (an empty file left by a
|
||||
@@ -750,6 +756,26 @@ family writes the release name as the first line of `.system/version.txt` at
|
||||
the card root (`MinUI-`, `NextUI-`, `MyMinUI-`), and that line names the file
|
||||
list.
|
||||
|
||||
A card that holds a system's own files is read the same way, without
|
||||
`--platform` or with the one that names its list, by the rules the table above
|
||||
gives for the device, below the card's root instead of its mount point:
|
||||
|
||||
| On the card | System | Files go to |
|
||||
|-------------|--------|-------------|
|
||||
| `MUOS/` | muOS | `MUOS/bios`, RetroArch's list |
|
||||
| `.tmp_update/onionVersion/version.txt` | Onion | `BIOS`, RetroArch's list |
|
||||
| `spruce/` | spruceOS | `BIOS`, RetroArch's list |
|
||||
| `System/usr/trimui/crossmix-version.txt` | CrossMix-OS | `BIOS`, RetroArch's list |
|
||||
| `.allium/` | Allium | `BIOS`, RetroArch's list |
|
||||
| `Koriki/version.txt` | Koriki | `BIOS`, RetroArch's list |
|
||||
| `system/batocera.conf` with `system.hostname=KNULLI`, as KNULLI's first boot writes its share | KNULLI | `bios`, KNULLI's list |
|
||||
| `.config/system/configs/system.cfg` with `root.password=rocknix`, as ROCKNIX's first boot writes its storage | ROCKNIX | `roms/bios`, ROCKNIX's list |
|
||||
|
||||
The full screen lists such a card on its first screen ("muOS SD card (E:\)").
|
||||
Named by its system (`--platform muos`, `--platform onion`), a `--dest` that
|
||||
is neither such a card nor its BIOS folder is refused, with the folder that
|
||||
system reads.
|
||||
|
||||
Desktop installs are read from their own configuration, and several can be
|
||||
found on one machine.
|
||||
|
||||
@@ -953,22 +979,25 @@ python install.py --platform retroarch --dest /storage/emulated/0/RetroArch/syst
|
||||
|
||||
RetroBIOS also runs on the handheld itself, in the
|
||||
[full-screen interface](#the-full-screen-interface), with the controller.
|
||||
Take the file for your system from the
|
||||
[latest release](https://github.com/Abdess/retrobios/releases/latest), copy
|
||||
it to the SD card, then start RetroBIOS from the menu.
|
||||
Copy the file for your system to the SD card, then start RetroBIOS from the
|
||||
menu.
|
||||
|
||||
The files below ship with the next release. Until then, build them from a
|
||||
clone of the repository with `python scripts/build_port.py`, which writes all
|
||||
four into `dist/`, or use the computer route at the end of this section.
|
||||
|
||||
| System | File | Copy it to | Start it from |
|
||||
|--------|------|------------|---------------|
|
||||
| Any system with [PortMaster](https://portmaster.games/): ArkOS, dArkOS, ROCKNIX, AmberELEC, JELOS, muOS, Knulli | `RetroBIOS.zip` | PortMaster's `autoinstall` folder: `/roms/ports/PortMaster/autoinstall/` on ROCKNIX and AmberELEC, `/roms/tools/PortMaster/autoinstall/` on ArkOS, `/mmc/MUOS/PortMaster/autoinstall/` on muOS, `/userdata/system/.local/share/PortMaster/autoinstall/` on Knulli | Ports, after opening PortMaster once |
|
||||
| Batocera, Knulli | `RetroBIOS-batocera.zip`, extracted | `roms/ports` on the SHARE partition | Ports, after updating the game list |
|
||||
| muOS | `RetroBIOS.muxapp` | `ARCHIVE` at the root of the card, then install it from the Archive Manager | Applications |
|
||||
| Any other system with [PortMaster](https://portmaster.games/): ArkOS, dArkOS, ROCKNIX, AmberELEC, JELOS, Knulli | `RetroBIOS.zip` | PortMaster's `autoinstall` folder: `/roms/ports/PortMaster/autoinstall/` on ROCKNIX and AmberELEC, `/roms/tools/PortMaster/autoinstall/` on ArkOS, `/userdata/system/.local/share/PortMaster/autoinstall/` on Knulli | Ports, after opening PortMaster once |
|
||||
| Batocera, Knulli | `RetroBIOS-batocera.zip`, extracted | `roms/ports` on the SHARE partition | Ports, after updating the game list |
|
||||
| ArkOS, dArkOS | `RetroBIOS-tools.zip`, extracted | `tools` on the EASYROMS partition | Options, then Tools |
|
||||
| ROCKNIX, JELOS | `RetroBIOS-tools.zip`, extracted | `/storage/.config/modules`, over the network share or SSH | Tools |
|
||||
| AmberELEC | `RetroBIOS-tools.zip`, extracted | `/storage/.config/distribution/modules` | Tools |
|
||||
|
||||
A release cut before these files existed carries none of them. From a clone
|
||||
of the repository, `python scripts/build_port.py` writes all four into
|
||||
`dist/`.
|
||||
`RetroBIOS.zip` also installs on muOS, from `MUOS/PortMaster/autoinstall/`
|
||||
on the first card (`/mnt/mmc` on the device). It then shows the interface in
|
||||
muxterm, as the muOS app does.
|
||||
|
||||
| Button | Action |
|
||||
|--------|--------|
|
||||
@@ -978,19 +1007,34 @@ of the repository, `python scripts/build_port.py` writes all four into
|
||||
| X | Tick or untick a system |
|
||||
| Y | Options |
|
||||
| L1, R1 | Page up, page down |
|
||||
| Start + Select | Quit at once (PortMaster port, Tools) |
|
||||
| Hold Select, then Start | Quit at once (PortMaster port, muOS, Tools) |
|
||||
|
||||
On muOS the interface runs in muxterm, which already reads Start, B, Y (tick)
|
||||
and the D-pad. A, X, L1 and R1 come from PortMaster's gptokeyb2 when
|
||||
PortMaster is installed, and Menu quits.
|
||||
Start pressed first also chooses the line in focus: hold Select first. A
|
||||
running install is then stopped the way B stops it, and the last screen says
|
||||
what it wrote. On muOS without PortMaster, muxterm reads the pad itself: Y
|
||||
ticks a system, Select shows its keyboard, and Menu quits.
|
||||
|
||||
**Without Wi-Fi.** On a computer, download the BIOS pack whose name contains
|
||||
your system (every part, for a pack in parts) and copy it next to RetroBIOS:
|
||||
into `ports/retrobios` for the PortMaster port and on Batocera, to the root
|
||||
of the card on muOS, into the `retrobios` folder of the Tools entry. With no
|
||||
Each launcher tells the interface which buttons reach it, in
|
||||
`RETROBIOS_PAD`, so the hints can name them:
|
||||
|
||||
| Value | Input | Set by |
|
||||
|-------|-------|--------|
|
||||
| `gptokeyb` | gptokeyb or gptokeyb2 with `retrobios.gptk`, the table above | the PortMaster port, the Tools entry, the muOS app with PortMaster |
|
||||
| `muxterm` | muxterm's own keys: D-pad, Start as Enter, B as Backspace, Y as Space | the muOS app without PortMaster |
|
||||
| `evmapy` | `RetroBIOS.sh.keys`, the same buttons as the table | the Batocera and Knulli port |
|
||||
| `miyoo` | buttons that are already keys, A as Space and B as Left Ctrl | no launcher yet |
|
||||
|
||||
A launcher with no mapper at all (a keyboard) leaves it unset.
|
||||
|
||||
**Without Wi-Fi.** On a computer, download the BIOS pack that serves your
|
||||
system (every part, for a pack in parts): RetroArch's on muOS, ArkOS,
|
||||
dArkOS and JELOS, Batocera's on Batocera. Copy it next to RetroBIOS: into
|
||||
`ports/retrobios` for the PortMaster port and on Batocera, to the root of
|
||||
either card on muOS, into the `retrobios` folder of the Tools entry. With no
|
||||
connection, RetroBIOS installs from it, as
|
||||
[`--from-pack`](#without-a-network-offline-install) does. With no pack there,
|
||||
it names the pack and the folder.
|
||||
it names the pack and the folder. The releases carry no pack for ROCKNIX,
|
||||
Knulli or AmberELEC: there RetroBIOS needs Wi-Fi, or the computer route.
|
||||
|
||||
On a two-card device, RetroBIOS without Wi-Fi first asks to **copy the BIOS
|
||||
files from your other card**: muOS's `MUOS/bios` on the card the system does
|
||||
@@ -1003,28 +1047,24 @@ still comes from the pack, so the pack is needed too.
|
||||
first use and the Tools entry then reuses. That runtime uses the system's
|
||||
OpenSSL 1.1, so on an image with OpenSSL 3 only it may not download (not
|
||||
tried on a device). The BIOS folder must be one the installer detects, see
|
||||
[Platform detection](#platform-detection).
|
||||
[Platform detection](#platform-detection). On a read-only or full card,
|
||||
RetroBIOS keeps its log in `retrobios` under `XDG_RUNTIME_DIR` or `/tmp`, and the interface names the
|
||||
folder it cannot write.
|
||||
|
||||
**Where no terminal shows.** The PortMaster port draws on the console, or in
|
||||
foot or xterm under a desktop session. Where neither can, it opens
|
||||
PortMaster's own screens instead: the same check, update and choice of
|
||||
systems, one list at a time. On Batocera and Knulli the visible terminal is
|
||||
not verified on a device yet: RetroBIOS tries xterm or foot, then console 4,
|
||||
and `retrobios/log.txt` names the route it took.
|
||||
and stops when console 4 does not come to the front. `retrobios/log.txt`
|
||||
names each route it tried, and `log.prev.txt` keeps the run before.
|
||||
|
||||
**From a computer.** Onion, MinUI, NextUI, Allium, GarlicOS and the Switch
|
||||
have no Python 3 to run the installer. Put the card in a computer and run the
|
||||
[one-line command](#one-line) there with
|
||||
have no Python 3 to run the installer. Put the card in a computer and run
|
||||
the [one-line command](#one-line) there with
|
||||
`--platform <platform> --dest <BIOS folder on the card>`. This works for
|
||||
every system above too.
|
||||
|
||||
**Which installer runs.** Each file carries `install.py`, the one the
|
||||
one-line command of the same commit downloads: `scripts/build_port.py`
|
||||
refuses any other (it compares it with the SHA-256 `install.sh` pins). The
|
||||
file lists come from `main`. The launchers in `ports/` start it with `--tui`,
|
||||
and read it through [Machine-readable events](#machine-readable-events) for
|
||||
the rest.
|
||||
|
||||
## Without a network (offline install) { #without-a-network-offline-install }
|
||||
|
||||
<span id="without-a-network"></span>
|
||||
@@ -1150,6 +1190,13 @@ The build scripts need 3.9 and are not bound by this.
|
||||
Every change to `install.py` recomputes the SHA-256 pinned in `install.sh` and
|
||||
`install.ps1` in the same commit. The tests compare the pins with the file.
|
||||
|
||||
The [handheld](#on-a-handheld-the-retrobios-port) archives each carry
|
||||
`install.py`, the one the one-line command of the same commit downloads:
|
||||
`scripts/build_port.py` refuses any other (it compares it with the SHA-256
|
||||
`install.sh` pins). The file lists come from `main`. The launchers in
|
||||
`ports/` start it with `--tui`, and read it through
|
||||
[Machine-readable events](#machine-readable-events) for the rest.
|
||||
|
||||
Narrowing by system, core, region or target replays the pack builder's rules
|
||||
from fields the manifest carries, and a parity test compares an installer
|
||||
selection with the pack built the same way. Given a manifest without those
|
||||
@@ -1165,6 +1212,10 @@ fields, the installer refuses to narrow rather than install everything.
|
||||
| `130` | stopped with Ctrl+C, or with `q`, Ctrl+C or Ctrl+D at a question. Run the same command again to carry on |
|
||||
| `141` | the output was piped to a reader that closed early (`--list-platforms \| head`) |
|
||||
|
||||
The full screen ended by SIGTERM (Start+Select on a handheld) before any
|
||||
install exits `143`. During an install it stops the install first, and exits
|
||||
`130`.
|
||||
|
||||
`install.ps1` passes the installer's code on unchanged: run as a file it exits
|
||||
with it, run through `irm \| iex` it sets `$LASTEXITCODE` (an `exit` there
|
||||
would close the PowerShell window). It declares no parameter of its own, so
|
||||
|
||||
@@ -591,6 +591,7 @@ does not look.
|
||||
| `known_hash_adler32` | expected Adler-32 hash (Dolphin's DSP ROMs). Pair with `adler32_byteswap` when the code hashes 16-bit byte-swapped data |
|
||||
| `adler32_pairs_with`, `adler32_pairs` | the file hashed together with this one and the combinations the code accepts, this file's value first, in table order: Dolphin warns on a DSP ROM pair outside its `known_roms` even when each file passes alone |
|
||||
| `pin_frozen` | why `source_commit` is held on purpose rather than followed. The profile is then judged against its own revision, like one pinned to a superseded tag |
|
||||
| `data_root` | per operating system (`android`, `ios`, `windows`, `macos`, `linux`), the folder the app reads its user data from, under which `standalone_path` destinations sit: `path` as the user finds it (`Android/data/<package>/files`, `%APPDATA%/...`), with the `source_ref` that builds it. Only the systems the app runs on are keyed |
|
||||
| `upstream_frozen` | why `upstream_commit` is held: the port vendors that upstream tree, synced to that release, or merged that commit. The upstream view is read at its own pin and a bump leaves it. Without it, a port that is a GitHub fork of its upstream follows the merge base of the two |
|
||||
| `prose_written_at` | the pin a reader confirmed the prose citations against. When the profile's history shows a text living under several pins, profile_sync takes this one as the revision the text describes, so a bump is no longer refused for a citation already reread. It never overrides a history with a single candidate |
|
||||
| `version_source` | for a project published outside any forge, `{url, pattern}`: the page that announces releases and a regular expression whose first group is the latest version. `profile_sync --check-version` compares it with `core_version`, and `check_freshness --profiles` calls the profile stale when they differ. `no_genuine_copy` names an announced version found only rebuilt or re-signed: the profile stays on the newest unmodified build and is not stale until the page announces a later one |
|
||||
|
||||
+10
-7
@@ -274,13 +274,16 @@ pins).
|
||||
|
||||
Each launcher then runs from its extracted archive on a simulated device: a
|
||||
pseudo-terminal stands for the console, and fakes stand for `install.py`,
|
||||
gptokeyb, setfont, chvt, curl, foot, xterm, muxterm, PortMaster's
|
||||
`control.txt` and muOS's `retro.sh`. The checks: gptokeyb starts in kill mode
|
||||
on the installer's path and is stopped, the console's settings and font are
|
||||
put back, an offline run picks the pack of its system from the card or says
|
||||
where to put one, a refused console falls back to PortMaster's screens, and
|
||||
each firmware's terminal is the one used. What reaches the screen and the
|
||||
pad on a real device is not exercised.
|
||||
gptokeyb and gptokeyb2 (each killing the way its source does), setfont,
|
||||
chvt, curl, sudo, foot, xterm, muxterm, PortMaster's `control.txt` and
|
||||
muOS's `retro.sh`. The checks: gptokeyb's kill reaches only the Start +
|
||||
Select sentinel and the interface then stops with its result written, the
|
||||
console's settings and font are put back, an offline run says so at once,
|
||||
picks the pack of its system from the card or says where to put one, and
|
||||
each firmware's terminal is the one used. The fakes also fail: a mapper that
|
||||
dies, a chvt the compositor refuses, a terminal app that cannot open, a
|
||||
read-only card. None of these may leave a console waiting for a button.
|
||||
What reaches the screen and the pad on a real device is not exercised.
|
||||
|
||||
```bash
|
||||
python -m unittest tests.test_handheld_launchers -v
|
||||
|
||||
@@ -38,7 +38,8 @@ Run it without `--check`. It adds missing files and replaces wrong ones.
|
||||
- **Right folder?** [Get your files](../which-pack.md) names it.
|
||||
- **Exact name?** `scph5501.bin` and `SCPH-5501.BIN` differ. On Linux,
|
||||
capitals count too.
|
||||
- **A `bios` folder inside your BIOS folder?** Move its files up one level.
|
||||
- **A `bios` folder inside your BIOS folder?** Run the installer: it copies
|
||||
those files up one level.
|
||||
- **Unzipped by hand on Linux?** The command adds names that differ by
|
||||
capitals.
|
||||
- **A ZIP in parts, like `Pack.part1of2.zip`?** Unzip every part into one
|
||||
|
||||
Reference in new issue
Block a user