From 53c550433068e1bcd021c514bbd4ae2af7b2f696 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 6 Oct 2026 12:44:32 +0200 Subject: [PATCH] fix: read extract folders from the registry --- platforms/_registry.yml | 12 +++++++ scripts/generate_readme.py | 47 ++++++++++++++++++--------- scripts/generate_site.py | 40 ++++++++++++----------- tests/test_extract_targets.py | 60 +++++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 33 deletions(-) create mode 100644 tests/test_extract_targets.py diff --git a/platforms/_registry.yml b/platforms/_registry.yml index e2a8c722..0ad0d19f 100644 --- a/platforms/_registry.yml +++ b/platforms/_registry.yml @@ -148,6 +148,7 @@ platforms: retroarch: config: retroarch.yml status: active + extract_to: system/ logo: https://raw.githubusercontent.com/libretro/RetroArch/master/media/retroarch-vector_invader-only.svg scraper: libretro source_url: https://raw.githubusercontent.com/libretro/libretro-database/master/dat/System.dat @@ -182,6 +183,7 @@ platforms: batocera: config: batocera.yml status: active + extract_to: /userdata/bios/ logo: https://raw.githubusercontent.com/batocera-linux/batocera-emulationstation/master/resources/splash_batocera.svg scraper: batocera source_url: https://raw.githubusercontent.com/batocera-linux/batocera.linux/master/package/batocera/core/batocera-scripts/scripts/batocera-systems @@ -407,6 +409,7 @@ platforms: recalbox: config: recalbox.yml status: active + extract_to: /recalbox/share/bios/ logo: https://raw.githubusercontent.com/homarr-labs/dashboard-icons/main/svg/recalbox.svg scraper: recalbox source_url: https://gitlab.com/recalbox/recalbox/-/raw/master/board/recalbox/fsoverlay/recalbox/share_init/system/.emulationstation/es_bios.xml @@ -620,6 +623,7 @@ platforms: retrobat: config: retrobat.yml status: active + extract_to: bios/ logo: https://raw.githubusercontent.com/RetroBat-Official/retrobat/main/system/resources/retrobat_logo_notext.png scraper: retrobat source_url: https://raw.githubusercontent.com/RetroBat-Official/emulatorlauncher/master/batocera-systems/Resources/batocera-systems.json @@ -808,6 +812,7 @@ platforms: emudeck: config: emudeck.yml status: active + extract_to: ~/Emulation/bios/ logo: https://raw.githubusercontent.com/dragoonDorise/EmuDeck/main/icons/EmuDeck.png scraper: emudeck source_url: https://raw.githubusercontent.com/dragoonDorise/EmuDeck/main/functions/checkBIOS.sh @@ -838,6 +843,7 @@ platforms: lakka: config: lakka.yml status: active + extract_to: /storage/system/ logo: https://raw.githubusercontent.com/libretro/retroarch-assets/master/src/xmb/flatui/lakka.svg scraper: libretro inherits_from: retroarch @@ -854,6 +860,7 @@ platforms: retrodeck: config: retrodeck.yml status: active + extract_to: ~/retrodeck/ logo: https://raw.githubusercontent.com/RetroDECK/RetroDECK/main/res/icon.svg scraper: retrodeck source_url: https://github.com/RetroDECK/components @@ -898,6 +905,7 @@ platforms: romm: config: romm.yml status: active + extract_to: library/bios/ logo: https://avatars.githubusercontent.com/u/168586850 scraper: romm source_url: https://raw.githubusercontent.com/rommapp/romm/master/backend/models/fixtures/known_bios_files.json @@ -945,6 +953,7 @@ platforms: rocknix: config: rocknix.yml status: active + extract_to: /storage/roms/bios/ logo: https://avatars.githubusercontent.com/u/152618023 scraper: rocknix cores: all_libretro @@ -959,6 +968,7 @@ platforms: retropie: config: retropie.yml status: archived + extract_to: ~/RetroPie/BIOS/ logo: https://avatars.githubusercontent.com/u/11378204 scraper: retropie source: https://github.com/RetroPie/RetroPie-Setup @@ -975,6 +985,7 @@ platforms: bizhawk: config: bizhawk.yml status: active + extract_to: Firmware/ logo: https://avatars.githubusercontent.com/u/11743303 scraper: bizhawk source_url: https://raw.githubusercontent.com/TASEmulators/BizHawk/master/src/BizHawk.Emulation.Common/Database/FirmwareDatabase.cs @@ -1039,6 +1050,7 @@ platforms: misterfpga: config: misterfpga.yml status: active + extract_to: /media/fat/games/ logo: https://avatars.githubusercontent.com/u/47264183 scraper: misterfpga source_url: https://raw.githubusercontent.com/ajgowans/BiosDB_MiSTer/db/bios_db.json.zip diff --git a/scripts/generate_readme.py b/scripts/generate_readme.py index db360d29..a77c6e64 100644 --- a/scripts/generate_readme.py +++ b/scripts/generate_readme.py @@ -118,6 +118,35 @@ def release_totals(platform_name: str, record: dict) -> tuple[int | None, int | return pack["files"], pack["extracted_size"] +def extract_targets(platforms_dir: str) -> list[tuple[str, str]]: + """(display name, folder) per registered platform, where its pack extracts. + + Read from the registry so the README, the site and the pack guide answer + from one place. Sorted by display name. + """ + rows = [ + (load_platform_config(key, platforms_dir).get("platform", key), entry["extract_to"]) + for key, entry in load_platform_registry(platforms_dir).items() + if entry.get("extract_to") + ] + return sorted(rows, key=lambda row: row[0].casefold()) + + +def extract_notes(platforms_dir: str) -> list[str]: + """A pack whose entries carry their own root extracts above the BIOS folder.""" + folders = dict(extract_targets(platforms_dir)) + notes = [] + for key in sorted(load_platform_registry(platforms_dir)): + config = load_platform_config(key, platforms_dir) + display = config.get("platform", key) + if config.get("base_destination") == "" and display in folders: + notes.append( + f"The {display} pack already carries its own `bios/` folder, so it" + f" extracts into `{folders[display]}` rather than into the BIOS folder." + ) + return notes + + def download_table( coverages: dict, archived: set[str], @@ -365,18 +394,7 @@ def generate_readme(db: dict, platforms_dir: str) -> str: # a pack whose entries already carry their own root (RetroDECK) extracts # one level above it. extract_paths = { - "RetroArch": "`system/`", - "Lakka": "`/storage/system/`", - "Batocera": "`/userdata/bios/`", - "BizHawk": "`Firmware/`", - "Recalbox": "`/recalbox/share/bios/`", - "RetroBat": "`bios/`", - "RetroPie": "`~/RetroPie/BIOS/`", - "RetroDECK": "`~/retrodeck/`", - "EmuDeck": "`~/Emulation/bios/`", - "RomM": "`bios/{platform_slug}/`", - "ROCKNIX": "`/storage/roms/bios/`", - "MiSTer FPGA": "`/media/fat/games/`", + display: f"`{folder}`" for display, folder in extract_targets(platforms_dir) } archived = { name @@ -390,12 +408,11 @@ def generate_readme(db: dict, platforms_dir: str) -> str: ) ) + for note in extract_notes(platforms_dir): + lines.extend(["", note]) if archived: lines.extend( [ - "", - "The RetroDECK pack already contains its own `bios/` folder, so it" - " extracts into `~/retrodeck/` rather than into the BIOS folder.", "", "\\* Archived: the configuration is kept and packs are still built," " but upstream is no longer scraped on a schedule.", diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 39f84098..b177260f 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -51,7 +51,13 @@ from nativemode import reads_file_contents yaml = require_yaml() -from generate_readme import compute_coverage, contributing_text, release_totals +from generate_readme import ( + compute_coverage, + contributing_text, + extract_notes, + extract_targets, + release_totals, +) from profile_sync import source_ref_values, split_source_ref from provenance_report import build_report import release_record @@ -200,7 +206,11 @@ def _timestamp() -> str: def generate_home( - db: dict, coverages: dict, profiles: dict, registry: dict | None = None + db: dict, + coverages: dict, + profiles: dict, + registry: dict | None = None, + platforms_dir: str = "platforms", ) -> str: total_files = db.get("total_files", 0) total_size = db.get("total_size", 0) @@ -318,22 +328,14 @@ def generate_home( "", " | Platform | Extract to |", " |----------|-----------|", - " | RetroArch | `system/` |", - " | Batocera | `/userdata/bios/` |", - " | BizHawk | `Firmware/` |", - " | EmuDeck | `~/Emulation/bios/` |", - " | Lakka | `/storage/system/` |", - " | MiSTer FPGA | `/media/fat/games/` |", - " | ROCKNIX | `/storage/roms/bios/` |", - " | Recalbox | `/recalbox/share/bios/` |", - " | RetroBat | `bios/` |", - " | RetroDECK | `~/retrodeck/` |", - " | RetroPie | `~/RetroPie/BIOS/` |", - " | RomM | `bios/{platform_slug}/` |", + *( + f" | {display} | `{folder}` |" + for display, folder in extract_targets(platforms_dir) + ), "", - " The RetroDECK pack already carries its own `bios/` folder, so it " - "extracts one level above it. Every other pack extracts straight into " - "the BIOS folder. [Full instructions per setup](which-pack.md).", + *(f" {note}" for note in extract_notes(platforms_dir)), + " Every other pack extracts straight into the BIOS folder." + " [Full instructions per setup](which-pack.md).", "", ] ) @@ -3608,7 +3610,9 @@ def main(): # Generate home print("Generating home page...") write_if_changed( - str(docs / "index.md"), generate_home(db, coverages, profiles, registry) + str(docs / "index.md"), generate_home( + db, coverages, profiles, registry, args.platforms_dir + ) ) stats = compute_stats(db, coverages, profiles) diff --git a/tests/test_extract_targets.py b/tests/test_extract_targets.py new file mode 100644 index 00000000..fb969ba8 --- /dev/null +++ b/tests/test_extract_targets.py @@ -0,0 +1,60 @@ +"""Where a pack extracts is read from the registry, by the README and the site. + +Both kept a hand table keyed by display name. RomM's row said +`bios/{platform_slug}/` while the pack already carries one folder per slug +at its root, so following it nested every file one level too deep. The +README's RetroDECK sentence sat inside the archived-platforms block and +would have vanished the day RetroPie became active again. +""" + +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(REPO_ROOT / "scripts")) + +from common import load_platform_config, load_platform_registry +from generate_readme import extract_notes, extract_targets + +PLATFORMS = str(REPO_ROOT / "platforms") + + +class ExtractTargets(unittest.TestCase): + def test_every_registered_platform_says_where_it_extracts(self): + registry = load_platform_registry(PLATFORMS) + missing = sorted(k for k, v in registry.items() if not v.get("extract_to")) + self.assertEqual(missing, []) + self.assertEqual(len(extract_targets(PLATFORMS)), len(registry)) + + def test_a_folder_names_no_placeholder(self): + """The pack lays out the per-system folders itself.""" + templated = [d for d, folder in extract_targets(PLATFORMS) if "{" in folder] + self.assertEqual(templated, []) + + def test_a_pack_with_its_own_root_is_explained_whatever_is_archived(self): + rooted = sorted( + load_platform_config(key, PLATFORMS).get("platform", key) + for key in load_platform_registry(PLATFORMS) + if load_platform_config(key, PLATFORMS).get("base_destination") == "" + ) + explained = sorted( + display for display in rooted + if any(note.startswith(f"The {display} pack") for note in extract_notes(PLATFORMS)) + ) + self.assertTrue(rooted) + self.assertEqual(explained, rooted) + + def test_no_generator_keeps_its_own_table(self): + """The which-pack page describes setups per OS; these two were the + per-platform copies of the registry.""" + readme = (REPO_ROOT / "scripts" / "generate_readme.py").read_text(encoding="utf-8") + site = (REPO_ROOT / "scripts" / "generate_site.py").read_text(encoding="utf-8") + self.assertNotIn('"RetroDECK": "`~/retrodeck/`"', readme) + self.assertNotIn('" | RetroDECK | `~/retrodeck/` |"', site) + + +if __name__ == "__main__": + unittest.main()