mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-21 11:19:11 -05:00
The scraper walks each package's functions the way RetroPie runs them in one shell (assignments, arrays, for loops, brace expansion, $md_id, calls into other packages) and records every link, copy, move and setting that names $biosdir, the addEmulator flags (sdltrs -romfile3), the BIOS read outside $biosdir (basilisk --rom, gngeopi -B) and the launch scripts that exit without their file (fs-uae kick13.rom, reicast dc_boot.bin). Each package also gets a package record with its rp_module_repo and the systems it registers, which maps a package to the build it compiles. The schema gains package, repo, systems and the moves and package kinds. platforms/retropie.yml is regenerated at 46128cd2: only the launchers section is new.
365 lines
15 KiB
Python
365 lines
15 KiB
Python
"""RetroPie's launcher layer read from the scriptmodules.
|
|
|
|
Each fixture copies the lines an extractor reads from RetroPie-Setup
|
|
46128cd252cdecae852a4d0bafa2aaf9d03ab563, placed at the line numbers they
|
|
have there, so the source_ref a record carries is the one platforms/retropie.yml
|
|
cites. A construct that is gone or unknown must stop the scrape.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import json
|
|
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 batocera_launchers # noqa: E402
|
|
from scraper import retropie_launchers as rl # noqa: E402
|
|
|
|
EMULATORS = "scriptmodules/emulators"
|
|
LIBRETRO = "scriptmodules/libretrocores"
|
|
|
|
|
|
def at(first_line: int, text: str) -> str:
|
|
"""Text placed so its first line is line `first_line` of the file."""
|
|
return "\n" * (first_line - 1) + textwrap.dedent(text).lstrip("\n")
|
|
|
|
|
|
def header(package: str, repo: str | None = None, repo_line: int = 15) -> str:
|
|
lines = {12: f'rp_module_id="{package}"'}
|
|
if repo is not None:
|
|
lines[repo_line] = f'rp_module_repo="{repo}"'
|
|
return "\n".join(lines.get(n, "") for n in range(1, max(lines) + 1)) + "\n"
|
|
|
|
|
|
def module(package: str, body: str, first_line: int, repo: str | None = None,
|
|
repo_line: int = 15) -> str:
|
|
head = header(package, repo, repo_line)
|
|
padding = first_line - 1 - head.count("\n")
|
|
return head + "\n" * padding + textwrap.dedent(body).lstrip("\n")
|
|
|
|
|
|
def records(files: dict[str, str], package: str | None = None) -> list[dict]:
|
|
found = rl.extract_launchers(files)
|
|
return [r for r in found if package is None or r["package"] == package]
|
|
|
|
|
|
PX68K = module("px68k", """
|
|
function configure_px68k() {
|
|
mkRomDir "x68000"
|
|
|
|
moveConfigDir "$home/.keropi" "$md_conf_root/x68000"
|
|
mkUserDir "$biosdir/keropi"
|
|
|
|
local bios
|
|
for bios in cgrom.dat iplrom30.dat iplromco.dat iplrom.dat iplromxv.dat; do
|
|
if [[ -f "$biosdir/$bios" ]]; then
|
|
mv "$biosdir/$bios" "$biosdir/keropi/$bios"
|
|
fi
|
|
ln -sf "$biosdir/keropi/$bios" "$md_conf_root/x68000/$bios"
|
|
done
|
|
|
|
addEmulator 1 "$md_id" "x68000" "$md_inst/px68k.sdl2 %ROM%"
|
|
addSystem "x68000"
|
|
}
|
|
""", 47, repo="git https://github.com/TurtleBazooka/px68k.git master")
|
|
|
|
REDREAM = module("redream", """
|
|
function configure_redream() {
|
|
mkUserDir "$biosdir/dc"
|
|
ln -sf "$biosdir/dc/dc_boot.bin" "$md_inst/boot.bin"
|
|
ln -sf "$biosdir/dc/dc_flash.bin" "$md_inst/flash.bin"
|
|
}
|
|
""", 56)
|
|
|
|
SDLTRS = module("sdltrs", """
|
|
function configure_sdltrs() {
|
|
local common_args
|
|
mkRomDir "trs-80"
|
|
|
|
common_args="-fullscreen -nomousepointer -showled"
|
|
addEmulator 1 "$md_id-model1" "trs-80" "$md_inst/sdl2trs $common_args -m1 -romfile $biosdir/level2.rom -disk0 %ROM%"
|
|
addEmulator 0 "$md_id-model3" "trs-80" "$md_inst/sdl2trs $common_args -m3 -romfile3 $biosdir/level3.rom -disk0 %ROM%"
|
|
addEmulator 0 "$md_id-model4" "trs-80" "$md_inst/sdl2trs $common_args -m4 -romfile3 $biosdir/level4.rom -disk0 %ROM%"
|
|
addEmulator 0 "$md_id-model4p" "trs-80" "$md_inst/sdl2trs $common_args -m4p -romfile4p $biosdir/level4p.rom -disk0 %ROM%"
|
|
addSystem "trs-80"
|
|
}
|
|
""", 44)
|
|
|
|
BASILISK = module("basilisk", """
|
|
function configure_basilisk() {
|
|
addEmulator 1 "$md_id" "macintosh" "$md_inst/bin/BasiliskII --rom $romdir/macintosh/mac.rom --disk $romdir/macintosh/disk.img"
|
|
addSystem "macintosh"
|
|
}
|
|
""", 55, repo="git https://github.com/cebix/macemu.git master", repo_line=16)
|
|
|
|
GNGEOPI = module("gngeopi", """
|
|
function configure_gngeopi() {
|
|
addEmulator 0 "$md_id" "arcade" "$md_inst/bin/gngeo -i $romdir/neogeo -B $md_inst/neogeobios %ROM%"
|
|
addEmulator 0 "$md_id" "neogeo" "$md_inst/bin/gngeo -i $romdir/neogeo -B $md_inst/neogeobios %ROM%"
|
|
addSystem "arcade"
|
|
addSystem "neogeo"
|
|
}
|
|
""", 59)
|
|
|
|
FS_UAE = module("fs-uae", """
|
|
function configure_fs-uae() {
|
|
mkRomDir "amiga"
|
|
|
|
addEmulator 1 "$md_id" "amiga" "CON:bash $md_inst/bin/fs-uae.sh %ROM%"
|
|
addSystem "amiga"
|
|
}
|
|
""", 76)
|
|
|
|
FS_UAE_LAUNCHER = at(15, """
|
|
rootdir="/opt/retropie"
|
|
datadir="$HOME/RetroPie"
|
|
romdir="$datadir/roms/amiga"
|
|
biosdir="$datadir/BIOS"
|
|
kickfile="$biosdir/kick13.rom"
|
|
|
|
source "$rootdir/lib/archivefuncs.sh"
|
|
|
|
if [[ ! -f "$kickfile" ]]; then
|
|
dialog --no-cancel --pause "You need to copy the Amiga kickstart file (kick13.rom) to the folder $biosdir to boot the Amiga emulator." 22 76 15
|
|
exit 1
|
|
fi
|
|
""") + "\n" * 14 + 'fs-uae --floppy_drive_0="$rom" --kickstart_file="$kickfile" --floppies_dir="$romdir"\n'
|
|
|
|
DOSBOX = module("dosbox", """
|
|
function configure_dosbox() {
|
|
local def=0
|
|
mkRomDir "pc"
|
|
addEmulator "$def" "$md_id" "pc" "bash $romdir/pc/launcher %ROM%"
|
|
addSystem "pc"
|
|
|
|
cat > "$romdir/pc/launcher" << _EOF_
|
|
function midi_synth() {
|
|
echo start
|
|
}
|
|
_EOF_
|
|
}
|
|
""", 64)
|
|
|
|
DOSBOX_SDL2 = module("dosbox-sdl2", """
|
|
function configure_dosbox-sdl2() {
|
|
configure_dosbox
|
|
}
|
|
""", 42, repo="git https://github.com/duganchen/dosbox.git master", repo_line=16)
|
|
|
|
FBNEO = module("lr-fbneo", """
|
|
function configure_lr-fbneo() {
|
|
local systems=(
|
|
"arcade"
|
|
"neogeo"
|
|
"pcengine"
|
|
)
|
|
|
|
local system
|
|
for system in "${systems[@]}"; do
|
|
addSystem "$system"
|
|
done
|
|
}
|
|
""", 94)
|
|
|
|
MAME2003 = module("lr-mame2003", """
|
|
function _get_dir_name_lr-mame2003() {
|
|
echo "mame2003"
|
|
}
|
|
|
|
function configure_lr-mame2003() {
|
|
addSystem "arcade"
|
|
local dir_name="$(_get_dir_name_${md_id})"
|
|
cp "$md_inst/metadata/"{hiscore.dat,cheat.dat} "$biosdir/$dir_name/"
|
|
if [[ "$md_id" == "lr-mame2003-plus" ]]; then
|
|
cp "$md_inst/metadata/artwork/"* "$biosdir/$dir_name/artwork/"
|
|
fi
|
|
}
|
|
""", 19)
|
|
|
|
MAME2003_PLUS = module("lr-mame2003-plus", """
|
|
function _get_dir_name_lr-mame2003-plus() {
|
|
echo "mame2003-plus"
|
|
}
|
|
|
|
function configure_lr-mame2003-plus() {
|
|
configure_lr-mame2003
|
|
}
|
|
""", 19)
|
|
|
|
|
|
class LinksAndCopies(unittest.TestCase):
|
|
def test_px68k_links_only_its_five_names(self):
|
|
found = records({f"{EMULATORS}/px68k.sh": PX68K}, "px68k")
|
|
links = next(r for r in found if r["kind"] == "links")
|
|
self.assertEqual(
|
|
links["names"],
|
|
["cgrom.dat", "iplrom30.dat", "iplromco.dat", "iplrom.dat", "iplromxv.dat"],
|
|
)
|
|
self.assertNotIn("cgrom30.dat", links["names"])
|
|
self.assertEqual(links["search_dir"], "$biosdir/keropi")
|
|
self.assertEqual(links["passes_to"], "$md_conf_root/x68000/<name>")
|
|
self.assertEqual(links["source_ref"], f"{EMULATORS}/px68k.sh:58")
|
|
|
|
def test_px68k_moves_a_root_copy_first(self):
|
|
found = records({f"{EMULATORS}/px68k.sh": PX68K}, "px68k")
|
|
moves = next(r for r in found if r["kind"] == "moves")
|
|
self.assertEqual((moves["search_dir"], moves["passes_to"]), ("$biosdir", "$biosdir/keropi"))
|
|
self.assertEqual(moves["depends_on_option"], '[[ -f "$biosdir/$bios" ]]')
|
|
self.assertEqual(moves["source_ref"], f"{EMULATORS}/px68k.sh:56")
|
|
|
|
def test_redream_renames_both_files(self):
|
|
found = records({f"{EMULATORS}/redream.sh": REDREAM}, "redream")
|
|
links = next(r for r in found if r["kind"] == "links")
|
|
self.assertEqual(links["names"], ["dc_boot.bin", "dc_flash.bin"])
|
|
self.assertEqual(links["passes_to"], "$md_inst/boot.bin, $md_inst/flash.bin")
|
|
self.assertEqual(links["source_ref"], f"{EMULATORS}/redream.sh:58-59")
|
|
|
|
def test_a_copy_kept_only_for_the_package_the_test_names(self):
|
|
files = {
|
|
f"{LIBRETRO}/lr-mame2003.sh": MAME2003,
|
|
f"{LIBRETRO}/lr-mame2003-plus.sh": MAME2003_PLUS,
|
|
}
|
|
base = [r for r in records(files, "lr-mame2003") if r["kind"] == "copies"]
|
|
plus = [r for r in records(files, "lr-mame2003-plus") if r["kind"] == "copies"]
|
|
self.assertEqual([r["passes_to"] for r in base], ["$biosdir/mame2003"])
|
|
self.assertEqual(
|
|
[r["passes_to"] for r in plus],
|
|
["$biosdir/mame2003-plus", "$biosdir/mame2003-plus/artwork"],
|
|
)
|
|
self.assertEqual(base[0]["names"], ["hiscore.dat", "cheat.dat"])
|
|
|
|
|
|
class CommandLines(unittest.TestCase):
|
|
def test_sdltrs_passes_one_rom_per_machine(self):
|
|
found = records({f"{EMULATORS}/sdltrs.sh": SDLTRS}, "sdltrs")
|
|
found = [r for r in found if r["kind"] == "fixed_name"]
|
|
self.assertEqual(
|
|
[(r["emulator"], r["names"], r["passes_to"]) for r in found],
|
|
[
|
|
("sdltrs-model1", ["level2.rom"], "command line -romfile"),
|
|
("sdltrs-model3", ["level3.rom"], "command line -romfile3"),
|
|
("sdltrs-model4", ["level4.rom"], "command line -romfile3"),
|
|
("sdltrs-model4p", ["level4p.rom"], "command line -romfile4p"),
|
|
],
|
|
)
|
|
self.assertEqual(found[2]["source_ref"], f"{EMULATORS}/sdltrs.sh:51")
|
|
|
|
def test_a_bios_read_outside_the_bios_folder(self):
|
|
files = {f"{EMULATORS}/basilisk.sh": BASILISK, f"{EMULATORS}/gngeopi.sh": GNGEOPI}
|
|
rom = next(r for r in records(files, "basilisk") if r["kind"] == "fixed_name")
|
|
self.assertEqual((rom["names"], rom["search_dir"]), (["mac.rom"], "$romdir/macintosh"))
|
|
bios = next(r for r in records(files, "gngeopi") if r["kind"] == "passes_path")
|
|
self.assertEqual(bios["search_dir"], "$md_inst/neogeobios")
|
|
self.assertEqual(bios["source_ref"], f"{EMULATORS}/gngeopi.sh:60-61")
|
|
|
|
def test_a_flag_dropped_from_the_command_raises(self):
|
|
files = {
|
|
f"{EMULATORS}/basilisk.sh": BASILISK.replace("--rom ", "--romfile "),
|
|
f"{EMULATORS}/gngeopi.sh": GNGEOPI,
|
|
}
|
|
with self.assertRaises(rl.LauncherPatternMissing):
|
|
rl.extract_launchers(files)
|
|
|
|
|
|
class LaunchScripts(unittest.TestCase):
|
|
def test_fs_uae_refuses_to_start_without_kick13(self):
|
|
files = {f"{EMULATORS}/fs-uae.sh": FS_UAE, f"{EMULATORS}/fs-uae/fs-uae.sh": FS_UAE_LAUNCHER}
|
|
check = next(r for r in records(files, "fs-uae") if r["kind"] == "fixed_name")
|
|
self.assertEqual(check["names"], ["kick13.rom"])
|
|
self.assertEqual(check["search_dir"], "$HOME/RetroPie/BIOS")
|
|
self.assertEqual(check["passes_to"], "command line --kickstart_file")
|
|
message = check["fails_when_missing"]
|
|
self.assertTrue(message.startswith("You need to copy the Amiga kickstart"))
|
|
self.assertEqual(check["source_ref"], f"{EMULATORS}/fs-uae/fs-uae.sh:23-26, 41")
|
|
|
|
def test_a_launch_script_without_a_check_raises(self):
|
|
script = FS_UAE_LAUNCHER.replace("exit 1", "true")
|
|
files = {f"{EMULATORS}/fs-uae.sh": FS_UAE, f"{EMULATORS}/fs-uae/fs-uae.sh": script}
|
|
with self.assertRaises(rl.LauncherPatternMissing):
|
|
records(files)
|
|
|
|
|
|
class Packages(unittest.TestCase):
|
|
def test_the_package_names_what_it_builds_and_registers(self):
|
|
found = records({f"{EMULATORS}/px68k.sh": PX68K}, "px68k")
|
|
package = next(r for r in found if r["kind"] == "package")
|
|
self.assertEqual(package["repo"], "git https://github.com/TurtleBazooka/px68k.git master")
|
|
self.assertEqual(package["systems"], ["x68000"])
|
|
self.assertEqual(package["source_ref"], f"{EMULATORS}/px68k.sh:12, 15, 62")
|
|
|
|
def test_a_package_inherits_the_systems_of_the_function_it_runs(self):
|
|
files = {f"{EMULATORS}/dosbox.sh": DOSBOX, f"{EMULATORS}/dosbox-sdl2.sh": DOSBOX_SDL2}
|
|
package = next(r for r in records(files, "dosbox-sdl2") if r["kind"] == "package")
|
|
self.assertEqual(package["systems"], ["pc"])
|
|
self.assertIn(f"{EMULATORS}/dosbox.sh:68", package["source_ref"])
|
|
|
|
def test_systems_from_a_multi_line_array(self):
|
|
found = records({f"{LIBRETRO}/lr-fbneo.sh": FBNEO}, "lr-fbneo")
|
|
package = next(r for r in found if r["kind"] == "package")
|
|
self.assertEqual(package["systems"], ["arcade", "neogeo", "pcengine"])
|
|
self.assertEqual(package["build"], "libretro")
|
|
|
|
def test_a_system_name_left_unresolved_raises(self):
|
|
broken = FBNEO.replace('"${systems[@]}"', '"${others[@]}"')
|
|
with self.assertRaises(rl.LauncherPatternMissing):
|
|
records({f"{LIBRETRO}/lr-fbneo.sh": broken})
|
|
|
|
|
|
class UnknownConstructs(unittest.TestCase):
|
|
def test_an_unclassified_use_of_the_bios_folder_raises(self):
|
|
broken = REDREAM.replace('mkUserDir "$biosdir/dc"', 'rsync -a "$biosdir/dc" "$md_inst"')
|
|
with self.assertRaises(rl.LauncherPatternMissing):
|
|
records({f"{EMULATORS}/redream.sh": broken})
|
|
|
|
def test_scripts_without_a_package_raise(self):
|
|
with self.assertRaises(rl.LauncherPatternMissing):
|
|
rl.Scripts({f"{EMULATORS}/redream.sh": "function x() {\n}\n"})
|
|
|
|
|
|
class PlatformRecords(unittest.TestCase):
|
|
"""The launchers platforms/retropie.yml carries, as the scraper wrote them."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
cls.config = yaml.safe_load((REPO_ROOT / "platforms" / "retropie.yml").read_text())
|
|
cls.schema = json.loads((REPO_ROOT / "schemas" / "platform.schema.json").read_text())
|
|
|
|
def test_one_package_record_per_core(self):
|
|
packages = [r["package"] for r in self.config["launchers"] if r["kind"] == "package"]
|
|
self.assertEqual(len(packages), len(set(packages)))
|
|
stripped = {p.removeprefix("lr-") for p in packages}
|
|
self.assertEqual(stripped, set(self.config["cores"]))
|
|
|
|
def test_every_record_belongs_to_a_package(self):
|
|
packages = {r["package"] for r in self.config["launchers"] if r["kind"] == "package"}
|
|
for record in self.config["launchers"]:
|
|
self.assertIn(record["package"], packages, record)
|
|
self.assertTrue(record["source_ref"].startswith("scriptmodules/"), record)
|
|
|
|
def test_the_standalone_builds_name_their_own_project(self):
|
|
repos = {
|
|
r["package"]: r.get("repo") for r in self.config["launchers"] if r["kind"] == "package"
|
|
}
|
|
self.assertEqual(repos["minivmac"], "git https://github.com/minivmac/minivmac.git master")
|
|
self.assertEqual(repos["quasi88"], "git https://github.com/winterheart/quasi88.git master")
|
|
self.assertEqual(repos["px68k"], "git https://github.com/TurtleBazooka/px68k.git master")
|
|
|
|
def test_the_schema_describes_every_scraper_field(self):
|
|
fields = {
|
|
f.name for cls in (batocera_launchers.Launcher, rl.Launcher)
|
|
for f in dataclasses.fields(cls)
|
|
} - {"lines"}
|
|
self.assertEqual(fields, set(self.schema["$defs"]["launcher"]["properties"]))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|