From 5c93c6dc6b2826f22cacd0dbe6b5808e75f2b192 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 08:12:19 +0100 Subject: [PATCH] refactor: one emudeck script-to-profile rule The target scraper kept its own copy of the EmuScripts table and took .ps1 files the platform scraper never read, so emudeck-we's template skeleton was published as a core and removed afterwards by an override. Both scrapers now read emulator_of_script, and the platform scraper lists the installers at the pinned revision, as launchers: and the SteamOS target already did. Targets rescraped, override dropped. --- install/targets/emudeck.json | 2 + platforms/targets/_overrides.yml | 7 --- platforms/targets/emudeck.yml | 7 +-- scripts/scraper/emudeck_launchers.py | 13 +++-- scripts/scraper/emudeck_scraper.py | 15 +++--- .../targets/emudeck_targets_scraper.py | 46 +++------------- tests/test_launchers_emudeck.py | 2 + tests/test_scraper_contract.py | 2 +- tests/test_target_scrapers.py | 53 +++++++++++++++++++ 9 files changed, 86 insertions(+), 61 deletions(-) diff --git a/install/targets/emudeck.json b/install/targets/emudeck.json index 36f30d211..4ef016714 100644 --- a/install/targets/emudeck.json +++ b/install/targets/emudeck.json @@ -83,6 +83,7 @@ "galaksija", "gam4980", "gambatte", + "gametank", "gearboy", "gearcoleco", "geargrafx", @@ -346,6 +347,7 @@ "galaksija", "gam4980", "gambatte", + "gametank", "gearboy", "gearcoleco", "geargrafx", diff --git a/platforms/targets/_overrides.yml b/platforms/targets/_overrides.yml index 1080067b0..d07226f89 100644 --- a/platforms/targets/_overrides.yml +++ b/platforms/targets/_overrides.yml @@ -121,13 +121,6 @@ retrodeck: # includes the frontend name alongside the actual cores. remove_cores: [retroarch] -emudeck: - targets: - _default: - # EmuDeck's EmuScripts tree carries a template.sh placeholder used - # to scaffold new installers. It is not an emulator. - remove_cores: [template] - lakka: targets: generic: diff --git a/platforms/targets/emudeck.yml b/platforms/targets/emudeck.yml index 49ac73621..c445457fb 100644 --- a/platforms/targets/emudeck.yml +++ b/platforms/targets/emudeck.yml @@ -1,6 +1,6 @@ platform: emudeck -source: https://github.com/dragoonDorise/EmuDeck -scraped_at: '2026-10-17T18:43:57Z' +source: https://github.com/dragoonDorise/EmuDeck/tree/dddb30229adbbd54d5ff7283cfd70e11c4322c26 +scraped_at: '2026-11-06T07:10:22Z' targets: steamos: architecture: x86_64 @@ -88,6 +88,7 @@ targets: - galaksija - gam4980 - gambatte + - gametank - gearboy - gearcoleco - geargrafx @@ -352,6 +353,7 @@ targets: - galaksija - gam4980 - gambatte + - gametank - gearboy - gearcoleco - geargrafx @@ -487,7 +489,6 @@ targets: - supermodel - swanstation - tamalibretro - - template - tgbdual - theodore - thepowdertoy diff --git a/scripts/scraper/emudeck_launchers.py b/scripts/scraper/emudeck_launchers.py index cbb7252ba..2723d62d8 100644 --- a/scripts/scraper/emudeck_launchers.py +++ b/scripts/scraper/emudeck_launchers.py @@ -46,14 +46,18 @@ STORAGE = "${storagePath}" FLATPAK_DATA = "${HOME}/.var/app/" # EmuScripts file names that differ from the emulator profile they set up. +# The platform and target scrapers both read them here. SCRIPT_EMULATORS = { "pcsx2qt": "pcsx2", "rpcs3legacy": "rpcs3", "cemuproton": "cemu", "rmg": "mupen64plus_next", } -# The main RetroArch config script installs no emulator. -NOT_EMULATORS = {"retroarch_maincfg"} +# The main RetroArch config script installs no emulator, and emudeck-we's +# emuDeckTemplate.ps1 is the skeleton new installers are copied from. +NOT_EMULATORS = {"retroarch_maincfg", "template"} +# EmuDeck's SteamOS installers are shell scripts, emudeck-we's PowerShell. +SCRIPT_SUFFIXES = (".sh", ".ps1") # What the emulator does with a directory written into each key. DIRECTORY_KINDS = { @@ -66,9 +70,10 @@ DIRECTORY_KINDS = { def emulator_of_script(file_name: str) -> str | None: """The emulator an EmuScripts file sets up, None for a helper script.""" - if not file_name.endswith(".sh"): + stem, dot, suffix = file_name.rpartition(".") + if not dot or f".{suffix}" not in SCRIPT_SUFFIXES: return None - key = re.sub(r"^emuDeck", "", file_name[: -len(".sh")], flags=re.IGNORECASE).lower() + key = re.sub(r"^emuDeck", "", stem, flags=re.IGNORECASE).lower() if not key or key in NOT_EMULATORS: return None return SCRIPT_EMULATORS.get(key, key) diff --git a/scripts/scraper/emudeck_scraper.py b/scripts/scraper/emudeck_scraper.py index 4f72adefb..d4fadacf0 100644 --- a/scripts/scraper/emudeck_scraper.py +++ b/scripts/scraper/emudeck_scraper.py @@ -469,9 +469,10 @@ class Scraper(BaseScraper): version = fetch_github_latest_version("dragoonDorise/EmuDeck") - cores = self._fetch_installed_emulators() + revision = launcher_revision(self.checkbios_url) + cores = self._fetch_installed_emulators(revision) standalone = [core for core in cores if core != "retroarch"] - tree = RepoTree.github(EMUDECK_REPO, launcher_revision(self.checkbios_url)) + tree = RepoTree.github(EMUDECK_REPO, revision) libretro = set(installed_libretro_cores(tree.read(RETROARCH_SCRIPT))) return { @@ -501,16 +502,16 @@ class Scraper(BaseScraper): "launchers": extract_launchers(tree), } - def _fetch_installed_emulators(self) -> list[str]: - """Fetch the list of emulators installed by EmuDeck from EmuScripts. - - Returns core names normalized to match emulator profile keys. + def _fetch_installed_emulators(self, revision: str) -> list[str]: + """The emulators EmuDeck's EmuScripts install at the pinned revision, + named as their profiles are: the launcher layer and the SteamOS + target are read at that revision too. """ import json api_url = ( "https://api.github.com/repos/dragoonDorise/EmuDeck/" - "contents/functions/EmuScripts" + f"contents/functions/EmuScripts?ref={revision}" ) try: req = urllib.request.Request(api_url, headers=github_headers()) diff --git a/scripts/scraper/targets/emudeck_targets_scraper.py b/scripts/scraper/targets/emudeck_targets_scraper.py index 0e69d055e..fa8fa75b2 100644 --- a/scripts/scraper/targets/emudeck_targets_scraper.py +++ b/scripts/scraper/targets/emudeck_targets_scraper.py @@ -12,7 +12,6 @@ from __future__ import annotations import argparse import json -import re import sys import urllib.error import urllib.request @@ -22,6 +21,7 @@ import yaml from common import github_token, yaml_load +from ..emudeck_launchers import SCRIPT_SUFFIXES, emulator_of_script from . import BaseTargetScraper PLATFORM_NAME = "emudeck" @@ -33,24 +33,6 @@ WINDOWS_API = ( "https://api.github.com/repos/EmuDeck/emudeck-we/contents/functions/EmuScripts" ) -# Map EmuDeck script names to emulator profile keys -# Script naming: emuDeckDolphin.sh -> dolphin -# Some need explicit mapping when names differ -_NAME_OVERRIDES: dict[str, str] = { - "pcsx2qt": "pcsx2", - "rpcs3legacy": "rpcs3", - "cemuproton": "cemu", - "rmg": "mupen64plus_next", - "bigpemu": "bigpemu", - "eden": "eden", - "suyu": "suyu", - "ares": "ares", -} - -# Scripts that are not emulators (config helpers, etc.) -_SKIP = {"retroarch_maincfg", "retroarch"} - - def _fetch(url: str) -> str: headers = {"User-Agent": "retrobios-scraper/1.0"} token = github_token() @@ -69,26 +51,13 @@ def _list_emuscripts(api_url: str) -> list[str]: """List emulator script filenames from GitHub API.""" entries = json.loads(_fetch(api_url)) names = [ - e["name"] for e in entries if e.get("name", "").endswith((".sh", ".ps1")) + e["name"] for e in entries if e.get("name", "").endswith(SCRIPT_SUFFIXES) ] if not names: raise RuntimeError(f"no emulator scripts listed at {api_url}") return names -def _script_to_core(filename: str) -> str | None: - """Convert EmuScripts filename to core profile key.""" - # Strip extension and emuDeck prefix - name = re.sub(r"\.(sh|ps1)$", "", filename, flags=re.IGNORECASE) - name = re.sub(r"^emuDeck", "", name, flags=re.IGNORECASE) - if not name: - return None - key = name.lower() - if key in _SKIP: - return None - return _NAME_OVERRIDES.get(key, key) - - class Scraper(BaseTargetScraper): """Fetches emulator lists for EmuDeck SteamOS and Windows targets.""" @@ -114,14 +83,13 @@ class Scraper(BaseTargetScraper): seen: set[str] = set() has_retroarch = False for script in scripts: - core = _script_to_core(script) - if core and core not in seen: + core = emulator_of_script(script) + # RetroArch stands for its libretro cores, added below. + if core == "retroarch": + has_retroarch = True + elif core and core not in seen: seen.add(core) cores.append(core) - # Detect RetroArch presence (provides all libretro cores) - name = re.sub(r"\.(sh|ps1)$", "", script, flags=re.IGNORECASE) - if name.lower() in ("emudeckretroarch", "retroarch_maincfg"): - has_retroarch = True standalone_count = len(cores) # EmuDeck ships RetroArch = all its libretro cores are available diff --git a/tests/test_launchers_emudeck.py b/tests/test_launchers_emudeck.py index 314994972..33d7596fe 100644 --- a/tests/test_launchers_emudeck.py +++ b/tests/test_launchers_emudeck.py @@ -373,6 +373,8 @@ class Helpers(unittest.TestCase): 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")) + self.assertEqual(el.emulator_of_script("emuDeckPCSX2QT.ps1"), "pcsx2") + self.assertIsNone(el.emulator_of_script("emuDeckTemplate.ps1")) def test_the_revision_comes_from_the_pinned_url(self): url = ( diff --git a/tests/test_scraper_contract.py b/tests/test_scraper_contract.py index bfe7f1553..cf9e04c20 100644 --- a/tests/test_scraper_contract.py +++ b/tests/test_scraper_contract.py @@ -724,7 +724,7 @@ class UnreadableReleaseStopsTheScrape(unittest.TestCase): with mock.patch("urllib.request.urlopen", self._refuse), self.assertRaises( RuntimeError ): - emudeck_scraper.Scraper()._fetch_installed_emulators() + emudeck_scraper.Scraper(checkbios_url="pinned")._fetch_installed_emulators("0" * 40) def test_libretro_core_metadata_raises(self): """libretro dropped every system name and manufacturer when the diff --git a/tests/test_target_scrapers.py b/tests/test_target_scrapers.py index c2c6626fb..6b05e356c 100644 --- a/tests/test_target_scrapers.py +++ b/tests/test_target_scrapers.py @@ -541,6 +541,59 @@ class TargetsReadThePlatformRevision(unittest.TestCase): self.assertTrue(steamos) self.assertTrue(all(u.endswith(f"?ref={pin}") for u in steamos), steamos) + def test_emudeck_platform_lists_its_installers_at_the_pin(self): + """cores: was read on master while launchers: and the SteamOS target + were read at the pin: the YAML described two revisions.""" + from scripts.scraper import emudeck_scraper + + pin = "d" * 40 + seen: list[str] = [] + + class _Body: + def __init__(self, url): + seen.append(url) + + def __enter__(self): + return self + + def __exit__(self, *_exc): + return False + + def read(self): + return json.dumps([{"name": "emuDeckRMG.sh"}]).encode() + + with mock.patch("urllib.request.urlopen", lambda req, **_kw: _Body(req.full_url)): + cores = emudeck_scraper.Scraper(checkbios_url="pinned")._fetch_installed_emulators(pin) + self.assertEqual(cores, ["mupen64plus_next"]) + self.assertEqual(len(seen), 1) + self.assertTrue(seen[0].endswith(f"?ref={pin}"), seen) + + +class EmuDeckScriptNamesHaveOneRule(unittest.TestCase): + """The target scraper kept its own copy of the EmuScripts-to-profile + table: it published emudeck-we's template skeleton as a core, and a + rename added to one copy only made --target filter that core out.""" + + def test_the_target_scraper_names_scripts_as_the_platform_does(self): + from scripts.scraper.emudeck_launchers import emulator_of_script + + listing = [ + "emuDeckRMG.sh", + "emuDeckPCSX2QT.ps1", + "emuDeckTemplate.ps1", + "emuDeckRetroArch.ps1", + "RetroArch_maincfg.sh", + "emuDeckXemu.ps1", + ] + scraper = emudeck_targets_scraper.Scraper(revision="0" * 40) + with mock.patch.object( + emudeck_targets_scraper, "_list_emuscripts", return_value=listing + ), mock.patch.object(scraper, "_load_retroarch_cores", return_value=["mgba"]): + cores = scraper._fetch_cores_for_target("unused", "Windows") + standalone = {emulator_of_script(name) for name in listing} - {None, "retroarch"} + self.assertEqual(standalone, {"mupen64plus_next", "pcsx2", "xemu"}) + self.assertEqual(cores, sorted(standalone | {"mgba"})) + class HashScrapersRefuseAnUnknownVersion(unittest.TestCase): """mame_hash_scraper wrote core_version "unknown" after a failed API call