chore: merge pack and verify fixes

This commit is contained in:
Abdessamad Derraz committed 2026-11-06 10:28:27 +01:00
commit d0c65dca22
37 files changed
+3267 -142

No files matched your search

+4 -4
View File
@@ -83,7 +83,7 @@ These are the files an emulator loads from disk instead of carrying inside itsel
Each file is checked the way your platform checks it. Most compare a checksum, the fingerprint of a file's contents, which catches a corrupt or unexpected copy. RetroArch, Lakka and RetroPie check that the file is present under its name without reading its contents (`path_is_valid` in RetroArch's `core_info.c`), and MinUI, NextUI and MyMinUI hand each core its folder without opening a file in it: the Coverage table says which applies to you. Independently of that, the collection records five fingerprints per file, and wherever an emulator's code states an expected size or hash, that value is read from its source and rechecked here. Each file is checked the way your platform checks it. Most compare a checksum, the fingerprint of a file's contents, which catches a corrupt or unexpected copy. RetroArch, Lakka and RetroPie check that the file is present under its name without reading its contents (`path_is_valid` in RetroArch's `core_info.c`), and MinUI, NextUI and MyMinUI hand each core its folder without opening a file in it: the Coverage table says which applies to you. Independently of that, the collection records five fingerprints per file, and wherever an emulator's code states an expected size or hash, that value is read from its source and rechecked here.
- **1,086 files** the platforms' emulators load are still to be found, and 138 more the collection does not hold and does not serve by name (retail data, per-user keys, user-filled slots), each with its reason. Both are named in the [gap analysis](https://abdess.github.io/retrobios/gaps/) - **1,087 files** the platforms' emulators load are still to be found, and 138 more the collection does not hold and does not serve by name (retail data, per-user keys, user-filled slots), each with its reason. Both are named in the [gap analysis](https://abdess.github.io/retrobios/gaps/)
- **18 platforms** supported with platform-specific verification - **18 platforms** supported with platform-specific verification
- **623 emulators** profiled from source (RetroArch cores + standalone) - **623 emulators** profiled from source (RetroArch cores + standalone)
- **532 systems** handled by those emulators (NES, SNES, PlayStation, Saturn, Dreamcast, ...) - **532 systems** handled by those emulators (NES, SNES, PlayStation, Saturn, Dreamcast, ...)
@@ -102,11 +102,11 @@ Full list with per-file details: **[https://abdess.github.io/retrobios/](https:/
| Platform | On its BIOS list | Files its emulators load | Checked by | | Platform | On its BIOS list | Files its emulators load | Checked by |
|----------|-----------------:|-------------------------:|------------| |----------|-----------------:|-------------------------:|------------|
| AmberELEC | 414/434 | 4,754/4,937 | MD5 | | AmberELEC | 414/434 | 4,754/4,937 | MD5 |
| Batocera | 353/353 | 14,173/15,292 | MD5 | | Batocera | 353/353 | 14,172/15,292 | MD5 |
| BizHawk | 118/118 | - | SHA-1 | | BizHawk | 118/118 | - | SHA-1 |
| EmuDeck | 168/168 | 8,427/8,529 | MD5 | | EmuDeck | 168/168 | 8,427/8,529 | MD5 |
| EmuELEC | 543/572 | 4,788/10,217 | MD5 | | EmuELEC | 543/572 | 4,788/10,217 | MD5 |
| KNULLI | 290/290 | 9,128/15,351 | MD5 | | KNULLI | 290/290 | 9,127/15,351 | MD5 |
| Lakka | 532/532 | 7,910/7,995 | name only | | Lakka | 532/532 | 7,910/7,995 | name only |
| MiSTer FPGA | 81/81 | - | MD5 | | MiSTer FPGA | 81/81 | - | MD5 |
| MinUI | 0/0 | 78/79 | name only | | MinUI | 0/0 | 78/79 | name only |
@@ -208,4 +208,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 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 project's legal position](https://abdess.github.io/retrobios/wiki/legal/). The reasoning, and where it is weakest, is in [the project's legal position](https://abdess.github.io/retrobios/wiki/legal/).
*Auto-generated on 2026-11-06T05:38:30Z* *Auto-generated on 2026-11-06T08:56:19Z*
+163 -2
View File
@@ -2106,6 +2106,10 @@
"repo_path": "bios/Atari/400-800/ATARIXL.ROM", "repo_path": "bios/Atari/400-800/ATARIXL.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"atari-400-800",
"atari-5200"
],
"declared_in": [
"atari-400-800" "atari-400-800"
], ],
"alternatives": [ "alternatives": [
@@ -2128,6 +2132,10 @@
"repo_path": "bios/Atari/400-800/ATARIBAS.ROM", "repo_path": "bios/Atari/400-800/ATARIBAS.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"atari-400-800",
"atari-5200"
],
"declared_in": [
"atari-400-800" "atari-400-800"
], ],
"alternatives": [ "alternatives": [
@@ -2150,6 +2158,10 @@
"repo_path": "bios/Atari/400-800/ATARIOSA.ROM", "repo_path": "bios/Atari/400-800/ATARIOSA.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"atari-400-800",
"atari-5200"
],
"declared_in": [
"atari-400-800" "atari-400-800"
], ],
"alternatives": [ "alternatives": [
@@ -2172,6 +2184,10 @@
"repo_path": "bios/Atari/400-800/ATARIOSB.ROM", "repo_path": "bios/Atari/400-800/ATARIOSB.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"atari-400-800",
"atari-5200"
],
"declared_in": [
"atari-400-800" "atari-400-800"
], ],
"alternatives": [ "alternatives": [
@@ -2194,6 +2210,10 @@
"repo_path": "bios/Atari/5200/5200.rom", "repo_path": "bios/Atari/5200/5200.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"atari-400-800",
"atari-5200"
],
"declared_in": [
"atari-5200" "atari-5200"
], ],
"alternatives": [ "alternatives": [
@@ -2361,6 +2381,11 @@
"repo_path": "bios/Microsoft/MSX/Databases/.variants/colecodb.xml.78113162", "repo_path": "bios/Microsoft/MSX/Databases/.variants/colecodb.xml.78113162",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"coleco-colecovision" "coleco-colecovision"
], ],
"alternatives": [ "alternatives": [
@@ -3432,6 +3457,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/ARAB1.ROM", "repo_path": "bios/Microsoft/MSX/Shared Roms/ARAB1.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -3454,6 +3484,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/ARABIC.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/ARABIC.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -3476,6 +3511,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/BEERIDE.ROM", "repo_path": "bios/Microsoft/MSX/Shared Roms/BEERIDE.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -3498,6 +3538,11 @@
"repo_path": "bios/Microsoft/MSX/FMPAC.ROM", "repo_path": "bios/Microsoft/MSX/FMPAC.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -3520,6 +3565,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/GCVMX80.ROM", "repo_path": "bios/Microsoft/MSX/Shared Roms/GCVMX80.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -3542,6 +3592,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/HANGUL.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/HANGUL.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -3564,6 +3619,11 @@
"repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_kanjifont.rom", "repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_kanjifont.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -3586,6 +3646,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/MICROSOLDISK.ROM", "repo_path": "bios/Microsoft/MSX/Shared Roms/MICROSOLDISK.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -3608,6 +3673,11 @@
"repo_path": "bios/Microsoft/MSX/openmsx/yrw801.rom", "repo_path": "bios/Microsoft/MSX/openmsx/yrw801.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -4301,6 +4371,11 @@
"repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_kanjibasic.rom", "repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_kanjibasic.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -4510,6 +4585,11 @@
"repo_path": "bios/Microsoft/MSX/share/systemroms/fs-5500_disk.rom", "repo_path": "bios/Microsoft/MSX/share/systemroms/fs-5500_disk.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -4532,6 +4612,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/NOVAXIS.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/NOVAXIS.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -4554,6 +4639,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/nowindDos1.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/nowindDos1.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -4576,6 +4666,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/nowindDos2.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/nowindDos2.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -4598,6 +4693,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/PAINT.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/PAINT.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -4620,6 +4720,11 @@
"repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_disk.rom", "repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_disk.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -4642,6 +4747,11 @@
"repo_path": "bios/Microsoft/MSX/nms8250_disk.rom", "repo_path": "bios/Microsoft/MSX/nms8250_disk.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -4664,6 +4774,11 @@
"repo_path": "bios/Other/msx-emu/Machines/Shared Roms/RS232.ROM", "repo_path": "bios/Other/msx-emu/Machines/Shared Roms/RS232.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -4697,6 +4812,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/SUNRISEIDE.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/SUNRISEIDE.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -4719,6 +4839,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/SWP.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/SWP.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -4741,6 +4866,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/XBASIC2.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/XBASIC2.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -5316,6 +5446,11 @@
"repo_path": "bios/Nintendo/SNES/BS-X.bin", "repo_path": "bios/Nintendo/SNES/BS-X.bin",
"cores": null, "cores": null,
"systems": [ "systems": [
"nintendo-gb",
"nintendo-gbc",
"nintendo-satellaview"
],
"declared_in": [
"nintendo-satellaview" "nintendo-satellaview"
], ],
"alternatives": [ "alternatives": [
@@ -5512,6 +5647,9 @@
"north-america", "north-america",
"oceania" "oceania"
], ],
"region_groups": [
"sega-master-system"
],
"region_system_groups": [ "region_system_groups": [
"sega-master-system" "sega-master-system"
], ],
@@ -5541,6 +5679,9 @@
"regions": [ "regions": [
"japan" "japan"
], ],
"region_groups": [
"sega-master-system"
],
"region_system_groups": [ "region_system_groups": [
"sega-master-system" "sega-master-system"
], ],
@@ -5571,6 +5712,9 @@
"europe", "europe",
"oceania" "oceania"
], ],
"region_groups": [
"sega-mega-cd"
],
"region_system_groups": [ "region_system_groups": [
"sega-mega-cd" "sega-mega-cd"
], ],
@@ -5602,6 +5746,9 @@
"latin-america", "latin-america",
"north-america" "north-america"
], ],
"region_groups": [
"sega-mega-cd"
],
"region_system_groups": [ "region_system_groups": [
"sega-mega-cd" "sega-mega-cd"
], ],
@@ -5632,6 +5779,9 @@
"regions": [ "regions": [
"japan" "japan"
], ],
"region_groups": [
"sega-mega-cd"
],
"region_system_groups": [ "region_system_groups": [
"sega-mega-cd" "sega-mega-cd"
], ],
@@ -6104,6 +6254,9 @@
"regions": [ "regions": [
"japan" "japan"
], ],
"region_groups": [
"sony-playstation"
],
"region_system_groups": [ "region_system_groups": [
"sony-playstation" "sony-playstation"
], ],
@@ -6136,6 +6289,9 @@
"latin-america", "latin-america",
"north-america" "north-america"
], ],
"region_groups": [
"sony-playstation"
],
"region_system_groups": [ "region_system_groups": [
"sony-playstation" "sony-playstation"
], ],
@@ -6168,6 +6324,9 @@
"europe", "europe",
"oceania" "oceania"
], ],
"region_groups": [
"sony-playstation"
],
"region_system_groups": [ "region_system_groups": [
"sony-playstation" "sony-playstation"
], ],
@@ -8987,7 +9146,8 @@
], ],
"region_groups": [ "region_groups": [
"sony-playstation" "sony-playstation"
] ],
"priority": 10
}, },
{ {
"dest": "bios/scph1002a.bin", "dest": "bios/scph1002a.bin",
@@ -9233,7 +9393,8 @@
], ],
"region_groups": [ "region_groups": [
"sony-playstation" "sony-playstation"
] ],
"priority": 10
}, },
{ {
"dest": "bios/scph7002.bin", "dest": "bios/scph7002.bin",
+1 -2
View File
@@ -1841,8 +1841,7 @@
"reason": "not_found", "reason": "not_found",
"cores": [ "cores": [
"dosbox-x" "dosbox-x"
], ]
"held": true
}, },
{ {
"dest": "extras.wad", "dest": "extras.wad",
+150
View File
@@ -3363,6 +3363,10 @@
"repo_path": "bios/Atari/400-800/ATARIXL.ROM", "repo_path": "bios/Atari/400-800/ATARIXL.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"atari-400-800",
"atari-5200"
],
"declared_in": [
"atari-400-800" "atari-400-800"
], ],
"alternatives": [ "alternatives": [
@@ -3385,6 +3389,10 @@
"repo_path": "bios/Atari/400-800/ATARIBAS.ROM", "repo_path": "bios/Atari/400-800/ATARIBAS.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"atari-400-800",
"atari-5200"
],
"declared_in": [
"atari-400-800" "atari-400-800"
], ],
"alternatives": [ "alternatives": [
@@ -3407,6 +3415,10 @@
"repo_path": "bios/Atari/400-800/ATARIOSA.ROM", "repo_path": "bios/Atari/400-800/ATARIOSA.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"atari-400-800",
"atari-5200"
],
"declared_in": [
"atari-400-800" "atari-400-800"
], ],
"alternatives": [ "alternatives": [
@@ -3429,6 +3441,10 @@
"repo_path": "bios/Atari/400-800/ATARIOSB.ROM", "repo_path": "bios/Atari/400-800/ATARIOSB.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"atari-400-800",
"atari-5200"
],
"declared_in": [
"atari-400-800" "atari-400-800"
], ],
"alternatives": [ "alternatives": [
@@ -3451,6 +3467,10 @@
"repo_path": "bios/Atari/5200/5200.rom", "repo_path": "bios/Atari/5200/5200.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"atari-400-800",
"atari-5200"
],
"declared_in": [
"atari-5200" "atari-5200"
], ],
"alternatives": [ "alternatives": [
@@ -3686,6 +3706,11 @@
"repo_path": "bios/Microsoft/MSX/Databases/.variants/colecodb.xml.78113162", "repo_path": "bios/Microsoft/MSX/Databases/.variants/colecodb.xml.78113162",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"coleco-colecovision" "coleco-colecovision"
], ],
"alternatives": [ "alternatives": [
@@ -4923,6 +4948,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/ARAB1.ROM", "repo_path": "bios/Microsoft/MSX/Shared Roms/ARAB1.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -4945,6 +4975,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/ARABIC.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/ARABIC.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -4967,6 +5002,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/BEERIDE.ROM", "repo_path": "bios/Microsoft/MSX/Shared Roms/BEERIDE.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -4989,6 +5029,11 @@
"repo_path": "bios/Microsoft/MSX/FMPAC.ROM", "repo_path": "bios/Microsoft/MSX/FMPAC.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -5011,6 +5056,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/GCVMX80.ROM", "repo_path": "bios/Microsoft/MSX/Shared Roms/GCVMX80.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -5033,6 +5083,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/HANGUL.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/HANGUL.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -5055,6 +5110,11 @@
"repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_kanjifont.rom", "repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_kanjifont.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -5077,6 +5137,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/MICROSOLDISK.ROM", "repo_path": "bios/Microsoft/MSX/Shared Roms/MICROSOLDISK.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -5099,6 +5164,11 @@
"repo_path": "bios/Microsoft/MSX/openmsx/yrw801.rom", "repo_path": "bios/Microsoft/MSX/openmsx/yrw801.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -5792,6 +5862,11 @@
"repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_kanjibasic.rom", "repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_kanjibasic.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -6001,6 +6076,11 @@
"repo_path": "bios/Microsoft/MSX/share/systemroms/fs-5500_disk.rom", "repo_path": "bios/Microsoft/MSX/share/systemroms/fs-5500_disk.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -6023,6 +6103,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/NOVAXIS.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/NOVAXIS.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -6045,6 +6130,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/nowindDos1.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/nowindDos1.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -6067,6 +6157,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/nowindDos2.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/nowindDos2.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -6089,6 +6184,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/PAINT.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/PAINT.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -6111,6 +6211,11 @@
"repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_disk.rom", "repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_disk.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -6133,6 +6238,11 @@
"repo_path": "bios/Microsoft/MSX/nms8250_disk.rom", "repo_path": "bios/Microsoft/MSX/nms8250_disk.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -6155,6 +6265,11 @@
"repo_path": "bios/Other/msx-emu/Machines/Shared Roms/RS232.ROM", "repo_path": "bios/Other/msx-emu/Machines/Shared Roms/RS232.ROM",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -6188,6 +6303,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/SUNRISEIDE.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/SUNRISEIDE.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -6210,6 +6330,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/SWP.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/SWP.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -6232,6 +6357,11 @@
"repo_path": "bios/Microsoft/MSX/Shared Roms/XBASIC2.rom", "repo_path": "bios/Microsoft/MSX/Shared Roms/XBASIC2.rom",
"cores": null, "cores": null,
"systems": [ "systems": [
"coleco-colecovision",
"microsoft-msx",
"sc-3000"
],
"declared_in": [
"microsoft-msx" "microsoft-msx"
], ],
"alternatives": [ "alternatives": [
@@ -6694,6 +6824,11 @@
"repo_path": "bios/Nintendo/SNES/Satellaview_BS-X.sfc", "repo_path": "bios/Nintendo/SNES/Satellaview_BS-X.sfc",
"cores": null, "cores": null,
"systems": [ "systems": [
"nintendo-gb",
"nintendo-gbc",
"nintendo-satellaview"
],
"declared_in": [
"nintendo-satellaview" "nintendo-satellaview"
], ],
"alternatives": [ "alternatives": [
@@ -6895,6 +7030,9 @@
"north-america", "north-america",
"oceania" "oceania"
], ],
"region_groups": [
"sega-master-system"
],
"region_system_groups": [ "region_system_groups": [
"sega-master-system" "sega-master-system"
], ],
@@ -6924,6 +7062,9 @@
"regions": [ "regions": [
"japan" "japan"
], ],
"region_groups": [
"sega-master-system"
],
"region_system_groups": [ "region_system_groups": [
"sega-master-system" "sega-master-system"
], ],
@@ -6954,6 +7095,9 @@
"europe", "europe",
"oceania" "oceania"
], ],
"region_groups": [
"sega-mega-cd"
],
"region_system_groups": [ "region_system_groups": [
"sega-mega-cd" "sega-mega-cd"
], ],
@@ -6985,6 +7129,9 @@
"latin-america", "latin-america",
"north-america" "north-america"
], ],
"region_groups": [
"sega-mega-cd"
],
"region_system_groups": [ "region_system_groups": [
"sega-mega-cd" "sega-mega-cd"
], ],
@@ -7015,6 +7162,9 @@
"regions": [ "regions": [
"japan" "japan"
], ],
"region_groups": [
"sega-mega-cd"
],
"region_system_groups": [ "region_system_groups": [
"sega-mega-cd" "sega-mega-cd"
], ],
+1 -1
View File
@@ -1 +1 @@
{"files":[["bios/128p-0.rom",16384,"bios"],["bios/128p-1.rom",16384,"bios"],["bios/256s-0.rom",16384,"bios"],["bios/256s-1.rom",16384,"bios"],["bios/256s-2.rom",16384,"bios"],["bios/256s-3.rom",16384,"bios"],["bios/32X_G_BIOS.BIN",256,"bios"],["bios/32X_M_BIOS.BIN",2048,"bios"],["bios/32X_S_BIOS.BIN",1024,"bios"],["bios/3do_arcade_saot.bin",524288,"bios"],["bios/5200.rom",2048,"bios"],["bios/64DD_IPL.bin",4194304,"bios"],["bios/7800 BIOS (E).rom",16384,"bios"],["bios/7800 BIOS (U).rom",4096,"bios"],["bios/ATARIBAS.ROM",8192,"bios"],["bios/ATARIOSA.ROM",10240,"bios"],["bios/ATARIOSB.ROM",10240,"bios"],["bios/ATARIXL.ROM",16384,"bios"],["bios/BB01R4_OS.ROM",16384,"bios"],["bios/BIOS.col",8192,"bios"],["bios/BLOOD.RFF",3536782,"game_data"],["bios/BS-X.bin",1048576,"bios"],["bios/CD32 Extended-ROM r40.60 (1993)(Commodore)(CD32).rom",524288,"bios"],["bios/CDTV Extended-ROM v1.0 (1991)(Commodore)(CDTV)[!].rom",262144,"bios"],["bios/CM32LN_CONTROL.ROM",65536,"bios"],["bios/CM32L_CONTROL.ROM",65536,"bios"],["bios/CM32L_PCM.ROM",1048576,"bios"],["bios/DOSBOX.SF2",32319396,"bios"],["bios/DUKE3D.GRP",11035779,"game_data"],["bios/Databases/colecodb.xml",164848,"bios"],["bios/Databases/msxcaswavdb.xml",819679,"bios"],["bios/Databases/msxromdb.xml",930986,"bios"],["bios/Databases/msxsysromdb.xml",157542,"bios"],["bios/Databases/segadb.xml",82318,"bios"],["bios/Databases/svidb.xml",261070,"bios"],["bios/ECS.bin",24576,"bios"],["bios/GUI.RFF",7145,"game_data"],["bios/IVOICE.bin",2048,"bios"],["bios/JiffyDOS_1541-II.bin",16384,"bios"],["bios/JiffyDOS_1571_repl310654.bin",32768,"bios"],["bios/JiffyDOS_1581.bin",32768,"bios"],["bios/JiffyDOS_C128.bin",16384,"bios"],["bios/JiffyDOS_C64.bin",8192,"bios"],["bios/JiffyDOS_SX-64.bin",8192,"bios"],["bios/Kickstart v1.2 r33.180 (1986)(Commodore)(A500-A1000-A2000)[!].rom",262144,"bios"],["bios/Kickstart v1.2 rev 33.166 (1986)(Commodore)(A1000).rom",262144,"bios"],["bios/Kickstart v1.3 r34.5 (1987)(Commodore)(A500-A1000-A2000-CDTV)[!].rom",262144,"bios"],["bios/Kickstart v1.3 r34.5 (1987)(Commodore)(A500-A1000-A2000-CDTV)[o].rom",524288,"bios"],["bios/Kickstart v2.04 r37.175 (1991)(Commodore)(A500+)[!].rom",524288,"bios"],["bios/Kickstart v2.05 r37.350 (1992)(Commodore)(A600HD)[!].rom",524288,"bios"],["bios/Kickstart v3.1 r40.60 (1993)(Commodore)(CD32).rom",524288,"bios"],["bios/Kickstart v3.1 r40.68 (1993)(Commodore)(A1200)[!].rom",524288,"bios"],["bios/Kickstart v3.1 r40.68 (1993)(Commodore)(A3000).rom",524288,"bios"],["bios/Kickstart v3.1 r40.68 (1993)(Commodore)(A4000).rom",524288,"bios"],["bios/MT32_CONTROL.ROM",65536,"bios"],["bios/MT32_PCM.ROM",524288,"bios"],["bios/Machines/COL - Bit Corporation Dina/czz50-1.rom",8192,"bios"],["bios/Machines/COL - Bit Corporation Dina/czz50-2.rom",8192,"bios"],["bios/Machines/COL - ColecoVision with Opcode Memory Extension/coleco.rom",8192,"bios"],["bios/Machines/COL - ColecoVision/coleco.rom",8192,"bios"],["bios/Machines/COL - ColecoVision/config.ini",347,"bios"],["bios/Machines/COL - Spectravideo SVI-603 Coleco/SVI603.ROM",8192,"bios"],["bios/Machines/Forte II Games - Pesadelo/pesadelo.rom",65536,"bios"],["bios/Machines/MSX - C-BIOS/cbios.txt",8924,"bios"],["bios/Machines/MSX - C-BIOS/cbios_logo_msx1.rom",16384,"bios"],["bios/Machines/MSX - C-BIOS/cbios_main_msx1.rom",32768,"bios"],["bios/Machines/MSX/MSX.ROM",32768,"bios"],["bios/Machines/MSX/MSX2.ROM",32768,"bios"],["bios/Machines/MSX/MSX2EXT.ROM",16384,"bios"],["bios/Machines/MSX/MSX2P.ROM",32768,"bios"],["bios/Machines/MSX/MSX2PEXT.ROM",16384,"bios"],["bios/Machines/MSX/MSXDOS2.ROM",65536,"bios"],["bios/Machines/MSX/config.ini",408,"bios"],["bios/Machines/MSX2 - C-BIOS/cbios.txt",8924,"bios"],["bios/Machines/MSX2 - C-BIOS/cbios_logo_msx2.rom",16384,"bios"],["bios/Machines/MSX2 - C-BIOS/cbios_main_msx2.rom",32768,"bios"],["bios/Machines/MSX2 - C-BIOS/cbios_sub.rom",16384,"bios"],["bios/Machines/MSX2+ - C-BIOS/cbios.txt",8924,"bios"],["bios/Machines/MSX2+ - C-BIOS/cbios_logo_msx2+.rom",16384,"bios"],["bios/Machines/MSX2+ - C-BIOS/cbios_main_msx2+.rom",32768,"bios"],["bios/Machines/MSX2+ - C-BIOS/cbios_music.rom",16384,"bios"],["bios/Machines/MSX2+ - C-BIOS/cbios_sub.rom",16384,"bios"],["bios/Machines/MSX2+/MSX.ROM",32768,"bios"],["bios/Machines/MSX2+/MSX2.ROM",32768,"bios"],["bios/Machines/MSX2+/MSX2EXT.ROM",16384,"bios"],["bios/Machines/MSX2+/MSX2P.ROM",32768,"bios"],["bios/Machines/MSX2+/MSX2PEXT.ROM",16384,"bios"],["bios/Machines/MSX2+/MSXDOS2.ROM",65536,"bios"],["bios/Machines/MSX2+/config.ini",773,"bios"],["bios/Machines/MSX2/MSX.ROM",32768,"bios"],["bios/Machines/MSX2/MSX2.ROM",32768,"bios"],["bios/Machines/MSX2/MSX2EXT.ROM",16384,"bios"],["bios/Machines/MSX2/MSX2P.ROM",32768,"bios"],["bios/Machines/MSX2/MSX2PEXT.ROM",16384,"bios"],["bios/Machines/MSX2/MSXDOS2.ROM",65536,"bios"],["bios/Machines/MSX2/config.ini",649,"bios"],["bios/Machines/MSXturboR/config.ini",941,"bios"],["bios/Machines/SVI - Spectravideo SVI-318/svi318.rom",32768,"bios"],["bios/Machines/SVI - Spectravideo SVI-328 Line truncated {"files":[["bios/128p-0.rom",16384,"bios"],["bios/128p-1.rom",16384,"bios"],["bios/256s-0.rom",16384,"bios"],["bios/256s-1.rom",16384,"bios"],["bios/256s-2.rom",16384,"bios"],["bios/256s-3.rom",16384,"bios"],["bios/32X_G_BIOS.BIN",256,"bios"],["bios/32X_M_BIOS.BIN",2048,"bios"],["bios/32X_S_BIOS.BIN",1024,"bios"],["bios/3do_arcade_saot.bin",524288,"bios"],["bios/5200.rom",2048,"bios"],["bios/64DD_IPL.bin",4194304,"bios"],["bios/7800 BIOS (E).rom",16384,"bios"],["bios/7800 BIOS (U).rom",4096,"bios"],["bios/ATARIBAS.ROM",8192,"bios"],["bios/ATARIOSA.ROM",10240,"bios"],["bios/ATARIOSB.ROM",10240,"bios"],["bios/ATARIXL.ROM",16384,"bios"],["bios/BB01R4_OS.ROM",16384,"bios"],["bios/BIOS.col",8192,"bios"],["bios/BLOOD.RFF",3536782,"game_data"],["bios/BS-X.bin",1048576,"bios"],["bios/CD32 Extended-ROM r40.60 (1993)(Commodore)(CD32).rom",524288,"bios"],["bios/CDTV Extended-ROM v1.0 (1991)(Commodore)(CDTV)[!].rom",262144,"bios"],["bios/CM32LN_CONTROL.ROM",65536,"bios"],["bios/CM32L_CONTROL.ROM",65536,"bios"],["bios/CM32L_PCM.ROM",1048576,"bios"],["bios/DOSBOX.SF2",32319396,"bios"],["bios/DUKE3D.GRP",11035779,"game_data"],["bios/Databases/colecodb.xml",164848,"bios"],["bios/Databases/msxcaswavdb.xml",819679,"bios"],["bios/Databases/msxromdb.xml",930986,"bios"],["bios/Databases/msxsysromdb.xml",157542,"bios"],["bios/Databases/segadb.xml",82318,"bios"],["bios/Databases/svidb.xml",261070,"bios"],["bios/ECS.bin",24576,"bios"],["bios/GUI.RFF",7145,"game_data"],["bios/IVOICE.bin",2048,"bios"],["bios/JiffyDOS_1541-II.bin",16384,"bios"],["bios/JiffyDOS_1571_repl310654.bin",32768,"bios"],["bios/JiffyDOS_1581.bin",32768,"bios"],["bios/JiffyDOS_C128.bin",16384,"bios"],["bios/JiffyDOS_C64.bin",8192,"bios"],["bios/JiffyDOS_SX-64.bin",8192,"bios"],["bios/Kickstart v1.2 r33.180 (1986)(Commodore)(A500-A1000-A2000)[!].rom",262144,"bios"],["bios/Kickstart v1.2 rev 33.166 (1986)(Commodore)(A1000).rom",262144,"bios"],["bios/Kickstart v1.3 r34.5 (1987)(Commodore)(A500-A1000-A2000-CDTV)[!].rom",262144,"bios"],["bios/Kickstart v1.3 r34.5 (1987)(Commodore)(A500-A1000-A2000-CDTV)[o].rom",524288,"bios"],["bios/Kickstart v2.04 r37.175 (1991)(Commodore)(A500+)[!].rom",524288,"bios"],["bios/Kickstart v2.05 r37.350 (1992)(Commodore)(A600HD)[!].rom",524288,"bios"],["bios/Kickstart v3.1 r40.60 (1993)(Commodore)(CD32).rom",524288,"bios"],["bios/Kickstart v3.1 r40.68 (1993)(Commodore)(A1200)[!].rom",524288,"bios"],["bios/Kickstart v3.1 r40.68 (1993)(Commodore)(A3000).rom",524288,"bios"],["bios/Kickstart v3.1 r40.68 (1993)(Commodore)(A4000).rom",524288,"bios"],["bios/MT32_CONTROL.ROM",65536,"bios"],["bios/MT32_PCM.ROM",524288,"bios"],["bios/Machines/COL - Bit Corporation Dina/czz50-1.rom",8192,"bios"],["bios/Machines/COL - Bit Corporation Dina/czz50-2.rom",8192,"bios"],["bios/Machines/COL - ColecoVision with Opcode Memory Extension/coleco.rom",8192,"bios"],["bios/Machines/COL - ColecoVision/coleco.rom",8192,"bios"],["bios/Machines/COL - ColecoVision/config.ini",347,"bios"],["bios/Machines/COL - Spectravideo SVI-603 Coleco/SVI603.ROM",8192,"bios"],["bios/Machines/Forte II Games - Pesadelo/pesadelo.rom",65536,"bios"],["bios/Machines/MSX - C-BIOS/cbios.txt",8924,"bios"],["bios/Machines/MSX - C-BIOS/cbios_logo_msx1.rom",16384,"bios"],["bios/Machines/MSX - C-BIOS/cbios_main_msx1.rom",32768,"bios"],["bios/Machines/MSX/MSX.ROM",32768,"bios"],["bios/Machines/MSX/MSX2.ROM",32768,"bios"],["bios/Machines/MSX/MSX2EXT.ROM",16384,"bios"],["bios/Machines/MSX/MSX2P.ROM",32768,"bios"],["bios/Machines/MSX/MSX2PEXT.ROM",16384,"bios"],["bios/Machines/MSX/MSXDOS2.ROM",65536,"bios"],["bios/Machines/MSX/config.ini",408,"bios"],["bios/Machines/MSX2 - C-BIOS/cbios.txt",8924,"bios"],["bios/Machines/MSX2 - C-BIOS/cbios_logo_msx2.rom",16384,"bios"],["bios/Machines/MSX2 - C-BIOS/cbios_main_msx2.rom",32768,"bios"],["bios/Machines/MSX2 - C-BIOS/cbios_sub.rom",16384,"bios"],["bios/Machines/MSX2+ - C-BIOS/cbios.txt",8924,"bios"],["bios/Machines/MSX2+ - C-BIOS/cbios_logo_msx2+.rom",16384,"bios"],["bios/Machines/MSX2+ - C-BIOS/cbios_main_msx2+.rom",32768,"bios"],["bios/Machines/MSX2+ - C-BIOS/cbios_music.rom",16384,"bios"],["bios/Machines/MSX2+ - C-BIOS/cbios_sub.rom",16384,"bios"],["bios/Machines/MSX2+/MSX.ROM",32768,"bios"],["bios/Machines/MSX2+/MSX2.ROM",32768,"bios"],["bios/Machines/MSX2+/MSX2EXT.ROM",16384,"bios"],["bios/Machines/MSX2+/MSX2P.ROM",32768,"bios"],["bios/Machines/MSX2+/MSX2PEXT.ROM",16384,"bios"],["bios/Machines/MSX2+/MSXDOS2.ROM",65536,"bios"],["bios/Machines/MSX2+/config.ini",773,"bios"],["bios/Machines/MSX2/MSX.ROM",32768,"bios"],["bios/Machines/MSX2/MSX2.ROM",32768,"bios"],["bios/Machines/MSX2/MSX2EXT.ROM",16384,"bios"],["bios/Machines/MSX2/MSX2P.ROM",32768,"bios"],["bios/Machines/MSX2/MSX2PEXT.ROM",16384,"bios"],["bios/Machines/MSX2/MSXDOS2.ROM",65536,"bios"],["bios/Machines/MSX2/config.ini",649,"bios"],["bios/Machines/MSXturboR/config.ini",941,"bios"],["bios/Machines/SVI - Spectravideo SVI-318/svi318.rom",32768,"bios"],["bios/Machines/SVI - Spectravideo SVI-328 Line truncated
+1 -1
View File
@@ -1 +1 @@
{"files":[["bios/32X_G_BIOS.BIN",256,"bios"],["bios/32X_M_BIOS.BIN",2048,"bios"],["bios/32X_S_BIOS.BIN",1024,"bios"],["bios/3do_arcade_saot.bin",524288,"bios"],["bios/5200.rom",2048,"bios"],["bios/64DD_IPL.bin",4194304,"bios"],["bios/7800 BIOS (E).rom",16384,"bios"],["bios/7800 BIOS (U).rom",4096,"bios"],["bios/ATARIBAS.ROM",8192,"bios"],["bios/ATARIOSA.ROM",10240,"bios"],["bios/ATARIOSB.ROM",10240,"bios"],["bios/ATARIXL.ROM",16384,"bios"],["bios/BB01R4_OS.ROM",16384,"bios"],["bios/BS-X.bin",1048576,"bios"],["bios/CD32 Extended-ROM r40.60 (1993)(Commodore)(CD32).rom",524288,"bios"],["bios/CDTV Extended-ROM v1.0 (1991)(Commodore)(CDTV)[!].rom",262144,"bios"],["bios/CM32LN_CONTROL.ROM",65536,"bios"],["bios/CM32L_CONTROL.ROM",65536,"bios"],["bios/CM32L_PCM.ROM",1048576,"bios"],["bios/DOSBOX.SF2",32319396,"bios"],["bios/Databases/colecodb.xml",164848,"bios"],["bios/Databases/msxcaswavdb.xml",819679,"bios"],["bios/Databases/msxromdb.xml",930986,"bios"],["bios/Databases/msxsysromdb.xml",157542,"bios"],["bios/Databases/segadb.xml",82318,"bios"],["bios/Databases/svidb.xml",261070,"bios"],["bios/ECS.bin",24576,"bios"],["bios/GBA/gba_bios.bin",16384,"bios"],["bios/GC/EUR/IPL.bin",2097152,"bios"],["bios/GC/JAP/IPL.bin",2097152,"bios"],["bios/GC/USA/IPL.bin",2097152,"bios"],["bios/GC/dsp_coef.bin",4096,"bios"],["bios/GC/dsp_rom.bin",8192,"bios"],["bios/GC/font_japanese.bin",259626,"bios"],["bios/GC/font_western.bin",6478,"bios"],["bios/IVOICE.bin",2048,"bios"],["bios/JiffyDOS_1541-II.bin",16384,"bios"],["bios/JiffyDOS_1571_repl310654.bin",32768,"bios"],["bios/JiffyDOS_1581.bin",32768,"bios"],["bios/JiffyDOS_C128.bin",16384,"bios"],["bios/JiffyDOS_C64.bin",8192,"bios"],["bios/JiffyDOS_SX-64.bin",8192,"bios"],["bios/Kickstart v1.2 r33.180 (1986)(Commodore)(A500-A1000-A2000)[!].rom",262144,"bios"],["bios/Kickstart v1.2 rev 33.166 (1986)(Commodore)(A1000).rom",262144,"bios"],["bios/Kickstart v1.3 r34.5 (1987)(Commodore)(A500-A1000-A2000-CDTV)[!].rom",262144,"bios"],["bios/Kickstart v1.3 r34.5 (1987)(Commodore)(A500-A1000-A2000-CDTV)[o].rom",524288,"bios"],["bios/Kickstart v2.04 r37.175 (1991)(Commodore)(A500+)[!].rom",524288,"bios"],["bios/Kickstart v2.05 r37.350 (1992)(Commodore)(A600HD)[!].rom",524288,"bios"],["bios/Kickstart v3.1 r40.60 (1993)(Commodore)(CD32).rom",524288,"bios"],["bios/Kickstart v3.1 r40.68 (1993)(Commodore)(A1200)[!].rom",524288,"bios"],["bios/Kickstart v3.1 r40.68 (1993)(Commodore)(A3000).rom",524288,"bios"],["bios/Kickstart v3.1 r40.68 (1993)(Commodore)(A4000).rom",524288,"bios"],["bios/Load/Firmware/rtl_bt/rtl8723a_fw.bin",24548,"bios"],["bios/Load/Firmware/rtl_bt/rtl8723b_fw.bin",45048,"bios"],["bios/Load/Firmware/rtl_bt/rtl8723d_fw.bin",52080,"bios"],["bios/Load/Firmware/rtl_bt/rtl8761a_fw.bin",74488,"bios"],["bios/Load/Firmware/rtl_bt/rtl8761bu_fw.bin",44484,"bios"],["bios/Load/Firmware/rtl_bt/rtl8821a_fw.bin",37420,"bios"],["bios/Load/Firmware/rtl_bt/rtl8821c_fw.bin",54636,"bios"],["bios/Load/Firmware/rtl_bt/rtl8822b_fw.bin",51176,"bios"],["bios/Load/Firmware/rtl_bt/rtl8822cu_fw.bin",38207,"bios"],["bios/Load/Firmware/rtl_bt/rtl8851bu_fw.bin",49760,"bios"],["bios/Load/Firmware/rtl_bt/rtl8852au_fw.bin",67188,"bios"],["bios/Load/Firmware/rtl_bt/rtl8852btu_fw.bin",128140,"bios"],["bios/Load/Firmware/rtl_bt/rtl8852bu_fw.bin",129733,"bios"],["bios/Load/Firmware/rtl_bt/rtl8852cu_fw.bin",113336,"bios"],["bios/Load/Firmware/rtl_bt/rtl8922au_fw.bin",71360,"bios"],["bios/Load/WiiSD.raw",134217728,"bios"],["bios/MT32_CONTROL.ROM",65536,"bios"],["bios/MT32_PCM.ROM",524288,"bios"],["bios/Machines/COL - Bit Corporation Dina/czz50-1.rom",8192,"bios"],["bios/Machines/COL - Bit Corporation Dina/czz50-2.rom",8192,"bios"],["bios/Machines/COL - ColecoVision with Opcode Memory Extension/coleco.rom",8192,"bios"],["bios/Machines/COL - ColecoVision/coleco.rom",8192,"bios"],["bios/Machines/COL - ColecoVision/config.ini",347,"bios"],["bios/Machines/COL - Spectravideo SVI-603 Coleco/SVI603.ROM",8192,"bios"],["bios/Machines/Forte II Games - Pesadelo/pesadelo.rom",65536,"bios"],["bios/Machines/MSX - C-BIOS/cbios.txt",8924,"bios"],["bios/Machines/MSX - C-BIOS/cbios_logo_msx1.rom",16384,"bios"],["bios/Machines/MSX - C-BIOS/cbios_main_msx1.rom",32768,"bios"],["bios/Machines/MSX/MSX.ROM",32768,"bios"],["bios/Machines/MSX/MSX2.ROM",32768,"bios"],["bios/Machines/MSX/MSX2EXT.ROM",16384,"bios"],["bios/Machines/MSX/MSX2P.ROM",32768,"bios"],["bios/Machines/MSX/MSX2PEXT.ROM",16384,"bios"],["bios/Machines/MSX/MSXDOS2.ROM",65536,"bios"],["bios/Machines/MSX/config.ini",408,"bios"],["bios/Machines/MSX2 - C-BIOS/cbios.txt",8924,"bios"],["bios/Machines/MSX2 - C-BIOS/cbios_logo_msx2.rom",16384,"bios"],["bios/Machines/MSX2 - C-BIOS/cbios_main_msx2.rom",32768,"bios"],["bios/Machines/MSX2 - C-BIOS/cbios_sub.rom",16384,"bios"],["bios/Machines/MSX2+ - C-BIOS/cbios.txt",8924,"bios"],["bios/Machines/MSX2+ - C-BIOS/cbios_logo_msx2+.rom",16384,"bios"],["bios/Machines/MSX2+ - C-BIOS/cbios_main_msx2+.rom",32768,"bios"],["bios/Machines/MSX2+ - C-BIOS/Line truncated {"files":[["bios/32X_G_BIOS.BIN",256,"bios"],["bios/32X_M_BIOS.BIN",2048,"bios"],["bios/32X_S_BIOS.BIN",1024,"bios"],["bios/3do_arcade_saot.bin",524288,"bios"],["bios/5200.rom",2048,"bios"],["bios/64DD_IPL.bin",4194304,"bios"],["bios/7800 BIOS (E).rom",16384,"bios"],["bios/7800 BIOS (U).rom",4096,"bios"],["bios/ATARIBAS.ROM",8192,"bios"],["bios/ATARIOSA.ROM",10240,"bios"],["bios/ATARIOSB.ROM",10240,"bios"],["bios/ATARIXL.ROM",16384,"bios"],["bios/BB01R4_OS.ROM",16384,"bios"],["bios/BS-X.bin",1048576,"bios"],["bios/CD32 Extended-ROM r40.60 (1993)(Commodore)(CD32).rom",524288,"bios"],["bios/CDTV Extended-ROM v1.0 (1991)(Commodore)(CDTV)[!].rom",262144,"bios"],["bios/CM32LN_CONTROL.ROM",65536,"bios"],["bios/CM32L_CONTROL.ROM",65536,"bios"],["bios/CM32L_PCM.ROM",1048576,"bios"],["bios/DOSBOX.SF2",32319396,"bios"],["bios/Databases/colecodb.xml",164848,"bios"],["bios/Databases/msxcaswavdb.xml",819679,"bios"],["bios/Databases/msxromdb.xml",930986,"bios"],["bios/Databases/msxsysromdb.xml",157542,"bios"],["bios/Databases/segadb.xml",82318,"bios"],["bios/Databases/svidb.xml",261070,"bios"],["bios/ECS.bin",24576,"bios"],["bios/GBA/gba_bios.bin",16384,"bios"],["bios/GC/EUR/IPL.bin",2097152,"bios"],["bios/GC/JAP/IPL.bin",2097152,"bios"],["bios/GC/USA/IPL.bin",2097152,"bios"],["bios/GC/dsp_coef.bin",4096,"bios"],["bios/GC/dsp_rom.bin",8192,"bios"],["bios/GC/font_japanese.bin",259626,"bios"],["bios/GC/font_western.bin",6478,"bios"],["bios/IVOICE.bin",2048,"bios"],["bios/JiffyDOS_1541-II.bin",16384,"bios"],["bios/JiffyDOS_1571_repl310654.bin",32768,"bios"],["bios/JiffyDOS_1581.bin",32768,"bios"],["bios/JiffyDOS_C128.bin",16384,"bios"],["bios/JiffyDOS_C64.bin",8192,"bios"],["bios/JiffyDOS_SX-64.bin",8192,"bios"],["bios/Kickstart v1.2 r33.180 (1986)(Commodore)(A500-A1000-A2000)[!].rom",262144,"bios"],["bios/Kickstart v1.2 rev 33.166 (1986)(Commodore)(A1000).rom",262144,"bios"],["bios/Kickstart v1.3 r34.5 (1987)(Commodore)(A500-A1000-A2000-CDTV)[!].rom",262144,"bios"],["bios/Kickstart v1.3 r34.5 (1987)(Commodore)(A500-A1000-A2000-CDTV)[o].rom",524288,"bios"],["bios/Kickstart v2.04 r37.175 (1991)(Commodore)(A500+)[!].rom",524288,"bios"],["bios/Kickstart v2.05 r37.350 (1992)(Commodore)(A600HD)[!].rom",524288,"bios"],["bios/Kickstart v3.1 r40.60 (1993)(Commodore)(CD32).rom",524288,"bios"],["bios/Kickstart v3.1 r40.68 (1993)(Commodore)(A1200)[!].rom",524288,"bios"],["bios/Kickstart v3.1 r40.68 (1993)(Commodore)(A3000).rom",524288,"bios"],["bios/Kickstart v3.1 r40.68 (1993)(Commodore)(A4000).rom",524288,"bios"],["bios/Load/Firmware/rtl_bt/rtl8723a_fw.bin",24548,"bios"],["bios/Load/Firmware/rtl_bt/rtl8723b_fw.bin",45048,"bios"],["bios/Load/Firmware/rtl_bt/rtl8723d_fw.bin",52080,"bios"],["bios/Load/Firmware/rtl_bt/rtl8761a_fw.bin",74488,"bios"],["bios/Load/Firmware/rtl_bt/rtl8761bu_fw.bin",44484,"bios"],["bios/Load/Firmware/rtl_bt/rtl8821a_fw.bin",37420,"bios"],["bios/Load/Firmware/rtl_bt/rtl8821c_fw.bin",54636,"bios"],["bios/Load/Firmware/rtl_bt/rtl8822b_fw.bin",51176,"bios"],["bios/Load/Firmware/rtl_bt/rtl8822cu_fw.bin",38207,"bios"],["bios/Load/Firmware/rtl_bt/rtl8851bu_fw.bin",49760,"bios"],["bios/Load/Firmware/rtl_bt/rtl8852au_fw.bin",67188,"bios"],["bios/Load/Firmware/rtl_bt/rtl8852btu_fw.bin",128140,"bios"],["bios/Load/Firmware/rtl_bt/rtl8852bu_fw.bin",129733,"bios"],["bios/Load/Firmware/rtl_bt/rtl8852cu_fw.bin",113336,"bios"],["bios/Load/Firmware/rtl_bt/rtl8922au_fw.bin",71360,"bios"],["bios/Load/WiiSD.raw",134217728,"bios"],["bios/MT32_CONTROL.ROM",65536,"bios"],["bios/MT32_PCM.ROM",524288,"bios"],["bios/Machines/COL - Bit Corporation Dina/czz50-1.rom",8192,"bios"],["bios/Machines/COL - Bit Corporation Dina/czz50-2.rom",8192,"bios"],["bios/Machines/COL - ColecoVision with Opcode Memory Extension/coleco.rom",8192,"bios"],["bios/Machines/COL - ColecoVision/coleco.rom",8192,"bios"],["bios/Machines/COL - ColecoVision/config.ini",347,"bios"],["bios/Machines/COL - Spectravideo SVI-603 Coleco/SVI603.ROM",8192,"bios"],["bios/Machines/Forte II Games - Pesadelo/pesadelo.rom",65536,"bios"],["bios/Machines/MSX - C-BIOS/cbios.txt",8924,"bios"],["bios/Machines/MSX - C-BIOS/cbios_logo_msx1.rom",16384,"bios"],["bios/Machines/MSX - C-BIOS/cbios_main_msx1.rom",32768,"bios"],["bios/Machines/MSX/MSX.ROM",32768,"bios"],["bios/Machines/MSX/MSX2.ROM",32768,"bios"],["bios/Machines/MSX/MSX2EXT.ROM",16384,"bios"],["bios/Machines/MSX/MSX2P.ROM",32768,"bios"],["bios/Machines/MSX/MSX2PEXT.ROM",16384,"bios"],["bios/Machines/MSX/MSXDOS2.ROM",65536,"bios"],["bios/Machines/MSX/config.ini",408,"bios"],["bios/Machines/MSX2 - C-BIOS/cbios.txt",8924,"bios"],["bios/Machines/MSX2 - C-BIOS/cbios_logo_msx2.rom",16384,"bios"],["bios/Machines/MSX2 - C-BIOS/cbios_main_msx2.rom",32768,"bios"],["bios/Machines/MSX2 - C-BIOS/cbios_sub.rom",16384,"bios"],["bios/Machines/MSX2+ - C-BIOS/cbios.txt",8924,"bios"],["bios/Machines/MSX2+ - C-BIOS/cbios_logo_msx2+.rom",16384,"bios"],["bios/Machines/MSX2+ - C-BIOS/cbios_main_msx2+.rom",32768,"bios"],["bios/Machines/MSX2+ - C-BIOS/Line truncated
+1 -1
View File
@@ -1 +1 @@
{"files":[["arcade/hiscore.dat",322458,"bios"],["atari2600/stella/KVB1.WAV",4219542,"game_data"],["atari2600/stella/KVB2.WAV",9593878,"game_data"],["atari2600/stella/KVB3.WAV",7676992,"game_data"],["atari2600/stella/KVS1.WAV",3436329,"game_data"],["atari2600/stella/KVS2.WAV",2086275,"game_data"],["atari2600/stella/KVS3.WAV",3720920,"game_data"],["atari2600/stella/KVSHARED.WAV",3059116,"game_data"],["bios/000-lo.lo",131072,"bios"],["bios/128-spanish-0.rom",16384,"bios"],["bios/128-spanish-1.rom",16384,"bios"],["bios/128p-0.rom",16384,"bios"],["bios/128p-1.rom",16384,"bios"],["bios/22vp931.zip",1405,"bios"],["bios/256s-0.rom",16384,"bios"],["bios/256s-1.rom",16384,"bios"],["bios/256s-2.rom",16384,"bios"],["bios/256s-3.rom",16384,"bios"],["bios/2608_BD.WAV",19192,"bios"],["bios/2608_HH.WAV",36722,"bios"],["bios/2608_RIM.WAV",5288,"bios"],["bios/2608_SD.WAV",15558,"bios"],["bios/2608_TOM.WAV",23092,"bios"],["bios/2608_TOP.WAV",57016,"bios"],["bios/32X_G_BIOS.BIN",256,"bios"],["bios/32X_M_BIOS.BIN",2048,"bios"],["bios/32X_S_BIOS.BIN",1024,"bios"],["bios/3c505.zip",10896,"bios"],["bios/3do_arcade_saot.bin",524288,"bios"],["bios/3dobios.zip",2351546,"bios"],["bios/3xtwin.zip",1417,"bios"],["bios/4dparprn.zip",480212,"bios"],["bios/5200.rom",2048,"bios"],["bios/64DD_IPL.bin",4194304,"bios"],["bios/64_cspeech.zip",2011,"bios"],["bios/7800 BIOS (E).rom",16384,"bios"],["bios/7800 BIOS (U).rom",4096,"bios"],["bios/82c606.zip",479847,"bios"],["bios/A78_highscore.bin",16364,"bios"],["bios/APPLE2GS.ROM",131072,"bios"],["bios/ARAB1.ROM",32768,"bios"],["bios/ARABIC.rom",32768,"bios"],["bios/ATARIBAS.ROM",8192,"bios"],["bios/ATARIOSA.ROM",10240,"bios"],["bios/ATARIOSB.ROM",10240,"bios"],["bios/ATARIXL.ROM",16384,"bios"],["bios/AmigaVision.rom",524288,"bios"],["bios/Atari Jaguar (World).j64",131072,"bios"],["bios/BB01R4_OS.ROM",16384,"bios"],["bios/BEERIDE.ROM",16384,"bios"],["bios/BIOS.col",8192,"bios"],["bios/BS-X (En) (DRM-Free).bin",1048576,"bios"],["bios/BS-X (En).bin",1048576,"bios"],["bios/BS-X.bin",1048576,"bios"],["bios/CARTS.SHA",33152,"bios"],["bios/CCNPATCH.cas",1481,"bios"],["bios/CCNPATCH.wav",71748,"bios"],["bios/CM32LN_CONTROL.ROM",65536,"bios"],["bios/CM32L_CONTROL.ROM",65536,"bios"],["bios/CM32L_PCM.ROM",1048576,"bios"],["bios/CYRILLIC.FNT",2048,"bios"],["bios/Cemu/cafeLibs/erreula.rpl",790080,"bios"],["bios/Cemu/cafeLibs/snd_user.rpl",84032,"bios"],["bios/Cemu/cafeLibs/snduser2.rpl",111360,"bios"],["bios/Cemu/cafeLibs/swkbd.rpl",1116288,"bios"],["bios/Cemu/keys.txt",449507,"bios"],["bios/Cemu/otp.bin",1024,"bios"],["bios/Cemu/resources/sharedFonts/CafeCn.ttf",9664448,"bios"],["bios/Cemu/resources/sharedFonts/CafeKr.ttf",5626464,"bios"],["bios/Cemu/resources/sharedFonts/CafeStd.ttf",5363768,"bios"],["bios/Cemu/resources/sharedFonts/CafeTw.ttf",6593548,"bios"],["bios/Cemu/seeprom.bin",512,"bios"],["bios/Cemu/shareddata.bin",33554432,"bios"],["bios/Complex.bin",1048576,"bios"],["bios/Complex_4627.bin",1048576,"bios"],["bios/Complex_4627v1.03.bin",1048576,"bios"],["bios/DBPATCH.cas",1481,"bios"],["bios/DBPATCH.wav",71900,"bios"],["bios/DEFAULT.FNT",2048,"bios"],["bios/DISK.ROM",16384,"bios"],["bios/DOSBOX.SF2",32319396,"bios"],["bios/FMPAC.ROM",65536,"bios"],["bios/FMPAC16.ROM",33152,"bios"],["bios/FONT.ROM",288768,"bios"],["bios/GBA/gba_bios.bin",16384,"bios"],["bios/GC/EUR/IPL.bin",2097152,"bios"],["bios/GC/JAP/IPL.bin",2097152,"bios"],["bios/GC/USA/IPL.bin",2097152,"bios"],["bios/GC/dsp_coef.bin",4096,"bios"],["bios/GC/dsp_rom.bin",8192,"bios"],["bios/GC/font_japanese.bin",259626,"bios"],["bios/GC/font_western.bin",6478,"bios"],["bios/GCVMX80.ROM",8192,"bios"],["bios/HANGUL.rom",131072,"bios"],["bios/INTERNAT.FNT",2048,"bios"],["bios/ITALIC.FNT",2048,"bios"],["bios/JAPANESE.FNT",2048,"bios"],["bios/JISYO.ROM",524288,"bios"],["bios/JiffyDOS_1541-II.bin",16384,"bios"],["bios/JiffyDOS_1571_repl310654.bin",32768,"bios"],["bios/JiffyDOS_1581.bin",32768,"bios"],["bios/JiffyDOS_C64.bin",8192,"bios"],["bios/JiffyDOS_SX-64.bin",8192,"bios"],["bios/KANJI.ROM",131072,"bios"],["bios/KANJI1.ROM",131072,"bios"],["bios/KOREAN.FNT",2048,"bios"],["bios/Load/Firmware/rtl_bt/rtl8723a_fw.bin",24548,"bios"],["bios/Load/Firmware/rtl_bt/rtl8723b_fw.bin",45048,"bios"],["bios/Load/Firmware/rtl_bt/rtl8723d_fw.bin",52080,"bios"],["bios/Load/Firmware/rtl_bt/rtl8761a_fw.bin",74488,"bios"],["bios/Load/Firmware/rtl_bt/rtl8761bu_fw.bin",44484,"bios"],["bios/Load/Firmware/rtl_bt/rtl8821a_fw.bin",37420,"bios"],["bios/Load/Firmware/rtl_bt/rtl8821c_fw.bin",54636,"bios"],["bios/Load/Firmware/rtl_bt/rtl8822b_fw.bin",51176,"bios"],["bios/Load/Firmware/rtl_bt/rtl8822cu_fw.bin",38207,"bios"],["bios/Load/Firmware/rtl_bt/rtl8851bu_fw.bin",49760,"bios"],["bios/Load/Firmware/rtl_bt/rtl8852au_fw.bin",67188,"bios"],["bios/Load/Firmware/rtl_bt/rtl8852btu_fw.bin",128140,"bios"],["bios/Load/Firmware/rtl_bt/rtl8852bu_fw.bin",129733,"bios"],["bios/Load/Firmware/rtl_bt/rtl8852cu_fw.bin",113336,"bios"],["bios/Load/Firmware/rtl_bt/rtl8922au_fw.bin",71360,"bios"Line truncated {"files":[["arcade/hiscore.dat",322458,"bios"],["atari2600/stella/KVB1.WAV",4219542,"game_data"],["atari2600/stella/KVB2.WAV",9593878,"game_data"],["atari2600/stella/KVB3.WAV",7676992,"game_data"],["atari2600/stella/KVS1.WAV",3436329,"game_data"],["atari2600/stella/KVS2.WAV",2086275,"game_data"],["atari2600/stella/KVS3.WAV",3720920,"game_data"],["atari2600/stella/KVSHARED.WAV",3059116,"game_data"],["bios/000-lo.lo",131072,"bios"],["bios/128-spanish-0.rom",16384,"bios"],["bios/128-spanish-1.rom",16384,"bios"],["bios/128p-0.rom",16384,"bios"],["bios/128p-1.rom",16384,"bios"],["bios/22vp931.zip",1405,"bios"],["bios/256s-0.rom",16384,"bios"],["bios/256s-1.rom",16384,"bios"],["bios/256s-2.rom",16384,"bios"],["bios/256s-3.rom",16384,"bios"],["bios/2608_BD.WAV",19192,"bios"],["bios/2608_HH.WAV",36722,"bios"],["bios/2608_RIM.WAV",5288,"bios"],["bios/2608_SD.WAV",15558,"bios"],["bios/2608_TOM.WAV",23092,"bios"],["bios/2608_TOP.WAV",57016,"bios"],["bios/32X_G_BIOS.BIN",256,"bios"],["bios/32X_M_BIOS.BIN",2048,"bios"],["bios/32X_S_BIOS.BIN",1024,"bios"],["bios/3c505.zip",10896,"bios"],["bios/3do_arcade_saot.bin",524288,"bios"],["bios/3dobios.zip",2351546,"bios"],["bios/3xtwin.zip",1417,"bios"],["bios/4dparprn.zip",480212,"bios"],["bios/5200.rom",2048,"bios"],["bios/64DD_IPL.bin",4194304,"bios"],["bios/64_cspeech.zip",2011,"bios"],["bios/7800 BIOS (E).rom",16384,"bios"],["bios/7800 BIOS (U).rom",4096,"bios"],["bios/82c606.zip",479847,"bios"],["bios/A78_highscore.bin",16364,"bios"],["bios/APPLE2GS.ROM",131072,"bios"],["bios/ARAB1.ROM",32768,"bios"],["bios/ARABIC.rom",32768,"bios"],["bios/ATARIBAS.ROM",8192,"bios"],["bios/ATARIOSA.ROM",10240,"bios"],["bios/ATARIOSB.ROM",10240,"bios"],["bios/ATARIXL.ROM",16384,"bios"],["bios/AmigaVision.rom",524288,"bios"],["bios/Atari Jaguar (World).j64",131072,"bios"],["bios/BB01R4_OS.ROM",16384,"bios"],["bios/BEERIDE.ROM",16384,"bios"],["bios/BIOS.col",8192,"bios"],["bios/BS-X (En) (DRM-Free).bin",1048576,"bios"],["bios/BS-X (En).bin",1048576,"bios"],["bios/BS-X.bin",1048576,"bios"],["bios/CARTS.SHA",33152,"bios"],["bios/CCNPATCH.cas",1481,"bios"],["bios/CCNPATCH.wav",71748,"bios"],["bios/CM32LN_CONTROL.ROM",65536,"bios"],["bios/CM32L_CONTROL.ROM",65536,"bios"],["bios/CM32L_PCM.ROM",1048576,"bios"],["bios/CYRILLIC.FNT",2048,"bios"],["bios/Cemu/cafeLibs/erreula.rpl",790080,"bios"],["bios/Cemu/cafeLibs/snd_user.rpl",84032,"bios"],["bios/Cemu/cafeLibs/snduser2.rpl",111360,"bios"],["bios/Cemu/cafeLibs/swkbd.rpl",1116288,"bios"],["bios/Cemu/keys.txt",449507,"bios"],["bios/Cemu/otp.bin",1024,"bios"],["bios/Cemu/resources/sharedFonts/CafeCn.ttf",9664448,"bios"],["bios/Cemu/resources/sharedFonts/CafeKr.ttf",5626464,"bios"],["bios/Cemu/resources/sharedFonts/CafeStd.ttf",5363768,"bios"],["bios/Cemu/resources/sharedFonts/CafeTw.ttf",6593548,"bios"],["bios/Cemu/seeprom.bin",512,"bios"],["bios/Cemu/shareddata.bin",33554432,"bios"],["bios/Complex.bin",1048576,"bios"],["bios/Complex_4627.bin",1048576,"bios"],["bios/Complex_4627v1.03.bin",1048576,"bios"],["bios/DBPATCH.cas",1481,"bios"],["bios/DBPATCH.wav",71900,"bios"],["bios/DEFAULT.FNT",2048,"bios"],["bios/DISK.ROM",16384,"bios"],["bios/DOSBOX.SF2",32319396,"bios"],["bios/FMPAC.ROM",65536,"bios"],["bios/FMPAC16.ROM",33152,"bios"],["bios/FONT.ROM",288768,"bios"],["bios/GBA/gba_bios.bin",16384,"bios"],["bios/GC/EUR/IPL.bin",2097152,"bios"],["bios/GC/JAP/IPL.bin",2097152,"bios"],["bios/GC/USA/IPL.bin",2097152,"bios"],["bios/GC/dsp_coef.bin",4096,"bios"],["bios/GC/dsp_rom.bin",8192,"bios"],["bios/GC/font_japanese.bin",259626,"bios"],["bios/GC/font_western.bin",6478,"bios"],["bios/GCVMX80.ROM",8192,"bios"],["bios/HANGUL.rom",131072,"bios"],["bios/INTERNAT.FNT",2048,"bios"],["bios/ITALIC.FNT",2048,"bios"],["bios/JAPANESE.FNT",2048,"bios"],["bios/JISYO.ROM",524288,"bios"],["bios/JiffyDOS_1541-II.bin",16384,"bios"],["bios/JiffyDOS_1571_repl310654.bin",32768,"bios"],["bios/JiffyDOS_1581.bin",32768,"bios"],["bios/JiffyDOS_C64.bin",8192,"bios"],["bios/JiffyDOS_SX-64.bin",8192,"bios"],["bios/KANJI.ROM",131072,"bios"],["bios/KANJI1.ROM",131072,"bios"],["bios/KOREAN.FNT",2048,"bios"],["bios/Load/Firmware/rtl_bt/rtl8723a_fw.bin",24548,"bios"],["bios/Load/Firmware/rtl_bt/rtl8723b_fw.bin",45048,"bios"],["bios/Load/Firmware/rtl_bt/rtl8723d_fw.bin",52080,"bios"],["bios/Load/Firmware/rtl_bt/rtl8761a_fw.bin",74488,"bios"],["bios/Load/Firmware/rtl_bt/rtl8761bu_fw.bin",44484,"bios"],["bios/Load/Firmware/rtl_bt/rtl8821a_fw.bin",37420,"bios"],["bios/Load/Firmware/rtl_bt/rtl8821c_fw.bin",54636,"bios"],["bios/Load/Firmware/rtl_bt/rtl8822b_fw.bin",51176,"bios"],["bios/Load/Firmware/rtl_bt/rtl8822cu_fw.bin",38207,"bios"],["bios/Load/Firmware/rtl_bt/rtl8851bu_fw.bin",49760,"bios"],["bios/Load/Firmware/rtl_bt/rtl8852au_fw.bin",67188,"bios"],["bios/Load/Firmware/rtl_bt/rtl8852btu_fw.bin",128140,"bios"],["bios/Load/Firmware/rtl_bt/rtl8852bu_fw.bin",129733,"bios"],["bios/Load/Firmware/rtl_bt/rtl8852cu_fw.bin",113336,"bios"],["bios/Load/Firmware/rtl_bt/rtl8922au_fw.bin",71360,"bios"Line truncated
+1 -1
View File
@@ -1 +1 @@
{"arcade":{"fbneo":{"file":"arcade/fbneo.json","gzip_size":115955,"sha256":"88961c9a6be804772baa767f6ee3a9162478c30c60fe55c8363d8fe4fbc909da","size":423362},"flycast":{"file":"arcade/flycast.json","gzip_size":1841,"sha256":"957189a6e993fda08e21c90ffc6ad94aed9ca9e6dd9ed8c7724e29db036791cb","size":5441},"mame":{"file":"arcade/mame.json","gzip_size":154527,"sha256":"b12010301293a218ecfac45706d81a26c3ac0c27eebeca7409bc8f6ce0163e9e","size":604648},"mame0258":{"file":"arcade/mame0258.json","gzip_size":141151,"sha256":"7ecff1979ed2ac123ea3ba4825dde6e2c31906415165fabefc9af93fa48a6de4","size":556059},"mame0278":{"file":"arcade/mame0278.json","gzip_size":148595,"sha256":"7881e3516f451ec83e886aaf41ebac9d60f497e1f954f49749d52890d7955cbf","size":582797},"mame2000":{"file":"arcade/mame2000.json","gzip_size":8624,"sha256":"f0d69b0872ecfc2845eeb7304543e5b17c11db82060b5514c206b6962ed08263","size":23316},"mame2003":{"file":"arcade/mame2003.json","gzip_size":17456,"sha256":"3b44e2a92c80a8f104b7781fcc1c776d025ad2cf4986638efa3ea041baa9a56d","size":48929},"mame2003_plus":{"file":"arcade/mame2003_plus.json","gzip_size":21077,"sha256":"2857283556fa49c20d8160ed113465bf0c2e643ed0b8acd2d38266a78be8e25e","size":62373},"mame2010":{"file":"arcade/mame2010.json","gzip_size":30869,"sha256":"50b1b381b4b07463f69f1f4c512dd1c98a78d4d0e9c3aeaf49b7b1b007b42152","size":96528},"mame2015":{"file":"arcade/mame2015.json","gzip_size":86990,"sha256":"afeb8c32f651421f74c6de057b0c0b837acd14c533b5160bb31f4411e0afffec","size":360117},"mame2016":{"file":"arcade/mame2016.json","gzip_size":101249,"sha256":"1f75147977d9d082a38b8f8e47a02b301f3d6e6db97c9b9282f3e7f27f26ee47","size":406190}},"containers":{"7z":{"description":"Per-file CRCs and sizes sit in the header at the end of the archive, LZMA-compressed by default (kEncodedHeader). One LZMA decode of the header, independent of the archive size, gives them. Python's lzma is an optional module.","re_read":true,"signature":{"bytes_hex":"377abcaf271c","offset":0},"source_ref":"ip7z/7zip@b88c6bc0 DOC/7zFormat.txt:171-189, DOC/7zFormat.txt:318-334, CPP/7zip/Archive/7z/7zHandlerOut.cpp:859"},"cd-sector":{"description":"User data of a CD sector. In a 2048-byte image (.iso) sector N starts at N * 2048. In a raw 2352-byte image it starts at N * 2352 plus 16 for Mode 1 and plus 24 for Mode 2 Form 1 (12 sync bytes, a 4-byte header, and for Mode 2 an 8-byte subheader).","re_read":true,"sector_size":{"iso":2048,"raw":2352},"source_ref":"stenzek/duckstation@526b4a12 src/util/iso_reader.cpp:157-171, src/util/cd_image.h:28-34","user_data_offset":{"mode1":16,"mode2":24}},"chd-v5":{"description":"MAME compressed hunks, big-endian. The header holds sizes, codecs and hashes only, never the system or the region. The overall SHA-1 is what MAME software lists record. Neither SHA-1 equals a Redump track hash: chdman hashes 2448-byte frames (subcode included, zero-filled), pads each track to 4 frames and byte-swaps audio, all tracks in one stream.","fields":{"compressors":{"encoding":"fourcc4","length":16,"offset":16},"header_length":{"encoding":"u32be","length":4,"offset":8,"value":124},"hunk_bytes":{"encoding":"u32be","length":4,"offset":56},"logical_bytes":{"encoding":"u64be","length":8,"offset":32},"map_offset":{"encoding":"u64be","length":8,"offset":40},"metadata_offset":{"encoding":"u64be","length":8,"offset":48},"overall_sha1":{"encoding":"hex","length":20,"offset":84},"parent_sha1":{"encoding":"hex","length":20,"offset":104},"raw_sha1":{"encoding":"hex","length":20,"offset":64},"unit_bytes":{"encoding":"u32be","length":4,"offset":60},"version":{"encoding":"u32be","length":4,"offset":12,"value":5}},"magic":{"bytes":"MComprHD","offset":0},"media_tags":{"CHCD":"cd","CHGD":"gd-rom","CHGT":"gd-rom","CHT2":"cd","CHTR":"cd","DVD ":"dvd","GDDD":"hard-disk"},"metadata_entry":{"description":"16-byte entry header, payload follows. Linked list from metadata_offset.","flags":{"encoding":"u8","length":1,"offset":4},"length":{"encoding":"u24be","length":3,"offset":5},"next":{"encoding":"u64be","length":8,"offset":8},"tag":{"encoding":"ascii","length":4,"offset":0}},"re_read":true,"source_ref":"mamedev/mame@f34f0250 src/lib/util/chd.h:148-162, src/lib/util/chd.cpp:2021-2061, src/lib/util/chd.cpp:2822-2831, src/lib/util/chd.h:212-235, src/lib/util/chd.cpp:1709-1749, src/tools/chdman.cpp:443-445, src/tools/chdman.cpp:2195-2203, src/lib/util/cdrom.cpp:2669-2672, src/emu/romload.cpp:1211"},"gamecube-disc":{"description":"Where disc offset 0 sits inside each container Dolphin opens.","formats":{"ciso":{"disc_header_at":32768,"magic":"CISO"},"gcz":{"disc_header_at":null,"magic_u32le":2970337281,"note":"block 0 is zlib unless bit 63 of its pointer is set"},"iso":{"disc_header_at":0,"magic":null},"rvz":{"disc_header_at":88,"magic":"RVZ\u0001","note":"first 0x80 disc bytes copied into header 2"},"wbfs":{"disc_header_at":null,"magic":"WBFS","note":"disc offset 0 at wlba[0] << wbfs_sector_shift"},"wia":{"disc_header_at":88,"magic":"WIA\u0001","note":Line truncated {"arcade":{"fbneo":{"file":"arcade/fbneo.json","gzip_size":115955,"sha256":"88961c9a6be804772baa767f6ee3a9162478c30c60fe55c8363d8fe4fbc909da","size":423362},"flycast":{"file":"arcade/flycast.json","gzip_size":1841,"sha256":"957189a6e993fda08e21c90ffc6ad94aed9ca9e6dd9ed8c7724e29db036791cb","size":5441},"mame":{"file":"arcade/mame.json","gzip_size":154527,"sha256":"b12010301293a218ecfac45706d81a26c3ac0c27eebeca7409bc8f6ce0163e9e","size":604648},"mame0258":{"file":"arcade/mame0258.json","gzip_size":141151,"sha256":"7ecff1979ed2ac123ea3ba4825dde6e2c31906415165fabefc9af93fa48a6de4","size":556059},"mame0278":{"file":"arcade/mame0278.json","gzip_size":148595,"sha256":"7881e3516f451ec83e886aaf41ebac9d60f497e1f954f49749d52890d7955cbf","size":582797},"mame2000":{"file":"arcade/mame2000.json","gzip_size":8624,"sha256":"f0d69b0872ecfc2845eeb7304543e5b17c11db82060b5514c206b6962ed08263","size":23316},"mame2003":{"file":"arcade/mame2003.json","gzip_size":17456,"sha256":"3b44e2a92c80a8f104b7781fcc1c776d025ad2cf4986638efa3ea041baa9a56d","size":48929},"mame2003_plus":{"file":"arcade/mame2003_plus.json","gzip_size":21077,"sha256":"2857283556fa49c20d8160ed113465bf0c2e643ed0b8acd2d38266a78be8e25e","size":62373},"mame2010":{"file":"arcade/mame2010.json","gzip_size":30869,"sha256":"50b1b381b4b07463f69f1f4c512dd1c98a78d4d0e9c3aeaf49b7b1b007b42152","size":96528},"mame2015":{"file":"arcade/mame2015.json","gzip_size":86990,"sha256":"afeb8c32f651421f74c6de057b0c0b837acd14c533b5160bb31f4411e0afffec","size":360117},"mame2016":{"file":"arcade/mame2016.json","gzip_size":101249,"sha256":"1f75147977d9d082a38b8f8e47a02b301f3d6e6db97c9b9282f3e7f27f26ee47","size":406190}},"containers":{"7z":{"description":"Per-file CRCs and sizes sit in the header at the end of the archive, LZMA-compressed by default (kEncodedHeader). One LZMA decode of the header, independent of the archive size, gives them. Python's lzma is an optional module.","re_read":true,"signature":{"bytes_hex":"377abcaf271c","offset":0},"source_ref":"ip7z/7zip@b88c6bc0 DOC/7zFormat.txt:171-189, DOC/7zFormat.txt:318-334, CPP/7zip/Archive/7z/7zHandlerOut.cpp:859"},"cd-sector":{"description":"User data of a CD sector. In a 2048-byte image (.iso) sector N starts at N * 2048. In a raw 2352-byte image it starts at N * 2352 plus 16 for Mode 1 and plus 24 for Mode 2 Form 1 (12 sync bytes, a 4-byte header, and for Mode 2 an 8-byte subheader).","re_read":true,"sector_size":{"iso":2048,"raw":2352},"source_ref":"stenzek/duckstation@526b4a12 src/util/iso_reader.cpp:157-171, src/util/cd_image.h:28-34","user_data_offset":{"mode1":16,"mode2":24}},"chd-v5":{"description":"MAME compressed hunks, big-endian. The header holds sizes, codecs and hashes only, never the system or the region. The overall SHA-1 is what MAME software lists record. Neither SHA-1 equals a Redump track hash: chdman hashes 2448-byte frames (subcode included, zero-filled), pads each track to 4 frames and byte-swaps audio, all tracks in one stream.","fields":{"compressors":{"encoding":"fourcc4","length":16,"offset":16},"header_length":{"encoding":"u32be","length":4,"offset":8,"value":124},"hunk_bytes":{"encoding":"u32be","length":4,"offset":56},"logical_bytes":{"encoding":"u64be","length":8,"offset":32},"map_offset":{"encoding":"u64be","length":8,"offset":40},"metadata_offset":{"encoding":"u64be","length":8,"offset":48},"overall_sha1":{"encoding":"hex","length":20,"offset":84},"parent_sha1":{"encoding":"hex","length":20,"offset":104},"raw_sha1":{"encoding":"hex","length":20,"offset":64},"unit_bytes":{"encoding":"u32be","length":4,"offset":60},"version":{"encoding":"u32be","length":4,"offset":12,"value":5}},"magic":{"bytes":"MComprHD","offset":0},"media_tags":{"CHCD":"cd","CHGD":"gd-rom","CHGT":"gd-rom","CHT2":"cd","CHTR":"cd","DVD ":"dvd","GDDD":"hard-disk"},"metadata_entry":{"description":"16-byte entry header, payload follows. Linked list from metadata_offset.","flags":{"encoding":"u8","length":1,"offset":4},"length":{"encoding":"u24be","length":3,"offset":5},"next":{"encoding":"u64be","length":8,"offset":8},"tag":{"encoding":"ascii","length":4,"offset":0}},"re_read":true,"source_ref":"mamedev/mame@f34f0250 src/lib/util/chd.h:148-162, src/lib/util/chd.cpp:2021-2061, src/lib/util/chd.cpp:2822-2831, src/lib/util/chd.h:212-235, src/lib/util/chd.cpp:1709-1749, src/tools/chdman.cpp:443-445, src/tools/chdman.cpp:2195-2203, src/lib/util/cdrom.cpp:2669-2672, src/emu/romload.cpp:1211"},"gamecube-disc":{"description":"Where disc offset 0 sits inside each container Dolphin opens.","formats":{"ciso":{"disc_header_at":32768,"magic":"CISO"},"gcz":{"disc_header_at":null,"magic_u32le":2970337281,"note":"block 0 is zlib unless bit 63 of its pointer is set"},"iso":{"disc_header_at":0,"magic":null},"rvz":{"disc_header_at":88,"magic":"RVZ\u0001","note":"first 0x80 disc bytes copied into header 2"},"wbfs":{"disc_header_at":null,"magic":"WBFS","note":"disc offset 0 at wlba[0] << wbfs_sector_shift"},"wia":{"disc_header_at":88,"magic":"WIA\u0001","note":Line truncated
+1 -2
View File
@@ -1598,8 +1598,7 @@
"reason": "not_found", "reason": "not_found",
"cores": [ "cores": [
"dosbox-x" "dosbox-x"
], ]
"held": true
}, },
{ {
"dest": "extras.wad", "dest": "extras.wad",
+1661 -2
View File
File diff suppressed because it is too large. Load diff
+4
View File
@@ -3036,12 +3036,16 @@ SYSTEM_ALIASES: dict[str, str] = {
} }
@functools.lru_cache(maxsize=None)
def _norm_system_id(sid: str) -> str: def _norm_system_id(sid: str) -> str:
"""Normalize system ID for cross-platform matching. """Normalize system ID for cross-platform matching.
Resolves known aliases, then strips manufacturer prefixes and separators Resolves known aliases, then strips manufacturer prefixes and separators
so that platform-specific IDs (e.g., "xbox", "nintendo-wiiu") match so that platform-specific IDs (e.g., "xbox", "nintendo-wiiu") match
profile IDs (e.g., "microsoft-xbox", "nintendo-wii-u"). profile IDs (e.g., "microsoft-xbox", "nintendo-wii-u").
A function of the string and two constant tables: a manifest run asks it
1.7 million times for about 1,100 ids.
""" """
s = sid.lower().replace("_", "-") s = sid.lower().replace("_", "-")
s = SYSTEM_ALIASES.get(s, s) s = SYSTEM_ALIASES.get(s, s)
+22 -4
View File
@@ -26,7 +26,9 @@ sys.path.insert(0, os.path.dirname(__file__))
from common import ( from common import (
platform_arg, platform_arg,
ProfileSelectionError, ProfileSelectionError,
_foreign_tree,
_norm_system_id, _norm_system_id,
_owner_tokens,
archive_entry, archive_entry,
directory_members, directory_members,
expand_directory_entries, expand_directory_entries,
@@ -215,8 +217,14 @@ def _resolve_source(
none of the hashes the entry declares: a name never wins over a declared none of the hashes the entry declares: a name never wins over a declared
hash, and zquestclassic's ending.mid read as held because Hydra Castle hash, and zquestclassic's ending.mid read as held because Hydra Castle
Labyrinth has another file of that name. Labyrinth has another file of that name.
Another casing of the name in another emulator's tree proves nothing, as
in resolve_local_file's case-insensitive step: dosbox-x's et4000.bin read
as held through pcem's et4000.BIN, which the pack and the installer
refuse it.
""" """
declared = _declared_content(file_entry or {}) declared = _declared_content(file_entry or {})
owners = _owner_tokens(file_entry or {})
def _can_be(sha1s: list | str) -> bool: def _can_be(sha1s: list | str) -> bool:
if not file_entry or db_files is None: if not file_entry or db_files is None:
@@ -233,6 +241,14 @@ def _resolve_source(
"""Whether the files indexed under *name* can be this entry.""" """Whether the files indexed under *name* can be this entry."""
return _can_be(by_name.get(name) or []) return _can_be(by_name.get(name) or [])
def _folded_hit(name: str) -> bool:
"""_name_hit for a name matched in another casing."""
return _can_be([
sha1 for sha1 in by_name.get(name) or []
if db_files is None
or not _foreign_tree(db_files.get(sha1, {}).get("path", ""), owners)
])
# bios/ via database.json by_name # bios/ via database.json by_name
if fname in by_name and _name_hit(fname): if fname in by_name and _name_hit(fname):
return "bios" return "bios"
@@ -241,10 +257,10 @@ def _resolve_source(
if basename and basename in by_name and _name_hit(basename): if basename and basename in by_name and _name_hit(basename):
return "bios" return "bios"
key = fname.lower() key = fname.lower()
if key in by_name_lower and _name_hit(by_name_lower[key]): if key in by_name_lower and _folded_hit(by_name_lower[key]):
return "bios" return "bios"
if basename: if basename:
if basename.lower() in by_name_lower and _name_hit( if basename.lower() in by_name_lower and _folded_hit(
by_name_lower[basename.lower()] by_name_lower[basename.lower()]
): ):
return "bios" return "bios"
@@ -257,7 +273,7 @@ def _resolve_source(
if canonical and canonical != fname: if canonical and canonical != fname:
if canonical in by_name and _name_hit(canonical): if canonical in by_name and _name_hit(canonical):
return "bios" return "bios"
if canonical.lower() in by_name_lower and _name_hit( if canonical.lower() in by_name_lower and _folded_hit(
by_name_lower[canonical.lower()] by_name_lower[canonical.lower()]
): ):
return "bios" return "bios"
@@ -901,7 +917,9 @@ def _cross_reference_profile(
source = next( source = next(
( (
found for version in versions[slot] found for version in versions[slot]
if (found := entry_source(version, index)) is not None if (found := entry_source(
{**version, "source_profile": emu_name}, index,
)) is not None
), ),
"missing", "missing",
) )
+146 -49
View File
@@ -26,6 +26,7 @@ import argparse
import contextlib import contextlib
import functools import functools
import hashlib import hashlib
import io
import json import json
import os import os
import re import re
@@ -158,6 +159,9 @@ class PackRecord:
self.members: list[str] = [] self.members: list[str] = []
self.files: list[str] = [] self.files: list[str] = []
self.trees: list[str] = [] self.trees: list[str] = []
# The collection file each copied member comes from: a split part
# writes a file only where the full pack writes the same bytes.
self.sources: dict[str, str] = {}
def _add_pack_member(zf: zipfile.ZipFile, source_path: str, arcname: str) -> None: def _add_pack_member(zf: zipfile.ZipFile, source_path: str, arcname: str) -> None:
@@ -173,6 +177,7 @@ def _add_pack_member(zf: zipfile.ZipFile, source_path: str, arcname: str) -> Non
""" """
if isinstance(zf, PackRecord): if isinstance(zf, PackRecord):
zf.members.append(arcname) zf.members.append(arcname)
zf.sources[arcname] = source_path
return return
info = zipfile.ZipInfo(filename=arcname, date_time=_FIXED_DATE_TIME) info = zipfile.ZipInfo(filename=arcname, date_time=_FIXED_DATE_TIME)
info.compress_type = _member_compression(arcname) info.compress_type = _member_compression(arcname)
@@ -603,6 +608,17 @@ def _pack_data_directories(
return added return added
def _case_twin_pairs(
twins: list[str], seen_destinations: set[str], base_dest: str, flatten: bool,
) -> list[tuple[str, str]]:
"""Each spelling left out, with the spelling the pack holds."""
held = {
_flat(dest, base_dest, flatten).lower(): _flat(dest, base_dest, flatten)
for dest in seen_destinations
}
return sorted({(twin, held[twin.lower()]) for twin in twins if twin.lower() in held})
def _report_pack_counts( def _report_pack_counts(
zip_path: str, zip_path: str,
file_status: dict, file_status: dict,
@@ -806,6 +822,7 @@ def generate_pack(
pack_name: str | None = None, pack_name: str | None = None,
no_game_data: bool = False, no_game_data: bool = False,
record: PackRecord | None = None, record: PackRecord | None = None,
within: dict[str, str] | None = None,
) -> str | None: ) -> str | None:
"""Generate a ZIP pack for a platform. """Generate a ZIP pack for a platform.
@@ -819,6 +836,12 @@ def generate_pack(
``record`` builds the pack in memory (PackRecord): no archive is written, ``record`` builds the pack in memory (PackRecord): no archive is written,
no external file fetched, and the paths are kept on it. no external file fetched, and the paths are kept on it.
``within`` is the full pack's PackRecord.sources, given to a split part:
a declared file or core extra is written only where the full pack writes
the same bytes. A part sees neither the other systems' declarations nor
their extras, so a path the full pack settles (a file/directory conflict,
a case twin, a path another system's file takes) was written again.
Returns the path to the generated ZIP, or None on failure. Returns the path to the generated ZIP, or None on failure.
""" """
config = load_platform_config(platform_name, platforms_dir) config = load_platform_config(platform_name, platforms_dir)
@@ -867,6 +890,8 @@ def generate_pack(
user_provided = [] user_provided = []
seen_destinations: set[str] = set() seen_destinations: set[str] = set()
seen_lower: set[str] = set() # only used when case_insensitive=True seen_lower: set[str] = set() # only used when case_insensitive=True
# Spellings left out as case twins of a packed file, for the README.
case_twins: list[str] = []
seen_parents: set[str] = ( seen_parents: set[str] = (
set() set()
) # parent dirs of added files (path conflict detection) ) # parent dirs of added files (path conflict detection)
@@ -1135,7 +1160,10 @@ def generate_pack(
config.get("hash_rule", ""), config.get("hash_rule", ""),
) )
if refusal: if refusal:
file_status[dedup_key] = "excluded" # An earlier declaration of this path shipped a file this
# one refuses: it is in the pack, untested as verify
# counts it, not excluded.
file_status[dedup_key] = "untested" if already_packed else "excluded"
file_reasons[dedup_key] = refusal file_reasons[dedup_key] = refusal
continue continue
if not hash_mismatch_excludes_file(verification_mode) and ( if not hash_mismatch_excludes_file(verification_mode) and (
@@ -1170,7 +1198,17 @@ def generate_pack(
) )
if already_packed: if already_packed:
if dedup_key not in seen_destinations:
case_twins.append(_flat(full_dest, base_dest, flatten))
continue continue
if within is not None and not _same_bytes(
local_path, within.get(_flat(full_dest, base_dest, flatten))
):
# The full pack holds another system's file here.
continue
if file_status.get(dedup_key) == "excluded":
# An earlier declaration refused the file this one ships.
file_status[dedup_key] = "untested"
seen_destinations.add(dedup_key) seen_destinations.add(dedup_key)
_register_path(dedup_key, seen_destinations, seen_parents) _register_path(dedup_key, seen_destinations, seen_parents)
if case_insensitive: if case_insensitive:
@@ -1227,7 +1265,7 @@ def generate_pack(
if required_only and fe.get("required") is False: if required_only and fe.get("required") is False:
continue continue
dest = sanitize_pack_path(fe.get("destination", fe["name"])) dest = sanitize_pack_path(fe.get("destination", fe["name"]))
if region_drops and dest in region_drops: if region_drops and extra_region_key(fe, declared_root(config)) in region_drops:
continue continue
if not dest: if not dest:
continue continue
@@ -1244,10 +1282,13 @@ def generate_pack(
# Taken, a case twin on a case-insensitive filesystem, or a # Taken, a case twin on a case-insensitive filesystem, or a
# file/directory conflict (SGB1.sfc vs SGB1.sfc/): verify replays # file/directory conflict (SGB1.sfc vs SGB1.sfc/): verify replays
# the same rule (packextras.hidden_extras). # the same rule (packextras.hidden_extras).
if placement_block( block = placement_block(
full_dest, seen_destinations, seen_lower, seen_parents, full_dest, seen_destinations, seen_lower, seen_parents,
case_insensitive, case_insensitive,
): )
if block:
if block == "case_twin" and extra_resolver.resolve(fe, dest)[0]:
case_twins.append(_flat(full_dest, base_dest, flatten))
continue continue
# The emulator's own check picks the copy, as for a declared file # The emulator's own check picks the copy, as for a declared file
@@ -1275,6 +1316,8 @@ def generate_pack(
) )
flat_dest = _flat(full_dest, base_dest, flatten) flat_dest = _flat(full_dest, base_dest, flatten)
if within is not None and not _same_bytes(local_path, within.get(flat_dest)):
continue
if local_path.endswith(".zip"): if local_path.endswith(".zip"):
_add_zip_to_pack(local_path, flat_dest, zf, fe) _add_zip_to_pack(local_path, flat_dest, zf, fe)
else: else:
@@ -1339,6 +1382,37 @@ def generate_pack(
if _registry_path.exists(): if _registry_path.exists():
with open(_registry_path) as _rf: with open(_registry_path) as _rf:
_pack_registry = (yaml_load(_rf) or {}).get("platforms", {}) _pack_registry = (yaml_load(_rf) or {}).get("platforms", {})
keeps_case = case_insensitive and any(
d.get("os") not in CASE_FOLDING_OS
for d in (_pack_registry.get(platform_name, {}).get("install", {}) or {})
.get("detect", [])
)
if keeps_case and source != "platform" and precomputed_extras is None:
# The spellings a folded platform counts as declared never reach
# the extras: collected as the filesystem that keeps case sees
# them, as the manifest's case_variants are (Mesen-CE's bios.col
# beside the declared BIOS.col).
unfolded = _collect_emulator_extras(
{**config, "case_insensitive_fs": False}, emulators_dir, db, set(),
base_dest, emu_profiles, target_cores=target_cores,
include_all=(source == "truth"),
)
if system_filter:
unfolded = _extras_for_systems(unfolded, system_filter)
prefix = _detect_extras_prefix(config, base_dest)
for fe in unfolded:
if required_only and fe.get("required") is False:
continue
if region_drops and extra_region_key(fe, declared_root(config)) in region_drops:
continue
full = extra_full_destination(fe, prefix)
if (
full and full not in seen_destinations and full.lower() in seen_lower
and extra_resolver.resolve(
fe, sanitize_pack_path(fe.get("destination", fe["name"]))
)[0]
):
case_twins.append(_flat(full, base_dest, flatten))
readme_text = _build_readme( readme_text = _build_readme(
platform_name, platform_display, base_dest, total_files, num_systems, platform_name, platform_display, base_dest, total_files, num_systems,
source=source, source=source,
@@ -1364,6 +1438,10 @@ def generate_pack(
) if save_count else "" ) if save_count else ""
), ),
save_by_system=bool(save_count and save_layout(config).by_system), save_by_system=bool(save_count and save_layout(config).by_system),
case_twins=(
_case_twin_pairs(case_twins, seen_destinations, base_dest, flatten)
if keeps_case else []
),
) )
_write_generated_member(zf, "README.txt", readme_text) _write_generated_member(zf, "README.txt", readme_text)
@@ -1408,6 +1486,7 @@ def _extract_zip_to_archive(
for name in map(sanitize_pack_path, src.namelist()) for name in map(sanitize_pack_path, src.namelist())
if name and not name.endswith("/") if name and not name.endswith("/")
) )
target_zf.sources[dest_prefix] = source_zip
return return
for info in src.infolist(): for info in src.infolist():
if info.is_dir(): if info.is_dir():
@@ -1441,6 +1520,7 @@ def _add_zip_to_pack(
""" """
if isinstance(target_zf, PackRecord): if isinstance(target_zf, PackRecord):
target_zf.members.append(dest_path) target_zf.members.append(dest_path)
target_zf.sources[dest_path] = source_zip
return return
_check_zip_members(source_zip) _check_zip_members(source_zip)
_add_pack_member(target_zf, source_zip, dest_path) _add_pack_member(target_zf, source_zip, dest_path)
@@ -2069,8 +2149,14 @@ def generate_split_packs(
one_per_slot: bool = False, one_per_slot: bool = False,
offline: bool | None = None, offline: bool | None = None,
no_game_data: bool = False, no_game_data: bool = False,
records: dict[str, PackRecord] | None = None,
) -> list[str]: ) -> list[str]:
"""Generate split packs (one ZIP per system or manufacturer).""" """Generate split packs (one ZIP per system or manufacturer).
The parts add up to the full pack: each is built against the full pack's
own placement (``within``). ``records`` builds the parts in memory, keyed
by part name, with no archive written.
"""
config = load_platform_config(platform_name, platforms_dir) config = load_platform_config(platform_name, platforms_dir)
# The split directory carries the same tags as the packs inside it, or two # The split directory carries the same tags as the packs inside it, or two
# differently narrowed runs write into one another. # differently narrowed runs write into one another.
@@ -2093,10 +2179,36 @@ def generate_split_packs(
f"{_platform_pack_stem([platform_name], platform_name, platforms_dir)}{split_tags}" f"{_platform_pack_stem([platform_name], platform_name, platforms_dir)}{split_tags}"
) )
split_dir = os.path.join(output_dir, f"{part_prefix}{group_tag}_Split") split_dir = os.path.join(output_dir, f"{part_prefix}{group_tag}_Split")
os.makedirs(split_dir, exist_ok=True) if records is None:
os.makedirs(split_dir, exist_ok=True)
if emu_profiles is None: if emu_profiles is None:
emu_profiles = load_emulator_profiles(emulators_dir) emu_profiles = load_emulator_profiles(emulators_dir)
narrowing = {
"emulators_dir": emulators_dir, "zip_contents": zip_contents,
"data_registry": data_registry, "emu_profiles": emu_profiles,
"target_cores": target_cores, "required_only": required_only,
"source": source, "regions": regions, "target_name": target_name,
"one_per_slot": one_per_slot, "offline": offline,
"no_game_data": no_game_data,
}
full = PackRecord()
with contextlib.redirect_stdout(io.StringIO()):
generate_pack(
platform_name, platforms_dir, db, bios_dir, split_dir,
record=full, **narrowing,
)
def part(pack_name: str, **scope: object) -> str | None:
record = PackRecord() if records is not None else None
zip_path = generate_pack(
platform_name, platforms_dir, db, bios_dir, split_dir,
pack_name=pack_name, record=record, within=full.sources,
**scope, **narrowing,
)
if record is not None and zip_path:
records[pack_name] = record
return zip_path
# The systems a core of the target runs: a part per declared system sent # The systems a core of the target runs: a part per declared system sent
# each one the target drops to a refusal naming it as unknown. # each one the target drops to a refusal naming it as unknown.
systems = platform_scope_systems(config, emu_profiles, target_cores) systems = platform_scope_systems(config, emu_profiles, target_cores)
@@ -2130,27 +2242,9 @@ def generate_split_packs(
results = [] results = []
for group_name, group_system_ids in sorted(groups.items()): for group_name, group_system_ids in sorted(groups.items()):
group_extras = _extras_for_systems(all_extras, group_system_ids) group_extras = _extras_for_systems(all_extras, group_system_ids)
zip_path = generate_pack( zip_path = part(
platform_name, f"{part_prefix}_{_name_part(group_name, '_')}_BIOS_Pack.zip",
platforms_dir, system_filter=group_system_ids, precomputed_extras=group_extras,
db,
bios_dir,
split_dir,
emulators_dir=emulators_dir,
zip_contents=zip_contents,
data_registry=data_registry,
emu_profiles=emu_profiles,
target_cores=target_cores,
required_only=required_only,
system_filter=group_system_ids,
precomputed_extras=group_extras,
source=source,
regions=regions,
target_name=target_name,
one_per_slot=one_per_slot,
offline=offline,
pack_name=f"{part_prefix}_{_name_part(group_name, '_')}_BIOS_Pack.zip",
no_game_data=no_game_data,
) )
if zip_path: if zip_path:
results.append(zip_path) results.append(zip_path)
@@ -2166,15 +2260,9 @@ def generate_split_packs(
} }
undistributed = [fe for fe in all_extras if id(fe) not in distributed] undistributed = [fe for fe in all_extras if id(fe) not in distributed]
if undistributed: if undistributed:
zip_path = generate_pack( zip_path = part(
platform_name, platforms_dir, db, bios_dir, split_dir, f"{part_prefix}_Other_Cores_BIOS_Pack.zip",
emulators_dir=emulators_dir, zip_contents=zip_contents, precomputed_extras=undistributed, extras_only=True,
data_registry=data_registry, emu_profiles=emu_profiles,
target_cores=target_cores, required_only=required_only,
precomputed_extras=undistributed, extras_only=True, source=source,
regions=regions, target_name=target_name, one_per_slot=one_per_slot,
offline=offline, pack_name=f"{part_prefix}_Other_Cores_BIOS_Pack.zip",
no_game_data=no_game_data,
) )
if zip_path: if zip_path:
results.append(zip_path) results.append(zip_path)
@@ -4133,7 +4221,8 @@ def _manifest_core_entries(
dest = sanitize_pack_path(fe.get("destination", fe["name"])) dest = sanitize_pack_path(fe.get("destination", fe["name"]))
if not dest: if not dest:
continue continue
if region_drops and dest in region_drops: region_key = extra_region_key(fe, declared_root(config))
if region_drops and region_key in region_drops:
continue continue
full_dest = extra_full_destination(fe, extras_pfx) full_dest = extra_full_destination(fe, extras_pfx)
if not full_dest: if not full_dest:
@@ -4207,7 +4296,7 @@ def _manifest_core_entries(
entry, file_size = _manifest_entry( entry, file_size = _manifest_entry(
local_path, manifest_dest, [source_emu] if source_emu else [], local_path, manifest_dest, [source_emu] if source_emu else [],
db, repo_root, db, repo_root,
selection.fields(dest, fe.get("name", "")), selection.fields(region_key, fe.get("name", "")),
) )
if entry is not None and save_root: if entry is not None and save_root:
entry.update(save_entry_fields(fe)) entry.update(save_entry_fields(fe))
@@ -4321,7 +4410,8 @@ def _manifest_selection(
else set() else set()
) )
return drops, SelectionIndex.build( return drops, SelectionIndex.build(
region_groups, region_index, pack_systems, extras, required_only region_groups, region_index, pack_systems, extras, required_only,
extras_prefix=declared_root(config),
), held ), held
@@ -4346,7 +4436,9 @@ def _record_absent(
if required_only and fe.get("required") is False: if required_only and fe.get("required") is False:
continue continue
dest = sanitize_pack_path(fe.get("destination", fe.get("name", ""))) dest = sanitize_pack_path(fe.get("destination", fe.get("name", "")))
if not dest or (region_drops and dest in region_drops): if not dest or (
region_drops and extra_region_key(fe, declared_root(config)) in region_drops
):
continue continue
full_dest = extra_full_destination(fe, extras_pfx) full_dest = extra_full_destination(fe, extras_pfx)
if not full_dest: if not full_dest:
@@ -4517,16 +4609,18 @@ def generate_manifest(
# (cache file, size). Settled once the trees are walked. # (cache file, size). Settled once the trees are walked.
cache_held: dict[str, tuple[str, int]] = {} cache_held: dict[str, tuple[str, int]] = {}
rename_notes: set[str] = set() rename_notes: set[str] = set()
# The full manifest records every core's answer at a contested slot, so a
# target narrowed from it gets what that target's own pack serves.
whole = target_cores is None and source != "platform" and bool(emu_profiles)
slot_choices = (
slots.slot_choices(config, emu_profiles, db, zip_contents, data_registry)
if whole else {}
)
# Derived from the same choices: computed twice, it was 8% of a run.
slot_overrides = slots.pack_overrides( slot_overrides = slots.pack_overrides(
config, emu_profiles, db, zip_contents, data_registry, config, emu_profiles, db, zip_contents, data_registry,
target_cores=target_cores, source=source, target_cores=target_cores, source=source,
) choices=slot_choices if whole else None,
# The full manifest records every core's answer at a contested slot, so a
# target narrowed from it gets what that target's own pack serves.
slot_choices = (
slots.slot_choices(config, emu_profiles, db, zip_contents, data_registry)
if target_cores is None and source != "platform" and emu_profiles
else {}
) )
def manifest_destination(full_destination: str) -> str: def manifest_destination(full_destination: str) -> str:
@@ -4541,8 +4635,10 @@ def generate_manifest(
reason: str, reason: str,
cores: list[str] | None, cores: list[str] | None,
) -> None: ) -> None:
bare = sanitize_pack_path( # A core extra (cores given) is keyed as the region pass keys it.
file_entry.get("destination", file_entry.get("name", "")) bare = (
sanitize_pack_path(file_entry.get("destination", file_entry.get("name", "")))
if cores is None else extra_region_key(file_entry, declared_root(config))
) )
save_root = file_entry.get("root") == "save" save_root = file_entry.get("root") == "save"
omitted_by_destination[full_destination] = { omitted_by_destination[full_destination] = {
@@ -5443,6 +5539,7 @@ from packpaths import ( # noqa: E402,F401
from packextras import ( # noqa: E402,F401 from packextras import ( # noqa: E402,F401
ExtraResolver, ExtraResolver,
extra_full_destination, extra_full_destination,
extra_region_key,
prefixed_destination, prefixed_destination,
save_entry_fields, save_entry_fields,
save_manifest_dest, save_manifest_dest,
+3 -3
View File
@@ -9478,7 +9478,7 @@ def _hash_spans(value: str | list, copy: bool = False) -> str:
def _availability_check( def _availability_check(
db: dict, data_names: set[str] | None db: dict, data_names: set[str] | None, owner: str = "",
) -> Callable[[dict], bool]: ) -> Callable[[dict], bool]:
"""A predicate answering whether the collection holds a file. """A predicate answering whether the collection holds a file.
@@ -9509,7 +9509,7 @@ def _availability_check(
# under the name belongs to another game or release. # under the name belongs to another game or release.
if not f.get("name") or f.get("unsourceable"): if not f.get("name") or f.get("unsourceable"):
return False return False
return entry_source(f, index) is not None return entry_source({**f, "source_profile": owner} if owner else f, index) is not None
return _file_available return _file_available
@@ -10242,7 +10242,7 @@ def emulator_pages(
lines.append("") lines.append("")
lines.extend(markup.aside("info", "Why no files", [_escape_tags(str(exclusion))])[:-1]) lines.extend(markup.aside("info", "Why no files", [_escape_tags(str(exclusion))])[:-1])
else: else:
_file_available = _availability_check(db, data_names) _file_available = _availability_check(db, data_names, name)
lines.extend(["## Files", ""]) lines.extend(["## Files", ""])
lines.extend(_emulator_file_summary(files)) lines.extend(_emulator_file_summary(files))
+73 -11
View File
@@ -12,6 +12,7 @@ import os
import sys import sys
import re import re
import tempfile import tempfile
import time
import urllib.error import urllib.error
import urllib.parse import urllib.parse
import urllib.request import urllib.request
@@ -26,6 +27,13 @@ from hashing import compute_hashes
LARGE_FILES_RELEASE = "large-files" LARGE_FILES_RELEASE = "large-files"
LARGE_FILES_REPO = "Abdess/retrobios" LARGE_FILES_REPO = "Abdess/retrobios"
LARGE_FILES_CACHE = ".cache/large" LARGE_FILES_CACHE = ".cache/large"
# Verified revisions, hard-linked as <sha1>/<asset> beside the cache. A name
# in the cache is replaced when the release serves new bytes under it, so a
# path handed out by name could hold other bytes by the time the caller read
# them. Nothing ever writes other bytes under a sha1. The asset name stays
# last: callers read the extension (an archive is packed as one).
VERIFIED_STORE = ".by-sha1"
ORPHAN_GRACE = 24 * 3600
GITIGNORE = Path(__file__).resolve().parent.parent / ".gitignore" GITIGNORE = Path(__file__).resolve().parent.parent / ".gitignore"
_UNSAFE_ASSET_CHARS = re.compile(r"[^A-Za-z0-9._-]+") _UNSAFE_ASSET_CHARS = re.compile(r"[^A-Za-z0-9._-]+")
@@ -169,14 +177,14 @@ def _fetch_asset(
if os.path.isdir(cached): if os.path.isdir(cached):
return None return None
if os.path.exists(cached) and not (expected_sha1 or expected_md5):
return cached
if os.path.exists(cached): if os.path.exists(cached):
snapshot = _read_once(cached) snapshot = _read_once(cached)
if snapshot is None: if snapshot is None:
pass pass
elif _matches(snapshot[0], expected_sha1, expected_md5): elif _matches(snapshot[0], expected_sha1, expected_md5):
return cached pinned = _pin_verified(cached, snapshot, dest_dir, name)
if pinned:
return pinned
elif not _replaced_on_release(name, snapshot[0]["sha256"], snapshot[1], offline): elif not _replaced_on_release(name, snapshot[0]["sha256"], snapshot[1], offline):
# A copy of this asset that answers another hash: the caller # A copy of this asset that answers another hash: the caller
# wants a different revision under the same name, and the # wants a different revision under the same name, and the
@@ -220,17 +228,71 @@ def _fetch_asset(
print(f" large file {candidate}: {exc}", file=sys.stderr) print(f" large file {candidate}: {exc}", file=sys.stderr)
if not downloaded: if not downloaded:
return None return None
if (expected_sha1 or expected_md5) and not _matches( snapshot = _read_once(tmp_path)
compute_hashes(tmp_path), expected_sha1, expected_md5 if snapshot is None or not _matches(snapshot[0], expected_sha1, expected_md5):
):
return None return None
set_plain_mode(tmp_path) set_plain_mode(tmp_path)
pinned = _pin_verified(tmp_path, snapshot, dest_dir, name)
os.replace(tmp_path, cached) os.replace(tmp_path, cached)
return cached _prune_verified(dest_dir)
return pinned
finally: finally:
_drop(tmp_path) _drop(tmp_path)
def _pin_verified(
path: str, snapshot: tuple[dict[str, str], int, int], dest_dir: str, name: str,
) -> str | None:
"""The verified revision at *path*, as <sha1>/<name> in VERIFIED_STORE.
*snapshot* is what _read_once read: the link is kept only if it holds
that inode, so a copy another run promoted after the hash is never
passed off as the one verified. None when the name moved in between.
"""
revision = os.path.join(dest_dir, VERIFIED_STORE, snapshot[0]["sha1"])
os.makedirs(revision, exist_ok=True)
final = os.path.join(revision, name)
if os.path.isfile(final):
return final
scratch = os.path.join(revision, f".{os.getpid()}.{os.urandom(4).hex()}.tmp")
try:
os.link(path, scratch)
except FileNotFoundError:
return None
try:
if os.stat(scratch).st_ino != snapshot[2]:
return None
os.replace(scratch, final)
return final
finally:
with contextlib.suppress(FileNotFoundError):
os.unlink(scratch)
def _prune_verified(dest_dir: str) -> None:
"""Drop the revisions no cache name has held for ORPHAN_GRACE seconds.
Losing its cache name changes the inode's ctime, so a revision another
run pinned just before the release replaced it stays readable while that
run packs it.
"""
store = os.path.join(dest_dir, VERIFIED_STORE)
now = time.time()
for revision in os.scandir(store):
if not revision.is_dir():
continue
for entry in os.scandir(revision.path):
if entry.name.endswith(".tmp"):
continue
with contextlib.suppress(FileNotFoundError):
stat = entry.stat()
if stat.st_nlink == 1 and now - stat.st_ctime > ORPHAN_GRACE:
os.unlink(entry.path)
# Another run may be linking a copy into it: left for the next prune.
with contextlib.suppress(FileNotFoundError, OSError):
os.rmdir(revision.path)
def _asset_urls(name: str) -> list[tuple[str, str]]: def _asset_urls(name: str) -> list[tuple[str, str]]:
"""Release URLs an asset may be published under, with their names. """Release URLs an asset may be published under, with their names.
@@ -252,9 +314,9 @@ def _asset_urls(name: str) -> list[tuple[str, str]]:
] ]
def _read_once(path: str) -> tuple[dict[str, str], int] | None: def _read_once(path: str) -> tuple[dict[str, str], int, int] | None:
"""Digests and size of the file at *path*, all read from one file, or """Digests, size and inode of the file at *path*, all read from one
None when it is gone. file, or None when it is gone.
Every judgement on a cache copy reads this one snapshot. Hashed, then Every judgement on a cache copy reads this one snapshot. Hashed, then
hashed again by path for the release check, a copy another run promoted hashed again by path for the release check, a copy another run promoted
@@ -271,7 +333,7 @@ def _read_once(path: str) -> tuple[dict[str, str], int] | None:
except FileNotFoundError: except FileNotFoundError:
return None return None
if (before.st_ino, before.st_mtime_ns) == (after.st_ino, after.st_mtime_ns): if (before.st_ino, before.st_mtime_ns) == (after.st_ino, after.st_mtime_ns):
return hashes, after.st_size return hashes, after.st_size, after.st_ino
return None return None
+10 -2
View File
@@ -19,7 +19,13 @@ from dataclasses import dataclass
import region as region_mod import region as region_mod
from common import _norm_system_id, sanitize_pack_path from common import _norm_system_id, sanitize_pack_path
from packextras import _kept, _serving_system_ids, extra_region_groups, system_spellings from packextras import (
_kept,
_serving_system_ids,
extra_region_groups,
extra_region_key,
system_spellings,
)
def _dest(entry: dict) -> str: def _dest(entry: dict) -> str:
@@ -44,6 +50,7 @@ class SelectionIndex:
pack_systems: dict[str, dict], pack_systems: dict[str, dict],
extras: list[dict], extras: list[dict],
required_only: bool = False, required_only: bool = False,
extras_prefix: str = "",
) -> SelectionIndex: ) -> SelectionIndex:
by_norm: dict[str, set[str]] = {} by_norm: dict[str, set[str]] = {}
for sys_id in pack_systems: for sys_id in pack_systems:
@@ -60,7 +67,8 @@ class SelectionIndex:
extra_groups: dict[str, set[str]] = {} extra_groups: dict[str, set[str]] = {}
spellings = system_spellings(pack_systems) spellings = system_spellings(pack_systems)
for extra in extras: for extra in extras:
dest = _dest(extra) # Keyed as platform_region_groups keys it (extra_region_key).
dest = extra_region_key(extra, extras_prefix)
if not dest: if not dest:
continue continue
extra_groups.setdefault(dest, set()).update( extra_groups.setdefault(dest, set()).update(
+18 -2
View File
@@ -62,6 +62,20 @@ def prefixed_destination(dest: str, prefix: str) -> str:
return f"{prefix}/{dest}" return f"{prefix}/{dest}"
def extra_region_key(fe: dict, prefix: str) -> str:
"""The key a core extra is grouped, dropped and held under by --region.
Its destination with the BIOS directory the platform writes in front of
its own (declared_root): RetroDECK declares bios/scph5501.bin, and
Beetle PSX's scph5501.bin grouped bare was looked up under another key,
kept by the pack while the declaration of the same file was dropped,
and the installer, which records the path once, dropped it.
"""
return prefixed_destination(
sanitize_pack_path(fe.get("destination", fe.get("name", ""))), prefix
)
def save_entry_fields(fe: dict) -> dict: def save_entry_fields(fe: dict) -> dict:
"""What a manifest entry adds for a file read below the save directory. """What a manifest entry adds for a file read below the save directory.
@@ -364,6 +378,7 @@ def hidden_extras(
content[dest.lower()] = sha1_by_path.get(local) content[dest.lower()] = sha1_by_path.get(local)
lower = set(content) if case_insensitive else set() lower = set(content) if case_insensitive else set()
prefix = _detect_extras_prefix(config, base_dest) prefix = _detect_extras_prefix(config, base_dest)
region_prefix = declared_root(config)
hidden: dict[tuple[str, str, str], str] = {} hidden: dict[tuple[str, str, str], str] = {}
packed_keys: set[tuple[str, str, str]] = set() packed_keys: set[tuple[str, str, str]] = set()
extras = _collect_emulator_extras( extras = _collect_emulator_extras(
@@ -377,7 +392,7 @@ def hidden_extras(
if required_only and fe.get("required") is False: if required_only and fe.get("required") is False:
continue continue
dest = sanitize_pack_path(fe.get("destination", fe.get("name", ""))) dest = sanitize_pack_path(fe.get("destination", fe.get("name", "")))
if not dest or (region_drops and dest in region_drops): if not dest or (region_drops and extra_region_key(fe, region_prefix) in region_drops):
continue continue
full = extra_full_destination(fe, prefix) full = extra_full_destination(fe, prefix)
if not full: if not full:
@@ -1705,8 +1720,9 @@ def platform_region_groups(
) )
held = {id(extra) for extra in extras} held = {id(extra) for extra in extras}
spellings = system_spellings(systems) spellings = system_spellings(systems)
region_prefix = declared_root(config)
for extra in _kept(extras + absent, required_only): for extra in _kept(extras + absent, required_only):
dest = sanitize_pack_path(extra.get("destination", extra.get("name", ""))) dest = extra_region_key(extra, region_prefix)
if not dest: if not dest:
continue continue
name = extra.get("name", "") name = extra.get("name", "")
+19
View File
@@ -207,6 +207,7 @@ def _build_readme(
save_folder: str = "", save_folder: str = "",
save_where: str = "", save_where: str = "",
save_by_system: bool = False, save_by_system: bool = False,
case_twins: list[tuple[str, str]] | None = None,
) -> str: ) -> str:
"""Build a personalized step-by-step README for each platform pack. """Build a personalized step-by-step README for each platform pack.
@@ -216,6 +217,10 @@ def _build_readme(
frontend's save directory, empty when the pack holds none, and frontend's save directory, empty when the pack holds none, and
``save_where`` that directory's default place (save_place). ``save_where`` that directory's default place (save_place).
``save_by_system`` is set when the launcher hands each system a folder. ``save_by_system`` is set when the launcher hands each system a folder.
``case_twins`` pairs each spelling the pack leaves out, a case twin of a
packed file, with the one it holds: given for a platform that also runs
where names keep their case (EmuDeck and RetroArch on Linux), where the
core asking for the other spelling finds nothing.
""" """
narrowings = narrowings or [] narrowings = narrowings or []
sep = "=" * 50 sep = "=" * 50
@@ -245,6 +250,20 @@ def _build_readme(
+ (pack_manual(platform_name) or _generic_manual(bios_paths)) + (pack_manual(platform_name) or _generic_manual(bios_paths))
) )
if case_twins:
listed = "".join(
f" {missing} (the pack has {held})\n" for missing, held in case_twins
)
guide += (
"NAMES THAT DIFFER ONLY BY CASE\n\n"
" Some cores open the same file under another spelling. This\n"
" pack holds one spelling of each, all Windows and macOS can\n"
" store in one folder. Linux, SteamOS and Android keep both:\n"
" there, run the installer, which adds these spellings, or\n"
" copy each file under the name below.\n\n"
f"{listed}\n"
)
if save_folder: if save_folder:
guide += _save_section( guide += _save_section(
platform_display, save_folder, save_where, save_by_system, platform_display, save_folder, save_where, save_by_system,
+106 -10
View File
@@ -55,6 +55,7 @@ import subprocess
import sys import sys
import tempfile import tempfile
import time import time
from collections.abc import Callable
from pathlib import Path from pathlib import Path
# The build scripts need CPython 3.9, which builds the same database, # The build scripts need CPython 3.9, which builds the same database,
@@ -562,6 +563,7 @@ def check_emulator_packs(
comparison holds them together. No archive is written and no file read. comparison holds them together. No archive is written and no file read.
""" """
import generate_pack # noqa: PLC0415 import generate_pack # noqa: PLC0415
import packresolve # noqa: PLC0415
import verify # noqa: PLC0415 import verify # noqa: PLC0415
from common import ( # noqa: PLC0415 from common import ( # noqa: PLC0415
ProfileSelectionError, ProfileSelectionError,
@@ -580,16 +582,23 @@ def check_emulator_packs(
def compare(names: list[str], standalone: bool, label: str, **narrowing: object) -> bool: def compare(names: list[str], standalone: bool, label: str, **narrowing: object) -> bool:
record = generate_pack.PackRecord() record = generate_pack.PackRecord()
with contextlib.redirect_stdout(io.StringIO()), \ was_offline = packresolve._OFFLINE
contextlib.redirect_stderr(io.StringIO()): # The pack is built offline: verify counts a release asset the cache
generate_pack.generate_emulator_pack( # lacks the same way, missing, as the narrowed-pack step does.
names, emulators_dir, db, "bios", "", standalone, zip_contents, packresolve.set_offline(True)
offline=True, platforms_dir=platforms_dir, record=record, **narrowing, try:
) with contextlib.redirect_stdout(io.StringIO()), \
report = verify.verify_emulator( contextlib.redirect_stderr(io.StringIO()):
names, emulators_dir, db, standalone, platforms_dir=platforms_dir, generate_pack.generate_emulator_pack(
**narrowing, names, emulators_dir, db, "bios", "", standalone, zip_contents,
) offline=True, platforms_dir=platforms_dir, record=record, **narrowing,
)
report = verify.verify_emulator(
names, emulators_dir, db, standalone, platforms_dir=platforms_dir,
**narrowing,
)
finally:
packresolve.set_offline(was_offline)
lines = _set_difference_lines( lines = _set_difference_lines(
label, set(report["pack_destinations"]), set(record.files) label, set(report["pack_destinations"]), set(record.files)
) )
@@ -802,6 +811,92 @@ def check_narrowed_packs(
return all_ok return all_ok
def split_differences(
full: object, parts: dict[str, object], same_bytes: Callable[[str, str], bool],
) -> list[str]:
"""Where the --split parts of a pack, together, are not the full pack.
A path in a part and not in the full pack, a path of the full pack in no
part, and a path a part fills with other bytes than the full pack.
"""
full_members = {m for m in full.members if not _PACK_NOTES.search(m)}
union = {
m for record in parts.values() for m in record.members
if not _PACK_NOTES.search(m)
}
lines = [f"in a part, not in the full pack: {m}" for m in sorted(union - full_members)]
lines += [f"in the full pack, in no part: {m}" for m in sorted(full_members - union)]
lines += sorted({
f"other bytes in {name}: {member}"
for name, record in parts.items()
for member, source in record.sources.items()
if member in full.sources and not same_bytes(source, full.sources[member])
})
return lines
def check_split_packs(
emulators_dir: str = "emulators", platforms_dir: str = "platforms",
db_path: str = "database.json", include_archived: bool = False,
) -> bool:
"""Build each platform's --split parts in memory and compare them with
the full pack: the parts must add up to it, path for path and byte for
byte. Each part is a pack of its own and used to settle its paths alone:
RetroDECK's parts carried bios/SGB1.sfc as a file and as a directory,
RetroBat's 21 openmsx/ twins of openMSX/ files. Offline, as every
comparison of this step.
"""
import generate_pack # noqa: PLC0415
import packresolve # noqa: PLC0415
from common import ( # noqa: PLC0415
build_zip_contents_index,
group_identical_platforms,
list_registered_platforms,
load_data_dir_registry,
load_database,
load_emulator_profiles,
)
print(f"\n--- {step_title(STEP_LABELS['consistency'])}, split packs ---")
db = load_database(db_path)
zip_contents = build_zip_contents_index(db)
profiles = load_emulator_profiles(emulators_dir)
registry = load_data_dir_registry(platforms_dir)
groups = group_identical_platforms(
list_registered_platforms(platforms_dir, include_archived=include_archived),
platforms_dir,
)
was_offline = packresolve._OFFLINE
packresolve.set_offline(True)
all_ok = True
try:
for _members, platform in groups:
args = dict(
emulators_dir=emulators_dir, zip_contents=zip_contents,
data_registry=registry, emu_profiles=profiles, offline=True,
)
full = generate_pack.PackRecord()
parts: dict[str, generate_pack.PackRecord] = {}
with contextlib.redirect_stdout(io.StringIO()), \
contextlib.redirect_stderr(io.StringIO()):
generate_pack.generate_pack(
platform, platforms_dir, db, "bios", "", record=full, **args,
)
generate_pack.generate_split_packs(
platform, platforms_dir, db, "bios", "", records=parts, **args,
)
lines = split_differences(full, parts, generate_pack._same_bytes)
if lines:
all_ok = False
print(f" {platform} --split: {len(lines)} differences")
print("\n".join(f" {line}" for line in lines[:20]))
finally:
packresolve.set_offline(was_offline)
print(f" {len(groups)} split platform packs compared, "
f"{'all equal' if all_ok else 'differences above'}")
return all_ok
class _Skipped: class _Skipped:
"""A step that did not run. Truthy, so it never fails the pipeline, and """A step that did not run. Truthy, so it never fails the pipeline, and
distinct, so the summary does not report it as work that was done.""" distinct, so the summary does not report it as work that was done."""
@@ -1365,6 +1460,7 @@ def main() -> None:
ok = check_narrowed_packs( ok = check_narrowed_packs(
include_archived=args.include_archived, full=args.full_narrowings, include_archived=args.include_archived, full=args.full_narrowings,
) and ok ) and ok
ok = check_split_packs(include_archived=args.include_archived) and ok
print( print(
f"--- {step_title(STEP_LABELS['consistency'])}, counts and paths: " f"--- {step_title(STEP_LABELS['consistency'])}, counts and paths: "
f"{'OK' if ok else 'FAILED'} ---" f"{'OK' if ok else 'FAILED'} ---"
+36 -3
View File
@@ -17,11 +17,14 @@ Usage:
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import contextlib
import hashlib import hashlib
import os import os
import sys import sys
import tempfile
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
from artifacts import set_plain_mode
from common import copy_file_atomic, load_database from common import copy_file_atomic, load_database
@@ -56,6 +59,36 @@ def index_cache(cache_dir: str) -> dict[str, str]:
return index return index
def _copy_verified(source: str, path: str, sha1: str) -> bool:
"""Copy *source* to *path* only if the copy hashes to *sha1*.
The cache was hashed before the copy, and another run can replace a
cached name in between (a re-uploaded asset): the copy is what counts.
Whole or absent: a scan running beside a plain copy hashed the truncated
file and published that in the database.
"""
handle, scratch = tempfile.mkstemp(
dir=os.path.dirname(os.path.abspath(path)),
prefix=f".{os.path.basename(path)}.", suffix=".tmp",
)
os.close(handle)
try:
copy_file_atomic(source, scratch)
digest = hashlib.sha1()
with open(scratch, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
digest.update(chunk)
if digest.hexdigest() != sha1:
print(f"Changed in the cache while restoring: {source}", file=sys.stderr)
return False
set_plain_mode(scratch)
os.replace(scratch, path)
return True
finally:
with contextlib.suppress(FileNotFoundError):
os.unlink(scratch)
def restore( def restore(
cache_dir: str, db_path: str, gitignore: str cache_dir: str, db_path: str, gitignore: str
) -> tuple[int, list[str]]: ) -> tuple[int, list[str]]:
@@ -79,9 +112,9 @@ def restore(
unsatisfied.append(path) unsatisfied.append(path)
continue continue
os.makedirs(os.path.dirname(path), exist_ok=True) os.makedirs(os.path.dirname(path), exist_ok=True)
# Whole or absent: a scan running beside a plain copy hashed the if not _copy_verified(source, path, sha1):
# truncated file and published that in the database. unsatisfied.append(path)
copy_file_atomic(source, path) continue
print(f"Restored: {path}") print(f"Restored: {path}")
restored += 1 restored += 1
print(f"Total: {restored} files restored") print(f"Total: {restored} files restored")
+7 -5
View File
@@ -524,6 +524,7 @@ def pack_overrides(
data_dir_registry: dict | None = None, data_dir_registry: dict | None = None,
target_cores: set[str] | None = None, target_cores: set[str] | None = None,
source: str = "full", source: str = "full",
choices: dict[str, list[tuple[str, str | None]]] | None = None,
) -> dict[str, str]: ) -> dict[str, str]:
"""Full pack destination -> the file a platform pack serves there instead. """Full pack destination -> the file a platform pack serves there instead.
@@ -532,7 +533,8 @@ def pack_overrides(
existence mode the frontend never reads the bytes, so serving the existence mode the frontend never reads the bytes, so serving the
emulator's file satisfies both. The ZIP builder, the install manifest and emulator's file satisfies both. The ZIP builder, the install manifest and
verify all read this: two of them deciding alone gave the one-line verify all read this: two of them deciding alone gave the one-line
installer different bytes than the ZIP. installer different bytes than the ZIP. ``choices`` is slot_choices
for the same arguments when the caller already holds it.
""" """
# A baseline-only pack serves what the platform declares, and a target's # A baseline-only pack serves what the platform declares, and a target's
# pack answers to the cores that hardware builds: DuckStation does not # pack answers to the cores that hardware builds: DuckStation does not
@@ -541,10 +543,10 @@ def pack_overrides(
return {} return {}
cores = set(resolve_platform_cores(config, profiles, target_cores=target_cores)) cores = set(resolve_platform_cores(config, profiles, target_cores=target_cores))
overrides: dict[str, str] = {} overrides: dict[str, str] = {}
for destination, choices in slot_choices( if choices is None:
config, profiles, db, zip_contents, data_dir_registry choices = slot_choices(config, profiles, db, zip_contents, data_dir_registry)
).items(): for destination, ordered in choices.items():
path = next((path for emulator, path in choices if emulator in cores), None) path = next((path for emulator, path in ordered if emulator in cores), None)
if path: if path:
overrides[destination] = path overrides[destination] = path
return overrides return overrides
+79 -20
View File
@@ -26,11 +26,12 @@ from __future__ import annotations
import argparse import argparse
import contextlib import contextlib
import functools
import json import json
import os import os
import sys import sys
from collections import Counter from collections import Counter
from collections.abc import Mapping from collections.abc import Callable, Mapping
from pathlib import Path from pathlib import Path
import slots import slots
@@ -38,6 +39,8 @@ import slots
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
from hashing import parse_md5_list from hashing import parse_md5_list
from common import ( from common import (
_foreign_tree,
_owner_tokens,
platform_arg, platform_arg,
target_listing_line, target_listing_line,
suggest_names, suggest_names,
@@ -419,14 +422,25 @@ def _build_expected(file_entry: dict, checks: list[str]) -> dict:
return expected return expected
def _in_foreign_tree(db: dict, owners: set[str], sha1: str) -> bool:
"""Whether the collection keeps this sha1 in another emulator's tree."""
return _foreign_tree(db.get("files", {}).get(sha1, {}).get("path", ""), owners)
def _name_in_index( def _name_in_index(
name: str, name: str,
by_name: dict, by_name: dict,
by_path_suffix: dict | None = None, by_path_suffix: dict | None = None,
data_names: set[str] | None = None, data_names: set[str] | None = None,
by_name_lower: dict[str, str] | None = None, by_name_lower: dict[str, str] | None = None,
foreign: Callable[[str], bool] | None = None,
) -> bool: ) -> bool:
"""Check if a name is resolvable in the database indexes or data directories.""" """Check if a name is resolvable in the database indexes or data directories.
*foreign* tells a copy in another emulator's tree from the sha1 of a
name: matched in another casing, such a copy is not the asker's, as in
resolve_local_file's case-insensitive step.
"""
# Strip trailing slash for directory-type entries (e.g. nestopia/samples/foo/) # Strip trailing slash for directory-type entries (e.g. nestopia/samples/foo/)
name = name.rstrip("/") name = name.rstrip("/")
if name in by_name: if name in by_name:
@@ -435,11 +449,11 @@ def _name_in_index(
if basename != name and basename in by_name: if basename != name and basename in by_name:
return True return True
if by_name_lower: if by_name_lower:
key = name.lower() for key in {name.lower(), basename.lower()}:
if key in by_name_lower: if key in by_name_lower and not (
return True foreign and all(foreign(s) for s in by_name[by_name_lower[key]])
if basename != name and basename.lower() in by_name_lower: ):
return True return True
if by_path_suffix and name in by_path_suffix: if by_path_suffix and name in by_path_suffix:
return True return True
if data_names: if data_names:
@@ -667,23 +681,30 @@ def find_undeclared_files(
# Resolution: name, then path basename # Resolution: name, then path basename
# storage: release names where the bytes are served from, not # storage: release names where the bytes are served from, not
# that the collection holds them: resolved like any file. # that the collection holds them: resolved like any file.
# Resolved as the asker, as the builder's ExtraResolver does:
# another casing of the name in another emulator's tree is
# not this emulator's file (dosbox-x's et4000.bin and pcem's
# et4000.BIN).
owned = {**f, "source_profile": emu_name}
foreign = functools.partial(_in_foreign_tree, db, _owner_tokens(owned))
if shipped_on_mismatch and (f.get("md5") or f.get("sha1")): if shipped_on_mismatch and (f.get("md5") or f.get("sha1")):
# The entry states what its content should be and the builder # The entry states what its content should be and the builder
# drops a copy that contradicts it, so content decides here # drops a copy that contradicts it, so content decides here
# too. A name match would not do: generic names collide across # too. A name match would not do: generic names collide across
# systems, and answering yes on one describes a pack that will # systems, and answering yes on one describes a pack that will
# not contain the file. # not contain the file.
_lp, _st = resolve_local_file(f, db, dest_hint=dest) _lp, _st = resolve_local_file(owned, db, dest_hint=dest)
in_repo = _lp is not None and _st != "hash_mismatch" in_repo = _lp is not None and _st != "hash_mismatch"
else: else:
in_repo = _name_in_index( in_repo = _name_in_index(
fname, by_name, by_path_suffix, data_names, by_name_lower, fname, by_name, by_path_suffix, data_names, by_name_lower,
foreign,
) )
if not in_repo and dest != fname: if not in_repo and dest != fname:
path_base = dest.rsplit("/", 1)[-1] path_base = dest.rsplit("/", 1)[-1]
in_repo = _name_in_index( in_repo = _name_in_index(
path_base, by_name, by_path_suffix, data_names, path_base, by_name, by_path_suffix, data_names,
by_name_lower, by_name_lower, foreign,
) )
if in_repo and not any( if in_repo and not any(
f.get(h) for h in ("sha1", "md5", "crc32", "sha256") f.get(h) for h in ("sha1", "md5", "crc32", "sha256")
@@ -693,7 +714,7 @@ def find_undeclared_files(
# rejects them all. NanoBoyAdvance's 16 KB bios.bin met # rejects them all. NanoBoyAdvance's 16 KB bios.bin met
# only PCem's and QEMU's PC BIOS images of that name. # only PCem's and QEMU's PC BIOS images of that name.
# The fallback below then decides as for any mismatch. # The fallback below then decides as for any mismatch.
_lp, _st = resolve_local_file(f, db, dest_hint=dest) _lp, _st = resolve_local_file(owned, db, dest_hint=dest)
if _st == "hash_mismatch": if _st == "hash_mismatch":
in_repo = False in_repo = False
if not in_repo: if not in_repo:
@@ -707,7 +728,7 @@ def find_undeclared_files(
# frontend would reject it, the builder leaves it out, and # frontend would reject it, the builder leaves it out, and
# calling it available here would describe a pack that # calling it available here would describe a pack that
# does not contain it. # does not contain it.
_lp, _st = resolve_local_file(f, db, dest_hint=dest) _lp, _st = resolve_local_file(owned, db, dest_hint=dest)
in_repo = _st != "not_found" and _lp is not None in_repo = _st != "not_found" and _lp is not None
if in_repo and _st == "hash_mismatch" and shipped_on_mismatch: if in_repo and _st == "hash_mismatch" and shipped_on_mismatch:
in_repo = False in_repo = False
@@ -717,7 +738,7 @@ def find_undeclared_files(
# held for it, the rule the builder's lookup applies # held for it, the rule the builder's lookup applies
# (packextras.ExtraResolver). The Apple IIGS' 128 KB # (packextras.ExtraResolver). The Apple IIGS' 128 KB
# ROM is not Basilisk II's ROM. # ROM is not Basilisk II's ROM.
_lp, _st = resolve_local_file(f, db, dest_hint=dest) _lp, _st = resolve_local_file(owned, db, dest_hint=dest)
if core_size_refusal( if core_size_refusal(
f, _lp, same_slot_entries(profile.get("files"), fname, dest) f, _lp, same_slot_entries(profile.get("files"), fname, dest)
): ):
@@ -1575,6 +1596,13 @@ def verify_platform(
"exclusion_notes": exclusions, "exclusion_notes": exclusions,
"region_fallbacks": sorted(region_fallbacks), "region_fallbacks": sorted(region_fallbacks),
**({"left_out": left_out} if no_game_data else {}), **({"left_out": left_out} if no_game_data else {}),
# Every tree of the registry answers a declaration by its tail, so
# one that was never refreshed leaves its files missing here while
# an online pack build refreshes it and packs them.
"data_dir_notices": sorted(
key for key, entry in (data_dir_registry or {}).items()
if not os.path.isdir(entry.get("local_cache", ""))
),
"details": details, "details": details,
"ground_truth_coverage": { "ground_truth_coverage": {
"with_validation": with_validation, "with_validation": with_validation,
@@ -1962,6 +1990,13 @@ def print_platform_result(
# them missing, as an offline build leaves them out. # them missing, as an offline build leaves them out.
shown = ", ".join(fetched[:10]) + (f" (+{len(fetched) - 10})" if len(fetched) > 10 else "") shown = ", ".join(fetched[:10]) + (f" (+{len(fetched) - 10})" if len(fetched) > 10 else "")
print(f" Release assets fetched when the pack is built ({len(fetched)}): {shown}") print(f" Release assets fetched when the pack is built ({len(fetched)}): {shown}")
uncached = result.get("data_dir_notices") or []
if uncached and result.get("status_counts", {}).get(Status.MISSING):
shown = ", ".join(uncached[:10]) + (f" (+{len(uncached) - 10})" if len(uncached) > 10 else "")
print(
f" Data directories not cached ({len(uncached)}), their files count missing "
f"until refresh_data_dirs.py runs (an online pack build refreshes them): {shown}"
)
for line in result.get("left_out") or []: for line in result.get("left_out") or []:
print(f" {line}") print(f" {line}")
fallbacks = result.get("region_fallbacks") or [] fallbacks = result.get("region_fallbacks") or []
@@ -2068,7 +2103,7 @@ def verify_emulator(
it and every member it carries are optional, as the builder leaves them. it and every member it carries are optional, as the builder leaves them.
""" """
from packextras import expand_agnostic_entries from packextras import expand_agnostic_entries
from packresolve import resolve_file from packresolve import pending_release_asset, resolve_file, run_is_offline
from packpaths import ( from packpaths import (
_archive_destination, _archive_destination,
_register_path, _register_path,
@@ -2094,6 +2129,16 @@ def verify_emulator(
validation_index = _build_validation_index(selected_profiles) validation_index = _build_validation_index(selected_profiles)
data_registry = load_data_dir_registry(platforms_dir) data_registry = load_data_dir_registry(platforms_dir)
def pending(entry: dict, local_path: str | None, status: str) -> tuple[str | None, str]:
# Read from disk and the cache, as _resolve_for_platform does: a
# release asset the cache lacks is fetched by an online build
# (generate_emulator_pack resolves with offline=offline).
if status in ("not_found", "hash_mismatch") and not run_is_offline():
asset = pending_release_asset(entry, db)
if asset:
return asset, RELEASE_ASSET_PENDING
return local_path, status
details = [] details = []
file_status: dict[str, str] = {} file_status: dict[str, str] = {}
file_severity: dict[str, str] = {} file_severity: dict[str, str] = {}
@@ -2233,14 +2278,15 @@ def verify_emulator(
archive = file_entry.get("archive") archive = file_entry.get("archive")
if archive and archive not in seen_archives: if archive and archive not in seen_archives:
seen_archives.add(archive) seen_archives.add(archive)
local_path, resolve_status = resolve_file( own_entry = archive_entry(archive, files)
archive_entry(archive, files), local_path, resolve_status = pending(own_entry, *resolve_file(
own_entry,
db, db,
DEFAULT_BIOS_DIR, DEFAULT_BIOS_DIR,
zip_contents, zip_contents,
data_dir_registry=data_registry, data_dir_registry=data_registry,
offline=True, offline=True,
) ))
required = any( required = any(
f.get("archive") == archive and f.get("required", True) f.get("archive") == archive and f.get("required", True)
for f in files for f in files
@@ -2264,6 +2310,8 @@ def verify_emulator(
"required": required, "required": required,
"path": local_path, "path": local_path,
} }
if resolve_status == RELEASE_ASSET_PENDING:
result["release_asset"] = local_path
else: else:
result = { result = {
"name": archive, "name": archive,
@@ -2324,7 +2372,7 @@ def verify_emulator(
dest_hint = file_entry.get("path", "") dest_hint = file_entry.get("path", "")
# The emulator pack's resolution, release assets included. # The emulator pack's resolution, release assets included.
local_path, resolve_status = resolve_file( local_path, resolve_status = pending(file_entry, *resolve_file(
{**with_archive_members(file_entry, files), "source_profile": emu_name}, {**with_archive_members(file_entry, files), "source_profile": emu_name},
db, db,
DEFAULT_BIOS_DIR, DEFAULT_BIOS_DIR,
@@ -2332,7 +2380,7 @@ def verify_emulator(
dest_hint=dest_hint, dest_hint=dest_hint,
data_dir_registry=data_registry, data_dir_registry=data_registry,
offline=True, offline=True,
) ))
if resolve_status in ("not_found", "user_provided", "external"): if resolve_status in ("not_found", "user_provided", "external"):
local_path = None local_path = None
name = file_entry.get("name", "") name = file_entry.get("name", "")
@@ -2342,6 +2390,15 @@ def verify_emulator(
stored = _storage_verdict(file_entry, emulator_pack=True) stored = _storage_verdict(file_entry, emulator_pack=True)
if stored is not None: if stored is not None:
result = stored result = stored
elif resolve_status == RELEASE_ASSET_PENDING:
result = {
"name": name,
"status": Status.OK,
"required": required,
"path": local_path,
"release_asset": local_path,
"reason": "release asset, fetched when the pack is built",
}
elif not local_path: elif not local_path:
result = {"name": name, "status": Status.MISSING, "required": required} result = {"name": name, "status": Status.MISSING, "required": required}
# An entry the profile documents as unsourceable is absent by # An entry the profile documents as unsourceable is absent by
@@ -2592,6 +2649,10 @@ def print_emulator_result(result: dict, verbose: bool = False) -> None:
if unsourceable: if unsourceable:
parts.append(f"{unsourceable} unsourceable") parts.append(f"{unsourceable} unsourceable")
print(f"{label}: {', '.join(parts)} [{mode}]") print(f"{label}: {', '.join(parts)} [{mode}]")
fetched = sorted({d["name"] for d in result["details"] if d.get("release_asset")})
if fetched:
shown = ", ".join(fetched[:10]) + (f" (+{len(fetched) - 10})" if len(fetched) > 10 else "")
print(f" Release assets fetched when the pack is built ({len(fetched)}): {shown}")
seen = set() seen = set()
for d in result["details"]: for d in result["details"]:
@@ -2722,8 +2783,6 @@ def _refuse_mode_flags(args: argparse.Namespace, parser: argparse.ArgumentParser
"--destinations describes the full pack and is incompatible with --system"), "--destinations describes the full pack and is incompatible with --system"),
(args.destinations and not (args.platform or args.all), (args.destinations and not (args.platform or args.all),
"--destinations requires --platform or --all"), "--destinations requires --platform or --all"),
(args.offline and not (args.platform or args.all),
"--offline requires --platform or --all"),
# Game data is categorized against a platform's list and its cores. # Game data is categorized against a platform's list and its cores.
(getattr(args, "no_game_data", False) and not (args.platform or args.all), (getattr(args, "no_game_data", False) and not (args.platform or args.all),
"--no-game-data requires --platform or --all"), "--no-game-data requires --platform or --all"),
+38
View File
@@ -2059,6 +2059,44 @@ class CheckoutCompletenessRegressions(unittest.TestCase):
finally: finally:
os.chdir(cwd) os.chdir(cwd)
def test_restore_refuses_a_copy_replaced_after_the_index(self):
"""The cache is hashed first and copied later: another run that
replaced the cached name in between put its bytes at a path the
database pins to the old ones."""
from scripts import restore_large_files
with tempfile.TemporaryDirectory(dir=TMP_ROOT) as directory:
root = Path(directory)
cache = root / "cache"
cache.mkdir()
payload = b"firmware bytes"
(cache / "asset.bin").write_bytes(payload)
sha1 = hashlib.sha1(payload).hexdigest()
(root / ".gitignore").write_text("bios/Sony/big.pup\n", encoding="utf-8")
(root / "database.json").write_text(
json.dumps({"files": {sha1: {"path": "bios/Sony/big.pup"}}}),
encoding="utf-8",
)
real_index = restore_large_files.index_cache
def index_then_replace(cache_dir: str) -> dict[str, str]:
index = real_index(cache_dir)
(cache / "asset.bin").write_bytes(b"re-uploaded bytes")
return index
cwd = os.getcwd()
os.chdir(root)
try:
with mock.patch.object(restore_large_files, "index_cache", index_then_replace):
restored, unsatisfied = restore_large_files.restore(
str(cache), "database.json", ".gitignore"
)
self.assertEqual((restored, unsatisfied), (0, ["bios/Sony/big.pup"]))
self.assertFalse((root / "bios/Sony/big.pup").exists())
self.assertEqual(os.listdir(root / "bios/Sony"), [])
finally:
os.chdir(cwd)
def test_restore_leaves_tracked_paths_alone(self): def test_restore_leaves_tracked_paths_alone(self):
from scripts.restore_large_files import restore from scripts.restore_large_files import restore
+82
View File
@@ -7,7 +7,12 @@ went red. The flag lived in retrobat.yml and vanished at the next scrape.
from __future__ import annotations from __future__ import annotations
import contextlib
import hashlib
import io
import os
import sys import sys
import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
@@ -29,5 +34,82 @@ class CaseInsensitiveWhereTheFilesystemIs(unittest.TestCase):
self.assertTrue(config.get("case_insensitive_fs")) self.assertTrue(config.get("case_insensitive_fs"))
class ThePackReadmeNamesTheSpellingsItLeavesOut(unittest.TestCase):
"""The EmuDeck pack is folded for Windows and extracted on a Steam Deck,
where the core asking for bios.col finds only BIOS.col. Only the
installer adds that spelling (case_variants), and the pack's README said
nothing about it."""
def setUp(self):
import common
import generate_db
import generate_pack
self.gp = generate_pack
tmp = tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp")
self.addCleanup(tmp.cleanup)
previous = os.getcwd()
os.chdir(tmp.name)
self.addCleanup(os.chdir, previous)
Path("bios/Coleco").mkdir(parents=True)
payload = b"coleco bios"
Path("bios/Coleco/BIOS.col").write_bytes(payload)
sha1 = hashlib.sha1(payload).hexdigest()
files = {sha1: {"path": "bios/Coleco/BIOS.col", "name": "BIOS.col",
"size": len(payload), "sha1": sha1,
"md5": hashlib.md5(payload).hexdigest(),
"sha256": hashlib.sha256(payload).hexdigest(),
"crc32": "00000001"}}
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
Path("platforms").mkdir()
Path("emulators").mkdir()
self.config = {
"platform": "P", "verification_mode": "existence",
"case_insensitive_fs": True, "cores": ["mesence"],
"systems": {"coleco": {"files": [{"name": "BIOS.col"}]}},
}
Path("emulators/mesence.yml").write_text(yaml.dump({
"emulator": "mesence", "type": "libretro", "systems": ["coleco"],
"files": [{"name": "bios.col", "required": True}],
}))
common._platform_config_cache.clear()
common._emulator_profiles_cache.clear()
self.addCleanup(common._platform_config_cache.clear)
self.addCleanup(common._emulator_profiles_cache.clear)
def _readme(self, systems: list[str]) -> str:
import common
Path("platforms/p.yml").write_text(yaml.dump(self.config))
Path("platforms/_registry.yml").write_text(yaml.dump({"platforms": {"p": {
"status": "active",
"install": {"detect": [{"os": os_name} for os_name in systems]},
}}}))
common._platform_config_cache.clear()
texts: dict[str, str] = {}
real = self.gp._write_generated_member
def capture(zf, arcname, text):
texts[arcname] = text
return real(zf, arcname, text)
self.gp._write_generated_member = capture
self.addCleanup(setattr, self.gp, "_write_generated_member", real)
with contextlib.redirect_stdout(io.StringIO()):
self.gp.generate_pack(
"p", "platforms", self.db, "bios", "", emulators_dir="emulators",
offline=True, record=self.gp.PackRecord(),
)
return texts["README.txt"]
def test_a_platform_also_on_linux_names_them(self):
readme = self._readme(["windows", "linux"])
self.assertIn("NAMES THAT DIFFER ONLY BY CASE", readme)
self.assertIn(" bios.col (the pack has BIOS.col)", readme)
def test_a_platform_only_on_windows_does_not(self):
self.assertNotIn("DIFFER ONLY BY CASE", self._readme(["windows"]))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+48
View File
@@ -185,5 +185,53 @@ class RetailDashboardTree(unittest.TestCase):
self.assertEqual(status, "path_exact", path) self.assertEqual(status, "path_exact", path)
class TheGapReportAsksAsTheEmulator(unittest.TestCase):
"""dosbox-x's et4000.bin, no hash: pcem's et4000.BIN was its only
casing. The resolver, asked as dosbox-x, refuses it and the pack and the
installer leave the file out, while the gap report, verify's core files
and the manifest's held flag counted it available."""
_hold = ForeignTreeNames._hold
def setUp(self):
ForeignTreeNames.setUp(self)
self._hold("bios/Other/pcem/roms/et4000.BIN", b"E" * 64)
self.entry = {"name": "et4000.bin", "required": True}
def _source(self, owner: str) -> str | None:
from cross_reference import entry_source
by_name = self.db["indexes"]["by_name"]
index = {
"by_name": by_name, "by_name_lower": {k.lower(): k for k in by_name},
"by_path_suffix": {}, "by_md5": {}, "by_sha256": {}, "by_crc32": {},
"db_files": self.db["files"], "data_names": None,
}
return entry_source({**self.entry, "source_profile": owner}, index)
def test_another_tree_casing_is_not_held(self):
self.assertIsNone(self._source("dosbox-x"))
entry = {**self.entry, "source_profile": "dosbox-x"}
self.assertEqual(resolve_local_file(entry, self.db), (None, "not_found"))
def test_the_tree_owner_holds_it(self):
self.assertEqual(self._source("pcem"), "bios")
def test_core_files_follow_the_resolver(self):
from verify import find_undeclared_files
profiles = {"dosbox-x": {
"emulator": "DOSBox-X", "type": "libretro", "systems": ["dos"],
"files": [self.entry],
}}
config = {"platform": "P", "verification_mode": "md5", "cores": ["dosbox-x"],
"systems": {"dos": {"files": []}}}
(found,) = [
u for u in find_undeclared_files(config, "emulators", self.db, profiles)
if u["name"] == "et4000.bin"
]
self.assertFalse(found["in_repo"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+103
View File
@@ -1148,6 +1148,109 @@ class AStandaloneLayoutBelowTheBiosFolder(unittest.TestCase):
self.assertEqual({f["dest"] for f in files}, {f["dest"] for f in built["files"]}) self.assertEqual({f["dest"] for f in files}, {f["dest"] for f in built["files"]})
class AnExtraBesideItsDeclarationBelowTheBiosFolder(unittest.TestCase):
"""RetroDECK declares bios/scph5501.bin and writes core files below bios/.
Beetle PSX's scph5501.bin was grouped bare, a key whose region lookup
another core leaves untagged, while the declaration's key resolved to
DuckStation's American tag: `--region japan` dropped the declaration and
the pack kept the file through the core's claim, while the installer,
which records the path once, dropped it."""
def setUp(self):
import common
import generate_db
import generate_pack
self.gp = generate_pack
tmp = tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp")
self.addCleanup(tmp.cleanup)
previous = os.getcwd()
os.chdir(tmp.name)
self.addCleanup(os.chdir, previous)
Path("bios/Sys").mkdir(parents=True)
files = {}
for name in ("us.bin", "jp.bin"):
payload = name.encode()
Path(f"bios/Sys/{name}").write_bytes(payload)
sha1 = hashlib.sha1(payload).hexdigest()
files[sha1] = {"path": f"bios/Sys/{name}", "name": name,
"size": len(payload), "sha1": sha1,
"md5": hashlib.md5(payload).hexdigest(),
"sha256": hashlib.sha256(payload).hexdigest(),
"crc32": "00000001"}
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
Path("platforms").mkdir()
Path("platforms/p.yml").write_text(json.dumps({
"platform": "P", "version": "2.1", "verification_mode": "existence",
"base_destination": "", "cores": ["beetle", "dsa", "other"],
"standalone_cores": ["dsa"], "systems": {"sys": {"files": [
{"name": name, "destination": f"bios/{name}", "required": False}
for name in ("us.bin", "jp.bin")
]}},
}))
Path("platforms/_registry.yml").write_text(
json.dumps({"platforms": {"p": {"status": "active"}}})
)
Path("emulators").mkdir()
Path("emulators/dsa.yml").write_text(json.dumps({
"emulator": "dsa", "type": "standalone + libretro", "systems": ["sys"],
"files": [
{"name": name, "standalone_path": f"bios/{name}", "system": "sys",
"region": [region_name], "required": False}
for name, region_name in (("us.bin", "north-america"), ("jp.bin", "japan"))
]}))
# Read before dsa, as beetle_psx is before duckstation, and untagged
# by name through a second reader, as pcsx1 leaves scph5501.bin.
Path("emulators/beetle.yml").write_text(json.dumps({
"emulator": "beetle", "type": "libretro", "systems": ["sys"], "files": [
{"name": "us.bin", "system": "sys", "region": ["north-america"],
"required": False},
]}))
Path("emulators/other.yml").write_text(json.dumps({
"emulator": "other", "type": "libretro", "systems": ["sys"], "files": [
{"name": "us.bin", "path": "alt/us.bin", "system": "sys", "required": False},
]}))
common._platform_config_cache.clear()
common._emulator_profiles_cache.clear()
self.addCleanup(common._platform_config_cache.clear)
self.addCleanup(common._emulator_profiles_cache.clear)
self.profiles = common.load_emulator_profiles("emulators")
def _manifest(self, regions):
return self.gp.generate_manifest(
"p", "platforms", self.db, "bios", "platforms/_registry.yml",
emulators_dir="emulators", emu_profiles=self.profiles,
regions=regions, offline=True,
)
def _pack(self, regions):
record = self.gp.PackRecord()
self.gp.generate_pack(
"p", "platforms", self.db, "bios", "", emulators_dir="emulators",
emu_profiles=self.profiles, regions=regions, offline=True, record=record,
)
return {m for m in record.members if m.startswith("bios/")}
def test_the_slot_choices_are_computed_once(self):
"""pack_overrides recomputed the choices the manifest had just
computed with the same arguments: 8% of a manifest run."""
import slots
real = slots.slot_choices
with mock.patch.object(slots, "slot_choices", side_effect=real) as counted:
self._manifest(None)
self.assertEqual(counted.call_count, 1)
def test_the_pack_and_the_installer_drop_the_same_files(self):
full = self._manifest(None)
files, _omitted = install.narrow(
full["files"], full["omitted_files"], [], [], ["japan"]
)
installed = {f["dest"] for f in files if f["dest"].startswith("bios/")}
self.assertIn("bios/jp.bin", installed)
self.assertEqual(self._pack(["japan"]), installed)
class ACoreWithNothingHeldIsAnOmission(unittest.TestCase): class ACoreWithNothingHeldIsAnOmission(unittest.TestCase):
"""DOSBox serves SC-55/WAVEROM3.BIN, DOSBox Pure wants it and nothing is """DOSBox serves SC-55/WAVEROM3.BIN, DOSBox Pure wants it and nothing is
held for it. A pack built for a target without DOSBox lists it as left held for it. A pack built for a target without DOSBox lists it as left
+55 -9
View File
@@ -158,9 +158,9 @@ class LargeFileCacheTest(unittest.TestCase):
raise AssertionError("must not download when the cache is valid") raise AssertionError("must not download when the cache is valid")
largefiles.urllib.request.urlopen = fail largefiles.urllib.request.urlopen = fail
self.assertEqual( self.assertTrue(os.path.samefile(
common.fetch_large_file("asset.bin", dest_dir=self.dir), str(cached) common.fetch_large_file("asset.bin", dest_dir=self.dir), cached
) ))
def test_offline_cache_miss_never_opens_the_network(self): def test_offline_cache_miss_never_opens_the_network(self):
def fail(req, timeout=None): def fail(req, timeout=None):
@@ -182,15 +182,15 @@ class LargeFileCacheTest(unittest.TestCase):
raise AssertionError("offline cache hit must not open the network") raise AssertionError("offline cache hit must not open the network")
largefiles.urllib.request.urlopen = fail largefiles.urllib.request.urlopen = fail
self.assertEqual( self.assertTrue(os.path.samefile(
common.fetch_large_file( common.fetch_large_file(
"asset.bin", "asset.bin",
dest_dir=self.dir, dest_dir=self.dir,
expected_sha1=hashlib.sha1(PAYLOAD_A).hexdigest(), expected_sha1=hashlib.sha1(PAYLOAD_A).hexdigest(),
offline=True, offline=True,
), ),
str(cached), cached,
) ))
def _serve(self, payload: bytes, gets: list): def _serve(self, payload: bytes, gets: list):
class _Head(io.BytesIO): class _Head(io.BytesIO):
@@ -222,7 +222,7 @@ class LargeFileCacheTest(unittest.TestCase):
"asset.bin", dest_dir=self.dir, "asset.bin", dest_dir=self.dir,
expected_sha1=hashlib.sha1(rebuilt).hexdigest(), expected_sha1=hashlib.sha1(rebuilt).hexdigest(),
) )
self.assertEqual(result, str(cached)) self.assertTrue(os.path.samefile(result, cached))
self.assertEqual(cached.read_bytes(), rebuilt) self.assertEqual(cached.read_bytes(), rebuilt)
self.assertEqual(len(gets), 1) self.assertEqual(len(gets), 1)
@@ -582,7 +582,7 @@ class ReleaseAssetNames(unittest.TestCase):
offline=True, offline=True,
registered=self.REGISTERED, registered=self.REGISTERED,
) )
self.assertEqual(got, os.path.join(cache, names[self.RTCW])) self.assertTrue(os.path.samefile(got, os.path.join(cache, names[self.RTCW])))
# The cache entry of the other asset is a valid file, not stale. # The cache entry of the other asset is a valid file, not stale.
self.assertTrue(Path(cache, names[self.ET]).exists()) self.assertTrue(Path(cache, names[self.ET]).exists())
@@ -762,7 +762,7 @@ class ASameSizeReuploadIsSeen(unittest.TestCase):
"dsi_nand.bin", str(self.dir), hashlib.sha1(new).hexdigest(), "", "dsi_nand.bin", str(self.dir), hashlib.sha1(new).hexdigest(), "",
offline=False, offline=False,
) )
self.assertEqual(found, str(self.cached)) self.assertTrue(os.path.samefile(found, self.cached))
def test_the_release_check_compares_digests_at_equal_size(self): def test_the_release_check_compares_digests_at_equal_size(self):
expected = {"bios/Nintendo/DSi/dsi_nand.bin": 8} expected = {"bios/Nintendo/DSi/dsi_nand.bin": 8}
@@ -790,5 +790,51 @@ class ASameSizeReuploadIsSeen(unittest.TestCase):
) )
class AVerifiedPathKeepsItsBytes(unittest.TestCase):
"""fetch_large_file verified a cached copy and returned its name. Another
run that found the release re-uploaded then replaced that name with the
new revision, and the first run packed bytes it never verified, under a
manifest sha1 that describes the old ones."""
def setUp(self):
_test_registry(self)
self.dir = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, self.dir, True)
(self.dir / "asset.bin").write_bytes(PAYLOAD_A)
self.served = {"asset.bin": hashlib.sha256(PAYLOAD_B).hexdigest()}
def _fetch(self, payload: bytes, offline: bool) -> str | None:
def urlopen(req, timeout=None):
return _SlowResponse(PAYLOAD_B)
with mock.patch.object(largefiles, "_served_digests", lambda: self.served), \
mock.patch.object(largefiles.urllib.request, "urlopen", urlopen):
return largefiles.fetch_large_file(
"asset.bin", dest_dir=str(self.dir),
expected_sha1=hashlib.sha1(payload).hexdigest(), offline=offline,
)
def test_a_revision_promoted_after_the_check_is_not_read(self):
first = self._fetch(PAYLOAD_A, offline=True)
second = self._fetch(PAYLOAD_B, offline=False)
self.assertEqual((self.dir / "asset.bin").read_bytes(), PAYLOAD_B)
self.assertEqual(Path(second).read_bytes(), PAYLOAD_B)
self.assertEqual(Path(first).read_bytes(), PAYLOAD_A)
self.assertEqual(Path(first).name, "asset.bin")
def test_an_orphaned_revision_is_dropped_after_the_grace(self):
first = self._fetch(PAYLOAD_A, offline=True)
self._fetch(PAYLOAD_B, offline=False)
self.assertTrue(Path(first).exists())
with mock.patch.object(largefiles, "ORPHAN_GRACE", -1):
largefiles._prune_verified(str(self.dir))
self.assertFalse(Path(first).exists())
self.assertEqual((self.dir / "asset.bin").read_bytes(), PAYLOAD_B)
self.assertEqual(
[p.name for p in (self.dir / largefiles.VERIFIED_STORE).iterdir()],
[hashlib.sha1(PAYLOAD_B).hexdigest()],
)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+169 -1
View File
@@ -7,6 +7,7 @@ and in no part (4507 of RetroArch's 5430).
from __future__ import annotations from __future__ import annotations
import hashlib
import sys import sys
import tempfile import tempfile
import unittest import unittest
@@ -19,7 +20,7 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts")) sys.path.insert(0, str(REPO_ROOT / "scripts"))
from common import build_zip_contents_index, compute_hashes, load_emulator_profiles # noqa: E402 from common import build_zip_contents_index, compute_hashes, load_emulator_profiles # noqa: E402
from generate_pack import generate_split_packs # noqa: E402 from generate_pack import PackRecord, generate_pack, generate_split_packs # noqa: E402
class SplitPartsCoverTheExtras(unittest.TestCase): class SplitPartsCoverTheExtras(unittest.TestCase):
@@ -70,5 +71,172 @@ class SplitPartsCoverTheExtras(unittest.TestCase):
self.assertIn("core_c.bin", members) self.assertIn("core_c.bin", members)
class SplitPartsHoldWhatTheFullPackHolds(unittest.TestCase):
"""A part sees neither the other systems' declarations nor their extras.
RetroDECK's Other_Cores part wrote bios/SGB1.sfc/program.rom beside the
SNES part's file bios/SGB1.sfc, a file/directory clash the full pack
refuses. RetroBat's ColecoVision part wrote openmsx/... beside the MSX
part's openMSX/..., case twins the full pack folds. A path two systems
declare got each system's file, one per part.
"""
def _build(self, platform: dict, profiles: dict[str, dict], blobs: dict[str, bytes]):
tmp = tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp")
self.addCleanup(tmp.cleanup)
root = Path(tmp.name)
(root / "platforms").mkdir()
(root / "emulators").mkdir()
files = {}
for rel, payload in blobs.items():
path = root / "bios" / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(payload)
files[rel] = (str(path), compute_hashes(str(path)))
by_name: dict[str, list[str]] = {}
for rel, (_p, h) in files.items():
by_name.setdefault(Path(rel).name, []).append(h["sha1"])
db = {
"files": {
h["sha1"]: {"name": Path(rel).name, "md5": h["md5"], "sha1": h["sha1"],
"sha256": h["sha256"], "path": p, "size": 1}
for rel, (p, h) in files.items()
},
"indexes": {
"by_md5": {h["md5"]: h["sha1"] for _p, h in files.values()},
"by_name": by_name, "by_crc32": {}, "by_path_suffix": {},
},
}
(root / "platforms" / "_registry.yml").write_text(
yaml.dump({"platforms": {"plat": {"status": "active"}}})
)
(root / "platforms" / "plat.yml").write_text(yaml.dump(platform))
for name, profile in profiles.items():
(root / "emulators" / f"{name}.yml").write_text(yaml.dump(profile))
args = dict(
emulators_dir=str(root / "emulators"),
zip_contents=build_zip_contents_index(db),
emu_profiles=load_emulator_profiles(str(root / "emulators")),
offline=True,
)
full = PackRecord()
generate_pack(
"plat", str(root / "platforms"), db, str(root / "bios"), "", record=full, **args
)
parts: dict[str, PackRecord] = {}
generate_split_packs(
"plat", str(root / "platforms"), db, str(root / "bios"), str(root / "dist"),
records=parts, **args,
)
self.assertFalse((root / "dist").exists())
return full, parts
def _assert_parts_add_up(self, full: PackRecord, parts: dict[str, PackRecord]) -> None:
notes = {"README.txt"}
union = {m for r in parts.values() for m in r.members} - notes
self.assertEqual(union, set(full.members) - notes)
for record in parts.values():
for member, source in record.sources.items():
self.assertEqual(
compute_hashes(source)["sha1"],
compute_hashes(full.sources[member])["sha1"], member,
)
def test_a_file_directory_conflict_stays_out_of_every_part(self):
full, parts = self._build(
{
"platform": "Plat", "verification_mode": "existence", "cores": ["sgb"],
"systems": {"snes": {"files": [{"name": "SGB1.sfc"}]}},
},
{"sgb": {
"emulator": "SGB", "type": "libretro", "systems": ["sgb-other"],
"files": [{"name": "program.rom", "path": "SGB1.sfc/program.rom"}],
}},
{"SGB1.sfc": b"cartridge", "sgb/program.rom": b"program"},
)
self.assertNotIn("SGB1.sfc/program.rom", full.members)
self._assert_parts_add_up(full, parts)
def test_a_case_twin_is_written_once(self):
full, parts = self._build(
{
"platform": "Plat", "verification_mode": "existence",
"case_insensitive_fs": True, "cores": ["msxcore"],
"systems": {
"msx": {"files": [{"name": "disk.rom", "destination": "openMSX/disk.rom"}]},
"coleco": {"files": []},
},
},
{"msxcore": {
"emulator": "MSX Core", "type": "libretro", "systems": ["msx", "coleco"],
"files": [{"name": "disk.rom", "path": "openmsx/disk.rom"}],
}},
{"MSX/disk.rom": b"disk"},
)
self.assertNotIn("openmsx/disk.rom", full.members)
self._assert_parts_add_up(full, parts)
def test_a_path_two_systems_declare_holds_one_file(self):
full, parts = self._build(
{
"platform": "Plat", "verification_mode": "existence",
"systems": {
"coco": {"files": [{
"name": "disk.rom", "sha1": hashlib.sha1(b"coco disk").hexdigest(),
}]},
"pc88": {"files": [{
"name": "disk.rom", "sha1": hashlib.sha1(b"pc-88 disk").hexdigest(),
}]},
},
},
{},
{"CoCo/disk.rom": b"coco disk", "PC88/disk.rom": b"pc-88 disk"},
)
self._assert_parts_add_up(full, parts)
class PipelineComparesTheSplitParts(unittest.TestCase):
"""Step 5 compared the --region, --target, --system, --required-only and
--no-game-data packs with verify, and nothing compared the --split parts
with the full pack: RetroDECK's SGB1.sfc file/directory clash and
RetroBat's 21 openmsx/openMSX twins passed every check."""
def setUp(self):
import pipeline
self.pipeline = pipeline
def _record(self, sources: dict[str, str]) -> PackRecord:
record = PackRecord()
record.members = [*sources, "README.txt"]
record.sources = dict(sources)
return record
def test_equal_parts_report_nothing(self):
full = self._record({"a.bin": "x", "b.bin": "y"})
parts = {"A": self._record({"a.bin": "x"}), "B": self._record({"b.bin": "y"})}
self.assertEqual(self.pipeline.split_differences(full, parts, str.__eq__), [])
def test_each_difference_is_named(self):
full = self._record({"SGB1.sfc": "x", "kept.bin": "k", "lost.bin": "l"})
parts = {
"Snes": self._record({"SGB1.sfc": "x", "kept.bin": "other"}),
"Other": self._record({"SGB1.sfc/program.rom": "p"}),
}
self.assertEqual(
self.pipeline.split_differences(full, parts, str.__eq__),
[
"in a part, not in the full pack: SGB1.sfc/program.rom",
"in the full pack, in no part: lost.bin",
"other bytes in Snes: kept.bin",
],
)
def test_step_five_runs_the_comparison(self):
source = (REPO_ROOT / "scripts" / "pipeline.py").read_text()
step = source[source.index("# Step 5: Consistency check"):source.index("# Step 6:")]
self.assertIn("check_split_packs(", step)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+12
View File
@@ -73,6 +73,18 @@ class PlatformNamesReachTheProfileSystem(unittest.TestCase):
with self.subTest(alias=alias): with self.subTest(alias=alias):
self.assertEqual(_norm_system_id(alias), _norm_system_id(canonical)) self.assertEqual(_norm_system_id(alias), _norm_system_id(canonical))
def test_each_id_is_normalized_once(self):
"""A manifest run asked 1.7 million times for about a thousand ids,
1.5 seconds of prefix loops: the answer is kept per id."""
_norm_system_id.cache_clear()
for _ in range(3):
_norm_system_id("Nintendo_WiiU")
info = _norm_system_id.cache_info()
self.assertEqual((info.misses, info.hits), (1, 2))
self.assertEqual(
_norm_system_id("Nintendo_WiiU"), _norm_system_id.__wrapped__("Nintendo_WiiU")
)
class AFoldedClaimKeepsItsProof(unittest.TestCase): class AFoldedClaimKeepsItsProof(unittest.TestCase):
"""One spelling per machine folds bsnes-hd's SGB1.sfc onto bsnes's: the """One spelling per machine folds bsnes-hd's SGB1.sfc onto bsnes's: the
+98
View File
@@ -0,0 +1,98 @@
"""A path declared twice, once unpinned and once with a pin the copy misses.
The unpinned declaration ships the file and the pinned one refuses it. The
builder wrote "excluded" over the shipped path, so its summary said "1 unsafe
excluded" for a file inside the archive, while verify counted it untested
and listed no exclusion. Both orders of the two systems are checked: the
refusal read first left "excluded" behind the file the second one ships.
"""
from __future__ import annotations
import contextlib
import hashlib
import io
import os
import sys
import tempfile
import unittest
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import common # noqa: E402
import generate_db # noqa: E402
import generate_pack as builder # noqa: E402
from verify import Status, verify_platform # noqa: E402
PAYLOAD = b"a console firmware"
SHA1 = hashlib.sha1(PAYLOAD).hexdigest()
class TwinDeclarationStatus(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp")
self._cwd = os.getcwd()
os.chdir(self._tmp.name)
for d in ("emulators", "platforms", "bios/Twin"):
Path(d).mkdir(parents=True)
Path("bios/Twin/fw.bin").write_bytes(PAYLOAD)
files = {SHA1: {
"path": "bios/Twin/fw.bin", "name": "fw.bin", "size": len(PAYLOAD),
"sha1": SHA1, "md5": hashlib.md5(PAYLOAD).hexdigest(),
"sha256": hashlib.sha256(PAYLOAD).hexdigest(), "crc32": "00000001",
}}
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
Path("platforms/_registry.yml").write_text(
yaml.dump({"platforms": {"twin": {"status": "active"}}})
)
self.addCleanup(common._platform_config_cache.clear)
def tearDown(self):
os.chdir(self._cwd)
self._tmp.cleanup()
def _config(self, unpinned: str, pinned: str) -> dict:
config = {
"platform": "Twin", "verification_mode": "md5",
"systems": {
unpinned: {"files": [{"name": "fw.bin", "destination": "fw.bin"}]},
pinned: {"files": [{
"name": "fw.bin", "destination": "fw.bin", "md5": "0" * 32,
}]},
},
}
Path("platforms/twin.yml").write_text(yaml.dump(config))
common._platform_config_cache.clear()
return config
def _check(self, config: dict) -> None:
record = builder.PackRecord()
out = io.StringIO()
with contextlib.redirect_stdout(out):
builder.generate_pack(
"twin", "platforms", self.db, "bios", "out",
emulators_dir="emulators", emu_profiles={}, record=record,
offline=True,
)
self.assertIn("fw.bin", record.members)
summary = out.getvalue()
self.assertNotIn("excluded", summary.lower())
self.assertIn("0/1 files OK, 1 untested", summary)
self.assertIn("UNTESTED: fw.bin", summary)
report = verify_platform(config, self.db, "emulators", {})
self.assertEqual(report["status_counts"], {Status.UNTESTED: 1})
self.assertEqual(report["excluded_files"], [])
def test_unpinned_declaration_read_first(self):
self._check(self._config("aaa", "bbb"))
def test_pinned_declaration_read_first(self):
self._check(self._config("zzz", "bbb"))
if __name__ == "__main__":
unittest.main()
+74
View File
@@ -0,0 +1,74 @@
"""verify --platform names the data directories that were never refreshed.
A platform entry can resolve only through a data directory tree (AmberELEC's
Machines/*/config.ini in the blueMSX tree). On a checkout where data/ was
never refreshed, verify counted 200 such entries missing with no word on the
cause, while an online pack build refreshes the trees and packs them.
verify --emulator already named its uncached trees.
"""
from __future__ import annotations
import contextlib
import io
import os
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import generate_db # noqa: E402
from verify import print_platform_result, verify_platform # noqa: E402
class UncachedDataDirectoryNotice(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp")
self._cwd = os.getcwd()
os.chdir(self._tmp.name)
Path("emulators").mkdir()
Path("data/present").mkdir(parents=True)
self.db = {"files": {}, "indexes": generate_db.build_indexes({}, {})}
self.config = {
"platform": "Tree", "verification_mode": "md5",
"systems": {"msx": {"files": [{
"name": "config.ini", "destination": "Machines/MSX/config.ini",
}]}},
}
self.registry = {
"absent": {"local_cache": "data/absent"},
"present": {"local_cache": "data/present"},
}
def tearDown(self):
os.chdir(self._cwd)
self._tmp.cleanup()
def _run(self) -> tuple[dict, str]:
result = verify_platform(
self.config, self.db, "emulators", {}, data_dir_registry=self.registry,
)
out = io.StringIO()
with contextlib.redirect_stdout(out):
print_platform_result(result, ["Tree"])
return result, out.getvalue()
def test_an_uncached_tree_is_named(self):
result, printed = self._run()
self.assertEqual(result["data_dir_notices"], ["absent"])
self.assertIn("Data directories not cached (1)", printed)
self.assertIn("refresh_data_dirs.py", printed)
self.assertIn(": absent", printed)
def test_no_note_when_every_tree_is_cached(self):
Path("data/absent").mkdir()
result, printed = self._run()
self.assertEqual(result["data_dir_notices"], [])
self.assertNotIn("refresh_data_dirs", printed)
if __name__ == "__main__":
unittest.main()
+60 -1
View File
@@ -29,7 +29,7 @@ import common # noqa: E402
import generate_db # noqa: E402 import generate_db # noqa: E402
import generate_pack as builder # noqa: E402 import generate_pack as builder # noqa: E402
import packresolve # noqa: E402 import packresolve # noqa: E402
from verify import Status, verify_platform # noqa: E402 from verify import Status, verify_emulator, verify_platform # noqa: E402
PAYLOAD = b"a firmware image the repository does not commit" PAYLOAD = b"a firmware image the repository does not commit"
SHA1 = hashlib.sha1(PAYLOAD).hexdigest() SHA1 = hashlib.sha1(PAYLOAD).hexdigest()
@@ -176,6 +176,65 @@ class CoreExtraReleaseAssetNotCached(ReleaseAssetNotCached):
test_the_online_build_packs_it = None test_the_online_build_packs_it = None
class EmulatorReleaseAssetNotCached(ReleaseAssetNotCached):
"""verify --emulator against generate_emulator_pack: the emulator pack
resolves with offline=offline and fetched the asset online, while verify
forced offline resolution and called it missing."""
def setUp(self):
super().setUp()
self.profile = {
"emulator": "Big Core", "type": "libretro", "systems": ["sys"],
"files": [{"name": "big.bin", "required": True, "sha1": SHA1}],
}
Path("emulators/bigcore.yml").write_text(yaml.dump(self.profile))
common._emulator_profiles_cache.clear()
def _report(self) -> dict:
common._emulator_profiles_cache.clear()
return verify_emulator(["bigcore"], "emulators", self.db, platforms_dir="platforms")
def _entry(self) -> dict:
return next(d for d in self._report()["details"] if d["name"] == "big.bin")
def test_counted_as_fetched_when_the_pack_is_built(self):
entry = self._entry()
self.assertEqual(entry["status"], Status.OK)
self.assertEqual(entry["release_asset"], REGISTERED[0])
def test_its_path_is_a_pack_destination(self):
self.assertIn("big.bin", self._report()["pack_destinations"])
def test_offline_counts_it_missing(self):
packresolve.set_offline(True)
self.assertEqual(self._entry()["status"], Status.MISSING)
self.assertNotIn("big.bin", self._report()["pack_destinations"])
def test_an_asset_with_other_content_is_not_counted(self):
self.profile["files"][0]["sha1"] = "0" * 40
Path("emulators/bigcore.yml").write_text(yaml.dump(self.profile))
self.assertEqual(self._entry()["status"], Status.MISSING)
def test_an_archive_asset_is_counted_too(self):
self.profile["files"] = [{"name": "inner.rom", "archive": "big.bin"}]
Path("emulators/bigcore.yml").write_text(yaml.dump(self.profile))
self.assertIn("big.bin", self._report()["pack_destinations"])
def test_the_online_build_packs_it(self):
cache = Path("cache/big.bin")
cache.parent.mkdir()
cache.write_bytes(PAYLOAD)
record = builder.PackRecord()
with mock.patch.object(
packresolve, "fetch_large_file", return_value=str(cache)
):
builder.generate_emulator_pack(
["bigcore"], "emulators", self.db, "bios", "out",
offline=False, platforms_dir="platforms", record=record,
)
self.assertEqual(set(record.files), set(self._report()["pack_destinations"]))
class OfflinePipelineVerifiesOffline(unittest.TestCase): class OfflinePipelineVerifiesOffline(unittest.TestCase):
"""The offline pipeline builds packs without release assets: its verify """The offline pipeline builds packs without release assets: its verify
step has to count them the same way, or step 5 compares two answers.""" step has to count them the same way, or step 5 compares two answers."""
+4
View File
@@ -63,6 +63,10 @@ Group the split ZIPs by manufacturer (Sony, Nintendo, Sega, etc.):
python scripts/generate_pack.py --platform retroarch --split --group-by manufacturer python scripts/generate_pack.py --platform retroarch --split --group-by manufacturer
``` ```
The parts together hold exactly the files of the full pack, so every part
can be extracted into the same folder. Core files of a system the platform
does not list go in an `Other_Cores` part.
### System-specific packs ### System-specific packs
Extract only the files for a single system within a platform: Extract only the files for a single system within a platform:
+1 -1
View File
@@ -405,7 +405,7 @@ resolution.
| MAME BIOS ZIPs | `contents` field documents inner structure | | MAME BIOS ZIPs | `contents` field documents inner structure |
| Trees addressed by path (RPG Maker, ScummVM, tzdata, c64-emu and the rest of `NODEDUP_DIRS`) | excluded from dedup to keep files that are equal by design | | Trees addressed by path (RPG Maker, ScummVM, tzdata, c64-emu and the rest of `NODEDUP_DIRS`) | excluded from dedup to keep files that are equal by design |
| `strip_components` in data dirs | flattens cache prefix to match expected path | | `strip_components` in data dirs | flattens cache prefix to match expected path |
| case-insensitive dedup | prevents `font.rom` + `FONT.ROM` conflicts on Windows/macOS | | case-insensitive dedup | prevents `font.rom` + `FONT.ROM` conflicts on Windows/macOS. Where the platform also runs on a filesystem that keeps case (EmuDeck and RetroArch on Linux), the pack README lists each spelling it leaves out, and the installer adds them (`case_variants`) |
| frozen snapshot cores | `.info` may reflect current version while code is pinned to an old one. Only the frozen source at the pinned tag is reliable (e.g. desmume2015, mame2003) | | frozen snapshot cores | `.info` may reflect current version while code is pinned to an old one. Only the frozen source at the pinned tag is reliable (e.g. desmume2015, mame2003) |
### File resolution chain ### File resolution chain
+6 -1
View File
@@ -206,7 +206,12 @@ file's own name. A path whose asset name differs from its file name is
uploaded from a copy carrying the asset name. uploaded from a copy carrying the asset name.
**Local cache.** `generate_pack.py` calls `fetch_large_file()` which downloads **Local cache.** `generate_pack.py` calls `fetch_large_file()` which downloads
from the release and caches in `.cache/large/` for subsequent runs. from the release and caches in `.cache/large/` for subsequent runs. The path it
returns is a hard link under `.cache/large/.by-sha1/<sha1>/`, never the cached
name: another run that finds the asset re-uploaded replaces the name, and the
first run still reads the bytes it verified. A revision no name holds is
dropped a day later, at the next download. `restore_large_files.py` hashes
each copy before it moves it into place.
**Check.** The installer refuses a download whose `Content-Length` differs **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 from the manifest size, and the manifest size is that of the local file. A
+2 -2
View File
@@ -52,8 +52,8 @@ Every module in `tests/`, by the area it guards.
| Area | Fixtures | Modules | | Area | Fixtures | Modules |
|------|----------|---------| |------|----------|---------|
| Core regression suites | synthetic | `test_e2e`, `test_audit_regressions`, `test_tool_messages` | | Core regression suites | synthetic | `test_e2e`, `test_audit_regressions`, `test_tool_messages` |
| File resolution and identity | synthetic + collection | `test_adler32_resolution`, `test_agnostic_scan`, `test_agnostic_scan_mode`, `test_agnostic_scope`, `test_agnostic_substitute`, `test_archive_choice`, `test_archive_recipes`, `test_bios_companions`, `test_bundled_identity`, `test_casefold_cache`, `test_db_aliases`, `test_declared_names`, `test_derivations`, `test_directory_entries`, `test_extra_identity`, `test_foreign_tree`, `test_foreign_tree_names`, `test_frontend_digest`, `test_game_data_homonyms`, `test_hashing`, `test_identity`, `test_maker_folder_names`, `test_md5_pack_context`, `test_name_tiebreak`, `test_nec_filing`, `test_np2_fonts`, `test_owner_resolution`, `test_path_suffix`, `test_resolution_evidence`, `test_sega_filing`, `test_shipped_content`, `test_twin_declarations`, `test_validated_choice`, `test_variant_choice`, `test_variant_identity`, `test_zipped_member` | | File resolution and identity | synthetic + collection | `test_adler32_resolution`, `test_agnostic_scan`, `test_agnostic_scan_mode`, `test_agnostic_scope`, `test_agnostic_substitute`, `test_archive_choice`, `test_archive_recipes`, `test_bios_companions`, `test_bundled_identity`, `test_casefold_cache`, `test_db_aliases`, `test_declared_names`, `test_derivations`, `test_directory_entries`, `test_extra_identity`, `test_foreign_tree`, `test_foreign_tree_names`, `test_frontend_digest`, `test_game_data_homonyms`, `test_hashing`, `test_identity`, `test_maker_folder_names`, `test_md5_pack_context`, `test_name_tiebreak`, `test_nec_filing`, `test_np2_fonts`, `test_owner_resolution`, `test_path_suffix`, `test_resolution_evidence`, `test_sega_filing`, `test_shipped_content`, `test_twin_declaration_status`, `test_twin_declarations`, `test_validated_choice`, `test_variant_choice`, `test_variant_identity`, `test_zipped_member` |
| Verification, cross-reference and profile checks | synthetic + profiles | `test_bios_layout`, `test_cache_independent_counts`, `test_case_insensitive_platforms`, `test_checks_have_values`, `test_core_file_fills_declaration`, `test_core_size_refusal`, `test_cross_reference_sources`, `test_crypto_aes`, `test_crypto_unique_data`, `test_crypto_verify`, `test_entry_categories`, `test_file_consumers`, `test_fileless_audit`, `test_flycast_layout`, `test_gap_settlement`, `test_hidden_extras`, `test_launcher_aliases`, `test_launcher_layer`, `test_native_mode`, `test_no_case_collisions`, `test_path_conflict_status`, `test_path_null_copies`, `test_profile_checks`, `test_profile_names`, `test_profile_paths`, `test_profile_repositories`, `test_romm_hash_rule`, `test_shared_groups`, `test_standalone_links`, `test_standalone_mode`, `test_undeclared_dedup`, `test_unplaced_extras`, `test_validation_scope`, `test_verify_all_dir`, `test_verify_emulator_evidence`, `test_verify_listing`, `test_verify_pack_choice`, `test_verify_release_asset` | | Verification, cross-reference and profile checks | synthetic + profiles | `test_bios_layout`, `test_cache_independent_counts`, `test_case_insensitive_platforms`, `test_checks_have_values`, `test_core_file_fills_declaration`, `test_core_size_refusal`, `test_cross_reference_sources`, `test_crypto_aes`, `test_crypto_unique_data`, `test_crypto_verify`, `test_entry_categories`, `test_file_consumers`, `test_fileless_audit`, `test_flycast_layout`, `test_gap_settlement`, `test_hidden_extras`, `test_launcher_aliases`, `test_launcher_layer`, `test_native_mode`, `test_no_case_collisions`, `test_path_conflict_status`, `test_path_null_copies`, `test_profile_checks`, `test_profile_names`, `test_profile_paths`, `test_profile_repositories`, `test_romm_hash_rule`, `test_shared_groups`, `test_standalone_links`, `test_standalone_mode`, `test_undeclared_dedup`, `test_unplaced_extras`, `test_validation_scope`, `test_verify_all_dir`, `test_verify_data_dir_notice`, `test_verify_emulator_evidence`, `test_verify_listing`, `test_verify_pack_choice`, `test_verify_release_asset` |
| Packs and manifests | synthetic + real packs | `test_artifact_lock`, `test_atomic_artifacts`, `test_batocera_cores`, `test_core_data_trees`, `test_deterministic_zip`, `test_emulator_pack_counts`, `test_finalize_pack_names`, `test_large_file_cache`, `test_libretro_layout`, `test_manifest_entry`, `test_minui_platform`, `test_no_game_data`, `test_pack_conformance_dirs`, `test_pack_core_extras`, `test_pack_counts`, `test_pack_destinations`, `test_pack_failures`, `test_pack_file_names`, `test_pack_grouping`, `test_pack_integrity`, `test_pack_member_identity`, `test_pack_readme`, `test_platform_inheritance`, `test_platform_keys`, `test_release_packs`, `test_required_only_archives`, `test_required_only_twins`, `test_save_root`, `test_split_extras`, `test_split_names`, `test_split_pack`, `test_system_pack_extras`, `test_system_pack_parity`, `test_torrentzip`, `test_which_pack` | | Packs and manifests | synthetic + real packs | `test_artifact_lock`, `test_atomic_artifacts`, `test_batocera_cores`, `test_core_data_trees`, `test_deterministic_zip`, `test_emulator_pack_counts`, `test_finalize_pack_names`, `test_large_file_cache`, `test_libretro_layout`, `test_manifest_entry`, `test_minui_platform`, `test_no_game_data`, `test_pack_conformance_dirs`, `test_pack_core_extras`, `test_pack_counts`, `test_pack_destinations`, `test_pack_failures`, `test_pack_file_names`, `test_pack_grouping`, `test_pack_integrity`, `test_pack_member_identity`, `test_pack_readme`, `test_platform_inheritance`, `test_platform_keys`, `test_release_packs`, `test_required_only_archives`, `test_required_only_twins`, `test_save_root`, `test_split_extras`, `test_split_names`, `test_split_pack`, `test_system_pack_extras`, `test_system_pack_parity`, `test_torrentzip`, `test_which_pack` |
| Slots and regions | synthetic + profiles | `test_region`, `test_region_agnostic_scan`, `test_region_groups`, `test_region_held`, `test_region_platform_index`, `test_region_required_only`, `test_slot`, `test_slots`, `test_slots_shipped_file` | | Slots and regions | synthetic + profiles | `test_region`, `test_region_agnostic_scan`, `test_region_groups`, `test_region_held`, `test_region_platform_index`, `test_region_required_only`, `test_slot`, `test_slots`, `test_slots_shipped_file` |
| Hardware targets and systems | synthetic + target files | `test_amberelec_targets`, `test_extract_targets`, `test_lakka_targets`, `test_libretro_slugs`, `test_system_ids`, `test_system_selection`, `test_target_names`, `test_target_scrapers`, `test_target_systems`, `test_target_verify` | | Hardware targets and systems | synthetic + target files | `test_amberelec_targets`, `test_extract_targets`, `test_lakka_targets`, `test_libretro_slugs`, `test_system_ids`, `test_system_selection`, `test_target_names`, `test_target_scrapers`, `test_target_systems`, `test_target_verify` |
+8 -2
View File
@@ -52,7 +52,7 @@ Pipeline steps:
| 4c | Generate target manifests | `--skip-packs` | | 4c | Generate target manifests | `--skip-packs` |
| 4d | Collected copies only an emulator install provides (`bundled_only.py --strict`, gates the run) | - | | 4d | Collected copies only an emulator install provides (`bundled_only.py --strict`, gates the run) | - |
| 4e | Games index: what the installer reads for the games on a card (`generate_games_index.py`, fails under its coverage floor) | - (reads the committed install lists) | | 4e | Games index: what the installer reads for the games on a card (`generate_games_index.py`, fails under its coverage floor) | - (reads the committed install lists) |
| 5 | Consistency check: counts, paths per archive, every emulator and system pack, the narrowed platform packs | `--skip-packs`, or no full pack built (`--source platform` or `truth`) | | 5 | Consistency check: counts, paths per archive, every emulator and system pack, the narrowed platform packs, the `--split` parts | `--skip-packs`, or no full pack built (`--source platform` or `truth`) |
| 6 | Pack integrity (extract + hash) | `--skip-packs`, or no full pack built | | 6 | Pack integrity (extract + hash) | `--skip-packs`, or no full pack built |
| 7 | Generate README and CONTRIBUTING, then the installer page's tables and run samples (`generate_installer_doc.py`) | `--skip-docs` | | 7 | Generate README and CONTRIBUTING, then the installer page's tables and run samples (`generate_installer_doc.py`) | `--skip-docs` |
| 8 | Generate site | `--skip-docs` | | 8 | Generate site | `--skip-docs` |
@@ -110,6 +110,12 @@ alone, run `python scripts/check_buildbot_system.py`.
`--source` and `--one-per-slot` are not compared: verify has no report of `--source` and `--one-per-slot` are not compared: verify has no report of
a platform-only, truth-only or one-per-slot pack. A test fails when a new a platform-only, truth-only or one-per-slot pack. A test fails when a new
narrowing reaches the pack name without being compared or exempted. narrowing reaches the pack name without being compared or exempted.
- **The `--split` parts add up to the full pack.** Step 5 builds the parts of
every platform in memory and compares their union with the full pack, path
for path and byte for byte: a part is built against the full pack's own
placement, so a path the full pack settles (a file/directory clash, a case
twin, a path two systems declare) is not written again in a part. The 17
platform packs took about four and a half minutes on 2026-11-06.
- **Generators write through `write_if_changed()`.** A new timestamp alone - **Generators write through `write_if_changed()`.** A new timestamp alone
rewrites nothing, so a diff on a generated file always means content. rewrites nothing, so a diff on a generated file always means content.
- **One run per output directory.** Pack generation takes a lock on - **One run per output directory.** Pack generation takes a lock on
@@ -154,7 +160,7 @@ python scripts/verify.py --platform recalbox --region us # regional priority l
python scripts/verify.py --platform emuelec --no-game-data # the firmware-only pack, game data named python scripts/verify.py --platform emuelec --no-game-data # the firmware-only pack, game data named
python scripts/verify.py --emulator mame --required-only # the pack built with --required-only python scripts/verify.py --emulator mame --required-only # the pack built with --required-only
python scripts/verify.py --all --destinations tmp/paths.json # also write the paths each pack holds python scripts/verify.py --all --destinations tmp/paths.json # also write the paths each pack holds
python scripts/verify.py --all --offline # a release asset the cache lacks counts missing, as in an offline build python scripts/verify.py --all --offline # a release asset the cache lacks counts missing, as in an offline build (any mode, --emulator included)
python scripts/verify.py --list-platforms # list all platforms python scripts/verify.py --list-platforms # list all platforms
python scripts/verify.py --list-emulators # list all emulators python scripts/verify.py --list-emulators # list all emulators
python scripts/verify.py --list-systems # list all systems python scripts/verify.py --list-systems # list all systems