mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-21 03:09:08 -05:00
The scraper reads the EmuScripts at the commit checkBIOS.sh is pinned to: the BIOS paths changeLine, iniFieldUpdate and RetroArch_setConfigOverride write, the ares and melonDS configs whose paths sed rewrites, and the bios/ directories linked to Flycast, Azahar, the Switch emulators and shadPS4. The script to emulator mapping moves beside the extractors so both read one table.
389 lines
14 KiB
Python
389 lines
14 KiB
Python
"""EmuDeck's launcher layer, read from its EmuScripts at the pinned commit.
|
|
|
|
Each functions/EmuScripts/emuDeck<Name>.sh sets an emulator up once, at
|
|
install or update: it writes BIOS paths into the emulator's config with
|
|
changeLine, iniFieldUpdate or RetroArch_setConfigOverride, rewrites the
|
|
paths of a config it ships with sed, or links a directory of bios/ to the
|
|
emulator's own. Nothing runs between that setup and a launch, so these are
|
|
the names and directories the emulator is left with.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import posixpath
|
|
import re
|
|
from collections.abc import Callable
|
|
|
|
try:
|
|
from .batocera_launchers import Launcher
|
|
from .shell_launchers import (
|
|
LauncherPatternMissing,
|
|
RepoTree,
|
|
Script,
|
|
describe_keys,
|
|
settings,
|
|
source_ref,
|
|
words,
|
|
)
|
|
except ImportError:
|
|
from batocera_launchers import Launcher
|
|
from shell_launchers import (
|
|
LauncherPatternMissing,
|
|
RepoTree,
|
|
Script,
|
|
describe_keys,
|
|
settings,
|
|
source_ref,
|
|
words,
|
|
)
|
|
|
|
EMUSCRIPTS = "functions/EmuScripts"
|
|
CONFIGS = "configs"
|
|
BIOS = "${biosPath}"
|
|
# EmuDeck's settings put the BIOS directory under the Emulation folder.
|
|
EMULATION_BIOS = "${emulationPath}/bios"
|
|
STORAGE = "${storagePath}"
|
|
FLATPAK_DATA = "${HOME}/.var/app/"
|
|
|
|
# EmuScripts file names that differ from the emulator profile they set up.
|
|
SCRIPT_EMULATORS = {
|
|
"pcsx2qt": "pcsx2",
|
|
"rpcs3legacy": "rpcs3",
|
|
"cemuproton": "cemu",
|
|
"rmg": "mupen64plus_next",
|
|
}
|
|
# The main RetroArch config script installs no emulator.
|
|
NOT_EMULATORS = {"retroarch_maincfg"}
|
|
|
|
# What the emulator does with a directory written into each key.
|
|
DIRECTORY_KINDS = {
|
|
"SearchDirectory": "scans_dir",
|
|
"Bios": "scans_dir",
|
|
"rompath": "scans_dir",
|
|
"system_directory": "passes_path",
|
|
}
|
|
|
|
|
|
def emulator_of_script(file_name: str) -> str | None:
|
|
"""The emulator an EmuScripts file sets up, None for a helper script."""
|
|
if not file_name.endswith(".sh"):
|
|
return None
|
|
key = re.sub(r"^emuDeck", "", file_name[: -len(".sh")], flags=re.IGNORECASE).lower()
|
|
if not key or key in NOT_EMULATORS:
|
|
return None
|
|
return SCRIPT_EMULATORS.get(key, key)
|
|
|
|
|
|
def _script(tree: RepoTree, name: str) -> Script:
|
|
path = f"{EMUSCRIPTS}/{name}"
|
|
return Script(path, tree.read(path))
|
|
|
|
|
|
def _emulator(name: str) -> str:
|
|
emulator = emulator_of_script(name)
|
|
if emulator is None:
|
|
raise LauncherPatternMissing(f"{EMUSCRIPTS}/{name}", "emulator script")
|
|
return emulator
|
|
|
|
|
|
def _is_bios(value: str) -> bool:
|
|
return any(value == root or value.startswith(root + "/") for root in (BIOS, EMULATION_BIOS))
|
|
|
|
|
|
def _repo_config(script: Script, path: str) -> str:
|
|
"""The repository copy of a Flatpak config EmuDeck installs."""
|
|
if not path.startswith(FLATPAK_DATA):
|
|
raise script.missing(f"repository copy of {path}")
|
|
return f"{CONFIGS}/{path[len(FLATPAK_DATA):]}"
|
|
|
|
|
|
def setters(script: Script) -> list[tuple[str, str, str, str, set[int]]]:
|
|
"""(config file, section, key, value, lines) for each path setter call.
|
|
|
|
Reads changeLine "$KEY" "$KEY$VALUE" "$FILE", iniFieldUpdate "$FILE"
|
|
"Section" "Key" "Value" and RetroArch_setConfigOverride 'key' "value" "$FILE".
|
|
"""
|
|
found = []
|
|
for number, line in script.code():
|
|
command = words(line)
|
|
if not command:
|
|
continue
|
|
used = {number}
|
|
args = [script.expand(w.text, number, used) for w in command[1:]]
|
|
name = command[0].raw
|
|
if name == "changeLine" and len(args) == 3:
|
|
key, value, file = args
|
|
if not value.startswith(key):
|
|
if BIOS in value:
|
|
raise script.missing(f"changeLine value starting with its key at line {number}")
|
|
continue
|
|
value = value[len(key) :].strip()
|
|
if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"":
|
|
value = value[1:-1]
|
|
found.append((file, "", key.strip().rstrip("=").strip(), value, used))
|
|
elif name == "iniFieldUpdate" and len(args) == 4:
|
|
file, section, key, value = args
|
|
found.append((file, section, key, value, used))
|
|
elif name == "RetroArch_setConfigOverride" and len(args) == 3:
|
|
key, value, file = args
|
|
found.append((file, "", key, value.strip('"'), used))
|
|
return found
|
|
|
|
|
|
def setter_records(tree: RepoTree, name: str, option_key: str | None = None) -> list[Launcher]:
|
|
"""Records for the paths a script's setters write under the BIOS or storage directory.
|
|
|
|
A file named under bios/ or storage/ is a fixed name, grouped by config
|
|
and directory. A bios directory is handed to the emulator, which picks.
|
|
"""
|
|
script = _script(tree, name)
|
|
emulator = _emulator(name)
|
|
records: list[Launcher] = []
|
|
groups: dict[tuple[str, str], tuple[list[tuple[str, str]], list[str], set[int]]] = {}
|
|
for file, section, key, value, lines in setters(script):
|
|
config = posixpath.basename(file)
|
|
paths = value.split(";")
|
|
if not any(_is_bios(p) for p in paths) and not value.startswith(STORAGE + "/"):
|
|
continue
|
|
is_file = len(paths) == 1 and re.search(r"\.[A-Za-z0-9]+$", posixpath.basename(value))
|
|
if is_file:
|
|
folder = posixpath.dirname(value)
|
|
keys, names, refs = groups.setdefault((config, folder), ([], [], set()))
|
|
keys.append((section, key))
|
|
names.append(posixpath.basename(value))
|
|
refs |= lines
|
|
continue
|
|
if not any(_is_bios(p) for p in paths):
|
|
continue
|
|
if key not in DIRECTORY_KINDS:
|
|
raise script.missing(f"known directory key, got {key}")
|
|
records.append(
|
|
Launcher(
|
|
emulator=emulator,
|
|
build="libretro" if emulator == "retroarch" else "standalone",
|
|
kind=DIRECTORY_KINDS[key],
|
|
names=[],
|
|
source_ref=source_ref([(script.path, lines)]),
|
|
search_dir=value,
|
|
passes_to=describe_keys(config, ((section, key),)),
|
|
)
|
|
)
|
|
for (config, folder), (keys, names, refs) in groups.items():
|
|
records.append(
|
|
Launcher(
|
|
emulator=emulator,
|
|
build="standalone",
|
|
kind="fixed_name",
|
|
names=names,
|
|
source_ref=source_ref([(script.path, refs)]),
|
|
search_dir=folder,
|
|
passes_to=describe_keys(config, tuple(keys)),
|
|
)
|
|
)
|
|
if not records:
|
|
raise script.missing(f"setter writing a path under {BIOS}")
|
|
if option_key:
|
|
for record in records:
|
|
_add_option(tree, script, record, option_key)
|
|
return records
|
|
|
|
|
|
def _add_option(tree: RepoTree, script: Script, record: Launcher, option_key: str) -> None:
|
|
"""depends_on_option from the value the shipped config gives option_key."""
|
|
config = record.passes_to.split(" ", 1)[0]
|
|
file_var = script.assignment_ending("/" + config)
|
|
if file_var is None:
|
|
raise script.missing(f"config variable for {config}")
|
|
path = _repo_config(script, script.expand(file_var.value, file_var.line, set()))
|
|
option = next((s for s in settings(tree.read(path)) if s.key == option_key), None)
|
|
if option is None:
|
|
raise LauncherPatternMissing(path, option_key)
|
|
record.depends_on_option = f"{option_key} = {option.value} in the shipped {config}"
|
|
record.source_ref += f", {source_ref([(path, {option.line})])}"
|
|
|
|
|
|
def sed_rewrites(script: Script, function: str) -> list[tuple[str, str, str, set[int]]]:
|
|
"""(config path, old prefix, new prefix, lines) for sed -i "s|old|new|g" "$file" in a function."""
|
|
start, _ = script.find(rf"^\s*{re.escape(function)}\s*\(\)\s*\{{", f"function {function}")
|
|
end = next(
|
|
(n for n, line in script.code() if n > start and re.match(r"^\}", line)), len(script.lines)
|
|
)
|
|
found = []
|
|
for number, line in script.code():
|
|
if not start < number < end:
|
|
continue
|
|
command = words(line)
|
|
if len(command) == 4 and command[0].raw == "sed" and command[1].raw == "-i":
|
|
pattern = re.fullmatch(r"s\|([^|]*)\|([^|]*)\|g", command[2].text)
|
|
if pattern is None:
|
|
continue
|
|
used: set[int] = {number}
|
|
old, new, file = (
|
|
script.expand(text, number, used).replace("\\/", "/")
|
|
for text in (pattern.group(1), pattern.group(2), command[3].text)
|
|
)
|
|
found.append((file, old, new, used))
|
|
return found
|
|
|
|
|
|
def _rewritten(value: str, old: str, new: str) -> str | None:
|
|
if not value.startswith(old):
|
|
return None
|
|
rest = value[len(old) :]
|
|
return new.rstrip("/") + "/" + rest.lstrip("/")
|
|
|
|
|
|
def ares(tree: RepoTree) -> list[Launcher]:
|
|
"""settings.bml Firmware keys, their /home/deck/Emulation/bios/ prefix rewritten."""
|
|
name = "emuDeckares.sh"
|
|
script = _script(tree, name)
|
|
rewrites = [
|
|
r for r in sed_rewrites(script, "ares_setEmulationFolder") if _is_bios(r[2].rstrip("/"))
|
|
]
|
|
if not rewrites:
|
|
raise script.missing("sed rewrite of the BIOS path")
|
|
file, old, new, script_lines = rewrites[0]
|
|
path = _repo_config(script, file)
|
|
records = []
|
|
system, in_firmware = "", False
|
|
entries: dict[str, list[tuple[str, str, int]]] = {}
|
|
for number, line in enumerate(tree.read(path).splitlines(), 1):
|
|
indent = len(line) - len(line.lstrip(" "))
|
|
text = line.strip()
|
|
if indent == 0 and text:
|
|
system, in_firmware = text, False
|
|
elif indent == 2:
|
|
in_firmware = text == "Firmware"
|
|
elif indent == 4 and in_firmware and ":" in text:
|
|
key, value = (part.strip() for part in text.split(":", 1))
|
|
target = _rewritten(value, old, new)
|
|
if target is not None:
|
|
entries.setdefault(system, []).append((key, target, number))
|
|
if not entries:
|
|
raise LauncherPatternMissing(path, f"Firmware path under {old}")
|
|
for system, found in entries.items():
|
|
folder = posixpath.commonpath([posixpath.dirname(t) for _, t, _ in found])
|
|
records.append(
|
|
Launcher(
|
|
emulator="ares",
|
|
build="standalone",
|
|
kind="fixed_name",
|
|
names=[posixpath.relpath(t, folder) for _, t, _ in found],
|
|
source_ref=source_ref(
|
|
[(path, {n for _, _, n in found}), (script.path, script_lines)]
|
|
),
|
|
slot=system,
|
|
search_dir=folder,
|
|
passes_to="settings.bml "
|
|
+ ", ".join(f"{system}/Firmware/{k}" for k, _, _ in found),
|
|
)
|
|
)
|
|
return records
|
|
|
|
|
|
def melonds(tree: RepoTree) -> list[Launcher]:
|
|
"""melonDS.ini paths set by changeLine, melonDS.toml paths shipped and rewritten by sed."""
|
|
name = "emuDeckMelonDS.sh"
|
|
records = setter_records(tree, name, option_key="ExternalBIOSEnable")
|
|
script = _script(tree, name)
|
|
# A config whose BIOS keys the setters write again keeps their values.
|
|
set_by_setters = {file for file, _, _, value, _ in setters(script) if _is_bios(value)}
|
|
for file, old, new, sed_lines in sed_rewrites(script, "melonDS_init"):
|
|
if file in set_by_setters:
|
|
continue
|
|
path = _repo_config(script, file)
|
|
found = []
|
|
for setting in settings(tree.read(path)):
|
|
target = _rewritten(setting.value, old, new)
|
|
if target is not None and _is_bios(posixpath.dirname(target)):
|
|
found.append((setting, target))
|
|
if not found:
|
|
continue
|
|
folder = posixpath.commonpath([posixpath.dirname(t) for _, t in found])
|
|
config = posixpath.basename(path)
|
|
option = next((s for s in settings(tree.read(path)) if s.key == "ExternalBIOSEnable"), None)
|
|
if option is None:
|
|
raise LauncherPatternMissing(path, "ExternalBIOSEnable")
|
|
record = Launcher(
|
|
emulator="melonds",
|
|
build="standalone",
|
|
kind="fixed_name",
|
|
names=[posixpath.relpath(t, folder) for _, t in found],
|
|
source_ref=source_ref(
|
|
[(path, {option.line, *(s.line for s, _ in found)}), (script.path, sed_lines)]
|
|
),
|
|
search_dir=folder,
|
|
passes_to=describe_keys(config, tuple((s.section, s.key) for s, _ in found)),
|
|
depends_on_option=f"ExternalBIOSEnable = {option.value} in the shipped {config}",
|
|
)
|
|
records.append(record)
|
|
if len(records) < 2:
|
|
raise script.missing("melonDS.toml BIOS paths rewritten by sed")
|
|
return records
|
|
|
|
|
|
# Scripts whose setters write a BIOS path or directory into the emulator config.
|
|
SETTER_SCRIPTS = (
|
|
"emuDeckRMG.sh",
|
|
"emuDeckXemu.sh",
|
|
"emuDeckDuckStation.sh",
|
|
"emuDeckPCSX2QT.sh",
|
|
"emuDeckARMSX2.sh",
|
|
"emuDeckMAME.sh",
|
|
"emuDeckRetroArch.sh",
|
|
)
|
|
|
|
# Scripts that link a directory of bios/ to one the emulator reads.
|
|
LINK_SCRIPTS = (
|
|
"emuDeckFlycast.sh",
|
|
"emuDeckAzahar.sh",
|
|
"emuDeckCitron.sh",
|
|
"emuDeckEden.sh",
|
|
"emuDeckSuyu.sh",
|
|
"emuDeckYuzu.sh",
|
|
"emuDeckRyujinx.sh",
|
|
"emuDeckShadPS4.sh",
|
|
)
|
|
|
|
|
|
def configured(tree: RepoTree) -> list[Launcher]:
|
|
return [record for name in SETTER_SCRIPTS for record in setter_records(tree, name)]
|
|
|
|
|
|
def links(tree: RepoTree) -> list[Launcher]:
|
|
records = []
|
|
for name in LINK_SCRIPTS:
|
|
script = _script(tree, name)
|
|
found = script.links(BIOS)
|
|
if not found:
|
|
raise script.missing(f"ln -s of a path under {BIOS}")
|
|
records.extend(
|
|
Launcher(
|
|
emulator=_emulator(name),
|
|
build="standalone",
|
|
kind="links",
|
|
names=list(link.names),
|
|
source_ref=source_ref([(script.path, link.lines)]),
|
|
expands=link.expands or None,
|
|
search_dir=link.bios_side,
|
|
passes_to=f"symlink {link.link} -> {link.target}",
|
|
)
|
|
for link in found
|
|
)
|
|
return records
|
|
|
|
|
|
EXTRACTORS: tuple[Callable[[RepoTree], list[Launcher]], ...] = (
|
|
ares,
|
|
melonds,
|
|
configured,
|
|
links,
|
|
)
|
|
|
|
SOURCE_SCRIPTS = ("emuDeckares.sh", "emuDeckMelonDS.sh", *SETTER_SCRIPTS, *LINK_SCRIPTS)
|
|
|
|
|
|
def extract_launchers(tree: RepoTree) -> list[dict]:
|
|
"""Every launcher record the extractors find, in extractor order."""
|
|
return [record.entry() for extractor in EXTRACTORS for record in extractor(tree)]
|