mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-18 09:23:26 -05:00
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.
This commit is contained in:
1 parent
d9c74888e8
commit
a8bfadd4c1
22 files changed
+1997
-24
No files matched your search
@@ -117,18 +117,14 @@ BLOBS_QUERY = (
|
||||
BLOBS_PER_QUERY = 50
|
||||
|
||||
|
||||
def system_ini_cores(text: str) -> tuple[set[str], set[str]]:
|
||||
"""The cores one Recalbox system.ini offers, and those run standalone.
|
||||
def _ini_sections(text: str) -> dict[str, dict[str, str]]:
|
||||
"""Sections and their key = value lines of one Recalbox system.ini.
|
||||
|
||||
Each ``[core.N]`` section (or its board override ``[core.N.<board>]``)
|
||||
names an ``emulator`` and a ``core``: ``libretro`` with the core's name,
|
||||
or a standalone emulator. es_bios.xml only names the cores that need a
|
||||
BIOS, so read alone it left most of Recalbox's cores outside the pack.
|
||||
Recalbox's .ini files are not strict INI (a value can run on a bare
|
||||
line), so only sections and key = value lines are read.
|
||||
"""
|
||||
import re
|
||||
|
||||
# Recalbox's .ini files are not strict INI (a value can run on a bare
|
||||
# line), so only sections and key = value lines are read.
|
||||
sections: dict[str, dict[str, str]] = {}
|
||||
current: dict[str, str] | None = None
|
||||
for raw in text.splitlines():
|
||||
@@ -142,25 +138,64 @@ def system_ini_cores(text: str) -> tuple[set[str], set[str]]:
|
||||
if current is not None and "=" in line:
|
||||
key, value = line.split("=", 1)
|
||||
current[key.strip()] = value.strip().strip('"')
|
||||
return sections
|
||||
|
||||
|
||||
def _core_names(values: dict[str, str]) -> tuple[set[str], set[str]]:
|
||||
"""(cores, standalone) one [core.N] section names."""
|
||||
emulator = values.get("emulator", "")
|
||||
core = values.get("core", "")
|
||||
if emulator == "libretro":
|
||||
return ({core} if core else set()), set()
|
||||
if not emulator:
|
||||
return set(), set()
|
||||
names = {emulator} | ({core} if core and core != emulator else set())
|
||||
return names, set(names)
|
||||
|
||||
|
||||
def system_ini_cores(text: str) -> tuple[set[str], set[str]]:
|
||||
"""The cores one Recalbox system.ini offers, and those run standalone.
|
||||
|
||||
Each ``[core.N]`` section (or its board override ``[core.N.<board>]``)
|
||||
names an ``emulator`` and a ``core``: ``libretro`` with the core's name,
|
||||
or a standalone emulator. es_bios.xml only names the cores that need a
|
||||
BIOS, so read alone it left most of Recalbox's cores outside the pack.
|
||||
"""
|
||||
import re
|
||||
|
||||
cores: set[str] = set()
|
||||
standalone: set[str] = set()
|
||||
for section, values in sections.items():
|
||||
for section, values in _ini_sections(text).items():
|
||||
if not re.fullmatch(r"core\.\d+(\.[\w-]+)?", section):
|
||||
continue
|
||||
emulator = values.get("emulator", "")
|
||||
core = values.get("core", "")
|
||||
if emulator == "libretro":
|
||||
if core:
|
||||
cores.add(core)
|
||||
elif emulator:
|
||||
cores.add(emulator)
|
||||
standalone.add(emulator)
|
||||
if core and core != emulator:
|
||||
cores.add(core)
|
||||
standalone.add(core)
|
||||
found, alone = _core_names(values)
|
||||
cores |= found
|
||||
standalone |= alone
|
||||
return cores, standalone
|
||||
|
||||
|
||||
def board_cores(text: str, board: str, enabled: set[str]) -> set[str]:
|
||||
"""The cores of one system.ini a board's image builds.
|
||||
|
||||
A core is built when the Buildroot package its section names is enabled
|
||||
in the board's defconfig. ``[core.N.<board>]`` replaces the section on
|
||||
that board and ``package.<board>`` its package.
|
||||
"""
|
||||
import re
|
||||
|
||||
sections = _ini_sections(text)
|
||||
cores: set[str] = set()
|
||||
for section, values in sections.items():
|
||||
match = re.fullmatch(r"core\.(\d+)", section)
|
||||
if not match:
|
||||
continue
|
||||
values = sections.get(f"{section}.{board}", values)
|
||||
package = values.get(f"package.{board}", values.get("package", ""))
|
||||
if package in enabled:
|
||||
cores |= _core_names(values)[0]
|
||||
return cores
|
||||
|
||||
|
||||
class Scraper(BaseScraper):
|
||||
"""Scraper for Recalbox es_bios.xml."""
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"""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()
|
||||
Reference in new issue
Block a user