Files
libretro/scripts/scraper/emuelec_launchers.py
T
Abdessamad Derraz c5d3e85bac feat: record config installs in shell extractors
ROCKNIX, EmuELEC, AmberELEC, EmuDeck and RetroDECK records citing a
config now carry the code installing it, whether that keeps a file
already there, and the launch code rewriting its keys. Rescraped at the
pinned revisions.

Records the shipped code never reads are no longer written: EmuELEC's
mame.ini folders (MAME.opt turns paths off), AmberELEC's atari800.cfg
(read only under an option that is off), the six RetroDECK XRoar ROM
lists no machine looks up, and the ROCKNIX melonDS DSi BIOS keys
(DSiExternalBIOSEnable is set by no shipped ini).

Conditions added: ROCKNIX and AmberELEC amiberry kickstarts hold only
outside a WHDLoad --autoload boot, ROCKNIX NanoBoyAdvance exits without
its BIOS, the EmuELEC scummvm.ini copy runs from an action no launch
passes, and the EmuDeck melonDS.toml keys need ExternalBIOSEnable (DS)
or ConsoleType 1 (DSi).
2026-11-11 11:04:02 +01:00

785 lines
30 KiB
Python

"""EmuELEC's launcher layer, read from its packages at the pinned tag.
emuelecRunEmu.sh starts every game. A libretro core runs in RetroArch (or
retrorun) with /storage/roms/bios as its system directory. A standalone
emulator runs from the script the `case ${PLATFORM}` arm names for the core
ES passes (`"${EMU}" = "flycastsa"`), and that script links, moves, copies
or passes the file. Some packages ship the config the emulator reads
(duckstation, hatari, advancemame, mame), others link a BIOS folder at
install time (amiberry, timidity). The emulator of each record is the core
name the dispatch tests, or the package that installs the link.
"""
from __future__ import annotations
import posixpath
import re
from collections.abc import Callable, Iterable
from dataclasses import dataclass, field
from .configgen_launchers import Launcher
from .rocknix_launchers import fixed_paths
from .shell_launchers import (
LauncherPatternMissing,
Link,
config_placement,
evidence_fields,
RepoTree,
Script,
describe_keys,
settings,
source_ref,
words,
)
SX05RE = "packages/sx05re"
BIN = f"{SX05RE}/emuelec/bin"
RUN_EMU = f"{BIN}/emuelecRunEmu.sh"
UTILS = f"{BIN}/emuelec-utils"
EMULATORS = f"{SX05RE}/emulators"
LIBRETRO = f"{SX05RE}/libretro"
RETROARCH_MK = f"{SX05RE}/libretro_base/retroarch/package.mk"
BIOS_ROOT = "/storage/roms/bios"
# The roms root that holds it (packages/sx05re/libretro_base/retroarch/package.mk:112,117).
ROMS_ROOT = "/storage/roms"
_EMU_TEST = re.compile(r'\$\{?EMU\}?"?\s*==?\s*"([^"]+)"')
_ARM = re.compile(r'^\s*((?:"[^"]+"\|?)+)\)\s*$')
_RUNTHIS = re.compile(r"""^\s*RUNTHIS\+?=(['"])(.*)\1\s*$""")
_SCRIPT = re.compile(r"[\w./-]+\.(?:sh|start)$")
_ADVMAME_INSTALL = re.compile(r"cp -r \$\{PKG_DIR\}/config/(advmame\.rc\w*) \S+/advmame\.rc$")
@dataclass
class Dispatch:
"""The cores emuelecRunEmu.sh hands to one script, and the lines that do it."""
emulators: list[str] = field(default_factory=list)
lines: set[int] = field(default_factory=set)
class Runs:
"""emuelecRunEmu.sh, read once: which core starts which script."""
def __init__(self, tree: RepoTree) -> None:
self.script = Script(RUN_EMU, tree.read(RUN_EMU))
self.by_script: dict[str, Dispatch] = {}
emu: tuple[str, int] | None = None
arm: tuple[list[str], int] | None = None
for number, line in self.script.code():
if found := _ARM.match(line):
arm = ([p.strip('"') for p in found.group(1).split("|") if p], number)
emu = None
continue
if re.match(r"\s*(;;|esac\b)", line):
arm, emu = None, None
continue
if found := _EMU_TEST.search(line):
emu = (found.group(1).removesuffix("_libretro"), number)
continue
if re.match(r"\s*fi\b", line):
emu = None
continue
run = _RUNTHIS.match(line)
command = words(run.group(2) if run else line)
name = next(
(posixpath.basename(w.text) for w in command if _SCRIPT.fullmatch(w.text)), None
)
if name is None or (emu is None and arm is None):
continue
names, at = ([emu[0]], emu[1]) if emu else arm # type: ignore[misc]
entry = self.by_script.setdefault(name, Dispatch())
entry.emulators.extend(n for n in names if n not in entry.emulators)
entry.lines |= {at, number}
if not self.by_script:
raise self.script.missing("script started for a core")
def of(self, name: str) -> Dispatch:
if name not in self.by_script:
raise self.script.missing(f"start of {name}")
return self.by_script[name]
def emulator(self, name: str) -> str:
return ", ".join(self.of(name).emulators)
def ref(self, name: str) -> tuple[str, set[int]]:
return (RUN_EMU, set(self.of(name).lines))
def _evidence(
record: Launcher,
script: Script,
target: str,
keys: list[str],
source_name: str = "",
rewrites: Iterable[int] = (),
) -> Launcher:
"""A config record with the start script lines that put its file in place.
``rewrites`` are lines the extractor read as rewriting the record's
values without naming their keys (advmame.sh's sed of /roms/mame).
"""
placement = config_placement(script, target, keys, source_name=source_name)
placement.overrides |= set(rewrites)
fields = evidence_fields([placement])
record.installs = fields["installs"]
record.kept_if_present = fields["kept_if_present"]
record.overrides = fields["overrides"]
return record
def _read(tree: RepoTree, path: str) -> Script:
return Script(path, tree.read(path))
def _under(path: str, root: str = BIOS_ROOT) -> bool:
return path.rstrip("/") == root or path.startswith(root + "/")
def retroarch(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""system_directory, written by sed into the retroarch.cfg the package installs."""
mk = _read(tree, RETROARCH_MK)
sed_line, sed = mk.find(
r"s/# system_directory =/system_directory =((?:\\/|[^/])+)/", "system_directory sed"
)
run_line, _ = runs.script.find(
r"RUNTHIS='\$\{RABIN\}.* -L /tmp/cores/\$\{EMU\}\.so --config \$\{RACONF\}",
"RetroArch start with --config",
)
return [
Launcher(
emulator="retroarch",
build="libretro",
kind="passes_path",
names=[],
source_ref=source_ref([(mk.path, {sed_line}), (RUN_EMU, {run_line})]),
search_dir=sed.group(1).replace("\\/", "/"),
passes_to="retroarch.cfg system_directory (--config)",
)
]
def retrorun(tree: RepoTree, runs: Runs) -> list[Launcher]:
script = runs.script
base_line, base = script.find(r'^\s*RUNTHIS="(\w+)"\s*$', "retrorun command")
test_line, _ = script.find(
rf'if \[\[ "\$\{{EMULATOR\}}" = "{base.group(1)}" \]\]', f"{base.group(1)} test"
)
run_line, run = script.find(r"RUNTHIS\+=.*\s(-d)\s+(\S+)\s", "retrorun -d", base_line)
return [
Launcher(
emulator=base.group(1),
build="libretro",
kind="passes_path",
names=[],
source_ref=source_ref([(RUN_EMU, {test_line, base_line, run_line})]),
search_dir=run.group(2),
passes_to=f"{base.group(1)} {run.group(1)}",
)
]
def freej2me(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""JAVA_HOME set for the core, and the jar its script copies beside the JDK."""
script = runs.script
test_line, test = script.find(r'if \[ \$\{EMU\} == "(\w+)_libretro" \]', "core test")
call_line, call = script.find(r"^\s*(\S+\.sh)\s*$", "script call", test_line)
java_line, java = script.find(r"^\s*(JAVA_HOME)='([^']+)'", "JAVA_HOME", test_line)
export_line, _ = script.find(rf"^\s*export {java.group(1)}\s*$", "export", java_line)
core = test.group(1)
path = f"{LIBRETRO}/{core}/scripts/{call.group(1)}"
started = _read(tree, path)
guard_line, guard = started.find(r'if \[\[ -f "([^"]+)" \]\]; then$', "JDK version test")
copy_line, copy = started.find(r"^\s*cp -rf (\S+) (\S+)$", "jar copy", guard_line)
source = copy.group(1)
return [
Launcher(
emulator=core,
build="libretro",
kind="passes_path",
names=[],
source_ref=source_ref(
[(RUN_EMU, {test_line, call_line, java_line, export_line}), (path, {guard_line})]
),
search_dir=java.group(2),
passes_to=f"environment {java.group(1)}",
),
Launcher(
emulator=core,
build="libretro",
kind="copies",
names=[posixpath.basename(source)],
source_ref=source_ref(
[(RUN_EMU, {test_line, call_line}), (path, {guard_line, copy_line})]
),
search_dir=posixpath.dirname(source),
passes_to=copy.group(2),
depends_on_option=f"{guard.group(1)} exists",
),
]
def pico8(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""The folder the user drops in bios/ is moved where the binary runs from."""
path = f"{BIN}/pico8.sh"
script = _read(tree, path)
guard_line, guard = script.find(r'if \[\[ -e "([^"]+)" \]\]; then$', "new version test")
folder = guard.group(1)
copy_line, copy = script.find(
rf"^\s*cp -rf {re.escape(folder)}/\* (\S+)$", "copy of the folder", guard_line
)
target = copy.group(1)
remove_line, _ = script.find(rf"^\s*rm -rf {re.escape(folder)}$", "removal", copy_line)
mode_line, mode = script.find(
rf"^\s*chmod \+x {re.escape(target)}/([^/\s]+)$", "executable bit", remove_line
)
binary = mode.group(1)
starts = {
n for n, line in script.code() if re.match(rf"\s*{re.escape(target)}/{binary} -", line)
}
if not starts:
raise script.missing(f"start of {target}/{binary}")
name = posixpath.basename(path)
return [
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="moves",
names=[binary],
source_ref=source_ref(
[runs.ref(name), (path, {guard_line, copy_line, remove_line, mode_line, *starts})]
),
case_insensitive=False,
search_dir=folder,
passes_to=target,
fails_when_missing=False,
depends_on_option=f"{folder} exists",
)
]
def link_record(emulator: str, script: Script, link: Link, extra: set[int]) -> Launcher:
return Launcher(
emulator=emulator,
build="standalone",
kind="links",
names=list(link.names),
source_ref=source_ref([(script.path, link.lines | extra)]),
expands=link.expands or None,
search_dir=link.bios_side,
passes_to=f"symlink {link.link} -> {link.target}",
fails_when_missing=False,
)
def _link_guard(script: Script, link: Link) -> set[int]:
"""The `[ ! -L <link> ]` test the link waits for, when it is the one just above."""
first = max(link.lines)
tests = [
number
for number, line in script.code()
if number < first
and re.search(r"\[ ! -L ", line)
and link.link in script.expand(line, number, set())
]
return {tests[-1]} if tests and first - tests[-1] <= 4 else set()
# Start scripts whose ln -s commands put a BIOS folder where the emulator reads.
LINK_SCRIPTS = (
f"{EMULATORS}/flycastsa/scripts/flycast.sh",
f"{EMULATORS}/flycast-dojo/scripts/flycastdojo.sh",
)
# Packages whose package.mk links a BIOS folder into the installed tree.
LINK_PACKAGES = (
f"{EMULATORS}/amiberry",
f"{EMULATORS}/amiberry-lite",
f"{SX05RE}/tools/sound/timidity",
)
def links(tree: RepoTree, runs: Runs) -> list[Launcher]:
records = []
for path in LINK_SCRIPTS:
script = _read(tree, path)
found = script.links(BIOS_ROOT)
if not found:
raise script.missing(f"ln -s of a path under {BIOS_ROOT}")
name = posixpath.basename(path)
for link in found:
record = link_record(runs.emulator(name), script, link, _link_guard(script, link))
record.source_ref = f"{source_ref([runs.ref(name)])}, {record.source_ref}"
records.append(record)
for package in LINK_PACKAGES:
script = _read(tree, f"{package}/package.mk")
found = script.links(BIOS_ROOT, fixed={"INSTALL": ""})
if not found:
raise script.missing(f"ln -s of a path under {BIOS_ROOT}")
records.extend(
link_record(posixpath.basename(package), script, link, set()) for link in found
)
return records
def duckstation(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""The settings.ini the package ships hands a directory and leaves the paths empty."""
package = f"{EMULATORS}/duckstation"
path = f"{package}/config/settings.ini"
found = {(s.section, s.key): s for s in settings(tree.read(path))}
folder_key = ("BIOS", "SearchDirectory")
file_keys = (("BIOS", "PathNTSCU"), ("BIOS", "PathNTSCJ"), ("BIOS", "PathPAL"))
if folder_key not in found:
raise LauncherPatternMissing(path, "[BIOS] SearchDirectory")
set_files = [k for k in file_keys if k in found and found[k].value]
if set_files:
raise LauncherPatternMissing(path, f"empty [BIOS] {set_files[0][1]}")
mk = _read(tree, f"{package}/package.mk")
mk_line, _ = mk.find(r"cp -rf \$\{PKG_DIR\}/config/\* ", "config install")
script = _read(tree, f"{package}/scripts/duckstation.sh")
run_line, _ = script.find(r'-settings "\$\{CONFIG_DIR\}/settings\.ini"', "-settings")
name = posixpath.basename(script.path)
lines = {found[k].line for k in (folder_key, *file_keys) if k in found}
unset = describe_keys("", file_keys).strip()
return [_evidence(
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="scans_dir",
names=[],
source_ref=source_ref(
[runs.ref(name), (path, lines), (mk.path, {mk_line}), (script.path, {run_line})]
),
search_dir=found[folder_key].value,
passes_to=f"settings.ini [BIOS] SearchDirectory (-settings), {unset} unset",
),
script,
script.expand("${CONFIG_DIR}/settings.ini", run_line, set()),
[f"[{s}] {k}" for s, k in (folder_key, *file_keys)],
)]
YABASANSHIRO_SCRIPTS = (
f"{EMULATORS}/yabasanshiroSA_1_11/scripts/yabasanshiro.sh",
f"{EMULATORS}/yabasanshiroSA_1_5/scripts/yabasanshiro1_5.sh",
)
def yabasanshiro(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""The BIOS flag, set when the HLE option is off and the file exists."""
records = []
for path in YABASANSHIRO_SCRIPTS:
script = _read(tree, path)
option_line, option = script.find(r"^\s*(\w+)=\$\(get_ee_setting (\w+) ", "HLE option")
test_line, test = script.find(
rf'if \[ "\$\{{{option.group(1)}\}}" != (\w+) \]', "HLE option test", option_line
)
exists_line, exists = script.find(r'if \[ -e "([^"]+)" \]', "BIOS test", test_line)
bios = exists.group(1)
flag_line, flag = script.find(
rf'^\s*(\w+)="(-\w) {re.escape(bios)}"$', "BIOS flag", exists_line
)
run_line, run = script.find(
rf"\{{\s*(\S+) .*\$\{{{flag.group(1)}\}}", "start with the flag", flag_line
)
name = posixpath.basename(path)
records.append(
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="fixed_name",
names=[posixpath.basename(bios)],
source_ref=source_ref(
[
runs.ref(name),
(path, {option_line, test_line, exists_line, flag_line, run_line}),
]
),
case_insensitive=False,
search_dir=posixpath.dirname(bios),
passes_to=f"{run.group(1)} {flag.group(2)}",
fails_when_missing=False,
depends_on_option=f"{option.group(2)} != {test.group(1)}",
)
)
return records
def jzintv(tree: RepoTree, runs: Runs) -> list[Launcher]:
path = f"{EMULATORS}/jzintv/scripts/jzintv.sh"
script = _read(tree, path)
run_line, run = script.find(r"^\s*exec (\S+) .*\s(-p)\s+(\S+)\s", "start with -p")
name = posixpath.basename(path)
return [
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="passes_path",
names=[],
source_ref=source_ref([runs.ref(name), (path, {run_line})]),
search_dir=run.group(3).rstrip("/"),
passes_to=f"{run.group(1)} {run.group(2)}",
)
]
def mupen64plus(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""The 64DD IPL names, tried in order, passed only beside a .ndd disk."""
path = f"{EMULATORS}/mupen64plussa/mupen64plussa-core/m64p.sh"
script = _read(tree, path)
fallbacks = [
(number, found)
for number, line in script.code()
if (found := re.search(r'\[ ! -f "\$\{(\w+)\}" \] && \1="([^"]+)"', line))
]
if not fallbacks:
raise script.missing("fallback name test")
var = fallbacks[0][1].group(1)
first = script.value(var, fallbacks[0][0])
if first is None:
raise script.missing(f"assignment of {var}")
used = {first.line, *(n for n, _ in fallbacks)}
paths = [
script.expand(v, n, used)
for n, v in [(first.line, first.value), *((n, f.group(2)) for n, f in fallbacks)]
]
folders = {posixpath.dirname(p) for p in paths}
if len(folders) != 1:
raise script.missing(f"one folder for the {var} names")
test_line, test = script.find(
rf'if \[ -f "([^"]+)" \] && \[ -f "\$\{{{var}\}}" \]', "disk and IPL test"
)
passes = [
(number, found)
for number, line in script.code()
if (found := re.search(rf'^\s*(\S+) .*(--[\w-]+) "\$\{{{var}\}}"', line))
]
if not passes:
raise script.missing(f"flag that hands {var}")
flags = sorted({f"{f.group(1)} {f.group(2)}" for _, f in passes})
name = posixpath.basename(path)
return [
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="names_searched",
names=[posixpath.basename(p) for p in paths],
source_ref=source_ref(
[runs.ref(name), (path, used | {test_line} | {n for n, _ in passes})]
),
case_insensitive=False,
search_dir=folders.pop(),
passes_to=", ".join(flags),
fails_when_missing=False,
depends_on_option=f'-f "{test.group(1)}"',
)
]
def hatari(tree: RepoTree, runs: Runs) -> list[Launcher]:
package = f"{EMULATORS}/hatarisa"
script = _read(tree, f"{package}/scripts/hatari.start")
copy_line, copy = script.find(r'^\s*cp \S+/([\w.-]+\.cfg) "\$(\w+)"', "default config copy")
run_lines = {
n
for n, line in script.code()
if n > copy_line and f'--configfile "${copy.group(2)}"' in line
}
if not run_lines:
raise script.missing(f"--configfile ${copy.group(2)}")
path = f"{package}/config/conf/{copy.group(1)}"
found = settings(tree.read(path))
keys = [
(s.section, s.key)
for s in found
if _under(s.value) and "." in posixpath.basename(s.value)
]
if not keys:
raise LauncherPatternMissing(path, f"key naming a file under {BIOS_ROOT}")
value, lines = fixed_paths(found, keys, path)
mk = _read(tree, f"{package}/package.mk")
mk_line, _ = mk.find(r"cp -R \$\{PKG_DIR\}/config/\* ", "config install")
name = posixpath.basename(script.path)
return [_evidence(
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="fixed_name",
names=list(value.names),
source_ref=source_ref(
[runs.ref(name), (path, lines), (mk.path, {mk_line}),
(script.path, {copy_line, *run_lines})]
),
search_dir=value.search_dir,
passes_to=f"{describe_keys(copy.group(1), value.keys)} (--configfile)",
),
script,
script.expand("${" + copy.group(2) + "}", copy_line, set()),
[f"[{s}] {k}" if s else k for s, k in value.keys],
source_name=copy.group(1),
)]
_ADVMAME_SYSTEM = re.compile(r'^\s*if \[\[ "\$\{1\}" = "([\w-]+)" \]\]; then$')
_ADVMAME_SED = re.compile(r'^\s*sed -i "s\|([^|]+)\|([^|]+)\|g" \$\{CONFIG_DIR\}/advmame\.rc$')
def advancemame(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""Folder keys of the advmame.rc package.mk installs, one per device branch.
A key under the roms root is one too: dir_rom is /storage/roms/mame. The
start script rewrites the config for the system it is launched for
(advmame.sh:26-30): /roms/mame becomes /roms/arcade for arcade, so that
system reads its own record.
"""
package = f"{EMULATORS}/advancemame"
mk = _read(tree, f"{package}/package.mk")
installs = [
(number, found.group(1))
for number, line in mk.code()
if (found := _ADVMAME_INSTALL.search(line))
]
if not installs:
raise mk.missing("advmame.rc install")
by_value: dict[tuple[tuple[str, str], ...], list[tuple[str, set[int], int]]] = {}
for mk_line, file in installs:
path = f"{package}/config/{file}"
keys = [s for s in settings(tree.read(path), separator=" ") if _under(s.value, ROMS_ROOT)]
if not any(_under(s.value) for s in keys):
raise LauncherPatternMissing(path, f"key naming a folder under {BIOS_ROOT}")
value = tuple((s.key, s.value) for s in keys)
by_value.setdefault(value, []).append((path, {s.line for s in keys}, mk_line))
if len(by_value) != 1:
raise mk.missing("one set of BIOS folders for every device")
((value, configs),) = by_value.items()
script = _read(tree, f"{package}/bin/advmame.sh")
run_line, _ = script.find(r"^\s*(?:\w+=\S+ )*advmame ", "advmame start")
test_line, test = script.find(_ADVMAME_SYSTEM.pattern, "system test", end=run_line)
sed_line, sed = script.find(_ADVMAME_SED.pattern, "system rewrite", test_line + 1, test_line + 1)
name = posixpath.basename(script.path)
def refs(*lines: int) -> str:
return source_ref([
runs.ref(name),
*((path, lines) for path, lines, _ in configs),
(mk.path, {mk_line for _, _, mk_line in configs}),
(script.path, {run_line, *lines}),
])
target = script.expand("${CONFIG_DIR}/advmame.rc", run_line, set())
def proved(record: Launcher, key: str) -> Launcher:
return _evidence(record, script, target, [key], rewrites={sed_line})
records = []
for key, folder in value:
rewritten = folder.replace(sed.group(1), sed.group(2))
if rewritten != folder:
# Every system but the one the script rewrites the key for, and
# that system with the rewritten folder.
records.append(proved(Launcher(
emulator=runs.emulator(name), build="standalone", kind="passes_path", names=[],
source_ref=refs(test_line, sed_line), search_dir=folder,
passes_to=f"advmame.rc {key}",
depends_on_option=f"system != {test.group(1)}",
), key))
records.append(proved(Launcher(
emulator=runs.emulator(name), build="standalone", kind="passes_path", names=[],
source_ref=refs(test_line, sed_line), search_dir=rewritten,
passes_to=f"advmame.rc {key}", systems=[test.group(1)],
), key))
continue
records.append(proved(Launcher(
emulator=runs.emulator(name), build="standalone", kind="passes_path", names=[],
source_ref=refs(), search_dir=folder, passes_to=f"advmame.rc {key}",
), key))
return records
def mame(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""The files mame.sh copies into bios/mame, and the folders mame.ini hands the core."""
package = f"{LIBRETRO}/mame"
script = _read(tree, f"{package}/scripts/mame.sh")
name = posixpath.basename(script.path)
emulator = runs.emulator(name)
records = []
guard: tuple[int, str] | None = None
for number, line in script.code():
if found := re.search(r'if \[ ! -f "([^"]+)" \]', line):
guard = (number, found.group(1))
continue
copy = re.match(r'\s*cp -rf "([^"]+)/"(\S+) "([^"]+)"\s*$', line)
if copy is None:
continue
if guard is None:
raise script.missing(f"test before the copy at line {number}")
if not _under(copy.group(3)):
continue
records.append(
Launcher(
emulator=emulator,
build="libretro",
kind="copies",
names=[copy.group(2)],
source_ref=source_ref([runs.ref(name), (script.path, {guard[0], number})]),
search_dir=copy.group(1),
passes_to=copy.group(3),
fails_when_missing=False,
depends_on_option=f"{guard[1]} absent",
)
)
if not records:
raise script.missing(f"copy into {BIOS_ROOT}")
# mame.ini hands the core no folder: MAME.opt turns on
# mame_mame_paths_enable, so the core passes no path option
# (libretro mame src/osd/libretro/libretro-internal/retro_init.cpp:554-555),
# and the ini MAME would read sits under a working directory no launcher
# sets (src/emu/emuopts.cpp:49). Only the copies are the launcher's.
return records
def scummvm(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""--extrapath for the standalone build, and the scummvm.ini copied for the core."""
path = f"{EMULATORS}/scummvmsa/bin/scummvm.start"
script = _read(tree, path)
name = posixpath.basename(path)
test_line, test = script.find(r'^\s*if \[ -d "([^"]+)" \]; then\s*$', "extra folder test")
set_line, set_ = script.find(r'^\s*(\w+)="(--[\w-]+)=([^"]+)"', "extra path", test_line)
run_line, run = script.find(rf"^\s*(\S+) .*\${set_.group(1)}\b", "start with it", set_line)
core_line, core = script.find(r"-L /tmp/cores/(\w+)_libretro\.so", "libretro start")
arm_line, arm = script.find(r'^\s*"(\w+)"\)', "setup arm", core_line)
copy_line, copy = script.find(r"^\s*cp (\S+) (\S+)$", "copy into the BIOS folder", arm_line)
launched = sorted({
m.group(1) for _, line in runs.script.code()
if (m := re.search(rf"\b{re.escape(name)} (\w+)", line))
})
if not launched:
raise runs.script.missing(f"start of {name} with an action")
action = f"{name} {arm.group(1)}"
if arm.group(1) not in launched:
# The copy runs from the setup action, never from a launch.
action += f", an action no launch runs ({RUN_EMU.rsplit('/', 1)[-1]} passes {', '.join(launched)})"
used = {copy_line}
source = script.expand(copy.group(1), copy_line, used)
target = script.expand(copy.group(2), copy_line, used)
if not _under(target):
raise script.missing(f"copy into {BIOS_ROOT}")
return [
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="passes_path",
names=[],
source_ref=source_ref([runs.ref(name), (path, {test_line, set_line, run_line})]),
search_dir=set_.group(3),
passes_to=f"{run.group(1)} {set_.group(2)}",
depends_on_option=f"{test.group(1)} exists",
),
Launcher(
emulator=core.group(1),
build="libretro",
kind="copies",
names=[posixpath.basename(source)],
source_ref=source_ref([(path, {core_line, arm_line} | used)]),
search_dir=posixpath.dirname(source),
passes_to=target,
fails_when_missing=False,
depends_on_option=action,
),
]
def dolphin(tree: RepoTree, runs: Runs) -> list[Launcher]:
path = f"{EMULATORS}/dolphinSA/scripts/dolphin.sh"
script = _read(tree, path)
run_line, run = script.find(r"\b(XDG_DATA_HOME)=(\S+) .*\bdolphin-emu", "XDG_DATA_HOME start")
name = posixpath.basename(path)
return [
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="passes_path",
names=[],
source_ref=source_ref([runs.ref(name), (path, {run_line})]),
search_dir=run.group(2),
passes_to=f"environment {run.group(1)}",
)
]
def midi(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""The MIDI synth set_midi_source starts for the ra_midi_output setting."""
script = runs.script
setting_line, setting = script.find(
r'^\s*(\w+)=\$\(get_ee_setting "(\w*midi\w*)" ', "MIDI setting"
)
call_line, _ = script.find(
rf'set_midi_source "\$\{{{setting.group(1)}\}}"', "set_midi_source call", setting_line
)
utils = _read(tree, UTILS)
head_line, _ = utils.find(r"^\s*function set_midi_source\(\)", "set_midi_source")
end_line, _ = utils.find(r"^\s*esac\b", "end of the source case", head_line)
records = []
arm: tuple[int, str] | None = None
for number, line in utils.code():
if number <= head_line or number >= end_line:
continue
if found := re.match(r"\s*([\w-]+)\)\s*$", line):
arm = (number, found.group(1))
continue
command = [w.text for w in words(line)]
paths = [
(command[i - 1], value)
for i, value in enumerate(command)
if i and command[i - 1].startswith("-") and re.search(r"(^|/)bios(/|$)", value)
]
if not paths or arm is None:
continue
((flag, value),) = paths
is_file = "." in posixpath.basename(value) and _under(value)
records.append(
Launcher(
emulator=command[0],
build="standalone",
kind="fixed_name" if is_file else "passes_path",
names=[posixpath.relpath(value, BIOS_ROOT)] if is_file else [],
source_ref=source_ref(
[(RUN_EMU, {setting_line, call_line}), (UTILS, {head_line, arm[0], number})]
),
slot=arm[1],
case_insensitive=False if is_file else None,
search_dir=BIOS_ROOT if is_file else value.rstrip("/"),
passes_to=f"{command[0]} {flag}",
fails_when_missing=False,
depends_on_option=f"{setting.group(2)} = {arm[1]}",
)
)
if not records:
raise utils.missing("MIDI synth started with a BIOS path")
return records
EXTRACTORS: tuple[Callable[[RepoTree, Runs], list[Launcher]], ...] = (
retroarch,
retrorun,
freej2me,
mame,
pico8,
links,
duckstation,
yabasanshiro,
jzintv,
mupen64plus,
hatari,
advancemame,
scummvm,
dolphin,
midi,
)
def extract_launchers(tree: RepoTree) -> list[dict]:
"""Every launcher record the extractors find, in extractor order."""
runs = Runs(tree)
return [record.entry() for extractor in EXTRACTORS for record in extractor(tree, runs)]