From ddc74328258c7b7eee2b156e6dba6d2ddaef22dd Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 20 Oct 2026 06:04:10 +0200 Subject: [PATCH] feat: packs leave out what the emulator installs A file or tree installed with the emulator is already on the user's machine, so packs, install manifests and verify's pack count leave it out. bundled is scoped by mode like any entry: Dolphin's Sys and PPSSPP's assets ship with the standalone releases, not with the libretro cores, so they now follow the libretro build only and still reach RetroArch packs. PrimeHack, standalone only, ships its own Sys. --- emulators/dolphin.yml | 1 + emulators/ishiiruka.yml | 1 + emulators/ppsspp.yml | 1 + emulators/primehack.yml | 1 + schemas/emulator.schema.json | 7 ++++++- scripts/generate_pack.py | 5 +++-- scripts/packextras.py | 5 +++++ scripts/validation.py | 11 +++++++++++ scripts/verify.py | 13 ++++++++++--- tests/test_audit_regressions.py | 30 ++++++++++++++++++++++++++++++ 10 files changed, 69 insertions(+), 6 deletions(-) diff --git a/emulators/dolphin.yml b/emulators/dolphin.yml index e129821c..b5843e25 100644 --- a/emulators/dolphin.yml +++ b/emulators/dolphin.yml @@ -27,6 +27,7 @@ pack_structure: data_directories: - ref: dolphin-sys + mode: libretro destination: "Sys" source_ref: "Source/Core/Common/CommonPaths.h:128-141" diff --git a/emulators/ishiiruka.yml b/emulators/ishiiruka.yml index b5ab5533..c8da1a89 100644 --- a/emulators/ishiiruka.yml +++ b/emulators/ishiiruka.yml @@ -25,6 +25,7 @@ pack_structure: data_directories: - ref: dolphin-sys + mode: libretro destination: "Sys" source_ref: "Source/Core/Common/CommonPaths.h:130-131" diff --git a/emulators/ppsspp.yml b/emulators/ppsspp.yml index 77337fc0..12a2b9de 100644 --- a/emulators/ppsspp.yml +++ b/emulators/ppsspp.yml @@ -16,6 +16,7 @@ pack_structure: data_directories: - ref: ppsspp-assets + mode: libretro destination: "" source_ref: "libretro/libretro.cpp:537,549" diff --git a/emulators/primehack.yml b/emulators/primehack.yml index 2bc4c87e..2eb43bf5 100644 --- a/emulators/primehack.yml +++ b/emulators/primehack.yml @@ -25,6 +25,7 @@ pack_structure: data_directories: - ref: dolphin-sys + bundled: true destination: "Sys" source_ref: "Source/Core/Common/CommonPaths.h:128-141" diff --git a/schemas/emulator.schema.json b/schemas/emulator.schema.json index cfdfdca2..73c42f7f 100644 --- a/schemas/emulator.schema.json +++ b/schemas/emulator.schema.json @@ -272,6 +272,10 @@ "both" ] }, + "bundled": { + "type": "boolean", + "description": "Installed with the emulator for the build mode names, so packs leave the tree out." + }, "source_ref": { "type": "string" } @@ -443,7 +447,8 @@ "type": "boolean" }, "bundled": { - "type": "boolean" + "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." }, "config_key": { "type": "string" diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 7f615122..71ed0b29 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -91,6 +91,7 @@ from validation import ( validated_choice, agnostic_substitute, _build_validation_index, + deliverable, filter_files_by_mode, ) @@ -1449,7 +1450,7 @@ def generate_emulator_pack( pack_structure = profile.get("pack_structure") files = expand_agnostic_entries( expand_directory_entries( - filter_files_by_mode(profile.get("files", []), standalone), + deliverable(profile.get("files", []), standalone), db, standalone, ), @@ -1675,7 +1676,7 @@ def generate_emulator_pack( # profile declares wins over the copy a tree bundles at the same # path (Dolphin's real dsp_rom.bin over Sys/GC's free one). # A tree only one build reads follows that build, like a file. - for dd in filter_files_by_mode( + for dd in deliverable( profile.get("data_directories", []), standalone ): ref_key = dd.get("ref", "") diff --git a/scripts/packextras.py b/scripts/packextras.py index b8b5c331..aaeb42b1 100644 --- a/scripts/packextras.py +++ b/scripts/packextras.py @@ -875,6 +875,11 @@ def _collect_emulator_extras( for destination in seen: taken[destination] = baseline_claims.setdefault(destination, {}) for u in undeclared: + if u.get("bundled"): + # Installed with the emulator for this build: already on the + # user's machine. The mode a file is bundled for was settled + # upstream, an entry scoped to another build never gets here. + continue if not u["in_repo"] and absent is None: continue # For archive entries, use the archive name for resolution diff --git a/scripts/validation.py b/scripts/validation.py index 56145e55..8b5c27db 100644 --- a/scripts/validation.py +++ b/scripts/validation.py @@ -525,6 +525,17 @@ def filter_files_by_mode(files: list[dict], standalone: bool) -> list[dict]: return result +def deliverable(entries: list[dict], standalone: bool) -> 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 + machine: shipping it again only weighs the pack. `bundled` is scoped by + `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")] + + def find_validated_variant( file_entry: dict, db: dict, diff --git a/scripts/verify.py b/scripts/verify.py index 7a124ab0..b9b534ac 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -94,6 +94,7 @@ from validation import ( _parse_validation, build_ground_truth, check_file_validation, + deliverable, filter_files_by_mode, outside_gap_scope, ) @@ -774,6 +775,12 @@ def _mark_unplaced(config: dict, undeclared: list[dict], profiles: dict) -> None unplaced = unplaceable_extras(config, undeclared, profiles) for u in undeclared: + if u.get("bundled"): + # Installed with the emulator, so already on the user's machine: + # the pack leaves it out, held or not. + u["in_pack"] = False + u["omitted"] = "installed with its emulator" + continue reason = unplaced.get( (u.get("emulator", ""), u.get("name", ""), u.get("path") or "") ) @@ -1713,7 +1720,7 @@ def verify_emulator( for emu_name, profile in sorted(selected): files = expand_agnostic_entries( expand_directory_entries( - filter_files_by_mode(profile.get("files", []), standalone), + deliverable(profile.get("files", []), standalone), db, standalone, ), @@ -1733,7 +1740,7 @@ def verify_emulator( ] # Check data directories (only notice if not cached) - for dd in filter_files_by_mode( + for dd in deliverable( profile.get("data_directories", []), standalone ): ref = dd.get("ref", "") @@ -2010,7 +2017,7 @@ def verify_emulator( file_severity[dest] = sev # The profile's data trees follow its files in the pack. - for dd in filter_files_by_mode(profile.get("data_directories", []), standalone): + for dd in deliverable(profile.get("data_directories", []), standalone): cache = (data_registry or {}).get(dd.get("ref", ""), {}).get("local_cache", "") if not cache or not os.path.isdir(cache): continue diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index a3a8d217..b992864f 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -4553,6 +4553,36 @@ class BundledFilesAreNotToSource(unittest.TestCase): }}) self.assertEqual([row["status"] for row in rows], ["bundled"]) + def test_a_held_bundled_file_is_not_packed(self): + """A file installed with the emulator is already on the user's + machine, so the pack leaves it out even when the collection holds it.""" + sys.path.insert(0, str(ROOT / "scripts")) + import verify + + entry = {"name": "n64.psh", "required": True, "in_repo": True, + "emulator": "daedalusx64", "category": "bios", "bundled": True} + with mock.patch("packextras.unplaceable_extras", return_value={}): + verify._mark_unplaced({}, [entry], {}) + self.assertFalse(entry["in_pack"]) + self.assertIn("installed with its emulator", entry["omitted"]) + + def test_a_build_scope_keeps_it_for_the_other_build(self): + """PPSSPP's standalone release ships assets/, its libretro core does + not: bundled for one build, the entry still reaches a core's pack.""" + sys.path.insert(0, str(ROOT / "scripts")) + from validation import deliverable + + entries = [{"ref": "ppsspp-assets", "mode": "libretro"}, + {"name": "a.bin", "bundled": True, "mode": "standalone"}, + {"name": "b.bin", "bundled": True}] + self.assertEqual([e.get("ref") or e["name"] for e in deliverable(entries, False)], + ["ppsspp-assets"]) + self.assertEqual(deliverable(entries, True), []) + + 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) + class AdlerValuesReadLikeTheDownloads(unittest.TestCase):