From a6716796a9822b27cafc5aaae6e7e5d870956c15 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 11 Nov 2026 11:11:50 +0100 Subject: [PATCH] feat: write config_reads into the profiles Where each shipped build finds the config a platform installs and reads every key it opens a file from, read at the build the platform ships: duckstation (EmuELEC, ROCKNIX, RetroBat), xemu, armsx2, hatari, melonDS, dsperate, NanoBoyAdvance, mednafen (ROCKNIX's sydarn fork) and ares, gzdoom, xroar, lrps2 and the amiberry builds at their pins. pin_build names the builds ROCKNIX, EmuELEC and AmberELEC compile, revision the ones RetroBat and Flathub install unpinned. tests/test_launcher_evidence.py covers the rule, the extractor helpers, the invariants and the chains that move placements. --- emulators/amiberry-5.6.5.yml | 7 + emulators/amiberry-7.1.1.yml | 9 + emulators/ares.yml | 43 +++++ emulators/armsx2.yml | 19 ++ emulators/dsperate.yml | 26 +++ emulators/duckstation.yml | 55 ++++++ emulators/gzdoom.yml | 11 ++ emulators/hatari.yml | 39 ++++ emulators/lrps2.yml | 7 + emulators/mednafen.yml | 55 ++++++ emulators/melonds.yml | 52 +++++ emulators/nanoboyadvance.yml | 8 + emulators/xemu.yml | 31 +++ emulators/xroar.yml | 51 +++++ tests/test_launcher_evidence.py | 330 ++++++++++++++++++++++++++++++++ 15 files changed, 743 insertions(+) create mode 100644 tests/test_launcher_evidence.py diff --git a/emulators/amiberry-5.6.5.yml b/emulators/amiberry-5.6.5.yml index 8671ce5eb..5db40d55e 100644 --- a/emulators/amiberry-5.6.5.yml +++ b/emulators/amiberry-5.6.5.yml @@ -32,6 +32,13 @@ systems: standalone_directories: - "getcwd" - "rom_path = kickstarts" +config_reads: + - file: "*.uae" + source_ref: "src/main.cpp:926-934" + keys: + - key: "kickstart_rom_file" + source_ref: "src/cfgfile.cpp:6050, src/memory.cpp:1907-1916" + notes: | Amiberry 5.6.5 (src/osdep/target.h:27). Every folder it reads is below its start path: $EXTERNAL_FILES_DIR or $XDG_DATA_HOME when either holds a diff --git a/emulators/amiberry-7.1.1.yml b/emulators/amiberry-7.1.1.yml index c617c4df7..fc433e525 100644 --- a/emulators/amiberry-7.1.1.yml +++ b/emulators/amiberry-7.1.1.yml @@ -43,6 +43,15 @@ standalone_directories: - "AMIBERRY_HOME_DIR/roms" - "~/Amiberry/roms" - "whdboot_path = whdboot" +config_reads: + - file: "*.uae" + source_ref: "src/main.cpp:986-994" + keys: + - key: "kickstart_rom_file" + source_ref: "src/cfgfile.cpp:6122, src/memory.cpp:1974-1983" + - key: "$(FILE_PATH)" + source_ref: "src/cfgfile.cpp:389" + notes: | Amiberry 7.1.1 (CMakeLists.txt:3-4). The ROM path is rom_path in amiberry.conf, else roms/ below the home directory: $AMIBERRY_HOME_DIR, diff --git a/emulators/ares.yml b/emulators/ares.yml index 8f991ad13..35623cf00 100644 --- a/emulators/ares.yml +++ b/emulators/ares.yml @@ -56,6 +56,49 @@ systems: - zx-spectrum - zx-spectrum-128 +config_reads: + - file: "settings.bml" + source_ref: "desktop-ui/desktop-ui.cpp:9-35, 100-103, 114, desktop-ui/settings/settings.cpp:31-42, 263-286, nall/nall/path.cpp:123-142" + keys: + - key: "ColecoVision/Firmware/BIOS.World" + source_ref: "desktop-ui/emulator/colecovision.cpp:10-12, 42-50, mia/system/colecovision.cpp:7-9" + - key: "MSX/Firmware/BIOS.Japan" + source_ref: "desktop-ui/emulator/msx.cpp:16-19, 44-53" + - key: "FamicomDiskSystem/Firmware/BIOS.Japan" + source_ref: "desktop-ui/emulator/famicom-disk-system.cpp:17-19, 89-97" + - key: + - "Nintendo64DD/Firmware/BIOS.Japan" + - "Nintendo64DD/Firmware/BIOS.US" + - "Nintendo64DD/Firmware/BIOS.DEV" + source_ref: "desktop-ui/emulator/nintendo-64dd.cpp:15-19, 59-73" + condition: "the game's region" + - key: "GameBoyAdvance/Firmware/BIOS.World" + source_ref: "desktop-ui/emulator/game-boy-advance.cpp:12-14, 55-63" + - key: + - "MasterSystem/Firmware/BIOS.US" + - "MasterSystem/Firmware/BIOS.Japan" + - "MasterSystem/Firmware/BIOS.Europe" + source_ref: "desktop-ui/emulator/master-system.cpp:12-16, 93-107" + condition: "the game's region" + - key: "GameGear/Firmware/BIOS.World" + source_ref: "desktop-ui/emulator/game-gear.cpp:10-12, 30-40" + - key: + - "MegaCD/Firmware/BIOS.US" + - "MegaCD/Firmware/BIOS.Japan" + - "MegaCD/Firmware/BIOS.Europe" + source_ref: "desktop-ui/emulator/mega-cd.cpp:15-19, 63-77" + condition: "the disc's region" + - key: "NeoGeoPocket/Firmware/BIOS.World" + source_ref: "desktop-ui/emulator/neo-geo-pocket.cpp:10-12, 32-40" + - key: "NeoGeoPocketColor/Firmware/BIOS.World" + source_ref: "desktop-ui/emulator/neo-geo-pocket-color.cpp:10-12, 32-40" + - key: + - "PlayStation/Firmware/BIOS.US" + - "PlayStation/Firmware/BIOS.Japan" + - "PlayStation/Firmware/BIOS.Europe" + source_ref: "desktop-ui/emulator/playstation.cpp:19-23, 87-101" + condition: "the disc's region" + notes: | Multi-system emulator by Near (byuu), successor to higan/bsnes. Focuses on accuracy and preservation. 40 emulators registered at this pin diff --git a/emulators/armsx2.yml b/emulators/armsx2.yml index 5ee879e61..10de74fdc 100644 --- a/emulators/armsx2.yml +++ b/emulators/armsx2.yml @@ -48,6 +48,25 @@ data_root: source_ref: "source pcsx2/Pcsx2Config.cpp:2523-2530, pcsx2/Pcsx2Config.cpp:2540-2560, pcsx2/Pcsx2Config.cpp:2567-2589" note: "portable.ini or portable.txt beside the binary makes its folder the root" +pin_build: + - platform: rocknix + repo: "https://github.com/ROCKNIX/distribution" + revision: "20261101" + path: "projects/ROCKNIX/packages/emulators/standalone/armsx2-sa/package.mk" + variable: PKG_VERSION + value: "19c83bf065ed21a19d8c46a1f578e37d2dab8efd" +config_reads: + - file: "PCSX2.ini" + platforms: [rocknix] + source_ref: "pcsx2-qt/QtHost.cpp:1307, 1325-1331, pcsx2/Pcsx2Config.cpp:2398-2491, pcsx2/VMManager.cpp:630-641" + keys: + - key: "[Folders] Bios" + source_ref: "pcsx2/Pcsx2Config.cpp:2514-2524" + condition: "[UI] SettingsVersion 1, else the ini is reset (pcsx2-qt/QtHost.cpp:1331-1342)" + - key: "[Filenames] BIOS" + source_ref: "pcsx2/Pcsx2Config.cpp:1879-1881, 2258-2264, pcsx2/ps2/BiosTools.cpp:258-282, 333-348, pcsx2/VMManager.cpp:1638" + condition: "empty: the first PS2 BIOS of 4 to 8 MB found in [Folders] Bios" + notes: | PlayStation 2 emulator, a GPL-3.0 fork of PCSX2 that replaces the x86-64 only EE, IOP, VU and vtlb fast memory recompilers with native AArch64 ones. One tree diff --git a/emulators/dsperate.yml b/emulators/dsperate.yml index 528c5a43f..2017c7e2e 100644 --- a/emulators/dsperate.yml +++ b/emulators/dsperate.yml @@ -32,6 +32,32 @@ data_root: source_ref: "src/frontend/sdl/config.cpp:39-49" note: "~/.config/dsperate when XDG_CONFIG_HOME is unset. System files are read wherever the [paths] keys of the ini point." +config_reads: + - file: "dsperate.ini" + platforms: [rocknix] + revision: "ddf2910e6d80220c6b22b4df61f0786ac9879cda" + revision_from: "PKG_VERSION 3.0.5, the release binaries CI built from tag v3.0.5" + source_ref: "src/frontend/sdl/main.cpp:1065, 1155-1157, src/frontend/sdl/config.cpp:64-81" + keys: + - key: + - "[paths] bios9" + - "[paths] bios7" + source_ref: "src/frontend/sdl/main.cpp:1175, 1270-1274, src/core/nds.cpp:114-129" + condition: "both present, otherwise FreeBIOS" + - key: "[paths] firmware" + source_ref: "src/frontend/sdl/main.cpp:1178, 1270, src/core/nds.cpp:123, 134-135" + - key: + - "[paths] bios9i" + - "[paths] bios7i" + source_ref: "src/frontend/sdl/main.cpp:1179-1186, 1319-1322, 1331-1334, src/core/nds_dsi.cpp:88-99" + condition: "DSi session" + - key: "[paths] dsi_firmware" + source_ref: "src/frontend/sdl/main.cpp:1176-1178, 1270" + condition: "DSi session" + - key: "[paths] dsi_nand" + source_ref: "src/frontend/sdl/main.cpp:1158-1162, 1324-1326, 1337, src/core/nds_dsi.cpp:114-120" + condition: "--dsi-mode, BootMenuDSi.nds or a .dspr.nds shortcut" + notes: | Nintendo DS emulator for ARM handhelds with a DSi mode. ROCKNIX installs the release tarball for its architecture and runs it with --config pointing at diff --git a/emulators/duckstation.yml b/emulators/duckstation.yml index 739810077..bfb8f089d 100644 --- a/emulators/duckstation.yml +++ b/emulators/duckstation.yml @@ -33,6 +33,61 @@ data_root: source_ref: "src/core/core.cpp:137-151, src/core/core.cpp:190-215" note: "portable.txt or settings.ini beside the executable, or beside the AppImage, makes that folder the root" +pin_build: + - platform: emuelec + repo: "https://github.com/EmuELEC/EmuELEC" + revision: "v4.8" + path: "packages/sx05re/emulators/duckstation/package.mk" + variable: PKG_VERSION + value: "fd3507c16d098fb32806c281caaefb205946da8a" + - platform: rocknix + repo: "https://github.com/ROCKNIX/distribution" + revision: "20261101" + path: "projects/ROCKNIX/packages/emulators/standalone/duckstation-sa/package.mk" + variable: PKG_VERSION + value: "0.1-10998" +config_reads: + - file: "settings.ini" + platforms: [emuelec] + source_ref: "src/frontend-common/common_host_interface.cpp:403-407, 3042-3050, 3304-3327, src/duckstation-nogui/main.cpp:62-64, 137-140, src/duckstation-nogui/nogui_host_interface.cpp:39-42" + keys: + - key: "[BIOS] SearchDirectory" + source_ref: "src/core/host_interface.cpp:253-260" + - key: + - "[BIOS] PathNTSCU" + - "[BIOS] PathNTSCJ" + - "[BIOS] PathPAL" + source_ref: "src/core/host_interface.cpp:262-375, src/core/system.cpp:817" + condition: "the console region; an empty value scans SearchDirectory" + - file: "settings.ini" + platforms: [rocknix] + revision: "9b0a4ec559ae83bfdb14685932b036ad7c1701be" + revision_from: "PKG_VERSION 0.1-10998, the AppImage of tag v0.1-10998" + source_ref: "src/core/core.cpp:105-223, 227-230, 232-275, src/duckstation-qt/qthost.cpp:796-800" + keys: + - key: "[BIOS] SearchDirectory" + source_ref: "src/core/settings.cpp:2709-2719, 2723" + - key: + - "[BIOS] PathNTSCU" + - "[BIOS] PathNTSCJ" + - "[BIOS] PathPAL" + source_ref: "src/core/bios.cpp:351-458, src/core/system.cpp:2923" + condition: "the console region; an empty value scans SearchDirectory" + - file: "settings.ini" + platforms: [retrobat] + revision: "5fd366809053fe287291de7a39752c4d5d5b146b" + revision_from: "version 0.1-11752-g5fd366809 of duckstation.exe in RetroBat's stable duckstation.7z, 2026-11-10" + source_ref: "src/core/core.cpp:131-249, 253-256, 258-301, src/duckstation-qt/qthost.cpp:803-807" + keys: + - key: "[BIOS] SearchDirectory" + source_ref: "src/core/settings.cpp:2859-2869, 2873" + - key: + - "[BIOS] PathNTSCU" + - "[BIOS] PathNTSCJ" + - "[BIOS] PathPAL" + source_ref: "src/core/bios.cpp:351-456, src/core/system.cpp:2671" + condition: "the console region; an empty value scans SearchDirectory" + notes: | DuckStation auto-detects BIOS by MD5 hash from a flat scan of the BIOS directory. ref: src/core/bios.cpp:458-479 (FindBIOSImagesInDirectory, scans all files by size) diff --git a/emulators/gzdoom.yml b/emulators/gzdoom.yml index ee25a5c18..d958edfa8 100644 --- a/emulators/gzdoom.yml +++ b/emulators/gzdoom.yml @@ -17,6 +17,17 @@ standalone_pools: "[IWADSearch.Directories] Path": "" "[IWADSearch.Directories] RecursivePath": "" +config_reads: + - file: "gzdoom.ini" + source_ref: "src/gameconfigfile.cpp:95-107, 942-951, src/common/platform/posix/unix/i_specialpaths.cpp:168-171" + keys: + - key: "[IWADSearch.Directories] Path" + source_ref: "src/d_iwad.cpp:456-470" + - key: "[FileSearch.Directories] Path" + source_ref: "src/common/utility/findfile.cpp:224-260" + - key: "[SoundfontSearch.Directories] Path" + source_ref: "src/common/audio/music/i_soundfont.cpp:393-410" + notes: | Doom-engine source port by Christoph Oelckers. Supports Doom, Heretic, Hexen, Strife, Chex Quest and derived games through a unified IWAD diff --git a/emulators/hatari.yml b/emulators/hatari.yml index 801bd818f..940f07f95 100644 --- a/emulators/hatari.yml +++ b/emulators/hatari.yml @@ -38,6 +38,45 @@ data_root: source_ref: "upstream src/paths.c:24-29, upstream src/paths.c:201-274" note: "~/.hatari is used instead when it exists and ~/.config/hatari does not. Holds hatari.cfg, hatari.nvram and auto.sav. The default tos.img is read from ../share/hatari beside the bin directory." +pin_build: + - platform: emuelec + repo: "https://github.com/EmuELEC/EmuELEC" + revision: "v4.8" + path: "packages/sx05re/emulators/hatarisa/package.mk" + variable: PKG_VERSION + value: "fda67f05188a925535fefc1b0ef24eb044a029d2" + - platform: amberelec + repo: "https://github.com/AmberELEC/AmberELEC" + revision: "a21975bfb1a3af68f66e07a931b994b523e1e6e2" + path: "packages/games/emulators/hatarisa/package.mk" + variable: PKG_VERSION + value: "d876db9c4b6911c059b1d9318f779f90c1c6f2d7" + - platform: rocknix + repo: "https://github.com/ROCKNIX/distribution" + revision: "20261101" + path: "projects/ROCKNIX/packages/emulators/standalone/hatarisa/package.mk" + variable: PKG_VERSION + value: "6da06056d89bb39777063388d82d065d9e2e31fd" +config_reads: + - file: "*.cfg" + platforms: [emuelec] + source_ref: "upstream src/options.c:1187-1193, upstream src/configuration.c:1088, 1149" + keys: + - key: "[ROM] szTosImageFileName" + source_ref: "upstream src/configuration.c:481, upstream src/tos.c:975-986" + - file: "*.cfg" + platforms: [amberelec] + source_ref: "upstream src/options.c:1283-1288, upstream src/configuration.c:1061, 1123" + keys: + - key: "[ROM] szTosImageFileName" + source_ref: "upstream src/configuration.c:489, upstream src/tos.c:976-987" + - file: "*.cfg" + platforms: [rocknix] + source_ref: "upstream src/options.c:1194-1199, upstream src/configuration.c:1050, 1111" + keys: + - key: "[ROM] szTosImageFileName" + source_ref: "upstream src/configuration.c:480, upstream src/tos.c:975-986" + notes: | The main branch of libretro/hatari is the upstream Hatari tree at c453b85f, whose libretro target in src/retro carries the VFS, disk diff --git a/emulators/lrps2.yml b/emulators/lrps2.yml index 58cc5208d..d8541e5d8 100644 --- a/emulators/lrps2.yml +++ b/emulators/lrps2.yml @@ -24,6 +24,13 @@ systems: [sony-playstation-2] bios_directory: "pcsx2/bios/" resources_directory: "pcsx2/resources/" +config_reads: + - file: "core options" + source_ref: "libretro/main.cpp:506, 525-527" + keys: + - key: "pcsx2_bios" + source_ref: "libretro/main.cpp:525-536, 2552-2560" + notes: | Hard fork of PCSX2 (pre-Qt, 1.7-dev era) ported to libretro, x86_64 only, with much of the emulator since rewritten in C89. The system directory of diff --git a/emulators/mednafen.yml b/emulators/mednafen.yml index 5cdc34595..7d1a95297 100644 --- a/emulators/mednafen.yml +++ b/emulators/mednafen.yml @@ -43,6 +43,61 @@ systems: - snk-ngpc - sony-playstation +pin_build: + - platform: rocknix + repo: "https://github.com/ROCKNIX/distribution" + revision: "20261101" + path: "projects/ROCKNIX/packages/emulators/standalone/mednafen/package.mk" + variable: PKG_URL + value: "https://github.com/sydarn/mednafen/archive/refs/tags/1.32.1-chd.tar.gz" +config_reads: + - file: "mednafen.cfg" + platforms: [rocknix] + revision: "e46995e49cbaf1e9996a8e29cc5be16d7fe6d834" + revision_from: "tag 1.32.1-chd of the sydarn/mednafen fork, the PKG_URL ROCKNIX builds" + source_ref: "src/drivers/main.cpp:1469-1520, 1883-1974, 2249, 2405, src/general.cpp:277-300" + keys: + - key: "gba.bios" + source_ref: "src/gba/GBA.cpp:3278, 772, 2472-2481" + - key: "md.cdbios" + source_ref: "src/md/system.cpp:755, 630-640, src/md/cd/cd.cpp:894-912, 971-982" + condition: "CD content" + - key: "nes.ggrom" + source_ref: "src/nes/nes.cpp:455, 298-299, src/nes/cart.cpp:570-583" + condition: "nes.gg 1" + - key: "pce.cdbios" + source_ref: "src/pce/pce.cpp:1103, 647-649" + condition: "disc not detected as Games Express" + - key: "pce.gecdbios" + source_ref: "src/pce/pce.cpp:1104, 647-649" + condition: "disc detected as Games Express" + - key: "pce_fast.cdbios" + source_ref: "src/pce_fast/pce.cpp:647, 476-487" + - key: "pcfx.bios" + source_ref: "src/pcfx/pcfx.cpp:1115, 606-611" + - key: + - "psx.bios_eu" + - "psx.bios_jp" + - "psx.bios_na" + source_ref: "src/psx/psx.cpp:2373-2375, 1764-1775" + condition: "the disc's region" + - key: + - "ss.bios_jp" + - "ss.bios_na_eu" + source_ref: "src/ss/ss.cpp:2441-2442, 1430-1439" + condition: "not ST-V, by SMPC area" + - key: + - "ss.bios_stv_eu" + - "ss.bios_stv_jp" + - "ss.bios_stv_na" + source_ref: "src/ss/ss.cpp:2444-2446, 1420-1439" + condition: "ST-V cart, by area" + - key: + - "ss.cart.kof95_path" + - "ss.cart.ultraman_path" + source_ref: "src/ss/ss.cpp:2477-2478, 1811-1840, 1320-1350, 1389-1396" + condition: "cart type KOF95 or ULTRAMAN" + notes: | Multi-system emulator by the Mednafen Team, the upstream of the beetle_* and mednafen_* libretro cores, which are forks of individual modules. A module is diff --git a/emulators/melonds.yml b/emulators/melonds.yml index 432e036a9..02e8ac401 100644 --- a/emulators/melonds.yml +++ b/emulators/melonds.yml @@ -28,6 +28,58 @@ data_root: source_ref: "upstream src/frontend/qt_sdl/main.cpp:182-184, 196-200, 206-211" note: "portable beside the executable when that folder exists" +pin_build: + - platform: rocknix + repo: "https://github.com/ROCKNIX/distribution" + revision: "20261101" + path: "projects/ROCKNIX/packages/emulators/standalone/melonds-sa/package.mk" + variable: PKG_VERSION + value: "906e9ebb27da8c6a715cd7abab4abfe8a8d29427" +config_reads: + - file: "melonDS.ini" + platforms: [rocknix] + source_ref: "upstream src/frontend/qt_sdl/main.cpp:366, upstream src/frontend/qt_sdl/Config.cpp:682-774, 777-783, 785-795, upstream src/frontend/qt_sdl/Platform.cpp:144-166, ROCKNIX projects/ROCKNIX/packages/emulators/standalone/melonds-sa/patches/001-default-config.patch:8-13, ROCKNIX projects/ROCKNIX/packages/emulators/standalone/melonds-sa/patches/003-hotkeys.patch:41-76" + keys: + - key: "BIOS9Path" + source_ref: "upstream src/frontend/qt_sdl/Config.cpp:238, 240, upstream src/frontend/qt_sdl/EmuInstance.cpp:855-876, 1261-1263" + condition: "ExternalBIOSEnable=1" + - key: "BIOS7Path" + source_ref: "upstream src/frontend/qt_sdl/Config.cpp:238, 241, upstream src/frontend/qt_sdl/EmuInstance.cpp:878-898, 1265-1267" + condition: "ExternalBIOSEnable=1" + - key: "FirmwarePath" + source_ref: "upstream src/frontend/qt_sdl/Config.cpp:238, 242, upstream src/frontend/qt_sdl/EmuInstance.cpp:992-1016, 1269" + condition: "ExternalBIOSEnable=1 and ConsoleType=0" + - key: "DSiNANDPath" + source_ref: "upstream src/frontend/qt_sdl/Config.cpp:227, 249, upstream src/frontend/qt_sdl/EmuInstance.cpp:1039-1043, 1227, 1312-1322" + condition: "ConsoleType=1" + - file: "melonDS.toml" + platforms: [emudeck, retrobat] + revision: "b86390e4428bf38ce4c1ce0e9ca446d6d25955e8" + revision_from: "melonDS 1.1: the Flathub manifest net.kuribo64.melonDS at e5ce4504 builds it, and RetroBat's melonDS.exe names it" + source_ref: "upstream src/frontend/qt_sdl/main.cpp:196-213, 328, upstream src/frontend/qt_sdl/Config.cpp:42, 785-799" + keys: + - key: "[DS] BIOS9Path" + source_ref: "upstream src/frontend/qt_sdl/EmuInstance.cpp:868-889, 1277" + condition: "[Emu] ExternalBIOSEnable = true" + - key: "[DS] BIOS7Path" + source_ref: "upstream src/frontend/qt_sdl/EmuInstance.cpp:891-911" + condition: "[Emu] ExternalBIOSEnable = true" + - key: "[DS] FirmwarePath" + source_ref: "upstream src/frontend/qt_sdl/EmuInstance.cpp:1013-1031, 1285" + condition: "[Emu] ExternalBIOSEnable = true and ConsoleType = 0" + - key: "[DSi] BIOS9Path" + source_ref: "upstream src/frontend/qt_sdl/EmuInstance.cpp:913-938, 1328-1336" + condition: "[Emu] ConsoleType = 1" + - key: "[DSi] BIOS7Path" + source_ref: "upstream src/frontend/qt_sdl/EmuInstance.cpp:940-965, 1328-1332" + condition: "[Emu] ConsoleType = 1" + - key: "[DSi] FirmwarePath" + source_ref: "upstream src/frontend/qt_sdl/EmuInstance.cpp:1013-1029, 1285" + condition: "[Emu] ConsoleType = 1" + - key: "[DSi] NANDPath" + source_ref: "upstream src/frontend/qt_sdl/EmuInstance.cpp:1064-1068, 1338-1340" + condition: "[Emu] ConsoleType = 1" + notes: | FreeBIOS fallback for DS mode (NTR), while DSi mode requires all native files. Libretro fork based on melonDS ~0.9.x era, validates size only. diff --git a/emulators/nanoboyadvance.yml b/emulators/nanoboyadvance.yml index 0e113e0f4..1141ff63e 100644 --- a/emulators/nanoboyadvance.yml +++ b/emulators/nanoboyadvance.yml @@ -24,6 +24,14 @@ cores: systems: - nintendo-gba +config_reads: + - file: "/storage/.config/nanoboyadvance/config.toml" + platforms: [rocknix] + source_ref: "src/platform/sdl/main.cpp:293, src/platform/core/src/config.cpp:16-31" + keys: + - key: "[general] bios_path" + source_ref: "src/platform/core/src/config.cpp:36, src/platform/sdl/main.cpp:177-191, src/platform/core/src/loader/bios.cpp:19-46" + notes: | Game Boy Advance emulator. ROCKNIX builds the SDL2 frontend only (PLATFORM_SDL2=ON, PLATFORM_QT=OFF). diff --git a/emulators/xemu.yml b/emulators/xemu.yml index 573fd6be8..b18aa80a1 100644 --- a/emulators/xemu.yml +++ b/emulators/xemu.yml @@ -45,6 +45,37 @@ data_root: source_ref: "ui/xemu-settings.cc:51-63, 73-92" note: "xemu.toml beside the executable makes its folder the root" +pin_build: + - platform: rocknix + repo: "https://github.com/ROCKNIX/distribution" + revision: "20261101" + path: "projects/ROCKNIX/packages/emulators/standalone/xemu-sa/package.mk" + variable: PKG_VERSION + value: "cbffb57d084c70351a596e99e32a8b73e0c5eabf" +config_reads: + - file: "xemu.toml" + platforms: [rocknix] + source_ref: "ui/xemu.c:1314-1325, ui/xemu-settings.cc:65-71, 94-104, 158-207" + keys: + - key: "[sys.files] bootrom_path" + source_ref: "system/vl.c:2983-3016" + - key: "[sys.files] flashrom_path" + source_ref: "system/vl.c:3041-3054" + condition: "[general] show_welcome false" + - key: "[sys.files] eeprom_path" + source_ref: "system/vl.c:2909-2949, 3030-3039" + - key: "[sys.files] hdd_path" + source_ref: "system/vl.c:3060-3074" + - file: "xemu.toml" + platforms: [retrobat] + revision: "fc24584ce88f0915ad7f04775bb7712c2e3f49ee" + revision_from: "xemu.exe 0.8.136 in RetroBat's stable xemu.7z, 2026-11-10" + source_ref: "ui/xemu.c:1325, ui/xemu-settings.cc:51-63, 73-92, 94-104, 158-207" + keys: + - key: "[sys.files] flashrom_path" + source_ref: "system/vl.c:3041-3054" + condition: "[general] show_welcome false" + notes: | Full-system Xbox emulator built on QEMU. The libretro core is built from libretro's xemu-libretro tree on its libretro branch, which merges diff --git a/emulators/xroar.yml b/emulators/xroar.yml index 3aa57fb79..ccf2e4f28 100644 --- a/emulators/xroar.yml +++ b/emulators/xroar.yml @@ -29,6 +29,57 @@ standalone_pools: rompath: "" "-rompath": "" +config_reads: + - file: "xroar.conf" + source_ref: "src/xroar.c:882-909, 975-993" + keys: + - key: "rompath" + source_ref: "src/xroar.c:3266, src/romlist.c:125-140" + - key: + - "romlist dragon" + - "romlist d64_1" + - "romlist d64_2" + - "romlist d32" + - "romlist d200e_1" + - "romlist d200e_2" + - "romlist dragon64" + - "romlist dragon64_alt" + - "romlist dragon32" + - "romlist dragon200e" + - "romlist dragon200e_alt" + - "romlist dragon200e_charset" + - "romlist dragonpro_boot" + - "romlist dragonpro_basic" + - "romlist coco" + - "romlist coco_ext" + - "romlist deluxecoco" + - "romlist deluxecoco1" + - "romlist deluxecoco2" + - "romlist deluxecoco3" + - "romlist mx1600" + - "romlist mx1600ext" + - "romlist coco3" + - "romlist coco3p" + - "romlist glenside_ide" + - "romlist mc10" + - "romlist alice" + - "romlist dragondos" + - "romlist dosplus" + - "romlist superdos" + - "romlist cumana" + - "romlist dragondos_compat" + - "romlist rsdos" + - "romlist delta" + - "romlist rsdos_becker" + - "romlist mcx128" + - "romlist mcx128_azerty" + source_ref: "src/xroar.c:646-731, 3267, src/romlist.c:125-140" + - file: "command line" + source_ref: "src/xroar.c:1007" + keys: + - key: "-rompath" + source_ref: "src/xroar.c:3266, src/romlist.c:125-140" + notes: | XRoar by Ciaran Anscomb emulates Dragon 32/64, Tandy CoCo 1/2/3, MC-10, Alice, and clones (MX-1600, CP-400, Dragon 200-E, Deluxe CoCo). diff --git a/tests/test_launcher_evidence.py b/tests/test_launcher_evidence.py new file mode 100644 index 000000000..5f01c2929 --- /dev/null +++ b/tests/test_launcher_evidence.py @@ -0,0 +1,330 @@ +"""A launcher record resting on a config file binds only when its chain holds. + +The platform's code installs the file (installs), its launch code was read +for what replaces it (overrides), and the emulator's profile says where the +build the platform ships loads the file and reads each key (config_reads). +""" + +from __future__ import annotations + +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) + +import yaml # noqa: E402 + +from common import load_platform_config # noqa: E402 +from launchers import ( # noqa: E402 + bound_launchers, + config_evidence, + config_target, + handed_directories, + launcher_places, + launcher_unproven, +) +from profile_sync import _check_build_refs, shipped_build_holders # noqa: E402 +from scraper.retrobat_launchers import _choices_apart # noqa: E402 +from scraper.shell_launchers import ( # noqa: E402 + LauncherPatternMissing, + Script, + config_placement, + config_ref, + is_feature_ref, +) +from validate_schemas import ( # noqa: E402 + _config_reads_without_build, + _config_records_without_evidence, +) + +REPO_ROOT = Path(__file__).resolve().parents[1] + + +def record(**fields: object) -> dict: + """A ROCKNIX-like DuckStation record citing its shipped settings.ini.""" + base = { + "emulator": "duck", "build": "standalone", "kind": "scans_dir", "names": [], + "search_dir": "/storage/roms/bios", + "passes_to": "settings.ini [BIOS] SearchDirectory = /storage/roms/bios, [BIOS] PathPAL unset", + "installs": {"path": "/storage/.config/duck/settings.ini", "source_ref": "setup.sh:13"}, + "overrides": [], + "source_ref": "duck/config/settings.ini:4, duck/package.mk:33", + } + return {**base, **fields} + + +def profile(**fields: object) -> dict: + base = { + "emulator": "Duck", "type": "standalone", "cores": ["duck"], + "standalone_directories": ["[BIOS] SearchDirectory"], + "config_reads": [{ + "file": "settings.ini", "source_ref": "src/core.cpp:10", + "keys": [ + {"key": "[BIOS] SearchDirectory", "source_ref": "src/settings.cpp:20"}, + {"key": ["[BIOS] PathPAL", "[BIOS] PathNTSCU"], "source_ref": "src/bios.cpp:30"}, + ], + }], + } + return {**base, **fields} + + +class ConfigRefs(unittest.TestCase): + def test_config_option_and_feature_files_are_cited_apart(self) -> None: + ref = "a/settings.ini:4, 6, a/start.sh:3, b/MAME.opt:19, .emulationstation/es_features.cfg:9" + self.assertEqual( + config_ref(ref), + ["a/settings.ini:4, 6", "b/MAME.opt:19", ".emulationstation/es_features.cfg:9"], + ) + self.assertEqual([is_feature_ref(p) for p in config_ref(ref)], [False, True, True]) + + def test_passes_to_names_the_file_and_its_keys(self) -> None: + cases = { + "settings.ini [BIOS] SearchDirectory (-settings), [BIOS] PathNTSCU, PathPAL unset": + ("settings.ini", ["[BIOS] SearchDirectory", "[BIOS] PathNTSCU", "[BIOS] PathPAL"]), + "melonDS.toml [DS] BIOS9Path [DSi] NANDPath, first file whose name holds the region": + ("melonDS.toml", ["[DS] BIOS9Path", "[DSi] NANDPath"]), + "settings.ini [BIOS] PathNTSCJ = PathNTSCU = ": + ("settings.ini", ["[BIOS] PathNTSCJ", "[BIOS] PathNTSCU"]), + "AmigaA500-default.uae kickstart_rom_file, $(FILE_PATH) = /storage/k/": + ("AmigaA500-default.uae", ["kickstart_rom_file", "$(FILE_PATH)"]), + "command line -rompath bios/coco": ("command line", ["-rompath"]), + "core option pcsx2_bios": ("core options", ["pcsx2_bios"]), + } + for passes_to, expected in cases.items(): + with self.subTest(passes_to=passes_to): + self.assertEqual(config_target({"passes_to": passes_to}), expected) + core = {"build": "libretro", "kind": "core_option", "passes_to": "hatarib_tos = x/"} + self.assertEqual(config_target(core), ("core options", ["hatarib_tos"])) + + def test_a_record_naming_no_file_takes_the_config_it_installs(self) -> None: + romlist = record( + passes_to="romlist coco", + installs={"path": "$xroar_config", "source_ref": "prepare.sh:13"}, + source_ref="xroar/rd_config/xroar.conf:305, xroar/prepare.sh:13", + ) + self.assertEqual(config_target(romlist), ("xroar.conf", ["romlist coco"])) + + +class Evidence(unittest.TestCase): + profiles = {"duck": profile()} + + def test_a_record_resting_on_code_holds(self) -> None: + code = {"emulator": "duck", "kind": "fixed_name", "source_ref": "start.sh:3"} + self.assertTrue(config_evidence(code, {}).proven) + + def test_the_whole_chain_holds(self) -> None: + evidence = config_evidence(record(), self.profiles, "rocknix") + self.assertTrue(evidence.proven, evidence.missing) + self.assertEqual(evidence.readers, ("duck",)) + + def test_each_missing_link_is_named(self) -> None: + bare = {k: v for k, v in record().items() if k not in ("installs", "overrides")} + missing = config_evidence(bare, self.profiles).missing + self.assertEqual(len(missing), 2) + self.assertTrue(missing[0].startswith("installs")) + self.assertTrue(missing[1].startswith("overrides")) + + def test_a_profile_that_reads_no_such_file_proves_nothing(self) -> None: + missing = config_evidence(record(), {"duck": profile(config_reads=[])}).missing + self.assertEqual(missing, ("config_reads: an emulator that loads settings.ini",)) + + def test_a_key_the_build_does_not_read_is_named(self) -> None: + unread = record(passes_to="settings.ini [BIOS] PathNTSCJ") + self.assertEqual( + config_evidence(unread, self.profiles).missing, + ("config_reads: no read of [BIOS] PathNTSCJ",), + ) + + def test_a_handle_with_its_code_ref_reads_the_key(self) -> None: + handled = profile( + config_reads=[{"file": "settings.ini", "source_ref": "src/core.cpp:10", "keys": [ + {"key": "[BIOS] PathPAL", "source_ref": "src/bios.cpp:30"}]}], + standalone_directories=[ + {"handle": "[BIOS] SearchDirectory", "source_ref": "src/settings.cpp:20"} + ], + ) + self.assertTrue(config_evidence(record(), {"duck": handled}).proven) + + def test_an_option_list_is_never_the_source(self) -> None: + listed = record(source_ref="gen.cs:3, .emulationstation/es_features.cfg:9") + self.assertIn( + "an option list cited as source, not as choices_from", + config_evidence(listed, self.profiles).missing, + ) + + def test_an_option_choice_needs_the_emulator_side_too(self) -> None: + chosen = { + "emulator": "duck", "kind": "core_option", "passes_to": "settings.ini [BIOS] PathPAL = ", + "choices_from": "es_features.cfg:9", "source_ref": "gen.cs:3", + } + self.assertTrue(config_evidence(chosen, self.profiles).proven) + self.assertFalse(config_evidence(chosen, {"duck": profile(config_reads=[])}).proven) + + def test_reads_written_for_a_shipped_build_apply_to_its_platform_only(self) -> None: + built = profile( + source_commit="a" * 40, + pin_build=[{"platform": "rocknix", "value": "b" * 40}], + config_reads=[{**profile()["config_reads"][0], "platforms": ["rocknix"]}], + ) + self.assertTrue(config_evidence(record(), {"duck": built}, "rocknix").proven) + self.assertFalse(config_evidence(record(), {"duck": built}, "emuelec").proven) + + def test_reads_at_the_pin_skip_a_platform_shipping_another_build(self) -> None: + other = profile(source_commit="a" * 40, pin_build=[{"platform": "rocknix", "value": "b" * 40}]) + self.assertFalse(config_evidence(record(), {"duck": other}, "rocknix").proven) + self.assertTrue(config_evidence(record(), {"duck": other}, "emuelec").proven) + + +class Gate(unittest.TestCase): + def config(self, *records: dict) -> dict: + return {"_key": "rocknix", "base_destination": "bios", "launchers": list(records)} + + def test_an_unproven_record_hands_no_directory(self) -> None: + proven = handed_directories(self.config(record()), {"duck": profile()}) + self.assertEqual([h.folder for h in proven], [""]) + self.assertEqual(handed_directories(self.config(record()), {"duck": profile(config_reads=[])}), []) + + def test_an_unproven_record_places_no_file(self) -> None: + named = record( + kind="fixed_name", names=["tos.img"], passes_to="settings.ini [BIOS] PathPAL", + ) + self.assertEqual(len(launcher_places(self.config(named), {"duck": profile()})), 1) + self.assertEqual(launcher_places(self.config(named), {"duck": profile(config_reads=[])}), []) + + def test_the_unproven_are_reported_with_what_they_lack(self) -> None: + config = self.config({"emulator": "x", "kind": "links", "source_ref": "a.sh:1"}, record()) + self.assertEqual(launcher_unproven({"rocknix": config}, {"duck": profile()}), []) + (row,) = launcher_unproven({"rocknix": config}, {"duck": profile(config_reads=[])}) + self.assertEqual((row["platform"], row["index"], row["emulator"]), ("rocknix", 1, "duck")) + self.assertEqual(row["config"], ["duck/config/settings.ini:4"]) + self.assertEqual(len(bound_launchers(config, {"duck": profile(config_reads=[])})), 1) + + +class StartScripts(unittest.TestCase): + SCRIPT = ( + 'CONF_DIR="/storage/.config/duck"\n' + 'CONF_FILE="${CONF_DIR}/settings.ini"\n' + '[ ! -d "${CONF_DIR}" ] && cp -r /usr/config/duck /storage/.config\n' + 'if [ ! -f "${CONF_FILE}" ]; then\n' + ' cp /usr/config/duck/settings.ini ${CONF_FILE}\n' + 'fi\n' + "sed -i '/^AspectRatio/c\\\\AspectRatio = 4:3' ${CONF_FILE}\n" + "sed -i '/^PathPAL/c\\\\PathPAL = x.bin' ${CONF_FILE}\n" + 'cp /usr/config/duck/other.ini ${CONF_DIR}/ >/dev/null 2>&1\n' + ) + + def test_a_guarded_copy_keeps_the_file_and_a_key_rewrite_is_an_override(self) -> None: + placement = config_placement( + Script("start.sh", self.SCRIPT), "/storage/.config/duck/settings.ini", ["[BIOS] PathPAL"] + ) + self.assertEqual(placement.install, {3, 5}) + self.assertTrue(placement.kept_if_present) + self.assertEqual(placement.overrides, {8}) + + def test_a_copy_at_each_launch_replaces_the_file(self) -> None: + script = Script("start.sh", 'cp /usr/config/duck/settings.ini /storage/.config/duck/settings.ini\n') + self.assertFalse(config_placement(script, "/storage/.config/duck/settings.ini").kept_if_present) + + def test_a_script_installing_nothing_raises(self) -> None: + with self.assertRaises(LauncherPatternMissing): + config_placement(Script("start.sh", "echo hi\n"), "/storage/.config/duck/settings.ini") + + def test_retrobat_moves_the_option_list_to_choices_from(self) -> None: + moved = _choices_apart({ + "emulator": "x", "passes_to": "p", + "source_ref": "Gen.cs:3-4, .emulationstation/es_features.cfg:9-12", + }) + self.assertEqual(moved["source_ref"], "Gen.cs:3-4") + self.assertEqual(moved["choices_from"], ".emulationstation/es_features.cfg:9-12") + self.assertEqual(list(moved)[-1], "source_ref") + + +class Invariants(unittest.TestCase): + def test_a_config_record_without_its_platform_links_is_an_error(self) -> None: + path = REPO_ROOT / "platforms" / "x.yml" + bare = {k: v for k, v in record().items() if k not in ("installs", "overrides")} + listed = record(source_ref="gen.cs:3, es_features.yml:9") + errors = _config_records_without_evidence(path, {"launchers": [record(), bare, listed]}) + self.assertEqual(len(errors), 2) + self.assertIn("without installs and overrides", errors[0]) + self.assertIn("choices_from holds it", errors[1]) + + def test_reads_naming_a_platform_need_its_build(self) -> None: + path = REPO_ROOT / "emulators" / "x.yml" + reads = [{"file": "a.ini", "platforms": ["rocknix"], "source_ref": "a:1", "keys": []}] + self.assertEqual(len(_config_reads_without_build(path, {"config_reads": reads})), 1) + self.assertEqual(_config_reads_without_build( + path, {"config_reads": reads, "pin_build": [{"platform": "rocknix"}]} + ), []) + pinned = [{**reads[0], "revision": "c" * 40, "revision_from": "the exe"}] + self.assertEqual(_config_reads_without_build(path, {"config_reads": pinned}), []) + + +class BuildRefs(unittest.TestCase): + def test_reads_for_a_shipped_build_are_read_at_that_build(self) -> None: + document = { + "pin_build": [{"platform": "rocknix", "value": "b" * 40}], + "config_reads": [{ + "file": "a.ini", "platforms": ["rocknix"], "source_ref": "src/a.c:2", + "keys": [{"key": "k", "source_ref": "src/a.c:9, ROCKNIX x.patch:1"}], + }], + } + held = shipped_build_holders(document) + self.assertEqual(len(held), 2) + + class View: + repo, field = "repo", "source" + + class Citation: + def __init__(self, ref: str, field: str) -> None: + self.ref, self.field = ref, field + + asked: list[tuple[str, str]] = [] + + def fetch(_repo: str, revision: str, path: str) -> list[str] | None: + asked.append((revision, path)) + return ["line"] * 5 if path == "src/a.c" else None + + rows = _check_build_refs( + document, + [(Citation("src/a.c:2", "c"), document["config_reads"][0]), + (Citation("src/a.c:9, ROCKNIX x.patch:1", "k"), document["config_reads"][0])], + [View()], fetch, + ) + self.assertEqual([r["status"] for r in rows], ["BUILD_OK", "BUILD_GONE"]) + self.assertTrue(all(revision == "b" * 40 for revision, _ in asked)) + self.assertNotIn(("b" * 40, "ROCKNIX x.patch"), asked) + + +class RepositoryRecords(unittest.TestCase): + """The placement chains the audit of 2026-11-10 found moving files.""" + + def test_the_chains_that_move_placements_still_bind(self) -> None: + platforms = REPO_ROOT / "platforms" + cases = ( + ("rocknix", "armsx2", "/storage/roms/bios/armsx2"), + ("retrodeck", "gzdoom", "$roms_path/doom"), + ("retrodeck", "xroar", "$roms_path/coco"), + ("retrobat", "xroar", "bios/coco"), + ) + for platform, emulator, directory in cases: + with self.subTest(platform=platform, emulator=emulator): + handed = handed_directories(load_platform_config(platform, str(platforms))) + self.assertIn( + directory, {h.directory for h in handed if h.emulator == emulator} + ) + + def test_every_config_record_carries_its_platform_links(self) -> None: + for path in sorted((REPO_ROOT / "platforms").glob("*.yml")): + if path.name.startswith("_"): + continue + with self.subTest(platform=path.stem): + document = yaml.safe_load(path.read_text(encoding="utf-8")) + self.assertEqual(_config_records_without_evidence(path, document), []) + + +if __name__ == "__main__": + unittest.main()