mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-06 10:03:49 -06:00
chore: merge pack and verify fixes
This commit is contained in:
commit
d0c65dca22
37 files changed
+3267
-142
No files matched your search
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -1841,8 +1841,7 @@
|
|||||||
"reason": "not_found",
|
"reason": "not_found",
|
||||||
"cores": [
|
"cores": [
|
||||||
"dosbox-x"
|
"dosbox-x"
|
||||||
],
|
]
|
||||||
"held": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dest": "extras.wad",
|
"dest": "extras.wad",
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
{"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 @@
|
|||||||
{"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 @@
|
|||||||
{"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 @@
|
|||||||
{"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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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", "")
|
||||||
|
|||||||
@@ -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
@@ -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'} ---"
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user