mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-06 10:03:49 -06:00
chore: merge site insights
This commit is contained in:
commit
c1b0149a81
146 files changed
+10427
-797
No files matched your search
@@ -49,6 +49,7 @@ on:
|
||||
- "scripts/sitediagrams.py"
|
||||
- "scripts/siteentries.py"
|
||||
- "scripts/siteguides.py"
|
||||
- "scripts/siteinputs.py"
|
||||
- "scripts/sitemarkup.py"
|
||||
- "scripts/sitemeasurements.py"
|
||||
- "scripts/siteexports.py"
|
||||
|
||||
@@ -22,6 +22,10 @@ the most useful contribution.
|
||||
hash differs from the one already held under its name goes to that folder's
|
||||
`.variants/`.
|
||||
|
||||
A ZIP that no list declares as a whole is read file by file, and the report
|
||||
says which of them the collection already holds: upload the files
|
||||
themselves, unzipped.
|
||||
|
||||
From a clone, the file goes to `bios/Manufacturer/Console/filename`, and
|
||||
`python scripts/validate_pr.py path/to/file` names that folder for a file still
|
||||
outside `bios/`, and `--place` copies it there under its declared name. Given a
|
||||
|
||||
@@ -43,7 +43,7 @@ First time? Follow the [first install, step by step](https://abdess.github.io/re
|
||||
|
||||
## Download BIOS packs
|
||||
|
||||
A pack holds everything its platform runs: its own BIOS list plus every file its emulator cores load. Pick your platform, download the ZIP, extract to the BIOS path. Each Download link is a page that keeps its address from one release to the next. The installer above does the same file by file: its screen installs everything, or fewer under **Choose systems**, and `--system`, `--core`, `--region` and `--target` make the same choice without questions. For a bare minimum, build your own pack below.
|
||||
A pack holds its platform's own BIOS list plus the files its emulator cores load. What no pack carries yet is in the [gap analysis](https://abdess.github.io/retrobios/gaps/). Pick your platform, download the ZIP, extract to the BIOS path. Each Download link is a page that keeps its address from one release to the next. The installer above does the same file by file: its screen installs everything, or fewer under **Choose systems**, and `--system`, `--core`, `--region` and `--target` make the same choice without questions. For a bare minimum, build your own pack below.
|
||||
|
||||
A pack over 2 GB comes in several parts (`Pack.part1of2.zip`, `Pack.part2of2.zip`), and every part is needed. Each part is an ordinary ZIP: extract them all into the same folder.
|
||||
|
||||
@@ -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.
|
||||
|
||||
- **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
|
||||
- **623 emulators** profiled from source (RetroArch cores + standalone)
|
||||
- **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 |
|
||||
|----------|-----------------:|-------------------------:|------------|
|
||||
| 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 |
|
||||
| EmuDeck | 168/168 | 8,427/8,529 | 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 |
|
||||
| MiSTer FPGA | 81/81 | - | MD5 |
|
||||
| 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 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-06T09:30:57Z*
|
||||
+1
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"generated_at": "2026-11-05T23:59:37Z",
|
||||
"generated_at": "2026-11-06T07:21:27Z",
|
||||
"total_files": 19947,
|
||||
"total_size": 16926035621,
|
||||
"bios_dir": "bios",
|
||||
@@ -281813,9 +281813,6 @@
|
||||
"TRIO64 (Ver. 1.5-07) [VGA] (S3 Incorporated).bin": [
|
||||
"a308b15f93da34961c0141ec95b98c211557e2d5"
|
||||
],
|
||||
"ROM2_ST.BIN": [
|
||||
"5d5ab4313a2d0effe7572bb129193b64cab002c1"
|
||||
],
|
||||
"mt32_ctrl_1_04.rom": [
|
||||
"5a5cb5a77d7d55ee69657c2f870416daed52dea7"
|
||||
],
|
||||
|
||||
@@ -54,7 +54,7 @@ notes: |
|
||||
upper-case names the DOS release ships and the lookup normalises to.
|
||||
|
||||
The sound/music path uses the game's XMI tracks when a SoundFont is
|
||||
available (App:findSoundFont at :1591 probes GeneralUser-GS.sf2,
|
||||
available (App:findSoundFont at CorsixTH/Lua/app.lua:1591 probes GeneralUser-GS.sf2,
|
||||
FluidR3_GM.sf2, FluidR3.sf3 and three Linux system paths), and the
|
||||
SoundFont is a user preference, not game data.
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ notes: |
|
||||
the mods/hf file list under the BUILD_MOD_MPQS option
|
||||
(CMake/Mods.cmake:11-36, 61-71). The compiled
|
||||
binary expects devilutionx.mpq next to the executable on every
|
||||
platform except Android, Apple and 3DS/Switch (:511-518), where its
|
||||
platform except Android, Apple and 3DS/Switch (Source/engine/assets.cpp:511-518), where its
|
||||
contents are shipped loose. After loading the core archives the engine
|
||||
reads the asset ASSETS_VERSION and aborts with "Failed to load UI
|
||||
resources" unless its content is "1\n" (Source/init.cpp:54, 79-85,
|
||||
|
||||
@@ -67,6 +67,7 @@ files:
|
||||
size: 8192
|
||||
crc32: "3aa93ef3"
|
||||
validation: [size, crc32]
|
||||
on_mismatch: {crc32: warn}
|
||||
source_ref: "platforms/libretro/libretro.cpp:779-840 (find_firmware), platforms/libretro/libretro.cpp:858-885 (load_colecovision_firmware), platforms/libretro/libretro.cpp:2453-2457, platforms/libretro/libretro.cpp:2258-2259, src/Adam.cpp:28-31, src/Adam.h:52, src/Memory.cpp:310-319, src/Memory.cpp:246-249"
|
||||
note: >
|
||||
Loaded for both machines. Any 8192-byte image is accepted, and a CRC32
|
||||
@@ -80,6 +81,7 @@ files:
|
||||
size: 8192
|
||||
crc32: "05a37a34"
|
||||
validation: [size, crc32]
|
||||
on_mismatch: {crc32: warn}
|
||||
source_ref: "platforms/libretro/libretro.cpp:887-948 (load_adam_firmware), platforms/libretro/libretro.cpp:2426-2427, platforms/libretro/libretro.cpp:2483-2487, platforms/libretro/libretro.cpp:2500-2501, platforms/libretro/libretro.cpp:2584-2588, src/Adam.cpp:32-35, src/Adam.h:53, src/Adam.cpp:137-146, src/Adam.cpp:166-188, src/GearcolecoCore.cpp:414-419, src/Adam.cpp:519-522"
|
||||
note: >
|
||||
The 8 KiB EOS bank, the first half of the 16 KiB u22 chip. A CRC32
|
||||
@@ -94,6 +96,7 @@ files:
|
||||
size: 32768
|
||||
crc32: "58d86a2a"
|
||||
validation: [size, crc32]
|
||||
on_mismatch: {crc32: warn}
|
||||
source_ref: "platforms/libretro/libretro.cpp:887-948 (load_adam_firmware), platforms/libretro/libretro.cpp:2426-2427, platforms/libretro/libretro.cpp:2483-2487, platforms/libretro/libretro.cpp:2500-2501, platforms/libretro/libretro.cpp:2584-2588, src/Adam.cpp:36-39, src/Adam.h:54, src/Adam.cpp:137-146, src/Adam.cpp:166-188, src/GearcolecoCore.cpp:414-419, src/Adam.cpp:513-522"
|
||||
note: >
|
||||
A CRC32 other than 58d86a2a logs "Unknown SmartWriter revision" and
|
||||
|
||||
@@ -110,7 +110,7 @@ notes: |
|
||||
Q3E/src/main/jni/quake2/yquake2/src/common/filesystem.c:1801-1850,
|
||||
Q3E/src/main/jni/zdoom/src/d_iwad.cpp:738,
|
||||
Q3E/src/main/jni/ecwolf/src/wl_iwad.cpp:766,
|
||||
idTech4Amm/src/main/java/com/karin/idTech4Amm/sys/GameResourceUrl.java:14-118
|
||||
idTech4Amm/src/main/java/com/karin/idTech4Amm/sys/GameResourceUrl.java:15-121
|
||||
|
||||
The Quake III lineage keeps id's release checksums for `baseq3` pak0 through
|
||||
pak8 and `missionpack` pak0 through pak3, and an archive whose value disagrees
|
||||
@@ -130,7 +130,7 @@ notes: |
|
||||
two games whose data is a file rather than a folder.
|
||||
ref: Q3E/src/main/jni/zdoom/src/d_iwad.cpp:287-297, 321-342,
|
||||
Q3E/src/main/jni/zdoom/src/version.h:100-102,
|
||||
idTech4Amm/src/main/java/com/karin/idTech4Amm/sys/Game.java:69-72,
|
||||
idTech4Amm/src/main/java/com/karin/idTech4Amm/sys/Game.java:72-75,
|
||||
Q3E/src/main/java/com/n0n3m4/q3e/Q3EInterface.java:710-713
|
||||
|
||||
Starting a game extracts that game's bundled resources first. A text file named
|
||||
|
||||
@@ -5,6 +5,8 @@ source: "https://github.com/simple64/simple64"
|
||||
upstream: "https://github.com/simple64/simple64"
|
||||
profiled_date: "2026-08-08"
|
||||
source_commit: "d8c969c7b932e3d76e6a25549d76348839dbaefd"
|
||||
upstream_gone: "simple64/simple64 answers 404 on the API and the web, and the simple64 organisation lists no repository. The README at the pin archives the project in favour of gopher64. Observed 2026-11-06. simple64-vault/simple64 keeps main at the analysed commit."
|
||||
source_mirror: "https://github.com/simple64-vault/simple64"
|
||||
core_version: "v2024.12.1-5-gd8c969c"
|
||||
version_basis:
|
||||
kind: tag
|
||||
|
||||
@@ -6,6 +6,8 @@ upstream: closed-source
|
||||
author: "TeknoGods"
|
||||
profiled_date: "2026-10-20"
|
||||
source_commit: "b75872addc1d5cce81a62f4c1468eab59e2d3cbe"
|
||||
upstream_gone: "teknogods/TeknoParrotUI answers 404 on the API and the web, and the teknogods organisation no longer lists it. Observed 2026-11-06. thecrixux/TeknoParrotUI keeps master at the analysed commit."
|
||||
source_mirror: "https://github.com/thecrixux/TeknoParrotUI"
|
||||
core_version: "TeknoParrotUI 7ffcdf1"
|
||||
version_basis:
|
||||
kind: unverified
|
||||
|
||||
+94
-4
@@ -6,14 +6,102 @@
|
||||
"files": [
|
||||
"batocera-systems"
|
||||
],
|
||||
"summary": "590 kept, 39 files added, 47 hashes added to an existing entry",
|
||||
"summary": "590 kept, 47 files added, 47 hashes added to an existing entry",
|
||||
"counts": {
|
||||
"kept": 590,
|
||||
"added": 39,
|
||||
"not_added": 1117,
|
||||
"added": 47,
|
||||
"not_added": 1712,
|
||||
"not_kept": 0
|
||||
},
|
||||
"changes": [
|
||||
{
|
||||
"kind": "added",
|
||||
"system": "amiga",
|
||||
"name": "kick33180.A500.RTB",
|
||||
"destination": "whdboot/save-data/Kickstarts/kick33180.A500.RTB",
|
||||
"field": null,
|
||||
"old": null,
|
||||
"new": {
|
||||
"md5": "c993feb1eeddec1d2e687aa514231028"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "added",
|
||||
"system": "amiga",
|
||||
"name": "kick34005.A500.RTB",
|
||||
"destination": "whdboot/save-data/Kickstarts/kick34005.A500.RTB",
|
||||
"field": null,
|
||||
"old": null,
|
||||
"new": {
|
||||
"md5": "7eb13d974af0992d672b67c317a7e0a1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "added",
|
||||
"system": "amiga",
|
||||
"name": "kick37175.A500.RTB",
|
||||
"destination": "whdboot/save-data/Kickstarts/kick37175.A500.RTB",
|
||||
"field": null,
|
||||
"old": null,
|
||||
"new": {
|
||||
"md5": "e057054449b5e21591ce27d2eb9ac720"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "added",
|
||||
"system": "amiga",
|
||||
"name": "kick36143.A3000.RTB",
|
||||
"destination": "whdboot/save-data/Kickstarts/kick36143.A3000.RTB",
|
||||
"field": null,
|
||||
"old": null,
|
||||
"new": {
|
||||
"md5": "2a2652daba6be3ef1d0bdf5ddd4347c6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "added",
|
||||
"system": "amiga",
|
||||
"name": "kick39106.A1200.RTB",
|
||||
"destination": "whdboot/save-data/Kickstarts/kick39106.A1200.RTB",
|
||||
"field": null,
|
||||
"old": null,
|
||||
"new": {
|
||||
"md5": "af8b2e2227f01a142e20c8ab463f14ed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "added",
|
||||
"system": "amiga",
|
||||
"name": "kick40063.A600.RTB",
|
||||
"destination": "whdboot/save-data/Kickstarts/kick40063.A600.RTB",
|
||||
"field": null,
|
||||
"old": null,
|
||||
"new": {
|
||||
"md5": "3f97c1963d82c808ef29e7c984c4c688"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "added",
|
||||
"system": "amiga",
|
||||
"name": "kick40068.A1200.RTB",
|
||||
"destination": "whdboot/save-data/Kickstarts/kick40068.A1200.RTB",
|
||||
"field": null,
|
||||
"old": null,
|
||||
"new": {
|
||||
"md5": "547b42b7b070c7f227ea45e0986d57a1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "added",
|
||||
"system": "amiga",
|
||||
"name": "kick40068.A4000.RTB",
|
||||
"destination": "whdboot/save-data/Kickstarts/kick40068.A4000.RTB",
|
||||
"field": null,
|
||||
"old": null,
|
||||
"new": {
|
||||
"md5": "f89f19b858ea6b1a04327c6f71ee1600"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "added",
|
||||
"system": "amiga",
|
||||
@@ -922,7 +1010,8 @@
|
||||
"hashes_cleared": [],
|
||||
"requirements_corrected": 0,
|
||||
"withheld": [],
|
||||
"found_by_content": 107,
|
||||
"found_by_content": 312,
|
||||
"unattributed": [],
|
||||
"removal_candidates": [
|
||||
{
|
||||
"system": "colecovision",
|
||||
@@ -4421,5 +4510,6 @@
|
||||
"unresolved_cores": []
|
||||
}
|
||||
],
|
||||
"doubled": [],
|
||||
"invalid": []
|
||||
}
|
||||
@@ -51,7 +51,25 @@ systems = {
|
||||
{ "md5": "e40a5dfb3d017ba8779faba30cbd1c8e", "file": "bios/kick40063.A600" },
|
||||
{ "md5": "646773759326fbac3b2311fd8c8793ee", "file": "bios/kick40068.A1200" },
|
||||
{ "md5": "9bdedde6a4f33555b4a270c8ca53297d", "file": "bios/kick40068.A4000" },
|
||||
{ "md5": "bb72565701b1b6faece07d68ea5da639", "file": "bios/CD32 Extended-ROM r40.60 (1993)(Commodore)(CD32).rom" } ] },
|
||||
{ "md5": "bb72565701b1b6faece07d68ea5da639", "file": "bios/CD32 Extended-ROM r40.60 (1993)(Commodore)(CD32).rom" },
|
||||
{ "md5": "c993feb1eeddec1d2e687aa514231028", "file": "whdboot/save-data/Kickstarts/kick33180.A500.RTB" },
|
||||
{ "md5": "7eb13d974af0992d672b67c317a7e0a1", "file": "whdboot/save-data/Kickstarts/kick34005.A500.RTB" },
|
||||
{ "md5": "e057054449b5e21591ce27d2eb9ac720", "file": "whdboot/save-data/Kickstarts/kick37175.A500.RTB" },
|
||||
{ "md5": "2a2652daba6be3ef1d0bdf5ddd4347c6", "file": "whdboot/save-data/Kickstarts/kick36143.A3000.RTB" },
|
||||
{ "md5": "af8b2e2227f01a142e20c8ab463f14ed", "file": "whdboot/save-data/Kickstarts/kick39106.A1200.RTB" },
|
||||
{ "md5": "3f97c1963d82c808ef29e7c984c4c688", "file": "whdboot/save-data/Kickstarts/kick40063.A600.RTB" },
|
||||
{ "md5": "547b42b7b070c7f227ea45e0986d57a1", "file": "whdboot/save-data/Kickstarts/kick40068.A1200.RTB" },
|
||||
{ "md5": "f89f19b858ea6b1a04327c6f71ee1600", "file": "whdboot/save-data/Kickstarts/kick40068.A4000.RTB" },
|
||||
{ "md5": "6c5b44c1a2bcee664129f850bfe4ed8a", "file": "uae_data/drive_click_A500.wav" },
|
||||
{ "md5": "7401f854a75f9ede18769168ab896a2a", "file": "uae_data/drive_spin_A500.wav" },
|
||||
{ "md5": "f9d954879fee8e96baa93d6139077cdb", "file": "uae_data/drive_spinnd_A500.wav" },
|
||||
{ "md5": "aebeacc7e7bc52ef890785c835bec3d0", "file": "uae_data/drive_startup_A500.wav" },
|
||||
{ "md5": "1a4b2d6254918ff769f26beeede75655", "file": "uae_data/drive_snatch_A500.wav" },
|
||||
{ "md5": "b64bee23f356635f67fb08b543df9797", "file": "uae_data/drive_click_LOUD.wav" },
|
||||
{ "md5": "56d8e36a2f04b5e8c5a1f1530cfc886c", "file": "uae_data/drive_spin_LOUD.wav" },
|
||||
{ "md5": "43ab05c6d969cb40805970a5b2352875", "file": "uae_data/drive_spinnd_LOUD.wav" },
|
||||
{ "md5": "743b4b9f0ca645c4c2a34953b5e01e45", "file": "uae_data/drive_startup_LOUD.wav" },
|
||||
{ "md5": "40cdf89f35b68cfefd4df5fb0256e02f", "file": "uae_data/drive_snatch_LOUD.wav" } ] },
|
||||
|
||||
# ---------- Fujistu FM-Towns ---------- #
|
||||
# https://github.com/captainys/TOWNSEMU
|
||||
@@ -492,7 +510,9 @@ systems = {
|
||||
# SNK
|
||||
"neogeo": { "name": "NeoGeo", "biosFiles": [ { "md5": "", "file": "neogeo/neogeo.zip" } ] },
|
||||
"neogeocd": { "name": "NeoGeo CD", "biosFiles": [ { "md5": "", "file": "neogeo/neogeo.zip" },
|
||||
{ "md5": "f39572af7584cb5b3f70ae8cc848aba2", "file": "bios/neocd/neocd_z.rom"} ] },
|
||||
{ "md5": "f39572af7584cb5b3f70ae8cc848aba2", "file": "bios/neocd/neocd_z.rom"},
|
||||
{ "md5": "b31a246b69493c35194fe0bac120dc37", "file": "neocdz.zip"},
|
||||
{ "md5": "006a881532f2caffc3fe9fb5593f2ed5", "file": "neocd.zip"} ] },
|
||||
|
||||
|
||||
# Sony Computer Entertainment
|
||||
@@ -656,7 +676,7 @@ systems = {
|
||||
{ "md5": "", "file": "ports/dinothawr/xbr-lv2-a.glslp" },
|
||||
{ "md5": "", "file": "ports/dinothawr/xbr-lv2-pass1.glsl" } ] },
|
||||
|
||||
"DOOM1": { "name": "DOOM1 (PrBoom)", "biosFiles": [ { "md5": "", "file": "ports/doom/doom1.wad" } ] },
|
||||
"DOOM1": { "name": "DOOM1 (PrBoom)", "biosFiles": [ { "md5": "", "file": "ports/doom/doom1.wad" }, { "md5": "5772f907fdee4284f6332d89c0c5609c", "file": "gzdoom.pk3" }, { "md5": "094baa608b85fbe636933b4098e7e1f1", "file": "game_widescreen_gfx.pk3" } ] },
|
||||
|
||||
"DOOM2": { "name": "DOOM2 (PrBoom)", "biosFiles": [ { "md5": "", "file": "ports/doom2/doom2.wad" } ] },
|
||||
|
||||
@@ -749,59 +769,60 @@ systems = {
|
||||
{ "md5": "", "file": "ports/opentyrian/newshi.shp" },
|
||||
{ "md5": "", "file": "ports/opentyrian/shapes).dat" } ] },
|
||||
|
||||
"Cannonball": { "name": "Cannonball", "biosFiles": [ { "md5": "", "file": "ports/cannonball/epr-10187.88" },
|
||||
{ "md5": "", "file": "ports/cannonball/epr-10327.76" },
|
||||
{ "md5": "", "file": "ports/cannonball/epr-10327a.76" },
|
||||
{ "md5": "", "file": "ports/cannonball/epr-10328.75" },
|
||||
{ "md5": "", "file": "ports/cannonball/epr-10328a.75" },
|
||||
{ "md5": "", "file": "ports/cannonball/epr-10329.58" },
|
||||
{ "md5": "", "file": "ports/cannonball/epr-10329a.58" },
|
||||
{ "md5": "", "file": "ports/cannonball/epr-10330.57" },
|
||||
{ "md5": "", "file": "ports/cannonball/epr-10330a.57" },
|
||||
{ "md5": "", "file": "ports/cannonball/epr-10380.133" },
|
||||
{ "md5": "", "file": "ports/cannonball/epr-10380b.133" },
|
||||
{ "md5": "", "file": "ports/cannonball/epr-10381.132" },
|
||||
{ "md5": "", "file": "ports/cannonball/epr-10381b.132" },
|
||||
{ "md5": "", "file": "ports/cannonball/epr-10382.118" },
|
||||
{ "md5": "", "file": "ports/cannonball/epr-10382b.118" },
|
||||
{ "md5": "", "file": "ports/cannonball/epr-10383.117" },
|
||||
{ "md5": "", "file": "ports/cannonball/epr-10383b.117" },
|
||||
{ "md5": "", "file": "ports/cannonball/mpr-10371.9" },
|
||||
{ "md5": "", "file": "ports/cannonball/mpr-10372.13" },
|
||||
{ "md5": "", "file": "ports/cannonball/mpr-10373.10" },
|
||||
{ "md5": "", "file": "ports/cannonball/mpr-10374.14" },
|
||||
{ "md5": "", "file": "ports/cannonball/mpr-10375.11" },
|
||||
{ "md5": "", "file": "ports/cannonball/mpr-10376.15" },
|
||||
{ "md5": "", "file": "ports/cannonball/mpr-10377.12" },
|
||||
{ "md5": "", "file": "ports/cannonball/mpr-10378.16" },
|
||||
{ "md5": "", "file": "ports/cannonball/opr-10185.11" },
|
||||
{ "md5": "", "file": "ports/cannonball/opr-10186.47" },
|
||||
{ "md5": "", "file": "ports/cannonball/opr-10188.71" },
|
||||
{ "md5": "", "file": "ports/cannonball/opr-10189.70" },
|
||||
{ "md5": "", "file": "ports/cannonball/opr-10190.69" },
|
||||
{ "md5": "", "file": "ports/cannonball/opr-10191.68" },
|
||||
{ "md5": "", "file": "ports/cannonball/opr-10192.67" },
|
||||
{ "md5": "", "file": "ports/cannonball/opr-10193.66" },
|
||||
{ "md5": "", "file": "ports/cannonball/opr-10230.104" },
|
||||
{ "md5": "", "file": "ports/cannonball/opr-10231.103" },
|
||||
{ "md5": "", "file": "ports/cannonball/opr-10232.102" },
|
||||
{ "md5": "", "file": "ports/cannonball/opr-10266.101" },
|
||||
{ "md5": "", "file": "ports/cannonball/opr-10267.100" },
|
||||
{ "md5": "", "file": "ports/cannonball/opr-10268.99" } ] },
|
||||
"Cannonball": { "name": "Cannonball", "biosFiles": [ { "md5": "0cc087bbf1d5ad46319756b5b17ab9be", "file": "ports/cannonball/epr-10187.88" },
|
||||
{ "md5": "eddae6f176ad5a3438e4dae1b8c4d4fb", "file": "ports/cannonball/epr-10327.76" },
|
||||
{ "md5": "35bdcb3d8eaae6505af896a55f26dda1", "file": "ports/cannonball/epr-10327a.76" },
|
||||
{ "md5": "73486b2da791109e322c71cbaf51338b", "file": "ports/cannonball/epr-10328.75" },
|
||||
{ "md5": "5c82ed7dd7450f5c037b4dcaa0103218", "file": "ports/cannonball/epr-10328a.75" },
|
||||
{ "md5": "7197dfbdf83029abd56e72b98b274c63", "file": "ports/cannonball/epr-10329.58" },
|
||||
{ "md5": "f8729ace245eb8fc4854819f783f6042", "file": "ports/cannonball/epr-10329a.58" },
|
||||
{ "md5": "9556c737b5506241a5a58635e58bdbad", "file": "ports/cannonball/epr-10330.57" },
|
||||
{ "md5": "f0f9e2cce2d64dbb28650b11679f745b", "file": "ports/cannonball/epr-10330a.57" },
|
||||
{ "md5": "591daabf7c4b9005171f9af83068f3a1", "file": "ports/cannonball/epr-10380.133" },
|
||||
{ "md5": "94bb116334e7076aeeee5fcdcb536f87", "file": "ports/cannonball/epr-10380b.133" },
|
||||
{ "md5": "ab1a9fe4feb5d5bd867552e3740bdc07", "file": "ports/cannonball/epr-10381.132" },
|
||||
{ "md5": "ab1a9fe4feb5d5bd867552e3740bdc07", "file": "ports/cannonball/epr-10381b.132" },
|
||||
{ "md5": "bcf3cae054791aa0c2c9d3c144bb1682", "file": "ports/cannonball/epr-10382.118" },
|
||||
{ "md5": "8ff3212b3a2e37a37ba73a998bbc424f", "file": "ports/cannonball/epr-10382b.118" },
|
||||
{ "md5": "161ceb196fcc9825bc981f933f305b5e", "file": "ports/cannonball/epr-10383.117" },
|
||||
{ "md5": "2d2b6149f1bccaaebf9c792f292e36d0", "file": "ports/cannonball/epr-10383b.117" },
|
||||
{ "md5": "9bc7fcfda9dea3a6d1c527b3146a4a95", "file": "ports/cannonball/mpr-10371.9" },
|
||||
{ "md5": "7da2895e20d2152ebddbdd2df29f3bd4", "file": "ports/cannonball/mpr-10372.13" },
|
||||
{ "md5": "5ed55433e64a4daecb9a0dbfc8771131", "file": "ports/cannonball/mpr-10373.10" },
|
||||
{ "md5": "5d09103268ac520609fb10ba186374b1", "file": "ports/cannonball/mpr-10374.14" },
|
||||
{ "md5": "187a6496e67c95e119ce97600902d178", "file": "ports/cannonball/mpr-10375.11" },
|
||||
{ "md5": "107a513ce97fb3b88d97425f19054471", "file": "ports/cannonball/mpr-10376.15" },
|
||||
{ "md5": "4a41f5ad01d78f52e8b6187c971c7270", "file": "ports/cannonball/mpr-10377.12" },
|
||||
{ "md5": "f6b7defe9860aa8a2f2e2915f1d9b94d", "file": "ports/cannonball/mpr-10378.16" },
|
||||
{ "md5": "866f1a596cc943bc5a416ff3e63cc9b0", "file": "ports/cannonball/opr-10185.11" },
|
||||
{ "md5": "866f1a596cc943bc5a416ff3e63cc9b0", "file": "ports/cannonball/opr-10186.47" },
|
||||
{ "md5": "ce4c62deb59049edb9972a5214ab39a1", "file": "ports/cannonball/opr-10188.71" },
|
||||
{ "md5": "c23c9998ed1b32918cdb3b0a80547aac", "file": "ports/cannonball/opr-10189.70" },
|
||||
{ "md5": "e555e967fcbb98d01c2da5730e9f6a28", "file": "ports/cannonball/opr-10190.69" },
|
||||
{ "md5": "edfdde9f0a1f2ea98139c9823a190e13", "file": "ports/cannonball/opr-10191.68" },
|
||||
{ "md5": "e36e9675a128d78acb191781e2271ae5", "file": "ports/cannonball/opr-10192.67" },
|
||||
{ "md5": "4980cb65d6ef5d4c4910d28fc200d369", "file": "ports/cannonball/opr-10193.66" },
|
||||
{ "md5": "e58109f14a0a4891afed803580577833", "file": "ports/cannonball/opr-10230.104" },
|
||||
{ "md5": "faf3f323326434cebbe3536359724985", "file": "ports/cannonball/opr-10231.103" },
|
||||
{ "md5": "7d6cae678c6a36bf0712958ee0e7711a", "file": "ports/cannonball/opr-10232.102" },
|
||||
{ "md5": "786da8c971d530bd51400da1904d5a3d", "file": "ports/cannonball/opr-10266.101" },
|
||||
{ "md5": "7ca240eb19818825f9d59d26b3037f69", "file": "ports/cannonball/opr-10267.100" },
|
||||
{ "md5": "080b53f5d3952a8a07981f9f66997887", "file": "ports/cannonball/opr-10268.99" },
|
||||
{ "md5": "7300090310ef7dc5cf4b7ebe06a38e66", "file": "cannonball/opr-10188.71f" } ] },
|
||||
|
||||
"CaveStory": { "name": "Cave Story (nxengine)", "biosFiles": [ { "md5": "", "file": "ports/CaveStory/Doukutsu.exe" } ] },
|
||||
"CaveStory": { "name": "Cave Story (nxengine)", "biosFiles": [ { "md5": "", "file": "ports/CaveStory/Doukutsu.exe" }, { "md5": "38695d3d69d7a0ada8178072dad4c58b", "file": "nxengine/Doukutsu.exe" } ] },
|
||||
|
||||
"Dinothawr": { "name": "Dinothawr", "biosFiles": [ { "md5": "", "file": "ports/dinothawr/dinothawr.game" } ] },
|
||||
|
||||
"Quake": { "name": "Quake (tryquake)", "biosFiles": [ { "md5": "", "file": "ports/quake/id1/pak0.pak" } ] },
|
||||
"Quake": { "name": "Quake (tryquake)", "biosFiles": [ { "md5": "", "file": "ports/quake/id1/pak0.pak" }, { "md5": "00480ea1008b9fe2e134896a015a31fd", "file": "quoth/pak0.pak" }, { "md5": "7e67039da2527d95f61c215ef6f879cd", "file": "quoth/pak1.pak" }, { "md5": "ae0a13119175f9b26b039d9bae508591", "file": "quoth/pak2.pak" } ] },
|
||||
|
||||
"REminiscence": { "name": "Flashback (REminiscence)", "biosFiles": [ { "md5": "", "file": "ports/reminiscence/level1.map" } ] },
|
||||
|
||||
"RickDangerous": { "name": "Rick Dangerous (xrick)", "biosFiles": [ { "md5": "", "file": "ports/xrick/data.zip" } ] },
|
||||
"RickDangerous": { "name": "Rick Dangerous (xrick)", "biosFiles": [ { "md5": "a471e64e9f69afbe59c10cc94ed1b184", "file": "ports/xrick/data.zip" } ] },
|
||||
|
||||
"VVVVVV": { "name": "VVVVVV", "biosFiles": [ { "md5": "", "file": "ports/VVVVVV/data.zip" } ] },
|
||||
|
||||
"eduke32": { "name": "Duke Nukem 3D", "biosFiles": [ { "md5": "", "file": "ports/eduke/duke3d.grp" } ] },
|
||||
"eduke32": { "name": "Duke Nukem 3D", "biosFiles": [ { "md5": "", "file": "ports/eduke/duke3d.grp" }, { "md5": "f8a7f96a84fc0f401abe2b1d617d1c76", "file": "penthous.zip" } ] },
|
||||
"sonic1": { "name": "Sonic 1", "biosFiles": [ { "md5": "", "file": "ports/sonic1/Data.rsdk" } ] },
|
||||
"sonic2": { "name": "Sonic 2", "biosFiles": [ { "md5": "", "file": "ports/sonic2/Data.rsdk" } ] },
|
||||
"soniccd": { "name": "Sonic CD", "biosFiles": [ { "md5": "", "file": "ports/soniccd/Data.rsdk" } ] },
|
||||
@@ -818,30 +839,7 @@ systems = {
|
||||
"cdi": { "name": "Phillips CDI", "biosFiles": [ { "md5": "", "file": "bios/same_cdi/bios/cdimono1.zip" },
|
||||
{ "md5": "", "file": "bios/same_cdi/bios/cdimono2.zip" },
|
||||
{ "md5": "", "file": "bios/same_cdi/bios/cdibios.zip" } ] },
|
||||
|
||||
"Cannonball": { "name": "Cannonball", "biosFiles": [ { "md5": "0cc087bbf1d5ad46319756b5b17ab9be", "file": "ports/cannonball/epr-10187.88" }, { "md5": "eddae6f176ad5a3438e4dae1b8c4d4fb", "file": "ports/cannonball/epr-10327.76" }, { "md5": "35bdcb3d8eaae6505af896a55f26dda1", "file": "ports/cannonball/epr-10327a.76" }, { "md5": "73486b2da791109e322c71cbaf51338b", "file": "ports/cannonball/epr-10328.75" }, { "md5": "5c82ed7dd7450f5c037b4dcaa0103218", "file": "ports/cannonball/epr-10328a.75" }, { "md5": "7197dfbdf83029abd56e72b98b274c63", "file": "ports/cannonball/epr-10329.58" }, { "md5": "f8729ace245eb8fc4854819f783f6042", "file": "ports/cannonball/epr-10329a.58" }, { "md5": "9556c737b5506241a5a58635e58bdbad", "file": "ports/cannonball/epr-10330.57" }, { "md5": "f0f9e2cce2d64dbb28650b11679f745b", "file": "ports/cannonball/epr-10330a.57" }, { "md5": "591daabf7c4b9005171f9af83068f3a1", "file": "ports/cannonball/epr-10380.133" }, { "md5": "94bb116334e7076aeeee5fcdcb536f87", "file": "ports/cannonball/epr-10380b.133" }, { "md5": "ab1a9fe4feb5d5bd867552e3740bdc07", "file": "ports/cannonball/epr-10381.132" }, { "md5": "ab1a9fe4feb5d5bd867552e3740bdc07", "file": "ports/cannonball/epr-10381b.132" }, { "md5": "bcf3cae054791aa0c2c9d3c144bb1682", "file": "ports/cannonball/epr-10382.118" }, { "md5": "8ff3212b3a2e37a37ba73a998bbc424f", "file": "ports/cannonball/epr-10382b.118" }, { "md5": "161ceb196fcc9825bc981f933f305b5e", "file": "ports/cannonball/epr-10383.117" }, { "md5": "2d2b6149f1bccaaebf9c792f292e36d0", "file": "ports/cannonball/epr-10383b.117" }, { "md5": "9bc7fcfda9dea3a6d1c527b3146a4a95", "file": "ports/cannonball/mpr-10371.9" }, { "md5": "7da2895e20d2152ebddbdd2df29f3bd4", "file": "ports/cannonball/mpr-10372.13" }, { "md5": "5ed55433e64a4daecb9a0dbfc8771131", "file": "ports/cannonball/mpr-10373.10" }, { "md5": "5d09103268ac520609fb10ba186374b1", "file": "ports/cannonball/mpr-10374.14" }, { "md5": "187a6496e67c95e119ce97600902d178", "file": "ports/cannonball/mpr-10375.11" }, { "md5": "107a513ce97fb3b88d97425f19054471", "file": "ports/cannonball/mpr-10376.15" }, { "md5": "4a41f5ad01d78f52e8b6187c971c7270", "file": "ports/cannonball/mpr-10377.12" }, { "md5": "f6b7defe9860aa8a2f2e2915f1d9b94d", "file": "ports/cannonball/mpr-10378.16" }, { "md5": "866f1a596cc943bc5a416ff3e63cc9b0", "file": "ports/cannonball/opr-10185.11" }, { "md5": "866f1a596cc943bc5a416ff3e63cc9b0", "file": "ports/cannonball/opr-10186.47" }, { "md5": "ce4c62deb59049edb9972a5214ab39a1", "file": "ports/cannonball/opr-10188.71" }, { "md5": "c23c9998ed1b32918cdb3b0a80547aac", "file": "ports/cannonball/opr-10189.70" }, { "md5": "e555e967fcbb98d01c2da5730e9f6a28", "file": "ports/cannonball/opr-10190.69" }, { "md5": "edfdde9f0a1f2ea98139c9823a190e13", "file": "ports/cannonball/opr-10191.68" }, { "md5": "e36e9675a128d78acb191781e2271ae5", "file": "ports/cannonball/opr-10192.67" }, { "md5": "4980cb65d6ef5d4c4910d28fc200d369", "file": "ports/cannonball/opr-10193.66" }, { "md5": "e58109f14a0a4891afed803580577833", "file": "ports/cannonball/opr-10230.104" }, { "md5": "faf3f323326434cebbe3536359724985", "file": "ports/cannonball/opr-10231.103" }, { "md5": "7d6cae678c6a36bf0712958ee0e7711a", "file": "ports/cannonball/opr-10232.102" }, { "md5": "786da8c971d530bd51400da1904d5a3d", "file": "ports/cannonball/opr-10266.101" }, { "md5": "7ca240eb19818825f9d59d26b3037f69", "file": "ports/cannonball/opr-10267.100" }, { "md5": "080b53f5d3952a8a07981f9f66997887", "file": "ports/cannonball/opr-10268.99" }, { "md5": "7300090310ef7dc5cf4b7ebe06a38e66", "file": "cannonball/opr-10188.71f" } ] },
|
||||
"CaveStory": { "name": "Cave Story (nxengine)", "biosFiles": [ { "md5": "", "file": "ports/CaveStory/Doukutsu.exe" }, { "md5": "38695d3d69d7a0ada8178072dad4c58b", "file": "nxengine/Doukutsu.exe" } ] },
|
||||
"DOOM1": { "name": "DOOM1 (PrBoom)", "biosFiles": [ { "md5": "", "file": "ports/doom/doom1.wad" }, { "md5": "5772f907fdee4284f6332d89c0c5609c", "file": "gzdoom.pk3" }, { "md5": "094baa608b85fbe636933b4098e7e1f1", "file": "game_widescreen_gfx.pk3" } ] },
|
||||
"DOOM2": { "name": "DOOM2 (PrBoom)", "biosFiles": [ { "md5": "", "file": "ports/doom2/doom2.wad" } ] },
|
||||
"Diablo": { "name": "Diablo 1 (devilutionx) ", "biosFiles": [ { "md5": "68f049866b44688a7af65ba766bef75a", "file": "ports/diablo/diabdat.mpq" } ] },
|
||||
"Dinothawr": { "name": "Dinothawr", "biosFiles": [ { "md5": "", "file": "ports/dinothawr/dinothawr.game" } ] },
|
||||
"OpenTyrian": { "name": "OpenTyrian", "biosFiles": [ { "md5": "", "file": "ports/opentyrian/cubetxt1.dat" }, { "md5": "", "file": "ports/opentyrian/cubetxt2.dat" }, { "md5": "", "file": "ports/opentyrian/cubetxt3.dat" }, { "md5": "", "file": "ports/opentyrian/cubetxt4.dat" }, { "md5": "", "file": "ports/opentyrian/demo.1" }, { "md5": "", "file": "ports/opentyrian/demo.2" }, { "md5": "", "file": "ports/opentyrian/demo.3" }, { "md5": "", "file": "ports/opentyrian/demo.4" }, { "md5": "", "file": "ports/opentyrian/demo.5" }, { "md5": "", "file": "ports/opentyrian/dpmi16bi.ovl" }, { "md5": "", "file": "ports/opentyrian/estsc.shp" }, { "md5": "", "file": "ports/opentyrian/exitmsg.bin" }, { "md5": "", "file": "ports/opentyrian/levels1.dat" }, { "md5": "", "file": "ports/opentyrian/levels2.dat" }, { "md5": "", "file": "ports/opentyrian/levels3.dat" }, { "md5": "", "file": "ports/opentyrian/levels4.dat" }, { "md5": "", "file": "ports/opentyrian/loudness.awe" }, { "md5": "", "file": "ports/opentyrian/modems.txt" }, { "md5": "", "file": "ports/opentyrian/music.mus" }, { "md5": "", "file": "ports/opentyrian/netarena.pcx" }, { "md5": "", "file": "ports/opentyrian/netfont1.pcx" }, { "md5": "", "file": "ports/opentyrian/netfont2.pcx" }, { "md5": "", "file": "ports/opentyrian/netmega.pcx" }, { "md5": "", "file": "ports/opentyrian/netset.pcx" }, { "md5": "", "file": "ports/opentyrian/newsh#.shp" }, { "md5": "", "file": "ports/opentyrian/newsh0.shp" }, { "md5": "", "file": "ports/opentyrian/newsh1.shp" }, { "md5": "", "file": "ports/opentyrian/newsh2.shp" }, { "md5": "", "file": "ports/opentyrian/newsh3.shp" }, { "md5": "", "file": "ports/opentyrian/newsh4.shp" }, { "md5": "", "file": "ports/opentyrian/newsh5.shp" }, { "md5": "", "file": "ports/opentyrian/newsh6.shp" }, { "md5": "", "file": "ports/opentyrian/newsh7.shp" }, { "md5": "", "file": "ports/opentyrian/newsh8.shp" }, { "md5": "", "file": "ports/opentyrian/newsh9.shp" }, { "md5": "", "file": "ports/opentyrian/newsha.shp" }, { "md5": "", "file": "ports/opentyrian/newshb.shp" }, { "md5": "", "file": "ports/opentyrian/newshc.shp" }, { "md5": "", "file": "ports/opentyrian/newshd.shp" }, { "md5": "", "file": "ports/opentyrian/newshe.shp" }, { "md5": "", "file": "ports/opentyrian/newshf.shp" }, { "md5": "", "file": "ports/opentyrian/newshg.shp" }, { "md5": "", "file": "ports/opentyrian/newshh.shp" }, { "md5": "", "file": "ports/opentyrian/newshj.shp" }, { "md5": "", "file": "ports/opentyrian/newshk.shp" }, { "md5": "", "file": "ports/opentyrian/newshl.shp" }, { "md5": "", "file": "ports/opentyrian/newshm.shp" }, { "md5": "", "file": "ports/opentyrian/newshn.shp" }, { "md5": "", "file": "ports/opentyrian/newsho.shp" }, { "md5": "", "file": "ports/opentyrian/newshp.shp" }, { "md5": "", "file": "ports/opentyrian/newshr.shp" }, { "md5": "", "file": "ports/opentyrian/newshs.shp" }, { "md5": "", "file": "ports/opentyrian/newsht.shp" }, { "md5": "", "file": "ports/opentyrian/newshu.shp" }, { "md5": "", "file": "ports/opentyrian/newshv.shp" }, { "md5": "", "file": "ports/opentyrian/newsh^.shp" }, { "md5": "", "file": "ports/opentyrian/newsh~.shp" }, { "md5": "", "file": "ports/opentyrian/order.tfp" }, { "md5": "", "file": "ports/opentyrian/palette.dat" }, { "md5": "", "file": "ports/opentyrian/setup.box" }, { "md5": "", "file": "ports/opentyrian/setup.ini" }, { "md5": "", "file": "ports/opentyrian/setup.int" }, { "md5": "", "file": "ports/opentyrian/shapesw.dat" }, { "md5": "", "file": "ports/opentyrian/shapesx.dat" }, { "md5": "", "file": "ports/opentyrian/shapesy.dat" }, { "md5": "", "file": "ports/opentyrian/shapesz.dat" }, { "md5": "", "file": "ports/opentyrian/shipedit.pcx" }, { "md5": "", "file": "ports/opentyrian/tshp2.pcx" }, { "md5": "", "file": "ports/opentyrian/tyrend.anm" }, { "md5": "", "file": "ports/opentyrian/tyrian.cdt" }, { "md5": "", "file": "ports/opentyrian/tyrian.hdt" }, { "md5": "", "file": "ports/opentyrian/tyrian.ico" }, { "md5": "", "file": "ports/opentyrian/tyrian.pic" }, { "md5": "", "file": "ports/opentyrian/tyrian.shp" }, { "md5": "", "file": "ports/opentyrian/tyrian.snd" }, { "md5": "", "file": "ports/opentyrian/tyrian1.lvl" }, { "md5": "", "file": "ports/opentyrian/tyrian2.lvl" }, { "md5": "", "file": "ports/opentyrian/tyrian3.lvl" }, { "md5": "", "file": "ports/opentyrian/tyrian4.lvl" }, { "md5": "", "file": "ports/opentyrian/tyrianc.shp" }, { "md5": "", "file": "ports/opentyrian/tyrset.pcx" }, { "md5": "", "file": "ports/opentyrian/user1.shp" }, { "md5": "", "file": "ports/opentyrian/user2.shp" }, { "md5": "", "file": "ports/opentyrian/voices.snd" }, { "md5": "", "file": "ports/opentyrian/voicesc.snd" }, { "md5": "", "file": "ports/opentyrian/netterm.int" }, { "md5": "", "file": "ports/opentyrian/newshi.shp" }, { "md5": "", "file": "ports/opentyrian/shapes).dat" } ] },
|
||||
"Quake": { "name": "Quake (tryquake)", "biosFiles": [ { "md5": "", "file": "ports/quake/id1/pak0.pak" }, { "md5": "00480ea1008b9fe2e134896a015a31fd", "file": "quoth/pak0.pak" }, { "md5": "7e67039da2527d95f61c215ef6f879cd", "file": "quoth/pak1.pak" }, { "md5": "ae0a13119175f9b26b039d9bae508591", "file": "quoth/pak2.pak" } ] },
|
||||
"REminiscence": { "name": "Flashback (REminiscence)", "biosFiles": [ { "md5": "", "file": "ports/reminiscence/level1.map" } ] },
|
||||
"RickDangerous": { "name": "Rick Dangerous (xrick)", "biosFiles": [ { "md5": "a471e64e9f69afbe59c10cc94ed1b184", "file": "ports/xrick/data.zip" } ] },
|
||||
"VVVVVV": { "name": "VVVVVV", "biosFiles": [ { "md5": "", "file": "ports/VVVVVV/data.zip" } ] },
|
||||
"amiga": { "name": "Amiga/CD32", "biosFiles": [ { "md5": "89da1838a24460e4b93f4f0c5d92d48d", "file": "bios/CDTV Extended-ROM v1.0 (1991)(Commodore)(CDTV)[!].rom" }, { "md5": "85ad74194e87c08904327de1a9443b7a", "file": "bios/Kickstart v1.2 r33.180 (1986)(Commodore)(A500-A1000-A2000)[!].rom" }, { "md5": "68c9c0826f6c0ca20546d588ee77391c", "file": "bios/Kickstart v1.2 rev 33.166 (1986)(Commodore)(A1000).rom" }, { "md5": "192d6d950d0ed3df8040b788502831c2", "file": "bios/Kickstart v1.3 r34.5 (1987)(Commodore)(A500-A1000-A2000-CDTV)[o].rom" }, { "md5": "82a21c1890cae844b3df741f2762d48d", "file": "bios/Kickstart v1.3 r34.5 (1987)(Commodore)(A500-A1000-A2000-CDTV)[!].rom" }, { "md5": "dc10d7bdd1b6f450773dfb558477c230", "file": "bios/Kickstart v2.04 r37.175 (1991)(Commodore)(A500+)[!].rom" }, { "md5": "465646c9b6729f77eea5314d1f057951", "file": "bios/Kickstart v2.05 r37.350 (1992)(Commodore)(A600HD)[!].rom" }, { "md5": "5f8924d013dd57a89cf349f4cdedc6b1", "file": "bios/Kickstart v3.1 r40.60 (1993)(Commodore)(CD32).rom" }, { "md5": "646773759326fbac3b2311fd8c8793ee", "file": "bios/Kickstart v3.1 r40.68 (1993)(Commodore)(A1200)[!].rom" }, { "md5": "413590e50098a056cfec418d3df0212d", "file": "bios/Kickstart v3.1 r40.68 (1993)(Commodore)(A3000).rom" }, { "md5": "9bdedde6a4f33555b4a270c8ca53297d", "file": "bios/Kickstart v3.1 r40.68 (1993)(Commodore)(A4000).rom" }, { "md5": "85ad74194e87c08904327de1a9443b7a", "file": "bios/kick33180.A500" }, { "md5": "82a21c1890cae844b3df741f2762d48d", "file": "bios/kick34005.A500" }, { "md5": "dc10d7bdd1b6f450773dfb558477c230", "file": "bios/kick37175.A500" }, { "md5": "5f8924d013dd57a89cf349f4cdedc6b1", "file": "bios/kick40060.CD32" }, { "md5": "bb72565701b1b6faece07d68ea5da639", "file": "bios/kick40060.CD32.ext" }, { "md5": "e40a5dfb3d017ba8779faba30cbd1c8e", "file": "bios/kick40063.A600" }, { "md5": "646773759326fbac3b2311fd8c8793ee", "file": "bios/kick40068.A1200" }, { "md5": "9bdedde6a4f33555b4a270c8ca53297d", "file": "bios/kick40068.A4000" }, { "md5": "bb72565701b1b6faece07d68ea5da639", "file": "bios/CD32 Extended-ROM r40.60 (1993)(Commodore)(CD32).rom" }, { "md5": "6c5b44c1a2bcee664129f850bfe4ed8a", "file": "uae_data/drive_click_A500.wav" }, { "md5": "7401f854a75f9ede18769168ab896a2a", "file": "uae_data/drive_spin_A500.wav" }, { "md5": "f9d954879fee8e96baa93d6139077cdb", "file": "uae_data/drive_spinnd_A500.wav" }, { "md5": "aebeacc7e7bc52ef890785c835bec3d0", "file": "uae_data/drive_startup_A500.wav" }, { "md5": "1a4b2d6254918ff769f26beeede75655", "file": "uae_data/drive_snatch_A500.wav" }, { "md5": "b64bee23f356635f67fb08b543df9797", "file": "uae_data/drive_click_LOUD.wav" }, { "md5": "56d8e36a2f04b5e8c5a1f1530cfc886c", "file": "uae_data/drive_spin_LOUD.wav" }, { "md5": "43ab05c6d969cb40805970a5b2352875", "file": "uae_data/drive_spinnd_LOUD.wav" }, { "md5": "743b4b9f0ca645c4c2a34953b5e01e45", "file": "uae_data/drive_startup_LOUD.wav" }, { "md5": "40cdf89f35b68cfefd4df5fb0256e02f", "file": "uae_data/drive_snatch_LOUD.wav" } ] },
|
||||
"atari7800": { "name": "Atari 7800", "biosFiles": [ { "md5": "0763f1ffb006ddbe32e52d497ee848ae", "file": "bios/7800 BIOS (U).rom" } ] },
|
||||
"atari800": { "name": "Atari 800", "biosFiles": [ { "md5": "06daac977823773a3eea3422fd26a703", "file": "bios/ATARIXL.ROM" }, { "md5": "0bac0c6a50104045d902df4503a4c30b", "file": "bios/ATARIBAS.ROM" }, { "md5": "eb1f32f5d9f382db1bbfb8d7f9cb343a", "file": "bios/ATARIOSA.ROM" }, { "md5": "a3e8d617c95d08031fe1b20d541434b2", "file": "bios/ATARIOSB.ROM" } ] },
|
||||
"colecovision": { "name": "Colecovision", "biosFiles": [ { "md5": "", "file": "bios/Machines/COL - Bit Corporation Dina/config.ini" }, { "md5": "1de922acdd742d31349c2801e9768c35", "file": "bios/Machines/COL - Bit Corporation Dina/czz50-1.rom" }, { "md5": "72b089dc55b7fe7ffb5028f365e8c045", "file": "bios/Machines/COL - Bit Corporation Dina/czz50-2.rom" }, { "md5": "2c66f5911e5b42b8ebe113403548eee7", "file": "bios/Machines/COL - ColecoVision/coleco.rom" }, { "md5": "", "file": "bios/Machines/COL - ColecoVision/config.ini" }, { "md5": "2c66f5911e5b42b8ebe113403548eee7", "file": "bios/Machines/COL - ColecoVision with Opcode Memory Extension/coleco.rom" }, { "md5": "", "file": "bios/Machines/COL - ColecoVision with Opcode Memory Extension/config.ini" }, { "md5": "", "file": "bios/Machines/COL - Spectravideo SVI-603 Coleco/config.ini" }, { "md5": "c60a2e85572c0ccb69505a7646d5c1b6", "file": "bios/Machines/COL - Spectravideo SVI-603 Coleco/SVI603.ROM" }, { "md5": "", "file": "bios/Databases/colecodb.xml" } ] },
|
||||
"eduke32": { "name": "Duke Nukem 3D", "biosFiles": [ { "md5": "", "file": "ports/eduke/duke3d.grp" }, { "md5": "f8a7f96a84fc0f401abe2b1d617d1c76", "file": "penthous.zip" } ] },
|
||||
"fds": { "name": "Nintendo Family Computer Disk System", "biosFiles": [ { "md5": "7bfe8c0540ed4bd6a0f1e2a0f0118ced", "file": "bios/NstDatabase.xml" }, { "md5": "ca30b50f880eb660a320674ed365ef7a", "file": "bios/disksys.rom" } ] },
|
||||
"gbc": { "name": "Game Boy Color", "biosFiles": [ { "md5": "32fbbd84168d3482956eb3c5051637f5", "file": "bios/gb_bios.bin" }, { "md5": "dbfce9db9deaa2567f6a84fde55f9680", "file": "bios/gbc_bios.bin" } ] },
|
||||
"neogeo": { "name": "NeoGeo", "biosFiles": [ { "md5": "", "file": "neogeo/neogeo.zip" } ] },
|
||||
"neogeocd": { "name": "NeoGeo CD", "biosFiles": [ { "md5": "", "file": "neogeo/neogeo.zip" }, { "md5": "f39572af7584cb5b3f70ae8cc848aba2", "file": "bios/neocd/neocd_z.rom" }, { "md5": "b31a246b69493c35194fe0bac120dc37", "file": "neocdz.zip" }, { "md5": "006a881532f2caffc3fe9fb5593f2ed5", "file": "neocd.zip" } ] },
|
||||
"o2em": { "name": "Odyssey 2", "biosFiles": [ { "md5": "562d5ebf9e030a40d6fabfc2f33139fd", "file": "bios/o2rom.bin" }, { "md5": "f1071cdb0b6b10dde94d3bc8a6146387", "file": "bios/c52.bin" }, { "md5": "c500ff71236068e0dc0d0603d265ae76", "file": "bios/g7400.bin" }, { "md5": "279008e4a0db2dc5f1c048853b033828", "file": "bios/jopac.bin" } ] },
|
||||
"pcengine": { "name": "PC Engine", "biosFiles": [ { "md5": "ff1a674273fe3540ccef576376407d1d", "file": "bios/syscard3.pce" } ] },
|
||||
|
||||
|
||||
}
|
||||
|
||||
class BiosStatus:
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
# and Invoke-WebRequest throws on a cut transfer before the hash is read:
|
||||
# the site's build rewrites this line there (scripts/sitebootstrap.py).
|
||||
$mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
|
||||
$defaultInstallSha256 = "34fd006cc0456c61b9a621e52f35cee6d3fd55ff14bbc6cea207ddc7bc57d858"
|
||||
$defaultInstallSha256 = "57e3cb6739a701181908773e603f17d2b10e1b0e2a5cde5cc4d5c97426da5dd4"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+222
-37
@@ -109,8 +109,8 @@ TARGETS_URL = f"{BASE_URL}/install/targets/{{platform}}.json"
|
||||
# has no hardware targets, where the run sent the user after a list that
|
||||
# exists nowhere.
|
||||
TARGET_PLATFORMS = frozenset({
|
||||
"batocera", "emudeck", "knulli", "lakka", "recalbox", "retroarch", "retrobat",
|
||||
"retrodeck", "retropie", "rocknix", "romm",
|
||||
"amberelec", "batocera", "emudeck", "emuelec", "knulli", "lakka", "recalbox", "retroarch",
|
||||
"retrobat", "retrodeck", "retropie", "rocknix", "romm",
|
||||
})
|
||||
RAW_FILE_URL = f"{BASE_URL}/{{path}}"
|
||||
RELEASE_URL = (
|
||||
@@ -291,7 +291,6 @@ def _os_default_dests(os_type: str) -> dict[str, Path]:
|
||||
the command line but not found must not land in ~/bios, which none of
|
||||
them reads.
|
||||
"""
|
||||
home = Path.home()
|
||||
if os_type == "windows":
|
||||
return {
|
||||
# RetroArch's system directory defaults to <exe folder>\system
|
||||
@@ -307,13 +306,13 @@ def _os_default_dests(os_type: str) -> dict[str, Path]:
|
||||
# RetroArch may be the Linux one inside the distribution, while RetroBat
|
||||
# only exists on the Windows side, reached through /mnt/c.
|
||||
return {
|
||||
"retroarch": home / ".config" / "retroarch" / "system",
|
||||
"retroarch": _unix_retroarch_base() / "system",
|
||||
"retrobat": _host_path("C:/RetroBat/bios"),
|
||||
}
|
||||
if os_type == "darwin":
|
||||
return {"retroarch": _macos_retroarch_base() / "system"}
|
||||
return {
|
||||
"retroarch": home / ".config" / "retroarch" / "system",
|
||||
"retroarch": _unix_retroarch_base() / "system",
|
||||
}
|
||||
|
||||
|
||||
@@ -681,8 +680,9 @@ EVENT_KEYS: dict[str, tuple[str, ...]] = {
|
||||
# found nowhere and no folder to assume) or from (a folder named with --from,
|
||||
# reason: not_found, not_a_folder, unreadable, or system for the root
|
||||
# of the system and /proc, /sys, /dev) or dest (reason: unreadable, a
|
||||
# folder above it this user cannot open, or not_writable, the BIOS
|
||||
# folder cannot be created, a summary following) or option (reason:
|
||||
# folder above it this user cannot open, not_a_folder, the BIOS folder
|
||||
# is a file or lies under one, or not_writable, the BIOS folder cannot
|
||||
# be created, a summary following) or option (reason:
|
||||
# unknown, a platform, target, system, core or emulator the run cannot
|
||||
# use, refused before any download).
|
||||
"error": ("stage", "reason", "platform", "message"),
|
||||
@@ -1385,6 +1385,17 @@ def _macos_retroarch_base() -> Path:
|
||||
return Path.home() / "Documents" / "RetroArch"
|
||||
|
||||
|
||||
def _unix_retroarch_base() -> Path:
|
||||
"""RetroArch's base folder on Linux: $XDG_CONFIG_HOME/retroarch when the
|
||||
variable is set, else ~/.config/retroarch (fill_pathname_application_data
|
||||
in file_path_special.c, frontend_unix_get_env in platform_unix.c). The
|
||||
code tests only that the variable exists, so an empty one counts."""
|
||||
xdg = os.environ.get("XDG_CONFIG_HOME")
|
||||
if xdg is not None:
|
||||
return Path(xdg) / "retroarch"
|
||||
return Path.home() / ".config" / "retroarch"
|
||||
|
||||
|
||||
# System directory -> the retroarch.cfg it was read from and the directory
|
||||
# ':' expands to there, for finding the cores of the same installation.
|
||||
_RETROARCH_CONFIGS: dict[Path, tuple[Path, Path]] = {}
|
||||
@@ -1472,7 +1483,12 @@ def _parse_bash_var(path: Path, key: str) -> str | None:
|
||||
# /usr/share/batocera to /usr/share/knulli (knulli-linux 57afa662,
|
||||
# board/scripts/post-build-script.sh:264-278) and writes KNULLI_SYSTEM_ARCH
|
||||
# to knulli.arch (package/system/knulli-system/knulli-system.mk:98-99).
|
||||
# AmberELEC's scripts/image writes COREELEC_DEVICE (a21975bf, line 168), the
|
||||
# DEVICE its Makefile builds (RG351P). EmuELEC's writes the same line (v4.8,
|
||||
# line 189), the DEVICE of the release image (OdroidGoAdvance).
|
||||
_BOARD_SOURCES: dict[str, tuple[str, str]] = {
|
||||
"amberelec": ("os_release", "COREELEC_DEVICE"),
|
||||
"emuelec": ("os_release", "COREELEC_DEVICE"),
|
||||
"batocera": ("file", "/usr/share/batocera/batocera.arch"),
|
||||
"recalbox": ("file", "/recalbox/recalbox.arch"),
|
||||
"rocknix": ("os_release", "HW_DEVICE"),
|
||||
@@ -2773,11 +2789,16 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]:
|
||||
if ra_dir:
|
||||
found.append(("retroarch", ra_dir))
|
||||
|
||||
# RetroArch native
|
||||
native_cfg = home / ".config" / "retroarch" / "retroarch.cfg"
|
||||
ra_dir = _parse_retroarch_system_dir(native_cfg)
|
||||
if ra_dir:
|
||||
found.append(("retroarch", ra_dir))
|
||||
# RetroArch native: the config under its base folder, then
|
||||
# ~/.retroarch.cfg when that one is missing (open_default_config_file
|
||||
# in configuration.c). Either way the system folder defaults to the
|
||||
# base folder's.
|
||||
native_base = _unix_retroarch_base()
|
||||
for native_cfg in (native_base / "retroarch.cfg", home / ".retroarch.cfg"):
|
||||
ra_dir = _parse_retroarch_system_dir(native_cfg, native_base)
|
||||
if ra_dir:
|
||||
found.append(("retroarch", ra_dir))
|
||||
break
|
||||
|
||||
if os_type == "darwin":
|
||||
# open_default_config_file reads <Application Support>/RetroArch/config
|
||||
@@ -3692,6 +3713,9 @@ def _check_data_directory(entry: object, where: str) -> None:
|
||||
strip = entry.get("strip_components", 0)
|
||||
if not isinstance(strip, int) or isinstance(strip, bool) or not 0 <= strip <= 16:
|
||||
raise ValueError(f"{where}.strip_components is invalid")
|
||||
size = entry.get("archive_size", 1)
|
||||
if not isinstance(size, int) or isinstance(size, bool) or size < 1:
|
||||
raise ValueError(f"{where}.archive_size is invalid")
|
||||
if kind == "tarball" and (
|
||||
not isinstance(entry.get("source_path"), str) or not entry["source_path"]
|
||||
):
|
||||
@@ -6440,7 +6464,8 @@ def _report_outcome(
|
||||
kept = _kept_lines() + ([
|
||||
f"{_plural(len(unplaced), 'file')} read from "
|
||||
f"{unplaced[0].get('frontend') or 'the frontend'}'s save directory left out: "
|
||||
f"its place is not known. {LINE_SAVE_REMEDY}"] if unplaced else [])
|
||||
+ (f"{unplaced[0]['save_why']}." if unplaced[0].get("save_why")
|
||||
else f"its place is not known. {LINE_SAVE_REMEDY}")] if unplaced else [])
|
||||
kept += list(notes or [])
|
||||
if interrupted:
|
||||
say(f"\nStopped. {_plural(downloaded, 'file')} installed.")
|
||||
@@ -6856,11 +6881,12 @@ def _print_plan(
|
||||
# files, and 2636 data files" read as two kinds of one thing. The kinds
|
||||
# follow, adding up to it.
|
||||
total = counts.files + tree_files
|
||||
parts = ([f"{_plural(total, 'file')} ({format_size(counts.size + tree_counts.size)})"]
|
||||
tree_bytes = sum(data_fetch_bytes(entry, needed) for entry, needed in trees)
|
||||
parts = ([f"{_plural(total, 'file')} ({format_size(counts.size + tree_bytes)})"]
|
||||
if total else [])
|
||||
split = parts_words(counts if (manifest or {}).get("categorized")
|
||||
else tally({"size": e.get("size")} for e in to_download),
|
||||
tree_files, tree_counts.size)
|
||||
tree_files, tree_bytes)
|
||||
if split and trees:
|
||||
split += f", the data files extracted from {_plural(len(trees), 'archive')}"
|
||||
# A pack is copied from, not downloaded: "Would download 23 files" was
|
||||
@@ -6935,8 +6961,11 @@ def _print_plan(
|
||||
"every system and its files.")
|
||||
for entry, needed in trees:
|
||||
source = LOCAL.pack.name if LOCAL.pack is not None else entry["url"]
|
||||
fetched = data_fetch_bytes(entry, needed)
|
||||
archive = ("" if fetched == sum(meta.get("size", 0) for meta in needed.values())
|
||||
else f" (a {format_size(fetched)} archive)")
|
||||
print(f" {term.bold(entry['ref'] + ' data')} {_sized(list(needed.values()))}, "
|
||||
f"from {source}")
|
||||
f"from {source}{archive}")
|
||||
for folder, (count, size) in sorted((kept or {}).items()):
|
||||
print(f"\n Would keep {_plural(count, 'file')} you have ({format_size(size)}), "
|
||||
f"whose contents differ, in {folder}")
|
||||
@@ -7062,20 +7091,72 @@ def download_files(
|
||||
return failed, interrupted
|
||||
|
||||
|
||||
def _fetch_archive(url: str, directory: Path, progress: Progress | None = None) -> Path:
|
||||
def _archive_validator(headers: object) -> str:
|
||||
"""What names this revision of an archive to its server: an If-Range
|
||||
with it gets the rest of the same bytes, or the whole new archive."""
|
||||
if not headers:
|
||||
return ""
|
||||
etag = headers.get("ETag", "") or ""
|
||||
# A weak tag never answers a range (RFC 9110 13.1.5).
|
||||
return etag if etag and not etag.startswith("W/") else headers.get("Last-Modified", "") or ""
|
||||
|
||||
|
||||
def _fetch_archive(url: str, directory: Path, progress: Progress | None = None,
|
||||
keep: Path | None = None) -> Path:
|
||||
"""A data directory's source archive, in *directory*, retried like files.
|
||||
progress draws its bytes as they come, an attempt that failed taken back."""
|
||||
progress draws its bytes as they come, an attempt that failed taken back.
|
||||
|
||||
With keep, the archive is written there and kept when the transfer is
|
||||
cut, beside a note of its address and the validator its server sent:
|
||||
the next attempt, or the next run, asks for the rest with Range and
|
||||
If-Range, and a server that changed the archive sends it whole. A cut
|
||||
50 MB archive started again from zero, and a link that drops more often
|
||||
than one archive takes never installed the tree.
|
||||
"""
|
||||
_network_allowed(url)
|
||||
note = keep.with_name(keep.name + ".source") if keep is not None else None
|
||||
reason: Exception | None = None
|
||||
for attempt in range(1, MAX_RETRIES + 1):
|
||||
handle = tempfile.NamedTemporaryFile(dir=directory, suffix=".archive", delete=False)
|
||||
offset = 0
|
||||
validator = ""
|
||||
if keep is not None:
|
||||
try:
|
||||
recorded = note.read_text(encoding="utf-8").split("\n")
|
||||
if recorded[0] == url and len(recorded) > 1 and recorded[1]:
|
||||
validator = recorded[1]
|
||||
offset = keep.stat().st_size
|
||||
except (OSError, UnicodeError):
|
||||
offset = 0
|
||||
request = urllib.request.Request(url)
|
||||
if offset:
|
||||
request.add_header("Range", f"bytes={offset}-")
|
||||
request.add_header("If-Range", validator)
|
||||
handle = (open(keep, "r+b" if offset else "wb") if keep is not None else
|
||||
tempfile.NamedTemporaryFile(dir=directory, suffix=".archive", delete=False))
|
||||
received = 0
|
||||
resumed = 0
|
||||
try:
|
||||
with handle, urllib.request.urlopen(url, timeout=60) as resp:
|
||||
with handle, urllib.request.urlopen(request, timeout=60) as resp:
|
||||
headers = resp.headers or {}
|
||||
ranged = (offset and getattr(resp, "status", 200) == 206 and str(
|
||||
headers.get("Content-Range", "")).startswith(f"bytes {offset}-"))
|
||||
if not ranged:
|
||||
offset = 0
|
||||
handle.seek(offset)
|
||||
handle.truncate()
|
||||
if keep is not None and not offset:
|
||||
# Written before the first byte: a cut transfer is then
|
||||
# resumed only against the revision it holds.
|
||||
note.write_text(f"{url}\n{_archive_validator(headers)}\n",
|
||||
encoding="utf-8")
|
||||
if progress is not None:
|
||||
length = resp.headers.get("Content-Length", "")
|
||||
length = headers.get("Content-Length", "")
|
||||
if length.isdigit():
|
||||
progress.expect(int(length))
|
||||
progress.expect(offset + int(length))
|
||||
if offset:
|
||||
progress.add(offset, resumed=True)
|
||||
resumed = offset
|
||||
total = offset
|
||||
while True:
|
||||
if _stop.is_set():
|
||||
raise _Interrupted()
|
||||
@@ -7083,30 +7164,51 @@ def _fetch_archive(url: str, directory: Path, progress: Progress | None = None)
|
||||
if not chunk:
|
||||
break
|
||||
received += len(chunk)
|
||||
if received > MAX_DATA_ARCHIVE_SIZE:
|
||||
total += len(chunk)
|
||||
if total > MAX_DATA_ARCHIVE_SIZE:
|
||||
raise ValueError("archive exceeds the size limit")
|
||||
handle.write(chunk)
|
||||
if progress is not None:
|
||||
progress.add(len(chunk))
|
||||
return Path(handle.name)
|
||||
return keep if keep is not None else Path(handle.name)
|
||||
except urllib.error.HTTPError as exc:
|
||||
Path(handle.name).unlink(missing_ok=True)
|
||||
if exc.code < 500 and exc.code != 429:
|
||||
if keep is None or exc.code == 416 or exc.code < 500 and exc.code != 429:
|
||||
# Refused, or a range past an archive that shrank: no
|
||||
# attempt resumes it.
|
||||
_drop_archive(Path(handle.name), note)
|
||||
if exc.code < 500 and exc.code not in (416, 429):
|
||||
raise
|
||||
reason = exc
|
||||
except (urllib.error.URLError, OSError, http.client.HTTPException) as exc:
|
||||
Path(handle.name).unlink(missing_ok=True)
|
||||
if keep is None:
|
||||
Path(handle.name).unlink(missing_ok=True)
|
||||
reason = exc if isinstance(exc, OSError) else ConnectionError(repr(exc))
|
||||
except BaseException:
|
||||
Path(handle.name).unlink(missing_ok=True)
|
||||
except ValueError:
|
||||
_drop_archive(Path(handle.name), note)
|
||||
raise
|
||||
if progress is not None and received:
|
||||
progress.add(-received)
|
||||
except BaseException:
|
||||
# Stopped: a kept archive waits for the next run.
|
||||
if keep is None:
|
||||
Path(handle.name).unlink(missing_ok=True)
|
||||
raise
|
||||
if progress is not None:
|
||||
if received:
|
||||
progress.add(-received)
|
||||
if resumed:
|
||||
progress.add(-resumed)
|
||||
progress.forget_resumed(resumed)
|
||||
if attempt < MAX_RETRIES:
|
||||
time.sleep(_retry_delay(attempt, None))
|
||||
raise reason if reason else OSError("download failed")
|
||||
|
||||
|
||||
def _drop_archive(path: Path, note: Path | None) -> None:
|
||||
"""Remove an archive and the note of where it came from."""
|
||||
path.unlink(missing_ok=True)
|
||||
if note is not None:
|
||||
note.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def _archive_members(
|
||||
archive: Path, entry: dict
|
||||
) -> Iterator[tuple[str, int, Callable[[], BinaryIO]]]:
|
||||
@@ -7167,6 +7269,19 @@ def data_directory_state(entry: dict, bios_path: Path,
|
||||
return root, needed, present
|
||||
|
||||
|
||||
def data_fetch_bytes(entry: dict, needed: dict[str, dict]) -> int:
|
||||
"""What getting a tree's missing files costs: its source archive, which
|
||||
comes whole for one file, or from a pack the files themselves. A list
|
||||
that records no archive size gives the files' own sizes: a tarball whose
|
||||
tree is 10 KB was announced as 10 KB and was a download of 21 MB."""
|
||||
if not needed:
|
||||
return 0
|
||||
size = entry.get("archive_size")
|
||||
if LOCAL.pack is None and isinstance(size, int) and size > 0:
|
||||
return size
|
||||
return sum(meta.get("size", 0) for meta in needed.values())
|
||||
|
||||
|
||||
def install_data_directory(entry: dict, bios_path: Path,
|
||||
state: tuple[Path, dict[str, dict], int] | None = None,
|
||||
progress: Progress | None = None) -> dict:
|
||||
@@ -7186,6 +7301,7 @@ def install_data_directory(entry: dict, bios_path: Path,
|
||||
if not needed:
|
||||
return counts
|
||||
_writing.set()
|
||||
fetched: Path | None = None
|
||||
try:
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(dir=root) as scratch:
|
||||
@@ -7193,7 +7309,11 @@ def install_data_directory(entry: dict, bios_path: Path,
|
||||
# The pack holds the tree where it extracts, already mapped.
|
||||
members = LOCAL.pack.tree(entry.get("dest", ""))
|
||||
else:
|
||||
archive = _fetch_archive(entry["url"], Path(scratch), progress)
|
||||
# Beside the tree, outside the scratch folder: a transfer
|
||||
# cut short is resumed by the next attempt or the next run.
|
||||
kept = root / f".{entry['ref']}.archive.part"
|
||||
archive = _fetch_archive(entry["url"], Path(scratch), progress, kept)
|
||||
fetched = archive
|
||||
members = _archive_members(archive, entry)
|
||||
for rel, declared, opener in members:
|
||||
meta = needed.pop(rel, None)
|
||||
@@ -7227,6 +7347,10 @@ def install_data_directory(entry: dict, bios_path: Path,
|
||||
except _MEMBER_ERRORS as exc:
|
||||
# A damaged, encrypted or unsupported member, in words.
|
||||
counts["error"] = unreadable_words(exc)
|
||||
finally:
|
||||
if fetched is not None:
|
||||
# Read whole: the next run fetches the archive afresh.
|
||||
_drop_archive(fetched, fetched.with_name(fetched.name + ".source"))
|
||||
counts["absent"] = len(needed)
|
||||
return counts
|
||||
|
||||
@@ -8620,6 +8744,13 @@ def saves_line(display: str, count: int, where: str) -> str:
|
||||
f"{shown_path(folder)}")
|
||||
|
||||
|
||||
def unlisted_saves_words(display: str) -> str:
|
||||
"""Why a file list holds none of the files read from the save
|
||||
directory: the platform keeps it in no fixed place, and the list leaves
|
||||
them out with no hash or address, which --save-dir cannot change."""
|
||||
return f"{display} keeps it in no fixed place, and its file list holds none of them"
|
||||
|
||||
|
||||
def unplaced_line(display: str, count: int, remedy: str) -> str:
|
||||
"""The files read from an unknown save directory, left out."""
|
||||
return (f"{_plural(count, 'file')} read from {display}'s save directory left out: "
|
||||
@@ -8939,7 +9070,7 @@ def found_head(files: list[dict], trees: list[dict], bios_path: Path) -> str:
|
||||
for tree in trees:
|
||||
_root, wanted, _present = data_directory_state(tree, bios_path, remember=True)
|
||||
data += len(wanted)
|
||||
data_bytes += sum(meta.get("size", 0) for meta in wanted.values())
|
||||
data_bytes += data_fetch_bytes(tree, wanted)
|
||||
left = [entry for entry in needed if id(entry) not in copies]
|
||||
rest = len(left) + data
|
||||
where = ", ".join(f"{count} found in {_last_name(label)}"
|
||||
@@ -13413,6 +13544,29 @@ def main() -> None:
|
||||
if (args.list_platforms or args.list_targets or args.list_systems
|
||||
or args.list_cores or args.list_emulators):
|
||||
parser.error("--for-games narrows an install, not a listing")
|
||||
# Applied or refused, never ignored: a run prints one list, and the
|
||||
# narrowing options apply to --list-systems and --list-cores only, past
|
||||
# --target and --installed-cores, which --list-emulators also reads.
|
||||
listings = [flag for flag, given in (
|
||||
("--list-platforms", args.list_platforms), ("--list-targets", args.list_targets),
|
||||
("--list-systems", args.list_systems), ("--list-cores", args.list_cores),
|
||||
("--list-emulators", args.list_emulators)) if given]
|
||||
if len(listings) > 1:
|
||||
parser.error(f"{listings[0]} and {listings[1]} each print a list: run them "
|
||||
"one at a time")
|
||||
if listings and listings[0] in ("--list-platforms", "--list-targets",
|
||||
"--list-emulators"):
|
||||
unread = (("--system", args.system), ("--core", args.core),
|
||||
("--region", args.region), ("--emulator", args.emulator))
|
||||
if listings[0] != "--list-emulators":
|
||||
unread += (("--target", args.target), ("--every-board", args.every_board),
|
||||
("--installed-cores", args.installed_cores))
|
||||
if listings[0] == "--list-platforms":
|
||||
unread += (("--platform", args.platform),)
|
||||
given = next((flag for flag, value in unread if value), "")
|
||||
if given:
|
||||
parser.error(f"{listings[0]} prints the whole list, which {given} "
|
||||
"does not narrow")
|
||||
if args.declared_only:
|
||||
# Applied or refused, never ignored.
|
||||
for given, reason in (
|
||||
@@ -13758,6 +13912,27 @@ def _refuse_closed(dest: Path, named: str) -> None:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _refuse_file(dest: Path, named: str) -> None:
|
||||
"""End the run on a BIOS folder that is a file, or lies under one.
|
||||
|
||||
mkdir then failed on EEXIST or ENOTDIR, and the run blamed permissions
|
||||
and offered sudo, which fails the same way.
|
||||
"""
|
||||
for folder in (dest, *dest.parents):
|
||||
if _exists(folder):
|
||||
break
|
||||
else:
|
||||
return
|
||||
if _is_dir(folder):
|
||||
return
|
||||
where = (f"{shown_path(dest)} is a file, not a folder" if folder == dest
|
||||
else f"{shown_path(dest)} cannot be a folder: {shown_path(folder)} is a file")
|
||||
EVENTS.emit("error", stage="dest", reason="not_a_folder", platform="", message=where)
|
||||
print(f"Error: {screen_text(where)}. Name the folder the BIOS files go in, not "
|
||||
f"the file {named}.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _missing_dest(dest: Path, ask: bool, named: str = "named with --dest",
|
||||
again: bool = False) -> bool:
|
||||
"""Say that a --dest folder does not exist before anything goes there.
|
||||
@@ -13772,6 +13947,7 @@ def _missing_dest(dest: Path, ask: bool, named: str = "named with --dest",
|
||||
a card's folder ended the run where it meant "not that card".
|
||||
"""
|
||||
_refuse_closed(dest, named)
|
||||
_refuse_file(dest, named)
|
||||
if dest.exists():
|
||||
return True
|
||||
nearest = dest.absolute()
|
||||
@@ -15099,6 +15275,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
else f"{found_in[0]} names" if found_in else "detected here")
|
||||
# One setup by now: two found without questions stopped above.
|
||||
_refuse_closed(path, f"{named} (name another with --dest)")
|
||||
_refuse_file(path, f"{named} (name another with --dest)")
|
||||
|
||||
total_downloaded = 0
|
||||
total_up_to_date = 0
|
||||
@@ -15137,7 +15314,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
# --installed-cores is below: EmuDeck installed 3.2 GB and only then was
|
||||
# steamos refused for RetroArch, with no summary of what had landed.
|
||||
target_lists: dict[str, dict] = {}
|
||||
if args.target and not args.list_targets:
|
||||
if args.target:
|
||||
for plat_name, _bios_path in platforms:
|
||||
targets = fetch_targets(plat_name)
|
||||
if targets is None:
|
||||
@@ -15333,6 +15510,10 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
display = display_name(plat_name, manifest)
|
||||
files, unplaced = place_save_entries(
|
||||
files, bios_path, manifest, args.save_dir, display, say=False)
|
||||
if not manifest.get("save_directory"):
|
||||
# Its own list leaves these files out: naming the folder with
|
||||
# --save-dir installs none of them, and was advised in a loop.
|
||||
_SAVE_WHY[bios_path] = unlisted_saves_words(display)
|
||||
# The games of the card, read before the choices: where it holds
|
||||
# some, what they need is the menu's first choice, and --for-games
|
||||
# narrows the list here, before the systems are counted.
|
||||
@@ -15535,7 +15716,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
total_omitted += len(omitted_files)
|
||||
# Named for the end of the run: ROCKNIX's own list leaves them out,
|
||||
# and the end said "the frontend's save directory".
|
||||
omitted_seen.extend({**entry, "frontend": display}
|
||||
omitted_seen.extend({**entry, "frontend": display,
|
||||
"save_why": unlisted_saves_words(display)}
|
||||
if entry.get("reason") == "no_save_directory"
|
||||
and not entry.get("frontend") else entry
|
||||
for entry in omitted_files)
|
||||
@@ -15608,8 +15790,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
plat_name, manifest, bios_path, files, up_to_date, mismatched,
|
||||
sum(len(state[1]) for _entry, state in data_states), len(omitted_files),
|
||||
chosen=selection[0] if selection else None,
|
||||
data_bytes=sum(meta.get("size", 0) for _entry, state in data_states
|
||||
for meta in state[1].values()),
|
||||
data_bytes=sum(data_fetch_bytes(entry, state[1])
|
||||
for entry, state in data_states),
|
||||
)
|
||||
emit_copies(plat_name, to_download, copies, bios_index, bios_path, files)
|
||||
emit_unreadable(plat_name, unread)
|
||||
@@ -21150,6 +21332,9 @@ def _unplaced_lines(summary: dict | None, state: TuiState,
|
||||
if not count:
|
||||
return []
|
||||
name = display_name(state.platform) if state.platform else "the frontend"
|
||||
if state.manifest and not state.manifest.get("save_directory"):
|
||||
return [f"{_plural(count, 'file')} read from {name}'s save directory left out: "
|
||||
f"{unlisted_saves_words(name)}."]
|
||||
return [f"{_plural(count, 'file')} read from {name}'s save directory left out: "
|
||||
f"its place is not known. {remedy or TUI_SAVE_REMEDY}"]
|
||||
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins
|
||||
# curl or wget stop on a cut transfer before the hash is read: the site's
|
||||
# build rewrites this line there (scripts/sitebootstrap.py).
|
||||
MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
|
||||
DEFAULT_INSTALL_SHA256="34fd006cc0456c61b9a621e52f35cee6d3fd55ff14bbc6cea207ddc7bc57d858"
|
||||
DEFAULT_INSTALL_SHA256="57e3cb6739a701181908773e603f17d2b10e1b0e2a5cde5cc4d5c97426da5dd4"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
+171
-6
@@ -2106,6 +2106,10 @@
|
||||
"repo_path": "bios/Atari/400-800/ATARIXL.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"atari-400-800",
|
||||
"atari-5200"
|
||||
],
|
||||
"declared_in": [
|
||||
"atari-400-800"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -2128,6 +2132,10 @@
|
||||
"repo_path": "bios/Atari/400-800/ATARIBAS.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"atari-400-800",
|
||||
"atari-5200"
|
||||
],
|
||||
"declared_in": [
|
||||
"atari-400-800"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -2150,6 +2158,10 @@
|
||||
"repo_path": "bios/Atari/400-800/ATARIOSA.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"atari-400-800",
|
||||
"atari-5200"
|
||||
],
|
||||
"declared_in": [
|
||||
"atari-400-800"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -2172,6 +2184,10 @@
|
||||
"repo_path": "bios/Atari/400-800/ATARIOSB.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"atari-400-800",
|
||||
"atari-5200"
|
||||
],
|
||||
"declared_in": [
|
||||
"atari-400-800"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -2194,6 +2210,10 @@
|
||||
"repo_path": "bios/Atari/5200/5200.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"atari-400-800",
|
||||
"atari-5200"
|
||||
],
|
||||
"declared_in": [
|
||||
"atari-5200"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -2361,6 +2381,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Databases/.variants/colecodb.xml.78113162",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"coleco-colecovision"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -3432,6 +3457,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/ARAB1.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -3454,6 +3484,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/ARABIC.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -3476,6 +3511,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/BEERIDE.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -3498,6 +3538,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/FMPAC.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -3520,6 +3565,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/GCVMX80.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -3542,6 +3592,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/HANGUL.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -3564,6 +3619,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_kanjifont.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -3586,6 +3646,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/MICROSOLDISK.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -3608,6 +3673,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/openmsx/yrw801.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -4301,6 +4371,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_kanjibasic.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -4510,6 +4585,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/share/systemroms/fs-5500_disk.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -4532,6 +4612,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/NOVAXIS.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -4554,6 +4639,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/nowindDos1.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -4576,6 +4666,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/nowindDos2.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -4598,6 +4693,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/PAINT.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -4620,6 +4720,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_disk.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -4642,6 +4747,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/nms8250_disk.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -4664,6 +4774,11 @@
|
||||
"repo_path": "bios/Other/msx-emu/Machines/Shared Roms/RS232.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -4697,6 +4812,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/SUNRISEIDE.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -4719,6 +4839,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/SWP.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -4741,6 +4866,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/XBASIC2.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -5316,6 +5446,11 @@
|
||||
"repo_path": "bios/Nintendo/SNES/BS-X.bin",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"nintendo-gb",
|
||||
"nintendo-gbc",
|
||||
"nintendo-satellaview"
|
||||
],
|
||||
"declared_in": [
|
||||
"nintendo-satellaview"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -5512,6 +5647,9 @@
|
||||
"north-america",
|
||||
"oceania"
|
||||
],
|
||||
"region_groups": [
|
||||
"sega-master-system"
|
||||
],
|
||||
"region_system_groups": [
|
||||
"sega-master-system"
|
||||
],
|
||||
@@ -5541,6 +5679,9 @@
|
||||
"regions": [
|
||||
"japan"
|
||||
],
|
||||
"region_groups": [
|
||||
"sega-master-system"
|
||||
],
|
||||
"region_system_groups": [
|
||||
"sega-master-system"
|
||||
],
|
||||
@@ -5571,6 +5712,9 @@
|
||||
"europe",
|
||||
"oceania"
|
||||
],
|
||||
"region_groups": [
|
||||
"sega-mega-cd"
|
||||
],
|
||||
"region_system_groups": [
|
||||
"sega-mega-cd"
|
||||
],
|
||||
@@ -5602,6 +5746,9 @@
|
||||
"latin-america",
|
||||
"north-america"
|
||||
],
|
||||
"region_groups": [
|
||||
"sega-mega-cd"
|
||||
],
|
||||
"region_system_groups": [
|
||||
"sega-mega-cd"
|
||||
],
|
||||
@@ -5632,6 +5779,9 @@
|
||||
"regions": [
|
||||
"japan"
|
||||
],
|
||||
"region_groups": [
|
||||
"sega-mega-cd"
|
||||
],
|
||||
"region_system_groups": [
|
||||
"sega-mega-cd"
|
||||
],
|
||||
@@ -6104,6 +6254,9 @@
|
||||
"regions": [
|
||||
"japan"
|
||||
],
|
||||
"region_groups": [
|
||||
"sony-playstation"
|
||||
],
|
||||
"region_system_groups": [
|
||||
"sony-playstation"
|
||||
],
|
||||
@@ -6136,6 +6289,9 @@
|
||||
"latin-america",
|
||||
"north-america"
|
||||
],
|
||||
"region_groups": [
|
||||
"sony-playstation"
|
||||
],
|
||||
"region_system_groups": [
|
||||
"sony-playstation"
|
||||
],
|
||||
@@ -6168,6 +6324,9 @@
|
||||
"europe",
|
||||
"oceania"
|
||||
],
|
||||
"region_groups": [
|
||||
"sony-playstation"
|
||||
],
|
||||
"region_system_groups": [
|
||||
"sony-playstation"
|
||||
],
|
||||
@@ -8987,7 +9146,8 @@
|
||||
],
|
||||
"region_groups": [
|
||||
"sony-playstation"
|
||||
]
|
||||
],
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"dest": "bios/scph1002a.bin",
|
||||
@@ -9233,7 +9393,8 @@
|
||||
],
|
||||
"region_groups": [
|
||||
"sony-playstation"
|
||||
]
|
||||
],
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"dest": "bios/scph7002.bin",
|
||||
@@ -63489,7 +63650,8 @@
|
||||
"size": 344
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 4465709
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-cheats",
|
||||
@@ -77218,7 +77380,8 @@
|
||||
"size": 10054
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 5083767
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-samples",
|
||||
@@ -77451,7 +77614,8 @@
|
||||
"size": 551776
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 53006540
|
||||
},
|
||||
{
|
||||
"ref": "freeintv-overlays",
|
||||
@@ -77823,7 +77987,8 @@
|
||||
"size": 248636
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 21311170
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1841,8 +1841,7 @@
|
||||
"reason": "not_found",
|
||||
"cores": [
|
||||
"dosbox-x"
|
||||
],
|
||||
"held": true
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "extras.wad",
|
||||
@@ -225291,7 +225290,8 @@
|
||||
"size": 10054
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 5083767
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-samples",
|
||||
@@ -225524,7 +225524,8 @@
|
||||
"size": 551776
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 53006540
|
||||
}
|
||||
],
|
||||
"save_directory": {
|
||||
|
||||
+18
-9
@@ -100486,7 +100486,8 @@
|
||||
"size": 344
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 4465709
|
||||
},
|
||||
{
|
||||
"ref": "dinothawr",
|
||||
@@ -100934,7 +100935,8 @@
|
||||
"size": 105
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 5752250
|
||||
},
|
||||
{
|
||||
"ref": "dirksimple",
|
||||
@@ -100990,7 +100992,8 @@
|
||||
"size": 12988
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 175384
|
||||
},
|
||||
{
|
||||
"ref": "dolphin-sys",
|
||||
@@ -109666,7 +109669,8 @@
|
||||
"strip_components": 2,
|
||||
"exclude": [
|
||||
"Themes"
|
||||
]
|
||||
],
|
||||
"archive_size": 3197914
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-cheats",
|
||||
@@ -123395,7 +123399,8 @@
|
||||
"size": 10054
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 5083767
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-samples",
|
||||
@@ -123628,7 +123633,8 @@
|
||||
"size": 551776
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 53006540
|
||||
},
|
||||
{
|
||||
"ref": "freeintv-overlays",
|
||||
@@ -124000,7 +124006,8 @@
|
||||
"size": 248636
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 21311170
|
||||
},
|
||||
{
|
||||
"ref": "lrps2",
|
||||
@@ -124016,7 +124023,8 @@
|
||||
"size": 1814032
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 318896
|
||||
},
|
||||
{
|
||||
"ref": "yaps2-resources",
|
||||
@@ -124476,7 +124484,8 @@
|
||||
"size": 637
|
||||
}
|
||||
},
|
||||
"source_path": "yaps2-57ea93c0ce8306063b31fe6658dab6b492bbedfe/bin/resources"
|
||||
"source_path": "yaps2-57ea93c0ce8306063b31fe6658dab6b492bbedfe/bin/resources",
|
||||
"archive_size": 21181421
|
||||
}
|
||||
],
|
||||
"save_directory": {
|
||||
|
||||
+160
-5
@@ -3363,6 +3363,10 @@
|
||||
"repo_path": "bios/Atari/400-800/ATARIXL.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"atari-400-800",
|
||||
"atari-5200"
|
||||
],
|
||||
"declared_in": [
|
||||
"atari-400-800"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -3385,6 +3389,10 @@
|
||||
"repo_path": "bios/Atari/400-800/ATARIBAS.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"atari-400-800",
|
||||
"atari-5200"
|
||||
],
|
||||
"declared_in": [
|
||||
"atari-400-800"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -3407,6 +3415,10 @@
|
||||
"repo_path": "bios/Atari/400-800/ATARIOSA.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"atari-400-800",
|
||||
"atari-5200"
|
||||
],
|
||||
"declared_in": [
|
||||
"atari-400-800"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -3429,6 +3441,10 @@
|
||||
"repo_path": "bios/Atari/400-800/ATARIOSB.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"atari-400-800",
|
||||
"atari-5200"
|
||||
],
|
||||
"declared_in": [
|
||||
"atari-400-800"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -3451,6 +3467,10 @@
|
||||
"repo_path": "bios/Atari/5200/5200.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"atari-400-800",
|
||||
"atari-5200"
|
||||
],
|
||||
"declared_in": [
|
||||
"atari-5200"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -3686,6 +3706,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Databases/.variants/colecodb.xml.78113162",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"coleco-colecovision"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -4923,6 +4948,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/ARAB1.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -4945,6 +4975,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/ARABIC.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -4967,6 +5002,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/BEERIDE.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -4989,6 +5029,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/FMPAC.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -5011,6 +5056,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/GCVMX80.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -5033,6 +5083,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/HANGUL.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -5055,6 +5110,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_kanjifont.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -5077,6 +5137,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/MICROSOLDISK.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -5099,6 +5164,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/openmsx/yrw801.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -5792,6 +5862,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_kanjibasic.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -6001,6 +6076,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/share/systemroms/fs-5500_disk.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -6023,6 +6103,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/NOVAXIS.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -6045,6 +6130,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/nowindDos1.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -6067,6 +6157,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/nowindDos2.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -6089,6 +6184,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/PAINT.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -6111,6 +6211,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/openmsx/fs-a1wsx_disk.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -6133,6 +6238,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/nms8250_disk.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -6155,6 +6265,11 @@
|
||||
"repo_path": "bios/Other/msx-emu/Machines/Shared Roms/RS232.ROM",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -6188,6 +6303,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/SUNRISEIDE.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -6210,6 +6330,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/SWP.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -6232,6 +6357,11 @@
|
||||
"repo_path": "bios/Microsoft/MSX/Shared Roms/XBASIC2.rom",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"coleco-colecovision",
|
||||
"microsoft-msx",
|
||||
"sc-3000"
|
||||
],
|
||||
"declared_in": [
|
||||
"microsoft-msx"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -6694,6 +6824,11 @@
|
||||
"repo_path": "bios/Nintendo/SNES/Satellaview_BS-X.sfc",
|
||||
"cores": null,
|
||||
"systems": [
|
||||
"nintendo-gb",
|
||||
"nintendo-gbc",
|
||||
"nintendo-satellaview"
|
||||
],
|
||||
"declared_in": [
|
||||
"nintendo-satellaview"
|
||||
],
|
||||
"alternatives": [
|
||||
@@ -6895,6 +7030,9 @@
|
||||
"north-america",
|
||||
"oceania"
|
||||
],
|
||||
"region_groups": [
|
||||
"sega-master-system"
|
||||
],
|
||||
"region_system_groups": [
|
||||
"sega-master-system"
|
||||
],
|
||||
@@ -6924,6 +7062,9 @@
|
||||
"regions": [
|
||||
"japan"
|
||||
],
|
||||
"region_groups": [
|
||||
"sega-master-system"
|
||||
],
|
||||
"region_system_groups": [
|
||||
"sega-master-system"
|
||||
],
|
||||
@@ -6954,6 +7095,9 @@
|
||||
"europe",
|
||||
"oceania"
|
||||
],
|
||||
"region_groups": [
|
||||
"sega-mega-cd"
|
||||
],
|
||||
"region_system_groups": [
|
||||
"sega-mega-cd"
|
||||
],
|
||||
@@ -6985,6 +7129,9 @@
|
||||
"latin-america",
|
||||
"north-america"
|
||||
],
|
||||
"region_groups": [
|
||||
"sega-mega-cd"
|
||||
],
|
||||
"region_system_groups": [
|
||||
"sega-mega-cd"
|
||||
],
|
||||
@@ -7015,6 +7162,9 @@
|
||||
"regions": [
|
||||
"japan"
|
||||
],
|
||||
"region_groups": [
|
||||
"sega-mega-cd"
|
||||
],
|
||||
"region_system_groups": [
|
||||
"sega-mega-cd"
|
||||
],
|
||||
@@ -69309,7 +69459,8 @@
|
||||
"size": 344
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 4465709
|
||||
},
|
||||
{
|
||||
"ref": "dinothawr",
|
||||
@@ -69757,7 +69908,8 @@
|
||||
"size": 105
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 5752250
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-cheats",
|
||||
@@ -83486,7 +83638,8 @@
|
||||
"size": 10054
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 5083767
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-samples",
|
||||
@@ -83719,7 +83872,8 @@
|
||||
"size": 551776
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 53006540
|
||||
},
|
||||
{
|
||||
"ref": "freeintv-overlays",
|
||||
@@ -84091,7 +84245,8 @@
|
||||
"size": 248636
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 21311170
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
+9
-6
@@ -1598,8 +1598,7 @@
|
||||
"reason": "not_found",
|
||||
"cores": [
|
||||
"dosbox-x"
|
||||
],
|
||||
"held": true
|
||||
]
|
||||
},
|
||||
{
|
||||
"dest": "extras.wad",
|
||||
@@ -157933,7 +157932,8 @@
|
||||
"size": 10054
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 5083767
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-samples",
|
||||
@@ -158166,7 +158166,8 @@
|
||||
"size": 551776
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 53006540
|
||||
},
|
||||
{
|
||||
"ref": "lrps2",
|
||||
@@ -158182,7 +158183,8 @@
|
||||
"size": 1814032
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 318896
|
||||
},
|
||||
{
|
||||
"ref": "ppsspp-assets",
|
||||
@@ -158870,7 +158872,8 @@
|
||||
"size": 15005
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 9844388
|
||||
}
|
||||
],
|
||||
"save_directory": {
|
||||
|
||||
+14
-7
@@ -115932,7 +115932,8 @@
|
||||
"size": 344
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 4465709
|
||||
},
|
||||
{
|
||||
"ref": "cemu-gameprofiles",
|
||||
@@ -116888,7 +116889,8 @@
|
||||
"size": 36
|
||||
}
|
||||
},
|
||||
"source_path": "Cemu-3310f3b8b184d64a62b89fd59088c799432badf5/bin/gameProfiles/default"
|
||||
"source_path": "Cemu-3310f3b8b184d64a62b89fd59088c799432badf5/bin/gameProfiles/default",
|
||||
"archive_size": 21360579
|
||||
},
|
||||
{
|
||||
"ref": "dirksimple",
|
||||
@@ -116944,7 +116946,8 @@
|
||||
"size": 12988
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 175384
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-cheats",
|
||||
@@ -130673,7 +130676,8 @@
|
||||
"size": 10054
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 5083767
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-samples",
|
||||
@@ -130906,7 +130910,8 @@
|
||||
"size": 551776
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 53006540
|
||||
},
|
||||
{
|
||||
"ref": "freeintv-overlays",
|
||||
@@ -131278,7 +131283,8 @@
|
||||
"size": 248636
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 21311170
|
||||
},
|
||||
{
|
||||
"ref": "yaps2-resources",
|
||||
@@ -131738,7 +131744,8 @@
|
||||
"size": 637
|
||||
}
|
||||
},
|
||||
"source_path": "yaps2-57ea93c0ce8306063b31fe6658dab6b492bbedfe/bin/resources"
|
||||
"source_path": "yaps2-57ea93c0ce8306063b31fe6658dab6b492bbedfe/bin/resources",
|
||||
"archive_size": 21181421
|
||||
}
|
||||
],
|
||||
"save_directory": {
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{
|
||||
"dest": "roms/ports/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1103111,
|
||||
"sha256": "34fd006cc0456c61b9a621e52f35cee6d3fd55ff14bbc6cea207ddc7bc57d858",
|
||||
"size": 1112438,
|
||||
"sha256": "57e3cb6739a701181908773e603f17d2b10e1b0e2a5cde5cc4d5c97426da5dd4",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -71,8 +71,8 @@
|
||||
"muos": {
|
||||
"archive": {
|
||||
"dest": "ARCHIVE/RetroBIOS.muxapp",
|
||||
"size": 1156738,
|
||||
"sha256": "35c0920008baadca997d0f778e99ce46d702bbe3d3f0d33104d7158422b292aa"
|
||||
"size": 1166065,
|
||||
"sha256": "1c9e5737a0b32c85108f556c2799cf0ebb5ce52cbd2bf501bc24783fa3a04b9a"
|
||||
},
|
||||
"members": [
|
||||
{
|
||||
@@ -92,8 +92,8 @@
|
||||
{
|
||||
"name": "RetroBIOS/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1103111,
|
||||
"sha256": "34fd006cc0456c61b9a621e52f35cee6d3fd55ff14bbc6cea207ddc7bc57d858",
|
||||
"size": 1112438,
|
||||
"sha256": "57e3cb6739a701181908773e603f17d2b10e1b0e2a5cde5cc4d5c97426da5dd4",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -166,8 +166,8 @@
|
||||
{
|
||||
"dest": ".config/modules/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1103111,
|
||||
"sha256": "34fd006cc0456c61b9a621e52f35cee6d3fd55ff14bbc6cea207ddc7bc57d858",
|
||||
"size": 1112438,
|
||||
"sha256": "57e3cb6739a701181908773e603f17d2b10e1b0e2a5cde5cc4d5c97426da5dd4",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"platform": "myminui",
|
||||
"display_name": "MyMinUI",
|
||||
"version": "2026-08-09",
|
||||
"generated": "2026-11-05T18:33:47Z",
|
||||
"generated": "2026-11-05T23:59:37Z",
|
||||
"base_destination": "",
|
||||
"detect": [
|
||||
{
|
||||
@@ -18957,7 +18957,8 @@
|
||||
"size": 10054
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 5083767
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-cheats",
|
||||
@@ -32686,7 +32687,8 @@
|
||||
"size": 10054
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 5083767
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-samples",
|
||||
@@ -32919,7 +32921,8 @@
|
||||
"size": 551776
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 53006540
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-samples",
|
||||
@@ -33152,7 +33155,8 @@
|
||||
"size": 551776
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 53006540
|
||||
}
|
||||
]
|
||||
}
|
||||
+7
-4
@@ -6,7 +6,7 @@
|
||||
"platform": "nextui",
|
||||
"display_name": "NextUI",
|
||||
"version": "2026-10-20",
|
||||
"generated": "2026-11-05T18:33:47Z",
|
||||
"generated": "2026-11-05T23:59:37Z",
|
||||
"base_destination": "",
|
||||
"detect": [
|
||||
{
|
||||
@@ -5635,7 +5635,8 @@
|
||||
"size": 344
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 4465709
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-cheats",
|
||||
@@ -19364,7 +19365,8 @@
|
||||
"size": 10054
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 5083767
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-samples",
|
||||
@@ -19597,7 +19599,8 @@
|
||||
"size": 551776
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 53006540
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -62833,7 +62833,8 @@
|
||||
"size": 10054
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 5083767
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-samples",
|
||||
@@ -63066,7 +63067,8 @@
|
||||
"size": 551776
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 53006540
|
||||
}
|
||||
],
|
||||
"save_directory": {
|
||||
|
||||
+32
-10
@@ -21,12 +21,24 @@
|
||||
"config": "$HOME/snap/retroarch/current/.config/retroarch/retroarch.cfg",
|
||||
"parse_key": "system_directory"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"method": "config_file",
|
||||
"config": "$XDG_CONFIG_HOME/retroarch/retroarch.cfg",
|
||||
"parse_key": "system_directory"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"method": "config_file",
|
||||
"config": "$HOME/.config/retroarch/retroarch.cfg",
|
||||
"parse_key": "system_directory"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"method": "config_file",
|
||||
"config": "$HOME/.retroarch.cfg",
|
||||
"parse_key": "system_directory"
|
||||
},
|
||||
{
|
||||
"os": "darwin",
|
||||
"method": "config_file",
|
||||
@@ -116911,7 +116923,8 @@
|
||||
"size": 344
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 4465709
|
||||
},
|
||||
{
|
||||
"ref": "cemu-gameprofiles",
|
||||
@@ -117867,7 +117880,8 @@
|
||||
"size": 36
|
||||
}
|
||||
},
|
||||
"source_path": "Cemu-3310f3b8b184d64a62b89fd59088c799432badf5/bin/gameProfiles/default"
|
||||
"source_path": "Cemu-3310f3b8b184d64a62b89fd59088c799432badf5/bin/gameProfiles/default",
|
||||
"archive_size": 21360579
|
||||
},
|
||||
{
|
||||
"ref": "dinothawr",
|
||||
@@ -118315,7 +118329,8 @@
|
||||
"size": 105
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 5752250
|
||||
},
|
||||
{
|
||||
"ref": "dirksimple",
|
||||
@@ -118371,7 +118386,8 @@
|
||||
"size": 12988
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 175384
|
||||
},
|
||||
{
|
||||
"ref": "dolphin-sys",
|
||||
@@ -127042,7 +127058,8 @@
|
||||
"strip_components": 2,
|
||||
"exclude": [
|
||||
"Themes"
|
||||
]
|
||||
],
|
||||
"archive_size": 3197914
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-cheats",
|
||||
@@ -140771,7 +140788,8 @@
|
||||
"size": 10054
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 5083767
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-samples",
|
||||
@@ -141004,7 +141022,8 @@
|
||||
"size": 551776
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 53006540
|
||||
},
|
||||
{
|
||||
"ref": "freeintv-overlays",
|
||||
@@ -141376,7 +141395,8 @@
|
||||
"size": 248636
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 21311170
|
||||
},
|
||||
{
|
||||
"ref": "ppsspp-assets",
|
||||
@@ -142064,7 +142084,8 @@
|
||||
"size": 15005
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 9844388
|
||||
},
|
||||
{
|
||||
"ref": "yaps2-resources",
|
||||
@@ -142524,7 +142545,8 @@
|
||||
"size": 637
|
||||
}
|
||||
},
|
||||
"source_path": "yaps2-57ea93c0ce8306063b31fe6658dab6b492bbedfe/bin/resources"
|
||||
"source_path": "yaps2-57ea93c0ce8306063b31fe6658dab6b492bbedfe/bin/resources",
|
||||
"archive_size": 21181421
|
||||
}
|
||||
],
|
||||
"save_directory": {
|
||||
|
||||
@@ -89075,7 +89075,8 @@
|
||||
"size": 10054
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 5083767
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-samples",
|
||||
@@ -89308,7 +89309,8 @@
|
||||
"size": 551776
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 53006540
|
||||
},
|
||||
{
|
||||
"ref": "freeintv-overlays",
|
||||
@@ -89680,7 +89682,8 @@
|
||||
"size": 248636
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 21311170
|
||||
}
|
||||
],
|
||||
"save_directory": {
|
||||
|
||||
+1671
-7
File diff suppressed because it is too large.
Load diff
+12
-6
@@ -124071,7 +124071,8 @@
|
||||
"size": 36
|
||||
}
|
||||
},
|
||||
"source_path": "Cemu-3310f3b8b184d64a62b89fd59088c799432badf5/bin/gameProfiles/default"
|
||||
"source_path": "Cemu-3310f3b8b184d64a62b89fd59088c799432badf5/bin/gameProfiles/default",
|
||||
"archive_size": 21360579
|
||||
},
|
||||
{
|
||||
"ref": "dolphin-sys",
|
||||
@@ -132742,7 +132743,8 @@
|
||||
"strip_components": 2,
|
||||
"exclude": [
|
||||
"Themes"
|
||||
]
|
||||
],
|
||||
"archive_size": 3197914
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-cheats",
|
||||
@@ -146471,7 +146473,8 @@
|
||||
"size": 10054
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 5083767
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-samples",
|
||||
@@ -146704,7 +146707,8 @@
|
||||
"size": 551776
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 53006540
|
||||
},
|
||||
{
|
||||
"ref": "freeintv-overlays",
|
||||
@@ -147076,7 +147080,8 @@
|
||||
"size": 248636
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 21311170
|
||||
},
|
||||
{
|
||||
"ref": "yaps2-resources",
|
||||
@@ -147536,7 +147541,8 @@
|
||||
"size": 637
|
||||
}
|
||||
},
|
||||
"source_path": "yaps2-57ea93c0ce8306063b31fe6658dab6b492bbedfe/bin/resources"
|
||||
"source_path": "yaps2-57ea93c0ce8306063b31fe6658dab6b492bbedfe/bin/resources",
|
||||
"archive_size": 21181421
|
||||
}
|
||||
]
|
||||
}
|
||||
+20
-10
@@ -86538,7 +86538,8 @@
|
||||
"size": 344
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 4465709
|
||||
},
|
||||
{
|
||||
"ref": "dinothawr",
|
||||
@@ -86986,7 +86987,8 @@
|
||||
"size": 105
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 5752250
|
||||
},
|
||||
{
|
||||
"ref": "dirksimple",
|
||||
@@ -87042,7 +87044,8 @@
|
||||
"size": 12988
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 175384
|
||||
},
|
||||
{
|
||||
"ref": "dolphin-sys",
|
||||
@@ -95719,7 +95722,8 @@
|
||||
"strip_components": 2,
|
||||
"exclude": [
|
||||
"Themes"
|
||||
]
|
||||
],
|
||||
"archive_size": 3197914
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-cheats",
|
||||
@@ -109448,7 +109452,8 @@
|
||||
"size": 10054
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 5083767
|
||||
},
|
||||
{
|
||||
"ref": "fbneo-samples",
|
||||
@@ -109681,7 +109686,8 @@
|
||||
"size": 551776
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 53006540
|
||||
},
|
||||
{
|
||||
"ref": "freeintv-overlays",
|
||||
@@ -110053,7 +110059,8 @@
|
||||
"size": 248636
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 21311170
|
||||
},
|
||||
{
|
||||
"ref": "lrps2",
|
||||
@@ -110069,7 +110076,8 @@
|
||||
"size": 1814032
|
||||
}
|
||||
},
|
||||
"strip_components": 0
|
||||
"strip_components": 0,
|
||||
"archive_size": 318896
|
||||
},
|
||||
{
|
||||
"ref": "ppsspp-assets",
|
||||
@@ -110757,7 +110765,8 @@
|
||||
"size": 15005
|
||||
}
|
||||
},
|
||||
"strip_components": 1
|
||||
"strip_components": 1,
|
||||
"archive_size": 9844388
|
||||
},
|
||||
{
|
||||
"ref": "yaps2-resources",
|
||||
@@ -111217,7 +111226,8 @@
|
||||
"size": 637
|
||||
}
|
||||
},
|
||||
"source_path": "yaps2-57ea93c0ce8306063b31fe6658dab6b492bbedfe/bin/resources"
|
||||
"source_path": "yaps2-57ea93c0ce8306063b31fe6658dab6b492bbedfe/bin/resources",
|
||||
"archive_size": 21181421
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,797 @@
|
||||
{
|
||||
"rg351mp": [
|
||||
"81",
|
||||
"AMIBERRY",
|
||||
"AdvanceMame",
|
||||
"DoubleCherryGB",
|
||||
"HATARISA",
|
||||
"OPENBOR",
|
||||
"PPSSPPSDL",
|
||||
"SCUMMVMSA",
|
||||
"a5200",
|
||||
"arduous",
|
||||
"atari800",
|
||||
"beetle_gba",
|
||||
"beetle_lynx",
|
||||
"beetle_ngp",
|
||||
"beetle_pce",
|
||||
"beetle_pce_fast",
|
||||
"beetle_pcfx",
|
||||
"beetle_supafaust",
|
||||
"beetle_supergrafx",
|
||||
"beetle_vb",
|
||||
"beetle_wswan",
|
||||
"bluemsx",
|
||||
"cap32",
|
||||
"crocods",
|
||||
"dosbox_core",
|
||||
"dosbox_pure",
|
||||
"drastic",
|
||||
"duckstation",
|
||||
"easyrpg",
|
||||
"ecwolf",
|
||||
"emuscv",
|
||||
"ep128emu_core",
|
||||
"fake08",
|
||||
"fbalpha2012",
|
||||
"fbalpha2019",
|
||||
"fbneo",
|
||||
"fceumm",
|
||||
"flycast",
|
||||
"flycast2021",
|
||||
"flycast2021le",
|
||||
"fmsx",
|
||||
"freechaf",
|
||||
"freeintv",
|
||||
"freej2me",
|
||||
"freej2me-plus",
|
||||
"fuse",
|
||||
"gambatte",
|
||||
"gearboy",
|
||||
"gearcoleco",
|
||||
"gearsystem",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_wide",
|
||||
"gpsp",
|
||||
"gw",
|
||||
"gzdoom",
|
||||
"handy",
|
||||
"hatari",
|
||||
"hypseus_singe",
|
||||
"jaxe",
|
||||
"lowresnx",
|
||||
"lzdoom",
|
||||
"mame",
|
||||
"mame2000",
|
||||
"mame2003_plus",
|
||||
"mame2010",
|
||||
"mame2015",
|
||||
"mame2016",
|
||||
"mgba",
|
||||
"mojozork",
|
||||
"mupen64plus_next",
|
||||
"mupen64plussa",
|
||||
"nekop2",
|
||||
"neocd",
|
||||
"nestopia",
|
||||
"np2kai",
|
||||
"o2em",
|
||||
"opera",
|
||||
"parallel_n64",
|
||||
"pcsx_rearmed",
|
||||
"pico8",
|
||||
"picodrive",
|
||||
"piemu",
|
||||
"pokemini",
|
||||
"potator",
|
||||
"ppsspp",
|
||||
"prboom",
|
||||
"prosystem",
|
||||
"puae",
|
||||
"puae2021",
|
||||
"px68k",
|
||||
"quasi88",
|
||||
"quicknes",
|
||||
"race",
|
||||
"raze",
|
||||
"same_cdi",
|
||||
"sameboy",
|
||||
"sameduck",
|
||||
"scummvm",
|
||||
"smsplus",
|
||||
"snes9x",
|
||||
"snes9x2002",
|
||||
"snes9x2005_plus",
|
||||
"snes9x2010",
|
||||
"solarus",
|
||||
"stella",
|
||||
"stella2014",
|
||||
"swanstation",
|
||||
"tgbdual",
|
||||
"theodore",
|
||||
"tic80",
|
||||
"uae4arm",
|
||||
"uw8",
|
||||
"uzem",
|
||||
"vba_next",
|
||||
"vbam",
|
||||
"vecx",
|
||||
"vice_x128",
|
||||
"vice_x64",
|
||||
"vice_xplus4",
|
||||
"vice_xvic",
|
||||
"vircon32",
|
||||
"virtualjaguar",
|
||||
"wasm4",
|
||||
"x1",
|
||||
"yabasanshiroSA"
|
||||
],
|
||||
"rg351p": [
|
||||
"81",
|
||||
"AMIBERRY",
|
||||
"AdvanceMame",
|
||||
"DoubleCherryGB",
|
||||
"HATARISA",
|
||||
"OPENBOR",
|
||||
"PPSSPPSDL",
|
||||
"SCUMMVMSA",
|
||||
"a5200",
|
||||
"arduous",
|
||||
"atari800",
|
||||
"beetle_gba",
|
||||
"beetle_lynx",
|
||||
"beetle_ngp",
|
||||
"beetle_pce",
|
||||
"beetle_pce_fast",
|
||||
"beetle_pcfx",
|
||||
"beetle_supafaust",
|
||||
"beetle_supergrafx",
|
||||
"beetle_vb",
|
||||
"beetle_wswan",
|
||||
"bluemsx",
|
||||
"cap32",
|
||||
"crocods",
|
||||
"dosbox_core",
|
||||
"dosbox_pure",
|
||||
"drastic",
|
||||
"duckstation",
|
||||
"easyrpg",
|
||||
"ecwolf",
|
||||
"emuscv",
|
||||
"ep128emu_core",
|
||||
"fake08",
|
||||
"fbalpha2012",
|
||||
"fbalpha2019",
|
||||
"fbneo",
|
||||
"fceumm",
|
||||
"flycast",
|
||||
"flycast2021",
|
||||
"flycast2021le",
|
||||
"fmsx",
|
||||
"freechaf",
|
||||
"freeintv",
|
||||
"freej2me",
|
||||
"freej2me-plus",
|
||||
"fuse",
|
||||
"gambatte",
|
||||
"gearboy",
|
||||
"gearcoleco",
|
||||
"gearsystem",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_wide",
|
||||
"gpsp",
|
||||
"gw",
|
||||
"gzdoom",
|
||||
"handy",
|
||||
"hatari",
|
||||
"hypseus_singe",
|
||||
"jaxe",
|
||||
"lowresnx",
|
||||
"lzdoom",
|
||||
"mame",
|
||||
"mame2000",
|
||||
"mame2003_plus",
|
||||
"mame2010",
|
||||
"mame2015",
|
||||
"mame2016",
|
||||
"mgba",
|
||||
"mojozork",
|
||||
"mupen64plus_next",
|
||||
"mupen64plussa",
|
||||
"nekop2",
|
||||
"neocd",
|
||||
"nestopia",
|
||||
"np2kai",
|
||||
"o2em",
|
||||
"opera",
|
||||
"parallel_n64",
|
||||
"pcsx_rearmed",
|
||||
"pico8",
|
||||
"picodrive",
|
||||
"piemu",
|
||||
"pokemini",
|
||||
"potator",
|
||||
"ppsspp",
|
||||
"prboom",
|
||||
"prosystem",
|
||||
"puae",
|
||||
"puae2021",
|
||||
"px68k",
|
||||
"quasi88",
|
||||
"quicknes",
|
||||
"race",
|
||||
"raze",
|
||||
"same_cdi",
|
||||
"sameboy",
|
||||
"sameduck",
|
||||
"scummvm",
|
||||
"smsplus",
|
||||
"snes9x",
|
||||
"snes9x2002",
|
||||
"snes9x2005_plus",
|
||||
"snes9x2010",
|
||||
"solarus",
|
||||
"stella",
|
||||
"stella2014",
|
||||
"swanstation",
|
||||
"tgbdual",
|
||||
"theodore",
|
||||
"tic80",
|
||||
"uae4arm",
|
||||
"uw8",
|
||||
"uzem",
|
||||
"vba_next",
|
||||
"vbam",
|
||||
"vecx",
|
||||
"vice_x128",
|
||||
"vice_x64",
|
||||
"vice_xplus4",
|
||||
"vice_xvic",
|
||||
"vircon32",
|
||||
"virtualjaguar",
|
||||
"wasm4",
|
||||
"x1",
|
||||
"yabasanshiroSA"
|
||||
],
|
||||
"rg351v": [
|
||||
"81",
|
||||
"AMIBERRY",
|
||||
"AdvanceMame",
|
||||
"DoubleCherryGB",
|
||||
"HATARISA",
|
||||
"OPENBOR",
|
||||
"PPSSPPSDL",
|
||||
"SCUMMVMSA",
|
||||
"a5200",
|
||||
"arduous",
|
||||
"atari800",
|
||||
"beetle_gba",
|
||||
"beetle_lynx",
|
||||
"beetle_ngp",
|
||||
"beetle_pce",
|
||||
"beetle_pce_fast",
|
||||
"beetle_pcfx",
|
||||
"beetle_supafaust",
|
||||
"beetle_supergrafx",
|
||||
"beetle_vb",
|
||||
"beetle_wswan",
|
||||
"bluemsx",
|
||||
"cap32",
|
||||
"crocods",
|
||||
"dosbox_core",
|
||||
"dosbox_pure",
|
||||
"drastic",
|
||||
"duckstation",
|
||||
"easyrpg",
|
||||
"ecwolf",
|
||||
"emuscv",
|
||||
"ep128emu_core",
|
||||
"fake08",
|
||||
"fbalpha2012",
|
||||
"fbalpha2019",
|
||||
"fbneo",
|
||||
"fceumm",
|
||||
"flycast",
|
||||
"flycast2021",
|
||||
"flycast2021le",
|
||||
"fmsx",
|
||||
"freechaf",
|
||||
"freeintv",
|
||||
"freej2me",
|
||||
"freej2me-plus",
|
||||
"fuse",
|
||||
"gambatte",
|
||||
"gearboy",
|
||||
"gearcoleco",
|
||||
"gearsystem",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_wide",
|
||||
"gpsp",
|
||||
"gw",
|
||||
"gzdoom",
|
||||
"handy",
|
||||
"hatari",
|
||||
"hypseus_singe",
|
||||
"jaxe",
|
||||
"lowresnx",
|
||||
"lzdoom",
|
||||
"mame",
|
||||
"mame2000",
|
||||
"mame2003_plus",
|
||||
"mame2010",
|
||||
"mame2015",
|
||||
"mame2016",
|
||||
"mgba",
|
||||
"mojozork",
|
||||
"mupen64plus_next",
|
||||
"mupen64plussa",
|
||||
"nekop2",
|
||||
"neocd",
|
||||
"nestopia",
|
||||
"np2kai",
|
||||
"o2em",
|
||||
"opera",
|
||||
"parallel_n64",
|
||||
"pcsx_rearmed",
|
||||
"pico8",
|
||||
"picodrive",
|
||||
"piemu",
|
||||
"pokemini",
|
||||
"potator",
|
||||
"ppsspp",
|
||||
"prboom",
|
||||
"prosystem",
|
||||
"puae",
|
||||
"puae2021",
|
||||
"px68k",
|
||||
"quasi88",
|
||||
"quicknes",
|
||||
"race",
|
||||
"raze",
|
||||
"same_cdi",
|
||||
"sameboy",
|
||||
"sameduck",
|
||||
"scummvm",
|
||||
"smsplus",
|
||||
"snes9x",
|
||||
"snes9x2002",
|
||||
"snes9x2005_plus",
|
||||
"snes9x2010",
|
||||
"solarus",
|
||||
"stella",
|
||||
"stella2014",
|
||||
"swanstation",
|
||||
"tgbdual",
|
||||
"theodore",
|
||||
"tic80",
|
||||
"uae4arm",
|
||||
"uw8",
|
||||
"uzem",
|
||||
"vba_next",
|
||||
"vbam",
|
||||
"vecx",
|
||||
"vice_x128",
|
||||
"vice_x64",
|
||||
"vice_xplus4",
|
||||
"vice_xvic",
|
||||
"vircon32",
|
||||
"virtualjaguar",
|
||||
"wasm4",
|
||||
"x1",
|
||||
"yabasanshiroSA"
|
||||
],
|
||||
"rg353": [
|
||||
"81",
|
||||
"AMIBERRY",
|
||||
"AdvanceMame",
|
||||
"DoubleCherryGB",
|
||||
"HATARISA",
|
||||
"OPENBOR",
|
||||
"PPSSPPSDL",
|
||||
"SCUMMVMSA",
|
||||
"a5200",
|
||||
"arduous",
|
||||
"atari800",
|
||||
"beetle_gba",
|
||||
"beetle_lynx",
|
||||
"beetle_ngp",
|
||||
"beetle_pce",
|
||||
"beetle_pce_fast",
|
||||
"beetle_pcfx",
|
||||
"beetle_snes",
|
||||
"beetle_supafaust",
|
||||
"beetle_supergrafx",
|
||||
"beetle_vb",
|
||||
"beetle_wswan",
|
||||
"bluemsx",
|
||||
"bsnes",
|
||||
"bsnes2014_balanced",
|
||||
"bsnes2014_performance",
|
||||
"bsnes_mercury_balanced",
|
||||
"bsnes_mercury_performance",
|
||||
"cap32",
|
||||
"crocods",
|
||||
"dosbox_core",
|
||||
"dosbox_pure",
|
||||
"drastic",
|
||||
"duckstation",
|
||||
"easyrpg",
|
||||
"ecwolf",
|
||||
"emuscv",
|
||||
"ep128emu_core",
|
||||
"fake08",
|
||||
"fbalpha2012",
|
||||
"fbalpha2019",
|
||||
"fbneo",
|
||||
"fceumm",
|
||||
"flycast",
|
||||
"flycast2021",
|
||||
"flycast2021le",
|
||||
"fmsx",
|
||||
"freechaf",
|
||||
"freeintv",
|
||||
"freej2me",
|
||||
"freej2me-plus",
|
||||
"fuse",
|
||||
"gambatte",
|
||||
"gearboy",
|
||||
"gearcoleco",
|
||||
"gearsystem",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_wide",
|
||||
"geolith",
|
||||
"gpsp",
|
||||
"gw",
|
||||
"gzdoom",
|
||||
"handy",
|
||||
"hatari",
|
||||
"hypseus_singe",
|
||||
"jaxe",
|
||||
"lowresnx",
|
||||
"lzdoom",
|
||||
"mame",
|
||||
"mame2000",
|
||||
"mame2003_plus",
|
||||
"mame2010",
|
||||
"mame2015",
|
||||
"mame2016",
|
||||
"melonds",
|
||||
"melondsds",
|
||||
"mesen",
|
||||
"mesen-s",
|
||||
"mgba",
|
||||
"mojozork",
|
||||
"mupen64plus_next",
|
||||
"mupen64plussa",
|
||||
"nekop2",
|
||||
"neocd",
|
||||
"nestopia",
|
||||
"np2kai",
|
||||
"o2em",
|
||||
"opera",
|
||||
"parallel_n64",
|
||||
"pcsx_rearmed",
|
||||
"pico8",
|
||||
"picodrive",
|
||||
"piemu",
|
||||
"pokemini",
|
||||
"potator",
|
||||
"ppsspp",
|
||||
"prboom",
|
||||
"prosystem",
|
||||
"puae",
|
||||
"puae2021",
|
||||
"px68k",
|
||||
"quasi88",
|
||||
"quicknes",
|
||||
"race",
|
||||
"raze",
|
||||
"same_cdi",
|
||||
"sameboy",
|
||||
"sameduck",
|
||||
"scummvm",
|
||||
"smsplus",
|
||||
"snes9x",
|
||||
"snes9x2002",
|
||||
"snes9x2005_plus",
|
||||
"snes9x2010",
|
||||
"solarus",
|
||||
"stella",
|
||||
"stella2014",
|
||||
"swanstation",
|
||||
"tgbdual",
|
||||
"theodore",
|
||||
"tic80",
|
||||
"uae4arm",
|
||||
"uw8",
|
||||
"uzem",
|
||||
"vba_next",
|
||||
"vbam",
|
||||
"vecx",
|
||||
"vice_x128",
|
||||
"vice_x64",
|
||||
"vice_xplus4",
|
||||
"vice_xvic",
|
||||
"vircon32",
|
||||
"virtualjaguar",
|
||||
"wasm4",
|
||||
"x1",
|
||||
"yabasanshiroSA"
|
||||
],
|
||||
"rg503": [
|
||||
"81",
|
||||
"AMIBERRY",
|
||||
"AdvanceMame",
|
||||
"DoubleCherryGB",
|
||||
"HATARISA",
|
||||
"OPENBOR",
|
||||
"PPSSPPSDL",
|
||||
"SCUMMVMSA",
|
||||
"a5200",
|
||||
"arduous",
|
||||
"atari800",
|
||||
"beetle_gba",
|
||||
"beetle_lynx",
|
||||
"beetle_ngp",
|
||||
"beetle_pce",
|
||||
"beetle_pce_fast",
|
||||
"beetle_pcfx",
|
||||
"beetle_snes",
|
||||
"beetle_supafaust",
|
||||
"beetle_supergrafx",
|
||||
"beetle_vb",
|
||||
"beetle_wswan",
|
||||
"bluemsx",
|
||||
"bsnes",
|
||||
"bsnes2014_balanced",
|
||||
"bsnes2014_performance",
|
||||
"bsnes_mercury_balanced",
|
||||
"bsnes_mercury_performance",
|
||||
"cap32",
|
||||
"crocods",
|
||||
"dosbox_core",
|
||||
"dosbox_pure",
|
||||
"drastic",
|
||||
"duckstation",
|
||||
"easyrpg",
|
||||
"ecwolf",
|
||||
"emuscv",
|
||||
"ep128emu_core",
|
||||
"fake08",
|
||||
"fbalpha2012",
|
||||
"fbalpha2019",
|
||||
"fbneo",
|
||||
"fceumm",
|
||||
"flycast",
|
||||
"flycast2021",
|
||||
"flycast2021le",
|
||||
"fmsx",
|
||||
"freechaf",
|
||||
"freeintv",
|
||||
"freej2me",
|
||||
"freej2me-plus",
|
||||
"fuse",
|
||||
"gambatte",
|
||||
"gearboy",
|
||||
"gearcoleco",
|
||||
"gearsystem",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_wide",
|
||||
"geolith",
|
||||
"gpsp",
|
||||
"gw",
|
||||
"gzdoom",
|
||||
"handy",
|
||||
"hatari",
|
||||
"hypseus_singe",
|
||||
"jaxe",
|
||||
"lowresnx",
|
||||
"lzdoom",
|
||||
"mame",
|
||||
"mame2000",
|
||||
"mame2003_plus",
|
||||
"mame2010",
|
||||
"mame2015",
|
||||
"mame2016",
|
||||
"melonds",
|
||||
"melondsds",
|
||||
"mesen",
|
||||
"mesen-s",
|
||||
"mgba",
|
||||
"mojozork",
|
||||
"mupen64plus_next",
|
||||
"mupen64plussa",
|
||||
"nekop2",
|
||||
"neocd",
|
||||
"nestopia",
|
||||
"np2kai",
|
||||
"o2em",
|
||||
"opera",
|
||||
"parallel_n64",
|
||||
"pcsx_rearmed",
|
||||
"pico8",
|
||||
"picodrive",
|
||||
"piemu",
|
||||
"pokemini",
|
||||
"potator",
|
||||
"ppsspp",
|
||||
"prboom",
|
||||
"prosystem",
|
||||
"puae",
|
||||
"puae2021",
|
||||
"px68k",
|
||||
"quasi88",
|
||||
"quicknes",
|
||||
"race",
|
||||
"raze",
|
||||
"same_cdi",
|
||||
"sameboy",
|
||||
"sameduck",
|
||||
"scummvm",
|
||||
"smsplus",
|
||||
"snes9x",
|
||||
"snes9x2002",
|
||||
"snes9x2005_plus",
|
||||
"snes9x2010",
|
||||
"solarus",
|
||||
"stella",
|
||||
"stella2014",
|
||||
"swanstation",
|
||||
"tgbdual",
|
||||
"theodore",
|
||||
"tic80",
|
||||
"uae4arm",
|
||||
"uw8",
|
||||
"uzem",
|
||||
"vba_next",
|
||||
"vbam",
|
||||
"vecx",
|
||||
"vice_x128",
|
||||
"vice_x64",
|
||||
"vice_xplus4",
|
||||
"vice_xvic",
|
||||
"vircon32",
|
||||
"virtualjaguar",
|
||||
"wasm4",
|
||||
"x1",
|
||||
"yabasanshiroSA"
|
||||
],
|
||||
"rg552": [
|
||||
"81",
|
||||
"AMIBERRY",
|
||||
"AdvanceMame",
|
||||
"DoubleCherryGB",
|
||||
"HATARISA",
|
||||
"OPENBOR",
|
||||
"PPSSPPSDL",
|
||||
"SCUMMVMSA",
|
||||
"a5200",
|
||||
"arduous",
|
||||
"atari800",
|
||||
"beetle_gba",
|
||||
"beetle_lynx",
|
||||
"beetle_ngp",
|
||||
"beetle_pce",
|
||||
"beetle_pce_fast",
|
||||
"beetle_pcfx",
|
||||
"beetle_snes",
|
||||
"beetle_supafaust",
|
||||
"beetle_supergrafx",
|
||||
"beetle_vb",
|
||||
"beetle_wswan",
|
||||
"bluemsx",
|
||||
"bsnes",
|
||||
"bsnes2014_balanced",
|
||||
"bsnes2014_performance",
|
||||
"bsnes_mercury_balanced",
|
||||
"bsnes_mercury_performance",
|
||||
"cap32",
|
||||
"crocods",
|
||||
"dosbox_core",
|
||||
"dosbox_pure",
|
||||
"drastic",
|
||||
"duckstation",
|
||||
"easyrpg",
|
||||
"ecwolf",
|
||||
"emuscv",
|
||||
"ep128emu_core",
|
||||
"fake08",
|
||||
"fbalpha2012",
|
||||
"fbalpha2019",
|
||||
"fbneo",
|
||||
"fceumm",
|
||||
"flycast",
|
||||
"flycast2021",
|
||||
"flycast2021le",
|
||||
"fmsx",
|
||||
"freechaf",
|
||||
"freeintv",
|
||||
"freej2me",
|
||||
"freej2me-plus",
|
||||
"fuse",
|
||||
"gambatte",
|
||||
"gearboy",
|
||||
"gearcoleco",
|
||||
"gearsystem",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_wide",
|
||||
"geolith",
|
||||
"gpsp",
|
||||
"gw",
|
||||
"gzdoom",
|
||||
"handy",
|
||||
"hatari",
|
||||
"hypseus_singe",
|
||||
"jaxe",
|
||||
"lowresnx",
|
||||
"lzdoom",
|
||||
"mame",
|
||||
"mame2000",
|
||||
"mame2003_plus",
|
||||
"mame2010",
|
||||
"mame2015",
|
||||
"mame2016",
|
||||
"melonds",
|
||||
"melondsds",
|
||||
"mesen",
|
||||
"mesen-s",
|
||||
"mgba",
|
||||
"mojozork",
|
||||
"mupen64plus_next",
|
||||
"mupen64plussa",
|
||||
"nekop2",
|
||||
"neocd",
|
||||
"nestopia",
|
||||
"np2kai",
|
||||
"o2em",
|
||||
"opera",
|
||||
"parallel_n64",
|
||||
"pcsx_rearmed",
|
||||
"pico8",
|
||||
"picodrive",
|
||||
"piemu",
|
||||
"pokemini",
|
||||
"potator",
|
||||
"ppsspp",
|
||||
"prboom",
|
||||
"prosystem",
|
||||
"puae",
|
||||
"puae2021",
|
||||
"px68k",
|
||||
"quasi88",
|
||||
"quicknes",
|
||||
"race",
|
||||
"raze",
|
||||
"same_cdi",
|
||||
"sameboy",
|
||||
"sameduck",
|
||||
"scummvm",
|
||||
"smsplus",
|
||||
"snes9x",
|
||||
"snes9x2002",
|
||||
"snes9x2005_plus",
|
||||
"snes9x2010",
|
||||
"solarus",
|
||||
"stella",
|
||||
"stella2014",
|
||||
"swanstation",
|
||||
"tgbdual",
|
||||
"theodore",
|
||||
"tic80",
|
||||
"uae4arm",
|
||||
"uw8",
|
||||
"uzem",
|
||||
"vba_next",
|
||||
"vbam",
|
||||
"vecx",
|
||||
"vice_x128",
|
||||
"vice_x64",
|
||||
"vice_xplus4",
|
||||
"vice_xvic",
|
||||
"vircon32",
|
||||
"virtualjaguar",
|
||||
"wasm4",
|
||||
"x1",
|
||||
"yabasanshiroSA"
|
||||
]
|
||||
}
|
||||
@@ -83,6 +83,7 @@
|
||||
"galaksija",
|
||||
"gam4980",
|
||||
"gambatte",
|
||||
"gametank",
|
||||
"gearboy",
|
||||
"gearcoleco",
|
||||
"geargrafx",
|
||||
@@ -346,6 +347,7 @@
|
||||
"galaksija",
|
||||
"gam4980",
|
||||
"gambatte",
|
||||
"gametank",
|
||||
"gearboy",
|
||||
"gearcoleco",
|
||||
"geargrafx",
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
{
|
||||
"amlogic-ng": [
|
||||
"2048",
|
||||
"81",
|
||||
"AMIBERRY",
|
||||
"AdvanceMame",
|
||||
"DOSBOXSDL2",
|
||||
"Duckstation",
|
||||
"ECWolf",
|
||||
"FBNEOSA",
|
||||
"HATARISA",
|
||||
"HYPSEUS",
|
||||
"OPENBOR",
|
||||
"PPSSPPSDL",
|
||||
"Pico-8",
|
||||
"SCUMMVMSA",
|
||||
"STELLASA",
|
||||
"Solarus",
|
||||
"a5200",
|
||||
"atari800",
|
||||
"bgdi",
|
||||
"bk",
|
||||
"bluemsx",
|
||||
"cannonball",
|
||||
"cap32",
|
||||
"daphne",
|
||||
"dinothawr",
|
||||
"dolphin",
|
||||
"dosbox_pure",
|
||||
"dosbox_svn",
|
||||
"easyrpg",
|
||||
"fake08",
|
||||
"fbalpha2012",
|
||||
"fbneo",
|
||||
"fceumm",
|
||||
"fceumm_mod",
|
||||
"flycast",
|
||||
"flycast_32b",
|
||||
"flycastsa",
|
||||
"fmsx",
|
||||
"freeintv",
|
||||
"freej2me",
|
||||
"fuse",
|
||||
"gambatte",
|
||||
"gearboy",
|
||||
"gearcoleco",
|
||||
"geargrafx",
|
||||
"gearlynx",
|
||||
"gearsystem",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_cart_special",
|
||||
"genesis_plus_gx_wide",
|
||||
"geolith",
|
||||
"gpsp",
|
||||
"gw",
|
||||
"handy",
|
||||
"hatari",
|
||||
"ikemen",
|
||||
"jzintv",
|
||||
"mame",
|
||||
"mame2003_plus",
|
||||
"mame2010",
|
||||
"mednafen_lynx",
|
||||
"mednafen_ngp",
|
||||
"mednafen_pce_fast",
|
||||
"mednafen_pcfx",
|
||||
"mednafen_saturn",
|
||||
"mednafen_supafaust",
|
||||
"mednafen_supergrafx",
|
||||
"mednafen_vb",
|
||||
"mednafen_wswan",
|
||||
"mesen",
|
||||
"mesen-s",
|
||||
"mgba",
|
||||
"mrboom",
|
||||
"mupen64plus_32b",
|
||||
"mupen64plus_next",
|
||||
"mupen64plus_next_alt",
|
||||
"mupen64plussa",
|
||||
"nekop2",
|
||||
"neocd",
|
||||
"nestopia",
|
||||
"np2kai",
|
||||
"nxengine",
|
||||
"o2em",
|
||||
"opera",
|
||||
"parallel_n64_32b",
|
||||
"pcsx_rearmed",
|
||||
"pcsx_rearmed_32b",
|
||||
"picodrive",
|
||||
"pocketcdg",
|
||||
"pokemini",
|
||||
"potator",
|
||||
"prboom",
|
||||
"prosystem",
|
||||
"puae",
|
||||
"puae2021",
|
||||
"px68k",
|
||||
"reminiscence",
|
||||
"same_cdi",
|
||||
"sameboy",
|
||||
"sameduck",
|
||||
"scummvm",
|
||||
"snes9x",
|
||||
"snes9x2002",
|
||||
"snes9x2005_plus",
|
||||
"snes9x2010",
|
||||
"stella",
|
||||
"swanstation",
|
||||
"tgbdual",
|
||||
"tic80",
|
||||
"tyrquake",
|
||||
"uae4arm_32b",
|
||||
"uzem",
|
||||
"vba_next",
|
||||
"vbam",
|
||||
"vecx",
|
||||
"vice_x128",
|
||||
"vice_x64",
|
||||
"vice_x64sc",
|
||||
"vice_xplus4",
|
||||
"vice_xvic",
|
||||
"virtualjaguar",
|
||||
"x1",
|
||||
"xrick",
|
||||
"yabasanshiro"
|
||||
],
|
||||
"gameforce": [
|
||||
"2048",
|
||||
"81",
|
||||
"AMIBERRY",
|
||||
"AdvanceMame",
|
||||
"DOSBOXSDL2",
|
||||
"ECWolf",
|
||||
"FBNEOSA",
|
||||
"HATARISA",
|
||||
"HYPSEUS",
|
||||
"OPENBOR",
|
||||
"PPSSPPSDL",
|
||||
"Pico-8",
|
||||
"SCUMMVMSA",
|
||||
"STELLASA",
|
||||
"Solarus",
|
||||
"a5200",
|
||||
"atari800",
|
||||
"bgdi",
|
||||
"bk",
|
||||
"bluemsx",
|
||||
"cannonball",
|
||||
"cap32",
|
||||
"daphne",
|
||||
"dinothawr",
|
||||
"dosbox_pure",
|
||||
"dosbox_svn",
|
||||
"easyrpg",
|
||||
"fake08",
|
||||
"fbalpha2012",
|
||||
"fbneo",
|
||||
"fceumm",
|
||||
"fceumm_mod",
|
||||
"flycast",
|
||||
"flycast_32b",
|
||||
"flycastsa",
|
||||
"fmsx",
|
||||
"freeintv",
|
||||
"freej2me",
|
||||
"fuse",
|
||||
"gambatte",
|
||||
"gearboy",
|
||||
"gearcoleco",
|
||||
"geargrafx",
|
||||
"gearlynx",
|
||||
"gearsystem",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_cart_special",
|
||||
"genesis_plus_gx_wide",
|
||||
"geolith",
|
||||
"gpsp",
|
||||
"gw",
|
||||
"handy",
|
||||
"hatari",
|
||||
"ikemen",
|
||||
"jzintv",
|
||||
"mame2003_plus",
|
||||
"mame2010",
|
||||
"mednafen_lynx",
|
||||
"mednafen_ngp",
|
||||
"mednafen_pce_fast",
|
||||
"mednafen_pcfx",
|
||||
"mednafen_supafaust",
|
||||
"mednafen_supergrafx",
|
||||
"mednafen_vb",
|
||||
"mednafen_wswan",
|
||||
"mgba",
|
||||
"mrboom",
|
||||
"mupen64plus_32b",
|
||||
"mupen64plus_next",
|
||||
"mupen64plus_next_alt",
|
||||
"mupen64plussa",
|
||||
"nekop2",
|
||||
"neocd",
|
||||
"nestopia",
|
||||
"np2kai",
|
||||
"nxengine",
|
||||
"o2em",
|
||||
"opera",
|
||||
"parallel_n64_32b",
|
||||
"pcsx_rearmed",
|
||||
"pcsx_rearmed_32b",
|
||||
"picodrive",
|
||||
"pocketcdg",
|
||||
"pokemini",
|
||||
"potator",
|
||||
"prboom",
|
||||
"prosystem",
|
||||
"puae",
|
||||
"puae2021",
|
||||
"px68k",
|
||||
"reminiscence",
|
||||
"same_cdi",
|
||||
"sameboy",
|
||||
"sameduck",
|
||||
"scummvm",
|
||||
"snes9x",
|
||||
"snes9x2002",
|
||||
"snes9x2005_plus",
|
||||
"snes9x2010",
|
||||
"stella",
|
||||
"swanstation",
|
||||
"tgbdual",
|
||||
"tic80",
|
||||
"tyrquake",
|
||||
"uae4arm_32b",
|
||||
"uzem",
|
||||
"vba_next",
|
||||
"vbam",
|
||||
"vecx",
|
||||
"vice_x128",
|
||||
"vice_x64",
|
||||
"vice_x64sc",
|
||||
"vice_xplus4",
|
||||
"vice_xvic",
|
||||
"x1",
|
||||
"xrick",
|
||||
"yabasanshiro"
|
||||
],
|
||||
"odroidgoadvance": [
|
||||
"2048",
|
||||
"81",
|
||||
"AMIBERRY",
|
||||
"AdvanceMame",
|
||||
"DOSBOXSDL2",
|
||||
"ECWolf",
|
||||
"FBNEOSA",
|
||||
"HATARISA",
|
||||
"HYPSEUS",
|
||||
"OPENBOR",
|
||||
"PPSSPPSDL",
|
||||
"Pico-8",
|
||||
"SCUMMVMSA",
|
||||
"STELLASA",
|
||||
"Solarus",
|
||||
"a5200",
|
||||
"atari800",
|
||||
"bgdi",
|
||||
"bk",
|
||||
"bluemsx",
|
||||
"cannonball",
|
||||
"cap32",
|
||||
"daphne",
|
||||
"dinothawr",
|
||||
"dosbox_pure",
|
||||
"dosbox_svn",
|
||||
"easyrpg",
|
||||
"fake08",
|
||||
"fbalpha2012",
|
||||
"fbneo",
|
||||
"fceumm",
|
||||
"fceumm_mod",
|
||||
"flycast",
|
||||
"flycast_32b",
|
||||
"flycastsa",
|
||||
"fmsx",
|
||||
"freeintv",
|
||||
"freej2me",
|
||||
"fuse",
|
||||
"gambatte",
|
||||
"gearboy",
|
||||
"gearcoleco",
|
||||
"geargrafx",
|
||||
"gearlynx",
|
||||
"gearsystem",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_cart_special",
|
||||
"genesis_plus_gx_wide",
|
||||
"geolith",
|
||||
"gpsp",
|
||||
"gw",
|
||||
"handy",
|
||||
"hatari",
|
||||
"ikemen",
|
||||
"jzintv",
|
||||
"mame2003_plus",
|
||||
"mame2010",
|
||||
"mednafen_lynx",
|
||||
"mednafen_ngp",
|
||||
"mednafen_pce_fast",
|
||||
"mednafen_pcfx",
|
||||
"mednafen_supafaust",
|
||||
"mednafen_supergrafx",
|
||||
"mednafen_vb",
|
||||
"mednafen_wswan",
|
||||
"mgba",
|
||||
"mrboom",
|
||||
"mupen64plus_32b",
|
||||
"mupen64plus_next",
|
||||
"mupen64plus_next_alt",
|
||||
"mupen64plussa",
|
||||
"nekop2",
|
||||
"neocd",
|
||||
"nestopia",
|
||||
"np2kai",
|
||||
"nxengine",
|
||||
"o2em",
|
||||
"opera",
|
||||
"parallel_n64_32b",
|
||||
"pcsx_rearmed",
|
||||
"pcsx_rearmed_32b",
|
||||
"picodrive",
|
||||
"pocketcdg",
|
||||
"pokemini",
|
||||
"potator",
|
||||
"prboom",
|
||||
"prosystem",
|
||||
"puae",
|
||||
"puae2021",
|
||||
"px68k",
|
||||
"reminiscence",
|
||||
"same_cdi",
|
||||
"sameboy",
|
||||
"sameduck",
|
||||
"scummvm",
|
||||
"snes9x",
|
||||
"snes9x2002",
|
||||
"snes9x2005_plus",
|
||||
"snes9x2010",
|
||||
"stella",
|
||||
"swanstation",
|
||||
"tgbdual",
|
||||
"tic80",
|
||||
"tyrquake",
|
||||
"uae4arm_32b",
|
||||
"uzem",
|
||||
"vba_next",
|
||||
"vbam",
|
||||
"vecx",
|
||||
"vice_x128",
|
||||
"vice_x64",
|
||||
"vice_x64sc",
|
||||
"vice_xplus4",
|
||||
"vice_xvic",
|
||||
"x1",
|
||||
"xrick",
|
||||
"yabasanshiro"
|
||||
],
|
||||
"odroidm1": [
|
||||
"2048",
|
||||
"81",
|
||||
"AMIBERRY",
|
||||
"AdvanceMame",
|
||||
"DOSBOXSDL2",
|
||||
"Duckstation",
|
||||
"ECWolf",
|
||||
"FBNEOSA",
|
||||
"HATARISA",
|
||||
"HYPSEUS",
|
||||
"OPENBOR",
|
||||
"PPSSPPSDL",
|
||||
"Pico-8",
|
||||
"SCUMMVMSA",
|
||||
"STELLASA",
|
||||
"Solarus",
|
||||
"a5200",
|
||||
"atari800",
|
||||
"bgdi",
|
||||
"bk",
|
||||
"bluemsx",
|
||||
"cannonball",
|
||||
"cap32",
|
||||
"daphne",
|
||||
"dinothawr",
|
||||
"dolphin",
|
||||
"dosbox_pure",
|
||||
"dosbox_svn",
|
||||
"easyrpg",
|
||||
"fake08",
|
||||
"fbalpha2012",
|
||||
"fbneo",
|
||||
"fceumm",
|
||||
"fceumm_mod",
|
||||
"flycast",
|
||||
"flycast_32b",
|
||||
"flycastsa",
|
||||
"fmsx",
|
||||
"freeintv",
|
||||
"freej2me",
|
||||
"fuse",
|
||||
"gambatte",
|
||||
"gearboy",
|
||||
"gearcoleco",
|
||||
"geargrafx",
|
||||
"gearlynx",
|
||||
"gearsystem",
|
||||
"genesis_plus_gx",
|
||||
"genesis_plus_gx_cart_special",
|
||||
"genesis_plus_gx_wide",
|
||||
"geolith",
|
||||
"gpsp",
|
||||
"gw",
|
||||
"handy",
|
||||
"hatari",
|
||||
"ikemen",
|
||||
"jzintv",
|
||||
"mame",
|
||||
"mame2003_plus",
|
||||
"mame2010",
|
||||
"mednafen_lynx",
|
||||
"mednafen_ngp",
|
||||
"mednafen_pce_fast",
|
||||
"mednafen_pcfx",
|
||||
"mednafen_saturn",
|
||||
"mednafen_supafaust",
|
||||
"mednafen_supergrafx",
|
||||
"mednafen_vb",
|
||||
"mednafen_wswan",
|
||||
"mesen",
|
||||
"mesen-s",
|
||||
"mgba",
|
||||
"mrboom",
|
||||
"mupen64plus_32b",
|
||||
"mupen64plus_next",
|
||||
"mupen64plus_next_alt",
|
||||
"mupen64plussa",
|
||||
"nekop2",
|
||||
"neocd",
|
||||
"nestopia",
|
||||
"np2kai",
|
||||
"nxengine",
|
||||
"o2em",
|
||||
"opera",
|
||||
"parallel_n64_32b",
|
||||
"pcsx_rearmed",
|
||||
"pcsx_rearmed_32b",
|
||||
"picodrive",
|
||||
"pocketcdg",
|
||||
"pokemini",
|
||||
"potator",
|
||||
"prboom",
|
||||
"prosystem",
|
||||
"puae",
|
||||
"puae2021",
|
||||
"px68k",
|
||||
"reminiscence",
|
||||
"same_cdi",
|
||||
"sameboy",
|
||||
"sameduck",
|
||||
"scummvm",
|
||||
"snes9x",
|
||||
"snes9x2002",
|
||||
"snes9x2005_plus",
|
||||
"snes9x2010",
|
||||
"stella",
|
||||
"swanstation",
|
||||
"tgbdual",
|
||||
"tic80",
|
||||
"tyrquake",
|
||||
"uae4arm_32b",
|
||||
"uzem",
|
||||
"vba_next",
|
||||
"vbam",
|
||||
"vecx",
|
||||
"vice_x128",
|
||||
"vice_x64",
|
||||
"vice_x64sc",
|
||||
"vice_xplus4",
|
||||
"vice_xvic",
|
||||
"virtualjaguar",
|
||||
"x1",
|
||||
"xrick",
|
||||
"yabasanshiro"
|
||||
]
|
||||
}
|
||||
+19
-6
@@ -219,10 +219,23 @@ platforms:
|
||||
method: config_file
|
||||
config: $HOME/snap/retroarch/current/.config/retroarch/retroarch.cfg
|
||||
parse_key: system_directory
|
||||
# Native: $XDG_CONFIG_HOME/retroarch when the variable is set, else
|
||||
# $HOME/.config/retroarch, then $HOME/.retroarch.cfg when that file is
|
||||
# missing (file_path_special.c fill_pathname_application_data,
|
||||
# configuration.c open_default_config_file). A value left at default
|
||||
# is the system folder of the first of the two folders.
|
||||
- os: linux
|
||||
method: config_file
|
||||
config: $XDG_CONFIG_HOME/retroarch/retroarch.cfg
|
||||
parse_key: system_directory
|
||||
- os: linux
|
||||
method: config_file
|
||||
config: $HOME/.config/retroarch/retroarch.cfg
|
||||
parse_key: system_directory
|
||||
- os: linux
|
||||
method: config_file
|
||||
config: $HOME/.retroarch.cfg
|
||||
parse_key: system_directory
|
||||
# configuration.c reads <Application Support>/RetroArch/config, and a value
|
||||
# left at default is ~/Documents/RetroArch/system (platform_darwin.m).
|
||||
# Older builds kept the file one level up.
|
||||
@@ -852,7 +865,6 @@ platforms:
|
||||
# 99-distribution.conf:320 passes --strictfilter, an argument the script
|
||||
# does not handle, so that path reports nothing. No release since
|
||||
# 20230203: the scraper reads the commit of the newest prerelease.
|
||||
# Several device images, no per-device core list: no target file.
|
||||
amberelec:
|
||||
config: amberelec.yml
|
||||
status: active
|
||||
@@ -868,8 +880,8 @@ platforms:
|
||||
# setsettings.py leaves it: saves sit beside each game, no single place.
|
||||
save_directory: null
|
||||
schedule: monthly
|
||||
target_scraper: null
|
||||
target_source: null
|
||||
target_scraper: amberelec_targets
|
||||
target_source: https://github.com/AmberELEC/AmberELEC/blob/dev/packages/amberelec/package.mk
|
||||
install:
|
||||
detect:
|
||||
# AmberELEC 5a3882cb scripts/image:150-152, options:17. CHAos
|
||||
@@ -893,7 +905,8 @@ platforms:
|
||||
# member's md5 and report UNTESTED whatever the archive holds. Run by
|
||||
# EmulationStation's BIOS screen and by ee_check_bios after a failed
|
||||
# launch (99-emuelec.conf:261). naomi and Dinothawr are written twice in
|
||||
# the dict and the later entry is the one Python keeps.
|
||||
# the dict and the later entry is the one Python keeps. One image per
|
||||
# device the release publishes, its cores read by emuelec_targets_scraper.
|
||||
emuelec:
|
||||
config: emuelec.yml
|
||||
status: active
|
||||
@@ -909,8 +922,8 @@ platforms:
|
||||
# RetroArch saves beside each game: no single place.
|
||||
save_directory: null
|
||||
schedule: monthly
|
||||
target_scraper: null
|
||||
target_source: null
|
||||
target_scraper: emuelec_targets
|
||||
target_source: https://github.com/EmuELEC/EmuELEC/blob/dev/packages/sx05re/emuelec/package.mk
|
||||
install:
|
||||
detect:
|
||||
# EmuELEC d4c7b2f1 scripts/image:172-176, options:4.
|
||||
|
||||
@@ -121,13 +121,6 @@ retrodeck:
|
||||
# includes the frontend name alongside the actual cores.
|
||||
remove_cores: [retroarch]
|
||||
|
||||
emudeck:
|
||||
targets:
|
||||
_default:
|
||||
# EmuDeck's EmuScripts tree carries a template.sh placeholder used
|
||||
# to scaffold new installers. It is not an emulator.
|
||||
remove_cores: [template]
|
||||
|
||||
lakka:
|
||||
targets:
|
||||
generic:
|
||||
@@ -202,7 +195,7 @@ knulli:
|
||||
- rice
|
||||
- vaixterm
|
||||
|
||||
# EmuELEC and AmberELEC have no target file: the scrapers read these lists.
|
||||
# Read by the EmuELEC and AmberELEC platform and target scrapers alike.
|
||||
# mpv and ffplay play the Media Player system's videos (EmuELEC
|
||||
# emuelecRunEmu.sh:301-303, AmberELEC runemu.py:122). gmloader runs a
|
||||
# GameMaker game's own Android library (emuelecRunEmu.sh:331-334). Ports
|
||||
|
||||
@@ -0,0 +1,805 @@
|
||||
platform: amberelec
|
||||
source: https://github.com/AmberELEC/AmberELEC/blob/a21975bfb1a3af68f66e07a931b994b523e1e6e2/packages/amberelec/package.mk
|
||||
scraped_at: '2026-11-06T07:53:33Z'
|
||||
targets:
|
||||
rg351mp:
|
||||
architecture: aarch64
|
||||
cores:
|
||||
- '81'
|
||||
- AMIBERRY
|
||||
- AdvanceMame
|
||||
- DoubleCherryGB
|
||||
- HATARISA
|
||||
- OPENBOR
|
||||
- PPSSPPSDL
|
||||
- SCUMMVMSA
|
||||
- a5200
|
||||
- arduous
|
||||
- atari800
|
||||
- beetle_gba
|
||||
- beetle_lynx
|
||||
- beetle_ngp
|
||||
- beetle_pce
|
||||
- beetle_pce_fast
|
||||
- beetle_pcfx
|
||||
- beetle_supafaust
|
||||
- beetle_supergrafx
|
||||
- beetle_vb
|
||||
- beetle_wswan
|
||||
- bluemsx
|
||||
- cap32
|
||||
- crocods
|
||||
- dosbox_core
|
||||
- dosbox_pure
|
||||
- drastic
|
||||
- duckstation
|
||||
- easyrpg
|
||||
- ecwolf
|
||||
- emuscv
|
||||
- ep128emu_core
|
||||
- fake08
|
||||
- fbalpha2012
|
||||
- fbalpha2019
|
||||
- fbneo
|
||||
- fceumm
|
||||
- flycast
|
||||
- flycast2021
|
||||
- flycast2021le
|
||||
- fmsx
|
||||
- freechaf
|
||||
- freeintv
|
||||
- freej2me
|
||||
- freej2me-plus
|
||||
- fuse
|
||||
- gambatte
|
||||
- gearboy
|
||||
- gearcoleco
|
||||
- gearsystem
|
||||
- genesis_plus_gx
|
||||
- genesis_plus_gx_wide
|
||||
- gpsp
|
||||
- gw
|
||||
- gzdoom
|
||||
- handy
|
||||
- hatari
|
||||
- hypseus_singe
|
||||
- jaxe
|
||||
- lowresnx
|
||||
- lzdoom
|
||||
- mame
|
||||
- mame2000
|
||||
- mame2003_plus
|
||||
- mame2010
|
||||
- mame2015
|
||||
- mame2016
|
||||
- mgba
|
||||
- mojozork
|
||||
- mupen64plus_next
|
||||
- mupen64plussa
|
||||
- nekop2
|
||||
- neocd
|
||||
- nestopia
|
||||
- np2kai
|
||||
- o2em
|
||||
- opera
|
||||
- parallel_n64
|
||||
- pcsx_rearmed
|
||||
- pico8
|
||||
- picodrive
|
||||
- piemu
|
||||
- pokemini
|
||||
- potator
|
||||
- ppsspp
|
||||
- prboom
|
||||
- prosystem
|
||||
- puae
|
||||
- puae2021
|
||||
- px68k
|
||||
- quasi88
|
||||
- quicknes
|
||||
- race
|
||||
- raze
|
||||
- same_cdi
|
||||
- sameboy
|
||||
- sameduck
|
||||
- scummvm
|
||||
- smsplus
|
||||
- snes9x
|
||||
- snes9x2002
|
||||
- snes9x2005_plus
|
||||
- snes9x2010
|
||||
- solarus
|
||||
- stella
|
||||
- stella2014
|
||||
- swanstation
|
||||
- tgbdual
|
||||
- theodore
|
||||
- tic80
|
||||
- uae4arm
|
||||
- uw8
|
||||
- uzem
|
||||
- vba_next
|
||||
- vbam
|
||||
- vecx
|
||||
- vice_x128
|
||||
- vice_x64
|
||||
- vice_xplus4
|
||||
- vice_xvic
|
||||
- vircon32
|
||||
- virtualjaguar
|
||||
- wasm4
|
||||
- x1
|
||||
- yabasanshiroSA
|
||||
rg351p:
|
||||
architecture: aarch64
|
||||
cores:
|
||||
- '81'
|
||||
- AMIBERRY
|
||||
- AdvanceMame
|
||||
- DoubleCherryGB
|
||||
- HATARISA
|
||||
- OPENBOR
|
||||
- PPSSPPSDL
|
||||
- SCUMMVMSA
|
||||
- a5200
|
||||
- arduous
|
||||
- atari800
|
||||
- beetle_gba
|
||||
- beetle_lynx
|
||||
- beetle_ngp
|
||||
- beetle_pce
|
||||
- beetle_pce_fast
|
||||
- beetle_pcfx
|
||||
- beetle_supafaust
|
||||
- beetle_supergrafx
|
||||
- beetle_vb
|
||||
- beetle_wswan
|
||||
- bluemsx
|
||||
- cap32
|
||||
- crocods
|
||||
- dosbox_core
|
||||
- dosbox_pure
|
||||
- drastic
|
||||
- duckstation
|
||||
- easyrpg
|
||||
- ecwolf
|
||||
- emuscv
|
||||
- ep128emu_core
|
||||
- fake08
|
||||
- fbalpha2012
|
||||
- fbalpha2019
|
||||
- fbneo
|
||||
- fceumm
|
||||
- flycast
|
||||
- flycast2021
|
||||
- flycast2021le
|
||||
- fmsx
|
||||
- freechaf
|
||||
- freeintv
|
||||
- freej2me
|
||||
- freej2me-plus
|
||||
- fuse
|
||||
- gambatte
|
||||
- gearboy
|
||||
- gearcoleco
|
||||
- gearsystem
|
||||
- genesis_plus_gx
|
||||
- genesis_plus_gx_wide
|
||||
- gpsp
|
||||
- gw
|
||||
- gzdoom
|
||||
- handy
|
||||
- hatari
|
||||
- hypseus_singe
|
||||
- jaxe
|
||||
- lowresnx
|
||||
- lzdoom
|
||||
- mame
|
||||
- mame2000
|
||||
- mame2003_plus
|
||||
- mame2010
|
||||
- mame2015
|
||||
- mame2016
|
||||
- mgba
|
||||
- mojozork
|
||||
- mupen64plus_next
|
||||
- mupen64plussa
|
||||
- nekop2
|
||||
- neocd
|
||||
- nestopia
|
||||
- np2kai
|
||||
- o2em
|
||||
- opera
|
||||
- parallel_n64
|
||||
- pcsx_rearmed
|
||||
- pico8
|
||||
- picodrive
|
||||
- piemu
|
||||
- pokemini
|
||||
- potator
|
||||
- ppsspp
|
||||
- prboom
|
||||
- prosystem
|
||||
- puae
|
||||
- puae2021
|
||||
- px68k
|
||||
- quasi88
|
||||
- quicknes
|
||||
- race
|
||||
- raze
|
||||
- same_cdi
|
||||
- sameboy
|
||||
- sameduck
|
||||
- scummvm
|
||||
- smsplus
|
||||
- snes9x
|
||||
- snes9x2002
|
||||
- snes9x2005_plus
|
||||
- snes9x2010
|
||||
- solarus
|
||||
- stella
|
||||
- stella2014
|
||||
- swanstation
|
||||
- tgbdual
|
||||
- theodore
|
||||
- tic80
|
||||
- uae4arm
|
||||
- uw8
|
||||
- uzem
|
||||
- vba_next
|
||||
- vbam
|
||||
- vecx
|
||||
- vice_x128
|
||||
- vice_x64
|
||||
- vice_xplus4
|
||||
- vice_xvic
|
||||
- vircon32
|
||||
- virtualjaguar
|
||||
- wasm4
|
||||
- x1
|
||||
- yabasanshiroSA
|
||||
rg351v:
|
||||
architecture: aarch64
|
||||
cores:
|
||||
- '81'
|
||||
- AMIBERRY
|
||||
- AdvanceMame
|
||||
- DoubleCherryGB
|
||||
- HATARISA
|
||||
- OPENBOR
|
||||
- PPSSPPSDL
|
||||
- SCUMMVMSA
|
||||
- a5200
|
||||
- arduous
|
||||
- atari800
|
||||
- beetle_gba
|
||||
- beetle_lynx
|
||||
- beetle_ngp
|
||||
- beetle_pce
|
||||
- beetle_pce_fast
|
||||
- beetle_pcfx
|
||||
- beetle_supafaust
|
||||
- beetle_supergrafx
|
||||
- beetle_vb
|
||||
- beetle_wswan
|
||||
- bluemsx
|
||||
- cap32
|
||||
- crocods
|
||||
- dosbox_core
|
||||
- dosbox_pure
|
||||
- drastic
|
||||
- duckstation
|
||||
- easyrpg
|
||||
- ecwolf
|
||||
- emuscv
|
||||
- ep128emu_core
|
||||
- fake08
|
||||
- fbalpha2012
|
||||
- fbalpha2019
|
||||
- fbneo
|
||||
- fceumm
|
||||
- flycast
|
||||
- flycast2021
|
||||
- flycast2021le
|
||||
- fmsx
|
||||
- freechaf
|
||||
- freeintv
|
||||
- freej2me
|
||||
- freej2me-plus
|
||||
- fuse
|
||||
- gambatte
|
||||
- gearboy
|
||||
- gearcoleco
|
||||
- gearsystem
|
||||
- genesis_plus_gx
|
||||
- genesis_plus_gx_wide
|
||||
- gpsp
|
||||
- gw
|
||||
- gzdoom
|
||||
- handy
|
||||
- hatari
|
||||
- hypseus_singe
|
||||
- jaxe
|
||||
- lowresnx
|
||||
- lzdoom
|
||||
- mame
|
||||
- mame2000
|
||||
- mame2003_plus
|
||||
- mame2010
|
||||
- mame2015
|
||||
- mame2016
|
||||
- mgba
|
||||
- mojozork
|
||||
- mupen64plus_next
|
||||
- mupen64plussa
|
||||
- nekop2
|
||||
- neocd
|
||||
- nestopia
|
||||
- np2kai
|
||||
- o2em
|
||||
- opera
|
||||
- parallel_n64
|
||||
- pcsx_rearmed
|
||||
- pico8
|
||||
- picodrive
|
||||
- piemu
|
||||
- pokemini
|
||||
- potator
|
||||
- ppsspp
|
||||
- prboom
|
||||
- prosystem
|
||||
- puae
|
||||
- puae2021
|
||||
- px68k
|
||||
- quasi88
|
||||
- quicknes
|
||||
- race
|
||||
- raze
|
||||
- same_cdi
|
||||
- sameboy
|
||||
- sameduck
|
||||
- scummvm
|
||||
- smsplus
|
||||
- snes9x
|
||||
- snes9x2002
|
||||
- snes9x2005_plus
|
||||
- snes9x2010
|
||||
- solarus
|
||||
- stella
|
||||
- stella2014
|
||||
- swanstation
|
||||
- tgbdual
|
||||
- theodore
|
||||
- tic80
|
||||
- uae4arm
|
||||
- uw8
|
||||
- uzem
|
||||
- vba_next
|
||||
- vbam
|
||||
- vecx
|
||||
- vice_x128
|
||||
- vice_x64
|
||||
- vice_xplus4
|
||||
- vice_xvic
|
||||
- vircon32
|
||||
- virtualjaguar
|
||||
- wasm4
|
||||
- x1
|
||||
- yabasanshiroSA
|
||||
rg353:
|
||||
architecture: aarch64
|
||||
cores:
|
||||
- '81'
|
||||
- AMIBERRY
|
||||
- AdvanceMame
|
||||
- DoubleCherryGB
|
||||
- HATARISA
|
||||
- OPENBOR
|
||||
- PPSSPPSDL
|
||||
- SCUMMVMSA
|
||||
- a5200
|
||||
- arduous
|
||||
- atari800
|
||||
- beetle_gba
|
||||
- beetle_lynx
|
||||
- beetle_ngp
|
||||
- beetle_pce
|
||||
- beetle_pce_fast
|
||||
- beetle_pcfx
|
||||
- beetle_snes
|
||||
- beetle_supafaust
|
||||
- beetle_supergrafx
|
||||
- beetle_vb
|
||||
- beetle_wswan
|
||||
- bluemsx
|
||||
- bsnes
|
||||
- bsnes2014_balanced
|
||||
- bsnes2014_performance
|
||||
- bsnes_mercury_balanced
|
||||
- bsnes_mercury_performance
|
||||
- cap32
|
||||
- crocods
|
||||
- dosbox_core
|
||||
- dosbox_pure
|
||||
- drastic
|
||||
- duckstation
|
||||
- easyrpg
|
||||
- ecwolf
|
||||
- emuscv
|
||||
- ep128emu_core
|
||||
- fake08
|
||||
- fbalpha2012
|
||||
- fbalpha2019
|
||||
- fbneo
|
||||
- fceumm
|
||||
- flycast
|
||||
- flycast2021
|
||||
- flycast2021le
|
||||
- fmsx
|
||||
- freechaf
|
||||
- freeintv
|
||||
- freej2me
|
||||
- freej2me-plus
|
||||
- fuse
|
||||
- gambatte
|
||||
- gearboy
|
||||
- gearcoleco
|
||||
- gearsystem
|
||||
- genesis_plus_gx
|
||||
- genesis_plus_gx_wide
|
||||
- geolith
|
||||
- gpsp
|
||||
- gw
|
||||
- gzdoom
|
||||
- handy
|
||||
- hatari
|
||||
- hypseus_singe
|
||||
- jaxe
|
||||
- lowresnx
|
||||
- lzdoom
|
||||
- mame
|
||||
- mame2000
|
||||
- mame2003_plus
|
||||
- mame2010
|
||||
- mame2015
|
||||
- mame2016
|
||||
- melonds
|
||||
- melondsds
|
||||
- mesen
|
||||
- mesen-s
|
||||
- mgba
|
||||
- mojozork
|
||||
- mupen64plus_next
|
||||
- mupen64plussa
|
||||
- nekop2
|
||||
- neocd
|
||||
- nestopia
|
||||
- np2kai
|
||||
- o2em
|
||||
- opera
|
||||
- parallel_n64
|
||||
- pcsx_rearmed
|
||||
- pico8
|
||||
- picodrive
|
||||
- piemu
|
||||
- pokemini
|
||||
- potator
|
||||
- ppsspp
|
||||
- prboom
|
||||
- prosystem
|
||||
- puae
|
||||
- puae2021
|
||||
- px68k
|
||||
- quasi88
|
||||
- quicknes
|
||||
- race
|
||||
- raze
|
||||
- same_cdi
|
||||
- sameboy
|
||||
- sameduck
|
||||
- scummvm
|
||||
- smsplus
|
||||
- snes9x
|
||||
- snes9x2002
|
||||
- snes9x2005_plus
|
||||
- snes9x2010
|
||||
- solarus
|
||||
- stella
|
||||
- stella2014
|
||||
- swanstation
|
||||
- tgbdual
|
||||
- theodore
|
||||
- tic80
|
||||
- uae4arm
|
||||
- uw8
|
||||
- uzem
|
||||
- vba_next
|
||||
- vbam
|
||||
- vecx
|
||||
- vice_x128
|
||||
- vice_x64
|
||||
- vice_xplus4
|
||||
- vice_xvic
|
||||
- vircon32
|
||||
- virtualjaguar
|
||||
- wasm4
|
||||
- x1
|
||||
- yabasanshiroSA
|
||||
rg503:
|
||||
architecture: aarch64
|
||||
cores:
|
||||
- '81'
|
||||
- AMIBERRY
|
||||
- AdvanceMame
|
||||
- DoubleCherryGB
|
||||
- HATARISA
|
||||
- OPENBOR
|
||||
- PPSSPPSDL
|
||||
- SCUMMVMSA
|
||||
- a5200
|
||||
- arduous
|
||||
- atari800
|
||||
- beetle_gba
|
||||
- beetle_lynx
|
||||
- beetle_ngp
|
||||
- beetle_pce
|
||||
- beetle_pce_fast
|
||||
- beetle_pcfx
|
||||
- beetle_snes
|
||||
- beetle_supafaust
|
||||
- beetle_supergrafx
|
||||
- beetle_vb
|
||||
- beetle_wswan
|
||||
- bluemsx
|
||||
- bsnes
|
||||
- bsnes2014_balanced
|
||||
- bsnes2014_performance
|
||||
- bsnes_mercury_balanced
|
||||
- bsnes_mercury_performance
|
||||
- cap32
|
||||
- crocods
|
||||
- dosbox_core
|
||||
- dosbox_pure
|
||||
- drastic
|
||||
- duckstation
|
||||
- easyrpg
|
||||
- ecwolf
|
||||
- emuscv
|
||||
- ep128emu_core
|
||||
- fake08
|
||||
- fbalpha2012
|
||||
- fbalpha2019
|
||||
- fbneo
|
||||
- fceumm
|
||||
- flycast
|
||||
- flycast2021
|
||||
- flycast2021le
|
||||
- fmsx
|
||||
- freechaf
|
||||
- freeintv
|
||||
- freej2me
|
||||
- freej2me-plus
|
||||
- fuse
|
||||
- gambatte
|
||||
- gearboy
|
||||
- gearcoleco
|
||||
- gearsystem
|
||||
- genesis_plus_gx
|
||||
- genesis_plus_gx_wide
|
||||
- geolith
|
||||
- gpsp
|
||||
- gw
|
||||
- gzdoom
|
||||
- handy
|
||||
- hatari
|
||||
- hypseus_singe
|
||||
- jaxe
|
||||
- lowresnx
|
||||
- lzdoom
|
||||
- mame
|
||||
- mame2000
|
||||
- mame2003_plus
|
||||
- mame2010
|
||||
- mame2015
|
||||
- mame2016
|
||||
- melonds
|
||||
- melondsds
|
||||
- mesen
|
||||
- mesen-s
|
||||
- mgba
|
||||
- mojozork
|
||||
- mupen64plus_next
|
||||
- mupen64plussa
|
||||
- nekop2
|
||||
- neocd
|
||||
- nestopia
|
||||
- np2kai
|
||||
- o2em
|
||||
- opera
|
||||
- parallel_n64
|
||||
- pcsx_rearmed
|
||||
- pico8
|
||||
- picodrive
|
||||
- piemu
|
||||
- pokemini
|
||||
- potator
|
||||
- ppsspp
|
||||
- prboom
|
||||
- prosystem
|
||||
- puae
|
||||
- puae2021
|
||||
- px68k
|
||||
- quasi88
|
||||
- quicknes
|
||||
- race
|
||||
- raze
|
||||
- same_cdi
|
||||
- sameboy
|
||||
- sameduck
|
||||
- scummvm
|
||||
- smsplus
|
||||
- snes9x
|
||||
- snes9x2002
|
||||
- snes9x2005_plus
|
||||
- snes9x2010
|
||||
- solarus
|
||||
- stella
|
||||
- stella2014
|
||||
- swanstation
|
||||
- tgbdual
|
||||
- theodore
|
||||
- tic80
|
||||
- uae4arm
|
||||
- uw8
|
||||
- uzem
|
||||
- vba_next
|
||||
- vbam
|
||||
- vecx
|
||||
- vice_x128
|
||||
- vice_x64
|
||||
- vice_xplus4
|
||||
- vice_xvic
|
||||
- vircon32
|
||||
- virtualjaguar
|
||||
- wasm4
|
||||
- x1
|
||||
- yabasanshiroSA
|
||||
rg552:
|
||||
architecture: aarch64
|
||||
cores:
|
||||
- '81'
|
||||
- AMIBERRY
|
||||
- AdvanceMame
|
||||
- DoubleCherryGB
|
||||
- HATARISA
|
||||
- OPENBOR
|
||||
- PPSSPPSDL
|
||||
- SCUMMVMSA
|
||||
- a5200
|
||||
- arduous
|
||||
- atari800
|
||||
- beetle_gba
|
||||
- beetle_lynx
|
||||
- beetle_ngp
|
||||
- beetle_pce
|
||||
- beetle_pce_fast
|
||||
- beetle_pcfx
|
||||
- beetle_snes
|
||||
- beetle_supafaust
|
||||
- beetle_supergrafx
|
||||
- beetle_vb
|
||||
- beetle_wswan
|
||||
- bluemsx
|
||||
- bsnes
|
||||
- bsnes2014_balanced
|
||||
- bsnes2014_performance
|
||||
- bsnes_mercury_balanced
|
||||
- bsnes_mercury_performance
|
||||
- cap32
|
||||
- crocods
|
||||
- dosbox_core
|
||||
- dosbox_pure
|
||||
- drastic
|
||||
- duckstation
|
||||
- easyrpg
|
||||
- ecwolf
|
||||
- emuscv
|
||||
- ep128emu_core
|
||||
- fake08
|
||||
- fbalpha2012
|
||||
- fbalpha2019
|
||||
- fbneo
|
||||
- fceumm
|
||||
- flycast
|
||||
- flycast2021
|
||||
- flycast2021le
|
||||
- fmsx
|
||||
- freechaf
|
||||
- freeintv
|
||||
- freej2me
|
||||
- freej2me-plus
|
||||
- fuse
|
||||
- gambatte
|
||||
- gearboy
|
||||
- gearcoleco
|
||||
- gearsystem
|
||||
- genesis_plus_gx
|
||||
- genesis_plus_gx_wide
|
||||
- geolith
|
||||
- gpsp
|
||||
- gw
|
||||
- gzdoom
|
||||
- handy
|
||||
- hatari
|
||||
- hypseus_singe
|
||||
- jaxe
|
||||
- lowresnx
|
||||
- lzdoom
|
||||
- mame
|
||||
- mame2000
|
||||
- mame2003_plus
|
||||
- mame2010
|
||||
- mame2015
|
||||
- mame2016
|
||||
- melonds
|
||||
- melondsds
|
||||
- mesen
|
||||
- mesen-s
|
||||
- mgba
|
||||
- mojozork
|
||||
- mupen64plus_next
|
||||
- mupen64plussa
|
||||
- nekop2
|
||||
- neocd
|
||||
- nestopia
|
||||
- np2kai
|
||||
- o2em
|
||||
- opera
|
||||
- parallel_n64
|
||||
- pcsx_rearmed
|
||||
- pico8
|
||||
- picodrive
|
||||
- piemu
|
||||
- pokemini
|
||||
- potator
|
||||
- ppsspp
|
||||
- prboom
|
||||
- prosystem
|
||||
- puae
|
||||
- puae2021
|
||||
- px68k
|
||||
- quasi88
|
||||
- quicknes
|
||||
- race
|
||||
- raze
|
||||
- same_cdi
|
||||
- sameboy
|
||||
- sameduck
|
||||
- scummvm
|
||||
- smsplus
|
||||
- snes9x
|
||||
- snes9x2002
|
||||
- snes9x2005_plus
|
||||
- snes9x2010
|
||||
- solarus
|
||||
- stella
|
||||
- stella2014
|
||||
- swanstation
|
||||
- tgbdual
|
||||
- theodore
|
||||
- tic80
|
||||
- uae4arm
|
||||
- uw8
|
||||
- uzem
|
||||
- vba_next
|
||||
- vbam
|
||||
- vecx
|
||||
- vice_x128
|
||||
- vice_x64
|
||||
- vice_xplus4
|
||||
- vice_xvic
|
||||
- vircon32
|
||||
- virtualjaguar
|
||||
- wasm4
|
||||
- x1
|
||||
- yabasanshiroSA
|
||||
@@ -1,6 +1,6 @@
|
||||
platform: emudeck
|
||||
source: https://github.com/dragoonDorise/EmuDeck
|
||||
scraped_at: '2026-10-17T18:43:57Z'
|
||||
source: https://github.com/dragoonDorise/EmuDeck/tree/dddb30229adbbd54d5ff7283cfd70e11c4322c26
|
||||
scraped_at: '2026-11-06T07:10:22Z'
|
||||
targets:
|
||||
steamos:
|
||||
architecture: x86_64
|
||||
@@ -88,6 +88,7 @@ targets:
|
||||
- galaksija
|
||||
- gam4980
|
||||
- gambatte
|
||||
- gametank
|
||||
- gearboy
|
||||
- gearcoleco
|
||||
- geargrafx
|
||||
@@ -352,6 +353,7 @@ targets:
|
||||
- galaksija
|
||||
- gam4980
|
||||
- gambatte
|
||||
- gametank
|
||||
- gearboy
|
||||
- gearcoleco
|
||||
- geargrafx
|
||||
@@ -487,7 +489,6 @@ targets:
|
||||
- supermodel
|
||||
- swanstation
|
||||
- tamalibretro
|
||||
- template
|
||||
- tgbdual
|
||||
- theodore
|
||||
- thepowdertoy
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
platform: emuelec
|
||||
source: https://github.com/EmuELEC/EmuELEC/blob/v4.8/packages/sx05re/emuelec/package.mk
|
||||
devices_source: https://github.com/EmuELEC/EmuELEC/releases/tag/v4.8
|
||||
scraped_at: '2026-11-06T10:27:34Z'
|
||||
targets:
|
||||
amlogic-ng:
|
||||
architecture: aarch64
|
||||
cores:
|
||||
- '2048'
|
||||
- '81'
|
||||
- AMIBERRY
|
||||
- AdvanceMame
|
||||
- DOSBOXSDL2
|
||||
- Duckstation
|
||||
- ECWolf
|
||||
- FBNEOSA
|
||||
- HATARISA
|
||||
- HYPSEUS
|
||||
- OPENBOR
|
||||
- PPSSPPSDL
|
||||
- Pico-8
|
||||
- SCUMMVMSA
|
||||
- STELLASA
|
||||
- Solarus
|
||||
- a5200
|
||||
- atari800
|
||||
- bgdi
|
||||
- bk
|
||||
- bluemsx
|
||||
- cannonball
|
||||
- cap32
|
||||
- daphne
|
||||
- dinothawr
|
||||
- dolphin
|
||||
- dosbox_pure
|
||||
- dosbox_svn
|
||||
- easyrpg
|
||||
- fake08
|
||||
- fbalpha2012
|
||||
- fbneo
|
||||
- fceumm
|
||||
- fceumm_mod
|
||||
- flycast
|
||||
- flycast_32b
|
||||
- flycastsa
|
||||
- fmsx
|
||||
- freeintv
|
||||
- freej2me
|
||||
- fuse
|
||||
- gambatte
|
||||
- gearboy
|
||||
- gearcoleco
|
||||
- geargrafx
|
||||
- gearlynx
|
||||
- gearsystem
|
||||
- genesis_plus_gx
|
||||
- genesis_plus_gx_cart_special
|
||||
- genesis_plus_gx_wide
|
||||
- geolith
|
||||
- gpsp
|
||||
- gw
|
||||
- handy
|
||||
- hatari
|
||||
- ikemen
|
||||
- jzintv
|
||||
- mame
|
||||
- mame2003_plus
|
||||
- mame2010
|
||||
- mednafen_lynx
|
||||
- mednafen_ngp
|
||||
- mednafen_pce_fast
|
||||
- mednafen_pcfx
|
||||
- mednafen_saturn
|
||||
- mednafen_supafaust
|
||||
- mednafen_supergrafx
|
||||
- mednafen_vb
|
||||
- mednafen_wswan
|
||||
- mesen
|
||||
- mesen-s
|
||||
- mgba
|
||||
- mrboom
|
||||
- mupen64plus_32b
|
||||
- mupen64plus_next
|
||||
- mupen64plus_next_alt
|
||||
- mupen64plussa
|
||||
- nekop2
|
||||
- neocd
|
||||
- nestopia
|
||||
- np2kai
|
||||
- nxengine
|
||||
- o2em
|
||||
- opera
|
||||
- parallel_n64_32b
|
||||
- pcsx_rearmed
|
||||
- pcsx_rearmed_32b
|
||||
- picodrive
|
||||
- pocketcdg
|
||||
- pokemini
|
||||
- potator
|
||||
- prboom
|
||||
- prosystem
|
||||
- puae
|
||||
- puae2021
|
||||
- px68k
|
||||
- reminiscence
|
||||
- same_cdi
|
||||
- sameboy
|
||||
- sameduck
|
||||
- scummvm
|
||||
- snes9x
|
||||
- snes9x2002
|
||||
- snes9x2005_plus
|
||||
- snes9x2010
|
||||
- stella
|
||||
- swanstation
|
||||
- tgbdual
|
||||
- tic80
|
||||
- tyrquake
|
||||
- uae4arm_32b
|
||||
- uzem
|
||||
- vba_next
|
||||
- vbam
|
||||
- vecx
|
||||
- vice_x128
|
||||
- vice_x64
|
||||
- vice_x64sc
|
||||
- vice_xplus4
|
||||
- vice_xvic
|
||||
- virtualjaguar
|
||||
- x1
|
||||
- xrick
|
||||
- yabasanshiro
|
||||
gameforce:
|
||||
architecture: aarch64
|
||||
cores:
|
||||
- '2048'
|
||||
- '81'
|
||||
- AMIBERRY
|
||||
- AdvanceMame
|
||||
- DOSBOXSDL2
|
||||
- ECWolf
|
||||
- FBNEOSA
|
||||
- HATARISA
|
||||
- HYPSEUS
|
||||
- OPENBOR
|
||||
- PPSSPPSDL
|
||||
- Pico-8
|
||||
- SCUMMVMSA
|
||||
- STELLASA
|
||||
- Solarus
|
||||
- a5200
|
||||
- atari800
|
||||
- bgdi
|
||||
- bk
|
||||
- bluemsx
|
||||
- cannonball
|
||||
- cap32
|
||||
- daphne
|
||||
- dinothawr
|
||||
- dosbox_pure
|
||||
- dosbox_svn
|
||||
- easyrpg
|
||||
- fake08
|
||||
- fbalpha2012
|
||||
- fbneo
|
||||
- fceumm
|
||||
- fceumm_mod
|
||||
- flycast
|
||||
- flycast_32b
|
||||
- flycastsa
|
||||
- fmsx
|
||||
- freeintv
|
||||
- freej2me
|
||||
- fuse
|
||||
- gambatte
|
||||
- gearboy
|
||||
- gearcoleco
|
||||
- geargrafx
|
||||
- gearlynx
|
||||
- gearsystem
|
||||
- genesis_plus_gx
|
||||
- genesis_plus_gx_cart_special
|
||||
- genesis_plus_gx_wide
|
||||
- geolith
|
||||
- gpsp
|
||||
- gw
|
||||
- handy
|
||||
- hatari
|
||||
- ikemen
|
||||
- jzintv
|
||||
- mame2003_plus
|
||||
- mame2010
|
||||
- mednafen_lynx
|
||||
- mednafen_ngp
|
||||
- mednafen_pce_fast
|
||||
- mednafen_pcfx
|
||||
- mednafen_supafaust
|
||||
- mednafen_supergrafx
|
||||
- mednafen_vb
|
||||
- mednafen_wswan
|
||||
- mgba
|
||||
- mrboom
|
||||
- mupen64plus_32b
|
||||
- mupen64plus_next
|
||||
- mupen64plus_next_alt
|
||||
- mupen64plussa
|
||||
- nekop2
|
||||
- neocd
|
||||
- nestopia
|
||||
- np2kai
|
||||
- nxengine
|
||||
- o2em
|
||||
- opera
|
||||
- parallel_n64_32b
|
||||
- pcsx_rearmed
|
||||
- pcsx_rearmed_32b
|
||||
- picodrive
|
||||
- pocketcdg
|
||||
- pokemini
|
||||
- potator
|
||||
- prboom
|
||||
- prosystem
|
||||
- puae
|
||||
- puae2021
|
||||
- px68k
|
||||
- reminiscence
|
||||
- same_cdi
|
||||
- sameboy
|
||||
- sameduck
|
||||
- scummvm
|
||||
- snes9x
|
||||
- snes9x2002
|
||||
- snes9x2005_plus
|
||||
- snes9x2010
|
||||
- stella
|
||||
- swanstation
|
||||
- tgbdual
|
||||
- tic80
|
||||
- tyrquake
|
||||
- uae4arm_32b
|
||||
- uzem
|
||||
- vba_next
|
||||
- vbam
|
||||
- vecx
|
||||
- vice_x128
|
||||
- vice_x64
|
||||
- vice_x64sc
|
||||
- vice_xplus4
|
||||
- vice_xvic
|
||||
- x1
|
||||
- xrick
|
||||
- yabasanshiro
|
||||
odroidgoadvance:
|
||||
architecture: aarch64
|
||||
cores:
|
||||
- '2048'
|
||||
- '81'
|
||||
- AMIBERRY
|
||||
- AdvanceMame
|
||||
- DOSBOXSDL2
|
||||
- ECWolf
|
||||
- FBNEOSA
|
||||
- HATARISA
|
||||
- HYPSEUS
|
||||
- OPENBOR
|
||||
- PPSSPPSDL
|
||||
- Pico-8
|
||||
- SCUMMVMSA
|
||||
- STELLASA
|
||||
- Solarus
|
||||
- a5200
|
||||
- atari800
|
||||
- bgdi
|
||||
- bk
|
||||
- bluemsx
|
||||
- cannonball
|
||||
- cap32
|
||||
- daphne
|
||||
- dinothawr
|
||||
- dosbox_pure
|
||||
- dosbox_svn
|
||||
- easyrpg
|
||||
- fake08
|
||||
- fbalpha2012
|
||||
- fbneo
|
||||
- fceumm
|
||||
- fceumm_mod
|
||||
- flycast
|
||||
- flycast_32b
|
||||
- flycastsa
|
||||
- fmsx
|
||||
- freeintv
|
||||
- freej2me
|
||||
- fuse
|
||||
- gambatte
|
||||
- gearboy
|
||||
- gearcoleco
|
||||
- geargrafx
|
||||
- gearlynx
|
||||
- gearsystem
|
||||
- genesis_plus_gx
|
||||
- genesis_plus_gx_cart_special
|
||||
- genesis_plus_gx_wide
|
||||
- geolith
|
||||
- gpsp
|
||||
- gw
|
||||
- handy
|
||||
- hatari
|
||||
- ikemen
|
||||
- jzintv
|
||||
- mame2003_plus
|
||||
- mame2010
|
||||
- mednafen_lynx
|
||||
- mednafen_ngp
|
||||
- mednafen_pce_fast
|
||||
- mednafen_pcfx
|
||||
- mednafen_supafaust
|
||||
- mednafen_supergrafx
|
||||
- mednafen_vb
|
||||
- mednafen_wswan
|
||||
- mgba
|
||||
- mrboom
|
||||
- mupen64plus_32b
|
||||
- mupen64plus_next
|
||||
- mupen64plus_next_alt
|
||||
- mupen64plussa
|
||||
- nekop2
|
||||
- neocd
|
||||
- nestopia
|
||||
- np2kai
|
||||
- nxengine
|
||||
- o2em
|
||||
- opera
|
||||
- parallel_n64_32b
|
||||
- pcsx_rearmed
|
||||
- pcsx_rearmed_32b
|
||||
- picodrive
|
||||
- pocketcdg
|
||||
- pokemini
|
||||
- potator
|
||||
- prboom
|
||||
- prosystem
|
||||
- puae
|
||||
- puae2021
|
||||
- px68k
|
||||
- reminiscence
|
||||
- same_cdi
|
||||
- sameboy
|
||||
- sameduck
|
||||
- scummvm
|
||||
- snes9x
|
||||
- snes9x2002
|
||||
- snes9x2005_plus
|
||||
- snes9x2010
|
||||
- stella
|
||||
- swanstation
|
||||
- tgbdual
|
||||
- tic80
|
||||
- tyrquake
|
||||
- uae4arm_32b
|
||||
- uzem
|
||||
- vba_next
|
||||
- vbam
|
||||
- vecx
|
||||
- vice_x128
|
||||
- vice_x64
|
||||
- vice_x64sc
|
||||
- vice_xplus4
|
||||
- vice_xvic
|
||||
- x1
|
||||
- xrick
|
||||
- yabasanshiro
|
||||
odroidm1:
|
||||
architecture: aarch64
|
||||
cores:
|
||||
- '2048'
|
||||
- '81'
|
||||
- AMIBERRY
|
||||
- AdvanceMame
|
||||
- DOSBOXSDL2
|
||||
- Duckstation
|
||||
- ECWolf
|
||||
- FBNEOSA
|
||||
- HATARISA
|
||||
- HYPSEUS
|
||||
- OPENBOR
|
||||
- PPSSPPSDL
|
||||
- Pico-8
|
||||
- SCUMMVMSA
|
||||
- STELLASA
|
||||
- Solarus
|
||||
- a5200
|
||||
- atari800
|
||||
- bgdi
|
||||
- bk
|
||||
- bluemsx
|
||||
- cannonball
|
||||
- cap32
|
||||
- daphne
|
||||
- dinothawr
|
||||
- dolphin
|
||||
- dosbox_pure
|
||||
- dosbox_svn
|
||||
- easyrpg
|
||||
- fake08
|
||||
- fbalpha2012
|
||||
- fbneo
|
||||
- fceumm
|
||||
- fceumm_mod
|
||||
- flycast
|
||||
- flycast_32b
|
||||
- flycastsa
|
||||
- fmsx
|
||||
- freeintv
|
||||
- freej2me
|
||||
- fuse
|
||||
- gambatte
|
||||
- gearboy
|
||||
- gearcoleco
|
||||
- geargrafx
|
||||
- gearlynx
|
||||
- gearsystem
|
||||
- genesis_plus_gx
|
||||
- genesis_plus_gx_cart_special
|
||||
- genesis_plus_gx_wide
|
||||
- geolith
|
||||
- gpsp
|
||||
- gw
|
||||
- handy
|
||||
- hatari
|
||||
- ikemen
|
||||
- jzintv
|
||||
- mame
|
||||
- mame2003_plus
|
||||
- mame2010
|
||||
- mednafen_lynx
|
||||
- mednafen_ngp
|
||||
- mednafen_pce_fast
|
||||
- mednafen_pcfx
|
||||
- mednafen_saturn
|
||||
- mednafen_supafaust
|
||||
- mednafen_supergrafx
|
||||
- mednafen_vb
|
||||
- mednafen_wswan
|
||||
- mesen
|
||||
- mesen-s
|
||||
- mgba
|
||||
- mrboom
|
||||
- mupen64plus_32b
|
||||
- mupen64plus_next
|
||||
- mupen64plus_next_alt
|
||||
- mupen64plussa
|
||||
- nekop2
|
||||
- neocd
|
||||
- nestopia
|
||||
- np2kai
|
||||
- nxengine
|
||||
- o2em
|
||||
- opera
|
||||
- parallel_n64_32b
|
||||
- pcsx_rearmed
|
||||
- pcsx_rearmed_32b
|
||||
- picodrive
|
||||
- pocketcdg
|
||||
- pokemini
|
||||
- potator
|
||||
- prboom
|
||||
- prosystem
|
||||
- puae
|
||||
- puae2021
|
||||
- px68k
|
||||
- reminiscence
|
||||
- same_cdi
|
||||
- sameboy
|
||||
- sameduck
|
||||
- scummvm
|
||||
- snes9x
|
||||
- snes9x2002
|
||||
- snes9x2005_plus
|
||||
- snes9x2010
|
||||
- stella
|
||||
- swanstation
|
||||
- tgbdual
|
||||
- tic80
|
||||
- tyrquake
|
||||
- uae4arm_32b
|
||||
- uzem
|
||||
- vba_next
|
||||
- vbam
|
||||
- vecx
|
||||
- vice_x128
|
||||
- vice_x64
|
||||
- vice_x64sc
|
||||
- vice_xplus4
|
||||
- vice_xvic
|
||||
- virtualjaguar
|
||||
- x1
|
||||
- xrick
|
||||
- yabasanshiro
|
||||
@@ -429,6 +429,11 @@
|
||||
"minimum": 0,
|
||||
"description": "With type zip: leading path parts dropped from each member."
|
||||
},
|
||||
"archive_size": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "Bytes of the source archive when it was last downloaded: what fetching the tree costs. Absent when not recorded."
|
||||
},
|
||||
"source_path": {
|
||||
"type": "string",
|
||||
"description": "With type tarball: only members below this path are kept, relative to it."
|
||||
|
||||
@@ -393,7 +393,8 @@
|
||||
"retired",
|
||||
"data-directory",
|
||||
"platform",
|
||||
"emulator"
|
||||
"emulator",
|
||||
"wanted"
|
||||
]
|
||||
},
|
||||
"left": {
|
||||
@@ -433,7 +434,8 @@
|
||||
"description": "On a platform or emulator record: the algorithm of the value its owner wrote, filed under that value and under the file's other hashes. name: the emulator reads the file by its name and declares no hash that matches it."
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "On a platform or emulator record: the platform or profile key. On a wanted record: the dump catalog that lists the value (Redump, No-Intro, TOSEC), the collection holding no file with it."
|
||||
},
|
||||
"system_name": {
|
||||
"type": "string",
|
||||
|
||||
@@ -19,6 +19,7 @@ import re
|
||||
_TIMESTAMP_PATTERNS = [
|
||||
re.compile(r'"generated_at":\s*"[^"]*"'), # database.json
|
||||
re.compile(r'"imported_at":\s*"[^"]*"'), # provenance snapshots
|
||||
re.compile(r'"fetched":\s*"[^"]*"'), # games/catalogs/
|
||||
re.compile(r"\*Auto-generated on [^*]*\*"), # README.md
|
||||
re.compile(r"\*Generated on [^*]*\*"), # docs site pages
|
||||
# The decorated pages carry the same stamp again as a rendered element,
|
||||
@@ -208,6 +209,10 @@ def _strip_timestamps(text: str) -> str:
|
||||
class ArtifactLockBusy(RuntimeError):
|
||||
"""Raised when another process already holds the artifact directory."""
|
||||
|
||||
|
||||
class ArtifactDirectoryMissing(FileNotFoundError):
|
||||
"""A reader named a directory that does not exist."""
|
||||
|
||||
# A run that holds artifact directories for its whole duration names them
|
||||
# here, os.pathsep apart, for the steps it spawns: they would otherwise be
|
||||
# refused by it. A pipeline holds the repository root and its --output-dir.
|
||||
@@ -227,6 +232,14 @@ def artifact_lock(directory: str, exclusive: bool = True) -> Iterator[None]:
|
||||
Writers take the lock exclusively, readers share it. On platforms
|
||||
without flock the lock is a no-op.
|
||||
"""
|
||||
# A reader creating the directory it was pointed at left a typo behind
|
||||
# as a directory holding a .lock, and the next run with the same typo
|
||||
# no longer said the path did not exist.
|
||||
if not exclusive and not os.path.isdir(directory):
|
||||
raise ArtifactDirectoryMissing(
|
||||
f"{directory}: no such directory, pass the --output-dir the packs "
|
||||
"were written to (default dist/)"
|
||||
)
|
||||
try:
|
||||
import fcntl
|
||||
except ImportError:
|
||||
|
||||
@@ -91,8 +91,19 @@ def sha1_of(path: Path) -> str:
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
class GitFailed(RuntimeError):
|
||||
"""A git command failed: its empty output says nothing about the file."""
|
||||
|
||||
|
||||
def git(root: Path, *args: str) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(["git", "-C", str(root), *args], capture_output=True, check=False)
|
||||
"""Run git in root. A failure raises: an unreadable index read as an empty
|
||||
status made a session's working change look unmodified, and --apply wrote
|
||||
HEAD's bytes over it."""
|
||||
done = subprocess.run(["git", "-C", str(root), *args], capture_output=True, check=False)
|
||||
if done.returncode:
|
||||
message = done.stderr.decode(errors="replace").strip()
|
||||
raise GitFailed(f"git {' '.join(args)} exited {done.returncode}: {message}")
|
||||
return done
|
||||
|
||||
|
||||
def head_blob(root: Path, path: str) -> str:
|
||||
@@ -197,7 +208,11 @@ def main() -> int:
|
||||
from common import load_database
|
||||
|
||||
root = Path.cwd()
|
||||
report = check(root, load_database(args.db), args.apply)
|
||||
try:
|
||||
report = check(root, load_database(args.db), args.apply)
|
||||
except GitFailed as exc:
|
||||
print(f"ERROR: {exc}. Nothing was restored past this point.", file=sys.stderr)
|
||||
return 2
|
||||
if args.json:
|
||||
print(json.dumps(report.as_dict(), indent=2))
|
||||
else:
|
||||
|
||||
+25
-17
@@ -32,7 +32,6 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
@@ -51,6 +50,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
import export_native # noqa: E402
|
||||
import pin_installer # noqa: E402
|
||||
import refresh_data_dirs # noqa: E402
|
||||
import upstream # noqa: E402
|
||||
from common import ( # noqa: E402
|
||||
@@ -83,7 +83,6 @@ PYPI_URL = "https://pypi.org/pypi/{name}/json"
|
||||
NPM_URL = "https://registry.npmjs.org/{name}/latest"
|
||||
# The site's npm dependencies, pinned to exact versions and built by Deploy Site.
|
||||
SITE_PACKAGE = REPO_ROOT / "website" / "package.json"
|
||||
INSTALLERS = ("install.sh", "install.ps1")
|
||||
|
||||
SHOWN_ITEMS = 4 # items named in a diff summary before ", ..."
|
||||
SHOWN_NAMES = 12 # core names named in an unresolved row before ", ..."
|
||||
@@ -509,7 +508,10 @@ def check_native(
|
||||
) -> list[Finding]:
|
||||
exporters = discover_exporters()
|
||||
index = cache_dir / export_native.SOURCES_INDEX
|
||||
recorded = export_native.load_sources(cache_dir)
|
||||
try:
|
||||
recorded = export_native.load_sources(cache_dir)
|
||||
except export_native.SourcesIndexUnreadable as exc:
|
||||
return [Finding("native", index.name, ERROR, detail=str(exc))]
|
||||
findings: list[Finding] = []
|
||||
for name in list_registered_platforms(str(platforms_dir), include_archived=True):
|
||||
exporter_class = exporters.get(name)
|
||||
@@ -1462,18 +1464,16 @@ def check_ci(cache_dir: str, offline: bool) -> list[Finding]:
|
||||
|
||||
def _check_installer_pins() -> Finding:
|
||||
"""The bootstraps pin install.py by SHA-256, so a stale pin refuses every install."""
|
||||
digest = hashlib.sha256((REPO_ROOT / "install.py").read_bytes()).hexdigest()
|
||||
stale = []
|
||||
for name in INSTALLERS:
|
||||
text = (REPO_ROOT / name).read_text(encoding="utf-8", errors="replace")
|
||||
if digest not in text:
|
||||
stale.append(name)
|
||||
stale = pin_installer.stale_pins(REPO_ROOT)
|
||||
return Finding(
|
||||
"ci",
|
||||
"install.py pin",
|
||||
STALE if stale else OK,
|
||||
local=digest[:12],
|
||||
detail=("pin outdated in " + ", ".join(stale)) if stale else "install.sh and install.ps1 match",
|
||||
local=pin_installer.installer_sha256(REPO_ROOT)[:12],
|
||||
detail=(
|
||||
f"pin outdated in {', '.join(stale)}, {pin_installer.COMMAND} writes it"
|
||||
if stale else "install.sh and install.ps1 match"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1492,6 +1492,11 @@ def check_source_paths(profiles: dict) -> Finding:
|
||||
sitesources.tree_key(repo, pin)
|
||||
for profile in profiles.values() if sitesources.cited_paths(profile)
|
||||
for repo, pin in sitesources.profile_trees(profile)
|
||||
} | {
|
||||
# The platform build files pin_build names, linked from a page too.
|
||||
sitesources.tree_key(repo, pin)
|
||||
for profile in profiles.values()
|
||||
for repo, pin, _path in sitesources.build_files(profile)
|
||||
}
|
||||
try:
|
||||
recorded = set(json.loads(sitesources.SOURCE_PATHS.read_text(encoding="utf-8")))
|
||||
@@ -1757,15 +1762,18 @@ def render(findings: list[Finding]) -> str:
|
||||
# Columns are padded, never cut: the profile_sync row says in its
|
||||
# local and remote values what to re-read, and lost the end of it.
|
||||
local_width = min(max((len(f.local) for f in rows), default=0), 44)
|
||||
remote_width = min(max((len(f.remote) for f in rows), default=0), 44)
|
||||
# The arrow and the remote value, or as many blanks where a row has none.
|
||||
remote_span = 4 + remote_width if remote_width else 0
|
||||
for f in rows:
|
||||
cols = f" {f.status:7} {f.subject:{width}}"
|
||||
if f.remote:
|
||||
cols += f" {f.local:{local_width}} -> {f.remote}"
|
||||
elif f.local or (f.detail and local_width):
|
||||
# An arrow pointing at nothing read as a lost value. An empty
|
||||
# local value still takes its column, or the detail starts
|
||||
# where the other rows show theirs.
|
||||
cols += f" {f.local:{local_width}}"
|
||||
cols += f" {f.local:{local_width}} -> {f.remote:{remote_width}}"
|
||||
elif f.local or (f.detail and (local_width or remote_span)):
|
||||
# An arrow pointing at nothing read as a lost value. Empty
|
||||
# local and remote values still take their columns, or the
|
||||
# detail starts where the other rows show theirs.
|
||||
cols += f" {f.local:{local_width}}" + " " * remote_span
|
||||
if f.detail:
|
||||
cols += f" {f.detail}"
|
||||
lines.append(cols.rstrip())
|
||||
|
||||
+6
-1
@@ -3036,12 +3036,16 @@ SYSTEM_ALIASES: dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def _norm_system_id(sid: str) -> str:
|
||||
"""Normalize system ID for cross-platform matching.
|
||||
|
||||
Resolves known aliases, then strips manufacturer prefixes and separators
|
||||
so that platform-specific IDs (e.g., "xbox", "nintendo-wiiu") match
|
||||
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 = SYSTEM_ALIASES.get(s, s)
|
||||
@@ -3587,6 +3591,7 @@ from artifacts import ( # noqa: E402,F401
|
||||
copy_file_atomic,
|
||||
plain_file_mode,
|
||||
set_plain_mode,
|
||||
ArtifactDirectoryMissing,
|
||||
ArtifactLockBusy,
|
||||
artifact_lock,
|
||||
hold_artifact_lock,
|
||||
@@ -3634,7 +3639,7 @@ def run_cli(main: Callable[[], int | None]) -> None:
|
||||
try:
|
||||
code = main()
|
||||
sys.stdout.flush()
|
||||
except (InputMissing, ArtifactLockBusy) as exc:
|
||||
except (InputMissing, ArtifactLockBusy, ArtifactDirectoryMissing) as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except BrokenPipeError:
|
||||
|
||||
@@ -26,7 +26,9 @@ sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import (
|
||||
platform_arg,
|
||||
ProfileSelectionError,
|
||||
_foreign_tree,
|
||||
_norm_system_id,
|
||||
_owner_tokens,
|
||||
archive_entry,
|
||||
directory_members,
|
||||
expand_directory_entries,
|
||||
@@ -215,8 +217,14 @@ def _resolve_source(
|
||||
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
|
||||
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 {})
|
||||
owners = _owner_tokens(file_entry or {})
|
||||
|
||||
def _can_be(sha1s: list | str) -> bool:
|
||||
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."""
|
||||
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
|
||||
if fname in by_name and _name_hit(fname):
|
||||
return "bios"
|
||||
@@ -241,10 +257,10 @@ def _resolve_source(
|
||||
if basename and basename in by_name and _name_hit(basename):
|
||||
return "bios"
|
||||
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"
|
||||
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()]
|
||||
):
|
||||
return "bios"
|
||||
@@ -257,7 +273,7 @@ def _resolve_source(
|
||||
if canonical and canonical != fname:
|
||||
if canonical in by_name and _name_hit(canonical):
|
||||
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()]
|
||||
):
|
||||
return "bios"
|
||||
@@ -901,7 +917,9 @@ def _cross_reference_profile(
|
||||
source = next(
|
||||
(
|
||||
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",
|
||||
)
|
||||
|
||||
@@ -36,6 +36,7 @@ from artifacts import file_lock, write_bytes_atomic, write_if_changed, write_tex
|
||||
import yaml
|
||||
|
||||
from common import (
|
||||
InputMissing,
|
||||
build_zip_contents_index,
|
||||
platform_arg,
|
||||
list_registered_platforms,
|
||||
@@ -62,13 +63,27 @@ _USER_AGENT = "retrobios-exporter/1.0"
|
||||
_MAX_BYTES = 64 * 1024 * 1024
|
||||
|
||||
|
||||
class SourcesIndexUnreadable(InputMissing):
|
||||
"""The cache's URL index exists and does not read as JSON."""
|
||||
|
||||
|
||||
def _load_sources(index: Path | None) -> dict[str, str]:
|
||||
"""The URL each cached file was fetched from.
|
||||
|
||||
An absent index records nothing. An index that does not read raises:
|
||||
read as empty it let every cached original pass for current, and the
|
||||
next download rewrote it with that one entry.
|
||||
"""
|
||||
if index is None or not index.is_file():
|
||||
return {}
|
||||
try:
|
||||
return json.loads(index.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise SourcesIndexUnreadable(
|
||||
f"{index} does not read ({exc}): remove it, then"
|
||||
" python scripts/export_native.py --all --refresh-cache"
|
||||
" downloads every original again and records its URL"
|
||||
) from exc
|
||||
|
||||
|
||||
def source_key(destination: Path, index: Path | None) -> str:
|
||||
|
||||
@@ -19,7 +19,9 @@ SOURCE_URL = (
|
||||
"/package/batocera/core/batocera-scripts/scripts/batocera-systems"
|
||||
)
|
||||
|
||||
_ENTRY_START = re.compile(r'^(\s{4})"([^"]+)":\s*\{')
|
||||
# Any indent: Batocera writes four spaces, EmuELEC one tab for 23 systems,
|
||||
# and an entry the pattern missed was appended again whole.
|
||||
_ENTRY_START = re.compile(r'^(\s+)"([^"]+)":\s*\{')
|
||||
_INDENT = " " * 4
|
||||
# One BIOS item: a dict literal with no nested braces.
|
||||
_ITEM = re.compile(r"\{[^{}\n]*\}")
|
||||
@@ -382,12 +384,13 @@ class Exporter(BaseExporter):
|
||||
return inner if isinstance(inner, dict) else {}
|
||||
|
||||
def _entry_line(
|
||||
self, system: NativeSystem, files: list[NativeFile], before: dict | None = None
|
||||
self, system: NativeSystem, files: list[NativeFile], before: dict | None = None,
|
||||
indent: str = _INDENT,
|
||||
) -> str:
|
||||
own = self._own_entry(system, before)
|
||||
runner = "".join(f"{key}, " for key in self._runner_keys(own))
|
||||
return (
|
||||
f'{_INDENT}"{system.native_id}": '
|
||||
f'{indent}"{system.native_id}": '
|
||||
f'{{ "name": "{self.display_name(system)}", {runner}'
|
||||
f'"biosFiles": [ {self._bios_files(files, own)} ] }},'
|
||||
)
|
||||
@@ -440,16 +443,17 @@ class Exporter(BaseExporter):
|
||||
before = self._parse_entry(original)
|
||||
own = self._own_entry(system, before)
|
||||
items = self._bios_items(files, own)
|
||||
indent = _ENTRY_START.match(original[0]).group(1)
|
||||
one_line = len(original) == 1 and not trailing and not leading
|
||||
if one_line:
|
||||
return [self._entry_line(system, files, before)]
|
||||
return [self._entry_line(system, files, before, indent)]
|
||||
|
||||
runner = "".join(f"{key}, " for key in self._runner_keys(own))
|
||||
head = (
|
||||
f'{_INDENT}"{system.native_id}": '
|
||||
f'{indent}"{system.native_id}": '
|
||||
f'{{ "name": "{self.display_name(system)}", {runner}"biosFiles": ['
|
||||
)
|
||||
pad = " " * (len(_INDENT) + 4)
|
||||
pad = indent + " " * 4
|
||||
lines = [head]
|
||||
for index, item in enumerate(items):
|
||||
key = _note_key(item)
|
||||
@@ -458,7 +462,7 @@ class Exporter(BaseExporter):
|
||||
note = trailing.get(key, "")
|
||||
suffix = f" {note}" if note else ""
|
||||
lines.append(f"{pad}{item}{separator}{suffix}")
|
||||
lines.append(f"{_INDENT}] }},")
|
||||
lines.append(f"{indent}] }},")
|
||||
return lines
|
||||
|
||||
def _patched_entry(
|
||||
|
||||
+23
-4
@@ -576,12 +576,26 @@ def _collect_all_aliases(
|
||||
# naming its content once 64DD_IPL.bin sorted first, and its aliases
|
||||
# 64dd_ipl_J.bin and 64DD_IPL_JP.n64 went with it.
|
||||
holders: dict[str, set[str]] = {}
|
||||
# The files a name designates once case is folded, as the resolver's
|
||||
# name step folds it, a primary and its own .variants/ counted as one
|
||||
# file. dosbox's SC-55 ROM2.BIN found Galaksija's ROM2.BIN the only
|
||||
# exact holder, and its alias ROM2_ST.BIN went to a 4 KiB BASIC ROM,
|
||||
# while SC-55's rom2.bin sat under the same folded name.
|
||||
families: dict[str, set[str]] = {}
|
||||
|
||||
def _hold(name: str, path: str, sha1: str) -> None:
|
||||
holders.setdefault(name, set()).add(sha1)
|
||||
directory = path.rpartition("/")[0].removesuffix("/.variants")
|
||||
families.setdefault(name.casefold(), set()).add(
|
||||
f"{directory}/{variant_base_name(name)}".casefold()
|
||||
)
|
||||
|
||||
for sha1, entry in files.items():
|
||||
holders.setdefault(entry["name"], set()).add(sha1)
|
||||
_hold(entry["name"], entry["path"], sha1)
|
||||
for sha1, copy_list in (copies or {}).items():
|
||||
if sha1 in files:
|
||||
for copy in copy_list:
|
||||
holders.setdefault(copy["name"], set()).add(sha1)
|
||||
_hold(copy["name"], copy["path"], sha1)
|
||||
|
||||
aliases = {}
|
||||
|
||||
@@ -691,11 +705,16 @@ def _collect_all_aliases(
|
||||
]
|
||||
if taken:
|
||||
shadowed.append((entry_name, set(matched), taken))
|
||||
if not matched and len(holders.get(entry_name, ())) == 1:
|
||||
held = holders.get(entry_name, set())
|
||||
if (
|
||||
not matched
|
||||
and len(held) == 1
|
||||
and len(families[entry_name.casefold()]) == 1
|
||||
):
|
||||
# A name carried by several contents names none of them:
|
||||
# quasi88's disk.rom aliases went to whichever
|
||||
# disk.rom the scan met last, a Tandy CoCo ROM.
|
||||
matched = set(holders[entry_name])
|
||||
matched = set(held)
|
||||
for sha in sorted(matched):
|
||||
for alias_name in entry_aliases:
|
||||
_add_alias(alias_name, sha)
|
||||
|
||||
+161
-50
@@ -26,6 +26,7 @@ import argparse
|
||||
import contextlib
|
||||
import functools
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -158,6 +159,9 @@ class PackRecord:
|
||||
self.members: list[str] = []
|
||||
self.files: 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:
|
||||
@@ -173,6 +177,7 @@ def _add_pack_member(zf: zipfile.ZipFile, source_path: str, arcname: str) -> Non
|
||||
"""
|
||||
if isinstance(zf, PackRecord):
|
||||
zf.members.append(arcname)
|
||||
zf.sources[arcname] = source_path
|
||||
return
|
||||
info = zipfile.ZipInfo(filename=arcname, date_time=_FIXED_DATE_TIME)
|
||||
info.compress_type = _member_compression(arcname)
|
||||
@@ -603,6 +608,17 @@ def _pack_data_directories(
|
||||
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(
|
||||
zip_path: str,
|
||||
file_status: dict,
|
||||
@@ -806,6 +822,7 @@ def generate_pack(
|
||||
pack_name: str | None = None,
|
||||
no_game_data: bool = False,
|
||||
record: PackRecord | None = None,
|
||||
within: dict[str, str] | None = None,
|
||||
) -> str | None:
|
||||
"""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,
|
||||
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.
|
||||
"""
|
||||
config = load_platform_config(platform_name, platforms_dir)
|
||||
@@ -867,6 +890,8 @@ def generate_pack(
|
||||
user_provided = []
|
||||
seen_destinations: set[str] = set()
|
||||
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] = (
|
||||
set()
|
||||
) # parent dirs of added files (path conflict detection)
|
||||
@@ -1135,7 +1160,10 @@ def generate_pack(
|
||||
config.get("hash_rule", ""),
|
||||
)
|
||||
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
|
||||
continue
|
||||
if not hash_mismatch_excludes_file(verification_mode) and (
|
||||
@@ -1170,7 +1198,17 @@ def generate_pack(
|
||||
)
|
||||
|
||||
if already_packed:
|
||||
if dedup_key not in seen_destinations:
|
||||
case_twins.append(_flat(full_dest, base_dest, flatten))
|
||||
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)
|
||||
_register_path(dedup_key, seen_destinations, seen_parents)
|
||||
if case_insensitive:
|
||||
@@ -1227,7 +1265,7 @@ def generate_pack(
|
||||
if required_only and fe.get("required") is False:
|
||||
continue
|
||||
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
|
||||
if not dest:
|
||||
continue
|
||||
@@ -1244,10 +1282,13 @@ def generate_pack(
|
||||
# Taken, a case twin on a case-insensitive filesystem, or a
|
||||
# file/directory conflict (SGB1.sfc vs SGB1.sfc/): verify replays
|
||||
# the same rule (packextras.hidden_extras).
|
||||
if placement_block(
|
||||
block = placement_block(
|
||||
full_dest, seen_destinations, seen_lower, seen_parents,
|
||||
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
|
||||
|
||||
# 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)
|
||||
if within is not None and not _same_bytes(local_path, within.get(flat_dest)):
|
||||
continue
|
||||
if local_path.endswith(".zip"):
|
||||
_add_zip_to_pack(local_path, flat_dest, zf, fe)
|
||||
else:
|
||||
@@ -1339,6 +1382,37 @@ def generate_pack(
|
||||
if _registry_path.exists():
|
||||
with open(_registry_path) as _rf:
|
||||
_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(
|
||||
platform_name, platform_display, base_dest, total_files, num_systems,
|
||||
source=source,
|
||||
@@ -1364,6 +1438,10 @@ def generate_pack(
|
||||
) if save_count else ""
|
||||
),
|
||||
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)
|
||||
|
||||
@@ -1408,6 +1486,7 @@ def _extract_zip_to_archive(
|
||||
for name in map(sanitize_pack_path, src.namelist())
|
||||
if name and not name.endswith("/")
|
||||
)
|
||||
target_zf.sources[dest_prefix] = source_zip
|
||||
return
|
||||
for info in src.infolist():
|
||||
if info.is_dir():
|
||||
@@ -1441,6 +1520,7 @@ def _add_zip_to_pack(
|
||||
"""
|
||||
if isinstance(target_zf, PackRecord):
|
||||
target_zf.members.append(dest_path)
|
||||
target_zf.sources[dest_path] = source_zip
|
||||
return
|
||||
_check_zip_members(source_zip)
|
||||
_add_pack_member(target_zf, source_zip, dest_path)
|
||||
@@ -2069,8 +2149,14 @@ def generate_split_packs(
|
||||
one_per_slot: bool = False,
|
||||
offline: bool | None = None,
|
||||
no_game_data: bool = False,
|
||||
records: dict[str, PackRecord] | None = None,
|
||||
) -> 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)
|
||||
# The split directory carries the same tags as the packs inside it, or two
|
||||
# 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}"
|
||||
)
|
||||
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:
|
||||
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
|
||||
# each one the target drops to a refusal naming it as unknown.
|
||||
systems = platform_scope_systems(config, emu_profiles, target_cores)
|
||||
@@ -2130,27 +2242,9 @@ def generate_split_packs(
|
||||
results = []
|
||||
for group_name, group_system_ids in sorted(groups.items()):
|
||||
group_extras = _extras_for_systems(all_extras, group_system_ids)
|
||||
zip_path = generate_pack(
|
||||
platform_name,
|
||||
platforms_dir,
|
||||
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,
|
||||
zip_path = part(
|
||||
f"{part_prefix}_{_name_part(group_name, '_')}_BIOS_Pack.zip",
|
||||
system_filter=group_system_ids, precomputed_extras=group_extras,
|
||||
)
|
||||
if 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]
|
||||
if undistributed:
|
||||
zip_path = generate_pack(
|
||||
platform_name, platforms_dir, 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,
|
||||
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,
|
||||
zip_path = part(
|
||||
f"{part_prefix}_Other_Cores_BIOS_Pack.zip",
|
||||
precomputed_extras=undistributed, extras_only=True,
|
||||
)
|
||||
if zip_path:
|
||||
results.append(zip_path)
|
||||
@@ -4133,7 +4221,8 @@ def _manifest_core_entries(
|
||||
dest = sanitize_pack_path(fe.get("destination", fe["name"]))
|
||||
if not dest:
|
||||
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
|
||||
full_dest = extra_full_destination(fe, extras_pfx)
|
||||
if not full_dest:
|
||||
@@ -4207,7 +4296,7 @@ def _manifest_core_entries(
|
||||
entry, file_size = _manifest_entry(
|
||||
local_path, manifest_dest, [source_emu] if source_emu else [],
|
||||
db, repo_root,
|
||||
selection.fields(dest, fe.get("name", "")),
|
||||
selection.fields(region_key, fe.get("name", "")),
|
||||
)
|
||||
if entry is not None and save_root:
|
||||
entry.update(save_entry_fields(fe))
|
||||
@@ -4321,7 +4410,8 @@ def _manifest_selection(
|
||||
else set()
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@@ -4346,7 +4436,9 @@ def _record_absent(
|
||||
if required_only and fe.get("required") is False:
|
||||
continue
|
||||
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
|
||||
full_dest = extra_full_destination(fe, extras_pfx)
|
||||
if not full_dest:
|
||||
@@ -4517,16 +4609,18 @@ def generate_manifest(
|
||||
# (cache file, size). Settled once the trees are walked.
|
||||
cache_held: dict[str, tuple[str, int]] = {}
|
||||
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(
|
||||
config, emu_profiles, db, zip_contents, data_registry,
|
||||
target_cores=target_cores, source=source,
|
||||
)
|
||||
# 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 {}
|
||||
choices=slot_choices if whole else None,
|
||||
)
|
||||
|
||||
def manifest_destination(full_destination: str) -> str:
|
||||
@@ -4541,8 +4635,10 @@ def generate_manifest(
|
||||
reason: str,
|
||||
cores: list[str] | None,
|
||||
) -> None:
|
||||
bare = sanitize_pack_path(
|
||||
file_entry.get("destination", file_entry.get("name", ""))
|
||||
# A core extra (cores given) is keyed as the region pass keys it.
|
||||
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"
|
||||
omitted_by_destination[full_destination] = {
|
||||
@@ -4949,9 +5045,12 @@ def _manifest_data_directories(
|
||||
(strip_components for a ZIP, source_path for a tarball, exclude) come
|
||||
from _data_dirs.yml, the same refresh_data_dirs applies. Every file
|
||||
carries the sha1 and size the cache holds, so a member the source
|
||||
changed since is refused rather than installed.
|
||||
changed since is refused rather than installed. archive_size is the
|
||||
size of the archive refresh_data_dirs last downloaded, what fetching
|
||||
the tree costs: a tarball whose tree is 10 KB is a download of 21 MB.
|
||||
"""
|
||||
grouped: dict[tuple[str, str], dict] = {}
|
||||
versions: dict[str, dict[str, dict]] = {}
|
||||
for src, full in members:
|
||||
ref, systems, prefix = refs[full]
|
||||
entry = registry.get(ref, {})
|
||||
@@ -4976,6 +5075,17 @@ def _manifest_data_directories(
|
||||
group["source_path"] = str(entry.get("source_path", "")).format(version=version)
|
||||
if entry.get("exclude"):
|
||||
group["exclude"] = [str(e) for e in entry["exclude"]]
|
||||
cache = entry.get("local_cache")
|
||||
if cache:
|
||||
recorded = Path(cache).parent / ".versions.json"
|
||||
if str(recorded) not in versions:
|
||||
try:
|
||||
versions[str(recorded)] = json.loads(recorded.read_text())
|
||||
except FileNotFoundError:
|
||||
versions[str(recorded)] = {}
|
||||
size = versions[str(recorded)].get(ref, {}).get("archive_size")
|
||||
if isinstance(size, int) and size > 0:
|
||||
group["archive_size"] = size
|
||||
group["systems"].extend(s for s in systems if s not in group["systems"])
|
||||
rel = full[len(prefix):].lstrip("/") if prefix else full
|
||||
hashes = compute_hashes(src, frozenset({"sha1"}))
|
||||
@@ -5429,6 +5539,7 @@ from packpaths import ( # noqa: E402,F401
|
||||
from packextras import ( # noqa: E402,F401
|
||||
ExtraResolver,
|
||||
extra_full_destination,
|
||||
extra_region_key,
|
||||
prefixed_destination,
|
||||
save_entry_fields,
|
||||
save_manifest_dest,
|
||||
|
||||
@@ -608,7 +608,7 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
|
||||
"",
|
||||
"## Download BIOS packs",
|
||||
"",
|
||||
"A pack holds everything its platform runs: its own BIOS list plus every file its emulator cores load. Pick your platform, download the ZIP, extract to the BIOS path. Each Download link is a page that keeps its address from one release to the next. The installer above does the same file by file: its screen installs everything, or fewer under **" + _installer_words().TUI_SYSTEMS_ROW + "**, and `--system`, `--core`, `--region` and `--target` make the same choice without questions. For a bare minimum, build your own pack below.",
|
||||
f"A pack holds its platform's own BIOS list plus the files its emulator cores load. What no pack carries yet is in the [gap analysis]({SITE_URL}gaps/). Pick your platform, download the ZIP, extract to the BIOS path. Each Download link is a page that keeps its address from one release to the next. The installer above does the same file by file: its screen installs everything, or fewer under **" + _installer_words().TUI_SYSTEMS_ROW + "**, and `--system`, `--core`, `--region` and `--target` make the same choice without questions. For a bare minimum, build your own pack below.",
|
||||
"",
|
||||
"A pack over 2 GB comes in several parts (`Pack.part1of2.zip`,"
|
||||
" `Pack.part2of2.zip`), and every part is needed. Each part is an"
|
||||
@@ -941,6 +941,10 @@ the most useful contribution.
|
||||
hash differs from the one already held under its name goes to that folder's
|
||||
`.variants/`.
|
||||
|
||||
A ZIP that no list declares as a whole is read file by file, and the report
|
||||
says which of them the collection already holds: upload the files
|
||||
themselves, unzipped.
|
||||
|
||||
From a clone, the file goes to `bios/Manufacturer/Console/filename`, and
|
||||
`python scripts/validate_pr.py path/to/file` names that folder for a file still
|
||||
outside `bios/`, and `--place` copies it there under its declared name. Given a
|
||||
|
||||
+179
-67
@@ -86,9 +86,10 @@ from generate_readme import (
|
||||
extract_targets,
|
||||
release_totals,
|
||||
)
|
||||
from provenance_report import build_report, distinct_missing
|
||||
from provenance_report import build_report, distinct_missing, profile_needs
|
||||
import sitemarkup as markup
|
||||
import siteentries
|
||||
import siteinputs
|
||||
import retired_files
|
||||
import sitestarlight
|
||||
from siteexports import generate_exports_page, load_export_reports
|
||||
@@ -662,7 +663,7 @@ def release_line(platform_name: str, record: dict) -> list[str]:
|
||||
f"[SHA256SUMS.txt]({base}/SHA256SUMS.txt) and its "
|
||||
f"[signature]({base}/SHA256SUMS.txt.sig).",
|
||||
]
|
||||
config = load_platform_config(platform_name, "platforms")
|
||||
config = load_platform_config(platform_name, str(siteinputs.current().platforms))
|
||||
page_version = str(config.get("version") or "")
|
||||
pack_version = _pack_version(pack_name)
|
||||
if page_version and pack_version and page_version.lstrip("v") != pack_version.lstrip("v"):
|
||||
@@ -687,7 +688,7 @@ def setup_version(platform_name: str, record: dict | None = None) -> str:
|
||||
platform YAML was read at. Empty when neither says, or when the
|
||||
platform has no YAML to read a version from."""
|
||||
try:
|
||||
config = load_platform_config(platform_name, "platforms")
|
||||
config = load_platform_config(platform_name, str(siteinputs.current().platforms))
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
pack_name = release_record.pack_name_for(platform_name, record or {})
|
||||
@@ -714,8 +715,7 @@ NO_ZIP_FAQ = "wiki/faq.md#why-is-there-no-zip-for-my-setup"
|
||||
|
||||
def has_release_pack(platform_name: str) -> bool:
|
||||
"""Whether a release publishes a ZIP for the platform (`release_pack`)."""
|
||||
entry = load_platform_registry("platforms").get(platform_name) or {}
|
||||
return entry.get("release_pack") is True
|
||||
return platform_name in _release_pack_platforms()
|
||||
|
||||
|
||||
def no_zip_line(display: str) -> str:
|
||||
@@ -734,7 +734,7 @@ def get_page_title(platform_name: str, display: str) -> str:
|
||||
the system's own download. A setup that is also an emulator (BizHawk)
|
||||
names it last: "BizHawk BIOS files" is the title of its emulator page."""
|
||||
kind = "BIOS pack" if has_release_pack(platform_name) else "BIOS files"
|
||||
if (_REPO_ROOT / "emulators" / f"{platform_name}.yml").is_file():
|
||||
if (siteinputs.current().emulators / f"{platform_name}.yml").is_file():
|
||||
return f"{kind} for {display}"
|
||||
return f"{display} {kind}"
|
||||
|
||||
@@ -779,7 +779,7 @@ def _pack_buttons(platform_name: str, label: str = "Pack", prefix: str = "../",
|
||||
frontend user asks. A setup with no release pack gets the line that
|
||||
says so, and why."""
|
||||
if not has_release_pack(platform_name):
|
||||
display = display or load_platform_config(platform_name, "platforms").get(
|
||||
display = display or load_platform_config(platform_name, str(siteinputs.current().platforms)).get(
|
||||
"platform", platform_name)
|
||||
return f"{no_zip_line(display)} [Why no ZIP?]({prefix}{NO_ZIP_FAQ})"
|
||||
record = release_record.load_advertised()
|
||||
@@ -804,7 +804,7 @@ def generate_get_page(platform_name: str) -> str:
|
||||
the files of the pack the current release holds, read from release.json,
|
||||
or the command when no release publishes one. The release process
|
||||
never touches it: committing release.json rebuilds it."""
|
||||
config = load_platform_config(platform_name, "platforms")
|
||||
config = load_platform_config(platform_name, str(siteinputs.current().platforms))
|
||||
display = config.get("platform", platform_name)
|
||||
record = release_record.load_advertised()
|
||||
pack = release_record.pack_for(platform_name, record)
|
||||
@@ -823,7 +823,7 @@ def generate_get_page(platform_name: str) -> str:
|
||||
if released and pack and pack.get("assets"):
|
||||
parts = len(pack["assets"])
|
||||
release = release_line(platform_name, record)
|
||||
extract_to = (load_platform_registry("platforms").get(platform_name) or {}).get("extract_to")
|
||||
extract_to = (load_platform_registry(str(siteinputs.current().platforms)).get(platform_name) or {}).get("extract_to")
|
||||
lines += [
|
||||
"This address stays the same. It always gives the latest ZIP.",
|
||||
"",
|
||||
@@ -872,7 +872,7 @@ def generate_get_page(platform_name: str) -> str:
|
||||
def generate_get_index() -> str:
|
||||
"""get/: the permanent page of every setup, grouped as the home tiles
|
||||
are. A link trimmed to /get/ answered 404."""
|
||||
registry = load_platform_registry("platforms")
|
||||
registry = load_platform_registry(str(siteinputs.current().platforms))
|
||||
lines = [
|
||||
"---", "search: exclude",
|
||||
'description: "The BIOS files of every setup, one page each. Each address stays the same."',
|
||||
@@ -886,7 +886,7 @@ def generate_get_index() -> str:
|
||||
continue
|
||||
lines += [f"## {label}", ""]
|
||||
for name in names:
|
||||
display = load_platform_config(name, "platforms").get("platform", name)
|
||||
display = load_platform_config(name, str(siteinputs.current().platforms)).get("platform", name)
|
||||
lines.append(f"- [{get_page_title(name, display)}]({name}.md)")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
@@ -1067,10 +1067,13 @@ def _split_contract(config: dict) -> tuple[dict, dict]:
|
||||
return contract, registry
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _release_pack_platforms() -> frozenset[str]:
|
||||
return frozenset(list_release_pack_platforms(
|
||||
str(_REPO_ROOT / "platforms"), include_archived=True))
|
||||
return _release_packs_in(str(siteinputs.current().platforms))
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def _release_packs_in(platforms_dir: str) -> frozenset[str]:
|
||||
return frozenset(list_release_pack_platforms(platforms_dir, include_archived=True))
|
||||
|
||||
|
||||
# Per-entry fields a folder check needs beyond the hashes: an md5 is a ZIP
|
||||
@@ -1617,6 +1620,7 @@ def generate_data_exports(
|
||||
|
||||
members = data_directory_members()
|
||||
locations = file_location_items(db, profiles, members)
|
||||
wanted = _provenance_wanted_rows(db)
|
||||
envelopes = {
|
||||
"platforms.json": ("platforms", platform_items),
|
||||
"emulators.json": ("emulators", emulator_items),
|
||||
@@ -1625,7 +1629,7 @@ def generate_data_exports(
|
||||
}
|
||||
write_lookup_shards(
|
||||
api, file_lookup_records(db, coverages, profiles, locations, members, consumers,
|
||||
retired_files.load(_REPO_ROOT / "retired.json")),
|
||||
retired_files.load(_REPO_ROOT / "retired.json"), wanted),
|
||||
generated_at, commit,
|
||||
)
|
||||
for filename, (kind, items) in envelopes.items():
|
||||
@@ -1744,7 +1748,7 @@ def generate_data_exports(
|
||||
_record(downloads / "retrobios.sqlite")
|
||||
write_if_changed(
|
||||
str(downloads / "provenance-wanted.csv"),
|
||||
_csv_document(list(WANTED_FIELDS), _provenance_wanted_rows(db)),
|
||||
_csv_document(list(WANTED_FIELDS), wanted),
|
||||
)
|
||||
|
||||
assets = [
|
||||
@@ -2104,7 +2108,7 @@ def _short_sha256(item: dict, catalog: str) -> str:
|
||||
|
||||
def _platforms_without_a_list(platforms_dir: Path | None = None) -> list[str]:
|
||||
"""The names of the platforms whose registry says they keep no list."""
|
||||
platforms_dir = platforms_dir or _REPO_ROOT / "platforms"
|
||||
platforms_dir = platforms_dir or siteinputs.current().platforms
|
||||
names = []
|
||||
for key, entry in sorted(load_platform_registry(str(platforms_dir)).items()):
|
||||
if entry.get("native_list") != "none":
|
||||
@@ -3236,7 +3240,9 @@ def _front_matter(markdown: str) -> dict:
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
_SHA1_BY_PREFIX: dict[str, str] = {}
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def _sha1_by_prefix(db_path: str) -> dict[str, str]:
|
||||
return {sha1[:12]: sha1 for sha1 in load_database(db_path)["files"]}
|
||||
|
||||
|
||||
def _cards_elsewhere(markdown: str) -> str:
|
||||
@@ -3247,11 +3253,9 @@ def _cards_elsewhere(markdown: str) -> str:
|
||||
away = {ident for ident in links if f'id="{ident}"' not in markdown}
|
||||
if not away:
|
||||
return markdown
|
||||
if not _SHA1_BY_PREFIX:
|
||||
db = load_database(str(_REPO_ROOT / "database.json"))
|
||||
_SHA1_BY_PREFIX.update({sha1[:12]: sha1 for sha1 in db["files"]})
|
||||
by_prefix = _sha1_by_prefix(str(siteinputs.current().db))
|
||||
for ident in away:
|
||||
full = _SHA1_BY_PREFIX[ident.removeprefix("sha1-")]
|
||||
full = by_prefix[ident.removeprefix("sha1-")]
|
||||
markdown = markdown.replace(f"](#{ident})", f"]({markup.site_href(f'files/?sha1={full}')})")
|
||||
return markdown
|
||||
|
||||
@@ -3562,10 +3566,14 @@ def _list_pages(
|
||||
f"[part {i}]({other}.md)" for i, other in enumerate(stems, 1))
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _collection_sha1_by_path() -> dict[str, str]:
|
||||
"""The sha1 of each file the collection holds, by its path."""
|
||||
with open(_REPO_ROOT / "database.json", encoding="utf-8") as handle:
|
||||
return _sha1_by_path_in(str(siteinputs.current().db))
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def _sha1_by_path_in(db_path: str) -> dict[str, str]:
|
||||
with open(db_path, encoding="utf-8") as handle:
|
||||
files = json.load(handle).get("files", {})
|
||||
return {str(entry.get("path", "")): sha1 for sha1, entry in files.items()}
|
||||
|
||||
@@ -3595,7 +3603,7 @@ def _system_named_link(sys_id: str, prefix: str = "") -> str:
|
||||
def _emulator_named_link(key: str, prefix: str = "") -> str:
|
||||
"""An emulator by the name its page carries, linked to that page. A
|
||||
profile with no page (an alias, a test) is named only."""
|
||||
profile = load_emulator_profiles(str(_REPO_ROOT / "emulators"), skip_aliases=False).get(key, {})
|
||||
profile = _profiles_on_disk().get(key, {})
|
||||
label = _escape_tags(str(profile.get("emulator") or key))
|
||||
if profile.get("type") in ("alias", "test"):
|
||||
return label
|
||||
@@ -3985,10 +3993,9 @@ def _platform_data_links(name: str) -> str:
|
||||
return ", ".join(links)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _site_profiles() -> dict[str, dict]:
|
||||
"""Every emulator profile, read once for the citations of a page."""
|
||||
return load_emulator_profiles(str(_REPO_ROOT / "emulators"))
|
||||
return _profiles_in(str(siteinputs.current().emulators), skip_aliases=True)
|
||||
|
||||
|
||||
# Whose code a source reference cites, in one word: a platform page names
|
||||
@@ -4072,13 +4079,17 @@ def _category_evidence_md(evidence: dict, launched: list[str] | None = None) ->
|
||||
return f"No reader found ({cores})"
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _profiles_by_core_name() -> dict[str, list[str]]:
|
||||
"""Core name as a platform writes it -> the profiles with a page for it.
|
||||
|
||||
MinUI writes `mednafen_vb`, the profile and its page are `beetle_vb`.
|
||||
"""
|
||||
profiles = load_emulator_profiles(str(_REPO_ROOT / "emulators"))
|
||||
return _core_names_in(str(siteinputs.current().emulators))
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def _core_names_in(emulators_dir: str) -> dict[str, list[str]]:
|
||||
profiles = _profiles_in(emulators_dir, skip_aliases=True)
|
||||
with_page = {k for k, v in profiles.items() if v.get("type") not in ("alias", "test")}
|
||||
return {
|
||||
core: sorted(k for k in keys if k in with_page)
|
||||
@@ -4248,7 +4259,7 @@ def _card_route(name: str, display: str, guide: Guide) -> list[str]:
|
||||
sees it is the reader's to name: no guide cites it."""
|
||||
if guide.on_card or _offline_line(name):
|
||||
return []
|
||||
extract_to = (load_platform_registry("platforms").get(name) or {}).get("extract_to")
|
||||
extract_to = (load_platform_registry(str(siteinputs.current().platforms)).get(name) or {}).get("extract_to")
|
||||
leaf = (extract_to or "bios").rstrip("/").rsplit("/", 1)[-1].lstrip("~") or "bios"
|
||||
tabs = guide.card_run or ("windows", "macos", "linux")
|
||||
args = {key: f"--platform {name} --dest "
|
||||
@@ -4449,7 +4460,7 @@ def generate_handheld_guide(guide: HandheldGuide) -> str:
|
||||
A handheld owner starts from the device name. The systems were named
|
||||
only in a closed note, with a path seen from inside the device."""
|
||||
uses = GUIDES[guide.uses]
|
||||
display = load_platform_config(guide.uses, "platforms").get("platform", guide.uses)
|
||||
display = load_platform_config(guide.uses, str(siteinputs.current().platforms)).get("platform", guide.uses)
|
||||
args = {"windows": f"--platform {guide.uses} --dest E:\\BIOS",
|
||||
"macos": f"--platform {guide.uses} --dest /path/to/card/BIOS",
|
||||
"linux": f"--platform {guide.uses} --dest /path/to/card/BIOS"}
|
||||
@@ -4504,7 +4515,7 @@ def _platform_guide(name: str, display: str, cov: dict, registry: dict, no_list:
|
||||
lines.append("")
|
||||
for note in guide.notes:
|
||||
lines += [note, ""]
|
||||
lines += [line for note in extract_notes("platforms", released_only=True)
|
||||
lines += [line for note in extract_notes(str(siteinputs.current().platforms), released_only=True)
|
||||
if f"The {display} pack " in note
|
||||
for line in (note, "")]
|
||||
if guide.more_folders:
|
||||
@@ -4979,7 +4990,7 @@ def _data_member_digests(
|
||||
from hashing import compute_hashes # noqa: PLC0415
|
||||
|
||||
if data_registry is None:
|
||||
data_registry = load_data_dir_registry(str(root / "platforms"))
|
||||
data_registry = load_data_dir_registry(str(siteinputs.current().platforms))
|
||||
found: dict[str, dict[str, str]] = {}
|
||||
for ref, tree in sorted(members.items()):
|
||||
cache = (data_registry.get(ref) or {}).get("local_cache")
|
||||
@@ -5062,12 +5073,14 @@ def file_lookup_records(
|
||||
members: dict | None = None,
|
||||
consumers: FileConsumers | None = None,
|
||||
retired: dict | None = None,
|
||||
wanted: list[dict] | None = None,
|
||||
) -> list[dict]:
|
||||
"""Every hash the site knows, one record per value, for files/?hash=.
|
||||
|
||||
The collection's own files lead to their card, a platform's declared
|
||||
value to the system section of its page, a profile's to the entry's
|
||||
card. The site search indexed these values as text, 4 MB of the 21 MB
|
||||
card, a wanted dump (``wanted``, the provenance-wanted.csv rows) to
|
||||
its catalog on the provenance page. The site search indexed these values as text, 4 MB of the 21 MB
|
||||
it made every visitor download: the lookup reads one shard instead.
|
||||
"""
|
||||
files = db.get("files", {})
|
||||
@@ -5140,6 +5153,7 @@ def file_lookup_records(
|
||||
**({"system_name": entry_label} if entry_label else {}),
|
||||
})
|
||||
records += retired_records(retired or {}, files, locations)
|
||||
records += wanted_records(wanted or [])
|
||||
records += _held_under_every_hash(records, files, db.get("indexes") or {})
|
||||
for record in records:
|
||||
if record["layer"] in ("platform", "emulator"):
|
||||
@@ -5151,6 +5165,23 @@ def file_lookup_records(
|
||||
r["value"], r["layer"], r.get("owner", ""), r["name"], r["url"]))
|
||||
|
||||
|
||||
def wanted_records(rows: list[dict]) -> list[dict]:
|
||||
"""A record under each hash of a dump a catalog lists and the collection
|
||||
lacks, from the rows of provenance-wanted.csv. A pasted hash of a
|
||||
wanted dump (Redump's ps2-dvd-212k.bin) answered that nothing the site
|
||||
knows has it, and never said the dump is wanted."""
|
||||
records = []
|
||||
for row in rows:
|
||||
label = _PROVENANCE_LABELS.get(row["catalog"], row["catalog"])
|
||||
url = _site_path(f"provenance.md#{_slugify_anchor(label)}")
|
||||
for algorithm in ("sha1", "md5", "crc32"):
|
||||
value = str(row.get(algorithm) or "").lower()
|
||||
if value:
|
||||
records.append({"value": value, "algorithm": algorithm, "layer": "wanted",
|
||||
"owner": label, "name": row["name"], "url": url})
|
||||
return records
|
||||
|
||||
|
||||
def _read_by_name(records: list[dict], files: dict, consumers: FileConsumers) -> list[dict]:
|
||||
"""The emulators the file's card names that no declared hash reached.
|
||||
|
||||
@@ -5836,6 +5867,13 @@ def _wanted_list(source: str, label: str, data: dict, by_dat: dict[str, list],
|
||||
_RELEASE_ASSET_NAMES: dict[str, str] | None = None
|
||||
|
||||
|
||||
def _download_name(path: str) -> str:
|
||||
"""The name a browser saves a collection file under: the last segment
|
||||
of its download URL. Neither host sends a Content-Disposition, and the
|
||||
download attribute is ignored on another origin."""
|
||||
return urllib.parse.unquote(_fetch_url(path).rsplit("/", 1)[-1])
|
||||
|
||||
|
||||
def _fetch_url(path: str) -> str:
|
||||
"""Where a collection file downloads from: its raw URL, or the
|
||||
large-files release asset when .gitignore keeps it out of git."""
|
||||
@@ -5867,7 +5905,7 @@ def _platform_display(name: str) -> str:
|
||||
display = _platform_names.get(name)
|
||||
if display is None:
|
||||
try:
|
||||
config = load_platform_config(name, str(_REPO_ROOT / "platforms"))
|
||||
config = load_platform_config(name, str(siteinputs.current().platforms))
|
||||
except FileNotFoundError:
|
||||
config = {}
|
||||
display = _platform_names[name] = str(config.get("platform") or name)
|
||||
@@ -6210,6 +6248,12 @@ def _system_file_card(
|
||||
# Each fact: its label, its values, whether the search reads it. What a
|
||||
# setup does with the file comes first, the fingerprints last.
|
||||
facts: list[tuple[str, list[str], bool]] = []
|
||||
saved = _download_name(f["path"]) if f.get("path") else name
|
||||
if saved != name:
|
||||
# A .variants/ copy is stored as advision.zip.1c086064, and the
|
||||
# browser names the download after the URL: copied as it came, the
|
||||
# file a platform pins was not found under its name.
|
||||
facts.append(("Save as", [f"{_code(name)}, the download is named {_code(saved)}"], False))
|
||||
if plats:
|
||||
# Each link lands on the platform's system section, and says where
|
||||
# that platform puts the file. Out of the search: the card's title
|
||||
@@ -6268,7 +6312,7 @@ def _system_file_card(
|
||||
# What a system card shows at a glance: who lists the file and who reads it.
|
||||
# Its fingerprints and the rest open on demand, so a page of 250 cards reads
|
||||
# as a list of files, not a wall of hashes.
|
||||
_CARD_KEY_FACTS = ("On the setup's list", "Packed for its emulators by", "Emulators")
|
||||
_CARD_KEY_FACTS = ("Save as", "On the setup's list", "Packed for its emulators by", "Emulators")
|
||||
_CARD_HASHES = ("SHA1", "MD5", "CRC32", "SHA256")
|
||||
# The words a card's fold names its other facts by.
|
||||
_CARD_MORE_WORDS = {"Romset": "romset", "Collection path": "collection path",
|
||||
@@ -6664,11 +6708,13 @@ def _reading_pairs(sha1: str, consumers: FileConsumers) -> list[tuple[str, dict]
|
||||
return pairs
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _profiles_on_disk() -> dict[str, dict]:
|
||||
from common import load_emulator_profiles # noqa: PLC0415
|
||||
return _profiles_in(str(siteinputs.current().emulators), skip_aliases=False)
|
||||
|
||||
return load_emulator_profiles(str(_REPO_ROOT / "emulators"), skip_aliases=False)
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def _profiles_in(emulators_dir: str, skip_aliases: bool) -> dict[str, dict]:
|
||||
return load_emulator_profiles(emulators_dir, skip_aliases=skip_aliases)
|
||||
|
||||
|
||||
def _entry_systems(emulator: str, entry: dict) -> list[str]:
|
||||
@@ -8212,6 +8258,26 @@ def _exact_size(size: int) -> str:
|
||||
return _fmt_size(size) if size < 1024 else f"{_fmt_size(size)} ({size:,} bytes)"
|
||||
|
||||
|
||||
def _build_file_links(pin_build: list[dict]) -> list[str]:
|
||||
"""Each platform build file that holds the revision, linked at the
|
||||
commit it was read at when the source record holds it. The prose of
|
||||
pin_frozen named the file as plain text."""
|
||||
from sitesources import _browse_repo, _source_permalink, tree_holds # noqa: PLC0415
|
||||
|
||||
links = []
|
||||
for build in pin_build:
|
||||
repo = _browse_repo(str(build["repo"]))
|
||||
path = str(build["path"])
|
||||
revision = str(build["revision"])
|
||||
text = f"{_escape_tags(_platform_display(str(build['platform'])))} {_code(path)}"
|
||||
# Linked where the record saw the file at that commit, as every
|
||||
# source link is: a tag can move, and is never recorded.
|
||||
if repo is not None and tree_holds(repo, revision, path):
|
||||
text = f"[{text}]({_source_permalink(repo, revision, path, None, None)})"
|
||||
links.append(f"{text} at {_code(str(build['revision']))} sets {_code(str(build['variable']))}")
|
||||
return links
|
||||
|
||||
|
||||
def _build_rows(profile: dict) -> list[str]:
|
||||
"""Meta card rows for who ships the build, how its files are found, why
|
||||
its revision is held and where its releases are announced."""
|
||||
@@ -8232,6 +8298,9 @@ def _build_rows(profile: dict) -> list[str]:
|
||||
if frozen:
|
||||
held = _escape_tags(' '.join(str(frozen).split())).replace('|', r'\|')
|
||||
rows.append(f"| {label} | {held} |")
|
||||
builds = _build_file_links(profile.get("pin_build") or [])
|
||||
if builds:
|
||||
rows.append(f"| Build file | {'<br>'.join(builds)} |")
|
||||
announced = profile.get("version_source")
|
||||
if isinstance(announced, dict) and announced.get("url"):
|
||||
url = str(announced["url"])
|
||||
@@ -8483,12 +8552,7 @@ def _render_emulator_file(
|
||||
if fnote:
|
||||
lines.append(f"- {data_root_words(_escape_tags(link_prose_citations(profile, str(fnote))), data_href)}")
|
||||
if not in_repo and f.get("unsourceable"):
|
||||
# The badge alone sent readers after a dump that cannot exist, and the
|
||||
# reason was only on the gaps page, which the card did not link.
|
||||
lines.append(
|
||||
f"- Not collectable: {_escape_tags(str(f['unsourceable']))} "
|
||||
"([acknowledged gaps](../gaps.md#acknowledged-gaps))"
|
||||
)
|
||||
lines.append(f"- {_unsourceable_line(f)}")
|
||||
lines.extend(_contents_lines(contents))
|
||||
lines[body_start:] = _search_skipped_apart(lines[body_start:])
|
||||
lines.append("")
|
||||
@@ -8600,12 +8664,7 @@ def _emulator_card_folds(
|
||||
open_lines.append(data_root_words(
|
||||
_escape_tags(link_prose_citations(profile, str(fnote))), data_href))
|
||||
if not in_repo and f.get("unsourceable"):
|
||||
# The badge alone sent readers after a dump that cannot exist, and the
|
||||
# reason was only on the gaps page, which the card did not link.
|
||||
open_lines.append(
|
||||
f"Not collectable: {_escape_tags(str(f['unsourceable']))} "
|
||||
"([acknowledged gaps](../gaps.md#acknowledged-gaps))"
|
||||
)
|
||||
open_lines.append(_unsourceable_line(f))
|
||||
lines = [f"- {d}" for d in open_lines]
|
||||
shut_fold = [f"- {d}" for d in hashes + [m for m in more if skipped(m)]]
|
||||
read_fold = [f"- {m}" for m in more if not skipped(m)]
|
||||
@@ -8988,10 +9047,14 @@ def emulator_install_route(name: str, profile: dict) -> tuple[str, list[str], tu
|
||||
return command, setups, oses
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _standalone_cores(platform: str) -> frozenset[str]:
|
||||
"""The emulators a setup launches as apps, by its platform file."""
|
||||
config = load_platform_config(platform, str(_REPO_ROOT / "platforms"))
|
||||
return _standalone_cores_in(str(siteinputs.current().platforms), platform)
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _standalone_cores_in(platforms_dir: str, platform: str) -> frozenset[str]:
|
||||
config = load_platform_config(platform, platforms_dir)
|
||||
return frozenset(str(c) for c in config.get("standalone_cores") or [])
|
||||
|
||||
|
||||
@@ -9478,7 +9541,7 @@ def _hash_spans(value: str | list, copy: bool = False) -> str:
|
||||
|
||||
|
||||
def _availability_check(
|
||||
db: dict, data_names: set[str] | None
|
||||
db: dict, data_names: set[str] | None, owner: str = "",
|
||||
) -> Callable[[dict], bool]:
|
||||
"""A predicate answering whether the collection holds a file.
|
||||
|
||||
@@ -9509,7 +9572,7 @@ def _availability_check(
|
||||
# under the name belongs to another game or release.
|
||||
if not f.get("name") or f.get("unsourceable"):
|
||||
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
|
||||
|
||||
@@ -10242,7 +10305,7 @@ def emulator_pages(
|
||||
lines.append("")
|
||||
lines.extend(markup.aside("info", "Why no files", [_escape_tags(str(exclusion))])[:-1])
|
||||
else:
|
||||
_file_available = _availability_check(db, data_names)
|
||||
_file_available = _availability_check(db, data_names, name)
|
||||
|
||||
lines.extend(["## Files", ""])
|
||||
lines.extend(_emulator_file_summary(files))
|
||||
@@ -10505,6 +10568,33 @@ def _hash_cell(hashes: dict[str, list[str]]) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _unsourceable_line(f: dict) -> str:
|
||||
"""Why the collection holds no copy of a file a profile marks
|
||||
`unsourceable:`, linked to the list of them. The flag only keeps a
|
||||
same-named file from being served in its place: the file is still
|
||||
wanted, and "Not collectable" sent readers away from KANJI2.ROM, which
|
||||
MAME's fm7740sx.zip carries."""
|
||||
return (f"Not collected yet: {_escape_tags(str(f['unsourceable']))} "
|
||||
"([files not collected yet](../gaps.md#not-collected-yet))")
|
||||
|
||||
|
||||
def _catalogued_needs(report: dict | None) -> dict[tuple[str, str], list[str]]:
|
||||
"""(profile, file name casefolded) -> where a dump catalog or an
|
||||
imported romset lists that file, from a provenance report built with
|
||||
the profiles' needs."""
|
||||
out: dict[tuple[str, str], list[str]] = {}
|
||||
for source, data in sorted((report or {}).items()):
|
||||
label = _PROVENANCE_LABELS.get(source, source)
|
||||
for entry in data.get("missing", []):
|
||||
where = [f"{label} `{_escape_tags(str(entry.get('name', '')))}`"]
|
||||
where += [f"{_escape_tags(str(hit['source']))} `{_escape_tags(str(hit['archive']))}`"
|
||||
for hit in entry.get("romsets", [])]
|
||||
for need in entry.get("needed_by", []):
|
||||
held = out.setdefault((need["emulator"], str(need["name"]).casefold()), [])
|
||||
held += [w for w in where if w not in held]
|
||||
return out
|
||||
|
||||
|
||||
def _reason_cell(text: str) -> str:
|
||||
"""A reason in a gaps table, in the class that keeps its column readable."""
|
||||
return f'<span class="rb-reason">{text}</span>' if text else "-"
|
||||
@@ -10799,9 +10889,14 @@ def generate_gap_analysis(
|
||||
registry: dict | None = None,
|
||||
gap_report: dict | None = None,
|
||||
moved: dict[str, str] | None = None,
|
||||
catalogued: dict | None = None,
|
||||
) -> str:
|
||||
"""Generate a unified gap analysis page.
|
||||
|
||||
``catalogued`` is a provenance report built with the profiles' needs:
|
||||
a file the collection does not hold yet names the catalog or romset
|
||||
that lists it.
|
||||
|
||||
With ``moved``, the list of files to source goes to pages of its own,
|
||||
keyed by stem: on the page it made 662 KB of HTML.
|
||||
|
||||
@@ -11232,7 +11327,7 @@ def generate_gap_analysis(
|
||||
"",
|
||||
])
|
||||
|
||||
# ---- Section 4: Acknowledged gaps (unsourceable files) ----
|
||||
# ---- Section 4: files the collection does not hold yet (unsourceable) ----
|
||||
|
||||
all_unsourceable: list[dict] = []
|
||||
for emu_name, data in sorted(report_all.items()):
|
||||
@@ -11246,19 +11341,26 @@ def generate_gap_analysis(
|
||||
})
|
||||
|
||||
if all_unsourceable:
|
||||
listed = _catalogued_needs(catalogued)
|
||||
# The old heading's address still leads here.
|
||||
lines.extend([
|
||||
"## Acknowledged gaps",
|
||||
'<span id="acknowledged-gaps"></span>',
|
||||
"",
|
||||
f"{len(all_unsourceable)} files documented as unsourceable "
|
||||
"(verified from source code).",
|
||||
"## Not collected yet",
|
||||
"",
|
||||
"| File | Emulator | Reason | Source ref |",
|
||||
"|------|----------|--------|-----------|",
|
||||
f"{len(all_unsourceable)} files the collection does not hold. A file "
|
||||
"with the same name is never served in their place. Each is still "
|
||||
"wanted, and the last column names a catalog or romset that lists it.",
|
||||
"",
|
||||
"| File | Emulator | Reason | Source ref | Listed in |",
|
||||
"|------|----------|--------|-----------|-----------|",
|
||||
])
|
||||
for u in sorted(all_unsourceable, key=lambda x: x["name"]):
|
||||
where = listed.get((u["emu_key"], str(u["name"]).casefold()), [])
|
||||
lines.append(
|
||||
f"| `{u['name']}` | {u['emulator']} | {_reason_cell(u['reason'])} "
|
||||
f"| {_source_ref_markdown(profiles[u['emu_key']], u['source_ref'])} |"
|
||||
f"| {_source_ref_markdown(profiles[u['emu_key']], u['source_ref'])} "
|
||||
f"| {', '.join(where) or '-'} |"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
@@ -11844,7 +11946,7 @@ def _own_root_folders(registry: dict) -> dict[str, str]:
|
||||
destination carries its root in every path."""
|
||||
folders = {}
|
||||
for key, entry in registry.items():
|
||||
config = load_platform_config(key, "platforms")
|
||||
config = load_platform_config(key, str(siteinputs.current().platforms))
|
||||
if config.get("base_destination") == "" and entry.get("extract_to"):
|
||||
folders[key] = entry["extract_to"]
|
||||
return folders
|
||||
@@ -11944,9 +12046,10 @@ def generate_which_pack() -> str:
|
||||
|
||||
The tiles are the choice. Open tables under them listed every setup a
|
||||
second time and made the page 11,800 px tall on a phone."""
|
||||
registry = load_platform_registry("platforms")
|
||||
registry = load_platform_registry(str(siteinputs.current().platforms))
|
||||
record = release_record.load_advertised()
|
||||
display = {key: load_platform_config(key, "platforms").get("platform", key) for key in registry}
|
||||
display = {key: load_platform_config(key, str(siteinputs.current().platforms)).get("platform", key)
|
||||
for key in registry}
|
||||
all_tabs = ("windows", "macos", "linux", "steamdeck", "android")
|
||||
lines = [
|
||||
"---",
|
||||
@@ -12322,6 +12425,12 @@ def main() -> None:
|
||||
|
||||
def _generate(args: argparse.Namespace) -> None:
|
||||
|
||||
# Every page reads these, never the repository's own files.
|
||||
siteinputs.use(siteinputs.SiteInputs(
|
||||
db=Path(args.db).resolve(),
|
||||
emulators=Path(args.emulators_dir).resolve(),
|
||||
platforms=Path(args.platforms_dir).resolve(),
|
||||
))
|
||||
db = load_database(args.db)
|
||||
docs = Path(args.docs_dir)
|
||||
# Files the site serves as they are (data, downloads, images), in the
|
||||
@@ -12602,12 +12711,16 @@ def _generate(args: argparse.Namespace) -> None:
|
||||
sitestarlight.MOVED_PAGES.setdefault(
|
||||
f"cross-reference-{pname}.md", f"platforms/{pname}.md")
|
||||
|
||||
provenance_report = build_report(
|
||||
db, load_provenance_snapshots(), needs=profile_needs(profiles, db))
|
||||
|
||||
print("Generating gap analysis page...")
|
||||
gap_pages: dict[str, str] = {}
|
||||
write_if_changed(
|
||||
str(docs / "gaps.md"),
|
||||
tables_out_of_search(generate_gap_analysis(
|
||||
profiles, coverages, db, suppl_names, registry, gap_report, gap_pages,
|
||||
catalogued=provenance_report,
|
||||
)) + reference_next(),
|
||||
)
|
||||
for stem, page in gap_pages.items():
|
||||
@@ -12623,7 +12736,6 @@ def _generate(args: argparse.Namespace) -> None:
|
||||
)
|
||||
|
||||
print("Generating dump provenance page...")
|
||||
provenance_report = build_report(db, load_provenance_snapshots())
|
||||
provenance_pages: dict[str, str] = {}
|
||||
write_if_changed(
|
||||
str(docs / "provenance.md"),
|
||||
|
||||
+15
-12
@@ -14,13 +14,13 @@ import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import (
|
||||
build_target_cores_cache,
|
||||
platform_arg,
|
||||
list_registered_platforms,
|
||||
load_data_dir_registry,
|
||||
load_database,
|
||||
load_emulator_profiles,
|
||||
load_platform_config,
|
||||
load_target_config,
|
||||
require_directory,
|
||||
require_platform,
|
||||
require_yaml,
|
||||
@@ -97,6 +97,19 @@ def main(argv: list[str] | None = None) -> None:
|
||||
require_platform(args.platform, args.platforms_dir)
|
||||
platforms = [args.platform]
|
||||
|
||||
# A named platform without that target is refused, as verify and
|
||||
# generate_pack refuse it: skipped, it printed Done. and exited 0 with
|
||||
# no model written. --all leaves such platforms out and says so.
|
||||
target_cores_cache: dict[str, set[str] | None] = {}
|
||||
if args.target:
|
||||
try:
|
||||
target_cores_cache, platforms = build_target_cores_cache(
|
||||
platforms, args.target, args.platforms_dir, is_all=args.all
|
||||
)
|
||||
except (FileNotFoundError, ValueError) as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# A target-narrowed model is a different artifact: written under the
|
||||
# platform's own name it overwrote the full one, and diff_truth then
|
||||
# compared a narrowed model against the whole scrape. Its directory is
|
||||
@@ -109,17 +122,7 @@ def main(argv: list[str] | None = None) -> None:
|
||||
output_dir, [name], args.target, args.platforms_dir
|
||||
)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
target_cores: set[str] | None = None
|
||||
if args.target:
|
||||
try:
|
||||
target_cores = load_target_config(
|
||||
name,
|
||||
args.target,
|
||||
args.platforms_dir,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
print(f" {name}: no target config, skipped")
|
||||
continue
|
||||
target_cores = target_cores_cache.get(name)
|
||||
|
||||
# Load platform config (with inheritance) and registry entry
|
||||
try:
|
||||
|
||||
+73
-11
@@ -12,6 +12,7 @@ import os
|
||||
import sys
|
||||
import re
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
@@ -26,6 +27,13 @@ from hashing import compute_hashes
|
||||
LARGE_FILES_RELEASE = "large-files"
|
||||
LARGE_FILES_REPO = "Abdess/retrobios"
|
||||
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"
|
||||
|
||||
_UNSAFE_ASSET_CHARS = re.compile(r"[^A-Za-z0-9._-]+")
|
||||
@@ -169,14 +177,14 @@ def _fetch_asset(
|
||||
|
||||
if os.path.isdir(cached):
|
||||
return None
|
||||
if os.path.exists(cached) and not (expected_sha1 or expected_md5):
|
||||
return cached
|
||||
if os.path.exists(cached):
|
||||
snapshot = _read_once(cached)
|
||||
if snapshot is None:
|
||||
pass
|
||||
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):
|
||||
# A copy of this asset that answers another hash: the caller
|
||||
# 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)
|
||||
if not downloaded:
|
||||
return None
|
||||
if (expected_sha1 or expected_md5) and not _matches(
|
||||
compute_hashes(tmp_path), expected_sha1, expected_md5
|
||||
):
|
||||
snapshot = _read_once(tmp_path)
|
||||
if snapshot is None or not _matches(snapshot[0], expected_sha1, expected_md5):
|
||||
return None
|
||||
set_plain_mode(tmp_path)
|
||||
pinned = _pin_verified(tmp_path, snapshot, dest_dir, name)
|
||||
os.replace(tmp_path, cached)
|
||||
return cached
|
||||
_prune_verified(dest_dir)
|
||||
return pinned
|
||||
finally:
|
||||
_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]]:
|
||||
"""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:
|
||||
"""Digests and size of the file at *path*, all read from one file, or
|
||||
None when it is gone.
|
||||
def _read_once(path: str) -> tuple[dict[str, str], int, int] | None:
|
||||
"""Digests, size and inode of the file at *path*, all read from one
|
||||
file, or None when it is gone.
|
||||
|
||||
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
|
||||
@@ -271,7 +333,7 @@ def _read_once(path: str) -> tuple[dict[str, str], int] | None:
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,13 @@ from dataclasses import dataclass
|
||||
|
||||
import region as region_mod
|
||||
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:
|
||||
@@ -44,6 +50,7 @@ class SelectionIndex:
|
||||
pack_systems: dict[str, dict],
|
||||
extras: list[dict],
|
||||
required_only: bool = False,
|
||||
extras_prefix: str = "",
|
||||
) -> SelectionIndex:
|
||||
by_norm: dict[str, set[str]] = {}
|
||||
for sys_id in pack_systems:
|
||||
@@ -60,7 +67,8 @@ class SelectionIndex:
|
||||
extra_groups: dict[str, set[str]] = {}
|
||||
spellings = system_spellings(pack_systems)
|
||||
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:
|
||||
continue
|
||||
extra_groups.setdefault(dest, set()).update(
|
||||
|
||||
+18
-2
@@ -62,6 +62,20 @@ def prefixed_destination(dest: str, prefix: str) -> str:
|
||||
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:
|
||||
"""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)
|
||||
lower = set(content) if case_insensitive else set()
|
||||
prefix = _detect_extras_prefix(config, base_dest)
|
||||
region_prefix = declared_root(config)
|
||||
hidden: dict[tuple[str, str, str], str] = {}
|
||||
packed_keys: set[tuple[str, str, str]] = set()
|
||||
extras = _collect_emulator_extras(
|
||||
@@ -377,7 +392,7 @@ def hidden_extras(
|
||||
if required_only and fe.get("required") is False:
|
||||
continue
|
||||
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
|
||||
full = extra_full_destination(fe, prefix)
|
||||
if not full:
|
||||
@@ -1705,8 +1720,9 @@ def platform_region_groups(
|
||||
)
|
||||
held = {id(extra) for extra in extras}
|
||||
spellings = system_spellings(systems)
|
||||
region_prefix = declared_root(config)
|
||||
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:
|
||||
continue
|
||||
name = extra.get("name", "")
|
||||
|
||||
+22
-3
@@ -207,6 +207,7 @@ def _build_readme(
|
||||
save_folder: str = "",
|
||||
save_where: str = "",
|
||||
save_by_system: bool = False,
|
||||
case_twins: list[tuple[str, str]] | None = None,
|
||||
) -> str:
|
||||
"""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
|
||||
``save_where`` that directory's default place (save_place).
|
||||
``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 []
|
||||
sep = "=" * 50
|
||||
@@ -245,6 +250,20 @@ def _build_readme(
|
||||
+ (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:
|
||||
guide += _save_section(
|
||||
platform_display, save_folder, save_where, save_by_system,
|
||||
@@ -300,8 +319,8 @@ def _build_readme(
|
||||
if fallback_systems:
|
||||
listed = "\n".join(f" {_group_label(s)}" for s in fallback_systems)
|
||||
region_info += (
|
||||
"\n These systems have no BIOS in those regions, so all\n"
|
||||
" of theirs were kept:\n"
|
||||
"\n These systems have no BIOS in those regions, so every\n"
|
||||
" BIOS file for them was kept:\n"
|
||||
f"{listed}\n"
|
||||
)
|
||||
region_info += (
|
||||
@@ -316,7 +335,7 @@ def _build_readme(
|
||||
slot_info = (
|
||||
" Where a core declares which BIOS it prefers, only that one was\n"
|
||||
" kept for each system and region. Systems whose cores declare no\n"
|
||||
" order keep all of theirs.\n"
|
||||
" order keep every BIOS file.\n"
|
||||
)
|
||||
if undecidable_slots:
|
||||
slot_info += (
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Write the SHA-256 of install.py into the bootstraps that pin it.
|
||||
|
||||
install.sh and install.ps1 refuse an installer whose hash is not the one
|
||||
they carry, so every change to install.py moves both pins in the same
|
||||
commit. --check writes nothing and exits 1 naming each stale pin.
|
||||
|
||||
Usage:
|
||||
python scripts/pin_installer.py
|
||||
python scripts/pin_installer.py --check
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
INSTALLER = "install.py"
|
||||
# Each bootstrap and its one pin line, the value between the two groups.
|
||||
PINS: dict[str, re.Pattern[str]] = {
|
||||
"install.sh": re.compile(r'^(DEFAULT_INSTALL_SHA256=")[0-9a-f]{64}(")', re.M),
|
||||
"install.ps1": re.compile(r'^(\s*\$defaultInstallSha256 = ")[0-9a-f]{64}(")', re.M),
|
||||
}
|
||||
COMMAND = "python scripts/pin_installer.py"
|
||||
|
||||
|
||||
def installer_sha256(root: Path = ROOT) -> str:
|
||||
return hashlib.sha256((root / INSTALLER).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def stale_pins(root: Path = ROOT) -> list[str]:
|
||||
"""The bootstraps whose pin is not the SHA-256 of install.py."""
|
||||
digest = installer_sha256(root)
|
||||
stale = []
|
||||
for name, pin in PINS.items():
|
||||
found = pin.search((root / name).read_bytes().decode("utf-8"))
|
||||
if found is None:
|
||||
raise ValueError(f"{name}: no installer pin line")
|
||||
if found.group(0) != f"{found.group(1)}{digest}{found.group(2)}":
|
||||
stale.append(name)
|
||||
return stale
|
||||
|
||||
|
||||
def write_pins(root: Path = ROOT) -> list[str]:
|
||||
"""Rewrite each stale pin. Returns the bootstraps written."""
|
||||
digest = installer_sha256(root)
|
||||
written = []
|
||||
for name in stale_pins(root):
|
||||
path = root / name
|
||||
text = path.read_bytes().decode("utf-8")
|
||||
new, count = PINS[name].subn(rf"\g<1>{digest}\g<2>", text)
|
||||
if count != 1:
|
||||
raise ValueError(f"{name}: {count} installer pin lines, expected 1")
|
||||
path.write_bytes(new.encode("utf-8"))
|
||||
written.append(name)
|
||||
return written
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.split("\n\n")[0])
|
||||
parser.add_argument("--check", action="store_true",
|
||||
help="write nothing, exit 1 if a pin is stale")
|
||||
args = parser.parse_args()
|
||||
if args.check:
|
||||
stale = stale_pins()
|
||||
for name in stale:
|
||||
print(f"{name}: pin is not the SHA-256 of {INSTALLER}, {COMMAND} writes it",
|
||||
file=sys.stderr)
|
||||
return 1 if stale else 0
|
||||
for name in write_pins():
|
||||
print(f"{name}: pinned {installer_sha256()}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from common import run_cli
|
||||
|
||||
run_cli(main)
|
||||
+133
-10
@@ -55,6 +55,7 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
# 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.
|
||||
"""
|
||||
import generate_pack # noqa: PLC0415
|
||||
import packresolve # noqa: PLC0415
|
||||
import verify # noqa: PLC0415
|
||||
from common import ( # noqa: PLC0415
|
||||
ProfileSelectionError,
|
||||
@@ -580,16 +582,23 @@ def check_emulator_packs(
|
||||
|
||||
def compare(names: list[str], standalone: bool, label: str, **narrowing: object) -> bool:
|
||||
record = generate_pack.PackRecord()
|
||||
with contextlib.redirect_stdout(io.StringIO()), \
|
||||
contextlib.redirect_stderr(io.StringIO()):
|
||||
generate_pack.generate_emulator_pack(
|
||||
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,
|
||||
)
|
||||
was_offline = packresolve._OFFLINE
|
||||
# The pack is built offline: verify counts a release asset the cache
|
||||
# lacks the same way, missing, as the narrowed-pack step does.
|
||||
packresolve.set_offline(True)
|
||||
try:
|
||||
with contextlib.redirect_stdout(io.StringIO()), \
|
||||
contextlib.redirect_stderr(io.StringIO()):
|
||||
generate_pack.generate_emulator_pack(
|
||||
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(
|
||||
label, set(report["pack_destinations"]), set(record.files)
|
||||
)
|
||||
@@ -802,6 +811,92 @@ def check_narrowed_packs(
|
||||
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:
|
||||
"""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."""
|
||||
@@ -883,6 +978,32 @@ def _refuse_offline_without_caches(
|
||||
)
|
||||
|
||||
|
||||
def _refuse_offline_export_without_originals(
|
||||
parser: argparse.ArgumentParser, args: argparse.Namespace,
|
||||
cache: str = ".cache/upstream-native",
|
||||
) -> None:
|
||||
"""An offline run with --with-export needs each patched file cached.
|
||||
|
||||
Seven formats carry code, so step 2e patches the platform's own file,
|
||||
which --offline does not fetch. On a fresh clone the database, verify
|
||||
and the truth model were built before step 2e failed on every one.
|
||||
"""
|
||||
if not (args.offline and args.with_export):
|
||||
return
|
||||
from exporter import discover_exporters # noqa: PLC0415
|
||||
|
||||
lacking = sorted(
|
||||
name for name, exporter in discover_exporters().items()
|
||||
if exporter.needs_original() and not any(Path(cache, name).glob("*"))
|
||||
)
|
||||
if lacking:
|
||||
parser.error(
|
||||
f"--with-export patches each platform's own file, and {cache}/ holds none "
|
||||
f"for {', '.join(lacking)}: run python scripts/export_native.py --all "
|
||||
"--refresh-cache once, or drop --offline"
|
||||
)
|
||||
|
||||
|
||||
def _truth_steps(args: argparse.Namespace, results: dict) -> bool:
|
||||
"""Steps 2c to 2e: truth model, its diff, the native export."""
|
||||
all_ok = True
|
||||
@@ -1010,6 +1131,7 @@ def main() -> None:
|
||||
args = parser.parse_args()
|
||||
_refuse_unapplied_flags(parser, args)
|
||||
_refuse_offline_without_caches(parser, args)
|
||||
_refuse_offline_export_without_originals(parser, args)
|
||||
if not args.offline:
|
||||
# Read once and put in the environment every step inherits: the
|
||||
# scrapers and refresh_data_dirs went anonymous unless the user had
|
||||
@@ -1365,6 +1487,7 @@ def main() -> None:
|
||||
ok = check_narrowed_packs(
|
||||
include_archived=args.include_archived, full=args.full_narrowings,
|
||||
) and ok
|
||||
ok = check_split_packs(include_archived=args.include_archived) and ok
|
||||
print(
|
||||
f"--- {step_title(STEP_LABELS['consistency'])}, counts and paths: "
|
||||
f"{'OK' if ok else 'FAILED'} ---"
|
||||
|
||||
+58
-63
@@ -2665,6 +2665,9 @@ def drift_score(
|
||||
# set again before any ref can be read, so each outranks a GONE ref:
|
||||
# pcem ranked last at 0 with an orphaned upstream_commit.
|
||||
score += PIN_WEIGHT * (len(report.orphaned) + len(report.moved_repos))
|
||||
# A profile no repository answers for cannot be read at all, and
|
||||
# --strict fails on it: it ranked at 0 under the clean profiles.
|
||||
score += PIN_WEIGHT * report.uncheckable()
|
||||
score += min(commits, 20) // 4
|
||||
return score
|
||||
|
||||
@@ -3742,15 +3745,6 @@ def _git_file_at(path: Path, sha: str) -> dict | None:
|
||||
return document if isinstance(document, dict) else None
|
||||
|
||||
|
||||
def _scalar_values(document: dict) -> dict[str, str]:
|
||||
"""String scalars of the document, keyed by field path."""
|
||||
return {
|
||||
field: value
|
||||
for kind, field, _, _, value in _walk_document(document)
|
||||
if kind == "str"
|
||||
}
|
||||
|
||||
|
||||
def _pin_of(
|
||||
document: dict, field: str, mode: str = "",
|
||||
repo: upstream.Repo | None = None,
|
||||
@@ -3788,26 +3782,6 @@ def _pin_of(
|
||||
return value
|
||||
|
||||
|
||||
_CITATION_RUN_RE = re.compile(
|
||||
PROSE_CITE_RE.pattern + r"(?:, *\d+(?:-\d+)?)*"
|
||||
)
|
||||
|
||||
|
||||
def _scalar_citations(text: str | None) -> tuple[str, ...] | None:
|
||||
"""The citations a scalar carries, in order, without the prose around them.
|
||||
|
||||
A note reworded around unchanged citations was still written under the
|
||||
pin those citations describe: comparing whole texts moved the writing
|
||||
revision to the rewording commit, whose pin is current, and the stale
|
||||
citations were never offered for realignment again.
|
||||
"""
|
||||
if text is None:
|
||||
return None
|
||||
return tuple(
|
||||
_citation_key(m.group(0)) for m in _CITATION_RUN_RE.finditer(text)
|
||||
)
|
||||
|
||||
|
||||
def _writing_pins(
|
||||
revisions: list[tuple[str, dict]], intro_sha: str, field: str, mode: str = "",
|
||||
repo: upstream.Repo | None = None,
|
||||
@@ -3943,7 +3917,8 @@ def _realign_part(
|
||||
|
||||
|
||||
def _writing_pairs(
|
||||
document: dict, repos: list, revisions: list[tuple[str, dict]], intro_sha: str
|
||||
document: dict, repos: list, revisions: list[tuple[str, dict]], intro_sha: str,
|
||||
served: dict | None = None,
|
||||
) -> tuple[list[list], list[str]]:
|
||||
"""Every reading of where the text was written, and the ambiguities.
|
||||
|
||||
@@ -3973,8 +3948,9 @@ def _writing_pairs(
|
||||
ambiguous.append(f"{pin_field}_commit {' -> '.join(written)}")
|
||||
# The current pin among the candidates reads as "no move" (None) for
|
||||
# that repository, which the other candidates must then agree with.
|
||||
reader = (served or {}).get(repo, repo)
|
||||
options.append([
|
||||
(repo, pin, current) if pin != current else None for pin in written
|
||||
(reader, pin, current) if pin != current else None for pin in written
|
||||
])
|
||||
readings = [
|
||||
[pair for pair in combo if pair is not None]
|
||||
@@ -4055,8 +4031,20 @@ def realign_prose(
|
||||
]
|
||||
if not repos:
|
||||
return []
|
||||
# A source declared gone is read through its mirror, which carries the
|
||||
# same commits: teknoparrot's notes stayed blocked once
|
||||
# teknogods/TeknoParrotUI answered 404, every reading of the old pin
|
||||
# calling the files absent.
|
||||
mirror = (
|
||||
upstream.parse_repo(str(document.get("source_mirror") or ""))
|
||||
if document.get("upstream_gone") else None
|
||||
)
|
||||
served = {
|
||||
repo: mirror for field, _mode, repo in repos
|
||||
if mirror is not None and field == "source"
|
||||
}
|
||||
pinned = [
|
||||
(repo, pin)
|
||||
(served.get(repo, repo), pin)
|
||||
for field, mode, repo in repos
|
||||
if (pin := _pin_of(document, f"{field}_commit", mode))
|
||||
]
|
||||
@@ -4066,72 +4054,79 @@ def realign_prose(
|
||||
if not history:
|
||||
return []
|
||||
|
||||
current_values = _scalar_values(document)
|
||||
fields = {citation.field for citation in citations}
|
||||
alive = dict.fromkeys(fields, True)
|
||||
intro: dict[str, str | None] = dict.fromkeys(fields)
|
||||
# Each citation is followed on its own: a field's text changes whenever
|
||||
# one of its citations is corrected by hand, and dating the whole field
|
||||
# from that commit put idtech4a++'s untouched Game.java:69-72 under the
|
||||
# current pin, so the stale range was never offered again.
|
||||
keys = [(c.field, _citation_key(c.ref)) for c in citations]
|
||||
alive = [True] * len(citations)
|
||||
intro: list[str | None] = [None] * len(citations)
|
||||
revisions: list[tuple[str, dict]] = []
|
||||
# Fields whose writing pin lies past a revision that cannot be read:
|
||||
# Citations whose writing pin lies past a revision that cannot be read:
|
||||
# where their text came in is unknown, so they are not moved.
|
||||
unknown: dict[str, str] = {}
|
||||
unknown: dict[int, str] = {}
|
||||
# A revision that does not parse is passed over unless the pins differ
|
||||
# on its two sides while the field's citations changed across it: then
|
||||
# the text may have come in there under either pin. Otherwise nothing
|
||||
# on its two sides while the citation is absent across it: then the
|
||||
# text may have come in there under either pin. Otherwise nothing
|
||||
# it could hold changes the reading. lindbergh-loader
|
||||
# committed invalid YAML once (df7346b3), fixed the same day, and every
|
||||
# note older than that commit stayed blocked for good.
|
||||
gap: str | None = None
|
||||
|
||||
def carried_by(past: dict) -> set[tuple[str, str]]:
|
||||
return {
|
||||
(c.field, _citation_key(c.ref))
|
||||
for c in collect_citations(past) if c.kind == "prose"
|
||||
}
|
||||
|
||||
for sha in history:
|
||||
if not any(alive.values()):
|
||||
if not any(alive):
|
||||
break
|
||||
past = _git_file_at(path, sha)
|
||||
if past is None:
|
||||
gap = gap or sha
|
||||
continue
|
||||
held = carried_by(past)
|
||||
if gap:
|
||||
newer = revisions[-1][1] if revisions else document
|
||||
moved = any(
|
||||
newer.get(pin) != past.get(pin)
|
||||
for pin in ("source_commit", "upstream_commit")
|
||||
)
|
||||
values = _scalar_values(past)
|
||||
for field, live in alive.items():
|
||||
if live and moved and _scalar_citations(values.get(field)) != (
|
||||
_scalar_citations(current_values.get(field))
|
||||
):
|
||||
unknown[field] = gap
|
||||
alive[field] = False
|
||||
for index, live in enumerate(alive):
|
||||
if live and moved and keys[index] not in held:
|
||||
unknown[index] = gap
|
||||
alive[index] = False
|
||||
gap = None
|
||||
revisions.append((sha, past))
|
||||
values = _scalar_values(past)
|
||||
for field in fields:
|
||||
if not alive[field]:
|
||||
for index, live in enumerate(alive):
|
||||
if not live:
|
||||
continue
|
||||
if _scalar_citations(values.get(field)) == _scalar_citations(
|
||||
current_values.get(field)
|
||||
):
|
||||
intro[field] = sha
|
||||
if keys[index] in held:
|
||||
intro[index] = sha
|
||||
else:
|
||||
alive[field] = False
|
||||
alive[index] = False
|
||||
if gap:
|
||||
# The oldest revisions do not parse: where the text came in is past
|
||||
# what can be read.
|
||||
unknown.update({field: gap for field, live in alive.items() if live})
|
||||
unknown.update({index: gap for index, live in enumerate(alive) if live})
|
||||
|
||||
messages: list[str] = []
|
||||
jobs: list[tuple[Citation, dict]] = []
|
||||
for citation in citations:
|
||||
if citation.field in unknown:
|
||||
for index, citation in enumerate(citations):
|
||||
if index in unknown:
|
||||
messages.append(
|
||||
f"read again: {citation.field}: {citation.ref} "
|
||||
f"(revision {unknown[citation.field][:8]} unreadable)"
|
||||
f"(revision {unknown[index][:8]} unreadable)"
|
||||
)
|
||||
continue
|
||||
intro_sha = intro.get(citation.field)
|
||||
intro_sha = intro[index]
|
||||
if intro_sha is None:
|
||||
# The scalar is not committed yet: written now, under this pin.
|
||||
continue
|
||||
readings, ambiguous = _writing_pairs(document, repos, revisions, intro_sha)
|
||||
readings, ambiguous = _writing_pairs(
|
||||
document, repos, revisions, intro_sha, served
|
||||
)
|
||||
if not any(readings):
|
||||
continue
|
||||
moves, blocked = _realign_citation(
|
||||
@@ -5042,7 +5037,7 @@ def _print_triage(
|
||||
state = triage_state(report)
|
||||
if refused:
|
||||
state += f", {refused}"
|
||||
pending += bool(report.needs_review())
|
||||
pending += bool(report.needs_review() or report.uncheckable())
|
||||
print(f"{score:5d} {report.name:30s} {state}")
|
||||
print(f"{len(ranked)} profiles, {pending} with something to review")
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from collections.abc import Iterator
|
||||
from collections.abc import Callable, Iterator
|
||||
from pathlib import Path
|
||||
from artifacts import file_lock as _file_lock, set_plain_mode
|
||||
from common import InputMissing, github_token, platform_arg, require_platform, suggest_names, yaml_load
|
||||
@@ -71,6 +71,24 @@ def cache_lock(
|
||||
return _announced_lock(cache_dir.with_name(f".{cache_dir.name}.lock"), shared, cache_dir)
|
||||
|
||||
|
||||
def _wait_message(lock_path: Path, shared: bool, cache_dir: Path) -> str:
|
||||
"""Who to wait for, and, for a refresh, how not to.
|
||||
|
||||
The bare "another run holds it" named neither the lock nor its holder,
|
||||
and the build waited with no limit.
|
||||
"""
|
||||
message = (
|
||||
f"waiting for {cache_dir}: another run holds {lock_path} "
|
||||
f"(lsof {lock_path} names it)"
|
||||
)
|
||||
if not shared:
|
||||
message += (
|
||||
". generate_pack.py --no-refresh-data, or --offline there and in"
|
||||
" pipeline.py, builds from the cache as it is without waiting"
|
||||
)
|
||||
return message
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _announced_lock(lock_path: Path, shared: bool, cache_dir: Path) -> Iterator[None]:
|
||||
"""file_lock, saying so when it has to wait.
|
||||
@@ -90,7 +108,7 @@ def _announced_lock(lock_path: Path, shared: bool, cache_dir: Path) -> Iterator[
|
||||
try:
|
||||
fcntl.flock(handle, mode | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
log.warning("waiting for %s: another run holds it", cache_dir)
|
||||
log.warning(_wait_message(lock_path, shared, cache_dir))
|
||||
fcntl.flock(handle, mode)
|
||||
try:
|
||||
yield
|
||||
@@ -270,10 +288,12 @@ def _download_and_extract(
|
||||
source_path: str,
|
||||
local_cache: str,
|
||||
exclude: list[str] | None = None,
|
||||
measured: Callable[[int], None] | None = None,
|
||||
) -> int:
|
||||
"""Download tarball, extract source_path subtree to local_cache.
|
||||
|
||||
Returns the number of files extracted.
|
||||
Returns the number of files extracted. measured receives the size of
|
||||
the archive downloaded.
|
||||
"""
|
||||
exclude = exclude or []
|
||||
cache_dir = Path(local_cache)
|
||||
@@ -290,6 +310,8 @@ def _download_and_extract(
|
||||
if not chunk:
|
||||
break
|
||||
f.write(chunk)
|
||||
if measured is not None:
|
||||
measured(tarball_path.stat().st_size)
|
||||
|
||||
log.info("extracting %s -> %s", source_path, local_cache)
|
||||
|
||||
@@ -347,6 +369,7 @@ def _download_and_extract_zip(
|
||||
local_cache: str,
|
||||
exclude: list[str] | None = None,
|
||||
strip_components: int = 0,
|
||||
measured: Callable[[int], None] | None = None,
|
||||
) -> int:
|
||||
"""Download ZIP, extract to local_cache. Returns file count.
|
||||
|
||||
@@ -369,6 +392,8 @@ def _download_and_extract_zip(
|
||||
if not chunk:
|
||||
break
|
||||
f.write(chunk)
|
||||
if measured is not None:
|
||||
measured(zip_path.stat().st_size)
|
||||
|
||||
extract_dir = Path(tmpdir) / "extract"
|
||||
extract_dir.mkdir()
|
||||
@@ -408,6 +433,46 @@ def _download_and_extract_zip(
|
||||
return file_count
|
||||
|
||||
|
||||
def _measure_archive(source_url: str) -> int | None:
|
||||
"""The size of the archive at source_url, read by streaming it: a
|
||||
tarball GitHub builds on request is sent without a Content-Length."""
|
||||
req = urllib.request.Request(source_url, headers={"User-Agent": USER_AGENT})
|
||||
size = 0
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=DOWNLOAD_TIMEOUT) as resp:
|
||||
while chunk := resp.read(65536):
|
||||
size += len(chunk)
|
||||
except _TRANSIENT as exc:
|
||||
log.warning("could not measure %s: %s", source_url, exc)
|
||||
return None
|
||||
return size or None
|
||||
|
||||
|
||||
def _record_archive_size(key: str, entry: dict, versions_path: str) -> None:
|
||||
"""Record the archive size of a tree current since before it was kept.
|
||||
|
||||
The installer announces a tree's download as that size: a tarball
|
||||
whose tree is 10 KB is a download of 21 MB. Measured, not refetched,
|
||||
so the tree the packs carry stays the one recorded.
|
||||
"""
|
||||
path = Path(versions_path)
|
||||
with _file_lock(path.with_name(f".{path.name}.lock")):
|
||||
versions = _load_versions(versions_path)
|
||||
record = versions.get(key)
|
||||
if record is None or "archive_size" in record:
|
||||
return
|
||||
version = entry.get("version", "master")
|
||||
size = _measure_archive(entry["source_url"].format(version=version))
|
||||
if size is None:
|
||||
return
|
||||
with _file_lock(path.with_name(f".{path.name}.lock")):
|
||||
versions = _load_versions(versions_path)
|
||||
if key in versions and "archive_size" not in versions[key]:
|
||||
versions[key]["archive_size"] = size
|
||||
_save_versions(versions, versions_path)
|
||||
log.info("[%s] archive size recorded: %d bytes", key, size)
|
||||
|
||||
|
||||
def _get_remote_etag(source_url: str) -> str | None:
|
||||
"""HEAD request to get ETag or Last-Modified for freshness check."""
|
||||
try:
|
||||
@@ -456,6 +521,7 @@ def refresh_entry(
|
||||
return None
|
||||
if not needs:
|
||||
log.info("[%s] up to date (tag: %s)", key, _short_tag(cached_tag))
|
||||
_record_archive_size(key, entry, versions_path)
|
||||
return False
|
||||
with cache_lock(entry["local_cache"]):
|
||||
return _refresh_entry(key, entry, force, dry_run, versions_path)
|
||||
@@ -516,13 +582,19 @@ def _refresh_entry(
|
||||
else:
|
||||
remote_tag = get_remote_sha(entry["source_url"], version)
|
||||
|
||||
archive_sizes: list[int] = []
|
||||
|
||||
def fetch() -> int:
|
||||
archive_sizes.clear()
|
||||
if source_type == "zip":
|
||||
return _download_and_extract_zip(
|
||||
source_url, local_cache, exclude, entry.get("strip_components", 0)
|
||||
source_url, local_cache, exclude, entry.get("strip_components", 0),
|
||||
measured=archive_sizes.append,
|
||||
)
|
||||
source_path = entry["source_path"].format(version=version)
|
||||
return _download_and_extract(source_url, source_path, local_cache, exclude)
|
||||
return _download_and_extract(
|
||||
source_url, source_path, local_cache, exclude, measured=archive_sizes.append
|
||||
)
|
||||
|
||||
# An archive that reads but holds nothing under the path is the same on
|
||||
# every try: only a failed or unreadable download is tried again.
|
||||
@@ -540,7 +612,10 @@ def _refresh_entry(
|
||||
log.warning("[%s] download failed (%s), trying again in %d s", key, exc, wait)
|
||||
time.sleep(wait)
|
||||
|
||||
_record_version(key, {"sha": remote_tag or "", "version": version}, versions_path)
|
||||
record = {"sha": remote_tag or "", "version": version}
|
||||
if archive_sizes:
|
||||
record["archive_size"] = archive_sizes[-1]
|
||||
_record_version(key, record, versions_path)
|
||||
|
||||
log.info("[%s] refreshed: %d files extracted to %s", key, file_count, local_cache)
|
||||
return True
|
||||
|
||||
@@ -34,7 +34,10 @@ has ended, and its export is rewritten even when no row named it.
|
||||
|
||||
Surfaced only (no refresh): data rows that are not a _data_dirs.yml key
|
||||
(the registry itself, buildbot assets it does not track), coreinfo gaps, redump/no-intro/tosec packs,
|
||||
CI pins (install.py SHA-256, PyPI, pinned actions), profile_sync. Each needs
|
||||
CI pins (PyPI, pinned actions, while scripts/pin_installer.py writes the
|
||||
install.py SHA-256 pin), profile_sync, rom-folder
|
||||
pins (moved by hand, then python -m scripts.scraper.romfolders --setup NAME
|
||||
reads them again). Each needs
|
||||
a human read (new .info file, manual DAT download, PyPI version bump).
|
||||
|
||||
Usage:
|
||||
@@ -65,7 +68,9 @@ PLATFORMS_REGISTRY = REPO_ROOT / "platforms" / "_registry.yml"
|
||||
DATA_DIRS_REGISTRY = REPO_ROOT / "platforms" / "_data_dirs.yml"
|
||||
|
||||
AUTO_AREAS = ("platforms", "native", "targets", "data", "catalogs")
|
||||
MANUAL_AREAS = ("coreinfo", "ci", "profiles")
|
||||
# A rom-folder pin is moved by hand in platforms/romfolders/_sources/,
|
||||
# then `python -m scripts.scraper.romfolders --setup NAME` reads it again.
|
||||
MANUAL_AREAS = ("romfolders", "coreinfo", "ci", "profiles")
|
||||
|
||||
JOB_TIMEOUT = 1800 # 30 minutes per refresher, scrapers rarely exceed 10
|
||||
|
||||
@@ -397,6 +402,10 @@ def _command_for(
|
||||
"--force",
|
||||
]
|
||||
|
||||
if area == "ci" and subject == "install.py pin":
|
||||
# The pin is the hash of the committed install.py, nothing to judge.
|
||||
return [sys.executable, "scripts/pin_installer.py"]
|
||||
|
||||
if area == "catalogs":
|
||||
if subject == "mame recipes":
|
||||
return [
|
||||
@@ -563,7 +572,8 @@ def main() -> int:
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.split("\n\n")[0])
|
||||
parser.add_argument(
|
||||
"--only",
|
||||
help="comma-separated areas to check: " + ",".join(AUTO_AREAS),
|
||||
help="comma-separated areas to check: " + ",".join(AUTO_AREAS)
|
||||
+ " (refreshed), " + ",".join(MANUAL_AREAS) + " (listed for a human)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
@@ -579,7 +589,7 @@ def main() -> int:
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="machine-readable summary (jobs, surfaced, ignored)",
|
||||
help="machine-readable summary (jobs, surfaced, ignored), the plan with --dry-run",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--findings",
|
||||
@@ -618,6 +628,22 @@ def main() -> int:
|
||||
registry = _load_platform_registry()
|
||||
jobs, surfaced, ignored = plan_jobs(findings, registry)
|
||||
|
||||
if args.dry_run and args.json:
|
||||
print(json.dumps({
|
||||
"planned": [
|
||||
{
|
||||
"area": job.area,
|
||||
"subject": job.subject,
|
||||
"command": job.command,
|
||||
"after": sorted(job.after),
|
||||
"then": [list(command) for command in job.then],
|
||||
}
|
||||
for job in sorted(jobs, key=lambda j: (j.area, j.subject))
|
||||
],
|
||||
"surfaced": surfaced,
|
||||
"ignored_count": len(ignored),
|
||||
}, indent=2))
|
||||
return 0
|
||||
if args.dry_run:
|
||||
print(f"\n[plan] {len(jobs)} refreshable, {len(surfaced)} manual, "
|
||||
f"{len(ignored)} already clean/skipped")
|
||||
|
||||
@@ -7,7 +7,8 @@ issue 81 happened. Install commands, the parts sentence and the table come
|
||||
from the record and the registry. Each row links the platform's page under
|
||||
get/ on the site, which outlives the release, and the setups the registry
|
||||
does not mark release_pack are named with the command. The maintainer writes
|
||||
what changed, and that text is appended as given.
|
||||
what changed, and that text is appended as given once it holds none of the
|
||||
REFUSED phrases.
|
||||
|
||||
Usage:
|
||||
python scripts/release_notes.py --whats-new tmp/whats-new.md --output notes.md
|
||||
@@ -102,6 +103,20 @@ def _link(name: str, pack: dict) -> str:
|
||||
return f"[{label}]({release_record.get_page(_owner(name, pack), SITE)})"
|
||||
|
||||
|
||||
# Words the published notes of v2026.10.10 carried by hand: "never from a
|
||||
# forum list" judged lists other people keep, and "a game never stops on a
|
||||
# missing file" was a claim the gap analysis contradicts. Text about other
|
||||
# projects says what changed, not who erred (wiki/release-process.md).
|
||||
REFUSED = ("their file", "theirs", "of ours", "corrected", "wrong", "drift",
|
||||
"left out", "never seen", "forum", "never stops", "never fails")
|
||||
|
||||
|
||||
def refused_words(text: str) -> list[str]:
|
||||
"""The refused phrases the text holds, case ignored."""
|
||||
lowered = text.lower()
|
||||
return [phrase for phrase in REFUSED if phrase in lowered]
|
||||
|
||||
|
||||
def render(record: dict, platforms_dir: str, whats_new: str) -> str:
|
||||
packs = record.get("packs", {})
|
||||
folders = _extract_folders(record, platforms_dir)
|
||||
@@ -124,7 +139,8 @@ def render(record: dict, platforms_dir: str, whats_new: str) -> str:
|
||||
"### Download a pack",
|
||||
"",
|
||||
("One complete pack for each platform below: the BIOS list the platform"
|
||||
" publishes plus every file its emulators open. Each link is a page"
|
||||
" publishes plus the files its emulators open. Files no pack carries yet"
|
||||
f" are listed in the [gap analysis]({SITE}gaps/). Each link is a page"
|
||||
" that keeps its address from one release to the next. Files is what the pack"
|
||||
" holds once extracted, the count a file manager shows. Extracted size"
|
||||
" is the space those files take, and the download is smaller."),
|
||||
@@ -173,6 +189,12 @@ def main() -> int:
|
||||
if not whats_new.strip():
|
||||
print(f"Error: {args.whats_new} is empty", file=sys.stderr)
|
||||
return 1
|
||||
refused = refused_words(whats_new)
|
||||
if refused:
|
||||
print(f"Error: {args.whats_new} says {', '.join(repr(w) for w in refused)}. State what"
|
||||
" changed and how it was checked, not a verdict on other projects or a"
|
||||
" promise the gap analysis contradicts", file=sys.stderr)
|
||||
return 1
|
||||
write_text_atomic(str(args.output), render(record, args.platforms_dir, whats_new))
|
||||
print(f"Wrote {args.output} for {record['tag']}, {len(record['packs'])} packs")
|
||||
return 0
|
||||
|
||||
@@ -17,11 +17,14 @@ Usage:
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from artifacts import set_plain_mode
|
||||
from common import copy_file_atomic, load_database
|
||||
|
||||
|
||||
@@ -56,6 +59,36 @@ def index_cache(cache_dir: str) -> dict[str, str]:
|
||||
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(
|
||||
cache_dir: str, db_path: str, gitignore: str
|
||||
) -> tuple[int, list[str]]:
|
||||
@@ -79,9 +112,9 @@ def restore(
|
||||
unsatisfied.append(path)
|
||||
continue
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
# Whole or absent: a scan running beside a plain copy hashed the
|
||||
# truncated file and published that in the database.
|
||||
copy_file_atomic(source, path)
|
||||
if not _copy_verified(source, path, sha1):
|
||||
unsatisfied.append(path)
|
||||
continue
|
||||
print(f"Restored: {path}")
|
||||
restored += 1
|
||||
print(f"Total: {restored} files restored")
|
||||
|
||||
@@ -49,7 +49,7 @@ from collections.abc import Callable, Iterator
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from ..artifacts import write_text_atomic
|
||||
from ..artifacts import write_if_changed
|
||||
from ..common import load_emulator_profiles, run_cli
|
||||
from .romset_dat_importer import (
|
||||
LISTXML_CHUNK,
|
||||
@@ -1114,14 +1114,6 @@ def dump(doc: dict) -> str:
|
||||
return json.dumps(doc, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"
|
||||
|
||||
|
||||
def write_if_changed(path: Path, text: str) -> bool:
|
||||
if path.is_file() and path.read_text(encoding="utf-8") == text:
|
||||
return False
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
write_text_atomic(str(path), text)
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.splitlines()[0])
|
||||
which = parser.add_mutually_exclusive_group(required=True)
|
||||
@@ -1197,7 +1189,8 @@ def main() -> int:
|
||||
continue
|
||||
if not args.dry_run:
|
||||
target = args.output_dir / f"{profile}.json"
|
||||
if write_if_changed(target, dump(doc)):
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
if write_if_changed(str(target), dump(doc)):
|
||||
print(f" wrote {target}")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
@@ -46,14 +46,18 @@ STORAGE = "${storagePath}"
|
||||
FLATPAK_DATA = "${HOME}/.var/app/"
|
||||
|
||||
# EmuScripts file names that differ from the emulator profile they set up.
|
||||
# The platform and target scrapers both read them here.
|
||||
SCRIPT_EMULATORS = {
|
||||
"pcsx2qt": "pcsx2",
|
||||
"rpcs3legacy": "rpcs3",
|
||||
"cemuproton": "cemu",
|
||||
"rmg": "mupen64plus_next",
|
||||
}
|
||||
# The main RetroArch config script installs no emulator.
|
||||
NOT_EMULATORS = {"retroarch_maincfg"}
|
||||
# The main RetroArch config script installs no emulator, and emudeck-we's
|
||||
# emuDeckTemplate.ps1 is the skeleton new installers are copied from.
|
||||
NOT_EMULATORS = {"retroarch_maincfg", "template"}
|
||||
# EmuDeck's SteamOS installers are shell scripts, emudeck-we's PowerShell.
|
||||
SCRIPT_SUFFIXES = (".sh", ".ps1")
|
||||
|
||||
# What the emulator does with a directory written into each key.
|
||||
DIRECTORY_KINDS = {
|
||||
@@ -66,9 +70,10 @@ DIRECTORY_KINDS = {
|
||||
|
||||
def emulator_of_script(file_name: str) -> str | None:
|
||||
"""The emulator an EmuScripts file sets up, None for a helper script."""
|
||||
if not file_name.endswith(".sh"):
|
||||
stem, dot, suffix = file_name.rpartition(".")
|
||||
if not dot or f".{suffix}" not in SCRIPT_SUFFIXES:
|
||||
return None
|
||||
key = re.sub(r"^emuDeck", "", file_name[: -len(".sh")], flags=re.IGNORECASE).lower()
|
||||
key = re.sub(r"^emuDeck", "", stem, flags=re.IGNORECASE).lower()
|
||||
if not key or key in NOT_EMULATORS:
|
||||
return None
|
||||
return SCRIPT_EMULATORS.get(key, key)
|
||||
|
||||
@@ -469,9 +469,10 @@ class Scraper(BaseScraper):
|
||||
|
||||
version = fetch_github_latest_version("dragoonDorise/EmuDeck")
|
||||
|
||||
cores = self._fetch_installed_emulators()
|
||||
revision = launcher_revision(self.checkbios_url)
|
||||
cores = self._fetch_installed_emulators(revision)
|
||||
standalone = [core for core in cores if core != "retroarch"]
|
||||
tree = RepoTree.github(EMUDECK_REPO, launcher_revision(self.checkbios_url))
|
||||
tree = RepoTree.github(EMUDECK_REPO, revision)
|
||||
libretro = set(installed_libretro_cores(tree.read(RETROARCH_SCRIPT)))
|
||||
|
||||
return {
|
||||
@@ -501,16 +502,16 @@ class Scraper(BaseScraper):
|
||||
"launchers": extract_launchers(tree),
|
||||
}
|
||||
|
||||
def _fetch_installed_emulators(self) -> list[str]:
|
||||
"""Fetch the list of emulators installed by EmuDeck from EmuScripts.
|
||||
|
||||
Returns core names normalized to match emulator profile keys.
|
||||
def _fetch_installed_emulators(self, revision: str) -> list[str]:
|
||||
"""The emulators EmuDeck's EmuScripts install at the pinned revision,
|
||||
named as their profiles are: the launcher layer and the SteamOS
|
||||
target are read at that revision too.
|
||||
"""
|
||||
import json
|
||||
|
||||
api_url = (
|
||||
"https://api.github.com/repos/dragoonDorise/EmuDeck/"
|
||||
"contents/functions/EmuScripts"
|
||||
f"contents/functions/EmuScripts?ref={revision}"
|
||||
)
|
||||
try:
|
||||
req = urllib.request.Request(api_url, headers=github_headers())
|
||||
|
||||
@@ -38,7 +38,7 @@ _SCRIPTS_DIR = str(Path(__file__).resolve().parents[1])
|
||||
if _SCRIPTS_DIR not in sys.path:
|
||||
sys.path.insert(0, _SCRIPTS_DIR)
|
||||
|
||||
from artifacts import copy_stream_atomic, write_text_atomic # noqa: E402
|
||||
from artifacts import copy_stream_atomic, write_if_changed, write_text_atomic # noqa: E402
|
||||
from common import parse_untrusted_xml # noqa: E402
|
||||
from region import REGIONS, WORLD # noqa: E402
|
||||
from region_audit import NOINTRO # noqa: E402
|
||||
@@ -275,14 +275,6 @@ def dump_catalog(doc: dict) -> str:
|
||||
return f'{text},"games":[\n{games}\n]}}\n'
|
||||
|
||||
|
||||
def write_if_changed(path: Path, text: str) -> bool:
|
||||
if path.is_file() and path.read_text(encoding="utf-8") == text:
|
||||
return False
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
write_text_atomic(str(path), text)
|
||||
return True
|
||||
|
||||
|
||||
# Fetching ---------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -503,7 +495,9 @@ def main() -> int:
|
||||
failures.append(f"{doc['source']}/{doc['system']}: {cov['games']} games, "
|
||||
f"floor {floor_of(doc)}")
|
||||
if not args.dry_run:
|
||||
write_if_changed(args.output_dir / doc["source"] / f"{doc['system']}.json", text)
|
||||
target = args.output_dir / doc["source"] / f"{doc['system']}.json"
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
write_if_changed(str(target), text)
|
||||
print(f"{len(docs)} catalogs, {raw_total} bytes raw, {gz_total} bytes gzip")
|
||||
for failure in failures:
|
||||
print(f"FAIL {failure}", file=sys.stderr)
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Scraper for AmberELEC per-device emulator availability.
|
||||
|
||||
Sources (AmberELEC/AmberELEC, at the commit platforms/amberelec.yml names):
|
||||
- Makefile -- one `DEVICE=<name> ARCH=<arch> ./scripts/build_distro` target
|
||||
per image (RG351P, RG351V, RG351MP, RG552, RG353, RG503 at a21975bf)
|
||||
- packages/amberelec/package.mk -- LIBRETRO_CORES_EXTRA joins PKG_EMUS
|
||||
only on the devices its `if` names (lines 23-31 at a21975bf: RG552,
|
||||
RG353, RG503), and on every other device makeinstall_target deletes
|
||||
each of those cores from es_systems.cfg (lines 45-58)
|
||||
- packages/ui/emulationstation/config/es_systems.cfg -- read the way the
|
||||
platform scraper reads it, after that deletion
|
||||
|
||||
An RG351P image carries neither melonDS nor bsnes: without a target list,
|
||||
its pack shipped the Nintendo DS firmware anyway.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import yaml
|
||||
|
||||
from . import BaseTargetScraper
|
||||
|
||||
PLATFORM_NAME = "amberelec"
|
||||
|
||||
RAW_BASE = "https://raw.githubusercontent.com/AmberELEC/AmberELEC/{ref}"
|
||||
META_MK = "packages/amberelec/package.mk"
|
||||
|
||||
try:
|
||||
from ..amberelec_scraper import BUILDS_OF, ES_SYSTEMS_TEMPLATE, RUNNERS
|
||||
from ..base_scraper import fetch_bytes
|
||||
from ..batocera_scraper import _not_cores
|
||||
from ..es_emulators import es_cores, xml_systems
|
||||
except ImportError:
|
||||
from scraper.amberelec_scraper import BUILDS_OF, ES_SYSTEMS_TEMPLATE, RUNNERS
|
||||
from scraper.base_scraper import fetch_bytes
|
||||
from scraper.batocera_scraper import _not_cores
|
||||
from scraper.es_emulators import es_cores, xml_systems
|
||||
|
||||
_DEVICE_RE = re.compile(
|
||||
r"^(\w+):\n\tDEVICE=(\w+) ARCH=(\w+) \./scripts/build_distro$", re.MULTILINE
|
||||
)
|
||||
_EXTRA_RE = re.compile(r'LIBRETRO_CORES_EXTRA="([^"]*)"')
|
||||
# The `if` whose body adds the extra cores to PKG_EMUS.
|
||||
_EXTRA_IF_RE = re.compile(
|
||||
r"if \[\[ (?P<test>[^\n]*?) \]\]; then\s*\n\s*PKG_EMUS\+=\" \$\{LIBRETRO_CORES_EXTRA\}\""
|
||||
)
|
||||
# What makeinstall_target does to es_systems.cfg on the other devices.
|
||||
_REMOVAL_LINES = (
|
||||
"for CORE in ${LIBRETRO_CORES_EXTRA}; do",
|
||||
'sed -i "s|<core>$CORE</core>||g"',
|
||||
r"sed -i -e '/<emulator name/{:a;N;/<\/emulator>/!ba;/melonds/d}'",
|
||||
)
|
||||
_EMULATOR_BLOCK_RE = re.compile(r"[^\n]*<emulator name.*?</emulator>[^\n]*\n?", re.DOTALL)
|
||||
|
||||
|
||||
def devices(makefile: str) -> dict[str, str]:
|
||||
"""{device: architecture} of each image the Makefile builds."""
|
||||
found = {device: arch for target, device, arch in _DEVICE_RE.findall(makefile)
|
||||
if target == device}
|
||||
if not found:
|
||||
raise RuntimeError("the AmberELEC Makefile builds no device image")
|
||||
return found
|
||||
|
||||
|
||||
def extra_cores(package_mk: str) -> tuple[list[str], set[str]]:
|
||||
"""(the extra cores, the devices that keep them) from the metapackage."""
|
||||
listed = _EXTRA_RE.search(package_mk)
|
||||
test = _EXTRA_IF_RE.search(package_mk)
|
||||
if listed is None or test is None:
|
||||
raise RuntimeError(f"{META_MK}: no LIBRETRO_CORES_EXTRA or no test adding it")
|
||||
missing = [line for line in _REMOVAL_LINES if line not in package_mk]
|
||||
if missing:
|
||||
raise RuntimeError(f"{META_MK}: makeinstall_target no longer reads {missing[0]!r}")
|
||||
keep = set(re.findall(r'"\$\{DEVICE\}" == "(\w+)"', test.group("test")))
|
||||
for alternatives in re.findall(r'"\$\{DEVICE\}" =~ \^\(([\w|]+)\)\$', test.group("test")):
|
||||
keep.update(alternatives.split("|"))
|
||||
if not keep:
|
||||
raise RuntimeError(f"{META_MK}: the test adding the extra cores names no device")
|
||||
return listed.group(1).split(), keep
|
||||
|
||||
|
||||
def without_cores(es_systems: str, cores: list[str]) -> str:
|
||||
"""es_systems.cfg as makeinstall_target leaves it on a device without them."""
|
||||
for core in cores:
|
||||
if core == "melonds":
|
||||
es_systems = _EMULATOR_BLOCK_RE.sub(
|
||||
lambda block: "" if "melonds" in block.group(0) else block.group(0),
|
||||
es_systems,
|
||||
)
|
||||
es_systems = es_systems.replace(f"<core>{core}</core>", "")
|
||||
return es_systems
|
||||
|
||||
|
||||
def device_cores(es_systems: str, extra: list[str], keeps_extra: bool) -> list[str]:
|
||||
"""Every core and standalone emulator one device's es_systems.cfg offers."""
|
||||
text = es_systems if keeps_extra else without_cores(es_systems, extra)
|
||||
cores, _standalone, _dual = es_cores(
|
||||
xml_systems(text), RUNNERS, BUILDS_OF, _not_cores(PLATFORM_NAME)
|
||||
)
|
||||
return sorted(set(cores))
|
||||
|
||||
|
||||
class Scraper(BaseTargetScraper):
|
||||
"""Per-device core lists from AmberELEC's metapackage."""
|
||||
|
||||
def __init__(self, url: str = "https://github.com/AmberELEC/AmberELEC") -> None:
|
||||
super().__init__(url=url)
|
||||
self.ref = ""
|
||||
|
||||
def _raw(self, path: str) -> str:
|
||||
return fetch_bytes(f"{RAW_BASE.format(ref=self.ref)}/{path}").decode("utf-8")
|
||||
|
||||
def fetch_targets(self) -> dict:
|
||||
self.ref = self.pinned_revision(PLATFORM_NAME)
|
||||
extra, keep = extra_cores(self._raw(META_MK))
|
||||
es_systems = fetch_bytes(ES_SYSTEMS_TEMPLATE.format(ref=self.ref)).decode("utf-8")
|
||||
targets: dict[str, dict] = {}
|
||||
for device, arch in sorted(devices(self._raw("Makefile")).items()):
|
||||
cores = device_cores(es_systems, extra, device in keep)
|
||||
targets[device.lower()] = {"architecture": arch, "cores": cores}
|
||||
print(f" {device.lower()} ({arch}): {len(cores)} cores", file=sys.stderr)
|
||||
return {
|
||||
"platform": PLATFORM_NAME,
|
||||
"source": f"{self.url}/blob/{self.ref}/{META_MK}",
|
||||
"scraped_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"targets": targets,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
allow_abbrev=False, description="Scrape AmberELEC per-device cores"
|
||||
)
|
||||
parser.add_argument("--dry-run", action="store_true", help="Show target summary")
|
||||
parser.add_argument("--output", "-o", help="Output YAML file")
|
||||
args = parser.parse_args()
|
||||
|
||||
scraper = Scraper()
|
||||
data = scraper.fetch_targets()
|
||||
if args.dry_run:
|
||||
for name, info in data["targets"].items():
|
||||
print(f" {name} ({info['architecture']}): {len(info['cores'])} cores")
|
||||
return
|
||||
if args.output:
|
||||
scraper.write_output(data, args.output)
|
||||
print(f"Written to {args.output}")
|
||||
return
|
||||
print(yaml.dump(data, default_flow_style=False, sort_keys=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -12,7 +12,6 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
@@ -22,6 +21,7 @@ import yaml
|
||||
|
||||
from common import github_token, yaml_load
|
||||
|
||||
from ..emudeck_launchers import SCRIPT_SUFFIXES, emulator_of_script
|
||||
from . import BaseTargetScraper
|
||||
|
||||
PLATFORM_NAME = "emudeck"
|
||||
@@ -33,24 +33,6 @@ WINDOWS_API = (
|
||||
"https://api.github.com/repos/EmuDeck/emudeck-we/contents/functions/EmuScripts"
|
||||
)
|
||||
|
||||
# Map EmuDeck script names to emulator profile keys
|
||||
# Script naming: emuDeckDolphin.sh -> dolphin
|
||||
# Some need explicit mapping when names differ
|
||||
_NAME_OVERRIDES: dict[str, str] = {
|
||||
"pcsx2qt": "pcsx2",
|
||||
"rpcs3legacy": "rpcs3",
|
||||
"cemuproton": "cemu",
|
||||
"rmg": "mupen64plus_next",
|
||||
"bigpemu": "bigpemu",
|
||||
"eden": "eden",
|
||||
"suyu": "suyu",
|
||||
"ares": "ares",
|
||||
}
|
||||
|
||||
# Scripts that are not emulators (config helpers, etc.)
|
||||
_SKIP = {"retroarch_maincfg", "retroarch"}
|
||||
|
||||
|
||||
def _fetch(url: str) -> str:
|
||||
headers = {"User-Agent": "retrobios-scraper/1.0"}
|
||||
token = github_token()
|
||||
@@ -69,26 +51,13 @@ def _list_emuscripts(api_url: str) -> list[str]:
|
||||
"""List emulator script filenames from GitHub API."""
|
||||
entries = json.loads(_fetch(api_url))
|
||||
names = [
|
||||
e["name"] for e in entries if e.get("name", "").endswith((".sh", ".ps1"))
|
||||
e["name"] for e in entries if e.get("name", "").endswith(SCRIPT_SUFFIXES)
|
||||
]
|
||||
if not names:
|
||||
raise RuntimeError(f"no emulator scripts listed at {api_url}")
|
||||
return names
|
||||
|
||||
|
||||
def _script_to_core(filename: str) -> str | None:
|
||||
"""Convert EmuScripts filename to core profile key."""
|
||||
# Strip extension and emuDeck prefix
|
||||
name = re.sub(r"\.(sh|ps1)$", "", filename, flags=re.IGNORECASE)
|
||||
name = re.sub(r"^emuDeck", "", name, flags=re.IGNORECASE)
|
||||
if not name:
|
||||
return None
|
||||
key = name.lower()
|
||||
if key in _SKIP:
|
||||
return None
|
||||
return _NAME_OVERRIDES.get(key, key)
|
||||
|
||||
|
||||
class Scraper(BaseTargetScraper):
|
||||
"""Fetches emulator lists for EmuDeck SteamOS and Windows targets."""
|
||||
|
||||
@@ -114,14 +83,13 @@ class Scraper(BaseTargetScraper):
|
||||
seen: set[str] = set()
|
||||
has_retroarch = False
|
||||
for script in scripts:
|
||||
core = _script_to_core(script)
|
||||
if core and core not in seen:
|
||||
core = emulator_of_script(script)
|
||||
# RetroArch stands for its libretro cores, added below.
|
||||
if core == "retroarch":
|
||||
has_retroarch = True
|
||||
elif core and core not in seen:
|
||||
seen.add(core)
|
||||
cores.append(core)
|
||||
# Detect RetroArch presence (provides all libretro cores)
|
||||
name = re.sub(r"\.(sh|ps1)$", "", script, flags=re.IGNORECASE)
|
||||
if name.lower() in ("emudeckretroarch", "retroarch_maincfg"):
|
||||
has_retroarch = True
|
||||
|
||||
standalone_count = len(cores)
|
||||
# EmuDeck ships RetroArch = all its libretro cores are available
|
||||
|
||||
@@ -0,0 +1,662 @@
|
||||
"""Scraper for EmuELEC per-device emulator availability.
|
||||
|
||||
Sources (EmuELEC/EmuELEC, at the tag platforms/emuelec.yml names, v4.8):
|
||||
- the release's asset names: one `EmuELEC-<DEVICE>.<ARCH>-<version>`
|
||||
image per device (Amlogic-ng, GameForce, OdroidGoAdvance, OdroidM1 at
|
||||
v4.8). The tree names no device list: projects/ holds the directories
|
||||
of many more devices than the release builds.
|
||||
- packages/sx05re/emuelec/package.mk -- the metapackage, whose
|
||||
PKG_DEPENDS_TARGET is each device's package list (lines 8-77)
|
||||
- distributions/EmuELEC/options -- LIBRETRO_CORES and
|
||||
LIBRETRO_S922X_CORES (lines 274-373)
|
||||
- each core package's package.mk -- PKG_ARCH, which scripts/build:28-30
|
||||
reads to skip a package, and the install step naming the .so or binary
|
||||
- packages/sx05re/emuelec-emulationstation/package.mk -- the cores and
|
||||
emulators makeinstall_target deletes from es_systems.cfg per device
|
||||
(lines 114-143)
|
||||
- es_systems.json and ports.yaml, read the way the platform scraper reads
|
||||
them
|
||||
|
||||
A device keeps a core when a package it builds installs that core and its
|
||||
es_systems.cfg still offers it. The OdroidGoAdvance image carries neither
|
||||
DuckStation nor Dolphin nor the MAME core: without a target list, its pack
|
||||
shipped their firmware anyway.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from typing import NamedTuple
|
||||
|
||||
import yaml
|
||||
|
||||
from . import BaseTargetScraper
|
||||
|
||||
PLATFORM_NAME = "emuelec"
|
||||
|
||||
REPO = "EmuELEC/EmuELEC"
|
||||
RAW = "https://raw.githubusercontent.com/EmuELEC/EmuELEC/{ref}/{path}"
|
||||
RELEASE_API = "https://api.github.com/repos/EmuELEC/EmuELEC/releases/tags/{ref}"
|
||||
META_MK = "packages/sx05re/emuelec/package.mk"
|
||||
OPTIONS = "distributions/EmuELEC/options"
|
||||
ES_MK = "packages/sx05re/emuelec-emulationstation/package.mk"
|
||||
|
||||
try:
|
||||
from ..base_scraper import fetch_bytes, github_headers
|
||||
from ..batocera_scraper import _not_cores
|
||||
from ..emuelec_scraper import (
|
||||
BUILDS_OF, ES_SYSTEMS_TEMPLATE, PORTS_TEMPLATE, RUNNERS, port_engines, with_ports,
|
||||
)
|
||||
from ..es_emulators import SystemEmulators, es_cores
|
||||
except ImportError:
|
||||
from scraper.base_scraper import fetch_bytes, github_headers
|
||||
from scraper.batocera_scraper import _not_cores
|
||||
from scraper.emuelec_scraper import (
|
||||
BUILDS_OF, ES_SYSTEMS_TEMPLATE, PORTS_TEMPLATE, RUNNERS, port_engines, with_ports,
|
||||
)
|
||||
from scraper.es_emulators import SystemEmulators, es_cores
|
||||
|
||||
_ASSET_RE = re.compile(r"^EmuELEC-(?P<device>[A-Za-z0-9-]+)\.(?P<arch>[a-z0-9_]+)-")
|
||||
|
||||
# The metapackage, line for line. Each rule below transcribes these lines;
|
||||
# a tag that rewrites one stops the scrape instead of reading it wrong.
|
||||
_META_LINES = (
|
||||
'PKG_DEPENDS_TARGET="toolchain ${OPENGLES} emuelec-emulationstation retroarch"',
|
||||
'PKG_DEPENDS_TARGET+=" emuelec-tools ${PKG_EMUS} ${PKG_EXPERIMENTAL}"',
|
||||
'if [ "${DEVICE}" == "Amlogic-ng" ] || [ "${DEVICE}" == "Amlogic-no" ] || '
|
||||
'[ "${DEVICE}" == "RK356x" ] || [ "${DEVICE}" == "OdroidM1" ]; then',
|
||||
'PKG_DEPENDS_TARGET+=" ${LIBRETRO_S922X_CORES}"',
|
||||
'if [ "${DEVICE}" == "OdroidGoAdvance" ] || [ "${DEVICE}" == "GameForce" ]; then',
|
||||
'PKG_DEPENDS_TARGET+=" kmscon odroidgoa-utils"',
|
||||
"for discore in duckstation mesen-s virtualjaguar quicknes MC; do",
|
||||
'PKG_DEPENDS_TARGET=$(echo ${PKG_DEPENDS_TARGET} | sed "s|${discore} | |")',
|
||||
'PKG_DEPENDS_TARGET+=" yabasanshiro"',
|
||||
'PKG_DEPENDS_TARGET+=" fbterm"',
|
||||
'if [ "${ARCH}" == "aarch64" ]; then',
|
||||
"for discore in quicknes parallel-n64 pcsx_rearmed; do",
|
||||
'PKG_DEPENDS_TARGET=$(echo ${PKG_DEPENDS_TARGET} | sed "s|${discore}| |")',
|
||||
'PKG_DEPENDS_TARGET+=" dolphinSA"',
|
||||
'if [ "${DEVICE}" == "Amlogic-old" ]; then',
|
||||
"for discore in yabasanshiroSA_1_11 yabasanshiroSA_1_5 yabasanshiro same_cdi duckstation; do",
|
||||
'PKG_DEPENDS_TARGET+=" mame"',
|
||||
'if [ "${DEVICE}" == "RK356x" ] || [ "${DEVICE}" == "OdroidM1" ]; then',
|
||||
"for discore in flycast-dojo; do",
|
||||
)
|
||||
_LIB32_RE = re.compile(r'PKG_DEPENDS_TARGET\+=" (swanstation [^"]*)"')
|
||||
_BIG = ("Amlogic-ng", "Amlogic-no", "RK356x", "OdroidM1")
|
||||
_HANDHELD = ("OdroidGoAdvance", "GameForce")
|
||||
|
||||
# What emuelec-emulationstation's makeinstall_target does to es_systems.cfg.
|
||||
_ES_LINES = (
|
||||
'if [[ "${DEVICE}" != "Amlogic-ng" || "${DEVICE}" != "Amlogic-ne" || '
|
||||
'"${DEVICE}" != "Amlogic-no" ]]; then',
|
||||
'if [[ ${DEVICE} == "OdroidGoAdvance" || "${DEVICE}" == "GameForce" ]]; then',
|
||||
'remove_cores="mesen-s quicknes mame2016 mesen"',
|
||||
'remove_cores="mesen-s quicknes mame2016 mesen yabasanshiroSA yabasanshiro"',
|
||||
"xmlstarlet ed -L -P -d \"/systemList/system[name='saturn']\" ${CORESFILE}",
|
||||
"xmlstarlet ed -L -P -d \"/systemList/system[name='phillips-cdi']\" ${CORESFILE}",
|
||||
'sed -i "s|<core>${discore}</core>||g" ${CORESFILE}',
|
||||
'if [[ ${DEVICE} != "OdroidGoAdvance" ]] && [[ "${DEVICE}" != "GameForce" ]]; then',
|
||||
"xmlstarlet ed -L -P -d \"/systemList/system/emulators/emulator[@name='retrorun']\" ${CORESFILE}",
|
||||
"xmlstarlet ed -L -P -d \"/systemList/system/emulators/emulator[@name='Duckstation']\" ${CORESFILE}",
|
||||
)
|
||||
|
||||
|
||||
class Install(NamedTuple):
|
||||
"""One line of a package.mk that puts a core on the image."""
|
||||
|
||||
path: str
|
||||
line: str
|
||||
cores: tuple[str, ...]
|
||||
# The ARCH test the line sits under in makeinstall_target, if any.
|
||||
arch: str | None = None
|
||||
|
||||
|
||||
def _lr(package: str, line: str, *cores: str, arch: str | None = None) -> Install:
|
||||
return Install(f"packages/sx05re/libretro/{package}/package.mk", line, cores, arch)
|
||||
|
||||
|
||||
def _sa(package: str, line: str, *cores: str) -> Install:
|
||||
return Install(f"packages/sx05re/emulators/{package}/package.mk", line, cores)
|
||||
|
||||
|
||||
def _lib32(package: str, line: str, *cores: str) -> Install:
|
||||
return Install(f"packages/lib32/emuelec/libretro/{package}/package.mk", line, cores)
|
||||
|
||||
|
||||
_TO = "${INSTALL}/usr/lib/libretro/"
|
||||
|
||||
# Package -> the ES core names its install step puts on the image, with the
|
||||
# line that does it (v4.8). A libretro core is the .so it copies to
|
||||
# /usr/lib/libretro, the name emuelecRunEmu.sh:101-116 loads as
|
||||
# ${CORE}_libretro. A standalone name is the program the runner's case for
|
||||
# it starts (emuelecRunEmu.sh:164-353), installed by the line cited, or by
|
||||
# the package's default make/cmake/meson install where the line cited only
|
||||
# adds the launch script beside it.
|
||||
PROVIDERS: dict[str, tuple[Install, ...]] = {
|
||||
"2048": (_lr("2048", f"cp 2048_libretro.so {_TO}", "2048"),),
|
||||
"81": (_lr("81", f"cp 81_libretro.so {_TO}", "81"),),
|
||||
"a5200": (_lr("a5200", f"cp a5200_libretro.so {_TO}", "a5200"),),
|
||||
"atari800": (_lr("atari800", f"cp atari800_libretro.so {_TO}", "atari800"),),
|
||||
"beetle-lynx": (_lr("beetle-lynx", f"cp mednafen_lynx_libretro.so {_TO}", "mednafen_lynx"),),
|
||||
"beetle-ngp": (_lr("beetle-ngp", f"cp mednafen_ngp_libretro.so {_TO}", "mednafen_ngp"),),
|
||||
"beetle-pce": (_lr("beetle-pce", f"cp mednafen_pce_fast_libretro.so {_TO}",
|
||||
"mednafen_pce_fast"),),
|
||||
"beetle-pcfx": (_lr("beetle-pcfx", f"cp mednafen_pcfx_libretro.so {_TO}", "mednafen_pcfx"),),
|
||||
# line 20: the build makes mednafen_saturn_libretro.so (Makefile TARGET_NAME).
|
||||
"beetle-saturn": (_lr("beetle-saturn", "cp mednafen_saturn_*.so $INSTALL/usr/lib/libretro/",
|
||||
"mednafen_saturn"),),
|
||||
"beetle-supergrafx": (_lr("beetle-supergrafx", f"cp mednafen_supergrafx_libretro.so {_TO}",
|
||||
"mednafen_supergrafx"),),
|
||||
"beetle-vb": (_lr("beetle-vb", f"cp mednafen_vb_libretro.so {_TO}", "mednafen_vb"),),
|
||||
"beetle-wswan": (_lr("beetle-wswan", f"cp mednafen_wswan_libretro.so {_TO}",
|
||||
"mednafen_wswan"),),
|
||||
"bk": (_lr("bk", f"cp ${{PKG_BUILD}}/bk_libretro.so {_TO}", "bk"),),
|
||||
"bluemsx": (_lr("bluemsx", f"cp bluemsx_libretro.so {_TO}", "bluemsx"),),
|
||||
"cannonball": (_lr("cannonball", f"cp cannonball_libretro.so {_TO}", "cannonball"),),
|
||||
"cap32": (_lr("cap32", f"cp cap32_libretro.so {_TO}", "cap32"),),
|
||||
"daphne": (_lr("daphne", f"cp daphne_libretro.so {_TO}", "daphne"),),
|
||||
"dinothawr": (_lr("dinothawr", f"cp dinothawr_libretro.so {_TO}", "dinothawr"),),
|
||||
"dosbox-pure": (_lr("dosbox-pure", f"cp dosbox_pure_libretro.so {_TO}dosbox_pure_libretro.so",
|
||||
"dosbox_pure"),),
|
||||
"dosbox-svn": (_lr("dosbox-svn", "cp ${PKG_BUILD}/libretro/dosbox_svn_libretro.so "
|
||||
"${INSTALL}/usr/lib/libretro", "dosbox_svn"),),
|
||||
"easyrpg": (_lr("easyrpg", f"cp ${{PKG_BUILD}}/.${{TARGET_NAME}}/easyrpg_libretro.so {_TO}",
|
||||
"easyrpg"),),
|
||||
"fake_08": (_lr("fake_08", f"cp -v ${{PKG_BUILD}}/platform/libretro/fake08_libretro.so {_TO}",
|
||||
"fake08"),),
|
||||
"fbalpha2012": (_lr("fbalpha2012", f"cp fbalpha2012_libretro.so {_TO}", "fbalpha2012"),),
|
||||
"fbneo": (_lr("fbneo", f"cp ${{PKG_BUILD}}/src/burner/libretro/fbneo_libretro.so {_TO}",
|
||||
"fbneo"),),
|
||||
"fceumm": (_lr("fceumm", f"cp fceumm_libretro.so {_TO}", "fceumm"),),
|
||||
"fceumm-mod": (_lr("fceumm-mod", f"cp ${{PKG_BUILD}}/fceumm_mod_libretro.* {_TO}",
|
||||
"fceumm_mod"),),
|
||||
# lines 47-51: flycast_32b on arm, flycast elsewhere.
|
||||
"flycast": (
|
||||
_lr("flycast", f"cp flycast_libretro.so {_TO}flycast_32b_libretro.so", "flycast_32b",
|
||||
arch="arm"),
|
||||
_lr("flycast", f"cp flycast_libretro.so {_TO}", "flycast", arch="!arm"),
|
||||
),
|
||||
"fmsx-libretro": (_lr("fmsx-libretro", f"cp fmsx_libretro.so {_TO}", "fmsx"),),
|
||||
"freeintv": (_lr("freeintv", f"cp freeintv_libretro.so {_TO}", "freeintv"),),
|
||||
"freej2me": (_lr("freej2me", 'cp `find . -name "freej2me_libretro.so" | xargs echo` '
|
||||
"${INSTALL}/usr/lib/libretro", "freej2me"),),
|
||||
"fuse-libretro": (_lr("fuse-libretro", f"cp fuse_libretro.so {_TO}", "fuse"),),
|
||||
"gambatte": (_lr("gambatte", f"cp gambatte_libretro.so {_TO}", "gambatte"),),
|
||||
"gearboy": (_lr("gearboy", f"cp platforms/libretro/gearboy_libretro.so {_TO}", "gearboy"),),
|
||||
"gearcoleco": (_lr("gearcoleco", f"cp platforms/libretro/gearcoleco_libretro.so {_TO}",
|
||||
"gearcoleco"),),
|
||||
"geargrafx": (_lr("geargrafx", f"cp platforms/libretro/geargrafx_libretro.so {_TO}",
|
||||
"geargrafx"),),
|
||||
"gearlynx": (_lr("gearlynx", f"cp platforms/libretro/gearlynx_libretro.so {_TO}",
|
||||
"gearlynx"),),
|
||||
"gearsystem": (_lr("gearsystem", f"cp platforms/libretro/gearsystem_libretro.so {_TO}",
|
||||
"gearsystem"),),
|
||||
"genesis-plus-gx": (_lr("genesis-plus-gx",
|
||||
f"cp genesis_plus_gx_libretro.so {_TO}genesis_plus_gx_libretro.so",
|
||||
"genesis_plus_gx"),),
|
||||
"genesis-plus-gx-wide": (_lr("genesis-plus-gx-wide", "cp genesis_plus_gx_wide_libretro.so "
|
||||
f"{_TO}genesis_plus_gx_wide_libretro.so",
|
||||
"genesis_plus_gx_wide"),),
|
||||
# PKG_NAME is genesis_plus_gx_cart_special (line 4).
|
||||
"genesis_plus_gx_cart_special": (_lr("genesis_plus_gx_cart_special",
|
||||
f"cp genesis_plus_gx_libretro.so {_TO}"
|
||||
"${PKG_NAME}_libretro.so",
|
||||
"genesis_plus_gx_cart_special"),),
|
||||
"geolith": (_lr("geolith", f"cp geolith_libretro.so {_TO}", "geolith"),),
|
||||
"gpsp": (_lr("gpsp", f"cp -v gpsp_libretro.so {_TO}", "gpsp"),),
|
||||
"gw-libretro": (_lr("gw-libretro", f"cp gw_libretro.so {_TO}", "gw"),),
|
||||
"handy": (_lr("handy", f"cp handy_libretro.so {_TO}", "handy"),),
|
||||
"hatari": (_lr("hatari", f"cp ../hatari_libretro.so {_TO}", "hatari"),),
|
||||
# line 65: the build's only .so is mame_libretro.so.
|
||||
"mame": (_lr("mame", f"cp *.so {_TO}", "mame"),),
|
||||
"mame2003-plus": (_lr("mame2003-plus", f"cp mame2003_plus_libretro.so {_TO}",
|
||||
"mame2003_plus"),),
|
||||
"mame2010": (_lr("mame2010", f"cp mame2010_libretro.so {_TO}", "mame2010"),),
|
||||
"meowpc98": (_lr("meowpc98", f"cp libretro/nekop2_libretro.so {_TO}", "nekop2"),),
|
||||
"mesen": (_lr("mesen", f"cp Libretro/mesen_libretro.so {_TO}", "mesen"),),
|
||||
"mesen-s": (_lr("mesen-s", f"cp Libretro/mesen-s_libretro.so {_TO}", "mesen-s"),),
|
||||
"mgba": (_lr("mgba", f"cp mgba_libretro.so {_TO}", "mgba"),),
|
||||
"mrboom": (_lr("mrboom", f"cp mrboom_libretro.so {_TO}", "mrboom"),),
|
||||
# PKG_ARCH="arm" (line 25): scripts/build skips it on aarch64.
|
||||
"mupen64plus": (_lr("mupen64plus", f"cp mupen64plus_libretro.so {_TO}"
|
||||
"mupen64plus_32b_libretro.so", "mupen64plus_32b"),),
|
||||
"mupen64plus-nx": (_lr("mupen64plus-nx", f"cp mupen64plus_next_libretro.so {_TO}",
|
||||
"mupen64plus_next"),),
|
||||
"mupen64plus-nx-alt": (_lr("mupen64plus-nx-alt", f"cp mupen64plus_next_libretro.so {_TO}"
|
||||
"mupen64plus_next_alt_libretro.so", "mupen64plus_next_alt"),),
|
||||
"neocd_libretro": (_lr("neocd_libretro", f"cp ${{PKG_BUILD}}/neocd_libretro.so {_TO}",
|
||||
"neocd"),),
|
||||
"nestopia": (_lr("nestopia", f"cp libretro/nestopia_libretro.so {_TO}", "nestopia"),),
|
||||
"np2kai": (_lr("np2kai", f"cp ${{PKG_BUILD}}/sdl/np2kai_libretro.so {_TO}", "np2kai"),),
|
||||
"nxengine": (_lr("nxengine", f"cp nxengine_libretro.so {_TO}", "nxengine"),),
|
||||
"o2em": (_lr("o2em", f"cp o2em_libretro.so {_TO}", "o2em"),),
|
||||
"opera": (_lr("opera", f"cp opera_libretro.so {_TO}", "opera"),),
|
||||
# lines 42-46: pcsx_rearmed_32b on arm, pcsx_rearmed elsewhere.
|
||||
"pcsx_rearmed": (
|
||||
_lr("pcsx_rearmed", "cp pcsx_rearmed_libretro.so "
|
||||
"${INSTALL}${INSTALLTO}/pcsx_rearmed_32b_libretro.so", "pcsx_rearmed_32b", arch="arm"),
|
||||
_lr("pcsx_rearmed", "cp pcsx_rearmed_libretro.so ${INSTALL}${INSTALLTO}",
|
||||
"pcsx_rearmed", arch="!arm"),
|
||||
),
|
||||
"picodrive": (_lr("picodrive", f"cp ${{PKG_BUILD}}/picodrive_libretro.so {_TO}",
|
||||
"picodrive"),),
|
||||
"pocketcdg": (_lr("pocketcdg", f"cp pocketcdg_libretro.so {_TO}", "pocketcdg"),),
|
||||
"pokemini": (_lr("pokemini", f"cp pokemini_libretro.so {_TO}", "pokemini"),),
|
||||
"potator": (_lr("potator", "cp ${PKG_BUILD}/platform/libretro/potator_libretro.so "
|
||||
f"{_TO}potator_libretro.so", "potator"),),
|
||||
"prboom": (_lr("prboom", f"cp prboom_libretro.so {_TO}prboom_libretro.so", "prboom"),),
|
||||
"prosystem": (_lr("prosystem", f"cp prosystem_libretro.so {_TO}", "prosystem"),),
|
||||
"puae": (_lr("puae", f"cp puae_libretro.so {_TO}", "puae"),),
|
||||
"puae2021": (_lr("puae2021", f"cp -v puae2021_libretro.so {_TO}", "puae2021"),),
|
||||
"px68k": (_lr("px68k", f"cp px68k_libretro.so {_TO}", "px68k"),),
|
||||
"reminiscence": (_lr("reminiscence", f"cp reminiscence_libretro.so {_TO}", "reminiscence"),),
|
||||
"same_cdi": (_lr("same_cdi", f"cp -v same_cdi_libretro.so {_TO}", "same_cdi"),),
|
||||
"sameboy": (_lr("sameboy", f"cp libretro/sameboy_libretro.so {_TO}", "sameboy"),),
|
||||
"sameduck": (_lr("sameduck", f"cp build/bin/sameduck_libretro.so {_TO}", "sameduck"),),
|
||||
"scummvm": (_lr("scummvm", 'cp "${PKG_BUILD}/backends/platform/libretro/scummvm_libretro."'
|
||||
"{so,info} ${INSTALL}/usr/lib/libretro/", "scummvm"),),
|
||||
"snes9x": (_lr("snes9x", f"cp libretro/snes9x_libretro.so {_TO}", "snes9x"),),
|
||||
"snes9x2002": (_lr("snes9x2002", f"cp snes9x2002_libretro.so {_TO}", "snes9x2002"),),
|
||||
"snes9x2005_plus": (_lr("snes9x2005_plus", f"cp snes9x2005_plus_libretro.so {_TO}",
|
||||
"snes9x2005_plus"),),
|
||||
"snes9x2010": (_lr("snes9x2010", f"cp snes9x2010_libretro.so {_TO}", "snes9x2010"),),
|
||||
"stella": (_lr("stella", f"cp ${{PKG_BUILD}}/src/os/libretro/stella_libretro.so {_TO}",
|
||||
"stella"),),
|
||||
"supafaust": (_lr("supafaust", f"cp -v mednafen_supafaust_libretro.so {_TO}",
|
||||
"mednafen_supafaust"),),
|
||||
"swanstation": (_lr("swanstation", "cp ${PKG_BUILD}/.${TARGET_NAME}/swanstation_libretro.so "
|
||||
f"{_TO}", "swanstation"),),
|
||||
"tgbdual": (_lr("tgbdual", f"cp tgbdual_libretro.so {_TO}", "tgbdual"),),
|
||||
"tic-80": (_lr("tic-80", "cp ${PKG_BUILD}/.${TARGET_NAME}/bin/tic80_libretro.so "
|
||||
f"{_TO}tic80_libretro.so", "tic80"),),
|
||||
"tyrquake": (_lr("tyrquake", f"cp tyrquake_libretro.so {_TO}", "tyrquake"),),
|
||||
# PKG_ARCH="arm" (line 25): scripts/build skips it on aarch64.
|
||||
"uae4arm": (_lr("uae4arm", f"cp uae4arm_libretro.so {_TO}uae4arm_32b_libretro.so",
|
||||
"uae4arm_32b"),),
|
||||
"uzem": (_lr("uzem", f"cp uzem_libretro.so {_TO}", "uzem"),),
|
||||
"vba-next": (_lr("vba-next", f"cp vba_next_libretro.so {_TO}", "vba_next"),),
|
||||
"vbam": (_lr("vbam", f"cp ../src/libretro/vbam_libretro.so {_TO}", "vbam"),),
|
||||
"vecx": (_lr("vecx", f"cp vecx_libretro.so {_TO}vecx_libretro.so", "vecx"),),
|
||||
# line 55: make_target builds one vice_<emu>_libretro.so per emulator it
|
||||
# loops over (lines 37-50), x64, x64sc, x128, xplus4 and xvic among them.
|
||||
"vice": (_lr("vice", f"cp built/vice_*_libretro.so {_TO}",
|
||||
"vice_x128", "vice_x64", "vice_x64sc", "vice_xplus4", "vice_xvic"),),
|
||||
"virtualjaguar": (_lr("virtualjaguar", f"cp virtualjaguar_libretro.so {_TO}",
|
||||
"virtualjaguar"),),
|
||||
"xmil": (_lr("xmil", f"cp ${{PKG_BUILD}}/libretro/x1_libretro.so {_TO}", "x1"),),
|
||||
"xrick": (_lr("xrick", f"cp xrick_libretro.so {_TO}", "xrick"),),
|
||||
"yabasanshiro": (_lr("yabasanshiro", f"cp yabause/src/libretro/yabasanshiro_libretro.so {_TO}",
|
||||
"yabasanshiro"),),
|
||||
"lib32-flycast": (_lib32("lib32-flycast", "cp -va flycast_libretro.so "
|
||||
f"{_TO}flycast_32b_libretro.so", "flycast_32b"),),
|
||||
"lib32-mupen64plus": (_lib32("lib32-mupen64plus", "cp mupen64plus_libretro.so "
|
||||
f"{_TO}mupen64plus_32b_libretro.so", "mupen64plus_32b"),),
|
||||
"lib32-parallel-n64": (_lib32("lib32-parallel-n64", "cp parallel_n64_libretro.so "
|
||||
f"{_TO}parallel_n64_32b_libretro.so", "parallel_n64_32b"),),
|
||||
"lib32-pcsx_rearmed": (_lib32("lib32-pcsx_rearmed", "cp pcsx_rearmed_libretro.so "
|
||||
f"{_TO}pcsx_rearmed_32b_libretro.so", "pcsx_rearmed_32b"),),
|
||||
"lib32-uae4arm": (_lib32("lib32-uae4arm", "cp uae4arm_libretro.so "
|
||||
f"{_TO}uae4arm_32b_libretro.so", "uae4arm_32b"),),
|
||||
# Standalone emulators, by the program the runner starts.
|
||||
"PPSSPPSDL": (_sa("PPSSPPSDL", 'cp `find . -name "PPSSPPSDL" | xargs echo` '
|
||||
"${INSTALL}/usr/bin/PPSSPPSDL", "PPSSPPSDL"),),
|
||||
"advancemame": (_sa("advancemame", "cp -r ${PKG_BUILD}/obj/mame/linux/blend/advmame "
|
||||
"${INSTALL}/usr/bin", "AdvanceMame"),),
|
||||
"amiberry": (_sa("amiberry", "cp -a amiberry* ${INSTALL}/usr/bin/amiberry", "AMIBERRY"),),
|
||||
"dolphinSA": (_sa("dolphinSA", "cp -rf ${PKG_BUILD}/.${TARGET_NAME}/Binaries/"
|
||||
"dolphin-emu-nogui ${INSTALL}/usr/bin", "dolphin"),),
|
||||
# dosbox comes from the meson install, dosbox.start from line 28.
|
||||
"dosbox-staging": (_sa("dosbox-staging", "cp -a ${PKG_DIR}/scripts/* ${INSTALL}/usr/bin/",
|
||||
"DOSBOXSDL2"),),
|
||||
"duckstation": (_sa("duckstation", "cp -rf ${PKG_BUILD}/.${TARGET_NAME}/bin/"
|
||||
"duckstation-nogui ${INSTALL}/usr/bin", "Duckstation"),),
|
||||
"ecwolf": (_sa("ecwolf", "cp ${PKG_BUILD}/ecwolf ${INSTALL}/usr/bin/ecwolf", "ECWolf"),),
|
||||
"fbneoSA": (_sa("fbneoSA", "cp -rf ${PKG_BUILD}/fbneo ${INSTALL}/usr/bin", "FBNEOSA"),),
|
||||
"flycastsa": (_sa("flycastsa", "cp ${PKG_BUILD}/.${TARGET_NAME}/flycast "
|
||||
"${INSTALL}/usr/bin/flycast", "flycastsa"),),
|
||||
"hatarisa": (_sa("hatarisa", "cp src/hatari ${INSTALL}/usr/bin", "HATARISA"),),
|
||||
# hypseus comes from the cmake install (PKG_TOOLCHAIN, line 13).
|
||||
"hypseus-singe": (_sa("hypseus-singe", 'PKG_TOOLCHAIN="cmake"', "HYPSEUS"),),
|
||||
"ikemen-go": (_sa("ikemen-go", 'cp "$PKG_BUILD/bin/Ikemen_Go_"* "$INSTALL/usr/bin/Ikemen_Go"',
|
||||
"ikemen"),),
|
||||
"jzintv": (_sa("jzintv", "cp bin/jzintv ${INSTALL}/usr/bin", "jzintv"),),
|
||||
# The metapackage's dependencies (line 10). mupen64plus itself comes from
|
||||
# mupen64plussa-ui-console's makeinstall_target.
|
||||
"mupen64plussa": (
|
||||
_sa("mupen64plussa", 'PKG_DEPENDS_TARGET="toolchain mupen64plussa-core '
|
||||
"mupen64plussa-ui-console mupen64plussa-audio-sdl mupen64plussa-input-sdl "
|
||||
'mupen64plussa-rsp-hle mupen64plussa-video-rice mupen64plussa-video-glide64mk2"',
|
||||
"mupen64plussa"),
|
||||
Install("packages/sx05re/emulators/mupen64plussa/mupen64plussa-ui-console/package.mk",
|
||||
"cp ${PKG_BUILD}/projects/unix/mupen64plus ${UBINDIR}", ()),
|
||||
),
|
||||
"openbor": (_sa("openbor", 'cp `find . -name "OpenBOR.elf" | xargs echo` '
|
||||
"${INSTALL}/usr/bin/OpenBOR", "OPENBOR"),),
|
||||
# scummvm comes from the make install, moved to /usr/bin at line 51.
|
||||
"scummvmsa": (_sa("scummvmsa", "mv ${INSTALL}/usr/local/bin ${INSTALL}/usr/", "SCUMMVMSA"),),
|
||||
# solarus-run comes from the cmake install, solarus.sh from line 22.
|
||||
"solarus": (_sa("solarus", "cp ${PKG_DIR}/scripts/* ${INSTALL}/usr/bin", "Solarus"),),
|
||||
"stellasa": (_sa("stellasa", "cp -rf ${PKG_BUILD}/stella ${INSTALL}/usr/bin", "STELLASA"),),
|
||||
"yabasanshiroSA_1_11": (_sa("yabasanshiroSA_1_11", "cp -a ${PKG_BUILD}/src/retro_arena/"
|
||||
"yabasanshiro ${INSTALL}/usr/bin", "yabasanshiro"),),
|
||||
"lib32-bennugd-monolithic": (Install(
|
||||
"packages/lib32/emuelec/ports/lib32-bennugd-monolithic/package.mk",
|
||||
"cp ${PKG_BUILD}/projects/cmake/bgdi/bgdi ${INSTALL}/usr/bin", ("bgdi",)),),
|
||||
# The metapackage's own bin/ (line 82) carries pico8.sh, which starts the
|
||||
# PICO-8 binary the user copies to bios/pico-8.
|
||||
"emuelec": (Install(META_MK, "cp -rf ${PKG_DIR}/bin ${INSTALL}/usr", ("Pico-8",)),),
|
||||
}
|
||||
|
||||
# Packages of a device list whose install step puts no ES core on the image.
|
||||
NO_ES_CORE: dict[str, str] = {
|
||||
"toolchain": "build toolchain",
|
||||
"${OPENGLES}": "graphics driver",
|
||||
"emuelec-emulationstation": "the frontend",
|
||||
"retroarch": "the libretro frontend",
|
||||
"lib32-retroarch": "the 32-bit libretro frontend, retroarch32",
|
||||
"emuelec-tools": "system tools",
|
||||
"emuelec-32bit-info": "the .info files of the 32-bit cores",
|
||||
"lib32-essential": "32-bit runtime",
|
||||
"lib32-libusb": "32-bit library",
|
||||
"lib32-box86": "x86 translator",
|
||||
"fbterm": "terminal",
|
||||
"kmscon": "terminal",
|
||||
"odroidgoa-utils": "device tools and retrorun, a runner",
|
||||
"amiberry-lite": "amiberry-lite, which es_systems.json names nowhere",
|
||||
"yabasanshiroSA_1_5": "yabasanshiro1_5, which es_systems.json names nowhere",
|
||||
"lib32-droidports": "gmloader, which es_cores drops (_overrides.yml)",
|
||||
"gme": "gme_libretro.so, which es_systems.json names nowhere",
|
||||
"nestopiaCV": "nestopiaCV_libretro.so, which es_systems.json names nowhere",
|
||||
"quasi88": "quasi88_libretro.so, which es_systems.json names nowhere",
|
||||
"spring": "spring_*_libretro.so, which es_systems.json names nowhere",
|
||||
}
|
||||
|
||||
# ES core names no package of any device builds, by the package that would.
|
||||
# The port engines are dependencies of emuelec-ports (its package.mk,
|
||||
# lines 17-59), and no package of the image depends on emuelec-ports;
|
||||
# flycast-dojo is in no device list.
|
||||
UNBUILT: dict[str, str] = {
|
||||
**dict.fromkeys(
|
||||
("CGeniusExe", "Chocolate-Doom", "GZDoom", "LZDoom", "OpenJazz", "RigelEngine",
|
||||
"VVVVVV", "abuse", "bs", "bstone", "ccleste", "cdogs-sdl", "devilutionx",
|
||||
"eduke32", "fallout-ce", "fallout2-ce", "fheroes2", "hcl", "hode", "hurrican",
|
||||
"opentyrian", "prince", "smw", "sonic2013", "soniccd", "sonicmania", "supertux2",
|
||||
"supertuxkart", "xash3d"),
|
||||
"emuelec-ports",
|
||||
),
|
||||
"flycast_dojo": "flycast-dojo",
|
||||
}
|
||||
|
||||
|
||||
def release_devices(asset_names: list[str]) -> dict[str, str]:
|
||||
"""{device: architecture} of each image a release publishes."""
|
||||
found: dict[str, str] = {}
|
||||
for name in asset_names:
|
||||
match = _ASSET_RE.match(name)
|
||||
if match:
|
||||
found.setdefault(match.group("device"), match.group("arch"))
|
||||
if not found:
|
||||
raise RuntimeError("the EmuELEC release publishes no device image")
|
||||
return found
|
||||
|
||||
|
||||
def _require(text: str, lines: tuple[str, ...], where: str) -> None:
|
||||
present = {line.strip() for line in text.splitlines()}
|
||||
missing = [line for line in lines if line not in present]
|
||||
if missing:
|
||||
raise RuntimeError(f"{where} no longer reads {missing[0]!r}")
|
||||
|
||||
|
||||
def _quoted(text: str, name: str, where: str) -> list[str]:
|
||||
"""The words of `NAME="..."`; a backslash-newline inside the quotes is
|
||||
removed by bash, so the value reads as one line."""
|
||||
match = re.search(rf'^\s*{name}="([^"]*)"', text, re.MULTILINE)
|
||||
if match is None:
|
||||
raise RuntimeError(f"{where} sets no {name}")
|
||||
return match.group(1).replace("\\\n", " ").split()
|
||||
|
||||
|
||||
class Metapackage(NamedTuple):
|
||||
emus: list[str]
|
||||
experimental: list[str]
|
||||
s922x: list[str]
|
||||
lib32: list[str]
|
||||
|
||||
|
||||
def read_metapackage(package_mk: str, options: str) -> Metapackage:
|
||||
"""The lists the metapackage rules work on, with PKG_EMUS expanded."""
|
||||
_require(package_mk, _META_LINES, META_MK)
|
||||
emus = _quoted(package_mk, "PKG_EMUS", META_MK)
|
||||
if emus[:1] != ["${LIBRETRO_CORES}"]:
|
||||
raise RuntimeError(f"{META_MK}: PKG_EMUS no longer starts with ${{LIBRETRO_CORES}}")
|
||||
lib32 = _LIB32_RE.search(package_mk)
|
||||
if lib32 is None:
|
||||
raise RuntimeError(f"{META_MK}: no aarch64 lib32 list")
|
||||
return Metapackage(
|
||||
emus=_quoted(options, "LIBRETRO_CORES", OPTIONS) + emus[1:],
|
||||
experimental=_quoted(package_mk, "PKG_EXPERIMENTAL", META_MK),
|
||||
s922x=_quoted(options, "LIBRETRO_S922X_CORES", OPTIONS),
|
||||
lib32=lib32.group(1).replace("\\\n", " ").split(),
|
||||
)
|
||||
|
||||
|
||||
def _sed_first(words: str, pattern: str) -> str:
|
||||
"""`echo ${PKG_DEPENDS_TARGET} | sed "s|<pattern>| |"`: the unquoted echo
|
||||
joins the words with single spaces, sed replaces the first match only,
|
||||
anywhere in the line, a word's tail included."""
|
||||
line = " ".join(words.split())
|
||||
at = line.find(pattern)
|
||||
return line if at < 0 else f"{line[:at]} {line[at + len(pattern):]}"
|
||||
|
||||
|
||||
def device_packages(meta: Metapackage, device: str, arch: str) -> list[str]:
|
||||
"""PKG_DEPENDS_TARGET of the metapackage for one device (lines 8-77)."""
|
||||
depends = "toolchain ${OPENGLES} emuelec-emulationstation retroarch"
|
||||
depends += " emuelec-tools " + " ".join(meta.emus) + " " + " ".join(meta.experimental)
|
||||
if device in _BIG:
|
||||
depends += " " + " ".join(meta.s922x)
|
||||
if device in _HANDHELD:
|
||||
depends += " kmscon odroidgoa-utils"
|
||||
for discore in ("duckstation", "mesen-s", "virtualjaguar", "quicknes", "MC"):
|
||||
depends = _sed_first(depends, f"{discore} ")
|
||||
depends += " yabasanshiro"
|
||||
else:
|
||||
depends += " fbterm"
|
||||
if arch == "aarch64":
|
||||
for discore in ("quicknes", "parallel-n64", "pcsx_rearmed"):
|
||||
depends = _sed_first(depends, discore)
|
||||
depends += " " + " ".join(meta.lib32)
|
||||
if device in _BIG:
|
||||
depends += " dolphinSA"
|
||||
if device == "Amlogic-old":
|
||||
for discore in ("yabasanshiroSA_1_11", "yabasanshiroSA_1_5", "yabasanshiro",
|
||||
"same_cdi", "duckstation"):
|
||||
depends = _sed_first(depends, f"{discore} ")
|
||||
if device in _BIG:
|
||||
depends += " mame"
|
||||
if device in ("RK356x", "OdroidM1"):
|
||||
depends = _sed_first(depends, "flycast-dojo")
|
||||
return depends.split()
|
||||
|
||||
|
||||
def arch_builds(pkg_arch: str, arch: str) -> bool:
|
||||
"""scripts/build:28-30: an empty PKG_ARCH builds everywhere, `!<arch>`
|
||||
excludes, otherwise the list names the arch or `any`."""
|
||||
words = pkg_arch.split()
|
||||
if not words:
|
||||
return True
|
||||
if f"!{arch}" in words:
|
||||
return False
|
||||
return arch in words or "any" in words
|
||||
|
||||
|
||||
def pkg_arch(package_mk: str) -> str:
|
||||
match = re.search(r'^PKG_ARCH="([^"]*)"', package_mk, re.MULTILINE)
|
||||
return match.group(1) if match else ""
|
||||
|
||||
|
||||
def es_systems_for(es_json: str, es_mk: str, device: str) -> SystemEmulators:
|
||||
"""es_systems.json as es_systems.cfg reads after makeinstall_target's
|
||||
deletions on one device (emuelec-emulationstation/package.mk:114-143).
|
||||
|
||||
`sed "s|<core>X</core>||g"` matches a core written without attributes:
|
||||
generate_es_systems.py gives `default="true"` to the first default core
|
||||
of a system and `incompatible_extensions` to a core listing them.
|
||||
"""
|
||||
_require(es_mk, _ES_LINES, ES_MK)
|
||||
if device in _HANDHELD:
|
||||
remove_cores = {"mesen-s", "quicknes", "mame2016", "mesen"}
|
||||
drop_systems: set[str] = set()
|
||||
drop_emulators = {"Duckstation"}
|
||||
elif device == "Amlogic-old":
|
||||
remove_cores = {"mesen-s", "quicknes", "mame2016", "mesen", "yabasanshiroSA",
|
||||
"yabasanshiro"}
|
||||
drop_systems = {"saturn", "phillips-cdi"}
|
||||
drop_emulators = {"Duckstation", "retrorun"}
|
||||
else:
|
||||
# Line 117 holds for every device: no name is all three at once.
|
||||
remove_cores = set()
|
||||
drop_systems = set()
|
||||
drop_emulators = {"retrorun"}
|
||||
systems: SystemEmulators = {}
|
||||
for system in json.loads(es_json).get("systems", []):
|
||||
if system["name"] in drop_systems:
|
||||
continue
|
||||
default_set = False
|
||||
emulators = []
|
||||
for emulator in system.get("emulators") or []:
|
||||
kept = []
|
||||
for core in emulator.get("cores") or []:
|
||||
bare = "incompatible" not in core
|
||||
if core.get("default", False) and not default_set:
|
||||
default_set = True
|
||||
bare = False
|
||||
if not (bare and core["name"] in remove_cores):
|
||||
kept.append(core["name"])
|
||||
if emulator["name"] not in drop_emulators:
|
||||
emulators.append((emulator["name"], kept))
|
||||
systems[system["name"]] = emulators
|
||||
return systems
|
||||
|
||||
|
||||
def device_cores(
|
||||
packages: list[str],
|
||||
arch: str,
|
||||
archs: dict[str, str],
|
||||
systems: SystemEmulators,
|
||||
) -> list[str]:
|
||||
"""ES core names a device both installs and offers.
|
||||
|
||||
`archs` maps each package of PROVIDERS to its PKG_ARCH.
|
||||
"""
|
||||
unknown = sorted(
|
||||
p for p in set(packages) if p not in PROVIDERS and p not in NO_ES_CORE
|
||||
and p not in UNBUILT.values()
|
||||
)
|
||||
if unknown:
|
||||
raise RuntimeError(f"{META_MK}: no install step read for {', '.join(unknown)}")
|
||||
unbuilt = sorted(set(packages) & set(UNBUILT.values()))
|
||||
if unbuilt:
|
||||
raise RuntimeError(f"{META_MK}: {unbuilt[0]} is built and its cores are read as absent")
|
||||
installed: set[str] = set()
|
||||
for package in [*set(packages), "emuelec"]:
|
||||
if package not in PROVIDERS or not arch_builds(archs[package], arch):
|
||||
continue
|
||||
for install in PROVIDERS[package]:
|
||||
if install.arch is None or (install.arch == arch) or (
|
||||
install.arch.startswith("!") and install.arch[1:] != arch
|
||||
):
|
||||
installed.update(install.cores)
|
||||
offered, _standalone, _dual = es_cores(
|
||||
systems, RUNNERS, BUILDS_OF, _not_cores(PLATFORM_NAME)
|
||||
)
|
||||
return sorted(installed & set(offered))
|
||||
|
||||
|
||||
def check_names(systems: SystemEmulators) -> None:
|
||||
"""Every ES core name is installed by a package read or known unbuilt."""
|
||||
offered, _standalone, _dual = es_cores(
|
||||
systems, RUNNERS, BUILDS_OF, _not_cores(PLATFORM_NAME)
|
||||
)
|
||||
provided = {core for installs in PROVIDERS.values() for i in installs for core in i.cores}
|
||||
unread = sorted(set(offered) - provided - set(UNBUILT))
|
||||
if unread:
|
||||
raise RuntimeError(f"es_systems.json names {', '.join(unread)}, no package read installs it")
|
||||
|
||||
|
||||
def check_installs(texts: dict[str, str]) -> dict[str, str]:
|
||||
"""{package: PKG_ARCH}, after checking each cited install line is there."""
|
||||
archs: dict[str, str] = {}
|
||||
for package, installs in PROVIDERS.items():
|
||||
for install in installs:
|
||||
_require(texts[install.path], (install.line,), install.path)
|
||||
archs[package] = pkg_arch(texts[installs[0].path])
|
||||
return archs
|
||||
|
||||
|
||||
class Scraper(BaseTargetScraper):
|
||||
"""Per-device core lists from EmuELEC's metapackage."""
|
||||
|
||||
def __init__(self, url: str = "https://github.com/" + REPO) -> None:
|
||||
super().__init__(url=url)
|
||||
self.ref = ""
|
||||
|
||||
def _raw(self, path: str) -> str:
|
||||
return fetch_bytes(RAW.format(ref=self.ref, path=path)).decode("utf-8")
|
||||
|
||||
def fetch_targets(self) -> dict:
|
||||
self.ref = self.pinned_revision(PLATFORM_NAME)
|
||||
release = json.loads(
|
||||
fetch_bytes(RELEASE_API.format(ref=self.ref), headers=github_headers())
|
||||
)
|
||||
devices = release_devices([asset["name"] for asset in release.get("assets", [])])
|
||||
meta = read_metapackage(self._raw(META_MK), self._raw(OPTIONS))
|
||||
paths = sorted({i.path for installs in PROVIDERS.values() for i in installs})
|
||||
archs = check_installs({path: self._raw(path) for path in paths})
|
||||
es_json = fetch_bytes(ES_SYSTEMS_TEMPLATE.format(ref=self.ref)).decode("utf-8")
|
||||
ports = port_engines(fetch_bytes(PORTS_TEMPLATE.format(ref=self.ref)).decode("utf-8"))
|
||||
es_mk = self._raw(ES_MK)
|
||||
check_names(with_ports(es_systems_for(es_json, es_mk, "Amlogic-ng"), ports))
|
||||
targets: dict[str, dict] = {}
|
||||
for device, arch in sorted(devices.items()):
|
||||
systems = with_ports(es_systems_for(es_json, es_mk, device), ports)
|
||||
cores = device_cores(device_packages(meta, device, arch), arch, archs, systems)
|
||||
targets[device.lower()] = {"architecture": arch, "cores": cores}
|
||||
print(f" {device.lower()} ({arch}): {len(cores)} cores", file=sys.stderr)
|
||||
return {
|
||||
"platform": PLATFORM_NAME,
|
||||
"source": f"{self.url}/blob/{self.ref}/{META_MK}",
|
||||
"devices_source": release["html_url"],
|
||||
"scraped_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"targets": targets,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
allow_abbrev=False, description="Scrape EmuELEC per-device cores"
|
||||
)
|
||||
parser.add_argument("--dry-run", action="store_true", help="Show target summary")
|
||||
parser.add_argument("--output", "-o", help="Output YAML file")
|
||||
args = parser.parse_args()
|
||||
|
||||
scraper = Scraper()
|
||||
data = scraper.fetch_targets()
|
||||
if args.dry_run:
|
||||
for name, info in data["targets"].items():
|
||||
print(f" {name} ({info['architecture']}): {len(info['cores'])} cores")
|
||||
return
|
||||
if args.output:
|
||||
scraper.write_output(data, args.output)
|
||||
print(f"Written to {args.output}")
|
||||
return
|
||||
print(yaml.dump(data, default_flow_style=False, sort_keys=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+27
-10
@@ -36,6 +36,7 @@ from xml.sax.saxutils import escape
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
import siteinputs # noqa: E402
|
||||
import sitemarkup as markup # noqa: E402
|
||||
|
||||
PLACEHOLDER = re.compile(r"(?m)^([ \t]*)<!--\s*image:\s*([A-Za-z0-9_-]+)\s*-->[ \t]*$")
|
||||
@@ -314,21 +315,29 @@ def svg(figure: Figure, layout: str, suffix: str) -> str:
|
||||
# Data
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def registry() -> dict:
|
||||
return _registry_in(str(siteinputs.current().platforms))
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _registry_in(platforms_dir: str) -> dict:
|
||||
import yaml
|
||||
|
||||
with open(ROOT / "platforms" / "_registry.yml", encoding="utf-8") as handle:
|
||||
with open(Path(platforms_dir) / "_registry.yml", encoding="utf-8") as handle:
|
||||
return yaml.safe_load(handle).get("platforms", {})
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def setups() -> tuple[str, ...]:
|
||||
"""The registered setups, archived ones included: each has its own page,
|
||||
pack and file list, so every count of setups on the site is this one."""
|
||||
return _setups_in(str(siteinputs.current().platforms))
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _setups_in(platforms_dir: str) -> tuple[str, ...]:
|
||||
from common import list_registered_platforms
|
||||
|
||||
return tuple(sorted(list_registered_platforms(str(ROOT / "platforms"), include_archived=True)))
|
||||
return tuple(sorted(list_registered_platforms(platforms_dir, include_archived=True)))
|
||||
|
||||
|
||||
def setups_named(named: list[str]) -> str:
|
||||
@@ -339,15 +348,19 @@ def setups_named(named: list[str]) -> str:
|
||||
return ", ".join(named) + (f" and {others} more" if others else "")
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def platform_modes() -> dict[str, tuple[str, str, str | None]]:
|
||||
"""name -> (display name, verification mode, hash rule) for every platform."""
|
||||
return _platform_modes_in(str(siteinputs.current().platforms))
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _platform_modes_in(platforms_dir: str) -> dict[str, tuple[str, str, str | None]]:
|
||||
from common import load_platform_config
|
||||
|
||||
out: dict[str, tuple[str, str, str | None]] = {}
|
||||
for name in setups():
|
||||
config = load_platform_config(name, str(ROOT / "platforms"))
|
||||
rule = registry().get(name, {}).get("hash_rule") or config.get("hash_rule")
|
||||
for name in _setups_in(platforms_dir):
|
||||
config = load_platform_config(name, platforms_dir)
|
||||
rule = _registry_in(platforms_dir).get(name, {}).get("hash_rule") or config.get("hash_rule")
|
||||
out[name] = (config.get("platform", name), config.get("verification_mode", ""), rule)
|
||||
return out
|
||||
|
||||
@@ -365,13 +378,17 @@ def _folder(name: str) -> str:
|
||||
CHAIN_EXAMPLES = (("batocera", "duckstation"), ("emudeck", "pcsx2"), ("retroarch", "beetle_psx"))
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def emulator_names() -> dict[str, str]:
|
||||
return _emulator_names_in(str(siteinputs.current().emulators))
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _emulator_names_in(emulators_dir: str) -> dict[str, str]:
|
||||
import yaml
|
||||
|
||||
names: dict[str, str] = {}
|
||||
for _, emulator in CHAIN_EXAMPLES:
|
||||
with open(ROOT / "emulators" / f"{emulator}.yml", encoding="utf-8") as handle:
|
||||
with open(Path(emulators_dir) / f"{emulator}.yml", encoding="utf-8") as handle:
|
||||
label = str(yaml.safe_load(handle).get("emulator", emulator))
|
||||
names[emulator] = re.sub(r"\s*\(.*\)$", "", label)
|
||||
return names
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""The collection, profiles and platform files the site describes.
|
||||
|
||||
generate_site.py takes them as --db, --emulators-dir and --platforms-dir and
|
||||
installs them here before writing a page. Every page module reads its inputs
|
||||
from current(), so a run on another collection describes that collection on
|
||||
every page. A module used on its own reads the repository's files.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SiteInputs:
|
||||
db: Path = ROOT / "database.json"
|
||||
emulators: Path = ROOT / "emulators"
|
||||
platforms: Path = ROOT / "platforms"
|
||||
|
||||
|
||||
_current = [SiteInputs()]
|
||||
|
||||
|
||||
def current() -> SiteInputs:
|
||||
return _current[0]
|
||||
|
||||
|
||||
def use(inputs: SiteInputs) -> None:
|
||||
_current[0] = inputs
|
||||
@@ -7,6 +7,7 @@ import urllib.parse
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import siteinputs
|
||||
import sitemarkup as markup
|
||||
|
||||
|
||||
@@ -401,7 +402,8 @@ def _build_boards() -> None:
|
||||
|
||||
from common import load_emulator_profiles
|
||||
|
||||
database = _REPO_ROOT / "database.json"
|
||||
inputs = siteinputs.current()
|
||||
database = inputs.db
|
||||
if not database.exists():
|
||||
_archive_boards.clear()
|
||||
_board_names.clear()
|
||||
@@ -409,7 +411,7 @@ def _build_boards() -> None:
|
||||
return
|
||||
with open(database, encoding="utf-8") as handle:
|
||||
db = json.load(handle)
|
||||
profiles = load_emulator_profiles(str(_REPO_ROOT / "emulators"), skip_aliases=False)
|
||||
profiles = load_emulator_profiles(str(inputs.emulators), skip_aliases=False)
|
||||
_arcade_source[0] = (profiles, db)
|
||||
profiles, db = _arcade_source[0]
|
||||
files = db.get("files", {})
|
||||
@@ -780,9 +782,10 @@ def _ensure_console_sections() -> None:
|
||||
from common import load_emulator_profiles
|
||||
from generate_site import _group_by_manufacturer
|
||||
|
||||
with open(_REPO_ROOT / "database.json", encoding="utf-8") as handle:
|
||||
inputs = siteinputs.current()
|
||||
with open(inputs.db, encoding="utf-8") as handle:
|
||||
db = json.load(handle)
|
||||
profiles = load_emulator_profiles(str(_REPO_ROOT / "emulators"), skip_aliases=False)
|
||||
profiles = load_emulator_profiles(str(inputs.emulators), skip_aliases=False)
|
||||
set_console_sections(_group_by_manufacturer(db), program_names(profiles))
|
||||
|
||||
|
||||
@@ -1431,7 +1434,7 @@ def system_search_aliases(sys_id: str) -> list[str]:
|
||||
platform_system_names,
|
||||
)
|
||||
|
||||
platforms_dir = str(_REPO_ROOT / "platforms")
|
||||
platforms_dir = str(siteinputs.current().platforms)
|
||||
index: dict[str, set[str]] = {}
|
||||
for platform in list_registered_platforms(platforms_dir, include_archived=True):
|
||||
config = load_platform_config(platform, platforms_dir)
|
||||
|
||||
@@ -321,9 +321,28 @@ def build_source_paths(
|
||||
# Recorded even when it holds no cited path, so a pinned tree
|
||||
# missing from the record is one no run has read.
|
||||
held.setdefault(tree_key(repo, pin), set()).update(paths & tree)
|
||||
for repo, pin, path in build_files(profile):
|
||||
tree = (_cached_tree(repo, pin, cache_dirs, fetch)
|
||||
or _listed_directories(repo, pin, {path}, cache_dirs, fetch))
|
||||
if tree is None:
|
||||
unread.add(tree_key(repo, pin))
|
||||
continue
|
||||
held.setdefault(tree_key(repo, pin), set()).update({path} & tree)
|
||||
return {key: sorted(paths) for key, paths in sorted(held.items())}, sorted(unread)
|
||||
|
||||
|
||||
def build_files(profile: dict) -> list[tuple[upstream.Repo, str, str]]:
|
||||
"""The platform build files `pin_build` names at a commit, as (repo,
|
||||
revision, path). A tag can move, so a build read at a tag is not linked."""
|
||||
found = []
|
||||
for build in profile.get("pin_build") or []:
|
||||
repo = _browse_repo(str(build.get("repo", "")))
|
||||
revision = str(build.get("revision", ""))
|
||||
if repo is not None and re.fullmatch(r"[0-9a-f]{40}", revision):
|
||||
found.append((repo, revision, str(build.get("path", ""))))
|
||||
return found
|
||||
|
||||
|
||||
def keep_unread(
|
||||
held: dict[str, list[str]], unread: list[str], previous: dict[str, list[str]]
|
||||
) -> dict[str, list[str]]:
|
||||
|
||||
+76
-17
@@ -43,7 +43,12 @@ from common import (
|
||||
variant_base_name,
|
||||
)
|
||||
from packresolve import preferred_entries
|
||||
from validation import outside_gap_scope, read_from_save_dir, with_archive_members
|
||||
from validation import (
|
||||
content_alternatives,
|
||||
outside_gap_scope,
|
||||
read_from_save_dir,
|
||||
with_archive_members,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from packextras import ExtraPlacement
|
||||
@@ -85,6 +90,9 @@ class Claim:
|
||||
entry: dict = field(default_factory=dict)
|
||||
local_path: str | None = None
|
||||
status: str = ""
|
||||
# A core that identifies its firmware by content reads no file by this
|
||||
# name: any dump it takes for the same system and region answers for it.
|
||||
accepts: tuple[dict, ...] = ()
|
||||
|
||||
@property
|
||||
def is_proven(self) -> bool:
|
||||
@@ -260,6 +268,7 @@ def profile_claims(
|
||||
)
|
||||
if status == "name_exact" and _size_the_code_reads(entry, local):
|
||||
status = SIZE_EXACT
|
||||
accepts = tuple(content_alternatives(entry, profile))
|
||||
claims.append(
|
||||
Claim(
|
||||
origin="profile",
|
||||
@@ -269,6 +278,7 @@ def profile_claims(
|
||||
entry=entry,
|
||||
local_path=local,
|
||||
status=status,
|
||||
accepts=accepts,
|
||||
)
|
||||
)
|
||||
return claims
|
||||
@@ -514,6 +524,7 @@ def pack_overrides(
|
||||
data_dir_registry: dict | None = None,
|
||||
target_cores: set[str] | None = None,
|
||||
source: str = "full",
|
||||
choices: dict[str, list[tuple[str, str | None]]] | None = None,
|
||||
) -> dict[str, str]:
|
||||
"""Full pack destination -> the file a platform pack serves there instead.
|
||||
|
||||
@@ -522,7 +533,8 @@ def pack_overrides(
|
||||
existence mode the frontend never reads the bytes, so serving the
|
||||
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
|
||||
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
|
||||
# pack answers to the cores that hardware builds: DuckStation does not
|
||||
@@ -531,10 +543,10 @@ def pack_overrides(
|
||||
return {}
|
||||
cores = set(resolve_platform_cores(config, profiles, target_cores=target_cores))
|
||||
overrides: dict[str, str] = {}
|
||||
for destination, choices in slot_choices(
|
||||
config, profiles, db, zip_contents, data_dir_registry
|
||||
).items():
|
||||
path = next((path for emulator, path in choices if emulator in cores), None)
|
||||
if choices is None:
|
||||
choices = slot_choices(config, profiles, db, zip_contents, data_dir_registry)
|
||||
for destination, ordered in choices.items():
|
||||
path = next((path for emulator, path in ordered if emulator in cores), None)
|
||||
if path:
|
||||
overrides[destination] = path
|
||||
return overrides
|
||||
@@ -677,12 +689,21 @@ class Collision:
|
||||
resolved: list[str]
|
||||
# Each declaration with the file it resolved to, in the platform's order.
|
||||
declarations: list[tuple[dict, str | None]] = field(default_factory=list)
|
||||
# The system of each declaration, in the same order.
|
||||
systems: list[str] = field(default_factory=list)
|
||||
# The file the pack ships at the destination, as the builder decides it.
|
||||
shipped: str | None = None
|
||||
|
||||
|
||||
def _collision_json(collision: Collision) -> dict:
|
||||
return {
|
||||
"destination": collision.destination,
|
||||
"resolved": list(collision.resolved),
|
||||
"shipped": collision.shipped,
|
||||
"declarations": [
|
||||
{"system": system, "name": entry.get("name", ""), "file": local}
|
||||
for system, (entry, local) in zip(collision.systems, collision.declarations)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -702,19 +723,27 @@ def find_collisions(
|
||||
CoCo disk ROM at one bios/disk.rom.
|
||||
"""
|
||||
fold = bool(config.get("case_insensitive_fs", False))
|
||||
by_dest: dict[str, list[dict]] = {}
|
||||
for system in (config.get("systems") or {}).values():
|
||||
systems = config.get("systems") or {}
|
||||
# The builder lets a hash-constrained declaration that resolves by
|
||||
# content take the path, else the first it reaches walking the systems
|
||||
# sorted: the same choice says which file ships.
|
||||
preferred = set(
|
||||
preferred_entries(systems, db, "bios", "", False, zip_contents, True).values()
|
||||
)
|
||||
by_dest: dict[str, list[tuple[str, dict]]] = {}
|
||||
for sys_id, system in systems.items():
|
||||
for entry in system.get("files") or []:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
dest = entry.get("destination") or entry.get("name") or ""
|
||||
if dest:
|
||||
by_dest.setdefault(_normalize(dest, fold), []).append(entry)
|
||||
by_dest.setdefault(_normalize(dest, fold), []).append((sys_id, entry))
|
||||
|
||||
collisions = []
|
||||
for key, entries in sorted(by_dest.items()):
|
||||
if len(entries) < 2:
|
||||
for key, claims in sorted(by_dest.items()):
|
||||
if len(claims) < 2:
|
||||
continue
|
||||
entries = [entry for _sys_id, entry in claims]
|
||||
resolved = []
|
||||
declarations = []
|
||||
for entry in entries:
|
||||
@@ -734,8 +763,22 @@ def find_collisions(
|
||||
# also names the first one's file.
|
||||
if any(_accepted_by_all(path, entries, db) for path in resolved):
|
||||
continue
|
||||
reached = sorted(
|
||||
(sys_id, local)
|
||||
for (sys_id, _entry), (_e, local) in zip(claims, declarations) if local
|
||||
)
|
||||
shipped = next(
|
||||
(local for entry, local in declarations if local and id(entry) in preferred),
|
||||
reached[0][1],
|
||||
)
|
||||
collisions.append(
|
||||
Collision(destination=key, resolved=resolved, declarations=declarations)
|
||||
Collision(
|
||||
destination=key,
|
||||
resolved=resolved,
|
||||
declarations=declarations,
|
||||
systems=[sys_id for sys_id, _entry in claims],
|
||||
shipped=shipped,
|
||||
)
|
||||
)
|
||||
return collisions
|
||||
|
||||
@@ -746,6 +789,10 @@ def _satisfies(profile: Claim, platform: Claim, db: dict) -> bool:
|
||||
They agree when they resolve to the same bytes, when the platform's own
|
||||
hash list accepts the profile's file, or when the profile's accepts the
|
||||
platform's. A contradiction needs both lists to reject the other side.
|
||||
A core found by content (found_by: content) reads no file by the
|
||||
destination's name, so any dump it takes for the same system and region
|
||||
settles the slot: DuckStation takes Batocera's scph1001.bin v2.0, which its profile
|
||||
labels scph1001_v20.bin.
|
||||
"""
|
||||
if profile.local_path == platform.local_path:
|
||||
return True
|
||||
@@ -764,6 +811,11 @@ def _satisfies(profile: Claim, platform: Claim, db: dict) -> bool:
|
||||
and _accepted_by_all(profile.local_path, [platform.entry], db)
|
||||
):
|
||||
return True
|
||||
if platform.local_path and any(
|
||||
_lists_hashes(label) and _accepted_by_all(platform.local_path, [label], db)
|
||||
for label in profile.accepts
|
||||
):
|
||||
return True
|
||||
return bool(
|
||||
platform.local_path
|
||||
and _lists_hashes(profile.entry)
|
||||
@@ -863,11 +915,18 @@ def _same_file_family(paths: list[str]) -> bool:
|
||||
|
||||
|
||||
def format_collision(collision: Collision) -> str:
|
||||
"""One line naming the destination and the files fighting for it."""
|
||||
return (
|
||||
f"{collision.destination}: declared for "
|
||||
+ " and for ".join(collision.resolved)
|
||||
)
|
||||
"""One line naming the destination, which system declares which file,
|
||||
and the one the pack ships there."""
|
||||
owners: dict[str, list[str]] = {}
|
||||
for system, (_entry, local) in zip(collision.systems, collision.declarations):
|
||||
if local and system not in owners.setdefault(local, []):
|
||||
owners[local].append(system)
|
||||
parts = [
|
||||
f"{path} ({', '.join(owners[path])}) "
|
||||
+ ("packed" if path == collision.shipped else "left out")
|
||||
for path in collision.resolved
|
||||
]
|
||||
return f"{collision.destination}: " + ", ".join(parts)
|
||||
|
||||
|
||||
def format_conflict(conflict: Conflict) -> str:
|
||||
|
||||
+88
-38
@@ -220,10 +220,21 @@ PROSE_EXTENSIONLESS = (
|
||||
"COPYING",
|
||||
"CHANGELOG",
|
||||
)
|
||||
PROSE_CITE_RE = re.compile(
|
||||
# Any other dotless file is cited with its directory, which a constant or a
|
||||
# hex address never carries: lightspark's tools/errorgen:20.
|
||||
_PROSE_PATH = (
|
||||
r"(?P<path>[A-Za-z0-9_][\w./+-]*\.[A-Za-z]\w*"
|
||||
r"|(?:[\w./+-]*/)?(?:" + "|".join(PROSE_EXTENSIONLESS) + r"))"
|
||||
r":(?P<range>\d+(?:-\d+)?)"
|
||||
r"|(?:[\w./+-]*/)?(?:" + "|".join(PROSE_EXTENSIONLESS) + r")"
|
||||
r"|(?:[\w.+-]+/)+[A-Za-z_][\w+-]*)"
|
||||
)
|
||||
PROSE_CITE_RE = re.compile(_PROSE_PATH + r":(?P<range>\d+(?:-\d+)?)")
|
||||
# A file named before its locations, `Source/engine/assets.cpp:` then a
|
||||
# list, and a location that leaves the file out, `(:507)`, which reads in
|
||||
# the file named last: devilutionx, corsixth and starship cite their load
|
||||
# order this way. Neither was collected, so no bump or guard followed them.
|
||||
_PROSE_FILE_NAMED_RE = re.compile(_PROSE_PATH + r":(?=\s|$)")
|
||||
_PROSE_ABBREVIATED_RE = re.compile(
|
||||
r"(?:(?<=[(\s])|^):(?P<range>\d+(?:-\d+)?)(?![\w:-])(?!\.\d)"
|
||||
)
|
||||
PROSE_LIST_ITEM_RE = re.compile(r"\d+[.)](?:\s|$)")
|
||||
PROSE_WRAPPED_RANGE_RE = re.compile(r":\d+(?:-\d+)?(?:[,)\s]|$)")
|
||||
@@ -344,6 +355,12 @@ def _prose_runs(
|
||||
joined line, so a writer can put each span back on its own line.
|
||||
"""
|
||||
lines = text.splitlines()
|
||||
# The file an abbreviated location reads in, and the role it was named
|
||||
# under, carried from line to line within the scalar.
|
||||
named: tuple[str, str] | None = None
|
||||
# Where the run wrapped from the line before ends in this one: its
|
||||
# ranges are that run's, not abbreviated locations of their own.
|
||||
carried = 0
|
||||
for number, own in enumerate(lines):
|
||||
# A literal block keeps its line breaks, so a citation wrapped after
|
||||
# `file:` or after a range's comma lost the ranges on the next line
|
||||
@@ -369,46 +386,35 @@ def _prose_runs(
|
||||
elif following and line.endswith(","):
|
||||
joined_at = len(line) + 1
|
||||
line += " " + following
|
||||
consumed = 0
|
||||
for match in PROSE_CITE_RE.finditer(line):
|
||||
consumed, carried = carried, 0
|
||||
events = sorted(
|
||||
[(m.start(), 0, m) for m in PROSE_CITE_RE.finditer(line)]
|
||||
+ [(m.start(), 1, m) for m in _PROSE_FILE_NAMED_RE.finditer(line)]
|
||||
+ [(m.start(), 2, m) for m in _PROSE_ABBREVIATED_RE.finditer(line)],
|
||||
key=lambda event: (event[0], event[1]),
|
||||
)
|
||||
for _start, kind, match in events:
|
||||
if match.start() >= boundary:
|
||||
break
|
||||
if match.start() < consumed:
|
||||
continue
|
||||
token_start = max(line.rfind(" ", 0, match.start()),
|
||||
line.rfind("\t", 0, match.start())) + 1
|
||||
token = line[token_start:].split()[0] if line[token_start:] else ""
|
||||
if "://" in token:
|
||||
if kind == 1:
|
||||
if "://" not in line[max(0, match.start() - 3):match.end()]:
|
||||
named = (match.group("path"), "")
|
||||
continue
|
||||
path = match.group("path")
|
||||
path_start = match.start("path")
|
||||
# A dotfile citation opens its token with the dot: `.gitlab-ci.yml`
|
||||
# after a delimiter is the filename, while the dot of `boot.cfg`
|
||||
# inside a word belongs to the word before it.
|
||||
if (
|
||||
path_start > 0
|
||||
and line[path_start - 1] == "."
|
||||
and (path_start == 1 or line[path_start - 2] in " \t(")
|
||||
):
|
||||
path_start -= 1
|
||||
path = "." + path
|
||||
# A path opening its line reads its role or project word at the
|
||||
# end of the line before: genesis_plus_gx_expanded wrapped
|
||||
# `(Batocera` and `package/batocera/...mk:7` one line apart.
|
||||
lead = line[:path_start]
|
||||
if number and not lead.strip():
|
||||
lead = lines[number - 1].rstrip() + " "
|
||||
role = _prose_role(lead, len(lead))
|
||||
if not role and _prose_external(lead, len(lead), known):
|
||||
# Kept with its project word, the source_ref convention: the
|
||||
# report looks for the project inside the declared trees
|
||||
# (mupen64plus-ae vendors `rice Config.cpp`) and calls the
|
||||
# citation EXTERNAL only when none carries it.
|
||||
before = lead.rstrip()
|
||||
word = before[max(before.rfind(" "), before.rfind("\t"),
|
||||
before.rfind("(")) + 1:]
|
||||
path = f"{word} {path}"
|
||||
path_span = (path_start, match.end("path"))
|
||||
if kind == 2:
|
||||
if named is None:
|
||||
continue
|
||||
path, role = named
|
||||
path_start = match.start()
|
||||
path_span = (path_start, path_start)
|
||||
else:
|
||||
path, role, path_start, path_span = _prose_path(
|
||||
line, lines, number, match, known
|
||||
)
|
||||
if path is None:
|
||||
continue
|
||||
named = (path, role)
|
||||
spans = [(match.start("range"), match.end("range"))]
|
||||
end = match.end()
|
||||
while True:
|
||||
@@ -429,6 +435,9 @@ def _prose_runs(
|
||||
spans.append((cont.start("range"), cont.end("range")))
|
||||
end = cont.end()
|
||||
consumed = end
|
||||
if end > boundary:
|
||||
indent = len(lines[number + 1]) - len(lines[number + 1].lstrip())
|
||||
carried = indent + end - joined_at
|
||||
parts = []
|
||||
for lo, hi in spans:
|
||||
rng = line[lo:hi]
|
||||
@@ -438,3 +447,44 @@ def _prose_runs(
|
||||
number, path_span, tuple(spans), tuple(parts),
|
||||
line[path_start:end], joined_at if end > boundary else 0,
|
||||
)
|
||||
|
||||
|
||||
def _prose_path(
|
||||
line: str, lines: list[str], number: int, match: re.Match,
|
||||
known: frozenset[str] | set[str],
|
||||
) -> tuple[str | None, str, int, tuple[int, int]]:
|
||||
"""(path, role, start, span) of one written citation, None for a URL."""
|
||||
token_start = max(line.rfind(" ", 0, match.start()),
|
||||
line.rfind("\t", 0, match.start())) + 1
|
||||
token = line[token_start:].split()[0] if line[token_start:] else ""
|
||||
if "://" in token:
|
||||
return None, "", 0, (0, 0)
|
||||
path = match.group("path")
|
||||
path_start = match.start("path")
|
||||
# A dotfile citation opens its token with the dot: `.gitlab-ci.yml`
|
||||
# after a delimiter is the filename, while the dot of `boot.cfg`
|
||||
# inside a word belongs to the word before it.
|
||||
if (
|
||||
path_start > 0
|
||||
and line[path_start - 1] == "."
|
||||
and (path_start == 1 or line[path_start - 2] in " \t(")
|
||||
):
|
||||
path_start -= 1
|
||||
path = "." + path
|
||||
# A path opening its line reads its role or project word at the
|
||||
# end of the line before: genesis_plus_gx_expanded wrapped
|
||||
# `(Batocera` and `package/batocera/...mk:7` one line apart.
|
||||
lead = line[:path_start]
|
||||
if number and not lead.strip():
|
||||
lead = lines[number - 1].rstrip() + " "
|
||||
role = _prose_role(lead, len(lead))
|
||||
if not role and _prose_external(lead, len(lead), known):
|
||||
# Kept with its project word, the source_ref convention: the
|
||||
# report looks for the project inside the declared trees
|
||||
# (mupen64plus-ae vendors `rice Config.cpp`) and calls the
|
||||
# citation EXTERNAL only when none carries it.
|
||||
before = lead.rstrip()
|
||||
word = before[max(before.rfind(" "), before.rfind("\t"),
|
||||
before.rfind("(")) + 1:]
|
||||
path = f"{word} {path}"
|
||||
return path, role, path_start, (path_start, match.end("path"))
|
||||
@@ -338,6 +338,10 @@ def main() -> int:
|
||||
# The packs are rewritten in place by generate_pack under this lock: a
|
||||
# build running beside the split mixes its bytes into the pack being
|
||||
# read, or writes a pack that the split then deletes.
|
||||
if not args.target.exists():
|
||||
print(f"Error: {args.target}: no such pack or directory, pass a pack or the "
|
||||
"--output-dir generate_pack wrote (default dist/)", file=sys.stderr)
|
||||
return 1
|
||||
directory = args.target if args.target.is_dir() else args.target.parent
|
||||
try:
|
||||
with artifact_lock(str(directory)):
|
||||
|
||||
+74
-6
@@ -18,6 +18,7 @@ Outputs a structured report suitable for PR comments.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -25,6 +26,7 @@ import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.parse
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
@@ -335,6 +337,53 @@ def load_profile_hashes(
|
||||
return known
|
||||
|
||||
|
||||
def _archive_members(
|
||||
filepath: str, db: dict | None, platform_hashes: dict, profile_hashes: dict
|
||||
) -> list[str] | None:
|
||||
"""What each file of a ZIP is, None when filepath is not a ZIP.
|
||||
|
||||
Read only for an archive no list declares as a whole: MAME and FBNeo
|
||||
sets are ZIP BIOS files in their own right.
|
||||
"""
|
||||
if not zipfile.is_zipfile(filepath):
|
||||
return None
|
||||
held = (db or {}).get("files", {})
|
||||
described = []
|
||||
try:
|
||||
with zipfile.ZipFile(filepath) as archive:
|
||||
for info in archive.infolist():
|
||||
if info.is_dir():
|
||||
continue
|
||||
if info.file_size > MAX_FILE_SIZE:
|
||||
described.append(f"`{info.filename}` too large to read here")
|
||||
continue
|
||||
sha1, md5 = hashlib.sha1(), hashlib.md5()
|
||||
with archive.open(info) as handle:
|
||||
for chunk in iter(lambda: handle.read(1 << 20), b""):
|
||||
sha1.update(chunk)
|
||||
md5.update(chunk)
|
||||
sha1_hex, md5_hex = sha1.hexdigest(), md5.hexdigest()
|
||||
crc = f"{info.CRC:08x}"
|
||||
if sha1_hex in held:
|
||||
state = f"already held at `{held[sha1_hex]['path']}`"
|
||||
elif (
|
||||
sha1_hex in platform_hashes.get("sha1", set())
|
||||
or md5_hex in platform_hashes.get("md5", set())
|
||||
or (crc, info.file_size) in platform_hashes.get("crc32", set())
|
||||
or profile_hashes.get("sha1", {}).get(sha1_hex)
|
||||
or profile_hashes.get("md5", {}).get(md5_hex)
|
||||
or profile_hashes.get("crc32", {}).get((crc, info.file_size))
|
||||
):
|
||||
state = "declared and not held"
|
||||
else:
|
||||
state = "not recognised"
|
||||
described.append(f"`{info.filename}` {state}")
|
||||
except (zipfile.BadZipFile, RuntimeError, OSError) as exc:
|
||||
# An encrypted member raises RuntimeError.
|
||||
described.append(f"unreadable ({exc})")
|
||||
return described
|
||||
|
||||
|
||||
def validate_file(
|
||||
filepath: str,
|
||||
db: dict | None,
|
||||
@@ -446,6 +495,8 @@ def validate_file(
|
||||
or profile_hashes.get("crc32", {}).get((result.crc32, None))
|
||||
)
|
||||
|
||||
# The files of a ZIP no list declares whole, when the upload is one.
|
||||
members: list[str] | None = None
|
||||
if sha1_known:
|
||||
result.add_check(True, "SHA1 matches known platform requirement")
|
||||
elif md5_known:
|
||||
@@ -466,11 +517,26 @@ def validate_file(
|
||||
)
|
||||
else:
|
||||
result.outcome = "unknown"
|
||||
result.add_warning(
|
||||
"Not recognised: no platform list or emulator profile declares this "
|
||||
f"file. Say in the {'issue' if too_large else 'pull request'} which "
|
||||
"emulator reads it, for which system, and where the dump comes from"
|
||||
members = None if too_large else _archive_members(
|
||||
filepath, db, platform_hashes, profile_hashes
|
||||
)
|
||||
if members is not None:
|
||||
# A folder zipped to upload many files at once was hashed as one
|
||||
# file and its contributor asked where that dump came from.
|
||||
shown = "; ".join(members[:10]) + (
|
||||
f"; and {len(members) - 10} more" if len(members) > 10 else ""
|
||||
)
|
||||
result.add_warning(
|
||||
"A ZIP no platform list or emulator profile declares as a whole: "
|
||||
"add the files it holds, unzipped, not the archive. Inside: "
|
||||
+ (shown or "nothing")
|
||||
)
|
||||
else:
|
||||
result.add_warning(
|
||||
"Not recognised: no platform list or emulator profile declares this "
|
||||
f"file. Say in the {'issue' if too_large else 'pull request'} which "
|
||||
"emulator reads it, for which system, and where the dump comes from"
|
||||
)
|
||||
|
||||
normalized = os.path.normpath(relative)
|
||||
# A platform's declaration first, then a profile's: most files the gaps
|
||||
@@ -503,7 +569,8 @@ def validate_file(
|
||||
result.add_check(True, f"Correct placement: bios/{parts[1]}/{parts[2]}/")
|
||||
else:
|
||||
result.add_warning("File should be in bios/Manufacturer/Console/ structure")
|
||||
elif result.outcome == "held":
|
||||
elif result.outcome == "held" or members is not None:
|
||||
# Nothing to place: a copy, or an archive whose files go in unzipped.
|
||||
pass
|
||||
elif expected_folders:
|
||||
# The contributor asks before forking: say where it goes.
|
||||
@@ -847,7 +914,8 @@ def main() -> None:
|
||||
{
|
||||
"file": r.filepath,
|
||||
"passed": r.passed,
|
||||
"outcome": r.outcome or "new",
|
||||
# A file that failed, unread or refused, is not a new one.
|
||||
"outcome": r.outcome or ("new" if r.passed else "failed"),
|
||||
"destination": r.destination,
|
||||
"sha1": r.sha1,
|
||||
"md5": r.md5,
|
||||
|
||||
@@ -14,7 +14,15 @@ from pathlib import Path, PurePosixPath
|
||||
import yaml
|
||||
|
||||
from common import source_yaml_paths, yaml_load
|
||||
from jsonschema import Draft202012Validator, FormatChecker
|
||||
|
||||
try:
|
||||
from jsonschema import Draft202012Validator, FormatChecker
|
||||
except ImportError:
|
||||
# Tests import this module and skip on the ImportError, the command
|
||||
# names the install pipeline.py names for the same step.
|
||||
if __name__ != "__main__":
|
||||
raise
|
||||
sys.exit('Error: jsonschema required (pip install "jsonschema[format-nongpl]")')
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SCHEMAS = ROOT / "schemas"
|
||||
|
||||
+68
-1
@@ -16,6 +16,7 @@ from common import (
|
||||
adler32_of,
|
||||
archive_members,
|
||||
compute_hashes,
|
||||
entry_system_ids,
|
||||
name_match_size_ok,
|
||||
sanitize_pack_path,
|
||||
size_fits,
|
||||
@@ -145,6 +146,72 @@ def _rule_failure(
|
||||
return None
|
||||
|
||||
|
||||
def _found_by_content(entry: dict, profile: dict) -> bool:
|
||||
"""Whether every build of the core identifies this entry by content."""
|
||||
return not opens_by_name(entry, profile) and not opens_by_name(
|
||||
entry, profile, standalone=True
|
||||
)
|
||||
|
||||
|
||||
def _regions(entry: dict) -> set[str]:
|
||||
value = entry.get("region")
|
||||
return set(value) if isinstance(value, list) else {value} if value else set()
|
||||
|
||||
|
||||
def content_alternatives(entry: dict, profile: dict) -> list[dict]:
|
||||
"""The labels a content-found core takes in place of this one.
|
||||
|
||||
rustation scans its folder by sha256 (found_by: content): its
|
||||
scph1001.bin is a label for v2.2, and the v2.0 dump ROCKNIX ships under
|
||||
that name boots the same North American console, labelled
|
||||
scph1001_v20.bin. Alternatives share the entry's system and declared
|
||||
region, the slot slot.py groups by: openMSX also finds its ROMs by
|
||||
sha1, but each one serves its own machine and declares no region.
|
||||
"""
|
||||
if not _found_by_content(entry, profile):
|
||||
return []
|
||||
regions = _regions(entry)
|
||||
if not regions:
|
||||
return []
|
||||
systems = set(entry_system_ids(entry, profile))
|
||||
return [
|
||||
other for other in profile.get("files") or []
|
||||
if isinstance(other, dict)
|
||||
and _found_by_content(other, profile)
|
||||
and regions & _regions(other)
|
||||
and systems == set(entry_system_ids(other, profile))
|
||||
]
|
||||
|
||||
|
||||
def _labels_widened(profile: dict) -> list[dict]:
|
||||
"""The profile's entries, each content-found label accepting the dumps
|
||||
of its alternatives (content_alternatives)."""
|
||||
widened = []
|
||||
for f in profile.get("files") or []:
|
||||
if not isinstance(f, dict):
|
||||
continue
|
||||
siblings = content_alternatives(f, profile)
|
||||
if len(siblings) < 2:
|
||||
widened.append(f)
|
||||
continue
|
||||
merged = dict(f)
|
||||
for field_name in ("crc32", "md5", "sha1", "sha256"):
|
||||
if f.get(field_name):
|
||||
merged[field_name] = list(dict.fromkeys(
|
||||
value for other in siblings
|
||||
for value in parse_hash_list(other.get(field_name))
|
||||
))
|
||||
if f.get("size") is not None:
|
||||
merged["size"] = sorted({
|
||||
size for other in siblings
|
||||
for size in (other["size"] if isinstance(other.get("size"), list)
|
||||
else [other.get("size")])
|
||||
if size is not None
|
||||
})
|
||||
widened.append(merged)
|
||||
return widened
|
||||
|
||||
|
||||
def _build_validation_index(profiles: dict) -> dict[str, dict]:
|
||||
"""Build per-filename validation rules from emulator profiles.
|
||||
|
||||
@@ -170,7 +237,7 @@ def _build_validation_index(profiles: dict) -> dict[str, dict]:
|
||||
for emu_name, profile in profiles.items():
|
||||
if profile.get("type") in ("launcher", "alias"):
|
||||
continue
|
||||
for f in profile.get("files", []):
|
||||
for f in _labels_widened(profile):
|
||||
fname = f.get("name", "")
|
||||
if not fname:
|
||||
continue
|
||||
|
||||
+100
-31
@@ -26,11 +26,12 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import Counter
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Callable, Mapping
|
||||
from pathlib import Path
|
||||
|
||||
import slots
|
||||
@@ -38,6 +39,8 @@ import slots
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from hashing import parse_md5_list
|
||||
from common import (
|
||||
_foreign_tree,
|
||||
_owner_tokens,
|
||||
platform_arg,
|
||||
target_listing_line,
|
||||
suggest_names,
|
||||
@@ -419,14 +422,25 @@ def _build_expected(file_entry: dict, checks: list[str]) -> dict:
|
||||
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(
|
||||
name: str,
|
||||
by_name: dict,
|
||||
by_path_suffix: dict | None = None,
|
||||
data_names: set[str] | None = None,
|
||||
by_name_lower: dict[str, str] | None = None,
|
||||
foreign: Callable[[str], bool] | None = None,
|
||||
) -> 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/)
|
||||
name = name.rstrip("/")
|
||||
if name in by_name:
|
||||
@@ -435,11 +449,11 @@ def _name_in_index(
|
||||
if basename != name and basename in by_name:
|
||||
return True
|
||||
if by_name_lower:
|
||||
key = name.lower()
|
||||
if key in by_name_lower:
|
||||
return True
|
||||
if basename != name and basename.lower() in by_name_lower:
|
||||
return True
|
||||
for key in {name.lower(), basename.lower()}:
|
||||
if key in by_name_lower and not (
|
||||
foreign and all(foreign(s) for s in by_name[by_name_lower[key]])
|
||||
):
|
||||
return True
|
||||
if by_path_suffix and name in by_path_suffix:
|
||||
return True
|
||||
if data_names:
|
||||
@@ -667,23 +681,30 @@ def find_undeclared_files(
|
||||
# Resolution: name, then path basename
|
||||
# storage: release names where the bytes are served from, not
|
||||
# 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")):
|
||||
# The entry states what its content should be and the builder
|
||||
# drops a copy that contradicts it, so content decides here
|
||||
# too. A name match would not do: generic names collide across
|
||||
# systems, and answering yes on one describes a pack that will
|
||||
# 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"
|
||||
else:
|
||||
in_repo = _name_in_index(
|
||||
fname, by_name, by_path_suffix, data_names, by_name_lower,
|
||||
foreign,
|
||||
)
|
||||
if not in_repo and dest != fname:
|
||||
path_base = dest.rsplit("/", 1)[-1]
|
||||
in_repo = _name_in_index(
|
||||
path_base, by_name, by_path_suffix, data_names,
|
||||
by_name_lower,
|
||||
by_name_lower, foreign,
|
||||
)
|
||||
if in_repo and not any(
|
||||
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
|
||||
# only PCem's and QEMU's PC BIOS images of that name.
|
||||
# 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":
|
||||
in_repo = False
|
||||
if not in_repo:
|
||||
@@ -707,7 +728,7 @@ def find_undeclared_files(
|
||||
# frontend would reject it, the builder leaves it out, and
|
||||
# calling it available here would describe a pack that
|
||||
# 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
|
||||
if in_repo and _st == "hash_mismatch" and shipped_on_mismatch:
|
||||
in_repo = False
|
||||
@@ -717,7 +738,7 @@ def find_undeclared_files(
|
||||
# held for it, the rule the builder's lookup applies
|
||||
# (packextras.ExtraResolver). The Apple IIGS' 128 KB
|
||||
# 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(
|
||||
f, _lp, same_slot_entries(profile.get("files"), fname, dest)
|
||||
):
|
||||
@@ -1575,6 +1596,13 @@ def verify_platform(
|
||||
"exclusion_notes": exclusions,
|
||||
"region_fallbacks": sorted(region_fallbacks),
|
||||
**({"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,
|
||||
"ground_truth_coverage": {
|
||||
"with_validation": with_validation,
|
||||
@@ -1845,6 +1873,13 @@ def _print_undeclared_section(result: dict, verbose: bool) -> None:
|
||||
|
||||
game_data, bios_files, unplaced = _split_undeclared(undeclared)
|
||||
|
||||
# A file installed with its emulator is never collected, firmware or
|
||||
# game data, required or not: cross_reference counts it apart. The
|
||||
# firmware already left the pack lists (_mark_unplaced), the game data
|
||||
# did not, and Abuse's three bundled data files read as missing.
|
||||
bundled = [u for u in undeclared if not u["in_repo"] and u.get("bundled")]
|
||||
game_data = [u for u in game_data if u["in_repo"] or not u.get("bundled")]
|
||||
|
||||
req_not_in_repo = _core_required_missing(bios_files)
|
||||
req_hle_not_in_repo = [
|
||||
u
|
||||
@@ -1854,12 +1889,6 @@ def _print_undeclared_section(result: dict, verbose: bool) -> None:
|
||||
req_in_repo = [u for u in bios_files if u["required"] and u["in_repo"]]
|
||||
opt_in_repo = [u for u in bios_files if not u["required"] and u["in_repo"]]
|
||||
opt_not_in_repo = [u for u in bios_files if not u["required"] and not u["in_repo"]]
|
||||
|
||||
bundled = [
|
||||
u for u in bios_files
|
||||
if u["required"] and not u["in_repo"] and u.get("bundled")
|
||||
and not u.get("hle_fallback")
|
||||
]
|
||||
# The builder's count, one file per pack path, when the platform pass
|
||||
# replayed it. Counting entries said 1130 for RetroArch, 48 more than the
|
||||
# pack holds: rom.key alone was claimed by three Amiga cores.
|
||||
@@ -1886,8 +1915,17 @@ def _print_undeclared_section(result: dict, verbose: bool) -> None:
|
||||
for u in opt_not_in_repo:
|
||||
_print_undeclared_entry(u, "MISSING (optional)", verbose, repeated)
|
||||
if bundled:
|
||||
names = ", ".join(sorted({u["name"] for u in bundled}))
|
||||
print(f" Bundled with their emulator, not collected: {names}")
|
||||
# The required firmware is named, as a missing one would be. The
|
||||
# rest (shaders, runtime libraries, engine data) on request.
|
||||
shown = bundled if verbose else [
|
||||
u for u in bundled
|
||||
if u["required"] and u.get("category", "bios") != "game_data"
|
||||
]
|
||||
names = ", ".join(sorted({u["name"] for u in shown}))
|
||||
print(
|
||||
f" Bundled with their emulator, not collected: {len(bundled)}"
|
||||
+ (f" ({names})" if names else "")
|
||||
)
|
||||
if unplaced:
|
||||
print(f" Core files held but not packed: {len(unplaced)} "
|
||||
f"({_omission_reasons(unplaced)})")
|
||||
@@ -1952,6 +1990,13 @@ def print_platform_result(
|
||||
# them missing, as an offline build leaves them out.
|
||||
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}")
|
||||
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 []:
|
||||
print(f" {line}")
|
||||
fallbacks = result.get("region_fallbacks") or []
|
||||
@@ -2058,7 +2103,7 @@ def verify_emulator(
|
||||
it and every member it carries are optional, as the builder leaves them.
|
||||
"""
|
||||
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 (
|
||||
_archive_destination,
|
||||
_register_path,
|
||||
@@ -2084,6 +2129,16 @@ def verify_emulator(
|
||||
validation_index = _build_validation_index(selected_profiles)
|
||||
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 = []
|
||||
file_status: dict[str, str] = {}
|
||||
file_severity: dict[str, str] = {}
|
||||
@@ -2223,14 +2278,15 @@ def verify_emulator(
|
||||
archive = file_entry.get("archive")
|
||||
if archive and archive not in seen_archives:
|
||||
seen_archives.add(archive)
|
||||
local_path, resolve_status = resolve_file(
|
||||
archive_entry(archive, files),
|
||||
own_entry = archive_entry(archive, files)
|
||||
local_path, resolve_status = pending(own_entry, *resolve_file(
|
||||
own_entry,
|
||||
db,
|
||||
DEFAULT_BIOS_DIR,
|
||||
zip_contents,
|
||||
data_dir_registry=data_registry,
|
||||
offline=True,
|
||||
)
|
||||
))
|
||||
required = any(
|
||||
f.get("archive") == archive and f.get("required", True)
|
||||
for f in files
|
||||
@@ -2254,6 +2310,8 @@ def verify_emulator(
|
||||
"required": required,
|
||||
"path": local_path,
|
||||
}
|
||||
if resolve_status == RELEASE_ASSET_PENDING:
|
||||
result["release_asset"] = local_path
|
||||
else:
|
||||
result = {
|
||||
"name": archive,
|
||||
@@ -2314,7 +2372,7 @@ def verify_emulator(
|
||||
|
||||
dest_hint = file_entry.get("path", "")
|
||||
# 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},
|
||||
db,
|
||||
DEFAULT_BIOS_DIR,
|
||||
@@ -2322,7 +2380,7 @@ def verify_emulator(
|
||||
dest_hint=dest_hint,
|
||||
data_dir_registry=data_registry,
|
||||
offline=True,
|
||||
)
|
||||
))
|
||||
if resolve_status in ("not_found", "user_provided", "external"):
|
||||
local_path = None
|
||||
name = file_entry.get("name", "")
|
||||
@@ -2332,6 +2390,15 @@ def verify_emulator(
|
||||
stored = _storage_verdict(file_entry, emulator_pack=True)
|
||||
if stored is not None:
|
||||
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:
|
||||
result = {"name": name, "status": Status.MISSING, "required": required}
|
||||
# An entry the profile documents as unsourceable is absent by
|
||||
@@ -2582,6 +2649,10 @@ def print_emulator_result(result: dict, verbose: bool = False) -> None:
|
||||
if unsourceable:
|
||||
parts.append(f"{unsourceable} unsourceable")
|
||||
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()
|
||||
for d in result["details"]:
|
||||
@@ -2712,8 +2783,6 @@ def _refuse_mode_flags(args: argparse.Namespace, parser: argparse.ArgumentParser
|
||||
"--destinations describes the full pack and is incompatible with --system"),
|
||||
(args.destinations and not (args.platform or args.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.
|
||||
(getattr(args, "no_game_data", False) and not (args.platform or args.all),
|
||||
"--no-game-data requires --platform or --all"),
|
||||
@@ -2988,13 +3057,13 @@ def _main(held: contextlib.ExitStack) -> None:
|
||||
print(_overall_summary(group_results))
|
||||
|
||||
if args.destinations:
|
||||
held = {}
|
||||
destinations = {}
|
||||
for (result, names), (group, _rep) in zip(group_results, groups):
|
||||
for name, key in zip(names, group):
|
||||
held[name] = {
|
||||
destinations[name] = {
|
||||
"platform": key, "pack_destinations": result["pack_destinations"],
|
||||
}
|
||||
Path(args.destinations).write_text(json.dumps(held, indent=1, sort_keys=True))
|
||||
Path(args.destinations).write_text(json.dumps(destinations, indent=1, sort_keys=True))
|
||||
|
||||
if args.json:
|
||||
for r in all_results.values():
|
||||
|
||||
+11
-3
@@ -13,6 +13,7 @@ temporary directory instead of naming the missing parent.
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault("RETROBIOS_OFFLINE", "1")
|
||||
@@ -22,9 +23,16 @@ os.environ.setdefault(
|
||||
str(Path(__file__).resolve().parent.parent / "tmp" / "tests" / "probe.lock"),
|
||||
)
|
||||
|
||||
(Path(__file__).resolve().parent.parent / "tmp" / "tests").mkdir(
|
||||
parents=True, exist_ok=True
|
||||
)
|
||||
_SCRATCH = Path(__file__).resolve().parent.parent / "tmp" / "tests"
|
||||
_SCRATCH.mkdir(parents=True, exist_ok=True)
|
||||
# A TemporaryDirectory() without dir= landed in /tmp, a 4 GB tmpfs here: once
|
||||
# another run filled it, modules unrelated to any change failed with ENOSPC.
|
||||
# The default is the repository's tmp/, for this process and its children.
|
||||
os.environ["TMPDIR"] = str(_SCRATCH)
|
||||
tempfile.tempdir = str(_SCRATCH)
|
||||
# A scratch directory is not part of this checkout: git run inside one must
|
||||
# not walk up to the repository, as it could not from /tmp.
|
||||
os.environ["GIT_CEILING_DIRECTORIES"] = str(_SCRATCH)
|
||||
# The installer's cache (the games index, what reading a card found) is the
|
||||
# suite's own: a run never reads nor fills the one of the person running it.
|
||||
os.environ.setdefault(
|
||||
|
||||
@@ -44,3 +44,12 @@ def pages() -> list[Path]:
|
||||
def name(path: Path) -> str:
|
||||
"""A page file's docs path, the name tests use for it."""
|
||||
return path.relative_to(CONTENT).with_suffix(".md").as_posix()
|
||||
|
||||
|
||||
def generator_sources() -> list[Path]:
|
||||
"""Every module whose strings reach a page: generate_site.py, each
|
||||
scripts/site*.py and the pack README writer. Listed by glob, so text
|
||||
moved out of generate_site.py stays under the guards that read it."""
|
||||
scripts = REPO_ROOT / "scripts"
|
||||
return sorted({*scripts.glob("site*.py"), scripts / "generate_site.py",
|
||||
scripts / "packreadme.py"})
|
||||
@@ -0,0 +1,102 @@
|
||||
"""AmberELEC's images differ by device, and only its metapackage says how.
|
||||
|
||||
packages/amberelec/package.mk adds LIBRETRO_CORES_EXTRA (melonDS, bsnes,
|
||||
Mesen, geolith) on RG552, RG353 and RG503 and deletes those cores from
|
||||
es_systems.cfg everywhere else. With no target list, an RG351P pack carried
|
||||
the firmware of cores its image never had.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
from scraper.targets import amberelec_targets_scraper as scraper # noqa: E402
|
||||
|
||||
MAKEFILE = """RG351P:
|
||||
\tDEVICE=RG351P ARCH=aarch64 ./scripts/build_distro
|
||||
|
||||
RG552:
|
||||
\tDEVICE=RG552 ARCH=aarch64 ./scripts/build_distro
|
||||
|
||||
toolchain-RK3326:
|
||||
\tDEVICE=RK3326 ARCH=aarch64 ./scripts/build toolchain
|
||||
"""
|
||||
|
||||
PACKAGE_MK = """ LIBRETRO_CORES_EXTRA="bsnes melonds"
|
||||
|
||||
PKG_EMUS="${LIBRETRO_CORES}"
|
||||
|
||||
if [[ "${DEVICE}" == "RG552" || "${DEVICE}" =~ ^(RG353|RG503)$ ]]; then
|
||||
PKG_EMUS+=" ${LIBRETRO_CORES_EXTRA}"
|
||||
fi
|
||||
|
||||
makeinstall_target() {
|
||||
for CORE in ${LIBRETRO_CORES_EXTRA}; do
|
||||
if [[ $CORE == "melonds" ]]; then
|
||||
sed -i -e '/<emulator name/{:a;N;/<\\/emulator>/!ba;/melonds/d}' es_systems.cfg
|
||||
fi
|
||||
sed -i "s|<core>$CORE</core>||g" es_systems.cfg
|
||||
done
|
||||
}
|
||||
"""
|
||||
|
||||
ES_SYSTEMS = """<systemList>
|
||||
<system>
|
||||
<name>snes</name>
|
||||
<emulators>
|
||||
<emulator name="retroarch">
|
||||
<cores>
|
||||
<core default="true">snes9x</core>
|
||||
<core>bsnes</core>
|
||||
</cores>
|
||||
</emulator>
|
||||
</emulators>
|
||||
</system>
|
||||
<system>
|
||||
<name>nds</name>
|
||||
<emulators>
|
||||
<emulator name="retroarch">
|
||||
<cores>
|
||||
<core>melonds</core>
|
||||
</cores>
|
||||
</emulator>
|
||||
<emulator name="drastic">
|
||||
<cores>
|
||||
<core default="true">drastic</core>
|
||||
</cores>
|
||||
</emulator>
|
||||
</emulators>
|
||||
</system>
|
||||
</systemList>
|
||||
"""
|
||||
|
||||
|
||||
class TheMetapackageDecidesEachDevice(unittest.TestCase):
|
||||
def test_devices_are_the_images_the_makefile_builds(self):
|
||||
self.assertEqual(scraper.devices(MAKEFILE), {"RG351P": "aarch64", "RG552": "aarch64"})
|
||||
|
||||
def test_the_extra_cores_and_their_devices(self):
|
||||
extra, keep = scraper.extra_cores(PACKAGE_MK)
|
||||
self.assertEqual(extra, ["bsnes", "melonds"])
|
||||
self.assertEqual(keep, {"RG552", "RG353", "RG503"})
|
||||
|
||||
def test_a_device_without_them_loses_their_cores(self):
|
||||
extra, _keep = scraper.extra_cores(PACKAGE_MK)
|
||||
full = scraper.device_cores(ES_SYSTEMS, extra, True)
|
||||
lean = scraper.device_cores(ES_SYSTEMS, extra, False)
|
||||
self.assertEqual(set(full) - set(lean), {"bsnes", "melonds"})
|
||||
self.assertIn("drastic", lean)
|
||||
|
||||
def test_a_changed_removal_stops_the_scrape(self):
|
||||
changed = PACKAGE_MK.replace('sed -i "s|<core>$CORE</core>||g"', "true")
|
||||
with self.assertRaises(RuntimeError):
|
||||
scraper.extra_cores(changed)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -61,6 +61,28 @@ class ArtifactLockTest(unittest.TestCase):
|
||||
with artifact_lock(self.dir, exclusive=False):
|
||||
pass
|
||||
|
||||
def test_a_reader_never_creates_the_directory(self):
|
||||
"""A mistyped --output-dir given to --verify-packs or release_record
|
||||
was created holding a .lock, and the next run with the same typo no
|
||||
longer said the path did not exist."""
|
||||
from common import ArtifactDirectoryMissing # noqa: PLC0415
|
||||
|
||||
missing = os.path.join(self.dir, "sub", "dsit")
|
||||
with self.assertRaises(ArtifactDirectoryMissing):
|
||||
with artifact_lock(missing, exclusive=False):
|
||||
pass
|
||||
self.assertFalse(os.path.exists(os.path.join(self.dir, "sub")))
|
||||
|
||||
def test_split_refuses_a_missing_target_before_locking(self):
|
||||
missing = Path(self.dir) / "sub" / "dsit"
|
||||
proc = subprocess.run(
|
||||
[sys.executable, "scripts/split_pack.py", str(missing)],
|
||||
cwd=REPO_ROOT, capture_output=True, text=True, check=False, timeout=60,
|
||||
)
|
||||
self.assertEqual(proc.returncode, 1)
|
||||
self.assertIn("no such pack or directory", proc.stderr)
|
||||
self.assertFalse(missing.parent.exists())
|
||||
|
||||
def test_lock_is_released_on_exit(self):
|
||||
with artifact_lock(self.dir):
|
||||
pass
|
||||
|
||||
@@ -206,11 +206,16 @@ class AModuleRunAloneHasItsScratchDirectory(unittest.TestCase):
|
||||
)
|
||||
proc = subprocess.run(
|
||||
[sys.executable, "-I", "-c",
|
||||
"import sys; sys.path.insert(0, '.'); import tests"],
|
||||
"import sys; sys.path.insert(0, '.'); import tests, tempfile, os;"
|
||||
" print(tempfile.gettempdir()); print(os.environ['TMPDIR'])"],
|
||||
cwd=clone, capture_output=True, text=True, timeout=60, check=False,
|
||||
)
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertTrue((clone / "tmp" / "tests").is_dir())
|
||||
# 264 TemporaryDirectory() calls without dir= wrote to the /tmp
|
||||
# tmpfs, and a full tmpfs failed modules no change touched.
|
||||
scratch = str((clone / "tmp" / "tests").resolve())
|
||||
self.assertEqual(proc.stdout.split(), [scratch, scratch])
|
||||
|
||||
|
||||
class ReleaseSigningRegressions(unittest.TestCase):
|
||||
@@ -1140,9 +1145,12 @@ class PipelineRegressions(unittest.TestCase):
|
||||
|
||||
argv = ["pipeline.py", "--offline", "--skip-packs", "--skip-docs",
|
||||
"--with-export", "--target", "switch"]
|
||||
# The originals the offline export patches are not cached in a
|
||||
# checkout: the run would be refused before any step.
|
||||
with mock.patch.object(sys, "argv", argv), mock.patch.object(
|
||||
pipeline, "run", side_effect=fake_run
|
||||
), contextlib.redirect_stdout(io.StringIO()), contextlib.suppress(SystemExit):
|
||||
), mock.patch.object(pipeline, "_refuse_offline_export_without_originals"), \
|
||||
contextlib.redirect_stdout(io.StringIO()), contextlib.suppress(SystemExit):
|
||||
pipeline.main()
|
||||
truth_runs = [c for c in commands if "scripts/generate_truth.py" in c]
|
||||
export = next(c for c in commands if "scripts/export_native.py" in c)
|
||||
@@ -2054,6 +2062,44 @@ class CheckoutCompletenessRegressions(unittest.TestCase):
|
||||
finally:
|
||||
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):
|
||||
from scripts.restore_large_files import restore
|
||||
|
||||
@@ -2291,7 +2337,9 @@ class EveryManifestEntryIsFetchable(unittest.TestCase):
|
||||
else "gitignored" if repo_path in ignored
|
||||
else "untracked, never committed"
|
||||
)
|
||||
if bool(entry.get("release_asset")) != (state == "gitignored"):
|
||||
if state == "untracked, never committed" or (
|
||||
bool(entry.get("release_asset")) != (state == "gitignored")
|
||||
):
|
||||
wrong.append((entry["dest"], repo_path, state))
|
||||
self.assertEqual(wrong, [], f"{name} picks the wrong source: {wrong[:3]}")
|
||||
|
||||
@@ -2305,6 +2353,36 @@ class EveryManifestEntryIsFetchable(unittest.TestCase):
|
||||
self.skipTest("not a git checkout")
|
||||
self.assertEqual(sorted(untracked)[:20], [], f"{len(untracked)} untracked files under bios/")
|
||||
|
||||
def test_every_database_path_is_committed_or_a_release_asset(self):
|
||||
"""A git mv under bios/ committed without regenerating anything left
|
||||
database.json and the manifests agreeing on the old path: the tests
|
||||
above compare them with each other, so all passed, and the installer
|
||||
got a 404 (issue 78, with the database stale too)."""
|
||||
database = ROOT / "database.json"
|
||||
if not database.is_file():
|
||||
self.skipTest("database.json not generated")
|
||||
files = json.loads(database.read_text(encoding="utf-8"))["files"]
|
||||
paths = {
|
||||
path
|
||||
for entry in files.values()
|
||||
for path in [entry.get("path"), *(entry.get("paths") or [])]
|
||||
if path
|
||||
}
|
||||
tracked = _git_paths("ls-files", "-z")
|
||||
if tracked is None:
|
||||
self.skipTest("not a git checkout")
|
||||
outside = sorted(paths - tracked)
|
||||
ignored = _git_paths("check-ignore", "-z", "--no-index", "--stdin", stdin=outside)
|
||||
if ignored is None:
|
||||
self.skipTest("not a git checkout")
|
||||
stale = [path for path in outside if path not in ignored]
|
||||
self.assertEqual(
|
||||
stale[:20], [],
|
||||
f"{len(stale)} database paths git neither holds nor ignores: "
|
||||
"python scripts/generate_db.py --force --bios-dir bios --output database.json, "
|
||||
"then the manifests",
|
||||
)
|
||||
|
||||
|
||||
class ReleaseAssetCriterion(unittest.TestCase):
|
||||
"""What decides whether the installer fetches from the release is git, not size.
|
||||
@@ -5143,8 +5221,32 @@ class UnsourceableCardSaysWhy(unittest.TestCase):
|
||||
text = "\n".join(_render_emulator_file(
|
||||
entry, {"files": [entry]}, None, [entry], lambda f: False, "file-x",
|
||||
))
|
||||
self.assertIn("Not collectable: written by the firmware editor", text)
|
||||
self.assertIn("../gaps.md#acknowledged-gaps", text)
|
||||
self.assertIn("Not collected yet: written by the firmware editor", text)
|
||||
self.assertIn("../gaps.md#not-collected-yet", text)
|
||||
self.assertNotIn("collectable", text)
|
||||
|
||||
def test_the_gaps_page_names_where_a_wanted_file_is_listed(self):
|
||||
"""The section read "Acknowledged gaps ... (verified from source
|
||||
code)" while No-Intro lists XM7's KANJI2.ROM and MAME's fm7740sx.zip
|
||||
carries it: the flag only keeps a homonym out, the file is wanted."""
|
||||
from scripts.generate_site import generate_gap_analysis
|
||||
|
||||
unsourced = {"name": "KANJI2.ROM", "reason": "no dump distributed", "source_ref": ""}
|
||||
report = {"xm7": {"emulator": "XM7", "gaps": 0, "gap_missing": 0, "gap_bios": 0,
|
||||
"unsourceable": [unsourced], "gap_details": []}}
|
||||
catalogued = {"no-intro": {"missing": [{
|
||||
"name": "KANJI2.rom",
|
||||
"romsets": [{"source": "mame", "archive": "fm7740sx.zip"}],
|
||||
"needed_by": [{"emulator": "xm7", "name": "KANJI2.ROM", "required": True}],
|
||||
}]}}
|
||||
page = generate_gap_analysis(
|
||||
{"xm7": {"emulator": "XM7", "files": []}}, {}, {"files": {}, "indexes": {}},
|
||||
gap_report=report, catalogued=catalogued)
|
||||
self.assertIn("## Not collected yet", page)
|
||||
self.assertIn('<span id="acknowledged-gaps"></span>', page)
|
||||
self.assertIn("| No-Intro `KANJI2.rom`, mame `fm7740sx.zip` |", page)
|
||||
self.assertNotIn("Acknowledged", page)
|
||||
self.assertNotIn("verified from source code", page)
|
||||
|
||||
|
||||
|
||||
@@ -5183,6 +5285,61 @@ class CollectionCardsCarryFullHashes(unittest.TestCase):
|
||||
self.assertIn(f'<dd class="rb-hash">{value}</dd>', text)
|
||||
|
||||
|
||||
class AHeldRevisionLinksItsBuildFile(unittest.TestCase):
|
||||
"""advanced_drastic's page printed the KNULLI build file that holds its
|
||||
revision as plain text, while pin_build carries the repository, the tag
|
||||
and the path a pinned link needs. 36 profiles carry one."""
|
||||
|
||||
def test_the_build_file_row(self):
|
||||
from scripts.generate_site import _build_rows
|
||||
|
||||
rows = _build_rows({"pin_frozen": "KNULLI builds this revision.", "pin_build": [{
|
||||
"platform": "knulli", "repo": "https://github.com/knulli-cfw/knulli-linux",
|
||||
"revision": "20260511", "path": "package/emulators/advanced_drastic/advanced_drastic.mk",
|
||||
"variable": "ADVANCED_DRASTIC_VERSION", "value": "eaf41ab9"}]})
|
||||
# A tag can move: named, never linked.
|
||||
self.assertIn(
|
||||
"| Build file | KNULLI `package/emulators/advanced_drastic/advanced_drastic.mk` "
|
||||
"at `20260511` sets `ADVANCED_DRASTIC_VERSION` |", rows)
|
||||
|
||||
def test_a_build_read_at_a_recorded_commit_links(self):
|
||||
from scripts.generate_site import _build_rows
|
||||
|
||||
pin = "46128cd252cdecae852a4d0bafa2aaf9d03ab563"
|
||||
rows = _build_rows({"pin_build": [{
|
||||
"platform": "retropie", "repo": "https://github.com/RetroPie/RetroPie-Setup",
|
||||
"revision": pin, "path": "scriptmodules/emulators/advmame-1.4.sh",
|
||||
"variable": "rp_module_repo", "value": "x"}]})
|
||||
self.assertIn(
|
||||
f"[RetroPie `scriptmodules/emulators/advmame-1.4.sh`](https://github.com/RetroPie/"
|
||||
f"RetroPie-Setup/blob/{pin}/scriptmodules/emulators/advmame-1.4.sh)", rows[0])
|
||||
|
||||
def test_the_record_holds_the_build_files(self):
|
||||
import sitesources # noqa: PLC0415
|
||||
|
||||
files = sitesources.build_files({"pin_build": [
|
||||
{"repo": "https://github.com/o/r", "revision": "a" * 40, "path": "p.mk"},
|
||||
{"repo": "https://github.com/o/r", "revision": "v1", "path": "q.mk"}]})
|
||||
self.assertEqual([(f[1], f[2]) for f in files], [("a" * 40, "p.mk")])
|
||||
|
||||
|
||||
class AVariantCardSaysWhatToSaveItAs(unittest.TestCase):
|
||||
"""The Download button of a .variants/ card saved advision.zip.1c086064,
|
||||
the storage name, and nothing said RetroBat looks for advision.zip."""
|
||||
|
||||
def test_the_name_to_save_it_under(self):
|
||||
from scripts.generate_site import FileConsumers, _system_file_card
|
||||
|
||||
entry = {"name": "advision.zip", "size": 10, "sha1": "a" * 40,
|
||||
"path": "bios/Entex/Adventure Vision/.variants/advision.zip.1c086064"}
|
||||
text = "\n".join(_system_file_card(entry, FileConsumers({}, {})))
|
||||
self.assertIn("Save as", text)
|
||||
self.assertIn("advision.zip.1c086064", text)
|
||||
plain = "\n".join(_system_file_card(
|
||||
{**entry, "path": "bios/Entex/Adventure Vision/advision.zip"}, FileConsumers({}, {})))
|
||||
self.assertNotIn("Save as", plain)
|
||||
|
||||
|
||||
|
||||
class NullReadsAsNone(unittest.TestCase):
|
||||
"""`upstream: ~` printed `| Upstream | None |` and `bios_filename: null`
|
||||
|
||||
@@ -891,6 +891,16 @@ class BiosIntegrity(Scratch):
|
||||
self.assertEqual([(f.path, f.verdict) for f in report.findings], [(rel, bib.SKIPPED)])
|
||||
self.assertEqual((self.repo / rel).read_bytes(), b"\x02" * 80)
|
||||
|
||||
def test_an_unreadable_index_stops_before_any_restore(self):
|
||||
"""git status failed on a corrupt index, its empty stdout read as
|
||||
"unmodified", and --apply wrote HEAD's bytes over the session's."""
|
||||
rel = "bios/Sony/edited.bin"
|
||||
(self.repo / rel).write_bytes(b"\x02" * 80)
|
||||
(self.repo / ".git" / "index").write_bytes(b"garbage\n")
|
||||
with self.assertRaises(bib.GitFailed):
|
||||
bib.check(self.repo, self.db, apply=True)
|
||||
self.assertEqual((self.repo / rel).read_bytes(), b"\x02" * 80)
|
||||
|
||||
def test_a_mismatch_the_second_read_does_not_confirm_is_a_misread(self):
|
||||
reads = {}
|
||||
expected = {e["path"]: e["sha1"] for e in self.db["files"].values()}
|
||||
|
||||
@@ -7,7 +7,12 @@ went red. The flag lived in retrobat.yml and vanished at the next scrape.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
@@ -29,5 +34,82 @@ class CaseInsensitiveWhereTheFilesystemIs(unittest.TestCase):
|
||||
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__":
|
||||
unittest.main()
|
||||
@@ -35,6 +35,14 @@ class NativeCacheTests(unittest.TestCase):
|
||||
self.root.mkdir()
|
||||
self.wanted = {"list.txt": "https://example.invalid/tag-2/list.txt"}
|
||||
|
||||
def test_an_unreadable_index_is_an_error_not_unrecorded(self):
|
||||
"""Read as empty, it named "no URL recorded" for every original."""
|
||||
self.index.write_text("{", encoding="utf-8")
|
||||
findings = cf.check_native(REPO_ROOT / "platforms", self.cache, self.cache)
|
||||
self.assertEqual([(f.subject, f.status) for f in findings],
|
||||
[(self.index.name, cf.ERROR)])
|
||||
self.assertIn("--refresh-cache", findings[0].detail)
|
||||
|
||||
def _cached(self, mtime: float) -> None:
|
||||
path = self.root / "list.txt"
|
||||
path.write_text("x", encoding="utf-8")
|
||||
@@ -734,6 +742,19 @@ class RowsSayAllTheyStandFor(unittest.TestCase):
|
||||
rows = [line for line in text.splitlines() if line.startswith(" STALE")]
|
||||
self.assertEqual(rows[0].index("no longer"), rows[1].index("is not what"))
|
||||
|
||||
def test_a_remote_value_keeps_the_detail_column(self):
|
||||
"""A row with a remote value pushed its detail past the column the
|
||||
rows without one used, in [coreinfo] by some fifteen characters."""
|
||||
text = cf.render([
|
||||
cf.Finding("coreinfo", "anybor", cf.STALE, detail="no profile claims"),
|
||||
cf.Finding("coreinfo", "amiberry", cf.STALE, local="e5d1f0f9a143",
|
||||
remote="ae545d85c06f", detail=".info changed"),
|
||||
cf.Finding("coreinfo", "x", cf.STALE, local="abc", remote="d", detail="third"),
|
||||
])
|
||||
rows = [line for line in text.splitlines() if line.startswith(" STALE")]
|
||||
columns = {row.index(word) for row, word in zip(rows, ("no profile", ".info", "third"))}
|
||||
self.assertEqual(len(columns), 1, rows)
|
||||
|
||||
def test_json_carries_every_unresolved_name(self):
|
||||
missing = {f"core{i:02d}": [] for i in range(50)}
|
||||
(row,) = cf._unresolved_findings("retropie", missing)
|
||||
|
||||
@@ -56,6 +56,25 @@ class AliasesNeedAnUnambiguousName(unittest.TestCase):
|
||||
named = {a["name"] for entries in aliases.values() for a in entries}
|
||||
self.assertNotIn("n88sub.rom", named)
|
||||
|
||||
def test_homonyms_differing_in_case_take_no_alias(self):
|
||||
"""dosbox's SC-55 ROM2.BIN: Galaksija's ROM2.BIN was the only exact
|
||||
holder, and ROM2_ST.BIN went to it beside SC-55's rom2.bin."""
|
||||
files = {
|
||||
"a": self._file("DISK.ROM", "bios/NEC/PC-98/DISK.ROM", "m1"),
|
||||
"b": self._file("disk.rom", "bios/Tandy/CoCo/disk.rom", "m2"),
|
||||
}
|
||||
aliases = self._aliases(files)
|
||||
named = {a["name"] for entries in aliases.values() for a in entries}
|
||||
self.assertNotIn("n88sub.rom", named)
|
||||
|
||||
def test_a_primary_and_its_variant_are_one_file(self):
|
||||
files = {
|
||||
"a": self._file("disk.rom", "bios/NEC/PC-88/disk.rom", "m1"),
|
||||
"b": self._file("DISK.ROM", "bios/NEC/PC-88/.variants/DISK.ROM.0123abcd", "m2"),
|
||||
}
|
||||
aliases = self._aliases(files)
|
||||
self.assertEqual([a["name"] for a in aliases.get("a", [])], ["n88sub.rom"])
|
||||
|
||||
def test_a_unique_name_still_carries_its_aliases(self):
|
||||
files = {"a": self._file("disk.rom", "bios/NEC/PC-88/disk.rom", "m1")}
|
||||
aliases = self._aliases(files)
|
||||
|
||||
@@ -1984,5 +1984,24 @@ class ArchivedIsWhatTheToolsDo(unittest.TestCase):
|
||||
self.assertNotIn("list_platforms", workflow.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
|
||||
class TheCatalogRowsStateFacts(unittest.TestCase):
|
||||
"""tools.md said "redump.org serves stale DATs" and "Dat-o-Matic blocks
|
||||
automation", verdicts on two preservation projects, and gave Redump's
|
||||
/static/bios links, which the scraper no longer reads."""
|
||||
|
||||
def test_dates_and_routes_not_verdicts(self):
|
||||
text = (WIKI / "tools.md").read_text(encoding="utf-8")
|
||||
rows = [line for line in text.splitlines()
|
||||
if line.startswith(("| Redump |", "| No-Intro |"))]
|
||||
self.assertEqual(len(rows), 2)
|
||||
for word in ("stale", "blocks"):
|
||||
self.assertNotIn(word, " ".join(rows))
|
||||
scraper = (ROOT / "scripts" / "scraper" / "redump_dat_scraper.py").read_text(encoding="utf-8")
|
||||
self.assertIn("/bios/<CODE>", scraper)
|
||||
self.assertIn("`/bios/<CODE>`", rows[0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -322,6 +322,8 @@ class ShownOutputIsPrintedOutput(unittest.TestCase):
|
||||
r"(0 optional missing|\d+ optional missing \(--verbose lists them\))$",
|
||||
'" Core files: {core_in_pack} in pack'),
|
||||
(r"^ Core files held but not packed: \d+ \(.+\)$", '" Core files held but not packed: '),
|
||||
(r"^ Bundled with their emulator, not collected: \d+( \(.+\))?$",
|
||||
'" Bundled with their emulator, not collected: {len(bundled)}"'),
|
||||
(r"^ Game data: \d+ in pack, (0 missing|\d+ missing \(--verbose lists them\))$",
|
||||
'" Game data: {gd_packed} in pack'),
|
||||
(r"^ Game data held but not packed: \d+ \(.+\)$", '" Game data held but not packed: '),
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
"""EmuELEC's images differ by device, and only its metapackage says how.
|
||||
|
||||
packages/sx05re/emuelec/package.mk (v4.8) builds the S922X cores, Dolphin
|
||||
and the MAME core only on Amlogic-ng, Amlogic-no, RK356x and OdroidM1, and
|
||||
takes DuckStation out on the two handhelds with a sed that replaces the
|
||||
first match only. With no target list, an OdroidGoAdvance pack carried the
|
||||
firmware of cores its image never had.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
from scraper.targets import emuelec_targets_scraper as scraper # noqa: E402
|
||||
|
||||
# packages/sx05re/emuelec/package.mk:8-77 at v4.8, PKG_EMUS and
|
||||
# PKG_EXPERIMENTAL shortened.
|
||||
PACKAGE_MK = """PKG_DEPENDS_TARGET="toolchain ${OPENGLES} emuelec-emulationstation retroarch"
|
||||
PKG_EXPERIMENTAL="nestopiaCV quasi88 yabasanshiroSA_1_11 yabasanshiroSA_1_5 fbneoSA"
|
||||
PKG_EMUS="${LIBRETRO_CORES} advancemame duckstation pcsx_rearmed flycastsa"
|
||||
PKG_DEPENDS_TARGET+=" emuelec-tools ${PKG_EMUS} ${PKG_EXPERIMENTAL}"
|
||||
|
||||
if [ "${DEVICE}" == "Amlogic-ng" ] || [ "${DEVICE}" == "Amlogic-no" ] || [ "${DEVICE}" == "RK356x" ] || [ "${DEVICE}" == "OdroidM1" ]; then
|
||||
\tPKG_DEPENDS_TARGET+=" ${LIBRETRO_S922X_CORES}"
|
||||
fi
|
||||
|
||||
if [ "${DEVICE}" == "OdroidGoAdvance" ] || [ "${DEVICE}" == "GameForce" ]; then
|
||||
\tPKG_DEPENDS_TARGET+=" kmscon odroidgoa-utils"
|
||||
\tfor discore in duckstation mesen-s virtualjaguar quicknes MC; do
|
||||
\t\tPKG_DEPENDS_TARGET=$(echo ${PKG_DEPENDS_TARGET} | sed "s|${discore} | |")
|
||||
\tdone
|
||||
\tPKG_DEPENDS_TARGET+=" yabasanshiro"
|
||||
else
|
||||
\tPKG_DEPENDS_TARGET+=" fbterm"
|
||||
fi
|
||||
|
||||
if [ "${ARCH}" == "aarch64" ]; then
|
||||
for discore in quicknes parallel-n64 pcsx_rearmed; do
|
||||
\t\tPKG_DEPENDS_TARGET=$(echo ${PKG_DEPENDS_TARGET} | sed "s|${discore}| |")
|
||||
\tdone
|
||||
|
||||
PKG_DEPENDS_TARGET+=" swanstation \\
|
||||
lib32-essential \\
|
||||
lib32-pcsx_rearmed \\
|
||||
lib32-parallel-n64
|
||||
lib32-libusb"
|
||||
|
||||
if [ "${DEVICE}" == "Amlogic-ng" ] || [ "${DEVICE}" == "Amlogic-no" ] || [ "${DEVICE}" == "RK356x" ] || [ "${DEVICE}" == "OdroidM1" ]; then
|
||||
PKG_DEPENDS_TARGET+=" dolphinSA"
|
||||
fi
|
||||
|
||||
if [ "${DEVICE}" == "Amlogic-old" ]; then
|
||||
for discore in yabasanshiroSA_1_11 yabasanshiroSA_1_5 yabasanshiro same_cdi duckstation; do
|
||||
PKG_DEPENDS_TARGET=$(echo ${PKG_DEPENDS_TARGET} | sed "s|${discore} | |")
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${DEVICE}" == "Amlogic-ng" ] || [ "${DEVICE}" == "Amlogic-no" ] || [ "${DEVICE}" == "RK356x" ] || [ "${DEVICE}" == "OdroidM1" ]; then
|
||||
\tPKG_DEPENDS_TARGET+=" mame"
|
||||
fi
|
||||
|
||||
if [ "${DEVICE}" == "RK356x" ] || [ "${DEVICE}" == "OdroidM1" ]; then
|
||||
for discore in flycast-dojo; do
|
||||
\t\tPKG_DEPENDS_TARGET=$(echo ${PKG_DEPENDS_TARGET} | sed "s|${discore}| |")
|
||||
\tdone
|
||||
fi
|
||||
"""
|
||||
|
||||
# distributions/EmuELEC/options:274-373 at v4.8, shortened.
|
||||
OPTIONS = """LIBRETRO_CORES="mgba \\
|
||||
parallel-n64 \\
|
||||
pcsx_rearmed \\
|
||||
flycast"
|
||||
|
||||
LIBRETRO_S922X_CORES="beetle-saturn \\
|
||||
mesen \\
|
||||
quicknes \\
|
||||
virtualjaguar"
|
||||
"""
|
||||
|
||||
# packages/sx05re/emuelec-emulationstation/package.mk:114-143 at v4.8.
|
||||
ES_MK = """CORESFILE="${INSTALL}/usr/config/emulationstation/es_systems.cfg"
|
||||
|
||||
if [[ "${DEVICE}" != "Amlogic-ng" || "${DEVICE}" != "Amlogic-ne" || "${DEVICE}" != "Amlogic-no" ]]; then
|
||||
if [[ ${DEVICE} == "OdroidGoAdvance" || "${DEVICE}" == "GameForce" ]]; then
|
||||
remove_cores="mesen-s quicknes mame2016 mesen"
|
||||
elif [ "${DEVICE}" == "Amlogic-old" ]; then
|
||||
remove_cores="mesen-s quicknes mame2016 mesen yabasanshiroSA yabasanshiro"
|
||||
xmlstarlet ed -L -P -d "/systemList/system[name='saturn']" ${CORESFILE}
|
||||
xmlstarlet ed -L -P -d "/systemList/system[name='phillips-cdi']" ${CORESFILE}
|
||||
xmlstarlet ed -L -P -d "/systemList/system/emulators/emulator[@name='Duckstation']" ${CORESFILE}
|
||||
fi
|
||||
for discore in ${remove_cores}; do
|
||||
sed -i "s|<core>${discore}</core>||g" ${CORESFILE}
|
||||
sed -i '/^[[:space:]]*$/d' ${CORESFILE}
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ ${DEVICE} != "OdroidGoAdvance" ]] && [[ "${DEVICE}" != "GameForce" ]]; then
|
||||
\txmlstarlet ed -L -P -d "/systemList/system/emulators/emulator[@name='retrorun']" ${CORESFILE}
|
||||
else
|
||||
\txmlstarlet ed -L -P -d "/systemList/system/emulators/emulator[@name='Duckstation']" ${CORESFILE}
|
||||
fi
|
||||
"""
|
||||
|
||||
|
||||
def _system(name: str, *emulators: tuple[str, list[dict]]) -> dict:
|
||||
return {"name": name, "emulators": [{"name": e, "cores": c} for e, c in emulators]}
|
||||
|
||||
|
||||
ES_JSON = json.dumps({"systems": [
|
||||
_system("nes", ("libretro", [{"name": "mesen", "default": True}])),
|
||||
_system("snes", ("libretro", [{"name": "snes9x", "default": True}, {"name": "mesen-s"}])),
|
||||
_system("psx",
|
||||
("libretro", [{"name": "pcsx_rearmed", "default": True},
|
||||
{"name": "pcsx_rearmed_32b"}, {"name": "swanstation"}]),
|
||||
("Duckstation", [{"name": "Duckstation"}]),
|
||||
("retrorun", [{"name": "pcsx_rearmed_32b"}])),
|
||||
_system("n64", ("libretro", [{"name": "parallel_n64_32b"}])),
|
||||
]})
|
||||
|
||||
ARCHS = {"mesen": "", "duckstation": "aarch64", "pcsx_rearmed": "any",
|
||||
"lib32-pcsx_rearmed": "aarch64", "swanstation": "any",
|
||||
"lib32-parallel-n64": "aarch64"}
|
||||
|
||||
ASSETS = [
|
||||
"EmuELEC-Amlogic-ng.aarch64-4.8-Generic.img.gz",
|
||||
"EmuELEC-Amlogic-ng.aarch64-4.8.tar",
|
||||
"EmuELEC-GameForce.aarch64-4.8-chi.img.gz.sha256",
|
||||
"EmuELEC-OdroidGoAdvance.aarch64-4.8-odroidgo2.tar",
|
||||
"EmuELEC-OdroidM1.aarch64-4.8.img.gz",
|
||||
]
|
||||
|
||||
|
||||
class TheMetapackageDecidesEachDevice(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.meta = scraper.read_metapackage(PACKAGE_MK, OPTIONS)
|
||||
|
||||
def test_devices_are_the_images_the_release_publishes(self):
|
||||
self.assertEqual(scraper.release_devices(ASSETS), {
|
||||
"Amlogic-ng": "aarch64", "GameForce": "aarch64",
|
||||
"OdroidGoAdvance": "aarch64", "OdroidM1": "aarch64",
|
||||
})
|
||||
|
||||
def test_the_lists_the_rules_work_on(self):
|
||||
self.assertEqual(self.meta.emus, ["mgba", "parallel-n64", "pcsx_rearmed", "flycast",
|
||||
"advancemame", "duckstation", "pcsx_rearmed",
|
||||
"flycastsa"])
|
||||
self.assertEqual(self.meta.lib32, ["swanstation", "lib32-essential",
|
||||
"lib32-pcsx_rearmed", "lib32-parallel-n64",
|
||||
"lib32-libusb"])
|
||||
|
||||
def test_the_big_devices_build_the_s922x_cores_dolphin_and_mame(self):
|
||||
packages = scraper.device_packages(self.meta, "Amlogic-ng", "aarch64")
|
||||
for package in ("beetle-saturn", "mesen", "virtualjaguar", "dolphinSA", "mame",
|
||||
"duckstation", "fbterm"):
|
||||
self.assertIn(package, packages)
|
||||
# quicknes is added, then the aarch64 loop takes it out.
|
||||
self.assertNotIn("quicknes", packages)
|
||||
|
||||
def test_the_handhelds_lose_duckstation_and_gain_their_tools(self):
|
||||
packages = scraper.device_packages(self.meta, "OdroidGoAdvance", "aarch64")
|
||||
for package in ("duckstation", "mesen", "beetle-saturn", "dolphinSA", "mame", "fbterm"):
|
||||
self.assertNotIn(package, packages)
|
||||
self.assertEqual(packages[-1:], ["lib32-libusb"])
|
||||
self.assertIn("kmscon", packages)
|
||||
self.assertIn("yabasanshiro", packages)
|
||||
|
||||
def test_sed_takes_out_the_first_match_only(self):
|
||||
# pcsx_rearmed is written twice, in LIBRETRO_CORES and in PKG_EMUS:
|
||||
# the aarch64 loop removes the first and the 64-bit core is built.
|
||||
for device in ("Amlogic-ng", "OdroidGoAdvance"):
|
||||
packages = scraper.device_packages(self.meta, device, "aarch64")
|
||||
self.assertEqual(packages.count("pcsx_rearmed"), 1, device)
|
||||
self.assertNotIn("parallel-n64", packages, device)
|
||||
|
||||
def test_a_pattern_with_a_trailing_space_misses_the_last_word(self):
|
||||
self.assertEqual(scraper._sed_first("a duckstation", "duckstation "), "a duckstation")
|
||||
self.assertEqual(scraper._sed_first("a duckstation b", "duckstation "), "a b")
|
||||
# Without the space, the pattern takes a word's tail too.
|
||||
self.assertEqual(scraper._sed_first("lib32-pcsx_rearmed", "pcsx_rearmed"), "lib32- ")
|
||||
|
||||
def test_a_changed_rule_stops_the_scrape(self):
|
||||
changed = PACKAGE_MK.replace("duckstation mesen-s virtualjaguar quicknes MC",
|
||||
"duckstation mesen-s")
|
||||
with self.assertRaises(RuntimeError):
|
||||
scraper.read_metapackage(changed, OPTIONS)
|
||||
|
||||
|
||||
class EachPackageBuildsWhereItsArchAllows(unittest.TestCase):
|
||||
def test_pkg_arch(self):
|
||||
self.assertTrue(scraper.arch_builds("", "aarch64"))
|
||||
self.assertTrue(scraper.arch_builds("any", "aarch64"))
|
||||
self.assertTrue(scraper.arch_builds("aarch64 arm", "aarch64"))
|
||||
# mupen64plus and uae4arm say PKG_ARCH="arm": skipped on aarch64.
|
||||
self.assertFalse(scraper.arch_builds("arm", "aarch64"))
|
||||
self.assertFalse(scraper.arch_builds("any !aarch64", "aarch64"))
|
||||
|
||||
def test_every_cited_install_line_is_read(self):
|
||||
install = scraper.PROVIDERS["mgba"][0]
|
||||
text = f'PKG_ARCH="any"\nmakeinstall_target() {{\n {install.line}\n}}\n'
|
||||
with self.assertRaises(RuntimeError):
|
||||
scraper._require(text.replace("mgba_libretro", "mgba2_libretro"),
|
||||
(install.line,), install.path)
|
||||
scraper._require(text, (install.line,), install.path)
|
||||
|
||||
|
||||
class TheFrontendOffersWhatTheDeviceKeeps(unittest.TestCase):
|
||||
def test_handhelds_drop_bare_cores_and_duckstation(self):
|
||||
systems = scraper.es_systems_for(ES_JSON, ES_MK, "GameForce")
|
||||
# mesen is the nes default: written with an attribute, sed misses it.
|
||||
self.assertEqual(systems["nes"], [("libretro", ["mesen"])])
|
||||
self.assertEqual(systems["snes"], [("libretro", ["snes9x"])])
|
||||
self.assertEqual([e for e, _ in systems["psx"]], ["libretro", "retrorun"])
|
||||
|
||||
def test_other_devices_drop_retrorun(self):
|
||||
systems = scraper.es_systems_for(ES_JSON, ES_MK, "Amlogic-ng")
|
||||
self.assertEqual([e for e, _ in systems["psx"]], ["libretro", "Duckstation"])
|
||||
self.assertEqual(systems["snes"], [("libretro", ["snes9x", "mesen-s"])])
|
||||
|
||||
def test_a_device_keeps_what_it_builds_and_offers(self):
|
||||
meta = scraper.read_metapackage(PACKAGE_MK, OPTIONS)
|
||||
archs = {**dict.fromkeys(scraper.PROVIDERS, "any"), **ARCHS}
|
||||
cores = {}
|
||||
for device in ("Amlogic-ng", "OdroidGoAdvance"):
|
||||
packages = [p for p in scraper.device_packages(meta, device, "aarch64")
|
||||
if p in scraper.PROVIDERS or p in scraper.NO_ES_CORE]
|
||||
systems = scraper.es_systems_for(ES_JSON, ES_MK, device)
|
||||
cores[device] = scraper.device_cores(packages, "aarch64", archs, systems)
|
||||
self.assertEqual(cores["Amlogic-ng"], ["Duckstation", "mesen", "parallel_n64_32b",
|
||||
"pcsx_rearmed", "pcsx_rearmed_32b",
|
||||
"swanstation"])
|
||||
self.assertEqual(cores["OdroidGoAdvance"], ["parallel_n64_32b", "pcsx_rearmed",
|
||||
"pcsx_rearmed_32b", "swanstation"])
|
||||
|
||||
def test_a_package_never_read_stops_the_scrape(self):
|
||||
archs = dict.fromkeys(scraper.PROVIDERS, "any")
|
||||
with self.assertRaises(RuntimeError):
|
||||
scraper.device_cores(["mgba", "newcore"], "aarch64", archs, {})
|
||||
|
||||
|
||||
class TheCommittedListFollowsTheRules(unittest.TestCase):
|
||||
def test_the_handhelds_list_no_core_they_do_not_build(self):
|
||||
import yaml # noqa: PLC0415
|
||||
|
||||
targets = yaml.safe_load((REPO_ROOT / "platforms" / "targets" / "emuelec.yml")
|
||||
.read_text(encoding="utf-8"))["targets"]
|
||||
self.assertEqual(set(targets), {"amlogic-ng", "gameforce", "odroidgoadvance", "odroidm1"})
|
||||
big = set(targets["amlogic-ng"]["cores"])
|
||||
for device in ("gameforce", "odroidgoadvance"):
|
||||
self.assertEqual(big - set(targets[device]["cores"]), {
|
||||
"Duckstation", "dolphin", "mame", "mednafen_saturn", "mesen", "mesen-s",
|
||||
"virtualjaguar",
|
||||
})
|
||||
self.assertEqual(targets["odroidm1"]["cores"], targets["amlogic-ng"]["cores"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1005,6 +1005,22 @@ class CachedOriginal(unittest.TestCase):
|
||||
self._upstream("v1.txt", "moved")
|
||||
self.assertEqual(fetch(url, target, self.index), b"one")
|
||||
|
||||
def test_an_unreadable_index_serves_nothing(self):
|
||||
"""A truncated index read as empty: the cached batocera-41 file was
|
||||
served for a 43.1 URL, and the next download erased every other
|
||||
platform's recorded URL."""
|
||||
import export_native
|
||||
|
||||
old = self._upstream("batocera-41.txt", "old")
|
||||
target = self.cache / "plat" / "list.txt"
|
||||
fetch(old, target, self.index)
|
||||
self.index.write_text('{"plat/list.txt": "' + old + '",', encoding="utf-8")
|
||||
new = self._upstream("batocera-43.txt", "new")
|
||||
with self.assertRaises(export_native.SourcesIndexUnreadable) as caught:
|
||||
fetch(new, target, self.index)
|
||||
self.assertIn("--refresh-cache", str(caught.exception))
|
||||
self.assertTrue(self.index.read_text(encoding="utf-8").endswith(","))
|
||||
|
||||
def test_refresh_reads_a_branch_url_again(self):
|
||||
"""A branch URL keeps its name while the platform moves."""
|
||||
url = self._upstream("branch.txt", "one")
|
||||
@@ -2909,6 +2925,44 @@ class BatoceraNotesStayWithTheirItem(unittest.TestCase):
|
||||
self.assertEqual(trailing, {("bios/bk0010.zip", "two.rom"): "# tail"})
|
||||
|
||||
|
||||
class TheWikiNamesWhatCountsAnExport(unittest.TestCase):
|
||||
"""adding-a-platform listed the methods that say what a format can hold
|
||||
and none of those that count what render wrote, which seven exporters
|
||||
override so the report matches the diff."""
|
||||
|
||||
def test_every_overridden_contract_method_is_documented(self):
|
||||
documented = ("carries", "requires", "can_add", "keys_by_system",
|
||||
"lands", "states", "fills", "clears", "names_anew", "withheld")
|
||||
overridden = {
|
||||
name
|
||||
for cls in discover_exporters().values()
|
||||
for name in documented
|
||||
if name in vars(cls)
|
||||
}
|
||||
wiki = (REPO_ROOT / "wiki" / "adding-a-platform.md").read_text(encoding="utf-8")
|
||||
for name in sorted(overridden):
|
||||
with self.subTest(method=name):
|
||||
self.assertIn(f"`{name}(", wiki)
|
||||
|
||||
|
||||
class BatoceraEntriesAtAnyIndent(unittest.TestCase):
|
||||
"""EmuELEC indents 23 of its systems with one tab. The entry pattern
|
||||
required four spaces, so each was appended again whole and the export
|
||||
carried 21 duplicated keys that ast.literal_eval kept silently."""
|
||||
|
||||
def test_a_tab_indented_entry_is_located(self):
|
||||
from exporter.batocera_exporter import Exporter
|
||||
|
||||
body = [
|
||||
' "atari5200": { "name": "5200", "biosFiles": [ { "md5": "a", "file": "bios/5200.rom" } ] },',
|
||||
'\t"amiga": { "name": "Amiga", "biosFiles": [ { "md5": "b", "file": "bios/kick.rom" },',
|
||||
'\t\t{ "md5": "c", "file": "bios/kick2.rom" } ] },',
|
||||
]
|
||||
self.assertEqual(
|
||||
Exporter._entry_spans(body), {"atari5200": (0, 0), "amiga": (1, 2)}
|
||||
)
|
||||
|
||||
|
||||
class RecalboxKeepsUnchangedText(unittest.TestCase):
|
||||
"""Every element the model held was re-rendered: attribute order,
|
||||
alignment, line breaks inside md5 and digest case changed in 222
|
||||
|
||||
@@ -185,5 +185,53 @@ class RetailDashboardTree(unittest.TestCase):
|
||||
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__":
|
||||
unittest.main()
|
||||
@@ -102,6 +102,17 @@ class FixtureImport(unittest.TestCase):
|
||||
twin = second / path.relative_to(first)
|
||||
self.assertEqual(path.read_bytes(), twin.read_bytes(), path.name)
|
||||
|
||||
def test_a_new_fetch_date_alone_rewrites_nothing(self) -> None:
|
||||
"""A refresh rewrote every catalog for the day it ran, and the diff
|
||||
read before commit no longer showed which DAT had moved."""
|
||||
self.assertEqual(run_import("redump", self.out).returncode, 0)
|
||||
path = self.out / "redump" / "sega-megacd.json"
|
||||
doc = json.loads(path.read_text())
|
||||
doc["fetched"] = "2000-01-01"
|
||||
path.write_text(importer.dump_catalog(doc))
|
||||
self.assertEqual(run_import("redump", self.out).returncode, 0)
|
||||
self.assertEqual(json.loads(path.read_text())["fetched"], "2000-01-01")
|
||||
|
||||
def test_floor_refuses_a_short_dat(self) -> None:
|
||||
result = run_import("no-intro", self.out, "--floor", "--dry-run")
|
||||
self.assertEqual(result.returncode, 1)
|
||||
|
||||
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user