Files
libretro/scripts/scraper/targets/recalbox_targets_scraper.py
T
Abdessamad Derraz a8bfadd4c1 feat: list recalbox cores per board
Recalbox builds seven board images with different emulators. The new
target scraper reads each board's defconfig against the package every
system.ini core names. The names that reached no profile are mapped to
the projects Recalbox builds or profiled from their source, and the
mupen64plus video plugins are set aside.
2026-10-17 15:51:39 +02:00

127 lines
4.6 KiB
Python

"""Scraper for Recalbox per-board emulator availability.
Sources (recalbox/recalbox on GitLab, at the tag platforms/recalbox.yml names):
- configs/recalbox-<board>_defconfig -- the Buildroot packages each image
enables, one `BR2_PACKAGE_<NAME>=y` line per emulator package
- package/recalbox-romfs2/systems/<system>/system.ini -- the cores of each
system, each `[core.N]` naming the package that builds it, with board
overrides (`package.<board>`, `[core.N.<board>]`)
Recalbox runs on Raspberry Pi 3 to 5, two handhelds and x86_64: without a
target list, a Pi 3 pack carried the firmware of emulators only x86_64 builds.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from datetime import datetime, timezone
import yaml
from . import BaseTargetScraper
PLATFORM_NAME = "recalbox"
CONFIGS_TREE_URL = (
"https://gitlab.com/api/v4/projects/recalbox%2Frecalbox/repository/tree"
"?path=configs&ref={tag}&per_page=100"
)
DEFCONFIG_URL = "https://gitlab.com/recalbox/recalbox/-/raw/{tag}/configs/{name}"
_DEFCONFIG_RE = re.compile(r"^recalbox-(\w+)_defconfig$")
_ENABLED_RE = re.compile(r"^(BR2_PACKAGE_\w+)=y\s*$", re.MULTILINE)
try:
from ..base_scraper import fetch_bytes
from ..recalbox_scraper import SYSTEMS_TREE_URL, Scraper as PlatformScraper, board_cores
except ImportError:
from scraper.base_scraper import fetch_bytes
from scraper.recalbox_scraper import SYSTEMS_TREE_URL, Scraper as PlatformScraper, board_cores
def enabled_packages(defconfig: str) -> set[str]:
"""The Buildroot packages a defconfig turns on."""
return set(_ENABLED_RE.findall(defconfig))
def defconfig_arch(defconfig: str) -> str:
"""The architecture a defconfig builds for, from its Buildroot arch symbol."""
for symbol, arch in (("BR2_x86_64", "x86_64"), ("BR2_aarch64", "aarch64"),
("BR2_arm", "armhf")):
if re.search(rf"^{symbol}=y\s*$", defconfig, re.MULTILINE):
return arch
raise RuntimeError("defconfig names no BR2_x86_64, BR2_aarch64 or BR2_arm")
class Scraper(BaseTargetScraper):
"""Per-board core lists from Recalbox's defconfigs and system.ini files."""
def __init__(self, url: str = "https://gitlab.com/recalbox/recalbox"):
super().__init__(url=url)
def fetch_targets(self) -> dict:
platform = PlatformScraper()
tag = platform.tag
listing = json.loads(fetch_bytes(CONFIGS_TREE_URL.format(tag=tag)))
boards = {
match.group(1): item["name"]
for item in listing
if (match := _DEFCONFIG_RE.match(item.get("name", "")))
}
if not boards:
raise RuntimeError(f"no recalbox-*_defconfig in configs/ at {tag}")
systems: list[str] = []
page = 1
while True:
entries = json.loads(fetch_bytes(SYSTEMS_TREE_URL.format(tag=tag, page=page)))
systems += [e["name"] for e in entries
if e.get("type") == "tree" and not e["name"].startswith(".")]
if len(entries) < 100:
break
page += 1
inis = platform._system_inis(systems)
targets: dict[str, dict] = {}
for board, name in sorted(boards.items()):
defconfig = fetch_bytes(DEFCONFIG_URL.format(tag=tag, name=name)).decode("utf-8")
enabled = enabled_packages(defconfig)
cores: set[str] = set()
for text in inis:
cores |= board_cores(text, board, enabled)
arch = defconfig_arch(defconfig)
targets[board] = {"architecture": arch, "cores": sorted(cores)}
print(f" {board} ({arch}): {len(cores)} cores", file=sys.stderr)
return {
"platform": PLATFORM_NAME,
"source": f"{self.url}/-/tree/{tag}/configs",
"scraped_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"targets": targets,
}
def main() -> None:
parser = argparse.ArgumentParser(description="Scrape Recalbox per-board cores")
parser.add_argument("--dry-run", action="store_true", help="Show target summary")
parser.add_argument("--output", "-o", help="Output YAML file")
args = parser.parse_args()
scraper = Scraper()
data = scraper.fetch_targets()
if args.dry_run:
for name, info in data["targets"].items():
print(f" {name} ({info['architecture']}): {len(info['cores'])} cores")
return
if args.output:
scraper.write_output(data, args.output)
print(f"Written to {args.output}")
return
print(yaml.dump(data, default_flow_style=False, sort_keys=False))
if __name__ == "__main__":
main()