From 3d2629f6529401698e98f4005cece8ee5f110047 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 10 Nov 2026 02:08:11 +0100 Subject: [PATCH] fix: keep the claims of every core on a file The first profile reading a requirement hosts the others' claims, and two ways of dropping the host dropped them too. DraStic brings its own usrcheat.dat, and DSPerate, which reads the same file, lost it wherever DraStic runs. A missing file was recorded for its first reader alone: NAPALM.GRP named Raze, Data.rsdk Sonic Mania. A list built for a board without that reader carried what the full list left out, and install.py --target disagreed with the builder on ROCKNIX, KNULLI, Batocera and RetroPie boards. --- scripts/generate_pack.py | 13 +++++++--- scripts/packextras.py | 11 +++++---- scripts/verify.py | 31 +++++++++++++++++++++++- tests/test_audit_regressions.py | 42 +++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 8 deletions(-) diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index fd3a39032..66b507e1d 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -4694,11 +4694,18 @@ def _record_absent( ): continue full_dest = extra_full_destination(fe, extras_pfx) - if not full_dest: - continue - if full_dest in seen_destinations or full_dest in omitted_by_destination: + if not full_dest or full_dest in seen_destinations: continue source_emu = fe.get("source_profile") or fe.get("source_emulator", "") + held = omitted_by_destination.get(full_dest) + if held is not None: + # Another core reads the same missing file there (Raze and + # EDuke32 both open NAPALM.GRP): one omission names them all. + claimed = held.get("cores") + if (held.get("reason") == "not_found" and claimed and source_emu + and source_emu not in claimed): + held["cores"] = sorted([*claimed, source_emu]) + continue systems = _extra_system_ids(fe) record_omission( full_dest, fe, systems[0] if systems else "", "not_found", diff --git a/scripts/packextras.py b/scripts/packextras.py index 13d7395d2..056eaedd0 100644 --- a/scripts/packextras.py +++ b/scripts/packextras.py @@ -1263,11 +1263,14 @@ def _collect_emulator_extras( placements = placement.places(u, dest) save_root = u.get("root") == "save" if not u["in_repo"]: + # Each core reading it is named on the omission: a list built + # without the first reader still has to say the file is missing. for placed in placements: - missing = _report_extra(u, name, placed[0]) - if save_root: - _save_fields(missing, placed[1], placement) - absent.append(missing) + for claim in (u, *u.get("other_claims", [])): + missing = _report_extra(claim, name, placed[0]) + if save_root: + _save_fields(missing, placed[1], placement) + absent.append(missing) continue if not placements: if unplaceable is not None: diff --git a/scripts/verify.py b/scripts/verify.py index beaeb9b6b..88537fa58 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -824,7 +824,36 @@ def find_undeclared_files( for entry in sorted(archive_entries.values(), key=lambda e: e["name"]): undeclared.append(entry) - return _settle_core_sets(undeclared, set_records, settled, profiles) + return _split_bundled_claims( + _settle_core_sets(undeclared, set_records, settled, profiles) + ) + + +def _split_bundled_claims(undeclared: list[dict]) -> list[dict]: + """A file one emulator installs itself, another reads from the pack. + + The first profile reading a requirement hosts the claims of the others, + and a host installed with its emulator is left out of the pack with all + of them: DraStic brings its own usrcheat.dat, and ROCKNIX's DSPerate, + which reads the same file, lost it wherever DraStic runs too. Built for + a board without DraStic, the pack carried it. + """ + out: list[dict] = [] + for entry in undeclared: + claims = entry.get("other_claims") or [] + rest = [c for c in claims if not c.get("bundled")] + if not entry.get("bundled") or not rest: + out.append(entry) + continue + own = [c for c in claims if c.get("bundled")] + kept = {k: v for k, v in entry.items() if k != "other_claims"} + if own: + kept["other_claims"] = own + promoted = dict(rest[0]) + if rest[1:]: + promoted["other_claims"] = rest[1:] + out += [kept, promoted] + return out def _settle_core_sets( diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index f273e85c2..aa6cbf37a 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -7805,5 +7805,47 @@ class AutoFetchIssueBodyReadsAsWritten(unittest.TestCase): self.assertIn("posts nothing", auto_fetch.__doc__) + +class EveryReaderOfACoreFileIsNamed(unittest.TestCase): + """A requirement two cores read is reported once, the first profile + hosting the others' claims. Whatever drops the host must not drop the + claims with it.""" + + def test_a_host_its_emulator_installs_leaves_the_others_in_the_pack(self): + from verify import _split_bundled_claims + + dsperate = {"profile": "dsperate", "name": "usrcheat.dat", "in_repo": True} + drastic = {"profile": "drastic", "name": "usrcheat.dat", "bundled": True, + "in_repo": True, "other_claims": [dsperate]} + out = _split_bundled_claims([drastic]) + self.assertEqual([(u["profile"], u.get("other_claims")) for u in out], + [("drastic", None), ("dsperate", None)]) + alone = {"profile": "drastic", "bundled": True, "other_claims": [ + {"profile": "drastic-legacy", "bundled": True}]} + self.assertEqual(_split_bundled_claims([alone]), [alone]) + + def test_a_missing_file_names_every_core_reading_it(self): + from generate_pack import _record_absent + + omitted: dict[str, dict] = {} + + def record(full_dest, entry, system, reason, cores): + omitted[full_dest] = {"dest": full_dest, "reason": reason, "cores": cores} + + absent = [ + {"name": "NAPALM.GRP", "destination": "NAPALM.GRP", "source_profile": profile, + "source_system": system} + for profile, system in (("raze", "nam"), ("eduke32", "napalm")) + ] + platform = {"dest": "SHARED.BIN", "reason": "not_found", "cores": None} + omitted["SHARED.BIN"] = platform + absent.append({"name": "SHARED.BIN", "destination": "SHARED.BIN", + "source_profile": "raze"}) + _record_absent(absent, {}, "", set(), False, set(), omitted, record) + self.assertEqual(omitted["NAPALM.GRP"]["cores"], ["eduke32", "raze"]) + # A platform's own omission stays the platform's. + self.assertIsNone(omitted["SHARED.BIN"]["cores"]) + + if __name__ == "__main__": unittest.main()