From 8a4b7aa8e649f0de8e0f1da2821160c813ea5c67 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Mon, 9 Nov 2026 22:22:57 +0100 Subject: [PATCH] fix: keep buildbot cores a standalone name hides Batocera's x86_64 RetroArch downloads the amiberry, cemu and xemu cores although the image builds standalone emulators of those names. They now stay in installable_cores, run both ways, and reach only the boards whose downloader offers them. --- platforms/batocera.yml | 14 +++++++ platforms/knulli.yml | 1 + scripts/common.py | 57 ++++++++++++++++++++++++++--- scripts/scraper/batocera_scraper.py | 18 ++++++--- scripts/scraper/knulli_scraper.py | 2 +- tests/test_standalone_links.py | 18 ++++++++- tests/test_target_scrapers.py | 7 +++- 7 files changed, 104 insertions(+), 13 deletions(-) diff --git a/platforms/batocera.yml b/platforms/batocera.yml index bde62cb95..99023f988 100644 --- a/platforms/batocera.yml +++ b/platforms/batocera.yml @@ -5803,9 +5803,11 @@ installable_cores: - 3dengine - DoubleCherryGB - amiarcadia +- amiberry - anarch - applewin - ardens +- azahar - b2 - bbkemu - boom3_xp @@ -5818,7 +5820,9 @@ installable_cores: - bsnes_mercury_accuracy - bsnes_mercury_balanced - bsnes_mercury_performance +- cannonball - cdi2015 +- cemu - chailove - citra - citra2018 @@ -5830,10 +5834,13 @@ installable_cores: - dingooemu - dinothawr - dirksimple +- dosbox - dosbox_core - dosbox_svn - dosbox_svn_ce - doukutsu_rs +- ecwolf +- eduke32 - ep128emu_core - fbalpha2012 - fbalpha2012_cps1 @@ -5843,6 +5850,7 @@ installable_cores: - fixgb - fixnes - frodo +- fsuae - galaksija - gam4980 - gametank @@ -5901,6 +5909,7 @@ installable_cores: - odin - omicron - openlara +- openmsx - panda3ds - parallel_dc - pcee2 @@ -5917,6 +5926,7 @@ installable_cores: - remotejoy - retro8 - romcleaner +- rpcs3 - rust_dos - rustynes - sameboy @@ -5928,6 +5938,7 @@ installable_cores: - spmp8000emu - squirreljme - stella2023 +- supermodel - tamalibretro - test - thepowdertoy @@ -5942,13 +5953,16 @@ installable_cores: - vice_xcbm5x0 - video_processor - virtualxt +- vita3k - vitaquake2-rogue - vitaquake2-xatrix - vitaquake2-zaero - vitaquake3 - wqxemu +- xemu - xenia_edge - yabause +- ymir libretro_systems: "81": - zx81 diff --git a/platforms/knulli.yml b/platforms/knulli.yml index dd5b3eff2..8d17ac162 100644 --- a/platforms/knulli.yml +++ b/platforms/knulli.yml @@ -4914,6 +4914,7 @@ installable_cores: - bsnes_mercury_accuracy - bsnes_mercury_balanced - bsnes_mercury_performance +- cannonball - chailove - crocods - dinothawr diff --git a/scripts/common.py b/scripts/common.py index 2dce4b0f6..8fa1fcf4e 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -2758,6 +2758,19 @@ def system_dir_path(entry: dict, path: str) -> str: return f"{directory}/{path}" if directory else path +def launched_both_ways(config: dict) -> set[str]: + """The names a platform runs as a standalone emulator and as a core. + + ``dual_mode_cores`` names those its frontend offers both ways. A name it + gives a standalone emulator only, whose buildbot core its RetroArch can + download (``installable_cores``), runs both ways too: Batocera's x86_64 + RetroArch installs the amiberry core beside its own standalone Amiberry. + """ + standalone = {str(c) for c in config.get("standalone_cores") or []} + installable = {str(c) for c in config.get("installable_cores") or []} + return {str(c) for c in config.get("dual_mode_cores") or []} | (standalone & installable) + + def standalone_set(config: dict) -> set[str]: """The emulators a platform launches standalone, with its dual-mode ones. @@ -2766,7 +2779,7 @@ def standalone_set(config: dict) -> set[str]: same set, tagged, so every reader of standalone_cores sees them. """ names = {str(c) for c in config.get("standalone_cores") or []} - names |= {DUAL_MODE_PREFIX + str(c) for c in config.get("dual_mode_cores") or []} + names |= {DUAL_MODE_PREFIX + c for c in launched_both_ways(config)} # The system directory each core is handed travels the same way, so # platform_layouts places a core's files below it for every reader. names |= { @@ -3191,6 +3204,7 @@ def resolve_platform_cores( """ cores_config = config.get("cores") platform_key = str(config.get("_key") or "") + downloaded_only: dict[str, str] = {} # The builds the platform compiles itself, by the names they answer to. # Such a name means that build there, and another revision of the same # project is not what the platform runs: ROCKNIX and RetroPie build only @@ -3265,14 +3279,23 @@ def resolve_platform_cores( # build too. Batocera, Recalbox and RetroBat run pcsx2 standalone and # as LRPS2, two profiles: the key match kept the standalone one and # LRPS2's files never reached their packs. - result |= { - profile - for c in config.get("dual_mode_cores") or [] - for profile in index.get(str(c), set()) + dual_names = {str(c) for c in config.get("dual_mode_cores") or []} + both_ways = { + profile: c + for c in sorted(launched_both_ways(config)) + for profile in index.get(c, set()) if "libretro" in str(profiles[profile].get("type", "")) and not profiles[profile].get("built_by") and not replaced_by_own(profile, profiles[profile], libretro_only=True) } + # A core reached only through the RetroArch downloader runs on the + # boards whose downloader offers it: Batocera's x86_64 RetroArch + # installs the amiberry core, its armv7 and aarch64 boards cannot. + downloaded_only = { + profile: c for profile, c in both_ways.items() + if c not in dual_names and profile not in result + } + result |= set(both_ways) # Support "all_libretro" as a list element: combines all libretro # profiles with explicitly listed standalone cores (e.g. RetroDECK # ships RetroArch + standalone emulators) @@ -3291,9 +3314,33 @@ def resolve_platform_cores( if target_cores is not None: result = result & designated_profiles(profiles, target_cores) + result -= { + profile for profile, c in downloaded_only.items() + if not downloadable_on(target_cores, c) + } return result +@functools.lru_cache(maxsize=1) +def _retroarch_target_sets() -> tuple[frozenset[str], ...]: + platforms = str(Path(__file__).resolve().parent.parent / "platforms") + return tuple( + frozenset(found) + for t in list_available_targets("retroarch", platforms) + if (found := load_target_config("retroarch", t["name"], platforms)) + ) + + +def downloadable_on(target_cores: set[str], name: str) -> bool: + """Whether a board's core set holds a RetroArch download list offering `name`. + + A board with a downloader carries a whole RetroArch targets entry in its + set (load_target_config, `downloads`), a board without one only what it + builds. + """ + return any(name in found and found <= target_cores for found in _retroarch_target_sets()) + + def unresolved_platform_cores(config: dict, profiles: dict[str, dict]) -> set[str]: """The core names a platform lists that no profile answers to. diff --git a/scripts/scraper/batocera_scraper.py b/scripts/scraper/batocera_scraper.py index 36a014dd8..49bf18192 100644 --- a/scripts/scraper/batocera_scraper.py +++ b/scripts/scraper/batocera_scraper.py @@ -14,7 +14,7 @@ import re import sys import urllib.error import urllib.request -from collections.abc import Callable +from collections.abc import Callable, Iterable from pathlib import Path @@ -557,19 +557,27 @@ def boards_build(selection: BuildSelection, launchers: list[dict]) -> Callable[[ return built -def installable_cores(selection: BuildSelection, cores: list[str]) -> list[str]: +def installable_cores( + selection: BuildSelection, + cores: list[str], + standalone: Iterable[str] = (), + dual: Iterable[str] = (), +) -> list[str]: """Buildbot cores a board's RetroArch core downloader installs, beyond the cores the image builds. The targets scraper names, per board, the RetroArch targets entry its downloader reads (`downloads`): a Batocera x86_64 board installs any - core of linux-x86_64, an armv7 one any of linux-armhf. + core of linux-x86_64, an armv7 one any of linux-armhf. A name the image + gives a standalone emulator only (Batocera's amiberry, cemu, xemu) is + not that core: the buildbot one stays installable. """ found: set[str] = set() for entry in selection.targets.values(): if entry.get("downloads"): found |= load_target_config("retroarch", entry["downloads"]) or set() - return sorted(found - set(cores)) + standalone_only = set(standalone) - set(dual) + return sorted(found - (set(cores) - standalone_only)) def records_run(launchers: list[dict], built: Callable[[str], bool]) -> list[dict]: @@ -642,7 +650,7 @@ class Scraper(BaseScraper): built = boards_build(selection, launchers) launchers = records_run(launchers, built) cores, standalone, dual = self._fetch_cores(built) - installable = installable_cores(selection, cores) + installable = installable_cores(selection, cores, standalone, dual) libretro_systems = es_systems_libretro_systems( self._fetch_text(ES_SYSTEMS_URL.format(tag=self.tag)), _not_cores() ) diff --git a/scripts/scraper/knulli_scraper.py b/scripts/scraper/knulli_scraper.py index 0522ae53a..6ec04da61 100644 --- a/scripts/scraper/knulli_scraper.py +++ b/scripts/scraper/knulli_scraper.py @@ -141,7 +141,7 @@ class Scraper(BaseScraper): built = boards_build(selection, launchers) launchers = records_run(launchers, built) cores, standalone, dual = configgen_cores(defaults, es_systems, dropped, built) - installable = installable_cores(selection, cores) + installable = installable_cores(selection, cores, standalone, dual) # Each system's own list (psx, es_systems.yml:640-653: libretro # pcsx_rearmed, swanstation, mednafen_psx, then duckstation and # duckstation-legacy). diff --git a/tests/test_standalone_links.py b/tests/test_standalone_links.py index 2a44c45d6..853595da9 100644 --- a/tests/test_standalone_links.py +++ b/tests/test_standalone_links.py @@ -687,7 +687,7 @@ class AmiberryBuildEachPlatformShips(unittest.TestCase): "rocknix": {"amiberry-5.6.5"}, "amberelec": {"amiberry-7.1.1"}, "emuelec": {"amiberry-8.0.0"}, - "batocera": {"amiberry-7.1.1-332"}, + "batocera": {"amiberry", "amiberry-7.1.1-332"}, "knulli": {"amiberry-7.1.1"}, "recalbox": {"amiberry-8.2.2"}, "retrobat": {"amiberry", "amiberry-8.3.0"}, @@ -711,6 +711,22 @@ class AmiberryBuildEachPlatformShips(unittest.TestCase): with self.subTest(platform=platform): self.assertEqual(self.resolved(platform), own) + def test_the_buildbot_core_runs_where_retroarch_installs_it(self) -> None: + from common import load_target_config, resolve_platform_cores + + for platform in ("retroarch", "lakka", "batocera"): + with self.subTest(platform=platform): + self.assertIn("amiberry", self.resolved(platform)) + cfg = load_platform_config("batocera", str(REPO_ROOT / "platforms")) + # x86_64 boards download from linux-x86_64, which builds the core. + # linux-armhf does not, and aarch64 boards have no downloader. + for target, runs in (("x86_64", True), ("zen3", True), ("h3", False), ("bcm2711", False)): + cores = load_target_config("batocera", target, str(REPO_ROOT / "platforms")) + with self.subTest(target=target): + resolved = resolve_platform_cores(cfg, self.profiles, cores) + self.assertEqual("amiberry" in resolved, runs) + self.assertIn("amiberry-7.1.1-332", resolved) + def test_retropie_reads_kickstarts_below_its_start_folder(self) -> None: # amiberry 5.7 and 6.3 read /kickstarts, which RetroPie # links to bios/amiga. Its whdboot and roms folders stay below it. diff --git a/tests/test_target_scrapers.py b/tests/test_target_scrapers.py index 8c17df288..8fb503723 100644 --- a/tests/test_target_scrapers.py +++ b/tests/test_target_scrapers.py @@ -298,8 +298,13 @@ class CoreDownloaderReach(unittest.TestCase): fetched |= load_target_config("retroarch", entry["downloads"], root) or set() if entry["architecture"] == "aarch64": self.assertNotIn("downloads", entry) + # A name the image gives a standalone emulator only is not the + # buildbot core of that name, which stays installable. + standalone_only = set(config.get("standalone_cores") or []) - set( + config.get("dual_mode_cores") or []) with self.subTest(platform=platform): - self.assertEqual(set(config["installable_cores"]), fetched - set(config["cores"])) + self.assertEqual(set(config["installable_cores"]), + fetched - (set(config["cores"]) - standalone_only)) class KnulliPerBoard(unittest.TestCase):