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