feat: read Recalbox launchers from configgen

The scraper now records, from configgen at the pinned tag, the names each Recalbox launcher tries and where it hands them: the Hatari TOS cascades (standalone, hatari and hatarib cores), GSplus, Amiberry kickstarts, the PCSX2 region choice, the Dolphin IPL presence test, xemu, the mupen64plus 64DD IPL, the Mini vMac boot disk and the o2em_bios option. The system_directory libretroRetroarch gives each core, and the per-system overrides after it, are passes_path records.
This commit is contained in:
Abdessamad Derraz committed 2026-10-20 20:59:51 +02:00
1 parent 4465edcd0a
commit fe9a081cfb
5 files changed
+2789 -4

No files matched your search

File diff suppressed because it is too large. Load diff
+22 -3
View File
@@ -18,6 +18,8 @@ from __future__ import annotations
from common import parse_untrusted_xml
from .base_scraper import BaseScraper, BiosRequirement, fetch_bytes, requirement_entry
from .recalbox_launchers import SOURCE_FILES as LAUNCHER_FILES
from .recalbox_launchers import LauncherShapeError, extract_launchers
PLATFORM_NAME = "recalbox"
@@ -218,9 +220,14 @@ class Scraper(BaseScraper):
def _system_inis(self, systems: list[str]) -> list[str]:
"""Every system's system.ini at the tag, read in batches."""
paths = [f"{SYSTEMS_DIR}/{system}/system.ini" for system in systems]
texts = self._blobs(paths)
return [texts[path] for path in paths]
def _blobs(self, paths: list[str]) -> dict[str, str]:
"""The files at `paths` at the tag, read in batches."""
import json
paths = [f"{SYSTEMS_DIR}/{system}/system.ini" for system in systems]
texts: dict[str, str] = {}
for start in range(0, len(paths), BLOBS_PER_QUERY):
batch = paths[start:start + BLOBS_PER_QUERY]
@@ -236,8 +243,19 @@ class Scraper(BaseScraper):
texts.update({n["path"]: n["rawBlob"] for n in nodes if n.get("rawBlob")})
missing = [path for path in paths if path not in texts]
if missing:
raise RuntimeError(f"no system.ini at {self.tag} for {', '.join(missing[:5])}")
return [texts[path] for path in paths]
raise RuntimeError(f"nothing at {self.tag} for {', '.join(missing[:5])}")
return texts
def _fetch_launchers(self) -> list[dict]:
"""Launcher records read from configgen at the scraped tag."""
texts = self._blobs(list(LAUNCHER_FILES))
def read(path: str) -> str:
if path not in texts:
raise LauncherShapeError(f"{path} is read but not in SOURCE_FILES")
return texts[path]
return extract_launchers(read)
def _fetch_cores(self) -> tuple[list[str], list[str], list[str]]:
"""Every core Recalbox's systems offer, those it runs standalone, and
@@ -379,6 +397,7 @@ class Scraper(BaseScraper):
# Launched both ways: each layout gets its files.
**({"dual_mode_cores": dual} if dual else {}),
"systems": systems,
"launchers": self._fetch_launchers(),
}
File diff suppressed because it is too large. Load diff
+7
View File
@@ -258,6 +258,13 @@ seven BIOS paths on every device but AMD64, where the keys are left empty:
An extractor that no longer recognises the shape of the code raises: an
empty list would read as a launcher that looks for nothing.
A libretro frontend that picks the system directory per core records each
choice as a `passes_path` entry with no `names`. Recalbox gives gpsp
`bios/gba` and geargrafx `bios/pcenginecd`, so a core that opens `gba_bios.bin`
reads it from there. `emulator: libretro` stands for every core Recalbox runs,
for the default directory and for the per-system overrides applied after the
per-core choice, and `<system>` in `search_dir` is the system's own name.
### Inheritance
If the new platform inherits from an existing one (e.g. Lakka inherits RetroArch),
+1 -1
View File
@@ -45,7 +45,7 @@ Every module in `tests/`, by the area it guards.
| Hardware targets and systems | synthetic + target files | `test_extract_targets`, `test_lakka_targets`, `test_libretro_slugs`, `test_system_ids`, `test_system_selection`, `test_target_names`, `test_target_scrapers`, `test_target_systems`, `test_target_verify` |
| Installer and downloads | loopback server | `test_check_release`, `test_download`, `test_install`, `test_install_cores`, `test_install_experience`, `test_install_selection` |
| Truth and native export | synthetic + cached originals | `test_export_counts`, `test_export_native_layer`, `test_export_report`, `test_exporters`, `test_truth_diff`, `test_truth_layout` |
| Scrapers, upstream and provenance | inline sources, synthetic | `test_check_freshness`, `test_dedup`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_retrobat`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_profile_sync`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_scraper_contract`, `test_upstream`, `test_upstream_index` |
| Scrapers, upstream and provenance | inline sources, synthetic | `test_check_freshness`, `test_dedup`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_profile_sync`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_scraper_contract`, `test_upstream`, `test_upstream_index` |
| Site, docs and repository contracts | rendered pages, sources | `test_contributing_text`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_optional_dependencies`, `test_prose_style`, `test_site_availability`, `test_site_cores`, `test_site_exports`, `test_site_platform_hashes`, `test_site_system_links`, `test_site_validation`, `test_site_write`, `test_validate_pr`, `test_validate_pr_hashes`, `test_workflow_paths` |
## Test architecture