diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 7238dd88..9086ea29 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -1444,11 +1444,6 @@ def generate_emulator_pack( archive_name, archive_prefix, pack_structure, standalone ) - if archive_dest.lower() in seen_lower: - continue - if _has_path_conflict(archive_dest, seen_destinations, seen_parents): - continue - own_entry = archive_entry(archive_name, files) if required_only and _archive_is_optional(own_entry, archive_name, files): continue @@ -1460,6 +1455,18 @@ def generate_emulator_pack( data_dir_registry=data_registry, offline=offline, ) + taken = archive_dest.lower() in seen_lower + if taken or _has_path_conflict(archive_dest, seen_destinations, seen_parents): + # Another profile's file holds this path. The same archive + # asked for twice is served; anything else leaves this + # profile without its archive, which used to pass in + # silence: a neocdz.zip lacking 000-lo.lo shipped as the + # one the second core needs. + if not (taken and local_path and _same_bytes( + local_path, packed_at.get(archive_dest.lower()) + )): + report_missing(archive_name, archive_dest) + continue if local_path and status not in ("not_found",): if local_path.endswith(".zip"): _add_zip_to_pack(local_path, archive_dest, zf, own_entry) diff --git a/scripts/verify.py b/scripts/verify.py index 46460037..dd6683f9 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -1688,7 +1688,20 @@ def verify_emulator( archive, profile.get("archive_prefix", ""), profile.get("pack_structure"), standalone, ) - if local_path: + block = placement_block( + dest, placed_files, set(placed), placed_parents, True + ) + if block and local_path and result["status"] != Status.MISSING: + other_path, owner, other_dest = placed.get(dest.lower(), (None, "", "")) + if block == "path_conflict" or content_id(local_path) != content_id(other_path): + # The pack serves the other profile's file here: this + # archive is not in it, whatever this profile resolves. + result["status"] = Status.MISSING + result["reason"] = ( + "a packed file occupies its directory" if block == "path_conflict" + else f"the pack holds {owner}'s {other_dest} at this path" + ) + elif local_path: occupy(dest, local_path, emu_name) dest_to_name[dest] = archive cur = result["status"] diff --git a/tests/test_path_conflict_status.py b/tests/test_path_conflict_status.py index af3ae883..2390994f 100644 --- a/tests/test_path_conflict_status.py +++ b/tests/test_path_conflict_status.py @@ -278,5 +278,65 @@ class AlternativesAgreeWithThePack(unittest.TestCase): self.assertEqual(result["status_counts"].get(Status.UNTESTED), 1) + +class AMaskedArchiveIsMissing(unittest.TestCase): + """Profile a ships a loose pack.zip; profile b needs pack.zip as an + archive holding a member a's copy lacks. The pack skipped b's archive + in silence and verify counted it OK on b's own copy.""" + + def setUp(self): + import zipfile + + self._tmp = tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") + self.root = Path(self._tmp.name) + self.emulators = self.root / "emulators" + files = {} + for folder, members in (("A", {"x.bin": b"x"}), ("B", {"x.bin": b"x", "y.bin": b"y"})): + target = self.root / "bios" / folder / "pack.zip" + target.parent.mkdir(parents=True) + with zipfile.ZipFile(target, "w") as archive: + for name, data in members.items(): + archive.writestr(name, data) + payload = target.read_bytes() + sha1 = hashlib.sha1(payload).hexdigest() + files[sha1] = {"path": str(target), "name": "pack.zip", "size": len(payload), + "sha1": sha1, "md5": hashlib.md5(payload).hexdigest(), + "sha256": hashlib.sha256(payload).hexdigest(), "crc32": "0"} + self.sha1_b = sha1 + self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})} + self.emulators.mkdir() + a_sha1 = next(k for k in files if k != sha1) + (self.emulators / "a.yml").write_text(yaml.dump({ + "emulator": "A", "type": "libretro", "cores": ["a"], "systems": ["x"], + "files": [{"name": "pack.zip", "required": True, "sha1": a_sha1}], + })) + (self.emulators / "b.yml").write_text(yaml.dump({ + "emulator": "B", "type": "libretro", "cores": ["b"], "systems": ["x"], + "files": [{"name": "y.bin", "archive": "pack.zip", "required": True, + "crc32": f"{__import__('zlib').crc32(b'y'):08x}"}], + })) + common._emulator_profiles_cache.clear() + + def tearDown(self): + common._emulator_profiles_cache.clear() + self._tmp.cleanup() + + def test_both_report_the_archive_missing(self): + from verify import Status, verify_emulator + + out = self.root / "dist" + out.mkdir() + report = io.StringIO() + with contextlib.redirect_stdout(report): + builder.generate_emulator_pack( + ["a", "b"], str(self.emulators), self.db, str(self.root / "bios"), + str(out), offline=True, + ) + self.assertIn("pack.zip", report.getvalue().split("MISSING", 1)[-1]) + result = verify_emulator(["a", "b"], str(self.emulators), self.db) + statuses = [d["status"] for d in result["details"] if d["name"] == "pack.zip"] + self.assertIn(Status.MISSING, statuses) + + if __name__ == "__main__": unittest.main()