fix: pin every pack member to a fixed date

A pack was still not a function of its inputs. ZipFile.write copies the
source file's mtime into the member: the wall clock for an archive this
build rebuilt in tmp/, the checkout time for a file from the collection.
Two consecutive builds of the Recalbox pack differed on 348 members
whose content matched byte for byte, and a pack built from a fresh clone
could never match one built from another.

Every member now goes through one writer that stamps the epoch the
archive rebuilder already uses, streaming the content so a firmware
image of several hundred megabytes is not read whole.

The pack was already covered by a two-builds-are-identical test, which
passed: its fixture held no romset, so it never reached the rebuild
path. The fixture has one now, and reverting the writer fails both that
test and the new one.

Verified on the real collection: Recalbox and RetroArch rebuild to the
same bytes twice, contents unchanged from the previous revision (1319
and 4517 members, zero CRC differences), and both still pass their
native integrity check.
This commit is contained in:
Abdessamad Derraz committed 2026-08-12 15:14:48 +02:00
1 parent f8fe1d1464
commit a2bd197b9b
2 files changed
+195 -81

No files matched your search

+40 -1
View File
@@ -168,6 +168,17 @@ class TestPackDeterminism(unittest.TestCase):
payload = b"DETERMINISM_PAYLOAD"
(self.bios / "boot.rom").write_bytes(payload)
sha1 = hashlib.sha1(payload).hexdigest()
# A romset goes through the deterministic rebuild, which stages the
# archive in a temporary file. Copying that file into the pack took
# its mtime -the wall clock -so 348 members of the real Recalbox
# pack moved between two builds while every byte matched. Without a
# romset here the fixture never reaches that path.
romset = self.bios / "romset.zip"
with zipfile.ZipFile(romset, "w") as archive:
archive.writestr("rom_a.bin", b"ROM A CONTENT")
archive.writestr("rom_b.bin", b"ROM B CONTENT")
rom_bytes = romset.read_bytes()
rom_sha1 = hashlib.sha1(rom_bytes).hexdigest()
self.db = {
"generated_at": "2026-01-02T03:04:05Z",
"files": {
@@ -178,10 +189,17 @@ class TestPackDeterminism(unittest.TestCase):
"sha1": sha1,
"md5": hashlib.md5(payload).hexdigest(),
},
rom_sha1: {
"path": str(romset),
"name": "romset.zip",
"size": len(rom_bytes),
"sha1": rom_sha1,
"md5": hashlib.md5(rom_bytes).hexdigest(),
},
},
"indexes": {
"by_md5": {hashlib.md5(payload).hexdigest(): sha1},
"by_name": {"boot.rom": [sha1]},
"by_name": {"boot.rom": [sha1], "romset.zip": [rom_sha1]},
"by_crc32": {},
"by_sha256": {},
"by_path_suffix": {},
@@ -197,6 +215,9 @@ class TestPackDeterminism(unittest.TestCase):
" - name: boot.rom\n"
" destination: boot.rom\n"
" required: true\n"
" - name: romset.zip\n"
" destination: romset.zip\n"
" required: true\n"
)
def tearDown(self):
@@ -223,6 +244,24 @@ class TestPackDeterminism(unittest.TestCase):
hashlib.sha256(second.read_bytes()).hexdigest(),
)
def test_the_rebuild_path_ran(self):
"""Guard the guard: a fixture without a romset proves nothing."""
pack = self._build("out1")
with zipfile.ZipFile(pack) as zf:
names = {i.filename.rsplit("/", 1)[-1] for i in zf.infolist()}
self.assertIn("romset.zip", names)
def test_every_member_carries_the_fixed_epoch(self):
"""A member's date must come from the build's inputs, not its clock.
ZipFile.write copies the source file's mtime, which is the checkout
time for a collection file and the wall clock for one staged in tmp/.
"""
pack = self._build("out1")
with zipfile.ZipFile(pack) as zf:
dates = {i.date_time for i in zf.infolist()}
self.assertEqual(dates, {_FIXED_DATE_TIME})
def test_generated_members_carry_the_fixed_epoch(self):
pack = self._build("out1")
with zipfile.ZipFile(pack) as zf: