From f7d561e42f97a437eb3250051ec8299309cd6a9d Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 21 Oct 2026 05:10:00 +0200 Subject: [PATCH] fix: store already-compressed pack members Every member was deflated, archives and PNGs included. On the 2026-10-20 RetroArch pack the .zip, .7z, .chd, .png, .ogg and .pk4 members are 2.1 GB that took 28 s to deflate again for 9.6 MB saved (0.45 %), and about 16 GB for 72 MB across all packs. Those formats are now stored. The choice reads the member name only, so a pack stays byte-reproducible. --- scripts/generate_pack.py | 20 ++++++++++++++++-- tests/test_deterministic_zip.py | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index acf92ef9..8aa52869 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -105,6 +105,22 @@ DEFAULT_BIOS_DIR = "bios" +# Formats that are already compressed: deflating them again took 28 s of the +# RetroArch build for 2.1 GB of members and saved 9.6 MB (0.45 %), 16 GB and +# about 72 MB across all packs. Measured per extension on the 2026-10-20 +# packs; .pk3 (3 %), .jpg (6 %), .avi (44 %) and .jar (60 %) still shrink +# and stay deflated. Chosen by name alone, so a pack stays a function of its +# inputs. +_STORED_EXTENSIONS = frozenset({".7z", ".chd", ".ogg", ".pk4", ".png", ".zip"}) + + +def _member_compression(arcname: str) -> int: + """ZIP_STORED for a member whose format is already compressed.""" + if os.path.splitext(arcname)[1].lower() in _STORED_EXTENSIONS: + return zipfile.ZIP_STORED + return zipfile.ZIP_DEFLATED + + def _add_pack_member(zf: zipfile.ZipFile, source_path: str, arcname: str) -> None: """Add a file to the pack with metadata that does not depend on the build. @@ -117,7 +133,7 @@ def _add_pack_member(zf: zipfile.ZipFile, source_path: str, arcname: str) -> Non because some members are firmware images of several hundred megabytes. """ info = zipfile.ZipInfo(filename=arcname, date_time=_FIXED_DATE_TIME) - info.compress_type = zipfile.ZIP_DEFLATED + info.compress_type = _member_compression(arcname) # The executable bit is the one permission worth carrying: the RetroDECK # pack ships the two Voxatron engine binaries, and a user who extracts # them at 644 cannot run them. Git records the bit, so reading it from @@ -1218,7 +1234,7 @@ def _extract_zip_to_archive( member = zipfile.ZipInfo( filename=target_path, date_time=_FIXED_DATE_TIME ) - member.compress_type = zipfile.ZIP_DEFLATED + member.compress_type = _member_compression(target_path) member.external_attr = 0o100644 << 16 target_zf.writestr(member, data) diff --git a/tests/test_deterministic_zip.py b/tests/test_deterministic_zip.py index 24ef3c0b..d5f09481 100644 --- a/tests/test_deterministic_zip.py +++ b/tests/test_deterministic_zip.py @@ -413,5 +413,41 @@ class ManifestInjectionIsAtomic(unittest.TestCase): ) +class PackMemberCompression(unittest.TestCase): + """A pack deflated every member, archives and PNGs included: 16 GB of + already-compressed data deflated again on each full build for 0.18 % of + the packs' size.""" + + def _pack(self, tmp: Path, name: str) -> Path: + from scripts.generate_pack import _add_pack_member + + source = tmp / "member" + source.write_bytes(b"\x00" * 4096) + pack = tmp / name + with zipfile.ZipFile(pack, "w", zipfile.ZIP_DEFLATED) as zf: + for arcname in ("bios/a.zip", "bios/b.PNG", "bios/c.bin", "bios/d.pk3"): + _add_pack_member(zf, str(source), arcname) + return pack + + def test_compressed_formats_are_stored_and_the_rest_deflated(self): + with tempfile.TemporaryDirectory() as tmp: + pack = self._pack(Path(tmp), "pack.zip") + with zipfile.ZipFile(pack) as zf: + kinds = {i.filename: i.compress_type for i in zf.infolist()} + self.assertEqual(kinds, { + "bios/a.zip": zipfile.ZIP_STORED, + "bios/b.PNG": zipfile.ZIP_STORED, + "bios/c.bin": zipfile.ZIP_DEFLATED, + "bios/d.pk3": zipfile.ZIP_DEFLATED, + }) + + def test_the_pack_stays_byte_reproducible(self): + with tempfile.TemporaryDirectory() as tmp: + first = self._pack(Path(tmp), "one.zip").read_bytes() + time.sleep(1.1) + second = self._pack(Path(tmp), "two.zip").read_bytes() + self.assertEqual(hashlib.sha256(first).hexdigest(), hashlib.sha256(second).hexdigest()) + + if __name__ == "__main__": unittest.main()