chore: merge integ into fix-launch-names

This commit is contained in:
Abdessamad Derraz committed 2026-11-09 17:47:35 +01:00
commit a6aea81c17
36 files changed
+2347 -101

No files matched your search

+5
View File
@@ -11,6 +11,7 @@ on:
- "exports/**"
# Every script the build runs or imports, and no other
# (tests/test_workflow_paths.py).
- "scripts/archive_build.py"
- "scripts/artifacts.py"
- "scripts/auto_fetch.py"
- "scripts/common.py"
@@ -60,6 +61,7 @@ on:
- "scripts/siteexports.py"
- "scripts/siterender.py"
- "scripts/retired_files.py"
- "scripts/romset_recipes.py"
- "scripts/sitesources.py"
- "scripts/sitesteps.py"
- "scripts/sitestarlight.py"
@@ -68,6 +70,7 @@ on:
- "scripts/slots.py"
- "scripts/sourceref.py"
- "scripts/split_pack.py"
- "scripts/torrentzip.py"
- "scripts/truth.py"
- "scripts/upstream.py"
- "scripts/validate_pr.py"
@@ -88,6 +91,8 @@ on:
- "release.json"
- "derivable.json"
- "retired.json"
# The README and the archive recipes page count the recipes.
- "recipes/archives.json"
- "measurements/**"
# The Proof lines of the emulator pages read what an issue's runs showed.
- "tests/campaign/observations.yml"
+2
View File
@@ -32,6 +32,7 @@ on:
- "release.json"
- "derivable.json"
- "retired.json"
- "recipes/**"
- "measurements/**"
- "website/**"
# The type check's configuration and baseline.
@@ -62,6 +63,7 @@ on:
- "release.json"
- "derivable.json"
- "retired.json"
- "recipes/**"
- "measurements/**"
- "website/**"
- "pyproject.toml"
+5 -2
View File
@@ -9,6 +9,8 @@
<p align="center"><a href="https://abdess.github.io/retrobios/">Website</a> · <a href="https://abdess.github.io/retrobios/which-pack/">Get the files</a> · <a href="https://abdess.github.io/retrobios/wiki/troubleshooting/">A game does not start</a> · <a href="https://abdess.github.io/retrobios/wiki/faq/">Questions</a></p>
<p align="center"><a href="https://pypi.org/project/retrobios/"><img src="https://img.shields.io/pypi/v/retrobios" alt="PyPI version"></a> <a href="https://pypi.org/project/retrobios/"><img src="https://img.shields.io/pypi/pyversions/retrobios" alt="Python versions"></a> <a href="https://github.com/Abdess/retrobios/actions/workflows/validate.yml"><img src="https://github.com/Abdess/retrobios/actions/workflows/validate.yml/badge.svg?branch=main" alt="Validation"></a> <a href="LICENSE"><img src="https://img.shields.io/github/license/Abdess/retrobios" alt="License"></a></p>
Some consoles need a small program, the **BIOS**, to start their games: PlayStation 1 and 2, Saturn, Neo Geo, Dreamcast and more. RetroBIOS installs the BIOS and firmware files for AmberELEC, Batocera, BizHawk, EmuDeck, EmuELEC, KNULLI, Lakka, MiSTer FPGA, MinUI, MyMinUI, NextUI, ROCKNIX, Recalbox, RetroArch, RetroBat, RetroDECK, RetroPie, and RomM, checked the way each one checks them.
## Install in one line
@@ -61,9 +63,10 @@ Unzip into the BIOS folder, files straight in it. A big pack comes in parts (`Pa
## How it is built
- **635 emulators profiled**, 601 from their source code and 34 closed-source builds from their binaries: 93% of the files they load cite the file and line that reads them
- **636 emulators profiled**, 602 from their source code and 34 closed-source builds from their binaries: 93% of the files they load cite the file and line that reads them
- **18 setups**, each checked its own way: MD5 for Batocera and Recalbox, the file name for RetroArch, as their own code does
- **535 systems**, 19,989 files in the collection, [what is still missing](https://abdess.github.io/retrobios/gaps/) named file by file
- **2,227 archives**, each with a recipe that rebuilds it from the ROMs the collection holds: 1,794 byte for byte, 433 with the same files inside ([rebuilding an archive](https://abdess.github.io/retrobios/wiki/archive-recipes/))
- Public data: [JSON, CSV and SQLite](https://abdess.github.io/retrobios/data/), the metadata under MIT
<details>
@@ -107,4 +110,4 @@ The scripts and tooling are released under the [MIT License](LICENSE).
The BIOS and firmware files are not covered by that license: they are third-party system software, preserved and provided for personal backup, archival, and interoperability with emulation software. [NOTICE](NOTICE) sets out their status and how to ask for a file to be removed.
The reasoning, and where it is weakest, is in [the project's legal position](https://abdess.github.io/retrobios/wiki/legal/).
*Auto-generated on 2026-11-09T11:53:49Z*
*Auto-generated on 2026-11-09T16:43:09Z*
Binary file not shown.
+12 -24
View File
@@ -136,38 +136,26 @@ files:
note: "Read only by --dsi-mode and NAND title shortcuts. DSiWare run from the game list does without it. Opened read-only, rejected without the \"DSi eMMC CID/CPU\" footer."
source_ref: "src/frontend/sdl/main.cpp:1153-1166, src/frontend/sdl/main.cpp:1331-1337, src/core/nds_dsi.cpp:112-118, src/core/io/dsi_sd.cpp:105-138"
- name: "TWLFontTable.dat"
- name: "dsi_boot.bin"
system: nintendo-dsi
required: false
hle_fallback: true
size: 863296
sha1: "c1101d7c9224acfa0350b5fc62da97d4c6d36141"
config_key: "paths.dsi_font"
description: "DSi system font (Japan, Americas, Europe, Australia)"
note: "Used for DSiWare run without a NAND. Absent, DSperate's own font table is generated. ROCKNIX's ini leaves the key unset. A file under 256 bytes is refused. No hash is checked, only the layout bytes 0x84 and 0x86, and a table whose layout differs from the title's region draws a warning. This is the normal layout (0x84 = 3, 0x86 = 0), read from nand:/sys/TWLFontTable.dat of a retail DSi NAND dump. The FAT16 partition is AES-CTR encrypted with the normal key ROL128((KeyX XOR KeyY) + 0xFFFEFB4E295902582A680F5F1A4F3E79, 42), where KeyX is built from the console ID in the nocash footer and KeyY is 0x0AB9DC76 0xBD4DC4D3 0x202DDD1D 0xE1A00005. The counter is the first 16 bytes of SHA-1 of the footer's eMMC CID, and each 16-byte block is byte-reversed before and after AES. The USA, Europe, Japan and Australia consoles, four different console IDs, give the same bytes."
source_ref: "src/core/nds_dsi.cpp:310-327, src/core/io/dsi_font.cpp:38-43, src/core/io/dsi_nand_fs.cpp:786-800, src/core/io/dsi_aes.cpp:38-46, src/frontend/sdl/main.cpp:1774-1778"
size: 340
validation: [size]
found_by: content
description: "DSi console data blob"
note: "Read only by dsperate-headless, through --dsi-boot, which has no default, so the name is a label. A file of any size other than 0x154 bytes stops the run. The blob is TWLCFG bytes 0x88-0x1AF, HWINFO_N bytes 0x88-0x9B and HWINFO_S bytes 0x88-0x9F, copied to 0x02000400, 0x02000600 and 0x02FFFD68 when a DSi title starts. Without it the same bytes are read from shared1/TWLCFG0.dat or TWLCFG1.dat (the higher save count at byte 0x81) and sys/HWINFO_N.dat and sys/HWINFO_S.dat of an attached NAND, generated for DSiWare run without a NAND, and left zero in card mode. Cut from a retail NAND's TWLCFG and HWINFO files, the blob differs per console."
source_ref: "src/frontend/headless/main.cpp:304, src/frontend/headless/main.cpp:386, src/frontend/headless/main.cpp:508, src/core/nds_dsi.cpp:107-111, src/core/nds_dsi.cpp:294, src/core/nds_dsi.cpp:340-343, src/core/nds_dsi.cpp:454-464, src/core/io/dsi_nand_launch.cpp:147-167, src/core/io/dsi_nand_synth.cpp:114-120"
- name: "TWLFontTable.dat"
system: nintendo-dsi
required: false
hle_fallback: true
size: 581664
sha1: "5170aff7e9766baadbbf2943c41f5634e82aaf2d"
min_size: 256
validation: [size]
config_key: "paths.dsi_font"
description: "DSi system font (China)"
note: "The Chinese layout (0x84 = 9, 0x86 = 4), which Chinese DSiWare needs to find its fonts. Read from nand:/sys/TWLFontTable.dat of a retail Chinese DSi NAND dump, decrypted as for the normal table."
source_ref: "src/core/nds_dsi.cpp:313-320, src/core/io/dsi_font.cpp:38-43, src/core/io/dsi_nand_fs.cpp:786-800, src/core/io/dsi_aes.cpp:38-46"
- name: "TWLFontTable.dat"
system: nintendo-dsi
required: false
hle_fallback: true
size: 162688
sha1: "f0a37b073eb0208d3a54c9a6bb94292dbec1192a"
config_key: "paths.dsi_font"
description: "DSi system font (Korea)"
note: "The Korean layout (0x84 = 9, 0x86 = 5), which Korean DSiWare needs to find its fonts. Read from nand:/sys/TWLFontTable.dat of a retail Korean DSi NAND dump, decrypted as for the normal table."
source_ref: "src/core/nds_dsi.cpp:313-320, src/core/io/dsi_font.cpp:38-43, src/core/io/dsi_nand_fs.cpp:786-800, src/core/io/dsi_aes.cpp:38-46"
description: "DSi system font"
note: "Used for DSiWare run without a NAND. A path that names no file falls back to DSperate's own tables, linked into the binary. ROCKNIX's ini leaves the key unset. A file under 256 bytes stops the session. No hash is checked. The layout is read from bytes 0x84 and 0x86: 3 and 0 for the normal table (Japan, Americas, Europe, Australia), 9 and 4 for the Chinese one, 9 and 5 for the Korean one. Chinese and Korean titles need their own layout and every other title the normal one. Another layout only draws a warning, and the title does not find its fonts."
source_ref: "src/core/nds_dsi.cpp:311-323, src/core/io/dsi_font.cpp:38-43, src/frontend/sdl/main.cpp:1774-1778"
- name: "usrcheat.dat"
system: nintendo-ds
+63
View File
@@ -0,0 +1,63 @@
emulator: "FluidSynth"
type: utility
core_classification: other
source: "https://github.com/FluidSynth/fluidsynth"
upstream: "https://github.com/FluidSynth/fluidsynth"
profiled_date: "2026-11-09"
source_commit: "f78486a50bd785200412ea16434b38edc4a45a6c"
upstream_commit: "f78486a50bd785200412ea16434b38edc4a45a6c"
pin_frozen: "EmuELEC, the only platform whose launch code starts it, builds this release (EmuELEC packages/sx05re/tools/sound/fluidsynth-git/package.mk:5-9 at tag v4.8, PKG_SHA256 e97e63c1). Later commits are not part of that build."
pin_build:
- platform: emuelec
repo: "https://github.com/EmuELEC/EmuELEC"
revision: "v4.8"
path: "packages/sx05re/tools/sound/fluidsynth-git/package.mk"
variable: PKG_VERSION
value: "2.0.6"
core_version: "2.0.6"
version_basis:
kind: tag
tag: "v2.0.6"
distance: 0
display_name: "FluidSynth (SoundFont MIDI synthesizer)"
cores:
- fluidsynth
systems: []
notes: |
A SoundFont 2 software synthesizer run as a program. -s starts its TCP
command server and -i turns off the command shell on the terminal.
ref: src/fluidsynth.c:543-544, src/fluidsynth.c:635-637
The program names no SoundFont. Every non-option argument whose header
reads RIFF then sfbk is loaded as a SoundFont, in argument order. An
argument that cannot be opened is skipped with a message and the synth
keeps running with no instrument loaded.
ref: src/fluidsynth.c:764-778, src/sfloader/fluid_sffile.c:335-375
At start it executes one command file: the -f argument, else
$HOME/.fluidsynth, else /etc/fluidsynth.conf. The second name is built
whenever HOME is set, so the system file is read only without HOME.
ref: src/fluidsynth.c:870-884, src/bindings/fluid_cmd.c:608-648
files:
- name: "SC-55.sf2"
description: "SoundFont handed on the command line"
required: false
unsourceable: "user supplied, no canonical file"
note: >-
The code opens no file by name and accepts any SoundFont 2 file. The
entry name is a label for the file EmuELEC names on the command line
(EmuELEC packages/sx05re/emuelec/bin/emuelec-utils:67 at v4.8), which
no EmuELEC package installs or downloads.
source_ref: "src/fluidsynth.c:764-778, src/sfloader/fluid_sffile.c:335-375"
- name: "default.sf2"
description: "SoundFont named by synth.default-soundfont"
required: false
unsourceable: "a path the build fixes for a SoundFont the system installs. FluidSynth ships none, and a same-named file of another program must not stand in"
note: >-
DEFAULT_SOUNDFONT is ${CMAKE_INSTALL_PREFIX}/share/soundfonts/default.sf2
outside Windows. It is loaded only when MIDI files are played and no
SoundFont was loaded from the command line.
source_ref: "cmake_admin/DefaultDirs.cmake:4-11, src/synth/fluid_synth.c:212-214, src/fluidsynth.c:837-859"
+1 -1
View File
@@ -31,4 +31,4 @@ files:
required: true
installed_on: [retrobat, rocknix]
source_ref: "src/libretro/freej2me_libretro.c:487-490 (GET_SYSTEM_DIRECTORY + fill_pathname_join + java -jar)"
note: "Not a BIOS dump. Java emulator loaded by the native core at startup via execvp('java', '-jar', path). Built from source via 'ant'. Must be in the system directory root. ROCKNIX builds the core and the jar from freej2me-plus 1.52, which keeps the freej2me-lr.jar name, and copies the jar into its BIOS folder before each launch when it is missing."
note: "Not a BIOS dump. Java emulator the native core joins to the system directory as freej2me-lr.jar and starts via execvp('java', '-jar', path). Built from source via 'ant'. Must be in the system directory root. ROCKNIX builds the core and the jar from freej2me-plus 1.52, which keeps the freej2me-lr.jar name, and copies the jar into its BIOS folder before each launch when it is missing."
+19 -11
View File
@@ -279,21 +279,23 @@ files:
the byte-swapped data (Source/Core/Core/HW/DSPLLE/DSPLLE.cpp:100-102).
# -- GameCube fonts --
# Sys/GC/ only. Skipped when an IPL dump is available, the fonts are then read
# out of the dump at 0x1aff00 and 0x1fcf00 instead.
# Sys/GC/ only. With Load IPL dump on and a GC/<region>/IPL.bin found, the
# fonts are read out of the dump at 0x1aff00 and 0x1fcf00 instead.
- name: "font_western.bin"
path: "Sys/GC/font_western.bin"
description: "Windows-1252 font used by GameCube and Wii text rendering"
size: 9589
required: false
installed_on: [retrobat]
hle_fallback: true
source_ref: "source Source/Core/Common/CommonPaths.h:142, source Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp:131, 203-247"
source_ref: "source Source/Core/Common/CommonPaths.h:142, source Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp:131, 161-176, 203-247, 330-344"
note: >
Size is the official font length the code reads from an IPL dump, 0x2575
(Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp:234-236). A standalone file is
read without any check. The fork's tree carries no font, RetroBat's
dolphin-triforce package installs the free replacement here.
Read without any size check into the IPL image at 0x1fcf00, keeping at
most 0x3100 bytes. The size is the IPL dump font's, 0x2575, the length
the code reads out of a dump (EXI_DeviceIPL.cpp:234-236). The fork's
tree carries no font, and RetroBat's dolphin-triforce package installs
Dolphin's free replacement here. Without a font a game that reads it
gets an alert that text may be missing or the game may crash, and runs
on.
- name: "font_japanese.bin"
path: "Sys/GC/font_japanese.bin"
@@ -301,9 +303,15 @@ files:
size: 303693
required: false
installed_on: [retrobat]
hle_fallback: true
source_ref: "source Source/Core/Common/CommonPaths.h:143, source Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp:130, 203-247"
note: "Size is the official font length read from an IPL dump, 0x4a24d. The fork's tree carries no font, RetroBat's dolphin-triforce package installs the free replacement here."
source_ref: "source Source/Core/Common/CommonPaths.h:143, source Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp:130, 161-176, 203-247, 330-344"
note: >
Read without any size check into the IPL image at 0x1aff00, keeping at
most 0x50100 bytes. The size is the IPL dump font's, 0x4a24d, the length
the code reads out of a dump (EXI_DeviceIPL.cpp:234-236). The fork's
tree carries no font, and RetroBat's dolphin-triforce package installs
Dolphin's free replacement here. Without a font a game that reads it
gets an alert that text may be missing or the game may crash, and runs
on.
# -- GBA BIOS --
- name: "gba_bios.bin"
+16 -21
View File
@@ -14,7 +14,6 @@ version_basis:
display_name: "The Ur-Quan Masters"
cores: [uqm]
systems: [ur-quan-masters, star-control-2]
verification: md5
notes: |
The Ur-Quan Masters (UQM) is the open-source port of Toys for Bob's
@@ -26,16 +25,18 @@ notes: |
prepareContentDir() in src/options.c:136 searches for content/
relative to the executable (default locations are CONTENTDIR, "",
"content", "../../content", while macOS also probes the app bundle). A
sentinel file content/version must exist (lines 138-150) or the game
sentinel file content/version must exist (lines 138-182) or the game
logs "Could not find content" and exits.
mountContentDir() (src/options.c:329) mounts content/ at the uio root
and then auto-mounts every .uqm file found under content/packages/ as
an overlay archive (lines 354-359). The engine itself does not treat
individual .uqm packages as mandatory: the fatal check is the
content/ tree. In practice content/packages/uqm-0.8.0-content.uqm is
"Core Data" in the Windows installer (uqm-installer.nsi:289-306,
SectionIn 1 2 3 4 6) and the game is unplayable without it.
mountContentDir() (src/options.c:330-361) mounts content/ at the uio
root and then mounts every .zip or .uqm file under content/packages/
below it (line 357, mountDirZips at 463-489). The archive name is not
read. LoadKernel then needs at least one .rmp index at the content
root (src/uqm/setup.c:113-115), and without one the game logs "Could
not load basic content", names uqm-0.8.0-content.uqm and exits
(src/uqm/starcon.c:194-204). The content pack carries uqm.rmp and
base/. A content/ tree that already holds them unpacked passes the
same check, as the tree at this pin does.
3DO music (uqm-0.8.0-3domusic.uqm) and voice overs
(uqm-0.8.0-voice.uqm) are optional sections (installer lines 315,
@@ -50,9 +51,10 @@ notes: |
(uqm.key, menu.key) are shipped verbatim in the source repository
and are not external data, so they are not listed here.
MD5 and size values come from build/win32_install/packages.nsh,
autogenerated by procpkgs.sh at release time. Sizes are the KiB
values the NSIS installer uses with AddSize.
The engine checks no hash. The MD5 values come from
build/win32_install/packages.nsh, autogenerated by procpkgs.sh at
release time, and only the Windows installer compares them, offering
a fresh download on a mismatch (uqm-installer.nsi:132-190).
files:
- name: uqm-0.8.0-content.uqm
@@ -62,10 +64,9 @@ files:
required: true
installed_on: [retropie]
md5: "6cbc9d51fa63e07c0f4d1d061136d816"
validation: [md5]
description: "UQM core game data archive"
note: "Published apart from the source release. The RetroPie scriptmodule downloads it into content/packages."
source_ref: "sc2/build/win32_install/packages.nsh:3-5; sc2/build/win32_install/uqm-installer.nsi:289-306"
note: "Published apart from the source release. Carries uqm.rmp and base/: without a .rmp index at the content root the game exits with \"Could not load basic content\". Any .zip or .uqm in content/packages is mounted, the name is not read. The RetroPie scriptmodule downloads it into content/packages."
source_ref: "sc2/src/uqm/starcon.c:194-204, sc2/src/uqm/setup.c:113-115, sc2/src/options.c:354-359, sc2/src/options.c:463-489, sc2/src/options.c:491-515, sc2/build/win32_install/packages.nsh:3-5"
- name: uqm-0.8.0-3domusic.uqm
path: uqm/content/addons/uqm-0.8.0-3domusic.uqm
@@ -74,7 +75,6 @@ files:
required: false
installed_on: [retropie]
md5: "9e5801d45ca12028b486cdeb83568c02"
validation: [md5]
description: "UQM 3DO music track pack"
note: "The RetroPie scriptmodule downloads it into content/addons."
source_ref: "sc2/build/win32_install/packages.nsh:6-8; sc2/build/win32_install/uqm-installer.nsi:315-327, sc2/src/options.c:393-402, 518-551, sc2/src/uqm/setup.c:118-132"
@@ -86,7 +86,6 @@ files:
required: false
installed_on: [retropie]
md5: "fc89f77d7b66c2669abca6c157f5259a"
validation: [md5]
description: "UQM alien-race voice-over pack"
note: "The RetroPie scriptmodule downloads it into content/addons."
source_ref: "sc2/build/win32_install/packages.nsh:9-11; sc2/build/win32_install/uqm-installer.nsi:329-341, sc2/src/options.c:393-402, 518-551, sc2/src/uqm/setup.c:118-132"
@@ -97,7 +96,6 @@ files:
category: game_data
required: false
md5: "09f242d8d72166d1d5ccbd3d99c93e7d"
validation: [md5]
description: "Precursor Remix disc 1 (modern remixes)"
source_ref: "sc2/build/win32_install/packages.nsh:12-14; sc2/build/win32_install/uqm-installer.nsi:345-358, sc2/src/options.c:393-402, 518-551, sc2/src/uqm/setup.c:118-132"
@@ -107,7 +105,6 @@ files:
category: game_data
required: false
md5: "fbc8bdcb709939d559d8c7216ad15cc2"
validation: [md5]
description: "Precursor Remix disc 2"
source_ref: "sc2/build/win32_install/packages.nsh:15-17, sc2/src/options.c:393-402, 518-551, sc2/src/uqm/setup.c:118-132"
@@ -117,7 +114,6 @@ files:
category: game_data
required: false
md5: "5ccc6d4ac301ae98e172ac6835dcdead"
validation: [md5]
description: "Precursor Remix disc 3"
source_ref: "sc2/build/win32_install/packages.nsh:18-20, sc2/src/options.c:393-402, 518-551, sc2/src/uqm/setup.c:118-132"
@@ -127,6 +123,5 @@ files:
category: game_data
required: false
md5: "3fb63f4ac514343ed4b4b5b194c413fb"
validation: [md5]
description: "Precursor Remix disc 4-1"
source_ref: "sc2/build/win32_install/packages.nsh:21-23, sc2/src/options.c:393-402, 518-551, sc2/src/uqm/setup.c:118-132"
+44 -25
View File
@@ -99,47 +99,67 @@ notes: |
files:
- name: mcpx_1.0.bin
path: "xemu/mcpx_1.0.bin"
standalone_path: mcpx_1.0.bin
required: true
mode: standalone
required: false
size: 512
validation: [size]
config_key: sys.files.bootrom_path
found_by: content
description: "MCPX boot ROM"
note: >-
Passed as the bootrom machine property and read over the last 512 bytes
of the flash image, which the machine maps as a writable overlay at the
top of the address space. qemu_init drops the path with an error when
the file cannot be opened or is not 512 bytes, and the machine then runs
the flash alone. The core reads system/xemu/mcpx_1.0.bin, else
mcpx_1.0.bin at the root of the system directory, and refuses to load
when neither is there.
top of the address space. bootrom_path has no default, so the name is a
label. qemu_init drops a path that cannot be opened or is not 512 bytes
and queues an error, and the machine then runs the flash alone, as it
does when the path is empty.
source_ref: "system/vl.c:2983-3006, hw/xbox/xbox.c:143-171, hw/xbox/xbox.c:468-470, config_spec.yml:362-366"
- name: mcpx_1.0.bin
mode: libretro
path: "xemu/mcpx_1.0.bin"
required: true
size: 512
validation: [size]
description: "MCPX boot ROM"
note: >-
Read from system/xemu/mcpx_1.0.bin, else mcpx_1.0.bin at the root of
the system directory. The core refuses to load when neither is there.
source_ref:
standalone: "system/vl.c:2983-3006, hw/xbox/xbox.c:143-171, hw/xbox/xbox.c:468-470"
libretro: "ui/libretro.c:3370-3415, ui/libretro.c:3418-3427"
- name: Complex_4627.bin
path: "xemu/Complex_4627.bin"
standalone_path: Complex_4627.bin
aliases: [Complex_4627v1.03.bin]
mode: standalone
required: true
size: [262144, 524288, 1048576]
validation: [size]
size_note: "Any size that is a positive multiple of 64 KiB is mapped, and the image is mirrored across the 16 MB flash window"
config_key: sys.files.flashrom_path
found_by: content
description: "Xbox flash ROM image"
note: >-
Passed as -bios and opened through qemu_find_file. An image whose size
is not a multiple of 65536 bytes counts as a failed load, like a missing
file: the 256 KB region is filled with 0xFF and the machine starts
anyway. An unreadable path clears autostart in the standalone, which
takes any file the settings name. The core tries
Complex_4627v1.03.bin, then Complex_4627.bin, in system/xemu/ and
then at the root of the system directory, and refuses to load when
none is there.
Passed as -bios and opened through qemu_find_file. flashrom_path has no
default, so the name is a label. A path that cannot be opened, an empty
one included, queues an error and clears autostart, so the machine does
not start. An image whose size is not a multiple of 65536 bytes counts
as a failed load in the machine: the 256 KB region is filled with 0xFF.
source_ref: "system/vl.c:2527-2535, system/vl.c:3041-3053, hw/xbox/xbox.c:63-123, config_spec.yml:362-366"
- name: Complex_4627.bin
mode: libretro
path: "xemu/Complex_4627.bin"
aliases: [Complex_4627v1.03.bin]
required: true
size: [262144, 524288, 1048576]
validation: [size]
size_note: "Any size that is a positive multiple of 64 KiB is mapped, and the image is mirrored across the 16 MB flash window"
description: "Xbox flash ROM image"
note: >-
The core tries Complex_4627v1.03.bin, then Complex_4627.bin, in
system/xemu/ and then at the root of the system directory, and refuses
to load when none is there.
source_ref:
standalone: "system/vl.c:3041-3053, hw/xbox/xbox.c:63-123"
libretro: "ui/libretro.c:3370-3415, ui/libretro.c:3429-3436"
libretro: "ui/libretro.c:3370-3415, ui/libretro.c:3429-3436, hw/xbox/xbox.c:63-123"
- name: xbox_eeprom.bin
path: "xemu/xbox_eeprom.bin"
@@ -167,8 +187,7 @@ files:
- name: xbox_hdd.qcow2
mode: standalone
path: xbox_hdd.qcow2
required: true
required: false
installed_on: [batocera, emudeck, recalbox, retrodeck, rocknix]
config_key: sys.files.hdd_path
found_by: content
@@ -176,7 +195,7 @@ files:
note: >-
Attached as the IDE drive at index 0 with locked=on when the path is set
and the file opens. An unreadable file only queues an error and an empty
path attaches no drive. Snapshots are stored inside the image.
path attaches no drive: the machine starts either way. Snapshots are stored inside the image.
hdd_path has no default, so the name is a label: it is the file of
the formatted image the project publishes apart from the xemu release
(xemu-hdd-image 1.0, xbox_hdd.qcow2.zip), which carries its own
+86
View File
@@ -0,0 +1,86 @@
emulator: "Yabadabadoo"
type: libretro
core_classification: community_fork
source: "https://github.com/ProRapSuperstarOriginalMaster/Yabadabadoo"
upstream: "https://github.com/libretro/yabause"
profiled_date: "2026-11-09"
source_commit: "4c06da9779c485b532f43b3f18c4b81cb93cf11b"
upstream_commit: "8926b0c6c347f8c5c755911ddb0ac695420ffbf8"
pin_frozen: "ROCKNIX 20261101 builds this revision, tagged v0.3. Later tags of the fork are not part of that build."
pin_build:
- platform: rocknix
repo: "https://github.com/ROCKNIX/distribution"
revision: "20261101"
path: "projects/ROCKNIX/packages/emulators/libretro/yabadabadoo-lr/package.mk"
variable: PKG_VERSION
value: "4c06da9779c485b532f43b3f18c4b81cb93cf11b"
core_version: "0.3"
version_basis:
kind: file
path: "yabause/src/libretro/libretro.c"
pattern: 'library_name\s+=\s+"Yabadabadoo";\s*\n\s*info->library_version\s+=\s+"([^ "]+)'
display_name: "Sega - Saturn (Yabadabadoo)"
cores:
- "yabadabadoo"
built_by: [rocknix]
systems:
- sega-saturn
notes: |
Fork of the libretro Yabause core (master), tuned for ARMv7 handhelds.
The core is yabadabadoo_libretro.so.
ref: yabause/src/libretro/Makefile:69, yabause/src/libretro/Makefile:265-282
Saturn BIOS, read from the frontend's system directory. retro_load_game
first tries saturn_bios.bin, sega_101.bin, then mpr-17933.bin to learn
whether any BIOS is present. YabauseInit then reads the disc's area code
and asks bios_for_region for the file to load: for J, T and K discs (and
discs with no area code) sega_101.bin, saturn_bios.bin, mpr-17933.bin, for
every other area mpr-17933.bin, saturn_bios.bin, sega_101.bin. The
yabadabadoo_bios option forces the Japanese or the USA/Europe order.
ref: yabause/src/libretro/libretro.c:262-284, yabause/src/libretro/libretro.c:1089-1099,
yabause/src/libretro/libretro.c:1793-1807, yabause/src/libretro/libretro.c:1870-1871,
yabause/src/yabause.c:346-363, yabause/src/smpc.c:92-101, yabause/src/cs2.c:4186-4214
The image is read by T123Load, which refuses a file above 512 KiB, and a
BIOS that fails to load stops YabauseInit. No hash is checked.
ref: yabause/src/memory.c:1264-1267, yabause/src/memory.h:196-218
Builds with the SH-2 recompiler (DYNAREC != 0, the miyoomini platform sets
3) refuse to start a game without a BIOS and show a screen naming the
files instead. Builds without it run the HLE BIOS. ROCKNIX builds the
miyoomini platform for its 32-bit RetroArch.
ref: yabause/src/libretro/libretro.c:192-204, yabause/src/libretro/libretro.c:1809-1821,
yabause/src/libretro/Makefile:7, yabause/src/libretro/Makefile:281,
yabause/src/libretro/Makefile.common:145-146
The cartridge option offers only the 1M and 4M RAM carts and no cartridge
or MPEG ROM path is passed, so no other firmware is read.
ref: yabause/src/libretro/libretro.c:1078-1087, yabause/src/libretro/libretro.c:1898-1902
files:
- name: "saturn_bios.bin"
system: sega-saturn
required: true
max_size: 524288
validation: [max_size]
note: "Any region. Second choice for every disc region."
source_ref: "yabause/src/libretro/libretro.c:270-271, yabause/src/libretro/libretro.c:1793"
- name: "sega_101.bin"
system: sega-saturn
required: false
region: [japan, asia-ntsc, south-korea]
max_size: 524288
validation: [max_size]
note: "Saturn BIOS v1.01 (Japan). First choice for J, T and K discs."
source_ref: "yabause/src/libretro/libretro.c:270, yabause/src/libretro/libretro.c:1797"
- name: "mpr-17933.bin"
system: sega-saturn
required: false
region: [north-america, europe, oceania, asia-pal, brazil, latin-america]
max_size: 524288
validation: [max_size]
note: "Saturn BIOS v1.00 (USA/Europe). First choice for U, B, A, E and L discs."
source_ref: "yabause/src/libretro/libretro.c:271, yabause/src/libretro/libretro.c:1801"
+358
View File
@@ -4226,3 +4226,361 @@ systems:
native_name: Phillips CDI
native_id: cdi
name: Phillips CDI
launchers:
- emulator: retroarch
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios
passes_to: "retroarch.cfg system_directory (--config)"
fails_when_missing: false
source_ref: "packages/sx05re/libretro_base/retroarch/package.mk:117, packages/sx05re/emuelec/bin/emuelecRunEmu.sh:380"
- emulator: retrorun
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios
passes_to: retrorun -d
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:110, 466, 476"
- emulator: freej2me
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/jdk
passes_to: environment JAVA_HOME
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:129-133, packages/sx05re/libretro/freej2me/scripts/freej2me.sh:11"
- emulator: freej2me
build: libretro
kind: copies
names:
- freej2me-lr.jar
search_dir: /usr/lib/libretro
passes_to: "${HOME}/roms/bios"
fails_when_missing: false
depends_on_option: /storage/roms/bios/jdk/eeversion exists
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:129-130, packages/sx05re/libretro/freej2me/scripts/freej2me.sh:11-12"
- emulator: mame
build: libretro
kind: copies
names:
- fm*
search_dir: /usr/config/emuelec/configs/mame/hash
passes_to: /storage/roms/bios/mame/hash
fails_when_missing: false
depends_on_option: /storage/roms/bios/mame/hash/fmtowns_cd.xml absent
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:6-8"
- emulator: mame
build: libretro
kind: copies
names:
- app*
search_dir: /usr/config/emuelec/configs/mame/hash
passes_to: /storage/roms/bios/mame/hash
fails_when_missing: false
depends_on_option: /storage/roms/bios/mame/hash/apple2_flop_orig.xml absent
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:11-13"
- emulator: mame
build: libretro
kind: copies
names:
- fm*
search_dir: /usr/config/emuelec/configs/mame/ini
passes_to: /storage/roms/bios/mame/ini
fails_when_missing: false
depends_on_option: /storage/roms/bios/mame/ini/fmtownsux.ini absent
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:16-18"
- emulator: mame
build: libretro
kind: copies
names:
- mame*
search_dir: /usr/config/emuelec/configs/mame/ini
passes_to: /storage/roms/bios/mame/ini
fails_when_missing: false
depends_on_option: /storage/roms/bios/mame/ini/mame.ini absent
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame
passes_to: mame.ini homepath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:10, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame/bios
passes_to: mame.ini rompath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:11, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame/hash
passes_to: mame.ini hashpath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:12, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame/samples
passes_to: mame.ini samplepath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:13, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame/artwork
passes_to: mame.ini artpath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:14, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame/ini
passes_to: mame.ini inipath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:16, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame/cheat
passes_to: mame.ini cheatpath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:18, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame/crosshair
passes_to: mame.ini crosshairpath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:19, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: mame
build: libretro
kind: passes_path
names: []
search_dir: /storage/roms/bios/mame/plugins
passes_to: mame.ini pluginspath
fails_when_missing: false
depends_on_option: mame_read_config = enabled
source_ref: "packages/sx05re/libretro/mame/config/ini/mame.ini:20, packages/sx05re/libretro/mame/config/MAME/MAME.opt:19,\
\ packages/sx05re/emuelec/bin/emuelecRunEmu.sh:357-358, packages/sx05re/libretro/mame/scripts/mame.sh:21-23"
- emulator: pico8
build: standalone
kind: moves
names:
- pico8_dyn
case_insensitive: false
search_dir: /storage/roms/bios/pico-8
passes_to: /emuelec/bin/pico-8
fails_when_missing: false
depends_on_option: /storage/roms/bios/pico-8 exists
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:305-307, packages/sx05re/emuelec/bin/pico8.sh:9-13,\
\ 40-42"
- emulator: flycastsa
build: standalone
kind: links
names: []
search_dir: /storage/roms/bios/dc
passes_to: symlink /storage/.local/share/flycast -> /storage/roms/bios/dc
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:198-200, packages/sx05re/emulators/flycastsa/scripts/flycast.sh:11,\
\ 14"
- emulator: flycast_dojo
build: standalone
kind: links
names: []
search_dir: /storage/roms/bios/dc_dojo
passes_to: symlink /storage/.local/share/flycast-dojo -> /storage/roms/bios/dc_dojo
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:201-203, packages/sx05re/emulators/flycast-dojo/scripts/flycastdojo.sh:11-12,\
\ 28-30"
- emulator: amiberry
build: standalone
kind: links
names: []
search_dir: /storage/roms/bios
passes_to: symlink /usr/config/amiberry/kickstarts -> /storage/roms/bios
fails_when_missing: false
source_ref: "packages/sx05re/emulators/amiberry/package.mk:36"
- emulator: amiberry-lite
build: standalone
kind: links
names: []
search_dir: /storage/roms/bios
passes_to: symlink /usr/config/amiberry-lite/kickstarts -> /storage/roms/bios
fails_when_missing: false
source_ref: "packages/sx05re/emulators/amiberry-lite/package.mk:36"
- emulator: timidity
build: standalone
kind: links
names: []
search_dir: /storage/roms/bios/timidity/freepats
passes_to: symlink /usr/config/timidity/freepats -> /storage/roms/bios/timidity/freepats
fails_when_missing: false
source_ref: "packages/sx05re/tools/sound/timidity/package.mk:38"
- emulator: duckstation
build: standalone
kind: scans_dir
names: []
search_dir: /storage/roms/bios
passes_to: "settings.ini [BIOS] SearchDirectory (-settings), [BIOS] PathNTSCU, PathNTSCJ,\
\ PathPAL unset"
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:207-209, packages/sx05re/emulators/duckstation/config/settings.ini:6-9,\
\ packages/sx05re/emulators/duckstation/package.mk:56, packages/sx05re/emulators/duckstation/scripts/duckstation.sh:40"
- emulator: yabasanshiroSA
build: standalone
kind: fixed_name
names:
- saturn_bios.bin
case_insensitive: false
search_dir: /storage/roms/bios
passes_to: yabasanshiro -b
fails_when_missing: false
depends_on_option: hlebios != 1
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:343-345, packages/sx05re/emulators/yabasanshiroSA_1_11/scripts/yabasanshiro.sh:30-34,\
\ 49"
- emulator: yabasanshiroSA1_5
build: standalone
kind: fixed_name
names:
- saturn_bios.bin
case_insensitive: false
search_dir: /storage/roms/bios
passes_to: yabasanshiro1_5 -b
fails_when_missing: false
depends_on_option: hlebios != 1
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:346-348, packages/sx05re/emulators/yabasanshiroSA_1_5/scripts/yabasanshiro1_5.sh:29-33,\
\ 47"
- emulator: jzintv
build: standalone
kind: passes_path
names: []
search_dir: /storage/roms/bios
passes_to: jzintv -p
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:337-339, packages/sx05re/emulators/jzintv/scripts/jzintv.sh:49"
- emulator: rice, glide64mk2
build: standalone
kind: names_searched
names:
- "N64DD IPLROM (J).n64"
- IPL.n64
case_insensitive: false
search_dir: /storage/roms/bios/Mupen64plus
passes_to: mupen64plus --dd-ipl-rom
fails_when_missing: false
depends_on_option: "-f \"${GAMEDIR}/${GAMENAME}.ndd\""
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:232-237, packages/sx05re/emulators/mupen64plussa/mupen64plussa-core/m64p.sh:16,\
\ 65-68, 96-98"
- emulator: HATARISA
build: standalone
kind: fixed_name
names:
- tos.img
search_dir: /storage/roms/bios
passes_to: "Atari-ST-default.cfg [ROM] szTosImageFileName (--configfile)"
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:172-174, packages/sx05re/emulators/hatarisa/config/conf/Atari-ST-default.cfg:200,\
\ packages/sx05re/emulators/hatarisa/package.mk:39, packages/sx05re/emulators/hatarisa/scripts/hatari.start:36,\
\ 66, 73"
- emulator: AdvanceMame
build: standalone
kind: passes_path
names: []
search_dir: /storage/roms/bios/advmame/samples
passes_to: advmame.rc dir_sample
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:213-215, packages/sx05re/emulators/advancemame/config/advmame.rc_oga:9,\
\ packages/sx05re/emulators/advancemame/config/advmame.rc_gf:9, packages/sx05re/emulators/advancemame/config/advmame.rc:12,\
\ packages/sx05re/emulators/advancemame/package.mk:45-49, packages/sx05re/emulators/advancemame/bin/advmame.sh:80"
- emulator: SCUMMVMSA
build: standalone
kind: passes_path
names: []
search_dir: /storage/.config/scummvm/extra
passes_to: scummvm --extrapath
fails_when_missing: false
depends_on_option: /storage/.config/scummvm/extra exists
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:251-255, packages/sx05re/emulators/scummvmsa/bin/scummvm.start:38-39,\
\ 45"
- emulator: scummvm
build: libretro
kind: copies
names:
- scummvm.ini
search_dir: /storage/.config/scummvm
passes_to: /storage/roms/bios/scummvm.ini
fails_when_missing: false
depends_on_option: scummvm.start add
source_ref: "packages/sx05re/emulators/scummvmsa/bin/scummvm.start:9, 49-51, 54"
- emulator: dolphin
build: standalone
kind: passes_path
names: []
search_dir: /storage/roms/dolphin
passes_to: environment XDG_DATA_HOME
fails_when_missing: false
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:270-272, packages/sx05re/emulators/dolphinSA/scripts/dolphin.sh:28"
- emulator: fluidsynth
build: standalone
kind: fixed_name
slot: fluidsynth
names:
- sc/SC-55.sf2
case_insensitive: false
search_dir: /storage/roms/bios
passes_to: fluidsynth -is
fails_when_missing: false
depends_on_option: ra_midi_output = fluidsynth
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:123-125, packages/sx05re/emuelec/bin/emuelec-utils:54,\
\ 66-67"
- emulator: mt32d
build: standalone
kind: passes_path
slot: cm32l
names: []
search_dir: /roms/bios
passes_to: mt32d -f
fails_when_missing: false
depends_on_option: ra_midi_output = cm32l
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:123-125, packages/sx05re/emuelec/bin/emuelec-utils:54,\
\ 75-76"
- emulator: mt32d
build: standalone
kind: passes_path
slot: mt32
names: []
search_dir: /roms/bios
passes_to: mt32d -f
fails_when_missing: false
depends_on_option: ra_midi_output = mt32
source_ref: "packages/sx05re/emuelec/bin/emuelecRunEmu.sh:123-125, packages/sx05re/emuelec/bin/emuelec-utils:54,\
\ 79-80"
+11
View File
@@ -83,6 +83,17 @@ def load_ledger(path: Path = ARCHIVES_FILE) -> dict[str, dict]:
return {record["path"]: record for record in data.get("archives", [])}
def rebuild_counts(path: Path = ARCHIVES_FILE) -> dict[str, int]:
"""How many recipes rebuild ``exact`` and ``canonical``, and ``total``:
the figures the README and the site print."""
records = load_ledger(path).values()
return {
"total": len(records),
**{kind: sum(1 for r in records if r.get("rebuild") == kind)
for kind in ("exact", "canonical")},
}
def ledger_text(records: dict[str, dict]) -> str:
"""One record per line, so a diff names the archive that changed."""
lines = [
+9 -3
View File
@@ -2891,15 +2891,21 @@ def resolve_platform_cores(
If target_cores is provided, result is intersected with it.
"""
cores_config = config.get("cores")
platform_key = str(config.get("_key") or "")
def every_libretro() -> set[str]:
# A build only some platforms compile (built_by) is reached by name:
# the buildbot RetroArch installs from does not publish it.
# A build only some platforms compile (built_by) is absent from the
# buildbot RetroArch installs from, so it is reached by name, or as one
# of the libretro cores of a platform that compiles it: ROCKNIX ships
# yabadabadoo with its RetroArch and names it nowhere else.
return {
name
for name, p in profiles.items()
if "libretro" in p.get("type", "") and p.get("type") != "alias"
and not p.get("built_by")
and (
not p.get("built_by")
or (platform_key and platform_key in p["built_by"])
)
}
if cores_config == "all_libretro":
+30
View File
@@ -38,6 +38,7 @@ from common import (
write_if_changed,
)
from nativemode import check_label
import archive_build
import release_record
from siteguides import PACKAGE_COMMAND, install_command
from validate_pr import LARGE_FILE_ISSUE_URL
@@ -563,6 +564,32 @@ def _catalog_matched_line(db: dict) -> list[str]:
]
# The badge row under the links: (alt text, image, link).
BADGES = (
("PyPI version", "https://img.shields.io/pypi/v/retrobios", "https://pypi.org/project/retrobios/"),
("Python versions", "https://img.shields.io/pypi/pyversions/retrobios",
"https://pypi.org/project/retrobios/"),
("Validation",
"https://github.com/Abdess/retrobios/actions/workflows/validate.yml/badge.svg?branch=main",
"https://github.com/Abdess/retrobios/actions/workflows/validate.yml"),
("License", "https://img.shields.io/github/license/Abdess/retrobios", "LICENSE"),
)
def badge_row() -> str:
badges = " ".join(f'<a href="{link}"><img src="{image}" alt="{alt}"></a>'
for alt, image, link in BADGES)
return f'<p align="center">{badges}</p>'
def archive_line(counts: dict[str, int]) -> str:
"""The README's line on archive recipes, counted from recipes/archives.json."""
return (f"- **{counts['total']:,} archives**, each with a recipe that rebuilds it from the"
f" ROMs the collection holds: {counts['exact']:,} byte for byte,"
f" {counts['canonical']:,} with the same files inside"
f" ([rebuilding an archive]({SITE_URL}wiki/archive-recipes/))")
def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json") -> str:
total_files = db.get("total_files", 0)
total_size = db.get("total_size", 0)
@@ -629,6 +656,8 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
f'<a href="{SITE_URL}wiki/faq/">Questions</a>'
"</p>",
"",
badge_row(),
"",
"Some consoles need a small program, the **BIOS**, to start their games: PlayStation 1"
" and 2, Saturn, Neo Geo, Dreamcast and more. RetroBIOS installs the BIOS and firmware"
f" files for {_names_list(coverages)}, checked the way each one checks them.",
@@ -715,6 +744,7 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
" the file name for RetroArch, as their own code does",
f"- **{len(system_ids)} systems**, {total_files:,} files in the collection,"
f" [what is still missing]({SITE_URL}gaps/) named file by file",
archive_line(archive_build.rebuild_counts()),
f"- Public data: [JSON, CSV and SQLite]({SITE_URL}data/), the metadata under MIT",
"",
"<details>",
+29
View File
@@ -124,6 +124,7 @@ from siteguides import (
terminal_tabs,
trust_box,
)
import archive_build
import release_record
import sitebootstrap
import sitemeasurements
@@ -8789,6 +8790,30 @@ def with_romset_table(page: str, profiles: dict) -> str:
return _ROMSET_BLOCK.sub(lambda m: f"{m.group(1)}{table}{m.group(3)}", page, count=1)
ARCHIVE_RECIPES_PAGE = Path(WIKI_SRC_DIR) / "archive-recipes.md"
_ARCHIVE_BLOCK = re.compile(
r"(<!-- generated: archive-recipes -->\n)(.*?)(<!-- /generated: archive-recipes -->)", re.S)
def archive_recipe_lines(counts: dict[str, int]) -> list[str]:
"""What an exact and a canonical rebuild are, each with its count."""
total = f"{counts['total']:,}"
return [
f"- **Exact**: the same bytes as the archive in the collection."
f" {counts['exact']:,} of {total} archives.",
f"- **Canonical**: the same files inside, packed another way."
f" {counts['canonical']:,} of {total} archives.",
]
def with_archive_counts(page: str, counts: dict[str, int]) -> str:
"""The archive recipes page with its counts read from recipes/archives.json."""
block = "\n".join(archive_recipe_lines(counts)) + "\n"
if not _ARCHIVE_BLOCK.search(page):
raise ValueError(f"{ARCHIVE_RECIPES_PAGE}: no archive-recipes block to write the counts in")
return _ARCHIVE_BLOCK.sub(lambda m: f"{m.group(1)}{block}{m.group(3)}", page, count=1)
def _render_metadata_rows(profile: dict) -> list[str]:
"""Scalar profile fields, one table row each.
@@ -12678,6 +12703,10 @@ def _generate(args: argparse.Namespace) -> None:
faq = _REPO_ROOT / ROMSET_FAQ_PAGE
if faq.is_file():
write_if_changed(str(faq), with_romset_table(faq.read_text(encoding="utf-8"), profiles))
recipes_page = _REPO_ROOT / ARCHIVE_RECIPES_PAGE
if recipes_page.is_file():
write_if_changed(str(recipes_page), with_archive_counts(
recipes_page.read_text(encoding="utf-8"), archive_build.rebuild_counts()))
if wiki_src.is_dir():
for src_file in wiki_src.glob("*.md"):
shutil.copy2(src_file, wiki_dest / src_file.name)
+755
View File
@@ -0,0 +1,755 @@
"""EmuELEC's launcher layer, read from its packages at the pinned tag.
emuelecRunEmu.sh starts every game. A libretro core runs in RetroArch (or
retrorun) with /storage/roms/bios as its system directory. A standalone
emulator runs from the script the `case ${PLATFORM}` arm names for the core
ES passes (`"${EMU}" = "flycastsa"`), and that script links, moves, copies
or passes the file. Some packages ship the config the emulator reads
(duckstation, hatari, advancemame, mame), others link a BIOS folder at
install time (amiberry, timidity). The emulator of each record is the core
name the dispatch tests, or the package that installs the link.
"""
from __future__ import annotations
import posixpath
import re
from collections.abc import Callable
from dataclasses import dataclass, field
try:
from .configgen_launchers import Launcher
from .rocknix_launchers import fixed_paths
from .shell_launchers import (
LauncherPatternMissing,
Link,
RepoTree,
Script,
describe_keys,
settings,
source_ref,
words,
)
except ImportError:
from configgen_launchers import Launcher
from rocknix_launchers import fixed_paths
from shell_launchers import (
LauncherPatternMissing,
Link,
RepoTree,
Script,
describe_keys,
settings,
source_ref,
words,
)
SX05RE = "packages/sx05re"
BIN = f"{SX05RE}/emuelec/bin"
RUN_EMU = f"{BIN}/emuelecRunEmu.sh"
UTILS = f"{BIN}/emuelec-utils"
EMULATORS = f"{SX05RE}/emulators"
LIBRETRO = f"{SX05RE}/libretro"
RETROARCH_MK = f"{SX05RE}/libretro_base/retroarch/package.mk"
BIOS_ROOT = "/storage/roms/bios"
_EMU_TEST = re.compile(r'\$\{?EMU\}?"?\s*==?\s*"([^"]+)"')
_ARM = re.compile(r'^\s*((?:"[^"]+"\|?)+)\)\s*$')
_RUNTHIS = re.compile(r"""^\s*RUNTHIS\+?=(['"])(.*)\1\s*$""")
_SCRIPT = re.compile(r"[\w./-]+\.(?:sh|start)$")
_ADVMAME_INSTALL = re.compile(r"cp -r \$\{PKG_DIR\}/config/(advmame\.rc\w*) \S+/advmame\.rc$")
@dataclass
class Dispatch:
"""The cores emuelecRunEmu.sh hands to one script, and the lines that do it."""
emulators: list[str] = field(default_factory=list)
lines: set[int] = field(default_factory=set)
class Runs:
"""emuelecRunEmu.sh, read once: which core starts which script."""
def __init__(self, tree: RepoTree) -> None:
self.script = Script(RUN_EMU, tree.read(RUN_EMU))
self.by_script: dict[str, Dispatch] = {}
emu: tuple[str, int] | None = None
arm: tuple[list[str], int] | None = None
for number, line in self.script.code():
if found := _ARM.match(line):
arm = ([p.strip('"') for p in found.group(1).split("|") if p], number)
emu = None
continue
if re.match(r"\s*(;;|esac\b)", line):
arm, emu = None, None
continue
if found := _EMU_TEST.search(line):
emu = (found.group(1).removesuffix("_libretro"), number)
continue
if re.match(r"\s*fi\b", line):
emu = None
continue
run = _RUNTHIS.match(line)
command = words(run.group(2) if run else line)
name = next(
(posixpath.basename(w.text) for w in command if _SCRIPT.fullmatch(w.text)), None
)
if name is None or (emu is None and arm is None):
continue
names, at = ([emu[0]], emu[1]) if emu else arm # type: ignore[misc]
entry = self.by_script.setdefault(name, Dispatch())
entry.emulators.extend(n for n in names if n not in entry.emulators)
entry.lines |= {at, number}
if not self.by_script:
raise self.script.missing("script started for a core")
def of(self, name: str) -> Dispatch:
if name not in self.by_script:
raise self.script.missing(f"start of {name}")
return self.by_script[name]
def emulator(self, name: str) -> str:
return ", ".join(self.of(name).emulators)
def ref(self, name: str) -> tuple[str, set[int]]:
return (RUN_EMU, set(self.of(name).lines))
def _read(tree: RepoTree, path: str) -> Script:
return Script(path, tree.read(path))
def _under(path: str, root: str = BIOS_ROOT) -> bool:
return path.rstrip("/") == root or path.startswith(root + "/")
def retroarch(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""system_directory, written by sed into the retroarch.cfg the package installs."""
mk = _read(tree, RETROARCH_MK)
sed_line, sed = mk.find(
r"s/# system_directory =/system_directory =((?:\\/|[^/])+)/", "system_directory sed"
)
run_line, _ = runs.script.find(
r"RUNTHIS='\$\{RABIN\}.* -L /tmp/cores/\$\{EMU\}\.so --config \$\{RACONF\}",
"RetroArch start with --config",
)
return [
Launcher(
emulator="retroarch",
build="libretro",
kind="passes_path",
names=[],
source_ref=source_ref([(mk.path, {sed_line}), (RUN_EMU, {run_line})]),
search_dir=sed.group(1).replace("\\/", "/"),
passes_to="retroarch.cfg system_directory (--config)",
)
]
def retrorun(tree: RepoTree, runs: Runs) -> list[Launcher]:
script = runs.script
base_line, base = script.find(r'^\s*RUNTHIS="(\w+)"\s*$', "retrorun command")
test_line, _ = script.find(
rf'if \[\[ "\$\{{EMULATOR\}}" = "{base.group(1)}" \]\]', f"{base.group(1)} test"
)
run_line, run = script.find(r"RUNTHIS\+=.*\s(-d)\s+(\S+)\s", "retrorun -d", base_line)
return [
Launcher(
emulator=base.group(1),
build="libretro",
kind="passes_path",
names=[],
source_ref=source_ref([(RUN_EMU, {test_line, base_line, run_line})]),
search_dir=run.group(2),
passes_to=f"{base.group(1)} {run.group(1)}",
)
]
def freej2me(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""JAVA_HOME set for the core, and the jar its script copies beside the JDK."""
script = runs.script
test_line, test = script.find(r'if \[ \$\{EMU\} == "(\w+)_libretro" \]', "core test")
call_line, call = script.find(r"^\s*(\S+\.sh)\s*$", "script call", test_line)
java_line, java = script.find(r"^\s*(JAVA_HOME)='([^']+)'", "JAVA_HOME", test_line)
export_line, _ = script.find(rf"^\s*export {java.group(1)}\s*$", "export", java_line)
core = test.group(1)
path = f"{LIBRETRO}/{core}/scripts/{call.group(1)}"
started = _read(tree, path)
guard_line, guard = started.find(r'if \[\[ -f "([^"]+)" \]\]; then$', "JDK version test")
copy_line, copy = started.find(r"^\s*cp -rf (\S+) (\S+)$", "jar copy", guard_line)
source = copy.group(1)
return [
Launcher(
emulator=core,
build="libretro",
kind="passes_path",
names=[],
source_ref=source_ref(
[(RUN_EMU, {test_line, call_line, java_line, export_line}), (path, {guard_line})]
),
search_dir=java.group(2),
passes_to=f"environment {java.group(1)}",
),
Launcher(
emulator=core,
build="libretro",
kind="copies",
names=[posixpath.basename(source)],
source_ref=source_ref(
[(RUN_EMU, {test_line, call_line}), (path, {guard_line, copy_line})]
),
search_dir=posixpath.dirname(source),
passes_to=copy.group(2),
depends_on_option=f"{guard.group(1)} exists",
),
]
def pico8(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""The folder the user drops in bios/ is moved where the binary runs from."""
path = f"{BIN}/pico8.sh"
script = _read(tree, path)
guard_line, guard = script.find(r'if \[\[ -e "([^"]+)" \]\]; then$', "new version test")
folder = guard.group(1)
copy_line, copy = script.find(
rf"^\s*cp -rf {re.escape(folder)}/\* (\S+)$", "copy of the folder", guard_line
)
target = copy.group(1)
remove_line, _ = script.find(rf"^\s*rm -rf {re.escape(folder)}$", "removal", copy_line)
mode_line, mode = script.find(
rf"^\s*chmod \+x {re.escape(target)}/([^/\s]+)$", "executable bit", remove_line
)
binary = mode.group(1)
starts = {
n for n, line in script.code() if re.match(rf"\s*{re.escape(target)}/{binary} -", line)
}
if not starts:
raise script.missing(f"start of {target}/{binary}")
name = posixpath.basename(path)
return [
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="moves",
names=[binary],
source_ref=source_ref(
[runs.ref(name), (path, {guard_line, copy_line, remove_line, mode_line, *starts})]
),
case_insensitive=False,
search_dir=folder,
passes_to=target,
fails_when_missing=False,
depends_on_option=f"{folder} exists",
)
]
def link_record(emulator: str, script: Script, link: Link, extra: set[int]) -> Launcher:
return Launcher(
emulator=emulator,
build="standalone",
kind="links",
names=list(link.names),
source_ref=source_ref([(script.path, link.lines | extra)]),
expands=link.expands or None,
search_dir=link.bios_side,
passes_to=f"symlink {link.link} -> {link.target}",
fails_when_missing=False,
)
def _link_guard(script: Script, link: Link) -> set[int]:
"""The `[ ! -L <link> ]` test the link waits for, when it is the one just above."""
first = max(link.lines)
tests = [
number
for number, line in script.code()
if number < first
and re.search(r"\[ ! -L ", line)
and link.link in script.expand(line, number, set())
]
return {tests[-1]} if tests and first - tests[-1] <= 4 else set()
# Start scripts whose ln -s commands put a BIOS folder where the emulator reads.
LINK_SCRIPTS = (
f"{EMULATORS}/flycastsa/scripts/flycast.sh",
f"{EMULATORS}/flycast-dojo/scripts/flycastdojo.sh",
)
# Packages whose package.mk links a BIOS folder into the installed tree.
LINK_PACKAGES = (
f"{EMULATORS}/amiberry",
f"{EMULATORS}/amiberry-lite",
f"{SX05RE}/tools/sound/timidity",
)
def links(tree: RepoTree, runs: Runs) -> list[Launcher]:
records = []
for path in LINK_SCRIPTS:
script = _read(tree, path)
found = script.links(BIOS_ROOT)
if not found:
raise script.missing(f"ln -s of a path under {BIOS_ROOT}")
name = posixpath.basename(path)
for link in found:
record = link_record(runs.emulator(name), script, link, _link_guard(script, link))
record.source_ref = f"{source_ref([runs.ref(name)])}, {record.source_ref}"
records.append(record)
for package in LINK_PACKAGES:
script = _read(tree, f"{package}/package.mk")
found = script.links(BIOS_ROOT, fixed={"INSTALL": ""})
if not found:
raise script.missing(f"ln -s of a path under {BIOS_ROOT}")
records.extend(
link_record(posixpath.basename(package), script, link, set()) for link in found
)
return records
def duckstation(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""The settings.ini the package ships hands a directory and leaves the paths empty."""
package = f"{EMULATORS}/duckstation"
path = f"{package}/config/settings.ini"
found = {(s.section, s.key): s for s in settings(tree.read(path))}
folder_key = ("BIOS", "SearchDirectory")
file_keys = (("BIOS", "PathNTSCU"), ("BIOS", "PathNTSCJ"), ("BIOS", "PathPAL"))
if folder_key not in found:
raise LauncherPatternMissing(path, "[BIOS] SearchDirectory")
set_files = [k for k in file_keys if k in found and found[k].value]
if set_files:
raise LauncherPatternMissing(path, f"empty [BIOS] {set_files[0][1]}")
mk = _read(tree, f"{package}/package.mk")
mk_line, _ = mk.find(r"cp -rf \$\{PKG_DIR\}/config/\* ", "config install")
script = _read(tree, f"{package}/scripts/duckstation.sh")
run_line, _ = script.find(r'-settings "\$\{CONFIG_DIR\}/settings\.ini"', "-settings")
name = posixpath.basename(script.path)
lines = {found[k].line for k in (folder_key, *file_keys) if k in found}
unset = describe_keys("", file_keys).strip()
return [
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="scans_dir",
names=[],
source_ref=source_ref(
[runs.ref(name), (path, lines), (mk.path, {mk_line}), (script.path, {run_line})]
),
search_dir=found[folder_key].value,
passes_to=f"settings.ini [BIOS] SearchDirectory (-settings), {unset} unset",
)
]
YABASANSHIRO_SCRIPTS = (
f"{EMULATORS}/yabasanshiroSA_1_11/scripts/yabasanshiro.sh",
f"{EMULATORS}/yabasanshiroSA_1_5/scripts/yabasanshiro1_5.sh",
)
def yabasanshiro(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""The BIOS flag, set when the HLE option is off and the file exists."""
records = []
for path in YABASANSHIRO_SCRIPTS:
script = _read(tree, path)
option_line, option = script.find(r"^\s*(\w+)=\$\(get_ee_setting (\w+) ", "HLE option")
test_line, test = script.find(
rf'if \[ "\$\{{{option.group(1)}\}}" != (\w+) \]', "HLE option test", option_line
)
exists_line, exists = script.find(r'if \[ -e "([^"]+)" \]', "BIOS test", test_line)
bios = exists.group(1)
flag_line, flag = script.find(
rf'^\s*(\w+)="(-\w) {re.escape(bios)}"$', "BIOS flag", exists_line
)
run_line, run = script.find(
rf"\{{\s*(\S+) .*\$\{{{flag.group(1)}\}}", "start with the flag", flag_line
)
name = posixpath.basename(path)
records.append(
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="fixed_name",
names=[posixpath.basename(bios)],
source_ref=source_ref(
[
runs.ref(name),
(path, {option_line, test_line, exists_line, flag_line, run_line}),
]
),
case_insensitive=False,
search_dir=posixpath.dirname(bios),
passes_to=f"{run.group(1)} {flag.group(2)}",
fails_when_missing=False,
depends_on_option=f"{option.group(2)} != {test.group(1)}",
)
)
return records
def jzintv(tree: RepoTree, runs: Runs) -> list[Launcher]:
path = f"{EMULATORS}/jzintv/scripts/jzintv.sh"
script = _read(tree, path)
run_line, run = script.find(r"^\s*exec (\S+) .*\s(-p)\s+(\S+)\s", "start with -p")
name = posixpath.basename(path)
return [
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="passes_path",
names=[],
source_ref=source_ref([runs.ref(name), (path, {run_line})]),
search_dir=run.group(3).rstrip("/"),
passes_to=f"{run.group(1)} {run.group(2)}",
)
]
def mupen64plus(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""The 64DD IPL names, tried in order, passed only beside a .ndd disk."""
path = f"{EMULATORS}/mupen64plussa/mupen64plussa-core/m64p.sh"
script = _read(tree, path)
fallbacks = [
(number, found)
for number, line in script.code()
if (found := re.search(r'\[ ! -f "\$\{(\w+)\}" \] && \1="([^"]+)"', line))
]
if not fallbacks:
raise script.missing("fallback name test")
var = fallbacks[0][1].group(1)
first = script.value(var, fallbacks[0][0])
if first is None:
raise script.missing(f"assignment of {var}")
used = {first.line, *(n for n, _ in fallbacks)}
paths = [
script.expand(v, n, used)
for n, v in [(first.line, first.value), *((n, f.group(2)) for n, f in fallbacks)]
]
folders = {posixpath.dirname(p) for p in paths}
if len(folders) != 1:
raise script.missing(f"one folder for the {var} names")
test_line, test = script.find(
rf'if \[ -f "([^"]+)" \] && \[ -f "\$\{{{var}\}}" \]', "disk and IPL test"
)
passes = [
(number, found)
for number, line in script.code()
if (found := re.search(rf'^\s*(\S+) .*(--[\w-]+) "\$\{{{var}\}}"', line))
]
if not passes:
raise script.missing(f"flag that hands {var}")
flags = sorted({f"{f.group(1)} {f.group(2)}" for _, f in passes})
name = posixpath.basename(path)
return [
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="names_searched",
names=[posixpath.basename(p) for p in paths],
source_ref=source_ref(
[runs.ref(name), (path, used | {test_line} | {n for n, _ in passes})]
),
case_insensitive=False,
search_dir=folders.pop(),
passes_to=", ".join(flags),
fails_when_missing=False,
depends_on_option=f'-f "{test.group(1)}"',
)
]
def hatari(tree: RepoTree, runs: Runs) -> list[Launcher]:
package = f"{EMULATORS}/hatarisa"
script = _read(tree, f"{package}/scripts/hatari.start")
copy_line, copy = script.find(r'^\s*cp \S+/([\w.-]+\.cfg) "\$(\w+)"', "default config copy")
run_lines = {
n
for n, line in script.code()
if n > copy_line and f'--configfile "${copy.group(2)}"' in line
}
if not run_lines:
raise script.missing(f"--configfile ${copy.group(2)}")
path = f"{package}/config/conf/{copy.group(1)}"
found = settings(tree.read(path))
keys = [
(s.section, s.key)
for s in found
if _under(s.value) and "." in posixpath.basename(s.value)
]
if not keys:
raise LauncherPatternMissing(path, f"key naming a file under {BIOS_ROOT}")
value, lines = fixed_paths(found, keys, path)
mk = _read(tree, f"{package}/package.mk")
mk_line, _ = mk.find(r"cp -R \$\{PKG_DIR\}/config/\* ", "config install")
name = posixpath.basename(script.path)
return [
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="fixed_name",
names=list(value.names),
source_ref=source_ref(
[runs.ref(name), (path, lines), (mk.path, {mk_line}),
(script.path, {copy_line, *run_lines})]
),
search_dir=value.search_dir,
passes_to=f"{describe_keys(copy.group(1), value.keys)} (--configfile)",
)
]
def advancemame(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""Folder keys of the advmame.rc package.mk installs, one per device branch."""
package = f"{EMULATORS}/advancemame"
mk = _read(tree, f"{package}/package.mk")
installs = [
(number, found.group(1))
for number, line in mk.code()
if (found := _ADVMAME_INSTALL.search(line))
]
if not installs:
raise mk.missing("advmame.rc install")
by_value: dict[tuple[tuple[str, str], ...], list[tuple[str, set[int], int]]] = {}
for mk_line, file in installs:
path = f"{package}/config/{file}"
found = [s for s in settings(tree.read(path), separator=" ") if _under(s.value)]
if not found:
raise LauncherPatternMissing(path, f"key naming a folder under {BIOS_ROOT}")
value = tuple((s.key, s.value) for s in found)
by_value.setdefault(value, []).append((path, {s.line for s in found}, mk_line))
if len(by_value) != 1:
raise mk.missing("one set of BIOS folders for every device")
((value, configs),) = by_value.items()
script = _read(tree, f"{package}/bin/advmame.sh")
run_line, _ = script.find(r"^\s*(?:\w+=\S+ )*advmame ", "advmame start")
name = posixpath.basename(script.path)
refs = [
runs.ref(name),
*((path, lines) for path, lines, _ in configs),
(mk.path, {mk_line for _, _, mk_line in configs}),
(script.path, {run_line}),
]
return [
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="passes_path",
names=[],
source_ref=source_ref(refs),
search_dir=folder,
passes_to=f"advmame.rc {key}",
)
for key, folder in value
]
def mame(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""The files mame.sh copies into bios/mame, and the folders mame.ini hands the core."""
package = f"{LIBRETRO}/mame"
script = _read(tree, f"{package}/scripts/mame.sh")
name = posixpath.basename(script.path)
emulator = runs.emulator(name)
records = []
guard: tuple[int, str] | None = None
for number, line in script.code():
if found := re.search(r'if \[ ! -f "([^"]+)" \]', line):
guard = (number, found.group(1))
continue
copy = re.match(r'\s*cp -rf "([^"]+)/"(\S+) "([^"]+)"\s*$', line)
if copy is None:
continue
if guard is None:
raise script.missing(f"test before the copy at line {number}")
if not _under(copy.group(3)):
continue
records.append(
Launcher(
emulator=emulator,
build="libretro",
kind="copies",
names=[copy.group(2)],
source_ref=source_ref([runs.ref(name), (script.path, {guard[0], number})]),
search_dir=copy.group(1),
passes_to=copy.group(3),
fails_when_missing=False,
depends_on_option=f"{guard[1]} absent",
)
)
if not records:
raise script.missing(f"copy into {BIOS_ROOT}")
ini = next((r for r in records if r.names == ["mame*"]), None)
if ini is None:
raise script.missing("copy of mame.ini")
option_path = f"{package}/config/MAME/MAME.opt"
option = next(
(s for s in settings(tree.read(option_path)) if s.key == "mame_read_config"), None
)
if option is None:
raise LauncherPatternMissing(option_path, "mame_read_config")
ini_path = f"{package}/config/ini/mame.ini"
# MAME reads from the folders its *path options name and writes to its
# *_directory ones.
folders = [
(s, [p for p in s.value.split(";") if _under(p)])
for s in settings(tree.read(ini_path), separator=" ")
if s.key.endswith("path")
]
folders = [(s, parts) for s, parts in folders if parts]
if not folders:
raise LauncherPatternMissing(ini_path, f"path under {BIOS_ROOT}")
for setting, parts in folders:
for part in parts:
records.append(
Launcher(
emulator=emulator,
build="libretro",
kind="passes_path",
names=[],
source_ref=source_ref(
[(ini_path, {setting.line}), (option_path, {option.line})]
) + f", {ini.source_ref}",
search_dir=part.rstrip("/"),
passes_to=f"mame.ini {setting.key}",
depends_on_option=f"mame_read_config = {option.value}",
)
)
return records
def scummvm(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""--extrapath for the standalone build, and the scummvm.ini copied for the core."""
path = f"{EMULATORS}/scummvmsa/bin/scummvm.start"
script = _read(tree, path)
name = posixpath.basename(path)
test_line, test = script.find(r'^\s*if \[ -d "([^"]+)" \]; then\s*$', "extra folder test")
set_line, set_ = script.find(r'^\s*(\w+)="(--[\w-]+)=([^"]+)"', "extra path", test_line)
run_line, run = script.find(rf"^\s*(\S+) .*\${set_.group(1)}\b", "start with it", set_line)
core_line, core = script.find(r"-L /tmp/cores/(\w+)_libretro\.so", "libretro start")
arm_line, arm = script.find(r'^\s*"(\w+)"\)', "setup arm", core_line)
copy_line, copy = script.find(r"^\s*cp (\S+) (\S+)$", "copy into the BIOS folder", arm_line)
used = {copy_line}
source = script.expand(copy.group(1), copy_line, used)
target = script.expand(copy.group(2), copy_line, used)
if not _under(target):
raise script.missing(f"copy into {BIOS_ROOT}")
return [
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="passes_path",
names=[],
source_ref=source_ref([runs.ref(name), (path, {test_line, set_line, run_line})]),
search_dir=set_.group(3),
passes_to=f"{run.group(1)} {set_.group(2)}",
depends_on_option=f"{test.group(1)} exists",
),
Launcher(
emulator=core.group(1),
build="libretro",
kind="copies",
names=[posixpath.basename(source)],
source_ref=source_ref([(path, {core_line, arm_line} | used)]),
search_dir=posixpath.dirname(source),
passes_to=target,
fails_when_missing=False,
depends_on_option=f"{name} {arm.group(1)}",
),
]
def dolphin(tree: RepoTree, runs: Runs) -> list[Launcher]:
path = f"{EMULATORS}/dolphinSA/scripts/dolphin.sh"
script = _read(tree, path)
run_line, run = script.find(r"\b(XDG_DATA_HOME)=(\S+) .*\bdolphin-emu", "XDG_DATA_HOME start")
name = posixpath.basename(path)
return [
Launcher(
emulator=runs.emulator(name),
build="standalone",
kind="passes_path",
names=[],
source_ref=source_ref([runs.ref(name), (path, {run_line})]),
search_dir=run.group(2),
passes_to=f"environment {run.group(1)}",
)
]
def midi(tree: RepoTree, runs: Runs) -> list[Launcher]:
"""The MIDI synth set_midi_source starts for the ra_midi_output setting."""
script = runs.script
setting_line, setting = script.find(
r'^\s*(\w+)=\$\(get_ee_setting "(\w*midi\w*)" ', "MIDI setting"
)
call_line, _ = script.find(
rf'set_midi_source "\$\{{{setting.group(1)}\}}"', "set_midi_source call", setting_line
)
utils = _read(tree, UTILS)
head_line, _ = utils.find(r"^\s*function set_midi_source\(\)", "set_midi_source")
end_line, _ = utils.find(r"^\s*esac\b", "end of the source case", head_line)
records = []
arm: tuple[int, str] | None = None
for number, line in utils.code():
if number <= head_line or number >= end_line:
continue
if found := re.match(r"\s*([\w-]+)\)\s*$", line):
arm = (number, found.group(1))
continue
command = [w.text for w in words(line)]
paths = [
(command[i - 1], value)
for i, value in enumerate(command)
if i and command[i - 1].startswith("-") and re.search(r"(^|/)bios(/|$)", value)
]
if not paths or arm is None:
continue
((flag, value),) = paths
is_file = "." in posixpath.basename(value) and _under(value)
records.append(
Launcher(
emulator=command[0],
build="standalone",
kind="fixed_name" if is_file else "passes_path",
names=[posixpath.relpath(value, BIOS_ROOT)] if is_file else [],
source_ref=source_ref(
[(RUN_EMU, {setting_line, call_line}), (UTILS, {head_line, arm[0], number})]
),
slot=arm[1],
case_insensitive=False if is_file else None,
search_dir=BIOS_ROOT if is_file else value.rstrip("/"),
passes_to=f"{command[0]} {flag}",
fails_when_missing=False,
depends_on_option=f"{setting.group(2)} = {arm[1]}",
)
)
if not records:
raise utils.missing("MIDI synth started with a BIOS path")
return records
EXTRACTORS: tuple[Callable[[RepoTree, Runs], list[Launcher]], ...] = (
retroarch,
retrorun,
freej2me,
mame,
pico8,
links,
duckstation,
yabasanshiro,
jzintv,
mupen64plus,
hatari,
advancemame,
scummvm,
dolphin,
midi,
)
def extract_launchers(tree: RepoTree) -> list[dict]:
"""Every launcher record the extractors find, in extractor order."""
runs = Runs(tree)
return [record.entry() for extractor in EXTRACTORS for record in extractor(tree, runs)]
+14
View File
@@ -43,6 +43,7 @@ from .batocera_scraper import (
platform_systems,
requirements_from_systems,
)
from .emuelec_launchers import extract_launchers
from .es_emulators import (
SystemEmulators,
attach_system_cores,
@@ -50,6 +51,7 @@ from .es_emulators import (
json_systems,
system_cores,
)
from .shell_launchers import RepoTree
PLATFORM_NAME = "emuelec"
@@ -120,6 +122,14 @@ BUILDS_OF = {
PORTS_SYSTEM = "ports"
def launcher_revision(url: str) -> str:
"""The tag or commit a batocera-systems URL is read at."""
found = re.search(rf"/{REPO}/([^/]+)/packages/", url)
if found is None:
raise ValueError(f"{url} names no {REPO} revision")
return found.group(1)
def port_engines(text: str) -> list[tuple[str, str, str | None, bool]]:
"""(port, engine, check_bios key, libretro?) for each entry of ports.yaml.
@@ -215,6 +225,10 @@ class Scraper(BaseScraper):
"standalone_cores": standalone,
"dual_mode_cores": dual,
"systems": systems,
# Read at the revision the BIOS list comes from.
"launchers": extract_launchers(
RepoTree.github(REPO, launcher_revision(self.url))
),
}
+36
View File
@@ -19,6 +19,8 @@ import json
from collections.abc import Callable
from pathlib import Path
from siteguides import PACKAGE_COMMAND
SITE = "https://abdess.github.io/retrobios/"
PUBLISHER = "abdess.github.io"
SH = "curl -fsSL https://abdess.github.io/retrobios/i | sh -s --"
@@ -31,6 +33,22 @@ QUOTING = (f"A folder goes in single quotes, attached to its option. One that ho
"command. On a page of the site, the `device_setup_command` WebMCP tool writes the "
"command and refuses such a folder.")
UVX = PACKAGE_COMMAND
# EVENT_SCHEMA of install.py, and the schema generate_event_schema.py writes
# from EVENT_KEYS, as the site publishes it. tests/test_site_agents.py holds
# both to install.py.
EVENT_SCHEMA = 1
EVENTS_SCHEMA_PATH = "api/v1/schemas/install-events.schema.json"
# Every code of the Exit status table of wiki/installer.md, in short.
EXIT_CODES = (
(0, "everything asked for is installed or already in place"),
(1, "a failure: a download, a file list or a folder that cannot be used"),
(2, "nothing was done: an option refused, every file failed, or the user quit"),
(3, "`--check` found files missing or wrong, nothing having failed"),
(130, "stopped with Ctrl+C"),
(141, "the output went to a reader that closed early"),
)
START_PAGES = (
("Which pack is needed", "which-pack/", "pick a device or setup, get the folder its BIOS files go in, "
"one command that installs and checks them, or a ZIP pack"),
@@ -108,6 +126,24 @@ def llms_txt(platforms: list[tuple[str, str]]) -> str:
"- A game the installer cannot identify gets every BIOS of its system, and the run names it.",
f"- The same steps as an Agent Skill: [SKILL.md]({SITE}{SKILL_PATH}).",
"",
"## Driving the installer for a user",
"",
f"With Python 3.8 or newer, the installer is also a package: `{UVX} --for-games "
f"--required-only --dry-run`, or `pipx run retrobios` with the same options.",
"",
"1. Plan: run it with `--dry-run --events`. Nothing is written, and standard output carries "
f"one JSON object per line, event schema {EVENT_SCHEMA} "
f"([JSON Schema]({SITE}{EVENTS_SCHEMA_PATH})): a `file` event per file with its "
"`state`, `check` with the totals and `bytes_needed`, then `summary` with `pending`.",
"2. Consent: show the user what will be installed, where, and the size, and ask.",
"3. Act, only on a yes: the same command with `--yes` in place of `--dry-run`.",
"",
"- `--yes` asks nothing: use it in a run without a terminal. A run that needs an "
"answer (two setups found, none found) stops and names the option to give.",
"- The exit code, also the `code` of the last `end` event. An option refused at once "
"writes no event at all:",
*(f" - `{code}`: {meaning}" for code, meaning in EXIT_CODES),
"",
"## Start",
"",
*(f"- [{title}]({SITE}{path}): {what}" for title, path, what in START_PAGES),
+69
View File
@@ -636,5 +636,74 @@ class ContributorChecksFetchThePlatformsFiles(unittest.TestCase):
self.assertIn("export_native.py --all --refresh-cache", _read("wiki/testing-guide.md"))
class ArchiveRecipesAreCountedFromTheRecipes(unittest.TestCase):
"""Every archive has a recipe in recipes/archives.json: the README and
the site's page say how many rebuild exact and canonical, counted from
that file, never typed."""
def setUp(self) -> None:
import archive_build # noqa: PLC0415
records = archive_build.load_ledger().values()
self.counts = {
"total": len(records),
"exact": sum(r["rebuild"] == "exact" for r in records),
"canonical": sum(r["rebuild"] == "canonical" for r in records),
}
self.assertEqual(archive_build.rebuild_counts(), self.counts)
self.assertEqual(self.counts["exact"] + self.counts["canonical"], self.counts["total"])
def test_the_readme_line(self) -> None:
from generate_readme import archive_line # noqa: PLC0415
line = archive_line(self.counts)
self.assertIn(line, _read("README.md").splitlines())
self.assertIn(f"**{self.counts['total']:,} archives**", line)
self.assertIn(f"{self.counts['exact']:,} byte for byte", line)
self.assertIn(f"{self.counts['canonical']:,} with the same files inside", line)
self.assertIn(f"({SITE_URL}wiki/archive-recipes/)", line)
def test_the_page_counts(self) -> None:
import generate_site # noqa: PLC0415
page = _read("wiki/archive-recipes.md")
self.assertEqual(generate_site.with_archive_counts(page, self.counts), page,
"wiki/archive-recipes.md counts are stale: run scripts/generate_site.py")
for line in generate_site.archive_recipe_lines(self.counts):
self.assertIn(line, page)
def test_the_page_command_is_the_tools(self) -> None:
command = re.search(r"python scripts/romset_recipes.py (.+)", _read("wiki/archive-recipes.md"))
source = _read("scripts/romset_recipes.py")
for flag in re.findall(r"--[a-z-]+", command.group(1)):
with self.subTest(flag=flag):
self.assertIn(f'"{flag}"', source)
class TheReadmeBadges(unittest.TestCase):
"""One row of four badges under the links, each linking its source."""
EXPECTED = (
'<p align="center">'
'<a href="https://pypi.org/project/retrobios/"><img src="https://img.shields.io/pypi/v/retrobios" alt="PyPI version"></a> '
'<a href="https://pypi.org/project/retrobios/"><img src="https://img.shields.io/pypi/pyversions/retrobios" alt="Python versions"></a> '
f'<a href="{REPO}/actions/workflows/validate.yml"><img src="{REPO}/actions/workflows/validate.yml/badge.svg?branch=main" alt="Validation"></a> '
'<a href="LICENSE"><img src="https://img.shields.io/github/license/Abdess/retrobios" alt="License"></a>'
"</p>"
)
def test_exactly_these_four(self) -> None:
readme = _read("README.md")
self.assertEqual(len(re.findall(r"<img src=\"https://(?:img\.shields\.io|github\.com)/", readme)), 4)
lines = readme.splitlines()
self.assertIn(self.EXPECTED, lines)
links = next(i for i, line in enumerate(lines) if ">Get the files</a>" in line)
self.assertEqual(lines[links + 2], self.EXPECTED)
def test_the_workflow_and_the_license_exist(self) -> None:
self.assertTrue((REPO_ROOT / ".github" / "workflows" / "validate.yml").is_file())
self.assertTrue((REPO_ROOT / "LICENSE").is_file())
if __name__ == "__main__":
unittest.main()
+17
View File
@@ -1183,6 +1183,23 @@ class TestE2E(unittest.TestCase):
recalbox = {"cores": ["mame0278"], "systems": {}}
self.assertEqual(resolve_platform_cores(recalbox, profiles), {"mame0278"})
def test_a_platform_s_own_libretro_build_comes_with_its_retroarch(self):
"""ROCKNIX ships yabadabadoo with its RetroArch and names it nowhere
else: a list naming retroarch takes the libretro builds whose
built_by names that platform, and no other platform's."""
profiles = {
"yabause": {"type": "libretro", "systems": ["saturn"], "files": []},
"yabadabadoo": {"type": "libretro", "built_by": ["rocknix"],
"systems": ["saturn"], "files": []},
}
rocknix = {"_key": "rocknix", "cores": ["retroarch"], "systems": {}}
self.assertEqual(resolve_platform_cores(rocknix, profiles),
{"yabause", "yabadabadoo"})
other = {"_key": "batocera", "cores": ["retroarch"], "systems": {}}
self.assertEqual(resolve_platform_cores(other, profiles), {"yabause"})
everyone = {"_key": "retroarch", "cores": "all_libretro", "systems": {}}
self.assertEqual(resolve_platform_cores(everyone, profiles), {"yabause"})
def test_resolve_cores_explicit_list(self):
"""Explicit cores list matches against profile dict keys."""
config = {"cores": ["fbneo", "opera"], "systems": {"arcade": {"files": []}}}
+13 -6
View File
@@ -366,6 +366,13 @@ HOME = Path("/home/user")
SYSTEM_DIR = HOME / ".config" / "retroarch" / "system"
def screen_text(lines) -> str:
"""The screen as one line of words, a path broken after a separator
joined again: a folder under the checkout is as long as the checkout's
own path, and a deep clone wrapped it across two lines."""
return re.sub(r"(?<=[/\\]) ", "", " ".join(" ".join(lines).split()))
def fresh_state() -> install.TuiState:
state = install.TuiState(os_type="linux", home=HOME)
state.setups = [("retroarch", SYSTEM_DIR, "RetroArch"),
@@ -1856,7 +1863,7 @@ class TicksBarAndPaths(unittest.TestCase):
state.stack = ["folder"]
self.assertEqual(install._choose(state, "minui", card / "Bios"), [])
self.assertEqual(state.screen, "cardroot")
text = " ".join(line.strip() for line in install.tui_view(state, 120, 40))
text = screen_text(install.tui_view(state, 120, 40))
self.assertIn(f"under the card's root: {card}", text)
self.assertEqual(press(state, "enter"), [("fetch", "minui")])
self.assertEqual(state.dest, card)
@@ -2343,7 +2350,7 @@ class CardsSpaceAndConfirm(unittest.TestCase):
state, effects = install.tui_start(_args(), "linux")
label = f"muOS SD card ({root.name})"
self.assertEqual(state.setups, [("retroarch", root / "MUOS" / "bios", label)])
text = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
text = screen_text(install.tui_view(state, 120, 40))
self.assertIn(f"> {label}", text)
self.assertIn(f"BIOS folder: {root / 'MUOS' / 'bios'}", text)
(root / "MUOS" / "bios").mkdir()
@@ -4135,7 +4142,7 @@ class AFolderThisUserCannotOpen(unittest.TestCase):
self.dest = self.lock / "system"
def said(self, state: install.TuiState) -> str:
return " ".join(" ".join(install.tui_view(state, 200, 40)).split())
return screen_text(install.tui_view(state, 200, 40))
def test_typed_it_stays_on_the_path_screen(self):
state = fresh_state()
@@ -6507,7 +6514,7 @@ class OneCardReading(unittest.TestCase):
self.assertEqual(state.screen, "checking" if one else "create")
if not one:
self.assertIn(note.split(":")[0],
" ".join(" ".join(install.tui_view(state, 120, 40)).split()))
screen_text(install.tui_view(state, 120, 40)))
install.tui_update(state, Key("enter"))
self.assertEqual(state.card_note, note)
@@ -11117,7 +11124,7 @@ class TapsGameDataAndExitCodes(_CardFixtures, unittest.TestCase):
_title, lines = install.tui_outcome(state)
self.assertIn(f"BIOS folder: {card / 'Bios'}", lines)
state.stack = ["checking"]
self.assertIn(str(card / "Bios"), " ".join(install.tui_view(state, 120, 40)))
self.assertIn(str(card / "Bios"), screen_text(install.tui_view(state, 120, 40)))
# Root cause 7: colour roles read the words.
@@ -11263,7 +11270,7 @@ class TapsGameDataAndExitCodes(_CardFixtures, unittest.TestCase):
folder = next(i for i, line in enumerate(lines) if line.startswith("BIOS folder:"))
named = next(i for i, line in enumerate(lines) if line.startswith("Named by"))
# Nothing between the folder, wrapped or not, and what named it.
self.assertTrue(all(line.startswith("/") for line in lines[folder + 1:named]), lines)
self.assertEqual("".join(lines[folder:named]), f"BIOS folder: {state.dest}", lines)
self.assertLess(named, next(i for i, line in enumerate(lines)
if line.startswith(("For ", "Copy them"))))
+655
View File
@@ -0,0 +1,655 @@
"""EmuELEC's launcher layer read from its packages.
Each fixture is a cut-down copy of the lines an extractor reads at EmuELEC
v4.8 (02b45fe53b85c45c75dbad8e44a648092dec2e72). The records must name the
core emuelecRunEmu.sh dispatches, carry the names in the order the code
tries them, point at the fixture's own lines, and a construct that is gone
must stop the scrape.
"""
from __future__ import annotations
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
import sys
import textwrap
import unittest
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
from scraper import emuelec_launchers as el # noqa: E402
from scraper.emuelec_scraper import launcher_revision # noqa: E402
from scraper.shell_launchers import LauncherPatternMissing, RepoTree # noqa: E402
EMU = el.EMULATORS
def text(block: str) -> str:
return textwrap.dedent(block).lstrip("\n")
RUN_EMU = text(
"""
if [[ "${EMULATOR}" = "libretro" ]]; then
EMU="${CORE}_libretro"
fi
if [[ "${EMULATOR}" = "retrorun" ]]; then
EMU="${CORE}_libretro"
fi
MIDI_OUTPUT=$(get_ee_setting "ra_midi_output" "${PLATFORM}" "${BASEROMNAME}")
if [[ ! -z "${MIDI_OUTPUT}" ]]; then
emuelec-utils set_midi_source "${MIDI_OUTPUT}" "${EMULATOR}"
fi
if [ ${EMU} == "freej2me_libretro" ]; then
freej2me.sh
JAVA_HOME='/storage/roms/bios/jdk'
export JAVA_HOME
fi
case ${PLATFORM} in
"atarist")
if [ "${EMU}" = "HATARISA" ]; then
RUNTHIS='${TBASH} hatari.start "${ROMNAME}"'
fi
;;
"dreamcast"|"naomi")
if [ "${EMU}" = "flycastsa" ]; then
RUNTHIS='${TBASH} flycast.sh "${ROMNAME}"'
elif [ "${EMU}" = "flycast_dojo" ]; then
RUNTHIS='flycastdojo.sh "${ROMNAME}"'
fi
;;
"psx")
if [ "${EMU}" = "duckstation" ]; then
RUNTHIS='${TBASH} duckstation.sh "${ROMNAME}"'
fi
;;
"mame")
if [ "${EMU}" = "AdvanceMame" ]; then
RUNTHIS='${TBASH} advmame.sh "${PLATFORM}" "${ROMNAME}"'
fi
;;
"n64")
if [ "${EMU}" = "rice" ]; then
RUNTHIS='${TBASH} m64p.sh "${ROMNAME}"'
elif [ "${EMU}" = "glide64mk2" ]; then
RUNTHIS='${TBASH} m64p.sh "${ROMNAME}" m64p_gl64mk2'
fi
;;
"scummvm")
if [ "${EMU}" = "SCUMMVMSA" ]; then
RUNTHIS='${TBASH} scummvm.start sa "${ROMNAME}"'
fi
;;
"wii"|"gamecube")
if [ "${EMU}" = "dolphin" ]; then
RUNTHIS='${TBASH} dolphin.sh "${ROMNAME}"'
fi
;;
"pico8")
RUNTHIS='${TBASH} pico8.sh "${ROMNAME}"'
;;
"intellivision")
if [ "${EMU}" = "jzintv" ]; then
RUNTHIS='jzintv.sh "${ROMNAME}"'
fi
;;
"saturn")
if [ "${EMU}" = "yabasanshiroSA" ]; then
RUNTHIS='yabasanshiro.sh "${ROMNAME}"'
elif [ "${EMU}" = "yabasanshiroSA1_5" ]; then
RUNTHIS='yabasanshiro1_5.sh "${ROMNAME}"'
fi
;;
esac
case ${PLATFORM} in
"arcade"|"mame")
if [ "$EMU" = "mame_libretro" ]; then
mame.sh
fi
;;
esac
RUNTHIS='${RABIN} ${VERBOSE} ${RA_ARGS} -L /tmp/cores/${EMU}.so --config ${RACONF} "${ROMNAME}"'
RUNTHIS="retrorun"
RUNTHIS+=' ${CMD_ROTATE} --triggers -g -d /storage/roms/bios /tmp/cores/${EMU}.so "${ROMNAME}"'
"""
)
UTILS = text(
"""
function set_midi_source()
{
case ${midi_source} in
fluidsynth)
fluidsynth -is /storage/roms/bios/sc/SC-55.sf2 &
;;
timidity)
timidity -iA --module 1 &
;;
mt32)
mt32d -o 2 -i 24 -f /roms/bios &
;;
esac
}
"""
)
RETROARCH_MK = text(
"""
sed -i -e "s/# savestate_directory =/savestate_directory =\\/storage\\/roms\\/savestates/" ${INSTALL}/etc/retroarch.cfg
sed -i -e "s/# system_directory =/system_directory =\\/storage\\/roms\\/bios/" ${INSTALL}/etc/retroarch.cfg
"""
)
FREEJ2ME_SH = text(
"""
install_java
if [[ -f "/storage/roms/bios/jdk/eeversion" ]]; then
cp -rf /usr/lib/libretro/freej2me-lr.jar ${HOME}/roms/bios
else
exit 1
fi
"""
)
PICO8_SH = text(
"""
if [[ -e "/storage/roms/bios/pico-8" ]]; then
mkdir -p /emuelec/bin/pico-8
cp -rf /storage/roms/bios/pico-8/* /emuelec/bin/pico-8
rm -rf /storage/roms/bios/pico-8
chmod +x /emuelec/bin/pico-8/pico8_dyn
fi
if [[ "${CART}" == *"/splore"* ]]; then
/emuelec/bin/pico-8/pico8_dyn -splore -home /emuelec/configs/pico-8
else
/emuelec/bin/pico-8/pico8_dyn -run "${CART}" -home /emuelec/configs/pico-8
fi
"""
)
FLYCAST_SH = text(
"""
if [ ! -L "/storage/.local/share/flycast" ]; then
mkdir -p "/storage/roms/bios/dc"
rm -rf "/storage/.local/share/flycast"
ln -sf "/storage/roms/bios/dc" "/storage/.local/share/flycast"
fi
"""
)
FLYCASTDOJO_SH = text(
"""
LOCAL_DATA="/storage/roms/bios/dc_dojo"
LOCAL_HOME="/storage/.local/share/flycast-dojo"
if [ ! -L "${LOCAL_HOME}" ]; then
rm -rf "${LOCAL_HOME}"
ln -sf "${LOCAL_DATA}" "${LOCAL_HOME}"
fi
"""
)
AMIBERRY_MK = text(
"""
cp -a ${PKG_DIR}/config/* ${INSTALL}/usr/config/amiberry/
ln -s /storage/roms/bios ${INSTALL}/usr/config/amiberry/kickstarts
"""
)
TIMIDITY_MK = text(
"""
ln -sf /storage/roms/bios/timidity/freepats ${INSTALL}/usr/config/timidity/freepats
ln -sf /storage/.config/timidity/freepats.cfg ${INSTALL}/etc/timidity/freepats.cfg
"""
)
DUCKSTATION_INI = text(
"""
[BIOS]
SearchDirectory = /storage/roms/bios
PathNTSCU =
PathNTSCJ =
PathPAL =
"""
)
DUCKSTATION_MK = text(
"""
cp -rf ${PKG_DIR}/config/* ${INSTALL}/usr/config/emuelec/configs/duckstation
"""
)
DUCKSTATION_SH = text(
"""
CONFIG_DIR="/emuelec/configs/duckstation"
duckstation-nogui -fullscreen -settings "${CONFIG_DIR}/settings.ini" -- "${1}"
"""
)
def yabasanshiro_sh(program: str) -> str:
return text(
f"""
BIOS=""
HLEBIOS=$(get_ee_setting hlebios saturn "${{ROMNAME}}")
if [ "${{HLEBIOS}}" != 1 ]; then
if [ -e "/storage/roms/bios/saturn_bios.bin" ]; then
BIOS="-b /storage/roms/bios/saturn_bios.bin"
else
BIOS=""
fi
fi
{{ {program} -r 2 -i "${{1}}" ${{BIOS}}; }} > /emuelec/logs/emuelec.log 2>&1
"""
)
JZINTV_SH = text(
"""
exec jzintv -f1 -z "$RES" -p /storage/roms/bios/ "$ROM" $KBD_OPT
"""
)
M64P_SH = text(
"""
BIOSDIR="/storage/roms/bios/Mupen64plus"
IPLROM="${BIOSDIR}/N64DD IPLROM (J).n64"
[ ! -f "${IPLROM}" ] && IPLROM="${BIOSDIR}/IPL.n64"
if [ -f "${GAMEDIR}/${GAMENAME}.ndd" ] && [ -f "${IPLROM}" ]; then
RENMODE="1"
else
IPLROM=""
fi
mupen64plus --fullscreen --dd-ipl-rom "${IPLROM}" --dd-disk "${DDDISK}" "${FILE}" ;;
"""
)
HATARI_START = text(
"""
HATARI_TMP_CONFIG="$HATARI_TMP_DIR"/.hatari-temp.cfg
cp $HATARI_CONFIG_DIR/Atari-ST-default.cfg "$HATARI_TMP_CONFIG"
hatari --configfile "$HATARI_TMP_CONFIG" --disk-a "$1"
"""
)
HATARI_CFG = text(
"""
[HardDisk]
szHardDiskDirectory = /storage/.hatari
[ROM]
szTosImageFileName = /storage/roms/bios/tos.img
szCartridgeImageFileName =
"""
)
HATARI_MK = text(
"""
cp -R ${PKG_DIR}/config/* ${INSTALL}/usr/config/hatari
"""
)
ADVMAME_MK = text(
"""
if [ "${DEVICE}" == "OdroidGoAdvance" ]; then
cp -r ${PKG_DIR}/config/advmame.rc_oga ${INSTALL}/usr/config/emuelec/configs/advmame/advmame.rc
else
cp -r ${PKG_DIR}/config/advmame.rc ${INSTALL}/usr/config/emuelec/configs/advmame/advmame.rc
fi
"""
)
ADVMAME_RC = text(
"""
dir_rom /storage/roms/mame
dir_sample /storage/roms/bios/advmame/samples
"""
)
ADVMAME_SH = text(
"""
SDL_AUDIODRIVER=alsa advmame ${ARG} -quiet
"""
)
MAME_SH = text(
"""
if [ ! -f "/storage/roms/bios/mame/hash/fmtowns_cd.xml" ]; then
mkdir -p /storage/roms/bios/mame/hash
cp -rf "/usr/config/emuelec/configs/mame/hash/"fm* "/storage/roms/bios/mame/hash"
fi
if [ ! -f "/storage/roms/bios/mame/ini/mame.ini" ]; then
mkdir -p /storage/roms/bios/mame/ini
cp -rf "/usr/config/emuelec/configs/mame/ini/"mame* "/storage/roms/bios/mame/ini"
fi
if [ ! -f "/storage/.config/retroarch/config/MAME/MAME.opt" ]; then
cp -rf "/usr/config/emuelec/configs/mame/MAME/"MAME* "/storage/.config/retroarch/config/MAME"
fi
"""
)
MAME_INI = text(
"""
rompath /storage/roms/bios/mame/bios;/storage/roms/mame;/storage/roms/arcade
samplepath /storage/roms/bios/mame/samples
cfg_directory /storage/roms/bios/mame/mame/cfg
"""
)
MAME_OPT = text(
"""
mame_boot_to_bios = "disabled"
mame_read_config = "enabled"
"""
)
SCUMMVM_START = text(
"""
CONFIG_DIR="/storage/.config/scummvm"
if [ -d "/storage/.config/scummvm/extra" ]; then
EXTRA="--extrapath=/storage/.config/scummvm/extra"
fi
case $1 in
"sa")
scummvm --fullscreen --joystick=0 $EXTRA "${GAME}"
;;
"libretro")
retroarch -L /tmp/cores/scummvm_libretro.so --config ${RATMPCONF} "${GAME}"
;;
"add")
scummvm --add --path="/storage/roms/scummvm" --recursive
cp ${CONFIG_DIR}/scummvm.ini /storage/roms/bios/scummvm.ini
;;
esac
"""
)
DOLPHIN_SH = text(
"""
XDG_CONFIG_HOME=/emuelec/configs XDG_DATA_HOME=/storage/roms/dolphin /usr/bin/dolphin-emu-nogui -p fbdev "${1}"
"""
)
FILES = {
el.RUN_EMU: RUN_EMU,
el.UTILS: UTILS,
el.RETROARCH_MK: RETROARCH_MK,
f"{el.LIBRETRO}/freej2me/scripts/freej2me.sh": FREEJ2ME_SH,
f"{el.BIN}/pico8.sh": PICO8_SH,
f"{EMU}/flycastsa/scripts/flycast.sh": FLYCAST_SH,
f"{EMU}/flycast-dojo/scripts/flycastdojo.sh": FLYCASTDOJO_SH,
f"{EMU}/amiberry/package.mk": AMIBERRY_MK,
f"{EMU}/amiberry-lite/package.mk": AMIBERRY_MK.replace("amiberry/", "amiberry-lite/"),
f"{el.SX05RE}/tools/sound/timidity/package.mk": TIMIDITY_MK,
f"{EMU}/duckstation/config/settings.ini": DUCKSTATION_INI,
f"{EMU}/duckstation/package.mk": DUCKSTATION_MK,
f"{EMU}/duckstation/scripts/duckstation.sh": DUCKSTATION_SH,
el.YABASANSHIRO_SCRIPTS[0]: yabasanshiro_sh("yabasanshiro"),
el.YABASANSHIRO_SCRIPTS[1]: yabasanshiro_sh("yabasanshiro1_5"),
f"{EMU}/jzintv/scripts/jzintv.sh": JZINTV_SH,
f"{EMU}/mupen64plussa/mupen64plussa-core/m64p.sh": M64P_SH,
f"{EMU}/hatarisa/scripts/hatari.start": HATARI_START,
f"{EMU}/hatarisa/config/conf/Atari-ST-default.cfg": HATARI_CFG,
f"{EMU}/hatarisa/package.mk": HATARI_MK,
f"{EMU}/advancemame/package.mk": ADVMAME_MK,
f"{EMU}/advancemame/config/advmame.rc": ADVMAME_RC,
f"{EMU}/advancemame/config/advmame.rc_oga": ADVMAME_RC,
f"{EMU}/advancemame/bin/advmame.sh": ADVMAME_SH,
f"{el.LIBRETRO}/mame/scripts/mame.sh": MAME_SH,
f"{el.LIBRETRO}/mame/config/ini/mame.ini": MAME_INI,
f"{el.LIBRETRO}/mame/config/MAME/MAME.opt": MAME_OPT,
f"{EMU}/scummvmsa/bin/scummvm.start": SCUMMVM_START,
f"{EMU}/dolphinSA/scripts/dolphin.sh": DOLPHIN_SH,
}
def tree(**changes: str | None) -> RepoTree:
files = dict(FILES)
for path, content in changes.items():
if content is None:
files.pop(path, None)
else:
files[path] = content
return RepoTree.from_files(files)
def records(extractor, **changes: str | None) -> list[dict]:
built = tree(**changes)
return [r.entry() for r in extractor(built, el.Runs(built))]
def ref(path: str, lines: str) -> str:
return f"{path}:{lines}"
class Dispatch(unittest.TestCase):
def test_each_script_is_named_after_the_core_that_starts_it(self):
runs = el.Runs(tree())
self.assertEqual(runs.emulator("flycast.sh"), "flycastsa")
self.assertEqual(runs.emulator("flycastdojo.sh"), "flycast_dojo")
self.assertEqual(runs.emulator("m64p.sh"), "rice, glide64mk2")
self.assertEqual(runs.emulator("mame.sh"), "mame")
self.assertEqual(runs.emulator("freej2me.sh"), "freej2me")
def test_an_arm_without_a_core_test_names_its_platform(self):
self.assertEqual(el.Runs(tree()).emulator("pico8.sh"), "pico8")
def test_a_script_no_longer_started_raises(self):
runs = el.Runs(tree())
with self.assertRaises(LauncherPatternMissing):
runs.of("gone.sh")
def test_a_dispatch_without_any_script_raises(self):
with self.assertRaises(LauncherPatternMissing):
el.Runs(tree(**{el.RUN_EMU: "echo nothing\n"}))
class LibretroRecords(unittest.TestCase):
def test_retroarch_system_directory_comes_from_the_sed(self):
(record,) = records(el.retroarch)
self.assertEqual(record["search_dir"], "/storage/roms/bios")
self.assertEqual(record["kind"], "passes_path")
self.assertEqual(
record["source_ref"], f"{ref(el.RETROARCH_MK, '2')}, {ref(el.RUN_EMU, '80')}"
)
def test_retrorun_is_handed_the_folder_with_d(self):
(record,) = records(el.retrorun)
self.assertEqual(
(record["emulator"], record["search_dir"], record["passes_to"]),
("retrorun", "/storage/roms/bios", "retrorun -d"),
)
self.assertEqual(record["source_ref"], ref(el.RUN_EMU, "4, 81-82"))
def test_freej2me_gets_java_home_and_its_jar_copied(self):
java, jar = records(el.freej2me)
self.assertEqual(java["passes_to"], "environment JAVA_HOME")
self.assertEqual(java["search_dir"], "/storage/roms/bios/jdk")
self.assertEqual(jar["kind"], "copies")
self.assertEqual(jar["names"], ["freej2me-lr.jar"])
self.assertEqual(jar["passes_to"], "${HOME}/roms/bios")
self.assertEqual(jar["depends_on_option"], "/storage/roms/bios/jdk/eeversion exists")
def test_mame_copies_its_own_files_and_reads_only_path_options(self):
found = records(el.mame)
copies = [r for r in found if r["kind"] == "copies"]
self.assertEqual([r["names"] for r in copies], [["fm*"], ["mame*"]])
folders = [(r["passes_to"], r["search_dir"]) for r in found if r["kind"] == "passes_path"]
self.assertEqual(
folders,
[
("mame.ini rompath", "/storage/roms/bios/mame/bios"),
("mame.ini samplepath", "/storage/roms/bios/mame/samples"),
],
)
self.assertTrue(all(r["depends_on_option"] == "mame_read_config = enabled"
for r in found if r["kind"] == "passes_path"))
def test_mame_without_the_read_config_option_raises(self):
with self.assertRaises(LauncherPatternMissing):
records(el.mame, **{f"{el.LIBRETRO}/mame/config/MAME/MAME.opt": "x = 1\n"})
class StandaloneRecords(unittest.TestCase):
def test_pico8_moves_the_folder_and_runs_the_binary(self):
(record,) = records(el.pico8)
self.assertEqual(record["kind"], "moves")
self.assertEqual(record["names"], ["pico8_dyn"])
self.assertEqual(record["search_dir"], "/storage/roms/bios/pico-8")
self.assertEqual(record["passes_to"], "/emuelec/bin/pico-8")
self.assertEqual(
record["source_ref"],
f"{ref(el.RUN_EMU, '57-58')}, {ref(f'{el.BIN}/pico8.sh', '1-5, 8-10')}",
)
def test_pico8_without_the_removal_is_not_a_move(self):
changed = PICO8_SH.replace(" rm -rf /storage/roms/bios/pico-8\n", "")
with self.assertRaises(LauncherPatternMissing):
records(el.pico8, **{f"{el.BIN}/pico8.sh": changed})
def test_links_from_scripts_and_packages(self):
found = records(el.links)
self.assertEqual(
[(r["emulator"], r["search_dir"]) for r in found],
[
("flycastsa", "/storage/roms/bios/dc"),
("flycast_dojo", "/storage/roms/bios/dc_dojo"),
("amiberry", "/storage/roms/bios"),
("amiberry-lite", "/storage/roms/bios"),
("timidity", "/storage/roms/bios/timidity/freepats"),
],
)
self.assertEqual(
found[1]["source_ref"],
f"{ref(el.RUN_EMU, '26-27')}, "
f"{ref(f'{EMU}/flycast-dojo/scripts/flycastdojo.sh', '1-5')}",
)
self.assertEqual(
found[2]["passes_to"], "symlink /usr/config/amiberry/kickstarts -> /storage/roms/bios"
)
def test_a_script_without_a_bios_link_raises(self):
with self.assertRaises(LauncherPatternMissing):
records(el.links, **{f"{EMU}/flycastsa/scripts/flycast.sh": "flycast \"${1}\"\n"})
def test_duckstation_scans_the_folder_its_config_names(self):
(record,) = records(el.duckstation)
self.assertEqual((record["kind"], record["search_dir"]), ("scans_dir", "/storage/roms/bios"))
def test_duckstation_with_a_fixed_bios_path_raises(self):
changed = DUCKSTATION_INI.replace("PathPAL =", "PathPAL = /storage/roms/bios/scph5502.bin")
with self.assertRaises(LauncherPatternMissing):
records(el.duckstation, **{f"{EMU}/duckstation/config/settings.ini": changed})
def test_yabasanshiro_passes_the_bios_unless_hle_is_chosen(self):
new, old = records(el.yabasanshiro)
self.assertEqual((new["emulator"], old["emulator"]), ("yabasanshiroSA", "yabasanshiroSA1_5"))
self.assertEqual(new["names"], ["saturn_bios.bin"])
self.assertEqual(new["passes_to"], "yabasanshiro -b")
self.assertEqual(old["passes_to"], "yabasanshiro1_5 -b")
self.assertEqual(new["depends_on_option"], "hlebios != 1")
self.assertIs(new["fails_when_missing"], False)
def test_jzintv_is_handed_the_bios_folder(self):
(record,) = records(el.jzintv)
self.assertEqual((record["search_dir"], record["passes_to"]), ("/storage/roms/bios", "jzintv -p"))
def test_mupen64plus_tries_its_ipl_names_in_order(self):
(record,) = records(el.mupen64plus)
self.assertEqual(record["kind"], "names_searched")
self.assertEqual(record["names"], ["N64DD IPLROM (J).n64", "IPL.n64"])
self.assertEqual(record["search_dir"], "/storage/roms/bios/Mupen64plus")
self.assertEqual(record["passes_to"], "mupen64plus --dd-ipl-rom")
self.assertEqual(record["depends_on_option"], '-f "${GAMEDIR}/${GAMENAME}.ndd"')
def test_mupen64plus_without_a_fallback_raises(self):
changed = M64P_SH.replace('[ ! -f "${IPLROM}" ] && IPLROM="${BIOSDIR}/IPL.n64"\n', "")
with self.assertRaises(LauncherPatternMissing):
records(el.mupen64plus, **{f"{EMU}/mupen64plussa/mupen64plussa-core/m64p.sh": changed})
def test_hatari_reads_the_tos_named_by_its_default_config(self):
(record,) = records(el.hatari)
self.assertEqual(record["names"], ["tos.img"])
self.assertEqual(
record["passes_to"], "Atari-ST-default.cfg [ROM] szTosImageFileName (--configfile)"
)
def test_advancemame_one_record_when_every_device_agrees(self):
(record,) = records(el.advancemame)
self.assertEqual(record["search_dir"], "/storage/roms/bios/advmame/samples")
self.assertNotIn("device", record)
def test_advancemame_devices_that_disagree_raise(self):
changed = ADVMAME_RC.replace("bios/advmame", "bios/other")
with self.assertRaises(LauncherPatternMissing):
records(el.advancemame, **{f"{EMU}/advancemame/config/advmame.rc_oga": changed})
def test_scummvm_extrapath_and_the_ini_copied_for_the_core(self):
extra, ini = records(el.scummvm)
self.assertEqual(extra["passes_to"], "scummvm --extrapath")
self.assertEqual(extra["depends_on_option"], "/storage/.config/scummvm/extra exists")
self.assertEqual((ini["emulator"], ini["build"]), ("scummvm", "libretro"))
self.assertEqual(ini["search_dir"], "/storage/.config/scummvm")
self.assertEqual(ini["passes_to"], "/storage/roms/bios/scummvm.ini")
def test_dolphin_data_home(self):
(record,) = records(el.dolphin)
self.assertEqual(record["passes_to"], "environment XDG_DATA_HOME")
self.assertEqual(record["search_dir"], "/storage/roms/dolphin")
def test_midi_sources_that_read_the_bios_folder(self):
found = records(el.midi)
self.assertEqual(
[(r["emulator"], r["kind"], r["names"], r["search_dir"]) for r in found],
[
("fluidsynth", "fixed_name", ["sc/SC-55.sf2"], "/storage/roms/bios"),
("mt32d", "passes_path", [], "/roms/bios"),
],
)
self.assertEqual(found[0]["depends_on_option"], "ra_midi_output = fluidsynth")
self.assertEqual(
found[1]["source_ref"], f"{ref(el.RUN_EMU, '7-9')}, {ref(el.UTILS, '1, 10-11')}"
)
def test_every_extractor_runs_on_the_fixtures(self):
found = el.extract_launchers(tree())
self.assertEqual(len(found), 26)
class Revision(unittest.TestCase):
def test_the_revision_is_the_one_the_bios_list_is_read_at(self):
url = (
"https://raw.githubusercontent.com/EmuELEC/EmuELEC/v4.8/"
"packages/sx05re/emuelec/bin/batocera/batocera-systems"
)
self.assertEqual(launcher_revision(url), "v4.8")
def test_a_url_without_a_revision_raises(self):
with self.assertRaises(ValueError):
launcher_revision("https://example.org/batocera-systems")
class PlatformRecords(unittest.TestCase):
"""The launchers platforms/emuelec.yml carries, as the scraper wrote them."""
@classmethod
def setUpClass(cls) -> None:
cls.config = yaml.safe_load((REPO_ROOT / "platforms" / "emuelec.yml").read_text())
def test_the_platform_carries_launchers(self):
self.assertTrue(self.config.get("launchers"))
def test_every_reference_is_in_the_emuelec_packages(self):
for record in self.config["launchers"]:
for part in record["source_ref"].split(", "):
if ":" in part:
self.assertTrue(part.startswith(f"{el.SX05RE}/"), record)
if __name__ == "__main__":
unittest.main()
+49
View File
@@ -50,6 +50,55 @@ class TheGuideLeadsToTheFewestFiles(unittest.TestCase):
self.assertTrue(target.is_file() or (target / "index.html").is_file(), url)
class TheGuideDrivesTheInstaller(unittest.TestCase):
"""What an agent needs to run the installer for a user: the package
command, the plan as JSON, the exit codes, and plan, consent, act. Each
fact is held to install.py, so the guide cannot drift from it."""
def setUp(self) -> None:
import generate_installer_doc # noqa: PLC0415
self.doc = generate_installer_doc
self.text = siteagents.llms_txt([("retroarch", "RetroArch")])
self.section = self.text.split("\n## Driving the installer for a user\n", 1)[1].split("\n## ", 1)[0]
def test_the_exit_codes_are_the_installer_page_table(self) -> None:
page = (REPO_ROOT / "wiki" / "installer.md").read_text(encoding="utf-8")
table = page.split("<!-- generated: exit-codes -->", 1)[1].split("<!-- /generated: exit-codes -->")[0]
in_table = [int(code) for code in re.findall(r"^\| `(\d+)` \|", table, re.M)]
self.assertEqual(in_table, self.doc.exit_codes())
self.assertEqual([code for code, _ in siteagents.EXIT_CODES], in_table)
listed = [int(code) for code in re.findall(r"^ - `(\d+)`: ", self.section, re.M)]
self.assertEqual(listed, in_table)
def test_the_event_schema_is_the_installers(self) -> None:
import json # noqa: PLC0415
self.assertEqual(siteagents.EVENT_SCHEMA, self.doc._install.EVENT_SCHEMA)
schema = json.loads((REPO_ROOT / "schemas" / "install-events.schema.json").read_text(encoding="utf-8"))
self.assertEqual(schema["$id"], siteagents.SITE + siteagents.EVENTS_SCHEMA_PATH)
self.assertIn(f"event schema {siteagents.EVENT_SCHEMA} ", self.section)
def test_every_option_and_event_named_exists(self) -> None:
options = {flag for action in self.doc._install.build_parser()._actions
for flag in action.option_strings}
for flag in set(re.findall(r"(?<![\w-])(--[a-z][a-z-]*)", self.section)):
with self.subTest(flag=flag):
self.assertIn(flag, options)
keys = self.doc._install.EVENT_KEYS
self.assertIn("state", keys["file"])
self.assertIn("bytes_needed", keys["check"])
self.assertIn("pending", keys["summary"])
self.assertEqual(tuple(keys["end"]), ("code",))
def test_the_package_commands_and_the_pattern(self) -> None:
self.assertIn("`uvx retrobios --for-games --required-only --dry-run`", self.section)
self.assertIn("`pipx run retrobios`", self.section)
self.assertIn("`--dry-run --events`", self.section)
steps = re.findall(r"^\d\. (\w+)", self.section, re.M)
self.assertEqual(steps, ["Plan", "Consent", "Act"])
class TheSkillIsAnAgentSkill(unittest.TestCase):
"""ARD reserves text/markdown for agent skills (Lighthouse warns on a
plain Markdown entry): the setup steps are published as one."""
+1
View File
@@ -41,6 +41,7 @@ START_PAGES = {
# Reference pages written for anyone, held to the Start here budgets.
START_LEVEL_PAGES = {
"wiki/measurements.md": 300,
"wiki/archive-recipes.md": 160,
}
# Level 1: the guide part of each setup page, above its Reference heading.
SETUP_CEILING = 220
+2 -1
View File
@@ -956,7 +956,8 @@ class EventsTranscript(unittest.TestCase):
self.assertEqual(list(event), ["event", *keys[event["event"]]])
self.assertEqual(events[0]["event"], "start")
self.assertEqual(events[0]["mode"], "check")
self.assertEqual(events[-1], {"event": "end", "code": 1})
# --check that finds files missing ends on 3 (the Exit status table).
self.assertEqual(events[-1], {"event": "end", "code": 3})
class OneOptionsTable(unittest.TestCase):
+3 -1
View File
@@ -198,7 +198,9 @@ resolved. Three strategies exist:
platform, that profile answers for the name. Used by Batocera, Recalbox, RetroBat,
RomM, KNULLI, AmberELEC, EmuELEC, MinUI, NextUI, MyMinUI, RetroPie, EmuDeck,
RetroDECK, ROCKNIX. A list that names `retroarch` (the last
four) also takes every libretro profile, as `all_libretro` does. An empty list
four) also takes every libretro profile, as `all_libretro` does, plus the
libretro builds whose `built_by` names this platform (ROCKNIX's
`yabadabadoo`). An empty list
(BizHawk, MiSTer FPGA) resolves no profile, and the pack holds the platform's own
files.
- **all_libretro**: `cores: all_libretro` - every profile whose `type` contains
+37
View File
@@ -0,0 +1,37 @@
---
level: reference
order: 45
group: Checking files
terms: recipe, rebuild, archive, zip, romset, exact, canonical
---
# Rebuilding an archive
Arcade emulators read their BIOS files from a ZIP archive.
The collection keeps a **recipe** for each archive. It lists the files
inside, in order, with their fingerprints. The recipe and those files make
the archive again. No copy from elsewhere is needed.
## Rebuild one archive
In a copy of the project:
```bash
python scripts/romset_recipes.py --build neogeo.zip --output-dir rebuilt
```
Each archive of that name is made under `rebuilt/`, in the folders it has in
the collection. It is checked against its recipe before it is kept.
## Exact or canonical
<!-- generated: archive-recipes -->
- **Exact**: the same bytes as the archive in the collection. 1,790 of 2,222 archives.
- **Canonical**: the same files inside, packed another way. 432 of 2,222 archives.
<!-- /generated: archive-recipes -->
An emulator reads the files inside, so both work the same. A setup that
checks the whole archive by its fingerprint needs the exact one.
Every option of the command: [tools](tools.md#archive-recipes).
+6 -6
View File
@@ -185,7 +185,7 @@ downloads, the folder and, for a list with files read from the frontend's save
directory, that directory. Each row names the option it sets, and **Show the
command line** gives the same choice as one command for a script. **Preview the
file list** shows what would be downloaded and writes nothing. **Copy the BIOS
files from your old card or a folder** opens the folder browser on the first
files from an old card or a folder** opens the folder browser on the first
mounted card, and adds [`--from`](#copy-from-an-old-card): the plan then says how
many files that folder holds and how many are left to download, or that it
holds none of them. After an install
@@ -662,7 +662,7 @@ nothing is replaced. The end of the run says how many files were kept, their
size, the folder, and the command that puts them back:
```
Kept 1 file you had (512.0 KB), which the collection's copies replaced, in /home/me/.config/retroarch/retrobios-backup/20261022-153000
Kept 1 file already there (512.0 KB), which the collection's copies replaced, in /home/me/.config/retroarch/retrobios-backup/20261022-153000
To put them back: python3 install.py --restore /home/me/.config/retroarch/retrobios-backup/20261022-153000
```
@@ -1007,7 +1007,7 @@ what the options name, everything by default.
| <span id="option-from-pack"></span>`--from-pack PATH` | Install from a pack already downloaded: its ZIP, one of its parts (the others are found in the same folder), or the folder holding them. Makes no network request and checks every file's hash. See [Without a network](#without-a-network-offline-install) |
| <span id="option-offline"></span>`--offline` | Make no network request. `--check`, `--from`, `--from-pack` and `--manifest` work without one, the file list stops the run at once when none of them gives it, and a file the run would download fails at once. `RETROBIOS_INSTALL_OFFLINE=1` does the same. `--target` and `--list-targets` read the core list per board a pack carries, or `targets/<platform>.json` beside `--manifest`, and are refused when neither gives one |
| <span id="option-manifest"></span>`--manifest FILE` | Read the file list from FILE, a copy of `install/<platform>.json`, instead of downloading it. See [Without a network](#without-a-network-offline-install) |
| <span id="option-restore"></span>`--restore FOLDER` | Put back the files a run replaced and kept in FOLDER, a dated folder inside `retrobios-backup`. Takes no other option but `--dry-run` and `--no-input`. See [Files you already had](#files-you-already-had) |
| <span id="option-restore"></span>`--restore FOLDER` | Put back the files a run replaced and kept in FOLDER, a dated folder inside `retrobios-backup`. Takes no other option but `--dry-run` and `--no-input`. See [Files already in the folder](#files-you-already-had) |
| <span id="option-tui"></span>`--tui` | Open the full-screen interface, or stop with the reason it cannot open here (code 2) instead of falling back to the questions. Wins over `RETROBIOS_PLAIN` and `ACCESSIBLE`. See [The full-screen interface](#the-full-screen-interface) |
| <span id="option-plain"></span>`--plain` | Ask the questions line by line instead of the full-screen interface, the mode for a screen reader. See [The full-screen interface](#the-full-screen-interface) |
| <span id="option-version"></span>`--version` | Print the installer's identity, the first 12 hex digits of the SHA-256 the bootstraps pin |
@@ -1323,7 +1323,7 @@ their systems`), and its `bios` folder comes first at the folder question,
under Enter, before this computer's usual folder. A drive with a `bios` folder
and none of those folders, a data drive or a stick, is numbered with a note
(`DATA has a bios folder but no folder a handheld system writes. Pick it below
if your files go there.`) and never taken under Enter. Another system's card or SHARE drive, and a
if the BIOS files go there.`) and never taken under Enter. Another system's card or SHARE drive, and a
drive two systems fit, are left out, and a line names them: each is reached
through its own setup. A folder typed is read from the current
one when it is not a full path, and named in full after. `q` quits there too
@@ -1573,7 +1573,7 @@ it names the pack and the folder. The releases carry no pack for ROCKNIX,
Knulli or AmberELEC: there RetroBIOS needs Wi-Fi, or the computer route.
On a two-card device, RetroBIOS without Wi-Fi first asks to **copy the BIOS
files from your other card**: muOS's `MUOS/bios` on the card the system does
files from the other card**: muOS's `MUOS/bios` on the card the system does
not read, or the other of `/roms/bios` and `/roms2/bios` on ArkOS and dArkOS.
A says yes, as [`--from`](#copy-from-an-old-card) does. The list of files
still comes from the pack, so the pack is needed too.
@@ -1872,7 +1872,7 @@ RetroArch folder that holds only `scph5501.bin`, in the order written:
{"event": "file", "platform": "retroarch", "dest": "scph5501.bin", "systems": ["sony-playstation"], "size": 524288, "state": "present"}
{"event": "check", "platform": "retroarch", "name": "RetroArch", "bios_dir": "system", "files": 118, "present": 1, "missing": 117, "wrong_hash": 0, "bytes_needed": 344457216, "data_missing": 0, "not_installable": 0}
{"event": "summary", "mode": "check", "installed": 0, "up_to_date": 1, "pending": 117, "failed": [], "skipped": [], "left_out": [], "kept": [], "interrupted": false}
{"event": "end", "code": 1}
{"event": "end", "code": 3}
```
## Environment overrides