mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-21 11:19:11 -05:00
The scraper reads the EmuScripts at the commit checkBIOS.sh is pinned to: the BIOS paths changeLine, iniFieldUpdate and RetroArch_setConfigOverride write, the ares and melonDS configs whose paths sed rewrites, and the bios/ directories linked to Flycast, Azahar, the Switch emulators and shadPS4. The script to emulator mapping moves beside the extractors so both read one table.
409 lines
16 KiB
Python
409 lines
16 KiB
Python
"""EmuDeck's launcher layer read from its EmuScripts.
|
|
|
|
Each fixture is a cut-down copy of the lines an extractor reads at
|
|
dragoonDorise/EmuDeck dddb30229adbbd54d5ff7283cfd70e11c4322c26. The records
|
|
must carry the names in the order the script writes them, point at the
|
|
fixture's own lines, and a construct that is gone must stop the scrape.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
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 emudeck_launchers as el # noqa: E402
|
|
from scraper.emudeck_scraper import launcher_revision # noqa: E402
|
|
from scraper.shell_launchers import LauncherPatternMissing, RepoTree, words # noqa: E402
|
|
|
|
S = el.EMUSCRIPTS
|
|
|
|
|
|
def text(block: str) -> str:
|
|
return textwrap.dedent(block).lstrip("\n")
|
|
|
|
|
|
ARES_SH = text(
|
|
"""
|
|
ares_configFile="$HOME/.var/app/dev.ares.ares/data/ares/settings.bml"
|
|
ares_setEmulationFolder(){
|
|
UserROMsPath='/home/deck/Emulation/roms/'
|
|
sed -i "s|$UserROMsPath|${romsPath}\\/|g" "$ares_configFile"
|
|
UserBIOSPath='/home/deck/Emulation/bios/'
|
|
sed -i "s|$UserBIOSPath|${biosPath}\\/|g" "$ares_configFile"
|
|
}
|
|
"""
|
|
)
|
|
|
|
ARES_BML = text(
|
|
"""
|
|
ColecoVision
|
|
Path: /home/deck/Emulation/roms/colecovision/
|
|
Firmware
|
|
BIOS.World: /home/deck/Emulation/bios/colecovision.rom
|
|
Visible: true
|
|
PCEngineCD
|
|
Path: /home/deck/Emulation/roms/pcenginecd/
|
|
Firmware
|
|
BIOS.US: /home/deck/Emulation/bios/syscard3u.pce
|
|
BIOS.Japan: /home/deck/Emulation/bios/syscard3.pce
|
|
Visible: true
|
|
NeoGeoMVS
|
|
Path: /home/deck/Emulation/roms/arcade/
|
|
Firmware
|
|
BIOS.World
|
|
Visible: true
|
|
"""
|
|
)
|
|
|
|
MELONDS_SH = text(
|
|
"""
|
|
melonDS_configFile="$HOME/.var/app/net.kuribo64.melonDS/config/melonDS/melonDS.ini"
|
|
melonDS_resolutionFile="$HOME/.var/app/net.kuribo64.melonDS/config/melonDS/melonDS.toml"
|
|
melonDS_init(){
|
|
sed -i "s|/run/media/mmcblk0p1/Emulation|$emulationPath|g" "$melonDS_configFile"
|
|
sed -i "s|/run/media/mmcblk0p1/Emulation|$emulationPath|g" "$melonDS_resolutionFile"
|
|
}
|
|
melonDS_setEmulationFolder(){
|
|
BIOS9PathSetting='BIOS9Path='
|
|
BIOS7PathSetting='BIOS7Path='
|
|
LastROMFolderSetting='LastROMFolder='
|
|
changeLine "$BIOS9PathSetting" "${BIOS9PathSetting}${biosPath}/bios9.bin" "${melonDS_configFile}"
|
|
changeLine "$BIOS7PathSetting" "${BIOS7PathSetting}${biosPath}/bios7.bin" "${melonDS_configFile}"
|
|
changeLine "$LastROMFolderSetting" "${LastROMFolderSetting}${romsPath}/nds" "${melonDS_configFile}"
|
|
}
|
|
melonDS_setResolution(){
|
|
changeLine "ScaleFactor = " "$scaleLine" "$melonDS_resolutionFile"
|
|
}
|
|
melonDS_setABXYstyle(){
|
|
buttonA='Joy_A='
|
|
changeLine "$buttonA" "$buttonA""0" "${melonDS_configFile}"
|
|
}
|
|
"""
|
|
)
|
|
|
|
MELONDS_INI = text(
|
|
"""
|
|
ExternalBIOSEnable=0
|
|
BIOS9Path=/run/media/mmcblk0p1/Emulation/bios/bios9.bin
|
|
BIOS7Path=/run/media/mmcblk0p1/Emulation/bios/bios7.bin
|
|
"""
|
|
)
|
|
|
|
MELONDS_TOML = text(
|
|
"""
|
|
LastROMFolder = "/run/media/mmcblk0p1/Emulation/roms/ds"
|
|
[Emu]
|
|
ExternalBIOSEnable = false
|
|
[DS]
|
|
FirmwarePath = "/run/media/mmcblk0p1/Emulation/bios/firmware.bin"
|
|
BIOS7Path = "/run/media/mmcblk0p1/Emulation/bios/bios7.bin"
|
|
[DSi]
|
|
NANDPath = "/run/media/mmcblk0p1/Emulation/bios/dsi_nand.bin"
|
|
"""
|
|
)
|
|
|
|
XEMU_SH = text(
|
|
"""
|
|
Xemu_configFile="$HOME/.var/app/app.xemu.xemu/data/xemu/xemu/xemu.toml"
|
|
Xemu_setEmulationFolder(){
|
|
bootrrom_path='bootrom_path = '
|
|
flashrom_path='flashrom_path = '
|
|
eeprom_path='eeprom_path = '
|
|
bootrrom_pathSetting="${bootrrom_path}""'${biosPath}/mcpx_1.0.bin'"
|
|
flashrom_pathSetting="${flashrom_path}""'${biosPath}/Complex_4627v1.03.bin'"
|
|
eeprom_pathSetting="${eeprom_path}""'${storagePath}/xemu/eeprom.bin'"
|
|
changeLine "${bootrrom_path}" "${bootrrom_pathSetting}" "$Xemu_configFile"
|
|
changeLine "${flashrom_path}" "${flashrom_pathSetting}" "$Xemu_configFile"
|
|
changeLine "${eeprom_path}" "${eeprom_pathSetting}" "$Xemu_configFile"
|
|
}
|
|
"""
|
|
)
|
|
|
|
DUCKSTATION_SH = text(
|
|
"""
|
|
DuckStation_configFile="$HOME/.local/share/duckstation/settings.ini"
|
|
DuckStation_setEmulationFolder(){
|
|
biosDir='SearchDirectory = '
|
|
biosDirSetting="${biosDir}""${biosPath}"
|
|
statesDir='SaveStates = '
|
|
statesDirSetting="${statesDir}""${savesPath}/duckstation/states"
|
|
changeLine "$biosDir" "$biosDirSetting" "$DuckStation_configFile"
|
|
changeLine "$statesDir" "$statesDirSetting" "$DuckStation_configFile"
|
|
}
|
|
"""
|
|
)
|
|
|
|
PCSX2_SH = text(
|
|
"""
|
|
PCSX2QT_configFile="$HOME/.config/PCSX2/inis/PCSX2.ini"
|
|
PCSX2QT_setEmulationFolder() {
|
|
iniFieldUpdate "$PCSX2QT_configFile" "UI" "StartFullscreen" "true"
|
|
iniFieldUpdate "$PCSX2QT_configFile" "Folders" "Bios" "${biosPath}"
|
|
iniFieldUpdate "$PCSX2QT_configFile" "Folders" "Snapshots" "${storagePath}/pcsx2/snaps"
|
|
}
|
|
"""
|
|
)
|
|
|
|
MAME_SH = text(
|
|
"""
|
|
MAME_configFile="$HOME/.mame/mame.ini"
|
|
MAME_setEmulationFolder(){
|
|
gameDirOpt='rompath '
|
|
newGameDirOpt="$gameDirOpt""${romsPath}/arcade;${biosPath};${biosPath}/mame"
|
|
changeLine "$gameDirOpt" "$newGameDirOpt" "$MAME_configFile"
|
|
}
|
|
"""
|
|
)
|
|
|
|
RETROARCH_SH = text(
|
|
"""
|
|
RetroArch_configFile="$HOME/.var/app/org.libretro.RetroArch/config/retroarch/retroarch.cfg"
|
|
RetroArch_setEmulationFolder(){
|
|
RetroArch_setConfigOverride 'system_directory' "\\"${biosPath}\\"" "$RetroArch_configFile"
|
|
RetroArch_setConfigOverride 'rgui_browser_directory' "\\"${romsPath}\\"" "$RetroArch_configFile"
|
|
}
|
|
"""
|
|
)
|
|
|
|
FLYCAST_SH = text(
|
|
"""
|
|
Flycast_configFile="$HOME/.var/app/org.flycast.Flycast/config/flycast/emu.cfg"
|
|
Flycast_setEmulationFolder(){
|
|
mkdir -p "${biosPath}/flycast/"
|
|
ln -sn "$HOME/.var/app/org.flycast.Flycast/data/flycast/" "${biosPath}/flycast/bios"
|
|
}
|
|
"""
|
|
)
|
|
|
|
CITRON_SH = text(
|
|
"""
|
|
Citron_setupStorage(){
|
|
unlink "${biosPath}/citron/keys" 2>/dev/null
|
|
mkdir -p "${biosPath}/citron"
|
|
ln -sn "$HOME/.local/share/citron/keys/" "${biosPath}/citron/keys"
|
|
ln -sn "$HOME/.local/share/citron/nand/system/Contents/registered/" "${biosPath}/citron/firmware"
|
|
}
|
|
"""
|
|
)
|
|
|
|
SHADPS4_SH = text(
|
|
"""
|
|
ShadPS4_dir="$HOME/.local/share/shadPS4"
|
|
ShadPS4_setupStorage(){
|
|
mkdir -p "${biosPath}/shadps4/"
|
|
ln -sn "$ShadPS4_dir/sys_modules" "${biosPath}/shadps4/sys_modules"
|
|
}
|
|
"""
|
|
)
|
|
|
|
|
|
def tree(**scripts: str) -> RepoTree:
|
|
files = {f"{S}/{name}": body for name, body in scripts.items()}
|
|
files[f"{el.CONFIGS}/dev.ares.ares/data/ares/settings.bml"] = ARES_BML
|
|
files[f"{el.CONFIGS}/net.kuribo64.melonDS/config/melonDS/melonDS.ini"] = MELONDS_INI
|
|
files[f"{el.CONFIGS}/net.kuribo64.melonDS/config/melonDS/melonDS.toml"] = MELONDS_TOML
|
|
return RepoTree.from_files(files)
|
|
|
|
|
|
class WrittenIntoTheConfig(unittest.TestCase):
|
|
def test_ares_firmware_follows_the_rewritten_prefix(self):
|
|
colecovision, pcecd = (r.entry() for r in el.ares(tree(**{"emuDeckares.sh": ARES_SH})))
|
|
self.assertEqual(
|
|
(colecovision["slot"], colecovision["names"]), ("ColecoVision", ["colecovision.rom"])
|
|
)
|
|
self.assertEqual(pcecd["names"], ["syscard3u.pce", "syscard3.pce"])
|
|
self.assertEqual(pcecd["search_dir"], "${biosPath}")
|
|
self.assertEqual(
|
|
pcecd["passes_to"],
|
|
"settings.bml PCEngineCD/Firmware/BIOS.US, PCEngineCD/Firmware/BIOS.Japan",
|
|
)
|
|
self.assertEqual(
|
|
pcecd["source_ref"],
|
|
f"{el.CONFIGS}/dev.ares.ares/data/ares/settings.bml:9-10, {S}/emuDeckares.sh:1, 5-6",
|
|
)
|
|
|
|
def test_a_firmware_key_without_a_value_is_not_a_name(self):
|
|
records = [r.entry() for r in el.ares(tree(**{"emuDeckares.sh": ARES_SH}))]
|
|
self.assertNotIn("NeoGeoMVS", [r["slot"] for r in records])
|
|
|
|
def test_ares_without_the_bios_rewrite_raises(self):
|
|
script = ARES_SH.replace("${biosPath}", "${romsPath}")
|
|
with self.assertRaises(LauncherPatternMissing):
|
|
el.ares(tree(**{"emuDeckares.sh": script}))
|
|
|
|
def test_melonds_ini_keys_and_toml_rewrite(self):
|
|
ini, toml = (r.entry() for r in el.melonds(tree(**{"emuDeckMelonDS.sh": MELONDS_SH})))
|
|
self.assertEqual(ini["names"], ["bios9.bin", "bios7.bin"])
|
|
self.assertEqual(ini["search_dir"], "${biosPath}")
|
|
self.assertEqual(ini["passes_to"], "melonDS.ini BIOS9Path, BIOS7Path")
|
|
self.assertEqual(
|
|
ini["depends_on_option"], "ExternalBIOSEnable = 0 in the shipped melonDS.ini"
|
|
)
|
|
self.assertEqual(toml["names"], ["firmware.bin", "bios7.bin", "dsi_nand.bin"])
|
|
self.assertEqual(toml["search_dir"], "${emulationPath}/bios")
|
|
self.assertEqual(
|
|
toml["passes_to"], "melonDS.toml [DS] FirmwarePath, BIOS7Path [DSi] NANDPath"
|
|
)
|
|
self.assertEqual(
|
|
toml["depends_on_option"], "ExternalBIOSEnable = false in the shipped melonDS.toml"
|
|
)
|
|
|
|
def test_a_config_the_setters_rewrite_keeps_their_values(self):
|
|
records = [r.entry() for r in el.melonds(tree(**{"emuDeckMelonDS.sh": MELONDS_SH}))]
|
|
self.assertEqual(
|
|
[r["search_dir"] for r in records], ["${biosPath}", "${emulationPath}/bios"]
|
|
)
|
|
|
|
def test_files_are_grouped_by_directory(self):
|
|
bios, storage = (
|
|
r.entry()
|
|
for r in el.setter_records(tree(**{"emuDeckXemu.sh": XEMU_SH}), "emuDeckXemu.sh")
|
|
)
|
|
self.assertEqual(bios["names"], ["mcpx_1.0.bin", "Complex_4627v1.03.bin"])
|
|
self.assertEqual(bios["passes_to"], "xemu.toml bootrom_path, flashrom_path")
|
|
self.assertEqual(
|
|
(storage["search_dir"], storage["names"]), ("${storagePath}/xemu", ["eeprom.bin"])
|
|
)
|
|
self.assertEqual(bios["source_ref"], f"{S}/emuDeckXemu.sh:1-10")
|
|
|
|
def test_a_bios_directory_is_left_to_the_emulator(self):
|
|
(record,) = (
|
|
r.entry()
|
|
for r in el.setter_records(
|
|
tree(**{"emuDeckDuckStation.sh": DUCKSTATION_SH}), "emuDeckDuckStation.sh"
|
|
)
|
|
)
|
|
self.assertEqual(record["kind"], "scans_dir")
|
|
self.assertEqual(record["search_dir"], "${biosPath}")
|
|
self.assertEqual(record["passes_to"], "settings.ini SearchDirectory")
|
|
(pcsx2,) = (
|
|
r.entry()
|
|
for r in el.setter_records(tree(**{"emuDeckPCSX2QT.sh": PCSX2_SH}), "emuDeckPCSX2QT.sh")
|
|
)
|
|
self.assertEqual(
|
|
(pcsx2["emulator"], pcsx2["passes_to"]), ("pcsx2", "PCSX2.ini [Folders] Bios")
|
|
)
|
|
|
|
def test_a_search_path_list_is_kept_whole(self):
|
|
(record,) = (
|
|
r.entry()
|
|
for r in el.setter_records(tree(**{"emuDeckMAME.sh": MAME_SH}), "emuDeckMAME.sh")
|
|
)
|
|
self.assertEqual(record["search_dir"], "${romsPath}/arcade;${biosPath};${biosPath}/mame")
|
|
self.assertEqual(record["passes_to"], "mame.ini rompath")
|
|
|
|
def test_retroarch_is_handed_the_directory(self):
|
|
(record,) = (
|
|
r.entry()
|
|
for r in el.setter_records(
|
|
tree(**{"emuDeckRetroArch.sh": RETROARCH_SH}), "emuDeckRetroArch.sh"
|
|
)
|
|
)
|
|
self.assertEqual(
|
|
(record["build"], record["kind"], record["search_dir"]),
|
|
("libretro", "passes_path", "${biosPath}"),
|
|
)
|
|
|
|
def test_an_unknown_directory_key_raises(self):
|
|
script = DUCKSTATION_SH.replace("SearchDirectory", "BiosFolder")
|
|
with self.assertRaises(LauncherPatternMissing):
|
|
el.setter_records(tree(**{"emuDeckDuckStation.sh": script}), "emuDeckDuckStation.sh")
|
|
|
|
def test_a_script_that_writes_no_bios_path_raises(self):
|
|
script = PCSX2_SH.replace('"Bios" "${biosPath}"', '"Bios" "${storagePath}/bios"')
|
|
with self.assertRaises(LauncherPatternMissing):
|
|
el.setter_records(tree(**{"emuDeckPCSX2QT.sh": script}), "emuDeckPCSX2QT.sh")
|
|
|
|
|
|
class LinkedIntoBios(unittest.TestCase):
|
|
def records(self) -> list[dict]:
|
|
# Every link script must hold a BIOS link: the ones not under test reuse Flycast's.
|
|
scripts = {name: FLYCAST_SH for name in el.LINK_SCRIPTS}
|
|
scripts.update({"emuDeckCitron.sh": CITRON_SH, "emuDeckShadPS4.sh": SHADPS4_SH})
|
|
return [r.entry() for r in el.links(tree(**scripts))]
|
|
|
|
def test_the_link_lives_in_bios_and_points_at_the_emulator(self):
|
|
flycast = next(r for r in self.records() if r["emulator"] == "flycast")
|
|
self.assertEqual(flycast["search_dir"], "${biosPath}/flycast/bios")
|
|
self.assertEqual(
|
|
flycast["passes_to"],
|
|
"symlink ${biosPath}/flycast/bios -> ${HOME}/.var/app/org.flycast.Flycast/data/flycast/",
|
|
)
|
|
self.assertEqual(flycast["source_ref"], f"{S}/emuDeckFlycast.sh:4")
|
|
|
|
def test_each_directory_of_a_different_name_is_its_own_record(self):
|
|
citron = [r for r in self.records() if r["emulator"] == "citron"]
|
|
self.assertEqual(
|
|
[r["search_dir"] for r in citron],
|
|
["${biosPath}/citron/keys", "${biosPath}/citron/firmware"],
|
|
)
|
|
|
|
def test_a_variable_in_the_target_is_expanded(self):
|
|
shadps4 = next(r for r in self.records() if r["emulator"] == "shadps4")
|
|
self.assertEqual(
|
|
shadps4["passes_to"],
|
|
"symlink ${biosPath}/shadps4/sys_modules -> ${HOME}/.local/share/shadPS4/sys_modules",
|
|
)
|
|
self.assertEqual(shadps4["source_ref"], f"{S}/emuDeckShadPS4.sh:1, 4")
|
|
|
|
def test_a_script_without_a_bios_link_raises(self):
|
|
scripts = {name: FLYCAST_SH for name in el.LINK_SCRIPTS}
|
|
scripts["emuDeckCitron.sh"] = 'ln -sn "$HOME/a" "$HOME/b"\n'
|
|
with self.assertRaises(LauncherPatternMissing):
|
|
el.links(tree(**scripts))
|
|
|
|
|
|
class Helpers(unittest.TestCase):
|
|
def test_a_quote_ends_a_variable_name(self):
|
|
self.assertEqual(
|
|
[w.text for w in words('changeLine "$buttonA""0" "${f}"')],
|
|
["changeLine", "${buttonA}0", "${f}"],
|
|
)
|
|
|
|
def test_script_names_map_to_the_emulator_profile(self):
|
|
self.assertEqual(el.emulator_of_script("emuDeckRMG.sh"), "mupen64plus_next")
|
|
self.assertEqual(el.emulator_of_script("emuDeckPCSX2QT.sh"), "pcsx2")
|
|
self.assertEqual(el.emulator_of_script("emuDeckXemu.sh"), "xemu")
|
|
self.assertIsNone(el.emulator_of_script("RetroArch_maincfg.sh"))
|
|
self.assertIsNone(el.emulator_of_script("README.md"))
|
|
|
|
def test_the_revision_comes_from_the_pinned_url(self):
|
|
url = (
|
|
"https://raw.githubusercontent.com/dragoonDorise/EmuDeck/"
|
|
"dddb30229adbbd54d5ff7283cfd70e11c4322c26/functions/checkBIOS.sh"
|
|
)
|
|
self.assertEqual(launcher_revision(url), "dddb30229adbbd54d5ff7283cfd70e11c4322c26")
|
|
with self.assertRaises(ValueError):
|
|
launcher_revision(
|
|
"https://raw.githubusercontent.com/dragoonDorise/EmuDeck/main/functions/checkBIOS.sh"
|
|
)
|
|
|
|
|
|
class PlatformRecords(unittest.TestCase):
|
|
"""The launchers platforms/emudeck.yml carries, as the scraper wrote them."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
cls.config = yaml.safe_load((REPO_ROOT / "platforms" / "emudeck.yml").read_text())
|
|
|
|
def test_every_emulator_is_one_emudeck_installs(self):
|
|
cores = set(self.config["cores"])
|
|
self.assertEqual(sorted({r["emulator"] for r in self.config["launchers"]} - cores), [])
|
|
|
|
def test_every_reference_is_an_emuscript_or_a_shipped_config(self):
|
|
for record in self.config["launchers"]:
|
|
for part in record["source_ref"].split(", "):
|
|
if ":" in part:
|
|
self.assertTrue(part.startswith((f"{S}/", f"{el.CONFIGS}/")), record)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|