Files
libretro/scripts/scraper/targets/recalbox_targets_scraper.py
T

127 lines
4.7 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") -> None:
super().__init__(url=url)
def fetch_targets(self) -> dict:
tag = self.pinned_revision(PLATFORM_NAME)
platform = PlatformScraper(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()