From 08bce47668162e616f1370bfdef0bd62ca9ba465 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Mon, 14 Sep 2026 01:17:49 +0200 Subject: [PATCH] fix: keep install manifests and release assets true --- .gitignore | 14 +- README.md | 28 +-- _mame_clones.json | 16 +- database.json | 64 ++++-- install/batocera.json | 34 ++-- install/bizhawk.json | 34 ++-- install/emudeck.json | 14 +- install/lakka.json | 46 ++--- install/recalbox.json | 36 ++-- install/retroarch.json | 46 ++--- install/retrobat.json | 42 ++-- install/retrodeck.json | 38 ++-- install/retropie.json | 46 ++--- install/rocknix.json | 40 ++-- install/romm.json | 28 +-- scripts/check_release_assets.py | 336 ++++++++++++++++++++++++++++++++ scripts/dedup.py | 7 +- scripts/generate_pack.py | 19 +- scripts/pipeline.py | 15 ++ tests/test_audit_regressions.py | 205 +++++++++++++++++++ tests/test_dedup.py | 17 ++ wiki/release-process.md | 16 ++ 22 files changed, 878 insertions(+), 263 deletions(-) create mode 100644 scripts/check_release_assets.py diff --git a/.gitignore b/.gitignore index f87de54e..c23e5f76 100644 --- a/.gitignore +++ b/.gitignore @@ -15,10 +15,6 @@ node_modules/ docs/ # Large files stored as GitHub Release assets (> 50MB) -bios/Arcade/Arcade/Firmware.19.0.0.zip -bios/Arcade/Arcade/maclc3.zip -bios/Nintendo/Nintendo DS/dsi_nand.bin -bios/Other/Batocera42/dsi_nand.bin bios/Sony/PlayStation 3/PS3UPDAT.PUP bios/Sony/PlayStation 3/.variants/PS3UPDAT.PUP.ed8ab192 bios/Sony/PlayStation Vita/PSVUPDAT.PUP @@ -66,20 +62,20 @@ tmp/ # FBA2012 Toaplan samples (>100MB) bios/Arcade/fba2012/samples/fireshrk.zip bios/Arcade/fba2012/samples/ghox.zip -bios/Arcade/fba2012/samples/samesame.zip bios/Arcade/fba2012/samples/tekipaki.zip bios/Arcade/fba2012/samples/vimana.zip -bios/Arcade/fba2012/samples/fireshrk.zip -bios/Arcade/fba2012/samples/samesame.zip -bios/Arcade/fba2012/samples/vimana.zip bios/Nintendo/Switch/Firmware_batocera43.zip bios/Nintendo/DS/.variants/dsi_nand.bin.8b79a73c # Wii NAND backup (528MB, fetched from large-files release) bios/Nintendo/Wii/nand.bin -# FBNeo audio samples (97MB and 155MB, fetched from large-files release) +# FBNeo audio samples (96MB, 148MB and 199MB, fetched from large-files release) +bios/Other/fbneo/fbneo/samples/donpachi.zip bios/Other/fbneo/fbneo/samples/sfz3mix.zip bios/Other/fbneo/fbneo/samples/twotiger.zip bios/Other/starboard/starboard-rootfs.tar.gz bios/Other/cspect/cspect-next-8gb.zip + +# Output-directory locks (artifact_lock) +.lock diff --git a/README.md b/README.md index 47e81870..d72b2f7a 100644 --- a/README.md +++ b/README.md @@ -39,18 +39,18 @@ Every release ships `SHA256SUMS.txt` and a detached signature of it, checkable a | Platform | Extracted size | Extract to | Download | |----------|---------------:|-----------|----------| -| Batocera | 4.0 GB | `/userdata/bios/` | [Download](../../releases/latest) | -| BizHawk | 2.2 GB | `Firmware/` | [Download](../../releases/latest) | +| Batocera | 4.2 GB | `/userdata/bios/` | [Download](../../releases/latest) | +| BizHawk | 2.4 GB | `Firmware/` | [Download](../../releases/latest) | | EmuDeck | 3.2 GB | `~/Emulation/bios/` | [Download](../../releases/latest) | -| Lakka | 5.5 GB | `/storage/system/` | [Download](../../releases/latest) | +| Lakka | 5.7 GB | `/storage/system/` | [Download](../../releases/latest) | | MiSTer FPGA | 24 MB | `/media/fat/games/` | [Download](../../releases/latest) | -| ROCKNIX | 5.3 GB | `/storage/roms/bios/` | [Download](../../releases/latest) | -| Recalbox | 3.6 GB | `/recalbox/share/bios/` | [Download](../../releases/latest) | -| RetroArch | 5.5 GB | `system/` | [Download](../../releases/latest) | -| RetroBat | 4.4 GB | `bios/` | [Download](../../releases/latest) | -| RetroDECK | 6.2 GB | `~/retrodeck/` | [Download](../../releases/latest) | -| RetroPie * | 5.5 GB | `~/RetroPie/BIOS/` | [Download](../../releases/latest) | -| RomM | 1.3 GB | `bios/{platform_slug}/` | [Download](../../releases/latest) | +| ROCKNIX | 5.5 GB | `/storage/roms/bios/` | [Download](../../releases/latest) | +| Recalbox | 3.8 GB | `/recalbox/share/bios/` | [Download](../../releases/latest) | +| RetroArch | 5.7 GB | `system/` | [Download](../../releases/latest) | +| RetroBat | 4.6 GB | `bios/` | [Download](../../releases/latest) | +| RetroDECK | 6.4 GB | `~/retrodeck/` | [Download](../../releases/latest) | +| RetroPie * | 5.7 GB | `~/RetroPie/BIOS/` | [Download](../../releases/latest) | +| RomM | 1.5 GB | `bios/{platform_slug}/` | [Download](../../releases/latest) | The RetroDECK pack already contains its own `bios/` folder, so it extracts into `~/retrodeck/` rather than into the BIOS folder. @@ -67,9 +67,9 @@ Each file is checked the way your platform checks it. Most compare a checksum, t - **12 platforms** supported with platform-specific verification - **449 emulators** profiled from source (RetroArch cores + standalone) - **470 systems** handled by those emulators (NES, SNES, PlayStation, Saturn, Dreamcast, ...) -- **10,330 files**, each with its SHA1, MD5, SHA256, CRC32 and Adler-32 fingerprints: 4,985 system files, 2,806 arcade ROM sets, 2,539 game and engine data files -- **553 of 4,985 system files** matched to dump-preservation catalogs (No-Intro, Redump, TOSEC); arcade sets and engine data fall outside what those catalogs index -- **12706 MB** total collection size +- **10,331 files**, each with its SHA1, MD5, SHA256, CRC32 and Adler-32 fingerprints: 4,986 system files, 2,806 arcade ROM sets, 2,539 game and engine data files +- **553 of 4,986 system files** matched to dump-preservation catalogs (No-Intro, Redump, TOSEC); arcade sets and engine data fall outside what those catalogs index +- **12909 MB** total collection size ## Supported systems @@ -174,4 +174,4 @@ The scripts and tooling are released under the [MIT License](LICENSE). The BIOS and firmware files are not covered by that license: they are third-party system software, preserved and provided for personal backup, archival, and interoperability with emulation software. [NOTICE](NOTICE) sets out their status and how to ask for a file to be removed. The reasoning, and where it is weakest, is in the [FAQ](https://abdess.github.io/retrobios/wiki/faq/#is-this-legal). -*Auto-generated on 2026-09-07T02:33:05Z* +*Auto-generated on 2026-09-13T22:17:47Z* diff --git a/_mame_clones.json b/_mame_clones.json index 98e6a9bc..7ee52e9b 100644 --- a/_mame_clones.json +++ b/_mame_clones.json @@ -173,20 +173,6 @@ "sha1": "2d7d999e2acb4da55c26d1517934d39e7d0a0c86", "total_copies": 1 }, - "bbb.zip": { - "clones": [ - "aaa_long_name.zip" - ], - "sha1": "0ebda75e11e0fdce192562b5bb65a00e3e3bf240", - "total_copies": 1 - }, - "bbc_m87.zip": { - "clones": [ - "bbc_24bbc.zip" - ], - "sha1": "0ebda75e11e0fdce192562b5bb65a00e3e3bf240", - "total_copies": 1 - }, "beebsid.zip": { "clones": [ "bbc_opus3.zip" @@ -607,4 +593,4 @@ "sha1": "9139c00e5986b7a86c7e8dcbce39986126ab7db4", "total_copies": 1 } -} \ No newline at end of file +} diff --git a/database.json b/database.json index 9e84a109..2a11327a 100644 --- a/database.json +++ b/database.json @@ -1,8 +1,8 @@ { "schema_version": 1, - "generated_at": "2026-09-07T02:25:37Z", - "total_files": 10330, - "total_size": 13322705446, + "generated_at": "2026-09-13T22:10:18Z", + "total_files": 10331, + "total_size": 13535566159, "files": { "520d3d1b5897800af47f92efd2444a26b7a7dead": { "path": "bios/3DO Company/3DO/3do_arcade_saot.bin", @@ -51128,15 +51128,25 @@ "crc32": "1294ff68", "adler32": "27661d30" }, - "227815ce9e55db427d68e075182caffe6ad60ad1": { + "d380fb29287eb7fc9ff901a7653ad40785f7deb1": { + "path": "bios/Other/fbneo/fbneo/samples/donpachi.zip", + "name": "donpachi.zip", + "size": 208549253, + "sha1": "d380fb29287eb7fc9ff901a7653ad40785f7deb1", + "md5": "aa8c5e742ba27cb30ce7a872edd684bf", + "sha256": "78a237f1e2774eb396e2f14999f1270597e0767a392729e7a4539455888b6fe0", + "crc32": "8651ffbb", + "adler32": "2f229899" + }, + "e39ee9abb19076ef095f67469ca95ff5e7f600dc": { "path": "bios/Other/fbneo/fbneo/samples/sfz3mix.zip", "name": "sfz3mix.zip", - "size": 96705917, - "sha1": "227815ce9e55db427d68e075182caffe6ad60ad1", - "md5": "486c88c15387b4bd83514899e68dec3f", - "sha256": "6a720035646b3488a047ab163a6975b7be82fe75c5eec88155c8503c93a631fe", - "crc32": "c6ab431d", - "adler32": "ecbdccac" + "size": 101017377, + "sha1": "e39ee9abb19076ef095f67469ca95ff5e7f600dc", + "md5": "ce49f09c41c1c4b59078b2350ff1acaf", + "sha256": "2c2e67ab762102f50df641280721164ddff0316e71081d5fa62f2a67365dea66", + "crc32": "ed686ecb", + "adler32": "498e0209" }, "b286035624978c94194ef5b83b5d784ec00c3a3c": { "path": "bios/Other/fbneo/fbneo/samples/twotiger.zip", @@ -113272,7 +113282,8 @@ "0eb6b12f36508b24f285ffd793438ef3": "d124045eccc562ff0423b23a240e27ad740fa0c9", "c78ac640fcf02eb866b79718f5ed15fa": "20fdca7131478e1ee12691bdafd2d5bb74cbd16f", "5bcfc3374fc9b0c16070f1ac42f3dc63": "c0c0b5db93a61276a6fe6842049cb53e564b8413", - "486c88c15387b4bd83514899e68dec3f": "227815ce9e55db427d68e075182caffe6ad60ad1", + "aa8c5e742ba27cb30ce7a872edd684bf": "d380fb29287eb7fc9ff901a7653ad40785f7deb1", + "ce49f09c41c1c4b59078b2350ff1acaf": "e39ee9abb19076ef095f67469ca95ff5e7f600dc", "733f951177e613819deef4c8f6feb4e7": "b286035624978c94194ef5b83b5d784ec00c3a3c", "659d579ba9aef5b025d87323044e83f4": "a8b7dce572b74e02e65cb949b2c366c87625157f", "e612770bc5da7ee1a21d499ce419ce2c": "8251a8e151e31201dcc346d75158ee3996895e74", @@ -132449,10 +132460,11 @@ "20fdca7131478e1ee12691bdafd2d5bb74cbd16f" ], "donpachi.zip": [ - "c0c0b5db93a61276a6fe6842049cb53e564b8413" + "c0c0b5db93a61276a6fe6842049cb53e564b8413", + "d380fb29287eb7fc9ff901a7653ad40785f7deb1" ], "sfz3mix.zip": [ - "227815ce9e55db427d68e075182caffe6ad60ad1" + "e39ee9abb19076ef095f67469ca95ff5e7f600dc" ], "twotiger.zip": [ "b286035624978c94194ef5b83b5d784ec00c3a3c" @@ -156840,7 +156852,8 @@ "88d0be26": "d124045eccc562ff0423b23a240e27ad740fa0c9", "3c79ef05": "20fdca7131478e1ee12691bdafd2d5bb74cbd16f", "1294ff68": "c0c0b5db93a61276a6fe6842049cb53e564b8413", - "c6ab431d": "227815ce9e55db427d68e075182caffe6ad60ad1", + "8651ffbb": "d380fb29287eb7fc9ff901a7653ad40785f7deb1", + "ed686ecb": "e39ee9abb19076ef095f67469ca95ff5e7f600dc", "b5d5b941": "b286035624978c94194ef5b83b5d784ec00c3a3c", "353fdbcd": "a8b7dce572b74e02e65cb949b2c366c87625157f", "31669117": "8251a8e151e31201dcc346d75158ee3996895e74", @@ -167172,7 +167185,8 @@ "f76b62e73e7410ab63813b9db36839be12213e4adfd3ee513eb382b7bc3a4a39": "d124045eccc562ff0423b23a240e27ad740fa0c9", "6597f75e97e5337198736b42a4d1ea66d4fdf01d685772482811a55e1eddf8f1": "20fdca7131478e1ee12691bdafd2d5bb74cbd16f", "a686ebf893d511b5b7667176cd6844593e914595a8bb9ab656b2c66969013b5c": "c0c0b5db93a61276a6fe6842049cb53e564b8413", - "6a720035646b3488a047ab163a6975b7be82fe75c5eec88155c8503c93a631fe": "227815ce9e55db427d68e075182caffe6ad60ad1", + "78a237f1e2774eb396e2f14999f1270597e0767a392729e7a4539455888b6fe0": "d380fb29287eb7fc9ff901a7653ad40785f7deb1", + "2c2e67ab762102f50df641280721164ddff0316e71081d5fa62f2a67365dea66": "e39ee9abb19076ef095f67469ca95ff5e7f600dc", "fe5408e3f513c44a64f573e7a8b68885bce9b263bc9c67bfef13fef13f3e976e": "b286035624978c94194ef5b83b5d784ec00c3a3c", "569541852ed3ff8fdacd04b1c6a57fd491a361ab6c41339bbddfd916bced241e": "a8b7dce572b74e02e65cb949b2c366c87625157f", "ea2e9f8eccb2d590a0d004e0eaffa95ca4eea36f15466b5aa1a9e47393bc004f": "8251a8e151e31201dcc346d75158ee3996895e74", @@ -211209,17 +211223,29 @@ "fbneo/donpachi.zip": [ "c0c0b5db93a61276a6fe6842049cb53e564b8413" ], + "Other/fbneo/fbneo/samples/donpachi.zip": [ + "d380fb29287eb7fc9ff901a7653ad40785f7deb1" + ], + "fbneo/fbneo/samples/donpachi.zip": [ + "d380fb29287eb7fc9ff901a7653ad40785f7deb1" + ], + "fbneo/samples/donpachi.zip": [ + "d380fb29287eb7fc9ff901a7653ad40785f7deb1" + ], + "samples/donpachi.zip": [ + "d380fb29287eb7fc9ff901a7653ad40785f7deb1" + ], "Other/fbneo/fbneo/samples/sfz3mix.zip": [ - "227815ce9e55db427d68e075182caffe6ad60ad1" + "e39ee9abb19076ef095f67469ca95ff5e7f600dc" ], "fbneo/fbneo/samples/sfz3mix.zip": [ - "227815ce9e55db427d68e075182caffe6ad60ad1" + "e39ee9abb19076ef095f67469ca95ff5e7f600dc" ], "fbneo/samples/sfz3mix.zip": [ - "227815ce9e55db427d68e075182caffe6ad60ad1" + "e39ee9abb19076ef095f67469ca95ff5e7f600dc" ], "samples/sfz3mix.zip": [ - "227815ce9e55db427d68e075182caffe6ad60ad1" + "e39ee9abb19076ef095f67469ca95ff5e7f600dc" ], "Other/fbneo/fbneo/samples/twotiger.zip": [ "b286035624978c94194ef5b83b5d784ec00c3a3c" diff --git a/install/batocera.json b/install/batocera.json index 96c286f4..4c90191d 100644 --- a/install/batocera.json +++ b/install/batocera.json @@ -5,7 +5,7 @@ "platform": "batocera", "display_name": "Batocera", "version": "1.0", - "generated": "2026-09-05T09:26:20Z", + "generated": "2026-09-13T22:10:18Z", "base_destination": "bios", "detect": [ { @@ -238,7 +238,7 @@ } ], "total_files": 1832, - "total_size": 4314998020, + "total_size": 4522217423, "total_omitted": 9, "omitted_files": [ { @@ -2271,10 +2271,10 @@ }, { "dest": "firmware.bin", - "sha1": "5b2acbd499b39fb1af1b36ef7de5eb047d83401c", - "sha256": "7ea4f06ca110f8b7ff7c946f6e4e228d94977dadd4bb51570d14c68488edb32a", + "sha1": "cfe072921ee3fb93f688743f8beef89043c3e9ad", + "sha256": "57a1d6b43c1399d219ea18644240c4d4c6787c829f0a67ded216472867857cd0", "size": 262144, - "repo_path": "bios/Arcade/Arcade/firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/firmware.bin", "cores": null }, { @@ -2311,10 +2311,10 @@ }, { "dest": "dsi_firmware.bin", - "sha1": "93c0598b08192a1f630e5edb2cd9f60d1e3ea7b3", - "sha256": "1f89935da2888cd3e401af2774e06a6b587ca059e85f922826494f317d31c6ae", + "sha1": "d2a5af338f09c5cbdd5d7628db5b9c075c69b616", + "sha256": "1d7c771de535e6e07182fc9820ac32e76c6f0b5bcefd256a861fdfab144761ec", "size": 131072, - "repo_path": "bios/Arcade/Arcade/dsi_firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/dsi_firmware.bin", "cores": null }, { @@ -8041,19 +8041,21 @@ }, { "dest": "fbneo/samples/donpachi.zip", - "sha1": "c0c0b5db93a61276a6fe6842049cb53e564b8413", - "sha256": "a686ebf893d511b5b7667176cd6844593e914595a8bb9ab656b2c66969013b5c", - "size": 5641310, - "repo_path": "bios/Other/fbneo/donpachi.zip", + "sha1": "d380fb29287eb7fc9ff901a7653ad40785f7deb1", + "sha256": "78a237f1e2774eb396e2f14999f1270597e0767a392729e7a4539455888b6fe0", + "size": 208549253, + "repo_path": "bios/Other/fbneo/fbneo/samples/donpachi.zip", "cores": [ "fbneo" - ] + ], + "storage": "release", + "release_asset": "donpachi.zip" }, { "dest": "fbneo/samples/sfz3mix.zip", - "sha1": "227815ce9e55db427d68e075182caffe6ad60ad1", - "sha256": "6a720035646b3488a047ab163a6975b7be82fe75c5eec88155c8503c93a631fe", - "size": 96705917, + "sha1": "e39ee9abb19076ef095f67469ca95ff5e7f600dc", + "sha256": "2c2e67ab762102f50df641280721164ddff0316e71081d5fa62f2a67365dea66", + "size": 101017377, "repo_path": "bios/Other/fbneo/fbneo/samples/sfz3mix.zip", "cores": [ "fbneo" diff --git a/install/bizhawk.json b/install/bizhawk.json index ed5f9cca..9e2b6287 100644 --- a/install/bizhawk.json +++ b/install/bizhawk.json @@ -5,7 +5,7 @@ "platform": "bizhawk", "display_name": "BizHawk", "version": "1.0", - "generated": "2026-09-05T09:26:20Z", + "generated": "2026-09-13T22:10:18Z", "base_destination": "Firmware", "detect": [ { @@ -21,7 +21,7 @@ ], "standalone_copies": [], "total_files": 539, - "total_size": 2328691103, + "total_size": 2535910506, "total_omitted": 1, "omitted_files": [ { @@ -1464,19 +1464,21 @@ }, { "dest": "fbneo/samples/donpachi.zip", - "sha1": "c0c0b5db93a61276a6fe6842049cb53e564b8413", - "sha256": "a686ebf893d511b5b7667176cd6844593e914595a8bb9ab656b2c66969013b5c", - "size": 5641310, - "repo_path": "bios/Other/fbneo/donpachi.zip", + "sha1": "d380fb29287eb7fc9ff901a7653ad40785f7deb1", + "sha256": "78a237f1e2774eb396e2f14999f1270597e0767a392729e7a4539455888b6fe0", + "size": 208549253, + "repo_path": "bios/Other/fbneo/fbneo/samples/donpachi.zip", "cores": [ "fbneo" - ] + ], + "storage": "release", + "release_asset": "donpachi.zip" }, { "dest": "fbneo/samples/sfz3mix.zip", - "sha1": "227815ce9e55db427d68e075182caffe6ad60ad1", - "sha256": "6a720035646b3488a047ab163a6975b7be82fe75c5eec88155c8503c93a631fe", - "size": 96705917, + "sha1": "e39ee9abb19076ef095f67469ca95ff5e7f600dc", + "sha256": "2c2e67ab762102f50df641280721164ddff0316e71081d5fa62f2a67365dea66", + "size": 101017377, "repo_path": "bios/Other/fbneo/fbneo/samples/sfz3mix.zip", "cores": [ "fbneo" @@ -3348,10 +3350,10 @@ }, { "dest": "firmware.bin", - "sha1": "5b2acbd499b39fb1af1b36ef7de5eb047d83401c", - "sha256": "7ea4f06ca110f8b7ff7c946f6e4e228d94977dadd4bb51570d14c68488edb32a", + "sha1": "cfe072921ee3fb93f688743f8beef89043c3e9ad", + "sha256": "57a1d6b43c1399d219ea18644240c4d4c6787c829f0a67ded216472867857cd0", "size": 262144, - "repo_path": "bios/Arcade/Arcade/firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/firmware.bin", "cores": [ "melonds" ] @@ -3378,10 +3380,10 @@ }, { "dest": "dsi_firmware.bin", - "sha1": "93c0598b08192a1f630e5edb2cd9f60d1e3ea7b3", - "sha256": "1f89935da2888cd3e401af2774e06a6b587ca059e85f922826494f317d31c6ae", + "sha1": "d2a5af338f09c5cbdd5d7628db5b9c075c69b616", + "sha256": "1d7c771de535e6e07182fc9820ac32e76c6f0b5bcefd256a861fdfab144761ec", "size": 131072, - "repo_path": "bios/Arcade/Arcade/dsi_firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/dsi_firmware.bin", "cores": [ "melonds" ] diff --git a/install/emudeck.json b/install/emudeck.json index 8f15c175..14ec067e 100644 --- a/install/emudeck.json +++ b/install/emudeck.json @@ -5,7 +5,7 @@ "platform": "emudeck", "display_name": "EmuDeck", "version": "1.0", - "generated": "2026-09-03T20:45:52Z", + "generated": "2026-09-13T17:22:08Z", "base_destination": "bios", "detect": [ { @@ -313,10 +313,10 @@ }, { "dest": "firmware.bin", - "sha1": "5b2acbd499b39fb1af1b36ef7de5eb047d83401c", - "sha256": "7ea4f06ca110f8b7ff7c946f6e4e228d94977dadd4bb51570d14c68488edb32a", + "sha1": "cfe072921ee3fb93f688743f8beef89043c3e9ad", + "sha256": "57a1d6b43c1399d219ea18644240c4d4c6787c829f0a67ded216472867857cd0", "size": 262144, - "repo_path": "bios/Arcade/Arcade/firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/firmware.bin", "cores": null }, { @@ -3991,10 +3991,10 @@ }, { "dest": "dsi_firmware.bin", - "sha1": "93c0598b08192a1f630e5edb2cd9f60d1e3ea7b3", - "sha256": "1f89935da2888cd3e401af2774e06a6b587ca059e85f922826494f317d31c6ae", + "sha1": "d2a5af338f09c5cbdd5d7628db5b9c075c69b616", + "sha256": "1d7c771de535e6e07182fc9820ac32e76c6f0b5bcefd256a861fdfab144761ec", "size": 131072, - "repo_path": "bios/Arcade/Arcade/dsi_firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/dsi_firmware.bin", "cores": [ "melonds" ] diff --git a/install/lakka.json b/install/lakka.json index 99c68433..9b49db4b 100644 --- a/install/lakka.json +++ b/install/lakka.json @@ -5,7 +5,7 @@ "platform": "lakka", "display_name": "Lakka", "version": "1.0", - "generated": "2026-09-05T09:26:20Z", + "generated": "2026-09-13T22:10:18Z", "base_destination": "system", "detect": [ { @@ -17,7 +17,7 @@ ], "standalone_copies": [], "total_files": 1871, - "total_size": 5865446694, + "total_size": 6072666103, "total_omitted": 1, "omitted_files": [ { @@ -1626,10 +1626,10 @@ }, { "dest": "dsi_firmware.bin", - "sha1": "93c0598b08192a1f630e5edb2cd9f60d1e3ea7b3", - "sha256": "1f89935da2888cd3e401af2774e06a6b587ca059e85f922826494f317d31c6ae", + "sha1": "d2a5af338f09c5cbdd5d7628db5b9c075c69b616", + "sha256": "1d7c771de535e6e07182fc9820ac32e76c6f0b5bcefd256a861fdfab144761ec", "size": 131072, - "repo_path": "bios/Arcade/Arcade/dsi_firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/dsi_firmware.bin", "cores": null }, { @@ -6912,19 +6912,21 @@ }, { "dest": "fbneo/samples/donpachi.zip", - "sha1": "c0c0b5db93a61276a6fe6842049cb53e564b8413", - "sha256": "a686ebf893d511b5b7667176cd6844593e914595a8bb9ab656b2c66969013b5c", - "size": 5641310, - "repo_path": "bios/Other/fbneo/donpachi.zip", + "sha1": "d380fb29287eb7fc9ff901a7653ad40785f7deb1", + "sha256": "78a237f1e2774eb396e2f14999f1270597e0767a392729e7a4539455888b6fe0", + "size": 208549253, + "repo_path": "bios/Other/fbneo/fbneo/samples/donpachi.zip", "cores": [ "fbneo" - ] + ], + "storage": "release", + "release_asset": "donpachi.zip" }, { "dest": "fbneo/samples/sfz3mix.zip", - "sha1": "227815ce9e55db427d68e075182caffe6ad60ad1", - "sha256": "6a720035646b3488a047ab163a6975b7be82fe75c5eec88155c8503c93a631fe", - "size": 96705917, + "sha1": "e39ee9abb19076ef095f67469ca95ff5e7f600dc", + "sha256": "2c2e67ab762102f50df641280721164ddff0316e71081d5fa62f2a67365dea66", + "size": 101017377, "repo_path": "bios/Other/fbneo/fbneo/samples/sfz3mix.zip", "cores": [ "fbneo" @@ -9368,10 +9370,10 @@ }, { "dest": "d2fdc.zip", - "sha1": "bc39fbd5b9a8d2287ac5d0a42e639fc4d3c2f9d4", - "sha256": "e5e30615040567c1e7a2d21599681f8dac820edbdcda177b816a64d74b3a12f2", - "size": 256, - "repo_path": "bios/Apple/Apple II/DiskII/state-machine-16.rom", + "sha1": "af56c948598291b284a528f3fce06b961dba55e3", + "sha256": "69ee0f8f0862e1a623bcbe9870f9531f7976312f292a20d1aff7ed61b6085a69", + "size": 262, + "repo_path": "bios/Arcade/Arcade/a3fdc.zip", "cores": [ "mamemess" ] @@ -13380,9 +13382,7 @@ "repo_path": "bios/Other/sdlpal/sdlpal/5.avi", "cores": [ "sdlpal" - ], - "storage": "release", - "release_asset": "5.avi" + ] }, { "dest": "sdlpal/6.avi", @@ -15966,10 +15966,10 @@ }, { "dest": "SkyEmu/firmware.bin", - "sha1": "5b2acbd499b39fb1af1b36ef7de5eb047d83401c", - "sha256": "7ea4f06ca110f8b7ff7c946f6e4e228d94977dadd4bb51570d14c68488edb32a", + "sha1": "cfe072921ee3fb93f688743f8beef89043c3e9ad", + "sha256": "57a1d6b43c1399d219ea18644240c4d4c6787c829f0a67ded216472867857cd0", "size": 262144, - "repo_path": "bios/Arcade/Arcade/firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/firmware.bin", "cores": [ "skyemu" ] diff --git a/install/recalbox.json b/install/recalbox.json index 5d5b4eff..8b03f631 100644 --- a/install/recalbox.json +++ b/install/recalbox.json @@ -5,7 +5,7 @@ "platform": "recalbox", "display_name": "Recalbox", "version": "1.0", - "generated": "2026-09-05T09:26:20Z", + "generated": "2026-09-13T22:10:18Z", "base_destination": "bios", "detect": [ { @@ -238,7 +238,7 @@ } ], "total_files": 1328, - "total_size": 3900250204, + "total_size": 4107469613, "total_omitted": 4, "omitted_files": [ { @@ -5927,19 +5927,21 @@ }, { "dest": "fbneo/samples/donpachi.zip", - "sha1": "c0c0b5db93a61276a6fe6842049cb53e564b8413", - "sha256": "a686ebf893d511b5b7667176cd6844593e914595a8bb9ab656b2c66969013b5c", - "size": 5641310, - "repo_path": "bios/Other/fbneo/donpachi.zip", + "sha1": "d380fb29287eb7fc9ff901a7653ad40785f7deb1", + "sha256": "78a237f1e2774eb396e2f14999f1270597e0767a392729e7a4539455888b6fe0", + "size": 208549253, + "repo_path": "bios/Other/fbneo/fbneo/samples/donpachi.zip", "cores": [ "fbneo" - ] + ], + "storage": "release", + "release_asset": "donpachi.zip" }, { "dest": "fbneo/samples/sfz3mix.zip", - "sha1": "227815ce9e55db427d68e075182caffe6ad60ad1", - "sha256": "6a720035646b3488a047ab163a6975b7be82fe75c5eec88155c8503c93a631fe", - "size": 96705917, + "sha1": "e39ee9abb19076ef095f67469ca95ff5e7f600dc", + "sha256": "2c2e67ab762102f50df641280721164ddff0316e71081d5fa62f2a67365dea66", + "size": 101017377, "repo_path": "bios/Other/fbneo/fbneo/samples/sfz3mix.zip", "cores": [ "fbneo" @@ -8191,10 +8193,10 @@ }, { "dest": "d2fdc.zip", - "sha1": "bc39fbd5b9a8d2287ac5d0a42e639fc4d3c2f9d4", - "sha256": "e5e30615040567c1e7a2d21599681f8dac820edbdcda177b816a64d74b3a12f2", - "size": 256, - "repo_path": "bios/Apple/Apple II/DiskII/state-machine-16.rom", + "sha1": "af56c948598291b284a528f3fce06b961dba55e3", + "sha256": "69ee0f8f0862e1a623bcbe9870f9531f7976312f292a20d1aff7ed61b6085a69", + "size": 262, + "repo_path": "bios/Arcade/Arcade/a3fdc.zip", "cores": [ "mamemess" ] @@ -8681,10 +8683,10 @@ }, { "dest": "dsi_firmware.bin", - "sha1": "93c0598b08192a1f630e5edb2cd9f60d1e3ea7b3", - "sha256": "1f89935da2888cd3e401af2774e06a6b587ca059e85f922826494f317d31c6ae", + "sha1": "d2a5af338f09c5cbdd5d7628db5b9c075c69b616", + "sha256": "1d7c771de535e6e07182fc9820ac32e76c6f0b5bcefd256a861fdfab144761ec", "size": 131072, - "repo_path": "bios/Arcade/Arcade/dsi_firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/dsi_firmware.bin", "cores": [ "melonds" ] diff --git a/install/retroarch.json b/install/retroarch.json index e304e8c2..fa5e62e8 100644 --- a/install/retroarch.json +++ b/install/retroarch.json @@ -5,7 +5,7 @@ "platform": "retroarch", "display_name": "RetroArch", "version": "1.0", - "generated": "2026-09-05T09:26:20Z", + "generated": "2026-09-13T22:10:18Z", "base_destination": "system", "detect": [ { @@ -256,7 +256,7 @@ } ], "total_files": 1871, - "total_size": 5865446694, + "total_size": 6072666103, "total_omitted": 1, "omitted_files": [ { @@ -1865,10 +1865,10 @@ }, { "dest": "dsi_firmware.bin", - "sha1": "93c0598b08192a1f630e5edb2cd9f60d1e3ea7b3", - "sha256": "1f89935da2888cd3e401af2774e06a6b587ca059e85f922826494f317d31c6ae", + "sha1": "d2a5af338f09c5cbdd5d7628db5b9c075c69b616", + "sha256": "1d7c771de535e6e07182fc9820ac32e76c6f0b5bcefd256a861fdfab144761ec", "size": 131072, - "repo_path": "bios/Arcade/Arcade/dsi_firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/dsi_firmware.bin", "cores": null }, { @@ -7151,19 +7151,21 @@ }, { "dest": "fbneo/samples/donpachi.zip", - "sha1": "c0c0b5db93a61276a6fe6842049cb53e564b8413", - "sha256": "a686ebf893d511b5b7667176cd6844593e914595a8bb9ab656b2c66969013b5c", - "size": 5641310, - "repo_path": "bios/Other/fbneo/donpachi.zip", + "sha1": "d380fb29287eb7fc9ff901a7653ad40785f7deb1", + "sha256": "78a237f1e2774eb396e2f14999f1270597e0767a392729e7a4539455888b6fe0", + "size": 208549253, + "repo_path": "bios/Other/fbneo/fbneo/samples/donpachi.zip", "cores": [ "fbneo" - ] + ], + "storage": "release", + "release_asset": "donpachi.zip" }, { "dest": "fbneo/samples/sfz3mix.zip", - "sha1": "227815ce9e55db427d68e075182caffe6ad60ad1", - "sha256": "6a720035646b3488a047ab163a6975b7be82fe75c5eec88155c8503c93a631fe", - "size": 96705917, + "sha1": "e39ee9abb19076ef095f67469ca95ff5e7f600dc", + "sha256": "2c2e67ab762102f50df641280721164ddff0316e71081d5fa62f2a67365dea66", + "size": 101017377, "repo_path": "bios/Other/fbneo/fbneo/samples/sfz3mix.zip", "cores": [ "fbneo" @@ -9607,10 +9609,10 @@ }, { "dest": "d2fdc.zip", - "sha1": "bc39fbd5b9a8d2287ac5d0a42e639fc4d3c2f9d4", - "sha256": "e5e30615040567c1e7a2d21599681f8dac820edbdcda177b816a64d74b3a12f2", - "size": 256, - "repo_path": "bios/Apple/Apple II/DiskII/state-machine-16.rom", + "sha1": "af56c948598291b284a528f3fce06b961dba55e3", + "sha256": "69ee0f8f0862e1a623bcbe9870f9531f7976312f292a20d1aff7ed61b6085a69", + "size": 262, + "repo_path": "bios/Arcade/Arcade/a3fdc.zip", "cores": [ "mamemess" ] @@ -13619,9 +13621,7 @@ "repo_path": "bios/Other/sdlpal/sdlpal/5.avi", "cores": [ "sdlpal" - ], - "storage": "release", - "release_asset": "5.avi" + ] }, { "dest": "sdlpal/6.avi", @@ -16205,10 +16205,10 @@ }, { "dest": "SkyEmu/firmware.bin", - "sha1": "5b2acbd499b39fb1af1b36ef7de5eb047d83401c", - "sha256": "7ea4f06ca110f8b7ff7c946f6e4e228d94977dadd4bb51570d14c68488edb32a", + "sha1": "cfe072921ee3fb93f688743f8beef89043c3e9ad", + "sha256": "57a1d6b43c1399d219ea18644240c4d4c6787c829f0a67ded216472867857cd0", "size": 262144, - "repo_path": "bios/Arcade/Arcade/firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/firmware.bin", "cores": [ "skyemu" ] diff --git a/install/retrobat.json b/install/retrobat.json index 6c579166..6b246ffc 100644 --- a/install/retrobat.json +++ b/install/retrobat.json @@ -5,7 +5,7 @@ "platform": "retrobat", "display_name": "RetroBat", "version": "1.0", - "generated": "2026-09-05T09:26:20Z", + "generated": "2026-09-13T22:10:18Z", "base_destination": "bios", "detect": [ { @@ -237,7 +237,7 @@ } ], "total_files": 1464, - "total_size": 4733583386, + "total_size": 4940802795, "total_omitted": 3, "omitted_files": [ { @@ -514,10 +514,10 @@ }, { "dest": "d2fdc.zip", - "sha1": "bc39fbd5b9a8d2287ac5d0a42e639fc4d3c2f9d4", - "sha256": "e5e30615040567c1e7a2d21599681f8dac820edbdcda177b816a64d74b3a12f2", - "size": 256, - "repo_path": "bios/Apple/Apple II/DiskII/state-machine-16.rom", + "sha1": "af56c948598291b284a528f3fce06b961dba55e3", + "sha256": "69ee0f8f0862e1a623bcbe9870f9531f7976312f292a20d1aff7ed61b6085a69", + "size": 262, + "repo_path": "bios/Arcade/Arcade/a3fdc.zip", "cores": null }, { @@ -2282,10 +2282,10 @@ }, { "dest": "firmware.bin", - "sha1": "5b2acbd499b39fb1af1b36ef7de5eb047d83401c", - "sha256": "7ea4f06ca110f8b7ff7c946f6e4e228d94977dadd4bb51570d14c68488edb32a", + "sha1": "cfe072921ee3fb93f688743f8beef89043c3e9ad", + "sha256": "57a1d6b43c1399d219ea18644240c4d4c6787c829f0a67ded216472867857cd0", "size": 262144, - "repo_path": "bios/Arcade/Arcade/firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/firmware.bin", "cores": null }, { @@ -2322,10 +2322,10 @@ }, { "dest": "dsi_firmware.bin", - "sha1": "93c0598b08192a1f630e5edb2cd9f60d1e3ea7b3", - "sha256": "1f89935da2888cd3e401af2774e06a6b587ca059e85f922826494f317d31c6ae", + "sha1": "d2a5af338f09c5cbdd5d7628db5b9c075c69b616", + "sha256": "1d7c771de535e6e07182fc9820ac32e76c6f0b5bcefd256a861fdfab144761ec", "size": 131072, - "repo_path": "bios/Arcade/Arcade/dsi_firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/dsi_firmware.bin", "cores": null }, { @@ -5430,19 +5430,21 @@ }, { "dest": "fbneo/samples/donpachi.zip", - "sha1": "c0c0b5db93a61276a6fe6842049cb53e564b8413", - "sha256": "a686ebf893d511b5b7667176cd6844593e914595a8bb9ab656b2c66969013b5c", - "size": 5641310, - "repo_path": "bios/Other/fbneo/donpachi.zip", + "sha1": "d380fb29287eb7fc9ff901a7653ad40785f7deb1", + "sha256": "78a237f1e2774eb396e2f14999f1270597e0767a392729e7a4539455888b6fe0", + "size": 208549253, + "repo_path": "bios/Other/fbneo/fbneo/samples/donpachi.zip", "cores": [ "fbneo" - ] + ], + "storage": "release", + "release_asset": "donpachi.zip" }, { "dest": "fbneo/samples/sfz3mix.zip", - "sha1": "227815ce9e55db427d68e075182caffe6ad60ad1", - "sha256": "6a720035646b3488a047ab163a6975b7be82fe75c5eec88155c8503c93a631fe", - "size": 96705917, + "sha1": "e39ee9abb19076ef095f67469ca95ff5e7f600dc", + "sha256": "2c2e67ab762102f50df641280721164ddff0316e71081d5fa62f2a67365dea66", + "size": 101017377, "repo_path": "bios/Other/fbneo/fbneo/samples/sfz3mix.zip", "cores": [ "fbneo" diff --git a/install/retrodeck.json b/install/retrodeck.json index b6fbf166..9972fed9 100644 --- a/install/retrodeck.json +++ b/install/retrodeck.json @@ -5,7 +5,7 @@ "platform": "retrodeck", "display_name": "RetroDECK", "version": "1.0", - "generated": "2026-09-05T09:26:20Z", + "generated": "2026-09-13T22:10:18Z", "base_destination": "", "detect": [ { @@ -238,7 +238,7 @@ } ], "total_files": 3515, - "total_size": 6656314549, + "total_size": 6863533952, "total_omitted": 4, "omitted_files": [ { @@ -19433,19 +19433,21 @@ }, { "dest": "bios/fbneo/samples/donpachi.zip", - "sha1": "c0c0b5db93a61276a6fe6842049cb53e564b8413", - "sha256": "a686ebf893d511b5b7667176cd6844593e914595a8bb9ab656b2c66969013b5c", - "size": 5641310, - "repo_path": "bios/Other/fbneo/donpachi.zip", + "sha1": "d380fb29287eb7fc9ff901a7653ad40785f7deb1", + "sha256": "78a237f1e2774eb396e2f14999f1270597e0767a392729e7a4539455888b6fe0", + "size": 208549253, + "repo_path": "bios/Other/fbneo/fbneo/samples/donpachi.zip", "cores": [ "fbneo" - ] + ], + "storage": "release", + "release_asset": "donpachi.zip" }, { "dest": "bios/fbneo/samples/sfz3mix.zip", - "sha1": "227815ce9e55db427d68e075182caffe6ad60ad1", - "sha256": "6a720035646b3488a047ab163a6975b7be82fe75c5eec88155c8503c93a631fe", - "size": 96705917, + "sha1": "e39ee9abb19076ef095f67469ca95ff5e7f600dc", + "sha256": "2c2e67ab762102f50df641280721164ddff0316e71081d5fa62f2a67365dea66", + "size": 101017377, "repo_path": "bios/Other/fbneo/fbneo/samples/sfz3mix.zip", "cores": [ "fbneo" @@ -21549,10 +21551,10 @@ }, { "dest": "bios/dsi_firmware.bin", - "sha1": "93c0598b08192a1f630e5edb2cd9f60d1e3ea7b3", - "sha256": "1f89935da2888cd3e401af2774e06a6b587ca059e85f922826494f317d31c6ae", + "sha1": "d2a5af338f09c5cbdd5d7628db5b9c075c69b616", + "sha256": "1d7c771de535e6e07182fc9820ac32e76c6f0b5bcefd256a861fdfab144761ec", "size": 131072, - "repo_path": "bios/Arcade/Arcade/dsi_firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/dsi_firmware.bin", "cores": [ "melonds" ] @@ -25815,9 +25817,7 @@ "repo_path": "bios/Other/sdlpal/sdlpal/5.avi", "cores": [ "sdlpal" - ], - "storage": "release", - "release_asset": "5.avi" + ] }, { "dest": "bios/sdlpal/6.avi", @@ -29865,10 +29865,10 @@ }, { "dest": "bios/SkyEmu/firmware.bin", - "sha1": "5b2acbd499b39fb1af1b36ef7de5eb047d83401c", - "sha256": "7ea4f06ca110f8b7ff7c946f6e4e228d94977dadd4bb51570d14c68488edb32a", + "sha1": "cfe072921ee3fb93f688743f8beef89043c3e9ad", + "sha256": "57a1d6b43c1399d219ea18644240c4d4c6787c829f0a67ded216472867857cd0", "size": 262144, - "repo_path": "bios/Arcade/Arcade/firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/firmware.bin", "cores": [ "skyemu" ] diff --git a/install/retropie.json b/install/retropie.json index 74486809..1a9babe4 100644 --- a/install/retropie.json +++ b/install/retropie.json @@ -5,7 +5,7 @@ "platform": "retropie", "display_name": "RetroPie", "version": "1.0", - "generated": "2026-09-05T09:26:20Z", + "generated": "2026-09-13T22:10:18Z", "base_destination": "system", "detect": [ { @@ -16,7 +16,7 @@ ], "standalone_copies": [], "total_files": 1871, - "total_size": 5865446694, + "total_size": 6072666103, "total_omitted": 1, "omitted_files": [ { @@ -1625,10 +1625,10 @@ }, { "dest": "dsi_firmware.bin", - "sha1": "93c0598b08192a1f630e5edb2cd9f60d1e3ea7b3", - "sha256": "1f89935da2888cd3e401af2774e06a6b587ca059e85f922826494f317d31c6ae", + "sha1": "d2a5af338f09c5cbdd5d7628db5b9c075c69b616", + "sha256": "1d7c771de535e6e07182fc9820ac32e76c6f0b5bcefd256a861fdfab144761ec", "size": 131072, - "repo_path": "bios/Arcade/Arcade/dsi_firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/dsi_firmware.bin", "cores": null }, { @@ -6911,19 +6911,21 @@ }, { "dest": "fbneo/samples/donpachi.zip", - "sha1": "c0c0b5db93a61276a6fe6842049cb53e564b8413", - "sha256": "a686ebf893d511b5b7667176cd6844593e914595a8bb9ab656b2c66969013b5c", - "size": 5641310, - "repo_path": "bios/Other/fbneo/donpachi.zip", + "sha1": "d380fb29287eb7fc9ff901a7653ad40785f7deb1", + "sha256": "78a237f1e2774eb396e2f14999f1270597e0767a392729e7a4539455888b6fe0", + "size": 208549253, + "repo_path": "bios/Other/fbneo/fbneo/samples/donpachi.zip", "cores": [ "fbneo" - ] + ], + "storage": "release", + "release_asset": "donpachi.zip" }, { "dest": "fbneo/samples/sfz3mix.zip", - "sha1": "227815ce9e55db427d68e075182caffe6ad60ad1", - "sha256": "6a720035646b3488a047ab163a6975b7be82fe75c5eec88155c8503c93a631fe", - "size": 96705917, + "sha1": "e39ee9abb19076ef095f67469ca95ff5e7f600dc", + "sha256": "2c2e67ab762102f50df641280721164ddff0316e71081d5fa62f2a67365dea66", + "size": 101017377, "repo_path": "bios/Other/fbneo/fbneo/samples/sfz3mix.zip", "cores": [ "fbneo" @@ -9367,10 +9369,10 @@ }, { "dest": "d2fdc.zip", - "sha1": "bc39fbd5b9a8d2287ac5d0a42e639fc4d3c2f9d4", - "sha256": "e5e30615040567c1e7a2d21599681f8dac820edbdcda177b816a64d74b3a12f2", - "size": 256, - "repo_path": "bios/Apple/Apple II/DiskII/state-machine-16.rom", + "sha1": "af56c948598291b284a528f3fce06b961dba55e3", + "sha256": "69ee0f8f0862e1a623bcbe9870f9531f7976312f292a20d1aff7ed61b6085a69", + "size": 262, + "repo_path": "bios/Arcade/Arcade/a3fdc.zip", "cores": [ "mamemess" ] @@ -13379,9 +13381,7 @@ "repo_path": "bios/Other/sdlpal/sdlpal/5.avi", "cores": [ "sdlpal" - ], - "storage": "release", - "release_asset": "5.avi" + ] }, { "dest": "sdlpal/6.avi", @@ -15965,10 +15965,10 @@ }, { "dest": "SkyEmu/firmware.bin", - "sha1": "5b2acbd499b39fb1af1b36ef7de5eb047d83401c", - "sha256": "7ea4f06ca110f8b7ff7c946f6e4e228d94977dadd4bb51570d14c68488edb32a", + "sha1": "cfe072921ee3fb93f688743f8beef89043c3e9ad", + "sha256": "57a1d6b43c1399d219ea18644240c4d4c6787c829f0a67ded216472867857cd0", "size": 262144, - "repo_path": "bios/Arcade/Arcade/firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/firmware.bin", "cores": [ "skyemu" ] diff --git a/install/rocknix.json b/install/rocknix.json index b56b2f7e..e4b56244 100644 --- a/install/rocknix.json +++ b/install/rocknix.json @@ -5,7 +5,7 @@ "platform": "rocknix", "display_name": "ROCKNIX", "version": "1.0", - "generated": "2026-09-05T09:26:20Z", + "generated": "2026-09-13T22:10:18Z", "base_destination": "bios", "detect": [ { @@ -18,7 +18,7 @@ ], "standalone_copies": [], "total_files": 1738, - "total_size": 5743476850, + "total_size": 5950696259, "total_omitted": 1, "omitted_files": [ { @@ -4015,19 +4015,21 @@ }, { "dest": "fbneo/samples/donpachi.zip", - "sha1": "c0c0b5db93a61276a6fe6842049cb53e564b8413", - "sha256": "a686ebf893d511b5b7667176cd6844593e914595a8bb9ab656b2c66969013b5c", - "size": 5641310, - "repo_path": "bios/Other/fbneo/donpachi.zip", + "sha1": "d380fb29287eb7fc9ff901a7653ad40785f7deb1", + "sha256": "78a237f1e2774eb396e2f14999f1270597e0767a392729e7a4539455888b6fe0", + "size": 208549253, + "repo_path": "bios/Other/fbneo/fbneo/samples/donpachi.zip", "cores": [ "fbneo" - ] + ], + "storage": "release", + "release_asset": "donpachi.zip" }, { "dest": "fbneo/samples/sfz3mix.zip", - "sha1": "227815ce9e55db427d68e075182caffe6ad60ad1", - "sha256": "6a720035646b3488a047ab163a6975b7be82fe75c5eec88155c8503c93a631fe", - "size": 96705917, + "sha1": "e39ee9abb19076ef095f67469ca95ff5e7f600dc", + "sha256": "2c2e67ab762102f50df641280721164ddff0316e71081d5fa62f2a67365dea66", + "size": 101017377, "repo_path": "bios/Other/fbneo/fbneo/samples/sfz3mix.zip", "cores": [ "fbneo" @@ -7211,10 +7213,10 @@ }, { "dest": "d2fdc.zip", - "sha1": "bc39fbd5b9a8d2287ac5d0a42e639fc4d3c2f9d4", - "sha256": "e5e30615040567c1e7a2d21599681f8dac820edbdcda177b816a64d74b3a12f2", - "size": 256, - "repo_path": "bios/Apple/Apple II/DiskII/state-machine-16.rom", + "sha1": "af56c948598291b284a528f3fce06b961dba55e3", + "sha256": "69ee0f8f0862e1a623bcbe9870f9531f7976312f292a20d1aff7ed61b6085a69", + "size": 262, + "repo_path": "bios/Arcade/Arcade/a3fdc.zip", "cores": [ "mamemess" ] @@ -12543,9 +12545,7 @@ "repo_path": "bios/Other/sdlpal/sdlpal/5.avi", "cores": [ "sdlpal" - ], - "storage": "release", - "release_asset": "5.avi" + ] }, { "dest": "sdlpal/6.avi", @@ -15389,10 +15389,10 @@ }, { "dest": "SkyEmu/firmware.bin", - "sha1": "5b2acbd499b39fb1af1b36ef7de5eb047d83401c", - "sha256": "7ea4f06ca110f8b7ff7c946f6e4e228d94977dadd4bb51570d14c68488edb32a", + "sha1": "cfe072921ee3fb93f688743f8beef89043c3e9ad", + "sha256": "57a1d6b43c1399d219ea18644240c4d4c6787c829f0a67ded216472867857cd0", "size": 262144, - "repo_path": "bios/Arcade/Arcade/firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/firmware.bin", "cores": [ "skyemu" ] diff --git a/install/romm.json b/install/romm.json index 93a06d4a..c12ab6d8 100644 --- a/install/romm.json +++ b/install/romm.json @@ -5,7 +5,7 @@ "platform": "romm", "display_name": "RomM", "version": "1.0", - "generated": "2026-09-03T20:45:52Z", + "generated": "2026-09-13T22:10:18Z", "base_destination": "bios", "detect": [ { @@ -16,7 +16,7 @@ ], "standalone_copies": [], "total_files": 615, - "total_size": 1366505831, + "total_size": 1573725234, "total_omitted": 0, "omitted_files": [], "files": [ @@ -3332,19 +3332,21 @@ }, { "dest": "colecovision/fbneo/samples/donpachi.zip", - "sha1": "c0c0b5db93a61276a6fe6842049cb53e564b8413", - "sha256": "a686ebf893d511b5b7667176cd6844593e914595a8bb9ab656b2c66969013b5c", - "size": 5641310, - "repo_path": "bios/Other/fbneo/donpachi.zip", + "sha1": "d380fb29287eb7fc9ff901a7653ad40785f7deb1", + "sha256": "78a237f1e2774eb396e2f14999f1270597e0767a392729e7a4539455888b6fe0", + "size": 208549253, + "repo_path": "bios/Other/fbneo/fbneo/samples/donpachi.zip", "cores": [ "fbneo" - ] + ], + "storage": "release", + "release_asset": "donpachi.zip" }, { "dest": "colecovision/fbneo/samples/sfz3mix.zip", - "sha1": "227815ce9e55db427d68e075182caffe6ad60ad1", - "sha256": "6a720035646b3488a047ab163a6975b7be82fe75c5eec88155c8503c93a631fe", - "size": 96705917, + "sha1": "e39ee9abb19076ef095f67469ca95ff5e7f600dc", + "sha256": "2c2e67ab762102f50df641280721164ddff0316e71081d5fa62f2a67365dea66", + "size": 101017377, "repo_path": "bios/Other/fbneo/fbneo/samples/sfz3mix.zip", "cores": [ "fbneo" @@ -3808,10 +3810,10 @@ }, { "dest": "nds/dsi_firmware.bin", - "sha1": "93c0598b08192a1f630e5edb2cd9f60d1e3ea7b3", - "sha256": "1f89935da2888cd3e401af2774e06a6b587ca059e85f922826494f317d31c6ae", + "sha1": "d2a5af338f09c5cbdd5d7628db5b9c075c69b616", + "sha256": "1d7c771de535e6e07182fc9820ac32e76c6f0b5bcefd256a861fdfab144761ec", "size": 131072, - "repo_path": "bios/Arcade/Arcade/dsi_firmware.bin", + "repo_path": "bios/Nintendo/Nintendo DS/dsi_firmware.bin", "cores": [ "melonds" ] diff --git a/scripts/check_release_assets.py b/scripts/check_release_assets.py new file mode 100644 index 00000000..5dc6e263 --- /dev/null +++ b/scripts/check_release_assets.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python3 +"""Compare the large-files release with the files the collection keeps out of git. + +Every gitignored file under bios/ is served to the installer as an asset of +the large-files release, and install.py refuses a download whose +Content-Length differs from the manifest size. The manifest size is that of +the local copy, so an asset uploaded before the local file was rebuilt fails +every install of it until it is uploaded again. + +Usage: + python scripts/check_release_assets.py + python scripts/check_release_assets.py --json +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import subprocess +import sys + +sys.path.insert(0, os.path.dirname(__file__)) +from common import load_database +from largefiles import LARGE_FILES_RELEASE, LARGE_FILES_REPO + +Finding = tuple[str, str, int, int | None] + + +def expected_assets(db: dict, gitignore_text: str) -> dict[str, int]: + """Map each gitignored database path to the size the manifests carry.""" + ignored = { + line.strip() + for line in gitignore_text.splitlines() + if line.strip().startswith("bios/") + } + return { + entry["path"]: entry["size"] + for entry in db.get("files", {}).values() + if entry.get("path") in ignored + } + + +def _asset_names(path: str) -> list[str]: + """Names the release may publish a file under. + + GitHub rewrites spaces to dots in asset names, so a file whose name + carries spaces is published under the dotted form. + """ + name = os.path.basename(path) + return [name, name.replace(" ", ".")] if " " in name else [name] + + +def compare( + expected: dict[str, int], assets: dict[str, int], notes_current: bool = True +) -> list[Finding]: + """Files the release lacks or serves at another size, sorted by kind. + + A release description that no longer matches what render_notes() would + write is a finding of its own: the page is what a reader checks a + download against. + """ + findings: list[Finding] = [] + if not notes_current: + findings.append(("notes", "release description", 0, None)) + for path, size in sorted(expected.items()): + published = next( + (assets[name] for name in _asset_names(path) if name in assets), None + ) + if published is None: + findings.append(("missing", path, size, None)) + elif published != size: + findings.append(("size", path, size, published)) + return sorted(findings) + + +ASSET_URL = f"https://github.com/{LARGE_FILES_REPO}/releases/download/{LARGE_FILES_RELEASE}/" +NOT_INDEXED = "Not indexed" +SECTIONS = ( + "Console firmware", + "Arcade", + "Computer", + "Game engine data", + "Virtual machine firmware", + "Other", + NOT_INDEXED, +) +_ROW = re.compile( + r"^\| \[(?P[^\]]+)\]\([^)]*\) \| (?P[^|]*?) \| [^|]* \|" + r"(?: `(?P[0-9a-f]{40})` \|)?$", + re.M, +) +_HEADING = re.compile(r"^## (.+)$", re.M) + + +def parse_notes(body: str) -> dict[str, tuple[str, str, str]]: + """Name -> (section, description, sha1) for every row of a release page.""" + rows: dict[str, tuple[str, str, str]] = {} + section = "" + for line in body.splitlines(): + heading = _HEADING.match(line) + if heading: + section = heading.group(1).strip() + continue + row = _ROW.match(line) + if row: + rows[row.group("name")] = ( + section, row.group("desc").strip(), row.group("sha1") or "" + ) + return rows + + +def _section_for(path: str) -> str: + parts = path.split("/") + top = parts[1] if len(parts) > 1 else "" + if top in ("Nintendo", "Sony", "Sega", "Microsoft", "NEC", "SNK"): + return "Console firmware" + if top == "Arcade" or "/samples/" in path: + return "Arcade" + if top == "QEMU": + return "Virtual machine firmware" + if top in ("RPG Maker", "ScummVM"): + return "Game engine data" + if top in ("Apple", "Commodore", "Atari", "Sinclair", "Amstrad"): + return "Computer" + return "Other" + + +def _mib(size: int) -> str: + return f"{round(size / 1048576)} MB" + + +def render_notes( + db: dict, + gitignore_text: str, + assets: dict[str, int], + previous: str, + descriptions: dict[str, str], + bundles: dict[str, str], + cache_sha1: dict[str, str], +) -> str: + """The release page, from what the release actually serves. + + Every asset gets one row. A file the database indexes carries its + database SHA1; a data-directory bundle carries the hash of its cached + copy when one exists; anything else is listed apart with its size only, + so the page never vouches for bytes nobody indexed. Descriptions and + sections already written by hand are kept by name. + """ + known = parse_notes(previous) + ignored = { + line.strip() + for line in gitignore_text.splitlines() + if line.strip().startswith("bios/") + } + indexed: dict[str, tuple[str, str]] = {} + for sha1, entry in db.get("files", {}).items(): + path = entry.get("path", "") + if path in ignored: + name = os.path.basename(path) + for candidate in (name, name.replace(" ", ".")): + indexed[candidate] = (sha1, path) + + rows: dict[str, list[tuple[int, str]]] = {section: [] for section in SECTIONS} + for name, size in assets.items(): + section, description, previous_sha1 = known.get(name, ("", "", "")) + if name in indexed: + sha1, path = indexed[name] + section = section if section in SECTIONS[:-1] else _section_for(path) + stem = name.split(".")[0] + "." + name.split(".")[1] if name.count(".") > 1 else name + description = description or descriptions.get(name) or descriptions.get(stem) or path + elif name in bundles: + sha1 = cache_sha1.get(name, previous_sha1) + section = section if section in SECTIONS[:-1] else "Game engine data" + description = description or bundles[name] + else: + sha1 = "" + section = NOT_INDEXED + description = description or name + link = f"[{name}]({ASSET_URL}{name})" + if section == NOT_INDEXED: + line = f"| {link} | {description} | {_mib(size)} |" + else: + line = f"| {link} | {description} | {_mib(size)} | `{sha1}` |" + rows[section].append((-size, line)) + + out = [ + "Files too large for the git repository (GitHub 100MB limit), plus game engine data packs.", + "Downloaded automatically by `generate_pack.py` when not present locally.", + "Manual: `gh release download large-files`", + ] + for section in SECTIONS: + if not rows[section]: + continue + out += ["", f"## {section}", ""] + if section == NOT_INDEXED: + out += [ + "Assets the collection does not index: no database entry names them, " + "so no hash is vouched for here.", + "", + "| File | Description | Size |", + "|------|-------------|-----:|", + ] + else: + out += ["| File | Description | Size | SHA1 |", "|------|-------------|-----:|------|"] + out += [line for _, line in sorted(rows[section])] + total = sum(assets.values()) + out += ["", f"{len(assets)} files, {total / 1073741824:.1f} GB total. Verify a download with `sha1sum `.", ""] + return "\n".join(out) + + +def profile_descriptions(emulators_dir: str = "emulators") -> dict[str, str]: + """Name -> description, from the first profile that declares the file.""" + from common import load_emulator_profiles + + found: dict[str, str] = {} + for profile in load_emulator_profiles(emulators_dir).values(): + for entry in profile.get("files", []): + name = str(entry.get("name") or "") + if name and entry.get("description"): + found.setdefault(name, str(entry["description"])) + return found + + +def registry_bundles(registry_path: str = "platforms/_data_dirs.yml") -> dict[str, str]: + """Asset name -> description for data directories served from the release.""" + from common import yaml_load + + with open(registry_path, encoding="utf-8") as handle: + registry = yaml_load(handle) or {} + bundles: dict[str, str] = {} + for entry in (registry.get("data_directories") or registry).values(): + if not isinstance(entry, dict): + continue + url = str(entry.get("source_url") or "") + if url.startswith(ASSET_URL): + bundles[url[len(ASSET_URL):]] = str(entry.get("description") or "") + return bundles + + +def cache_hashes(names: set[str], cache_dir: str = ".cache/large") -> dict[str, str]: + """SHA1 of the cached copy of each named asset that the cache holds.""" + hashes: dict[str, str] = {} + for name in names: + path = os.path.join(cache_dir, name) + if os.path.isfile(path): + digest = hashlib.sha1() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1 << 20), b""): + digest.update(chunk) + hashes[name] = digest.hexdigest() + return hashes + + +def fetch_release() -> tuple[dict[str, int], str]: + """Name and size of every asset of the large-files release, and its page.""" + result = subprocess.run( + [ + "gh", "release", "view", LARGE_FILES_RELEASE, + "-R", LARGE_FILES_REPO, "--json", "assets,body", + ], + capture_output=True, text=True, check=True, + ) + data = json.loads(result.stdout) + return {a["name"]: a["size"] for a in data["assets"]}, data.get("body", "") + + +def fetch_assets() -> dict[str, int]: + """Name and size of every asset of the large-files release.""" + result = subprocess.run( + [ + "gh", "release", "view", LARGE_FILES_RELEASE, + "-R", LARGE_FILES_REPO, "--json", "assets", + ], + capture_output=True, text=True, check=True, + ) + return {a["name"]: a["size"] for a in json.loads(result.stdout)["assets"]} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + parser.add_argument("--db", default="database.json") + parser.add_argument("--gitignore", default=".gitignore") + parser.add_argument("--json", action="store_true") + parser.add_argument( + "--notes", metavar="FILE", + help="write the release page rendered from the collection to FILE", + ) + args = parser.parse_args() + + with open(args.gitignore, encoding="utf-8") as handle: + gitignore_text = handle.read() + db = load_database(args.db) + expected = expected_assets(db, gitignore_text) + try: + assets, body = fetch_release() + except (subprocess.CalledProcessError, FileNotFoundError, KeyError) as exc: + print(f"ERROR: cannot list release assets: {exc}", file=sys.stderr) + return 2 + bundles = registry_bundles() + rendered = render_notes( + db, gitignore_text, assets, body, profile_descriptions(), bundles, + cache_hashes(set(bundles)), + ) + if args.notes: + with open(args.notes, "w", encoding="utf-8") as handle: + handle.write(rendered) + findings = compare(expected, assets, notes_current=rendered.strip() == body.strip()) + + if args.json: + print(json.dumps( + [ + {"kind": kind, "path": path, "expected": size, "published": published} + for kind, path, size, published in findings + ], + indent=2, + )) + else: + print(f"{len(expected)} gitignored files, {len(assets)} release assets") + for kind, path, size, published in findings: + if kind == "missing": + print(f" MISSING {path} ({size} bytes)") + elif kind == "size": + print(f" SIZE {path}: local {size} != release {published}") + else: + print(" NOTES release description differs from the rendered page" + " (--notes FILE, then gh release edit large-files --notes-file FILE)") + if not findings: + print(" release assets and description match the collection") + return 1 if findings else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/dedup.py b/scripts/dedup.py index 36767ead..d01d31a8 100644 --- a/scripts/dedup.py +++ b/scripts/dedup.py @@ -230,7 +230,12 @@ def deduplicate(bios_dir: str, dry_run: bool = False) -> dict: # Write MAME clone mapping if mame_clones: - clone_path = "_mame_clones.json" + # Beside the scanned tree, where get_mame_clone_map() reads it for + # the real layout. A path relative to the working directory let a + # scan of a fixture tree rewrite the repository's own map. + clone_path = os.path.join( + os.path.dirname(os.path.abspath(bios_dir)), "_mame_clones.json" + ) # A group is only visible while both copies are on disk, and this run # has just deleted the clone. Writing only what was seen this time # therefore erases every mapping an earlier run recorded, and the diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 3666146c..c61717ca 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -2707,13 +2707,16 @@ def _load_gitignore_entries(repo_root: str) -> set[str]: return entries -def _is_large_file(local_path: str, repo_root: str) -> bool: - """Check if a file is a large file (>50MB or in .gitignore).""" - if local_path and os.path.exists(local_path): - if os.path.getsize(local_path) > 50_000_000: - return True +def _is_release_asset(local_path: str, repo_root: str) -> bool: + """Whether the installer fetches this file from the large-files release. + + The repository serves every committed file from its raw URL; only the + files kept out of git live as release assets, and .gitignore is the + ledger of those. Size is not the criterion: a committed file over 50 MB + is still served by the repository, and announcing it as a release asset + sends the installer to an asset nobody uploaded. + """ gitignore = _load_gitignore_entries(repo_root) - # Check if the path relative to repo root is in .gitignore try: rel = os.path.relpath(local_path, repo_root) except ValueError: @@ -2826,7 +2829,7 @@ def _manifest_core_entries( "cores": [source_emu] if source_emu else [], } - if _is_large_file(local_path or "", repo_root): + if _is_release_asset(local_path or "", repo_root): entry["storage"] = "release" entry["release_asset"] = ( os.path.basename(local_path) if local_path else fe["name"] @@ -3021,7 +3024,7 @@ def generate_manifest( sha256 = hashes["sha256"] repo_path = _get_repo_path(sha1, db) if sha1 else "" - is_release_asset = _is_large_file(local_path or "", repo_root) + is_release_asset = _is_release_asset(local_path or "", repo_root) # An entry needs somewhere to be fetched from. Resolution can # land on a file the database does not index -- a data diff --git a/scripts/pipeline.py b/scripts/pipeline.py index f7514a47..1932f619 100644 --- a/scripts/pipeline.py +++ b/scripts/pipeline.py @@ -6,6 +6,7 @@ Steps: 1b. provenance_report.py (dump-catalog coverage from provenance/) 1c. romset_recipes.py (archive identification, reconstruction targets) 2. refresh_data_dirs.py (update Dolphin Sys, PPSSPP, etc.) + 2b2. check_release_assets.py (large-files release vs gitignored files, online) 3. verify.py --all (check all platforms) 4. generate_pack.py --all (build ZIP packs) 4b. generate install manifests @@ -346,6 +347,20 @@ def main(): elif args.check_buildbot: print("\n--- 2b check buildbot system: SKIPPED (--offline) ---") + # Step 2b2: The release must serve the bytes the manifests describe. The + # installer compares Content-Length with the manifest size, so an asset + # uploaded before its local file was rebuilt fails every install of it. + if not args.offline: + ok, _ = run( + [sys.executable, "scripts/check_release_assets.py"], + "2b2 check release assets", + ) + results["check_release_assets"] = ok + all_ok = all_ok and ok + else: + print("\n--- 2b2 check release assets: SKIPPED (--offline) ---") + results["check_release_assets"] = SKIPPED + # Step 2c: Generate truth YAMLs # A targeted run writes its model in a subdirectory of its own, and the # diff has to read the same one or it compares a narrowed model against diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index 9f3faf24..bfd454df 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -1184,6 +1184,211 @@ class EveryManifestEntryIsFetchable(unittest.TestCase): for entry in data.get("omitted_files", []): self.assertIn(entry.get("reason"), allowed) + def _manifests(self) -> list[Path]: + paths = sorted((ROOT / "install").glob("*.json")) + paths += sorted((ROOT / "install" / "targets").glob("*.json")) + self.assertTrue(paths, "no install manifests to check") + return paths + + def test_repo_paths_name_the_file_the_database_holds(self): + """A manifest written before a file moved sends the installer to 404. + + database.json is regenerated whenever a file is refiled; the manifests + are a separate artefact and were left behind once (the two DS + firmware dumps of the 7 September refile). The installer fetches + repo_path from raw.githubusercontent.com, so a stale one is a + download that can never succeed. + """ + database = ROOT / "database.json" + if not database.is_file(): + self.skipTest("database.json not generated") + files = json.loads(database.read_text(encoding="utf-8"))["files"] + for path in self._manifests(): + with self.subTest(manifest=path.name): + data = json.loads(path.read_text(encoding="utf-8")) + stale = [ + (entry["dest"], entry["repo_path"], files.get(entry["sha1"], {}).get("path")) + for entry in data.get("files", []) + if entry.get("repo_path") + and files.get(entry.get("sha1", ""), {}).get("path") != entry["repo_path"] + ] + self.assertEqual(stale, [], f"{path.name} names moved files: {stale[:3]}") + + def test_release_entries_are_exactly_the_files_git_does_not_hold(self): + """The installer has two sources, and the manifest must pick the right one. + + A committed file is served by the repository; a gitignored one by + the large-files release. Marking a committed file as a release asset + sends the installer to an asset nobody uploaded (sdlpal/5.avi, 83 MB + in git, was announced as release asset `5.avi`). Marking a gitignored + file as a repo file sends it to a raw URL that has no content. + """ + tracked = set( + subprocess.run( + ["git", "ls-files", "-z", "--", "bios"], + capture_output=True, text=True, cwd=ROOT, check=True, + ).stdout.split("\0") + ) + if not tracked: + self.skipTest("not a git checkout") + for path in self._manifests(): + with self.subTest(manifest=path.name): + data = json.loads(path.read_text(encoding="utf-8")) + wrong = [] + for entry in data.get("files", []): + repo_path = entry.get("repo_path") + if not repo_path: + continue + committed = repo_path in tracked + if bool(entry.get("release_asset")) == committed: + wrong.append((entry["dest"], repo_path, "committed" if committed else "gitignored")) + self.assertEqual(wrong, [], f"{path.name} picks the wrong source: {wrong[:3]}") + + +class ReleaseAssetCriterion(unittest.TestCase): + """What decides whether the installer fetches from the release is git, not size. + + Files over 50 MB are supposed to be gitignored and uploaded to the + large-files release, but four committed files break that rule and one of + them reached a manifest as a release asset that does not exist. The + truthful question is whether the repository serves the bytes, and the + ledger of what it does not serve is .gitignore. + """ + + def _repo(self, directory: str, ignored: str) -> str: + Path(directory, ".gitignore").write_text(ignored + "\n", encoding="utf-8") + return directory + + def test_a_large_committed_file_is_served_by_the_repository(self): + from scripts import generate_pack + + with tempfile.TemporaryDirectory(dir=TMP_ROOT) as directory: + root = self._repo(directory, "bios/elsewhere.bin") + big = Path(root, "bios", "big.avi") + big.parent.mkdir() + with big.open("wb") as handle: + handle.truncate(60_000_000) + generate_pack._GITIGNORE_ENTRIES = None + try: + self.assertFalse(generate_pack._is_release_asset(str(big), root)) + finally: + generate_pack._GITIGNORE_ENTRIES = None + + def test_a_gitignored_file_is_served_by_the_release_whatever_its_size(self): + from scripts import generate_pack + + with tempfile.TemporaryDirectory(dir=TMP_ROOT) as directory: + root = self._repo(directory, "bios/small.zip") + small = Path(root, "bios", "small.zip") + small.parent.mkdir() + small.write_bytes(b"PK") + generate_pack._GITIGNORE_ENTRIES = None + try: + self.assertTrue(generate_pack._is_release_asset(str(small), root)) + finally: + generate_pack._GITIGNORE_ENTRIES = None + + +class ReleaseAssetsMatchTheCollection(unittest.TestCase): + """The bytes the release serves must be the bytes the manifests describe. + + install.py checks Content-Length against the manifest size before it + downloads, and the manifest size is that of the local copy. Both FBNeo + sample archives were rebuilt locally after their upload and never + re-uploaded: same members, different container, every install of them + failed. The comparison is offline here on a fixture; the script runs it + against the live release. + """ + + def test_missing_and_resized_assets_are_reported_and_nothing_else(self): + from scripts import check_release_assets + + expected = { + "bios/Other/fbneo/fbneo/samples/twotiger.zip": 154985212, + "bios/Arcade/MAME/MAME 0.174 Arcade XML.dat": 53677408, + "bios/Other/sdlpal/sdlpal/5.avi": 82833972, + } + assets = { + "twotiger.zip": 154888877, + "MAME.0.174.Arcade.XML.dat": 53677408, + "unrelated.zip": 1, + } + findings = check_release_assets.compare(expected, assets) + self.assertEqual( + findings, + [ + ("missing", "bios/Other/sdlpal/sdlpal/5.avi", 82833972, None), + ("size", "bios/Other/fbneo/fbneo/samples/twotiger.zip", 154985212, 154888877), + ], + ) + + def test_expected_assets_are_the_gitignored_database_entries(self): + from scripts import check_release_assets + + db = { + "files": { + "a" * 40: {"path": "bios/Other/x/big.zip", "size": 10}, + "b" * 40: {"path": "bios/Other/x/small.bin", "size": 2}, + } + } + gitignore = "# large\nbios/Other/x/big.zip\ntmp/\n" + self.assertEqual( + check_release_assets.expected_assets(db, gitignore), + {"bios/Other/x/big.zip": 10}, + ) + + def test_release_notes_are_rendered_from_the_collection(self): + """The release page is written by hand and drifts; it is rendered instead. + + Three assets had no row, three rows carried the SHA1 of an asset + that had since been replaced, and the footer counted 41 files for 44. + Hand-written descriptions survive by name; a new row takes its + description from the profile that declares the file; an asset the + collection does not index is listed apart, with its size only. + """ + from scripts import check_release_assets + + db = {"files": { + "a" * 40: {"path": "bios/Nintendo/Wii/nand.bin", "size": 553649152}, + "b" * 40: {"path": "bios/Other/fbneo/fbneo/samples/twotiger.zip", "size": 154985212}, + "c" * 40: {"path": "bios/Other/x/small.bin", "size": 2}, + }} + gitignore = "bios/Nintendo/Wii/nand.bin\nbios/Other/fbneo/fbneo/samples/twotiger.zip\n" + assets = {"nand.bin": 553649152, "twotiger.zip": 154985212, + "sdlpal-data.zip": 140773720, "whoopee.zip": 222665810} + previous = ( + "## Arcade\n\n| File | Description | Size | SHA1 |\n|---|---|---|---|\n" + "| [twotiger.zip](u) | Two Tigers samples | 148 MB | `" + "0" * 40 + "` |\n" + "| [whoopee.zip](u) | Toaplan Whoopee samples | 212 MB | `" + "9" * 40 + "` |\n" + ) + descriptions = {"nand.bin": "BootMii NAND backup"} + bundles = {"sdlpal-data.zip": "SDLPAL Chinese Paladin game data (.mkf archives)"} + body = check_release_assets.render_notes( + db, gitignore, assets, previous, descriptions, bundles, {"sdlpal-data.zip": "d" * 40} + ) + self.assertIn("| [twotiger.zip](https://github.com/Abdess/retrobios/releases/download/large-files/twotiger.zip) | Two Tigers samples | 148 MB | `" + "b" * 40 + "` |", body) + self.assertIn("| [nand.bin](https://github.com/Abdess/retrobios/releases/download/large-files/nand.bin) | BootMii NAND backup | 528 MB | `" + "a" * 40 + "` |", body) + self.assertIn("| [sdlpal-data.zip](https://github.com/Abdess/retrobios/releases/download/large-files/sdlpal-data.zip) | SDLPAL Chinese Paladin game data (.mkf archives) | 134 MB | `" + "d" * 40 + "` |", body) + self.assertIn("## Not indexed", body) + self.assertIn("| [whoopee.zip](https://github.com/Abdess/retrobios/releases/download/large-files/whoopee.zip) | Toaplan Whoopee samples | 212 MB |", body) + self.assertNotIn("9" * 40, body) + self.assertNotIn("small.bin", body) + self.assertIn("4 files, 1.0 GB total", body) + self.assertEqual(body.index("## Console firmware"), min( + body.index(h) for h in ("## Console firmware", "## Arcade", "## Not indexed") + )) + + def test_stale_notes_are_a_finding(self): + from scripts import check_release_assets + + findings = check_release_assets.compare({}, {}, notes_current=False) + self.assertEqual(findings, [("notes", "release description", 0, None)]) + + def test_pipeline_runs_the_check_online_and_marks_it_skipped_offline(self): + source = (ROOT / "scripts" / "pipeline.py").read_text(encoding="utf-8") + self.assertIn("check_release_assets.py", source) + self.assertIn('results["check_release_assets"] = SKIPPED', source) + class PreservedBytesAreNeverNormalised(unittest.TestCase): """git must not rewrite a preserved file's line endings. diff --git a/tests/test_dedup.py b/tests/test_dedup.py index efc2c246..5aee288f 100644 --- a/tests/test_dedup.py +++ b/tests/test_dedup.py @@ -204,6 +204,23 @@ class CloneMapSurvivesASecondRun(_Bios): path = Path(self._tmp.name) / "_mame_clones.json" return json.loads(path.read_text()) if path.exists() else {} + def test_the_map_is_written_beside_the_scanned_tree_not_the_cwd(self): + """A test or a run from another directory must not touch the repo's map. + + The map was written to the working directory. The test suite runs + from the repo root, so a fixture scan of a temporary tree rewrote the + committed _mame_clones.json with two fixture entries, and a later + rewrite left 2 of its 71 parents. + """ + elsewhere = Path(self._tmp.name) / "elsewhere" + elsewhere.mkdir() + os.chdir(elsewhere) + self.write("Arcade/MAME/bbc_m87.zip") + self.write("Arcade/MAME/bbc_24bbc.zip") + self.run_dedup() + self.assertFalse((elsewhere / "_mame_clones.json").exists()) + self.assertIn("bbc_m87.zip", self._map()) + def test_a_recorded_mapping_survives_a_later_run(self): self.write("Arcade/MAME/bbc_m87.zip") self.write("Arcade/MAME/bbc_24bbc.zip") diff --git a/wiki/release-process.md b/wiki/release-process.md index 47d6c293..447d0416 100644 --- a/wiki/release-process.md +++ b/wiki/release-process.md @@ -132,6 +132,22 @@ gh release upload large-files "bios/Sony/PS3/PS3UPDAT.PUP#PS3UPDAT.PUP" **Local cache.** `generate_pack.py` calls `fetch_large_file()` which downloads from the release and caches in `.cache/large/` for subsequent runs. +**Check.** The installer refuses a download whose `Content-Length` differs +from the manifest size, and the manifest size is that of the local file. A +file rebuilt locally after its upload therefore fails every install until it +is uploaded again (`--clobber`). `python scripts/check_release_assets.py` +compares every gitignored `bios/` path in the database with the asset of the +same name, and the release page with the one it renders from the collection; +the online pipeline runs it as step 2b2. To refresh the page: + +```bash +python scripts/check_release_assets.py --notes tmp/notes.md +gh release edit large-files --notes-file tmp/notes.md +``` + +An asset no database entry names is listed on the page under "Not indexed", +with its size only: the collection vouches for no hash it does not hold. + ## Cutting a release Releasing is deliberate and local. Nothing on GitHub builds a pack: the