feat: read RetroDECK launchers from components

The scraper reads each component's prepare, launcher and functions scripts at the components commit the BIOS list comes from, and the presets they copy: the fixed paths set_setting_value writes (melonDS seven, xemu three, Dolphin GBA), the folders handed over, the dir_prep links (LRPS2 pcsx2/bios back to bios/, Dolphin and PrimeHack GC regions under saves/), package copies into bios/, RetroArch core options naming a file (pcsx2_bios, DuckStation per region), XRoar romlists, the PICO-8 exec and the RPCS3 and Vita3K firmware downloads.

platforms/retrodeck.yml is regenerated at fcd0d729: only the launchers section is new.
This commit is contained in:
Abdessamad Derraz committed 2026-10-20 21:13:09 +02:00
1 parent 15ffeddfe3
commit 74a9d7beea
4 files changed
+1813

No files matched your search

+949
View File
@@ -16040,3 +16040,952 @@ systems:
native_system: coco
component: xroar
description: Generic disk ROM
launchers:
- emulator: cemu
build: standalone
kind: links
names:
- keys.txt
case_insensitive: false
search_dir: $bios_path/cemu
passes_to: $XDG_DATA_HOME/Cemu/keys.txt is a symlink to it
fails_when_missing: false
depends_on_option: "[[ -e \"$bios_path/cemu/keys.txt\" ]]"
source_ref: "cemu/component_prepare.sh:17"
- emulator: cemu
build: standalone
kind: passes_path
names: []
search_dir: $bios_path/cemu
passes_to: settings.xml mlc_path
fails_when_missing: false
source_ref: "cemu/component_prepare.sh:14, 29"
- emulator: dolphin
build: standalone
kind: links
slot: EUR
names: []
search_dir: $saves_path/gc/dolphin/EU
passes_to: $XDG_DATA_HOME/dolphin-emu/GC/EUR is a symlink to it
fails_when_missing: false
source_ref: "dolphin/component_prepare.sh:19, 38"
- emulator: dolphin
build: standalone
kind: links
slot: USA
names: []
search_dir: $saves_path/gc/dolphin/US
passes_to: $XDG_DATA_HOME/dolphin-emu/GC/USA is a symlink to it
fails_when_missing: false
source_ref: "dolphin/component_prepare.sh:20, 39"
- emulator: dolphin
build: standalone
kind: links
slot: JAP
names: []
search_dir: $saves_path/gc/dolphin/JP
passes_to: $XDG_DATA_HOME/dolphin-emu/GC/JAP is a symlink to it
fails_when_missing: false
source_ref: "dolphin/component_prepare.sh:21, 40"
- emulator: dolphin
build: standalone
kind: links
names: []
search_dir: $bios_path/Triforce
passes_to: $XDG_DATA_HOME/dolphin-emu/Triforce is a symlink to it
fails_when_missing: false
source_ref: "dolphin/component_prepare.sh:30, 49"
- emulator: dolphin
build: standalone
kind: fixed_name
names:
- gba_bios.bin
case_insensitive: false
search_dir: $bios_path
passes_to: "Dolphin.ini [GBA] BIOS"
fails_when_missing: false
source_ref: "dolphin/component_prepare.sh:14, 50"
- emulator: duckstation
build: standalone
kind: passes_path
names: []
search_dir: $bios_path
passes_to: "settings.ini [BIOS] SearchDirectory"
fails_when_missing: false
source_ref: "duckstation/component_prepare.sh:15, 33"
- emulator: gzdoom
build: standalone
kind: passes_path
names: []
search_dir: $bios_path/gzdoom
passes_to: "gzdoom.ini [FileSearch.Directories] Path"
fails_when_missing: false
source_ref: "gzdoom/rd_assets/rd_config/gzdoom.ini:19, gzdoom/component_prepare.sh:23,\
\ 28"
- emulator: mame
build: standalone
kind: passes_path
names: []
search_dir: $bios_path/mame-sa/samples
passes_to: mame.ini samplepath
fails_when_missing: false
source_ref: "mame/component_prepare.sh:94, 117"
- emulator: melonds
build: standalone
kind: links
names: []
search_dir: $bios_path
passes_to: $XDG_CONFIG_HOME/melonDS/bios is a symlink to it
fails_when_missing: false
source_ref: "melonds/component_prepare.sh:19, 41"
- emulator: melonds
build: standalone
kind: fixed_name
names:
- bios9.bin
- bios7.bin
- firmware.bin
- dsi_nand.bin
- dsi_bios7.bin
- dsi_bios9.bin
- dsi_firmware.bin
case_insensitive: false
search_dir: $bios_path
passes_to: "melonDS.toml [DS] BIOS9Path, [DS] BIOS7Path, [DS] FirmwarePath, [DSi]\
\ NANDPath, [DSi] BIOS7Path, [DSi] BIOS9Path, [DSi] FirmwarePath"
fails_when_missing: false
source_ref: "melonds/component_prepare.sh:21-23, 25-28, 43-45, 47-50"
- emulator: pcsx2
build: standalone
kind: passes_path
names: []
search_dir: $bios_path
passes_to: "PCSX2.ini [Folders] Bios"
fails_when_missing: false
source_ref: "pcsx2/component_prepare.sh:16, 80"
- emulator: pico-8
build: standalone
kind: links
names: []
search_dir: $bios_path/pico-8
passes_to: $HOME/.lexaloffle/pico-8 is a symlink to it
fails_when_missing: false
source_ref: "pico-8/component_prepare.sh:15"
- emulator: pico-8
build: standalone
kind: fixed_name
names:
- pico8
case_insensitive: false
search_dir: $bios_path/pico-8
passes_to: exec
fails_when_missing: true
source_ref: "pico-8/component_launcher.sh:3"
- emulator: ppsspp
build: standalone
kind: copies
names: []
search_dir: $component_extras/ppsspp_foss_bios.tar.gz
passes_to: $bios_path/PPSSPP
fails_when_missing: false
source_ref: "ppsspp/component_prepare.sh:38"
- emulator: primehack
build: standalone
kind: links
slot: EUR
names: []
search_dir: $saves_path/gc/primehack/EU
passes_to: $XDG_DATA_HOME/primehack/GC/EUR is a symlink to it
fails_when_missing: false
source_ref: "primehack/component_prepare.sh:15, 33"
- emulator: primehack
build: standalone
kind: links
slot: USA
names: []
search_dir: $saves_path/gc/primehack/US
passes_to: $XDG_DATA_HOME/primehack/GC/USA is a symlink to it
fails_when_missing: false
source_ref: "primehack/component_prepare.sh:16, 34"
- emulator: primehack
build: standalone
kind: links
slot: JAP
names: []
search_dir: $saves_path/gc/primehack/JP
passes_to: $XDG_DATA_HOME/primehack/GC/JAP is a symlink to it
fails_when_missing: false
source_ref: "primehack/component_prepare.sh:17, 35"
- emulator: retroarch
build: libretro
kind: copies
names: []
search_dir: $retroarch_extras_path/mame2003-plus.tar.gz
passes_to: $bios_path/mame2003-plus
fails_when_missing: false
source_ref: "retroarch/component_prepare.sh:86"
- emulator: retroarch
build: libretro
kind: links
names: []
search_dir: $bios_path
passes_to: $bios_path/pcsx2/bios is a symlink to it
fails_when_missing: false
source_ref: "retroarch/component_prepare.sh:96"
- emulator: retroarch
build: libretro
kind: passes_path
names: []
search_dir: $bios_path
passes_to: retroarch.cfg system_directory
fails_when_missing: false
source_ref: "retroarch/component_prepare.sh:44, 216"
- emulator: retroarch
build: libretro
kind: copies
names:
- cannonball
- dinothawr
- DirkSimple
- dolphin-emu
- ecwolf.pk3
- nxengine
- PPSSPP
- prboom.wad
- qemu
- xrick
- mame2003
- mame2003-plus
- pcsx2
- fbneo
search_dir: $retroarch_extras_path
passes_to: $bios_path/<name>
fails_when_missing: false
source_ref: "retroarch/component_prepare.sh:58-67, 70, 87, 95, 109"
- emulator: retroarch
build: libretro
kind: copies
names:
- Databases
- Machines
search_dir: $retroarch_extras_path/MSX
passes_to: $bios_path/<name>
fails_when_missing: false
source_ref: "retroarch/component_prepare.sh:122, 124"
- emulator: retroarch
build: libretro
kind: copies
names:
- capsimg.so
search_dir: $retroarch_extras_path/Amiga
passes_to: $bios_path/<name>
fails_when_missing: false
source_ref: "retroarch/component_prepare.sh:131"
- emulator: retroarch
build: libretro
kind: copies
names:
- scummvm.ini
search_dir: $component_config
passes_to: $bios_path/<name>
fails_when_missing: false
source_ref: "retroarch/component_prepare.sh:137"
- emulator: rpcs3
build: standalone
kind: fixed_name
names:
- PS3UPDAT.PUP
case_insensitive: false
search_dir: $roms_path/ps3/tmp
passes_to: component_launcher.sh --installfw
fails_when_missing: false
depends_on_option: "update_rpcs3_firmware downloads http://dus01.ps3.update.playstation.net/update/ps3/image/us/2026_0318_a2b60b6ac1d2e49e230144345616927c/PS3UPDAT.PUP"
source_ref: "rpcs3/component_functions.sh:11, 19-20"
- emulator: vita3k
build: standalone
kind: fixed_name
names:
- PSVUPDAT.PUP
case_insensitive: false
search_dir: /tmp
passes_to: component_launcher.sh --firmware
fails_when_missing: false
depends_on_option: "update_vita3k_firmware downloads http://dus01.psv.update.playstation.net/update/psv/image/2022_0209/rel_f2c7b12fe85496ec88a0391b514d6e3b/PSVUPDAT.PUP"
source_ref: "vita3k/component_functions.sh:5, 18, 20"
- emulator: vita3k
build: standalone
kind: fixed_name
names:
- PSP2UPDAT.PUP
case_insensitive: false
search_dir: /tmp
passes_to: component_launcher.sh --firmware
fails_when_missing: false
depends_on_option: "update_vita3k_firmware downloads http://dus01.psp2.update.playstation.net/update/psp2/image/2019_0924/sd_8b5f60b56c3da8365b973dba570c53a5/PSP2UPDAT.PUP?dest=us"
source_ref: "vita3k/component_functions.sh:6, 19, 21"
- emulator: xemu
build: standalone
kind: fixed_name
names:
- mcpx_1.0.bin
- Complex.bin
- xbox_hdd.qcow2
case_insensitive: false
search_dir: $bios_path
passes_to: "xemu.toml [sys.files] bootrom_path, [sys.files] flashrom_path, [sys.files]\
\ hdd_path"
fails_when_missing: false
source_ref: "xemu/component_prepare.sh:23-24, 27, 43-44, 46"
- emulator: xemu
build: standalone
kind: copies
names:
- xbox_hdd.qcow2
search_dir: $component_extras
passes_to: $bios_path/<name>
fails_when_missing: false
depends_on_option: "[ ! -f \"$bios_path/xbox_hdd.qcow2\" ]"
source_ref: "xemu/component_prepare.sh:32"
- emulator: xroar
build: standalone
kind: passes_path
names: []
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: xroar.conf rompath
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:290, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: dragon
names:
- dragon
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist dragon
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:291, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: d64_1
names:
- d64_1
- d64rom1
- Dragon Data Ltd - Dragon 64 - IC17
- Dragon Data Ltd - TANO IC18
- Eurohard S.A. - Dragon 200 IC18
- dragrom
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist d64_1
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:292, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: d64_2
names:
- d64_2
- d64rom2
- Dragon Data Ltd - Dragon 64 - IC18
- Dragon Data Ltd - TANO IC17
- Eurohard S.A. - Dragon 200 IC17
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist d64_2
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:293, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: d32
names:
- d32
- dragon32
- d32rom
- Dragon Data Ltd - Dragon 32 - IC17
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist d32
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:294, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: d200e_1
names:
- d200e_1
- d200e_rom1
- ic18_v1.4e.ic34
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist d200e_1
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:295, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: d200e_2
names:
- d200e_2
- d200e_rom2
- ic17_v1.4e.ic37
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist d200e_2
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:296, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: dragon64
names:
- d64_1
- d64rom1
- Dragon Data Ltd - Dragon 64 - IC17
- Dragon Data Ltd - TANO IC18
- Eurohard S.A. - Dragon 200 IC18
- dragrom
- dragon
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist dragon64
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:297, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: dragon64_alt
names:
- d64_2
- d64rom2
- Dragon Data Ltd - Dragon 64 - IC18
- Dragon Data Ltd - TANO IC17
- Eurohard S.A. - Dragon 200 IC17
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist dragon64_alt
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:298, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: dragon32
names:
- d32
- dragon32
- d32rom
- Dragon Data Ltd - Dragon 32 - IC17
- dragon
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist dragon32
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:299, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: dragon200e
names:
- d200e_1
- d200e_rom1
- ic18_v1.4e.ic34
- d64_1
- d64rom1
- Dragon Data Ltd - Dragon 64 - IC17
- Dragon Data Ltd - TANO IC18
- Eurohard S.A. - Dragon 200 IC18
- dragrom
- dragon
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist dragon200e
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:300, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: dragon200e_alt
names:
- d200e_2
- d200e_rom2
- ic17_v1.4e.ic37
- d64_2
- d64rom2
- Dragon Data Ltd - Dragon 64 - IC18
- Dragon Data Ltd - TANO IC17
- Eurohard S.A. - Dragon 200 IC17
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist dragon200e_alt
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:301, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: dragon200e_charset
names:
- d200e_26
- rom26.ic1
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist dragon200e_charset
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:302, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: dragonpro_boot
names:
- alpha-boot-v1.0
- alpha_bt_10
- Dragon Data Ltd - Dragon Alpha Boot v1.0 150584
- alpha-boot-v0.4
- alpha_bt_04
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist dragonpro_boot
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:303, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: dragonpro_basic
names:
- alpha-basic
- alpha_ba
- Dragon Data Ltd - Dragon Alpga Basic
- d64_1
- d64rom1
- Dragon Data Ltd - Dragon 64 - IC17
- Dragon Data Ltd - TANO IC18
- Eurohard S.A. - Dragon 200 IC18
- dragrom
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist dragonpro_basic
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:304, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: coco
names:
- bas13
- bas12
- "Color Basic v1.2 (1982)(Tandy)"
- bas11
- bas10
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist coco
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:305, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: coco_ext
names:
- extbas11
- extbas10
- coco
- COCO
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist coco_ext
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:306, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: coco1
names:
- bas10
- bas13
- bas12
- "Color Basic v1.2 (1982)(Tandy)"
- bas11
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist coco1
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:307, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: coco1e
names:
- bas11
- bas13
- bas12
- "Color Basic v1.2 (1982)(Tandy)"
- bas10
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist coco1e
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:308, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: coco1e_ext
names:
- extbas10
- extbas11
- coco
- COCO
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist coco1e_ext
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:309, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: coco2
names:
- bas12
- bas13
- "Color Basic v1.2 (1982)(Tandy)"
- bas11
- bas10
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist coco2
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:310, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: coco2_ext
names:
- extbas11
- extbas10
- coco
- COCO
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist coco2_ext
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:311, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: coco2b
names:
- bas13
- bas12
- "Color Basic v1.2 (1982)(Tandy)"
- bas11
- bas10
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist coco2b
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:312, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: deluxecoco
names:
- adv070_u24
- deluxe
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist deluxecoco
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:313, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: deluxecoco1
names:
- adv071_u24
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist deluxecoco1
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:314, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: deluxecoco2
names:
- adv072_u24
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist deluxecoco2
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:315, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: deluxecoco3
names:
- adv073-2_u24
- adv073_u24
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist deluxecoco3
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:316, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: mx1600
names:
- mx1600bas
- mx1600bas_zephyr
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist mx1600
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:317, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: mx1600ext
names:
- mx1600extbas
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist mx1600ext
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:318, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: coco3
names:
- coco3
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist coco3
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:319, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: coco3p
names:
- coco3p
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist coco3p
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:320, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: glenside_ide
names:
- yados
- hdblba
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist glenside_ide
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:321, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: mc10
names:
- mc10
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist mc10
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:322, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: alice
names:
- alice
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist alice
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:323, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: dragondos
names:
- ddos12a
- ddos12
- ddos40
- ddos15
- ddos10
- Dragon Data Ltd - DragonDOS 1.0
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist dragondos
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:324, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: dosplus
names:
- dplus49b
- dplus48
- dosplus-4.8
- DOSPLUS
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist dosplus
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:325, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: superdos
names:
- sdose6
- PNP - SuperDOS E6
- sdose5
- sdose4
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist superdos
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:326, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: cumana
names:
- cdos20
- CDOS20
- CumanaDOSv2.0
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist cumana
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:327, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: dragondos_compat
names:
- dplus49b
- dplus48
- dosplus-4.8
- DOSPLUS
- sdose6
- PNP - SuperDOS E6
- sdose5
- sdose4
- ddos12a
- ddos12
- ddos40
- ddos15
- ddos10
- Dragon Data Ltd - DragonDOS 1.0
- cdos20
- CDOS20
- CumanaDOSv2.0
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist dragondos_compat
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:328, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: rsdos
names:
- disk11
- disk10
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist rsdos
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:329, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: delta
names:
- delta2
- delta1a
- delta19
- delta
- deltados
- Premier Micros - DeltaDOS
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist delta
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:330, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: rsdos_becker
names:
- hdbdw3bck
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist rsdos_becker
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:331, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: mcx128
names:
- mcx128
- MCX_Basic_21
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist mcx128
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:332, xroar/component_prepare.sh:13,\
\ 15"
- emulator: xroar
build: standalone
kind: names_searched
slot: mcx128_azerty
names:
- mcx128a
- MCX_Basic_21_AZERTY
search_dir: "$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path"
passes_to: romlist mcx128_azerty
fails_when_missing: false
source_ref: "xroar/rd_assets/rd_config/xroar.conf:333, xroar/component_prepare.sh:13,\
\ 15"
- emulator: retroarch
build: libretro
kind: core_option
names:
- scph5500.bin
passes_to: core option duckstation_BIOS.PathNTSCJ
fails_when_missing: false
source_ref: "retroarch/rd_assets/rd_config/retroarch-core-options.cfg:165, retroarch/component_prepare.sh:24"
- emulator: retroarch
build: libretro
kind: core_option
names:
- scph5501.bin
passes_to: core option duckstation_BIOS.PathNTSCU
fails_when_missing: false
source_ref: "retroarch/rd_assets/rd_config/retroarch-core-options.cfg:166, retroarch/component_prepare.sh:24"
- emulator: retroarch
build: libretro
kind: core_option
names:
- scph5502.bin
passes_to: core option duckstation_BIOS.PathPAL
fails_when_missing: false
source_ref: "retroarch/rd_assets/rd_config/retroarch-core-options.cfg:167, retroarch/component_prepare.sh:24"
- emulator: retroarch
build: libretro
kind: core_option
names:
- scph10000.bin
passes_to: core option pcsx2_bios
fails_when_missing: false
source_ref: "retroarch/rd_assets/rd_config/retroarch-core-options.cfg:515, retroarch/component_prepare.sh:24"
+498
View File
@@ -0,0 +1,498 @@
"""RetroDECK's launcher layer, read from each component's scripts.
A component's `component_prepare.sh` writes the emulator's config on reset:
melonDS gets seven fixed paths under $bios_path, xemu three, DuckStation and
PCSX2 a directory to search. It links folders the emulator reads into or out
of bios/ (LRPS2's pcsx2/bios points back at bios/, Dolphin's GC/<region>
folders live under saves/), and copies package-provided trees into bios/.
The presets it copies carry the rest: RetroArch core options that name a
BIOS file, XRoar's ROM search lists. RPCS3's firmware is downloaded by
RetroDECK itself and PICO-8 is started from a binary the user drops in bios/.
The shell is read as text, never run. Every line of a prepare or launcher
script that names $bios_path must match a known construct, else the scrape
stops with LauncherPatternMissing: an unclassified use would vanish from the
platform file without a word.
"""
from __future__ import annotations
import re
import shlex
from collections.abc import Callable
from pathlib import PurePosixPath
from typing import Any
try:
from .retropie_launchers import Launcher, LauncherPatternMissing, merge_records, source_ref
except ImportError:
from retropie_launchers import Launcher, LauncherPatternMissing, merge_records, source_ref
BIOS = "$bios_path"
PLACEHOLDER = "RETRODECKBIOSDIR"
CORE_OPTIONS = "retroarch/rd_assets/rd_config/retroarch-core-options.cfg"
# The component that runs libretro cores.
LIBRETRO_COMPONENT = "retroarch"
# Values of a core option that name a file rather than a setting.
_FILE_VALUE = re.compile(r"\.(bin|rom|img|zip|bios)$", re.IGNORECASE)
_EXTENSION = re.compile(r"\.[A-Za-z0-9]{1,6}$")
_ASSIGN = re.compile(r'^([A-Za-z_]\w*)="([^"]*)"$')
_GC_REGION = re.compile(r"/GC/(EUR|USA|JAP)$")
# Commands whose $bios_path argument creates or backs up a folder, logs, or
# only tests a path.
HOUSEKEEPING = frozenset({"create_dir", "log", "mkdir", "chmod", "rm", "unlink"})
Read = Callable[[str], "str | None"]
def _words(text: str) -> list[str]:
try:
return shlex.split(text, comments=True)
except ValueError:
return text.split()
def _unquote(value: str) -> str:
return value.strip().strip("'\"")
def _split_path(path: str) -> tuple[str, str]:
pure = PurePosixPath(path.rstrip("/"))
return str(pure.parent), pure.name
def _is_file(path: str) -> bool:
return bool(_EXTENSION.search(PurePosixPath(path).name))
def _mentions_bios(text: str) -> bool:
return BIOS in text or "${bios_path}" in text
class Script:
"""One component shell script, line by line, with its enclosing tests."""
def __init__(self, path: str, text: str) -> None:
self.path = path
self.lines = text.splitlines()
def statements(self) -> list[tuple[int, list[str], str, list[str]]]:
"""(line, words, raw, enclosing conditions) for each command, `&&` chains split."""
found = []
branches: list[str] = []
for number, line in enumerate(self.lines, 1):
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
opened = re.match(r"^(if|elif)\s+(.*?)\s*;\s*then\s*$", stripped)
if opened is not None:
condition = opened.group(2)
if opened.group(1) == "if":
branches.append(condition)
elif branches:
branches[-1] = condition
continue
if stripped == "else" and branches:
branches[-1] = f"not ({branches[-1]})"
continue
if stripped == "fi":
if branches:
branches.pop()
continue
shown = [c for c in branches if "$action" not in c]
for part in re.split(r"\s+&&\s+|\s+\|\|\s+", stripped):
words = _words(part)
if words:
found.append((number, words, part, shown))
return found
class Component:
"""What one component's scripts and presets hand its emulator."""
def __init__(self, name: str, read: Read) -> None:
self.name = name
self.read = read
self.build = "libretro" if name == LIBRETRO_COMPONENT else "standalone"
self.records: list[Launcher] = []
self.variables = self._variables()
def _variables(self) -> dict[str, str]:
text = self.read(f"{self.name}/component_functions.sh") or ""
return {
m.group(1): m.group(2)
for m in (_ASSIGN.match(line.strip()) for line in text.splitlines())
if m is not None
}
def resolve(self, word: str) -> str:
"""A word with the component's own bios/ paths filled in (`$retroarch_config_scummvm`)."""
def fill(found: re.Match[str]) -> str:
value = self.variables.get(found.group(1) or found.group(2), "")
return value if _mentions_bios(value) else found.group(0)
return re.sub(r"\$\{(\w+)\}|\$(\w+)", fill, word)
def config_name(self, value: str) -> str:
"""`melonDS.toml` for `$melonds_config`, as component_functions.sh defines it."""
variable = re.fullmatch(r"\$\{?(\w+)\}?", value)
if variable is None or variable.group(1) not in self.variables:
return value
return PurePosixPath(self.variables[variable.group(1)]).name
def record(
self, kind: str, names: list[str], lines: list[tuple[str, int]], **fields: Any
) -> None:
self.records.append(Launcher(
emulator=fields.pop("emulator", self.name), build=fields.pop("build", self.build),
kind=kind, names=names, source_ref=source_ref(lines), lines=list(lines), **fields,
))
def read_prepare(self) -> None:
path = f"{self.name}/component_prepare.sh"
text = self.read(path)
if text is None:
return
script = Script(path, text)
settings: dict[tuple[str, ...], list[tuple[int, str, str]]] = {}
copies: dict[tuple[str, str | None], list[tuple[int, str, str]]] = {}
for number, words, raw, conditions in script.statements():
depends = "; ".join(conditions) or None
command = words[0]
if command != "set_setting_value":
words = [self.resolve(w) for w in words]
raw = " ".join(words)
if command == "set_setting_value" and len(words) >= 4:
value = _unquote(words[3])
if _mentions_bios(value):
section = words[5] if len(words) > 5 else None
where = f"[{section}] {words[2]}" if section else words[2]
target = self.config_name(words[1])
if _is_file(value):
settings.setdefault(("fixed_name", target, depends or ""), []).append(
(number, value, where)
)
else:
key = ("passes_path", target, where, depends or "")
settings.setdefault(key, []).append((number, value, where))
continue
if command == "dir_prep" and len(words) == 3:
self._dir_prep(path, number, words[1], words[2], depends)
continue
if command == "ln" and _mentions_bios(raw):
self._link(path, number, words, depends)
continue
if command == "cp" and len(words) >= 3 and _mentions_bios(words[-1]):
source, target = words[-2], words[-1]
folder, name = _split_path(source)
copies.setdefault((folder, depends), []).append((number, name, target))
continue
if command == "tar" and "-C" in words and _mentions_bios(words[words.index("-C") + 1]):
if "-czf" in words:
continue
archive = next((w for w in words if w.endswith((".tar.gz", ".tgz", ".tar"))), None)
if archive is None:
raise LauncherPatternMissing(
f"{path}:{number}", f"tar into bios/ of no archive: {raw}"
)
self.record(
"copies", [], [(path, number)], search_dir=archive,
passes_to=words[words.index("-C") + 1], fails_when_missing=False,
depends_on_option=depends,
)
continue
if command == "sed" and PLACEHOLDER in raw:
self._preset(script, number, words)
continue
if not _mentions_bios(raw):
continue
if command in HOUSEKEEPING or command in ("[[", "[", "if") or command.startswith("[["):
continue
raise LauncherPatternMissing(
f"{path}:{number}", f"unrecognised use of $bios_path: {raw}"
)
for key, hits in settings.items():
self._settings(path, key, hits)
for (folder, depends), hits in copies.items():
names = [name for _n, name, _t in hits]
targets = [target for _n, _name, target in hits]
self.record(
"copies", names, [(path, n) for n, _name, _t in hits], search_dir=folder,
passes_to=_render_targets(names, targets), fails_when_missing=False,
depends_on_option=depends or None,
)
def _settings(self, path: str, key: tuple[str, ...], hits: list[tuple[int, str, str]]) -> None:
kind, target = key[0], key[1]
depends = key[-1] or None
lines = [(path, n) for n, _v, _w in hits]
if kind == "fixed_name":
folders = {_split_path(value)[0] for _n, value, _w in hits}
if len(folders) != 1:
raise LauncherPatternMissing(
f"{path}:{hits[0][0]}", "fixed names in several folders"
)
names = list(dict.fromkeys(_split_path(value)[1] for _n, value, _w in hits))
wheres = list(dict.fromkeys(w for _n, _v, w in hits))
self.record(
"fixed_name", names, lines,
case_insensitive=False, search_dir=folders.pop(),
passes_to=f"{target} " + ", ".join(wheres),
fails_when_missing=False, depends_on_option=depends,
)
return
values = {v for _n, v, _w in hits}
if len(values) != 1:
raise LauncherPatternMissing(f"{path}:{hits[0][0]}", f"{key[2]} set to several folders")
self.record(
"passes_path", [], lines, search_dir=values.pop(),
passes_to=f"{target} {key[2]}", fails_when_missing=False, depends_on_option=depends,
)
def _dir_prep(self, path: str, number: int, real: str, link: str, depends: str | None) -> None:
"""`dir_prep REAL LINK` makes LINK a symlink to REAL (functions/other_functions.sh)."""
region = _GC_REGION.search(link)
if region is not None:
self.record(
"links", [], [(path, number)], slot=region.group(1), search_dir=real,
passes_to=f"{link} is a symlink to it", fails_when_missing=False,
depends_on_option=depends,
)
return
if real.startswith(BIOS):
self.record(
"links", [], [(path, number)], search_dir=real,
passes_to=f"{link} is a symlink to it", fails_when_missing=False,
depends_on_option=depends,
)
# A user folder (saves, textures, cheats) linked into bios/ hands no BIOS over.
def _link(self, path: str, number: int, words: list[str], depends: str | None) -> None:
paths = [w for w in words[1:] if not w.startswith("-")]
if len(paths) != 2 or not _mentions_bios(paths[0]):
raise LauncherPatternMissing(
f"{path}:{number}", f"link without a bios/ source: {words}"
)
folder, name = _split_path(paths[0])
self.record(
"links", [name], [(path, number)], case_insensitive=False, search_dir=folder,
passes_to=f"{paths[1]} is a symlink to it", fails_when_missing=False,
depends_on_option=depends,
)
def _preset(self, script: Script, number: int, words: list[str]) -> None:
"""`sed -i "s#RETRODECKBIOSDIR#${bios_path}#g" "$cfg"` on a preset copied just above."""
target = words[-1]
substitutions = self._substitutions(script, target)
copied = None
for line_no, line in enumerate(script.lines[:number], 1):
cp = _words(line.strip().split("&&")[0])
if cp and cp[0] == "cp" and cp[-1] == target and "$component_config/" in cp[-2]:
copied = (line_no, cp[-2].split("$component_config/", 1)[1])
if copied is None:
raise LauncherPatternMissing(f"{script.path}:{number}", f"no preset copied to {target}")
preset = f"{self.name}/rd_assets/rd_config/{copied[1]}"
text = self.read(preset)
if text is None:
raise LauncherPatternMissing(preset, "preset is missing")
section = None
found = 0
for line_no, line in enumerate(text.splitlines(), 1):
stripped = line.strip()
if re.fullmatch(r"\[[^\]]+\]", stripped):
section = stripped
if PLACEHOLDER not in stripped or stripped.startswith("#"):
continue
setting = re.match(r"^([\w.$()-]+)\s*(?:=|\s)\s*(.+)$", stripped)
if setting is None:
raise LauncherPatternMissing(f"{preset}:{line_no}", f"placeholder in {stripped}")
key, value = setting.group(1), _unquote(setting.group(2))
for placeholder, variable in substitutions.items():
value = value.replace(placeholder, variable)
where = f"{section} {key}" if section else key
found += 1
applied = [(script.path, copied[0]), (script.path, number)]
self.record(
"passes_path", [], [(preset, line_no)] + applied,
search_dir=value, passes_to=f"{self.config_name(target)} {where}",
fails_when_missing=False,
)
if key == "rompath":
self.romlists(preset, text, value, applied)
if not found:
raise LauncherPatternMissing(preset, f"no {PLACEHOLDER} line")
@staticmethod
def _substitutions(script: Script, target: str) -> dict[str, str]:
found = {}
for line in script.lines:
for m in re.finditer(r"s#(RETRODECK\w+)#\$\{?(\w+)\}?#g[\"']?\s+\"([^\"]+)\"", line):
if m.group(3) == target:
found[m.group(1)] = f"${m.group(2)}"
return found
def romlists(
self, preset: str, text: str, rompath: str, applied: list[tuple[str, int]]
) -> None:
"""XRoar's `romlist NAME=a,b,@other`: the names tried for each ROM, in order."""
lists: dict[str, tuple[int, list[str]]] = {}
for line_no, line in enumerate(text.splitlines(), 1):
found = re.match(r"^romlist\s+([\w-]+)=(.*)$", line.strip())
if found is not None:
items = [_unquote(i) for i in re.findall(r'"[^"]*"|[^,]+', found.group(2))]
lists[found.group(1)] = (line_no, items)
if not lists:
raise LauncherPatternMissing(preset, "rompath with no romlist")
def expand(name: str, seen: tuple[str, ...]) -> list[str]:
names = []
for item in lists[name][1]:
if item.startswith("@"):
ref = item[1:]
if ref not in lists or ref in seen:
raise LauncherPatternMissing(preset, f"romlist {name} refers to {item}")
names += expand(ref, seen + (ref,))
else:
names.append(item)
return list(dict.fromkeys(names))
for name, (line_no, _items) in lists.items():
self.record(
"names_searched", expand(name, (name,)), [(preset, line_no)] + applied,
slot=name, search_dir=rompath, passes_to=f"romlist {name}",
fails_when_missing=False,
)
def read_launcher(self) -> None:
path = f"{self.name}/component_launcher.sh"
text = self.read(path)
if text is None:
return
for number, words, raw, conditions in Script(path, text).statements():
if not _mentions_bios(raw):
continue
if words[0] == "exec" and len(words) > 1 and _mentions_bios(words[1]):
folder, name = _split_path(words[1])
self.record(
"fixed_name", [name], [(path, number)], case_insensitive=False,
search_dir=folder, passes_to="exec", fails_when_missing=True,
depends_on_option="; ".join(conditions) or None,
)
continue
raise LauncherPatternMissing(
f"{path}:{number}", f"unrecognised use of $bios_path: {raw}"
)
def read_firmware_download(self) -> None:
"""A firmware URL RetroDECK downloads and hands to the emulator's installer."""
path = f"{self.name}/component_functions.sh"
text = self.read(path)
if text is None:
return
urls = {
name: value for name, value in self.variables.items()
if re.match(r"https?://", value)
}
script = Script(path, text)
statements = script.statements()
for number, words, _raw, _conditions in statements:
if words[0] != "download_file" or len(words) < 3:
continue
source = re.fullmatch(r"\$\{?(\w+)\}?", words[1])
if source is None or source.group(1) not in urls:
continue
destination = words[2]
installs = [
(n, w) for n, w, _r, _c in statements
if destination in w and w[0] != "download_file"
and any(_installs(f) for f in w)
]
if not installs:
raise LauncherPatternMissing(
f"{path}:{number}", f"{destination} is downloaded but never installed"
)
install_line, install = installs[0]
flag = next(f for f in install if _installs(f))
function = self._function_at(script, number)
assigned = next(
n for n, line in enumerate(script.lines, 1)
if line.strip().startswith(f"{source.group(1)}=")
)
folder, name = _split_path(destination)
self.record(
"fixed_name", [name], [(path, assigned), (path, number), (path, install_line)],
case_insensitive=False, search_dir=folder,
passes_to=f"component_launcher.sh {flag}", fails_when_missing=False,
depends_on_option=f"{function} downloads {urls[source.group(1)]}",
)
@staticmethod
def _function_at(script: Script, number: int) -> str:
for line in reversed(script.lines[:number]):
found = re.match(r"^([\w-]+)\s*\(\)\s*\{", line)
if found is not None:
return found.group(1)
raise LauncherPatternMissing(f"{script.path}:{number}", "download outside a function")
def _installs(word: str) -> bool:
"""A launcher flag that installs a firmware file: RPCS3 --installfw, Vita3K --firmware."""
return word.startswith("--install") or word == "--firmware"
def _render_targets(names: list[str], targets: list[str]) -> str:
"""Where copied names land: one folder when each keeps its name, else each path."""
folders = {_split_path(t)[0] for t in targets}
kept = [_split_path(t)[1] for t in targets] == [PurePosixPath(n).name for n in names]
if kept and len(folders) == 1:
return f"{folders.pop()}/<name>"
return ", ".join(targets)
def core_options(read: Read) -> list[Launcher]:
"""RetroArch core options in RetroDECK's preset whose value names a BIOS file."""
text = read(CORE_OPTIONS)
prepare_path = f"{LIBRETRO_COMPONENT}/component_prepare.sh"
prepare = read(prepare_path) or ""
if text is None:
raise LauncherPatternMissing(CORE_OPTIONS, "preset is missing")
copied = [
n for n, line in enumerate(prepare.splitlines(), 1)
if "retroarch-core-options.cfg" in line and line.strip().startswith("cp ")
]
if not copied:
raise LauncherPatternMissing(prepare_path, "the core options preset is never copied")
records = []
for line_no, line in enumerate(text.splitlines(), 1):
found = re.match(r'^([\w.-]+)\s*=\s*"([^"]*)"$', line.strip())
if found is None or not _FILE_VALUE.search(found.group(2)):
continue
key, value = found.group(1), found.group(2)
records.append(Launcher(
emulator=LIBRETRO_COMPONENT, build="libretro", kind="core_option", names=[value],
passes_to=f"core option {key}", fails_when_missing=False,
source_ref=source_ref([(CORE_OPTIONS, line_no), (prepare_path, copied[0])]),
))
if not records:
raise LauncherPatternMissing(CORE_OPTIONS, "no core option names a file")
return records
def extract_launchers(components: list[str], read: Read) -> list[dict[str, Any]]:
"""Every launcher record of the given components, read through `read(repo path)`.
`read` returns None only for a file the component does not have.
"""
records: list[Launcher] = []
for name in components:
component = Component(name, read)
component.read_prepare()
component.read_launcher()
component.read_firmware_download()
records += merge_records(component.records)
if LIBRETRO_COMPONENT in components:
records += core_options(read)
if not records:
raise LauncherPatternMissing("components", "no component hands a file to its emulator")
return [record.entry() for record in records]
+32
View File
@@ -40,16 +40,20 @@ try:
BaseScraper,
BiosRequirement,
fetch_branch_head,
github_headers,
requirement_entry,
)
from .retrodeck_launchers import extract_launchers
except ImportError:
sys.path.insert(0, str(Path(__file__).parent.parent))
from scraper.base_scraper import (
BaseScraper,
BiosRequirement,
fetch_branch_head,
github_headers,
requirement_entry,
)
from scraper.retrodeck_launchers import extract_launchers
PLATFORM_NAME = "retrodeck"
COMPONENTS_REPO = "RetroDECK/components"
@@ -233,6 +237,7 @@ class Scraper(BaseScraper):
super().__init__()
self.manifests_dir = manifests_dir
self._manifests: list[tuple[str, dict]] | None = None
self._files: dict[str, str | None] = {}
# The manifests are read from main: one commit for the tree and for
# every file, and that commit is the one recorded.
self.revision = (
@@ -317,6 +322,32 @@ class Scraper(BaseScraper):
print(f" WARNING: {mf}: {e}", file=sys.stderr)
return manifests
def component_file(self, path: str) -> str | None:
"""A file of the components repo at the scraped revision, None when it has none.
Only a 404 says the file is absent: any other failure stops the
scrape, as for the manifests.
"""
if path in self._files:
return self._files[path]
if self.manifests_dir:
local = Path(self.manifests_dir) / path
text = local.read_text(encoding="utf-8") if local.is_file() else None
else:
url = f"{RAW_BASE.format(rev=self.revision)}/{path}"
try:
req = urllib.request.Request(url, headers=github_headers())
with urllib.request.urlopen(req, timeout=30) as resp:
text = resp.read().decode("utf-8")
except urllib.error.HTTPError as e:
if e.code != 404:
raise ConnectionError(f"{url}: HTTP {e.code}") from e
text = None
except urllib.error.URLError as e:
raise ConnectionError(f"{url}: {e.reason}") from e
self._files[path] = text
return text
def validate_format(self, raw_data: str) -> bool:
try:
return isinstance(json.loads(raw_data), dict)
@@ -478,6 +509,7 @@ class Scraper(BaseScraper):
# its own, and retroarch is the one that runs cores.
"standalone_cores": [c for c in cores if c != "retroarch"],
"systems": systems,
"launchers": extract_launchers(cores, self.component_file),
}
+334
View File
@@ -0,0 +1,334 @@
"""RetroDECK's launcher layer read from each component's scripts and presets.
Each fixture copies the lines an extractor reads from RetroDECK/components
fcd0d72995d7aa2e741e2753ac6914025b2ff1ac, placed at the line numbers they
have there, so the source_ref a record carries is the one platforms/retrodeck.yml
cites. A construct that is gone or unknown must stop the scrape.
"""
from __future__ import annotations
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 retrodeck_launchers as rd # noqa: E402
def at(first_line: int, text: str) -> str:
"""Text placed so its first line is line `first_line` of the file."""
return place((first_line, text))
def place(*parts: tuple[int, str]) -> str:
"""Excerpts each placed at the line it starts on in the real file."""
lines: list[str] = []
for first_line, text in parts:
excerpt = textwrap.dedent(text).lstrip("\n").splitlines()
if len(lines) >= first_line:
raise ValueError(f"excerpt at line {first_line} overlaps the one before")
lines += [""] * (first_line - 1 - len(lines)) + excerpt
return "\n".join(lines) + "\n"
MELONDS_FUNCTIONS = at(3, 'melonds_config="$XDG_CONFIG_HOME/melonDS/melonDS.toml"\n')
MELONDS_PREPARE = at(7, """
if [[ "$action" == "reset" ]]; then # Run reset-only commands
log i "----------------------"
log i "Resetting $component_name"
log i "----------------------"
create_dir -d "$XDG_CONFIG_HOME/melonDS/"
cp -fv "$component_config/melonDS.toml" "$melonds_config"
create_dir "$saves_path/nds/melonds"
create_dir "$states_path/nds/melonds"
create_dir "$cheats_path/MelonDS"
dir_prep "$bios_path" "$XDG_CONFIG_HOME/melonDS/bios"
set_setting_value "$melonds_config" "BIOS9Path" "\\"$bios_path/bios9.bin\\"" "melonds" "DS"
set_setting_value "$melonds_config" "BIOS7Path" "\\"$bios_path/bios7.bin\\"" "melonds" "DS"
set_setting_value "$melonds_config" "FirmwarePath" "\\"$bios_path/firmware.bin\\"" "melonds" "DS"
set_setting_value "$melonds_config" "NANDPath" "\\"$bios_path/dsi_nand.bin\\"" "melonds" "DSi"
set_setting_value "$melonds_config" "BIOS7Path" "\\"$bios_path/dsi_bios7.bin\\"" "melonds" "DSi"
set_setting_value "$melonds_config" "BIOS9Path" "\\"$bios_path/dsi_bios9.bin\\"" "melonds" "DSi"
set_setting_value "$melonds_config" "FirmwarePath" "\\"$bios_path/dsi_firmware.bin\\"" "melonds" "DSi"
set_setting_value "$melonds_config" "SaveFilePath" "\\"$saves_path/nds/melonds\\"" "melonds" "Instance0"
fi
""")
XEMU_FUNCTIONS = at(3, 'xemu_conf="$XDG_CONFIG_HOME/xemu/xemu.toml"\n')
XEMU_PREPARE = at(20, """
cp -fv "$component_config/xemu.toml" "$xemu_conf"
create_dir "$screenshots_path/xemu"
set_setting_value "$xemu_conf" "screenshot_dir" "'$screenshots_path/xemu'" "xemu" "General"
set_setting_value "$xemu_conf" "bootrom_path" "'$bios_path/mcpx_1.0.bin'" "xemu" "sys.files"
set_setting_value "$xemu_conf" "flashrom_path" "'$bios_path/Complex.bin'" "xemu" "sys.files"
create_dir "$saves_path/xbox/xemu"
set_setting_value "$xemu_conf" "eeprom_path" "'$saves_path/xbox/xemu/xbox-eeprom.bin'" "xemu" "sys.files"
set_setting_value "$xemu_conf" "hdd_path" "'$bios_path/xbox_hdd.qcow2'" "xemu" "sys.files"
# Preparing HD dummy Image if the image is not found
if [ ! -f "$bios_path/xbox_hdd.qcow2" ];then
cp -f "$component_extras/xbox_hdd.qcow2" "$bios_path/xbox_hdd.qcow2"
fi
""")
RETROARCH_PREPARE = place((23, """
cp -fv "$component_config/retroarch.cfg" "$retroarch_config"
cp -fv "$component_config/retroarch-core-options.cfg" "$retroarch_config_core_options"
"""), (44, """
set_setting_value "$retroarch_config" "system_directory" "$bios_path" "retroarch"
"""), (95, """
cp -rf "$retroarch_extras_path/pcsx2" "$bios_path/pcsx2"
dir_prep "$bios_path" "$bios_path/pcsx2/bios"
dir_prep "$saves_path/ps2/retroarch-core/LRPS2/memcards" "$bios_path/pcsx2/memcards"
"""))
CORE_OPTIONS = place((165, """
duckstation_BIOS.PathNTSCJ = "scph5500.bin"
duckstation_BIOS.PathNTSCU = "scph5501.bin"
duckstation_BIOS.PathPAL = "scph5502.bin"
"""), (515, """
pcsx2_bios = "scph10000.bin"
pcsx2_boot_bios = "disabled"
"""))
DOLPHIN_PREPARE = at(19, """
dir_prep "$saves_path/gc/dolphin/EU" "$XDG_DATA_HOME/dolphin-emu/GC/EUR" # TODO: Multi-user one-off
dir_prep "$saves_path/gc/dolphin/US" "$XDG_DATA_HOME/dolphin-emu/GC/USA" # TODO: Multi-user one-off
dir_prep "$saves_path/gc/dolphin/JP" "$XDG_DATA_HOME/dolphin-emu/GC/JAP" # TODO: Multi-user one-off
dir_prep "$screenshots_path" "$XDG_DATA_HOME/dolphin-emu/ScreenShots"
""")
RPCS3_FUNCTIONS = at(11, """
rpcs3_firmware="http://dus01.ps3.update.playstation.net/update/ps3/image/us/2026_0318_a2b60b6ac1d2e49e230144345616927c/PS3UPDAT.PUP"
update_rpcs3_firmware() {
if [[ $(check_network_connectivity) == "true" ]]; then
configurator_generic_dialog "RetroDECK Configurator - Install: RPCS3 Firmware" "This tool will download the firmware."
(
create_dir "$roms_path/ps3/tmp"
chmod 777 "$roms_path/ps3/tmp"
download_file "$rpcs3_firmware" "$roms_path/ps3/tmp/PS3UPDAT.PUP" "RPCS3 Firmware"
bash "$rpcs3_component_dir/component_launcher.sh" --installfw "$roms_path/ps3/tmp/PS3UPDAT.PUP"
rm -rf "$roms_path/ps3/tmp"
)
fi
}
""")
PICO8_LAUNCHER = at(3, 'exec "$bios_path/pico-8/pico8" -desktop_path "$screenshots_path" "$@"\n')
XROAR_PREPARE = at(12, """
create_dir -d "$XDG_CONFIG_HOME/xroar"
cp -f "$component_config/xroar.conf" "$xroar_config" && log i "Copied default xroar.conf to $xroar_config"
sed -i "s#RETRODECKROMSDIR#${roms_path}#g" "$xroar_config" && log i "Set ROMs directory in xroar.conf"
sed -i "s#RETRODECKBIOSDIR#${bios_path}#g" "$xroar_config" && log i "Set BIOS directory in xroar.conf"
""")
XROAR_CONF = place((290, """
rompath RETRODECKROMSDIR/coco:RETRODECKROMSDIR/dragon32:RETRODECKROMSDIR/tanodragon:RETRODECKBIOSDIR
romlist dragon=dragon
romlist d64_1=d64_1,d64rom1,"Dragon Data Ltd - Dragon 64 - IC17","Dragon Data Ltd - TANO IC18","Eurohard S.A. - Dragon 200 IC18",dragrom
"""), (297, """
romlist dragon64=@d64_1,@dragon
"""))
def tree(**components: dict[str, str]) -> dict[str, str]:
return {
f"{name}/{path}": text
for name, files in components.items()
for path, text in files.items()
}
def melonds(prepare: str) -> dict[str, str]:
return tree(melonds={
"component_prepare.sh": prepare, "component_functions.sh": MELONDS_FUNCTIONS,
})
def launchers(files: dict[str, str], components: list[str]) -> list[dict]:
return rd.extract_launchers(components, files.get)
RETROARCH = {
"component_prepare.sh": RETROARCH_PREPARE,
"rd_assets/rd_config/retroarch-core-options.cfg": CORE_OPTIONS,
}
class FixedNames(unittest.TestCase):
def test_melonds_gets_seven_paths_in_the_order_written(self):
files = melonds(MELONDS_PREPARE)
fixed = next(r for r in launchers(files, ["melonds"]) if r["kind"] == "fixed_name")
self.assertEqual(fixed["names"], [
"bios9.bin", "bios7.bin", "firmware.bin",
"dsi_nand.bin", "dsi_bios7.bin", "dsi_bios9.bin", "dsi_firmware.bin",
])
self.assertEqual(fixed["search_dir"], "$bios_path")
self.assertTrue(
fixed["passes_to"].startswith("melonDS.toml [DS] BIOS9Path, [DS] BIOS7Path")
)
self.assertEqual(fixed["source_ref"], "melonds/component_prepare.sh:21-23, 25-28")
def test_melonds_reads_bios_through_a_link(self):
files = melonds(MELONDS_PREPARE)
link = next(r for r in launchers(files, ["melonds"]) if r["kind"] == "links")
self.assertEqual(link["passes_to"], "$XDG_CONFIG_HOME/melonDS/bios is a symlink to it")
self.assertEqual(link["source_ref"], "melonds/component_prepare.sh:19")
def test_xemu_writes_three_paths_and_copies_a_blank_disk(self):
files = tree(xemu={
"component_prepare.sh": XEMU_PREPARE, "component_functions.sh": XEMU_FUNCTIONS,
})
found = launchers(files, ["xemu"])
fixed = next(r for r in found if r["kind"] == "fixed_name")
self.assertEqual(fixed["names"], ["mcpx_1.0.bin", "Complex.bin", "xbox_hdd.qcow2"])
self.assertEqual(fixed["source_ref"], "xemu/component_prepare.sh:23-24, 27")
copy = next(r for r in found if r["kind"] == "copies")
self.assertEqual(copy["depends_on_option"], '[ ! -f "$bios_path/xbox_hdd.qcow2" ]')
self.assertEqual(copy["source_ref"], "xemu/component_prepare.sh:32")
def test_pico8_starts_the_binary_the_user_supplies(self):
files = tree(**{"pico-8": {"component_launcher.sh": PICO8_LAUNCHER}})
record = launchers(files, ["pico-8"])[0]
self.assertEqual((record["names"], record["search_dir"]), (["pico8"], "$bios_path/pico-8"))
self.assertIs(record["fails_when_missing"], True)
self.assertEqual(record["source_ref"], "pico-8/component_launcher.sh:3")
def test_rpcs3_firmware_is_downloaded_then_installed(self):
files = tree(rpcs3={"component_functions.sh": RPCS3_FUNCTIONS})
record = launchers(files, ["rpcs3"])[0]
self.assertEqual(record["names"], ["PS3UPDAT.PUP"])
self.assertEqual(record["passes_to"], "component_launcher.sh --installfw")
self.assertIn("2026_0318_a2b60b6ac1d2e49e230144345616927c", record["depends_on_option"])
self.assertEqual(record["source_ref"], "rpcs3/component_functions.sh:11, 19-20")
def test_a_download_never_installed_raises(self):
broken = RPCS3_FUNCTIONS.replace("--installfw", "--run")
with self.assertRaises(rd.LauncherPatternMissing):
launchers(tree(rpcs3={"component_functions.sh": broken}), ["rpcs3"])
class RetroArch(unittest.TestCase):
def test_core_options_that_name_a_file(self):
found = launchers(tree(retroarch=RETROARCH), ["retroarch"])
found = [r for r in found if r["kind"] == "core_option"]
self.assertEqual(
[(r["names"], r["passes_to"]) for r in found],
[
(["scph5500.bin"], "core option duckstation_BIOS.PathNTSCJ"),
(["scph5501.bin"], "core option duckstation_BIOS.PathNTSCU"),
(["scph5502.bin"], "core option duckstation_BIOS.PathPAL"),
(["scph10000.bin"], "core option pcsx2_bios"),
],
)
self.assertEqual(
found[3]["source_ref"],
"retroarch/rd_assets/rd_config/retroarch-core-options.cfg:515, "
"retroarch/component_prepare.sh:24",
)
def test_lrps2_bios_folder_points_back_at_bios(self):
found = launchers(tree(retroarch=RETROARCH), ["retroarch"])
links = [r for r in found if r["kind"] == "links"]
self.assertEqual([(r["search_dir"], r["passes_to"]) for r in links],
[("$bios_path", "$bios_path/pcsx2/bios is a symlink to it")])
self.assertEqual(links[0]["source_ref"], "retroarch/component_prepare.sh:96")
def test_a_preset_never_copied_raises(self):
prepare = RETROARCH_PREPARE.replace("retroarch-core-options.cfg", "other.cfg")
files = tree(retroarch={**RETROARCH, "component_prepare.sh": prepare})
with self.assertRaises(rd.LauncherPatternMissing):
launchers(files, ["retroarch"])
class Links(unittest.TestCase):
def test_dolphin_reads_ipl_from_save_folders(self):
found = launchers(tree(dolphin={"component_prepare.sh": DOLPHIN_PREPARE}), ["dolphin"])
self.assertEqual(
[(r["slot"], r["search_dir"]) for r in found],
[("EUR", "$saves_path/gc/dolphin/EU"), ("USA", "$saves_path/gc/dolphin/US"),
("JAP", "$saves_path/gc/dolphin/JP")],
)
self.assertEqual(found[1]["source_ref"], "dolphin/component_prepare.sh:20")
class Presets(unittest.TestCase):
def files(self, conf: str = XROAR_CONF) -> dict[str, str]:
return tree(xroar={
"component_prepare.sh": XROAR_PREPARE, "rd_assets/rd_config/xroar.conf": conf,
})
def test_xroar_romlists_expand_their_references_in_order(self):
found = launchers(self.files(), ["xroar"])
search = next(r for r in found if r["kind"] == "passes_path")
self.assertEqual(
search["search_dir"],
"$roms_path/coco:$roms_path/dragon32:$roms_path/tanodragon:$bios_path",
)
lists = {r["slot"]: r for r in found if r["kind"] == "names_searched"}
self.assertEqual(lists["dragon64"]["names"][:2], ["d64_1", "d64rom1"])
self.assertEqual(lists["dragon64"]["names"][-1], "dragon")
self.assertEqual(
lists["d64_1"]["source_ref"],
"xroar/rd_assets/rd_config/xroar.conf:292, xroar/component_prepare.sh:13, 15",
)
def test_a_reference_to_an_unknown_romlist_raises(self):
with self.assertRaises(rd.LauncherPatternMissing):
launchers(self.files(XROAR_CONF.replace("@dragon", "@dragonx")), ["xroar"])
def test_a_preset_without_the_placeholder_raises(self):
with self.assertRaises(rd.LauncherPatternMissing):
launchers(self.files(XROAR_CONF.replace("RETRODECKBIOSDIR", "/bios")), ["xroar"])
class UnknownConstructs(unittest.TestCase):
def test_an_unclassified_use_of_bios_path_raises(self):
broken = MELONDS_PREPARE.replace(
'create_dir "$cheats_path/MelonDS"', 'rsync -a "$bios_path/" "$XDG_DATA_HOME/melonDS/"'
)
files = melonds(broken)
with self.assertRaises(rd.LauncherPatternMissing):
launchers(files, ["melonds"])
def test_no_record_at_all_raises(self):
with self.assertRaises(rd.LauncherPatternMissing):
launchers({}, ["azahar"])
class PlatformRecords(unittest.TestCase):
"""The launchers platforms/retrodeck.yml carries, as the scraper wrote them."""
@classmethod
def setUpClass(cls) -> None:
cls.config = yaml.safe_load((REPO_ROOT / "platforms" / "retrodeck.yml").read_text())
def test_every_emulator_is_a_component_retrodeck_ships(self):
named = {r["emulator"] for r in self.config["launchers"]}
self.assertEqual(sorted(named - set(self.config["cores"])), [])
def test_every_reference_is_in_a_component(self):
for record in self.config["launchers"]:
first = record["source_ref"].split("/", 1)[0]
self.assertIn(first, self.config["cores"], record)
if __name__ == "__main__":
unittest.main()