mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-12 13:03:54 -06:00
AdvanceMAME reads its sets from dir_rom, a list of folders (advance/osd/fileio.c:74, doc/advmame.d:680,699-701). EmuELEC's advmame.rc sets dir_rom /storage/roms/mame (advmame.rc:11), and advmame.sh rewrites /roms/mame to /roms/arcade when it is started for the arcade system (advmame.sh:26-30). The extractor kept the keys under the BIOS root alone: it now records those under the roms root too, and a key the script rewrites gives one record for arcade and one for every other system. A configgen record carries the systems it is written for. The profile lays its 29 sets out at the top of dir_rom. On EmuELEC they land in roms/mame. The 12 root archives AdvanceMAME alone claimed there now hold the copies of the cores that also declare them (mame, geolith), none of them lost. A destination outside the BIOS tree is no proof of content: the moved entry resolves by its layout path alone. roms/mame/konamigv.zip took bios/Arcade/MAME/konamigv.zip, MAME 0.289's set, by that tail.
206 lines
7.4 KiB
Python
206 lines
7.4 KiB
Python
"""What the configgen launchers of Batocera and Recalbox have in common.
|
|
|
|
Both platforms start their emulators from a Python configgen, so their
|
|
launcher layers are read the same way: parse each generator with ast,
|
|
locate one known shape, and cite the lines it spans at the pinned revision.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
from collections.abc import Callable, Iterator
|
|
from dataclasses import dataclass
|
|
from typing import Any, NoReturn
|
|
|
|
|
|
class LauncherShapeError(ValueError):
|
|
"""A generator no longer has the shape its extractor reads."""
|
|
|
|
|
|
@dataclass
|
|
class Launcher:
|
|
"""One thing a launcher looks for or hands over, as its code says it."""
|
|
|
|
emulator: str
|
|
build: str
|
|
kind: str
|
|
names: list[str]
|
|
source_ref: str
|
|
slot: str | None = None
|
|
expands: dict[str, list[str]] | None = None
|
|
case_insensitive: bool | None = None
|
|
search_dir: str | None = None
|
|
passes_to: str | None = None
|
|
fails_when_missing: bool | str = False
|
|
fails_scope: str | None = None
|
|
depends_on_option: str | None = None
|
|
device: str | None = None
|
|
# The machines the platform offers under the record's systems.
|
|
machines: list[str] | None = None
|
|
# The platform systems the record is written for, none for every one.
|
|
systems: list[str] | None = None
|
|
|
|
def entry(self) -> dict[str, Any]:
|
|
"""The platform YAML form, in a fixed key order, unset keys left out."""
|
|
order = (
|
|
"emulator", "systems", "build", "kind", "slot", "names", "expands",
|
|
"case_insensitive", "search_dir", "passes_to",
|
|
"fails_when_missing", "fails_scope", "depends_on_option",
|
|
"device", "machines", "source_ref",
|
|
)
|
|
return {key: getattr(self, key) for key in order if getattr(self, key) is not None}
|
|
|
|
|
|
class Source:
|
|
"""One configgen file, parsed, with the lines its nodes span."""
|
|
|
|
def __init__(self, path: str, text: str) -> None:
|
|
self.path = path
|
|
self.lines = text.splitlines()
|
|
try:
|
|
self.tree = ast.parse(text)
|
|
except SyntaxError as e:
|
|
raise LauncherShapeError(f"{path}: {e}") from e
|
|
|
|
def fail(self, what: str) -> NoReturn:
|
|
raise LauncherShapeError(f"{self.path}: {what}")
|
|
|
|
def function(self, name: str) -> ast.FunctionDef:
|
|
found = [
|
|
node for node in ast.walk(self.tree)
|
|
if isinstance(node, ast.FunctionDef) and node.name == name
|
|
]
|
|
if len(found) != 1:
|
|
self.fail(f"expected one function {name}, found {len(found)}")
|
|
return found[0]
|
|
|
|
def assignment(self, name: str, scope: ast.AST | None = None) -> ast.Assign | ast.AnnAssign:
|
|
found = [
|
|
node for node in ast.walk(scope or self.tree)
|
|
if _assigned_name(node) == name
|
|
]
|
|
if len(found) != 1:
|
|
self.fail(f"expected one assignment to {name}, found {len(found)}")
|
|
return found[0]
|
|
|
|
def literal(self, node: ast.AST | None, what: str) -> Any:
|
|
try:
|
|
return ast.literal_eval(node) # type: ignore[arg-type]
|
|
except (ValueError, TypeError, SyntaxError) as e:
|
|
self.fail(f"{what} is not a literal: {e}")
|
|
|
|
def span(self, node: ast.AST) -> tuple[int, int]:
|
|
"""Lines of a node, with the comment lines written right above it."""
|
|
start = node.lineno # type: ignore[attr-defined]
|
|
while start > 1 and self.lines[start - 2].strip().startswith("#"):
|
|
start -= 1
|
|
return start, node.end_lineno # type: ignore[attr-defined]
|
|
|
|
def ref(self, spans: list[tuple[int, int]]) -> str:
|
|
"""`path:a-b, c-d`, ranges joined across blank and comment lines."""
|
|
merged: list[list[int]] = []
|
|
for start, end in sorted(spans):
|
|
if merged and self._only_filler(merged[-1][1] + 1, start - 1):
|
|
merged[-1][1] = max(merged[-1][1], end)
|
|
else:
|
|
merged.append([start, end])
|
|
parts = [f"{a}" if a == b else f"{a}-{b}" for a, b in merged]
|
|
return f"{self.path}:{', '.join(parts)}"
|
|
|
|
def _only_filler(self, first: int, last: int) -> bool:
|
|
return all(
|
|
not line.strip() or line.strip().startswith("#")
|
|
for line in self.lines[max(first - 1, 0):max(last, 0)]
|
|
)
|
|
|
|
|
|
def _assigned_name(node: ast.AST) -> str | None:
|
|
if isinstance(node, ast.Assign) and len(node.targets) == 1:
|
|
target = node.targets[0]
|
|
elif isinstance(node, ast.AnnAssign) and node.value is not None:
|
|
target = node.target
|
|
else:
|
|
return None
|
|
return target.id if isinstance(target, ast.Name) else None
|
|
|
|
|
|
def _statement_lists(node: ast.AST) -> Iterator[list[ast.stmt]]:
|
|
for child in ast.walk(node):
|
|
for attr in ("body", "orelse", "finalbody"):
|
|
value = getattr(child, attr, None)
|
|
if isinstance(value, list) and value and isinstance(value[0], ast.stmt):
|
|
yield value
|
|
|
|
|
|
def _locate(
|
|
scope: ast.AST, wanted: Callable[[ast.stmt], bool]
|
|
) -> tuple[list[ast.stmt], int] | None:
|
|
"""The innermost statement list holding a statement that satisfies `wanted`."""
|
|
for body in _statement_lists(scope):
|
|
for index, stmt in enumerate(body):
|
|
if wanted(stmt):
|
|
return body, index
|
|
return None
|
|
|
|
|
|
def _outermost(scope: ast.FunctionDef, node: ast.AST) -> ast.stmt:
|
|
"""The statement of the function body that contains `node`."""
|
|
for stmt in scope.body:
|
|
if any(child is node for child in ast.walk(stmt)):
|
|
return stmt
|
|
line = getattr(node, "lineno", "?")
|
|
raise LauncherShapeError(f"node at line {line} is outside {scope.name}")
|
|
|
|
|
|
def _mentions(stmt: ast.AST, constants: set[str], names: set[str]) -> bool:
|
|
for node in ast.walk(stmt):
|
|
if isinstance(node, ast.Constant) and node.value in constants:
|
|
return True
|
|
if isinstance(node, ast.Name) and node.id in names:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _run(
|
|
body: list[ast.stmt], index: int, keep: Callable[[ast.stmt], bool]
|
|
) -> tuple[ast.stmt, ast.stmt]:
|
|
"""The contiguous statements around body[index] that `keep` accepts."""
|
|
first = last = index
|
|
while first > 0 and keep(body[first - 1]):
|
|
first -= 1
|
|
while last + 1 < len(body) and keep(body[last + 1]):
|
|
last += 1
|
|
return body[first], body[last]
|
|
|
|
|
|
def _calls(scope: ast.AST, attr: str | None = None, name: str | None = None) -> list[ast.Call]:
|
|
found = []
|
|
for node in ast.walk(scope):
|
|
if not isinstance(node, ast.Call):
|
|
continue
|
|
func = node.func
|
|
if attr is not None and isinstance(func, ast.Attribute) and func.attr == attr:
|
|
found.append(node)
|
|
elif name is not None and isinstance(func, ast.Name) and func.id == name:
|
|
found.append(node)
|
|
return sorted(found, key=lambda call: (call.lineno, call.col_offset))
|
|
|
|
|
|
def _enclosing_test(scope: ast.AST, node: ast.AST) -> str | None:
|
|
for parent in ast.walk(scope):
|
|
if isinstance(parent, ast.If) and any(
|
|
child is node for stmt in parent.body for child in ast.walk(stmt)
|
|
):
|
|
return ast.unparse(parent.test)
|
|
return None
|
|
|
|
|
|
def _innermost_function(src: Source, node: ast.AST) -> ast.FunctionDef:
|
|
holders = [
|
|
f for f in ast.walk(src.tree)
|
|
if isinstance(f, ast.FunctionDef) and any(child is node for child in ast.walk(f))
|
|
]
|
|
if not holders:
|
|
src.fail(f"line {node.lineno} is outside any function") # type: ignore[attr-defined]
|
|
return max(holders, key=lambda f: f.lineno)
|