From 3129eaf10cb41347ece458731ab8ba8c33afd420 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 11 Nov 2026 11:03:41 +0100 Subject: [PATCH 1/4] feat: bind config launcher records on evidence A launcher record citing a config, option or feature file binds only when the platform's code installs the file (installs), its launch code was read for what replaces it (overrides), and the emulator's profile shows where the shipped build loads it and reads each key (config_reads, read at pin_build). Unproven records place no file and hand no directory. verify, cross_reference and the platform page list them apart, outside severity and --strict. --- schemas/emulator.schema.json | 66 +++++- schemas/platform.schema.json | 23 +++ scripts/common.py | 13 +- scripts/cross_reference.py | 47 ++++- scripts/generate_site.py | 32 ++- scripts/launchers.py | 319 ++++++++++++++++++++++++++++- scripts/profile_sync.py | 97 +++++++++ scripts/scraper/shell_launchers.py | 185 +++++++++++++++++ scripts/validate_schemas.py | 56 +++++ scripts/verify.py | 20 +- tests/test_launcher_gaps.py | 7 +- tests/test_launcher_layer.py | 8 +- tests/test_launcher_placement.py | 11 + wiki/adding-a-platform.md | 16 +- 14 files changed, 872 insertions(+), 28 deletions(-) diff --git a/schemas/emulator.schema.json b/schemas/emulator.schema.json index 7de174331..7bb563009 100644 --- a/schemas/emulator.schema.json +++ b/schemas/emulator.schema.json @@ -11,15 +11,16 @@ ], "allOf": [ { - "comment": "A build reference documents why a pin is held: it never stands without the pin_frozen that says so.", + "comment": "A build reference documents why a pin is held, or the build config_reads are read at: it never stands without the pin_frozen or the config_reads that say so.", "if": { "required": [ "pin_build" ] }, "then": { - "required": [ - "pin_frozen" + "anyOf": [ + {"required": ["pin_frozen"]}, + {"required": ["config_reads"]} ] } }, @@ -1078,14 +1079,36 @@ "standalone_directories": { "type": "array", "items": { - "type": "string" + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "additionalProperties": false, + "required": ["handle", "source_ref"], + "properties": { + "handle": {"type": "string", "minLength": 1}, + "source_ref": {"type": "string", "minLength": 1, "description": "Where the build reads the handle and resolves its files against it"} + } + } + ] }, "description": "What the standalone build resolves standalone_path against, as its code names it: a directory ($XDG_DATA_HOME/flycast), a setting (Dreamcast.BiosPath), an environment variable or the function returning the directory. A platform launcher record that links or passes a folder of its BIOS tree under one of these puts the standalone files in that folder." }, "standalone_pools": { "type": "object", "additionalProperties": { - "type": "string" + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "additionalProperties": false, + "required": ["folder", "source_ref"], + "properties": { + "folder": {"type": "string"}, + "source_ref": {"type": "string", "minLength": 1, "description": "Where the build reads the setting as a search list"} + } + } + ] }, "description": "Settings the standalone build reads as a list of directories it searches one after the other, each mapped to the subfolder of the layout (relative to standalone_directories, empty for the whole of it) its directories stand for: openMSX's filepool entries are searched like share/systemroms, XRoar's rompath entries hold the ROMs themselves. A file in any entry is read, so several platform records naming entries give the first one a pack can fill." }, @@ -1101,7 +1124,7 @@ "pin_build": { "type": "array", "minItems": 1, - "description": "The build file each platform that ships a pin_frozen build reads its revision from, so check_freshness can read it again where the platform's YAML now stands and say when the platform builds another revision.", + "description": "The build file each platform that ships a pin_frozen build, or the build a config_reads entry is read at, reads its revision from, so check_freshness can read it again where the platform's YAML now stands and say when the platform builds another revision.", "items": { "type": "object", "additionalProperties": false, @@ -1135,6 +1158,37 @@ } } }, + "config_reads": { + "type": "array", + "description": "The configs the build loads and the keys it opens a file from, as a platform launcher record citing a config needs them: link (c) is where the file is found and loaded, link (d) where each key is read and its value opened. An entry naming platforms is read at their pin_build revision, one naming none at source_commit.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["file", "source_ref", "keys"], + "dependentRequired": {"revision": ["platforms", "revision_from"]}, + "properties": { + "file": {"type": "string", "minLength": 1, "description": "The config as the code finds it: a name, a glob for any file a flag passes (*.uae), a path, the flag itself, \"command line\" or \"core options\""}, + "platforms": {"type": "array", "items": {"type": "string"}, "minItems": 1, "uniqueItems": true, "description": "Registry slugs of the platforms shipping the build these refs are read at, each with a pin_build entry unless revision names the build"}, + "revision": {"type": "string", "pattern": "^[0-9a-f]{40}$", "description": "The commit these refs are read at, for platforms installing a package no build file of theirs pins (RetroBat's stable archives, a Flathub app)"}, + "revision_from": {"type": "string", "minLength": 1, "description": "Where revision was read: the version string of the shipped binary, the Flathub manifest commit"}, + "source_ref": {"type": "string", "minLength": 1}, + "keys": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["key", "source_ref"], + "properties": { + "key": {"oneOf": [{"type": "string", "minLength": 1}, {"type": "array", "items": {"type": "string", "minLength": 1}, "minItems": 1}], "description": "Spelled as a launcher record's passes_to spells it: [Section] Key, or Key"}, + "source_ref": {"type": "string", "minLength": 1}, + "condition": {"type": "string", "description": "What else the read depends on"} + } + } + } + } + } + }, "pin_follows": { "type": "string", "description": "Why source_commit follows the branch tip although a single platform ships the profile: that platform builds the tip, cited by its build file. Without it or pin_frozen, a bump refuses a profile one platform ships." diff --git a/schemas/platform.schema.json b/schemas/platform.schema.json index c0a972192..1b3fb9479 100644 --- a/schemas/platform.schema.json +++ b/schemas/platform.schema.json @@ -341,6 +341,29 @@ "type": "string", "description": "Devices whose shipped config this record reads, comma-separated, set only where the configs of a package differ between devices" }, + "installs": { + "type": "object", + "additionalProperties": false, + "required": ["path", "source_ref"], + "properties": { + "path": {"type": "string", "minLength": 1, "description": "Where the config the record cites lands on the device, as the platform's code spells it"}, + "source_ref": {"type": "string", "minLength": 1, "description": "path:lines of the code that puts it there"} + }, + "description": "How the config file a record cites reaches the emulator: link (a) of the evidence a config record carries" + }, + "kept_if_present": { + "type": "boolean", + "description": "True when the install leaves a file already there in place (cp if absent, rsync --ignore-existing): the record holds on a fresh install" + }, + "overrides": { + "type": "array", + "items": {"type": "string"}, + "description": "path:lines of launch code that rewrites the record's keys or replaces the installed config, empty when the launch path writes neither: link (b)" + }, + "choices_from": { + "type": "string", + "description": "path:lines of the feature or option file (es_features, a core .opt) listing the values the option the record writes takes. It is never the record's evidence: source_ref cites the code writing the choice" + }, "source_ref": { "type": "string", "description": "path:lines in the platform's repository at the pinned revision" diff --git a/scripts/common.py b/scripts/common.py index cb3e294d1..9ed339cb7 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -3189,11 +3189,12 @@ def standalone_bindings_for( ``systems`` are those the entry is launched for (entry_launch_systems). """ - declared = profile.get("standalone_directories") or [] - pools = profile.get("standalone_pools") or {} + from launchers import directory_handles, pool_folders, standalone_bindings # noqa: PLC0415 + + declared = directory_handles(profile) + pools = pool_folders(profile) if not declared and not pools: return {} - from launchers import standalone_bindings # noqa: PLC0415 names = {emu_name, *(str(c) for c in profile.get("cores") or [])} layout = [ @@ -3298,9 +3299,9 @@ def platform_layouts( # The layout folders a search setting stands for (standalone_pools): # bound, the launcher passes the setting and the default is not read. - pool_folders = { - str(v).strip("/") for v in (profile.get("standalone_pools") or {}).values() - } - {""} + from launchers import pool_folders as _pool_folders # noqa: PLC0415 + + pool_folders = {v.strip("/") for v in _pool_folders(profile).values()} - {""} def passed_setting(entry: dict) -> bool: layout = str(standalone_layout_path(entry, profile) or "") diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index 2f993d8c8..580bacd39 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -56,9 +56,11 @@ from common import ( from hashing import parse_hash_list from launchers import ( LauncherLayer, + bound_launchers, catalogued_model_files, declared_destinations, held_record, + launcher_unproven, name_settings, set_by, setting_readers, @@ -1095,6 +1097,8 @@ def _settle_launcher_places( # The --json key of the launcher reads category, beside the emulator keys. LAUNCHER_READS_KEY = "_launcher_reads_not_held" +# The --json key of the launcher records a config file does not prove. +LAUNCHER_UNPROVEN_KEY = "_launcher_unproven" # A package record names no file: it records what a package builds. _NAMELESS_KINDS = frozenset({"package"}) @@ -1238,7 +1242,9 @@ def launcher_reads_not_held( rows: list[dict] = [] for platform, config in configs.items(): by_key: dict[str, dict] = {} - for record in config.get("launchers") or []: + # A record resting on a config file it cannot prove reads nothing + # yet: it is reported apart (launcher_unproven). + for record in bound_launchers(config, profiles): names = launcher_file_names(record) if not names: continue @@ -1331,6 +1337,25 @@ def launcher_reads_not_held( return rows +def print_launcher_unproven(rows: list[dict]) -> None: + """The launcher records resting on a config file the code does not prove. + + Neither a pack, a manifest nor a severity reads them: each is listed + with the links it lacks. + """ + if not rows: + return + platforms = sorted({r["platform"] for r in rows}) + noun = "platform" if len(platforms) == 1 else "platforms" + print(f"\nLauncher records unproven: {len(rows)} across {len(platforms)} {noun}") + for platform in platforms: + print(f"\n{platform}") + for row in (r for r in rows if r["platform"] == platform): + print(f" [{row['index']}] {row['emulator']} {row['kind']}: {row['passes_to']}") + for link in row["missing"]: + print(f" missing {link}") + + def print_launcher_reads(rows: list[dict]) -> None: """The launcher reads gap category, after the profile gaps. @@ -1649,8 +1674,25 @@ def main() -> None: db, all_profiles, registry, emulator or "", ) + unproven = [ + row for row in launcher_unproven( + { + name: load_platform_config(name, args.platforms_dir) + for name in (selected or list_registered_platforms(args.platforms_dir)) + }, + all_profiles, + ) + if not emulator or any( + emulator in LauncherLayer._profiles_named(part, all_profiles, set(all_profiles)) + for part in row["emulator"].split(", ") + ) + ] + if args.json: - print(json.dumps({**report, LAUNCHER_READS_KEY: launcher_reads}, indent=2)) + print(json.dumps( + {**report, LAUNCHER_READS_KEY: launcher_reads, LAUNCHER_UNPROVEN_KEY: unproven}, + indent=2, + )) else: if emulator: scope = emulator @@ -1660,6 +1702,7 @@ def main() -> None: scope = "all emulators" print_report(report, scope, show_clean=bool(args.emulator)) print_launcher_reads(launcher_reads) + print_launcher_unproven(unproven) if __name__ == "__main__": diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 8272392a7..c67e31afd 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -3646,13 +3646,15 @@ def _served_line(f: dict, declared_sha1: str = "") -> str: def _launcher_refs(config: dict) -> dict[str, list[str]]: """Entry name, casefolded -> each launcher record that opens it, rendered.""" from launchers import _names as launcher_names + from launchers import bound_launchers from sitesources import launcher_ref_markdown, list_source # Every scraper reads its launchers in the repository and at the revision - # of the list it scrapes, so `source` pins each source_ref. + # of the list it scrapes, so `source` pins each source_ref. A record a + # config file does not prove opens nothing: it is listed apart. source = list_source(str(config.get("source") or "")) found: dict[str, list[str]] = {} - for record in config.get("launchers") or []: + for record in bound_launchers(config): if not isinstance(record, dict) or not record.get("source_ref"): continue text = (f"{_escape_tags(str(record.get('emulator', '')))} at " @@ -3664,6 +3666,31 @@ def _launcher_refs(config: dict) -> dict[str, list[str]]: return found +UNPROVEN_TITLE = "Launcher records a config does not prove" + + +def _launcher_unproven_section(config: dict) -> list[str]: + """The platform's launcher records resting on a config the code does not prove. + + A config file is proof only when the code installing it and the code + reading it are shown: these records place no file. + """ + from launchers import launcher_unproven + from sitesources import launcher_ref_markdown, list_source + + rows = launcher_unproven({str(config.get("_key") or ""): config}) + if not rows: + return [] + source = list_source(str(config.get("source") or "")) + body = [ + f"- {_escape_tags(row['emulator'])}, `{_escape_tags(row['passes_to'] or row['kind'])}` " + f"at {launcher_ref_markdown(source, row['source_ref'])}: " + f"{_escape_tags('; '.join(row['missing']))}" + for row in rows + ] + return ["", *markup.details("note", f"{UNPROVEN_TITLE} ({len(rows)})", body)] + + # The hash a row shows first, when the list does not say which it checks. _ROW_HASHES = ("sha1", "md5", "sha256", "crc32") # An untested entry whose reason is a hash the bytes do not match. @@ -4802,6 +4829,7 @@ def generate_platform_page( _render_platform_systems(by_system, config_files, emulator_files, (name, display), moved, config) ) + lines.extend(_launcher_unproven_section(config)) lines.extend(["", _date_line()]) return "\n".join(lines) + "\n" diff --git a/scripts/launchers.py b/scripts/launchers.py index 2884035b9..c790af076 100644 --- a/scripts/launchers.py +++ b/scripts/launchers.py @@ -254,11 +254,318 @@ def _names(record: dict) -> list[str]: return names -def launcher_places(config: dict) -> list[LauncherPlace]: +# How a record's passes_to names what carries the file when it names no +# config file: an option on the command line, or a libretro core option. +COMMAND_LINE = "command line" +CORE_OPTIONS = "core options" +_TAIL_WORDS = re.compile(r"\s+(?:unset|left empty)$") +_KEY_TOKEN = re.compile(r"^-{0,2}[\w.$()/\\]+$") + + +def config_target(record: dict) -> tuple[str, list[str]]: + """The config a record writes its file to, and the keys it names there. + + passes_to spells them as describe_keys does: "settings.ini [BIOS] + SearchDirectory, [BIOS] PathNTSCU, PathNTSCJ unset", "command line + -hard", "core option pcsx2_bios", "hatarib_tos = hatarib/". A + part that reads as prose ("only if the file exists") names no key. When + passes_to names no file ("romlist coco"), the file is the one the record + installs and the whole of passes_to is the key. + """ + # A parenthesis says how the file is passed, unless a config variable + # opens it: $(FILE_PATH) is amiberry's. + text = re.sub(r"(?" sets every key before the value. + names = [p.strip() for p in re.split(r"\s*=\s*", chunk)] + if len(names) > 1: + names = names[:-1] + for name in names: + if not _KEY_TOKEN.match(name): + continue + key = f"{section} {name}" if section else name + if key not in keys: + keys.append(key) + return head, keys + + +def _read_keys(read: dict) -> dict[str, str]: + """Each key a config_reads entry says the build reads, to its source_ref.""" + found: dict[str, str] = {} + for entry in read.get("keys") or []: + spelled = entry.get("key") + for key in spelled if isinstance(spelled, list) else [spelled]: + if key: + found[str(key)] = str(entry.get("source_ref") or "") + return found + + +def _handle_refs(profile: dict) -> dict[str, str]: + """The handles a profile names with the code that reads them.""" + found: dict[str, str] = {} + for item in profile.get("standalone_directories") or []: + if isinstance(item, dict) and item.get("source_ref"): + handle = str(item.get("handle") or "").partition(" = ")[0] + found[handle] = str(item["source_ref"]) + for handle, value in (profile.get("standalone_pools") or {}).items(): + if isinstance(value, dict) and value.get("source_ref"): + found[str(handle)] = str(value["source_ref"]) + return found + + +def directory_handles(profile: dict) -> list[str]: + """standalone_directories as handle strings, a {handle, source_ref} item + by its handle.""" + return [ + str(item.get("handle") or "") if isinstance(item, dict) else str(item) + for item in profile.get("standalone_directories") or [] + ] + + +def pool_folders(profile: dict) -> dict[str, str]: + """standalone_pools as handle to subfolder, a {folder, source_ref} value + by its folder.""" + return { + str(handle): str(value.get("folder") or "") if isinstance(value, dict) else str(value) + for handle, value in (profile.get("standalone_pools") or {}).items() + } + + +def _reads_apply(read: dict, profile: dict, platform: str) -> bool: + """Whether a config_reads entry describes the build ``platform`` ships. + + An entry naming platforms is read at their pin_build revision. One that + names none is read at the profile's pin, the build of every platform + with no pin_build of its own or one pinning the same commit. + """ + named = [str(p) for p in read.get("platforms") or []] + if named: + return platform in named + pin = str(profile.get("source_commit") or "") + builds = [b for b in profile.get("pin_build") or [] if b.get("platform") == platform] + return all(pin and str(b.get("value") or "").startswith(pin[:12]) for b in builds) + + +def _file_matches(read_file: str, file: str, record: dict) -> bool: + """Whether a config_reads file is the one a record writes. + + The entry names the file as the build finds it: a name, a glob ("*.uae", + any file the launcher passes), a path, or the flag the launcher passes + it with (amiberry's -f). + """ + from fnmatch import fnmatch # noqa: PLC0415 + + spelled = read_file.rsplit("/", 1)[-1].casefold() + if file and fnmatch(file.casefold(), spelled): + return True + installed = str((record.get("installs") or {}).get("path") or "").rsplit("/", 1)[-1] + if installed and fnmatch(installed.casefold(), spelled): + return True + return read_file.startswith("-") and read_file in str(record.get("passes_to") or "").split() + + +@dataclass(frozen=True) +class ConfigEvidence: + """Whether a launcher record that rests on a config holds, and why not. + + ``config`` holds the parts of its source_ref that cite a config, option + or feature file. A record citing none, and choosing among no listed + option values, rests on code alone and holds. + """ + + config: tuple[str, ...] + missing: tuple[str, ...] + readers: tuple[str, ...] = () + + @property + def proven(self) -> bool: + return not self.missing + + +def _named_profiles(record: dict, profiles: Mapping[str, dict], platform: str) -> list[str]: + """The profiles a record's emulator names, a platform's own build first.""" + found = [] + for part in (e.strip() for e in str(record.get("emulator", "")).split(",")): + named = [ + key for key, profile in profiles.items() + if profile.get("type") not in ("alias", "launcher") + and (key == part or part in {str(c) for c in profile.get("cores") or []}) + ] + own = [k for k in named if platform in (profiles[k].get("built_by") or [])] + found.extend(own or [k for k in named if not profiles[k].get("built_by")]) + return sorted(set(found)) + + +def config_evidence( + record: dict, profiles: Mapping[str, dict], platform: str = "" +) -> ConfigEvidence: + """What a record citing a config file proves, link by link. + + A config file is proof only when the code that installs it and the code + that reads it are shown: (a) the platform installs the file at a path + (``installs``), (b) its launch code was read for what replaces the file + or the key (``overrides``), (c) the emulator loads that file and (d) + reads each key and opens the value (``config_reads`` of its profile, at + the build the platform ships). A feature or option file only lists the + values a menu offers: it goes in ``choices_from``, beside the code that + writes the choice, and such a record needs (c) and (d) too. + """ + from scraper.shell_launchers import config_ref, is_feature_ref # noqa: PLC0415 + + cited = config_ref(str(record.get("source_ref") or "")) + choices = str(record.get("choices_from") or "") + if not cited and not choices: + return ConfigEvidence((), ()) + missing: list[str] = [] + features = [part for part in cited if is_feature_ref(part)] + if features: + missing.append("an option list cited as source, not as choices_from") + shipped = [part for part in cited if not is_feature_ref(part)] + if shipped: + installs = record.get("installs") or {} + if not (installs.get("path") and installs.get("source_ref")): + missing.append("installs: the code that puts the config in place") + if "overrides" not in record: + missing.append("overrides: the launch code read for what replaces it") + file, keys = config_target(record) + if not keys: + missing.append("a key the config hands the file to") + readers: list[str] = [] + uncovered = list(keys) + loaded = False + for key in _named_profiles(record, profiles, platform) or sorted(profiles): + profile = profiles[key] + handles = _handle_refs(profile) + for read in profile.get("config_reads") or []: + if not _reads_apply(read, profile, platform): + continue + if not _file_matches(str(read.get("file") or ""), file, record): + continue + loaded = True + known = _read_keys(read) + before = len(uncovered) + uncovered = [k for k in uncovered if not (known.get(k) or handles.get(k))] + if len(uncovered) < before and key not in readers: + readers.append(key) + if not loaded: + missing.append(f"config_reads: an emulator that loads {file or 'the config'}") + elif uncovered: + missing.append("config_reads: no read of " + ", ".join(uncovered)) + return ConfigEvidence(tuple(cited or [choices]), tuple(missing), tuple(readers)) + + +_DEFAULT_PROFILES: dict[str, dict] | None = None + + +def _profiles_or_repo(profiles: Mapping[str, dict] | None) -> Mapping[str, dict]: + """The profiles given, else this repository's, read once.""" + global _DEFAULT_PROFILES # noqa: PLW0603 + if profiles is not None: + return profiles + if _DEFAULT_PROFILES is None: + from pathlib import Path # noqa: PLC0415 + + from common import load_emulator_profiles # noqa: PLC0415 + + _DEFAULT_PROFILES = load_emulator_profiles( + str(Path(__file__).resolve().parent.parent / "emulators") + ) + return _DEFAULT_PROFILES + + +def _cites_config(record: dict) -> bool: + from scraper.shell_launchers import config_ref # noqa: PLC0415 + + return bool(record.get("choices_from")) or bool(config_ref(str(record.get("source_ref") or ""))) + + +def bound_launchers(config: dict, profiles: Mapping[str, dict] | None = None) -> list[dict]: + """The launcher records a platform's packs, manifests and reports consume. + + A record resting on a config file binds only when config_evidence holds + for it. The repository's profiles are read only when a record cites one. + """ + records = [r for r in config.get("launchers") or [] if isinstance(r, dict)] + if not any(_cites_config(r) for r in records): + return records + known = _profiles_or_repo(profiles) + platform = str(config.get("_key") or "") + return [r for r in records if config_evidence(r, known, platform).proven] + + +def launcher_unproven( + configs: Mapping[str, dict], profiles: Mapping[str, dict] | None = None +) -> list[dict]: + """The records resting on a config file whose evidence does not hold. + + One row per record, with the config parts it cites and each missing + link. Reported apart: no severity, count or exit code reads it. + """ + rows: list[dict] = [] + for platform, config in configs.items(): + records = [r for r in config.get("launchers") or [] if isinstance(r, dict)] + if not any(_cites_config(r) for r in records): + continue + known = _profiles_or_repo(profiles) + for index, record in enumerate(records): + evidence = config_evidence(record, known, str(config.get("_key") or platform)) + if evidence.proven: + continue + rows.append({ + "platform": platform, + "index": index, + "emulator": str(record.get("emulator", "")), + "kind": str(record.get("kind", "")), + "passes_to": str(record.get("passes_to") or ""), + "source_ref": str(record.get("source_ref", "")), + "config": list(evidence.config), + "missing": list(evidence.missing), + }) + return rows + + +def launcher_places( + config: dict, profiles: Mapping[str, dict] | None = None +) -> list[LauncherPlace]: """Every place a platform's launchers open a named file from.""" base = str(config.get("base_destination") or "bios") places: list[LauncherPlace] = [] - for record in config.get("launchers") or []: + for record in bound_launchers(config, profiles): kind = record.get("kind") if kind not in PLACE_KINDS: continue @@ -484,7 +791,9 @@ def _listed_directories(raw: str) -> list[str] | None: return None -def handed_directories(config: dict) -> list[HandedDirectory]: +def handed_directories( + config: dict, profiles: Mapping[str, dict] | None = None +) -> list[HandedDirectory]: """Every whole directory a platform links or passes to a standalone build. A record naming files (gpsp's gba_bios.bin) or one slot of a directory @@ -499,7 +808,7 @@ def handed_directories(config: dict) -> list[HandedDirectory]: natives = native_systems(config) declared = {d.lower() for d in declared_destinations(config)} found: list[HandedDirectory] = [] - for record in config.get("launchers") or []: + for record in bound_launchers(config, profiles): if record.get("build") != "standalone" or record.get("kind") not in HANDED_KINDS: continue if record.get("names") or record.get("slot"): @@ -988,7 +1297,7 @@ class LauncherLayer: db: dict, declared: Iterable[str], ) -> None: - self.places = launcher_places(config) + self.places = launcher_places(config, profiles) self.declared = set(declared) self.declared_folded = {d.lower() for d in self.declared} self.declared_names = {d.rsplit("/", 1)[-1] for d in self.declared} diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index b10c2531c..94c663b81 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -1047,6 +1047,11 @@ class ProfileReport: new_files_elided: dict[str, int] = field(default_factory=dict) new_hashes: list[dict] | None = None detection_skipped: str | None = None + # config_reads refs written for a build a platform ships, each read at + # that build (pin_build) rather than at source_commit: {platform, + # revision, ref, status, detail}. A status other than BUILD_OK is review + # work. + build_refs: list[dict] = field(default_factory=list) # Skips that say the profile cannot be checked as written: a repository # that no longer answers, no pin to read, a death the forge contradicts. @@ -1070,6 +1075,7 @@ class ProfileReport: + len(self.orphaned) + len(self.stale_prose or ()) + len(self.moved_repos) + + sum(1 for ref in self.build_refs if ref["status"] != "BUILD_OK") ) @@ -1603,6 +1609,81 @@ def moved_repositories( return moved +def shipped_build_holders(profile: dict) -> dict[int, dict]: + """The source_ref holders of config_reads entries naming platforms, by id. + + Such an entry describes the build those platforms ship, so its refs are + read at their pin_build revision, never at source_commit. + """ + found: dict[int, dict] = {} + for read in profile.get("config_reads") or []: + if not isinstance(read, dict) or not read.get("platforms"): + continue + found[id(read)] = read + for key in read.get("keys") or []: + if isinstance(key, dict): + found[id(key)] = read + return found + + +def build_revision(profile: dict, platform: str) -> str | None: + """The emulator revision a platform's pin_build entry names, None when + its value is no revision (a URL, a package name).""" + for build in profile.get("pin_build") or []: + if build.get("platform") != platform: + continue + value = str(build.get("value") or "").strip() + return value if value and re.fullmatch(r"[\w.+-]+", value) else None + return None + + +def _check_build_refs( + profile: dict, + deferred: list[tuple[Citation, dict]], + views: list[RepoView], + fetch: Callable[[upstream.Repo, str, str], list[str] | None], +) -> list[dict]: + """Each deferred config_reads ref read at the build of its platforms. + + The file must exist at that revision and every cited line lie inside + it. The entry declares no value to anchor on, so this is the check a + ref past the end of its file fails (BUILD_GONE), not a content match. + """ + results: list[dict] = [] + for citation, read in deferred: + for platform in read.get("platforms") or []: + revision = str(read.get("revision") or "") or build_revision(profile, str(platform)) + row = {"platform": str(platform), "revision": revision or "", + "ref": citation.ref, "field": citation.field} + if revision is None: + results.append({**row, "status": "BUILD_UNREAD", + "detail": "no pin_build revision for this platform"}) + continue + problems: list[str] = [] + for part in split_source_ref(citation.ref): + role, path = ref_role(part.path) + if " " in path: + # A project word in front (`ROCKNIX patches/x.patch:4`): + # a file of the platform's tree, read with the record. + continue + lines = next( + (found for view in views + if (not role or view.field == role) + and (found := fetch(view.repo, revision, path)) is not None), + None, + ) + if lines is None: + problems.append(f"{part.path} absent at {revision[:12]}") + elif (part.end or part.start or 0) > len(lines): + problems.append(f"{part.raw} past the end ({len(lines)} lines)") + results.append({ + **row, + "status": "BUILD_GONE" if problems else "BUILD_OK", + "detail": "; ".join(problems), + }) + return results + + def build_report( name: str, profile: dict, cache_dir: str, offline: bool = False, text: str | None = None, @@ -1621,7 +1702,13 @@ def build_report( ) modes = {mode for _, mode, _ in declared_repositories(profile) if mode} refs = [] + at_build = shipped_build_holders(profile) + deferred: list[tuple[Citation, dict]] = [] for citation in collect_citations(profile, text): + if citation.kind == "ref" and id(citation.holder) in at_build: + # Written for the build a platform ships: read at pin_build. + deferred.append((citation, at_build[id(citation.holder)])) + continue if citation.kind == "ref": entry = citation.entry display = str(entry.get("name") or "") or citation.field @@ -2204,6 +2291,8 @@ def build_report( ) return result + report.build_refs = _check_build_refs(profile, deferred, views, fetch_from) + staged = [] for entry_name, ref, tokens, hashes, ref_parts, citation in refs: flags = [self_checked(part, tokens) for part in ref_parts] @@ -2335,6 +2424,14 @@ def format_report(report: ProfileReport, changed_only: bool = False) -> str: lines.append(f" {len(report.entries)} refs: {summary}") for line in report.stale_prose or (): lines.append(f" prose under an older pin: {line}") + for ref in report.build_refs: + if changed_only and ref["status"] == "BUILD_OK": + continue + detail = f": {ref['detail']}" if ref["detail"] else "" + lines.append( + f" {ref['status']} at {ref['platform']} build " + f"{ref['revision'][:12] or '-'} {ref['ref']}{detail}" + ) lines.append(f" {report.needs_review()} to review") return "\n".join(lines) diff --git a/scripts/scraper/shell_launchers.py b/scripts/scraper/shell_launchers.py index 5aa9a56bf..d847b2666 100644 --- a/scripts/scraper/shell_launchers.py +++ b/scripts/scraper/shell_launchers.py @@ -63,6 +63,48 @@ def source_ref(parts: Iterable[tuple[str, Iterable[int]]]) -> str: ) +# The files a platform ships as configuration: an emulator reads them, the +# platform's code never runs them. Batocera's .yml lists and an EmuELEC .opt +# only offer the values a menu option takes. +_CONFIG_FILE = re.compile( + r"\.(?:ini|cfg|conf|rc|rc_\w+|opt|bml|toml|uae|template|yml|yaml)$", re.IGNORECASE +) +_FEATURE_FILE = re.compile(r"(?:^|/)es_features[^/]*$|\.opt$", re.IGNORECASE) + + +def split_ref(ref: str) -> list[str]: + """The parts of a source_ref, each "path:lines", a bare line span + continuing the path before it.""" + parts: list[str] = [] + for part in (p.strip() for p in ref.split(", ")): + if not part: + continue + if re.fullmatch(r"\d+(?:-\d+)?", part) and parts: + parts[-1] += f", {part}" + else: + parts.append(part) + return parts + + +def ref_path(part: str) -> str: + return part.rsplit(":", 1)[0] if re.search(r":\d", part) else part + + +def config_ref(ref: str) -> list[str]: + """The parts of a source_ref that cite a config, option or feature file. + + Such a part proves what the file says, never that an emulator reads it: + melonDS.ini keys are written on ROCKNIX for a build that reads them only + when another key is set. + """ + return [part for part in split_ref(ref) if _CONFIG_FILE.search(ref_path(part))] + + +def is_feature_ref(part: str) -> bool: + """Whether a source_ref part cites a file that lists a menu option's values.""" + return bool(_FEATURE_FILE.search(ref_path(part))) + + @dataclass class Word: """A shell word: as written, and with quotes removed.""" @@ -394,6 +436,149 @@ def _merge_links(links: list[Link]) -> list[Link]: return [link for link in merged if id(link) not in folded] +_COPY = re.compile(r"(?:^|[\s;&|(])(cp|rsync|install|mv)\s") +_GUARD = re.compile(r"\[\[?\s*!\s+-[fde]\s+(\S+)\s*\]") + + +@dataclass +class ConfigPlacement: + """How a start script puts a config where the emulator reads it. + + ``install`` are the lines copying it there, ``kept_if_present`` whether + every such copy runs only when the file (or its folder) is absent, and + ``overrides`` the sed lines rewriting one of the record's keys in it. + """ + + script: str + path: str + install: set[int] + kept_if_present: bool + overrides: set[int] + + +def _bare_key(key: str) -> str: + return key.rsplit("] ", 1)[-1].rsplit("/", 1)[-1].strip() + + +def _guarded(script: Script, number: int, target: str) -> bool: + """Whether a line runs only when target, or a folder above it, is absent.""" + + def covers(found: re.Match[str], at: int) -> bool: + guarded = script.expand(words(found.group(1))[0].text, at, set()).rstrip("/") + return target == guarded or target.startswith(guarded + "/") + + line = script.lines[number - 1] + if any(covers(m, number) for m in _GUARD.finditer(line)): + return True + depth = 0 + for at in range(number - 1, 0, -1): + text = script.lines[at - 1].strip() + if re.match(r"fi\b", text): + depth += 1 + elif re.match(r"if\b", text): + if depth: + depth -= 1 + continue + return any(covers(m, at) for m in _GUARD.finditer(text)) + return False + + +def config_placement( + script: Script, + target: str, + keys: Iterable[str] = (), + writers: Iterable[str] = (), + source_name: str = "", + required: bool = True, + sources: tuple[str, ...] = (), +) -> ConfigPlacement: + """The lines of a start script that install ``target`` and rewrite its keys. + + A copy installs the target when its destination is the file or a folder + above it. ``writers`` are other commands that write the file (ROCKNIX + generates mednafen.cfg with mednafen_gen_config.sh). A copy guarded by + `[ ! -f X ]` or `[ ! -d X ]` on the target or a folder above it, or made + with cp -n or rsync --ignore-existing, leaves a file already there. + """ + install: set[int] = set() + kept = True + overrides: set[int] = set() + bare = {_bare_key(k) for k in keys} - {""} + writer = re.compile("|".join(re.escape(w) for w in writers)) if writers else None + for number, line in script.code(): + if (copy := _COPY.search(line)) or (writer and writer.search(line)): + if copy: + args = [] + for word in words(line[copy.start(1):])[1:]: + # The command ends at a separator or a redirection. + if word.raw in {";", "&&", "||", "|"} or re.match(r"\d*>", word.raw): + break + args.append(word.text) + operands = [a for a in args if not a.startswith("-")] + if not operands: + continue + destination = script.expand(operands[-1], number, set()).rstrip("/") + source = script.expand(operands[0], number, set()).rstrip("/") + hits = target == destination or target.startswith(destination + "/") or ( + target.startswith(f"{destination}/{source.rsplit('/', 1)[-1]}") + ) + # A copy of another file into the same place is another + # record's: amiberry copies the A500 or the A1200 setup. + named = source.rsplit("/", 1)[-1] + if not hits or (source_name and "." in named and named != source_name): + continue + # A named file copied into a folder above the target is + # another file of that folder (ARMSX2's secrets.ini). + if ( + not source_name and "." in named and "*" not in named + and destination != target and named != target.rsplit("/", 1)[-1] + ): + continue + if sources and not source.startswith(sources): + continue + skip = "-n" in args or "--ignore-existing" in args + else: + skip = False + install.add(number) + if not (skip or _guarded(script, number, target)): + kept = False + elif re.search(r"\bsed\b.*\s-i\b|\bsed -i", line) and bare: + expanded = script.expand(line, number, set()) + if target in expanded or target.rsplit("/", 1)[-1] in expanded: + if any(re.search(rf"\b{re.escape(k)}\b", expanded) for k in bare): + overrides.add(number) + if not install and required: + raise script.missing(f"install of {target}") + return ConfigPlacement(script.path, target, install, kept, overrides) + + +def evidence_fields( + placements: Iterable[ConfigPlacement], + installed: Iterable[tuple[str, Iterable[int]]] = (), +) -> dict: + """The installs, kept_if_present and overrides fields of a config record. + + ``placements`` are the steps that put the file in place, in the order + they run (a first-boot copy, then the start script). ``installed`` are + the build steps that put it where they copy it from (a package.mk copy + into /usr/config). + """ + placements = list(placements) + steps = [p for p in placements if p.install] + if not steps: + raise LauncherPatternMissing(placements[0].path if placements else "", "install") + return { + "installs": { + "path": steps[-1].path, + "source_ref": source_ref([*installed, *((p.script, p.install) for p in steps)]), + }, + "kept_if_present": all(p.kept_if_present for p in steps), + "overrides": [ + source_ref([(p.script, {line})]) for p in placements for line in sorted(p.overrides) + ], + } + + @dataclass class Setting: section: str diff --git a/scripts/validate_schemas.py b/scripts/validate_schemas.py index 97b40707a..917dea729 100644 --- a/scripts/validate_schemas.py +++ b/scripts/validate_schemas.py @@ -81,7 +81,10 @@ def _validate_yaml_directory( continue out.extend(_errors(validator, data, str(path.relative_to(ROOT)))) out.extend(_unreachable_citations(path, data)) + if schema_name == "platform.schema.json": + out.extend(_config_records_without_evidence(path, data)) if schema_name == "emulator.schema.json": + out.extend(_config_reads_without_build(path, data)) out.extend(_checks_without_values(path, data)) out.extend(_save_dir_without_library_name(path, data)) out.extend(_systems_outside_profile(path, data)) @@ -102,6 +105,59 @@ _CHECK_FIELDS = { } +def _config_records_without_evidence(path: Path, document: object) -> list[str]: + """A launcher record citing a config without the links its extractor reads. + + A config file is proof only when the code installing it and the code + reading it are shown (launchers.config_evidence). The platform's half + is the extractor's to write: a record citing a shipped config carries + installs and overrides, and a feature or option file is never a + record's source, only its choices_from. + """ + if not isinstance(document, dict): + return [] + from scraper.shell_launchers import config_ref, is_feature_ref # noqa: PLC0415 + + out: list[str] = [] + for index, record in enumerate(document.get("launchers") or []): + if not isinstance(record, dict): + continue + cited = config_ref(str(record.get("source_ref") or "")) + where = f"{path.relative_to(ROOT)}: launchers[{index}] ({record.get('emulator')})" + features = [part for part in cited if is_feature_ref(part)] + if features: + out.append(f"{where} cites {features[0]} as source, choices_from holds it") + if [part for part in cited if not is_feature_ref(part)] and not ( + record.get("installs") and "overrides" in record + ): + out.append(f"{where} cites a config without installs and overrides") + return out + + +def _config_reads_without_build(path: Path, document: object) -> list[str]: + """A config_reads entry naming a platform the profile gives no pin_build. + + Such an entry is read at the build that platform ships, and pin_build is + where that build is named. + """ + if not isinstance(document, dict): + return [] + builds = {str(b.get("platform")) for b in document.get("pin_build") or []} + out: list[str] = [] + for index, read in enumerate(document.get("config_reads") or []): + if not isinstance(read, dict): + continue + if read.get("revision"): + continue + for platform in read.get("platforms") or []: + if str(platform) not in builds: + out.append( + f"{path.relative_to(ROOT)}: config_reads[{index}] ({read.get('file')}) " + f"names {platform}, which has no pin_build" + ) + return out + + def _systems_outside_profile(path: Path, document: object) -> list[str]: """An entry's system: or systems: naming a system its profile does not list. diff --git a/scripts/verify.py b/scripts/verify.py index 8c2d40e57..a3fe5fb02 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -100,7 +100,7 @@ from common import ( yaml = require_yaml() from exporter.candidates import BIOS, EntryCategories -from launchers import LauncherLayer, declared_destinations, held_record +from launchers import LauncherLayer, declared_destinations, held_record, launcher_unproven from cross_reference import launcher_reads_not_held from nativemode import ( SIZE_AND_ANY_HASH, @@ -1764,6 +1764,8 @@ def verify_platform( "launcher_reads_not_held": launcher_reads_not_held( {config.get("_key", platform): config}, db, profiles, data_dir_registry ), + # Records a config file does not prove: listed, never consumed. + "launcher_unproven": launcher_unproven({config.get("_key", platform): config}, profiles), "exclusion_notes": exclusions, "region_fallbacks": sorted(region_fallbacks), **({"left_out": left_out} if no_game_data else {}), @@ -2130,6 +2132,21 @@ def _print_undeclared_section(result: dict, verbose: bool) -> None: ) +def _print_launcher_unproven(result: dict, verbose: bool) -> None: + """Launcher records resting on a config file the code does not prove.""" + rows = result.get("launcher_unproven") or [] + if not rows: + return + listed = "" if verbose else " (--verbose lists them)" + print(f" Launcher records unproven: {len(rows)}{listed}") + if not verbose: + return + for row in rows: + print(f" [{row['index']}] {row['emulator']} {row['kind']}: {row['passes_to']}") + for link in row["missing"]: + print(f" missing {link}") + + def _print_launcher_reads(result: dict, verbose: bool) -> None: """Names the platform's launch code reads that the collection lacks.""" every = result.get("launcher_reads_not_held") or [] @@ -2220,6 +2237,7 @@ def print_platform_result( _print_detail_entries(result["details"], seen_details, verbose) _print_undeclared_section(result, verbose) _print_launcher_reads(result, verbose) + _print_launcher_unproven(result, verbose) # The same notes come back for every platform running these cores and # filled 985 of the 1114 lines of verify --all: counted, listed on ask. diff --git a/tests/test_launcher_gaps.py b/tests/test_launcher_gaps.py index e1bdac934..a2e8bdbc1 100644 --- a/tests/test_launcher_gaps.py +++ b/tests/test_launcher_gaps.py @@ -33,6 +33,7 @@ import verify # noqa: E402 from common import _emulator_profiles_cache, load_emulator_profiles, load_platform_config # noqa: E402 from cross_reference import ( # noqa: E402 LAUNCHER_READS_KEY, + LAUNCHER_UNPROVEN_KEY, launcher_file_names, launcher_reads_not_held, print_launcher_reads, @@ -278,7 +279,11 @@ class LauncherGapTest(unittest.TestCase): names = {r["name"] for r in report[LAUNCHER_READS_KEY]} self.assertEqual(names, {"absent.bin", "mixed.rom", "labelled.rom", "user.sf2"}) # The emulator rows keep their own shape and counts. - self.assertEqual(set(report) - {LAUNCHER_READS_KEY}, {"aliasemu", "missemu", "setemu"}) + self.assertEqual( + set(report) - {LAUNCHER_READS_KEY, LAUNCHER_UNPROVEN_KEY}, + {"aliasemu", "missemu", "setemu"}, + ) + self.assertEqual(report[LAUNCHER_UNPROVEN_KEY], []) def test_verify_counts_and_exit_code_are_unchanged(self) -> None: """A launcher name is reported, not judged: nothing else moves.""" diff --git a/tests/test_launcher_layer.py b/tests/test_launcher_layer.py index 0e6e8fae0..7f37fef6b 100644 --- a/tests/test_launcher_layer.py +++ b/tests/test_launcher_layer.py @@ -314,9 +314,9 @@ class LauncherLayerTest(unittest.TestCase): config = {"base_destination": "bios", "launchers": [{ "emulator": "e", "build": "libretro", "kind": "fixed_name", "names": ["a.rom", "b.rom"], "search_dir": "/storage/roms/bios", - "passes_to": "e.cfg ROM_A, ROM_B", "source_ref": "e.cfg:1", + "passes_to": "e.cfg ROM_A, ROM_B", "source_ref": "launch.sh:1", }]} - places = {p.paths[0]: p.passes_to for p in launcher_places(config)} + places = {p.paths[0]: p.passes_to for p in launcher_places(config, {})} self.assertEqual(places, {"a.rom": "e.cfg ROM_A", "b.rom": "e.cfg ROM_B"}) def test_a_core_option_names_the_dump_of_a_model(self) -> None: @@ -341,12 +341,16 @@ class LauncherLayerTest(unittest.TestCase): "files": [{"name": ".bin", "path": "ps2/bios/.bin", "required": True, "min_size": 32, "max_size": 64, "validation": ["size"], "config_key": "emu_bios"}], + "config_reads": [{"file": "core options", "source_ref": "core.c:1", + "keys": [{"key": "emu_bios", "source_ref": "core.c:2"}]}], } with open(os.path.join(self.emulators_dir, "optemu.yml"), "w") as fh: yaml.dump(profile, fh) self.config["launchers"].append({ "emulator": "retroarch", "build": "libretro", "kind": "core_option", "names": ["scph10000.bin"], "passes_to": "core option emu_bios", + "installs": {"path": "retroarch-core-options.cfg", "source_ref": "prepare.sh:24"}, + "overrides": [], "source_ref": "core-options.cfg:515", }) _emulator_profiles_cache.clear() diff --git a/tests/test_launcher_placement.py b/tests/test_launcher_placement.py index b8a24598c..71550bebb 100644 --- a/tests/test_launcher_placement.py +++ b/tests/test_launcher_placement.py @@ -962,6 +962,17 @@ class GzdoomOnRetroDECK(unittest.TestCase): self.assertFalse(any(e.get("standalone_path") for e in view["files"])) +def _reads_configs(profile: str) -> bool: + """Whether a repository profile carries config_reads.""" + path = REPO_ROOT / "emulators" / f"{profile}.yml" + return bool((yaml.safe_load(path.read_text(encoding="utf-8")) or {}).get("config_reads")) + + +# EmuELEC hands AdvanceMAME its folders through advmame.rc: the chain binds +# once the profile says where the build reads them (config_reads). +@unittest.skipUnless( + _reads_configs("advancemame"), "advancemame.yml carries no config_reads, dir_rom is unproven" +) class AdvanceMameOnEmuELEC(unittest.TestCase): """advmame.rc sets dir_rom /storage/roms/mame, /storage/roms/arcade for the arcade system (advmame.rc:11, advmame.sh:26-30).""" diff --git a/wiki/adding-a-platform.md b/wiki/adding-a-platform.md index 446d020bf..2e559804e 100644 --- a/wiki/adding-a-platform.md +++ b/wiki/adding-a-platform.md @@ -445,6 +445,10 @@ launchers: | `fails_scope` | `emulator` when the failure needs every record of the emulator to come up empty | | `depends_on_option` | Options or conditions that decide whether the record applies | | `device` | Devices whose shipped config the record reads, set only where a package ships configs that differ between devices | +| `installs` | For a record citing a config: `path` where the platform's code puts it and the `source_ref` of that code | +| `kept_if_present` | Whether that install leaves a file already there (a copy only if absent, rsync `--ignore-existing`) | +| `overrides` | Launch code rewriting the record's keys or replacing the config, empty when none does | +| `choices_from` | The feature or option file listing the values the option takes (es_features, a core `.opt`), never the record's evidence | | `source_ref` | `path:lines` in the repository and at the revision `source` pins, which the site links | Batocera's generators are read with `ast`, RetroBat's C# generators as text. @@ -453,18 +457,24 @@ package, so `scripts/scraper/shell_launchers.py` reads assignments, `for` loops, `ln -s` commands and `key = value` lines. A ROCKNIX package can ship one config per device: the configs are grouped by the value read, and `device` appears only when there is more than one group. melonDS writes its -seven BIOS paths on every device but AMD64, where the keys are left empty: +BIOS paths on every device but AMD64, where the keys are left empty: ```yaml - emulator: melonds build: standalone kind: fixed_name names: [] - passes_to: melonDS.ini BIOS9Path, BIOS7Path, FirmwarePath, DSiBIOS9Path, - DSiBIOS7Path, DSiFirmwarePath, DSiNANDPath left empty + passes_to: melonDS.ini BIOS9Path, BIOS7Path, FirmwarePath, DSiNANDPath left empty device: AMD64 ``` +A config file is proof only when the code installing it and the code reading +it are shown. A record citing a config carries `installs` and `overrides`, and +the emulator's profile says, in `config_reads`, where the build it ships loads +that file and reads each key. A record whose chain does not hold is listed as +unproven by `verify.py` and `cross_reference.py`, and no pack or manifest +places a file from it. + An extractor that no longer recognises the shape of the code raises: an empty list would read as a launcher that looks for nothing. From c5d3e85baced8d516daa5f39b495a7c7d13d3689 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 11 Nov 2026 11:04:02 +0100 Subject: [PATCH 2/4] feat: record config installs in shell extractors ROCKNIX, EmuELEC, AmberELEC, EmuDeck and RetroDECK records citing a config now carry the code installing it, whether that keeps a file already there, and the launch code rewriting its keys. Rescraped at the pinned revisions. Records the shipped code never reads are no longer written: EmuELEC's mame.ini folders (MAME.opt turns paths off), AmberELEC's atari800.cfg (read only under an option that is off), the six RetroDECK XRoar ROM lists no machine looks up, and the ROCKNIX melonDS DSi BIOS keys (DSiExternalBIOSEnable is set by no shipped ini). Conditions added: ROCKNIX and AmberELEC amiberry kickstarts hold only outside a WHDLoad --autoload boot, ROCKNIX NanoBoyAdvance exits without its BIOS, the EmuELEC scummvm.ini copy runs from an action no launch passes, and the EmuDeck melonDS.toml keys need ExternalBIOSEnable (DS) or ConsoleType 1 (DSi). --- platforms/amberelec.yml | 45 ++-- platforms/emudeck.yml | 107 ++++++++- platforms/emuelec.yml | 229 ++++++++++-------- platforms/retrodeck.yml | 312 ++++++++++++++++++------- platforms/rocknix.yml | 152 ++++++++++-- scripts/scraper/amberelec_launchers.py | 84 ++++--- scripts/scraper/configgen_launchers.py | 11 +- scripts/scraper/emudeck_launchers.py | 63 ++++- scripts/scraper/emuelec_launchers.py | 119 +++++----- scripts/scraper/retrodeck_launchers.py | 48 +++- scripts/scraper/retropie_launchers.py | 8 +- scripts/scraper/rocknix_launchers.py | 177 +++++++++++--- tests/test_launchers_amberelec.py | 60 ++--- tests/test_launchers_emudeck.py | 33 ++- tests/test_launchers_emuelec.py | 37 +-- tests/test_launchers_rocknix.py | 86 ++++--- 16 files changed, 1153 insertions(+), 418 deletions(-) diff --git a/platforms/amberelec.yml b/platforms/amberelec.yml index a4085d839..dbbe87c88 100644 --- a/platforms/amberelec.yml +++ b/platforms/amberelec.yml @@ -3180,6 +3180,11 @@ launchers: search_dir: ~/roms/bios passes_to: retroarch.cfg system_directory fails_when_missing: false + installs: + path: /storage/.config/retroarch/retroarch.cfg + source_ref: "packages/sysutils/systemd/scripts/userconfig-setup:20" + kept_if_present: true + overrides: [] source_ref: "packages/games/tools/retroarch/sources/retroarch.cfg:611" - emulator: retrorun build: libretro @@ -3198,25 +3203,6 @@ launchers: fails_when_missing: false depends_on_option: self.emulator == 'mame' source_ref: "packages/amberelec/sources/scripts/runemu.py:233, 252, 255" -- emulator: atari800 - build: libretro - kind: fixed_name - names: - - ATARIOSA.ROM - - ATARIXL.ROM - - 5200.rom - - 5200a.rom - - BASIC_Rev_A.rom - - BASIC_Rev_B.rom - - ATARIBAS.ROM - - ATARIOSB.ROM - case_insensitive: false - search_dir: /storage/roms/bios - passes_to: atari800.cfg ROM_OS_A_PAL, ROM_OS_BB01R2, ROM_5200, ROM_5200_A, ROM_BASIC_A, - ROM_BASIC_B, ROM_BASIC_C, ROM_400/800_CUSTOM - fails_when_missing: false - source_ref: "packages/games/libretro/atari800/patches/atari800-01-add-emuelec-platform.patch:27,\ - \ packages/amberelec/config/distribution/configs/atari800.cfg:3, 8, 16-22" - emulator: scummvm build: libretro kind: passes_path @@ -3291,7 +3277,13 @@ launchers: passes_to: "AmigaA1200-default.uae kickstart_rom_file, $(FILE_PATH) = /storage/.config/amiberry/kickstarts/" fails_when_missing: false depends_on_option: "[ `echo $1 | egrep 'AGA|CD32' | wc -l` -eq 1 -o `echo \"$AMIBERRY_TMP_DIR\"\ - /* | egrep 'AGA|CD32' | wc -l` -eq 1 ]" + /* | egrep 'AGA|CD32' | wc -l` -eq 1 ], content not booted with --autoload, a\ + \ WHDLoad .lha or .zip (amiberry.sh:83, 122)" + installs: + path: /tmp/emulation/amiberry/.amiberry_conf.uae + source_ref: "packages/games/emulators/amiberry/scripts/amiberry.sh:59" + kept_if_present: false + overrides: [] source_ref: "packages/amberelec/sources/scripts/runemu.py:111, packages/games/emulators/amiberry/scripts/amiberry.sh:57-59,\ \ packages/games/emulators/amiberry/config/conf/AmigaA1200-default.uae:35, 85" - emulator: AMIBERRY @@ -3305,7 +3297,13 @@ launchers: passes_to: "AmigaA500-default.uae kickstart_rom_file, $(FILE_PATH) = /storage/.config/amiberry/kickstarts/" fails_when_missing: false depends_on_option: "not ([ `echo $1 | egrep 'AGA|CD32' | wc -l` -eq 1 -o `echo \"\ - $AMIBERRY_TMP_DIR\"/* | egrep 'AGA|CD32' | wc -l` -eq 1 ])" + $AMIBERRY_TMP_DIR\"/* | egrep 'AGA|CD32' | wc -l` -eq 1 ]), content not booted\ + \ with --autoload, a WHDLoad .lha or .zip (amiberry.sh:83, 122)" + installs: + path: /tmp/emulation/amiberry/.amiberry_conf.uae + source_ref: "packages/games/emulators/amiberry/scripts/amiberry.sh:62" + kept_if_present: false + overrides: [] source_ref: "packages/amberelec/sources/scripts/runemu.py:111, packages/games/emulators/amiberry/scripts/amiberry.sh:60-62,\ \ packages/games/emulators/amiberry/config/conf/AmigaA500-default.uae:35, 85" - emulator: HATARISA @@ -3317,6 +3315,11 @@ launchers: search_dir: /storage/roms/bios passes_to: "Atari-ST-default.cfg [ROM] szTosImageFileName" fails_when_missing: false + installs: + path: /tmp/hatari/.hatari-temp.cfg + source_ref: "packages/games/emulators/hatarisa/scripts/hatari.sh:36" + kept_if_present: false + overrides: [] source_ref: "packages/amberelec/sources/scripts/runemu.py:113, packages/games/emulators/hatarisa/scripts/hatari.sh:36,\ \ 66, packages/games/emulators/hatarisa/config/conf/Atari-ST-default.cfg:200" - emulator: pico8 diff --git a/platforms/emudeck.yml b/platforms/emudeck.yml index 5ef9d2ebd..6513cf18f 100644 --- a/platforms/emudeck.yml +++ b/platforms/emudeck.yml @@ -937,6 +937,12 @@ launchers: search_dir: "${biosPath}" passes_to: settings.bml ColecoVision/Firmware/BIOS.World fails_when_missing: false + installs: + path: "${HOME}/.var/app/dev.ares.ares/data/ares/settings.bml" + source_ref: "functions/EmuScripts/emuDeckares.sh:26, 42, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] source_ref: "configs/dev.ares.ares/data/ares/settings.bml:270, functions/EmuScripts/emuDeckares.sh:6,\ \ 63-64" - emulator: ares @@ -948,6 +954,12 @@ launchers: search_dir: "${biosPath}" passes_to: settings.bml MSX/Firmware/BIOS.Japan fails_when_missing: false + installs: + path: "${HOME}/.var/app/dev.ares.ares/data/ares/settings.bml" + source_ref: "functions/EmuScripts/emuDeckares.sh:26, 42, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] source_ref: "configs/dev.ares.ares/data/ares/settings.bml:278, functions/EmuScripts/emuDeckares.sh:6,\ \ 63-64" - emulator: ares @@ -959,6 +971,12 @@ launchers: search_dir: "${biosPath}" passes_to: settings.bml MSX2/Firmware/BIOS.Japan fails_when_missing: false + installs: + path: "${HOME}/.var/app/dev.ares.ares/data/ares/settings.bml" + source_ref: "functions/EmuScripts/emuDeckares.sh:26, 42, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] source_ref: "configs/dev.ares.ares/data/ares/settings.bml:283, functions/EmuScripts/emuDeckares.sh:6,\ \ 63-64" - emulator: ares @@ -971,6 +989,12 @@ launchers: search_dir: "${biosPath}" passes_to: settings.bml PCEngineCD/Firmware/BIOS.US, PCEngineCD/Firmware/BIOS.Japan fails_when_missing: false + installs: + path: "${HOME}/.var/app/dev.ares.ares/data/ares/settings.bml" + source_ref: "functions/EmuScripts/emuDeckares.sh:26, 42, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] source_ref: "configs/dev.ares.ares/data/ares/settings.bml:291-292, functions/EmuScripts/emuDeckares.sh:6,\ \ 63-64" - emulator: ares @@ -982,6 +1006,12 @@ launchers: search_dir: "${biosPath}" passes_to: settings.bml SuperGrafxCD/Firmware/BIOS.Japan fails_when_missing: false + installs: + path: "${HOME}/.var/app/dev.ares.ares/data/ares/settings.bml" + source_ref: "functions/EmuScripts/emuDeckares.sh:26, 42, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] source_ref: "configs/dev.ares.ares/data/ares/settings.bml:300, functions/EmuScripts/emuDeckares.sh:6,\ \ 63-64" - emulator: ares @@ -993,6 +1023,12 @@ launchers: search_dir: "${biosPath}" passes_to: settings.bml FamicomDiskSystem/Firmware/BIOS.Japan fails_when_missing: false + installs: + path: "${HOME}/.var/app/dev.ares.ares/data/ares/settings.bml" + source_ref: "functions/EmuScripts/emuDeckares.sh:26, 42, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] source_ref: "configs/dev.ares.ares/data/ares/settings.bml:308, functions/EmuScripts/emuDeckares.sh:6,\ \ 63-64" - emulator: ares @@ -1007,6 +1043,12 @@ launchers: passes_to: settings.bml Nintendo64DD/Firmware/BIOS.Japan, Nintendo64DD/Firmware/BIOS.US, Nintendo64DD/Firmware/BIOS.DEV fails_when_missing: false + installs: + path: "${HOME}/.var/app/dev.ares.ares/data/ares/settings.bml" + source_ref: "functions/EmuScripts/emuDeckares.sh:26, 42, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] source_ref: "configs/dev.ares.ares/data/ares/settings.bml:319-321, functions/EmuScripts/emuDeckares.sh:6,\ \ 63-64" - emulator: ares @@ -1018,6 +1060,12 @@ launchers: search_dir: "${biosPath}" passes_to: settings.bml GameBoyAdvance/Firmware/BIOS.World fails_when_missing: false + installs: + path: "${HOME}/.var/app/dev.ares.ares/data/ares/settings.bml" + source_ref: "functions/EmuScripts/emuDeckares.sh:26, 42, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] source_ref: "configs/dev.ares.ares/data/ares/settings.bml:332, functions/EmuScripts/emuDeckares.sh:6,\ \ 63-64" - emulator: ares @@ -1032,6 +1080,12 @@ launchers: passes_to: settings.bml MasterSystem/Firmware/BIOS.US, MasterSystem/Firmware/BIOS.Japan, MasterSystem/Firmware/BIOS.Europe fails_when_missing: false + installs: + path: "${HOME}/.var/app/dev.ares.ares/data/ares/settings.bml" + source_ref: "functions/EmuScripts/emuDeckares.sh:26, 42, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] source_ref: "configs/dev.ares.ares/data/ares/settings.bml:340-342, functions/EmuScripts/emuDeckares.sh:6,\ \ 63-64" - emulator: ares @@ -1043,6 +1097,12 @@ launchers: search_dir: "${biosPath}" passes_to: settings.bml GameGear/Firmware/BIOS.World fails_when_missing: false + installs: + path: "${HOME}/.var/app/dev.ares.ares/data/ares/settings.bml" + source_ref: "functions/EmuScripts/emuDeckares.sh:26, 42, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] source_ref: "configs/dev.ares.ares/data/ares/settings.bml:347, functions/EmuScripts/emuDeckares.sh:6,\ \ 63-64" - emulator: ares @@ -1056,6 +1116,12 @@ launchers: search_dir: "${biosPath}" passes_to: settings.bml MegaCD/Firmware/BIOS.US, MegaCD/Firmware/BIOS.Japan, MegaCD/Firmware/BIOS.Europe fails_when_missing: false + installs: + path: "${HOME}/.var/app/dev.ares.ares/data/ares/settings.bml" + source_ref: "functions/EmuScripts/emuDeckares.sh:26, 42, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] source_ref: "configs/dev.ares.ares/data/ares/settings.bml:358-360, functions/EmuScripts/emuDeckares.sh:6,\ \ 63-64" - emulator: ares @@ -1067,6 +1133,12 @@ launchers: search_dir: "${biosPath}" passes_to: settings.bml NeoGeoAES/Firmware/BIOS.World fails_when_missing: false + installs: + path: "${HOME}/.var/app/dev.ares.ares/data/ares/settings.bml" + source_ref: "functions/EmuScripts/emuDeckares.sh:26, 42, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] source_ref: "configs/dev.ares.ares/data/ares/settings.bml:368, functions/EmuScripts/emuDeckares.sh:6,\ \ 63-64" - emulator: ares @@ -1078,6 +1150,12 @@ launchers: search_dir: "${biosPath}" passes_to: settings.bml NeoGeoPocket/Firmware/BIOS.World fails_when_missing: false + installs: + path: "${HOME}/.var/app/dev.ares.ares/data/ares/settings.bml" + source_ref: "functions/EmuScripts/emuDeckares.sh:26, 42, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] source_ref: "configs/dev.ares.ares/data/ares/settings.bml:378, functions/EmuScripts/emuDeckares.sh:6,\ \ 63-64" - emulator: ares @@ -1089,6 +1167,12 @@ launchers: search_dir: "${biosPath}" passes_to: settings.bml NeoGeoPocketColor/Firmware/BIOS.World fails_when_missing: false + installs: + path: "${HOME}/.var/app/dev.ares.ares/data/ares/settings.bml" + source_ref: "functions/EmuScripts/emuDeckares.sh:26, 42, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] source_ref: "configs/dev.ares.ares/data/ares/settings.bml:383, functions/EmuScripts/emuDeckares.sh:6,\ \ 63-64" - emulator: ares @@ -1103,6 +1187,12 @@ launchers: passes_to: settings.bml PlayStation/Firmware/BIOS.US, PlayStation/Firmware/BIOS.Japan, PlayStation/Firmware/BIOS.Europe fails_when_missing: false + installs: + path: "${HOME}/.var/app/dev.ares.ares/data/ares/settings.bml" + source_ref: "functions/EmuScripts/emuDeckares.sh:26, 42, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] source_ref: "configs/dev.ares.ares/data/ares/settings.bml:388-390, functions/EmuScripts/emuDeckares.sh:6,\ \ 63-64" - emulator: melonds @@ -1121,6 +1211,12 @@ launchers: DSiFirmwarePath, DSiNANDPath fails_when_missing: false depends_on_option: ExternalBIOSEnable = 0 in the shipped melonDS.ini + installs: + path: "${HOME}/.var/app/net.kuribo64.melonDS/config/melonDS/melonDS.ini" + source_ref: "functions/EmuScripts/emuDeckMelonDS.sh:29, 48, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] source_ref: "functions/EmuScripts/emuDeckMelonDS.sh:7, 62-68, 71-77, configs/net.kuribo64.melonDS/config/melonDS/melonDS.ini:77" - emulator: melonds build: standalone @@ -1137,8 +1233,15 @@ launchers: passes_to: "melonDS.toml [DS] FirmwarePath, BIOS7Path, BIOS9Path [DSi] FirmwarePath,\ \ NANDPath, BIOS7Path, BIOS9Path" fails_when_missing: false - depends_on_option: ExternalBIOSEnable = false in the shipped melonDS.toml - source_ref: "configs/net.kuribo64.melonDS/config/melonDS/melonDS.toml:176, 181-183,\ + depends_on_option: "[DS] keys: ExternalBIOSEnable = true, false in the shipped melonDS.toml;\ + \ [DSi] keys: ConsoleType = 1, 0 in the shipped melonDS.toml" + installs: + path: "${HOME}/.var/app/net.kuribo64.melonDS/config/melonDS/melonDS.toml" + source_ref: "functions/EmuScripts/emuDeckMelonDS.sh:29, 48, functions/configEmuFP.sh:11,\ + \ 15" + kept_if_present: true + overrides: [] + source_ref: "configs/net.kuribo64.melonDS/config/melonDS/melonDS.toml:176-178, 181-183,\ \ 186-189, functions/EmuScripts/emuDeckMelonDS.sh:8, 32" - emulator: mupen64plus_next build: standalone diff --git a/platforms/emuelec.yml b/platforms/emuelec.yml index 1c0622059..3c181e6a4 100644 --- a/platforms/emuelec.yml +++ b/platforms/emuelec.yml @@ -4241,96 +4241,6 @@ launchers: fails_when_missing: false depends_on_option: /storage/roms/bios/mame/ini/mame.ini absent source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23" -- emulator: mame - build: libretro - kind: passes_path - names: [] - search_dir: /storage/roms/bios/mame - passes_to: mame.ini homepath - fails_when_missing: false - depends_on_option: mame_read_config = enabled - source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:10, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\ - \ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23" -- emulator: mame - build: libretro - kind: passes_path - names: [] - search_dir: /storage/roms/bios/mame/bios - passes_to: mame.ini rompath - fails_when_missing: false - depends_on_option: mame_read_config = enabled - source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:11, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\ - \ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23" -- emulator: mame - build: libretro - kind: passes_path - names: [] - search_dir: /storage/roms/bios/mame/hash - passes_to: mame.ini hashpath - fails_when_missing: false - depends_on_option: mame_read_config = enabled - source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:12, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\ - \ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23" -- emulator: mame - build: libretro - kind: passes_path - names: [] - search_dir: /storage/roms/bios/mame/samples - passes_to: mame.ini samplepath - fails_when_missing: false - depends_on_option: mame_read_config = enabled - source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:13, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\ - \ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23" -- emulator: mame - build: libretro - kind: passes_path - names: [] - search_dir: /storage/roms/bios/mame/artwork - passes_to: mame.ini artpath - fails_when_missing: false - depends_on_option: mame_read_config = enabled - source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:14, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\ - \ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23" -- emulator: mame - build: libretro - kind: passes_path - names: [] - search_dir: /storage/roms/bios/mame/ini - passes_to: mame.ini inipath - fails_when_missing: false - depends_on_option: mame_read_config = enabled - source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:16, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\ - \ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23" -- emulator: mame - build: libretro - kind: passes_path - names: [] - search_dir: /storage/roms/bios/mame/cheat - passes_to: mame.ini cheatpath - fails_when_missing: false - depends_on_option: mame_read_config = enabled - source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:18, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\ - \ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23" -- emulator: mame - build: libretro - kind: passes_path - names: [] - search_dir: /storage/roms/bios/mame/crosshair - passes_to: mame.ini crosshairpath - fails_when_missing: false - depends_on_option: mame_read_config = enabled - source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:19, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\ - \ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23" -- emulator: mame - build: libretro - kind: passes_path - names: [] - search_dir: /storage/roms/bios/mame/plugins - passes_to: mame.ini pluginspath - fails_when_missing: false - depends_on_option: mame_read_config = enabled - source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:20, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\ - \ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23" - emulator: pico8 build: standalone kind: moves @@ -4384,6 +4294,11 @@ launchers: passes_to: "settings.ini [BIOS] SearchDirectory (-settings), [BIOS] PathNTSCU, PathNTSCJ,\ \ PathPAL unset" fails_when_missing: false + installs: + path: /emuelec/configs/duckstation/settings.ini + source_ref: "packages/sx05re/emulators/duckstation/scripts/duckstation.sh:15" + kept_if_present: true + overrides: [] source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:207-209, packages/sx05re/emulators/duckstation/config/settings.ini:6-9,\ \ packages/sx05re/emulators/duckstation/package.mk:56, packages/sx05re/emulators/duckstation/scripts/duckstation.sh:40" - emulator: yabasanshiroSA @@ -4439,6 +4354,11 @@ launchers: search_dir: /storage/roms/bios passes_to: "Atari-ST-default.cfg [ROM] szTosImageFileName (--configfile)" fails_when_missing: false + installs: + path: /tmp/hatari/.hatari-temp.cfg + source_ref: "packages/sx05re/emulators/hatarisa/scripts/hatari.start:36" + kept_if_present: false + overrides: [] source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:172-174, packages/sx05re/emulators/hatarisa/config/conf/Atari-ST-default.cfg:200,\ \ packages/sx05re/emulators/hatarisa/package.mk:39, packages/sx05re/emulators/hatarisa/scripts/hatari.start:36,\ \ 66, 73" @@ -4450,6 +4370,12 @@ launchers: passes_to: advmame.rc dir_artwork fails_when_missing: false depends_on_option: system != arcade + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4463,6 +4389,12 @@ launchers: search_dir: /storage/roms/arcade/artwork passes_to: advmame.rc dir_artwork fails_when_missing: false + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4475,6 +4407,12 @@ launchers: passes_to: advmame.rc dir_diff fails_when_missing: false depends_on_option: system != arcade + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4488,6 +4426,12 @@ launchers: search_dir: /storage/roms/arcade/diff passes_to: advmame.rc dir_diff fails_when_missing: false + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4500,6 +4444,12 @@ launchers: passes_to: advmame.rc dir_hi fails_when_missing: false depends_on_option: system != arcade + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4513,6 +4463,12 @@ launchers: search_dir: /storage/roms/arcade/hi passes_to: advmame.rc dir_hi fails_when_missing: false + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4525,6 +4481,12 @@ launchers: passes_to: advmame.rc dir_image fails_when_missing: false depends_on_option: system != arcade + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4538,6 +4500,12 @@ launchers: search_dir: /storage/roms/arcade/image passes_to: advmame.rc dir_image fails_when_missing: false + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4550,6 +4518,12 @@ launchers: passes_to: advmame.rc dir_inp fails_when_missing: false depends_on_option: system != arcade + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4563,6 +4537,12 @@ launchers: search_dir: /storage/roms/arcade/inp passes_to: advmame.rc dir_inp fails_when_missing: false + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4575,6 +4555,12 @@ launchers: passes_to: advmame.rc dir_memcard fails_when_missing: false depends_on_option: system != arcade + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4588,6 +4574,12 @@ launchers: search_dir: /storage/roms/arcade/memcard passes_to: advmame.rc dir_memcard fails_when_missing: false + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4600,6 +4592,12 @@ launchers: passes_to: advmame.rc dir_nvram fails_when_missing: false depends_on_option: system != arcade + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4613,6 +4611,12 @@ launchers: search_dir: /storage/roms/arcade/nvram passes_to: advmame.rc dir_nvram fails_when_missing: false + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4625,6 +4629,12 @@ launchers: passes_to: advmame.rc dir_rom fails_when_missing: false depends_on_option: system != arcade + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4638,6 +4648,12 @@ launchers: search_dir: /storage/roms/arcade passes_to: advmame.rc dir_rom fails_when_missing: false + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4649,6 +4665,12 @@ launchers: search_dir: /storage/roms/bios/advmame/samples passes_to: advmame.rc dir_sample fails_when_missing: false + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:80" @@ -4660,6 +4682,12 @@ launchers: passes_to: advmame.rc dir_snap fails_when_missing: false depends_on_option: system != arcade + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4673,6 +4701,12 @@ launchers: search_dir: /storage/roms/arcade/snap passes_to: advmame.rc dir_snap fails_when_missing: false + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4685,6 +4719,12 @@ launchers: passes_to: advmame.rc dir_sta fails_when_missing: false depends_on_option: system != arcade + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4698,6 +4738,12 @@ launchers: search_dir: /storage/roms/arcade/nvram passes_to: advmame.rc dir_sta fails_when_missing: false + installs: + path: /emuelec/configs/advmame/advmame.rc + source_ref: "packages/sx05re/emulators/advancemame/bin/advmame.sh:17, 21" + kept_if_present: false + overrides: + - "packages/sx05re/emulators/advancemame/bin/advmame.sh:27" source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:1-11,\ \ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:1-11, packages/sx05re/emulators/advancemame/config/advmame.rc:4-14,\ \ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:26-27,\ @@ -4720,7 +4766,8 @@ launchers: search_dir: /storage/.config/scummvm passes_to: /storage/roms/bios/scummvm.ini fails_when_missing: false - depends_on_option: scummvm.start add + depends_on_option: "scummvm.start add, an action no launch runs (emuelecRunEmu.sh\ + \ passes libretro, sa)" source_ref: "packages/sx05re/emulators/scummvmsa/bin/scummvm.start:9, 49-51, 54" - emulator: dolphin build: standalone diff --git a/platforms/retrodeck.yml b/platforms/retrodeck.yml index 3592a2537..5df006cdd 100644 --- a/platforms/retrodeck.yml +++ b/platforms/retrodeck.yml @@ -16763,6 +16763,11 @@ launchers: search_dir: $roms_path/doom passes_to: "gzdoom.ini [IWADSearch.Directories] Path" fails_when_missing: false + installs: + path: $gzdoom_config + source_ref: "gzdoom/component_prepare.sh:23, 26-30" + kept_if_present: false + overrides: [] source_ref: "gzdoom/rd_assets/rd_config/gzdoom.ini:9, gzdoom/component_prepare.sh:23,\ \ 27-28" - emulator: gzdoom @@ -16772,6 +16777,11 @@ launchers: search_dir: $roms_path/doom passes_to: "gzdoom.ini [FileSearch.Directories] Path" fails_when_missing: false + installs: + path: $gzdoom_config + source_ref: "gzdoom/component_prepare.sh:23, 26-30" + kept_if_present: false + overrides: [] source_ref: "gzdoom/rd_assets/rd_config/gzdoom.ini:18, gzdoom/component_prepare.sh:23,\ \ 27-28" - emulator: gzdoom @@ -16781,6 +16791,11 @@ launchers: search_dir: $bios_path/gzdoom passes_to: "gzdoom.ini [FileSearch.Directories] Path" fails_when_missing: false + installs: + path: $gzdoom_config + source_ref: "gzdoom/component_prepare.sh:23, 26-30" + kept_if_present: false + overrides: [] source_ref: "gzdoom/rd_assets/rd_config/gzdoom.ini:19, gzdoom/component_prepare.sh:23,\ \ 28" - emulator: gzdoom @@ -16790,6 +16805,11 @@ launchers: search_dir: $roms_path/bios/gzdoom passes_to: "gzdoom.ini [SoundfontSearch.Directories] Path" fails_when_missing: false + installs: + path: $gzdoom_config + source_ref: "gzdoom/component_prepare.sh:23, 26-30" + kept_if_present: false + overrides: [] source_ref: "gzdoom/rd_assets/rd_config/gzdoom.ini:31, gzdoom/component_prepare.sh:23,\ \ 27-28" - emulator: mame @@ -17023,6 +17043,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: xroar.conf rompath fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:290, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17033,6 +17058,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist dragon fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:291, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17048,6 +17078,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist d64_1 fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:292, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17062,6 +17097,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist d64_2 fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:293, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17075,6 +17115,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist d32 fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:294, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17087,6 +17132,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist d200e_1 fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:295, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17099,6 +17149,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist d200e_2 fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:296, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17115,6 +17170,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist dragon64 fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:297, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17129,6 +17189,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist dragon64_alt fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:298, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17143,6 +17208,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist dragon32 fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:299, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17162,6 +17232,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist dragon200e fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:300, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17179,6 +17254,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist dragon200e_alt fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:301, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17190,6 +17270,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist dragon200e_charset fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:302, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17204,6 +17289,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist dragonpro_boot fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:303, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17222,6 +17312,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist dragonpro_basic fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:304, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17236,6 +17331,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist coco fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:305, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17249,89 +17349,12 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist coco_ext fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:306, xroar/component_prepare.sh:13-15" -- emulator: xroar - build: standalone - kind: names_searched - slot: coco1 - names: - - bas10 - - bas13 - - bas12 - - "Color Basic v1.2 (1982)(Tandy)" - - bas11 - search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" - passes_to: romlist coco1 - fails_when_missing: false - source_ref: "xroar/rd_assets/rd_config/xroar.conf:307, xroar/component_prepare.sh:13-15" -- emulator: xroar - build: standalone - kind: names_searched - slot: coco1e - names: - - bas11 - - bas13 - - bas12 - - "Color Basic v1.2 (1982)(Tandy)" - - bas10 - search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" - passes_to: romlist coco1e - fails_when_missing: false - source_ref: "xroar/rd_assets/rd_config/xroar.conf:308, xroar/component_prepare.sh:13-15" -- emulator: xroar - build: standalone - kind: names_searched - slot: coco1e_ext - names: - - extbas10 - - extbas11 - - coco - - COCO - search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" - passes_to: romlist coco1e_ext - fails_when_missing: false - source_ref: "xroar/rd_assets/rd_config/xroar.conf:309, xroar/component_prepare.sh:13-15" -- emulator: xroar - build: standalone - kind: names_searched - slot: coco2 - names: - - bas12 - - bas13 - - "Color Basic v1.2 (1982)(Tandy)" - - bas11 - - bas10 - search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" - passes_to: romlist coco2 - fails_when_missing: false - source_ref: "xroar/rd_assets/rd_config/xroar.conf:310, xroar/component_prepare.sh:13-15" -- emulator: xroar - build: standalone - kind: names_searched - slot: coco2_ext - names: - - extbas11 - - extbas10 - - coco - - COCO - search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" - passes_to: romlist coco2_ext - fails_when_missing: false - source_ref: "xroar/rd_assets/rd_config/xroar.conf:311, xroar/component_prepare.sh:13-15" -- emulator: xroar - build: standalone - kind: names_searched - slot: coco2b - names: - - bas13 - - bas12 - - "Color Basic v1.2 (1982)(Tandy)" - - bas11 - - bas10 - search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" - passes_to: romlist coco2b - fails_when_missing: false - source_ref: "xroar/rd_assets/rd_config/xroar.conf:312, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone kind: names_searched @@ -17342,6 +17365,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist deluxecoco fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:313, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17352,6 +17380,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist deluxecoco1 fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:314, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17362,6 +17395,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist deluxecoco2 fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:315, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17373,6 +17411,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist deluxecoco3 fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:316, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17384,6 +17427,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist mx1600 fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:317, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17394,6 +17442,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist mx1600ext fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:318, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17404,6 +17457,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist coco3 fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:319, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17414,6 +17472,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist coco3p fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:320, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17425,6 +17488,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist glenside_ide fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:321, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17435,6 +17503,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist mc10 fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:322, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17445,6 +17518,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist alice fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:323, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17460,6 +17538,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist dragondos fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:324, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17473,6 +17556,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist dosplus fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:325, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17486,6 +17574,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist superdos fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:326, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17498,6 +17591,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist cumana fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:327, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17524,6 +17622,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist dragondos_compat fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:328, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17535,6 +17638,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist rsdos fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:329, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17550,6 +17658,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist delta fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:330, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17560,6 +17673,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist rsdos_becker fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:331, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17571,6 +17689,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist mcx128 fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:332, xroar/component_prepare.sh:13-15" - emulator: xroar build: standalone @@ -17582,6 +17705,11 @@ launchers: search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path" passes_to: romlist mcx128_azerty fails_when_missing: false + installs: + path: $xroar_config + source_ref: "xroar/component_prepare.sh:13-15" + kept_if_present: false + overrides: [] source_ref: "xroar/rd_assets/rd_config/xroar.conf:333, xroar/component_prepare.sh:13-15" - emulator: retroarch build: libretro @@ -17590,6 +17718,11 @@ launchers: - scph5500.bin passes_to: core option duckstation_BIOS.PathNTSCJ fails_when_missing: false + installs: + path: $retroarch_config_core_options + source_ref: "retroarch/component_prepare.sh:24" + kept_if_present: false + overrides: [] source_ref: "retroarch/rd_assets/rd_config/retroarch-core-options.cfg:165, retroarch/component_prepare.sh:24" - emulator: retroarch build: libretro @@ -17598,6 +17731,11 @@ launchers: - scph5501.bin passes_to: core option duckstation_BIOS.PathNTSCU fails_when_missing: false + installs: + path: $retroarch_config_core_options + source_ref: "retroarch/component_prepare.sh:24" + kept_if_present: false + overrides: [] source_ref: "retroarch/rd_assets/rd_config/retroarch-core-options.cfg:166, retroarch/component_prepare.sh:24" - emulator: retroarch build: libretro @@ -17606,6 +17744,11 @@ launchers: - scph5502.bin passes_to: core option duckstation_BIOS.PathPAL fails_when_missing: false + installs: + path: $retroarch_config_core_options + source_ref: "retroarch/component_prepare.sh:24" + kept_if_present: false + overrides: [] source_ref: "retroarch/rd_assets/rd_config/retroarch-core-options.cfg:167, retroarch/component_prepare.sh:24" - emulator: retroarch build: libretro @@ -17614,4 +17757,9 @@ launchers: - scph10000.bin passes_to: core option pcsx2_bios fails_when_missing: false + installs: + path: $retroarch_config_core_options + source_ref: "retroarch/component_prepare.sh:24" + kept_if_present: false + overrides: [] source_ref: "retroarch/rd_assets/rd_config/retroarch-core-options.cfg:515, retroarch/component_prepare.sh:24" diff --git a/platforms/rocknix.yml b/platforms/rocknix.yml index b01c2b45d..9f85b7126 100644 --- a/platforms/rocknix.yml +++ b/platforms/rocknix.yml @@ -570,6 +570,11 @@ launchers: passes_to: retroarch.cfg system_directory = "~/roms/bios", systemfiles_in_content_dir = "false" fails_when_missing: false + installs: + path: /storage/.config/retroarch/retroarch.cfg + source_ref: "projects/ROCKNIX/packages/sysutils/systemd/scripts/userconfig-setup:13" + kept_if_present: true + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/libretro/retroarch/sources/AMD64/retroarch.cfg:3395-3396,\ \ projects/ROCKNIX/packages/emulators/libretro/retroarch/sources/H700/retroarch.cfg:711-712,\ \ projects/ROCKNIX/packages/emulators/libretro/retroarch/sources/RK3326/retroarch.cfg:716-717,\ @@ -594,7 +599,13 @@ launchers: search_dir: /storage/roms/bios passes_to: "AmigaA1200-default.uae kickstart_rom_file (amiberry -f)" fails_when_missing: false - depends_on_option: game name matches AGA|CD32 + depends_on_option: "game name matches AGA|CD32, content not booted with --autoload,\ + \ a WHDLoad .lha or .zip (start_amiberry.sh:90, 129)" + installs: + path: /tmp/emulation/amiberry/.amiberry_conf.uae + source_ref: "projects/ROCKNIX/packages/emulators/standalone/amiberry/scripts/start_amiberry.sh:64" + kept_if_present: false + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/standalone/amiberry/scripts/start_amiberry.sh:17,\ \ 62-69, projects/ROCKNIX/packages/emulators/standalone/amiberry/config/conf/AmigaA1200-default.uae:114" - emulator: amiberry @@ -606,7 +617,13 @@ launchers: search_dir: /storage/roms/bios passes_to: "AmigaA500-default.uae kickstart_rom_file (amiberry -f)" fails_when_missing: false - depends_on_option: game name does not match AGA|CD32 + depends_on_option: "game name does not match AGA|CD32, content not booted with --autoload,\ + \ a WHDLoad .lha or .zip (start_amiberry.sh:90, 129)" + installs: + path: /tmp/emulation/amiberry/.amiberry_conf.uae + source_ref: "projects/ROCKNIX/packages/emulators/standalone/amiberry/scripts/start_amiberry.sh:67" + kept_if_present: false + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/standalone/amiberry/scripts/start_amiberry.sh:17,\ \ 62-69, projects/ROCKNIX/packages/emulators/standalone/amiberry/config/conf/AmigaA500-default.uae:114" - emulator: amiberry @@ -645,6 +662,11 @@ launchers: search_dir: /storage/roms/bios passes_to: "Atari-ST-default.cfg [ROM] szTosImageFileName (--configfile)" fails_when_missing: false + installs: + path: /tmp/hatari/.hatari-temp.cfg + source_ref: "projects/ROCKNIX/packages/emulators/standalone/hatarisa/scripts/start_hatarisa.sh:35" + kept_if_present: false + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/standalone/hatarisa/config/conf/Atari-ST-default.cfg:200,\ \ projects/ROCKNIX/packages/emulators/standalone/hatarisa/scripts/start_hatarisa.sh:35,\ \ 65" @@ -652,13 +674,20 @@ launchers: build: standalone kind: fixed_name names: [] - passes_to: melonDS.ini BIOS9Path, BIOS7Path, FirmwarePath, DSiBIOS9Path, DSiBIOS7Path, - DSiFirmwarePath, DSiNANDPath left empty + passes_to: melonDS.ini BIOS9Path, BIOS7Path, FirmwarePath, DSiNANDPath left empty fails_when_missing: false depends_on_option: direct_boot = 0 sets ExternalBIOSEnable=1, otherwise 0 device: AMD64 - source_ref: "projects/ROCKNIX/packages/emulators/standalone/melonds-sa/config/AMD64/melonDS.ini:84-90,\ - \ projects/ROCKNIX/packages/emulators/standalone/melonds-sa/package.mk:60, projects/ROCKNIX/packages/emulators/standalone/melonds-sa/scripts/start_melonds.sh:41,\ + installs: + path: /storage/.config/melonDS/melonDS.ini + source_ref: "projects/ROCKNIX/packages/sysutils/systemd/scripts/userconfig-setup:13,\ + \ projects/ROCKNIX/packages/emulators/standalone/melonds-sa/scripts/start_melonds.sh:20,\ + \ 34" + kept_if_present: true + overrides: [] + source_ref: "projects/ROCKNIX/packages/emulators/standalone/melonds-sa/config/AMD64/melonDS.ini:84-86,\ + \ 90, projects/ROCKNIX/packages/emulators/standalone/melonds-sa/package.mk:60,\ + \ projects/ROCKNIX/packages/emulators/standalone/melonds-sa/scripts/start_melonds.sh:41,\ \ 71-73" - emulator: melonds build: standalone @@ -667,24 +696,28 @@ launchers: - bios9.bin - bios7.bin - firmware.bin - - dsi_bios9.bin - - dsi_bios7.bin - - dsi_firmware.bin - dsi_nand.bin search_dir: /storage/roms/bios - passes_to: melonDS.ini BIOS9Path, BIOS7Path, FirmwarePath, DSiBIOS9Path, DSiBIOS7Path, - DSiFirmwarePath, DSiNANDPath + passes_to: melonDS.ini BIOS9Path, BIOS7Path, FirmwarePath, DSiNANDPath fails_when_missing: false depends_on_option: direct_boot = 0 sets ExternalBIOSEnable=1, otherwise 0 device: RK3399, RK3566, RK3576, RK3588, S922X, SDM845, SM4450, SM6115, SM8250, SM8550, SM8650, SM8750 - source_ref: "projects/ROCKNIX/packages/emulators/standalone/melonds-sa/config/InputPlumber/melonDS.ini:94-100,\ - \ projects/ROCKNIX/packages/emulators/standalone/melonds-sa/config/RK3399/melonDS.ini:93-99,\ - \ projects/ROCKNIX/packages/emulators/standalone/melonds-sa/config/RK3566/melonDS.ini:93-99,\ - \ projects/ROCKNIX/packages/emulators/standalone/melonds-sa/config/RK3588/melonDS.ini:93-99,\ - \ projects/ROCKNIX/packages/emulators/standalone/melonds-sa/config/S922X/melonDS.ini:95-101,\ - \ projects/ROCKNIX/packages/emulators/standalone/melonds-sa/config/SM4450/melonDS.ini:90-96,\ - \ projects/ROCKNIX/packages/emulators/standalone/melonds-sa/package.mk:60, projects/ROCKNIX/packages/emulators/standalone/melonds-sa/scripts/start_melonds.sh:41,\ + installs: + path: /storage/.config/melonDS/melonDS.ini + source_ref: "projects/ROCKNIX/packages/sysutils/systemd/scripts/userconfig-setup:13,\ + \ projects/ROCKNIX/packages/emulators/standalone/melonds-sa/scripts/start_melonds.sh:20,\ + \ 34" + kept_if_present: true + overrides: [] + source_ref: "projects/ROCKNIX/packages/emulators/standalone/melonds-sa/config/InputPlumber/melonDS.ini:94-96,\ + \ 100, projects/ROCKNIX/packages/emulators/standalone/melonds-sa/config/RK3399/melonDS.ini:93-95,\ + \ 99, projects/ROCKNIX/packages/emulators/standalone/melonds-sa/config/RK3566/melonDS.ini:93-95,\ + \ 99, projects/ROCKNIX/packages/emulators/standalone/melonds-sa/config/RK3588/melonDS.ini:93-95,\ + \ 99, projects/ROCKNIX/packages/emulators/standalone/melonds-sa/config/S922X/melonDS.ini:95-97,\ + \ 101, projects/ROCKNIX/packages/emulators/standalone/melonds-sa/config/SM4450/melonDS.ini:90-92,\ + \ 96, projects/ROCKNIX/packages/emulators/standalone/melonds-sa/package.mk:60,\ + \ projects/ROCKNIX/packages/emulators/standalone/melonds-sa/scripts/start_melonds.sh:41,\ \ 71-73" - emulator: dsperate build: standalone @@ -701,6 +734,12 @@ launchers: passes_to: "dsperate.ini [paths] bios9, bios7, firmware, bios9i, bios7i, dsi_firmware,\ \ dsi_nand (--config)" fails_when_missing: false + installs: + path: /storage/.config/dsperate/dsperate.ini + source_ref: "projects/ROCKNIX/packages/sysutils/systemd/scripts/userconfig-setup:13,\ + \ projects/ROCKNIX/packages/emulators/standalone/dsperate-sa/scripts/start_dsperate.sh:19" + kept_if_present: true + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/standalone/dsperate-sa/config/H700/dsperate.ini:11-17,\ \ projects/ROCKNIX/packages/emulators/standalone/dsperate-sa/config/InputPlumber/dsperate.ini:11-17,\ \ projects/ROCKNIX/packages/emulators/standalone/dsperate-sa/config/RK3326/dsperate.ini:11-17,\ @@ -716,7 +755,13 @@ launchers: - gba_bios.bin search_dir: /storage/roms/bios passes_to: "config.toml [general] bios_path" - fails_when_missing: false + fails_when_missing: true + installs: + path: /storage/.config/nanoboyadvance/config.toml + source_ref: "projects/ROCKNIX/packages/sysutils/systemd/scripts/userconfig-setup:13,\ + \ projects/ROCKNIX/packages/emulators/standalone/nanoboyadvance-sa/scripts/start_nanoboyadvance.sh:12" + kept_if_present: true + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/standalone/nanoboyadvance-sa/config/config.toml:2,\ \ projects/ROCKNIX/packages/emulators/standalone/nanoboyadvance-sa/package.mk:30" - emulator: xemu @@ -731,6 +776,13 @@ launchers: passes_to: "xemu.toml [sys.files] bootrom_path, flashrom_path, eeprom_path, hdd_path\ \ (copied at each launch)" fails_when_missing: false + installs: + path: /storage/.config/xemu/xemu.toml + source_ref: "projects/ROCKNIX/packages/sysutils/systemd/scripts/userconfig-setup:13,\ + \ projects/ROCKNIX/packages/emulators/standalone/xemu-sa/scripts/start_xemu.sh:15,\ + \ 19" + kept_if_present: false + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/standalone/xemu-sa/config/AMD64/xemu.toml:28-31,\ \ projects/ROCKNIX/packages/emulators/standalone/xemu-sa/config/SM4450/xemu.toml:29-32,\ \ projects/ROCKNIX/packages/emulators/standalone/xemu-sa/config/SM8250/xemu.toml:29-32,\ @@ -747,6 +799,12 @@ launchers: search_dir: /storage/roms/bios/ passes_to: mednafen.cfg gba.bios fails_when_missing: false + installs: + path: /storage/.config/mednafen/mednafen.cfg + source_ref: "projects/ROCKNIX/packages/sysutils/systemd/scripts/userconfig-setup:13,\ + \ projects/ROCKNIX/packages/emulators/standalone/mednafen/scripts/start_mednafen.sh:21" + kept_if_present: true + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/standalone/mednafen/config/common/mednafen.template:570,\ \ 729, projects/ROCKNIX/packages/emulators/standalone/mednafen/scripts/start_mednafen.sh:19-21" - emulator: mednafen @@ -758,6 +816,12 @@ launchers: search_dir: /storage/roms/bios/ passes_to: mednafen.cfg md.cdbios fails_when_missing: false + installs: + path: /storage/.config/mednafen/mednafen.cfg + source_ref: "projects/ROCKNIX/packages/sysutils/systemd/scripts/userconfig-setup:13,\ + \ projects/ROCKNIX/packages/emulators/standalone/mednafen/scripts/start_mednafen.sh:21" + kept_if_present: true + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/standalone/mednafen/config/common/mednafen.template:570,\ \ 1050, projects/ROCKNIX/packages/emulators/standalone/mednafen/scripts/start_mednafen.sh:19-21" - emulator: mednafen @@ -769,6 +833,12 @@ launchers: search_dir: /storage/roms/bios/ passes_to: mednafen.cfg nes.ggrom fails_when_missing: false + installs: + path: /storage/.config/mednafen/mednafen.cfg + source_ref: "projects/ROCKNIX/packages/sysutils/systemd/scripts/userconfig-setup:13,\ + \ projects/ROCKNIX/packages/emulators/standalone/mednafen/scripts/start_mednafen.sh:21" + kept_if_present: true + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/standalone/mednafen/config/common/mednafen.template:570,\ \ 2292, projects/ROCKNIX/packages/emulators/standalone/mednafen/scripts/start_mednafen.sh:19-21" - emulator: mednafen @@ -781,6 +851,12 @@ launchers: search_dir: /storage/roms/bios/ passes_to: mednafen.cfg pce.cdbios, pce.gecdbios fails_when_missing: false + installs: + path: /storage/.config/mednafen/mednafen.cfg + source_ref: "projects/ROCKNIX/packages/sysutils/systemd/scripts/userconfig-setup:13,\ + \ projects/ROCKNIX/packages/emulators/standalone/mednafen/scripts/start_mednafen.sh:21" + kept_if_present: true + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/standalone/mednafen/config/common/mednafen.template:570,\ \ 3309, 3342, projects/ROCKNIX/packages/emulators/standalone/mednafen/scripts/start_mednafen.sh:19-21" - emulator: mednafen @@ -792,6 +868,12 @@ launchers: search_dir: /storage/roms/bios/ passes_to: mednafen.cfg pce_fast.cdbios fails_when_missing: false + installs: + path: /storage/.config/mednafen/mednafen.cfg + source_ref: "projects/ROCKNIX/packages/sysutils/systemd/scripts/userconfig-setup:13,\ + \ projects/ROCKNIX/packages/emulators/standalone/mednafen/scripts/start_mednafen.sh:21" + kept_if_present: true + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/standalone/mednafen/config/common/mednafen.template:570,\ \ 4128, projects/ROCKNIX/packages/emulators/standalone/mednafen/scripts/start_mednafen.sh:19-21" - emulator: mednafen @@ -803,6 +885,12 @@ launchers: search_dir: /storage/roms/bios/ passes_to: mednafen.cfg pcfx.bios fails_when_missing: false + installs: + path: /storage/.config/mednafen/mednafen.cfg + source_ref: "projects/ROCKNIX/packages/sysutils/systemd/scripts/userconfig-setup:13,\ + \ projects/ROCKNIX/packages/emulators/standalone/mednafen/scripts/start_mednafen.sh:21" + kept_if_present: true + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/standalone/mednafen/config/common/mednafen.template:570,\ \ 4611, projects/ROCKNIX/packages/emulators/standalone/mednafen/scripts/start_mednafen.sh:19-21" - emulator: mednafen @@ -816,6 +904,12 @@ launchers: search_dir: /storage/roms/bios/ passes_to: mednafen.cfg psx.bios_eu, psx.bios_jp, psx.bios_na fails_when_missing: false + installs: + path: /storage/.config/mednafen/mednafen.cfg + source_ref: "projects/ROCKNIX/packages/sysutils/systemd/scripts/userconfig-setup:13,\ + \ projects/ROCKNIX/packages/emulators/standalone/mednafen/scripts/start_mednafen.sh:21" + kept_if_present: true + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/standalone/mednafen/config/common/mednafen.template:570,\ \ 7366-7370, projects/ROCKNIX/packages/emulators/standalone/mednafen/scripts/start_mednafen.sh:19-21" - emulator: mednafen @@ -834,6 +928,12 @@ launchers: passes_to: mednafen.cfg ss.bios_jp, ss.bios_na_eu, ss.bios_stv_eu, ss.bios_stv_jp, ss.bios_stv_na, ss.cart.kof95_path, ss.cart.ultraman_path fails_when_missing: false + installs: + path: /storage/.config/mednafen/mednafen.cfg + source_ref: "projects/ROCKNIX/packages/sysutils/systemd/scripts/userconfig-setup:13,\ + \ projects/ROCKNIX/packages/emulators/standalone/mednafen/scripts/start_mednafen.sh:21" + kept_if_present: true + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/standalone/mednafen/config/common/mednafen.template:570,\ \ 9818-9820, 9824-9828, 9834-9836, projects/ROCKNIX/packages/emulators/standalone/mednafen/scripts/start_mednafen.sh:19-21" - emulator: yabasanshiro @@ -861,6 +961,13 @@ launchers: passes_to: "settings.ini [BIOS] SearchDirectory = /storage/roms/bios, [BIOS] PathNTSCU,\ \ PathNTSCJ, PathPAL unset" fails_when_missing: false + installs: + path: /storage/.config/duckstation/settings.ini + source_ref: "projects/ROCKNIX/packages/sysutils/systemd/scripts/userconfig-setup:13,\ + \ projects/ROCKNIX/packages/emulators/standalone/duckstation-sa/scripts/start_duckstation.sh:19,\ + \ 22" + kept_if_present: true + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/standalone/duckstation-sa/config/AMD64/settings.ini:134,\ \ projects/ROCKNIX/packages/emulators/standalone/duckstation-sa/config/H700/settings.ini:24,\ \ projects/ROCKNIX/packages/emulators/standalone/duckstation-sa/config/InputPlumber/settings.ini:29-32,\ @@ -878,6 +985,13 @@ launchers: passes_to: "PCSX2.ini [Folders] Bios = /storage/roms/bios/armsx2, [Filenames] BIOS\ \ unset" fails_when_missing: false + installs: + path: /storage/.config/ARMSX2/inis/PCSX2.ini + source_ref: "projects/ROCKNIX/packages/sysutils/systemd/scripts/userconfig-setup:13,\ + \ projects/ROCKNIX/packages/emulators/standalone/armsx2-sa/scripts/start_armsx2.sh:11,\ + \ 16" + kept_if_present: true + overrides: [] source_ref: "projects/ROCKNIX/packages/emulators/standalone/armsx2-sa/config/S922X/ARMSX2/inis/PCSX2.ini:21,\ \ 475, projects/ROCKNIX/packages/emulators/standalone/armsx2-sa/config/inputplumber/ARMSX2/inis/PCSX2.ini:21,\ \ 473, projects/ROCKNIX/packages/emulators/standalone/armsx2-sa/package.mk:117-118" diff --git a/scripts/scraper/amberelec_launchers.py b/scripts/scraper/amberelec_launchers.py index 259d46cd4..f4f7819a0 100644 --- a/scripts/scraper/amberelec_launchers.py +++ b/scripts/scraper/amberelec_launchers.py @@ -7,7 +7,7 @@ runs the script its standalone_emulators table names (/usr/bin/.sh, installed from the package). Those scripts, and the configs their packages install, say which file under /storage/roms/bios is opened: yabasanshiro.sh tests saturn_bios.bin and passes it with -b, Hatari's default config names -tos.img, the atari800 core is built to read a config naming its OS ROMs. +tos.img. runemu.py is read with ast, the scripts with shell_launchers' readers. An extractor whose construct is gone raises instead of returning nothing. @@ -26,7 +26,9 @@ try: from .shell_launchers import ( LauncherPatternMissing, Script, + config_placement, describe_keys, + evidence_fields, settings, source_ref, ) @@ -35,7 +37,9 @@ except ImportError: from shell_launchers import ( # type: ignore[no-redef] LauncherPatternMissing, Script, + config_placement, describe_keys, + evidence_fields, settings, source_ref, ) @@ -45,8 +49,8 @@ RUNEMU = "packages/amberelec/sources/scripts/runemu.py" RETROARCH_CFG = "packages/games/tools/retroarch/sources/retroarch.cfg" RETRORUN = "packages/games/tools/retrorun/retrorun.sh" HATARI_CONFIG = "packages/games/emulators/hatarisa/config/conf/Atari-ST-default.cfg" -ATARI800_CONFIG = "packages/amberelec/config/distribution/configs/atari800.cfg" -ATARI800_PATCH = "packages/games/libretro/atari800/patches/atari800-01-add-emuelec-platform.patch" +# The first-boot copy of every package's /usr/config into /storage/.config. +USERCONFIG = "packages/sysutils/systemd/scripts/userconfig-setup" AMIBERRY_PACKAGE = "packages/games/emulators/amiberry/package.mk" AMIBERRY_CONFIGS = "packages/games/emulators/amiberry/config/conf" FREEJ2ME = ( @@ -103,16 +107,35 @@ class Runemu: return path, line +def _evidence( + read: Read, record: Launcher, target: str, keys: list[str], + start: Script | None = None, source_name: str = "", +) -> Launcher: + """A config record with the steps that put its file at ``target``: the + first-boot copy of /usr/config, then the start script's own copy.""" + setup = Script(USERCONFIG, read(USERCONFIG)) + placements = [config_placement(setup, target, required=False, sources=("/usr/config/",))] + if start is not None: + placements.append( + config_placement(start, target, keys, source_name=source_name, required=False) + ) + fields = evidence_fields(placements) + record.installs = fields["installs"] + record.kept_if_present = fields["kept_if_present"] + record.overrides = fields["overrides"] + return record + + def retroarch(read: Read) -> list[Launcher]: """system_directory of the retroarch.cfg the retroarch package installs.""" found = [s for s in settings(read(RETROARCH_CFG)) if s.key == "system_directory"] if len(found) != 1: raise LauncherPatternMissing(RETROARCH_CFG, "one system_directory") - return [Launcher( + return [_evidence(read, Launcher( emulator="retroarch", build="libretro", kind="passes_path", names=[], search_dir=found[0].value, passes_to="retroarch.cfg system_directory", fails_when_missing=False, source_ref=source_ref([(RETROARCH_CFG, [found[0].line])]), - )] + ), "/storage/.config/retroarch/retroarch.cfg", ["system_directory"])] def retrorun(read: Read) -> list[Launcher]: @@ -170,10 +193,12 @@ def hatari(read: Read, runemu: Runemu) -> list[Launcher]: path, table_line = runemu.script("HATARISA") script = Script(path, read(path)) name = posixpath.basename(HATARI_CONFIG) - copied, _ = script.find(rf"^\s*cp\s+\S*/{re.escape(name)}\s", f"cp of {name}") + copied, copy = script.find( + rf"^\s*cp\s+\S*/{re.escape(name)}\s+\"?(\$\{{?\w+\}}?)", f"cp of {name}" + ) passed, _ = script.find(r"\shatari\s+--configfile\s", "hatari --configfile") files, lines = _bios_files(HATARI_CONFIG, read(HATARI_CONFIG)) - return [Launcher( + return [_evidence(read, Launcher( emulator="HATARISA", build="standalone", kind="fixed_name", names=[n for _k, n in files], case_insensitive=False, search_dir=BIOS, passes_to=describe_keys(name, tuple(k for k, _n in files)), @@ -181,24 +206,8 @@ def hatari(read: Read, runemu: Runemu) -> list[Launcher]: source_ref=source_ref([ (RUNEMU, [table_line]), (path, [copied, passed]), (HATARI_CONFIG, lines), ]), - )] - - -def atari800(read: Read) -> list[Launcher]: - """The ROM keys of the config the atari800 core is built to read.""" - patch = Script(ATARI800_PATCH, read(ATARI800_PATCH)) - line, found = patch.find(r'DEFAULT_CFG_NAME="\\"([^"\\]+)\\""', "DEFAULT_CFG_NAME") - built = found.group(1) - if not built.endswith("/" + ATARI800_CONFIG.split("/config/", 1)[1]): - raise LauncherPatternMissing(ATARI800_PATCH, f"DEFAULT_CFG_NAME naming {ATARI800_CONFIG}") - files, lines = _bios_files(ATARI800_CONFIG, read(ATARI800_CONFIG)) - return [Launcher( - emulator="atari800", build="libretro", kind="fixed_name", - names=[n for _k, n in files], case_insensitive=False, search_dir=BIOS, - passes_to=describe_keys(posixpath.basename(ATARI800_CONFIG), tuple(k for k, _n in files)), - fails_when_missing=False, - source_ref=source_ref([(ATARI800_PATCH, [line]), (ATARI800_CONFIG, lines)]), - )] + ), script.expand(copy.group(1), copied, set()), + [f"[{s}] {k}" if s else k for (s, k), _n in files], script, name)] def yabasanshiro(read: Read, runemu: Runemu) -> list[Launcher]: @@ -279,6 +288,15 @@ def amiberry(read: Read, runemu: Runemu) -> list[Launcher]: installed = re.sub(r"^\$\{INSTALL\}", "", link.link) path, table_line = runemu.script("AMIBERRY") script = Script(path, read(path)) + # A WHDLoad archive is booted by --autoload, which builds its own setup + # over the config: the kickstart records hold for the other content. + autoload = [n for n, text in script.code() if re.search(r"amiberry .*--autoload", text)] + if not autoload: + raise script.missing("WHDLoad boot with --autoload") + content = ( + "content not booted with --autoload, a WHDLoad .lha or .zip " + f"({posixpath.basename(path)}:{', '.join(map(str, autoload))})" + ) launchers = [Launcher( emulator="AMIBERRY", build="standalone", kind="links", names=[], case_insensitive=False, search_dir=BIOS, @@ -314,19 +332,23 @@ def amiberry(read: Read, runemu: Runemu) -> list[Launcher]: else: test = _shell_test(script, condition_line) condition = test - launchers.append(Launcher( + target = re.search(r'"?(\$\{?\w+\}?)"?\s*$', line) + if target is None: + raise script.missing(f"destination of the copy at line {number}") + launchers.append(_evidence(read, Launcher( emulator="AMIBERRY", build="standalone", kind="fixed_name", slot=copied.group(1).removesuffix("-default.uae").removeprefix("Amiga"), names=[found[0].value.removeprefix("$(FILE_PATH)/")], case_insensitive=False, search_dir=BIOS, passes_to=f"{copied.group(1)} kickstart_rom_file, $(FILE_PATH) = {rom_path[0].value}", fails_when_missing=False, - depends_on_option=condition, + depends_on_option=f"{condition}, {content}", source_ref=source_ref([ (RUNEMU, [table_line]), (path, [condition_line, number]), (config, [rom_path[0].line, found[0].line]), ]), - )) + ), script.expand(target.group(1), number, set()), ["kickstart_rom_file"], + script, copied.group(1))) if len(launchers) == 1: raise script.missing("cp of a default .uae config") return launchers @@ -420,9 +442,13 @@ def easyrpg(read: Read) -> list[Launcher]: def extract_launchers(read: Read) -> list[dict[str, Any]]: """Every launcher record, read through `read(repo-relative path)`.""" runemu = Runemu(read) + # The atari800.cfg AmberELEC ships names the OS ROMs, but the core opens + # it only under its atari800_cfg option, off by default, and finds the + # ROMs by a scan of the content folder (libretro-atari800 atari.c:512-516, + # 537, libretro_core_options.h:1203): no record reads it. launchers = [ *retroarch(read), *retrorun(read), *mame(runemu), - *atari800(read), *scummvm(read), *freej2me(read), *easyrpg(read), + *scummvm(read), *freej2me(read), *easyrpg(read), *amiberry(read, runemu), *hatari(read, runemu), *pico8(read, runemu), *piemu(read, runemu), *yabasanshiro(read, runemu), ] diff --git a/scripts/scraper/configgen_launchers.py b/scripts/scraper/configgen_launchers.py index 03957145e..4f6d05442 100644 --- a/scripts/scraper/configgen_launchers.py +++ b/scripts/scraper/configgen_launchers.py @@ -39,6 +39,14 @@ class Launcher: machines: list[str] | None = None # The platform systems the record is written for, none for every one. systems: list[str] | None = None + # For a record citing a config: where the platform's code puts it + # ({path, source_ref}), whether that leaves a file already there, and + # the launch code rewriting its keys ([] when none does). + installs: dict[str, str] | None = None + kept_if_present: bool | None = None + overrides: list[str] | None = None + # The feature or option file listing the values the option written takes. + choices_from: str | None = None def entry(self) -> dict[str, Any]: """The platform YAML form, in a fixed key order, unset keys left out.""" @@ -46,7 +54,8 @@ class Launcher: "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", + "device", "machines", "installs", "kept_if_present", "overrides", + "choices_from", "source_ref", ) return {key: getattr(self, key) for key in order if getattr(self, key) is not None} diff --git a/scripts/scraper/emudeck_launchers.py b/scripts/scraper/emudeck_launchers.py index 2723d62d8..b8f42696f 100644 --- a/scripts/scraper/emudeck_launchers.py +++ b/scripts/scraper/emudeck_launchers.py @@ -44,6 +44,8 @@ BIOS = "${biosPath}" EMULATION_BIOS = "${emulationPath}/bios" STORAGE = "${storagePath}" FLATPAK_DATA = "${HOME}/.var/app/" +# The function every EmuScript installs its Flatpak config with. +CONFIG_EMU_FP = "functions/configEmuFP.sh" # EmuScripts file names that differ from the emulator profile they set up. # The platform and target scrapers both read them here. @@ -95,6 +97,31 @@ def _is_bios(value: str) -> bool: return any(value == root or value.startswith(root + "/") for root in (BIOS, EMULATION_BIOS)) +def with_evidence(tree: RepoTree, script: Script, record: Launcher, config: str) -> Launcher: + """A record citing a shipped config, with the steps that install it. + + configEmuFP rsyncs the repository's configs/ over the Flatpak's: + the _init call overwrites (keeping a .bak), every other call passes + --ignore-existing and leaves the user's file in place. + """ + fp = Script(CONFIG_EMU_FP, tree.read(CONFIG_EMU_FP)) + rsync_line, _ = fp.find(r"^\s*rsync .*\$emudeckBackend/configs/", "rsync of the configs") + keep_line, _ = fp.find(r'overwrite="--ignore-existing"', "--ignore-existing") + calls = {n for n, line in script.code() if re.match(r"\s*configEmuFP\s", line)} + if not calls: + raise script.missing("configEmuFP call") + variable = script.assignment_ending("/" + config) + if variable is None: + raise script.missing(f"config variable for {config}") + record.installs = { + "path": script.expand(variable.value, variable.line, set()), + "source_ref": source_ref([(script.path, calls), (fp.path, {keep_line, rsync_line})]), + } + record.kept_if_present = True + record.overrides = [] + return record + + def _repo_config(script: Script, path: str) -> str: """The repository copy of a Flatpak config EmuDeck installs.""" if not path.startswith(FLATPAK_DATA): @@ -268,7 +295,8 @@ def ares(tree: RepoTree) -> list[Launcher]: raise LauncherPatternMissing(path, f"Firmware path under {old}") for system, found in entries.items(): folder = posixpath.commonpath([posixpath.dirname(t) for _, t, _ in found]) - records.append( + records.append(with_evidence( + tree, script, Launcher( emulator="ares", build="standalone", @@ -281,16 +309,20 @@ def ares(tree: RepoTree) -> list[Launcher]: search_dir=folder, passes_to="settings.bml " + ", ".join(f"{system}/Firmware/{k}" for k, _, _ in found), - ) - ) + ), + posixpath.basename(path), + )) return records def melonds(tree: RepoTree) -> list[Launcher]: """melonDS.ini paths set by changeLine, melonDS.toml paths shipped and rewritten by sed.""" name = "emuDeckMelonDS.sh" - records = setter_records(tree, name, option_key="ExternalBIOSEnable") script = _script(tree, name) + records = [ + with_evidence(tree, script, record, str(record.passes_to or "").split(" ", 1)[0]) + for record in setter_records(tree, name, option_key="ExternalBIOSEnable") + ] # A config whose BIOS keys the setters write again keeps their values. set_by_setters = {file for file, _, _, value, _ in setters(script) if _is_bios(value)} for file, old, new, sed_lines in sed_rewrites(script, "melonDS_init"): @@ -306,22 +338,29 @@ def melonds(tree: RepoTree) -> list[Launcher]: continue folder = posixpath.commonpath([posixpath.dirname(t) for _, t in found]) config = posixpath.basename(path) - option = next((s for s in settings(tree.read(path)) if s.key == "ExternalBIOSEnable"), None) - if option is None: - raise LauncherPatternMissing(path, "ExternalBIOSEnable") + shipped = {s.key: s for s in settings(tree.read(path))} + if "ExternalBIOSEnable" not in shipped or "ConsoleType" not in shipped: + raise LauncherPatternMissing(path, "ExternalBIOSEnable and ConsoleType") + option, console = shipped["ExternalBIOSEnable"], shipped["ConsoleType"] + # The DS keys are read under ExternalBIOSEnable, the DSi keys only + # when the console is a DSi (ConsoleType 1). record = Launcher( emulator="melonds", build="standalone", kind="fixed_name", names=[posixpath.relpath(t, folder) for _, t in found], - source_ref=source_ref( - [(path, {option.line, *(s.line for s, _ in found)}), (script.path, sed_lines)] - ), + source_ref=source_ref([ + (path, {option.line, console.line, *(s.line for s, _ in found)}), + (script.path, sed_lines), + ]), search_dir=folder, passes_to=describe_keys(config, tuple((s.section, s.key) for s, _ in found)), - depends_on_option=f"ExternalBIOSEnable = {option.value} in the shipped {config}", + depends_on_option=( + f"[DS] keys: ExternalBIOSEnable = true, {option.value} in the shipped {config}; " + f"[DSi] keys: ConsoleType = 1, {console.value} in the shipped {config}" + ), ) - records.append(record) + records.append(with_evidence(tree, script, record, config)) if len(records) < 2: raise script.missing("melonDS.toml BIOS paths rewritten by sed") return records diff --git a/scripts/scraper/emuelec_launchers.py b/scripts/scraper/emuelec_launchers.py index f4aed1e60..e52a23fc9 100644 --- a/scripts/scraper/emuelec_launchers.py +++ b/scripts/scraper/emuelec_launchers.py @@ -14,7 +14,7 @@ from __future__ import annotations import posixpath import re -from collections.abc import Callable +from collections.abc import Callable, Iterable from dataclasses import dataclass, field from .configgen_launchers import Launcher @@ -22,6 +22,8 @@ from .rocknix_launchers import fixed_paths from .shell_launchers import ( LauncherPatternMissing, Link, + config_placement, + evidence_fields, RepoTree, Script, describe_keys, @@ -104,6 +106,28 @@ class Runs: return (RUN_EMU, set(self.of(name).lines)) +def _evidence( + record: Launcher, + script: Script, + target: str, + keys: list[str], + source_name: str = "", + rewrites: Iterable[int] = (), +) -> Launcher: + """A config record with the start script lines that put its file in place. + + ``rewrites`` are lines the extractor read as rewriting the record's + values without naming their keys (advmame.sh's sed of /roms/mame). + """ + placement = config_placement(script, target, keys, source_name=source_name) + placement.overrides |= set(rewrites) + fields = evidence_fields([placement]) + record.installs = fields["installs"] + record.kept_if_present = fields["kept_if_present"] + record.overrides = fields["overrides"] + return record + + def _read(tree: RepoTree, path: str) -> Script: return Script(path, tree.read(path)) @@ -317,7 +341,7 @@ def duckstation(tree: RepoTree, runs: Runs) -> list[Launcher]: name = posixpath.basename(script.path) lines = {found[k].line for k in (folder_key, *file_keys) if k in found} unset = describe_keys("", file_keys).strip() - return [ + return [_evidence( Launcher( emulator=runs.emulator(name), build="standalone", @@ -328,8 +352,11 @@ def duckstation(tree: RepoTree, runs: Runs) -> list[Launcher]: ), search_dir=found[folder_key].value, passes_to=f"settings.ini [BIOS] SearchDirectory (-settings), {unset} unset", - ) - ] + ), + script, + script.expand("${CONFIG_DIR}/settings.ini", run_line, set()), + [f"[{s}] {k}" for s, k in (folder_key, *file_keys)], + )] YABASANSHIRO_SCRIPTS = ( @@ -473,7 +500,7 @@ def hatari(tree: RepoTree, runs: Runs) -> list[Launcher]: mk = _read(tree, f"{package}/package.mk") mk_line, _ = mk.find(r"cp -R \$\{PKG_DIR\}/config/\* ", "config install") name = posixpath.basename(script.path) - return [ + return [_evidence( Launcher( emulator=runs.emulator(name), build="standalone", @@ -485,8 +512,12 @@ def hatari(tree: RepoTree, runs: Runs) -> list[Launcher]: ), search_dir=value.search_dir, passes_to=f"{describe_keys(copy.group(1), value.keys)} (--configfile)", - ) - ] + ), + script, + script.expand("${" + copy.group(2) + "}", copy_line, set()), + [f"[{s}] {k}" if s else k for s, k in value.keys], + source_name=copy.group(1), + )] _ADVMAME_SYSTEM = re.compile(r'^\s*if \[\[ "\$\{1\}" = "([\w-]+)" \]\]; then$') @@ -535,29 +566,34 @@ def advancemame(tree: RepoTree, runs: Runs) -> list[Launcher]: (script.path, {run_line, *lines}), ]) - found = [] + target = script.expand("${CONFIG_DIR}/advmame.rc", run_line, set()) + + def proved(record: Launcher, key: str) -> Launcher: + return _evidence(record, script, target, [key], rewrites={sed_line}) + + records = [] for key, folder in value: rewritten = folder.replace(sed.group(1), sed.group(2)) if rewritten != folder: # Every system but the one the script rewrites the key for, and # that system with the rewritten folder. - found.append(Launcher( + records.append(proved(Launcher( emulator=runs.emulator(name), build="standalone", kind="passes_path", names=[], source_ref=refs(test_line, sed_line), search_dir=folder, passes_to=f"advmame.rc {key}", depends_on_option=f"system != {test.group(1)}", - )) - found.append(Launcher( + ), key)) + records.append(proved(Launcher( emulator=runs.emulator(name), build="standalone", kind="passes_path", names=[], source_ref=refs(test_line, sed_line), search_dir=rewritten, passes_to=f"advmame.rc {key}", systems=[test.group(1)], - )) + ), key)) continue - found.append(Launcher( + records.append(proved(Launcher( emulator=runs.emulator(name), build="standalone", kind="passes_path", names=[], source_ref=refs(), search_dir=folder, passes_to=f"advmame.rc {key}", - )) - return found + ), key)) + return records def mame(tree: RepoTree, runs: Runs) -> list[Launcher]: @@ -594,42 +630,11 @@ def mame(tree: RepoTree, runs: Runs) -> list[Launcher]: ) if not records: raise script.missing(f"copy into {BIOS_ROOT}") - ini = next((r for r in records if r.names == ["mame*"]), None) - if ini is None: - raise script.missing("copy of mame.ini") - option_path = f"{package}/config/MAME/MAME.opt" - option = next( - (s for s in settings(tree.read(option_path)) if s.key == "mame_read_config"), None - ) - if option is None: - raise LauncherPatternMissing(option_path, "mame_read_config") - ini_path = f"{package}/config/ini/mame.ini" - # MAME reads from the folders its *path options name and writes to its - # *_directory ones. - folders = [ - (s, [p for p in s.value.split(";") if _under(p)]) - for s in settings(tree.read(ini_path), separator=" ") - if s.key.endswith("path") - ] - folders = [(s, parts) for s, parts in folders if parts] - if not folders: - raise LauncherPatternMissing(ini_path, f"path under {BIOS_ROOT}") - for setting, parts in folders: - for part in parts: - records.append( - Launcher( - emulator=emulator, - build="libretro", - kind="passes_path", - names=[], - source_ref=source_ref( - [(ini_path, {setting.line}), (option_path, {option.line})] - ) + f", {ini.source_ref}", - search_dir=part.rstrip("/"), - passes_to=f"mame.ini {setting.key}", - depends_on_option=f"mame_read_config = {option.value}", - ) - ) + # mame.ini hands the core no folder: MAME.opt turns on + # mame_mame_paths_enable, so the core passes no path option + # (libretro mame src/osd/libretro/libretro-internal/retro_init.cpp:554-555), + # and the ini MAME would read sits under a working directory no launcher + # sets (src/emu/emuopts.cpp:49). Only the copies are the launcher's. return records @@ -644,6 +649,16 @@ def scummvm(tree: RepoTree, runs: Runs) -> list[Launcher]: core_line, core = script.find(r"-L /tmp/cores/(\w+)_libretro\.so", "libretro start") arm_line, arm = script.find(r'^\s*"(\w+)"\)', "setup arm", core_line) copy_line, copy = script.find(r"^\s*cp (\S+) (\S+)$", "copy into the BIOS folder", arm_line) + launched = sorted({ + m.group(1) for _, line in runs.script.code() + if (m := re.search(rf"\b{re.escape(name)} (\w+)", line)) + }) + if not launched: + raise runs.script.missing(f"start of {name} with an action") + action = f"{name} {arm.group(1)}" + if arm.group(1) not in launched: + # The copy runs from the setup action, never from a launch. + action += f", an action no launch runs ({RUN_EMU.rsplit('/', 1)[-1]} passes {', '.join(launched)})" used = {copy_line} source = script.expand(copy.group(1), copy_line, used) target = script.expand(copy.group(2), copy_line, used) @@ -669,7 +684,7 @@ def scummvm(tree: RepoTree, runs: Runs) -> list[Launcher]: search_dir=posixpath.dirname(source), passes_to=target, fails_when_missing=False, - depends_on_option=f"{name} {arm.group(1)}", + depends_on_option=action, ), ] diff --git a/scripts/scraper/retrodeck_launchers.py b/scripts/scraper/retrodeck_launchers.py index 51b6cf06b..3d7ed0a52 100644 --- a/scripts/scraper/retrodeck_launchers.py +++ b/scripts/scraper/retrodeck_launchers.py @@ -35,6 +35,8 @@ PLACEHOLDER = "RETRODECKBIOSDIR" # gzdoom/component_prepare.sh:27 rewrites to $roms_path. ROMS_PLACEHOLDER = "RETRODECKROMSDIR" CORE_OPTIONS = "retroarch/rd_assets/rd_config/retroarch-core-options.cfg" +# XRoar ROM lists RetroDECK's xroar.conf defines that no machine looks up. +UNREAD_ROMLISTS = frozenset({"coco1", "coco1e", "coco1e_ext", "coco2", "coco2_ext", "coco2b"}) # The component that runs libretro cores. LIBRETRO_COMPONENT = "retroarch" # Values of a core option that name a file rather than a setting. @@ -301,6 +303,19 @@ class Component: if copied is None: raise LauncherPatternMissing(f"{script.path}:{number}", f"no preset copied to {target}") preset = f"{self.name}/rd_assets/rd_config/{copied[1]}" + # The reset copies the preset over the user's file, then fills its + # placeholders: that is the install, and nothing keeps an older file. + evidence: dict[str, Any] = { + "installs": { + "path": target, + "source_ref": source_ref( + [(script.path, copied[0])] + + [(script.path, line) for _v, line in substitutions.values()] + ), + }, + "kept_if_present": False, + "overrides": self.launch_rewrites(target), + } text = self.read(preset) if text is None: raise LauncherPatternMissing(preset, "preset is missing") @@ -332,10 +347,10 @@ class Component: self.record( "passes_path", [], [(preset, line_no)] + applied, search_dir=value, passes_to=f"{self.config_name(target)} {where}", - fails_when_missing=False, + fails_when_missing=False, **evidence, ) if key == "rompath": - self.romlists(preset, text, value, applied) + self.romlists(preset, text, value, applied, evidence) if not found: raise LauncherPatternMissing(preset, f"no {PLACEHOLDER} line") @@ -349,10 +364,28 @@ class Component: found[m.group(1)] = (f"${m.group(2)}", line_no) return found + def launch_rewrites(self, target: str) -> list[str]: + """The sed lines of the component launcher rewriting ``target``.""" + path = f"{self.name}/component_launcher.sh" + text = self.read(path) + if text is None: + return [] + return [ + source_ref([(path, number)]) + for number, line in enumerate(text.splitlines(), 1) + if re.search(r"\bsed\b.*\s-i\b", line) and target in line + ] + def romlists( - self, preset: str, text: str, rompath: str, applied: list[tuple[str, int]] + self, preset: str, text: str, rompath: str, applied: list[tuple[str, int]], + evidence: dict[str, Any], ) -> None: - """XRoar's `romlist NAME=a,b,@other`: the names tried for each ROM, in order.""" + """XRoar's `romlist NAME=a,b,@other`: the names tried for each ROM, in order. + + A list no XRoar machine or cartridge looks up is no read: coco1, + coco1e, coco1e_ext, coco2, coco2_ext and coco2b are defined by XRoar + (src/xroar.c:663-670) and named by nothing else. + """ lists: dict[str, tuple[int, list[str]]] = {} for line_no, line in enumerate(text.splitlines(), 1): found = re.match(r"^romlist\s+([\w-]+)=(.*)$", line.strip()) @@ -375,10 +408,12 @@ class Component: return list(dict.fromkeys(names)) for name, (line_no, _items) in lists.items(): + if name in UNREAD_ROMLISTS: + continue self.record( "names_searched", expand(name, (name,)), [(preset, line_no)] + applied, slot=name, search_dir=rompath, passes_to=f"romlist {name}", - fails_when_missing=False, + fails_when_missing=False, **evidence, ) def read_launcher(self) -> None: @@ -480,6 +515,7 @@ def core_options(read: Read) -> list[Launcher]: ] if not copied: raise LauncherPatternMissing(prepare_path, "the core options preset is never copied") + target = _words(prepare.splitlines()[copied[0] - 1].strip())[-1] records = [] for line_no, line in enumerate(text.splitlines(), 1): found = re.match(r'^([\w.-]+)\s*=\s*"([^"]*)"$', line.strip()) @@ -489,6 +525,8 @@ def core_options(read: Read) -> list[Launcher]: records.append(Launcher( emulator=LIBRETRO_COMPONENT, build="libretro", kind="core_option", names=[value], passes_to=f"core option {key}", fails_when_missing=False, + installs={"path": target, "source_ref": source_ref([(prepare_path, copied[0])])}, + kept_if_present=False, overrides=[], source_ref=source_ref([(CORE_OPTIONS, line_no), (prepare_path, copied[0])]), )) if not records: diff --git a/scripts/scraper/retropie_launchers.py b/scripts/scraper/retropie_launchers.py index 1f48b28c0..e6462323c 100644 --- a/scripts/scraper/retropie_launchers.py +++ b/scripts/scraper/retropie_launchers.py @@ -94,6 +94,11 @@ class Launcher: passes_to: str | None = None fails_when_missing: bool | str | None = None depends_on_option: str | None = None + # For a record citing a config: where the platform's code puts it, whether + # that leaves a file already there, and the launch code rewriting it. + installs: dict[str, str] | None = None + kept_if_present: bool | None = None + overrides: list[str] | None = None lines: list[tuple[str, int]] = field(default_factory=list, repr=False) def entry(self) -> dict[str, Any]: @@ -101,7 +106,8 @@ class Launcher: order = ( "emulator", "package", "repo", "systems", "build", "kind", "slot", "names", "case_insensitive", "search_dir", "passes_to", - "fails_when_missing", "depends_on_option", "source_ref", + "fails_when_missing", "depends_on_option", "installs", "kept_if_present", + "overrides", "source_ref", ) return {key: getattr(self, key) for key in order if getattr(self, key) is not None} diff --git a/scripts/scraper/rocknix_launchers.py b/scripts/scraper/rocknix_launchers.py index 303d92365..eb18cba3e 100644 --- a/scripts/scraper/rocknix_launchers.py +++ b/scripts/scraper/rocknix_launchers.py @@ -18,6 +18,7 @@ from dataclasses import dataclass try: from .configgen_launchers import Launcher + from .shell_launchers import config_placement, evidence_fields from .shell_launchers import ( LauncherPatternMissing, Link, @@ -30,6 +31,7 @@ try: ) except ImportError: from configgen_launchers import Launcher + from shell_launchers import config_placement, evidence_fields # type: ignore[no-redef] from shell_launchers import ( LauncherPatternMissing, Link, @@ -46,6 +48,9 @@ DEVICES = f"{ROCKNIX}/devices" STANDALONE = f"{ROCKNIX}/packages/emulators/standalone" RETROARCH = f"{ROCKNIX}/packages/emulators/libretro/retroarch" BIOS_ROOT = "/storage/roms/bios" +CONFIG_ROOT = "/storage/.config" +# The first-boot step copying every package's /usr/config to /storage/.config. +USERCONFIG = f"{ROCKNIX}/packages/sysutils/systemd/scripts/userconfig-setup" Lines = list[tuple[str, set[int]]] @@ -186,6 +191,38 @@ def bios_keys(tree: RepoTree, paths: list[str], root: str = BIOS_ROOT) -> list[t return keys +def with_evidence( + tree: RepoTree, + record: Launcher, + target: str, + keys: list[str], + start: Script | None = None, + writers: tuple[str, ...] = (), + source_name: str = "", +) -> Launcher: + """A config record with the steps that put its file at ``target``. + + The first boot copies /usr/config to /storage/.config, leaving a file + already there. A start script can copy or generate it again, and its + sed lines rewriting one of the record's keys are the overrides. + """ + setup = Script(USERCONFIG, tree.read(USERCONFIG)) + placements = [config_placement(setup, target, required=False, sources=("/usr/config/",))] + if start is not None: + placements.append(config_placement( + start, target, keys, writers, source_name, required=False + )) + fields = evidence_fields(placements) + record.installs = fields["installs"] + record.kept_if_present = fields["kept_if_present"] + record.overrides = fields["overrides"] + return record + + +def _keys(value: FixedPaths) -> list[str]: + return [f"[{section}] {key}" if section else key for section, key in value.keys] + + def config_records( tree: RepoTree, emulator: str, @@ -193,10 +230,17 @@ def config_records( file: str, passes_to_suffix: str = "", depends_on_option: str | None = None, + target: str = "", + start: Script | None = None, + unread: Callable[[tuple[str, str]], bool] = lambda key: False, ) -> list[Launcher]: - """fixed_name records for the bios paths of a config installed per device.""" + """fixed_name records for the bios paths of a config installed per device. + + ``unread`` drops the keys the shipped configs leave the emulator + reading no file from. + """ configs = device_configs(tree, package, file) - keys = bios_keys(tree, [c.path for c in configs]) + keys = [k for k in bios_keys(tree, [c.path for c in configs]) if not unread(k)] name = posixpath.basename(file) def read(path: str, text: str) -> tuple[object, set[int]]: @@ -206,7 +250,8 @@ def config_records( for device, value, refs in per_device(tree, configs, f"{package}/package.mk", read): assert isinstance(value, FixedPaths) described = describe_keys(name, value.keys) - records.append( + records.append(with_evidence( + tree, Launcher( emulator=emulator, build="standalone", @@ -219,8 +264,11 @@ def config_records( ), device=device, depends_on_option=depends_on_option, - ) - ) + ), + target or f"{CONFIG_ROOT}/{posixpath.basename(package)}/{name}", + _keys(value), + start, + )) return records @@ -241,7 +289,8 @@ def retroarch(tree: RepoTree) -> list[Launcher]: for device, value, refs in per_device(tree, configs, f"{RETROARCH}/package.mk", read): assert isinstance(value, tuple) values = dict(value) - records.append( + records.append(with_evidence( + tree, Launcher( emulator="retroarch", build="libretro", @@ -251,8 +300,10 @@ def retroarch(tree: RepoTree) -> list[Launcher]: search_dir=values["system_directory"], passes_to="retroarch.cfg " + ", ".join(f'{k} = "{v}"' for k, v in value), device=device, - ) - ) + ), + f"{CONFIG_ROOT}/retroarch/retroarch.cfg", + [k for k, _ in value], + )) return records @@ -275,6 +326,19 @@ def amiberry(tree: RepoTree) -> list[Launcher]: ) used: set[int] = {test_line, then_line, else_line, other_line, sed_line} root = script.expand("${" + sed.group(2) + "}", sed_line, used) + written = re.search(r'"?(\$\{?\w+\}?)"?\s*$', script.lines[then_line - 1]) + if written is None: + raise script.missing("destination of the config copy") + target = script.expand(written.group(1), then_line, set()) + # A WHDLoad archive is booted by --autoload, which builds its own setup + # (kickstart included) over the config: the record holds for floppies. + autoload = [n for n, line in script.code() if re.search(r"amiberry .*--autoload", line)] + if not autoload: + raise script.missing("WHDLoad boot with --autoload") + content = ( + "content not booted with --autoload, a WHDLoad .lha or .zip " + f"({posixpath.basename(script.path)}:{', '.join(map(str, autoload))})" + ) records = [] for config, condition in ( (then.group(1), f"game name matches {test.group(1)}"), @@ -290,7 +354,8 @@ def amiberry(tree: RepoTree) -> list[Launcher]: raise LauncherPatternMissing(path, f"key naming a file under {sed.group(1)}") value, lines = fixed_paths(found, keys, path) slot = re.fullmatch(r"Amiga(\w+)-default\.uae", config) - records.append( + records.append(with_evidence( + tree, Launcher( emulator="amiberry", build="standalone", @@ -300,9 +365,13 @@ def amiberry(tree: RepoTree) -> list[Launcher]: slot=slot.group(1) if slot else config, search_dir=value.search_dir, passes_to=f"{describe_keys(config, value.keys)} (amiberry -f)", - depends_on_option=condition, - ) - ) + depends_on_option=f"{condition}, {content}", + ), + target, + _keys(value), + script, + source_name=config, + )) mk = Script(f"{package}/package.mk", tree.read(f"{package}/package.mk")) links = mk.links(BIOS_ROOT, fixed={"INSTALL": ""}) if not links: @@ -375,7 +444,11 @@ def hatari(tree: RepoTree) -> list[Launcher]: if not keys: raise LauncherPatternMissing(path, f"key naming a file under {BIOS_ROOT}") value, lines = fixed_paths(found, keys, path) - return [ + copied = re.search(r'"?(\$\{?\w+\}?)"?\s*$', script.lines[copy_line - 1]) + if copied is None: + raise script.missing("destination of the default config copy") + return [with_evidence( + tree, Launcher( emulator="hatari", build="standalone", @@ -384,8 +457,12 @@ def hatari(tree: RepoTree) -> list[Launcher]: source_ref=source_ref([(path, lines), (script.path, {copy_line, run_line})]), search_dir=value.search_dir, passes_to=f"{describe_keys(copy.group(1), value.keys)} (--configfile)", - ) - ] + ), + script.expand(copied.group(1), copy_line, set()), + _keys(value), + script, + source_name=copy.group(1), + )] def melonds(tree: RepoTree) -> list[Launcher]: @@ -406,7 +483,22 @@ def melonds(tree: RepoTree) -> list[Launcher]: rf"^\s*{test.group(1)}=\$\(get_setting (\w+) ", f"setting read into {test.group(1)}" ) condition = f"{setting.group(1)} = {test.group(2)} sets ExternalBIOSEnable=1, otherwise 0" - records = config_records(tree, "melonds", package, "melonDS.ini", depends_on_option=condition) + configs = [c.path for c in device_configs(tree, package, "melonDS.ini")] + # melonDS opens the DSi BIOS and firmware keys only under + # DSiExternalBIOSEnable, off unless a config sets it. + dsi_external = any( + s.key == "DSiExternalBIOSEnable" and s.value not in ("", "0") + for path in configs for s in settings(tree.read(path)) + ) + + def unread(key: tuple[str, str]) -> bool: + return not dsi_external and key[1] in ("DSiBIOS9Path", "DSiBIOS7Path", "DSiFirmwarePath") + + conf_line, conf = script.find(r'^\s*CONF_DIR="([^"]+)"', "CONF_DIR") + records = config_records( + tree, "melonds", package, "melonDS.ini", depends_on_option=condition, + target=f"{conf.group(1)}/melonDS.ini", start=script, unread=unread, + ) for record in records: record.source_ref += ( f", {source_ref([(script.path, {setting_line, test_line, enable_line})])}" @@ -420,7 +512,10 @@ def dsperate(tree: RepoTree) -> list[Launcher]: f"{package}/scripts/start_dsperate.sh", tree.read(f"{package}/scripts/start_dsperate.sh") ) run_line, _ = script.find(r"/usr/bin/dsperate --config ", "dsperate --config") - records = config_records(tree, "dsperate", package, "dsperate.ini", " (--config)") + records = config_records( + tree, "dsperate", package, "dsperate.ini", " (--config)", + target=script.expand("${DSPERATE_INI}", run_line, set()), start=script, + ) for record in records: record.source_ref += f", {source_ref([(script.path, {run_line})])}" return records @@ -434,7 +529,10 @@ def xemu(tree: RepoTree) -> list[Launcher]: copy_line, _ = script.find( r'cp -r "\$\{SOURCE_DIR\}/\$\{XEMU_INI\}"', "xemu.toml copy at launch" ) - records = config_records(tree, "xemu", package, "xemu.toml", " (copied at each launch)") + records = config_records( + tree, "xemu", package, "xemu.toml", " (copied at each launch)", + target=script.expand("${CONF_DIR}/${XEMU_INI}", copy_line, set()), start=script, + ) for record in records: record.source_ref += f", {source_ref([(script.path, {copy_line})])}" return records @@ -452,7 +550,9 @@ def nanoboyadvance(tree: RepoTree) -> list[Launcher]: mk_line, _ = Script(mk_path, tree.read(mk_path)).find( r"config/config\.toml", "config.toml install" ) - return [ + start_path = f"{package}/scripts/start_nanoboyadvance.sh" + return [with_evidence( + tree, Launcher( emulator="nanoboyadvance", build="standalone", @@ -461,8 +561,14 @@ def nanoboyadvance(tree: RepoTree) -> list[Launcher]: source_ref=source_ref([(path, lines), (mk_path, {mk_line})]), search_dir=value.search_dir, passes_to=describe_keys("config.toml", value.keys), - ) - ] + # NanoBoyAdvance exits when its BIOS cannot be read + # (src/platform/sdl/main.cpp:180-185 at the build ROCKNIX ships). + fails_when_missing=True, + ), + f"{CONFIG_ROOT}/nanoboyadvance/config.toml", + _keys(value), + Script(start_path, tree.read(start_path)), + )] def mednafen(tree: RepoTree) -> list[Launcher]: @@ -486,7 +592,9 @@ def mednafen(tree: RepoTree) -> list[Launcher]: modules: dict[str, list[Setting]] = {} for setting in firmware: modules.setdefault(setting.key.split(".", 1)[0], []).append(setting) - return [ + home = script.expand("${MEDNAFEN_HOME}", gen_line, set()) + return [with_evidence( + tree, Launcher( emulator="mednafen", build="standalone", @@ -501,9 +609,12 @@ def mednafen(tree: RepoTree) -> list[Launcher]: slot=module, search_dir=folder.value, passes_to="mednafen.cfg " + ", ".join(s.key for s in group), - ) - for module, group in modules.items() - ] + ), + f"{home}/mednafen.cfg", + [s.key for s in group], + script, + writers=("mednafen_gen_config.sh",), + ) for module, group in modules.items()] def yabasanshiro(tree: RepoTree) -> list[Launcher]: @@ -549,6 +660,8 @@ def directory_records( file: str, folder_key: tuple[str, str], file_keys: tuple[tuple[str, str], ...], + target: str = "", + start: str = "", ) -> list[Launcher]: """scans_dir records: the config hands a directory and leaves the file keys empty.""" configs = device_configs(tree, package, file) @@ -568,7 +681,8 @@ def directory_records( records = [] for device, folder, refs in per_device(tree, configs, f"{package}/package.mk", read): passes = f"{name} [{folder_key[0]}] {folder_key[1]} = {folder}, {unset} unset" - records.append( + records.append(with_evidence( + tree, Launcher( emulator=emulator, build="standalone", @@ -578,8 +692,11 @@ def directory_records( search_dir=folder, passes_to=passes, device=device, - ) - ) + ), + target, + [f"[{s}] {k}" for s, k in (folder_key, *file_keys)], + Script(start, tree.read(start)) if start else None, + )) return records @@ -591,6 +708,8 @@ def duckstation(tree: RepoTree) -> list[Launcher]: "settings.ini", ("BIOS", "SearchDirectory"), (("BIOS", "PathNTSCU"), ("BIOS", "PathNTSCJ"), ("BIOS", "PathPAL")), + f"{CONFIG_ROOT}/duckstation/settings.ini", + f"{STANDALONE}/duckstation-sa/scripts/start_duckstation.sh", ) @@ -602,6 +721,8 @@ def armsx2(tree: RepoTree) -> list[Launcher]: "ARMSX2/inis/PCSX2.ini", ("Folders", "Bios"), (("Filenames", "BIOS"),), + f"{CONFIG_ROOT}/ARMSX2/inis/PCSX2.ini", + f"{STANDALONE}/armsx2-sa/scripts/start_armsx2.sh", ) diff --git a/tests/test_launchers_amberelec.py b/tests/test_launchers_amberelec.py index c0ba27d36..39ce37130 100644 --- a/tests/test_launchers_amberelec.py +++ b/tests/test_launchers_amberelec.py @@ -105,22 +105,6 @@ HATARI_CFG = text( """ ) -ATARI800_CFG = text( - """ - Atari 800 Emulator, Version 3.1.0 - ROM_OS_A_NTSC= - ROM_OS_A_PAL=/storage/roms/bios/ATARIOSA.ROM - ROM_5200=/storage/roms/bios/5200.rom - """ -) - -ATARI800_PATCH = text( - """ - + LDFLAGS += -static-libgcc - + CFLAGS += -DDEFAULT_CFG_NAME="\\"/storage/.config/distribution/configs/atari800.cfg\\"" - """ -) - AMIBERRY_MK = text( """ makeinstall_target() { @@ -141,6 +125,17 @@ AMIBERRY_SH = text( cp $AMIBERRY_CONFIG_DIR/AmigaA500-default.uae "$AMIBERRY_TMP_CONFIG" fi } + amiberry -f "$AMIBERRY_TMP_CONFIG" --autoload "$1" >> $AMIBERRY_LOG 2>&1 + """ +) + +USERCONFIG = text( + """ + #!/bin/bash + if [ ! -e "/storage/.configured" ] + then + rsync -a --ignore-existing --exclude=es_systems.cfg /usr/config/* /storage/.config/ || exit 1 + fi """ ) @@ -195,8 +190,7 @@ def tree(**overrides: str) -> dict[str, str]: al.SCRIPTS["piemu"]: PIEMU, al.SCRIPTS["HATARISA"]: HATARI_SH, al.HATARI_CONFIG: HATARI_CFG, - al.ATARI800_CONFIG: ATARI800_CFG, - al.ATARI800_PATCH: ATARI800_PATCH, + al.USERCONFIG: USERCONFIG, al.AMIBERRY_PACKAGE: AMIBERRY_MK, al.SCRIPTS["AMIBERRY"]: AMIBERRY_SH, f"{al.AMIBERRY_CONFIGS}/AmigaA1200-default.uae": uae("kick40068.A1200.rom"), @@ -218,6 +212,13 @@ class Frontends(unittest.TestCase): (record,) = al.retroarch(read()) self.assertEqual(record.search_dir, "~/roms/bios") self.assertEqual(record.source_ref, f"{al.RETROARCH_CFG}:2") + # The first boot copies /usr/config and leaves a file already there. + self.assertEqual(record.installs, { + "path": "/storage/.config/retroarch/retroarch.cfg", + "source_ref": f"{al.USERCONFIG}:4", + }) + self.assertIs(record.kept_if_present, True) + self.assertEqual(record.overrides, []) def test_retrorun_directory(self): (record,) = al.retrorun(read()) @@ -266,6 +267,9 @@ class Standalone(unittest.TestCase): record.source_ref, f"{al.RUNEMU}:3, {al.SCRIPTS['HATARISA']}:3-4, {al.HATARI_CONFIG}:2", ) + # Copied over at each launch: nothing the user edits survives. + self.assertEqual(record.installs["source_ref"], f"{al.SCRIPTS['HATARISA']}:3") + self.assertIs(record.kept_if_present, False) def test_amiberry_link_and_one_kickstart_per_config(self): link, a1200, a500 = al.amiberry(read(), al.Runemu(read())) @@ -273,7 +277,11 @@ class Standalone(unittest.TestCase): self.assertEqual(link.passes_to, "/usr/config/amiberry/kickstarts is a symlink to it") self.assertEqual((a1200.slot, a1200.names), ("A1200", ["kick40068.A1200.rom"])) self.assertEqual((a500.slot, a500.names), ("A500", ["kick34005.A500.rom"])) - self.assertEqual(a500.depends_on_option, f"not ({a1200.depends_on_option})") + self.assertTrue(a500.depends_on_option.startswith("not (")) + # A WHDLoad boot replaces the setup the config gives. + self.assertIn("--autoload", a1200.depends_on_option) + self.assertEqual(a1200.installs["source_ref"], f"{al.SCRIPTS['AMIBERRY']}:5") + self.assertEqual(a500.installs["source_ref"], f"{al.SCRIPTS['AMIBERRY']}:7") def test_a_table_running_another_script_raises(self): broken = RUNEMU.replace("/usr/bin/yabasanshiro.sh", "/usr/bin/yaba.sh") @@ -287,15 +295,9 @@ class Standalone(unittest.TestCase): class Cores(unittest.TestCase): - def test_atari800_built_to_read_the_shipped_config(self): - (record,) = al.atari800(read()) - self.assertEqual(record.names, ["ATARIOSA.ROM", "5200.rom"]) - self.assertEqual(record.source_ref, f"{al.ATARI800_PATCH}:2, {al.ATARI800_CONFIG}:3-4") - - def test_atari800_built_for_another_config_raises(self): - broken = ATARI800_PATCH.replace("atari800.cfg", "other.cfg") - with self.assertRaises(LauncherShapeError): - al.atari800(read(**{al.ATARI800_PATCH: broken})) + def test_atari800_config_is_no_record(self): + # The core reads atari800.cfg only under an option that is off. + self.assertFalse(any(r["emulator"] == "atari800" for r in al.extract_launchers(read()))) def test_scummvm_paths_written_under_bios(self): theme, extra = al.scummvm(read()) @@ -319,7 +321,7 @@ class Cores(unittest.TestCase): def test_every_extractor_runs_on_the_fixture(self): records = al.extract_launchers(read()) # One easyrpg runtime in the fixture, two at the pinned commit. - self.assertEqual(len(records), 16) + self.assertEqual(len(records), 15) class PlatformFile(unittest.TestCase): diff --git a/tests/test_launchers_emudeck.py b/tests/test_launchers_emudeck.py index 1dd106f6c..acd606440 100644 --- a/tests/test_launchers_emudeck.py +++ b/tests/test_launchers_emudeck.py @@ -40,6 +40,9 @@ ARES_SH = text( UserBIOSPath='/home/deck/Emulation/bios/' sed -i "s|$UserBIOSPath|${biosPath}\\/|g" "$ares_configFile" } + ares_init(){ + configEmuFP "${ares_emuName}" "${ares_emuPath}" "true" + } """ ) @@ -87,6 +90,9 @@ MELONDS_SH = text( buttonA='Joy_A=' changeLine "$buttonA" "$buttonA""0" "${melonDS_configFile}" } + melonDS_update(){ + configEmuFP "${melonDS_emuName}" "${melonDS_emuPath}" + } """ ) @@ -103,6 +109,7 @@ MELONDS_TOML = text( LastROMFolder = "/run/media/mmcblk0p1/Emulation/roms/ds" [Emu] ExternalBIOSEnable = false + ConsoleType = 0 [DS] FirmwarePath = "/run/media/mmcblk0p1/Emulation/bios/firmware.bin" BIOS7Path = "/run/media/mmcblk0p1/Emulation/bios/bios7.bin" @@ -206,8 +213,23 @@ SHADPS4_SH = text( ) +CONFIG_EMU_FP = text( + """ + configEmuFP(){ + if [[ $overwrite == 'true' ]]; then + overwrite="--backup --suffix=.bak" + else + overwrite="--ignore-existing" + fi + rsync -avhp --mkpath "$emudeckBackend/configs/${ID}" "$HOME/.var/app/" $overwrite + } +""" +) + + def tree(**scripts: str) -> RepoTree: files = {f"{S}/{name}": body for name, body in scripts.items()} + files[el.CONFIG_EMU_FP] = CONFIG_EMU_FP files[f"{el.CONFIGS}/dev.ares.ares/data/ares/settings.bml"] = ARES_BML files[f"{el.CONFIGS}/net.kuribo64.melonDS/config/melonDS/melonDS.ini"] = MELONDS_INI files[f"{el.CONFIGS}/net.kuribo64.melonDS/config/melonDS/melonDS.toml"] = MELONDS_TOML @@ -253,8 +275,17 @@ class WrittenIntoTheConfig(unittest.TestCase): self.assertEqual( toml["passes_to"], "melonDS.toml [DS] FirmwarePath, BIOS7Path [DSi] NANDPath" ) + # The DS keys wait for ExternalBIOSEnable, the DSi keys for a DSi. self.assertEqual( - toml["depends_on_option"], "ExternalBIOSEnable = false in the shipped melonDS.toml" + toml["depends_on_option"], + "[DS] keys: ExternalBIOSEnable = true, false in the shipped melonDS.toml; " + "[DSi] keys: ConsoleType = 1, 0 in the shipped melonDS.toml", + ) + # configEmuFP's update call passes --ignore-existing. + self.assertIs(toml["kept_if_present"], True) + self.assertEqual( + toml["installs"]["path"], + "${HOME}/.var/app/net.kuribo64.melonDS/config/melonDS/melonDS.toml", ) def test_a_config_the_setters_rewrite_keeps_their_values(self): diff --git a/tests/test_launchers_emuelec.py b/tests/test_launchers_emuelec.py index ae6315c98..630547701 100644 --- a/tests/test_launchers_emuelec.py +++ b/tests/test_launchers_emuelec.py @@ -228,6 +228,9 @@ DUCKSTATION_SH = text( """ CONFIG_DIR="/emuelec/configs/duckstation" duckstation-nogui -fullscreen -settings "${CONFIG_DIR}/settings.ini" -- "${1}" + if [ ! -d "${CONFIG_DIR}" ]; then + cp -rf /usr/config/emuelec/configs/duckstation/* "${CONFIG_DIR}" + fi """ ) @@ -319,6 +322,9 @@ ADVMAME_SH = text( sed -i "s|/roms/arcade|/roms/mame|g" ${CONFIG_DIR}/advmame.rc fi SDL_AUDIODRIVER=alsa advmame ${ARG} -quiet + if [ ! -d "${CONFIG_DIR}" ]; then + cp -rf /usr/config/emuelec/configs/advmame/* "${CONFIG_DIR}/" + fi """ ) @@ -480,24 +486,12 @@ class LibretroRecords(unittest.TestCase): self.assertEqual(jar["passes_to"], "${HOME}/roms/bios") self.assertEqual(jar["depends_on_option"], "/storage/roms/bios/jdk/eeversion exists") - def test_mame_copies_its_own_files_and_reads_only_path_options(self): + def test_mame_copies_its_own_files_and_hands_no_folder(self): + # The core passes no path option and reads no mame.ini: only the + # copies are the launcher's. found = records(el.mame) - copies = [r for r in found if r["kind"] == "copies"] - self.assertEqual([r["names"] for r in copies], [["fm*"], ["mame*"]]) - folders = [(r["passes_to"], r["search_dir"]) for r in found if r["kind"] == "passes_path"] - self.assertEqual( - folders, - [ - ("mame.ini rompath", "/storage/roms/bios/mame/bios"), - ("mame.ini samplepath", "/storage/roms/bios/mame/samples"), - ], - ) - self.assertTrue(all(r["depends_on_option"] == "mame_read_config = enabled" - for r in found if r["kind"] == "passes_path")) - - def test_mame_without_the_read_config_option_raises(self): - with self.assertRaises(LauncherPatternMissing): - records(el.mame, **{f"{el.LIBRETRO}/mame/config/MAME/MAME.opt": "x = 1\n"}) + self.assertEqual([r["kind"] for r in found], ["copies", "copies"]) + self.assertEqual([r["names"] for r in found], [["fm*"], ["mame*"]]) class StandaloneRecords(unittest.TestCase): @@ -546,6 +540,12 @@ class StandaloneRecords(unittest.TestCase): (record,) = records(el.duckstation) self.assertEqual((record["kind"], record["search_dir"]), ("scans_dir", "/storage/roms/bios")) + def test_duckstation_config_is_kept_when_present(self): + (record,) = records(el.duckstation) + self.assertEqual(record["installs"]["path"], "/emuelec/configs/duckstation/settings.ini") + self.assertIs(record["kept_if_present"], True) + self.assertEqual(record["overrides"], []) + def test_duckstation_with_a_fixed_bios_path_raises(self): changed = DUCKSTATION_INI.replace("PathPAL =", "PathPAL = /storage/roms/bios/scph5502.bin") with self.assertRaises(LauncherPatternMissing): @@ -631,7 +631,8 @@ class StandaloneRecords(unittest.TestCase): def test_every_extractor_runs_on_the_fixtures(self): found = el.extract_launchers(tree()) - self.assertEqual(len(found), 28) + # The two mame.ini folders are no longer records. + self.assertEqual(len(found), 26) class Revision(unittest.TestCase): diff --git a/tests/test_launchers_rocknix.py b/tests/test_launchers_rocknix.py index 29d31dce7..730645c63 100644 --- a/tests/test_launchers_rocknix.py +++ b/tests/test_launchers_rocknix.py @@ -85,13 +85,14 @@ MELONDS_SH = text( sed -i '/^ExternalBIOSEnable=/c\\ExternalBIOSEnable=0' /storage/.config/melonDS/melonDS.ini fi fi + CONF_DIR="/storage/.config/melonDS" """ ) def melonds_tree() -> RepoTree: pkg = f"{SA}/melonds-sa" - return RepoTree.from_files( + return rocknix_tree( { **devices("AMD64", "H700", "RK3576", "S922X"), f"{pkg}/package.mk": MELONDS_MK, @@ -148,6 +149,7 @@ AMIBERRY_SH = text( cp -rf /usr/config/amiberry ${AMIBERRY_DIR} fi cd ${AMIBERRY_DIR} + amiberry -f "${AMIBERRY_TMP_CONFIG}" --autoload "$1" >> ${AMIBERRY_LOG} 2>&1 """ ) @@ -291,6 +293,22 @@ FLYCAST_SH = text( ) +USERCONFIG = text( + """ + #!/bin/bash + if [ ! -e "/storage/.configured" ] + then + rsync -a --ignore-existing --exclude={es_features.cfg,es_systems.cfg} /usr/config/* /storage/.config/ >/var/log/configure.log 2>&1 + fi + """ +) + + +def rocknix_tree(files: dict[str, str], links: dict[str, str] | None = None) -> RepoTree: + """A ROCKNIX tree holding the first-boot copy every config record cites.""" + return RepoTree.from_files({rl.USERCONFIG: USERCONFIG, **files}, links) + + def by(records: list[dict], emulator: str) -> list[dict]: return [r for r in records if r["emulator"] == emulator] @@ -302,23 +320,26 @@ class ConfigsPerDevice(unittest.TestCase): empty, named = records self.assertEqual(empty["names"], []) self.assertTrue(empty["passes_to"].endswith("DSiNANDPath left empty")) + # melonDS opens the DSi BIOS and firmware keys only under + # DSiExternalBIOSEnable, which no shipped config sets. self.assertEqual( - named["names"], - [ - "bios9.bin", - "bios7.bin", - "firmware.bin", - "dsi_bios9.bin", - "dsi_bios7.bin", - "dsi_firmware.bin", - "dsi_nand.bin", - ], + named["names"], ["bios9.bin", "bios7.bin", "firmware.bin", "dsi_nand.bin"] ) self.assertEqual(named["search_dir"], "/storage/roms/bios") + self.assertEqual(named["installs"]["path"], "/storage/.config/melonDS/melonDS.ini") + self.assertIs(named["kept_if_present"], True) + self.assertEqual(named["overrides"], []) + + def test_a_config_setting_the_dsi_switch_keeps_the_dsi_keys(self): + tree = melonds_tree() + path = f"{SA}/melonds-sa/config/S922X/melonDS.ini" + tree._texts[path] = tree.read(path) + "DSiExternalBIOSEnable=1\n" + named = rl.melonds(tree)[1].entry() + self.assertIn("dsi_bios9.bin", named["names"]) def test_a_linked_device_directory_reads_its_target(self): named = rl.melonds(melonds_tree())[1].entry() - self.assertIn(f"{SA}/melonds-sa/config/InputPlumber/melonDS.ini:2-8", named["source_ref"]) + self.assertIn(f"{SA}/melonds-sa/config/InputPlumber/melonDS.ini:2-4, 8", named["source_ref"]) self.assertNotIn("config/RK3576", named["source_ref"]) def test_the_external_bios_switch_is_the_option(self): @@ -330,11 +351,14 @@ class ConfigsPerDevice(unittest.TestCase): def test_identical_configs_give_one_record_without_device(self): pkg = f"{SA}/dsperate-sa" - tree = RepoTree.from_files( + tree = rocknix_tree( { **devices("H700", "RK3576", "S922X"), f"{pkg}/package.mk": DSPERATE_MK, - f"{pkg}/scripts/start_dsperate.sh": '${EMUPERF} /usr/bin/dsperate --config "${DSPERATE_INI}" "${ROM}"\n', + f"{pkg}/scripts/start_dsperate.sh": ( + '${EMUPERF} /usr/bin/dsperate --config "${DSPERATE_INI}" "${ROM}"\n' + 'DSPERATE_INI="/storage/.config/dsperate/dsperate.ini"\n' + ), f"{pkg}/config/H700/dsperate.ini": DSPERATE_INI, f"{pkg}/config/InputPlumber/dsperate.ini": DSPERATE_INI, f"{pkg}/config/S922X/dsperate.ini": DSPERATE_INI, @@ -352,10 +376,11 @@ class ConfigsPerDevice(unittest.TestCase): def test_an_absent_key_and_an_empty_one_hand_over_the_same(self): pkg = f"{SA}/duckstation-sa" - tree = RepoTree.from_files( + tree = rocknix_tree( { **devices("AMD64", "S922X"), f"{pkg}/package.mk": DUCKSTATION_MK, + f"{pkg}/scripts/start_duckstation.sh": "#!/bin/bash\n", f"{pkg}/config/AMD64/settings.ini": "[BIOS]\nSearchDirectory = /storage/roms/bios\n", f"{pkg}/config/S922X/settings.ini": text( """ @@ -375,10 +400,11 @@ class ConfigsPerDevice(unittest.TestCase): def test_a_bios_written_into_a_path_key_is_an_unknown_shape(self): pkg = f"{SA}/duckstation-sa" - tree = RepoTree.from_files( + tree = rocknix_tree( { **devices("S922X"), f"{pkg}/package.mk": DUCKSTATION_MK, + f"{pkg}/scripts/start_duckstation.sh": "#!/bin/bash\n", f"{pkg}/config/S922X/settings.ini": "[BIOS]\nSearchDirectory = /storage/roms/bios\nPathNTSCU = scph5501.bin\n", } ) @@ -387,7 +413,7 @@ class ConfigsPerDevice(unittest.TestCase): def test_a_package_without_a_device_copy_raises(self): pkg = f"{SA}/duckstation-sa" - tree = RepoTree.from_files( + tree = rocknix_tree( { **devices("S922X"), f"{pkg}/package.mk": "makeinstall_target() {\n true\n}\n", @@ -402,7 +428,7 @@ class NamesTheConfigsFix(unittest.TestCase): def test_amiberry_machine_follows_the_game_name(self): pkg = f"{SA}/amiberry" uae = "amiberry.rom_path=/storage/.config/amiberry/kickstarts/\nkickstart_rom_file=@BIOS_PATH@/{}\nkickstart_ext_rom_file=\n" - tree = RepoTree.from_files( + tree = rocknix_tree( { f"{pkg}/scripts/start_amiberry.sh": AMIBERRY_SH, f"{pkg}/package.mk": AMIBERRY_MK, @@ -412,9 +438,15 @@ class NamesTheConfigsFix(unittest.TestCase): ) a1200, a500, link, entered, copied = (r.entry() for r in rl.amiberry(tree)) self.assertEqual((a1200["slot"], a1200["names"]), ("A1200", ["kick40068.A1200.rom"])) - self.assertEqual(a1200["depends_on_option"], "game name matches AGA|CD32") + self.assertEqual( + a1200["depends_on_option"], + "game name matches AGA|CD32, content not booted with --autoload, " + "a WHDLoad .lha or .zip (start_amiberry.sh:17)", + ) + self.assertEqual(a1200["installs"]["path"], "${AMIBERRY_TMP_CONFIG}") + self.assertIs(a1200["kept_if_present"], False) self.assertEqual((a500["slot"], a500["names"]), ("A500", ["kick34005.A500.rom"])) - self.assertEqual(a500["depends_on_option"], "game name does not match AGA|CD32") + self.assertTrue(a500["depends_on_option"].startswith("game name does not match AGA|CD32")) self.assertEqual(a500["search_dir"], "/storage/roms/bios") self.assertEqual( a1200["source_ref"], @@ -443,7 +475,7 @@ class NamesTheConfigsFix(unittest.TestCase): def test_mednafen_one_record_per_module(self): pkg = f"{SA}/mednafen" - tree = RepoTree.from_files( + tree = rocknix_tree( { f"{pkg}/config/common/mednafen.template": MEDNAFEN_TEMPLATE, f"{pkg}/scripts/start_mednafen.sh": MEDNAFEN_SH, @@ -461,7 +493,7 @@ class NamesTheConfigsFix(unittest.TestCase): def test_mednafen_without_a_firmware_directory_raises(self): pkg = f"{SA}/mednafen" - tree = RepoTree.from_files( + tree = rocknix_tree( { f"{pkg}/config/common/mednafen.template": "psx.bios_eu scph5502.bin\n", f"{pkg}/scripts/start_mednafen.sh": MEDNAFEN_SH, @@ -473,7 +505,7 @@ class NamesTheConfigsFix(unittest.TestCase): class SearchedNames(unittest.TestCase): def tree(self, script: str) -> RepoTree: - return RepoTree.from_files({f"{SA}/yabasanshiro-sa/scripts/start_yabasanshiro.sh": script}) + return rocknix_tree({f"{SA}/yabasanshiro-sa/scripts/start_yabasanshiro.sh": script}) def test_yabasanshiro_tries_its_names_in_order(self): (record,) = [r.entry() for r in rl.yabasanshiro(self.tree(YABASANSHIRO_SH))] @@ -514,7 +546,7 @@ class Links(unittest.TestCase): def tree(self) -> RepoTree: scripts = self.all_scripts() - return RepoTree.from_files({p: scripts[e] for e, p in rl.LINK_SCRIPTS}) + return rocknix_tree({p: scripts[e] for e, p in rl.LINK_SCRIPTS}) def emulator(self, name: str) -> list[dict]: return by([r.entry() for r in rl.links(self.tree())], name) @@ -566,7 +598,7 @@ class Links(unittest.TestCase): def test_a_script_without_a_bios_link_raises(self): scripts = self.all_scripts() scripts["flycast"] = 'ln -sf "/storage/.config/flycast" "/storage/.local/share/flycast"\n' - tree = RepoTree.from_files({p: scripts[e] for e, p in rl.LINK_SCRIPTS}) + tree = rocknix_tree({p: scripts[e] for e, p in rl.LINK_SCRIPTS}) with self.assertRaises(LauncherPatternMissing): rl.links(tree) @@ -603,7 +635,7 @@ class ProgramsRunOutsideTheBiosFolder(unittest.TestCase): PATH = f"{SA}/pico-8/scripts/start_pico8.sh" def test_pico8_runs_its_binary_from_roms(self): - (record,) = [r.entry() for r in rl.pico8(RepoTree.from_files({self.PATH: START_PICO8}))] + (record,) = [r.entry() for r in rl.pico8(rocknix_tree({self.PATH: START_PICO8}))] self.assertEqual( (record["emulator"], record["kind"], record["names"], record["search_dir"], record["passes_to"]), @@ -615,7 +647,7 @@ class ProgramsRunOutsideTheBiosFolder(unittest.TestCase): def test_a_script_without_its_binary_choice_raises(self): broken = START_PICO8.replace('STATIC_BIN="pico8_dyn"', "STATIC_BIN=$OTHER") with self.assertRaises(LauncherPatternMissing): - rl.pico8(RepoTree.from_files({self.PATH: broken})) + rl.pico8(rocknix_tree({self.PATH: broken})) class Revision(unittest.TestCase): From f9504825678cff9ee81fd8456584ffeff2b8fab9 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 11 Nov 2026 11:04:08 +0100 Subject: [PATCH 3/4] feat: cite option lists as choices_from RetroBat's es_features.cfg and KNULLI's es_features.yml list the values a menu offers. They move out of source_ref into choices_from, beside the generator that writes the choice. RetroBat's xemu flashrom record holds outside a Chihiro launch (Xemu.Generator.cs:48, 284), and KNULLI's libretro boot disk records name the one core es_features offers them for. Rescraped at the pinned revisions. --- platforms/knulli.yml | 10 ++++--- platforms/retrobat.yml | 40 +++++++++++++++++---------- scripts/scraper/knulli_launchers.py | 16 ++++++++--- scripts/scraper/retrobat_launchers.py | 38 +++++++++++++++++++++++-- tests/test_launchers_knulli.py | 9 +++--- 5 files changed, 84 insertions(+), 29 deletions(-) diff --git a/platforms/knulli.yml b/platforms/knulli.yml index 2bbbad334..63aadbd72 100644 --- a/platforms/knulli.yml +++ b/platforms/knulli.yml @@ -5662,9 +5662,10 @@ launchers: depends_on_option: "bootdisk (not (messSysName[messMode] == '' or messMode == -1)\ \ and softList == '' and system.name == 'macintosh' and system.isOptSet('bootdisk')\ \ and not (system.config['bootdisk'] in ['macos30', 'macos608', 'macos701', 'macos75']))" + choices_from: "package/emulationstation/knulli-es-system/es_features.yml:11037-11043" source_ref: "package/system/knulli-configgen/configgen/configgen/generators/mame/mameGenerator.py:303-305,\ - \ 375-379, 384-385, package/emulationstation/knulli-es-system/es_features.yml:11037-11043" -- emulator: mame, mess, mamevirtual, same_cdi + \ 375-379, 384-385" +- emulator: mame build: libretro kind: fixed_name names: @@ -5685,7 +5686,7 @@ launchers: \ in ['macos30', 'macos608', 'macos701', 'macos75'])" source_ref: "package/system/knulli-configgen/configgen/configgen/generators/libretro/libretroMAMEConfig.py:64,\ \ 123, 197, 265-268, 270, 274" -- emulator: mame, mess, mamevirtual, same_cdi +- emulator: mame build: libretro kind: fixed_name names: @@ -5704,8 +5705,9 @@ launchers: \ 'plugnplay', 'vis']) and not (messSysName[messMode] == '') and not (softList\ \ != '') and system.name == 'macintosh' and system.isOptSet('bootdisk') and not\ \ (system.config['bootdisk'] in ['macos30', 'macos608', 'macos701', 'macos75']))" + choices_from: "package/emulationstation/knulli-es-system/es_features.yml:3727-3733" source_ref: "package/system/knulli-configgen/configgen/configgen/generators/libretro/libretroMAMEConfig.py:64,\ - \ 123, 197, 265-268, 273-274, package/emulationstation/knulli-es-system/es_features.yml:3727-3733" + \ 123, 197, 265-268, 273-274" - emulator: dolphin build: standalone kind: fixed_name diff --git a/platforms/retrobat.yml b/platforms/retrobat.yml index 185be4204..290d649fa 100644 --- a/platforms/retrobat.yml +++ b/platforms/retrobat.yml @@ -4259,7 +4259,8 @@ launchers: passes_to: "settings.ini [BIOS] PathNTSCJ = PathNTSCU = PathPAL = " fails_when_missing: not checked by the launcher depends_on_option: duck_bios - source_ref: "emulatorLauncher/Generators/Duckstation.Generator.cs:197-202, .emulationstation/es_features.cfg:12489-12497" + choices_from: ".emulationstation/es_features.cfg:12489-12497" + source_ref: "emulatorLauncher/Generators/Duckstation.Generator.cs:197-202" - emulator: eka2l1 build: standalone kind: passes_path @@ -4599,7 +4600,8 @@ launchers: passes_to: "hatari.cfg [ROM] szTosImageFileName=/hatari/tos/" fails_when_missing: ApplicationException 'Missing TOS file in 'bios' folder.' depends_on_option: hatari_tos - source_ref: "emulatorLauncher/Generators/Hatari.Generator.cs:410-415, 454, .emulationstation/es_features.cfg:13768-13774" + choices_from: ".emulationstation/es_features.cfg:13768-13774" + source_ref: "emulatorLauncher/Generators/Hatari.Generator.cs:410-415, 454" - emulator: hatari build: standalone kind: fixed_name @@ -4639,7 +4641,8 @@ launchers: passes_to: hatari_tosimage = , if the file is there | default fails_when_missing: hatari_tosimage = default depends_on_option: hatari2014_tos - source_ref: "emulatorLauncher/Generators/LibRetro.CoreOptions.cs:3409-3417, .emulationstation/es_features.cfg:7095-7102" + choices_from: ".emulationstation/es_features.cfg:7095-7102" + source_ref: "emulatorLauncher/Generators/LibRetro.CoreOptions.cs:3409-3417" - emulator: hatarib build: libretro kind: fixed_name @@ -4667,8 +4670,8 @@ launchers: passes_to: hatarib_tos = hatarib/ fails_when_missing: hatarib_tos = , or when bios holds it depends_on_option: hatarib_tos - source_ref: "emulatorLauncher/Generators/LibRetro.CoreOptions.cs:3434-3435, 3457-3474,\ - \ .emulationstation/es_features.cfg:7125-7134" + choices_from: ".emulationstation/es_features.cfg:7125-7134" + source_ref: "emulatorLauncher/Generators/LibRetro.CoreOptions.cs:3434-3435, 3457-3474" - emulator: hatarib build: libretro kind: copies @@ -4885,7 +4888,8 @@ launchers: fails_when_missing: falls back to dsi_nand.bin depends_on_option: melonds_console == "dsi", melonds_nandregion set, rom extension not ".bin" - source_ref: "emulatorLauncher/Generators/MelonDS.Generator.cs:193-202, .emulationstation/es_features.cfg:16816-16823" + choices_from: ".emulationstation/es_features.cfg:16816-16823" + source_ref: "emulatorLauncher/Generators/MelonDS.Generator.cs:193-202" - emulator: mesen build: standalone kind: copies @@ -5081,7 +5085,8 @@ launchers: passes_to: "kronos.ini [1.0] General\\Bios = /, only if the file exists" fails_when_missing: General\Bios not written depends_on_option: kronos_force_bios - source_ref: "emulatorLauncher/Generators/Kronos.Generator.cs:162-170, .emulationstation/es_features.cfg:14519-14524" + choices_from: ".emulationstation/es_features.cfg:14519-14524" + source_ref: "emulatorLauncher/Generators/Kronos.Generator.cs:162-170" - emulator: ssf build: standalone kind: core_option @@ -5094,7 +5099,8 @@ launchers: passes_to: "SSF.ini [Peripheral] SaturnBIOS = /, only if the file exists" fails_when_missing: SaturnBIOS not written depends_on_option: use_external_bios - source_ref: "emulatorLauncher/Generators/SSF.Generator.cs:76-83, .emulationstation/es_features.cfg:20762-20767" + choices_from: ".emulationstation/es_features.cfg:20762-20767" + source_ref: "emulatorLauncher/Generators/SSF.Generator.cs:76-83" - emulator: yabasanshiro build: standalone kind: fixed_name @@ -5119,7 +5125,8 @@ launchers: \ exists" fails_when_missing: General\Bios not written depends_on_option: yabasanshiro_force_bios - source_ref: "emulatorLauncher/Generators/Yabasanshiro.Generator.cs:123-131, .emulationstation/es_features.cfg:22724-22729" + choices_from: ".emulationstation/es_features.cfg:22724-22729" + source_ref: "emulatorLauncher/Generators/Yabasanshiro.Generator.cs:123-131" - emulator: ymir build: standalone kind: fixed_name @@ -5144,7 +5151,8 @@ launchers: \ check" fails_when_missing: path written without an existence check depends_on_option: ymir_force_bios - source_ref: "emulatorLauncher/Generators/Ymir.Generator.cs:120-125, .emulationstation/es_features.cfg:22784-22788" + choices_from: ".emulationstation/es_features.cfg:22784-22788" + source_ref: "emulatorLauncher/Generators/Ymir.Generator.cs:120-125" - emulator: ymir build: standalone kind: passes_path @@ -5222,7 +5230,8 @@ launchers: fails_when_missing: ApplicationException 'BIOS file does not exist in ''.' depends_on_option: lrps2_bios - source_ref: "emulatorLauncher/Generators/LibRetro.CoreOptions.cs:3695-3704, .emulationstation/es_features.cfg:5875-5881" + choices_from: ".emulationstation/es_features.cfg:5875-5881" + source_ref: "emulatorLauncher/Generators/LibRetro.CoreOptions.cs:3695-3704" - emulator: pcsx2 build: libretro kind: scans_dir @@ -5635,8 +5644,10 @@ launchers: search_dir: bios passes_to: "xemu.toml [sys.files] flashrom_path=/, default Complex_4627.bin" fails_when_missing: path written without an existence check - depends_on_option: xemu_flashrom - source_ref: "emulatorLauncher/Generators/Xemu.Generator.cs:286-289, .emulationstation/es_features.cfg:22081-22085" + depends_on_option: "xemu_flashrom, not (emulator == \"xemu-chihiro\" || system ==\ + \ \"chihiro\")" + choices_from: ".emulationstation/es_features.cfg:22081-22085" + source_ref: "emulatorLauncher/Generators/Xemu.Generator.cs:48, 284-289" - emulator: xemu build: standalone kind: names_searched @@ -5752,7 +5763,8 @@ launchers: - coco3h - coco3p - coco3ph - source_ref: "emulatorLauncher/Generators/Xroar.Generator.cs:76-80, .emulationstation/es_features.cfg:22650-22659" + choices_from: ".emulationstation/es_features.cfg:22650-22659" + source_ref: "emulatorLauncher/Generators/Xroar.Generator.cs:76-80" - emulator: xroar build: standalone kind: passes_path diff --git a/scripts/scraper/knulli_launchers.py b/scripts/scraper/knulli_launchers.py index e5a138afd..d2e464324 100644 --- a/scripts/scraper/knulli_launchers.py +++ b/scripts/scraper/knulli_launchers.py @@ -1001,6 +1001,14 @@ def boot_disks( emulator = ( ", ".join(_caller_cores(cg, spec.caller)) if spec.caller else cg.emulator(module) ) + if spec.caller and spec.feature[:2] == ("libretro", "cores"): + # The option is offered for one core of the system: es_features + # lists it under that core, the other cores the generator serves + # never see it (KNULLI's macintosh offers mame alone). + core = spec.feature[2] + if core not in emulator.split(", "): + src.fail(f"es_features offers the option for {core}, which the generator does not serve") + emulator = core found = 0 for chain in ast.walk(src.tree): if not ( @@ -1054,10 +1062,10 @@ def boot_disks( expands={option: values}, case_insensitive=False, search_dir=cg.bios, passes_to=f"command line {flag}", fails_when_missing=False, depends_on_option=f"{option} ({' and '.join(tests)})", - source_ref=", ".join([ - src.ref([*spans, src.span(statement), _lines(added)]), - f"{ES_FEATURES}:{lines[0]}-{lines[1]}" if not listed else "", - ]).rstrip(", "), + # The values the menu offers come from es_features.yml; + # the generator writing the choice is the evidence. + choices_from=None if listed else f"{ES_FEATURES}:{lines[0]}-{lines[1]}", + source_ref=src.ref([*spans, src.span(statement), _lines(added)]), )) if not found: src.fail("names no BIOS file from an option value") diff --git a/scripts/scraper/retrobat_launchers.py b/scripts/scraper/retrobat_launchers.py index 5405580c6..a49709194 100644 --- a/scripts/scraper/retrobat_launchers.py +++ b/scripts/scraper/retrobat_launchers.py @@ -14,6 +14,11 @@ import bisect import re from collections.abc import Callable, Iterator +try: + from .shell_launchers import is_feature_ref, split_ref +except ImportError: + from shell_launchers import is_feature_ref, split_ref # type: ignore[no-redef] + GENERATORS = "emulatorLauncher/Generators" FEATURES = ".emulationstation/es_features.cfg" @@ -1254,6 +1259,11 @@ def xemu(sources: dict[str, Source]) -> list[Record]: ) flash = src.find(r'isOptSet\("(xemu_flashrom)"\)', "xemu_flashrom") + # The flashrom is written outside a Chihiro launch only. + chihiro_set = src.find(r"(_chihiro) = \(([^;]+)\);", "Chihiro test") + chihiro_guard = src.find_all( + rf"if \(!{chihiro_set.group(1)}\)", "flashrom guard", end=flash.start() + )[-1] flash_default = src.find( r'ini\.WriteValue\("([\w.]+)", "(flashrom_path)", "\'" \+ Path\.Combine\(flashromPath, "([^"]+)"\)', "flashrom default", @@ -1267,7 +1277,10 @@ def xemu(sources: dict[str, Source]) -> list[Record]: "core_option", _unique([flash_default.group(3), *choices]), source_ref( - (src.path, [src.lines(flash.start(), flash_default.end())]), + (src.path, [ + src.lines(chihiro_set.start(), chihiro_set.end()), + src.lines(chihiro_guard.start(), flash_default.end()), + ]), (features.path, [choice_lines]), ), case_insensitive=True, @@ -1277,7 +1290,7 @@ def xemu(sources: dict[str, Source]) -> list[Record]: f" default {flash_default.group(3)}" ), fails_when_missing="path written without an existence check", - depends_on_option=flash.group(1), + depends_on_option=f"{flash.group(1)}, not ({chihiro_set.group(2)})", ) ) @@ -3890,4 +3903,23 @@ def extract_launchers(texts: dict[str, str]) -> list[Record]: if missing: raise LauncherPatternMissing(missing[0], "source file") sources = {path: Source(path, text) for path, text in texts.items()} - return [record for extractor in EXTRACTORS for record in extractor(sources)] + return [ + _choices_apart(record) for extractor in EXTRACTORS for record in extractor(sources) + ] + + +def _choices_apart(record: Record) -> Record: + """The es_features.cfg part of a record's source_ref, moved to choices_from. + + The feature file lists the values a menu option offers. The generator + writing the choice is the record's evidence, never the list. + """ + parts = split_ref(str(record["source_ref"])) + listed = [p for p in parts if is_feature_ref(p)] + if not listed: + return record + code = [p for p in parts if not is_feature_ref(p)] + if not code: + raise LauncherPatternMissing(listed[0], "generator code beside the option list") + moved = {k: v for k, v in record.items() if k != "source_ref"} + return {**moved, "choices_from": ", ".join(listed), "source_ref": ", ".join(code)} diff --git a/tests/test_launchers_knulli.py b/tests/test_launchers_knulli.py index 2657c8dd5..16500d681 100644 --- a/tests/test_launchers_knulli.py +++ b/tests/test_launchers_knulli.py @@ -598,10 +598,11 @@ class BootDisks(unittest.TestCase): self.assertEqual((hard.names, hard.passes_to), (["{bootdisk}.chd"], "command line -hard")) self.assertEqual(hard.expands, {"bootdisk": ["macos3", "mac755"]}) self.assertTrue(hard.depends_on_option.startswith("bootdisk (system.name == 'macintosh'")) - self.assertEqual( - hard.source_ref, - f"{G}/mame/mameGenerator.py:3-4, 9-10, {kl.ES_FEATURES}:8-10", - ) + # The menu's values are where the choices come from, the generator + # writing the choice is the evidence. + self.assertEqual(hard.source_ref, f"{G}/mame/mameGenerator.py:3-4, 9-10") + self.assertEqual(hard.choices_from, f"{kl.ES_FEATURES}:8-10") + self.assertIsNone(floppy.choices_from) def test_a_name_built_another_way_raises(self): broken = MAME.replace('"/userdata/bios/" + system.config["bootdisk"] + ".chd"', "disk") From a6716796a9822b27cafc5aaae6e7e5d870956c15 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 11 Nov 2026 11:11:50 +0100 Subject: [PATCH 4/4] feat: write config_reads into the profiles Where each shipped build finds the config a platform installs and reads every key it opens a file from, read at the build the platform ships: duckstation (EmuELEC, ROCKNIX, RetroBat), xemu, armsx2, hatari, melonDS, dsperate, NanoBoyAdvance, mednafen (ROCKNIX's sydarn fork) and ares, gzdoom, xroar, lrps2 and the amiberry builds at their pins. pin_build names the builds ROCKNIX, EmuELEC and AmberELEC compile, revision the ones RetroBat and Flathub install unpinned. tests/test_launcher_evidence.py covers the rule, the extractor helpers, the invariants and the chains that move placements. --- emulators/amiberry-5.6.5.yml | 7 + emulators/amiberry-7.1.1.yml | 9 + emulators/ares.yml | 43 +++++ emulators/armsx2.yml | 19 ++ emulators/dsperate.yml | 26 +++ emulators/duckstation.yml | 55 ++++++ emulators/gzdoom.yml | 11 ++ emulators/hatari.yml | 39 ++++ emulators/lrps2.yml | 7 + emulators/mednafen.yml | 55 ++++++ emulators/melonds.yml | 52 +++++ emulators/nanoboyadvance.yml | 8 + emulators/xemu.yml | 31 +++ emulators/xroar.yml | 51 +++++ tests/test_launcher_evidence.py | 330 ++++++++++++++++++++++++++++++++ 15 files changed, 743 insertions(+) create mode 100644 tests/test_launcher_evidence.py diff --git a/emulators/amiberry-5.6.5.yml b/emulators/amiberry-5.6.5.yml index 8671ce5eb..5db40d55e 100644 --- a/emulators/amiberry-5.6.5.yml +++ b/emulators/amiberry-5.6.5.yml @@ -32,6 +32,13 @@ systems: standalone_directories: - "getcwd" - "rom_path = kickstarts" +config_reads: + - file: "*.uae" + source_ref: "src/main.cpp:926-934" + keys: + - key: "kickstart_rom_file" + source_ref: "src/cfgfile.cpp:6050, src/memory.cpp:1907-1916" + notes: | Amiberry 5.6.5 (src/osdep/target.h:27). Every folder it reads is below its start path: $EXTERNAL_FILES_DIR or $XDG_DATA_HOME when either holds a diff --git a/emulators/amiberry-7.1.1.yml b/emulators/amiberry-7.1.1.yml index c617c4df7..fc433e525 100644 --- a/emulators/amiberry-7.1.1.yml +++ b/emulators/amiberry-7.1.1.yml @@ -43,6 +43,15 @@ standalone_directories: - "AMIBERRY_HOME_DIR/roms" - "~/Amiberry/roms" - "whdboot_path = whdboot" +config_reads: + - file: "*.uae" + source_ref: "src/main.cpp:986-994" + keys: + - key: "kickstart_rom_file" + source_ref: "src/cfgfile.cpp:6122, src/memory.cpp:1974-1983" + - key: "$(FILE_PATH)" + source_ref: "src/cfgfile.cpp:389" + notes: | Amiberry 7.1.1 (CMakeLists.txt:3-4). The ROM path is rom_path in amiberry.conf, else roms/ below the home directory: $AMIBERRY_HOME_DIR, diff --git a/emulators/ares.yml b/emulators/ares.yml index 8f991ad13..35623cf00 100644 --- a/emulators/ares.yml +++ b/emulators/ares.yml @@ -56,6 +56,49 @@ systems: - zx-spectrum - zx-spectrum-128 +config_reads: + - file: "settings.bml" + source_ref: "desktop-ui/desktop-ui.cpp:9-35, 100-103, 114, desktop-ui/settings/settings.cpp:31-42, 263-286, nall/nall/path.cpp:123-142" + keys: + - key: "ColecoVision/Firmware/BIOS.World" + source_ref: "desktop-ui/emulator/colecovision.cpp:10-12, 42-50, mia/system/colecovision.cpp:7-9" + - key: "MSX/Firmware/BIOS.Japan" + source_ref: "desktop-ui/emulator/msx.cpp:16-19, 44-53" + - key: "FamicomDiskSystem/Firmware/BIOS.Japan" + source_ref: "desktop-ui/emulator/famicom-disk-system.cpp:17-19, 89-97" + - key: + - "Nintendo64DD/Firmware/BIOS.Japan" + - "Nintendo64DD/Firmware/BIOS.US" + - "Nintendo64DD/Firmware/BIOS.DEV" + source_ref: "desktop-ui/emulator/nintendo-64dd.cpp:15-19, 59-73" + condition: "the game's region" + - key: "GameBoyAdvance/Firmware/BIOS.World" + source_ref: "desktop-ui/emulator/game-boy-advance.cpp:12-14, 55-63" + - key: + - "MasterSystem/Firmware/BIOS.US" + - "MasterSystem/Firmware/BIOS.Japan" + - "MasterSystem/Firmware/BIOS.Europe" + source_ref: "desktop-ui/emulator/master-system.cpp:12-16, 93-107" + condition: "the game's region" + - key: "GameGear/Firmware/BIOS.World" + source_ref: "desktop-ui/emulator/game-gear.cpp:10-12, 30-40" + - key: + - "MegaCD/Firmware/BIOS.US" + - "MegaCD/Firmware/BIOS.Japan" + - "MegaCD/Firmware/BIOS.Europe" + source_ref: "desktop-ui/emulator/mega-cd.cpp:15-19, 63-77" + condition: "the disc's region" + - key: "NeoGeoPocket/Firmware/BIOS.World" + source_ref: "desktop-ui/emulator/neo-geo-pocket.cpp:10-12, 32-40" + - key: "NeoGeoPocketColor/Firmware/BIOS.World" + source_ref: "desktop-ui/emulator/neo-geo-pocket-color.cpp:10-12, 32-40" + - key: + - "PlayStation/Firmware/BIOS.US" + - "PlayStation/Firmware/BIOS.Japan" + - "PlayStation/Firmware/BIOS.Europe" + source_ref: "desktop-ui/emulator/playstation.cpp:19-23, 87-101" + condition: "the disc's region" + notes: | Multi-system emulator by Near (byuu), successor to higan/bsnes. Focuses on accuracy and preservation. 40 emulators registered at this pin diff --git a/emulators/armsx2.yml b/emulators/armsx2.yml index 5ee879e61..10de74fdc 100644 --- a/emulators/armsx2.yml +++ b/emulators/armsx2.yml @@ -48,6 +48,25 @@ data_root: source_ref: "source pcsx2/Pcsx2Config.cpp:2523-2530, pcsx2/Pcsx2Config.cpp:2540-2560, pcsx2/Pcsx2Config.cpp:2567-2589" note: "portable.ini or portable.txt beside the binary makes its folder the root" +pin_build: + - platform: rocknix + repo: "https://github.com/ROCKNIX/distribution" + revision: "20261101" + path: "projects/ROCKNIX/packages/emulators/standalone/armsx2-sa/package.mk" + variable: PKG_VERSION + value: "19c83bf065ed21a19d8c46a1f578e37d2dab8efd" +config_reads: + - file: "PCSX2.ini" + platforms: [rocknix] + source_ref: "pcsx2-qt/QtHost.cpp:1307, 1325-1331, pcsx2/Pcsx2Config.cpp:2398-2491, pcsx2/VMManager.cpp:630-641" + keys: + - key: "[Folders] Bios" + source_ref: "pcsx2/Pcsx2Config.cpp:2514-2524" + condition: "[UI] SettingsVersion 1, else the ini is reset (pcsx2-qt/QtHost.cpp:1331-1342)" + - key: "[Filenames] BIOS" + source_ref: "pcsx2/Pcsx2Config.cpp:1879-1881, 2258-2264, pcsx2/ps2/BiosTools.cpp:258-282, 333-348, pcsx2/VMManager.cpp:1638" + condition: "empty: the first PS2 BIOS of 4 to 8 MB found in [Folders] Bios" + notes: | PlayStation 2 emulator, a GPL-3.0 fork of PCSX2 that replaces the x86-64 only EE, IOP, VU and vtlb fast memory recompilers with native AArch64 ones. One tree diff --git a/emulators/dsperate.yml b/emulators/dsperate.yml index 528c5a43f..2017c7e2e 100644 --- a/emulators/dsperate.yml +++ b/emulators/dsperate.yml @@ -32,6 +32,32 @@ data_root: source_ref: "src/frontend/sdl/config.cpp:39-49" note: "~/.config/dsperate when XDG_CONFIG_HOME is unset. System files are read wherever the [paths] keys of the ini point." +config_reads: + - file: "dsperate.ini" + platforms: [rocknix] + revision: "ddf2910e6d80220c6b22b4df61f0786ac9879cda" + revision_from: "PKG_VERSION 3.0.5, the release binaries CI built from tag v3.0.5" + source_ref: "src/frontend/sdl/main.cpp:1065, 1155-1157, src/frontend/sdl/config.cpp:64-81" + keys: + - key: + - "[paths] bios9" + - "[paths] bios7" + source_ref: "src/frontend/sdl/main.cpp:1175, 1270-1274, src/core/nds.cpp:114-129" + condition: "both present, otherwise FreeBIOS" + - key: "[paths] firmware" + source_ref: "src/frontend/sdl/main.cpp:1178, 1270, src/core/nds.cpp:123, 134-135" + - key: + - "[paths] bios9i" + - "[paths] bios7i" + source_ref: "src/frontend/sdl/main.cpp:1179-1186, 1319-1322, 1331-1334, src/core/nds_dsi.cpp:88-99" + condition: "DSi session" + - key: "[paths] dsi_firmware" + source_ref: "src/frontend/sdl/main.cpp:1176-1178, 1270" + condition: "DSi session" + - key: "[paths] dsi_nand" + source_ref: "src/frontend/sdl/main.cpp:1158-1162, 1324-1326, 1337, src/core/nds_dsi.cpp:114-120" + condition: "--dsi-mode, BootMenuDSi.nds or a .dspr.nds shortcut" + notes: | Nintendo DS emulator for ARM handhelds with a DSi mode. ROCKNIX installs the release tarball for its architecture and runs it with --config pointing at diff --git a/emulators/duckstation.yml b/emulators/duckstation.yml index 739810077..bfb8f089d 100644 --- a/emulators/duckstation.yml +++ b/emulators/duckstation.yml @@ -33,6 +33,61 @@ data_root: source_ref: "src/core/core.cpp:137-151, src/core/core.cpp:190-215" note: "portable.txt or settings.ini beside the executable, or beside the AppImage, makes that folder the root" +pin_build: + - platform: emuelec + repo: "https://github.com/EmuELEC/EmuELEC" + revision: "v4.8" + path: "packages/sx05re/emulators/duckstation/package.mk" + variable: PKG_VERSION + value: "fd3507c16d098fb32806c281caaefb205946da8a" + - platform: rocknix + repo: "https://github.com/ROCKNIX/distribution" + revision: "20261101" + path: "projects/ROCKNIX/packages/emulators/standalone/duckstation-sa/package.mk" + variable: PKG_VERSION + value: "0.1-10998" +config_reads: + - file: "settings.ini" + platforms: [emuelec] + source_ref: "src/frontend-common/common_host_interface.cpp:403-407, 3042-3050, 3304-3327, src/duckstation-nogui/main.cpp:62-64, 137-140, src/duckstation-nogui/nogui_host_interface.cpp:39-42" + keys: + - key: "[BIOS] SearchDirectory" + source_ref: "src/core/host_interface.cpp:253-260" + - key: + - "[BIOS] PathNTSCU" + - "[BIOS] PathNTSCJ" + - "[BIOS] PathPAL" + source_ref: "src/core/host_interface.cpp:262-375, src/core/system.cpp:817" + condition: "the console region; an empty value scans SearchDirectory" + - file: "settings.ini" + platforms: [rocknix] + revision: "9b0a4ec559ae83bfdb14685932b036ad7c1701be" + revision_from: "PKG_VERSION 0.1-10998, the AppImage of tag v0.1-10998" + source_ref: "src/core/core.cpp:105-223, 227-230, 232-275, src/duckstation-qt/qthost.cpp:796-800" + keys: + - key: "[BIOS] SearchDirectory" + source_ref: "src/core/settings.cpp:2709-2719, 2723" + - key: + - "[BIOS] PathNTSCU" + - "[BIOS] PathNTSCJ" + - "[BIOS] PathPAL" + source_ref: "src/core/bios.cpp:351-458, src/core/system.cpp:2923" + condition: "the console region; an empty value scans SearchDirectory" + - file: "settings.ini" + platforms: [retrobat] + revision: "5fd366809053fe287291de7a39752c4d5d5b146b" + revision_from: "version 0.1-11752-g5fd366809 of duckstation.exe in RetroBat's stable duckstation.7z, 2026-11-10" + source_ref: "src/core/core.cpp:131-249, 253-256, 258-301, src/duckstation-qt/qthost.cpp:803-807" + keys: + - key: "[BIOS] SearchDirectory" + source_ref: "src/core/settings.cpp:2859-2869, 2873" + - key: + - "[BIOS] PathNTSCU" + - "[BIOS] PathNTSCJ" + - "[BIOS] PathPAL" + source_ref: "src/core/bios.cpp:351-456, src/core/system.cpp:2671" + condition: "the console region; an empty value scans SearchDirectory" + notes: | DuckStation auto-detects BIOS by MD5 hash from a flat scan of the BIOS directory. ref: src/core/bios.cpp:458-479 (FindBIOSImagesInDirectory, scans all files by size) diff --git a/emulators/gzdoom.yml b/emulators/gzdoom.yml index ee25a5c18..d958edfa8 100644 --- a/emulators/gzdoom.yml +++ b/emulators/gzdoom.yml @@ -17,6 +17,17 @@ standalone_pools: "[IWADSearch.Directories] Path": "" "[IWADSearch.Directories] RecursivePath": "" +config_reads: + - file: "gzdoom.ini" + source_ref: "src/gameconfigfile.cpp:95-107, 942-951, src/common/platform/posix/unix/i_specialpaths.cpp:168-171" + keys: + - key: "[IWADSearch.Directories] Path" + source_ref: "src/d_iwad.cpp:456-470" + - key: "[FileSearch.Directories] Path" + source_ref: "src/common/utility/findfile.cpp:224-260" + - key: "[SoundfontSearch.Directories] Path" + source_ref: "src/common/audio/music/i_soundfont.cpp:393-410" + notes: | Doom-engine source port by Christoph Oelckers. Supports Doom, Heretic, Hexen, Strife, Chex Quest and derived games through a unified IWAD diff --git a/emulators/hatari.yml b/emulators/hatari.yml index 801bd818f..940f07f95 100644 --- a/emulators/hatari.yml +++ b/emulators/hatari.yml @@ -38,6 +38,45 @@ data_root: source_ref: "upstream src/paths.c:24-29, upstream src/paths.c:201-274" note: "~/.hatari is used instead when it exists and ~/.config/hatari does not. Holds hatari.cfg, hatari.nvram and auto.sav. The default tos.img is read from ../share/hatari beside the bin directory." +pin_build: + - platform: emuelec + repo: "https://github.com/EmuELEC/EmuELEC" + revision: "v4.8" + path: "packages/sx05re/emulators/hatarisa/package.mk" + variable: PKG_VERSION + value: "fda67f05188a925535fefc1b0ef24eb044a029d2" + - platform: amberelec + repo: "https://github.com/AmberELEC/AmberELEC" + revision: "a21975bfb1a3af68f66e07a931b994b523e1e6e2" + path: "packages/games/emulators/hatarisa/package.mk" + variable: PKG_VERSION + value: "d876db9c4b6911c059b1d9318f779f90c1c6f2d7" + - platform: rocknix + repo: "https://github.com/ROCKNIX/distribution" + revision: "20261101" + path: "projects/ROCKNIX/packages/emulators/standalone/hatarisa/package.mk" + variable: PKG_VERSION + value: "6da06056d89bb39777063388d82d065d9e2e31fd" +config_reads: + - file: "*.cfg" + platforms: [emuelec] + source_ref: "upstream src/options.c:1187-1193, upstream src/configuration.c:1088, 1149" + keys: + - key: "[ROM] szTosImageFileName" + source_ref: "upstream src/configuration.c:481, upstream src/tos.c:975-986" + - file: "*.cfg" + platforms: [amberelec] + source_ref: "upstream src/options.c:1283-1288, upstream src/configuration.c:1061, 1123" + keys: + - key: "[ROM] szTosImageFileName" + source_ref: "upstream src/configuration.c:489, upstream src/tos.c:976-987" + - file: "*.cfg" + platforms: [rocknix] + source_ref: "upstream src/options.c:1194-1199, upstream src/configuration.c:1050, 1111" + keys: + - key: "[ROM] szTosImageFileName" + source_ref: "upstream src/configuration.c:480, upstream src/tos.c:975-986" + notes: | The main branch of libretro/hatari is the upstream Hatari tree at c453b85f, whose libretro target in src/retro carries the VFS, disk diff --git a/emulators/lrps2.yml b/emulators/lrps2.yml index 58cc5208d..d8541e5d8 100644 --- a/emulators/lrps2.yml +++ b/emulators/lrps2.yml @@ -24,6 +24,13 @@ systems: [sony-playstation-2] bios_directory: "pcsx2/bios/" resources_directory: "pcsx2/resources/" +config_reads: + - file: "core options" + source_ref: "libretro/main.cpp:506, 525-527" + keys: + - key: "pcsx2_bios" + source_ref: "libretro/main.cpp:525-536, 2552-2560" + notes: | Hard fork of PCSX2 (pre-Qt, 1.7-dev era) ported to libretro, x86_64 only, with much of the emulator since rewritten in C89. The system directory of diff --git a/emulators/mednafen.yml b/emulators/mednafen.yml index 5cdc34595..7d1a95297 100644 --- a/emulators/mednafen.yml +++ b/emulators/mednafen.yml @@ -43,6 +43,61 @@ systems: - snk-ngpc - sony-playstation +pin_build: + - platform: rocknix + repo: "https://github.com/ROCKNIX/distribution" + revision: "20261101" + path: "projects/ROCKNIX/packages/emulators/standalone/mednafen/package.mk" + variable: PKG_URL + value: "https://github.com/sydarn/mednafen/archive/refs/tags/1.32.1-chd.tar.gz" +config_reads: + - file: "mednafen.cfg" + platforms: [rocknix] + revision: "e46995e49cbaf1e9996a8e29cc5be16d7fe6d834" + revision_from: "tag 1.32.1-chd of the sydarn/mednafen fork, the PKG_URL ROCKNIX builds" + source_ref: "src/drivers/main.cpp:1469-1520, 1883-1974, 2249, 2405, src/general.cpp:277-300" + keys: + - key: "gba.bios" + source_ref: "src/gba/GBA.cpp:3278, 772, 2472-2481" + - key: "md.cdbios" + source_ref: "src/md/system.cpp:755, 630-640, src/md/cd/cd.cpp:894-912, 971-982" + condition: "CD content" + - key: "nes.ggrom" + source_ref: "src/nes/nes.cpp:455, 298-299, src/nes/cart.cpp:570-583" + condition: "nes.gg 1" + - key: "pce.cdbios" + source_ref: "src/pce/pce.cpp:1103, 647-649" + condition: "disc not detected as Games Express" + - key: "pce.gecdbios" + source_ref: "src/pce/pce.cpp:1104, 647-649" + condition: "disc detected as Games Express" + - key: "pce_fast.cdbios" + source_ref: "src/pce_fast/pce.cpp:647, 476-487" + - key: "pcfx.bios" + source_ref: "src/pcfx/pcfx.cpp:1115, 606-611" + - key: + - "psx.bios_eu" + - "psx.bios_jp" + - "psx.bios_na" + source_ref: "src/psx/psx.cpp:2373-2375, 1764-1775" + condition: "the disc's region" + - key: + - "ss.bios_jp" + - "ss.bios_na_eu" + source_ref: "src/ss/ss.cpp:2441-2442, 1430-1439" + condition: "not ST-V, by SMPC area" + - key: + - "ss.bios_stv_eu" + - "ss.bios_stv_jp" + - "ss.bios_stv_na" + source_ref: "src/ss/ss.cpp:2444-2446, 1420-1439" + condition: "ST-V cart, by area" + - key: + - "ss.cart.kof95_path" + - "ss.cart.ultraman_path" + source_ref: "src/ss/ss.cpp:2477-2478, 1811-1840, 1320-1350, 1389-1396" + condition: "cart type KOF95 or ULTRAMAN" + notes: | Multi-system emulator by the Mednafen Team, the upstream of the beetle_* and mednafen_* libretro cores, which are forks of individual modules. A module is diff --git a/emulators/melonds.yml b/emulators/melonds.yml index 432e036a9..02e8ac401 100644 --- a/emulators/melonds.yml +++ b/emulators/melonds.yml @@ -28,6 +28,58 @@ data_root: source_ref: "upstream src/frontend/qt_sdl/main.cpp:182-184, 196-200, 206-211" note: "portable beside the executable when that folder exists" +pin_build: + - platform: rocknix + repo: "https://github.com/ROCKNIX/distribution" + revision: "20261101" + path: "projects/ROCKNIX/packages/emulators/standalone/melonds-sa/package.mk" + variable: PKG_VERSION + value: "906e9ebb27da8c6a715cd7abab4abfe8a8d29427" +config_reads: + - file: "melonDS.ini" + platforms: [rocknix] + source_ref: "upstream src/frontend/qt_sdl/main.cpp:366, upstream src/frontend/qt_sdl/Config.cpp:682-774, 777-783, 785-795, upstream src/frontend/qt_sdl/Platform.cpp:144-166, ROCKNIX projects/ROCKNIX/packages/emulators/standalone/melonds-sa/patches/001-default-config.patch:8-13, ROCKNIX projects/ROCKNIX/packages/emulators/standalone/melonds-sa/patches/003-hotkeys.patch:41-76" + keys: + - key: "BIOS9Path" + source_ref: "upstream src/frontend/qt_sdl/Config.cpp:238, 240, upstream src/frontend/qt_sdl/EmuInstance.cpp:855-876, 1261-1263" + condition: "ExternalBIOSEnable=1" + - key: "BIOS7Path" + source_ref: "upstream src/frontend/qt_sdl/Config.cpp:238, 241, upstream src/frontend/qt_sdl/EmuInstance.cpp:878-898, 1265-1267" + condition: "ExternalBIOSEnable=1" + - key: "FirmwarePath" + source_ref: "upstream src/frontend/qt_sdl/Config.cpp:238, 242, upstream src/frontend/qt_sdl/EmuInstance.cpp:992-1016, 1269" + condition: "ExternalBIOSEnable=1 and ConsoleType=0" + - key: "DSiNANDPath" + source_ref: "upstream src/frontend/qt_sdl/Config.cpp:227, 249, upstream src/frontend/qt_sdl/EmuInstance.cpp:1039-1043, 1227, 1312-1322" + condition: "ConsoleType=1" + - file: "melonDS.toml" + platforms: [emudeck, retrobat] + revision: "b86390e4428bf38ce4c1ce0e9ca446d6d25955e8" + revision_from: "melonDS 1.1: the Flathub manifest net.kuribo64.melonDS at e5ce4504 builds it, and RetroBat's melonDS.exe names it" + source_ref: "upstream src/frontend/qt_sdl/main.cpp:196-213, 328, upstream src/frontend/qt_sdl/Config.cpp:42, 785-799" + keys: + - key: "[DS] BIOS9Path" + source_ref: "upstream src/frontend/qt_sdl/EmuInstance.cpp:868-889, 1277" + condition: "[Emu] ExternalBIOSEnable = true" + - key: "[DS] BIOS7Path" + source_ref: "upstream src/frontend/qt_sdl/EmuInstance.cpp:891-911" + condition: "[Emu] ExternalBIOSEnable = true" + - key: "[DS] FirmwarePath" + source_ref: "upstream src/frontend/qt_sdl/EmuInstance.cpp:1013-1031, 1285" + condition: "[Emu] ExternalBIOSEnable = true and ConsoleType = 0" + - key: "[DSi] BIOS9Path" + source_ref: "upstream src/frontend/qt_sdl/EmuInstance.cpp:913-938, 1328-1336" + condition: "[Emu] ConsoleType = 1" + - key: "[DSi] BIOS7Path" + source_ref: "upstream src/frontend/qt_sdl/EmuInstance.cpp:940-965, 1328-1332" + condition: "[Emu] ConsoleType = 1" + - key: "[DSi] FirmwarePath" + source_ref: "upstream src/frontend/qt_sdl/EmuInstance.cpp:1013-1029, 1285" + condition: "[Emu] ConsoleType = 1" + - key: "[DSi] NANDPath" + source_ref: "upstream src/frontend/qt_sdl/EmuInstance.cpp:1064-1068, 1338-1340" + condition: "[Emu] ConsoleType = 1" + notes: | FreeBIOS fallback for DS mode (NTR), while DSi mode requires all native files. Libretro fork based on melonDS ~0.9.x era, validates size only. diff --git a/emulators/nanoboyadvance.yml b/emulators/nanoboyadvance.yml index 0e113e0f4..1141ff63e 100644 --- a/emulators/nanoboyadvance.yml +++ b/emulators/nanoboyadvance.yml @@ -24,6 +24,14 @@ cores: systems: - nintendo-gba +config_reads: + - file: "/storage/.config/nanoboyadvance/config.toml" + platforms: [rocknix] + source_ref: "src/platform/sdl/main.cpp:293, src/platform/core/src/config.cpp:16-31" + keys: + - key: "[general] bios_path" + source_ref: "src/platform/core/src/config.cpp:36, src/platform/sdl/main.cpp:177-191, src/platform/core/src/loader/bios.cpp:19-46" + notes: | Game Boy Advance emulator. ROCKNIX builds the SDL2 frontend only (PLATFORM_SDL2=ON, PLATFORM_QT=OFF). diff --git a/emulators/xemu.yml b/emulators/xemu.yml index 573fd6be8..b18aa80a1 100644 --- a/emulators/xemu.yml +++ b/emulators/xemu.yml @@ -45,6 +45,37 @@ data_root: source_ref: "ui/xemu-settings.cc:51-63, 73-92" note: "xemu.toml beside the executable makes its folder the root" +pin_build: + - platform: rocknix + repo: "https://github.com/ROCKNIX/distribution" + revision: "20261101" + path: "projects/ROCKNIX/packages/emulators/standalone/xemu-sa/package.mk" + variable: PKG_VERSION + value: "cbffb57d084c70351a596e99e32a8b73e0c5eabf" +config_reads: + - file: "xemu.toml" + platforms: [rocknix] + source_ref: "ui/xemu.c:1314-1325, ui/xemu-settings.cc:65-71, 94-104, 158-207" + keys: + - key: "[sys.files] bootrom_path" + source_ref: "system/vl.c:2983-3016" + - key: "[sys.files] flashrom_path" + source_ref: "system/vl.c:3041-3054" + condition: "[general] show_welcome false" + - key: "[sys.files] eeprom_path" + source_ref: "system/vl.c:2909-2949, 3030-3039" + - key: "[sys.files] hdd_path" + source_ref: "system/vl.c:3060-3074" + - file: "xemu.toml" + platforms: [retrobat] + revision: "fc24584ce88f0915ad7f04775bb7712c2e3f49ee" + revision_from: "xemu.exe 0.8.136 in RetroBat's stable xemu.7z, 2026-11-10" + source_ref: "ui/xemu.c:1325, ui/xemu-settings.cc:51-63, 73-92, 94-104, 158-207" + keys: + - key: "[sys.files] flashrom_path" + source_ref: "system/vl.c:3041-3054" + condition: "[general] show_welcome false" + notes: | Full-system Xbox emulator built on QEMU. The libretro core is built from libretro's xemu-libretro tree on its libretro branch, which merges diff --git a/emulators/xroar.yml b/emulators/xroar.yml index 3aa57fb79..ccf2e4f28 100644 --- a/emulators/xroar.yml +++ b/emulators/xroar.yml @@ -29,6 +29,57 @@ standalone_pools: rompath: "" "-rompath": "" +config_reads: + - file: "xroar.conf" + source_ref: "src/xroar.c:882-909, 975-993" + keys: + - key: "rompath" + source_ref: "src/xroar.c:3266, src/romlist.c:125-140" + - key: + - "romlist dragon" + - "romlist d64_1" + - "romlist d64_2" + - "romlist d32" + - "romlist d200e_1" + - "romlist d200e_2" + - "romlist dragon64" + - "romlist dragon64_alt" + - "romlist dragon32" + - "romlist dragon200e" + - "romlist dragon200e_alt" + - "romlist dragon200e_charset" + - "romlist dragonpro_boot" + - "romlist dragonpro_basic" + - "romlist coco" + - "romlist coco_ext" + - "romlist deluxecoco" + - "romlist deluxecoco1" + - "romlist deluxecoco2" + - "romlist deluxecoco3" + - "romlist mx1600" + - "romlist mx1600ext" + - "romlist coco3" + - "romlist coco3p" + - "romlist glenside_ide" + - "romlist mc10" + - "romlist alice" + - "romlist dragondos" + - "romlist dosplus" + - "romlist superdos" + - "romlist cumana" + - "romlist dragondos_compat" + - "romlist rsdos" + - "romlist delta" + - "romlist rsdos_becker" + - "romlist mcx128" + - "romlist mcx128_azerty" + source_ref: "src/xroar.c:646-731, 3267, src/romlist.c:125-140" + - file: "command line" + source_ref: "src/xroar.c:1007" + keys: + - key: "-rompath" + source_ref: "src/xroar.c:3266, src/romlist.c:125-140" + notes: | XRoar by Ciaran Anscomb emulates Dragon 32/64, Tandy CoCo 1/2/3, MC-10, Alice, and clones (MX-1600, CP-400, Dragon 200-E, Deluxe CoCo). diff --git a/tests/test_launcher_evidence.py b/tests/test_launcher_evidence.py new file mode 100644 index 000000000..5f01c2929 --- /dev/null +++ b/tests/test_launcher_evidence.py @@ -0,0 +1,330 @@ +"""A launcher record resting on a config file binds only when its chain holds. + +The platform's code installs the file (installs), its launch code was read +for what replaces it (overrides), and the emulator's profile says where the +build the platform ships loads the file and reads each key (config_reads). +""" + +from __future__ import annotations + +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) + +import yaml # noqa: E402 + +from common import load_platform_config # noqa: E402 +from launchers import ( # noqa: E402 + bound_launchers, + config_evidence, + config_target, + handed_directories, + launcher_places, + launcher_unproven, +) +from profile_sync import _check_build_refs, shipped_build_holders # noqa: E402 +from scraper.retrobat_launchers import _choices_apart # noqa: E402 +from scraper.shell_launchers import ( # noqa: E402 + LauncherPatternMissing, + Script, + config_placement, + config_ref, + is_feature_ref, +) +from validate_schemas import ( # noqa: E402 + _config_reads_without_build, + _config_records_without_evidence, +) + +REPO_ROOT = Path(__file__).resolve().parents[1] + + +def record(**fields: object) -> dict: + """A ROCKNIX-like DuckStation record citing its shipped settings.ini.""" + base = { + "emulator": "duck", "build": "standalone", "kind": "scans_dir", "names": [], + "search_dir": "/storage/roms/bios", + "passes_to": "settings.ini [BIOS] SearchDirectory = /storage/roms/bios, [BIOS] PathPAL unset", + "installs": {"path": "/storage/.config/duck/settings.ini", "source_ref": "setup.sh:13"}, + "overrides": [], + "source_ref": "duck/config/settings.ini:4, duck/package.mk:33", + } + return {**base, **fields} + + +def profile(**fields: object) -> dict: + base = { + "emulator": "Duck", "type": "standalone", "cores": ["duck"], + "standalone_directories": ["[BIOS] SearchDirectory"], + "config_reads": [{ + "file": "settings.ini", "source_ref": "src/core.cpp:10", + "keys": [ + {"key": "[BIOS] SearchDirectory", "source_ref": "src/settings.cpp:20"}, + {"key": ["[BIOS] PathPAL", "[BIOS] PathNTSCU"], "source_ref": "src/bios.cpp:30"}, + ], + }], + } + return {**base, **fields} + + +class ConfigRefs(unittest.TestCase): + def test_config_option_and_feature_files_are_cited_apart(self) -> None: + ref = "a/settings.ini:4, 6, a/start.sh:3, b/MAME.opt:19, .emulationstation/es_features.cfg:9" + self.assertEqual( + config_ref(ref), + ["a/settings.ini:4, 6", "b/MAME.opt:19", ".emulationstation/es_features.cfg:9"], + ) + self.assertEqual([is_feature_ref(p) for p in config_ref(ref)], [False, True, True]) + + def test_passes_to_names_the_file_and_its_keys(self) -> None: + cases = { + "settings.ini [BIOS] SearchDirectory (-settings), [BIOS] PathNTSCU, PathPAL unset": + ("settings.ini", ["[BIOS] SearchDirectory", "[BIOS] PathNTSCU", "[BIOS] PathPAL"]), + "melonDS.toml [DS] BIOS9Path [DSi] NANDPath, first file whose name holds the region": + ("melonDS.toml", ["[DS] BIOS9Path", "[DSi] NANDPath"]), + "settings.ini [BIOS] PathNTSCJ = PathNTSCU = ": + ("settings.ini", ["[BIOS] PathNTSCJ", "[BIOS] PathNTSCU"]), + "AmigaA500-default.uae kickstart_rom_file, $(FILE_PATH) = /storage/k/": + ("AmigaA500-default.uae", ["kickstart_rom_file", "$(FILE_PATH)"]), + "command line -rompath bios/coco": ("command line", ["-rompath"]), + "core option pcsx2_bios": ("core options", ["pcsx2_bios"]), + } + for passes_to, expected in cases.items(): + with self.subTest(passes_to=passes_to): + self.assertEqual(config_target({"passes_to": passes_to}), expected) + core = {"build": "libretro", "kind": "core_option", "passes_to": "hatarib_tos = x/"} + self.assertEqual(config_target(core), ("core options", ["hatarib_tos"])) + + def test_a_record_naming_no_file_takes_the_config_it_installs(self) -> None: + romlist = record( + passes_to="romlist coco", + installs={"path": "$xroar_config", "source_ref": "prepare.sh:13"}, + source_ref="xroar/rd_config/xroar.conf:305, xroar/prepare.sh:13", + ) + self.assertEqual(config_target(romlist), ("xroar.conf", ["romlist coco"])) + + +class Evidence(unittest.TestCase): + profiles = {"duck": profile()} + + def test_a_record_resting_on_code_holds(self) -> None: + code = {"emulator": "duck", "kind": "fixed_name", "source_ref": "start.sh:3"} + self.assertTrue(config_evidence(code, {}).proven) + + def test_the_whole_chain_holds(self) -> None: + evidence = config_evidence(record(), self.profiles, "rocknix") + self.assertTrue(evidence.proven, evidence.missing) + self.assertEqual(evidence.readers, ("duck",)) + + def test_each_missing_link_is_named(self) -> None: + bare = {k: v for k, v in record().items() if k not in ("installs", "overrides")} + missing = config_evidence(bare, self.profiles).missing + self.assertEqual(len(missing), 2) + self.assertTrue(missing[0].startswith("installs")) + self.assertTrue(missing[1].startswith("overrides")) + + def test_a_profile_that_reads_no_such_file_proves_nothing(self) -> None: + missing = config_evidence(record(), {"duck": profile(config_reads=[])}).missing + self.assertEqual(missing, ("config_reads: an emulator that loads settings.ini",)) + + def test_a_key_the_build_does_not_read_is_named(self) -> None: + unread = record(passes_to="settings.ini [BIOS] PathNTSCJ") + self.assertEqual( + config_evidence(unread, self.profiles).missing, + ("config_reads: no read of [BIOS] PathNTSCJ",), + ) + + def test_a_handle_with_its_code_ref_reads_the_key(self) -> None: + handled = profile( + config_reads=[{"file": "settings.ini", "source_ref": "src/core.cpp:10", "keys": [ + {"key": "[BIOS] PathPAL", "source_ref": "src/bios.cpp:30"}]}], + standalone_directories=[ + {"handle": "[BIOS] SearchDirectory", "source_ref": "src/settings.cpp:20"} + ], + ) + self.assertTrue(config_evidence(record(), {"duck": handled}).proven) + + def test_an_option_list_is_never_the_source(self) -> None: + listed = record(source_ref="gen.cs:3, .emulationstation/es_features.cfg:9") + self.assertIn( + "an option list cited as source, not as choices_from", + config_evidence(listed, self.profiles).missing, + ) + + def test_an_option_choice_needs_the_emulator_side_too(self) -> None: + chosen = { + "emulator": "duck", "kind": "core_option", "passes_to": "settings.ini [BIOS] PathPAL = ", + "choices_from": "es_features.cfg:9", "source_ref": "gen.cs:3", + } + self.assertTrue(config_evidence(chosen, self.profiles).proven) + self.assertFalse(config_evidence(chosen, {"duck": profile(config_reads=[])}).proven) + + def test_reads_written_for_a_shipped_build_apply_to_its_platform_only(self) -> None: + built = profile( + source_commit="a" * 40, + pin_build=[{"platform": "rocknix", "value": "b" * 40}], + config_reads=[{**profile()["config_reads"][0], "platforms": ["rocknix"]}], + ) + self.assertTrue(config_evidence(record(), {"duck": built}, "rocknix").proven) + self.assertFalse(config_evidence(record(), {"duck": built}, "emuelec").proven) + + def test_reads_at_the_pin_skip_a_platform_shipping_another_build(self) -> None: + other = profile(source_commit="a" * 40, pin_build=[{"platform": "rocknix", "value": "b" * 40}]) + self.assertFalse(config_evidence(record(), {"duck": other}, "rocknix").proven) + self.assertTrue(config_evidence(record(), {"duck": other}, "emuelec").proven) + + +class Gate(unittest.TestCase): + def config(self, *records: dict) -> dict: + return {"_key": "rocknix", "base_destination": "bios", "launchers": list(records)} + + def test_an_unproven_record_hands_no_directory(self) -> None: + proven = handed_directories(self.config(record()), {"duck": profile()}) + self.assertEqual([h.folder for h in proven], [""]) + self.assertEqual(handed_directories(self.config(record()), {"duck": profile(config_reads=[])}), []) + + def test_an_unproven_record_places_no_file(self) -> None: + named = record( + kind="fixed_name", names=["tos.img"], passes_to="settings.ini [BIOS] PathPAL", + ) + self.assertEqual(len(launcher_places(self.config(named), {"duck": profile()})), 1) + self.assertEqual(launcher_places(self.config(named), {"duck": profile(config_reads=[])}), []) + + def test_the_unproven_are_reported_with_what_they_lack(self) -> None: + config = self.config({"emulator": "x", "kind": "links", "source_ref": "a.sh:1"}, record()) + self.assertEqual(launcher_unproven({"rocknix": config}, {"duck": profile()}), []) + (row,) = launcher_unproven({"rocknix": config}, {"duck": profile(config_reads=[])}) + self.assertEqual((row["platform"], row["index"], row["emulator"]), ("rocknix", 1, "duck")) + self.assertEqual(row["config"], ["duck/config/settings.ini:4"]) + self.assertEqual(len(bound_launchers(config, {"duck": profile(config_reads=[])})), 1) + + +class StartScripts(unittest.TestCase): + SCRIPT = ( + 'CONF_DIR="/storage/.config/duck"\n' + 'CONF_FILE="${CONF_DIR}/settings.ini"\n' + '[ ! -d "${CONF_DIR}" ] && cp -r /usr/config/duck /storage/.config\n' + 'if [ ! -f "${CONF_FILE}" ]; then\n' + ' cp /usr/config/duck/settings.ini ${CONF_FILE}\n' + 'fi\n' + "sed -i '/^AspectRatio/c\\\\AspectRatio = 4:3' ${CONF_FILE}\n" + "sed -i '/^PathPAL/c\\\\PathPAL = x.bin' ${CONF_FILE}\n" + 'cp /usr/config/duck/other.ini ${CONF_DIR}/ >/dev/null 2>&1\n' + ) + + def test_a_guarded_copy_keeps_the_file_and_a_key_rewrite_is_an_override(self) -> None: + placement = config_placement( + Script("start.sh", self.SCRIPT), "/storage/.config/duck/settings.ini", ["[BIOS] PathPAL"] + ) + self.assertEqual(placement.install, {3, 5}) + self.assertTrue(placement.kept_if_present) + self.assertEqual(placement.overrides, {8}) + + def test_a_copy_at_each_launch_replaces_the_file(self) -> None: + script = Script("start.sh", 'cp /usr/config/duck/settings.ini /storage/.config/duck/settings.ini\n') + self.assertFalse(config_placement(script, "/storage/.config/duck/settings.ini").kept_if_present) + + def test_a_script_installing_nothing_raises(self) -> None: + with self.assertRaises(LauncherPatternMissing): + config_placement(Script("start.sh", "echo hi\n"), "/storage/.config/duck/settings.ini") + + def test_retrobat_moves_the_option_list_to_choices_from(self) -> None: + moved = _choices_apart({ + "emulator": "x", "passes_to": "p", + "source_ref": "Gen.cs:3-4, .emulationstation/es_features.cfg:9-12", + }) + self.assertEqual(moved["source_ref"], "Gen.cs:3-4") + self.assertEqual(moved["choices_from"], ".emulationstation/es_features.cfg:9-12") + self.assertEqual(list(moved)[-1], "source_ref") + + +class Invariants(unittest.TestCase): + def test_a_config_record_without_its_platform_links_is_an_error(self) -> None: + path = REPO_ROOT / "platforms" / "x.yml" + bare = {k: v for k, v in record().items() if k not in ("installs", "overrides")} + listed = record(source_ref="gen.cs:3, es_features.yml:9") + errors = _config_records_without_evidence(path, {"launchers": [record(), bare, listed]}) + self.assertEqual(len(errors), 2) + self.assertIn("without installs and overrides", errors[0]) + self.assertIn("choices_from holds it", errors[1]) + + def test_reads_naming_a_platform_need_its_build(self) -> None: + path = REPO_ROOT / "emulators" / "x.yml" + reads = [{"file": "a.ini", "platforms": ["rocknix"], "source_ref": "a:1", "keys": []}] + self.assertEqual(len(_config_reads_without_build(path, {"config_reads": reads})), 1) + self.assertEqual(_config_reads_without_build( + path, {"config_reads": reads, "pin_build": [{"platform": "rocknix"}]} + ), []) + pinned = [{**reads[0], "revision": "c" * 40, "revision_from": "the exe"}] + self.assertEqual(_config_reads_without_build(path, {"config_reads": pinned}), []) + + +class BuildRefs(unittest.TestCase): + def test_reads_for_a_shipped_build_are_read_at_that_build(self) -> None: + document = { + "pin_build": [{"platform": "rocknix", "value": "b" * 40}], + "config_reads": [{ + "file": "a.ini", "platforms": ["rocknix"], "source_ref": "src/a.c:2", + "keys": [{"key": "k", "source_ref": "src/a.c:9, ROCKNIX x.patch:1"}], + }], + } + held = shipped_build_holders(document) + self.assertEqual(len(held), 2) + + class View: + repo, field = "repo", "source" + + class Citation: + def __init__(self, ref: str, field: str) -> None: + self.ref, self.field = ref, field + + asked: list[tuple[str, str]] = [] + + def fetch(_repo: str, revision: str, path: str) -> list[str] | None: + asked.append((revision, path)) + return ["line"] * 5 if path == "src/a.c" else None + + rows = _check_build_refs( + document, + [(Citation("src/a.c:2", "c"), document["config_reads"][0]), + (Citation("src/a.c:9, ROCKNIX x.patch:1", "k"), document["config_reads"][0])], + [View()], fetch, + ) + self.assertEqual([r["status"] for r in rows], ["BUILD_OK", "BUILD_GONE"]) + self.assertTrue(all(revision == "b" * 40 for revision, _ in asked)) + self.assertNotIn(("b" * 40, "ROCKNIX x.patch"), asked) + + +class RepositoryRecords(unittest.TestCase): + """The placement chains the audit of 2026-11-10 found moving files.""" + + def test_the_chains_that_move_placements_still_bind(self) -> None: + platforms = REPO_ROOT / "platforms" + cases = ( + ("rocknix", "armsx2", "/storage/roms/bios/armsx2"), + ("retrodeck", "gzdoom", "$roms_path/doom"), + ("retrodeck", "xroar", "$roms_path/coco"), + ("retrobat", "xroar", "bios/coco"), + ) + for platform, emulator, directory in cases: + with self.subTest(platform=platform, emulator=emulator): + handed = handed_directories(load_platform_config(platform, str(platforms))) + self.assertIn( + directory, {h.directory for h in handed if h.emulator == emulator} + ) + + def test_every_config_record_carries_its_platform_links(self) -> None: + for path in sorted((REPO_ROOT / "platforms").glob("*.yml")): + if path.name.startswith("_"): + continue + with self.subTest(platform=path.stem): + document = yaml.safe_load(path.read_text(encoding="utf-8")) + self.assertEqual(_config_records_without_evidence(path, document), []) + + +if __name__ == "__main__": + unittest.main()