From f7d57dd91df15d41be3039556d33e973df9c7e82 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 20 Oct 2026 06:11:10 +0200 Subject: [PATCH] feat: a platform package can differ from upstream bundled describes the emulator's own release, and a platform package can differ both ways: Batocera's hcl package installs the binary alone and sends users to fetch the data, while its blueMSX core package copies the machines and databases beside the core. not_bundled_on and installed_on name those platforms, and the builder reads them through one rule. --- schemas/emulator.schema.json | 20 ++++++++++++++++++++ scripts/common.py | 3 +++ scripts/validation.py | 21 +++++++++++++++++++-- scripts/verify.py | 3 ++- tests/test_agnostic_scope.py | 4 +++- tests/test_audit_regressions.py | 30 ++++++++++++++++++++++++++++++ 6 files changed, 77 insertions(+), 4 deletions(-) diff --git a/schemas/emulator.schema.json b/schemas/emulator.schema.json index 73c42f7f..8d073676 100644 --- a/schemas/emulator.schema.json +++ b/schemas/emulator.schema.json @@ -276,6 +276,16 @@ "type": "boolean", "description": "Installed with the emulator for the build mode names, so packs leave the tree out." }, + "installed_on": { + "type": "array", + "items": {"type": "string"}, + "description": "Platforms (registry keys) whose own package installs this beside the emulator, so their packs leave it out." + }, + "not_bundled_on": { + "type": "array", + "items": {"type": "string"}, + "description": "Platforms (registry keys) whose package leaves out a file the emulator's release bundles, so their packs bring it." + }, "source_ref": { "type": "string" } @@ -450,6 +460,16 @@ "type": "boolean", "description": "Installed with the emulator for the build mode names (all builds when mode is absent): already on the user's machine, so packs and install manifests leave it out." }, + "installed_on": { + "type": "array", + "items": {"type": "string"}, + "description": "Platforms (registry keys) whose own package installs this beside the emulator, so their packs leave it out." + }, + "not_bundled_on": { + "type": "array", + "items": {"type": "string"}, + "description": "Platforms (registry keys) whose package leaves out a file the emulator's release bundles, so their packs bring it." + }, "config_key": { "type": "string" }, diff --git a/scripts/common.py b/scripts/common.py index 510faea5..2534e46d 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -353,6 +353,9 @@ def load_platform_config(platform_name: str, platforms_dir: str = "platforms") - for sys_id, system in config.get("systems", {}).items() } + # The registry key, which per-platform profile fields (installed_on, + # not_bundled_on) name: the display name differs (MiSTer FPGA). + config["_key"] = platform_name _platform_config_cache[cache_key] = config return config diff --git a/scripts/validation.py b/scripts/validation.py index 8b5c27db..1b7b2688 100644 --- a/scripts/validation.py +++ b/scripts/validation.py @@ -525,7 +525,21 @@ def filter_files_by_mode(files: list[dict], standalone: bool) -> list[dict]: return result -def deliverable(entries: list[dict], standalone: bool) -> list[dict]: +def installed_with_emulator(entry: dict, platform: str = "") -> bool: + """Whether the emulator as this platform installs it already brings the entry. + + `bundled` describes the emulator's own release. A platform package can + differ both ways: Batocera's hcl package installs the binary alone and + sends users to fetch the data (`not_bundled_on`), while its blueMSX core + package copies the machines and databases beside the core + (`installed_on`). + """ + if platform and platform in (entry.get("installed_on") or []): + return True + return bool(entry.get("bundled")) and platform not in (entry.get("not_bundled_on") or []) + + +def deliverable(entries: list[dict], standalone: bool, platform: str = "") -> list[dict]: """The entries of one build a pack has to bring. A file or tree installed with the emulator is already on the user's @@ -533,7 +547,10 @@ def deliverable(entries: list[dict], standalone: bool) -> list[dict]: `mode` like any entry, so PPSSPP's assets, shipped by the standalone release and not by the libretro core, still reach a RetroArch pack. """ - return [e for e in filter_files_by_mode(entries, standalone) if not e.get("bundled")] + return [ + e for e in filter_files_by_mode(entries, standalone) + if not installed_with_emulator(e, platform) + ] def find_validated_variant( diff --git a/scripts/verify.py b/scripts/verify.py index b9b534ac..6f737384 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -96,6 +96,7 @@ from validation import ( check_file_validation, deliverable, filter_files_by_mode, + installed_with_emulator, outside_gap_scope, ) @@ -650,7 +651,7 @@ def find_undeclared_files( "path": dest, "required": f.get("required", False), "hle_fallback": f.get("hle_fallback", False), - "bundled": f.get("bundled", False), + "bundled": installed_with_emulator(f, config.get("_key", "")), "category": f.get("category", "bios"), "in_repo": in_repo, "note": f.get("note", ""), diff --git a/tests/test_agnostic_scope.py b/tests/test_agnostic_scope.py index a99635be..fac69e8e 100644 --- a/tests/test_agnostic_scope.py +++ b/tests/test_agnostic_scope.py @@ -80,7 +80,9 @@ class BundledHashIsProof(unittest.TestCase): wrong = [] for name, profile in sorted(load_emulator_profiles("emulators", skip_aliases=False).items()): for entry in profile.get("files") or []: - if not entry.get("bundled") or entry.get("unsourceable"): + # A bundled entry is packed only where a platform's package + # leaves it out: there the copy has to be the declared bytes. + if not entry.get("not_bundled_on") or entry.get("unsourceable"): continue if not any(entry.get(k) for k in ("sha1", "md5", "sha256", "crc32")): continue diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index 553ad5b6..f99c7076 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -4579,6 +4579,36 @@ class BundledFilesAreNotToSource(unittest.TestCase): ["ppsspp-assets"]) self.assertEqual(deliverable(entries, True), []) + def test_a_platform_package_can_differ_both_ways(self): + """Batocera's hcl package installs the binary alone, its blueMSX core + package copies the machines beside the core.""" + sys.path.insert(0, str(ROOT / "scripts")) + from validation import installed_with_emulator + + hcl = {"name": "data.pak", "bundled": True, "not_bundled_on": ["batocera"]} + machines = {"name": "Machines/MSX/config.ini", "installed_on": ["batocera", "retropie"]} + self.assertTrue(installed_with_emulator(hcl, "retrobat")) + self.assertFalse(installed_with_emulator(hcl, "batocera")) + self.assertTrue(installed_with_emulator(machines, "retropie")) + self.assertFalse(installed_with_emulator(machines, "retroarch")) + self.assertTrue(installed_with_emulator(hcl)) + + def test_every_platform_named_is_registered(self): + sys.path.insert(0, str(ROOT / "scripts")) + from common import list_registered_platforms, load_emulator_profiles + + known = set(list_registered_platforms(str(ROOT / "platforms"), include_archived=True)) + unknown = sorted({ + (name, platform) + for name, profile in load_emulator_profiles(str(ROOT / "emulators")).items() + for entry in (profile.get("files") or []) + (profile.get("data_directories") or []) + if isinstance(entry, dict) + for field in ("installed_on", "not_bundled_on") + for platform in entry.get(field) or [] + if platform not in known + }) + self.assertEqual(unknown, []) + def test_the_pack_builder_skips_it(self): source = (ROOT / "scripts" / "packextras.py").read_text(encoding="utf-8") self.assertIn('if u.get("bundled"):', source)