mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-01 00:27:20 -05:00
chore: merge the region reference frames
This commit is contained in:
commit
e6f7ea8f07
794 files changed
+14502
-1807
No files matched your search
+276
-3
@@ -33,18 +33,24 @@ cores:
|
||||
OpenBIOS, a wrong one logs "Firmware found but has invalid SHA1" and is
|
||||
loaded anyway (libretro.c:849, 856, 3504-3511 at 7fea57a5).
|
||||
error_patterns: []
|
||||
error_screens: []
|
||||
error_screens:
|
||||
- sha1: dfbfc299250c0983f86bbf951efd187441306d92
|
||||
shows: the Japanese BIOS main menu, the disc refused (a US disc under a Japanese BIOS)
|
||||
"Beetle PSX HW":
|
||||
note: The same code as Beetle PSX, built with the hardware renderers.
|
||||
error_patterns: []
|
||||
error_screens: []
|
||||
error_screens:
|
||||
- sha1: dfbfc299250c0983f86bbf951efd187441306d92
|
||||
shows: the Japanese BIOS main menu, the disc refused (a US disc under a Japanese BIOS)
|
||||
"SwanStation":
|
||||
note: >-
|
||||
"No BIOS image found for <region> region" is followed by the built-in
|
||||
OpenBIOS (src/core/host_interface.cpp:182, src/core/system.cpp:678-682
|
||||
at 631f0fb3).
|
||||
error_patterns: []
|
||||
error_screens: []
|
||||
error_screens:
|
||||
- sha1: 167fb7b6527e295b12dca5806a29856f5f31d401
|
||||
shows: the Japanese BIOS main menu, the disc refused (a US disc under a Japanese BIOS)
|
||||
"PCSX-ReARMed":
|
||||
note: >-
|
||||
Without a BIOS, or with one of the wrong size, it logs "Using HLE BIOS"
|
||||
@@ -57,6 +63,16 @@ cores:
|
||||
source_ref: frontend/libretro.c:3646-3669 at c8816799
|
||||
error_patterns: []
|
||||
error_screens: []
|
||||
"Beetle Saturn":
|
||||
note: Without a usable BIOS the core logs one of these and retro_load_game fails.
|
||||
error_patterns:
|
||||
- pattern: '^Cannot open BIOS file '
|
||||
source_ref: mednafen/ss/ss.c:1943-1947 at 2ce77e20
|
||||
- pattern: '^BIOS file ".*" is of an incorrect size\.'
|
||||
source_ref: mednafen/ss/ss.c:1961-1967 at 2ce77e20
|
||||
- pattern: '^BIOS file ".*" could not be fully read'
|
||||
source_ref: mednafen/ss/ss.c:1978-1983 at 2ce77e20
|
||||
error_screens: []
|
||||
"PCSX1":
|
||||
note: An HLE BIOS stands in when none is found, and the run logs nothing about it.
|
||||
error_patterns: []
|
||||
@@ -176,3 +192,260 @@ content:
|
||||
- sha1: 2238516839dc43c0b22c00db5fe76128f5fb6f3f
|
||||
stage: game
|
||||
shows: a black screen with CREDIT 6
|
||||
- name: "Puzzle Bobble 2 (Japan)"
|
||||
catalog: redump
|
||||
sha1: 3b51b45b8b78777c04086fcd1ef1dd0439c365e6
|
||||
references:
|
||||
"Beetle PSX":
|
||||
recorded_from: 2026-10-27-a2bd916f1765
|
||||
frames:
|
||||
- sha1: 7b72ebae83893b63f9fffd1685f03b1d048d9fa9
|
||||
stage: bios
|
||||
shows: the Sony Computer Entertainment logo
|
||||
- sha1: 1d5305661b110a78aff8e41d60c19168c4af77ac
|
||||
stage: bios
|
||||
shows: the PlayStation logo licensed by Sony Computer Entertainment Inc.
|
||||
- sha1: d51ab7783f8eeafc63a28f14ce39f8e2babeffb3
|
||||
stage: game
|
||||
shows: Now Loading with the yellow bubble
|
||||
- sha1: 80d023ecbf776a97b6bb2bdeb13e43460f60fd53
|
||||
stage: game
|
||||
shows: the Taito logo on white, CREDIT 6
|
||||
- sha1: 1bda114b66fd098623abca772cf23813293a3849
|
||||
stage: game
|
||||
shows: a black screen with CREDIT 6
|
||||
- sha1: 3bb53b8a3036554a4bf9ec4785df7a4b4e06a87d
|
||||
stage: game
|
||||
shows: the attract-mode game on the beach, PASSAGE TIME IS
|
||||
"Beetle PSX HW":
|
||||
recorded_from: 2026-10-27-6e0e946eeba5
|
||||
frames:
|
||||
- sha1: 7b72ebae83893b63f9fffd1685f03b1d048d9fa9
|
||||
stage: bios
|
||||
shows: the Sony Computer Entertainment logo
|
||||
- sha1: 1d5305661b110a78aff8e41d60c19168c4af77ac
|
||||
stage: bios
|
||||
shows: the PlayStation logo licensed by Sony Computer Entertainment Inc.
|
||||
- sha1: d51ab7783f8eeafc63a28f14ce39f8e2babeffb3
|
||||
stage: game
|
||||
shows: Now Loading with the yellow bubble
|
||||
- sha1: 80d023ecbf776a97b6bb2bdeb13e43460f60fd53
|
||||
stage: game
|
||||
shows: the Taito logo on white, CREDIT 6
|
||||
- sha1: 1bda114b66fd098623abca772cf23813293a3849
|
||||
stage: game
|
||||
shows: a black screen with CREDIT 6
|
||||
- sha1: 3bb53b8a3036554a4bf9ec4785df7a4b4e06a87d
|
||||
stage: game
|
||||
shows: the attract-mode game on the beach, PASSAGE TIME IS
|
||||
"PCSX-ReARMed":
|
||||
recorded_from: 2026-10-27-65923ebc1836
|
||||
frames:
|
||||
- sha1: e5af1f2023d0d729ce7aabf52d8e03f75bb8d230
|
||||
stage: game
|
||||
shows: Now Loading with the yellow bubble
|
||||
- sha1: 2443e73be7ce3203408d7398c997fe6ec7cebd9f
|
||||
stage: game
|
||||
shows: the Taito logo on white, CREDIT 6
|
||||
- sha1: 373e5e3d7c524ed1556b9292a794169bf1de27d0
|
||||
stage: game
|
||||
shows: a black screen with CREDIT 6
|
||||
- sha1: 53f32c4acd5a522f359952ee48f138e44aee6e61
|
||||
stage: game
|
||||
shows: the attract-mode game on the beach, PASSAGE TIME IS
|
||||
"SwanStation":
|
||||
recorded_from: 2026-10-27-abe38e414339
|
||||
frames:
|
||||
- sha1: fad859d9b8211ce9f35ad3a05b0b7d184fe14d83
|
||||
stage: bios
|
||||
shows: the Sony Computer Entertainment logo
|
||||
- sha1: 9c168a8d6a6c706157307910e2b46039d019e48b
|
||||
stage: bios
|
||||
shows: the PlayStation logo licensed by Sony Computer Entertainment Inc.
|
||||
- sha1: e5af1f2023d0d729ce7aabf52d8e03f75bb8d230
|
||||
stage: game
|
||||
shows: Now Loading with the yellow bubble
|
||||
- sha1: 2443e73be7ce3203408d7398c997fe6ec7cebd9f
|
||||
stage: game
|
||||
shows: the Taito logo on white, CREDIT 6
|
||||
- sha1: 373e5e3d7c524ed1556b9292a794169bf1de27d0
|
||||
stage: game
|
||||
shows: a black screen with CREDIT 6
|
||||
- sha1: 53f32c4acd5a522f359952ee48f138e44aee6e61
|
||||
stage: game
|
||||
shows: the attract-mode game on the beach, PASSAGE TIME IS
|
||||
- name: "Bust-A-Move 2 - Arcade Edition (Europe)"
|
||||
catalog: redump
|
||||
sha1: fbfb904cdb7a0e25a659980a5ed1206298bc7a1e
|
||||
references:
|
||||
"Beetle PSX":
|
||||
recorded_from: 2026-10-27-4b9e700d03a9
|
||||
frames:
|
||||
- sha1: 7b72ebae83893b63f9fffd1685f03b1d048d9fa9
|
||||
stage: bios
|
||||
shows: the Sony Computer Entertainment logo
|
||||
- sha1: 408effe5ed13b50c97a21d2efe9930ab0d2b6f43
|
||||
stage: bios
|
||||
shows: the PlayStation logo licensed by Sony Computer Entertainment Europe
|
||||
- sha1: d51ab7783f8eeafc63a28f14ce39f8e2babeffb3
|
||||
stage: game
|
||||
shows: Now Loading with the yellow bubble
|
||||
- sha1: f5649c8df958220324753968e8785ea80a021837
|
||||
stage: game
|
||||
shows: the anti-piracy notice
|
||||
- sha1: 2cccff1e0706fcc8bdd4fdcd35e0310c9aaf9ac5
|
||||
stage: game
|
||||
shows: the Acclaim and Taito credits text
|
||||
- sha1: dd70e8e057c3de89f0c39c2b91b72fce902a8588
|
||||
stage: game
|
||||
shows: the Acclaim logo
|
||||
- sha1: 368ac3ce2a4ef7dcc1720e0e48f18e3e6404e191
|
||||
stage: game
|
||||
shows: the Taito logo on white
|
||||
"Beetle PSX HW":
|
||||
recorded_from: 2026-10-27-f3b61598f415
|
||||
frames:
|
||||
- sha1: 7b72ebae83893b63f9fffd1685f03b1d048d9fa9
|
||||
stage: bios
|
||||
shows: the Sony Computer Entertainment logo
|
||||
- sha1: 408effe5ed13b50c97a21d2efe9930ab0d2b6f43
|
||||
stage: bios
|
||||
shows: the PlayStation logo licensed by Sony Computer Entertainment Europe
|
||||
- sha1: d51ab7783f8eeafc63a28f14ce39f8e2babeffb3
|
||||
stage: game
|
||||
shows: Now Loading with the yellow bubble
|
||||
- sha1: f5649c8df958220324753968e8785ea80a021837
|
||||
stage: game
|
||||
shows: the anti-piracy notice
|
||||
- sha1: 2cccff1e0706fcc8bdd4fdcd35e0310c9aaf9ac5
|
||||
stage: game
|
||||
shows: the Acclaim and Taito credits text
|
||||
- sha1: dd70e8e057c3de89f0c39c2b91b72fce902a8588
|
||||
stage: game
|
||||
shows: the Acclaim logo
|
||||
- sha1: 368ac3ce2a4ef7dcc1720e0e48f18e3e6404e191
|
||||
stage: game
|
||||
shows: the Taito logo on white
|
||||
"PCSX-ReARMed":
|
||||
recorded_from: 2026-10-27-c71f71b579b9
|
||||
frames:
|
||||
- sha1: e5af1f2023d0d729ce7aabf52d8e03f75bb8d230
|
||||
stage: game
|
||||
shows: Now Loading with the yellow bubble
|
||||
- sha1: d0c5e568fd2585ad5c0ed244c25bdacdbb596d55
|
||||
stage: game
|
||||
shows: the anti-piracy notice
|
||||
- sha1: 4ea5612d272a8b05fabf98ed7dc4b245fe759001
|
||||
stage: game
|
||||
shows: the Acclaim and Taito credits text
|
||||
- sha1: ed433c340dc7ef65c8afba4434542c1827251127
|
||||
stage: game
|
||||
shows: the Acclaim logo
|
||||
- sha1: a909b761d6f46ec28bdf46aac874873db40396b7
|
||||
stage: game
|
||||
shows: the Taito logo on white
|
||||
"SwanStation":
|
||||
recorded_from: 2026-10-27-57e092562c10
|
||||
frames:
|
||||
- sha1: fad859d9b8211ce9f35ad3a05b0b7d184fe14d83
|
||||
stage: bios
|
||||
shows: the Sony Computer Entertainment logo
|
||||
- sha1: e6f4f8524f3c8a47d5dba0d33f63c64dcebccde7
|
||||
stage: bios
|
||||
shows: the PlayStation logo licensed by Sony Computer Entertainment Europe
|
||||
- sha1: e5af1f2023d0d729ce7aabf52d8e03f75bb8d230
|
||||
stage: game
|
||||
shows: Now Loading with the yellow bubble
|
||||
- sha1: d0c5e568fd2585ad5c0ed244c25bdacdbb596d55
|
||||
stage: game
|
||||
shows: the anti-piracy notice
|
||||
- sha1: 4ea5612d272a8b05fabf98ed7dc4b245fe759001
|
||||
stage: game
|
||||
shows: the Acclaim and Taito credits text
|
||||
- sha1: ed433c340dc7ef65c8afba4434542c1827251127
|
||||
stage: game
|
||||
shows: the Acclaim logo
|
||||
- sha1: a909b761d6f46ec28bdf46aac874873db40396b7
|
||||
stage: game
|
||||
shows: the Taito logo on white
|
||||
- name: "Puzzle Bobble 2X (Japan) (2M1, 2MB1)"
|
||||
catalog: redump
|
||||
sha1: b962ec9148a189239810a10677dcba073983733f
|
||||
references:
|
||||
"Beetle Saturn":
|
||||
recorded_from: 2026-10-27-ea155398ce4b
|
||||
frames:
|
||||
- sha1: 5edec5180d1c04dc0ead1e5fbbc7d6d53cc23fdb
|
||||
stage: bios
|
||||
shows: the SEGA SATURN logo of the Japanese BIOS
|
||||
- sha1: dcd45679fcd31a423d0e54690e89b52f417d8728
|
||||
stage: bios
|
||||
shows: SEGA, produced by or under license from Sega Enterprises
|
||||
- sha1: 7efe0a96245086d186a702a253525614d03f01f4
|
||||
stage: game
|
||||
shows: NOW LOADING
|
||||
- sha1: 79894146225fefecb7db22b6aa296cb035a8d8ae
|
||||
stage: game
|
||||
shows: the Taito logo on white, CREDIT 9
|
||||
- sha1: 475fce75c7c581b7367ac1a4adaa0aadc18afab1
|
||||
stage: game
|
||||
shows: GAME OVER, press 1P or 2P start button
|
||||
- sha1: f610c613f723fa640250121d6086b5e2c53b695c
|
||||
stage: game
|
||||
shows: the Puzzle Bobble 2X title, press start button
|
||||
- name: "Bust-A-Move 2 - Arcade Edition (USA)"
|
||||
catalog: redump
|
||||
sha1: 782b93d04ecb9da4cc534489e67feeac7ea9dc78
|
||||
references:
|
||||
"Beetle Saturn":
|
||||
recorded_from: 2026-10-27-35dd5587262d
|
||||
frames:
|
||||
- sha1: f6f6ad7714da4b7c32a7736276c8cba7ef9d45d0
|
||||
stage: bios
|
||||
shows: the SEGA SATURN globe logo
|
||||
- sha1: dcd45679fcd31a423d0e54690e89b52f417d8728
|
||||
stage: bios
|
||||
shows: SEGA, produced by or under license from Sega Enterprises
|
||||
- sha1: e0b0fd36c4da2579b509934b8fe010f4741edfa5
|
||||
stage: game
|
||||
shows: NOW LOADING
|
||||
- sha1: 97f62d0835bc7f27b660f330d794c149c50c5716
|
||||
stage: game
|
||||
shows: the Acclaim and Taito credits text, CREDIT 9
|
||||
- sha1: 0cd323a8a93adec3a2173549300acbf9a3fe5596
|
||||
stage: game
|
||||
shows: the Acclaim logo, CREDIT 9
|
||||
- sha1: 76ed20462b9f45e247611fa77feff69e47052875
|
||||
stage: game
|
||||
shows: the Taito logo on white, CREDIT 9
|
||||
- sha1: e90793d79a1c815cd74e7a56f0e9d8f124bfc547
|
||||
stage: game
|
||||
shows: the Bust-A-Move 2 title, press start button
|
||||
- name: "Bust-A-Move 2 - Arcade Edition (Europe)"
|
||||
catalog: redump
|
||||
sha1: 9298fabcdd797272c8c993abc81fcdd51a4e7830
|
||||
references:
|
||||
"Beetle Saturn":
|
||||
recorded_from: 2026-10-27-1880da25f150
|
||||
frames:
|
||||
- sha1: 2d870b2352923df4b2563f75c8a01a3d6dcb4237
|
||||
stage: bios
|
||||
shows: the SEGA SATURN globe logo
|
||||
- sha1: 95410b3a4a9f69ae981e2774c55574886efa67ea
|
||||
stage: bios
|
||||
shows: SEGA, produced by or under license from Sega Enterprises
|
||||
- sha1: 4d426aec90be9bff76e2d32da0437d91f9880950
|
||||
stage: game
|
||||
shows: NOW LOADING
|
||||
- sha1: 5bfbc97567caa32bad7489acc6c9a4f3f65ed863
|
||||
stage: game
|
||||
shows: the Acclaim and Taito credits text, CREDIT 9
|
||||
- sha1: 8af02ae2f2f0864657a3c7a68293a4da163000d6
|
||||
stage: game
|
||||
shows: the Acclaim logo, CREDIT 9
|
||||
- sha1: e3245a31197fa028c2da63f387f58a38e010c98d
|
||||
stage: game
|
||||
shows: the Taito logo on white, CREDIT 9
|
||||
- sha1: 9ae4dbf2ded891887635634d224dedccf176c1a0
|
||||
stage: game
|
||||
shows: the Bust-A-Move 2 title, press start button
|
||||
+1
-1
@@ -9,4 +9,4 @@
|
||||
|
||||
|
||||
|
||||
Ctrl-C Quit B/Esc Quit
|
||||
Ctrl-C Quit B Quit
|
||||
+1
-1
@@ -12,4 +12,4 @@
|
||||
|
||||
|
||||
|
||||
Ctrl-C Quit B/Esc Quit
|
||||
Ctrl-C Quit B Quit
|
||||
Vendored
+902
@@ -0,0 +1,902 @@
|
||||
=== setup
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Where do you play?^[[0m
|
||||
|
||||
Found on this machine:
|
||||
|
||||
^[[0;7;35m > RetroArch^[[0m
|
||||
KNULLI (Batocera files)
|
||||
Another setup or folder
|
||||
|
||||
BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m
|
||||
ES-DE found: its BIOS files go to each emulator, not to ES-DE itself.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Continue^[[0;2m B/Esc Quit ? Help^[[0m
|
||||
=== platforms
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which setup?^[[0m
|
||||
|
||||
Choose the setup that will use the BIOS files.
|
||||
|
||||
^[[0;2m Filter: setup or device (/)^[[0m
|
||||
^[[0;7;35m > RetroArch^[[0m
|
||||
Batocera
|
||||
Recalbox
|
||||
RetroBat
|
||||
EmuDeck
|
||||
Lakka
|
||||
RetroDECK
|
||||
RomM
|
||||
BizHawk
|
||||
MiSTer FPGA
|
||||
RetroPie
|
||||
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
|
||||
AmberELEC, SD card RG351, RG552
|
||||
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
|
||||
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, Odroid Go Super, RG351V, +6
|
||||
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, R35S, RGB10
|
||||
CrossMix-OS, SD card TrimUI Smart Pro
|
||||
dArkOS, SD card Odroid Go Advance, RGB10, RG351MP, G350
|
||||
EmuELEC, SD card Odroid Go
|
||||
JELOS, SD card RG351M, RG351V, RG353M, RG353P, RG353V, RG353VS, RG503, RG552, R33S, R36S, +24
|
||||
KNULLI, SD card RG ARC-S, RG28XX, RG34XX, RG35XX, RG35XX Plus, RG35XX H, RG35XX 2024, +15
|
||||
KOLOS, SD card RG353P, RG353M, RG353V, RG353VS, RG503, RK2023, RGB30, RGB10 Max 3
|
||||
Koriki, SD card Miyoo Mini
|
||||
MinUI, SD card RG28XX, RG34XX, RG34XX SP, RG35XX, RG35XX Plus, RG35XX H, RG35XX SP, +16
|
||||
MiyooCFW, SD card BittBoy, Pocket Go, PowKiddy Q90, PowKiddy V90, PowKiddy Q20 Mini, Sup M3, +1
|
||||
muOS, SD card RG35XX H, RG35XX Plus, RG35XX SP, RG35XX 2024, RG34XX, RG40XX H, RG40XX V, +4
|
||||
MyMinUI, SD card RG35XX, Miyoo Mini, Miyoo Mini Flip, Miyoo Mini Plus, Miyoo A30, +16
|
||||
NextUI, SD card TrimUI Brick, TrimUI Smart Pro
|
||||
Onion, SD card Miyoo Mini, Miyoo Mini Plus
|
||||
plumOS, SD card MagicX XU20 V32
|
||||
RetroOZ, SD card Odroid Go Super, Odroid Go Advance, RGB10 Max, RGB10 Max 2
|
||||
ROCKNIX, SD card RG351, RG353, RG503, RG552, RG 55G1, RG ARC, RG35XX 2024, RG35XX Plus, +37
|
||||
spruceOS, SD card Miyoo A30, Miyoo Flip, Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip, +5
|
||||
UnofficialOS, SD card R35S, R36S, RG351M, RG351P, RG351V, RG353M, RG353P, RG353V, RG503, +5
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Choose^[[0;2m B/Esc Back / Filter ? Help^[[0m
|
||||
=== folder
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which folder?^[[0m
|
||||
|
||||
RetroArch usually reads its BIOS files from:
|
||||
/home/user/.config/retroarch/system
|
||||
|
||||
^[[0;7;35m > Use this folder^[[0m
|
||||
Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Use this folder^[[0;2m B/Esc Back ? Help^[[0m
|
||||
=== folder-card
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Where is the SD card?^[[0m
|
||||
|
||||
NextUI keeps its BIOS files at the root of its SD card. Choose the card's top folder, the one
|
||||
holding .system.
|
||||
No NextUI card is mounted. Insert it, then choose Look again.
|
||||
|
||||
^[[0;7;35m > Browse for a folder^[[0m
|
||||
Type a path
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Browse^[[0;2m B/Esc Back ? Help^[[0m
|
||||
=== browse
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Browse for a folder^[[0m
|
||||
|
||||
/home/user/.config/retroarch
|
||||
|
||||
^[[0;7;35m > Use this folder^[[0m
|
||||
..
|
||||
/media/user/SDCARD (NextUI card)
|
||||
cores/
|
||||
system/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Use this folder^[[0;2m B/Esc Back ? Help^[[0m
|
||||
=== path
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Type a path^[[0m
|
||||
|
||||
The folder for RetroArch:
|
||||
^[[0;7;35m > ~/bios_^[[0m
|
||||
|
||||
Ctrl-U clears. Quotes are removed, ~ and variables are expanded.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mEnter^[[0m Use this folder^[[0;2m Esc Back Ctrl-U Clear^[[0m
|
||||
=== create
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Create the folder?^[[0m
|
||||
|
||||
/home/user/bios/new does not exist.
|
||||
The nearest folder that does: /home/user
|
||||
|
||||
^[[0;7;35m > Create it and continue^[[0m
|
||||
Choose another folder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Create^[[0;2m B/Esc Quit^[[0m
|
||||
=== checking
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Checking^[[0m
|
||||
|
||||
RetroArch: ~/.config/retroarch/system
|
||||
|
||||
File list received.
|
||||
Reading the files already in the folder... 0:04
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mCtrl-C^[[0m Quit^[[0;2m B/Esc Quit^[[0m
|
||||
=== plan
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m
|
||||
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
|
||||
^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m
|
||||
The ^[[0;33m1 wrong copy^[[0m is replaced, the old one kept in a backup folder.
|
||||
Free space: 50.0 GB
|
||||
|
||||
^[[0;7;35m > Install^[[0m
|
||||
Choose systems
|
||||
Copy the BIOS files from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Install^[[0;2m B/Esc Back Y/o Options ? Help^[[0m
|
||||
=== plan-in-place
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
|
||||
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
|
||||
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
|
||||
Free space: 50.0 GB
|
||||
|
||||
^[[0;7;35m > Close^[[0m
|
||||
Options
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Close^[[0;2m B/Esc Back Y/o Options ? Help^[[0m
|
||||
=== systems
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Choose systems^[[0m
|
||||
|
||||
All
|
||||
None
|
||||
^[[0;2m Filter (/ to type)^[[0m
|
||||
^[[0;7;35m > [x] nintendo-gba (gba) 1 file, 16.0 KB^[[0m
|
||||
[x] sony-playstation 3 files, 1.0 MB
|
||||
^[[0;2m [ ] nintendo-famicom-disk (ファミコン) in place^[[0m
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;1m ^[[0;1;35m2^[[0;1m of ^[[0;1;35m3^[[0;1m systems: ^[[0;1;35m4^[[0;1m files, ^[[0;1;35m1.0 MB^[[0m
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Continue^[[0;2m B/Esc Back X/Space Tick ? Help^[[0m
|
||||
=== options
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Options^[[0m
|
||||
|
||||
Each row is an option of the command line, named on the right.
|
||||
|
||||
^[[0;7;35m > Preferred regions: North America, Japan --region^[[0m
|
||||
Game data: kept --no-game-data
|
||||
Installed cores only: no (1 found) --installed-cores
|
||||
Cores: all --core
|
||||
Parallel downloads: 8 --jobs
|
||||
BIOS folder: ^[[0;1m~/.config/retroarch/system --dest^[[0m
|
||||
Copy from: none --from
|
||||
Preview the file list --dry-run
|
||||
Show the command line
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Open^[[0;2m B/Esc Back Left/Right Set ? Help^[[0m
|
||||
=== regions
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preferred regions^[[0m
|
||||
|
||||
Tick the regions to keep, best first. Where a system has one BIOS per region, the best one found
|
||||
is installed.
|
||||
|
||||
^[[0;7;35m > [1] North America^[[0m
|
||||
[ ] Japan
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Tick^[[0;2m B/Esc Done Left/Right Order ? Help^[[0m
|
||||
=== cores
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Cores^[[0m
|
||||
|
||||
Tick the cores you use. With none ticked, every core's files are kept.
|
||||
|
||||
^[[0;7;35m > Filter (/ to type)^[[0m
|
||||
[ ] mesen
|
||||
[x] pcsx_rearmed
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Filter^[[0;2m B/Esc Back X/Space Tick ? Help^[[0m
|
||||
=== preview
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preview^[[0m
|
||||
|
||||
4 files to download, 1.0 MB. Nothing is written.
|
||||
|
||||
missing 512.0 KB scph5500.bin
|
||||
wrong copy 512.0 KB scph5501.bin
|
||||
missing 4 B pcsx/extra.bin
|
||||
missing 16.0 KB gba_bios.bin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Back
|
||||
=== command
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Command line^[[0m
|
||||
|
||||
The same choice without questions:
|
||||
|
||||
python3 install.py --platform retroarch --dest /home/user/.config/retroarch/system --system
|
||||
nintendo-gba
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Back
|
||||
=== confirm
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Confirm^[[0m
|
||||
|
||||
Counted again for your choices. Nothing is written before Install.
|
||||
^[[0;1m ^[[0;1;35m1^[[0;1m file to download, ^[[0;1;35m16.0 KB^[[0m
|
||||
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
|
||||
0 in place, ^[[0;33m1 missing^[[0m, 0 wrong copies
|
||||
Free space: 50.0 GB
|
||||
|
||||
^[[0;7;35m > Install^[[0m
|
||||
Back
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Install^[[0;2m B/Esc Back ? Help^[[0m
|
||||
=== installing
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Installing^[[0m
|
||||
|
||||
[^[[0;1;35m##############################################^[[0;2m---------------------------------------------^[[0m] 50%
|
||||
528.0 KB of 1.0 MB
|
||||
128.9 KB/s, 0:04 left
|
||||
2 of 4 files
|
||||
^[[0;2m B, Ctrl-C or Select+Start stops. What is done stays done.^[[0m
|
||||
|
||||
^[[0;1;31mFAILED^[[0m scph5500.bin (HTTP 404)
|
||||
^[[0;32m ok^[[0m gba_bios.bin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mB/Esc^[[0m Stop^[[0;2m ? Help^[[0m
|
||||
=== result
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mFinished with errors^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;32m3 installed^[[0;1m, ^[[0;1;31m1 failed^[[0;1m.^[[0m
|
||||
BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m
|
||||
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
To put them back: python3 install.py --restore
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
Same choice without questions:
|
||||
python3 install.py --platform retroarch --dest /home/user/.config/retroarch/system
|
||||
Details: /tmp/retrobios-x/install.log
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Exit
|
||||
=== error
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mSomething went wrong^[[0m
|
||||
|
||||
Could not fetch the file list for retroarch after 3 attempts: [Errno -2] Name or service not
|
||||
known
|
||||
Address: https://raw.githubusercontent.com/Abdess/retrobios/main/install/retroarch.json
|
||||
|
||||
^[[0;7;35m > Choose another setup or folder^[[0m
|
||||
Quit
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Choose^[[0;2m B/Esc Back^[[0m
|
||||
=== quit
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Quit?^[[0m
|
||||
|
||||
Quit without installing? Nothing was written.
|
||||
|
||||
^[[0;7;35m > Quit^[[0m
|
||||
Stay
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Quit^[[0;2m B/Esc Stay^[[0m
|
||||
=== help
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Keys^[[0m
|
||||
|
||||
Up/Down, k/j Move
|
||||
A/Enter, Start Install
|
||||
Y, o, Tab Options
|
||||
B/Esc, Backspace Back
|
||||
q, Ctrl-C Quit
|
||||
?, F1 This help
|
||||
|
||||
BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m Any key closes this help^[[0m
|
||||
Vendored
+286
@@ -0,0 +1,286 @@
|
||||
=== setup
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Where do you play?^[[0m
|
||||
|
||||
Found on this machine:
|
||||
|
||||
^[[0;7;35m > RetroArch^[[0m
|
||||
KNULLI (Batocera files)
|
||||
Another setup or folder
|
||||
|
||||
BIOS folder:
|
||||
/home/user/.config/retroarch/system
|
||||
ES-DE found: its BIOS files go to each
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Continue^[[0;2m B Quit ? Help^[[0m
|
||||
=== platforms
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which setup?^[[0m
|
||||
|
||||
^[[0;2m Filter: setup or device (/)^[[0m
|
||||
^[[0;7;35m > RetroArch^[[0m
|
||||
Batocera
|
||||
Recalbox
|
||||
RetroBat
|
||||
EmuDeck
|
||||
Lakka
|
||||
RetroDECK
|
||||
^[[0;2m 1-7 of 34^[[0m
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Choose^[[0;2m B Back / Filter ? Help^[[0m
|
||||
=== folder
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which folder?^[[0m
|
||||
|
||||
RetroArch usually reads its BIOS files
|
||||
from:
|
||||
/home/user/.config/retroarch/system
|
||||
|
||||
^[[0;7;35m > Use this folder^[[0m
|
||||
Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Use this folder^[[0;2m B Back ? Help^[[0m
|
||||
=== folder-card
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Where is the SD card?^[[0m
|
||||
|
||||
NextUI keeps its BIOS files at the
|
||||
root of its SD card. Choose the
|
||||
card's top folder, the one holding
|
||||
.system.
|
||||
No NextUI card is mounted. Insert it,
|
||||
then choose Look again.
|
||||
^[[0;7;35m > Browse for a folder^[[0m
|
||||
Type a path
|
||||
Look again
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Browse^[[0;2m B Back ? Help^[[0m
|
||||
=== browse
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Browse for a folder^[[0m
|
||||
|
||||
/home/user/.config/retroarch
|
||||
|
||||
^[[0;7;35m > Use this folder^[[0m
|
||||
..
|
||||
/media/user/SDCARD (NextUI card)
|
||||
cores/
|
||||
system/
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Use this folder^[[0;2m B Back ? Help^[[0m
|
||||
=== path
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Type a path^[[0m
|
||||
|
||||
The folder for RetroArch:
|
||||
^[[0;7;35m > ~/bios_^[[0m
|
||||
|
||||
Ctrl-U clears. Quotes are removed, ~
|
||||
and variables are expanded.
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mEnter^[[0m Use this folder^[[0;2m Esc Back^[[0m
|
||||
=== create
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Create the folder?^[[0m
|
||||
|
||||
/home/user/bios/new does not exist.
|
||||
The nearest folder that does:
|
||||
/home/user
|
||||
|
||||
^[[0;7;35m > Create it and continue^[[0m
|
||||
Choose another folder
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Create^[[0;2m B Quit^[[0m
|
||||
=== checking
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Checking^[[0m
|
||||
|
||||
RetroArch: ~/.config/retroarch/system
|
||||
|
||||
File list received.
|
||||
Reading the files already in the
|
||||
folder... 0:04
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mCtrl-C^[[0m Quit^[[0;2m B Quit^[[0m
|
||||
=== plan
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m
|
||||
BIOS folder: ^[[0;1m~/.co.../retroarch/system^[[0m
|
||||
^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m
|
||||
^[[0;33m1 wrong copy^[[0m: backed up, replaced.
|
||||
^[[0;7;35m > Install^[[0m
|
||||
Choose systems
|
||||
Copy from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Install^[[0;2m B Back Y Options ? Help^[[0m
|
||||
=== plan-in-place
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
|
||||
BIOS folder: ^[[0;1m~/.co.../retroarch/system^[[0m
|
||||
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
|
||||
Free space: 50.0 GB
|
||||
|
||||
^[[0;7;35m > Close^[[0m
|
||||
Options
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Close^[[0;2m B Back Y Options ? Help^[[0m
|
||||
=== systems
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Choose systems^[[0m
|
||||
|
||||
All
|
||||
None
|
||||
^[[0;2m Filter (/ to type)^[[0m
|
||||
^[[0;7;35m > [x] nintendo-gba (gba) 16.0 KB^[[0m
|
||||
[x] sony-playstation 1.0 MB
|
||||
^[[0;2m [ ] nintendo-famicom-disk in place^[[0m
|
||||
|
||||
|
||||
^[[0;1m ^[[0;1;35m2^[[0;1m of ^[[0;1;35m3^[[0;1m systems: ^[[0;1;35m4^[[0;1m files, ^[[0;1;35m1.0 MB^[[0m
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Continue^[[0;2m B Back X Tick ? Help^[[0m
|
||||
=== options
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Options^[[0m
|
||||
|
||||
^[[0;7;35m > Preferred regions: North America, Ja^[[0m
|
||||
Game data: kept
|
||||
Installed cores only: no (1 found)
|
||||
Cores: all
|
||||
Parallel downloads: 8
|
||||
BIOS folder: ^[[0;1m~/.config...ch/system^[[0m
|
||||
Copy from: none
|
||||
Preview the file list
|
||||
Show the command line
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Open^[[0;2m B Back <> Set ? Help^[[0m
|
||||
=== regions
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preferred regions^[[0m
|
||||
|
||||
Tick the regions to keep, best first.
|
||||
Where a system has one BIOS per
|
||||
region, the best one found is
|
||||
installed.
|
||||
|
||||
^[[0;7;35m > [1] North America^[[0m
|
||||
[ ] Japan
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Tick^[[0;2m B Done <> Order ? Help^[[0m
|
||||
=== cores
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Cores^[[0m
|
||||
|
||||
Tick the cores you use. With none
|
||||
ticked, every core's files are kept.
|
||||
|
||||
^[[0;7;35m > Filter (/ to type)^[[0m
|
||||
[ ] mesen
|
||||
[x] pcsx_rearmed
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Filter^[[0;2m B Back X Tick ? Help^[[0m
|
||||
=== preview
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preview^[[0m
|
||||
|
||||
4 files to download, 1.0 MB. Nothing
|
||||
is written.
|
||||
|
||||
missing 512.0 KB scph5500.bin
|
||||
wrong copy 512.0 KB scph5501.bin
|
||||
missing 4 B pcsx/extra.bin
|
||||
missing 16.0 KB gba_bios.bin
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Back
|
||||
=== command
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Command line^[[0m
|
||||
|
||||
The same choice without questions:
|
||||
|
||||
python3 install.py --platform
|
||||
retroarch --dest
|
||||
/home/user/.config/retroarch/system
|
||||
--system nintendo-gba
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Back
|
||||
=== confirm
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Confirm^[[0m
|
||||
|
||||
Counted again for your choices.
|
||||
Nothing is written before Install.
|
||||
^[[0;1m ^[[0;1;35m1^[[0;1m file to download, ^[[0;1;35m16.0 KB^[[0m
|
||||
BIOS folder: ^[[0;1m~/.co.../retroarch/system^[[0m
|
||||
0 in place, ^[[0;33m1 missing^[[0m, 0 wrong copies
|
||||
Free space: 50.0 GB
|
||||
|
||||
^[[0;7;35m > Install^[[0m
|
||||
Back
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Install^[[0;2m B Back ? Help^[[0m
|
||||
=== installing
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Installing^[[0m
|
||||
|
||||
[^[[0;1;35m###############^[[0;2m----------------^[[0m] 50%
|
||||
528.0 KB of 1.0 MB
|
||||
128.9 KB/s, 0:04 left
|
||||
2 of 4 files
|
||||
^[[0;2m B, Ctrl-C or Select+Start stops. What^[[0m
|
||||
^[[0;2m is done stays done.^[[0m
|
||||
|
||||
^[[0;1;31mFAILED^[[0m scph5500.bin (HTTP 404)
|
||||
^[[0;32m ok^[[0m gba_bios.bin
|
||||
^[[0;2m ^[[0;1;35mB^[[0m Stop^[[0;2m ? Help^[[0m
|
||||
=== result
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mFinished with errors^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;32m3 installed^[[0;1m, ^[[0;1;31m1 failed^[[0;1m.^[[0m
|
||||
BIOS folder:
|
||||
/home/user/.config/retroarch/system
|
||||
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
To put them back: python3 install.py
|
||||
^[[0;2m 1-8 of 17^[[0m
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Exit^[[0;2m ^v Scroll^[[0m
|
||||
=== error
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mSomething went wrong^[[0m
|
||||
|
||||
Could not fetch the file list for
|
||||
retroarch after 3 attempts: [Errno
|
||||
-2] Name or service not known
|
||||
Address:
|
||||
https://raw.githubusercontent.com/
|
||||
^[[0;2m 1-5 of 7^[[0m
|
||||
|
||||
^[[0;7;35m > Choose another setup or folder^[[0m
|
||||
Quit
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Choose^[[0;2m B Back PgUp/PgDn Scroll^[[0m
|
||||
=== quit
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Quit?^[[0m
|
||||
|
||||
Quit without installing? Nothing was
|
||||
written.
|
||||
|
||||
^[[0;7;35m > Quit^[[0m
|
||||
Stay
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Quit^[[0;2m B Stay^[[0m
|
||||
=== help
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Keys^[[0m
|
||||
|
||||
Up/Down, k/j Move
|
||||
A/Enter, Start Install
|
||||
Y, o, Tab Options
|
||||
B/Esc, Backspace Back
|
||||
q, Ctrl-C Quit
|
||||
?, F1 This help
|
||||
|
||||
BIOS folder:
|
||||
/home/user/.config/retroarch/system
|
||||
^[[0;2m Any key closes this help^[[0m
|
||||
Vendored
+352
@@ -0,0 +1,352 @@
|
||||
=== setup
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Where do you play?^[[0m
|
||||
|
||||
Found on this machine:
|
||||
|
||||
^[[0;7;35m > RetroArch^[[0m
|
||||
KNULLI (Batocera files)
|
||||
Another setup or folder
|
||||
|
||||
BIOS folder:
|
||||
/home/user/.config/retroarch/system
|
||||
ES-DE found: its BIOS files go to each
|
||||
emulator, not to ES-DE itself.
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Continue^[[0;2m B Quit ? Help^[[0m
|
||||
=== platforms
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which setup?^[[0m
|
||||
|
||||
^[[0;2m Filter: setup or device (/)^[[0m
|
||||
^[[0;7;35m > RetroArch^[[0m
|
||||
Batocera
|
||||
Recalbox
|
||||
RetroBat
|
||||
EmuDeck
|
||||
Lakka
|
||||
RetroDECK
|
||||
RomM
|
||||
BizHawk
|
||||
MiSTer FPGA
|
||||
^[[0;2m 1-10 of 34^[[0m
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Choose^[[0;2m B Back / Filter ? Help^[[0m
|
||||
=== folder
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which folder?^[[0m
|
||||
|
||||
RetroArch usually reads its BIOS files
|
||||
from:
|
||||
/home/user/.config/retroarch/system
|
||||
|
||||
^[[0;7;35m > Use this folder^[[0m
|
||||
Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Use this folder^[[0;2m B Back ? Help^[[0m
|
||||
=== folder-card
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Where is the SD card?^[[0m
|
||||
|
||||
NextUI keeps its BIOS files at the
|
||||
root of its SD card. Choose the
|
||||
card's top folder, the one holding
|
||||
.system.
|
||||
No NextUI card is mounted. Insert it,
|
||||
then choose Look again.
|
||||
|
||||
^[[0;7;35m > Browse for a folder^[[0m
|
||||
Type a path
|
||||
Look again
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Browse^[[0;2m B Back ? Help^[[0m
|
||||
=== browse
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Browse for a folder^[[0m
|
||||
|
||||
/home/user/.config/retroarch
|
||||
|
||||
^[[0;7;35m > Use this folder^[[0m
|
||||
..
|
||||
/media/user/SDCARD (NextUI card)
|
||||
cores/
|
||||
system/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Use this folder^[[0;2m B Back ? Help^[[0m
|
||||
=== path
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Type a path^[[0m
|
||||
|
||||
The folder for RetroArch:
|
||||
^[[0;7;35m > ~/bios_^[[0m
|
||||
|
||||
Ctrl-U clears. Quotes are removed, ~
|
||||
and variables are expanded.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mEnter^[[0m Use this folder^[[0;2m Esc Back^[[0m
|
||||
=== create
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Create the folder?^[[0m
|
||||
|
||||
/home/user/bios/new does not exist.
|
||||
The nearest folder that does:
|
||||
/home/user
|
||||
|
||||
^[[0;7;35m > Create it and continue^[[0m
|
||||
Choose another folder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Create^[[0;2m B Quit^[[0m
|
||||
=== checking
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Checking^[[0m
|
||||
|
||||
RetroArch: ~/.config/retroarch/system
|
||||
|
||||
File list received.
|
||||
Reading the files already in the
|
||||
folder... 0:04
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mCtrl-C^[[0m Quit^[[0;2m B Quit^[[0m
|
||||
=== plan
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m
|
||||
BIOS folder: ^[[0;1m~/.co.../retroarch/system^[[0m
|
||||
^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m
|
||||
^[[0;33m1 wrong copy^[[0m: backed up, replaced.
|
||||
Free space: 50.0 GB
|
||||
|
||||
^[[0;7;35m > Install^[[0m
|
||||
Choose systems
|
||||
Copy from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Install^[[0;2m B Back Y Options ? Help^[[0m
|
||||
=== plan-in-place
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
|
||||
BIOS folder: ^[[0;1m~/.co.../retroarch/system^[[0m
|
||||
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
|
||||
Free space: 50.0 GB
|
||||
|
||||
^[[0;7;35m > Close^[[0m
|
||||
Options
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Close^[[0;2m B Back Y Options ? Help^[[0m
|
||||
=== systems
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Choose systems^[[0m
|
||||
|
||||
All
|
||||
None
|
||||
^[[0;2m Filter (/ to type)^[[0m
|
||||
^[[0;7;35m > [x] nintendo-gba (gba) 16.0 KB^[[0m
|
||||
[x] sony-playstation 1.0 MB
|
||||
^[[0;2m [ ] nintendo-famicom-disk in place^[[0m
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;1m ^[[0;1;35m2^[[0;1m of ^[[0;1;35m3^[[0;1m systems: ^[[0;1;35m4^[[0;1m files, ^[[0;1;35m1.0 MB^[[0m
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Continue^[[0;2m B Back X Tick ? Help^[[0m
|
||||
=== options
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Options^[[0m
|
||||
|
||||
^[[0;7;35m > Preferred regions: North America, Ja^[[0m
|
||||
Game data: kept
|
||||
Installed cores only: no (1 found)
|
||||
Cores: all
|
||||
Parallel downloads: 8
|
||||
BIOS folder: ^[[0;1m~/.config...ch/system^[[0m
|
||||
Copy from: none
|
||||
Preview the file list
|
||||
Show the command line
|
||||
|
||||
Sets --region
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Open^[[0;2m B Back <> Set ? Help^[[0m
|
||||
=== regions
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preferred regions^[[0m
|
||||
|
||||
Tick the regions to keep, best first.
|
||||
Where a system has one BIOS per
|
||||
region, the best one found is
|
||||
installed.
|
||||
|
||||
^[[0;7;35m > [1] North America^[[0m
|
||||
[ ] Japan
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Tick^[[0;2m B Done <> Order ? Help^[[0m
|
||||
=== cores
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Cores^[[0m
|
||||
|
||||
Tick the cores you use. With none
|
||||
ticked, every core's files are kept.
|
||||
|
||||
^[[0;7;35m > Filter (/ to type)^[[0m
|
||||
[ ] mesen
|
||||
[x] pcsx_rearmed
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Filter^[[0;2m B Back X Tick ? Help^[[0m
|
||||
=== preview
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preview^[[0m
|
||||
|
||||
4 files to download, 1.0 MB. Nothing
|
||||
is written.
|
||||
|
||||
missing 512.0 KB scph5500.bin
|
||||
wrong copy 512.0 KB scph5501.bin
|
||||
missing 4 B pcsx/extra.bin
|
||||
missing 16.0 KB gba_bios.bin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Back
|
||||
=== command
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Command line^[[0m
|
||||
|
||||
The same choice without questions:
|
||||
|
||||
python3 install.py --platform
|
||||
retroarch --dest
|
||||
/home/user/.config/retroarch/system
|
||||
--system nintendo-gba
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Back
|
||||
=== confirm
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Confirm^[[0m
|
||||
|
||||
Counted again for your choices.
|
||||
Nothing is written before Install.
|
||||
^[[0;1m ^[[0;1;35m1^[[0;1m file to download, ^[[0;1;35m16.0 KB^[[0m
|
||||
BIOS folder: ^[[0;1m~/.co.../retroarch/system^[[0m
|
||||
0 in place, ^[[0;33m1 missing^[[0m, 0 wrong copies
|
||||
Free space: 50.0 GB
|
||||
|
||||
^[[0;7;35m > Install^[[0m
|
||||
Back
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Install^[[0;2m B Back ? Help^[[0m
|
||||
=== installing
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Installing^[[0m
|
||||
|
||||
[^[[0;1;35m###############^[[0;2m----------------^[[0m] 50%
|
||||
528.0 KB of 1.0 MB
|
||||
128.9 KB/s, 0:04 left
|
||||
2 of 4 files
|
||||
^[[0;2m B, Ctrl-C or Select+Start stops. What^[[0m
|
||||
^[[0;2m is done stays done.^[[0m
|
||||
|
||||
^[[0;1;31mFAILED^[[0m scph5500.bin (HTTP 404)
|
||||
^[[0;32m ok^[[0m gba_bios.bin
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mB^[[0m Stop^[[0;2m ? Help^[[0m
|
||||
=== result
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mFinished with errors^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;32m3 installed^[[0;1m, ^[[0;1;31m1 failed^[[0;1m.^[[0m
|
||||
BIOS folder:
|
||||
/home/user/.config/retroarch/system
|
||||
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
To put them back: python3 install.py
|
||||
--restore
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
^[[0;2m 1-11 of 17^[[0m
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Exit^[[0;2m ^v Scroll^[[0m
|
||||
=== error
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mSomething went wrong^[[0m
|
||||
|
||||
Could not fetch the file list for
|
||||
retroarch after 3 attempts: [Errno
|
||||
-2] Name or service not known
|
||||
Address:
|
||||
https://raw.githubusercontent.com/
|
||||
Abdess/retrobios/main/install/
|
||||
retroarch.json
|
||||
|
||||
^[[0;7;35m > Choose another setup or folder^[[0m
|
||||
Quit
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Choose^[[0;2m B Back^[[0m
|
||||
=== quit
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Quit?^[[0m
|
||||
|
||||
Quit without installing? Nothing was
|
||||
written.
|
||||
|
||||
^[[0;7;35m > Quit^[[0m
|
||||
Stay
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA^[[0m Quit^[[0;2m B Stay^[[0m
|
||||
=== help
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Keys^[[0m
|
||||
|
||||
Up/Down, k/j Move
|
||||
A/Enter, Start Install
|
||||
Y, o, Tab Options
|
||||
B/Esc, Backspace Back
|
||||
q, Ctrl-C Quit
|
||||
?, F1 This help
|
||||
|
||||
BIOS folder:
|
||||
/home/user/.config/retroarch/system
|
||||
|
||||
|
||||
|
||||
^[[0;2m Any key closes this help^[[0m
|
||||
Vendored
+550
@@ -0,0 +1,550 @@
|
||||
=== setup
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Where do you play?^[[0m
|
||||
|
||||
Found on this machine:
|
||||
|
||||
^[[0;7;35m > RetroArch^[[0m
|
||||
KNULLI (Batocera files)
|
||||
Another setup or folder
|
||||
|
||||
BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m
|
||||
ES-DE found: its BIOS files go to each emulator, not to ES-DE itself.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Continue^[[0;2m B/Esc Quit ? Help^[[0m
|
||||
=== platforms
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which setup?^[[0m
|
||||
|
||||
^[[0;2m Filter: setup or device (/)^[[0m
|
||||
^[[0;7;35m > RetroArch^[[0m
|
||||
Batocera
|
||||
Recalbox
|
||||
RetroBat
|
||||
EmuDeck
|
||||
Lakka
|
||||
RetroDECK
|
||||
RomM
|
||||
BizHawk
|
||||
MiSTer FPGA
|
||||
RetroPie
|
||||
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
|
||||
AmberELEC, SD card RG351, RG552
|
||||
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
|
||||
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, +8
|
||||
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, +2
|
||||
CrossMix-OS, SD card TrimUI Smart Pro
|
||||
dArkOS, SD card Odroid Go Advance, RGB10, RG351MP, G350
|
||||
EmuELEC, SD card Odroid Go
|
||||
^[[0;2m 1-19 of 34^[[0m
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Choose^[[0;2m B/Esc Back / Filter ? Help^[[0m
|
||||
=== folder
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which folder?^[[0m
|
||||
|
||||
RetroArch usually reads its BIOS files from:
|
||||
/home/user/.config/retroarch/system
|
||||
|
||||
^[[0;7;35m > Use this folder^[[0m
|
||||
Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Use this folder^[[0;2m B/Esc Back ? Help^[[0m
|
||||
=== folder-card
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Where is the SD card?^[[0m
|
||||
|
||||
NextUI keeps its BIOS files at the root of its SD card. Choose the card's top
|
||||
folder, the one holding .system.
|
||||
No NextUI card is mounted. Insert it, then choose Look again.
|
||||
|
||||
^[[0;7;35m > Browse for a folder^[[0m
|
||||
Type a path
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Browse^[[0;2m B/Esc Back ? Help^[[0m
|
||||
=== browse
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Browse for a folder^[[0m
|
||||
|
||||
/home/user/.config/retroarch
|
||||
|
||||
^[[0;7;35m > Use this folder^[[0m
|
||||
..
|
||||
/media/user/SDCARD (NextUI card)
|
||||
cores/
|
||||
system/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Use this folder^[[0;2m B/Esc Back ? Help^[[0m
|
||||
=== path
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Type a path^[[0m
|
||||
|
||||
The folder for RetroArch:
|
||||
^[[0;7;35m > ~/bios_^[[0m
|
||||
|
||||
Ctrl-U clears. Quotes are removed, ~ and variables are expanded.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mEnter^[[0m Use this folder^[[0;2m Esc Back Ctrl-U Clear^[[0m
|
||||
=== create
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Create the folder?^[[0m
|
||||
|
||||
/home/user/bios/new does not exist.
|
||||
The nearest folder that does: /home/user
|
||||
|
||||
^[[0;7;35m > Create it and continue^[[0m
|
||||
Choose another folder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Create^[[0;2m B/Esc Quit^[[0m
|
||||
=== checking
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Checking^[[0m
|
||||
|
||||
RetroArch: ~/.config/retroarch/system
|
||||
|
||||
File list received.
|
||||
Reading the files already in the folder... 0:04
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mCtrl-C^[[0m Quit^[[0;2m B/Esc Quit^[[0m
|
||||
=== plan
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m
|
||||
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
|
||||
^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m
|
||||
The ^[[0;33m1 wrong copy^[[0m is replaced, the old one kept in a backup folder.
|
||||
Free space: 50.0 GB
|
||||
|
||||
^[[0;7;35m > Install^[[0m
|
||||
Choose systems
|
||||
Copy the BIOS files from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Install^[[0;2m B/Esc Back Y/o Options ? Help^[[0m
|
||||
=== plan-in-place
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
|
||||
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
|
||||
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
|
||||
Free space: 50.0 GB
|
||||
|
||||
^[[0;7;35m > Close^[[0m
|
||||
Options
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Close^[[0;2m B/Esc Back Y/o Options ? Help^[[0m
|
||||
=== systems
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Choose systems^[[0m
|
||||
|
||||
All
|
||||
None
|
||||
^[[0;2m Filter (/ to type)^[[0m
|
||||
^[[0;7;35m > [x] nintendo-gba (gba) 1 file, 16.0 KB^[[0m
|
||||
[x] sony-playstation 3 files, 1.0 MB
|
||||
^[[0;2m [ ] nintendo-famicom-disk (ファミコン) in place^[[0m
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;1m ^[[0;1;35m2^[[0;1m of ^[[0;1;35m3^[[0;1m systems: ^[[0;1;35m4^[[0;1m files, ^[[0;1;35m1.0 MB^[[0m
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Continue^[[0;2m B/Esc Back X/Space Tick ? Help^[[0m
|
||||
=== options
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Options^[[0m
|
||||
|
||||
Each row is an option of the command line, named on the right.
|
||||
|
||||
^[[0;7;35m > Preferred regions: North America, Japan --region^[[0m
|
||||
Game data: kept --no-game-data
|
||||
Installed cores only: no (1 found) --installed-cores
|
||||
Cores: all --core
|
||||
Parallel downloads: 8 --jobs
|
||||
BIOS folder: ^[[0;1m~/.config/retroarch/system --dest^[[0m
|
||||
Copy from: none --from
|
||||
Preview the file list --dry-run
|
||||
Show the command line
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Open^[[0;2m B/Esc Back Left/Right Set ? Help^[[0m
|
||||
=== regions
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preferred regions^[[0m
|
||||
|
||||
Tick the regions to keep, best first. Where a system has one BIOS per region,
|
||||
the best one found is installed.
|
||||
|
||||
^[[0;7;35m > [1] North America^[[0m
|
||||
[ ] Japan
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Tick^[[0;2m B/Esc Done Left/Right Order ? Help^[[0m
|
||||
=== cores
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Cores^[[0m
|
||||
|
||||
Tick the cores you use. With none ticked, every core's files are kept.
|
||||
|
||||
^[[0;7;35m > Filter (/ to type)^[[0m
|
||||
[ ] mesen
|
||||
[x] pcsx_rearmed
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Filter^[[0;2m B/Esc Back X/Space Tick ? Help^[[0m
|
||||
=== preview
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preview^[[0m
|
||||
|
||||
4 files to download, 1.0 MB. Nothing is written.
|
||||
|
||||
missing 512.0 KB scph5500.bin
|
||||
wrong copy 512.0 KB scph5501.bin
|
||||
missing 4 B pcsx/extra.bin
|
||||
missing 16.0 KB gba_bios.bin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Back
|
||||
=== command
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Command line^[[0m
|
||||
|
||||
The same choice without questions:
|
||||
|
||||
python3 install.py --platform retroarch --dest
|
||||
/home/user/.config/retroarch/system --system nintendo-gba
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Back
|
||||
=== confirm
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Confirm^[[0m
|
||||
|
||||
Counted again for your choices. Nothing is written before Install.
|
||||
^[[0;1m ^[[0;1;35m1^[[0;1m file to download, ^[[0;1;35m16.0 KB^[[0m
|
||||
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
|
||||
0 in place, ^[[0;33m1 missing^[[0m, 0 wrong copies
|
||||
Free space: 50.0 GB
|
||||
|
||||
^[[0;7;35m > Install^[[0m
|
||||
Back
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Install^[[0;2m B/Esc Back ? Help^[[0m
|
||||
=== installing
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Installing^[[0m
|
||||
|
||||
[^[[0;1;35m####################################^[[0;2m-----------------------------------^[[0m] 50%
|
||||
528.0 KB of 1.0 MB
|
||||
128.9 KB/s, 0:04 left
|
||||
2 of 4 files
|
||||
^[[0;2m B, Ctrl-C or Select+Start stops. What is done stays done.^[[0m
|
||||
|
||||
^[[0;1;31mFAILED^[[0m scph5500.bin (HTTP 404)
|
||||
^[[0;32m ok^[[0m gba_bios.bin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mB/Esc^[[0m Stop^[[0;2m ? Help^[[0m
|
||||
=== result
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mFinished with errors^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;32m3 installed^[[0;1m, ^[[0;1;31m1 failed^[[0;1m.^[[0m
|
||||
BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m
|
||||
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
To put them back: python3 install.py --restore
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
Same choice without questions:
|
||||
python3 install.py --platform retroarch --dest
|
||||
/home/user/.config/retroarch/system
|
||||
Details: /tmp/retrobios-x/install.log
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Exit
|
||||
=== error
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mSomething went wrong^[[0m
|
||||
|
||||
Could not fetch the file list for retroarch after 3 attempts: [Errno -2]
|
||||
Name or service not known
|
||||
Address: https://raw.githubusercontent.com/Abdess/retrobios/main/install/
|
||||
retroarch.json
|
||||
|
||||
^[[0;7;35m > Choose another setup or folder^[[0m
|
||||
Quit
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Choose^[[0;2m B/Esc Back^[[0m
|
||||
=== quit
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Quit?^[[0m
|
||||
|
||||
Quit without installing? Nothing was written.
|
||||
|
||||
^[[0;7;35m > Quit^[[0m
|
||||
Stay
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m ^[[0;1;35mA/Enter^[[0m Quit^[[0;2m B/Esc Stay^[[0m
|
||||
=== help
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Keys^[[0m
|
||||
|
||||
Up/Down, k/j Move
|
||||
A/Enter, Start Install
|
||||
Y, o, Tab Options
|
||||
B/Esc, Backspace Back
|
||||
q, Ctrl-C Quit
|
||||
?, F1 This help
|
||||
|
||||
BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;2m Any key closes this help^[[0m
|
||||
Vendored
+1
-1
@@ -9,4 +9,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Back
|
||||
A Back
|
||||
Vendored
+1
-1
@@ -12,4 +12,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Back
|
||||
A Back
|
||||
Vendored
+1
-1
@@ -9,4 +9,4 @@
|
||||
|
||||
> Install
|
||||
Back
|
||||
A/Enter Install B/Esc Back ? Help
|
||||
A Install B Back ? Help
|
||||
Vendored
+1
-1
@@ -12,4 +12,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Install B/Esc Back ? Help
|
||||
A Install B Back ? Help
|
||||
Vendored
+1
-1
@@ -9,4 +9,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Create B/Esc Quit
|
||||
A Create B Quit
|
||||
Vendored
+1
-1
@@ -12,4 +12,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Create B/Esc Quit
|
||||
A Create B Quit
|
||||
Vendored
+3
-3
@@ -5,8 +5,8 @@
|
||||
-2] Name or service not known
|
||||
Address:
|
||||
https://raw.githubusercontent.com/
|
||||
Abdess/retrobios/main/install/
|
||||
retroarch.json
|
||||
1-5 of 7
|
||||
|
||||
> Choose another setup or folder
|
||||
Quit
|
||||
A/Enter Choose B/Esc Back
|
||||
A Choose B Back PgUp/PgDn Scroll
|
||||
Vendored
+1
-1
@@ -12,4 +12,4 @@
|
||||
Quit
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Back
|
||||
A Choose B Back
|
||||
+1
-1
@@ -9,4 +9,4 @@
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
Look again
|
||||
A/Enter Browse B/Esc Back ? Help
|
||||
A Browse B Back ? Help
|
||||
+1
-1
@@ -12,4 +12,4 @@
|
||||
Look again
|
||||
|
||||
|
||||
A/Enter Browse B/Esc Back ? Help
|
||||
A Browse B Back ? Help
|
||||
+1
-1
@@ -9,4 +9,4 @@
|
||||
|
||||
FAILED scph5500.bin (HTTP 404)
|
||||
ok gba_bios.bin
|
||||
B/Esc Stop ? Help
|
||||
B Stop ? Help
|
||||
+1
-1
@@ -12,4 +12,4 @@
|
||||
|
||||
|
||||
|
||||
B/Esc Stop ? Help
|
||||
B Stop ? Help
|
||||
+8
-8
@@ -2,14 +2,14 @@
|
||||
|
||||
Each row is an option of the command line, named on the right.
|
||||
|
||||
> Preferred regions: North America, Japan --region
|
||||
Game data: kept --no-game-data
|
||||
Installed cores only: no (1 found) --installed-cores
|
||||
Cores: all --core
|
||||
Parallel downloads: 8 --jobs
|
||||
BIOS folder: ~/.config/retroarch/system --dest
|
||||
Copy from: none --from
|
||||
Preview the file list --dry-run
|
||||
> Preferred regions: North America, Japan --region
|
||||
Game data: kept --no-game-data
|
||||
Installed cores only: no (1 found) --installed-cores
|
||||
Cores: all --core
|
||||
Parallel downloads: 8 --jobs
|
||||
BIOS folder: ~/.config/retroarch/system --dest
|
||||
Copy from: none --from
|
||||
Preview the file list --dry-run
|
||||
Show the command line
|
||||
|
||||
|
||||
|
||||
+13
-13
@@ -14,26 +14,26 @@
|
||||
BizHawk
|
||||
MiSTer FPGA
|
||||
RetroPie
|
||||
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
|
||||
AmberELEC, SD card RG351, RG552
|
||||
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
|
||||
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
|
||||
AmberELEC, SD card RG351, RG552
|
||||
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
|
||||
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, Odroid Go Super, RG351V, +6
|
||||
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, R35S, RGB10
|
||||
CrossMix-OS, SD card TrimUI Smart Pro
|
||||
dArkOS, SD card Odroid Go Advance, RGB10, RG351MP, G350
|
||||
EmuELEC, SD card Odroid Go
|
||||
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, R35S, RGB10
|
||||
CrossMix-OS, SD card TrimUI Smart Pro
|
||||
dArkOS, SD card Odroid Go Advance, RGB10, RG351MP, G350
|
||||
EmuELEC, SD card Odroid Go
|
||||
JELOS, SD card RG351M, RG351V, RG353M, RG353P, RG353V, RG353VS, RG503, RG552, R33S, R36S, +24
|
||||
KNULLI, SD card RG ARC-S, RG28XX, RG34XX, RG35XX, RG35XX Plus, RG35XX H, RG35XX 2024, +15
|
||||
KOLOS, SD card RG353P, RG353M, RG353V, RG353VS, RG503, RK2023, RGB30, RGB10 Max 3
|
||||
Koriki, SD card Miyoo Mini
|
||||
KOLOS, SD card RG353P, RG353M, RG353V, RG353VS, RG503, RK2023, RGB30, RGB10 Max 3
|
||||
Koriki, SD card Miyoo Mini
|
||||
MinUI, SD card RG28XX, RG34XX, RG34XX SP, RG35XX, RG35XX Plus, RG35XX H, RG35XX SP, +16
|
||||
MiyooCFW, SD card BittBoy, Pocket Go, PowKiddy Q90, PowKiddy V90, PowKiddy Q20 Mini, Sup M3, +1
|
||||
muOS, SD card RG35XX H, RG35XX Plus, RG35XX SP, RG35XX 2024, RG34XX, RG40XX H, RG40XX V, +4
|
||||
MyMinUI, SD card RG35XX, Miyoo Mini, Miyoo Mini Flip, Miyoo Mini Plus, Miyoo A30, +16
|
||||
NextUI, SD card TrimUI Brick, TrimUI Smart Pro
|
||||
Onion, SD card Miyoo Mini, Miyoo Mini Plus
|
||||
plumOS, SD card MagicX XU20 V32
|
||||
RetroOZ, SD card Odroid Go Super, Odroid Go Advance, RGB10 Max, RGB10 Max 2
|
||||
NextUI, SD card TrimUI Brick, TrimUI Smart Pro
|
||||
Onion, SD card Miyoo Mini, Miyoo Mini Plus
|
||||
plumOS, SD card MagicX XU20 V32
|
||||
RetroOZ, SD card Odroid Go Super, Odroid Go Advance, RGB10 Max, RGB10 Max 2
|
||||
ROCKNIX, SD card RG351, RG353, RG503, RG552, RG 55G1, RG ARC, RG35XX 2024, RG35XX Plus, +37
|
||||
spruceOS, SD card Miyoo A30, Miyoo Flip, Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip, +5
|
||||
UnofficialOS, SD card R35S, R36S, RG351M, RG351P, RG351V, RG353M, RG353P, RG353V, RG503, +5
|
||||
|
||||
Vendored
+1
-1
@@ -9,4 +9,4 @@
|
||||
missing 16.0 KB gba_bios.bin
|
||||
|
||||
|
||||
A/Enter Back
|
||||
A Back
|
||||
Vendored
+1
-1
@@ -12,4 +12,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Back
|
||||
A Back
|
||||
Vendored
+1
-1
@@ -9,4 +9,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Quit B/Esc Stay
|
||||
A Quit B Stay
|
||||
Vendored
+1
-1
@@ -12,4 +12,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Quit B/Esc Stay
|
||||
A Quit B Stay
|
||||
Vendored
+3
-3
@@ -5,8 +5,8 @@
|
||||
/home/user/.config/retroarch/system
|
||||
failed: scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
To put them back: python3 install.py
|
||||
1-8 of 17
|
||||
A/Enter Exit Up/Down Scroll
|
||||
A Exit ^v Scroll
|
||||
Vendored
+5
-5
@@ -5,11 +5,11 @@
|
||||
/home/user/.config/retroarch/system
|
||||
failed: scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
To put them back: python3 install.py
|
||||
--restore
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
1-11 of 17
|
||||
A/Enter Exit Up/Down Scroll
|
||||
A Exit ^v Scroll
|
||||
Vendored
+10
-142
@@ -322,35 +322,6 @@
|
||||
|
||||
|
||||
A Use this folder B Back
|
||||
=== create 40x12
|
||||
RetroBIOS: Create the folder?
|
||||
|
||||
/home/user/bios/new does not exist.
|
||||
The nearest folder that does:
|
||||
/home/user
|
||||
|
||||
> Create it and continue
|
||||
Choose another folder
|
||||
|
||||
|
||||
|
||||
A Create B Quit
|
||||
=== create 40x15
|
||||
RetroBIOS: Create the folder?
|
||||
|
||||
/home/user/bios/new does not exist.
|
||||
The nearest folder that does:
|
||||
/home/user
|
||||
|
||||
> Create it and continue
|
||||
Choose another folder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Create B Quit
|
||||
=== create 80x24
|
||||
RetroBIOS: Create the folder?
|
||||
|
||||
@@ -604,7 +575,7 @@
|
||||
Copy from: none
|
||||
Preview the file list
|
||||
Show the command line
|
||||
A Open B Back Left/Right Set
|
||||
A Open B Back <> Set
|
||||
=== options 40x15
|
||||
RetroBIOS: Options
|
||||
|
||||
@@ -620,7 +591,7 @@
|
||||
|
||||
Sets --region
|
||||
|
||||
A Open B Back Left/Right Set
|
||||
A Open B Back <> Set
|
||||
=== options 80x24
|
||||
RetroBIOS: Options
|
||||
|
||||
@@ -658,7 +629,7 @@
|
||||
[ ] Japan
|
||||
|
||||
|
||||
A Tick B Done Left/Right Order
|
||||
A Tick B Done <> Order
|
||||
=== regions 40x15
|
||||
RetroBIOS: Preferred regions
|
||||
|
||||
@@ -674,7 +645,7 @@
|
||||
|
||||
|
||||
|
||||
A Tick B Done Left/Right Order
|
||||
A Tick B Done <> Order
|
||||
=== regions 80x24
|
||||
RetroBIOS: Preferred regions
|
||||
|
||||
@@ -754,35 +725,6 @@
|
||||
|
||||
|
||||
A Filter B Back X Tick
|
||||
=== preview 40x12
|
||||
RetroBIOS: Preview
|
||||
|
||||
4 files to download, 1.0 MB. Nothing
|
||||
is written.
|
||||
|
||||
missing 512.0 KB scph5500.bin
|
||||
wrong copy 512.0 KB scph5501.bin
|
||||
missing 4 B pcsx/extra.bin
|
||||
missing 16.0 KB gba_bios.bin
|
||||
|
||||
|
||||
A Back
|
||||
=== preview 40x15
|
||||
RetroBIOS: Preview
|
||||
|
||||
4 files to download, 1.0 MB. Nothing
|
||||
is written.
|
||||
|
||||
missing 512.0 KB scph5500.bin
|
||||
wrong copy 512.0 KB scph5501.bin
|
||||
missing 4 B pcsx/extra.bin
|
||||
missing 16.0 KB gba_bios.bin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
=== preview 80x24
|
||||
RetroBIOS: Preview
|
||||
|
||||
@@ -807,35 +749,6 @@
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
=== command 40x12
|
||||
RetroBIOS: Command line
|
||||
|
||||
The same choice without questions:
|
||||
|
||||
python3 install.py --platform
|
||||
retroarch --dest
|
||||
/home/user/.config/retroarch/system
|
||||
--system nintendo-gba
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
=== command 40x15
|
||||
RetroBIOS: Command line
|
||||
|
||||
The same choice without questions:
|
||||
|
||||
python3 install.py --platform
|
||||
retroarch --dest
|
||||
/home/user/.config/retroarch/system
|
||||
--system nintendo-gba
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
=== command 80x24
|
||||
RetroBIOS: Command line
|
||||
@@ -978,8 +891,8 @@
|
||||
/home/user/.config/retroarch/system
|
||||
failed: scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
Details: /tmp/retrobios-x/install.log
|
||||
|
||||
A Exit
|
||||
@@ -991,8 +904,8 @@
|
||||
/home/user/.config/retroarch/system
|
||||
failed: scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
Details: /tmp/retrobios-x/install.log
|
||||
|
||||
|
||||
@@ -1032,27 +945,11 @@
|
||||
-2] Name or service not known
|
||||
Address:
|
||||
https://raw.githubusercontent.com/
|
||||
Abdess/retrobios/main/install/
|
||||
retroarch.json
|
||||
> Choose another setup or folder
|
||||
Quit
|
||||
A Choose B Back
|
||||
=== error 40x15
|
||||
RetroBIOS: Something went wrong
|
||||
|
||||
Could not fetch the file list for
|
||||
retroarch after 3 attempts: [Errno
|
||||
-2] Name or service not known
|
||||
Address:
|
||||
https://raw.githubusercontent.com/
|
||||
Abdess/retrobios/main/install/
|
||||
retroarch.json
|
||||
1-5 of 7
|
||||
|
||||
> Choose another setup or folder
|
||||
Quit
|
||||
|
||||
|
||||
A Choose B Back
|
||||
A Choose B Back L1/R1 Scroll
|
||||
=== error 80x24
|
||||
RetroBIOS: Something went wrong
|
||||
|
||||
@@ -1078,35 +975,6 @@
|
||||
|
||||
|
||||
A Choose B Back
|
||||
=== quit 40x12
|
||||
RetroBIOS: Quit?
|
||||
|
||||
Quit without installing? Nothing was
|
||||
written.
|
||||
|
||||
> Quit
|
||||
Stay
|
||||
|
||||
|
||||
|
||||
|
||||
A Quit B Stay
|
||||
=== quit 40x15
|
||||
RetroBIOS: Quit?
|
||||
|
||||
Quit without installing? Nothing was
|
||||
written.
|
||||
|
||||
> Quit
|
||||
Stay
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Quit B Stay
|
||||
=== quit 80x24
|
||||
RetroBIOS: Quit?
|
||||
|
||||
|
||||
+10
-142
@@ -322,35 +322,6 @@
|
||||
|
||||
|
||||
A Use this folder B Back
|
||||
=== create 40x12
|
||||
RetroBIOS: Create the folder?
|
||||
|
||||
/home/user/bios/new does not exist.
|
||||
The nearest folder that does:
|
||||
/home/user
|
||||
|
||||
> Create it and continue
|
||||
Choose another folder
|
||||
|
||||
|
||||
|
||||
A Create B Quit
|
||||
=== create 40x15
|
||||
RetroBIOS: Create the folder?
|
||||
|
||||
/home/user/bios/new does not exist.
|
||||
The nearest folder that does:
|
||||
/home/user
|
||||
|
||||
> Create it and continue
|
||||
Choose another folder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Create B Quit
|
||||
=== create 80x24
|
||||
RetroBIOS: Create the folder?
|
||||
|
||||
@@ -604,7 +575,7 @@
|
||||
Copy from: none
|
||||
Preview the file list
|
||||
Show the command line
|
||||
A Open B Back Left/Right Set
|
||||
A Open B Back <> Set
|
||||
=== options 40x15
|
||||
RetroBIOS: Options
|
||||
|
||||
@@ -620,7 +591,7 @@
|
||||
|
||||
Sets --region
|
||||
|
||||
A Open B Back Left/Right Set
|
||||
A Open B Back <> Set
|
||||
=== options 80x24
|
||||
RetroBIOS: Options
|
||||
|
||||
@@ -658,7 +629,7 @@
|
||||
[ ] Japan
|
||||
|
||||
|
||||
A Tick B Done Left/Right Order
|
||||
A Tick B Done <> Order
|
||||
=== regions 40x15
|
||||
RetroBIOS: Preferred regions
|
||||
|
||||
@@ -674,7 +645,7 @@
|
||||
|
||||
|
||||
|
||||
A Tick B Done Left/Right Order
|
||||
A Tick B Done <> Order
|
||||
=== regions 80x24
|
||||
RetroBIOS: Preferred regions
|
||||
|
||||
@@ -754,35 +725,6 @@
|
||||
|
||||
|
||||
A Filter B Back X Tick
|
||||
=== preview 40x12
|
||||
RetroBIOS: Preview
|
||||
|
||||
4 files to download, 1.0 MB. Nothing
|
||||
is written.
|
||||
|
||||
missing 512.0 KB scph5500.bin
|
||||
wrong copy 512.0 KB scph5501.bin
|
||||
missing 4 B pcsx/extra.bin
|
||||
missing 16.0 KB gba_bios.bin
|
||||
|
||||
|
||||
A Back
|
||||
=== preview 40x15
|
||||
RetroBIOS: Preview
|
||||
|
||||
4 files to download, 1.0 MB. Nothing
|
||||
is written.
|
||||
|
||||
missing 512.0 KB scph5500.bin
|
||||
wrong copy 512.0 KB scph5501.bin
|
||||
missing 4 B pcsx/extra.bin
|
||||
missing 16.0 KB gba_bios.bin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
=== preview 80x24
|
||||
RetroBIOS: Preview
|
||||
|
||||
@@ -807,35 +749,6 @@
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
=== command 40x12
|
||||
RetroBIOS: Command line
|
||||
|
||||
The same choice without questions:
|
||||
|
||||
python3 install.py --platform
|
||||
retroarch --dest
|
||||
/home/user/.config/retroarch/system
|
||||
--system nintendo-gba
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
=== command 40x15
|
||||
RetroBIOS: Command line
|
||||
|
||||
The same choice without questions:
|
||||
|
||||
python3 install.py --platform
|
||||
retroarch --dest
|
||||
/home/user/.config/retroarch/system
|
||||
--system nintendo-gba
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
=== command 80x24
|
||||
RetroBIOS: Command line
|
||||
@@ -978,8 +891,8 @@
|
||||
/home/user/.config/retroarch/system
|
||||
failed: scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
Details: /tmp/retrobios-x/install.log
|
||||
|
||||
A Exit
|
||||
@@ -991,8 +904,8 @@
|
||||
/home/user/.config/retroarch/system
|
||||
failed: scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
Details: /tmp/retrobios-x/install.log
|
||||
|
||||
|
||||
@@ -1032,27 +945,11 @@
|
||||
-2] Name or service not known
|
||||
Address:
|
||||
https://raw.githubusercontent.com/
|
||||
Abdess/retrobios/main/install/
|
||||
retroarch.json
|
||||
> Choose another setup or folder
|
||||
Quit
|
||||
A Choose B Back
|
||||
=== error 40x15
|
||||
RetroBIOS: Something went wrong
|
||||
|
||||
Could not fetch the file list for
|
||||
retroarch after 3 attempts: [Errno
|
||||
-2] Name or service not known
|
||||
Address:
|
||||
https://raw.githubusercontent.com/
|
||||
Abdess/retrobios/main/install/
|
||||
retroarch.json
|
||||
1-5 of 7
|
||||
|
||||
> Choose another setup or folder
|
||||
Quit
|
||||
|
||||
|
||||
A Choose B Back
|
||||
A Choose B Back L1/R1 Scroll
|
||||
=== error 80x24
|
||||
RetroBIOS: Something went wrong
|
||||
|
||||
@@ -1078,35 +975,6 @@
|
||||
|
||||
|
||||
A Choose B Back
|
||||
=== quit 40x12
|
||||
RetroBIOS: Quit?
|
||||
|
||||
Quit without installing? Nothing was
|
||||
written.
|
||||
|
||||
> Quit
|
||||
Stay
|
||||
|
||||
|
||||
|
||||
|
||||
A Quit B Stay
|
||||
=== quit 40x15
|
||||
RetroBIOS: Quit?
|
||||
|
||||
Quit without installing? Nothing was
|
||||
written.
|
||||
|
||||
> Quit
|
||||
Stay
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Quit B Stay
|
||||
=== quit 80x24
|
||||
RetroBIOS: Quit?
|
||||
|
||||
|
||||
+9
-141
@@ -322,35 +322,6 @@
|
||||
|
||||
|
||||
A Use this folder B Back
|
||||
=== create 40x12
|
||||
RetroBIOS: Create the folder?
|
||||
|
||||
/home/user/bios/new does not exist.
|
||||
The nearest folder that does:
|
||||
/home/user
|
||||
|
||||
> Create it and continue
|
||||
Choose another folder
|
||||
|
||||
|
||||
|
||||
A Create B Quit
|
||||
=== create 40x15
|
||||
RetroBIOS: Create the folder?
|
||||
|
||||
/home/user/bios/new does not exist.
|
||||
The nearest folder that does:
|
||||
/home/user
|
||||
|
||||
> Create it and continue
|
||||
Choose another folder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Create B Quit
|
||||
=== create 80x24
|
||||
RetroBIOS: Create the folder?
|
||||
|
||||
@@ -604,7 +575,7 @@
|
||||
Copy from: none
|
||||
Preview the file list
|
||||
Show the command line
|
||||
A Open B Back Left/Right Set
|
||||
A Open B Back <> Set
|
||||
=== options 40x15
|
||||
RetroBIOS: Options
|
||||
|
||||
@@ -620,7 +591,7 @@
|
||||
|
||||
Sets --region
|
||||
|
||||
A Open B Back Left/Right Set
|
||||
A Open B Back <> Set
|
||||
=== options 80x24
|
||||
RetroBIOS: Options
|
||||
|
||||
@@ -658,7 +629,7 @@
|
||||
[ ] Japan
|
||||
|
||||
|
||||
A Tick B Done Left/Right Order
|
||||
A Tick B Done <> Order
|
||||
=== regions 40x15
|
||||
RetroBIOS: Preferred regions
|
||||
|
||||
@@ -674,7 +645,7 @@
|
||||
|
||||
|
||||
|
||||
A Tick B Done Left/Right Order
|
||||
A Tick B Done <> Order
|
||||
=== regions 80x24
|
||||
RetroBIOS: Preferred regions
|
||||
|
||||
@@ -754,35 +725,6 @@
|
||||
|
||||
|
||||
A Filter B Back X Tick
|
||||
=== preview 40x12
|
||||
RetroBIOS: Preview
|
||||
|
||||
4 files to download, 1.0 MB. Nothing
|
||||
is written.
|
||||
|
||||
missing 512.0 KB scph5500.bin
|
||||
wrong copy 512.0 KB scph5501.bin
|
||||
missing 4 B pcsx/extra.bin
|
||||
missing 16.0 KB gba_bios.bin
|
||||
|
||||
|
||||
A Back
|
||||
=== preview 40x15
|
||||
RetroBIOS: Preview
|
||||
|
||||
4 files to download, 1.0 MB. Nothing
|
||||
is written.
|
||||
|
||||
missing 512.0 KB scph5500.bin
|
||||
wrong copy 512.0 KB scph5501.bin
|
||||
missing 4 B pcsx/extra.bin
|
||||
missing 16.0 KB gba_bios.bin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
=== preview 80x24
|
||||
RetroBIOS: Preview
|
||||
|
||||
@@ -807,35 +749,6 @@
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
=== command 40x12
|
||||
RetroBIOS: Command line
|
||||
|
||||
The same choice without questions:
|
||||
|
||||
python3 install.py --platform
|
||||
retroarch --dest
|
||||
/home/user/.config/retroarch/system
|
||||
--system nintendo-gba
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
=== command 40x15
|
||||
RetroBIOS: Command line
|
||||
|
||||
The same choice without questions:
|
||||
|
||||
python3 install.py --platform
|
||||
retroarch --dest
|
||||
/home/user/.config/retroarch/system
|
||||
--system nintendo-gba
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
=== command 80x24
|
||||
RetroBIOS: Command line
|
||||
@@ -978,8 +891,8 @@
|
||||
/home/user/.config/retroarch/system
|
||||
failed: scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
Details: /tmp/retrobios-x/install.log
|
||||
|
||||
A Exit
|
||||
@@ -991,8 +904,8 @@
|
||||
/home/user/.config/retroarch/system
|
||||
failed: scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
Details: /tmp/retrobios-x/install.log
|
||||
|
||||
|
||||
@@ -1032,26 +945,10 @@
|
||||
-2] Name or service not known
|
||||
Address:
|
||||
https://raw.githubusercontent.com/
|
||||
Abdess/retrobios/main/install/
|
||||
retroarch.json
|
||||
> Choose another setup or folder
|
||||
Quit
|
||||
A Choose B Back
|
||||
=== error 40x15
|
||||
RetroBIOS: Something went wrong
|
||||
|
||||
Could not fetch the file list for
|
||||
retroarch after 3 attempts: [Errno
|
||||
-2] Name or service not known
|
||||
Address:
|
||||
https://raw.githubusercontent.com/
|
||||
Abdess/retrobios/main/install/
|
||||
retroarch.json
|
||||
1-5 of 7
|
||||
|
||||
> Choose another setup or folder
|
||||
Quit
|
||||
|
||||
|
||||
A Choose B Back
|
||||
=== error 80x24
|
||||
RetroBIOS: Something went wrong
|
||||
@@ -1078,35 +975,6 @@
|
||||
|
||||
|
||||
A Choose B Back
|
||||
=== quit 40x12
|
||||
RetroBIOS: Quit?
|
||||
|
||||
Quit without installing? Nothing was
|
||||
written.
|
||||
|
||||
> Quit
|
||||
Stay
|
||||
|
||||
|
||||
|
||||
|
||||
A Quit B Stay
|
||||
=== quit 40x15
|
||||
RetroBIOS: Quit?
|
||||
|
||||
Quit without installing? Nothing was
|
||||
written.
|
||||
|
||||
> Quit
|
||||
Stay
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Quit B Stay
|
||||
=== quit 80x24
|
||||
RetroBIOS: Quit?
|
||||
|
||||
|
||||
Vendored
+10
-10
@@ -924,11 +924,11 @@
|
||||
/home/user/.config/retroarch/system
|
||||
failed: scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
To put them back: python3 install.py
|
||||
1-8 of 17
|
||||
Enter Exit Up/Down Scroll
|
||||
Enter Exit ^v Scroll
|
||||
=== result 40x15
|
||||
RetroBIOS: Finished with errors
|
||||
|
||||
@@ -937,14 +937,14 @@
|
||||
/home/user/.config/retroarch/system
|
||||
failed: scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
To put them back: python3 install.py
|
||||
--restore
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
1-11 of 17
|
||||
Enter Exit Up/Down Scroll
|
||||
Enter Exit ^v Scroll
|
||||
=== result 80x24
|
||||
RetroBIOS: Finished with errors
|
||||
|
||||
@@ -978,8 +978,8 @@
|
||||
-2] Name or service not known
|
||||
Address:
|
||||
https://raw.githubusercontent.com/
|
||||
Abdess/retrobios/main/install/
|
||||
retroarch.json
|
||||
1-5 of 7
|
||||
|
||||
> Choose another setup or folder
|
||||
Quit
|
||||
Enter Choose Esc Back
|
||||
|
||||
Vendored
+11
-69
@@ -604,7 +604,7 @@
|
||||
Copy from: none
|
||||
Preview the file list
|
||||
Show the command line
|
||||
A Open R2 Back Left/Right Set
|
||||
A Open R2 Back <> Set
|
||||
=== options 40x15
|
||||
RetroBIOS: Options
|
||||
|
||||
@@ -620,7 +620,7 @@
|
||||
|
||||
Sets --region
|
||||
|
||||
A Open R2 Back Left/Right Set
|
||||
A Open R2 Back <> Set
|
||||
=== options 80x24
|
||||
RetroBIOS: Options
|
||||
|
||||
@@ -658,7 +658,7 @@
|
||||
[ ] Japan
|
||||
|
||||
|
||||
A Tick R2 Done Left/Right Order
|
||||
A Tick R2 Done <> Order
|
||||
=== regions 40x15
|
||||
RetroBIOS: Preferred regions
|
||||
|
||||
@@ -674,7 +674,7 @@
|
||||
|
||||
|
||||
|
||||
A Tick R2 Done Left/Right Order
|
||||
A Tick R2 Done <> Order
|
||||
=== regions 80x24
|
||||
RetroBIOS: Preferred regions
|
||||
|
||||
@@ -754,35 +754,6 @@
|
||||
|
||||
|
||||
A Filter R2 Back
|
||||
=== preview 40x12
|
||||
RetroBIOS: Preview
|
||||
|
||||
4 files to download, 1.0 MB. Nothing
|
||||
is written.
|
||||
|
||||
missing 512.0 KB scph5500.bin
|
||||
wrong copy 512.0 KB scph5501.bin
|
||||
missing 4 B pcsx/extra.bin
|
||||
missing 16.0 KB gba_bios.bin
|
||||
|
||||
|
||||
A Back
|
||||
=== preview 40x15
|
||||
RetroBIOS: Preview
|
||||
|
||||
4 files to download, 1.0 MB. Nothing
|
||||
is written.
|
||||
|
||||
missing 512.0 KB scph5500.bin
|
||||
wrong copy 512.0 KB scph5501.bin
|
||||
missing 4 B pcsx/extra.bin
|
||||
missing 16.0 KB gba_bios.bin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
=== preview 80x24
|
||||
RetroBIOS: Preview
|
||||
|
||||
@@ -807,35 +778,6 @@
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
=== command 40x12
|
||||
RetroBIOS: Command line
|
||||
|
||||
The same choice without questions:
|
||||
|
||||
python3 install.py --platform
|
||||
retroarch --dest
|
||||
/home/user/.config/retroarch/system
|
||||
--system nintendo-gba
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
=== command 40x15
|
||||
RetroBIOS: Command line
|
||||
|
||||
The same choice without questions:
|
||||
|
||||
python3 install.py --platform
|
||||
retroarch --dest
|
||||
/home/user/.config/retroarch/system
|
||||
--system nintendo-gba
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
=== command 80x24
|
||||
RetroBIOS: Command line
|
||||
@@ -978,8 +920,8 @@
|
||||
/home/user/.config/retroarch/system
|
||||
failed: scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
Details: /tmp/retrobios-x/install.log
|
||||
|
||||
A Exit
|
||||
@@ -991,8 +933,8 @@
|
||||
/home/user/.config/retroarch/system
|
||||
failed: scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
Details: /tmp/retrobios-x/install.log
|
||||
|
||||
|
||||
@@ -1032,11 +974,11 @@
|
||||
-2] Name or service not known
|
||||
Address:
|
||||
https://raw.githubusercontent.com/
|
||||
Abdess/retrobios/main/install/
|
||||
retroarch.json
|
||||
1-5 of 7
|
||||
|
||||
> Choose another setup or folder
|
||||
Quit
|
||||
A Choose R2 Back
|
||||
A Choose R2 Back L1/R1 Scroll
|
||||
=== error 40x15
|
||||
RetroBIOS: Something went wrong
|
||||
|
||||
|
||||
Vendored
+10
-10
@@ -604,7 +604,7 @@
|
||||
Copy from: none
|
||||
Preview the file list
|
||||
Show the command line
|
||||
Start Open B Back Left/Right Set
|
||||
Start Open B Back <> Set
|
||||
=== options 40x15
|
||||
RetroBIOS: Options
|
||||
|
||||
@@ -620,7 +620,7 @@
|
||||
|
||||
Sets --region
|
||||
|
||||
Start Open B Back Left/Right Set
|
||||
Start Open B Back <> Set
|
||||
=== options 80x24
|
||||
RetroBIOS: Options
|
||||
|
||||
@@ -658,7 +658,7 @@
|
||||
[ ] Japan
|
||||
|
||||
|
||||
Start Tick B Done Left/Right Order
|
||||
Start Tick B Done <> Order
|
||||
=== regions 40x15
|
||||
RetroBIOS: Preferred regions
|
||||
|
||||
@@ -674,7 +674,7 @@
|
||||
|
||||
|
||||
|
||||
Start Tick B Done Left/Right Order
|
||||
Start Tick B Done <> Order
|
||||
=== regions 80x24
|
||||
RetroBIOS: Preferred regions
|
||||
|
||||
@@ -978,8 +978,8 @@
|
||||
/home/user/.config/retroarch/system
|
||||
failed: scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
Details: /tmp/retrobios-x/install.log
|
||||
|
||||
Start Exit
|
||||
@@ -991,8 +991,8 @@
|
||||
/home/user/.config/retroarch/system
|
||||
failed: scph5500.bin (HTTP 404)
|
||||
The files it replaced are kept in
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
|
||||
|
||||
Details: /tmp/retrobios-x/install.log
|
||||
|
||||
|
||||
@@ -1032,8 +1032,8 @@
|
||||
-2] Name or service not known
|
||||
Address:
|
||||
https://raw.githubusercontent.com/
|
||||
Abdess/retrobios/main/install/
|
||||
retroarch.json
|
||||
1-5 of 7
|
||||
|
||||
> Choose another setup or folder
|
||||
Quit
|
||||
Start Choose B Back
|
||||
|
||||
Vendored
+1
-1
@@ -9,4 +9,4 @@
|
||||
BIOS folder:
|
||||
/home/user/.config/retroarch/system
|
||||
ES-DE found: its BIOS files go to each
|
||||
A/Enter Continue B/Esc Quit ? Help
|
||||
A Continue B Quit ? Help
|
||||
Vendored
+1
-1
@@ -12,4 +12,4 @@
|
||||
emulator, not to ES-DE itself.
|
||||
|
||||
|
||||
A/Enter Continue B/Esc Quit ? Help
|
||||
A Continue B Quit ? Help
|
||||
+3
-3
@@ -3,9 +3,9 @@
|
||||
All
|
||||
None
|
||||
Filter (/ to type)
|
||||
> [x] nintendo-gba (gba) 1 file, 16.0 KB
|
||||
[x] sony-playstation 3 files, 1.0 MB
|
||||
[ ] nintendo-famicom-disk (ファミコン) in place
|
||||
> [x] nintendo-gba (gba) 1 file, 16.0 KB
|
||||
[x] sony-playstation 3 files, 1.0 MB
|
||||
[ ] nintendo-famicom-disk (ファミコン) in place
|
||||
|
||||
|
||||
|
||||
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"api": {
|
||||
"https://api.github.com/repos/libretro/dolphin/compare/f84df02055ab9610feec48e65648cac5a3c098fa...3aac3f052f733f8df2f1616843a4d2e14002ed11": {
|
||||
"ahead_by": 437,
|
||||
"behind_by": 0,
|
||||
"status": "ahead"
|
||||
},
|
||||
"https://api.github.com/repos/libretro/dolphin/git/ref/tags/2609": {
|
||||
"object": {
|
||||
"sha": "b134d026ab4e71df13b3657ea6787d499962664b",
|
||||
"type": "tag"
|
||||
}
|
||||
},
|
||||
"https://api.github.com/repos/libretro/dolphin/git/tags/b134d026ab4e71df13b3657ea6787d499962664b": {
|
||||
"object": {
|
||||
"sha": "f84df02055ab9610feec48e65648cac5a3c098fa",
|
||||
"type": "commit"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"github.com/libretro/FBNeo/32958a89abd645ed20387c8d31d21ede3bc48161/src/burn/version.h": "// Version number, written as vV.V.BB or vV.V.BBaa\n// (0xVVBBaa, in BCD notation)\n\n#define VER_MAJOR 1\n#define VER_MINOR 0\n#define VER_BETA 0\n#define VER_ALPHA 3\n\n#define BURN_VERSION (VER_MAJOR * 0x100000) + (VER_MINOR * 0x010000) + (((VER_BETA / 10) * 0x001000) + ((VER_BETA % 10) * 0x000100)) + (((VER_ALPHA / 10) * 0x000010) + (VER_ALPHA % 10))\n\n\n\n\n",
|
||||
"github.com/libretro/snes9x/fae2fea08f74180759ef540ee94259213f503480/snes9x.h": "/*****************************************************************************\\\n Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.\n This file is licensed under the Snes9x License.\n For further information, consult the LICENSE file in the root directory.\n\\*****************************************************************************/\n\n#ifndef _SNES9X_H_\n#define _SNES9X_H_\n\n#ifndef VERSION\n#define VERSION\t\"1.63\"\n#endif\n\n#include \"port.h\"\n#include \"65c816.h\"\n#include \"messages.h\"\n\n/* All core file I/O goes through the libretro VFS via the\n file_stream_transforms wrappers (RFILE). The frontend supplies the\n VFS interface when available (see retro_set_environment);\n filestream falls back to its local implementation otherwise.\n Compressed (.gz) stream support from the old ZLIB build is gone --\n under libretro the frontend handles compressed content itself. */\n#include <streams/file_stream_transforms.h>\n#define FSTREAM\t\t\t\t\tRFILE *\n#define READ_FSTREAM(p, l, s)\trfread(p, 1, l, s)\n#define WRITE_FSTREAM(p, l, s)\trfwrite(p, 1, l, s)\n#define GETS_FSTREAM(p, l, s)\trfgets(p, l, s)\n#define GETC_FSTREAM(s)\t\t\trfgetc(s)\n#define OPEN_FSTREAM(f, m)\t\trfopen(f, m)\n#define FIND_FSTREAM(s)\t\t\trftell(s)\n#define REVERT_FSTREAM(s, o, p)\trfseek(s, o, p)\n#define CLOSE_FSTREAM(s)\t\t\trfclose(s)\n\n\n#define SNES_WIDTH\t\t\t\t\t256\n#define SNES_HEIGHT\t\t\t\t\t224\n#define SNES_HEIGHT_EXTENDED\t\t239\n#define MAX_SNES_WIDTH\t\t\t\t(SNES_WIDTH * 2)\n#define MAX_SNES_HEIGHT\t\t\t\t(SNES_HEIGHT_EXTENDED * 2)\n\n#define\tNTSC_MASTER_CLOCK\t\t\t21477272.727272 // 21477272 + 8/11 exact\n#define\tPAL_MASTER_CLOCK\t\t\t21281370.0\n#define NTSC_PROGRESSIVE_FRAME_RATE\t60.09881389744051\n#define NTSC_INTERLACED_FRAME_RATE\t59.94005994\n#define PAL_PROGRESSIVE_FRAME_RATE\t50.006977968\n\n\n#define SNES_MAX_NTSC_VCOUNTER\t\t262\n#define SNES_MAX_PAL_VCOUNTER\t\t312\n#define SNES_HCOUNTER_MAX\t\t\t341\n\n#ifndef ALLOW_CPU_OVERCLOCK\n#define ONE_CYCLE\t\t\t\t\t6\n#define SLOW_ONE_CYCLE\t\t\t\t8\n#define TWO_CYCLES\t\t\t\t\t12\n#else\n#define ONE_CYCLE (Settings.OneClockCycle)\n#define SLOW_ONE_CYCLE (Settings.OneSlowClockCycle)\n#define TWO_CYCLES (Settings.TwoClockCycles)\n#endif\n#define\tONE_DOT_CYCLE\t\t\t\t4\n\n#define SNES_CYCLES_PER_SCANLINE\t(SNES_HCOUNTER_MAX * ONE_DOT_CYCLE)\n#define SNES_SCANLINE_TIME\t\t\t(SNES_CYCLES_PER_SCANLINE / NTSC_MASTER_CLOCK)\n\n#define SNES_WRAM_REFRESH_HC_v1\t\t530\n#define SNES_WRAM_REFRESH_HC_v2\t\t538\n#define SNES_WRAM_REFRESH_CYCLES\t40\n\n#define SNES_HBLANK_START_HC\t\t1096\t\t\t\t\t// H=274\n#define\tSNES_HDMA_START_HC\t\t\t1106\t\t\t\t\t// FIXME: not true\n#define\tSNES_HBLANK_END_HC\t\t\t4\t\t\t\t\t\t// H=1\n#define\tSNES_HDMA_INIT_HC\t\t\t20\t\t\t\t\t\t// FIXME: not true\n#define\tSNES_RENDER_START_HC\t\t(128 * ONE_DOT_CYCLE)\t// FIXME: Snes9x renders a line at a time.\n\n#define SNES_TR_MASK\t\t(1 << 4)\n#define SNES_TL_MASK\t\t(1 << 5)\n#define SNES_X_MASK\t\t\t(1 << 6)\n#define SNES_A_MASK\t\t\t(1 << 7)\n#define SNES_RIGHT_MASK\t\t(1 << 8)\n#define SNES_LEFT_MASK\t\t(1 << 9)\n#define SNES_DOWN_MASK\t\t(1 << 10)\n#define SNES_UP_MASK\t\t(1 << 11)\n#define SNES_START_MASK\t\t(1 << 12)\n#define SNES_SELECT_MASK\t(1 << 13)\n#define SNES_Y_MASK\t\t\t(1 << 14)\n#define SNES_B_MASK\t\t\t(1 << 15)\n\n#define DEBUG_MODE_FLAG\t\t(1 << 0)\t// debugger\n#define TRACE_FLAG\t\t\t(1 << 1)\t// debugger\n#define SINGLE_STEP_FLAG\t(1 << 2)\t// debugger\n#define BREAK_FLAG\t\t\t(1 << 3)\t// debugger\n#define SCAN_KEYS_FLAG\t\t(1 << 4)\t// CPU\n#define HALTED_FLAG\t\t\t(1 << 12)\t// APU\n#define FRAME_ADVANCE_FLAG\t(1 << 9)\n\n#define ROM_NAME_LEN\t23\n#define AUTO_FRAMERATE\t200\n\nstruct SCPUState\n{\n\tuint32\tFlags;\n\tint32\tCycles;\n\tint32\tPrevCycles;\n\tint32\tV_Counter;\n\tuint8\t*PCBase;\n\tbool8\tNMIPending;\n\tbool8\tIRQLine;\n\tbool8\tIRQTransition;\n\tbool8\tIRQLastState;\n\tbool8\tIRQExternal;\n\tint32\tIRQPending;\n\tint32\tMemSpeed;\n\tint32\tMemSpeedx2;\n\tint32\tFastROMSpeed;\n\tbool8\tInDMA;\n\tbool8\tInHDMA;\n\tbool8\tInDMAorHDMA;\n\tbool8\tInWRAMDMAorHDMA;\n\tuint8\tHDMARanInDMA;\n\tint32\tCurrentDMAorHDMAChannel;\n\tuint8\tWhichEvent;\n\tint32\tNextEvent;\n\tbool8\tWaitingForInterrupt;\n\tuint32\tAutoSaveTimer;\n\tbool8\tSRAMModified;\n};\n\nenum\n{\n\tHC_HBLANK_START_EVENT = 1,\n\tHC_HDMA_START_EVENT = 2,\n\tHC_HCOUNTER_MAX_EVENT = 3,\n\tHC_HDMA_INIT_EVENT = 4,\n\tHC_RENDER_EVENT = 5,\n\tHC_WRAM_REFRESH_EVENT = 6\n};\n\nenum\n{\n\tIRQ_NONE = 0x0,\n\tIRQ_SET_FLAG = 0x1,\n\tIRQ_CLEAR_FLAG = 0x2,\n\tIRQ_TRIGGER_NMI = 0x4\n};\n\nstruct STimings\n{\n\tint32\tH_Max_Master;\n\tint32\tH_Max;\n\tint32\tV_Max_Master;\n\tint32\tV_Max;\n\tint32\tHBlankStart;\n\tint32\tHBlankEnd;\n\tint32\tHDMAInit;\n\tint32\tHDMAStart;\n\tint32\tNMITriggerPos;\n\tint32\tNextIRQTimer;\n\tinLine truncated
|
||||
},
|
||||
"profiles": {
|
||||
"anarch": {
|
||||
"core_version": null,
|
||||
"emulator": "Anarch",
|
||||
"type": "libretro",
|
||||
"version_basis": {
|
||||
"kind": "none"
|
||||
}
|
||||
},
|
||||
"beetle_psx": {
|
||||
"core_version": "v0.9.44.1",
|
||||
"emulator": "Beetle PSX",
|
||||
"type": "libretro",
|
||||
"version_basis": {
|
||||
"artifact": "mednafen_psx_libretro v0.9.44.1.zip",
|
||||
"kind": "package",
|
||||
"pattern": "libretro (v[0-9.]+)\\.zip"
|
||||
}
|
||||
},
|
||||
"dolphin": {
|
||||
"core_version": "2609-437-g3aac3f0",
|
||||
"emulator": "Dolphin",
|
||||
"source": "https://github.com/libretro/dolphin",
|
||||
"source_commit": "3aac3f052f733f8df2f1616843a4d2e14002ed11",
|
||||
"type": "standalone + libretro",
|
||||
"upstream": "https://github.com/dolphin-emu/dolphin",
|
||||
"version_basis": {
|
||||
"distance": 437,
|
||||
"kind": "tag",
|
||||
"tag": "2609"
|
||||
}
|
||||
},
|
||||
"fbneo": {
|
||||
"core_version": "v1.0.0.03",
|
||||
"emulator": "FinalBurn Neo",
|
||||
"source": "https://github.com/libretro/FBNeo",
|
||||
"source_commit": "32958a89abd645ed20387c8d31d21ede3bc48161",
|
||||
"type": "libretro",
|
||||
"version_basis": {
|
||||
"format": "v{1}.{2}.{3}.{4:02d}",
|
||||
"kind": "file",
|
||||
"path": "src/burn/version.h",
|
||||
"pattern": "VER_MAJOR\\s+(\\d+)\\s+#define\\s+VER_MINOR\\s+(\\d+)\\s+#define\\s+VER_BETA\\s+(\\d+)\\s+#define\\s+VER_ALPHA\\s+(\\d+)"
|
||||
}
|
||||
},
|
||||
"mednafen_psx": {
|
||||
"alias_of": "beetle_psx",
|
||||
"core_version": "v0.9.44.1",
|
||||
"emulator": "Beetle PSX",
|
||||
"type": "alias",
|
||||
"version_basis": {
|
||||
"kind": "alias",
|
||||
"of": "beetle_psx"
|
||||
}
|
||||
},
|
||||
"snes9x": {
|
||||
"core_version": "1.63",
|
||||
"emulator": "Snes9x",
|
||||
"source": "https://github.com/libretro/snes9x",
|
||||
"source_commit": "fae2fea08f74180759ef540ee94259213f503480",
|
||||
"type": "libretro",
|
||||
"upstream": "https://github.com/snes9xgit/snes9x",
|
||||
"version_basis": {
|
||||
"kind": "file",
|
||||
"path": "snes9x.h",
|
||||
"pattern": "#define\\s+VERSION\\s+\"([^\"]+)\""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -790,16 +790,6 @@ class FaqRegressions(unittest.TestCase):
|
||||
existence platform. Readers act on all three.
|
||||
"""
|
||||
|
||||
_MAME_GENERATIONS = {
|
||||
"MAME 2000": "mame2000",
|
||||
"MAME 2003": "mame2003",
|
||||
"MAME 2009": "mame2009",
|
||||
"MAME 2010": "mame2010",
|
||||
"MAME 2015": "mame2015",
|
||||
"MAME 2016": "mame2016",
|
||||
"current MAME": "mame",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _faq() -> str:
|
||||
# The questions about hashes and checks moved to the technical FAQ.
|
||||
@@ -813,17 +803,23 @@ class FaqRegressions(unittest.TestCase):
|
||||
with (ROOT / "emulators" / f"{key}.yml").open(encoding="utf-8") as handle:
|
||||
return yaml.safe_load(handle)
|
||||
|
||||
def test_mame_versions_match_their_profiles(self):
|
||||
# The list is wrapped, so a label and its version can straddle a line.
|
||||
faq = " ".join(self._faq().split())
|
||||
for label, key in self._MAME_GENERATIONS.items():
|
||||
match = re.search(rf"{re.escape(label)} \(([^)]+)\)", faq)
|
||||
self.assertIsNotNone(match, f"FAQ no longer states a version for {label}")
|
||||
self.assertEqual(
|
||||
match.group(1),
|
||||
self._profile(key)["core_version"],
|
||||
f"FAQ version for {label} has drifted from emulators/{key}.yml",
|
||||
)
|
||||
def test_the_romset_table_is_written_from_the_profiles(self):
|
||||
# Typed by hand, the table and the prose under it kept MAME 0.160
|
||||
# and FinalBurn Neo 1.0.0.2, versions no source at the pins states,
|
||||
# and the test holding them to the profiles kept the errors in step.
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
import generate_site # noqa: PLC0415
|
||||
from common import load_emulator_profiles # noqa: PLC0415
|
||||
|
||||
profiles = load_emulator_profiles(str(ROOT / "emulators"), skip_aliases=False)
|
||||
page = (ROOT / "wiki" / "technical-faq.md").read_text(encoding="utf-8")
|
||||
self.assertEqual(
|
||||
page, generate_site.with_romset_table(page, profiles),
|
||||
"wiki/technical-faq.md romset table is stale: run scripts/generate_site.py",
|
||||
)
|
||||
prose = page.split("### How this is known", 1)[1].split("\n## ", 1)[0]
|
||||
self.assertIsNone(re.search(r"\(0\.\d", prose),
|
||||
"the prose under the table types a version again")
|
||||
|
||||
def test_adler32_is_attributed_to_the_files_that_carry_it(self):
|
||||
dolphin = self._profile("dolphin")
|
||||
@@ -2080,6 +2076,59 @@ def _git_paths(*args: str, stdin: list[str] | None = None) -> set[str] | None:
|
||||
return {p for p in result.stdout.split("\0") if p}
|
||||
|
||||
|
||||
class ManifestVersionIsThePlatformsOwn(unittest.TestCase):
|
||||
"""Every install manifest named its list "1.0", and an inheriting YAML
|
||||
took its parent's version: Lakka was published as RetroArch v1.22.2.
|
||||
The installer compares the list's version with the one a machine runs,
|
||||
so each manifest states the version its own platform YAML records."""
|
||||
|
||||
def setUp(self):
|
||||
from scripts.common import _platform_config_cache
|
||||
_platform_config_cache.clear()
|
||||
|
||||
def test_committed_manifests_state_their_platform_version(self):
|
||||
from scripts.common import load_platform_config
|
||||
manifests = sorted((ROOT / "install").glob("*.json"))
|
||||
self.assertTrue(manifests)
|
||||
for path in manifests:
|
||||
with self.subTest(manifest=path.name):
|
||||
data = json.loads(path.read_text())
|
||||
config = load_platform_config(data["platform"], str(ROOT / "platforms"))
|
||||
self.assertTrue(config.get("version"))
|
||||
self.assertEqual(data["version"], str(config["version"]))
|
||||
|
||||
def test_no_child_shows_its_parents_version(self):
|
||||
from scripts.common import load_platform_config
|
||||
platforms = ROOT / "platforms"
|
||||
children = {}
|
||||
for path in sorted(platforms.glob("*.yml")):
|
||||
raw = yaml.safe_load(path.read_text()) or {}
|
||||
if isinstance(raw, dict) and raw.get("inherits"):
|
||||
children[path.stem] = raw
|
||||
self.assertIn("lakka", children)
|
||||
for name, raw in children.items():
|
||||
with self.subTest(platform=name):
|
||||
parent = yaml.safe_load(
|
||||
(platforms / f"{raw['inherits']}.yml").read_text())
|
||||
version = load_platform_config(name, str(platforms)).get("version")
|
||||
self.assertEqual(version, raw.get("version"))
|
||||
self.assertNotEqual(version, parent.get("version"))
|
||||
|
||||
def test_an_unversioned_child_has_no_version(self):
|
||||
from scripts.common import load_platform_config
|
||||
with tempfile.TemporaryDirectory(dir=TMP_ROOT) as tmp:
|
||||
Path(tmp, "parent.yml").write_text("platform: P\nversion: '2.0'\nsystems: {}\n")
|
||||
Path(tmp, "child.yml").write_text("inherits: parent\nplatform: C\n")
|
||||
self.assertNotIn("version", load_platform_config("child", tmp))
|
||||
|
||||
def test_lakka_names_the_release_its_cores_are_read_at(self):
|
||||
platforms = ROOT / "platforms"
|
||||
lakka = yaml.safe_load((platforms / "lakka.yml").read_text())
|
||||
targets = yaml.safe_load((platforms / "targets" / "lakka.yml").read_text())
|
||||
tag = re.search(r"/blob/([^/]+)/", targets["source"]).group(1)
|
||||
self.assertEqual(lakka["version"], tag)
|
||||
|
||||
|
||||
class EveryManifestEntryIsFetchable(unittest.TestCase):
|
||||
"""An install manifest may not list a file the installer cannot get.
|
||||
|
||||
@@ -5000,7 +5049,7 @@ class CollectionCardsCarryFullHashes(unittest.TestCase):
|
||||
"crc32": "504086c1", "sha256": "b" * 64}
|
||||
text = "\n".join(_system_file_card(entry, FileConsumers({}, {})))
|
||||
for value in (entry["sha1"], entry["md5"], entry["crc32"], entry["sha256"]):
|
||||
self.assertIn(f'<span class="rb-hash">{value}</span>', text)
|
||||
self.assertIn(f'<dd class="rb-hash">{value}</dd>', text)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -217,6 +217,7 @@ class RuleTable(unittest.TestCase):
|
||||
for zeros, half, entry in cases:
|
||||
verdict = rules.rule_validation(zeros, half, NONE_BOOTS, PRESENT, entry)
|
||||
self.assertEqual((verdict.rule, verdict.apply), ("validation_contradicted", False), entry)
|
||||
self.assertEqual(set(verdict.value), {"content", "size"})
|
||||
self.assertFalse(rules.agrees(entry, verdict))
|
||||
self.assertIsNone(rules.rule_validation(run(rules.UNKNOWN), None, NONE_BOOTS, PRESENT,
|
||||
{"name": "b"}))
|
||||
@@ -239,6 +240,11 @@ class RuleTable(unittest.TestCase):
|
||||
verdict = rules.rule_variants([run(rules.BOOTS, "b"), run(rules.HANG, "a")], self.ENTRY)
|
||||
self.assertEqual((verdict.value, verdict.apply), (["a", "b"], False))
|
||||
|
||||
def test_every_settled_field_can_carry_evidence(self) -> None:
|
||||
schema = json.loads((REPO_ROOT / "schemas" / "emulator.schema.json").read_text())
|
||||
evidence = schema["properties"]["files"]["items"]["properties"]["evidence"]["properties"]
|
||||
self.assertEqual(set(rules.SETTLED_FIELDS), set(evidence))
|
||||
|
||||
def test_agreement_and_evidence(self) -> None:
|
||||
self.assertTrue(rules.agrees({"name": "a"}, rules.Verdict("r", "hle_fallback", False, "", ())))
|
||||
self.assertFalse(rules.agrees({"name": "a"}, rules.Verdict("r", "hle_fallback", True, "", ())))
|
||||
@@ -866,6 +872,15 @@ class Findings(Workspace):
|
||||
self.c.settle("fake", self.content, self.core, 0, self.entry(), [later])
|
||||
self.assertEqual([line["status"] for line in self.lines()], ["open", "resolved"])
|
||||
|
||||
def test_new_evidence_writes_the_finding_again(self) -> None:
|
||||
verdict = rules.Verdict("unsettled", "hle_fallback", None, "x", (self.RID,))
|
||||
self.c.settle("fake", self.content, self.core, 0, self.entry(), [verdict])
|
||||
self.c.settle("fake", self.content, self.core, 0, self.entry(), [verdict])
|
||||
later = rules.Verdict("unsettled", "hle_fallback", None, "x", ("2026-10-28-000000000000",))
|
||||
self.c.settle("fake", self.content, self.core, 0, self.entry(), [later])
|
||||
self.assertEqual([line["evidence"] for line in self.lines()],
|
||||
[[self.RID], ["2026-10-28-000000000000"]])
|
||||
|
||||
def test_never_opened_is_flagged(self) -> None:
|
||||
verdict = rules.rule_never_opened(self.entry(1), [self.RID], "Fake Game (World)")
|
||||
self.c.settle("fake", self.content, self.core, 1, self.entry(1), [verdict])
|
||||
@@ -1015,6 +1030,11 @@ class FakeCorePasses(Workspace):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
# These passes prove the cores run sandboxed. Without a working bwrap
|
||||
# the campaign refuses, and the cause is named here instead of a KeyError.
|
||||
_prefix, isolation = campaign.core_probe.sandbox_argv([Path(tempfile.gettempdir())])
|
||||
if isolation != campaign.core_probe.SANDBOX:
|
||||
raise unittest.SkipTest(f"no sandbox here ({isolation}): install bubblewrap")
|
||||
TMP.mkdir(parents=True, exist_ok=True)
|
||||
cls.build = Path(tempfile.mkdtemp(dir=TMP))
|
||||
source = cls.build / "fake.c"
|
||||
|
||||
@@ -58,6 +58,7 @@ class ACoreFileFillsAFreeDeclaration(unittest.TestCase):
|
||||
self.emulators.mkdir()
|
||||
(self.platforms / "fakeplat.yml").write_text(yaml.safe_dump({
|
||||
"platform": "FakePlat",
|
||||
"version": "2.1",
|
||||
"verification_mode": "existence",
|
||||
"base_destination": "system",
|
||||
"cores": ["fakecore"],
|
||||
@@ -118,7 +119,7 @@ class AFileDeclaredUnderAnotherName(unittest.TestCase):
|
||||
(root / "platforms").mkdir()
|
||||
(root / "emulators").mkdir()
|
||||
(root / "platforms" / "fakeplat.yml").write_text(yaml.safe_dump({
|
||||
"platform": "FakePlat", "verification_mode": "existence",
|
||||
"platform": "FakePlat", "version": "2.1", "verification_mode": "existence",
|
||||
"base_destination": "system", "cores": ["gs"],
|
||||
"systems": {"apple-iigs": {"files": [
|
||||
{"name": "ROM1", "destination": "ROM1", "md5": md5},
|
||||
@@ -156,7 +157,7 @@ class AMissingDataDirectoryFailsTheRun(unittest.TestCase):
|
||||
(root / "emulators").mkdir()
|
||||
(root / "bios").mkdir()
|
||||
(root / "platforms" / "fakeplat.yml").write_text(yaml.safe_dump({
|
||||
"platform": "FakePlat", "verification_mode": "existence",
|
||||
"platform": "FakePlat", "version": "2.1", "verification_mode": "existence",
|
||||
"base_destination": "system", "cores": [],
|
||||
"systems": {"console-a": {"files": [], "data_directories": [
|
||||
{"ref": "assets", "destination": "assets"},
|
||||
@@ -197,7 +198,7 @@ class AFileNobodyHoldsIsAnOmission(unittest.TestCase):
|
||||
for folder in ("platforms", "emulators", "bios"):
|
||||
(root / folder).mkdir()
|
||||
(root / "platforms" / "fakeplat.yml").write_text(yaml.safe_dump({
|
||||
"platform": "FakePlat", "verification_mode": "existence",
|
||||
"platform": "FakePlat", "version": "2.1", "verification_mode": "existence",
|
||||
"base_destination": "system", "cores": ["devil"],
|
||||
"systems": {"diablo": {"files": []}},
|
||||
}))
|
||||
@@ -238,7 +239,7 @@ class AMemberNameIsNotALooseFile(unittest.TestCase):
|
||||
(root / "platforms").mkdir()
|
||||
(root / "emulators").mkdir()
|
||||
(root / "platforms" / "fakeplat.yml").write_text(yaml.safe_dump({
|
||||
"platform": "FakePlat", "verification_mode": "existence",
|
||||
"platform": "FakePlat", "version": "2.1", "verification_mode": "existence",
|
||||
"base_destination": "system", "cores": ["naomi2core"],
|
||||
"systems": {"sega-naomi2": {"files": []}},
|
||||
}))
|
||||
@@ -283,7 +284,7 @@ class TwoDeclarationsOfOnePath(unittest.TestCase):
|
||||
for folder in ("platforms", "emulators"):
|
||||
(root / folder).mkdir()
|
||||
(root / "platforms" / "fakeplat.yml").write_text(yaml.safe_dump({
|
||||
"platform": "FakePlat", "verification_mode": "existence",
|
||||
"platform": "FakePlat", "version": "2.1", "verification_mode": "existence",
|
||||
"base_destination": "system", "cores": [],
|
||||
"systems": {
|
||||
"a-system": {"files": [{"name": first, "destination": "shared.bin"}]},
|
||||
@@ -335,7 +336,7 @@ class ANamelessWhitelistEntry(unittest.TestCase):
|
||||
(root / folder).mkdir()
|
||||
values = [md5.upper(), f"{'0' * 32},{md5}", "f" * 32]
|
||||
(root / "platforms" / "fakeplat.yml").write_text(yaml.safe_dump({
|
||||
"platform": "FakePlat", "verification_mode": "md5",
|
||||
"platform": "FakePlat", "version": "2.1", "verification_mode": "md5",
|
||||
"base_destination": "bios", "cores": [],
|
||||
"systems": {"sony-playstation": {"files": [
|
||||
{"name": f"sony-playstation:{v}", "destination": "", "md5": v}
|
||||
|
||||
@@ -286,6 +286,45 @@ class OracleTests(unittest.TestCase):
|
||||
self.assertEqual(core_probe.oracle_for(data, "b" * 40, "Fake")["references"], [])
|
||||
self.assertEqual(core_probe.oracle_for(data, "a" * 40, "Nobody")["error_patterns"], ["shared"])
|
||||
|
||||
def test_every_reference_judges_its_own_record_a_boot(self) -> None:
|
||||
"""Each reference block names the run it was taken from: judged
|
||||
again with the oracle as it stands, every repetition of that run
|
||||
boots, so a frame copied wrong or a stage mislabelled fails here."""
|
||||
data, _ = core_probe.load_oracle(core_probe.DEFAULT_ORACLE)
|
||||
records = {s.record["id"]: s.record for s in measurement_records.read_records(REPO_ROOT)}
|
||||
checked = 0
|
||||
for item in data.get("content") or ():
|
||||
for library, block in item["references"].items():
|
||||
record = records.get(block["recorded_from"])
|
||||
self.assertIsNotNone(record, f"{item['name']} on {library}: {block['recorded_from']}")
|
||||
self.assertEqual(record["inputs"]["content"]["sha1"], item["sha1"], item["name"])
|
||||
for rep in record["variants"][0]["repetitions"]:
|
||||
oracle = core_probe.oracle_for(data, item["sha1"], rep["library_name"])
|
||||
verdict, evidence = core_probe.judge(rep, oracle)
|
||||
self.assertEqual(verdict, "boots", f"{item['name']} on {library}: {evidence}")
|
||||
checked += 1
|
||||
self.assertGreaterEqual(checked, 2 * 14)
|
||||
|
||||
def test_the_repository_error_patterns_and_screens(self) -> None:
|
||||
data, _ = core_probe.load_oracle(core_probe.DEFAULT_ORACLE)
|
||||
saturn = core_probe.oracle_for(data, None, "Beetle Saturn")
|
||||
for line in ('Cannot open BIOS file "/system/sega_101.bin".',
|
||||
'BIOS file "/system/sega_101.bin" is of an incorrect size.',
|
||||
'BIOS file "/system/sega_101.bin" could not be fully read (short or failed read).'):
|
||||
rep = {"loaded": False, "logs": [{"frame": 0, "level": "error", "text": line}]}
|
||||
self.assertEqual(core_probe.judge(rep, saturn)[0], "bios_error", line)
|
||||
menu = {"Beetle PSX": "dfbfc299250c0983f86bbf951efd187441306d92",
|
||||
"Beetle PSX HW": "dfbfc299250c0983f86bbf951efd187441306d92",
|
||||
"SwanStation": "167fb7b6527e295b12dca5806a29856f5f31d401"}
|
||||
for library, sha1 in menu.items():
|
||||
rep = {"loaded": True, "logs": [], "frames": 100, "warmup": 0,
|
||||
"observables": {"frame_hashes": [[90, sha1]], "uniform_frames": []}}
|
||||
verdict, evidence = core_probe.judge(rep, core_probe.oracle_for(data, None, library))
|
||||
self.assertEqual((verdict, evidence["rule"]), ("bios_error", "error_screen"), library)
|
||||
# The PlayStation cores fall back to a built-in BIOS: nothing they log is an error.
|
||||
for library in ("Beetle PSX", "SwanStation", "PCSX-ReARMed", "PCSX1"):
|
||||
self.assertEqual(core_probe.oracle_for(data, None, library)["error_patterns"], [])
|
||||
|
||||
def test_the_repository_oracle_is_well_formed(self) -> None:
|
||||
import yaml
|
||||
data = yaml.safe_load(core_probe.DEFAULT_ORACLE.read_text(encoding="utf-8"))
|
||||
@@ -658,5 +697,45 @@ class Sandbox(unittest.TestCase):
|
||||
self.assertIn("--require-sandbox: environment only: bwrap not found", problems)
|
||||
|
||||
|
||||
|
||||
class TheInterpreterIsInTheSandbox(unittest.TestCase):
|
||||
"""CI's setup-python, a venv and uv put Python outside /usr. The sandbox
|
||||
bound only the system trees, so the worker could not start and every run
|
||||
crashed on the runner."""
|
||||
|
||||
def test_each_link_to_the_interpreter_is_bound(self):
|
||||
from unittest import mock # noqa: PLC0415
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
real = root / "uv" / "cpython-3.13.12" / "bin"
|
||||
real.mkdir(parents=True)
|
||||
(real / "python3.13").write_text("")
|
||||
alias = root / "uv" / "cpython-3.13"
|
||||
alias.symlink_to(root / "uv" / "cpython-3.13.12")
|
||||
venv = root / "venv" / "bin"
|
||||
venv.mkdir(parents=True)
|
||||
(venv / "python").symlink_to(alias / "bin" / "python3.13")
|
||||
with mock.patch.object(core_probe.sys, "executable", str(venv / "python")), \
|
||||
mock.patch.object(core_probe.sys, "prefix", str(root / "venv")), \
|
||||
mock.patch.object(core_probe.sys, "base_prefix", str(root / "uv" / "cpython-3.13.12")):
|
||||
paths = core_probe.interpreter_paths()
|
||||
self.assertIn(root / "venv", paths)
|
||||
self.assertIn(alias, paths)
|
||||
self.assertIn(root / "uv" / "cpython-3.13.12", paths)
|
||||
|
||||
def test_a_link_is_mounted_at_its_own_path(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
target = Path(tmp) / "real"
|
||||
target.mkdir()
|
||||
link = Path(tmp) / "link"
|
||||
link.symlink_to(target)
|
||||
prefix, isolation = core_probe.sandbox_argv([], [link])
|
||||
if isolation != core_probe.SANDBOX:
|
||||
self.skipTest(f"no sandbox here ({isolation})")
|
||||
self.assertIn(["--ro-bind", str(target.resolve()), str(link)],
|
||||
[prefix[i:i + 3] for i in range(len(prefix) - 2)])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -76,7 +76,7 @@ class _Collection(unittest.TestCase):
|
||||
}}
|
||||
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
|
||||
self.config = {
|
||||
"platform": "Exist", "verification_mode": "existence",
|
||||
"platform": "Exist", "version": "2.1", "verification_mode": "existence",
|
||||
"cores": ["sized", "unsized"],
|
||||
"systems": {"mac": {"files": []}, "iigs": {"files": []}},
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ files:
|
||||
|
||||
PLATFORM = {
|
||||
"platform": "Demo",
|
||||
"version": "2.1",
|
||||
"verification_mode": "existence",
|
||||
"base_destination": "",
|
||||
"cores": ["demo"],
|
||||
|
||||
+24
-2
@@ -98,8 +98,30 @@ class InstallerOutputExample(unittest.TestCase):
|
||||
self.assertIn(part.rsplit(":", 1)[0], install.OMISSION_REASONS.values())
|
||||
count, size = re.search(r"\n (\d+) files \(([\d.]+ [KMG]B)\)", run).groups()
|
||||
self.assertIn(f"0/{count} present", run)
|
||||
menu = re.search(r"RetroArch: (\d+) files, ([\d.]+ [KMG]B)\.", text)
|
||||
self.assertEqual(menu.groups(), (count, size))
|
||||
self.assertSplitAddsUp(run, count, size)
|
||||
menu = re.search(r"RetroArch: (\d+) files, ([\d.]+ [KMG]B)[.,]([^`]*)", text)
|
||||
self.assertEqual(menu.groups()[:2], (count, size))
|
||||
self.assertSplitAddsUp(menu.group(3), count, size)
|
||||
games = re.search(r"g\) leave out the (\d+) game data files \(([\d.]+ [KMG]B)\)",
|
||||
menu.group(3))
|
||||
self.assertIsNotNone(games, "the menu offers to leave the game data out")
|
||||
split = re.search(r"(\d+) BIOS files \(([\d.]+ [KMG]B)\) and (\d+) game data files "
|
||||
r"\(([\d.]+ [KMG]B)\)", menu.group(3))
|
||||
self.assertEqual(games.groups(), split.groups()[2:])
|
||||
|
||||
def assertSplitAddsUp(self, text: str, count: str, size: str) -> None:
|
||||
"""The run counted 8069 files where its menu counted 3259 BIOS files
|
||||
and 5089 game data files: the parts add up to the total they divide,
|
||||
the sizes within the rounding of their three figures."""
|
||||
split = re.search(r"(\d+) BIOS files \(([\d.]+) ([KMG]B)\) and (\d+) game data "
|
||||
r"files \(([\d.]+) ([KMG]B)\)", text)
|
||||
self.assertIsNotNone(split, "a categorized list says its BIOS files and game data")
|
||||
bios, bios_size, bios_unit, games, game_size, game_unit = split.groups()
|
||||
self.assertEqual(int(bios) + int(games), int(count))
|
||||
units = {"KB": 1, "MB": 1024, "GB": 1024 ** 2}
|
||||
total, unit = size.split()
|
||||
added = (float(bios_size) * units[bios_unit] + float(game_size) * units[game_unit])
|
||||
self.assertLessEqual(abs(added - float(total) * units[unit]), 0.15 * units[unit])
|
||||
|
||||
|
||||
class OneResolutionChain(unittest.TestCase):
|
||||
|
||||
+49
-1
@@ -224,6 +224,7 @@ class TestE2E(unittest.TestCase):
|
||||
def _create_existence_platform(self):
|
||||
config = {
|
||||
"platform": "TestExistence",
|
||||
"version": "9.1",
|
||||
"verification_mode": "existence",
|
||||
"base_destination": "system",
|
||||
"systems": {
|
||||
@@ -3676,6 +3677,51 @@ class TestE2E(unittest.TestCase):
|
||||
self.assertIsInstance(f["size"], int)
|
||||
self.assertGreater(len(f["sha1"]), 0)
|
||||
|
||||
def test_91_manifest_version_is_the_platform_list_version(self):
|
||||
"""The manifest names the version of the list it was built from.
|
||||
|
||||
It was the constant "1.0" for every platform, so the installer could
|
||||
not tell which Batocera or Recalbox release a list described. Two
|
||||
platforms of different versions show the value comes from each YAML.
|
||||
A YAML without one, an inheriting child included, is refused rather
|
||||
than written with a stand-in or its parent's version.
|
||||
"""
|
||||
from common import _platform_config_cache
|
||||
from generate_pack import generate_manifest
|
||||
|
||||
registry_path = os.path.join(self.platforms_dir, "_test_registry_version.yml")
|
||||
with open(registry_path, "w") as fh:
|
||||
yaml.dump({"platforms": {}}, fh)
|
||||
with open(os.path.join(self.platforms_dir, "test_md5.yml")) as fh:
|
||||
config = yaml.safe_load(fh)
|
||||
config["version"] = "41"
|
||||
with open(os.path.join(self.platforms_dir, "versioned_md5.yml"), "w") as fh:
|
||||
yaml.dump(config, fh)
|
||||
del config["version"]
|
||||
with open(os.path.join(self.platforms_dir, "unversioned.yml"), "w") as fh:
|
||||
yaml.dump(config, fh)
|
||||
_platform_config_cache.clear()
|
||||
|
||||
def version_of(name: str) -> str:
|
||||
return generate_manifest(
|
||||
name, self.platforms_dir, self.db, self.bios_dir, registry_path,
|
||||
emulators_dir=self.emulators_dir, offline=True,
|
||||
)["version"]
|
||||
|
||||
self.assertEqual(version_of("test_existence"), "9.1")
|
||||
self.assertEqual(version_of("versioned_md5"), "41")
|
||||
for name in ("unversioned", "test_inherited"):
|
||||
with self.subTest(platform=name), \
|
||||
self.assertRaisesRegex(ValueError, "records no version"):
|
||||
version_of(name)
|
||||
with open(os.path.join(self.platforms_dir, "test_inherited.yml")) as fh:
|
||||
child = yaml.safe_load(fh)
|
||||
child["version"] = "2"
|
||||
with open(os.path.join(self.platforms_dir, "test_inherited.yml"), "w") as fh:
|
||||
yaml.dump(child, fh)
|
||||
_platform_config_cache.clear()
|
||||
self.assertEqual(version_of("test_inherited"), "2")
|
||||
|
||||
def test_91b_manifest_hash_points_at_a_fetchable_file(self):
|
||||
"""A declared hash no local file carries must not blank repo_path.
|
||||
|
||||
@@ -4905,7 +4951,8 @@ struct BurnDriver BurnDrvneogeo = {
|
||||
rom = next(f for f in result["files"] if f["name"] == "sp-s2.sp1")
|
||||
self.assertEqual(rom["crc32"], "newcrc")
|
||||
self.assertEqual(rom["system"], "snk-neogeo-mvs")
|
||||
self.assertEqual(result["core_version"], "v1.0.0.03")
|
||||
# The version is the pin's, read through version_basis.
|
||||
self.assertEqual(result["core_version"], "v1.0.0.02")
|
||||
|
||||
|
||||
def _load_config(self, platform_name: str) -> dict:
|
||||
@@ -5726,6 +5773,7 @@ struct BurnDriver BurnDrvneogeo = {
|
||||
self.db = self._build_db()
|
||||
config = {
|
||||
"platform": "RegionPlat",
|
||||
"version": "3.0",
|
||||
"verification_mode": "existence",
|
||||
"systems": {
|
||||
"region-console": {
|
||||
|
||||
@@ -47,6 +47,7 @@ class FrontendDigestDecides(unittest.TestCase):
|
||||
)
|
||||
(self.platforms / "digestplat.yml").write_text(yaml.safe_dump({
|
||||
"platform": "DigestPlat",
|
||||
"version": "2.1",
|
||||
"verification_mode": "md5",
|
||||
"base_destination": "bios",
|
||||
"systems": {"psx": {"files": [{
|
||||
|
||||
@@ -592,7 +592,10 @@ class TestFbneoMerge(unittest.TestCase):
|
||||
self.assertEqual(len(archive_files), 1)
|
||||
self.assertNotIn("_upstream_removed", archive_files[0])
|
||||
|
||||
def test_merge_updates_core_version(self) -> None:
|
||||
def test_merge_leaves_core_version_to_the_pin(self) -> None:
|
||||
# The hashes are read at finalburnneo/FBNeo's tip, which the
|
||||
# profile's libretro/FBNeo pin does not name: the version stays the
|
||||
# one version_basis reads at that pin.
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
p = Path(td)
|
||||
profile_path = _write_yaml(p / "fbneo.yml", _make_fbneo_profile())
|
||||
@@ -600,7 +603,7 @@ class TestFbneoMerge(unittest.TestCase):
|
||||
|
||||
result = merge_fbneo_profile(profile_path, hashes_path)
|
||||
|
||||
self.assertEqual(result["core_version"], "v1.0.0.03")
|
||||
self.assertEqual(result["core_version"], "v1.0.0.02")
|
||||
|
||||
|
||||
class TestDiff(unittest.TestCase):
|
||||
@@ -849,7 +852,7 @@ class WritersPutTheValuesOnDisk(unittest.TestCase):
|
||||
merge_fbneo_profile(str(profile), hashes, write=True, add_new=False)
|
||||
written = yaml.safe_load(profile.read_text(encoding="utf-8"))
|
||||
entry = written["files"][0]
|
||||
self.assertEqual(written["core_version"], "v1.0.0.03")
|
||||
self.assertEqual(written["core_version"], "v1.0.0.02")
|
||||
self.assertEqual(entry["crc32"], "9036d879")
|
||||
self.assertEqual(entry["note"], "MVS Asia/Europe ver. 5")
|
||||
self.assertEqual(entry["source_ref"], "src/burn/drv/neogeo/d_neogeo.cpp:1625")
|
||||
|
||||
@@ -23,6 +23,7 @@ import unittest
|
||||
from types import SimpleNamespace
|
||||
import unittest.mock
|
||||
import urllib.error
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -5852,5 +5853,157 @@ class KeptFilesEndToEnd(unittest.TestCase, _SyntheticTree):
|
||||
self.assertEqual(len(list((self.home / install.BACKUP_DIR).iterdir())), 2)
|
||||
|
||||
|
||||
|
||||
class TheListVersionIsComparedWithTheSystems(unittest.TestCase):
|
||||
"""The list says which version of the platform it was built for, and the
|
||||
installer reads the version the system runs from the file the system
|
||||
writes. A difference is said, never refused."""
|
||||
|
||||
def note(self, plat: str, listed: str, bios: Path = Path("/userdata/bios")):
|
||||
return install.version_note(plat, bios, {"platform": plat, "version": listed})
|
||||
|
||||
def test_batocera_reads_the_first_word_of_its_version_file(self):
|
||||
_device(self, {"/usr/share/batocera/batocera.version": "41 2025/01/17 10:12\n"})
|
||||
listed, installed, text = self.note("batocera", "43.1")
|
||||
self.assertEqual((listed, installed), ("43.1", "41"))
|
||||
self.assertEqual(
|
||||
text, "This list was built for Batocera 43.1, this machine runs 41. "
|
||||
"Batocera's own BIOS check may report some files as wrong though they work.")
|
||||
self.assertIsNone(self.note("batocera", "41"))
|
||||
|
||||
def test_recalbox_reads_only_a_release(self):
|
||||
_device(self, {"/recalbox/recalbox.version": "10.0\n"})
|
||||
self.assertEqual(self.note("recalbox", "10.1.1")[:2], ("10.1.1", "10.0"))
|
||||
self.assertIsNone(self.note("recalbox", "10.0"))
|
||||
for built in ("development", "2026/10/01 main (4242)"):
|
||||
with self.subTest(built=built):
|
||||
_device(self, {"/recalbox/recalbox.version": built})
|
||||
self.assertIsNone(self.note("recalbox", "10.1.1"))
|
||||
|
||||
def test_retrobat_reads_the_folder_beside_its_bios_folder(self):
|
||||
root = Path(_tempdir(self))
|
||||
version = root / "system" / "version.info"
|
||||
version.parent.mkdir()
|
||||
version.write_text("8.3.1.1-stable-win64")
|
||||
# The release tagged 8.3.1 ships 8.3.1.1.
|
||||
self.assertIsNone(self.note("retrobat", "8.3.1", root / "bios"))
|
||||
version.write_text("8.2.0-stable-win64")
|
||||
self.assertEqual(self.note("retrobat", "8.3.1", root / "bios")[:2],
|
||||
("8.3.1", "8.2.0"))
|
||||
|
||||
def test_lakka_and_emuelec_read_os_release(self):
|
||||
_device(self, {}, release='NAME="Lakka"\nID="lakka"\nVERSION_ID="6.0"\n')
|
||||
listed, installed, text = self.note("lakka", "v6.1")
|
||||
self.assertEqual((listed, installed), ("v6.1", "6.0"))
|
||||
self.assertIn("Lakka checks only that a file is there", text)
|
||||
_device(self, {}, release='NAME="Lakka"\nID="lakka"\nVERSION_ID="6.1"\n')
|
||||
self.assertIsNone(self.note("lakka", "v6.1"))
|
||||
_device(self, {}, release='NAME="EmuELEC"\nVERSION_ID="4.7"\n')
|
||||
self.assertEqual(self.note("emuelec", "4.8")[:2], ("4.8", "4.7"))
|
||||
# The file of another system says nothing about this one.
|
||||
self.assertIsNone(self.note("lakka", "v6.1"))
|
||||
|
||||
def test_an_unknown_version_says_nothing(self):
|
||||
_device(self, {"/usr/share/batocera/batocera.version": "41 2025/01/17 10:12"})
|
||||
self.assertIsNone(self.note("batocera", "1.0"))
|
||||
self.assertIsNone(install.version_note("batocera", Path("/userdata/bios"),
|
||||
{"platform": "batocera"}))
|
||||
self.assertIsNone(self.note("retroarch", "v1.22.2"))
|
||||
_device(self, {"/usr/share/knulli/knulli.version": "scarab 2026/05/11 10:00"})
|
||||
self.assertIsNone(self.note("knulli", "20260511"))
|
||||
_device(self, {})
|
||||
self.assertIsNone(self.note("batocera", "43.1"))
|
||||
_device(self, {"/usr/share/batocera/batocera.version": "\x1b[2J 2025/01/17"})
|
||||
self.assertIsNone(self.note("batocera", "43.1"))
|
||||
|
||||
def test_a_pack_alone_gives_the_version_of_its_list(self):
|
||||
"""A handheld without network installs from the pack: its manifest.json
|
||||
records the list's version, which generate_pack takes from the
|
||||
install list built with the same choices."""
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
import generate_pack
|
||||
data = b"bios"
|
||||
install_list = {"version": "43.1", "files": [
|
||||
{"dest": "a.bin", "sha1": hashlib.sha1(data).hexdigest(), "size": 4}]}
|
||||
pack_list = {"files": [{"path": "a.bin", "sha1": hashlib.sha1(data).hexdigest(),
|
||||
"size": 4}]}
|
||||
generate_pack.annotate_pack_systems(pack_list, generate_pack.pack_systems(install_list))
|
||||
self.assertEqual(pack_list["list_version"], "43.1")
|
||||
pack = Path(_tempdir(self)) / "Batocera_43.1_BIOS_Pack.zip"
|
||||
with zipfile.ZipFile(pack, "w") as archive:
|
||||
archive.writestr("manifest.json", json.dumps(pack_list))
|
||||
archive.writestr("a.bin", data)
|
||||
source = install.PackSource([pack])
|
||||
self.addCleanup(source.close)
|
||||
self.assertEqual(source.manifest("batocera")["version"], "43.1")
|
||||
|
||||
def test_the_result_repeats_it_in_every_ending(self):
|
||||
for kw in ({"downloaded": 3}, {"errors": 2}, {"downloaded": 1, "errors": 1}):
|
||||
with self.subTest(**kw):
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf), contextlib.suppress(SystemExit):
|
||||
install._report_outcome([Path("/userdata/bios")], omitted=0,
|
||||
interrupted=False, notes=["Built for 43.1."],
|
||||
**{"downloaded": 0, "up_to_date": 0,
|
||||
"errors": 0, **kw})
|
||||
self.assertIn("Built for 43.1.", buf.getvalue())
|
||||
|
||||
|
||||
class TheListVersionIsSaidBeforeAndAfter(unittest.TestCase):
|
||||
"""A RetroBat folder holding 8.2.0 and a list built for 8.3.1, through
|
||||
the installer as a person and as the screen run it."""
|
||||
|
||||
DATA = b"retrobat bios" * 8
|
||||
|
||||
def setUp(self):
|
||||
self.root = Path(_tempdir(self))
|
||||
(self.root / "system").mkdir()
|
||||
(self.root / "system" / "version.info").write_text("8.2.0-stable-win64")
|
||||
self.bios = self.root / "bios"
|
||||
self.bios.mkdir()
|
||||
(self.bios / "a.bin").write_bytes(self.DATA)
|
||||
self.list = self.root / "retrobat.json"
|
||||
self.list.write_text(json.dumps({
|
||||
"manifest_version": 2, "platform": "retrobat", "version": "8.3.1",
|
||||
"files": [{"dest": "a.bin", "sha1": hashlib.sha1(self.DATA).hexdigest(),
|
||||
"size": len(self.DATA), "repo_path": "bios/a.bin"}],
|
||||
}))
|
||||
|
||||
def run_installer(self, *options: str) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
[sys.executable, str(REPO_ROOT / "install.py"), "--platform", "retrobat",
|
||||
"--dest", str(self.bios), "--manifest", str(self.list), "--offline",
|
||||
"--yes", *options],
|
||||
capture_output=True, text=True, timeout=120, check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
SAID = ("This list was built for RetroBat 8.3.1, this machine runs 8.2.0. "
|
||||
"RetroBat's own BIOS check may report some files as wrong though they work.")
|
||||
|
||||
def test_line_mode_says_it_before_the_install_and_with_the_result(self):
|
||||
proc = self.run_installer()
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertEqual(proc.stdout.count(self.SAID), 2, proc.stdout)
|
||||
self.assertLess(proc.stdout.index(self.SAID), proc.stdout.index("Checking existing"))
|
||||
self.assertGreater(proc.stdout.rindex(self.SAID), proc.stdout.index("Done."))
|
||||
|
||||
def test_the_screen_reads_an_event(self):
|
||||
proc = self.run_installer("--check", "--events")
|
||||
events = [json.loads(line) for line in proc.stdout.splitlines()]
|
||||
said = [event for event in events if event["event"] == "version"]
|
||||
self.assertEqual(said, [{"event": "version", "platform": "retrobat",
|
||||
"list_version": "8.3.1", "installed_version": "8.2.0",
|
||||
"message": self.SAID}])
|
||||
kinds = [event["event"] for event in events]
|
||||
self.assertLess(kinds.index("version"), kinds.index("check"))
|
||||
|
||||
def test_the_same_version_says_nothing(self):
|
||||
(self.root / "system" / "version.info").write_text("8.3.1.1-stable-win64")
|
||||
proc = self.run_installer()
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertNotIn("This list was built for", proc.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+25
-13
@@ -370,18 +370,28 @@ class RefusedBeforeAnyInstall(unittest.TestCase):
|
||||
(tmp / "retroarch.cfg").write_text("")
|
||||
(tmp / "cores").mkdir()
|
||||
(tmp / "cores" / "mgba_libretro.so").write_bytes(b"")
|
||||
with mock.patch.object(install, "detect_os", return_value="linux"), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=detected), \
|
||||
mock.patch.object(install, "fetch_manifest") as fetch, \
|
||||
mock.patch.object(sys, "argv", ["install.py", "--installed-cores", "--no-input"]), \
|
||||
contextlib.redirect_stdout(io.StringIO()), \
|
||||
contextlib.redirect_stderr(io.StringIO()) as err:
|
||||
with self.assertRaises(SystemExit) as stop:
|
||||
install.main()
|
||||
self.assertEqual(stop.exception.code, 1)
|
||||
self.assertIn("batocera ships its cores", err.getvalue())
|
||||
fetch.assert_not_called()
|
||||
# Two setups found stop a run without questions before either is
|
||||
# read (code_audit_18 P2-3): the refusal is checked with Batocera
|
||||
# named, and with both found nothing is fetched either.
|
||||
for argv, said in (
|
||||
(["--platform", "batocera", "--dest", str(tmp / "bios")],
|
||||
"batocera ships its cores"),
|
||||
([], "2 setups were found here")):
|
||||
with self.subTest(argv=argv), \
|
||||
mock.patch.object(install, "detect_os", return_value="linux"), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=detected), \
|
||||
mock.patch.object(install, "_mounted_cards", return_value=[]), \
|
||||
mock.patch.object(install, "fetch_manifest") as fetch, \
|
||||
mock.patch.object(sys, "argv", ["install.py", "--installed-cores",
|
||||
"--no-input", *argv]), \
|
||||
contextlib.redirect_stdout(io.StringIO()), \
|
||||
contextlib.redirect_stderr(io.StringIO()) as err:
|
||||
with self.assertRaises(SystemExit) as stop:
|
||||
install.main()
|
||||
self.assertEqual(stop.exception.code, 1)
|
||||
self.assertIn(said, err.getvalue())
|
||||
fetch.assert_not_called()
|
||||
|
||||
|
||||
class AnUnknownTargetStopsTheRunFirst(unittest.TestCase):
|
||||
@@ -393,12 +403,14 @@ class AnUnknownTargetStopsTheRunFirst(unittest.TestCase):
|
||||
self.addCleanup(shutil.rmtree, tmp, True)
|
||||
detected = [("emudeck", tmp / "bios"), ("retroarch", tmp / "system")]
|
||||
lists = {"emudeck": {"steamos": {"cores": []}}, "retroarch": {"x86_64": {"cores": []}}}
|
||||
# Listed for both platforms: a listing reads every list found.
|
||||
with mock.patch.object(install, "detect_os", return_value="linux"), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=detected), \
|
||||
mock.patch.object(install, "fetch_targets", side_effect=lists.get), \
|
||||
mock.patch.object(install, "fetch_manifest") as fetch, \
|
||||
mock.patch.object(sys, "argv", ["install.py", "--target", "steamos", "--no-input"]), \
|
||||
mock.patch.object(sys, "argv", ["install.py", "--target", "steamos",
|
||||
"--list-systems", "--no-input"]), \
|
||||
contextlib.redirect_stdout(io.StringIO()), \
|
||||
contextlib.redirect_stderr(io.StringIO()) as err:
|
||||
with self.assertRaises(SystemExit) as stop:
|
||||
|
||||
@@ -918,6 +918,37 @@ class ColourFollowsNoColorOrg(unittest.TestCase):
|
||||
self.assertFalse(term.live)
|
||||
|
||||
|
||||
class TheEndOfARunPaintsItsStatusWords(unittest.TestCase):
|
||||
"""Line mode ends on the words the full screen paints, in the same
|
||||
roles, and on the same text without colour."""
|
||||
|
||||
def _end(self, colour: bool, *counts: int, **given) -> str:
|
||||
term = install.Terminal(io.StringIO())
|
||||
term.colour = colour
|
||||
out = io.StringIO()
|
||||
with contextlib.redirect_stdout(out), contextlib.suppress(SystemExit):
|
||||
install._report_outcome([Path("bios")], *counts, term=term, **given)
|
||||
return out.getvalue()
|
||||
|
||||
def test_the_same_words_with_and_without_colour(self):
|
||||
for counts, given, painted in (
|
||||
((3, 1, 1, 0, False), {"failures": [("a.bin", "HTTP 404")]},
|
||||
(("failure", "Finished with errors"), ("success", "3 files installed"),
|
||||
("failure", "1 file failed"), ("failure", "failed:"))),
|
||||
((2, 1, 0, 0, False), {},
|
||||
(("success", "Done"), ("success", "2 files installed"),
|
||||
("success", "1 file already up to date"))),
|
||||
((1, 0, 0, 0, False), {"offline": 2},
|
||||
(("warning", "Finished with files missing"), ("warning", "2 not fetched")))):
|
||||
with self.subTest(counts=counts):
|
||||
plain, coloured = self._end(False, *counts, **given), \
|
||||
self._end(True, *counts, **given)
|
||||
self.assertNotIn("\x1b", plain)
|
||||
self.assertEqual(install.unpainted(coloured), plain)
|
||||
for role, word in painted:
|
||||
self.assertIn(install._sgr(role) + word, coloured)
|
||||
|
||||
|
||||
class SmallWordings(unittest.TestCase):
|
||||
def test_check_asks_which_to_check(self):
|
||||
"""--check with several platforms asked "Install for:", and
|
||||
@@ -1174,11 +1205,17 @@ class ADetectedClosedFolderSaysWhereItCameFrom(unittest.TestCase):
|
||||
said = " ".join(proc.stderr.split())
|
||||
self.assertIn(f"than the one {self.cfg} names (name another with --dest)", said)
|
||||
|
||||
def test_nothing_installed_comes_after_the_error(self):
|
||||
def test_two_setups_stop_before_either_folder_is_read(self):
|
||||
"""code_audit_18 P2-3: a run without questions installs for one
|
||||
setup, so two found stop on what to name before either folder is
|
||||
read, the closed one included."""
|
||||
proc = self.run_detected(True)
|
||||
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
|
||||
error = proc.stderr.index("Error: ")
|
||||
self.assertGreater(proc.stderr.index("Nothing was installed."), error, proc.stderr)
|
||||
said = " ".join(proc.stderr.split())
|
||||
self.assertIn("2 setups were found here, and a run without questions installs for "
|
||||
"one", said)
|
||||
self.assertIn("--platform emudeck --dest", said)
|
||||
self.assertNotIn("Downloading", proc.stdout)
|
||||
|
||||
def test_an_assumed_default_reads_plainly(self):
|
||||
self.assertIn("used by default, since {display_name(name)} was not found",
|
||||
|
||||
@@ -828,8 +828,8 @@ class OfflineLeavesFilesMissing(Scratch):
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertNotIn("Would download", proc.stdout)
|
||||
self.assertIn("4 files (", proc.stdout)
|
||||
self.assertIn("need the internet, which --offline does not use. Without --offline "
|
||||
"they would go to", proc.stdout)
|
||||
self.assertIn("need the internet, which --offline does not use. Run without "
|
||||
"--offline, and they would go to", proc.stdout)
|
||||
|
||||
def test_an_archive_not_read_is_only_its_error(self):
|
||||
listed = manifest()
|
||||
@@ -967,7 +967,8 @@ class AuditFifteenOffline(Scratch):
|
||||
# The screen reads that summary as missing files, not failures.
|
||||
state = install.TuiState()
|
||||
state.summary = summary
|
||||
with unittest.mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}):
|
||||
with unittest.mock.patch.dict(os.environ,
|
||||
{install.OFFLINE_VARIABLE: install.OFFLINE_FLAG_VALUE}):
|
||||
title, lines = install.tui_outcome(state)
|
||||
self.assertEqual(title, "Finished with files missing")
|
||||
self.assertIn("1 installed, 3 not fetched (--offline).", lines[0])
|
||||
|
||||
@@ -612,7 +612,8 @@ class TheOneLinerNarrows(unittest.TestCase):
|
||||
with FakeRepository(selection=False) as repo:
|
||||
proc = repo.run("--region", "us")
|
||||
self.assertEqual(proc.returncode, 1)
|
||||
self.assertIn("records no systems or regions", proc.stderr)
|
||||
self.assertIn("records no regions, so --region cannot narrow it", proc.stderr)
|
||||
self.assertIn("older release", proc.stderr)
|
||||
self.assertEqual(repo.installed(), [])
|
||||
|
||||
def test_a_pinned_ref_is_named_as_the_way_out(self):
|
||||
@@ -675,18 +676,19 @@ class TheOneLinerNarrows(unittest.TestCase):
|
||||
|
||||
@unittest.skipUnless(shutil.which("script"), "script(1) provides the terminal")
|
||||
def test_the_custom_mode_at_a_terminal(self):
|
||||
"""Typed answers: customise, PlayStation only, no core, US first."""
|
||||
"""Typed answers: customise, PlayStation only, US first. Its one
|
||||
core is not asked about (tui_audit_11 P3-10)."""
|
||||
with FakeRepository() as repo:
|
||||
command = " ".join(shlex.quote(part) for part in repo.command())
|
||||
proc = subprocess.run(
|
||||
["script", "-qec", command, "/dev/null"],
|
||||
input="c\n2\n\n2\n\n", env=repo.env(), capture_output=True,
|
||||
input="c\n2\n2\n\n", env=repo.env(), capture_output=True,
|
||||
text=True, timeout=60, check=False,
|
||||
)
|
||||
out = proc.stdout
|
||||
self.assertEqual(proc.returncode, 0, out + proc.stderr)
|
||||
self.assertIn("choose systems, cores and regions", out)
|
||||
self.assertIn("pcsx_rearmed", out)
|
||||
self.assertNotIn("Cores:", out)
|
||||
self.assertIn("--system sony-playstation --region north-america", out)
|
||||
self.assertEqual(repo.installed(), ["pcsx/extra.bin", "scph5501.bin"])
|
||||
|
||||
@@ -1019,7 +1021,7 @@ class AHeldFileTheInstallerCannotFetch(unittest.TestCase):
|
||||
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
|
||||
Path("platforms").mkdir()
|
||||
Path("platforms/p.yml").write_text(json.dumps({
|
||||
"platform": "P", "verification_mode": "existence", "cores": ["core"],
|
||||
"platform": "P", "version": "2.1", "verification_mode": "existence", "cores": ["core"],
|
||||
"systems": {"sys": {"files": []}},
|
||||
}))
|
||||
Path("platforms/_registry.yml").write_text(
|
||||
@@ -1094,7 +1096,7 @@ class AStandaloneLayoutBelowTheBiosFolder(unittest.TestCase):
|
||||
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
|
||||
Path("platforms").mkdir()
|
||||
Path("platforms/p.yml").write_text(json.dumps({
|
||||
"platform": "P", "verification_mode": "existence",
|
||||
"platform": "P", "version": "2.1", "verification_mode": "existence",
|
||||
"base_destination": "bios", "cores": ["dsa", "other"],
|
||||
"standalone_cores": ["dsa"], "systems": {"sys": {"files": []}},
|
||||
}))
|
||||
|
||||
+1967
-82
File diff suppressed because it is too large.
Load diff
@@ -458,17 +458,17 @@ class SitePages(unittest.TestCase):
|
||||
cases = [
|
||||
({"subject": "mpeg-card-lunar-cr2-build", "variable": "mpeg_card.bin",
|
||||
"content": "Redump: Lunar - Silver Star Story Complete (Japan), T-27904G V1.00"},
|
||||
"`mpeg_card.bin`, Lunar (CR2 build)"),
|
||||
"mpeg_card.bin, Lunar (CR2 build)"),
|
||||
({"subject": "negative-scph5501-bin-mednafen-psx-bust-a-move-2-arcade-edition-usa",
|
||||
"variable": "scph5501.bin", "content": "Bust-A-Move 2 - Arcade Edition (USA)",
|
||||
"core": {"file": "mednafen_psx_libretro.so"}},
|
||||
"`scph5501.bin`, Bust-A-Move 2 (wrong files)"),
|
||||
"scph5501.bin, Bust-A-Move 2 (wrong files)"),
|
||||
({"subject": "issue85-lunar-opening-movie", "variable": None,
|
||||
"content": "Redump: Lunar - Silver Star Story Complete (Japan), T-27904G V1.00"},
|
||||
"Lunar (issue 85 opening movie)"),
|
||||
({"subject": "with-command-auth-device-and-command-get-9a211715", "variable": "mpeg_card.bin",
|
||||
"content": "Redump: Lunar"},
|
||||
"`mpeg_card.bin`, Lunar (with command auth device and get)"),
|
||||
"mpeg_card.bin, Lunar (with command auth device and get)"),
|
||||
]
|
||||
for item, title in cases:
|
||||
self.assertEqual(sm.short_title(item), title)
|
||||
@@ -498,6 +498,29 @@ class SitePages(unittest.TestCase):
|
||||
# Alone in its file, a test is named by its game.
|
||||
self.assertEqual(sm.distinct_titles(items[:1]), {"a": "Lunar"})
|
||||
|
||||
def test_run_output_is_text_never_markup(self) -> None:
|
||||
nasty = "`code` {expr} <script>alert(1)</script> [x](javascript:alert(1)) \"q\" 'q' **b**"
|
||||
item = self.item(
|
||||
claim=nasty, method=nasty, statement=nasty, host=nasty, host_short=nasty, content=nasty,
|
||||
variable=f"{nasty}.bin", subject="s", title=nasty, proves=[nasty], runs=2, flags=1,
|
||||
core={"library": nasty, "commit": "{`<x>`}", "file": f"{nasty}_libretro.so"},
|
||||
groups=[{"variant": nasty, "group": 0, "result": nasty, "absent": False}],
|
||||
record_url="javascript:alert(1)")
|
||||
bench = self.item(id="b", kind="bench", status="measured", subject="bench", title=nasty,
|
||||
content=nasty, record_url="java\nscript:x",
|
||||
timing={"ms_per_frame": 1.0, "fps": 1.0, "stdev": 0.0})
|
||||
earlier = self.item(id="e", status="no verdict", subject="e", content=nasty, claim=nasty)
|
||||
text = "\n".join(sm.tested_section("alpha", [item, bench, earlier]))
|
||||
self.assertNotIn("<script", text)
|
||||
self.assertNotRegex(text, r'href="(?!https?://|/)')
|
||||
self.assertNotIn("javascript:alert(1)\"", text)
|
||||
self.assertNotRegex(text, r"[{}]")
|
||||
self.assertNotIn("<strong>b</strong>", text)
|
||||
self.assertNotIn("<code>code</code>", text)
|
||||
self.assertNotRegex(text, r'="[^"]*"q"')
|
||||
self.assertIn("<script>", text)
|
||||
self.assertIn("{expr}", text)
|
||||
|
||||
def test_no_empty_sentence(self) -> None:
|
||||
text = "\n".join(sm.tested_section("beetle_saturn", [self.item(claim=None, method=None, statement="",
|
||||
kind="run", status="ran")]))
|
||||
|
||||
@@ -193,6 +193,7 @@ class PackPlacesCoreFilesUnderTheTag(unittest.TestCase):
|
||||
|
||||
platform = {
|
||||
"platform": "MinUI Test",
|
||||
"version": "2025-11-27",
|
||||
"base_destination": "",
|
||||
"verification_mode": "existence",
|
||||
"cores": ["gambatte", "arcadecore", "unlisted"],
|
||||
|
||||
@@ -92,7 +92,7 @@ class _Fixture(unittest.TestCase):
|
||||
for key, profile in PROFILES.items():
|
||||
(self.emulators / f"{key}.yml").write_text(yaml.dump(profile))
|
||||
platform = {
|
||||
"platform": "Handheld", "verification_mode": "existence",
|
||||
"platform": "Handheld", "version": "2.1", "verification_mode": "existence",
|
||||
"cores": ["emu", "doomcore"],
|
||||
"systems": {
|
||||
"console": {"files": [{"name": "firmware.rom", "destination": "firmware.rom"}]},
|
||||
@@ -289,7 +289,8 @@ class InstallerLeavesGameDataOut(unittest.TestCase):
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertTrue((self.bios / "a.bin").exists())
|
||||
self.assertFalse((self.bios / "game.wad").exists())
|
||||
self.assertIn("Left out as game data (--no-game-data): 2 files: game.wad, level.dat",
|
||||
self.assertIn("Left out as game data (--no-game-data): 2 files, 1 of them not installable: "
|
||||
"game.wad, level.dat",
|
||||
proc.stdout)
|
||||
|
||||
def test_check_and_dry_run_leave_it_out(self):
|
||||
@@ -299,7 +300,8 @@ class InstallerLeavesGameDataOut(unittest.TestCase):
|
||||
with self.subTest(mode=mode):
|
||||
proc = self.run_installer("--no-game-data", mode)
|
||||
self.assertIn(
|
||||
"Left out as game data (--no-game-data): 2 files: game.wad, level.dat",
|
||||
"Left out as game data (--no-game-data): 2 files, 1 of them not installable: "
|
||||
"game.wad, level.dat",
|
||||
proc.stdout,
|
||||
)
|
||||
self.assertIn(listed, proc.stdout)
|
||||
|
||||
@@ -52,6 +52,7 @@ files:
|
||||
|
||||
PLATFORM = {
|
||||
"platform": "Demo",
|
||||
"version": "2.1",
|
||||
"verification_mode": "existence",
|
||||
"base_destination": "",
|
||||
"cores": ["demo"],
|
||||
@@ -494,7 +495,7 @@ class RefusedFilesAreNotPresent(unittest.TestCase):
|
||||
}
|
||||
db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
|
||||
config = {
|
||||
"platform": "Hashy", "verification_mode": "md5", "base_destination": "bios",
|
||||
"platform": "Hashy", "version": "2.1", "verification_mode": "md5", "base_destination": "bios",
|
||||
"systems": {"sys": {"files": [
|
||||
{"name": "good.bin", "destination": "good.bin",
|
||||
"md5": hashlib.md5(b"good.bin").hexdigest()},
|
||||
|
||||
@@ -60,9 +60,29 @@ from profile_sync import (
|
||||
worst_status,
|
||||
)
|
||||
import upstream
|
||||
import version_basis
|
||||
from upstream import CompareResult, FileChange
|
||||
|
||||
|
||||
def _version_read_as_written(profile, cache_dir, offline, profiles=None, recompute=False):
|
||||
"""bump_commit reads core_version again at the new pin. These fixtures
|
||||
have no forge behind them, so the version reads as written here:
|
||||
tests/test_version_basis.py holds the reading and the refusals."""
|
||||
stated = version_basis.declared(profile, "")
|
||||
return [version_basis.Reading("", "file", stated, stated, "fixture", version_basis.MATCH)]
|
||||
|
||||
|
||||
_VERSION_AS_WRITTEN = mock.patch.object(version_basis, "check", _version_read_as_written)
|
||||
|
||||
|
||||
def setUpModule():
|
||||
_VERSION_AS_WRITTEN.start()
|
||||
|
||||
|
||||
def tearDownModule():
|
||||
_VERSION_AS_WRITTEN.stop()
|
||||
|
||||
|
||||
class TestParseSourceRef(unittest.TestCase):
|
||||
def test_path_with_single_line(self):
|
||||
self.assertEqual(
|
||||
|
||||
@@ -51,6 +51,7 @@ class RequiredTwins(unittest.TestCase):
|
||||
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
|
||||
platform = {
|
||||
"platform": "Twins",
|
||||
"version": "2.1",
|
||||
"verification_mode": "md5",
|
||||
"base_destination": "bios",
|
||||
"cores": [],
|
||||
|
||||
@@ -243,7 +243,7 @@ class _Fixture(unittest.TestCase):
|
||||
"required": False}]}
|
||||
for name in ("testra", "testnosave"):
|
||||
(self.platforms / f"{name}.yml").write_text(yaml.safe_dump({
|
||||
"platform": name, "verification_mode": "existence",
|
||||
"platform": name, "version": "2.1", "verification_mode": "existence",
|
||||
"base_destination": "system", "cores": ["corex"],
|
||||
"systems": {"test-console": system},
|
||||
}))
|
||||
@@ -399,7 +399,7 @@ class AFolderPerSystem(_Fixture):
|
||||
"required": False}]}
|
||||
for name in ("testbato", "testrb"):
|
||||
(self.platforms / f"{name}.yml").write_text(yaml.safe_dump({
|
||||
"platform": name, "verification_mode": "existence",
|
||||
"platform": name, "version": "2.1", "verification_mode": "existence",
|
||||
"base_destination": "bios", "cores": ["corex", "corey"],
|
||||
"systems": {"test-console": system},
|
||||
# Two systems run CoreX, none runs CoreY.
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
"""File cards and install boxes read at a glance (density round, 2026-10-27).
|
||||
|
||||
A system page wrote every setup's install command for each console, twice:
|
||||
12 long lines for the PSP. A file card showed four full hashes, the
|
||||
collection path and the catalogue records before the next file. Now a box
|
||||
shows one command for the setup picked, and a card shows the name, the
|
||||
size, the download, a short SHA1, the setups and the emulators. The rest
|
||||
sits in closed folds, every fact still on the page.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
import generate_site as gs # noqa: E402
|
||||
import sitemarkup as markup # noqa: E402
|
||||
from cross_reference import FileConsumers # noqa: E402
|
||||
|
||||
DIST = REPO_ROOT / "website" / "dist"
|
||||
SITE_TS = REPO_ROOT / "website" / "src" / "scripts" / "site.ts"
|
||||
|
||||
SHA1 = "30a33f3651f8e1b0967a5dfe6ff714baebcce41a"
|
||||
MD5 = "866855cc330b9b95cc69135fb7b41d38"
|
||||
CRC32 = "7b57fa78"
|
||||
SHA256 = "3ab7fcf6956d0281ada94d501fc6d1cfa8fc034f139e25fed6c829f723d92a95"
|
||||
|
||||
|
||||
class _Visible(HTMLParser):
|
||||
"""Words shown before any fold is opened: text outside closed details
|
||||
(their summary counts), hidden elements, scripts and styles."""
|
||||
|
||||
VOID = {"br", "img", "input", "wbr", "hr", "meta", "link", "source"}
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(convert_charrefs=True)
|
||||
self.stack: list[tuple[str, object]] = []
|
||||
self.hidden = 0
|
||||
self.words: list[str] = []
|
||||
|
||||
def handle_starttag(self, tag: str, attrs: list) -> None:
|
||||
if tag in self.VOID:
|
||||
return
|
||||
names = dict(attrs)
|
||||
if tag == "summary" and self.stack and self.stack[-1] == ("details", True):
|
||||
self.hidden -= 1
|
||||
self.stack.append((tag, "shown"))
|
||||
return
|
||||
hides = tag in ("script", "style") or "hidden" in names or (
|
||||
tag == "details" and "open" not in names)
|
||||
self.hidden += hides
|
||||
self.stack.append((tag, hides))
|
||||
|
||||
def handle_endtag(self, tag: str) -> None:
|
||||
while self.stack:
|
||||
name, hides = self.stack.pop()
|
||||
if hides == "shown":
|
||||
self.hidden += 1
|
||||
elif hides:
|
||||
self.hidden -= 1
|
||||
if name == tag:
|
||||
break
|
||||
|
||||
def handle_data(self, data: str) -> None:
|
||||
if not self.hidden:
|
||||
self.words += data.split()
|
||||
|
||||
|
||||
def visible_words(html_text: str) -> list[str]:
|
||||
parser = _Visible()
|
||||
parser.feed(html_text)
|
||||
return parser.words
|
||||
|
||||
|
||||
def _main(page: str) -> str:
|
||||
text = (DIST / page / "index.html").read_text(encoding="utf-8")
|
||||
return text[text.index("<main"):text.index("</main>")]
|
||||
|
||||
|
||||
def _card_consumers(sha1: str) -> types.SimpleNamespace:
|
||||
return types.SimpleNamespace(
|
||||
platforms={sha1: {"batocera", "retroarch"}}, emulators={sha1: {"ppsspp"}},
|
||||
platform_systems={sha1: {"batocera": {"sony-psp"}, "retroarch": {"sony-psp"}}},
|
||||
platform_destinations={sha1: {"batocera": {"psp/ppge_atlas.zim"},
|
||||
"retroarch": {"PPSSPP/ppge_atlas.zim"}}},
|
||||
installed={sha1: {"knulli": {"PPSSPP/ppge_atlas.zim"}}},
|
||||
emulator_entries={}, emulator_files={},
|
||||
)
|
||||
|
||||
|
||||
def _card() -> dict:
|
||||
return {"name": "ppge_atlas.zim", "sha1": SHA1, "md5": MD5, "crc32": CRC32,
|
||||
"sha256": SHA256, "size": 666539,
|
||||
"path": "bios/Sony/PlayStation Portable/ppge_atlas.zim",
|
||||
"provenance": {"tosec": {"name": "ppge_atlas (PPSSPP).zim"}}}
|
||||
|
||||
|
||||
class ASystemCardShowsTheAnswerFirst(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.lines = gs._system_file_card(_card(), _card_consumers(SHA1))
|
||||
self.text = "\n".join(self.lines)
|
||||
self.fold = self.text[self.text.index("<details"):self.text.index("</details>")]
|
||||
self.shown = self.text.replace(self.fold, "")
|
||||
|
||||
def test_the_open_part(self) -> None:
|
||||
for fact in ("<code>ppge_atlas.zim</code>", ">650.9 KB<", ">Download</a>",
|
||||
"<dt>On the setup's list</dt>", "<dt>Packed for its emulators by</dt>",
|
||||
"<dt>Emulators</dt>", f'<dt>SHA1</dt><dd class="rb-hash">{SHA1[:12]}</dd>'):
|
||||
self.assertIn(fact, self.shown)
|
||||
|
||||
def test_every_hash_whole_in_one_fold_with_the_rest(self) -> None:
|
||||
for label, value in (("SHA1", SHA1), ("MD5", MD5), ("CRC32", CRC32), ("SHA256", SHA256)):
|
||||
self.assertIn(f'<dt>{label}</dt><dd class="rb-hash">{value}</dd>', self.fold)
|
||||
shown = " ".join(visible_words(self.text))
|
||||
for value in (MD5, CRC32, SHA256, SHA1):
|
||||
self.assertNotIn(value, shown)
|
||||
self.assertIn("<dt>Collection path</dt>", self.fold)
|
||||
self.assertIn("<dt>Catalogued</dt>", self.fold)
|
||||
self.assertEqual(self.text.count("<details"), 1)
|
||||
self.assertIn("<summary>Hashes, collection path and catalogues</summary>", self.fold)
|
||||
|
||||
def test_the_fold_is_out_of_the_search(self) -> None:
|
||||
self.assertTrue(self.fold.startswith("<details data-pagefind-ignore>\n<summary>"))
|
||||
|
||||
def test_no_fact_is_lost(self) -> None:
|
||||
# The yardstick list a page is cut by holds every fact: the
|
||||
# Starlight card holds each of its values.
|
||||
with markup.using("mkdocs"):
|
||||
yardstick = "\n".join(gs._system_file_card(_card(), _card_consumers(SHA1)))
|
||||
for label, values in re.findall(r"(?m)^- ([^:]+): (.*)$", yardstick):
|
||||
self.assertIn(f"<dt>{label}</dt>", self.text)
|
||||
for value in re.findall(r"[0-9a-f]{8,64}|`[^`]+`", values):
|
||||
self.assertIn(value.strip("`"), self.text, label)
|
||||
|
||||
def test_a_card_reads_in_few_words(self) -> None:
|
||||
# Measured on this card: 31 words open, 40 before the round.
|
||||
self.assertLessEqual(len(visible_words(self.text)), min(40, int(31 * 1.05)))
|
||||
|
||||
|
||||
class AnEmulatorCardFoldsItsHashes(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
entry = {"name": "scph5501.bin", "md5": "490f666e1afb15b7362b406ed1cea246",
|
||||
"sha1": "b05def971d8ec59f346f2d9ac21fb742e3eb6917", "size": 524288,
|
||||
"required": True, "note": "Read at boot.", "priority": 5,
|
||||
"validation": ["md5"], "source_ref": "src/core/bios.cpp:47"}
|
||||
profile = {"emulator": "DuckStation", "files": [entry], "systems": ["sony-playstation"]}
|
||||
self.text = "\n".join(gs._render_emulator_file(
|
||||
entry, profile, None, profile["files"], lambda _f: True, anchor="file-scph5501-bin"))
|
||||
self.fold = self.text[self.text.index("<details"):self.text.index("</details>")]
|
||||
self.shown = self.text.replace(self.fold, "")
|
||||
|
||||
def test_a_short_hash_open_and_every_hash_folded(self) -> None:
|
||||
# On the size's line.
|
||||
self.assertIn('- Size: 512.0 KB, SHA1 <span class="rb-hash" '
|
||||
'data-copy="b05def971d8ec59f346f2d9ac21fb742e3eb6917">b05def971d8e</span>',
|
||||
self.shown)
|
||||
self.assertIn('<code class="rb-hash" data-copy="">490f666e1afb15b7362b406ed1cea246</code>',
|
||||
self.fold)
|
||||
self.assertNotIn("490f666e1afb15b7362b406ed1cea246", self.shown)
|
||||
|
||||
def test_notes_open_the_rest_folded(self) -> None:
|
||||
self.assertIn("Read at boot.", self.shown)
|
||||
for fact in ("Priority: 5", "Validation: md5", "Source:"):
|
||||
self.assertIn(fact, self.fold)
|
||||
self.assertNotIn(fact, self.shown)
|
||||
self.assertIn("<summary>Hashes, priority, validation and more</summary>", self.fold)
|
||||
|
||||
def test_the_markdown_yardstick_is_unchanged(self) -> None:
|
||||
with markup.using("mkdocs"):
|
||||
entry = {"name": "a.bin", "md5": "b" * 32, "size": 4}
|
||||
text = "\n".join(gs._render_emulator_file(
|
||||
entry, {"files": [entry]}, None, [entry], lambda _f: True))
|
||||
self.assertNotIn("<details", text)
|
||||
self.assertIn("- MD5: ", text)
|
||||
|
||||
|
||||
PAIRS = [("emudeck", "sony-psp"), ("retroarch", "sony-psp"), ("retrobat", "Neo Geo")]
|
||||
|
||||
|
||||
class AnInstallBoxShowsOneCommand(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.text = "\n".join(gs._console_install_block("PlayStation Portable", PAIRS))
|
||||
|
||||
def test_one_command_per_tab_for_the_setup_picked(self) -> None:
|
||||
tabs = self.text[self.text.index("<Tabs"):self.text.index("</Tabs>")]
|
||||
self.assertEqual(re.findall(r'<TabItem label="([^"]+)"', tabs),
|
||||
["Windows", "macOS", "Linux", "Steam Deck"])
|
||||
self.assertEqual(tabs.count("--platform"), 4)
|
||||
self.assertEqual(tabs.count("--platform emudeck --system sony-psp"), 4)
|
||||
|
||||
def test_the_setup_list_carries_every_command(self) -> None:
|
||||
select = re.search(r"<select>(.*?)</select>", self.text).group(1)
|
||||
options = re.findall(r'<option value="([^"]+)" data-sh="([^"]+)" data-ps="([^"]+)">', select)
|
||||
self.assertEqual([value for value, _sh, _ps in options], ["emudeck", "retroarch", "retrobat"])
|
||||
self.assertIn(("retrobat", "--platform retrobat --system 'Neo Geo'",
|
||||
"--platform retrobat --system 'Neo Geo'"), options)
|
||||
|
||||
def test_without_script_the_list_of_every_command_shows(self) -> None:
|
||||
self.assertIn('<div class="rb-install" data-rb-install="" hidden>', self.text)
|
||||
listed = self.text[self.text.index('<details class="rb-install-all" open>'):]
|
||||
for platform, _system in PAIRS:
|
||||
self.assertEqual(listed.count(f"--platform {platform} --system"), 2, platform)
|
||||
self.assertIn("install.sh | sh -s -- --platform retrobat --system 'Neo Geo'</code>", listed)
|
||||
self.assertIn("install.ps1))) --platform retrobat --system 'Neo Geo'</code>", listed)
|
||||
# The URL is no link inside the command, and the break adds nothing.
|
||||
self.assertIn("https://<wbr>raw.githubusercontent.com", listed)
|
||||
|
||||
def test_one_setup_keeps_its_two_blocks(self) -> None:
|
||||
text = "\n".join(gs._console_install_block("PlayStation Portable", PAIRS[:1]))
|
||||
self.assertNotIn("<select", text)
|
||||
self.assertIn('```sh title="Terminal"', text)
|
||||
self.assertIn('```powershell title="PowerShell"', text)
|
||||
|
||||
def test_the_yardstick_is_unchanged(self) -> None:
|
||||
# The budgets that cut a system page are measured on this Markdown:
|
||||
# a box that changed it would move cards to other pages.
|
||||
with markup.using("mkdocs"):
|
||||
text = "\n".join(gs._console_install_block("PlayStation Portable", PAIRS))
|
||||
self.assertNotIn("<select", text)
|
||||
self.assertEqual(text.count("# RetroBat"), 2)
|
||||
self.assertEqual(text.count("install.sh | sh"), 3)
|
||||
|
||||
|
||||
class TheSetupListLooksLikeAList(unittest.TestCase):
|
||||
"""The setup list had a rule of its own without the arrow, and read as
|
||||
a text field at 390 and 1440 px."""
|
||||
|
||||
def test_the_select_takes_the_site_arrow(self) -> None:
|
||||
css = (REPO_ROOT / "website" / "src" / "styles" / "retrobios.css").read_text(encoding="utf-8")
|
||||
shared = re.search(r"\.sl-markdown-content select \{([^}]*)\}", css)
|
||||
self.assertIsNotNone(shared)
|
||||
self.assertIn("appearance: none", shared.group(1))
|
||||
self.assertIn("var(--rb-select-arrow)", shared.group(1))
|
||||
# No rule for the install box's select overrides it.
|
||||
self.assertNotRegex(css, r"rb-install-setup select")
|
||||
# The arrow is defined for both themes.
|
||||
self.assertEqual(len(re.findall(r"--rb-select-arrow:", css)), 2)
|
||||
# The box's select sits in Markdown content, where the shared rule applies.
|
||||
box = "\n".join(gs._console_install_block("PlayStation Portable", PAIRS))
|
||||
self.assertIn('<label class="rb-install-setup">Your setup <select>', box)
|
||||
|
||||
def test_the_built_select_shows_the_arrow(self) -> None:
|
||||
if not (DIST / "systems").is_dir():
|
||||
self.skipTest("no built site")
|
||||
css = "".join(path.read_text(encoding="utf-8") for path in (DIST / "_astro").glob("*.css"))
|
||||
self.assertNotIn("rb-install-setup select", css)
|
||||
self.assertRegex(css, r"\.sl-markdown-content select\{[^}]*--rb-select-arrow")
|
||||
|
||||
|
||||
class TheScriptWiresBoth(unittest.TestCase):
|
||||
def test_pickers_and_copy_buttons_run(self) -> None:
|
||||
source = SITE_TS.read_text(encoding="utf-8")
|
||||
self.assertIn("\ninstallPickers();\n", source)
|
||||
self.assertIn("\ncopyButtons();\n", source)
|
||||
self.assertIn("const SETUP_KEY = 'rb-setup';", source)
|
||||
self.assertIn("const COPYABLE = '[data-copy]:not(button), .rb-sys-file dd.rb-hash';", source)
|
||||
# A platform row's button carries its own data-copy: never given a second button.
|
||||
self.assertIn("const own = button.dataset.copy;", source)
|
||||
|
||||
|
||||
# Visible words of each built page: measured before the round and after it,
|
||||
# the ceiling being the after value with 5 % of room.
|
||||
WORD_CEILINGS = {
|
||||
"systems/sony": (2012, 1407),
|
||||
"systems/sony-playstation": (2709, 2130),
|
||||
"systems/nintendo": (3598, 2941),
|
||||
"systems/sega": (4151, 3370),
|
||||
"emulators/duckstation": (1188, 1117),
|
||||
"emulators/yaps2": (3159, 2655),
|
||||
}
|
||||
|
||||
|
||||
class BuiltPagesReadInFewerWords(unittest.TestCase):
|
||||
def test_visible_words_under_the_ceiling(self) -> None:
|
||||
if not (DIST / "systems").is_dir():
|
||||
self.skipTest("no built site")
|
||||
for page, (before, after) in WORD_CEILINGS.items():
|
||||
with self.subTest(page=page):
|
||||
ceiling = min(before, int(after * 1.05))
|
||||
self.assertLessEqual(len(visible_words(_main(page))), ceiling)
|
||||
|
||||
def test_a_built_card_holds_its_hashes(self) -> None:
|
||||
if not (DIST / "systems").is_dir():
|
||||
self.skipTest("no built site")
|
||||
main = _main("systems/sony")
|
||||
card = main[main.index('id="sha1-30a33f3651f8"'):]
|
||||
card = card[:card.index('class="rb-sys-file"')]
|
||||
self.assertRegex(card, r'<dd class="rb-hash">30a33f3651f8</dd>')
|
||||
self.assertIn('<details data-pagefind-ignore', card)
|
||||
self.assertIn(">30a33f3651f8e1b0967a5dfe6ff714baebcce41a</dd>", card)
|
||||
# No link inside a command of the list: the URL is text.
|
||||
listed = main[main.index("rb-install-all"):]
|
||||
self.assertNotRegex(listed[:listed.index("</details>")], r"<code[^>]*>[^<]*<a ")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,311 @@
|
||||
"""Long lists read as a table first, details on demand (density round).
|
||||
|
||||
MAME's card pages read 13,000 to 16,700 words, card after card, and
|
||||
GroovyMAME 14,400 before its last entry. The TOSEC wanted list was 46
|
||||
closed blocks, one per DAT, opened one by one to find a name. Each entry is
|
||||
now a table row that opens to its card, and each wanted list one filtered
|
||||
table under its counts. Nothing the pages said is lost: the tests below read
|
||||
every name and hash back.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
import generate_site as gs # noqa: E402
|
||||
import siteentries # noqa: E402
|
||||
import validate_site # noqa: E402
|
||||
|
||||
EMPTY_DB = {"files": {}, "indexes": {"by_name": {}}}
|
||||
DIST = REPO_ROOT / "website" / "dist"
|
||||
|
||||
|
||||
def _archive(i: int, members: int = 3, **fields: object) -> dict:
|
||||
return {
|
||||
"name": f"set{i:04d}.zip", "required": True, "category": "bios_zip",
|
||||
"contents": [{"name": f"m{i}_{k}.rom", "size": 1024, "crc32": f"{i:04x}{k:04x}",
|
||||
"sha1": f"{i:04x}{k:04x}" * 5} for k in range(members)],
|
||||
**fields,
|
||||
}
|
||||
|
||||
|
||||
class TheEntryTable(unittest.TestCase):
|
||||
def _rows(self, count: int) -> list[siteentries.Row]:
|
||||
return [siteentries.Row(f'<h3 id="file-{i}" class="rb-file-title">n{i}</h3>',
|
||||
[f"- Size: {i} bytes"], [str(i), "", f"MD5 <code>{i}</code>"])
|
||||
for i in range(count)]
|
||||
|
||||
def test_a_long_table_has_its_filter_box_and_stacks(self):
|
||||
lines = siteentries.entry_table(self._rows(25), gs.ENTRY_HEADS, "rb-entry-filter-x", "Required")
|
||||
text = "\n".join(lines)
|
||||
self.assertRegex(text, r'<div class="rb-table-filter"><label for="rb-entry-filter-x">'
|
||||
r"Filter Required</label><input id=\"rb-entry-filter-x\" type=\"search\"")
|
||||
self.assertIn('aria-live="polite">25 rows</span></div>\n<table class="rb-entries" '
|
||||
'data-rb-stack="true">', text)
|
||||
# The Status column is empty on every row: not written.
|
||||
self.assertIn('<th scope="col">File</th><th scope="col">Contents</th>'
|
||||
'<th scope="col">Hash</th>', text)
|
||||
self.assertNotIn("Status", text)
|
||||
self.assertEqual(text.count('<details class="rb-entry">'), 25)
|
||||
# Labels are written by site.ts on a table this long, as on a Markdown one.
|
||||
self.assertNotIn("data-label", text)
|
||||
|
||||
def test_a_card_opens_under_its_name_as_markdown(self):
|
||||
lines = siteentries.entry_table(self._rows(2), gs.ENTRY_HEADS, "x", "Required")
|
||||
at = lines.index('<summary><h3 id="file-0" class="rb-file-title">n0</h3></summary>')
|
||||
# A blank line after the summary: the body is read as Markdown.
|
||||
self.assertEqual(lines[at + 1:at + 4], ["", "- Size: 0 bytes", ""])
|
||||
self.assertNotIn("rb-table-filter", "\n".join(lines))
|
||||
self.assertIn('<td data-label="Contents" data-pagefind-ignore>1</td>', lines)
|
||||
|
||||
def test_a_card_loses_only_its_wrapper(self):
|
||||
card = ['<div class="rb-file-entry">', "", '<h3 id="a">x</h3>', "", "Desc.", "",
|
||||
'<div class="rb-search-skip">', "", "- SHA1: s", "", "</div>", "", "</div>", ""]
|
||||
title, body = siteentries.card_title_and_body(card)
|
||||
self.assertEqual(title, '<h3 id="a">x</h3>')
|
||||
self.assertEqual(body, ["Desc.", "", '<div class="rb-search-skip">', "", "- SHA1: s", "",
|
||||
"</div>"])
|
||||
|
||||
def test_the_placeholder_reads_like_the_plugin(self):
|
||||
self.assertEqual(siteentries.filter_placeholder(["File", "Contents", "Status", "Hash"]),
|
||||
"File, contents, status, hash...")
|
||||
self.assertEqual(siteentries.filter_placeholder(["Name", "DAT", "Description", "SHA1"]),
|
||||
"Name, DAT, description, SHA1...")
|
||||
|
||||
|
||||
class ASplitEmulatorListsItsCardsAsRows(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.files = [_archive(i, members=1 + i % 4) for i in range(gs.EMULATOR_CARDS_PER_PAGE * 2)]
|
||||
self.pages = gs.emulator_pages(
|
||||
"big", {"emulator": "Big", "type": "standalone", "files": self.files}, EMPTY_DB)
|
||||
|
||||
def test_parts_hold_at_most_a_part_of_cards(self):
|
||||
parts = gs.emulator_parts("big", self.files)
|
||||
self.assertEqual(len(parts), -(-len(self.files) // gs.EMULATOR_CARDS_PER_PART))
|
||||
self.assertLessEqual(max(len(entries) for _, entries in parts), gs.EMULATOR_CARDS_PER_PART)
|
||||
|
||||
def test_every_name_and_member_hash_is_on_its_part(self):
|
||||
parts = {stem: page for stem, page in self.pages.items() if stem != "big"}
|
||||
anchors = gs.emulator_file_anchors(self.files)
|
||||
for stem, entries in gs.emulator_parts("big", self.files):
|
||||
page = parts[stem]
|
||||
self.assertIn('<table class="rb-entries" data-rb-stack="true">', page)
|
||||
self.assertNotIn('class="rb-file-entry"', page)
|
||||
for i in entries:
|
||||
f = self.files[i]
|
||||
self.assertIn(f'<summary><h3 id="{anchors[i]}" class="rb-file-title"', page)
|
||||
for member in f["contents"]:
|
||||
self.assertIn(f"`{member['name']}`", page)
|
||||
self.assertIn(f"CRC32 `{member['crc32']}`", page)
|
||||
self.assertIn(f"SHA1 `{member['sha1']}`", page)
|
||||
# The row says how many members the archive holds.
|
||||
self.assertIn(f"data-pagefind-ignore>{len(f['contents'])}</td>", page)
|
||||
|
||||
def test_the_section_badge_is_not_repeated_on_each_row(self):
|
||||
page = self.pages["big-files-1"]
|
||||
self.assertIn("## Required", page)
|
||||
self.assertNotIn(">required</span>", page)
|
||||
self.assertIn(">missing</span>", page)
|
||||
|
||||
|
||||
class ALongEmulatorPageIsOneTable(unittest.TestCase):
|
||||
def test_rows_replace_the_index_and_the_cards(self):
|
||||
files = ([_archive(i) for i in range(gs.EMULATOR_TABLE_MIN - 10)]
|
||||
+ [{"name": f"opt{i}.bin", "md5": f"{i:032x}"} for i in range(10)])
|
||||
page = gs.generate_emulator_page(
|
||||
"long", {"emulator": "Long", "type": "standalone", "files": files}, EMPTY_DB, None, set())
|
||||
self.assertNotIn("| File |", page)
|
||||
self.assertNotIn('class="rb-file-entry"', page)
|
||||
self.assertEqual(page.count('<details class="rb-entry">'), len(files))
|
||||
optional = page[page.index("## Optional"):]
|
||||
# Hash column: the first 12 characters, the whole value in the title
|
||||
# and in the card.
|
||||
md5 = f"{9:032x}"
|
||||
self.assertIn(f'MD5 <code title="{md5}">{md5[:12]}</code>', optional)
|
||||
self.assertRegex(optional, rf'MD5: <code class="rb-hash"[^>]*>{md5}</code>')
|
||||
|
||||
def test_a_shorter_page_keeps_its_cards(self):
|
||||
files = [_archive(i) for i in range(gs.EMULATOR_TABLE_MIN - 1)]
|
||||
page = gs.generate_emulator_page(
|
||||
"short", {"emulator": "Short", "type": "standalone", "files": files}, EMPTY_DB, None, set())
|
||||
self.assertIn('class="rb-file-entry"', page)
|
||||
self.assertNotIn('class="rb-entries"', page)
|
||||
|
||||
|
||||
class TheWantedListIsOneFilteredTable(unittest.TestCase):
|
||||
def setUp(self):
|
||||
dats = ["Sony - PlayStation 2 - BIOS Images", "Nintendo - GameCube - BIOS Images"]
|
||||
self.missing = [{"name": f"bios{i}.bin", "sha1": f"{i:040x}", "description": f"Dump {i}",
|
||||
"dat": dats[i % 2]} for i in range(30)]
|
||||
self.missing.append({"name": "both.bin", "sha1": "f" * 40, "dats": dats})
|
||||
report = {"redump": {
|
||||
"imported_at": "2026-01-01", "dats": {}, "matched": 12, "out_of_scope": 7,
|
||||
"covered_dats": dats + ["Sega - Dreamcast - BIOS Images"],
|
||||
"missing": [dict(entry) for entry in self.missing] + [
|
||||
{"name": "made.bin", "sha1": "a" * 40, "dat": dats[0]}],
|
||||
}}
|
||||
moved: dict[str, str] = {}
|
||||
self.main = gs.generate_provenance_page(
|
||||
{"files": {}}, report, {"a" * 40: {"from": ["bios/x.bin"], "transform": "half"}}, moved)
|
||||
self.page = moved["provenance-redump"]
|
||||
|
||||
def test_the_counts_come_first(self):
|
||||
head = self.page[:self.page.index("<details")]
|
||||
self.assertIn("Held: **12** of 44, in the 3 DATs the collection reaches. "
|
||||
"Wanted: **31**, from 2 DATs.", head)
|
||||
self.assertIn("1 more is [derivable]", head)
|
||||
self.assertIn("7 more sit in DATs the collection does not reach.", head)
|
||||
self.assertIn("| Sony - PlayStation 2 - BIOS Images | 16 |", self.page)
|
||||
self.assertIn("| Nintendo - GameCube - BIOS Images | 16 |", self.page)
|
||||
|
||||
def test_every_dump_is_one_row_under_a_filter_box(self):
|
||||
box = re.search(r'<div class="rb-table-filter">.*?</div>\n<details[^>]*>\n'
|
||||
r"<summary>All 31 wanted dumps</summary>\n<table class=\"rb-wanted\"",
|
||||
self.page, re.S)
|
||||
self.assertIsNotNone(box)
|
||||
self.assertIn('aria-live="polite">31 rows</span>', box.group(0))
|
||||
self.assertEqual(self.page.count("<tr><td>"), 31)
|
||||
for entry in self.missing:
|
||||
self.assertIn(f"<tr><td>{entry['name']}</td>", self.page)
|
||||
self.assertIn(f'<td class="rb-hash" data-pagefind-ignore>{entry["sha1"]}</td>',
|
||||
self.page)
|
||||
self.assertIn("Sony - PlayStation 2 - BIOS Images, Nintendo - GameCube - BIOS Images",
|
||||
self.page)
|
||||
self.assertNotIn("made.bin", self.page)
|
||||
|
||||
|
||||
class TheBrowserSide(unittest.TestCase):
|
||||
CSS = (REPO_ROOT / "website" / "src" / "styles" / "retrobios.css").read_text(encoding="utf-8")
|
||||
TS = (REPO_ROOT / "website" / "src" / "scripts" / "site.ts").read_text(encoding="utf-8")
|
||||
|
||||
def test_a_filtered_row_stays_hidden_on_a_phone(self):
|
||||
# A stacked row is `display: block`, which outweighed `hidden`.
|
||||
self.assertIn(".sl-markdown-content table[data-rb-stack] tr[hidden] { display: none; }",
|
||||
self.CSS)
|
||||
|
||||
def test_a_box_above_a_closed_block_opens_it(self):
|
||||
self.assertIn("block?.previousElementSibling?.matches('.rb-table-filter')", self.TS)
|
||||
self.assertIn("if (block && query) block.open = true;", self.TS)
|
||||
|
||||
def test_a_link_to_a_folded_card_opens_its_row(self):
|
||||
start = self.TS.index("const openTargetBlock = () =>")
|
||||
body = self.TS[start:self.TS.index("};", start)]
|
||||
self.assertIn("closest('details')", body)
|
||||
self.assertIn("block.open = true;", body)
|
||||
self.assertIn("addEventListener('hashchange', openTargetBlock);", self.TS)
|
||||
|
||||
def test_an_open_card_takes_the_table_width(self):
|
||||
self.assertIn(".sl-markdown-content .rb-entries-wrap { container-type: inline-size; }",
|
||||
self.CSS)
|
||||
self.assertIn("details.rb-entry[open] > :not(summary) { width: 100cqi; }", self.CSS)
|
||||
|
||||
|
||||
class _Visible(HTMLParser):
|
||||
"""Words in <main> outside closed blocks (their summary shows)."""
|
||||
|
||||
VOID = frozenset({"area", "base", "br", "col", "embed", "hr", "img", "input", "link",
|
||||
"meta", "source", "track", "wbr"})
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(convert_charrefs=True)
|
||||
self.stack: list[tuple[str, str]] = []
|
||||
self.hidden = self.main = self.skip = 0
|
||||
self.words = 0
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag in self.VOID:
|
||||
return
|
||||
effect = ""
|
||||
if tag == "summary" and self.stack and self.stack[-1][1] == "hide":
|
||||
self.hidden -= 1
|
||||
effect = "show"
|
||||
elif tag == "details" and "open" not in dict(attrs):
|
||||
self.hidden += 1
|
||||
effect = "hide"
|
||||
if tag in ("script", "style", "template"):
|
||||
self.skip += 1
|
||||
if tag == "main":
|
||||
self.main += 1
|
||||
self.stack.append((tag, effect))
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag in self.VOID:
|
||||
return
|
||||
while self.stack:
|
||||
name, effect = self.stack.pop()
|
||||
self.hidden += {"hide": -1, "show": 1}.get(effect, 0)
|
||||
self.skip -= name in ("script", "style", "template")
|
||||
self.main -= name == "main"
|
||||
if name == tag:
|
||||
break
|
||||
|
||||
def handle_data(self, data):
|
||||
if self.main and not self.skip and not self.hidden:
|
||||
self.words += len(data.split())
|
||||
|
||||
|
||||
def _visible_words(page: str) -> int:
|
||||
parser = _Visible()
|
||||
parser.feed((DIST / page / "index.html").read_text(encoding="utf-8"))
|
||||
return parser.words
|
||||
|
||||
|
||||
# Words a visitor sees with every block closed, by page type: the largest
|
||||
# measured after the change plus 5 %, never above the largest before it.
|
||||
# Before: MAME parts 16,680, long emulator pages 14,396 (groovymame),
|
||||
# wanted lists 346 (tosec). After: 1,612, 2,000 (yaps2), 118.
|
||||
VISIBLE_CEILINGS = {
|
||||
"emulator card parts": (r"emulators/[a-z0-9-]+-files-\d+$", 1_693),
|
||||
"long emulator pages": (r"emulators/(groovymame|yaps2|bizhawk|bluemsx|fbneo)$", 2_100),
|
||||
"wanted lists": (r"provenance-[a-z-]+$", 124),
|
||||
}
|
||||
|
||||
|
||||
@unittest.skipUnless((DIST / "emulators" / "mame-files-1" / "index.html").exists(),
|
||||
"website/dist is not built")
|
||||
class TheBuiltPagesStayShort(unittest.TestCase):
|
||||
def _pages(self, pattern: str) -> list[str]:
|
||||
found = [path.parent.relative_to(DIST).as_posix() for path in DIST.glob("**/index.html")]
|
||||
return sorted(page for page in found if re.fullmatch(pattern, page))
|
||||
|
||||
def test_visible_words_under_each_ceiling(self):
|
||||
for kind, (pattern, ceiling) in VISIBLE_CEILINGS.items():
|
||||
pages = self._pages(pattern)
|
||||
self.assertTrue(pages, kind)
|
||||
for page in pages:
|
||||
with self.subTest(page=page):
|
||||
self.assertLessEqual(_visible_words(page), ceiling, kind)
|
||||
|
||||
def test_under_the_element_budget(self):
|
||||
for pattern, _ceiling in VISIBLE_CEILINGS.values():
|
||||
for page in self._pages(pattern):
|
||||
with self.subTest(page=page):
|
||||
parsed = validate_site._PageParser(DIST / page / "index.html")
|
||||
parsed.feed((DIST / page / "index.html").read_text(encoding="utf-8"))
|
||||
self.assertLessEqual(parsed.page.elements, validate_site.MAX_PAGE_ELEMENTS)
|
||||
|
||||
def test_every_mame_set_and_member_is_on_a_part(self):
|
||||
profile = yaml.safe_load((REPO_ROOT / "emulators" / "mame.yml").read_text(encoding="utf-8"))
|
||||
html = "".join((DIST / page / "index.html").read_text(encoding="utf-8")
|
||||
for page in self._pages(r"emulators/mame-files-\d+"))
|
||||
for f in profile["files"]:
|
||||
with self.subTest(name=f["name"]):
|
||||
self.assertIn(f"{f['name']}</code></h3></summary>", html)
|
||||
for member in f.get("contents") or []:
|
||||
if member.get("crc32"):
|
||||
self.assertIn(str(member["crc32"]), html)
|
||||
if member.get("sha1"):
|
||||
self.assertIn(str(member["sha1"]), html)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -340,7 +340,9 @@ class APlatformPageSaysWhereEachFileGoes(unittest.TestCase):
|
||||
lines = generate_site._render_platform_systems(
|
||||
{"nintendo-gamecube": files}, {("nintendo-gamecube", "IPL.bin"): declared}, {})
|
||||
for region in regions:
|
||||
self.assertIn(f"- `IPL.bin` at `dolphin-emu/Sys/GC/{region}/IPL.bin` - OK",
|
||||
# The folder: the name is before it in the row.
|
||||
self.assertIn(f"<summary>IPL.bin<code>dolphin-emu/Sys/GC/{region}/</code>"
|
||||
'<span class="rb-badge rb-badge-success">OK</span></summary>',
|
||||
"\n".join(lines))
|
||||
|
||||
|
||||
@@ -369,7 +371,7 @@ class AVariantShowsWhoPinsIt(unittest.TestCase):
|
||||
".variants/advision.zip.1c086064</code>", block)
|
||||
self.assertIn('<a href="../platforms/batocera.md">Batocera</a>', block)
|
||||
self.assertIn('<a href="../platforms/retrobat.md">RetroBat</a>', block)
|
||||
self.assertIn('<span class="rb-hash">' + "c" * 40 + "</span>", block)
|
||||
self.assertIn('<dd class="rb-hash">' + "c" * 40 + "</dd>", block)
|
||||
self.assertIn("Catalogued", block)
|
||||
|
||||
|
||||
@@ -992,7 +994,8 @@ class TheOneLinerIsWhereUsersLand(unittest.TestCase):
|
||||
page = generate_site.generate_system_page(
|
||||
"Microsoft", {"MSX": [entry]}, FileConsumers({"files": {}}),
|
||||
{"MSX": [("recalbox", "msx1,msx2,msxturbor"), ("batocera", "msx1")]})
|
||||
self.assertIn("--platform recalbox --system msx1,msx2,msxturbor\n", page)
|
||||
# The whole system id, up to the end of the command.
|
||||
self.assertRegex(page, r"--platform recalbox --system msx1,msx2,msxturbor(\n|</code>)")
|
||||
self.assertIn("--platform batocera --system msx1", page)
|
||||
|
||||
def test_a_system_is_offered_only_where_the_manifest_records_it(self):
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
"""A platform page lists one row per file, the evidence on demand.
|
||||
|
||||
Batocera, AmberELEC and RomM ran to 14,000 to 16,000 words: every file
|
||||
spelled its full MD5, the copy serving it and each source reference of
|
||||
each reader, with "(emulator code, libretro port)" after every one. A row
|
||||
now shows the name, the folder, the status and a short hash. Opened, it
|
||||
holds every hash, the served copy and the readers across the list, the
|
||||
role of each reference named by one word that a legend defines once.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
import generate_site # noqa: E402
|
||||
|
||||
_SKIPPED = frozenset({"script", "style", "template"})
|
||||
_VOID = frozenset({"area", "base", "br", "col", "embed", "hr", "img", "input",
|
||||
"link", "meta", "source", "track", "wbr"})
|
||||
|
||||
|
||||
class _Words(HTMLParser):
|
||||
"""Words in <main>: all of them, and those a reader sees with every
|
||||
details block closed (a closed block shows its summary only)."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(convert_charrefs=True)
|
||||
self.stack: list[tuple[str, bool]] = []
|
||||
self.visible = 0
|
||||
self.total = 0
|
||||
|
||||
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
||||
if tag not in _VOID:
|
||||
closed = tag == "details" and "open" not in dict(attrs)
|
||||
self.stack.append((tag, closed))
|
||||
|
||||
def handle_endtag(self, tag: str) -> None:
|
||||
for index in range(len(self.stack) - 1, -1, -1):
|
||||
if self.stack[index][0] == tag:
|
||||
del self.stack[index:]
|
||||
return
|
||||
|
||||
def _counted(self) -> bool:
|
||||
names = {name for name, _ in self.stack}
|
||||
return "main" in names and not names & _SKIPPED
|
||||
|
||||
def _shown(self) -> bool:
|
||||
for index, (_name, closed) in enumerate(self.stack):
|
||||
if closed:
|
||||
rest = self.stack[index + 1:]
|
||||
if not rest or rest[0][0] != "summary":
|
||||
return False
|
||||
return True
|
||||
|
||||
def handle_data(self, data: str) -> None:
|
||||
if not self._counted():
|
||||
return
|
||||
count = len(data.split())
|
||||
self.total += count
|
||||
if self._shown():
|
||||
self.visible += count
|
||||
|
||||
|
||||
def page_words(html: str) -> tuple[int, int]:
|
||||
"""(visible words, all words) in a page's <main>."""
|
||||
parser = _Words()
|
||||
parser.feed(html)
|
||||
return parser.visible, parser.total
|
||||
|
||||
|
||||
class VisibleWordsAreCounted(unittest.TestCase):
|
||||
def test_a_closed_block_shows_its_summary_only(self):
|
||||
page = ("<main><p>one two</p><details><summary>three</summary>four five"
|
||||
"<details open><summary>six</summary>seven</details></details>"
|
||||
"<details open><summary>eight</summary>nine</details>"
|
||||
"<script>no</script></main><p>outside</p>")
|
||||
self.assertEqual(page_words(page), (5, 9))
|
||||
|
||||
|
||||
class ARowPerFile(unittest.TestCase):
|
||||
def _row(self, f: dict, entry: dict, hash_type: str = "md5") -> str:
|
||||
return generate_site._platform_file_row(f, entry, hash_type)
|
||||
|
||||
def test_the_row_at_a_glance(self):
|
||||
md5, sha1 = "a" * 32, "b" * 40
|
||||
row = self._row({"name": "bios_a.bin", "status": "ok", "size": 2048},
|
||||
{"name": "bios_a.bin", "md5": md5, "sha1": sha1, "crc32": "0badf00d",
|
||||
"destination": "bios/bios_a.bin"})
|
||||
# The summary is the row: the name, its folder, the status.
|
||||
self.assertIn('<details class="rb-file"><summary>bios_a.bin<code>bios/</code>'
|
||||
'<span class="rb-badge rb-badge-success">OK</span></summary>', row)
|
||||
# The hash the platform checks, short, on one button beside the
|
||||
# summary (a summary is a control: no button inside it).
|
||||
details, button = row.split("</details>")
|
||||
self.assertNotIn("<button", details)
|
||||
self.assertEqual(button, f'<button class="rb-copy" data-copy="{md5}" '
|
||||
f'aria-label="Copy MD5 {md5[:generate_site.SHORT_HASH]}">{md5[:generate_site.SHORT_HASH]}</button>')
|
||||
# Every hash whole in the details, with the size.
|
||||
for value in (md5, sha1, "0badf00d"):
|
||||
self.assertIn(value, details)
|
||||
self.assertIn("(2.0 KB)", details)
|
||||
|
||||
def test_a_file_at_the_root_prints_no_path(self):
|
||||
for destination in ("", "x.bin"):
|
||||
row = self._row({"name": "x.bin", "status": "ok"}, {"destination": destination})
|
||||
self.assertIn("<summary>x.bin<span", row)
|
||||
|
||||
def test_a_list_drops_the_path_column_when_no_row_has_one(self):
|
||||
files = [{"name": "a.bin", "status": "ok"}, {"name": "b.bin", "status": "ok"}]
|
||||
system = "nintendo-gamecube"
|
||||
config = {(system, "a.bin"): [{"destination": "a.bin"}], (system, "b.bin"): [{}]}
|
||||
text = "\n".join(generate_site._render_platform_systems({system: files}, config, {}))
|
||||
self.assertIn('<div class="rb-files">', text)
|
||||
config[(system, "b.bin")] = [{"destination": "sub/b.bin"}]
|
||||
text = "\n".join(generate_site._render_platform_systems({system: files}, config, {}))
|
||||
self.assertIn('<div class="rb-files rb-files-path">', text)
|
||||
|
||||
def test_status_words(self):
|
||||
cases = {
|
||||
("missing", ""): ("Missing", "danger"),
|
||||
("untested", "declared hash contradicted by the local file"): ("Wrong hash", "warning"),
|
||||
("untested", "expected 1234... got 5678..."): ("Wrong hash", "warning"),
|
||||
("untested", "bios/x read error"): ("Untested", "warning"),
|
||||
}
|
||||
for (status, reason), (word, tone) in cases.items():
|
||||
row = self._row({"name": "x", "status": status, "reason": reason}, {"md5": "c" * 32})
|
||||
self.assertIn(f'<span class="rb-badge rb-badge-{tone}">{word}</span>', row)
|
||||
if reason:
|
||||
self.assertIn(f"Not confirmed: {reason}", row)
|
||||
|
||||
def test_accepted_values_all_in_the_details(self):
|
||||
values = ["1" * 32, "2" * 32, "3" * 32]
|
||||
row = self._row({"name": "x", "status": "ok"}, {"md5": ",".join(values)})
|
||||
self.assertIn(f'data-copy="{values[0]}"', row)
|
||||
self.assertIn(f">{values[0][:generate_site.SHORT_HASH]} +2</button>", row)
|
||||
for value in values:
|
||||
self.assertIn(f'<code class="rb-hash">{value}</code>', row)
|
||||
|
||||
def test_markdown_never_reads_the_text(self):
|
||||
row = self._row({"name": "*a_b*[1].bin", "status": "ok"}, {})
|
||||
self.assertIn("*a_b*[1].bin", row)
|
||||
|
||||
def test_a_long_list_gets_a_filter(self):
|
||||
rows = ["<details></details>"] * generate_site.FILTER_ROWS
|
||||
block = generate_site._platform_file_list(rows, False, "arcade")
|
||||
self.assertTrue(block[0].startswith('<div class="rb-table-filter"><label for="rb-files-filter-arcade">'))
|
||||
self.assertNotIn("rb-table-filter", "".join(
|
||||
generate_site._platform_file_list(rows[1:], False, "arcade")))
|
||||
|
||||
|
||||
class ReferencesNameTheirRoleOnce(unittest.TestCase):
|
||||
PROFILE = {"type": "libretro", "source": "https://example.org/port",
|
||||
"upstream": "https://example.org/emu"}
|
||||
REFS = "source libretro/libretro.c:1, source src/a.c:2, source src/b.c:3, upstream src/c.c:4"
|
||||
|
||||
def test_grouped_by_role(self):
|
||||
text = generate_site._refs_by_role(self.PROFILE, self.REFS)
|
||||
self.assertEqual(text, "glue `libretro/libretro.c:1` and port `src/a.c:2`, `src/b.c:3` "
|
||||
"and upstream `src/c.c:4`")
|
||||
for word in generate_site.ROLE_WORDS.values():
|
||||
self.assertIn(f"**{word}**", generate_site.ROLE_LEGEND)
|
||||
|
||||
def test_other_pages_keep_the_role_after_each_reference(self):
|
||||
text = generate_site._source_ref_markdown(self.PROFILE, self.REFS)
|
||||
self.assertEqual(text.count("(emulator code, libretro port)"), 2)
|
||||
self.assertIn("`src/c.c:4` (upstream emulator)", text)
|
||||
|
||||
|
||||
def _unescaped(text: str) -> str:
|
||||
return html.unescape(text)
|
||||
|
||||
|
||||
@unittest.skipUnless((REPO_ROOT / "database.json").is_file(), "database.json absent")
|
||||
class NothingIsLost(unittest.TestCase):
|
||||
"""Every fact a file row carried is on the page or its parts: each
|
||||
declared hash whole, the served copy, every reader's references."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
from common import load_database
|
||||
from generate_readme import compute_coverage
|
||||
|
||||
db = load_database(str(REPO_ROOT / "database.json"))
|
||||
cls.pages = {}
|
||||
for name in ("batocera", "retroarch"):
|
||||
cov = compute_coverage(name, str(REPO_ROOT / "platforms"), db)
|
||||
moved: dict[str, str] = {}
|
||||
page = generate_site.generate_platform_page(name, cov, {}, {}, moved)
|
||||
cls.pages[name] = (cov, page, moved)
|
||||
|
||||
def test_every_fact_is_there(self):
|
||||
for name, (cov, page, moved) in self.pages.items():
|
||||
text = _unescaped("\n".join([page, *moved.values()]))
|
||||
systems = {d.get("system") for d in cov["details"]}
|
||||
for sys_id, system in cov["config"].get("systems", {}).items():
|
||||
if sys_id not in systems:
|
||||
continue
|
||||
for entry in system.get("files", []):
|
||||
for algorithm in ("sha1", "md5", "crc32", "sha256"):
|
||||
for value in re.split(r"[,\s]+", str(entry.get(algorithm) or "")):
|
||||
if value:
|
||||
self.assertIn(value, text, (name, entry.get("name"), algorithm))
|
||||
for detail in cov["details"]:
|
||||
self.assertIn(detail["name"], text)
|
||||
if detail.get("path") and detail.get("status") != "missing":
|
||||
self.assertIn(detail["path"], text, (name, detail["name"]))
|
||||
readers = (detail.get("category_evidence") or {}).get("read_by", [])
|
||||
# Cores of other systems that share the name are named, not
|
||||
# cited, when a core of this system reads the file.
|
||||
running = [reader for reader in readers if reader.get("runs_system")]
|
||||
for reader in running or readers:
|
||||
for url in re.findall(r"\]\((https://[^)]+)\)", generate_site._category_evidence_md(
|
||||
{"read_by": [reader]})):
|
||||
self.assertIn(url, text, (name, detail["name"], reader["emulator"]))
|
||||
|
||||
def test_the_role_is_a_word_defined_once(self):
|
||||
for name, (_cov, page, moved) in self.pages.items():
|
||||
for text in (page, *moved.values()):
|
||||
self.assertNotIn("(emulator code, libretro port)", text, name)
|
||||
self.assertEqual(text.count(generate_site.ROLE_LEGEND), 1, name)
|
||||
|
||||
|
||||
# Visible words (outside closed details) measured on the built site,
|
||||
# 2026-10-27. A ceiling is the measured value plus 5 %, never above the
|
||||
# value before the rows were tables (Batocera 2,433, AmberELEC 1,301, RomM
|
||||
# 1,607, RetroArch 2,207, the largest main page 2,459, the largest part
|
||||
# 19,127: RetroDECK's arcade list).
|
||||
PAGE_CEILINGS = {"batocera": 2433, "amberelec": 1301, "romm": 1607, "retroarch": 2192}
|
||||
MAIN_PAGE_CEILING = 2459
|
||||
PART_PAGE_CEILING = 2219
|
||||
|
||||
_SITE = REPO_ROOT / "website" / "dist" / "platforms"
|
||||
|
||||
|
||||
@unittest.skipUnless(_SITE.is_dir(), "website/dist not built")
|
||||
class VisibleWordsStayUnderTheirCeiling(unittest.TestCase):
|
||||
def test_platform_pages(self):
|
||||
from common import list_registered_platforms
|
||||
|
||||
mains = set(list_registered_platforms(str(REPO_ROOT / "platforms"), include_archived=True))
|
||||
for page in sorted(_SITE.glob("*/index.html")):
|
||||
name = page.parent.name
|
||||
text = page.read_text(encoding="utf-8")
|
||||
if 'http-equiv="refresh"' in text[:2000]:
|
||||
continue
|
||||
visible, _total = page_words(text)
|
||||
ceiling = PAGE_CEILINGS.get(name, MAIN_PAGE_CEILING if name in mains else PART_PAGE_CEILING)
|
||||
self.assertLessEqual(visible, ceiling, name)
|
||||
|
||||
|
||||
# Each platform with its part pages, before the rows were rewritten: pages,
|
||||
# elements (outside scripts and styles, as validate_site counts them) and
|
||||
# bytes of HTML, on the built site of 2026-10-27.
|
||||
BEFORE_WEIGHT = {
|
||||
"batocera": (12, 18_854, 1_211_117),
|
||||
"amberelec": (1, 7_928, 473_388),
|
||||
"romm": (2, 9_561, 572_487),
|
||||
"retroarch": (14, 21_503, 1_430_802),
|
||||
}
|
||||
|
||||
|
||||
@unittest.skipUnless(_SITE.is_dir(), "website/dist not built")
|
||||
class APlatformWeighsNoMoreThanBefore(unittest.TestCase):
|
||||
"""The split budget measured Markdown length: rows written as a table
|
||||
were longer Markdown, 38 systems moved to new part pages and Batocera
|
||||
with its parts rose from 18,854 to 21,875 elements."""
|
||||
|
||||
def test_pages_elements_and_bytes(self):
|
||||
from validate_site import _PageParser
|
||||
|
||||
for name, (pages, elements, size) in BEFORE_WEIGHT.items():
|
||||
paths, weight = [], 0
|
||||
for path in [_SITE / name / "index.html", *sorted(_SITE.glob(f"{name}-*/index.html"))]:
|
||||
parser = _PageParser(path)
|
||||
parser.feed(path.read_text(encoding="utf-8"))
|
||||
# A part folded back into its platform page left a redirect.
|
||||
if not parser.page.redirect:
|
||||
paths.append(path)
|
||||
weight += parser.page.elements
|
||||
self.assertLessEqual(len(paths), pages, name)
|
||||
self.assertLessEqual(weight, elements, name)
|
||||
self.assertLessEqual(sum(path.stat().st_size for path in paths), size, name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -86,9 +86,10 @@ class TheHeaderReadsAsWords(unittest.TestCase):
|
||||
self.assertNotIn('rb-badge-muted">japan</span>', self.page)
|
||||
|
||||
def test_profile_version_names_the_pinned_build(self):
|
||||
# DuckStation numbers builds v0.1-<commits since tag 0.1>: the pin,
|
||||
# 4122fed9, is 12,070 commits past it.
|
||||
self.assertRegex(str(_profile("duckstation")["core_version"]), r"^v0\.1-\d+$")
|
||||
# DuckStation tags builds v0.1-<commits since tag 0.1>, and the
|
||||
# pin sits past the last such tag: git describe's form names both.
|
||||
self.assertRegex(str(_profile("duckstation")["core_version"]),
|
||||
r"^v0\.1-\d+(-\d+-g[0-9a-f]{7})?$")
|
||||
|
||||
def test_the_second_spelling_is_a_platform_name(self):
|
||||
# The lookup is case-sensitive and EmuELEC launches "Duckstation":
|
||||
|
||||
@@ -285,7 +285,12 @@ class CommandBlocks(unittest.TestCase):
|
||||
self.assertIn("install.ps1", group)
|
||||
block = "\n".join(generate_site._console_install_block(
|
||||
"PlayStation", [("batocera", "psx"), ("recalbox", "psx")]))
|
||||
self.assertEqual(block.count("install.sh | sh"), block.count("install.ps1))) "))
|
||||
# Every setup's options in both forms: the list of every command,
|
||||
# and the one command under the OS tabs.
|
||||
sh_args = re.findall(r"install\.sh \| sh -s -- ([^<\n]+)", block)
|
||||
ps_args = re.findall(r"install\.ps1\)\)\) ([^<\n]+)", block)
|
||||
self.assertEqual(sorted(set(sh_args)), sorted(set(ps_args)))
|
||||
self.assertEqual(len(set(sh_args)), 2)
|
||||
self.assertIn('```powershell title="PowerShell"', block)
|
||||
for key, guide in siteguides.GUIDES.items():
|
||||
if "windows" in guide.run:
|
||||
|
||||
@@ -158,16 +158,13 @@ class EachEntryNamesItsFile(unittest.TestCase):
|
||||
# Existence mode reports no path: nothing is claimed, held or not.
|
||||
self.assertEqual(generate_site._served_line({"status": "ok"}), "")
|
||||
|
||||
def test_a_long_system_list_is_cut_between_files(self):
|
||||
body = ["2/2 files verified", ""]
|
||||
for n in range(4):
|
||||
body += [f"- `f{n}.bin` - OK", " " + "x" * 100]
|
||||
runs = generate_site._file_runs(body, 250)
|
||||
self.assertEqual(len(runs), 2)
|
||||
for run in runs:
|
||||
self.assertEqual(run[:2], body[:2])
|
||||
self.assertTrue(run[2].startswith("- "))
|
||||
self.assertEqual(sum(len(run) - 2 for run in runs), len(body) - 2)
|
||||
def test_a_long_system_list_is_cut_between_rows_by_elements(self):
|
||||
rows = [[f'<details class="rb-file"><summary>f{n}.bin</summary>x<br>y</details>']
|
||||
for n in range(4)]
|
||||
# Three elements a row: two rows fit in a budget of seven.
|
||||
runs = generate_site._blocks_in_runs(rows, 7, generate_site._element_weight)
|
||||
self.assertEqual([len(run) for run in runs], [2, 2])
|
||||
self.assertEqual(sum(runs, []), sum(rows, []))
|
||||
|
||||
|
||||
class AlsoNamedIsNotCited(unittest.TestCase):
|
||||
|
||||
@@ -182,8 +182,14 @@ class TheCardLinesSayWhatTheyMean(unittest.TestCase):
|
||||
def test_the_facts_are_out_of_the_search(self):
|
||||
# The setup list made every card's excerpt a run of names and
|
||||
# paths (round 4, V6): the card's title is what the search reads.
|
||||
lists = [line for line in self.lines if line.startswith("<dl")]
|
||||
# The facts shown open are one list, the others one fold, both
|
||||
# out of the search.
|
||||
shown = self.lines[:next(i for i, line in enumerate(self.lines) if line.startswith("<details"))]
|
||||
lists = [line for line in shown if line.startswith("<dl")]
|
||||
self.assertEqual(lists, ['<dl class="rb-facts" data-pagefind-ignore>'])
|
||||
folds = [line for line in self.lines if line.startswith("<details")]
|
||||
self.assertEqual(len(folds), 1)
|
||||
self.assertEqual(folds, ["<details data-pagefind-ignore>"])
|
||||
|
||||
def test_the_page_defines_both_lines(self):
|
||||
page = gs.generate_system_page("Sony", {"PlayStation": [_file("scph5501.bin", "4")]},
|
||||
@@ -206,15 +212,17 @@ class FileFactsAreAGrid(unittest.TestCase):
|
||||
"provenance": {"redump": {"name": "ps-30a.bin"},
|
||||
"tosec": {"name": "SCPH-5501 (1996).bin"}}}
|
||||
lines = gs._system_file_card(entry, FileConsumers({"files": {}}))
|
||||
facts = [line for line in lines if line.startswith("<dt>")]
|
||||
self.assertTrue(facts)
|
||||
for line in facts:
|
||||
self.assertEqual(line.count("<dt>"), 1, line)
|
||||
self.assertEqual(line.count("<dd>"), 1, line)
|
||||
text = "\n".join(lines)
|
||||
self.assertIn('<dt>SHA1</dt><dd><span class="rb-hash">' + "4" * 40 + "</span></dd>", text)
|
||||
# One value cell after each label, in the open list and in the fold.
|
||||
facts = re.findall(r"<dt>[^<]*</dt><dd[^>]*>.*?</dd>", text)
|
||||
self.assertTrue(facts)
|
||||
self.assertEqual(len(facts), text.count("<dt>"))
|
||||
self.assertEqual(text.count("<dt>"), len(re.findall(r"<dd[ >]", text)))
|
||||
# The hash is the cell's text, styled by the cell: no element around it.
|
||||
self.assertIn('<dt>SHA1</dt><dd class="rb-hash">' + "4" * 40 + "</dd>", text)
|
||||
self.assertNotIn("<code class=\"rb-hash\">", text)
|
||||
catalogued = next(line for line in facts if line.startswith("<dt>Catalogued"))
|
||||
self.assertNotIn('<span class="rb-hash">', text)
|
||||
catalogued = next(fact for fact in facts if fact.startswith("<dt>Catalogued"))
|
||||
self.assertIn("<br>", catalogued)
|
||||
self.assertNotRegex(text, r"(?m)^- ")
|
||||
|
||||
|
||||
@@ -393,7 +393,7 @@ class PlatformPagesLinkTheirSystemsAndEmulators(unittest.TestCase):
|
||||
|
||||
def test_a_moved_list_opens_on_its_console_and_names_its_parent(self):
|
||||
# Past PLATFORM_SECTION_BUDGET, so the list leaves for a page of its own.
|
||||
files = [{"name": f"{'f' * 200}{i}.bin", "status": "ok"} for i in range(1200)]
|
||||
files = [{"name": f"{'f' * 200}{i}.bin", "status": "ok"} for i in range(2000)]
|
||||
moved: dict[str, str] = {}
|
||||
generate_site._render_platform_systems(
|
||||
{"sony-playstation": files}, {}, {}, ("retroarch", "RetroArch"), moved, {})
|
||||
|
||||
@@ -141,7 +141,10 @@ class TheVisitorsTab(unittest.TestCase):
|
||||
block = source[source.index("const osTabs = () =>"):source.index("// The sidebar lists")]
|
||||
self.assertIn("savedTab(OS_SYNC_KEY)", block)
|
||||
self.assertIn("set.switchTab(set.tabs[index], index, false)", block)
|
||||
self.assertNotIn("localStorage.setItem", source)
|
||||
self.assertNotIn("localStorage.setItem", block)
|
||||
# The one choice the script saves is the setup picked in an install
|
||||
# box, under its own key: never the OS guess.
|
||||
self.assertEqual(re.findall(r"localStorage\.setItem\((\w+)", source), ["SETUP_KEY"])
|
||||
self.assertNotIn("keepTabInView", source)
|
||||
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ class AnAppThatReadsAnyBios(unittest.TestCase):
|
||||
|
||||
class EveryArcadeCoreNamesItsRomset(unittest.TestCase):
|
||||
def test_finalburn_and_mame_2003_plus(self):
|
||||
for name, romset in (("fbneo", "FinalBurn Neo v1.0.0.2"),
|
||||
for name, romset in (("fbneo", f"FinalBurn Neo {_profile('fbneo')['core_version']}"),
|
||||
("mame2003_plus", "MAME 0.78 (plus backports)")):
|
||||
page = _pages(name)[name]
|
||||
line = page.index(f"Its BIOS and game sets must come from the {romset} romset.")
|
||||
|
||||
@@ -181,9 +181,10 @@ class FaqInstallsFromWhatYouHold(unittest.TestCase):
|
||||
for name in ("muOS", "Onion", "spruceOS", "MinUI", "KNULLI", "ROCKNIX"):
|
||||
with self.subTest(card=name):
|
||||
self.assertIn(name, known)
|
||||
source = (REPO_ROOT / "install.py").read_text(encoding="utf-8")
|
||||
self.assertIn('f"{card[2]} {kind} ({_mount_name(mount)})"', source)
|
||||
self.assertIn('else "SD card"', source)
|
||||
# The label the first screen gives a card found: "Onion SD card".
|
||||
self.assertEqual(install._card_word(("retroarch", Path("/x/BIOS"), "Onion")), "SD card")
|
||||
self.assertEqual(install._card_word(("batocera", Path("/x/bios"), "Batocera")),
|
||||
"SHARE drive")
|
||||
|
||||
def test_a_wiped_card_links_the_copy(self):
|
||||
section = self._section("Lost or wiped card?")
|
||||
|
||||
@@ -149,7 +149,8 @@ class TheNeoGeoOpensOnItsSet(unittest.TestCase):
|
||||
pairs = gs._install_pairs([("batocera", "snk-neogeo")], "a" * 40, consumers)
|
||||
self.assertIn(("retroarch", "arcade"), pairs)
|
||||
block = "\n".join(gs._console_install_block("Neo Geo", pairs))
|
||||
self.assertIn("# RetroArch\n", block)
|
||||
self.assertIn("<li>RetroArch: <code", block)
|
||||
self.assertIn("--platform retroarch --system arcade</code>", block)
|
||||
|
||||
def test_the_built_section(self) -> None:
|
||||
html = _page(DIST / "systems" / "snk" / "index.html")
|
||||
|
||||
@@ -260,8 +260,9 @@ class EnginesGoByEngine(unittest.TestCase):
|
||||
if not SYSTEMS.is_dir():
|
||||
self.skipTest("pages not generated")
|
||||
for old, new in gs.SYSTEM_PAGES_MOVED.items():
|
||||
self.assertFalse((CONTENT / old).exists(), old)
|
||||
self.assertTrue((CONTENT / new).exists(), new)
|
||||
self.assertFalse((CONTENT / old).exists() or (CONTENT / old).with_suffix(".mdx").exists(), old)
|
||||
# A page with tabs is written as MDX.
|
||||
self.assertTrue((CONTENT / new).exists() or (CONTENT / new).with_suffix(".mdx").exists(), new)
|
||||
redirects = CONTENT.parents[1] / "generated" / "redirects.json"
|
||||
if redirects.is_file():
|
||||
table = json.loads(redirects.read_text(encoding="utf-8"))
|
||||
|
||||
@@ -377,7 +377,7 @@ class PlatformPagesPrintTheDeclaredCrc32(unittest.TestCase):
|
||||
path.read_text(encoding="utf-8") for path in sitepages.pages()
|
||||
if sitepages.name(path) == "platforms/retroarch.md"
|
||||
or sitepages.name(path).startswith("platforms/retroarch-"))
|
||||
self.assertIn("CRC32: `91e93107`", text)
|
||||
self.assertIn('CRC32: <code class="rb-hash">91e93107</code>', text)
|
||||
|
||||
|
||||
class PlainHouseStyle(unittest.TestCase):
|
||||
|
||||
@@ -119,7 +119,7 @@ class APlatformSystemOverItsBudgetMoves(unittest.TestCase):
|
||||
config = {("arcade", f["name"]): [{"name": f["name"], "md5": "a" * 32}] for f in files}
|
||||
moved: dict[str, str] = {}
|
||||
budget = generate_site.PLATFORM_SECTION_BUDGET
|
||||
generate_site.PLATFORM_SECTION_BUDGET = 500
|
||||
generate_site.PLATFORM_SECTION_BUDGET = 5
|
||||
try:
|
||||
lines = generate_site._render_platform_systems(
|
||||
{"arcade": files}, config, {}, ("deck", "Deck"), moved)
|
||||
|
||||
@@ -58,6 +58,7 @@ class SlugPlatform(unittest.TestCase):
|
||||
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
|
||||
self.config = {
|
||||
"platform": "Slugs",
|
||||
"version": "2.1",
|
||||
"verification_mode": "md5",
|
||||
"base_destination": "bios",
|
||||
"cores": ["dosbox"],
|
||||
@@ -139,7 +140,7 @@ class RegionKeysForEveryClaimant(unittest.TestCase):
|
||||
)
|
||||
common._emulator_profiles_cache.clear()
|
||||
profiles = common.load_emulator_profiles("emulators")
|
||||
config = {"platform": "P", "verification_mode": "existence", "cores": ["one", "two"],
|
||||
config = {"platform": "P", "version": "2.1", "verification_mode": "existence", "cores": ["one", "two"],
|
||||
"systems": {"sys": {"files": []}}}
|
||||
_groups, extra_dests = platform_region_groups(
|
||||
config, config["systems"], "emulators", db, "", profiles
|
||||
@@ -178,7 +179,7 @@ class FlatFirmwareFolder(unittest.TestCase):
|
||||
}
|
||||
db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
|
||||
config = {
|
||||
"platform": "Flat", "verification_mode": "md5",
|
||||
"platform": "Flat", "version": "2.1", "verification_mode": "md5",
|
||||
"base_destination": "bios", "cores": ["psx"],
|
||||
"systems": {
|
||||
"psx": {"files": [{"name": "scph5501.bin",
|
||||
@@ -300,7 +301,7 @@ class OnePathCountsOnce(unittest.TestCase):
|
||||
common._emulator_profiles_cache.clear()
|
||||
self.addCleanup(common._emulator_profiles_cache.clear)
|
||||
profiles = common.load_emulator_profiles("emulators")
|
||||
config = {"platform": "P", "verification_mode": "existence", "cores": ["one", "two"],
|
||||
config = {"platform": "P", "version": "2.1", "verification_mode": "existence", "cores": ["one", "two"],
|
||||
"systems": {"amiga": {"files": []}}}
|
||||
report = verify_platform(config, db, "emulators", profiles)
|
||||
claims = [u for u in report["undeclared_files"] if u["name"] == "rom.key"]
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
"""core_version is read from the code at the pin, where version_basis says.
|
||||
|
||||
Four bulk pin advances moved 295 pins and no version, and 54 profiles said
|
||||
"Git" or "SVN" where the code states a release. These tests read the
|
||||
recorded sources offline from a fixture of forge answers, hold every profile
|
||||
to a basis, and hold a pin advance to reading the version again.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import copy
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
import profile_sync # noqa: E402
|
||||
import upstream # noqa: E402
|
||||
import version_basis # noqa: E402
|
||||
from common import load_emulator_profiles # noqa: E402
|
||||
from profile_sync import ProfileReport, bump_commit # noqa: E402
|
||||
|
||||
FIXTURE = ROOT / "tests" / "fixtures" / "version_basis.json"
|
||||
|
||||
|
||||
def _fixture_cache(directory: Path) -> dict:
|
||||
"""The fixture's forge answers, laid out as upstream caches them."""
|
||||
data = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
||||
for url, payload in data["api"].items():
|
||||
target = directory / "_api" / f"{hashlib.sha256(url.encode()).hexdigest()}.json"
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(upstream.ABSENT if payload is None else json.dumps(payload),
|
||||
encoding="utf-8")
|
||||
for key, text in data["files"].items():
|
||||
host, owner, name, sha, path = key.split("/", 4)
|
||||
repo = upstream.make_repo(host, owner, name)
|
||||
target = upstream.cache_path(str(directory), repo, sha, path)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(upstream.ABSENT if text is None else text, encoding="utf-8")
|
||||
return data["profiles"]
|
||||
|
||||
|
||||
class ReadsTheRecordedSourceOffline(unittest.TestCase):
|
||||
"""The check needs nothing the cache does not hold."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.cache = Path(self.tmp.name) / "cache"
|
||||
self.profiles = _fixture_cache(self.cache)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _check(self, name: str, profiles: dict | None = None) -> list:
|
||||
profiles = profiles or self.profiles
|
||||
return version_basis.check(profiles[name], str(self.cache), True, profiles)
|
||||
|
||||
def test_every_kind_reads_its_version(self):
|
||||
for name in self.profiles:
|
||||
with self.subTest(profile=name):
|
||||
readings = self._check(name)
|
||||
self.assertEqual([r.status for r in readings], [version_basis.MATCH],
|
||||
readings)
|
||||
|
||||
def test_a_file_basis_names_the_line_it_read(self):
|
||||
(reading,) = self._check("snes9x")
|
||||
self.assertEqual(reading.read, "1.63")
|
||||
self.assertTrue(reading.where.endswith("snes9x.h:11"), reading.where)
|
||||
|
||||
def test_groups_are_laid_out_by_the_format(self):
|
||||
(reading,) = self._check("fbneo")
|
||||
self.assertEqual(reading.read, "v1.0.0.03")
|
||||
|
||||
def test_a_version_the_code_does_not_state_is_drift(self):
|
||||
for name, stale in (("snes9x", "1.61"), ("fbneo", "v1.0.0.2"),
|
||||
("dolphin", "2609"), ("mednafen_psx", "v0.9.44"),
|
||||
("anarch", "Git")):
|
||||
with self.subTest(profile=name):
|
||||
profiles = copy.deepcopy(self.profiles)
|
||||
profiles[name]["core_version"] = stale
|
||||
(reading,) = self._check(name, profiles)
|
||||
self.assertEqual(reading.status, version_basis.DRIFT)
|
||||
|
||||
def test_a_tag_distance_the_forge_contradicts_is_drift(self):
|
||||
profiles = copy.deepcopy(self.profiles)
|
||||
profiles["dolphin"]["version_basis"]["distance"] = 400
|
||||
(reading,) = self._check("dolphin", profiles)
|
||||
self.assertEqual(reading.status, version_basis.DRIFT)
|
||||
self.assertEqual(dict(reading.updates)["distance"], 437)
|
||||
|
||||
def test_a_pattern_that_no_longer_matches_is_unreadable(self):
|
||||
profiles = copy.deepcopy(self.profiles)
|
||||
profiles["snes9x"]["version_basis"]["pattern"] = "#define\\s+NOPE\\s+(\\S+)"
|
||||
(reading,) = self._check("snes9x", profiles)
|
||||
self.assertEqual(reading.status, version_basis.UNREADABLE)
|
||||
|
||||
def test_a_profile_without_basis_is_reported(self):
|
||||
profiles = copy.deepcopy(self.profiles)
|
||||
del profiles["snes9x"]["version_basis"]
|
||||
(reading,) = self._check("snes9x", profiles)
|
||||
self.assertEqual(reading.status, version_basis.NO_BASIS)
|
||||
|
||||
def test_the_command_fails_on_drift_and_passes_clean(self):
|
||||
emulators = Path(self.tmp.name) / "emulators"
|
||||
emulators.mkdir()
|
||||
for name, profile in self.profiles.items():
|
||||
body = dict(profile, files=[] if profile["type"] != "alias" else None)
|
||||
body = {k: v for k, v in body.items() if v is not None or k == "core_version"}
|
||||
(emulators / f"{name}.yml").write_text(yaml.safe_dump(body), encoding="utf-8")
|
||||
argv = ["profile_sync.py", "--all", "--verify-version", "--offline",
|
||||
"--emulators-dir", str(emulators), "--cache-dir", str(self.cache)]
|
||||
|
||||
def run() -> int:
|
||||
out, err = io.StringIO(), io.StringIO()
|
||||
with mock.patch.object(sys, "argv", argv), contextlib.redirect_stdout(out), \
|
||||
contextlib.redirect_stderr(err):
|
||||
try:
|
||||
profile_sync.main()
|
||||
except SystemExit as exc:
|
||||
return int(exc.code or 0)
|
||||
return 0
|
||||
|
||||
import common # noqa: PLC0415
|
||||
|
||||
common._emulator_profiles_cache.clear()
|
||||
self.assertEqual(run(), 0)
|
||||
stale = emulators / "snes9x.yml"
|
||||
stale.write_text(stale.read_text(encoding="utf-8").replace("1.63", "1.61"),
|
||||
encoding="utf-8")
|
||||
common._emulator_profiles_cache.clear()
|
||||
self.assertEqual(run(), 1)
|
||||
|
||||
|
||||
class EveryProfileSaysWhereItsVersionIsRead(unittest.TestCase):
|
||||
"""What can be held without a forge is held here, for every profile."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.profiles = load_emulator_profiles(str(ROOT / "emulators"), skip_aliases=False)
|
||||
|
||||
def test_every_profile_has_a_basis(self):
|
||||
missing = sorted(name for name, p in self.profiles.items()
|
||||
if not version_basis.bases(p))
|
||||
self.assertEqual(missing, [], "profiles without version_basis")
|
||||
|
||||
def test_no_placeholder_is_a_version(self):
|
||||
named = sorted(
|
||||
name for name, p in self.profiles.items()
|
||||
for mode in (version_basis.bases(p) or {"": {}})
|
||||
if version_basis.is_placeholder(version_basis.declared(p, mode))
|
||||
)
|
||||
self.assertEqual(named, [])
|
||||
|
||||
def test_keyed_bases_follow_the_keyed_version(self):
|
||||
for name, profile in self.profiles.items():
|
||||
found = version_basis.bases(profile)
|
||||
if not found or "" in found:
|
||||
continue
|
||||
with self.subTest(profile=name):
|
||||
self.assertIsInstance(profile.get("core_version"), dict)
|
||||
self.assertEqual(set(found), set(profile["core_version"]))
|
||||
|
||||
def test_none_states_no_version(self):
|
||||
for name, profile in self.profiles.items():
|
||||
for mode, basis in version_basis.bases(profile).items():
|
||||
if basis.get("kind") == "none":
|
||||
with self.subTest(profile=name, mode=mode):
|
||||
self.assertIsNone(version_basis.declared(profile, mode))
|
||||
|
||||
def test_a_tag_version_is_the_describe_form_of_its_pin(self):
|
||||
for name, profile in self.profiles.items():
|
||||
for mode, basis in version_basis.bases(profile).items():
|
||||
if basis.get("kind") != "tag" or basis.get("url"):
|
||||
continue
|
||||
field = version_basis.PIN_FIELDS[str(basis.get("repo") or "source")]
|
||||
pin = version_basis._by_mode(profile, field, mode)
|
||||
if not pin:
|
||||
continue
|
||||
with self.subTest(profile=name, mode=mode):
|
||||
self.assertEqual(
|
||||
version_basis.declared(profile, mode),
|
||||
version_basis.describe_form(basis["tag"], basis["distance"], pin),
|
||||
)
|
||||
|
||||
def test_an_alias_carries_the_version_of_its_profile(self):
|
||||
for name, profile in self.profiles.items():
|
||||
for mode, basis in version_basis.bases(profile).items():
|
||||
if basis.get("kind") != "alias":
|
||||
continue
|
||||
with self.subTest(profile=name):
|
||||
(reading,) = [r for r in version_basis.check(
|
||||
profile, "", True, self.profiles) if r.mode == mode]
|
||||
self.assertEqual(reading.status, version_basis.MATCH, reading)
|
||||
|
||||
def test_a_package_named_after_its_version_states_it(self):
|
||||
for name, profile in self.profiles.items():
|
||||
for mode, basis in version_basis.bases(profile).items():
|
||||
if basis.get("kind") != "package" or not basis.get("pattern"):
|
||||
continue
|
||||
with self.subTest(profile=name):
|
||||
(reading,) = [r for r in version_basis.check(profile, "", True)
|
||||
if r.mode == mode]
|
||||
self.assertEqual(reading.status, version_basis.MATCH, reading)
|
||||
|
||||
|
||||
SAMPLE = """emulator: Sample
|
||||
type: libretro
|
||||
source: "https://github.com/o/n"
|
||||
profiled_date: "2026-01-01"
|
||||
source_commit: "aaaaaaa"
|
||||
core_version: "1.0"
|
||||
version_basis:
|
||||
kind: file
|
||||
path: "version.h"
|
||||
pattern: '#define\\s+VERSION\\s+"([^"]+)"'
|
||||
files: []
|
||||
"""
|
||||
|
||||
|
||||
class AnAdvanceReadsTheVersionAgain(unittest.TestCase):
|
||||
"""A pin never moves without its version: read again, or refused."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.path = Path(self.tmp.name) / "p.yml"
|
||||
self.path.write_text(SAMPLE, encoding="utf-8")
|
||||
self.report = ProfileReport(
|
||||
name="p", repo="o/n", pin="aaaaaaa", head="bbbbbbb", entries=[], counts={},
|
||||
skipped=profile_sync.NO_SOURCE_REF,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _reading(self, status: str, value: str | None):
|
||||
return version_basis.Reading("", "file", "1.0", value, "o/n@bbbbbbb:version.h:3", status)
|
||||
|
||||
def test_the_version_moves_with_the_pin(self):
|
||||
seen = []
|
||||
|
||||
def check(profile, cache_dir, offline, profiles=None, recompute=False):
|
||||
seen.append((profile["source_commit"], recompute))
|
||||
return [self._reading(version_basis.DRIFT, "2.0")]
|
||||
|
||||
with mock.patch.object(version_basis, "check", check):
|
||||
self.assertTrue(bump_commit(self.path, self.report))
|
||||
written = yaml.safe_load(self.path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(written["source_commit"], "bbbbbbb")
|
||||
self.assertEqual(written["core_version"], "2.0")
|
||||
# Read at the new pin, the nearest tag asked again.
|
||||
self.assertEqual(seen, [("bbbbbbb", True)])
|
||||
|
||||
def test_an_unreadable_version_holds_the_pin(self):
|
||||
with mock.patch.object(version_basis, "check",
|
||||
return_value=[self._reading(version_basis.UNREADABLE, None)]):
|
||||
self.assertFalse(bump_commit(self.path, self.report))
|
||||
self.assertEqual(self.path.read_text(encoding="utf-8"), SAMPLE)
|
||||
|
||||
def test_a_profile_without_basis_is_not_advanced(self):
|
||||
self.path.write_text(SAMPLE.split("version_basis:")[0] + "files: []\n", encoding="utf-8")
|
||||
before = self.path.read_text(encoding="utf-8")
|
||||
self.assertFalse(bump_commit(self.path, self.report))
|
||||
self.assertEqual(self.path.read_text(encoding="utf-8"), before)
|
||||
|
||||
def test_a_package_version_is_advanced_by_hand(self):
|
||||
reading = version_basis.Reading("", "package", "1.0", None, "read from x.apk",
|
||||
version_basis.UNCHECKED)
|
||||
with mock.patch.object(version_basis, "check", return_value=[reading]):
|
||||
self.assertFalse(bump_commit(self.path, self.report))
|
||||
|
||||
|
||||
class TheWriterKeepsTheDocument(unittest.TestCase):
|
||||
def test_keyed_versions_and_bases_round_trip(self):
|
||||
text = ('emulator: X\nsource_commit: "abc"\ncore_version:\n standalone: "1"\n'
|
||||
' libretro: "2"\ndisplay_name: X\n')
|
||||
basis = {"standalone": {"kind": "tag", "tag": "2407", "distance": 0},
|
||||
"libretro": {"kind": "none"}}
|
||||
written = profile_sync.set_version_fields(
|
||||
text, {"standalone": "2407", "libretro": None}, basis)
|
||||
document = yaml.safe_load(written)
|
||||
self.assertEqual(document["core_version"], {"standalone": "2407", "libretro": None})
|
||||
self.assertEqual(document["version_basis"], basis)
|
||||
self.assertEqual(document["display_name"], "X")
|
||||
self.assertIn('tag: "2407"', written)
|
||||
|
||||
def test_a_missing_version_is_written_after_the_pins(self):
|
||||
text = 'emulator: X\nsource_commit: "abc"\nfiles: []\n'
|
||||
written = profile_sync.set_version_fields(text, None, {"kind": "none"})
|
||||
self.assertEqual(written.splitlines()[2:4], ["core_version: null", "version_basis:"])
|
||||
|
||||
|
||||
class TheSchemaRefusesPlaceholders(unittest.TestCase):
|
||||
def setUp(self):
|
||||
try:
|
||||
import jsonschema # noqa: PLC0415
|
||||
except ImportError:
|
||||
self.skipTest("jsonschema is not installed")
|
||||
schema = json.loads((ROOT / "schemas" / "emulator.schema.json").read_text())
|
||||
self.validator = jsonschema.Draft202012Validator(schema)
|
||||
|
||||
def _valid(self, **fields) -> bool:
|
||||
profile = {"emulator": "X", "type": "libretro", "files": [], **fields}
|
||||
return self.validator.is_valid(profile)
|
||||
|
||||
def test_placeholders(self):
|
||||
for value in ("Git", "GIT", "svn", "SVN (2015 snapshot)", "unknown", "master",
|
||||
"latest", "dev", "main", "HEAD", ""):
|
||||
with self.subTest(value=value):
|
||||
self.assertFalse(self._valid(core_version=value))
|
||||
|
||||
def test_versions(self):
|
||||
for value in ("1.63", "v1.0.0.03", "2609-437-g3aac3f0", "0.37b5", "r22",
|
||||
"2026.4.1git", "developer-1", "Dev-4.0.0"):
|
||||
with self.subTest(value=value):
|
||||
self.assertTrue(self._valid(core_version=value))
|
||||
|
||||
def test_none_means_null(self):
|
||||
self.assertTrue(self._valid(core_version=None, version_basis={"kind": "none"}))
|
||||
self.assertFalse(self._valid(core_version="1.0", version_basis={"kind": "none"}))
|
||||
|
||||
def test_each_kind_names_what_it_reads(self):
|
||||
self.assertFalse(self._valid(version_basis={"kind": "file", "path": "v.h"}))
|
||||
self.assertFalse(self._valid(version_basis={"kind": "tag", "tag": "v1"}))
|
||||
self.assertFalse(self._valid(version_basis={"kind": "package"}))
|
||||
self.assertTrue(self._valid(core_version="1", version_basis={"kind": "tag", "tag": "1",
|
||||
"distance": 0}))
|
||||
|
||||
|
||||
class VersionModesRefuseOtherFlags(unittest.TestCase):
|
||||
def test_refused(self):
|
||||
for extra in (["--bump-commit"], ["--check-version"], ["--strict"]):
|
||||
argv = ["profile_sync.py", "--all", "--verify-version", *extra]
|
||||
with self.subTest(flags=extra), mock.patch.object(sys, "argv", argv), \
|
||||
contextlib.redirect_stderr(io.StringIO()):
|
||||
with self.assertRaises(SystemExit) as ctx:
|
||||
profile_sync.main()
|
||||
self.assertEqual(ctx.exception.code, 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user