mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-18 01:13:30 -05:00
fix: name each exported addition's core
Batocera, ROCKNIX and Recalbox find a file through the core that wants it. Additions now carry that core under the platform's own name: the emulator/core pair checkBios reads (ROCKNIX's from its emulators package), or Recalbox's core attribute. One with no nameable core, or filed in a system gated on another core, is counted as not stated.
This commit is contained in:
1 parent
e9f9260301
commit
0754d3b64f
7 files changed
+337
-28
No files matched your search
@@ -16,6 +16,7 @@ Usage:
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import json
|
||||
import sys
|
||||
import urllib.error
|
||||
@@ -146,10 +147,29 @@ def wanted_sources(
|
||||
|
||||
base = pinned_base(wanted, scraped)
|
||||
if base:
|
||||
wanted = {relative: base + relative for relative in wanted}
|
||||
wanted = {
|
||||
relative: _at_revision(wanted[relative], base) or base + relative
|
||||
for relative in wanted
|
||||
}
|
||||
return wanted
|
||||
|
||||
|
||||
_RAW_GITHUB = re.compile(r"^https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/")
|
||||
|
||||
|
||||
def _at_revision(url: str, base: str) -> str:
|
||||
"""A file of the pinned repository, read at the pinned revision; empty
|
||||
when the URL is not a raw GitHub URL of that repository.
|
||||
|
||||
ROCKNIX's checker sits in packages/rocknix/, the emulators it names in
|
||||
packages/virtual/: no shared directory, one revision.
|
||||
"""
|
||||
pinned, other = _RAW_GITHUB.match(base), _RAW_GITHUB.match(url)
|
||||
if pinned and other and pinned.group(1, 2) == other.group(1, 2):
|
||||
return pinned.group(0) + url[other.end():]
|
||||
return ""
|
||||
|
||||
|
||||
def collect_originals(
|
||||
exporter: object,
|
||||
systems: dict,
|
||||
@@ -347,7 +367,7 @@ def export_platform(
|
||||
for entry in system.files:
|
||||
if not entry.name:
|
||||
continue
|
||||
if exporter.writable(entry):
|
||||
if exporter.lands(entry):
|
||||
landed += entry.platform is None
|
||||
elif entry.platform is None:
|
||||
refused += 1
|
||||
|
||||
@@ -127,6 +127,11 @@ class BaseExporter(ABC):
|
||||
"""
|
||||
return False
|
||||
|
||||
def lands(self, fe: NativeFile) -> bool:
|
||||
"""Whether this render writes the entry: writable(), plus whatever
|
||||
the render learned from the platform's own file."""
|
||||
return self.writable(fe)
|
||||
|
||||
@classmethod
|
||||
def writable(cls, fe: NativeFile, require: str = "") -> bool:
|
||||
"""Whether this format can carry the entry.
|
||||
|
||||
@@ -65,6 +65,9 @@ class NativeFile:
|
||||
filled: list[str] = field(default_factory=list)
|
||||
# An addition filed under our own slug: the platform names no such system.
|
||||
foreign_system: bool = False
|
||||
# For an addition, each core that wants it as (runs standalone, name the
|
||||
# platform gives it); empty when no core the platform runs is named.
|
||||
runners: list[tuple[bool, str]] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def origin(self) -> str:
|
||||
@@ -198,13 +201,14 @@ class NativeFile:
|
||||
return default
|
||||
|
||||
def cores(self) -> list[str]:
|
||||
"""Cores that want this file, the platform's naming preferred."""
|
||||
"""Cores that want this file, as Recalbox writes them: the declared
|
||||
value, else each runner, a standalone bare and a core under
|
||||
libretro/. A profile key is not a name the platform knows:
|
||||
`libretro/duckstation` named no core Recalbox runs."""
|
||||
declared = self.native("core", "")
|
||||
if declared:
|
||||
return [c.strip() for c in str(declared).split(",") if c.strip()]
|
||||
if self.truth:
|
||||
return [f"libretro/{c}" for c in self.truth.get("_cores", [])]
|
||||
return []
|
||||
return [name if standalone else f"libretro/{name}" for standalone, name in self.runners]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -483,9 +487,46 @@ def build_native_model(
|
||||
tail = [fe for fe in system.files if fe.platform is None]
|
||||
system.files = head + search_order(tail)
|
||||
|
||||
_attach_runners(systems, scraped)
|
||||
return systems, report
|
||||
|
||||
|
||||
def _attach_runners(systems: dict[str, NativeSystem], scraped: dict | None) -> None:
|
||||
"""Name the cores behind each addition the way the platform names them.
|
||||
|
||||
The platform lists its cores by its own names (Batocera's
|
||||
`mednafen_psx`, Recalbox's `mednafen_psx`), our truth by profile key
|
||||
(`beetle_psx`); a profile's `cores:` holds the names it answers to.
|
||||
The build a platform runs an emulator as decides the runner.
|
||||
"""
|
||||
if not scraped:
|
||||
return
|
||||
from common import load_emulator_profiles, runs_standalone
|
||||
|
||||
declared = scraped.get("cores")
|
||||
libretro = {str(c) for c in declared} if isinstance(declared, list) else set()
|
||||
standalone = {str(c) for c in scraped.get("standalone_cores") or []}
|
||||
profiles = load_emulator_profiles(str(Path(__file__).resolve().parents[2] / "emulators"))
|
||||
for system in systems.values():
|
||||
for fe in system.files:
|
||||
if fe.platform is not None or not fe.truth:
|
||||
continue
|
||||
runners: list[tuple[bool, str]] = []
|
||||
for key in sorted(fe.truth.get("_cores") or ()):
|
||||
profile = profiles.get(key, {})
|
||||
names = [key, *(str(c) for c in profile.get("cores") or [])]
|
||||
if runs_standalone(key, profile, standalone):
|
||||
name = next((n for n in names if n in standalone), None)
|
||||
runner = (True, name) if name else None
|
||||
else:
|
||||
name = next((n for n in names if n in libretro - standalone), None) \
|
||||
or next((n for n in names if n in libretro), None)
|
||||
runner = (False, name) if name else None
|
||||
if runner and runner not in runners:
|
||||
runners.append(runner)
|
||||
fe.runners = runners
|
||||
|
||||
|
||||
def search_order(files: list[NativeFile]) -> list[NativeFile]:
|
||||
"""Order files the way the code looks for them, best first.
|
||||
|
||||
|
||||
@@ -25,6 +25,17 @@ _INDENT = " " * 4
|
||||
class Exporter(BaseExporter):
|
||||
"""Write Batocera's batocera-systems, corrected."""
|
||||
|
||||
# checkBios reads emulator names from es_systems, and Batocera's
|
||||
# standalone_cores are those names. ROCKNIX's list holds package names
|
||||
# (duckstation-sa), which its checker does not know.
|
||||
STANDALONE_RUNNERS = True
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
# The emulator and core each system entry of the original is gated
|
||||
# on, read by render.
|
||||
self._gates: dict[str, tuple[str, str] | None] = {}
|
||||
|
||||
@staticmethod
|
||||
def platform_name() -> str:
|
||||
return "batocera"
|
||||
@@ -55,8 +66,60 @@ class Exporter(BaseExporter):
|
||||
"""
|
||||
if fe.platform is None and len(set(fe.hashes("md5"))) > 1:
|
||||
return False
|
||||
if fe.platform is None and not cls._usable_runners(fe):
|
||||
# Written without its core's emulator and core, checkBios asks
|
||||
# every build for it (the defect _runner_keys repairs for the
|
||||
# platform's own items).
|
||||
return False
|
||||
return super().writable(fe, require)
|
||||
|
||||
@classmethod
|
||||
def _usable_runners(cls, fe: NativeFile) -> list[tuple[bool, str]]:
|
||||
return [r for r in fe.runners if cls.STANDALONE_RUNNERS or not r[0]]
|
||||
|
||||
def _runner_pair(self, runner: tuple[bool, str]) -> tuple[str, str | None] | None:
|
||||
"""The emulator and core checkBios finds a runner under, None when
|
||||
the checker has no name for it. Batocera's es_systems names a
|
||||
libretro core under `libretro` and a standalone by itself."""
|
||||
standalone, name = runner
|
||||
return (name, None) if standalone else ("libretro", name)
|
||||
|
||||
def _addition_keys(self, fe: NativeFile) -> list[tuple[str, str | None]] | None:
|
||||
"""The emulator/core pairs an addition is written with, [] when its
|
||||
system's own gate already names its core, None when the gate names
|
||||
another: checkBios walks a gated system only when that core is
|
||||
installed, so a file of another core there is asked of the wrong
|
||||
users and never of its own."""
|
||||
pairs = [
|
||||
pair for r in self._usable_runners(fe)
|
||||
if (pair := self._runner_pair(r)) is not None
|
||||
]
|
||||
if not pairs:
|
||||
return None
|
||||
gate = self._gates.get(fe.native_system)
|
||||
if gate is None:
|
||||
return pairs
|
||||
return [] if gate in pairs else None
|
||||
|
||||
def exportable(
|
||||
self,
|
||||
systems: dict[str, NativeSystem],
|
||||
require: str = "",
|
||||
) -> list[tuple[NativeSystem, list[NativeFile]]]:
|
||||
"""What the format can carry, less the additions its gates refuse:
|
||||
render and validate read the same list."""
|
||||
result = []
|
||||
for system, files in super().exportable(systems, require):
|
||||
kept = [fe for fe in files if self.lands(fe)]
|
||||
if kept:
|
||||
result.append((system, kept))
|
||||
return result
|
||||
|
||||
def lands(self, fe: NativeFile) -> bool:
|
||||
if not self.writable(fe):
|
||||
return False
|
||||
return fe.platform is not None or self._addition_keys(fe) is not None
|
||||
|
||||
def states(self, fe: NativeFile, field_name: str) -> bool:
|
||||
if field_name == "md5" and "md5" in fe.filled:
|
||||
return bool(self.single_value(fe, "md5"))
|
||||
@@ -96,22 +159,38 @@ class Exporter(BaseExporter):
|
||||
}
|
||||
parts: list[str] = []
|
||||
for fe in files:
|
||||
# The platform states an unhashed file as an empty md5 rather
|
||||
# than leaving it out, and so do we.
|
||||
item = [f'"md5": "{self.single_value(fe, "md5")}"']
|
||||
alt = fe.native("alt_md5", "")
|
||||
if alt:
|
||||
item.append(f'"altmd5": "{alt}"')
|
||||
declared = fe.native("native_path", "")
|
||||
path = str(declared) if declared else f"bios/{fe.destination}"
|
||||
item.append(f'"file": "{path}"')
|
||||
zipped = fe.native("zipped_file", "")
|
||||
if zipped:
|
||||
item.append(f'"zippedFile": "{zipped}"')
|
||||
item.extend(self._runner_keys(originals.get(path)))
|
||||
parts.append("{ " + ", ".join(item) + " }")
|
||||
for runner in self._item_runners(fe):
|
||||
parts.append(self._bios_item(fe, runner, originals))
|
||||
return parts
|
||||
|
||||
def _item_runners(self, fe: NativeFile) -> list[list[str] | None]:
|
||||
"""One item per runner an addition names; None keeps the original's."""
|
||||
if fe.platform is not None:
|
||||
return [None]
|
||||
pairs = self._addition_keys(fe) or []
|
||||
if not pairs:
|
||||
return [[]]
|
||||
return [
|
||||
[f'"emulator": "{emulator}"'] + ([f'"core": "{core}"'] if core else [])
|
||||
for emulator, core in pairs
|
||||
]
|
||||
|
||||
def _bios_item(self, fe: NativeFile, runner: list[str] | None, originals: dict) -> str:
|
||||
# The platform states an unhashed file as an empty md5 rather
|
||||
# than leaving it out, and so do we.
|
||||
item = [f'"md5": "{self.single_value(fe, "md5")}"']
|
||||
alt = fe.native("alt_md5", "")
|
||||
if alt:
|
||||
item.append(f'"altmd5": "{alt}"')
|
||||
declared = fe.native("native_path", "")
|
||||
path = str(declared) if declared else f"bios/{fe.destination}"
|
||||
item.append(f'"file": "{path}"')
|
||||
zipped = fe.native("zipped_file", "")
|
||||
if zipped:
|
||||
item.append(f'"zippedFile": "{zipped}"')
|
||||
item.extend(self._runner_keys(originals.get(path)) if runner is None else runner)
|
||||
return "{ " + ", ".join(item) + " }"
|
||||
|
||||
@staticmethod
|
||||
def _own_entry(system: NativeSystem, before: dict | None) -> dict:
|
||||
"""The maintainer's entry for this system, out of the parsed pair."""
|
||||
@@ -257,11 +336,6 @@ class Exporter(BaseExporter):
|
||||
originals: dict[str, str],
|
||||
scraped: dict | None = None,
|
||||
) -> dict[str, str]:
|
||||
exportable = dict(
|
||||
(system.native_id, (system, files))
|
||||
for system, files in self.exportable(systems, require="md5")
|
||||
)
|
||||
|
||||
original = originals.get(self.native_filename(), "")
|
||||
if not original:
|
||||
raise ValueError(
|
||||
@@ -272,6 +346,18 @@ class Exporter(BaseExporter):
|
||||
|
||||
head, body, tail = self._split_original(original)
|
||||
spans = self._entry_spans(body)
|
||||
for key, (start, end) in spans.items():
|
||||
parsed = self._parse_entry(body[start:end + 1]) or {}
|
||||
own = parsed.get(key) if isinstance(parsed.get(key), dict) else {}
|
||||
emulator, core = own.get("emulator"), own.get("core")
|
||||
self._gates[key] = (
|
||||
(emulator, core if isinstance(core, str) else None)
|
||||
if isinstance(emulator, str) else None
|
||||
)
|
||||
exportable = {
|
||||
system.native_id: (system, files)
|
||||
for system, files in self.exportable(systems, require="md5")
|
||||
}
|
||||
|
||||
rebuilt: list[str] = []
|
||||
written: set[str] = set()
|
||||
|
||||
@@ -6,6 +6,9 @@ runs, so only the mapping is rewritten.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from .baseline import NativeSystem, Report
|
||||
from .batocera_exporter import Exporter as BatoceraExporter
|
||||
|
||||
SOURCE_URL = (
|
||||
@@ -14,9 +17,47 @@ SOURCE_URL = (
|
||||
)
|
||||
|
||||
|
||||
EMULATORS_URL = (
|
||||
"https://raw.githubusercontent.com/ROCKNIX/distribution/next/projects/ROCKNIX"
|
||||
"/packages/virtual/emulators/package.mk"
|
||||
)
|
||||
EMULATORS_MK = "emulators/package.mk"
|
||||
# `add_emu_core <system> <emulator> <core> <default>`: the pairs es_systems.cfg
|
||||
# lists, and so the names checkBios's emuDict holds.
|
||||
_ADD_EMU_CORE = re.compile(r"^\s*add_emu_core\s+(\S+)\s+(\S+)\s+(\S+)", re.MULTILINE)
|
||||
|
||||
|
||||
class Exporter(BatoceraExporter):
|
||||
"""Write ROCKNIX's rocknix-systems, corrected."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._emulator_of: dict[str, str] = {}
|
||||
|
||||
def _runner_pair(self, runner: tuple[bool, str]) -> tuple[str, str | None] | None:
|
||||
"""ROCKNIX registers libretro cores under `retroarch` and each
|
||||
standalone under its own emulator (`azahar azahar-sa`), in the
|
||||
emulators package; the `libretro` Batocera writes names nothing
|
||||
there."""
|
||||
_standalone, name = runner
|
||||
emulator = self._emulator_of.get(name)
|
||||
return (emulator, name) if emulator else None
|
||||
|
||||
def render(
|
||||
self,
|
||||
systems: dict[str, NativeSystem],
|
||||
report: Report,
|
||||
originals: dict[str, str],
|
||||
scraped: dict | None = None,
|
||||
) -> dict[str, str]:
|
||||
self._emulator_of = {
|
||||
core: emulator
|
||||
for _system, emulator, core in _ADD_EMU_CORE.findall(
|
||||
originals.get(EMULATORS_MK, "")
|
||||
)
|
||||
}
|
||||
return super().render(systems, report, originals, scraped)
|
||||
|
||||
@staticmethod
|
||||
def platform_name() -> str:
|
||||
return "rocknix"
|
||||
@@ -27,4 +68,4 @@ class Exporter(BatoceraExporter):
|
||||
|
||||
@staticmethod
|
||||
def native_sources() -> dict[str, str]:
|
||||
return {"rocknix-systems": SOURCE_URL}
|
||||
return {"rocknix-systems": SOURCE_URL, EMULATORS_MK: EMULATORS_URL}
|
||||
Reference in new issue
Block a user