mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
feat: detect handheld systems by their own files
AmberELEC, EmuELEC, JELOS, KOLOS, UnofficialOS, ArchR, PAN4ELEC, dArkOS, RetroOZ, spruceOS, CrossMix-OS, Onion, Allium, Koriki, MiyooCFW and plumOS are found by what their builds write, named as they name themselves, and installed to the system_directory of their RetroArch config, which follows ArkOS and dArkOS to a second card. TrimUI stock, MinUI and a plumOS build without a known folder are named and not installed to. Two unrelated systems matching stop a run without a terminal.
This commit is contained in:
1 parent
ff2137de9d
commit
e27a583975
6 files changed
+897
-142
No files matched your search
+1
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
$defaultInstallSha256 = "fe8de4eda0919c268d4d2c0d571c7d0dc1cf675d7604263766f5f99a35aa3724"
|
||||
$defaultInstallSha256 = "9fbee0f382ca68880697ed573b79ecc1ffca793a406a3f258dad16e40d2845d6"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+359
-67
@@ -41,7 +41,7 @@ import xml.etree.ElementTree as ET
|
||||
import zipfile
|
||||
from collections.abc import Callable, Iterator
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import BinaryIO, TextIO
|
||||
from typing import BinaryIO, NamedTuple, TextIO
|
||||
|
||||
# Manifests are read from the same ref the bootstrap verified this installer
|
||||
# against, so a file list and the code reading it always come from one commit.
|
||||
@@ -481,7 +481,7 @@ def detect_os() -> str:
|
||||
def _parse_os_release() -> dict[str, str]:
|
||||
"""Parse /etc/os-release KEY=value format."""
|
||||
result: dict[str, str] = {}
|
||||
path = Path("/etc/os-release")
|
||||
path = _on_device("/etc/os-release")
|
||||
if not path.exists():
|
||||
return result
|
||||
try:
|
||||
@@ -535,7 +535,7 @@ def _enter_wsl() -> None:
|
||||
os.environ[variable] = str(_host_path(value))
|
||||
|
||||
|
||||
def _expand_retroarch_path(value: str, app_dir: Path) -> Path:
|
||||
def _expand_retroarch_path(value: str, app_dir: Path, home: Path | None = None) -> Path:
|
||||
"""Expand the notations RetroArch writes into its config values.
|
||||
|
||||
fill_pathname_expand_special (libretro-common/file/file_path.c) maps a
|
||||
@@ -543,14 +543,14 @@ def _expand_retroarch_path(value: str, app_dir: Path) -> Path:
|
||||
directory, dropping the two leading characters in both cases.
|
||||
"""
|
||||
if value[:1] == "~":
|
||||
return Path(str(Path.home())) / value[2:]
|
||||
return (home or Path(str(Path.home()))) / value[2:]
|
||||
if value[:1] == ":":
|
||||
return app_dir / value[2:]
|
||||
return _host_path(os.path.expandvars(os.path.expanduser(value)))
|
||||
|
||||
|
||||
def _retroarch_cfg_dir(
|
||||
cfg_path: Path, key: str, app_dir: Path, default: str
|
||||
cfg_path: Path, key: str, app_dir: Path, default: str, home: Path | None = None
|
||||
) -> Path | None:
|
||||
"""A directory setting of retroarch.cfg, as RetroArch expands it.
|
||||
|
||||
@@ -569,7 +569,7 @@ def _retroarch_cfg_dir(
|
||||
value = value.strip().strip('"').strip("'")
|
||||
if not value or value == "default":
|
||||
return app_dir / default
|
||||
return _expand_retroarch_path(value, app_dir)
|
||||
return _expand_retroarch_path(value, app_dir, home)
|
||||
return None
|
||||
|
||||
|
||||
@@ -661,18 +661,19 @@ _RETROARCH_CONFIGS: dict[Path, tuple[Path, Path]] = {}
|
||||
|
||||
|
||||
def _parse_retroarch_system_dir(
|
||||
cfg_path: Path, app_dir: Path | None = None
|
||||
cfg_path: Path, app_dir: Path | None = None, home: Path | None = None
|
||||
) -> Path | None:
|
||||
"""Parse system_directory from retroarch.cfg.
|
||||
|
||||
app_dir is RetroArch's application directory, used to expand ':' values.
|
||||
It defaults to the directory holding the config, which is where a portable
|
||||
install keeps both.
|
||||
install keeps both. home is what '~' means to that RetroArch, the user's
|
||||
home when not given.
|
||||
"""
|
||||
if not cfg_path.exists():
|
||||
return None
|
||||
app_dir = app_dir or cfg_path.parent
|
||||
system_dir = _retroarch_cfg_dir(cfg_path, "system_directory", app_dir, "system")
|
||||
system_dir = _retroarch_cfg_dir(cfg_path, "system_directory", app_dir, "system", home)
|
||||
if system_dir is not None:
|
||||
_RETROARCH_CONFIGS.setdefault(system_dir, (cfg_path, app_dir))
|
||||
return system_dir
|
||||
@@ -817,70 +818,345 @@ def _parse_ps1_var(path: Path, key: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
# Embedded systems, first match wins. The registry publishes the same rules
|
||||
# as each manifest's `detect`, and a test holds the two equal. MiSTer is a
|
||||
# path rule below.
|
||||
_EMBEDDED_RULES: tuple[tuple[str, str, tuple[str, ...], str], ...] = (
|
||||
# ROCKNIX writes OS_NAME and no ID since 2025 (scripts/image). Older
|
||||
# images wrote ID=rocknix.
|
||||
("rocknix", "os_release", ("OS_NAME=rocknix", "ID=rocknix"), "/storage/roms/bios"),
|
||||
# Knulli (knulli-linux 57afa662): OS_NAME in os-release
|
||||
# (board/scripts/post-build-script.sh:233-236) and knulli.version
|
||||
# (package/system/knulli-system/knulli-system.mk:98-102).
|
||||
("batocera", "os_release", ("OS_NAME=knulli",), "/userdata/bios"),
|
||||
("batocera", "file_exists", ("/usr/share/knulli/knulli.version",), "/userdata/bios"),
|
||||
("lakka", "os_release", ("ID=lakka",), "/storage/system"),
|
||||
# batocera.linux 38845725, batocera-system.mk:21-22 (the same lines in
|
||||
# batocera-30): batocera.arch and batocera.version.
|
||||
("batocera", "file_exists", ("/usr/share/batocera/batocera.version",), "/userdata/bios"),
|
||||
("recalbox", "file_exists", ("/recalbox/recalbox.version",), "/recalbox/share/bios"),
|
||||
("recalbox", "file_exists", ("/usr/bin/recalbox-settings",), "/recalbox/share/bios"),
|
||||
("retroarch", "file_exists", ("/opt/muos",), "/run/muos/storage/bios"),
|
||||
("retroarch", "file_exists", ("/mnt/mmc/MUOS",), "/mnt/mmc/MUOS/bios"),
|
||||
("retroarch", "file_exists", ("/home/ark", "/opt/system"), "/roms/bios"),
|
||||
("retroarch", "file_exists", ("/mnt/vendor/bin/dmenu.bin",), "/mnt/mmc/bios"),
|
||||
)
|
||||
# The root the embedded rules read under: "/" on a device, a folder holding a
|
||||
# copy of one in the tests.
|
||||
_SYSTEM_ROOT = Path("/")
|
||||
|
||||
|
||||
def _detect_embedded() -> list[tuple[str, Path]]:
|
||||
"""Check for embedded Linux retrogaming OSes."""
|
||||
release = {k: v.lower() for k, v in _parse_os_release().items()}
|
||||
for platform_name, method, values, bios in _EMBEDDED_RULES:
|
||||
if method == "os_release":
|
||||
# Any of the KEY=value pairs, the value compared without case.
|
||||
matched = any(
|
||||
release.get(key) == value.lower()
|
||||
for key, _, value in (pair.partition("=") for pair in values)
|
||||
)
|
||||
else:
|
||||
matched = all(Path(value).exists() for value in values)
|
||||
if matched:
|
||||
return [(platform_name, Path(bios))]
|
||||
return []
|
||||
def _on_device(path: str) -> Path:
|
||||
"""A path of the running system, read under _SYSTEM_ROOT."""
|
||||
if _SYSTEM_ROOT == Path("/"):
|
||||
return Path(path)
|
||||
return _SYSTEM_ROOT / path.lstrip("/")
|
||||
|
||||
|
||||
class _OsRule(NamedTuple):
|
||||
"""One piece of evidence that names an embedded system.
|
||||
|
||||
manifest is the platform whose files it gets, "" for a system the
|
||||
installer does not lay out. name is the system's own name. values are the
|
||||
KEY=value pairs of os_release (any one), the files of file_exists (all),
|
||||
or the file and the pattern of file_matches. bios is the folder used when
|
||||
none of configs (retroarch.cfg files, read in order, `*` allowed) names a
|
||||
system_directory. home is what `~` means to that system's RetroArch.
|
||||
base is the system this one is built on: both match, this one is meant.
|
||||
"""
|
||||
|
||||
manifest: str
|
||||
name: str
|
||||
method: str
|
||||
values: tuple[str, ...]
|
||||
bios: str | None
|
||||
configs: tuple[str, ...] = ()
|
||||
home: str = ""
|
||||
base: str = ""
|
||||
|
||||
|
||||
# Systems whose own platform is not published yet. Their files come from the
|
||||
# platform whose core set they ship, and this is the one place that changes
|
||||
# when the manifest of their own is published.
|
||||
_STANDIN_MANIFESTS = {
|
||||
"AmberELEC": "retroarch",
|
||||
"EmuELEC": "retroarch",
|
||||
"KNULLI": "batocera",
|
||||
}
|
||||
|
||||
# Where the ELEC family keeps RetroArch: the config runemu writes and copies
|
||||
# back on every launch, and HOME=/storage for the services that start it.
|
||||
_ELEC_CONFIGS = ("/storage/.config/retroarch/retroarch.cfg",)
|
||||
# ArkOS and dArkOS: the 64-bit and the 32-bit RetroArch. "Switch to SD2 for
|
||||
# Roms" rewrites /roms/ to /roms2/ in both (darkos daecf488,
|
||||
# scripts/Switch to SD2 for Roms.sh:91-92), so a second card is read here.
|
||||
_ARK_CONFIGS = ("/home/ark/.config/retroarch/retroarch.cfg",
|
||||
"/home/ark/.config/retroarch32/retroarch.cfg")
|
||||
_PLYMOUTH_TITLE = "/usr/share/plymouth/themes/text.plymouth"
|
||||
|
||||
# muOS runs RetroArch on the config it copies from its default on first launch
|
||||
# (internal f41f4a89, script/control/retroarch.sh:8-20). The default names
|
||||
# /run/muos/storage/bios, a bind mount of MUOS/bios on the second card when
|
||||
# that card holds one, else on the first (script/device/bind.sh:4,116-150),
|
||||
# and loads cores from /opt/muos/share/core (retroarch.default.cfg:2806,3151).
|
||||
MUOS_ROOT = Path("/opt/muos")
|
||||
MUOS_RETROARCH_CONFIGS = (
|
||||
MUOS_ROOT / "share" / "info" / "config" / "retroarch.cfg",
|
||||
MUOS_ROOT / "share" / "emulator" / "retroarch" / "retroarch.default.cfg",
|
||||
"/opt/muos/share/info/config/retroarch.cfg",
|
||||
"/opt/muos/share/emulator/retroarch/retroarch.default.cfg",
|
||||
)
|
||||
|
||||
# Embedded systems, by what their own builds write. The registry publishes the
|
||||
# same rules as each manifest's `detect`, and a test holds the two equal.
|
||||
# MiSTer is a path rule below.
|
||||
_EMBEDDED_RULES: tuple[_OsRule, ...] = (
|
||||
# ROCKNIX writes OS_NAME and no ID since 2025 (scripts/image). Older
|
||||
# images wrote ID=rocknix.
|
||||
_OsRule("rocknix", "ROCKNIX", "os_release", ("OS_NAME=rocknix", "ID=rocknix"),
|
||||
"/storage/roms/bios"),
|
||||
# Knulli (knulli-linux 57afa662): OS_NAME in os-release
|
||||
# (board/scripts/post-build-script.sh:233-236) and knulli.version
|
||||
# (package/system/knulli-system/knulli-system.mk:98-102).
|
||||
_OsRule(_STANDIN_MANIFESTS["KNULLI"], "KNULLI", "os_release", ("OS_NAME=knulli",),
|
||||
"/userdata/bios", base="Batocera"),
|
||||
_OsRule(_STANDIN_MANIFESTS["KNULLI"], "KNULLI", "file_exists",
|
||||
("/usr/share/knulli/knulli.version",), "/userdata/bios", base="Batocera"),
|
||||
_OsRule("lakka", "Lakka", "os_release", ("ID=lakka",), "/storage/system"),
|
||||
# batocera.linux 38845725, batocera-system.mk:21-22 (the same lines in
|
||||
# batocera-30): batocera.arch and batocera.version.
|
||||
_OsRule("batocera", "Batocera", "file_exists", ("/usr/share/batocera/batocera.version",),
|
||||
"/userdata/bios"),
|
||||
_OsRule("recalbox", "Recalbox", "file_exists", ("/recalbox/recalbox.version",),
|
||||
"/recalbox/share/bios"),
|
||||
_OsRule("recalbox", "Recalbox", "file_exists", ("/usr/bin/recalbox-settings",),
|
||||
"/recalbox/share/bios"),
|
||||
_OsRule("retroarch", "muOS", "file_exists", ("/opt/muos",), "/run/muos/storage/bios",
|
||||
configs=MUOS_RETROARCH_CONFIGS),
|
||||
_OsRule("retroarch", "muOS", "file_exists", ("/mnt/mmc/MUOS",), "/mnt/mmc/MUOS/bios"),
|
||||
# AmberELEC 5a3882cb: scripts/image:150-152 writes NAME="${DISTRONAME}",
|
||||
# AmberELEC in distributions/AmberELEC/options:17. CHAos (tech4bot/AmberELEC
|
||||
# c0a4b0c6) is built from the same tree and writes the same name
|
||||
# (distributions/AmberELEC/options:14). runemu.py:22 launches with
|
||||
# /storage/.config/retroarch/retroarch.cfg, whose default is "~/roms/bios"
|
||||
# (packages/games/tools/retroarch/sources/retroarch.cfg:611), and
|
||||
# emustation.service:6 sets HOME=/storage.
|
||||
_OsRule(_STANDIN_MANIFESTS["AmberELEC"], "AmberELEC", "os_release", ("NAME=AmberELEC",),
|
||||
"/storage/roms/bios", _ELEC_CONFIGS, "/storage"),
|
||||
# PAN4ELEC (Kegg1701/P4ELEC 9c19bfb2), an overlay on an AmberELEC image:
|
||||
# AmberELEC/usr/bin/batocera-info:101 prints its name.
|
||||
_OsRule(_STANDIN_MANIFESTS["AmberELEC"], "PAN4ELEC", "file_matches",
|
||||
("/usr/bin/batocera-info", r"OPERATING SYSTEM: PAN4ELEC"),
|
||||
"/storage/roms/bios", _ELEC_CONFIGS, "/storage", base="AmberELEC"),
|
||||
# EmuELEC d4c7b2f1: scripts/image:172-176 NAME and ID from DISTRONAME,
|
||||
# EmuELEC in distributions/EmuELEC/options:4. setsettings.sh:22 names the
|
||||
# config, and libretro_base/retroarch/package.mk:117 sets its
|
||||
# system_directory to /storage/roms/bios.
|
||||
_OsRule(_STANDIN_MANIFESTS["EmuELEC"], "EmuELEC", "os_release", ("NAME=EmuELEC",),
|
||||
"/storage/roms/bios", _ELEC_CONFIGS, "/storage"),
|
||||
# JELOS 7b2e116c: scripts/image:118-120 OS_NAME="${DISTRONAME}", JELOS in
|
||||
# distributions/JELOS/options:17. runemu.sh:37 names the config, whose
|
||||
# default is "~/roms/bios" (retroarch/sources/RK3566/retroarch.cfg:716),
|
||||
# and emustation.service:8 sets HOME=/storage.
|
||||
_OsRule("retroarch", "JELOS", "os_release", ("OS_NAME=JELOS",),
|
||||
"/storage/roms/bios", _ELEC_CONFIGS, "/storage"),
|
||||
# KOLOS (AnonLinuxOS/KOLOS 7daf5950) keeps OS_NAME=JELOS
|
||||
# (distributions/JELOS/options:17) and adds jelos-ready.service
|
||||
# ("KOLOS system ready script"), installed by scripts/install:100-102.
|
||||
_OsRule("retroarch", "KOLOS", "file_exists",
|
||||
("/usr/lib/systemd/system/jelos-ready.service",),
|
||||
"/storage/roms/bios", _ELEC_CONFIGS, "/storage", base="JELOS"),
|
||||
# UnofficialOS 22635796: scripts/image:119-120 OS_NAME, UnofficialOS in
|
||||
# distributions/UnofficialOS/options:17, config at
|
||||
# packages/unofficialos/sources/scripts/runemu.sh:37.
|
||||
_OsRule("retroarch", "UnofficialOS", "os_release", ("OS_NAME=UnofficialOS",),
|
||||
"/storage/roms/bios", _ELEC_CONFIGS, "/storage"),
|
||||
# Arch R (archr-linux/Arch-R dbd45d45): scripts/image:164-177 NAME and
|
||||
# OS_NAME, ArchR in distributions/ArchR/options:17. factoryreset:9-10
|
||||
# names the config, whose default is "~/roms/bios"
|
||||
# (retroarch/sources/RK3326/retroarch.cfg:718).
|
||||
_OsRule("retroarch", "ArchR", "os_release", ("OS_NAME=ArchR",),
|
||||
"/storage/roms/bios", _ELEC_CONFIGS, "/storage"),
|
||||
# ArkOS: the user and the menu folder, and the boot splash title its
|
||||
# updates write (christianhaitian/arkos dedf4278, Update-RGB10.sh:76).
|
||||
_OsRule("retroarch", "ArkOS", "file_exists", ("/home/ark", "/opt/system"), "/roms/bios",
|
||||
_ARK_CONFIGS),
|
||||
_OsRule("retroarch", "ArkOS", "file_matches", (_PLYMOUTH_TITLE, r"^title=\s*ArkOS\b"),
|
||||
"/roms/bios", _ARK_CONFIGS),
|
||||
# dArkOS daecf488: finishing_touches.sh:359-361 writes "title=dArkOS
|
||||
# (<date>)", kept by the dArkOSRE, dArkOSen and ArkOS4Clone overlays.
|
||||
# Default system_directory "/roms/bios" (retroarch.cfg.rgb10:3043).
|
||||
_OsRule("retroarch", "dArkOS", "file_matches", (_PLYMOUTH_TITLE, r"^title=\s*dArkOS"),
|
||||
"/roms/bios", _ARK_CONFIGS, base="ArkOS"),
|
||||
# RetroOZ 0.60 image (no build source is published): title=RetroOZ in
|
||||
# the splash theme, /opt/.retrooz/version, and /etc/retrooz_version, the
|
||||
# file PortMaster reads (device_info.txt:95). RetroArch is started with
|
||||
# --config /home/odroid/.config/retroarch/retroarch.cfg (es_systems.cfg),
|
||||
# system_directory "/roms/bios" (retroarch32/retroarch.cfg:3046).
|
||||
_OsRule("retroarch", "RetroOZ", "file_matches", (_PLYMOUTH_TITLE, r"^title=\s*RetroOZ"),
|
||||
"/roms/bios", ("/home/odroid/.config/retroarch/retroarch.cfg",
|
||||
"/home/odroid/.config/retroarch32/retroarch.cfg")),
|
||||
_OsRule("retroarch", "RetroOZ", "file_exists", ("/opt/.retrooz",),
|
||||
"/roms/bios", ("/home/odroid/.config/retroarch/retroarch.cfg",
|
||||
"/home/odroid/.config/retroarch32/retroarch.cfg")),
|
||||
_OsRule("retroarch", "RetroOZ", "file_exists", ("/etc/retrooz_version",),
|
||||
"/roms/bios", ("/home/odroid/.config/retroarch/retroarch.cfg",
|
||||
"/home/odroid/.config/retroarch32/retroarch.cfg")),
|
||||
# TheRA is left undetected: no image or source of its Odroid Go builds is
|
||||
# published, so neither its markers nor its folder could be read.
|
||||
# spruceOS 824b1aae: the spruce folder at the card root. RetroArch reads
|
||||
# Saves/ra-configs/retroarch-<platform>.cfg (spruce/scripts/
|
||||
# helperFunctions.sh:157-165), seeded with system_directory
|
||||
# "/mnt/SDCARD/BIOS" (RetroArch/platform/retroarch-Brick.cfg.bak:3279).
|
||||
_OsRule("retroarch", "spruceOS", "file_exists", ("/mnt/SDCARD/spruce",),
|
||||
"/mnt/SDCARD/BIOS", ("/mnt/SDCARD/Saves/ra-configs/retroarch-*.cfg",),
|
||||
base="TrimUI"),
|
||||
# CrossMix-OS 206d612f: crossmix-version.txt (System/usr/trimui/scripts/
|
||||
# update_common.sh:20), RetroArch/retroarch.cfg:3202 (patch_ra_cfg.sh:5).
|
||||
_OsRule("retroarch", "CrossMix-OS", "file_exists",
|
||||
("/mnt/SDCARD/System/usr/trimui/crossmix-version.txt",),
|
||||
"/mnt/SDCARD/BIOS", ("/mnt/SDCARD/RetroArch/retroarch.cfg",), base="TrimUI"),
|
||||
# Onion 07505ea5: Makefile:75-76 writes onionVersion/version.txt, and
|
||||
# static/build/.tmp_update/script/patch_ra_cfg.sh:6 names the config
|
||||
# (system_directory at static/configs/RetroArch/.retroarch/retroarch.cfg:3009).
|
||||
_OsRule("retroarch", "Onion", "file_exists",
|
||||
("/mnt/SDCARD/.tmp_update/onionVersion/version.txt",),
|
||||
"/mnt/SDCARD/BIOS", ("/mnt/SDCARD/RetroArch/.retroarch/retroarch.cfg",)),
|
||||
# Allium ee0dda97: its base folder /mnt/SDCARD/.allium
|
||||
# (crates/common/src/constants.rs:19-34). RetroArch runs with
|
||||
# HOME=/mnt/SDCARD/RetroArch on .retroarch/retroarch.cfg
|
||||
# (static/.allium/cores/retroarch/launch_without_savestate_auto_load.sh:2-7),
|
||||
# and Allium's RetroArch patch makes $ROOT/BIOS the default system folder.
|
||||
_OsRule("retroarch", "Allium", "file_exists", ("/mnt/SDCARD/.allium",),
|
||||
"/mnt/SDCARD/BIOS", ("/mnt/SDCARD/RetroArch/.retroarch/retroarch.cfg",)),
|
||||
# Koriki 8513071c: src/systemInfo/main.c:106 reads Koriki/version.txt,
|
||||
# .tmp_update/runtime.sh:271 installs RetroArch/.retroarch/retroarch.cfg
|
||||
# (system_directory at base/RetroArch/.retroarch/retroarch.cfg:3191).
|
||||
_OsRule("retroarch", "Koriki", "file_exists", ("/mnt/SDCARD/Koriki/version.txt",),
|
||||
"/mnt/SDCARD/BIOS", ("/mnt/SDCARD/RetroArch/.retroarch/retroarch.cfg",)),
|
||||
# MiyooCFW (buildroot d5912eeb): board/miyoo/rootfs/etc/main sets HOME=/mnt
|
||||
# (line 4), package/miyoo/miyoo-ctl/miyoo-ctl.mk:12 installs miyooctl.
|
||||
# RetroArch is a DINGUX build: $HOME/.retroarch/retroarch.cfg, and
|
||||
# $HOME/.retroarch/system when no config names a folder.
|
||||
_OsRule("retroarch", "MiyooCFW", "file_exists", ("/etc/main", "/usr/bin/miyooctl"),
|
||||
"/mnt/.retroarch/system", ("/mnt/.retroarch/retroarch.cfg",), "/mnt"),
|
||||
# plumOS: os-release ID=plumos (game-de-it/plumOS-pixel2_v2 dec872c0,
|
||||
# rootfs/pixel2/etc/os-release:2, PortMaster device_info.txt:101). Each
|
||||
# device build appends its own system_directory at launch: the Pixel 2
|
||||
# build /mnt/plumos-user/bios (package/app-layer-pixel2/bin/
|
||||
# plumos-retroarch-launch:342, mount point made by rootfs/pixel2/sbin/
|
||||
# init:29), the MF build <card>/BIOS (plumOS-MF 0095017c,
|
||||
# plumos-retroarch-launch:15). Only the first is told apart by a file.
|
||||
_OsRule("retroarch", "plumOS", "file_exists", ("/mnt/plumos-user",),
|
||||
"/mnt/plumos-user/bios"),
|
||||
_OsRule("retroarch", "plumOS", "os_release", ("ID=plumos",), None),
|
||||
# TrimUI's stock firmware publishes no source naming its RetroArch BIOS
|
||||
# folder. PortMaster finds its libraries in /usr/trimui
|
||||
# (trimui/launch_hooks.txt:37). Named so that what runs on top of it is
|
||||
# told apart, never installed to by a guess.
|
||||
_OsRule("", "TrimUI", "file_exists", ("/usr/trimui",), None),
|
||||
# MinUI, MyMinUI and NextUI keep their system in .system at the card root
|
||||
# (MinUI dbf89435, workspace/all/common/defines.h:14-16) and give each core
|
||||
# Bios/<tag>/ (workspace/all/minarch/minarch.c:2942), a layout the
|
||||
# installer does not write.
|
||||
_OsRule("", "MinUI", "file_exists", ("/mnt/SDCARD/.system/res",), None, base="TrimUI"),
|
||||
_OsRule("", "MinUI", "file_exists", ("/mnt/sdcard/.system/res",), None, base="TrimUI"),
|
||||
_OsRule("retroarch", "Anbernic stock firmware", "file_exists",
|
||||
("/mnt/vendor/bin/dmenu.bin",), "/mnt/mmc/bios"),
|
||||
)
|
||||
|
||||
def _muos_system_dir() -> Path | None:
|
||||
"""The system directory of muOS's RetroArch, read from its config, which
|
||||
also records the config for finding the installed cores."""
|
||||
if not MUOS_ROOT.is_dir():
|
||||
return None
|
||||
for cfg in MUOS_RETROARCH_CONFIGS:
|
||||
system_dir = _parse_retroarch_system_dir(cfg)
|
||||
if system_dir is not None:
|
||||
return system_dir
|
||||
return None
|
||||
# Why a system found here gets no folder: said once per run.
|
||||
_NO_FOLDER = {
|
||||
"TrimUI": "its firmware publishes no source naming the folder its RetroArch reads",
|
||||
"MinUI": "MinUI, MyMinUI and NextUI give each core its own Bios/<tag>/ folder, "
|
||||
"a layout the installer does not write yet",
|
||||
}
|
||||
|
||||
# (platform, BIOS folder) -> the name the system found there gives itself.
|
||||
_DETECTED_OS: dict[tuple[str, Path], str] = {}
|
||||
|
||||
|
||||
def _rule_matches(rule: _OsRule, release: dict[str, str]) -> bool:
|
||||
if rule.method == "os_release":
|
||||
# Any of the KEY=value pairs, the value compared without case.
|
||||
return any(
|
||||
release.get(key) == value.lower()
|
||||
for key, _, value in (pair.partition("=") for pair in rule.values)
|
||||
)
|
||||
if rule.method == "file_matches":
|
||||
path, pattern = rule.values
|
||||
try:
|
||||
text = _on_device(path).read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
return False
|
||||
return re.search(pattern, text, re.IGNORECASE | re.MULTILINE) is not None
|
||||
return all(_on_device(value).exists() for value in rule.values)
|
||||
|
||||
|
||||
def _config_files(pattern: str) -> list[tuple[Path, Path]]:
|
||||
"""(file to read, its folder as the system sees it) for a config rule."""
|
||||
device = pattern.replace("$HOME", str(Path.home()), 1)
|
||||
folder, _, name = device.rpartition("/")
|
||||
if "*" not in name:
|
||||
return [(_on_device(device), Path(folder))]
|
||||
try:
|
||||
return [(path, Path(folder)) for path in sorted(_on_device(folder).glob(name))
|
||||
if path.is_file()]
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
|
||||
def _os_system_dir(rules: list[_OsRule]) -> Path | None:
|
||||
"""The folder a system's RetroArch reads: the system_directory of its
|
||||
first config that sets one, else the folder its build defaults to.
|
||||
|
||||
The config also records where the cores of that installation are.
|
||||
"""
|
||||
for rule in rules:
|
||||
home = Path(rule.home) if rule.home else None
|
||||
for pattern in rule.configs:
|
||||
for path, folder in _config_files(pattern):
|
||||
system_dir = _parse_retroarch_system_dir(path, folder, home)
|
||||
if system_dir is not None:
|
||||
seen = _on_device(str(system_dir))
|
||||
_RETROARCH_CONFIGS.setdefault(seen, _RETROARCH_CONFIGS[system_dir])
|
||||
return seen
|
||||
bios = next((rule.bios for rule in rules if rule.bios), None)
|
||||
return _on_device(bios) if bios else None
|
||||
|
||||
|
||||
def _detect_embedded() -> list[tuple[str, Path]]:
|
||||
"""The embedded system this is, by what its own build writes.
|
||||
|
||||
A system built on another matches both: the derivative is meant. Two
|
||||
unrelated systems matching are both returned, and os_conflict() names
|
||||
them, so that no folder is chosen between them in silence.
|
||||
"""
|
||||
release = {k: v.lower() for k, v in _parse_os_release().items()}
|
||||
matched = [rule for rule in _EMBEDDED_RULES if _rule_matches(rule, release)]
|
||||
bases = {rule.base for rule in matched if rule.base}
|
||||
found: list[tuple[str, Path]] = []
|
||||
for name in dict.fromkeys(rule.name for rule in matched):
|
||||
if name in bases:
|
||||
continue
|
||||
rules = [rule for rule in matched if rule.name == name]
|
||||
folder = _os_system_dir(rules) if rules[0].manifest else None
|
||||
if folder is None:
|
||||
reason = _NO_FOLDER.get(
|
||||
name, "nothing on it names the folder its RetroArch reads")
|
||||
print(f"{name} found here, but {reason}. Name the folder with --dest.",
|
||||
file=sys.stderr)
|
||||
continue
|
||||
_DETECTED_OS[(rules[0].manifest, folder)] = name
|
||||
found.append((rules[0].manifest, folder))
|
||||
return found
|
||||
|
||||
|
||||
def detected_os(plat: str, path: Path) -> str:
|
||||
"""The name of the system detection found this folder on, or ""."""
|
||||
return _DETECTED_OS.get((plat, path), "")
|
||||
|
||||
|
||||
def os_conflict(platforms: list[tuple[str, Path]]) -> list[str]:
|
||||
"""The systems among these when there are two or more: a machine runs one."""
|
||||
names = list(dict.fromkeys(filter(None, (detected_os(n, p) for n, p in platforms))))
|
||||
return names if len(names) > 1 else []
|
||||
|
||||
|
||||
def _system_lines() -> list[str]:
|
||||
"""Each embedded system detection knows, with the files it gets."""
|
||||
lines = []
|
||||
for name in dict.fromkeys(rule.name for rule in _EMBEDDED_RULES):
|
||||
plat = next(rule.manifest for rule in _EMBEDDED_RULES if rule.name == name)
|
||||
if plat:
|
||||
lines.append(f"{name}: {plat}")
|
||||
else:
|
||||
lines.append(f"{name}: not installed to, {_NO_FOLDER[name]} (use --dest)")
|
||||
return lines
|
||||
|
||||
|
||||
def found_label(plat: str, path: Path) -> str:
|
||||
"""How a detected folder is announced: the system's own name, and the
|
||||
platform whose files it gets when that is another name."""
|
||||
name = detected_os(plat, path)
|
||||
shown = display_name(plat)
|
||||
if not name or name == shown:
|
||||
return f"{shown} at {path}"
|
||||
return f"{name} at {path} ({shown} files)"
|
||||
|
||||
|
||||
def _android_storage_root() -> Path:
|
||||
@@ -1259,9 +1535,7 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]:
|
||||
found.extend(_detect_android())
|
||||
|
||||
if os_type in ("linux", "wsl"):
|
||||
embedded = _detect_embedded()
|
||||
muos = _muos_system_dir() if embedded and embedded[0][0] == "retroarch" else None
|
||||
found.extend([("retroarch", muos)] if muos else embedded)
|
||||
found.extend(_detect_embedded())
|
||||
|
||||
if os_type in ("linux", "wsl", "darwin"):
|
||||
# EmuDeck on Linux and macOS: functions/all.sh:3-4 and 24 (EmuDeck
|
||||
@@ -1391,7 +1665,9 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]:
|
||||
_detect_by_executable(os_type, found)
|
||||
_detect_by_registry(os_type, found)
|
||||
_detect_by_path(os_type, found)
|
||||
return found
|
||||
# An embedded system's config read again as the home folder's one (HOME is
|
||||
# /storage on the ELEC family) is the same folder.
|
||||
return list(dict.fromkeys(found))
|
||||
|
||||
|
||||
def _emudeck_windows_bios() -> Path | None:
|
||||
@@ -4175,7 +4451,10 @@ def _prompt_platform_choice(
|
||||
"""Prompt user to choose among detected platforms."""
|
||||
print("\nInstall for:")
|
||||
for i, (name, path) in enumerate(platforms, 1):
|
||||
print(f" {i}) {display_name(name)} ({path})")
|
||||
system = detected_os(name, path)
|
||||
label = f"{system}, {display_name(name)} files" if system and system != display_name(name) \
|
||||
else display_name(name)
|
||||
print(f" {i}) {label} ({path})")
|
||||
last = len(platforms) + (1 if len(platforms) > 1 else 0)
|
||||
if len(platforms) > 1:
|
||||
print(f" {last}) All")
|
||||
@@ -4883,11 +5162,15 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
print("Available platforms:")
|
||||
for p in AVAILABLE_PLATFORMS:
|
||||
print(f" {p}")
|
||||
print("\nSystems recognized by their own files:")
|
||||
for line in _system_lines():
|
||||
print(f" {line}")
|
||||
detected = detect_platforms(os_type)
|
||||
if detected:
|
||||
print("\nDetected on this system:")
|
||||
for name, path in detected:
|
||||
print(f" {name}: {path}")
|
||||
system = detected_os(name, path)
|
||||
print(f" {name}: {path}" + (f" ({system})" if system else ""))
|
||||
return
|
||||
|
||||
# Platform detection or override. A destination taken from the default
|
||||
@@ -4968,8 +5251,17 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
sys.exit(1)
|
||||
platforms = _prompt_manual_platform(os_type)
|
||||
for name, path in platforms:
|
||||
print(f" Found {display_name(name)} at {path}")
|
||||
print(f" Found {found_label(name, path)}")
|
||||
|
||||
conflict = os_conflict(platforms)
|
||||
if conflict and not interactive and not listing:
|
||||
# A machine runs one system: installing to each folder would write
|
||||
# where nothing reads, and picking one would be a guess.
|
||||
print(f"Error: {' and '.join(conflict)} are both detected here, and a "
|
||||
"machine runs one. Name the folder with --dest (and --platform for "
|
||||
"its file list).",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if len(platforms) > 1 and not listing and interactive:
|
||||
platforms = _prompt_platform_choice(platforms)
|
||||
|
||||
|
||||
+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="fe8de4eda0919c268d4d2c0d571c7d0dc1cf675d7604263766f5f99a35aa3724"
|
||||
DEFAULT_INSTALL_SHA256="9fbee0f382ca68880697ed573b79ecc1ffca793a406a3f258dad16e40d2845d6"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
+222
-15
@@ -267,34 +267,231 @@ platforms:
|
||||
method: config_file
|
||||
config: '%ProgramFiles%\Steam\steamapps\common\RetroArch\retroarch.cfg'
|
||||
parse_key: system_directory
|
||||
# Handheld firmwares that run RetroArch as their frontend: muOS,
|
||||
# ArkOS, and the stock Linux of Anbernic devices (dmenu).
|
||||
# muOS copies its default config on first launch and RetroArch reads
|
||||
# /run/muos/storage/bios, bound to MUOS/bios on the second card when
|
||||
# it holds one, else the first (MustardOS/internal f41f4a89,
|
||||
# script/control/retroarch.sh:8-20, script/device/bind.sh:4,116-150,
|
||||
# share/emulator/retroarch/retroarch.default.cfg:2806,3151).
|
||||
- os: linux
|
||||
method: config_file
|
||||
config: /opt/muos/share/info/config/retroarch.cfg
|
||||
parse_key: system_directory
|
||||
- os: linux
|
||||
method: config_file
|
||||
config: /opt/muos/share/emulator/retroarch/retroarch.default.cfg
|
||||
parse_key: system_directory
|
||||
# Handheld systems that run RetroArch as their frontend, each found by
|
||||
# what its own build writes. A system built on another (derives_from)
|
||||
# matches both and is the one meant.
|
||||
# muOS: MustardOS/internal f41f4a89: retroarch.sh:8-20, bind.sh:4,116-150,
|
||||
# retroarch.default.cfg:2806,3151. /mnt/mmc/MUOS is the older layout.
|
||||
- os: linux
|
||||
name: muOS
|
||||
method: file_exists
|
||||
file: /opt/muos
|
||||
bios_path: /run/muos/storage/bios
|
||||
config: [/opt/muos/share/info/config/retroarch.cfg, /opt/muos/share/emulator/retroarch/retroarch.default.cfg]
|
||||
- os: linux
|
||||
name: muOS
|
||||
method: file_exists
|
||||
file: /mnt/mmc/MUOS
|
||||
bios_path: /mnt/mmc/MUOS/bios
|
||||
# AmberELEC: AmberELEC 5a3882cb scripts/image:150-152, options:17, runemu.py:22,
|
||||
# retroarch.cfg:611 ("~/roms/bios"), emustation.service:6 (HOME). CHAos
|
||||
# (tech4bot/AmberELEC c0a4b0c6) writes the same name. Served by RetroArch's
|
||||
# file list until its own platform is published.
|
||||
- os: linux
|
||||
name: AmberELEC
|
||||
method: os_release
|
||||
match: [NAME=AmberELEC]
|
||||
bios_path: /storage/roms/bios
|
||||
config: [/storage/.config/retroarch/retroarch.cfg]
|
||||
home: /storage
|
||||
# PAN4ELEC: Kegg1701/P4ELEC 9c19bfb2, AmberELEC/usr/bin/batocera-info:101.
|
||||
- os: linux
|
||||
name: PAN4ELEC
|
||||
method: file_matches
|
||||
file: /usr/bin/batocera-info
|
||||
pattern: 'OPERATING SYSTEM: PAN4ELEC'
|
||||
bios_path: /storage/roms/bios
|
||||
config: [/storage/.config/retroarch/retroarch.cfg]
|
||||
home: /storage
|
||||
derives_from: AmberELEC
|
||||
# EmuELEC: EmuELEC d4c7b2f1 scripts/image:172-176, options:4, setsettings.sh:22,
|
||||
# libretro_base/retroarch/package.mk:117. Served by RetroArch's file list
|
||||
# until its own platform is published.
|
||||
- os: linux
|
||||
name: EmuELEC
|
||||
method: os_release
|
||||
match: [NAME=EmuELEC]
|
||||
bios_path: /storage/roms/bios
|
||||
config: [/storage/.config/retroarch/retroarch.cfg]
|
||||
home: /storage
|
||||
# JELOS: JustEnoughLinuxOS/distribution 7b2e116c scripts/image:118-120,
|
||||
# options:17, runemu.sh:37, RK3566/retroarch.cfg:716.
|
||||
- os: linux
|
||||
name: JELOS
|
||||
method: os_release
|
||||
match: [OS_NAME=JELOS]
|
||||
bios_path: /storage/roms/bios
|
||||
config: [/storage/.config/retroarch/retroarch.cfg]
|
||||
home: /storage
|
||||
# KOLOS: AnonLinuxOS/KOLOS 7daf5950: OS_NAME=JELOS kept, jelos-ready.service
|
||||
# ("KOLOS system ready script") installed by scripts/install:100-102.
|
||||
- os: linux
|
||||
name: KOLOS
|
||||
method: file_exists
|
||||
file: /usr/lib/systemd/system/jelos-ready.service
|
||||
bios_path: /storage/roms/bios
|
||||
config: [/storage/.config/retroarch/retroarch.cfg]
|
||||
home: /storage
|
||||
derives_from: JELOS
|
||||
# UnofficialOS: RetroGFX/UnofficialOS 22635796 scripts/image:119-120, options:17,
|
||||
# packages/unofficialos/sources/scripts/runemu.sh:37.
|
||||
- os: linux
|
||||
name: UnofficialOS
|
||||
method: os_release
|
||||
match: [OS_NAME=UnofficialOS]
|
||||
bios_path: /storage/roms/bios
|
||||
config: [/storage/.config/retroarch/retroarch.cfg]
|
||||
home: /storage
|
||||
# ArchR: archr-linux/Arch-R dbd45d45 scripts/image:164-177, options:17,
|
||||
# factoryreset:9-10, RK3326/retroarch.cfg:718.
|
||||
- os: linux
|
||||
name: ArchR
|
||||
method: os_release
|
||||
match: [OS_NAME=ArchR]
|
||||
bios_path: /storage/roms/bios
|
||||
config: [/storage/.config/retroarch/retroarch.cfg]
|
||||
home: /storage
|
||||
# ArkOS: christianhaitian/arkos dedf4278 Update-RGB10.sh:76 (splash title).
|
||||
# Both configs move to /roms2 with "Switch to SD2 for Roms" (darkos
|
||||
# daecf488, scripts/Switch to SD2 for Roms.sh:91-92).
|
||||
- os: linux
|
||||
name: ArkOS
|
||||
method: file_exists
|
||||
file: [/home/ark, /opt/system]
|
||||
bios_path: /roms/bios
|
||||
config: [/home/ark/.config/retroarch/retroarch.cfg, /home/ark/.config/retroarch32/retroarch.cfg]
|
||||
- os: linux
|
||||
name: ArkOS
|
||||
method: file_matches
|
||||
file: /usr/share/plymouth/themes/text.plymouth
|
||||
pattern: '^title=\s*ArkOS\b'
|
||||
bios_path: /roms/bios
|
||||
config: [/home/ark/.config/retroarch/retroarch.cfg, /home/ark/.config/retroarch32/retroarch.cfg]
|
||||
# dArkOS: christianhaitian/darkos daecf488 finishing_touches.sh:359-361,
|
||||
# retroarch.cfg.rgb10:3043. The dArkOSRE, dArkOSen and ArkOS4Clone overlays
|
||||
# keep the title.
|
||||
- os: linux
|
||||
name: dArkOS
|
||||
method: file_matches
|
||||
file: /usr/share/plymouth/themes/text.plymouth
|
||||
pattern: '^title=\s*dArkOS'
|
||||
bios_path: /roms/bios
|
||||
config: [/home/ark/.config/retroarch/retroarch.cfg, /home/ark/.config/retroarch32/retroarch.cfg]
|
||||
derives_from: ArkOS
|
||||
# RetroOZ: RetroOZ 0.60 image: splash title, /opt/.retrooz/version,
|
||||
# retroarch32/retroarch.cfg:3046. /etc/retrooz_version is the file PortMaster
|
||||
# reads (PortMaster-GUI ccea6b74, PortMaster/device_info.txt:95).
|
||||
- os: linux
|
||||
name: RetroOZ
|
||||
method: file_matches
|
||||
file: /usr/share/plymouth/themes/text.plymouth
|
||||
pattern: '^title=\s*RetroOZ'
|
||||
bios_path: /roms/bios
|
||||
config: [/home/odroid/.config/retroarch/retroarch.cfg, /home/odroid/.config/retroarch32/retroarch.cfg]
|
||||
- os: linux
|
||||
name: RetroOZ
|
||||
method: file_exists
|
||||
file: /opt/.retrooz
|
||||
bios_path: /roms/bios
|
||||
config: [/home/odroid/.config/retroarch/retroarch.cfg, /home/odroid/.config/retroarch32/retroarch.cfg]
|
||||
- os: linux
|
||||
name: RetroOZ
|
||||
method: file_exists
|
||||
file: /etc/retrooz_version
|
||||
bios_path: /roms/bios
|
||||
config: [/home/odroid/.config/retroarch/retroarch.cfg, /home/odroid/.config/retroarch32/retroarch.cfg]
|
||||
# spruceOS: spruceOS 824b1aae spruce/scripts/helperFunctions.sh:157-165,
|
||||
# RetroArch/platform/retroarch-Brick.cfg.bak:3279.
|
||||
- os: linux
|
||||
name: spruceOS
|
||||
method: file_exists
|
||||
file: /mnt/SDCARD/spruce
|
||||
bios_path: /mnt/SDCARD/BIOS
|
||||
config: ['/mnt/SDCARD/Saves/ra-configs/retroarch-*.cfg']
|
||||
derives_from: TrimUI
|
||||
# CrossMix-OS: cizia64/CrossMix-OS 206d612f System/usr/trimui/scripts/
|
||||
# update_common.sh:20, patch_ra_cfg.sh:5, RetroArch/retroarch.cfg:3202.
|
||||
- os: linux
|
||||
name: CrossMix-OS
|
||||
method: file_exists
|
||||
file: /mnt/SDCARD/System/usr/trimui/crossmix-version.txt
|
||||
bios_path: /mnt/SDCARD/BIOS
|
||||
config: [/mnt/SDCARD/RetroArch/retroarch.cfg]
|
||||
derives_from: TrimUI
|
||||
# Onion: OnionUI/Onion 07505ea5 Makefile:75-76,
|
||||
# .tmp_update/script/patch_ra_cfg.sh:6, retroarch.cfg:3009.
|
||||
- os: linux
|
||||
name: Onion
|
||||
method: file_exists
|
||||
file: /mnt/SDCARD/.tmp_update/onionVersion/version.txt
|
||||
bios_path: /mnt/SDCARD/BIOS
|
||||
config: [/mnt/SDCARD/RetroArch/.retroarch/retroarch.cfg]
|
||||
# Allium: goweiwen/Allium ee0dda97 crates/common/src/constants.rs:19-34,
|
||||
# launch_without_savestate_auto_load.sh:2-7. Its RetroArch patch makes
|
||||
# $ROOT/BIOS the default system folder.
|
||||
- os: linux
|
||||
name: Allium
|
||||
method: file_exists
|
||||
file: /mnt/SDCARD/.allium
|
||||
bios_path: /mnt/SDCARD/BIOS
|
||||
config: [/mnt/SDCARD/RetroArch/.retroarch/retroarch.cfg]
|
||||
# Koriki: Rparadise-Team/Koriki 8513071c src/systemInfo/main.c:106,
|
||||
# .tmp_update/runtime.sh:271, RetroArch/.retroarch/retroarch.cfg:3191.
|
||||
- os: linux
|
||||
name: Koriki
|
||||
method: file_exists
|
||||
file: /mnt/SDCARD/Koriki/version.txt
|
||||
bios_path: /mnt/SDCARD/BIOS
|
||||
config: [/mnt/SDCARD/RetroArch/.retroarch/retroarch.cfg]
|
||||
# MiyooCFW: MiyooCFW buildroot d5912eeb board/miyoo/rootfs/etc/main:4 (HOME),
|
||||
# package/miyoo/miyoo-ctl/miyoo-ctl.mk:12. DINGUX RetroArch defaults to
|
||||
# $HOME/.retroarch/system.
|
||||
- os: linux
|
||||
name: MiyooCFW
|
||||
method: file_exists
|
||||
file: [/etc/main, /usr/bin/miyooctl]
|
||||
bios_path: /mnt/.retroarch/system
|
||||
config: [/mnt/.retroarch/retroarch.cfg]
|
||||
home: /mnt
|
||||
# plumOS: game-de-it/plumOS-pixel2_v2 dec872c0 rootfs/pixel2/etc/os-release:2,
|
||||
# sbin/init:29, plumos-retroarch-launch:342. Other plumOS builds append a
|
||||
# folder of their own at launch (plumOS-MF 0095017c,
|
||||
# plumos-retroarch-launch:15): found, not installed to.
|
||||
- os: linux
|
||||
name: plumOS
|
||||
method: file_exists
|
||||
file: /mnt/plumos-user
|
||||
bios_path: /mnt/plumos-user/bios
|
||||
- os: linux
|
||||
name: plumOS
|
||||
method: os_release
|
||||
match: [ID=plumos]
|
||||
# TrimUI: the stock firmware publishes no source naming its BIOS folder.
|
||||
# /usr/trimui is where PortMaster finds its libraries
|
||||
# (trimui/launch_hooks.txt:37). Found so that what runs on it is told
|
||||
# apart, never installed to.
|
||||
- os: linux
|
||||
name: TrimUI
|
||||
method: file_exists
|
||||
file: /usr/trimui
|
||||
installs: false
|
||||
# MinUI: MinUI dbf89435 workspace/all/common/defines.h:14-16, minarch.c:2942.
|
||||
# One Bios/<tag>/ folder per core, not written by the installer.
|
||||
- os: linux
|
||||
name: MinUI
|
||||
method: file_exists
|
||||
file: /mnt/SDCARD/.system/res
|
||||
installs: false
|
||||
derives_from: TrimUI
|
||||
- os: linux
|
||||
name: MinUI
|
||||
method: file_exists
|
||||
file: /mnt/sdcard/.system/res
|
||||
installs: false
|
||||
derives_from: TrimUI
|
||||
# Anbernic stock firmware: the stock Linux of Anbernic devices.
|
||||
- os: linux
|
||||
name: 'Anbernic stock firmware'
|
||||
method: file_exists
|
||||
file: /mnt/vendor/bin/dmenu.bin
|
||||
bios_path: /mnt/mmc/bios
|
||||
@@ -316,17 +513,23 @@ platforms:
|
||||
# Knulli, a Batocera derivative (knulli-cfw/knulli-linux 57afa662):
|
||||
# OS_NAME in os-release (board/scripts/post-build-script.sh:233-236),
|
||||
# and knulli.version (package/system/knulli-system/knulli-system.mk:98-102).
|
||||
# Served by Batocera's file list until its own platform is published.
|
||||
- os: linux
|
||||
name: KNULLI
|
||||
method: os_release
|
||||
match: [OS_NAME=knulli]
|
||||
bios_path: /userdata/bios
|
||||
derives_from: Batocera
|
||||
- os: linux
|
||||
name: KNULLI
|
||||
method: file_exists
|
||||
file: /usr/share/knulli/knulli.version
|
||||
bios_path: /userdata/bios
|
||||
derives_from: Batocera
|
||||
# batocera.linux 38845725, package/batocera/core/batocera-system/
|
||||
# batocera-system.mk:21-22 writes batocera.arch and batocera.version.
|
||||
- os: linux
|
||||
name: Batocera
|
||||
method: file_exists
|
||||
file: /usr/share/batocera/batocera.version
|
||||
bios_path: /userdata/bios
|
||||
@@ -348,10 +551,12 @@ platforms:
|
||||
install:
|
||||
detect:
|
||||
- os: linux
|
||||
name: Recalbox
|
||||
method: file_exists
|
||||
file: /recalbox/recalbox.version
|
||||
bios_path: /recalbox/share/bios
|
||||
- os: linux
|
||||
name: Recalbox
|
||||
method: file_exists
|
||||
file: /usr/bin/recalbox-settings
|
||||
bios_path: /recalbox/share/bios
|
||||
@@ -456,6 +661,7 @@ platforms:
|
||||
install:
|
||||
detect:
|
||||
- os: linux
|
||||
name: Lakka
|
||||
method: os_release
|
||||
match: [ID=lakka]
|
||||
bios_path: /storage/system
|
||||
@@ -568,6 +774,7 @@ platforms:
|
||||
install:
|
||||
detect:
|
||||
- os: linux
|
||||
name: ROCKNIX
|
||||
method: os_release
|
||||
match: [OS_NAME=rocknix, ID=rocknix]
|
||||
bios_path: /storage/roms/bios
|
||||
|
||||
+265
-44
@@ -1007,15 +1007,25 @@ class DetectionFollowsTheRegistry(unittest.TestCase):
|
||||
registry = yaml.safe_load(
|
||||
(Path(__file__).resolve().parents[1] / "platforms" / "_registry.yml").read_text()
|
||||
)
|
||||
rules = set()
|
||||
rules = []
|
||||
for name, entry in registry["platforms"].items():
|
||||
for rule in (entry.get("install") or {}).get("detect", []):
|
||||
if rule["method"] == "os_release":
|
||||
rules.add((name, "os_release", tuple(rule["match"]), rule["bios_path"]))
|
||||
values = tuple(rule["match"])
|
||||
elif rule["method"] == "file_exists":
|
||||
files = rule["file"] if isinstance(rule["file"], list) else [rule["file"]]
|
||||
rules.add((name, "file_exists", tuple(files), rule["bios_path"]))
|
||||
self.assertEqual(set(install._EMBEDDED_RULES), rules)
|
||||
files = rule["file"]
|
||||
values = tuple(files if isinstance(files, list) else [files])
|
||||
elif rule["method"] == "file_matches":
|
||||
values = (rule["file"], rule["pattern"])
|
||||
else:
|
||||
continue
|
||||
rules.append(install._OsRule(
|
||||
"" if rule.get("installs") is False else name, rule["name"],
|
||||
rule["method"], values, rule.get("bios_path"),
|
||||
tuple(rule.get("config", ())), rule.get("home", ""),
|
||||
rule.get("derives_from", ""),
|
||||
))
|
||||
self.assertEqual(sorted(install._EMBEDDED_RULES, key=repr), sorted(rules, key=repr))
|
||||
|
||||
def test_every_config_file_read_is_published(self):
|
||||
"""The registry published five RetroArch config files and the fixed
|
||||
@@ -2758,8 +2768,9 @@ class TheInstallerPageNamesEveryEvidence(unittest.TestCase):
|
||||
def test_every_rule_is_on_the_page(self):
|
||||
page = (REPO_ROOT / "wiki" / "installer.md").read_text(encoding="utf-8")
|
||||
page = page.replace("\\\\", "\\")
|
||||
expected = [path for _p, method, values, _b in install._EMBEDDED_RULES
|
||||
if method == "file_exists" for path in values]
|
||||
expected = [path for rule in install._EMBEDDED_RULES
|
||||
if rule.method == "file_exists" for path in rule.values]
|
||||
expected += [rule.name for rule in install._EMBEDDED_RULES]
|
||||
expected += [path.replace("$HOME", "~").rstrip("/")
|
||||
for _os, _p, path, _b in install._PATH_RULES]
|
||||
missing = [path for path in expected if path not in page]
|
||||
@@ -4196,54 +4207,264 @@ class EmuDeckWindowsIsFoundAsItsScriptsFindIt(unittest.TestCase):
|
||||
install._host_path("C:/Emulation/bios"))
|
||||
|
||||
|
||||
def _device(test: unittest.TestCase, files: dict[str, str],
|
||||
release: str = "", home: str = "/root") -> Path:
|
||||
"""A fake root holding these files, read by the embedded rules."""
|
||||
root = Path(_tempdir(test))
|
||||
if release:
|
||||
files = {**files, "/etc/os-release": release}
|
||||
for name, text in files.items():
|
||||
path = root / name.lstrip("/")
|
||||
if name.endswith("/"):
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
continue
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text)
|
||||
for patcher in (
|
||||
unittest.mock.patch.object(install, "_SYSTEM_ROOT", root),
|
||||
unittest.mock.patch.dict(os.environ, {"HOME": home}),
|
||||
unittest.mock.patch.dict(install._RETROARCH_CONFIGS, clear=True),
|
||||
unittest.mock.patch.dict(install._DETECTED_OS, clear=True),
|
||||
):
|
||||
patcher.start()
|
||||
test.addCleanup(patcher.stop)
|
||||
return root
|
||||
|
||||
|
||||
class MuOsIsReadFromItsRetroArchConfig(unittest.TestCase):
|
||||
"""muOS wrote to /mnt/mmc/MUOS/bios while RetroArch reads
|
||||
/run/muos/storage/bios, bound to the second card when it holds MUOS/bios,
|
||||
and --installed-cores found no core in /opt/muos/share/core."""
|
||||
|
||||
def _muos(self, *, user_config: bool) -> tuple[Path, Path]:
|
||||
root = Path(_tempdir(self))
|
||||
muos = root / "opt" / "muos"
|
||||
system_dir, cores = root / "run" / "bios", root / "core"
|
||||
cores.mkdir(parents=True)
|
||||
configs = (muos / "share/info/config/retroarch.cfg",
|
||||
muos / "share/emulator/retroarch/retroarch.default.cfg")
|
||||
default = configs[1]
|
||||
default.parent.mkdir(parents=True)
|
||||
default.write_text(f'libretro_directory = "{cores}"\n'
|
||||
f'system_directory = "{root / "default-bios"}"\n')
|
||||
if user_config:
|
||||
configs[0].parent.mkdir(parents=True)
|
||||
configs[0].write_text(f'libretro_directory = "{cores}"\n'
|
||||
f'system_directory = "{system_dir}"\n')
|
||||
for patcher in (
|
||||
unittest.mock.patch.object(install, "MUOS_ROOT", muos),
|
||||
unittest.mock.patch.object(install, "MUOS_RETROARCH_CONFIGS", configs),
|
||||
unittest.mock.patch.object(
|
||||
install, "_detect_embedded",
|
||||
lambda: [("retroarch", Path("/run/muos/storage/bios"))]),
|
||||
unittest.mock.patch.dict(os.environ, {"HOME": str(root / "home")}),
|
||||
unittest.mock.patch.dict(install._RETROARCH_CONFIGS, clear=True),
|
||||
):
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
return (system_dir if user_config else root / "default-bios"), cores
|
||||
USER = "/opt/muos/share/info/config/retroarch.cfg"
|
||||
DEFAULT = "/opt/muos/share/emulator/retroarch/retroarch.default.cfg"
|
||||
|
||||
def _muos(self, files: dict[str, str]) -> Path:
|
||||
root = _device(self, {"/opt/muos/": "", **files})
|
||||
found = install._detect_embedded()
|
||||
self.assertEqual(len(found), 1)
|
||||
self.assertEqual(install.detected_os(*found[0]), "muOS")
|
||||
return root
|
||||
|
||||
def test_the_users_config_decides(self):
|
||||
system_dir, cores = self._muos(user_config=True)
|
||||
found = install.detect_platforms("linux")
|
||||
self.assertIn(("retroarch", system_dir), found)
|
||||
self.assertNotIn(("retroarch", Path("/run/muos/storage/bios")), found)
|
||||
self.assertIn(cores, install.core_dirs("retroarch", system_dir))
|
||||
root = self._muos({
|
||||
self.USER: 'libretro_directory = "/opt/muos/share/core"\n'
|
||||
'system_directory = "/mnt/sdcard/MUOS/bios"\n',
|
||||
self.DEFAULT: 'system_directory = "/run/muos/storage/bios"\n',
|
||||
})
|
||||
self.assertEqual(install._detect_embedded(),
|
||||
[("retroarch", root / "mnt/sdcard/MUOS/bios")])
|
||||
self.assertIn(Path("/opt/muos/share/core"),
|
||||
install.core_dirs("retroarch", root / "mnt/sdcard/MUOS/bios"))
|
||||
|
||||
def test_before_the_first_launch_the_default_does(self):
|
||||
system_dir, cores = self._muos(user_config=False)
|
||||
self.assertIn(("retroarch", system_dir), install.detect_platforms("linux"))
|
||||
self.assertIn(cores, install.core_dirs("retroarch", system_dir))
|
||||
root = self._muos({self.DEFAULT: 'system_directory = "/run/muos/storage/bios"\n'})
|
||||
self.assertEqual(install._detect_embedded(),
|
||||
[("retroarch", root / "run/muos/storage/bios")])
|
||||
|
||||
def test_without_a_config_the_bound_folder_is_used(self):
|
||||
rule = next(r for r in install._EMBEDDED_RULES if r[2] == ("/opt/muos",))
|
||||
self.assertEqual(rule[3], "/run/muos/storage/bios")
|
||||
root = self._muos({})
|
||||
self.assertEqual(install._detect_embedded(),
|
||||
[("retroarch", root / "run/muos/storage/bios")])
|
||||
|
||||
|
||||
class HandheldSystemsByTheirOwnFiles(unittest.TestCase):
|
||||
"""AmberELEC, EmuELEC, JELOS, ArkOS on a second card and a dozen other
|
||||
handheld systems were not detected, or got /roms/bios whatever their
|
||||
RetroArch read: each is found by what its own build writes, and its
|
||||
folder is the system_directory of its RetroArch config."""
|
||||
|
||||
ELEC_CFG = "/storage/.config/retroarch/retroarch.cfg"
|
||||
ARK_CFG = "/home/ark/.config/retroarch/retroarch.cfg"
|
||||
TITLE = "/usr/share/plymouth/themes/text.plymouth"
|
||||
|
||||
def _found(self, files: dict, release: str = "", home: str = "/root"):
|
||||
root = _device(self, files, release, home)
|
||||
with contextlib.redirect_stderr(io.StringIO()) as err:
|
||||
found = install._detect_embedded()
|
||||
names = [install.detected_os(plat, path) for plat, path in found]
|
||||
return root, found, names, err.getvalue()
|
||||
|
||||
def _one(self, files: dict, release: str = "", home: str = "/root"):
|
||||
root, found, names, _err = self._found(files, release, home)
|
||||
self.assertEqual(len(found), 1, found)
|
||||
return root, found[0], names[0]
|
||||
|
||||
def _elec(self, release: str, name: str, extra: dict | None = None,
|
||||
plat: str = "retroarch"):
|
||||
cfg = {self.ELEC_CFG: 'system_directory = "~/roms/bios"\n'}
|
||||
root, (found_plat, path), found_name = self._one({**cfg, **(extra or {})}, release)
|
||||
self.assertEqual((found_plat, path, found_name),
|
||||
(plat, root / "storage/roms/bios", name))
|
||||
|
||||
def test_amberelec(self):
|
||||
self._elec('NAME="AmberELEC"\nID="coreelec"\n', "AmberELEC",
|
||||
plat=install._STANDIN_MANIFESTS["AmberELEC"])
|
||||
|
||||
def test_chaos_names_itself_amberelec(self):
|
||||
# tech4bot/AmberELEC writes the same os-release as AmberELEC.
|
||||
self._elec('NAME="AmberELEC"\nID="coreelec"\nCOREELEC_DEVICE="RG552"\n', "AmberELEC")
|
||||
|
||||
def test_pan4elec_over_amberelec(self):
|
||||
self._elec('NAME="AmberELEC"\n', "PAN4ELEC",
|
||||
{"/usr/bin/batocera-info": 'echo "OPERATING SYSTEM: PAN4ELEC"\n'})
|
||||
|
||||
def test_emuelec(self):
|
||||
self._elec('NAME="EmuELEC"\nID="emuelec"\n', "EmuELEC",
|
||||
plat=install._STANDIN_MANIFESTS["EmuELEC"])
|
||||
|
||||
def test_jelos(self):
|
||||
self._elec('OS_NAME="JELOS"\nHW_DEVICE="RK3566"\n', "JELOS")
|
||||
|
||||
def test_kolos_over_jelos(self):
|
||||
self._elec('OS_NAME="JELOS"\n', "KOLOS",
|
||||
{"/usr/lib/systemd/system/jelos-ready.service":
|
||||
"Description=KOLOS system ready script\n"})
|
||||
|
||||
def test_unofficialos(self):
|
||||
self._elec('OS_NAME="UnofficialOS"\n', "UnofficialOS")
|
||||
|
||||
def test_archr(self):
|
||||
self._elec('NAME="ArchR"\nID="archr"\nOS_NAME="ArchR"\n', "ArchR")
|
||||
|
||||
def test_an_elec_config_moved_elsewhere_is_followed(self):
|
||||
root, (_plat, path), _name = self._one(
|
||||
{self.ELEC_CFG: 'system_directory = "/storage/roms2/bios"\n'}, 'OS_NAME="JELOS"\n')
|
||||
self.assertEqual(path, root / "storage/roms2/bios")
|
||||
|
||||
def test_arkos_first_and_second_card(self):
|
||||
base = {"/home/ark/": "", "/opt/system/": "",
|
||||
self.TITLE: "[ubuntu-text]\ntitle=ArkOS 2.0 (02012025)-3\n"}
|
||||
for folder in ("/roms/bios", "/roms2/bios"):
|
||||
with self.subTest(folder=folder):
|
||||
root, found, name = self._one(
|
||||
{**base, self.ARK_CFG: f'system_directory = "{folder}"\n'})
|
||||
self.assertEqual((found, name),
|
||||
(("retroarch", root / folder.lstrip("/")), "ArkOS"))
|
||||
|
||||
def test_arkos_32_bit_config_when_the_64_bit_one_is_missing(self):
|
||||
root, found, _name = self._one({
|
||||
"/home/ark/": "", "/opt/system/": "",
|
||||
"/home/ark/.config/retroarch32/retroarch.cfg": 'system_directory = "/roms2/bios"\n',
|
||||
})
|
||||
self.assertEqual(found, ("retroarch", root / "roms2/bios"))
|
||||
|
||||
def test_darkos_first_and_second_card(self):
|
||||
base = {"/home/ark/": "", "/opt/system/": "",
|
||||
self.TITLE: "title=dArkOS (05102026)\n"}
|
||||
for folder in ("/roms/bios", "/roms2/bios"):
|
||||
with self.subTest(folder=folder):
|
||||
root, found, name = self._one(
|
||||
{**base, self.ARK_CFG: f'system_directory = "{folder}"\n'})
|
||||
self.assertEqual((found, name),
|
||||
(("retroarch", root / folder.lstrip("/")), "dArkOS"))
|
||||
|
||||
def test_darkos_without_a_config(self):
|
||||
root, found, name = self._one({"/home/ark/": "", "/opt/system/": "",
|
||||
self.TITLE: "title=dArkOS (05102026)\n"})
|
||||
self.assertEqual((found, name), (("retroarch", root / "roms/bios"), "dArkOS"))
|
||||
|
||||
def test_retrooz(self):
|
||||
cfg = "/home/odroid/.config/retroarch32/retroarch.cfg"
|
||||
root, found, name = self._one({
|
||||
"/opt/.retrooz/version": "0.60\n", self.TITLE: "[ubuntu-text]\ntitle=RetroOZ\n",
|
||||
cfg: 'system_directory = "/roms/bios"\n',
|
||||
})
|
||||
self.assertEqual((found, name), (("retroarch", root / "roms/bios"), "RetroOZ"))
|
||||
root, found, name = self._one({"/etc/retrooz_version": "1\n"})
|
||||
self.assertEqual((found, name), (("retroarch", root / "roms/bios"), "RetroOZ"))
|
||||
|
||||
def test_thera_is_not_detected(self):
|
||||
_root, found, _names, _err = self._found({self.TITLE: "title= *TheRA RC 1*\n"})
|
||||
self.assertEqual(found, [])
|
||||
|
||||
def test_spruceos_on_a_trimui(self):
|
||||
root, found, name = self._one({
|
||||
"/usr/trimui/": "", "/mnt/SDCARD/spruce/": "",
|
||||
"/mnt/SDCARD/Saves/ra-configs/retroarch-Brick.cfg":
|
||||
'system_directory = "/mnt/SDCARD/BIOS"\n',
|
||||
})
|
||||
self.assertEqual((found, name), (("retroarch", root / "mnt/SDCARD/BIOS"), "spruceOS"))
|
||||
|
||||
def test_crossmix_os(self):
|
||||
root, found, name = self._one({
|
||||
"/usr/trimui/": "", "/mnt/SDCARD/System/usr/trimui/crossmix-version.txt": "1.3\n",
|
||||
"/mnt/SDCARD/RetroArch/retroarch.cfg": 'system_directory = "/mnt/SDCARD/BIOS"\n',
|
||||
})
|
||||
self.assertEqual((found, name), (("retroarch", root / "mnt/SDCARD/BIOS"), "CrossMix-OS"))
|
||||
|
||||
def _miyoo_mini(self, marker: str, name: str):
|
||||
root, found, found_name = self._one({
|
||||
marker: "x\n",
|
||||
"/mnt/SDCARD/RetroArch/.retroarch/retroarch.cfg":
|
||||
'system_directory = "/mnt/SDCARD/BIOS"\n',
|
||||
})
|
||||
self.assertEqual((found, found_name), (("retroarch", root / "mnt/SDCARD/BIOS"), name))
|
||||
|
||||
def test_onion(self):
|
||||
self._miyoo_mini("/mnt/SDCARD/.tmp_update/onionVersion/version.txt", "Onion")
|
||||
|
||||
def test_allium(self):
|
||||
self._miyoo_mini("/mnt/SDCARD/.allium/version.txt", "Allium")
|
||||
|
||||
def test_koriki(self):
|
||||
self._miyoo_mini("/mnt/SDCARD/Koriki/version.txt", "Koriki")
|
||||
|
||||
def test_miyoocfw_default_and_config(self):
|
||||
base = {"/etc/main": "export HOME=/mnt\n", "/usr/bin/miyooctl": ""}
|
||||
root, found, name = self._one(base)
|
||||
self.assertEqual((found, name),
|
||||
(("retroarch", root / "mnt/.retroarch/system"), "MiyooCFW"))
|
||||
root, found, _name = self._one(
|
||||
{**base, "/mnt/.retroarch/retroarch.cfg": 'system_directory = "~/bios"\n'})
|
||||
self.assertEqual(found, ("retroarch", root / "mnt/bios"))
|
||||
|
||||
def test_plumos(self):
|
||||
root, found, name = self._one({"/mnt/plumos-user/": ""}, 'NAME="plumOS"\nID=plumos\n')
|
||||
self.assertEqual((found, name), (("retroarch", root / "mnt/plumos-user/bios"), "plumOS"))
|
||||
_root, found, _names, err = self._found({}, 'NAME="plumOS"\nID=plumos\n')
|
||||
self.assertEqual(found, [])
|
||||
self.assertIn("plumOS found here", err)
|
||||
|
||||
def test_trimui_stock_and_minui_are_named_not_installed_to(self):
|
||||
_root, found, _names, err = self._found({"/usr/trimui/": ""})
|
||||
self.assertEqual(found, [])
|
||||
self.assertIn("TrimUI found here", err)
|
||||
_root, found, _names, err = self._found(
|
||||
{"/usr/trimui/": "", "/mnt/SDCARD/.system/res/": ""})
|
||||
self.assertEqual(found, [])
|
||||
self.assertIn("MinUI found here", err)
|
||||
self.assertNotIn("TrimUI", err)
|
||||
|
||||
def test_two_unrelated_systems_are_refused(self):
|
||||
_root, found, names, _err = self._found(
|
||||
{"/mnt/SDCARD/Koriki/version.txt": "1\n"}, 'OS_NAME="JELOS"\n')
|
||||
self.assertEqual(names, ["JELOS", "Koriki"])
|
||||
self.assertEqual(install.os_conflict(found), ["JELOS", "Koriki"])
|
||||
with unittest.mock.patch.object(sys, "argv", ["install.py", "--no-input"]), \
|
||||
unittest.mock.patch.object(install, "detect_os", return_value="linux"), \
|
||||
unittest.mock.patch.object(install, "detect_platforms", return_value=found), \
|
||||
unittest.mock.patch.object(install, "detect_frontends", return_value=[]), \
|
||||
contextlib.redirect_stdout(io.StringIO()), \
|
||||
contextlib.redirect_stderr(io.StringIO()) as err, \
|
||||
self.assertRaises(SystemExit) as stop:
|
||||
install.main()
|
||||
self.assertEqual(stop.exception.code, 1)
|
||||
self.assertIn("JELOS and Koriki are both detected here", err.getvalue())
|
||||
|
||||
def test_the_system_is_announced_by_its_own_name(self):
|
||||
root, found, _name = self._one({self.ELEC_CFG: ""}, 'NAME="AmberELEC"\n')
|
||||
self.assertEqual(install.found_label(*found),
|
||||
f"AmberELEC at {root / 'storage/roms/bios'} (RetroArch files)")
|
||||
|
||||
def test_list_platforms_names_every_system(self):
|
||||
with unittest.mock.patch.object(sys, "argv", ["install.py", "--list-platforms"]), \
|
||||
unittest.mock.patch.object(install, "detect_os", return_value="linux"), \
|
||||
unittest.mock.patch.object(install, "detect_platforms", return_value=[]), \
|
||||
contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
install.main()
|
||||
for rule in install._EMBEDDED_RULES:
|
||||
self.assertIn(f" {rule.name}: ", out.getvalue())
|
||||
|
||||
|
||||
class BatoceraAndKnulliByWhatTheirBuildsWrite(unittest.TestCase):
|
||||
|
||||
+49
-14
@@ -283,21 +283,56 @@ Files the list does not name are never read, moved or deleted.
|
||||
|
||||
## Platform detection
|
||||
|
||||
Detection reads what the platform itself writes, never a guess from a directory
|
||||
name. The embedded systems are tested in the order below and the first match
|
||||
wins, since a machine is only one of them.
|
||||
Detection reads what the system itself writes, never a guess from a directory
|
||||
name. A machine runs one embedded system, so every rule below is tested. A
|
||||
system built on another one (KOLOS on JELOS, dArkOS on ArkOS, spruceOS on a
|
||||
TrimUI) matches both and is the one meant. Two unrelated systems matching
|
||||
are both named: at a terminal the run asks which one, otherwise it stops and
|
||||
asks for `--dest`. The run names the system as it names itself, with the file
|
||||
list it gets: `Found AmberELEC at /storage/roms/bios (RetroArch files)`.
|
||||
`--list-platforms` lists every system below.
|
||||
|
||||
| Evidence | Platform | BIOS directory |
|
||||
|----------|----------|----------------|
|
||||
| `/etc/os-release` `OS_NAME=ROCKNIX` (or `ID=rocknix` on images before 2025) | ROCKNIX | `/storage/roms/bios` |
|
||||
| `/etc/os-release` `OS_NAME=knulli` or `/usr/share/knulli/knulli.version` | Batocera (Knulli) | `/userdata/bios` |
|
||||
| `/etc/os-release` `ID=lakka` | Lakka | `/storage/system` |
|
||||
| `/usr/share/batocera/batocera.version` | Batocera | `/userdata/bios` |
|
||||
| `/recalbox/recalbox.version` or `/usr/bin/recalbox-settings` | Recalbox | `/recalbox/share/bios` |
|
||||
| `/opt/muos` | RetroArch (muOS) | `system_directory` of `/opt/muos/share/info/config/retroarch.cfg`, else of the default it is copied from: `/run/muos/storage/bios`, which muOS binds to `MUOS/bios` on the second card when that card holds one, else the first. Cores are read from that config's `libretro_directory` (`/opt/muos/share/core`) |
|
||||
| `/mnt/mmc/MUOS/` | RetroArch | `/mnt/mmc/MUOS/bios` |
|
||||
| `/home/ark` and `/opt/system` | RetroArch | `/roms/bios` |
|
||||
| `/mnt/vendor/bin/dmenu.bin` | RetroArch | `/mnt/mmc/bios` |
|
||||
Where a system's RetroArch config is named, its `system_directory` is the
|
||||
folder, `~` being that system's home for RetroArch. The folder in the last
|
||||
column is used when no config sets one. ArkOS and dArkOS keep two configs
|
||||
(64-bit and 32-bit RetroArch), and their "Switch to SD2 for Roms" script
|
||||
rewrites both to `/roms2/bios`: a second card is followed through them.
|
||||
|
||||
| Evidence | System | File list | BIOS directory |
|
||||
|----------|--------|-----------|----------------|
|
||||
| `/etc/os-release` `OS_NAME=ROCKNIX` (or `ID=rocknix` on images before 2025) | ROCKNIX | ROCKNIX | `/storage/roms/bios` |
|
||||
| `/etc/os-release` `OS_NAME=knulli` or `/usr/share/knulli/knulli.version` | KNULLI | Batocera | `/userdata/bios` |
|
||||
| `/etc/os-release` `ID=lakka` | Lakka | Lakka | `/storage/system` |
|
||||
| `/usr/share/batocera/batocera.version` | Batocera | Batocera | `/userdata/bios` |
|
||||
| `/recalbox/recalbox.version` or `/usr/bin/recalbox-settings` | Recalbox | Recalbox | `/recalbox/share/bios` |
|
||||
| `/opt/muos` | muOS | RetroArch | `system_directory` of `/opt/muos/share/info/config/retroarch.cfg`, else of the default it is copied from: `/run/muos/storage/bios`, which muOS binds to `MUOS/bios` on the second card when that card holds one, else the first. Cores are read from that config's `libretro_directory` (`/opt/muos/share/core`) |
|
||||
| `/mnt/mmc/MUOS/` | muOS | RetroArch | `/mnt/mmc/MUOS/bios` |
|
||||
| `/etc/os-release` `NAME=AmberELEC` (CHAos writes the same) | AmberELEC | RetroArch | `/storage/.config/retroarch/retroarch.cfg`, else `/storage/roms/bios` |
|
||||
| `/usr/bin/batocera-info` naming PAN4ELEC, on AmberELEC | PAN4ELEC | RetroArch | as AmberELEC |
|
||||
| `/etc/os-release` `NAME=EmuELEC` | EmuELEC | RetroArch | as AmberELEC |
|
||||
| `/etc/os-release` `OS_NAME=JELOS` | JELOS | RetroArch | as AmberELEC |
|
||||
| `/usr/lib/systemd/system/jelos-ready.service`, on JELOS | KOLOS | RetroArch | as AmberELEC |
|
||||
| `/etc/os-release` `OS_NAME=UnofficialOS` | UnofficialOS | RetroArch | as AmberELEC |
|
||||
| `/etc/os-release` `OS_NAME=ArchR` | ArchR | RetroArch | as AmberELEC |
|
||||
| `/home/ark` and `/opt/system`, or `title=ArkOS` in `/usr/share/plymouth/themes/text.plymouth` | ArkOS | RetroArch | `/home/ark/.config/retroarch/retroarch.cfg` then `retroarch32/retroarch.cfg`, else `/roms/bios` |
|
||||
| `title=dArkOS` in the same file (the dArkOSRE, dArkOSen and ArkOS4Clone images keep it) | dArkOS | RetroArch | as ArkOS |
|
||||
| `title=RetroOZ` in the same file, `/opt/.retrooz` or `/etc/retrooz_version` | RetroOZ | RetroArch | `/home/odroid/.config/retroarch/retroarch.cfg` then `retroarch32/retroarch.cfg`, else `/roms/bios` |
|
||||
| `/mnt/SDCARD/spruce` | spruceOS | RetroArch | `/mnt/SDCARD/Saves/ra-configs/retroarch-*.cfg`, else `/mnt/SDCARD/BIOS` |
|
||||
| `/mnt/SDCARD/System/usr/trimui/crossmix-version.txt` | CrossMix-OS | RetroArch | `/mnt/SDCARD/RetroArch/retroarch.cfg`, else `/mnt/SDCARD/BIOS` |
|
||||
| `/mnt/SDCARD/.tmp_update/onionVersion/version.txt` | Onion | RetroArch | `/mnt/SDCARD/RetroArch/.retroarch/retroarch.cfg`, else `/mnt/SDCARD/BIOS` |
|
||||
| `/mnt/SDCARD/.allium` | Allium | RetroArch | as Onion |
|
||||
| `/mnt/SDCARD/Koriki/version.txt` | Koriki | RetroArch | as Onion |
|
||||
| `/etc/main` and `/usr/bin/miyooctl` | MiyooCFW | RetroArch | `/mnt/.retroarch/retroarch.cfg`, else `/mnt/.retroarch/system` |
|
||||
| `/mnt/plumos-user` | plumOS | RetroArch | `/mnt/plumos-user/bios`, which its launcher appends to every RetroArch run |
|
||||
| `/etc/os-release` `ID=plumos` without the folder above | plumOS | none | its builds choose a folder per launch: named, not installed to |
|
||||
| `/usr/trimui` | TrimUI | none | no published source names the stock firmware's folder: named, not installed to |
|
||||
| `/mnt/SDCARD/.system/res` or `/mnt/sdcard/.system/res` | MinUI | none | MinUI, MyMinUI and NextUI give each core its own `Bios/<tag>/`, a layout not written yet: named, not installed to |
|
||||
| `/mnt/vendor/bin/dmenu.bin` | Anbernic stock firmware | RetroArch | `/mnt/mmc/bios` |
|
||||
|
||||
AmberELEC, EmuELEC and KNULLI get the file list of the platform whose cores
|
||||
they ship until a list of their own is published. TheRA is not detected: no
|
||||
image or source of its Odroid Go builds is published, so neither its marker
|
||||
files nor its folder could be read.
|
||||
|
||||
Batocera, Recalbox, ROCKNIX and Lakka also name the board they were built for, and
|
||||
that board narrows the install unless `--target` or `--every-board` says
|
||||
|
||||
Reference in new issue
Block a user