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):