mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-29 15:17:20 -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
+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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user