feat: read the emuelec launchers

emuelec_launchers.py reads emuelecRunEmu.sh and the scripts and
configs it starts at the tag batocera-systems is read at (v4.8): 36
records for RetroArch, retrorun, freej2me, MAME, PICO-8, Flycast,
Amiberry, DuckStation, Yabasanshiro, jzIntv, Mupen64Plus, Hatari,
AdvanceMAME, ScummVM, Dolphin and the MIDI synths.
This commit is contained in:
Abdessamad Derraz committed 2026-11-09 17:34:03 +01:00
1 parent 7aacbbe214
commit 82a6b2f833
4 files changed
+1782

No files matched your search

+358
View File
@@ -4226,3 +4226,361 @@ systems:
native_name: Phillips CDI
native_id: cdi
name: Phillips CDI
launchers:
- emulator: retroarch
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios
passes_to: "retroarch.cfg system_directory (--config)"
fails_when_missing: false
source_ref: "packages/sx05re/libretro_base/retroarch/package.mk:117, packages/sx05re/emuelec/bin/emuelecRunEmu.sh:380"
- emulator: retrorun
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios
passes_to: retrorun -d
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:110, 466, 476"
- emulator: freej2me
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/jdk
passes_to: environment JAVA_HOME
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:129-133, packages/sx05re/libretro/freej2me/scripts/freej2me.sh:11"
- emulator: freej2me
build: libretro
kind: copies
names:
- freej2me-lr.jar
search_dir: /usr/lib/libretro
passes_to: "${HOME}/roms/bios"
fails_when_missing: false
depends_on_option: /storage/roms/bios/jdk/eeversion exists
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:129-130, packages/sx05re/libretro/freej2me/scripts/freej2me.sh:11-12"
- emulator: mame
build: libretro
kind: copies
names:
- fm*
search_dir: /usr/config/emuelec/configs/mame/hash
passes_to: /storage/roms/bios/mame/hash
fails_when_missing: false
depends_on_option: /storage/roms/bios/mame/hash/fmtowns_cd.xml absent
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:6-8"
- emulator: mame
build: libretro
kind: copies
names:
- app*
search_dir: /usr/config/emuelec/configs/mame/hash
passes_to: /storage/roms/bios/mame/hash
fails_when_missing: false
depends_on_option: /storage/roms/bios/mame/hash/apple2_flop_orig.xml absent
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:11-13"
- emulator: mame
build: libretro
kind: copies
names:
- fm*
search_dir: /usr/config/emuelec/configs/mame/ini
passes_to: /storage/roms/bios/mame/ini
fails_when_missing: false
depends_on_option: /storage/roms/bios/mame/ini/fmtownsux.ini absent
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:16-18"
- emulator: mame
build: libretro
kind: copies
names:
- mame*
search_dir: /usr/config/emuelec/configs/mame/ini
passes_to: /storage/roms/bios/mame/ini
fails_when_missing: false
depends_on_option: /storage/roms/bios/mame/ini/mame.ini absent
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame
passes_to: mame.ini homepath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:10, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame/bios
passes_to: mame.ini rompath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:11, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame/hash
passes_to: mame.ini hashpath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:12, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame/samples
passes_to: mame.ini samplepath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:13, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame/artwork
passes_to: mame.ini artpath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:14, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame/ini
passes_to: mame.ini inipath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:16, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame/cheat
passes_to: mame.ini cheatpath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:18, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame/crosshair
passes_to: mame.ini crosshairpath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:19, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame/plugins
passes_to: mame.ini pluginspath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:20, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: pico8
build: standalone
kind: moves
names:
- pico8_dyn
case_insensitive: false
search_dir: /storage/roms/bios/pico-8
passes_to: /emuelec/bin/pico-8
fails_when_missing: false
depends_on_option: /storage/roms/bios/pico-8 exists
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:305-307, packages/sx05re/emuelec/bin/pico8.sh:9-13,\
\ 40-42"
- emulator: flycastsa
build: standalone
kind: links
names: []
search_dir: /storage/roms/bios/dc
passes_to: symlink /storage/.local/share/flycast -> /storage/roms/bios/dc
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:198-200, packages/sx05re/emulators/flycastsa/scripts/flycast.sh:11,\
\ 14"
- emulator: flycast_dojo
build: standalone
kind: links
names: []
search_dir: /storage/roms/bios/dc_dojo
passes_to: symlink /storage/.local/share/flycast-dojo -> /storage/roms/bios/dc_dojo
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:201-203, packages/sx05re/emulators/flycast-dojo/scripts/flycastdojo.sh:11-12,\
\ 28-30"
- emulator: amiberry
build: standalone
kind: links
names: []
search_dir: /storage/roms/bios
passes_to: symlink /usr/config/amiberry/kickstarts -> /storage/roms/bios
fails_when_missing: false
source_ref: "packages/sx05re/emulators/amiberry/package.mk:36"
- emulator: amiberry-lite
build: standalone
kind: links
names: []
search_dir: /storage/roms/bios
passes_to: symlink /usr/config/amiberry-lite/kickstarts -> /storage/roms/bios
fails_when_missing: false
source_ref: "packages/sx05re/emulators/amiberry-lite/package.mk:36"
- emulator: timidity
build: standalone
kind: links
names: []
search_dir: /storage/roms/bios/timidity/freepats
passes_to: symlink /usr/config/timidity/freepats -> /storage/roms/bios/timidity/freepats
fails_when_missing: false
source_ref: "packages/sx05re/tools/sound/timidity/package.mk:38"
- emulator: duckstation
build: standalone
kind: scans_dir
names: []
search_dir: /storage/roms/bios
passes_to: "settings.ini [BIOS] SearchDirectory (-settings), [BIOS] PathNTSCU, PathNTSCJ,\
\ PathPAL unset"
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:207-209, packages/sx05re/emulators/duckstation/config/settings.ini:6-9,\
\ packages/sx05re/emulators/duckstation/package.mk:56, packages/sx05re/emulators/duckstation/scripts/duckstation.sh:40"
- emulator: yabasanshiroSA
build: standalone
kind: fixed_name
names:
- saturn_bios.bin
case_insensitive: false
search_dir: /storage/roms/bios
passes_to: yabasanshiro -b
fails_when_missing: false
depends_on_option: hlebios != 1
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:343-345, packages/sx05re/emulators/yabasanshiroSA_1_11/scripts/yabasanshiro.sh:30-34,\
\ 49"
- emulator: yabasanshiroSA1_5
build: standalone
kind: fixed_name
names:
- saturn_bios.bin
case_insensitive: false
search_dir: /storage/roms/bios
passes_to: yabasanshiro1_5 -b
fails_when_missing: false
depends_on_option: hlebios != 1
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:346-348, packages/sx05re/emulators/yabasanshiroSA_1_5/scripts/yabasanshiro1_5.sh:29-33,\
\ 47"
- emulator: jzintv
build: standalone
kind: passes_path
names: []
search_dir: /storage/roms/bios
passes_to: jzintv -p
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:337-339, packages/sx05re/emulators/jzintv/scripts/jzintv.sh:49"
- emulator: rice, glide64mk2
build: standalone
kind: names_searched
names:
- "N64DD IPLROM (J).n64"
- IPL.n64
case_insensitive: false
search_dir: /storage/roms/bios/Mupen64plus
passes_to: mupen64plus --dd-ipl-rom
fails_when_missing: false
depends_on_option: "-f \"${GAMEDIR}/${GAMENAME}.ndd\""
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:232-237, packages/sx05re/emulators/mupen64plussa/mupen64plussa-core/m64p.sh:16,\
\ 65-68, 96-98"
- emulator: HATARISA
build: standalone
kind: fixed_name
names:
- tos.img
search_dir: /storage/roms/bios
passes_to: "Atari-ST-default.cfg [ROM] szTosImageFileName (--configfile)"
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:172-174, packages/sx05re/emulators/hatarisa/config/conf/Atari-ST-default.cfg:200,\
\ packages/sx05re/emulators/hatarisa/package.mk:39, packages/sx05re/emulators/hatarisa/scripts/hatari.start:36,\
\ 66, 73"
- emulator: AdvanceMame
build: standalone
kind: passes_path
names: []
search_dir: /storage/roms/bios/advmame/samples
passes_to: advmame.rc dir_sample
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:9,\
\ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:9, packages/sx05re/emulators/advancemame/config/advmame.rc:12,\
\ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:80"
- emulator: SCUMMVMSA
build: standalone
kind: passes_path
names: []
search_dir: /storage/.config/scummvm/extra
passes_to: scummvm --extrapath
fails_when_missing: false
depends_on_option: /storage/.config/scummvm/extra exists
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:251-255, packages/sx05re/emulators/scummvmsa/bin/scummvm.start:38-39,\
\ 45"
- emulator: scummvm
build: libretro
kind: copies
names:
- scummvm.ini
search_dir: /storage/.config/scummvm
passes_to: /storage/roms/bios/scummvm.ini
fails_when_missing: false
depends_on_option: scummvm.start add
source_ref: "packages/sx05re/emulators/scummvmsa/bin/scummvm.start:9, 49-51, 54"
- emulator: dolphin
build: standalone
kind: passes_path
names: []
search_dir: /storage/roms/dolphin
passes_to: environment XDG_DATA_HOME
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:270-272, packages/sx05re/emulators/dolphinSA/scripts/dolphin.sh:28"
- emulator: fluidsynth
build: standalone
kind: fixed_name
slot: fluidsynth
names:
- sc/SC-55.sf2
case_insensitive: false
search_dir: /storage/roms/bios
passes_to: fluidsynth -is
fails_when_missing: false
depends_on_option: ra_midi_output = fluidsynth
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:123-125, packages/sx05re/emuelec/bin/emuelec-utils:54,\
\ 66-67"
- emulator: mt32d
build: standalone
kind: passes_path
slot: cm32l
names: []
search_dir: /roms/bios
passes_to: mt32d -f
fails_when_missing: false
depends_on_option: ra_midi_output = cm32l
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:123-125, packages/sx05re/emuelec/bin/emuelec-utils:54,\
\ 75-76"
- emulator: mt32d
build: standalone
kind: passes_path
slot: mt32
names: []
search_dir: /roms/bios
passes_to: mt32d -f
fails_when_missing: false
depends_on_option: ra_midi_output = mt32
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:123-125, packages/sx05re/emuelec/bin/emuelec-utils:54,\
\ 79-80"
+755
View File
@@ -0,0 +1,755 @@
"""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
from dataclasses import dataclass, field
try:
from .configgen_launchers import Launcher
from .rocknix_launchers import fixed_paths
from .shell_launchers import (
LauncherPatternMissing,
Link,
RepoTree,
Script,
describe_keys,
settings,
source_ref,
words,
)
except ImportError:
from configgen_launchers import Launcher
from rocknix_launchers import fixed_paths
from shell_launchers import (
LauncherPatternMissing,
Link,
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"
_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 _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 [
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",
)
]
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 [
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)",
)
]
def advancemame(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""Folder keys of the advmame.rc package.mk installs, one per device branch."""
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}"
found = [s for s in settings(tree.read(path), separator=" ") if _under(s.value)]
if not found:
raise LauncherPatternMissing(path, f"key naming a folder under {BIOS_ROOT}")
value = tuple((s.key, s.value) for s in found)
by_value.setdefault(value, []).append((path, {s.line for s in found}, 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")
name = posixpath.basename(script.path)
refs = [
runs.ref(name),
*((path, lines) for path, lines, _ in configs),
(mk.path, {mk_line for _, _, mk_line in configs}),
(script.path, {run_line}),
]
return [
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="passes_path",
names=[],
source_ref=source_ref(refs),
search_dir=folder,
passes_to=f"advmame.rc {key}",
)
for key, folder in value
]
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}")
ini = next((r for r in records if r.names == ["mame*"]), None)
if ini is None:
raise script.missing("copy of mame.ini")
option_path = f"{package}/config/MAME/MAME.opt"
option = next(
(s for s in settings(tree.read(option_path)) if s.key == "mame_read_config"), None
)
if option is None:
raise LauncherPatternMissing(option_path, "mame_read_config")
ini_path = f"{package}/config/ini/mame.ini"
# MAME reads from the folders its *path options name and writes to its
# *_directory ones.
folders = [
(s, [p for p in s.value.split(";") if _under(p)])
for s in settings(tree.read(ini_path), separator=" ")
if s.key.endswith("path")
]
folders = [(s, parts) for s, parts in folders if parts]
if not folders:
raise LauncherPatternMissing(ini_path, f"path under {BIOS_ROOT}")
for setting, parts in folders:
for part in parts:
records.append(
Launcher(
emulator=emulator,
build="libretro",
kind="passes_path",
names=[],
source_ref=source_ref(
[(ini_path, {setting.line}), (option_path, {option.line})]
) + f", {ini.source_ref}",
search_dir=part.rstrip("/"),
passes_to=f"mame.ini {setting.key}",
depends_on_option=f"mame_read_config = {option.value}",
)
)
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)
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=f"{name} {arm.group(1)}",
),
]
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)]
+14
View File
@@ -43,6 +43,7 @@ from .batocera_scraper import (
platform_systems,
requirements_from_systems,
)
from .emuelec_launchers import extract_launchers
from .es_emulators import (
SystemEmulators,
attach_system_cores,
@@ -50,6 +51,7 @@ from .es_emulators import (
json_systems,
system_cores,
)
from .shell_launchers import RepoTree
PLATFORM_NAME = "emuelec"
@@ -120,6 +122,14 @@ BUILDS_OF = {
PORTS_SYSTEM = "ports"
def launcher_revision(url: str) -> str:
"""The tag or commit a batocera-systems URL is read at."""
found = re.search(rf"/{REPO}/([^/]+)/packages/", url)
if found is None:
raise ValueError(f"{url} names no {REPO} revision")
return found.group(1)
def port_engines(text: str) -> list[tuple[str, str, str | None, bool]]:
"""(port, engine, check_bios key, libretro?) for each entry of ports.yaml.
@@ -215,6 +225,10 @@ class Scraper(BaseScraper):
"standalone_cores": standalone,
"dual_mode_cores": dual,
"systems": systems,
# Read at the revision the BIOS list comes from.
"launchers": extract_launchers(
RepoTree.github(REPO, launcher_revision(self.url))
),
}
+655
View File
@@ -0,0 +1,655 @@
"""EmuELEC's launcher layer read from its packages.
Each fixture is a cut-down copy of the lines an extractor reads at EmuELEC
v4.8 (02b45fe53b85c45c75dbad8e44a648092dec2e72). The records must name the
core emuelecRunEmu.sh dispatches, carry the names in the order the code
tries them, point at the fixture's own lines, and a construct that is gone
must stop the scrape.
"""
from __future__ import annotations
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
import sys
import textwrap
import unittest
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
from scraper import emuelec_launchers as el # noqa: E402
from scraper.emuelec_scraper import launcher_revision # noqa: E402
from scraper.shell_launchers import LauncherPatternMissing, RepoTree # noqa: E402
EMU = el.EMULATORS
def text(block: str) -> str:
return textwrap.dedent(block).lstrip("\n")
RUN_EMU = text(
"""
if [[ "${EMULATOR}" = "libretro" ]]; then
EMU="${CORE}_libretro"
fi
if [[ "${EMULATOR}" = "retrorun" ]]; then
EMU="${CORE}_libretro"
fi
MIDI_OUTPUT=$(get_ee_setting "ra_midi_output" "${PLATFORM}" "${BASEROMNAME}")
if [[ ! -z "${MIDI_OUTPUT}" ]]; then
emuelec-utils set_midi_source "${MIDI_OUTPUT}" "${EMULATOR}"
fi
if [ ${EMU} == "freej2me_libretro" ]; then
freej2me.sh
JAVA_HOME='/storage/roms/bios/jdk'
export JAVA_HOME
fi
case ${PLATFORM} in
"atarist")
if [ "${EMU}" = "HATARISA" ]; then
RUNTHIS='${TBASH} hatari.start "${ROMNAME}"'
fi
;;
"dreamcast"|"naomi")
if [ "${EMU}" = "flycastsa" ]; then
RUNTHIS='${TBASH} flycast.sh "${ROMNAME}"'
elif [ "${EMU}" = "flycast_dojo" ]; then
RUNTHIS='flycastdojo.sh "${ROMNAME}"'
fi
;;
"psx")
if [ "${EMU}" = "duckstation" ]; then
RUNTHIS='${TBASH} duckstation.sh "${ROMNAME}"'
fi
;;
"mame")
if [ "${EMU}" = "AdvanceMame" ]; then
RUNTHIS='${TBASH} advmame.sh "${PLATFORM}" "${ROMNAME}"'
fi
;;
"n64")
if [ "${EMU}" = "rice" ]; then
RUNTHIS='${TBASH} m64p.sh "${ROMNAME}"'
elif [ "${EMU}" = "glide64mk2" ]; then
RUNTHIS='${TBASH} m64p.sh "${ROMNAME}" m64p_gl64mk2'
fi
;;
"scummvm")
if [ "${EMU}" = "SCUMMVMSA" ]; then
RUNTHIS='${TBASH} scummvm.start sa "${ROMNAME}"'
fi
;;
"wii"|"gamecube")
if [ "${EMU}" = "dolphin" ]; then
RUNTHIS='${TBASH} dolphin.sh "${ROMNAME}"'
fi
;;
"pico8")
RUNTHIS='${TBASH} pico8.sh "${ROMNAME}"'
;;
"intellivision")
if [ "${EMU}" = "jzintv" ]; then
RUNTHIS='jzintv.sh "${ROMNAME}"'
fi
;;
"saturn")
if [ "${EMU}" = "yabasanshiroSA" ]; then
RUNTHIS='yabasanshiro.sh "${ROMNAME}"'
elif [ "${EMU}" = "yabasanshiroSA1_5" ]; then
RUNTHIS='yabasanshiro1_5.sh "${ROMNAME}"'
fi
;;
esac
case ${PLATFORM} in
"arcade"|"mame")
if [ "$EMU" = "mame_libretro" ]; then
mame.sh
fi
;;
esac
RUNTHIS='${RABIN} ${VERBOSE} ${RA_ARGS} -L /tmp/cores/${EMU}.so --config ${RACONF} "${ROMNAME}"'
RUNTHIS="retrorun"
RUNTHIS+=' ${CMD_ROTATE} --triggers -g -d /storage/roms/bios /tmp/cores/${EMU}.so "${ROMNAME}"'
"""
)
UTILS = text(
"""
function set_midi_source()
{
case ${midi_source} in
fluidsynth)
fluidsynth -is /storage/roms/bios/sc/SC-55.sf2 &
;;
timidity)
timidity -iA --module 1 &
;;
mt32)
mt32d -o 2 -i 24 -f /roms/bios &
;;
esac
}
"""
)
RETROARCH_MK = text(
"""
sed -i -e "s/# savestate_directory =/savestate_directory =\\/storage\\/roms\\/savestates/" ${INSTALL}/etc/retroarch.cfg
sed -i -e "s/# system_directory =/system_directory =\\/storage\\/roms\\/bios/" ${INSTALL}/etc/retroarch.cfg
"""
)
FREEJ2ME_SH = text(
"""
install_java
if [[ -f "/storage/roms/bios/jdk/eeversion" ]]; then
cp -rf /usr/lib/libretro/freej2me-lr.jar ${HOME}/roms/bios
else
exit 1
fi
"""
)
PICO8_SH = text(
"""
if [[ -e "/storage/roms/bios/pico-8" ]]; then
mkdir -p /emuelec/bin/pico-8
cp -rf /storage/roms/bios/pico-8/* /emuelec/bin/pico-8
rm -rf /storage/roms/bios/pico-8
chmod +x /emuelec/bin/pico-8/pico8_dyn
fi
if [[ "${CART}" == *"/splore"* ]]; then
/emuelec/bin/pico-8/pico8_dyn -splore -home /emuelec/configs/pico-8
else
/emuelec/bin/pico-8/pico8_dyn -run "${CART}" -home /emuelec/configs/pico-8
fi
"""
)
FLYCAST_SH = text(
"""
if [ ! -L "/storage/.local/share/flycast" ]; then
mkdir -p "/storage/roms/bios/dc"
rm -rf "/storage/.local/share/flycast"
ln -sf "/storage/roms/bios/dc" "/storage/.local/share/flycast"
fi
"""
)
FLYCASTDOJO_SH = text(
"""
LOCAL_DATA="/storage/roms/bios/dc_dojo"
LOCAL_HOME="/storage/.local/share/flycast-dojo"
if [ ! -L "${LOCAL_HOME}" ]; then
rm -rf "${LOCAL_HOME}"
ln -sf "${LOCAL_DATA}" "${LOCAL_HOME}"
fi
"""
)
AMIBERRY_MK = text(
"""
cp -a ${PKG_DIR}/config/* ${INSTALL}/usr/config/amiberry/
ln -s /storage/roms/bios ${INSTALL}/usr/config/amiberry/kickstarts
"""
)
TIMIDITY_MK = text(
"""
ln -sf /storage/roms/bios/timidity/freepats ${INSTALL}/usr/config/timidity/freepats
ln -sf /storage/.config/timidity/freepats.cfg ${INSTALL}/etc/timidity/freepats.cfg
"""
)
DUCKSTATION_INI = text(
"""
[BIOS]
SearchDirectory = /storage/roms/bios
PathNTSCU =
PathNTSCJ =
PathPAL =
"""
)
DUCKSTATION_MK = text(
"""
cp -rf ${PKG_DIR}/config/* ${INSTALL}/usr/config/emuelec/configs/duckstation
"""
)
DUCKSTATION_SH = text(
"""
CONFIG_DIR="/emuelec/configs/duckstation"
duckstation-nogui -fullscreen -settings "${CONFIG_DIR}/settings.ini" -- "${1}"
"""
)
def yabasanshiro_sh(program: str) -> str:
return text(
f"""
BIOS=""
HLEBIOS=$(get_ee_setting hlebios saturn "${{ROMNAME}}")
if [ "${{HLEBIOS}}" != 1 ]; then
if [ -e "/storage/roms/bios/saturn_bios.bin" ]; then
BIOS="-b /storage/roms/bios/saturn_bios.bin"
else
BIOS=""
fi
fi
{{ {program} -r 2 -i "${{1}}" ${{BIOS}}; }} > /emuelec/logs/emuelec.log 2>&1
"""
)
JZINTV_SH = text(
"""
exec jzintv -f1 -z "$RES" -p /storage/roms/bios/ "$ROM" $KBD_OPT
"""
)
M64P_SH = text(
"""
BIOSDIR="/storage/roms/bios/Mupen64plus"
IPLROM="${BIOSDIR}/N64DD IPLROM (J).n64"
[ ! -f "${IPLROM}" ] && IPLROM="${BIOSDIR}/IPL.n64"
if [ -f "${GAMEDIR}/${GAMENAME}.ndd" ] && [ -f "${IPLROM}" ]; then
RENMODE="1"
else
IPLROM=""
fi
mupen64plus --fullscreen --dd-ipl-rom "${IPLROM}" --dd-disk "${DDDISK}" "${FILE}" ;;
"""
)
HATARI_START = text(
"""
HATARI_TMP_CONFIG="$HATARI_TMP_DIR"/.hatari-temp.cfg
cp $HATARI_CONFIG_DIR/Atari-ST-default.cfg "$HATARI_TMP_CONFIG"
hatari --configfile "$HATARI_TMP_CONFIG" --disk-a "$1"
"""
)
HATARI_CFG = text(
"""
[HardDisk]
szHardDiskDirectory = /storage/.hatari
[ROM]
szTosImageFileName = /storage/roms/bios/tos.img
szCartridgeImageFileName =
"""
)
HATARI_MK = text(
"""
cp -R ${PKG_DIR}/config/* ${INSTALL}/usr/config/hatari
"""
)
ADVMAME_MK = text(
"""
if [ "${DEVICE}" == "OdroidGoAdvance" ]; then
cp -r ${PKG_DIR}/config/advmame.rc_oga ${INSTALL}/usr/config/emuelec/configs/advmame/advmame.rc
else
cp -r ${PKG_DIR}/config/advmame.rc ${INSTALL}/usr/config/emuelec/configs/advmame/advmame.rc
fi
"""
)
ADVMAME_RC = text(
"""
dir_rom /storage/roms/mame
dir_sample /storage/roms/bios/advmame/samples
"""
)
ADVMAME_SH = text(
"""
SDL_AUDIODRIVER=alsa advmame ${ARG} -quiet
"""
)
MAME_SH = text(
"""
if [ ! -f "/storage/roms/bios/mame/hash/fmtowns_cd.xml" ]; then
mkdir -p /storage/roms/bios/mame/hash
cp -rf "/usr/config/emuelec/configs/mame/hash/"fm* "/storage/roms/bios/mame/hash"
fi
if [ ! -f "/storage/roms/bios/mame/ini/mame.ini" ]; then
mkdir -p /storage/roms/bios/mame/ini
cp -rf "/usr/config/emuelec/configs/mame/ini/"mame* "/storage/roms/bios/mame/ini"
fi
if [ ! -f "/storage/.config/retroarch/config/MAME/MAME.opt" ]; then
cp -rf "/usr/config/emuelec/configs/mame/MAME/"MAME* "/storage/.config/retroarch/config/MAME"
fi
"""
)
MAME_INI = text(
"""
rompath /storage/roms/bios/mame/bios;/storage/roms/mame;/storage/roms/arcade
samplepath /storage/roms/bios/mame/samples
cfg_directory /storage/roms/bios/mame/mame/cfg
"""
)
MAME_OPT = text(
"""
mame_boot_to_bios = "disabled"
mame_read_config = "enabled"
"""
)
SCUMMVM_START = text(
"""
CONFIG_DIR="/storage/.config/scummvm"
if [ -d "/storage/.config/scummvm/extra" ]; then
EXTRA="--extrapath=/storage/.config/scummvm/extra"
fi
case $1 in
"sa")
scummvm --fullscreen --joystick=0 $EXTRA "${GAME}"
;;
"libretro")
retroarch -L /tmp/cores/scummvm_libretro.so --config ${RATMPCONF} "${GAME}"
;;
"add")
scummvm --add --path="/storage/roms/scummvm" --recursive
cp ${CONFIG_DIR}/scummvm.ini /storage/roms/bios/scummvm.ini
;;
esac
"""
)
DOLPHIN_SH = text(
"""
XDG_CONFIG_HOME=/emuelec/configs XDG_DATA_HOME=/storage/roms/dolphin /usr/bin/dolphin-emu-nogui -p fbdev "${1}"
"""
)
FILES = {
el.RUN_EMU: RUN_EMU,
el.UTILS: UTILS,
el.RETROARCH_MK: RETROARCH_MK,
f"{el.LIBRETRO}/freej2me/scripts/freej2me.sh": FREEJ2ME_SH,
f"{el.BIN}/pico8.sh": PICO8_SH,
f"{EMU}/flycastsa/scripts/flycast.sh": FLYCAST_SH,
f"{EMU}/flycast-dojo/scripts/flycastdojo.sh": FLYCASTDOJO_SH,
f"{EMU}/amiberry/package.mk": AMIBERRY_MK,
f"{EMU}/amiberry-lite/package.mk": AMIBERRY_MK.replace("amiberry/", "amiberry-lite/"),
f"{el.SX05RE}/tools/sound/timidity/package.mk": TIMIDITY_MK,
f"{EMU}/duckstation/config/settings.ini": DUCKSTATION_INI,
f"{EMU}/duckstation/package.mk": DUCKSTATION_MK,
f"{EMU}/duckstation/scripts/duckstation.sh": DUCKSTATION_SH,
el.YABASANSHIRO_SCRIPTS[0]: yabasanshiro_sh("yabasanshiro"),
el.YABASANSHIRO_SCRIPTS[1]: yabasanshiro_sh("yabasanshiro1_5"),
f"{EMU}/jzintv/scripts/jzintv.sh": JZINTV_SH,
f"{EMU}/mupen64plussa/mupen64plussa-core/m64p.sh": M64P_SH,
f"{EMU}/hatarisa/scripts/hatari.start": HATARI_START,
f"{EMU}/hatarisa/config/conf/Atari-ST-default.cfg": HATARI_CFG,
f"{EMU}/hatarisa/package.mk": HATARI_MK,
f"{EMU}/advancemame/package.mk": ADVMAME_MK,
f"{EMU}/advancemame/config/advmame.rc": ADVMAME_RC,
f"{EMU}/advancemame/config/advmame.rc_oga": ADVMAME_RC,
f"{EMU}/advancemame/bin/advmame.sh": ADVMAME_SH,
f"{el.LIBRETRO}/mame/scripts/mame.sh": MAME_SH,
f"{el.LIBRETRO}/mame/config/ini/mame.ini": MAME_INI,
f"{el.LIBRETRO}/mame/config/MAME/MAME.opt": MAME_OPT,
f"{EMU}/scummvmsa/bin/scummvm.start": SCUMMVM_START,
f"{EMU}/dolphinSA/scripts/dolphin.sh": DOLPHIN_SH,
}
def tree(**changes: str | None) -> RepoTree:
files = dict(FILES)
for path, content in changes.items():
if content is None:
files.pop(path, None)
else:
files[path] = content
return RepoTree.from_files(files)
def records(extractor, **changes: str | None) -> list[dict]:
built = tree(**changes)
return [r.entry() for r in extractor(built, el.Runs(built))]
def ref(path: str, lines: str) -> str:
return f"{path}:{lines}"
class Dispatch(unittest.TestCase):
def test_each_script_is_named_after_the_core_that_starts_it(self):
runs = el.Runs(tree())
self.assertEqual(runs.emulator("flycast.sh"), "flycastsa")
self.assertEqual(runs.emulator("flycastdojo.sh"), "flycast_dojo")
self.assertEqual(runs.emulator("m64p.sh"), "rice, glide64mk2")
self.assertEqual(runs.emulator("mame.sh"), "mame")
self.assertEqual(runs.emulator("freej2me.sh"), "freej2me")
def test_an_arm_without_a_core_test_names_its_platform(self):
self.assertEqual(el.Runs(tree()).emulator("pico8.sh"), "pico8")
def test_a_script_no_longer_started_raises(self):
runs = el.Runs(tree())
with self.assertRaises(LauncherPatternMissing):
runs.of("gone.sh")
def test_a_dispatch_without_any_script_raises(self):
with self.assertRaises(LauncherPatternMissing):
el.Runs(tree(**{el.RUN_EMU: "echo nothing\n"}))
class LibretroRecords(unittest.TestCase):
def test_retroarch_system_directory_comes_from_the_sed(self):
(record,) = records(el.retroarch)
self.assertEqual(record["search_dir"], "/storage/roms/bios")
self.assertEqual(record["kind"], "passes_path")
self.assertEqual(
record["source_ref"], f"{ref(el.RETROARCH_MK, '2')}, {ref(el.RUN_EMU, '80')}"
)
def test_retrorun_is_handed_the_folder_with_d(self):
(record,) = records(el.retrorun)
self.assertEqual(
(record["emulator"], record["search_dir"], record["passes_to"]),
("retrorun", "/storage/roms/bios", "retrorun -d"),
)
self.assertEqual(record["source_ref"], ref(el.RUN_EMU, "4, 81-82"))
def test_freej2me_gets_java_home_and_its_jar_copied(self):
java, jar = records(el.freej2me)
self.assertEqual(java["passes_to"], "environment JAVA_HOME")
self.assertEqual(java["search_dir"], "/storage/roms/bios/jdk")
self.assertEqual(jar["kind"], "copies")
self.assertEqual(jar["names"], ["freej2me-lr.jar"])
self.assertEqual(jar["passes_to"], "${HOME}/roms/bios")
self.assertEqual(jar["depends_on_option"], "/storage/roms/bios/jdk/eeversion exists")
def test_mame_copies_its_own_files_and_reads_only_path_options(self):
found = records(el.mame)
copies = [r for r in found if r["kind"] == "copies"]
self.assertEqual([r["names"] for r in copies], [["fm*"], ["mame*"]])
folders = [(r["passes_to"], r["search_dir"]) for r in found if r["kind"] == "passes_path"]
self.assertEqual(
folders,
[
("mame.ini rompath", "/storage/roms/bios/mame/bios"),
("mame.ini samplepath", "/storage/roms/bios/mame/samples"),
],
)
self.assertTrue(all(r["depends_on_option"] == "mame_read_config = enabled"
for r in found if r["kind"] == "passes_path"))
def test_mame_without_the_read_config_option_raises(self):
with self.assertRaises(LauncherPatternMissing):
records(el.mame, **{f"{el.LIBRETRO}/mame/config/MAME/MAME.opt": "x = 1\n"})
class StandaloneRecords(unittest.TestCase):
def test_pico8_moves_the_folder_and_runs_the_binary(self):
(record,) = records(el.pico8)
self.assertEqual(record["kind"], "moves")
self.assertEqual(record["names"], ["pico8_dyn"])
self.assertEqual(record["search_dir"], "/storage/roms/bios/pico-8")
self.assertEqual(record["passes_to"], "/emuelec/bin/pico-8")
self.assertEqual(
record["source_ref"],
f"{ref(el.RUN_EMU, '57-58')}, {ref(f'{el.BIN}/pico8.sh', '1-5, 8-10')}",
)
def test_pico8_without_the_removal_is_not_a_move(self):
changed = PICO8_SH.replace(" rm -rf /storage/roms/bios/pico-8\n", "")
with self.assertRaises(LauncherPatternMissing):
records(el.pico8, **{f"{el.BIN}/pico8.sh": changed})
def test_links_from_scripts_and_packages(self):
found = records(el.links)
self.assertEqual(
[(r["emulator"], r["search_dir"]) for r in found],
[
("flycastsa", "/storage/roms/bios/dc"),
("flycast_dojo", "/storage/roms/bios/dc_dojo"),
("amiberry", "/storage/roms/bios"),
("amiberry-lite", "/storage/roms/bios"),
("timidity", "/storage/roms/bios/timidity/freepats"),
],
)
self.assertEqual(
found[1]["source_ref"],
f"{ref(el.RUN_EMU, '26-27')}, "
f"{ref(f'{EMU}/flycast-dojo/scripts/flycastdojo.sh', '1-5')}",
)
self.assertEqual(
found[2]["passes_to"], "symlink /usr/config/amiberry/kickstarts -> /storage/roms/bios"
)
def test_a_script_without_a_bios_link_raises(self):
with self.assertRaises(LauncherPatternMissing):
records(el.links, **{f"{EMU}/flycastsa/scripts/flycast.sh": "flycast \"${1}\"\n"})
def test_duckstation_scans_the_folder_its_config_names(self):
(record,) = records(el.duckstation)
self.assertEqual((record["kind"], record["search_dir"]), ("scans_dir", "/storage/roms/bios"))
def test_duckstation_with_a_fixed_bios_path_raises(self):
changed = DUCKSTATION_INI.replace("PathPAL =", "PathPAL = /storage/roms/bios/scph5502.bin")
with self.assertRaises(LauncherPatternMissing):
records(el.duckstation, **{f"{EMU}/duckstation/config/settings.ini": changed})
def test_yabasanshiro_passes_the_bios_unless_hle_is_chosen(self):
new, old = records(el.yabasanshiro)
self.assertEqual((new["emulator"], old["emulator"]), ("yabasanshiroSA", "yabasanshiroSA1_5"))
self.assertEqual(new["names"], ["saturn_bios.bin"])
self.assertEqual(new["passes_to"], "yabasanshiro -b")
self.assertEqual(old["passes_to"], "yabasanshiro1_5 -b")
self.assertEqual(new["depends_on_option"], "hlebios != 1")
self.assertIs(new["fails_when_missing"], False)
def test_jzintv_is_handed_the_bios_folder(self):
(record,) = records(el.jzintv)
self.assertEqual((record["search_dir"], record["passes_to"]), ("/storage/roms/bios", "jzintv -p"))
def test_mupen64plus_tries_its_ipl_names_in_order(self):
(record,) = records(el.mupen64plus)
self.assertEqual(record["kind"], "names_searched")
self.assertEqual(record["names"], ["N64DD IPLROM (J).n64", "IPL.n64"])
self.assertEqual(record["search_dir"], "/storage/roms/bios/Mupen64plus")
self.assertEqual(record["passes_to"], "mupen64plus --dd-ipl-rom")
self.assertEqual(record["depends_on_option"], '-f "${GAMEDIR}/${GAMENAME}.ndd"')
def test_mupen64plus_without_a_fallback_raises(self):
changed = M64P_SH.replace('[ ! -f "${IPLROM}" ] && IPLROM="${BIOSDIR}/IPL.n64"\n', "")
with self.assertRaises(LauncherPatternMissing):
records(el.mupen64plus, **{f"{EMU}/mupen64plussa/mupen64plussa-core/m64p.sh": changed})
def test_hatari_reads_the_tos_named_by_its_default_config(self):
(record,) = records(el.hatari)
self.assertEqual(record["names"], ["tos.img"])
self.assertEqual(
record["passes_to"], "Atari-ST-default.cfg [ROM] szTosImageFileName (--configfile)"
)
def test_advancemame_one_record_when_every_device_agrees(self):
(record,) = records(el.advancemame)
self.assertEqual(record["search_dir"], "/storage/roms/bios/advmame/samples")
self.assertNotIn("device", record)
def test_advancemame_devices_that_disagree_raise(self):
changed = ADVMAME_RC.replace("bios/advmame", "bios/other")
with self.assertRaises(LauncherPatternMissing):
records(el.advancemame, **{f"{EMU}/advancemame/config/advmame.rc_oga": changed})
def test_scummvm_extrapath_and_the_ini_copied_for_the_core(self):
extra, ini = records(el.scummvm)
self.assertEqual(extra["passes_to"], "scummvm --extrapath")
self.assertEqual(extra["depends_on_option"], "/storage/.config/scummvm/extra exists")
self.assertEqual((ini["emulator"], ini["build"]), ("scummvm", "libretro"))
self.assertEqual(ini["search_dir"], "/storage/.config/scummvm")
self.assertEqual(ini["passes_to"], "/storage/roms/bios/scummvm.ini")
def test_dolphin_data_home(self):
(record,) = records(el.dolphin)
self.assertEqual(record["passes_to"], "environment XDG_DATA_HOME")
self.assertEqual(record["search_dir"], "/storage/roms/dolphin")
def test_midi_sources_that_read_the_bios_folder(self):
found = records(el.midi)
self.assertEqual(
[(r["emulator"], r["kind"], r["names"], r["search_dir"]) for r in found],
[
("fluidsynth", "fixed_name", ["sc/SC-55.sf2"], "/storage/roms/bios"),
("mt32d", "passes_path", [], "/roms/bios"),
],
)
self.assertEqual(found[0]["depends_on_option"], "ra_midi_output = fluidsynth")
self.assertEqual(
found[1]["source_ref"], f"{ref(el.RUN_EMU, '7-9')}, {ref(el.UTILS, '1, 10-11')}"
)
def test_every_extractor_runs_on_the_fixtures(self):
found = el.extract_launchers(tree())
self.assertEqual(len(found), 26)
class Revision(unittest.TestCase):
def test_the_revision_is_the_one_the_bios_list_is_read_at(self):
url = (
"https://raw.githubusercontent.com/EmuELEC/EmuELEC/v4.8/"
"packages/sx05re/emuelec/bin/batocera/batocera-systems"
)
self.assertEqual(launcher_revision(url), "v4.8")
def test_a_url_without_a_revision_raises(self):
with self.assertRaises(ValueError):
launcher_revision("https://example.org/batocera-systems")
class PlatformRecords(unittest.TestCase):
"""The launchers platforms/emuelec.yml carries, as the scraper wrote them."""
@classmethod
def setUpClass(cls) -> None:
cls.config = yaml.safe_load((REPO_ROOT / "platforms" / "emuelec.yml").read_text())
def test_the_platform_carries_launchers(self):
self.assertTrue(self.config.get("launchers"))
def test_every_reference_is_in_the_emuelec_packages(self):
for record in self.config["launchers"]:
for part in record["source_ref"].split(", "):
if ":" in part:
self.assertTrue(part.startswith(f"{el.SX05RE}/"), record)
if __name__ == "__main__":
unittest.main()