fix: list in the manifest what the pack ships

This commit is contained in:
Abdessamad Derraz committed 2026-10-04 21:08:00 +02:00
1 parent 243e95725b
commit 79c94468a0
2 files changed
+111 -18

No files matched your search

+40 -18
View File
@@ -293,6 +293,24 @@ def download_external(file_entry: dict, dest_path: str) -> bool:
PACK_DOCUMENTS = ("README.txt", "manifest.json")
def _inner_rom_check(file_entry: dict, local_path: str) -> str:
"""How an archive answers an entry that pins a ROM inside it.
Batocera, RetroBat and ROCKNIX hash a member of the ZIP, never the ZIP:
the resolver hands back the archive as a mismatch and this decides it.
Returns check_inside_zip's answer for the first accepted MD5 that
matches, else for the last one tried. The pack and the install manifest
both read it, so a file one of them ships is a file the other lists.
"""
declared = [m.strip() for m in file_entry.get("md5", "").split(",") if m.strip()]
result = "not_in_zip"
for candidate in declared or [""]:
result = check_inside_zip(local_path, file_entry["zipped_file"], candidate)
if result == "ok":
break
return result
def _data_directory_members(
pack_systems: dict,
data_registry: dict | None,
@@ -891,21 +909,8 @@ def generate_pack(
):
zf_name = file_entry.get("zipped_file")
if zf_name and local_path:
inner_md5_raw = file_entry.get("md5", "")
inner_md5_list = (
[m.strip() for m in inner_md5_raw.split(",") if m.strip()]
if inner_md5_raw
else [""]
)
zip_ok = False
last_result = "not_in_zip"
for md5_candidate in inner_md5_list:
last_result = check_inside_zip(
local_path, zf_name, md5_candidate
)
if last_result == "ok":
zip_ok = True
break
last_result = _inner_rom_check(file_entry, local_path)
zip_ok = last_result == "ok"
if zip_ok:
status = "zip_exact"
file_status.setdefault(dedup_key, "ok")
@@ -2952,6 +2957,11 @@ def generate_manifest(
manifest_files: list[dict] = []
omitted_by_destination: dict[str, dict] = {}
total_size = 0
# Sizes of the files the pack carries and the installer cannot fetch: the
# collection does not index them, a data directory cache answered.
pack_only_sizes: list[int] = []
if data_registry is None:
data_registry = load_data_dir_registry(platforms_dir)
def manifest_destination(full_destination: str) -> str:
if base_dest and full_destination.startswith(f"{base_dest}/"):
@@ -3044,8 +3054,16 @@ def generate_manifest(
db,
bios_dir,
zip_contents,
data_dir_registry=data_registry,
offline=offline,
)
if (
status == "hash_mismatch"
and local_path
and file_entry.get("zipped_file")
and _inner_rom_check(file_entry, local_path) == "ok"
):
status = "zip_exact"
# An existence platform never reads the bytes, so a declared
# hash the local dump contradicts is not a reason to withhold
# the file. Hash platforms would reject it, so they omit it.
@@ -3080,6 +3098,12 @@ def generate_manifest(
# entry.
if not repo_path and not is_release_asset:
record_omission(full_dest, file_entry, sys_id, "not_found", None)
if file_size and full_dest not in seen_destinations:
pack_only_sizes.append(file_size)
seen_destinations.add(dedup_key)
_register_path(dedup_key, seen_destinations, seen_parents)
if case_insensitive:
seen_lower.add(dedup_key.lower())
continue
entry: dict = {
@@ -3129,9 +3153,7 @@ def generate_manifest(
# Phase 3: data directories. The installer does not fetch them, so they
# stay out of the file list; the pack carries them, so they count toward
# what an extraction shows.
if data_registry is None:
data_registry = load_data_dir_registry(platforms_dir)
data_sizes = [
data_sizes = pack_only_sizes + [
os.path.getsize(src)
for src, _dest in _data_directory_members(
pack_systems, data_registry, platform_name, base_dest,
+71
View File
@@ -158,6 +158,77 @@ class ManifestStatesWhatThePackHolds(PackCountFixture):
self.assertEqual(errors, [])
class ManifestFollowsTheBuilder(PackCountFixture):
"""Two places where the pack carried a file the manifest left out.
The release record compares each archive with the count its manifest
expects, and the first comparison named both: four `pc6001*.zip` in the
Batocera pack, two MSX2+ ROMs in the Recalbox pack.
"""
def _platform(self, files: list[dict], mode: str = "md5") -> None:
config = dict(PLATFORM, verification_mode=mode, cores=[])
config["systems"] = {"demo-system": {"files": files}}
(self.platforms / "demo.yml").write_text(yaml.dump(config))
def _pack_names(self) -> set[str]:
out = self.root / "dist"
out.mkdir(exist_ok=True)
zip_path = builder.generate_pack(
"demo", str(self.platforms), self.db, str(self.bios), str(out),
include_extras=True, emulators_dir=str(self.emulators),
emu_profiles=self.profiles, data_registry=self.registry,
offline=True,
)
with zipfile.ZipFile(zip_path) as archive:
return set(archive.namelist()) | {"manifest.json"}
def test_an_archive_checked_by_the_rom_inside_it_is_listed(self):
"""Batocera pins the MD5 of a ROM inside the ZIP, not of the ZIP. The
builder looked inside, the manifest did not, and an installation by
script lost four required archives the pack shipped."""
rom = b"rom inside the set"
archive = self.bios / "SystemA" / "set.zip"
with zipfile.ZipFile(archive, "w") as handle:
handle.writestr("basic.rom", rom)
# A second ROM, so the archive is not identified by the first.
handle.writestr("chargen.rom", b"another rom of the set")
raw = archive.read_bytes()
sha1 = hashlib.sha1(raw).hexdigest()
self.db["files"][sha1] = {
"path": str(archive), "name": "set.zip", "size": len(raw),
"sha1": sha1, "md5": hashlib.md5(raw).hexdigest(),
"sha256": hashlib.sha256(raw).hexdigest(), "crc32": "00000000",
}
self.db["indexes"] = generate_db.build_indexes(self.db["files"], {})
self._platform([{
"name": "set.zip", "destination": "set.zip",
"md5": hashlib.md5(rom).hexdigest(), "zipped_file": "basic.rom",
}])
names = self._pack_names()
self.assertIn("set.zip", names)
manifest = self._manifest()
self.assertIn("set.zip", {entry["dest"] for entry in manifest["files"]})
self.assertEqual(manifest["pack_files"], len(names))
def test_a_file_only_a_data_directory_holds_still_counts(self):
"""The installer cannot fetch it, so it stays out of the list. The
pack carries it, so it is part of what an extraction shows."""
(self.data / "Machines").mkdir()
(self.data / "Machines" / "Msx2pe.rom").write_bytes(b"machine rom")
self._platform([{
"name": "Msx2pe.rom", "destination": "Machines/Msx2pe.rom",
"md5": hashlib.md5(b"machine rom").hexdigest(),
}])
names = self._pack_names()
self.assertIn("Machines/Msx2pe.rom", names)
manifest = self._manifest()
self.assertNotIn(
"Machines/Msx2pe.rom", {entry["dest"] for entry in manifest["files"]}
)
self.assertEqual(manifest["pack_files"], len(names))
RECORD = {
"tag": "v2026.09.04",
"packs": {