chore: merge the region reference frames

This commit is contained in:
Abdessamad Derraz committed 2026-10-27 23:07:23 +01:00
commit e6f7ea8f07
794 files changed
+14502 -1807

No files matched your search

+276 -3
View File
@@ -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
View File
@@ -9,4 +9,4 @@
Ctrl-C Quit B/Esc Quit
Ctrl-C Quit B Quit
+1 -1
View File
@@ -12,4 +12,4 @@
Ctrl-C Quit B/Esc Quit
Ctrl-C Quit B Quit
+902
View File
@@ -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
+286
View File
@@ -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
+352
View File
@@ -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
+550
View File
@@ -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
+1 -1
View File
@@ -9,4 +9,4 @@
A/Enter Back
A Back
+1 -1
View File
@@ -12,4 +12,4 @@
A/Enter Back
A Back
+1 -1
View File
@@ -9,4 +9,4 @@
> Install
Back
A/Enter Install B/Esc Back ? Help
A Install B Back ? Help
+1 -1
View File
@@ -12,4 +12,4 @@
A/Enter Install B/Esc Back ? Help
A Install B Back ? Help
+1 -1
View File
@@ -9,4 +9,4 @@
A/Enter Create B/Esc Quit
A Create B Quit
+1 -1
View File
@@ -12,4 +12,4 @@
A/Enter Create B/Esc Quit
A Create B Quit
+3 -3
View File
@@ -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
+1 -1
View File
@@ -12,4 +12,4 @@
Quit
A/Enter Choose B/Esc Back
A Choose B Back
+1 -1
View File
@@ -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
View File
@@ -12,4 +12,4 @@
Look again
A/Enter Browse B/Esc Back ? Help
A Browse B Back ? Help
+1 -1
View File
@@ -9,4 +9,4 @@
FAILED scph5500.bin (HTTP 404)
ok gba_bios.bin
B/Esc Stop ? Help
B Stop ? Help
+1 -1
View File
@@ -12,4 +12,4 @@
B/Esc Stop ? Help
B Stop ? Help
+8 -8
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -9,4 +9,4 @@
missing 16.0 KB gba_bios.bin
A/Enter Back
A Back
+1 -1
View File
@@ -12,4 +12,4 @@
A/Enter Back
A Back
+1 -1
View File
@@ -9,4 +9,4 @@
A/Enter Quit B/Esc Stay
A Quit B Stay
+1 -1
View File
@@ -12,4 +12,4 @@
A/Enter Quit B/Esc Stay
A Quit B Stay
+3 -3
View File
@@ -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
+5 -5
View File
@@ -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
+10 -142
View File
@@ -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
View File
@@ -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
View File
@@ -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?
+10 -10
View File
@@ -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
+11 -69
View File
@@ -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
+10 -10
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -12,4 +12,4 @@
emulator, not to ES-DE itself.
A/Enter Continue B/Esc Quit ? Help
A Continue B Quit ? Help
+3 -3
View File
@@ -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
+94
View File
@@ -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+\"([^\"]+)\""
}
}
}
}
+71 -22
View File
@@ -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)
+20
View File
@@ -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"
+7 -6
View File
@@ -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}
+79
View File
@@ -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()
+1 -1
View File
@@ -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": []}},
}
+1
View File
@@ -56,6 +56,7 @@ files:
PLATFORM = {
"platform": "Demo",
"version": "2.1",
"verification_mode": "existence",
"base_destination": "",
"cores": ["demo"],
+24 -2
View File
@@ -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
View File
@@ -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": {
+1
View File
@@ -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": [{
+6 -3
View File
@@ -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")
+153
View File
@@ -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
View File
@@ -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:
+40 -3
View File
@@ -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",
+4 -3
View File
@@ -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])
+8 -6
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+26 -3
View File
@@ -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("&lt;script&gt;", text)
self.assertIn("&#123;expr&#125;", 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")]))
+1
View File
@@ -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"],
+5 -3
View File
@@ -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)
+2 -1
View File
@@ -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()},
+20
View File
@@ -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(
+1
View File
@@ -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": [],
+2 -2
View File
@@ -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.
+304
View File
@@ -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()
+311
View File
@@ -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()
+6 -3
View File
@@ -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):
+294
View File
@@ -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&#95;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("&#42;a&#95;b&#42;&#91;1&#93;.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()
+4 -3
View File
@@ -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":
+6 -1
View File
@@ -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:
+7 -10
View File
@@ -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):
+16 -8
View File
@@ -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)^- ")
+1 -1
View File
@@ -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, {})
+4 -1
View File
@@ -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)
+1 -1
View File
@@ -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.")
+4 -3
View File
@@ -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?")
+2 -1
View File
@@ -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")
+3 -2
View File
@@ -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"))
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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)
+4 -3
View File
@@ -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"]
+351
View File
@@ -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()