chore: merge the minui family platforms

This commit is contained in:
Abdessamad Derraz committed 2026-10-22 04:09:17 +02:00
commit 841017c887
34 files changed
+13514 -77

No files matched your search

+3
View File
@@ -47,6 +47,9 @@ body:
- EmuELEC
- BizHawk
- MiSTer FPGA
- MinUI
- NextUI
- MyMinUI
- Other
validations:
required: true
+1 -1
View File
@@ -11,7 +11,7 @@
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "03b97db812dcdc161aecedd55f0c8996fababa27f47e3c0c90d52a4f0365565b"
$defaultInstallSha256 = "5c12411406dd1c0542f8051eb5c9e5aaff022de208e4c446bfc81cc03896db8a"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+67
View File
@@ -186,6 +186,7 @@ AVAILABLE_PLATFORMS = (
"retroarch", "batocera", "recalbox", "retrobat", "emudeck",
"lakka", "retrodeck", "rocknix", "romm", "bizhawk", "misterfpga",
"retropie", "amberelec", "emuelec", "knulli",
"minui", "nextui", "myminui",
)
# The names the frontends give themselves, as each manifest's display_name
@@ -198,8 +199,23 @@ PLATFORM_NAMES = {
"retrodeck": "RetroDECK", "rocknix": "ROCKNIX", "romm": "RomM",
"bizhawk": "BizHawk", "misterfpga": "MiSTer FPGA", "retropie": "RetroPie",
"amberelec": "AmberELEC", "emuelec": "EmuELEC", "knulli": "KNULLI",
"minui": "MinUI", "nextui": "NextUI", "myminui": "MyMinUI",
}
# Frontends whose BIOS live on their SD card, installed from a computer the
# card is mounted on. Each release unpacks as .system/ at the card root and
# writes its release name as the first line of .system/version.txt (MinUI
# makefile:22-24 and 152-157, NextUI makefile:33-35 and 249, MyMinUI
# makefile:16-19 and 124). The registry publishes the same rules as each
# manifest's `detect` (card_marker), and a test holds the two equal.
CARD_MARKER = ".system/version.txt"
_CARD_RULES: tuple[tuple[str, str, str], ...] = (
("minui", CARD_MARKER, "MinUI-"),
("nextui", CARD_MARKER, "NextUI-"),
("myminui", CARD_MARKER, "MyMinUI-"),
)
CARD_PLATFORMS = frozenset(name for name, _file, _prefix in _CARD_RULES)
SITE_URL = "https://abdess.github.io/retrobios/"
INSTALLER_PAGE = f"{SITE_URL}wiki/installer/"
TROUBLESHOOTING_PAGE = f"{SITE_URL}wiki/troubleshooting/"
@@ -5298,6 +5314,39 @@ def _no_core_found(plat: str, bios_path: Path, searched: list[Path], dest_given:
sys.exit(1)
def card_platform(root: Path) -> str | None:
"""The frontend whose card ``root`` is, from the release name it wrote."""
for name, marker, prefix in _CARD_RULES:
try:
with open(root / marker, encoding="utf-8", errors="replace") as handle:
first = handle.readline().strip()
except OSError:
continue
if first.startswith(prefix):
return name
return None
def _check_card_root(plat: str, dest: Path) -> None:
"""Refuse a folder inside the card where its root is meant.
The file list writes Bios/<TAG>/ below --dest: given the card's Bios
folder, every file would land in Bios/Bios/<TAG>/, which no core reads.
"""
found = card_platform(dest)
if found is None:
for parent in list(dest.absolute().parents)[:2]:
holder = card_platform(parent)
if holder:
print(f"Error: {dest} is inside a {display_name(holder)} card. "
f"Name the card's root: --dest {parent}", file=sys.stderr)
sys.exit(1)
return
if found != plat:
print(f" Note: this card holds {display_name(found)} ({CARD_MARKER}), and the "
f"{display_name(plat)} file list was asked for.")
def _platform_for_dest(os_type: str, dest: Path, interactive: bool) -> str:
"""The file list for a --dest given without --platform.
@@ -5307,6 +5356,11 @@ def _platform_for_dest(os_type: str, dest: Path, interactive: bool) -> str:
terminal the user picks, Enter taking the one found. Otherwise the one
found is used, RetroArch when none is, and several found are refused.
"""
card = card_platform(dest)
if card:
print(f" File list: {display_name(card)} (the card's {CARD_MARKER}). "
f"Same choice without detection: --platform {card}")
return card
print("Detecting platform...")
found = list(dict.fromkeys(name for name, _path in detect_platforms(os_type)))
default = found[0] if len(found) == 1 else "" if found else "retroarch"
@@ -5368,6 +5422,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
if args.platform:
args.platform = normalize_platform(args.platform)
if args.platform and args.dest:
if args.platform in CARD_PLATFORMS:
_check_card_root(args.platform, args.dest)
platforms = [(args.platform, args.dest)]
elif args.platform:
print("Detecting platform...")
@@ -5380,6 +5436,17 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
# needed, and asking BizHawk's for one refused a question asked
# before downloading BizHawk.
platforms = [(args.platform, _default_dest(os_type, args.platform) or Path.cwd())]
elif args.platform in CARD_PLATFORMS:
# Its files go on its SD card, which is never this machine's
# own folder: the card's mount point is the user's to name.
print(
f" {display_name(args.platform)} keeps its BIOS files on its SD card. "
f"Mount the card and name its root with --dest, for example: "
f"--platform {args.platform} --dest /media/$USER/<card> "
f"(Windows: --dest E:\\)",
file=sys.stderr,
)
sys.exit(1)
elif args.platform in PORTABLE_ONLY:
# A portable emulator reads the folder beside its executable:
# any default would be a folder it never opens.
+1 -1
View File
@@ -18,7 +18,7 @@ esac
TEMP_INSTALLER=""
TEMP_DIRECTORY=""
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
DEFAULT_INSTALL_SHA256="03b97db812dcdc161aecedd55f0c8996fababa27f47e3c0c90d52a4f0365565b"
DEFAULT_INSTALL_SHA256="5c12411406dd1c0542f8051eb5c9e5aaff022de208e4c446bfc81cc03896db8a"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+1145
View File
File diff suppressed because it is too large. Load diff
+5057
View File
File diff suppressed because it is too large. Load diff
+4714
View File
File diff suppressed because it is too large. Load diff
+83
View File
@@ -918,3 +918,86 @@ platforms:
method: path_exists
path: /media/fat/MiSTer
bios_path: /media/fat/games
minui:
config: minui.yml
status: active
# The card root: minarch hands each core SDCARD_PATH/Bios/<TAG>
# (workspace/all/minarch/minarch.c:2943).
extract_to: <SD card>/
logo: https://avatars.githubusercontent.com/u/225589
scraper: minui
# minarch hands each core a folder and names no file (workspace/all/minarch/minarch.c:2943):
# there is no BIOS list of its own to export corrected.
native_list: none
source_url: https://github.com/shauninman/MinUI
source_format: pak_launch_scripts
hash_type: sha1
# The primary card is formatted FAT32 (skeleton/BASE/README.txt:15).
case_insensitive_fs: true
schedule: null
target_scraper: null
target_source: null
install:
detect:
# The release zip holds the build's SYSTEM tree as .system/ at the card
# root with version.txt, whose first line is the release name
# (makefile:22-24, 152-157). The installer runs on the computer the
# card is mounted on and reads it from --dest.
- os: any
method: card_marker
file: .system/version.txt
first_line_prefix: MinUI-
nextui:
config: nextui.yml
status: active
# minarch hands each core SDCARD_PATH/Bios/<TAG>
# (workspace/all/minarch/ma_core.c:76).
extract_to: <SD card>/
logo: https://avatars.githubusercontent.com/u/202386615
scraper: nextui
# minarch hands each core a folder and names no file (workspace/all/minarch/ma_core.c:76):
# there is no BIOS list of its own to export corrected.
native_list: none
source_url: https://github.com/LoveRetro/NextUI
source_format: pak_launch_scripts
hash_type: sha1
# The primary card is formatted FAT32 (skeleton/BASE/README.txt:17).
case_insensitive_fs: true
schedule: null
target_scraper: null
target_source: null
install:
detect:
# makefile:33-35 and 249: version.txt in .system/, first line the
# release name.
- os: any
method: card_marker
file: .system/version.txt
first_line_prefix: NextUI-
myminui:
config: myminui.yml
status: active
# minarch hands each core SDCARD_PATH/Bios/<TAG>
# (workspace/all/minarch/minarch.c:3532).
extract_to: <SD card>/
logo: https://avatars.githubusercontent.com/u/10894566
scraper: myminui
# minarch hands each core a folder and names no file (workspace/all/minarch/minarch.c:3532):
# there is no BIOS list of its own to export corrected.
native_list: none
source_url: https://github.com/Turro75/MyMinUI
source_format: pak_launch_scripts
hash_type: sha1
# The card is formatted FAT32 (MANUAL.md:13).
case_insensitive_fs: true
schedule: null
target_scraper: null
target_source: null
install:
detect:
# makefile:16-19 and 124: version.txt in .system/, first line the
# release name.
- os: any
method: card_marker
file: .system/version.txt
first_line_prefix: MyMinUI-
+354
View File
@@ -0,0 +1,354 @@
platform: MinUI
version: '2025-11-27'
homepage: "https://github.com/shauninman/MinUI"
source: "https://github.com/shauninman/MinUI/tree/dbf89435a5179788418ff3573042cf9c7e75d9e0"
base_destination: ''
hash_type: sha1
verification_mode: existence
system_directory_ref: "workspace/all/minarch/minarch.c:2943"
cores:
- fake08
- fceumm
- gambatte
- gpsp
- mednafen_pce_fast
- mednafen_supafaust
- mednafen_vb
- mgba
- pcsx_rearmed
- picodrive
- pokemini
- race
- snes9x2005_plus
core_system_dirs:
Bios/FC:
cores:
- fceumm
devices:
- gkdpixel
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/SYSTEM/gkdpixel/paks/Emus/FC.pak/launch.sh:3"
Bios/GB:
cores:
- gambatte
devices:
- gkdpixel
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/SYSTEM/gkdpixel/paks/Emus/GB.pak/launch.sh:3"
Bios/GBA:
cores:
- gpsp
devices:
- gkdpixel
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/SYSTEM/gkdpixel/paks/Emus/GBA.pak/launch.sh:3"
Bios/GBC:
cores:
- gambatte
devices:
- gkdpixel
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/SYSTEM/gkdpixel/paks/Emus/GBC.pak/launch.sh:3"
Bios/GG:
cores:
- picodrive
devices:
- gkdpixel
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/EXTRAS/Emus/gkdpixel/GG.pak/launch.sh:3"
Bios/MD:
cores:
- picodrive
devices:
- gkdpixel
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/SYSTEM/gkdpixel/paks/Emus/MD.pak/launch.sh:3"
Bios/MGBA:
cores:
- mgba
devices:
- gkdpixel
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/EXTRAS/Emus/gkdpixel/MGBA.pak/launch.sh:3"
Bios/NGP:
cores:
- race
devices:
- gkdpixel
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/EXTRAS/Emus/gkdpixel/NGP.pak/launch.sh:3"
Bios/NGPC:
cores:
- race
devices:
- gkdpixel
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/EXTRAS/Emus/gkdpixel/NGPC.pak/launch.sh:3"
Bios/P8:
cores:
- fake08
devices:
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/EXTRAS/Emus/m17/P8.pak/launch.sh:3"
Bios/PCE:
cores:
- mednafen_pce_fast
devices:
- gkdpixel
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/EXTRAS/Emus/gkdpixel/PCE.pak/launch.sh:3"
Bios/PKM:
cores:
- pokemini
devices:
- gkdpixel
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/EXTRAS/Emus/gkdpixel/PKM.pak/launch.sh:3"
Bios/PS:
cores:
- pcsx_rearmed
devices:
- gkdpixel
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/SYSTEM/gkdpixel/paks/Emus/PS.pak/launch.sh:3"
Bios/SFC:
cores:
- snes9x2005_plus
devices:
- gkdpixel
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/SYSTEM/gkdpixel/paks/Emus/SFC.pak/launch.sh:3"
Bios/SGB:
cores:
- mgba
devices:
- gkdpixel
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/EXTRAS/Emus/gkdpixel/SGB.pak/launch.sh:3"
Bios/SMS:
cores:
- picodrive
devices:
- gkdpixel
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/EXTRAS/Emus/gkdpixel/SMS.pak/launch.sh:3"
Bios/SUPA:
cores:
- mednafen_supafaust
devices:
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/EXTRAS/Emus/m17/SUPA.pak/launch.sh:3"
Bios/VB:
cores:
- mednafen_vb
devices:
- m17
- magicmini
- miyoomini
- my282
- my355
- rg35xx
- rg35xxplus
- rgb30
- tg5040
- trimuismart
- zero28
source_ref: "skeleton/EXTRAS/Emus/m17/VB.pak/launch.sh:3"
core_repositories:
fake08:
- "https://github.com/jtothebell/fake-08"
fceumm:
- "https://github.com/libretro/libretro-fceumm"
gambatte:
- "https://github.com/libretro/gambatte-libretro"
gpsp:
- "https://github.com/libretro/gpsp"
mednafen_pce_fast:
- "https://github.com/libretro/beetle-pce-fast-libretro"
mednafen_supafaust:
- "https://github.com/libretro/supafaust"
mednafen_vb:
- "https://github.com/libretro/beetle-vb-libretro"
mgba:
- "https://github.com/libretro/mgba"
pcsx_rearmed:
- "https://github.com/libretro/pcsx_rearmed"
picodrive:
- "https://github.com/irixxxx/picodrive"
pokemini:
- "https://github.com/libretro/PokeMini"
race:
- "https://github.com/libretro/race"
snes9x2005_plus:
- "https://github.com/libretro/snes9x2005"
systems: {}
+426
View File
@@ -0,0 +1,426 @@
platform: MyMinUI
version: '2026-08-09'
homepage: "https://github.com/Turro75/MyMinUI"
source: "https://github.com/Turro75/MyMinUI/tree/72ed4f3dd80d96646c11bf9363242565320d9566"
base_destination: ''
hash_type: sha1
verification_mode: existence
system_directory_ref: "workspace/all/minarch/minarch.c:3532"
cores:
- a5200
- dosbox_pure
- fake08
- fbneo
- fceumm
- fuse
- gambatte
- gpsp
- mame2003_plus
- mednafen_ngp
- mednafen_pce_fast
- mednafen_supafaust
- mednafen_vb
- mednafen_wswan
- mgba
- neocd
- pcsx_rearmed
- picodrive
- pokemini
- prboom
- prosystem
- puae2021
- snes9x2005_plus
- stella2014
- tyrquake
core_system_dirs:
Bios/A2600:
cores:
- stella2014
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/A2600.pak/launch.sh:3"
Bios/A5200:
cores:
- a5200
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/A5200.pak/launch.sh:3"
Bios/A7800:
cores:
- prosystem
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/A7800.pak/launch.sh:3"
Bios/DOOM:
cores:
- prboom
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/DOOM.pak/launch.sh:3"
Bios/DOS:
cores:
- dosbox_pure
devices:
- h700
- m21
- miyoomini
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/DOS.pak/launch.sh:3"
Bios/FBN:
cores:
- fbneo
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/FBN.pak/launch.sh:3"
Bios/FC:
cores:
- fceumm
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/FC.pak/launch.sh:3"
Bios/GB:
cores:
- gambatte
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/GB.pak/launch.sh:3"
Bios/GBA:
cores:
- gpsp
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/GBA.pak/launch.sh:3"
Bios/GBC:
cores:
- gambatte
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/GBC.pak/launch.sh:3"
Bios/GG:
cores:
- picodrive
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/GG.pak/launch.sh:3"
Bios/MAME:
cores:
- mame2003_plus
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/MAME.pak/launch.sh:3"
Bios/MD:
cores:
- picodrive
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/MD.pak/launch.sh:3"
Bios/MGBA:
cores:
- mgba
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/MGBA.pak/launch.sh:3"
Bios/NG:
cores:
- fbneo
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/NG.pak/launch.sh:3"
Bios/NGCD:
cores:
- neocd
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/NGCD.pak/launch.sh:3"
Bios/NGP:
cores:
- mednafen_ngp
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/NGP.pak/launch.sh:3"
Bios/NGPC:
cores:
- mednafen_ngp
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/NGPC.pak/launch.sh:3"
Bios/P8:
cores:
- fake08
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/P8.pak/launch.sh:3"
Bios/PCE:
cores:
- mednafen_pce_fast
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/PCE.pak/launch.sh:3"
Bios/PKM:
cores:
- pokemini
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/PKM.pak/launch.sh:3"
Bios/PS:
cores:
- pcsx_rearmed
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/PS.pak/launch.sh:3"
Bios/PUAE:
cores:
- puae2021
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/PUAE.pak/launch.sh:3"
Bios/QUAKE:
cores:
- tyrquake
devices:
- h700
- m21
- miyoomini
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/QUAKE.pak/launch.sh:3"
Bios/SFC:
cores:
- snes9x2005_plus
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/SFC.pak/launch.sh:3"
Bios/SGB:
cores:
- mgba
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/SGB.pak/launch.sh:3"
Bios/SMS:
cores:
- picodrive
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/SMS.pak/launch.sh:3"
Bios/SUPA:
cores:
- mednafen_supafaust
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/SUPA.pak/launch.sh:3"
Bios/VB:
cores:
- mednafen_vb
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/VB.pak/launch.sh:3"
Bios/WS:
cores:
- mednafen_wswan
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/WS.pak/launch.sh:3"
Bios/ZXS:
cores:
- fuse
devices:
- h700
- m21
- miyoomini
- my282
- r36s
- rg35xx
source_ref: "skeleton/EXTRAS/Emus/h700/ZXS.pak/launch.sh:3"
core_repositories:
a5200:
- "https://github.com/libretro/a5200"
dosbox_pure:
- "https://github.com/libretro/dosbox-pure"
fake08:
- "https://github.com/jtothebell/fake-08"
fbneo:
- "https://github.com/libretro/fbneo"
fceumm:
- "https://github.com/libretro/libretro-fceumm"
fuse:
- "https://github.com/libretro/fuse-libretro"
gambatte:
- "https://github.com/libretro/gambatte-libretro"
gpsp:
- "https://github.com/libretro/gpsp"
mame2003_plus:
- "https://github.com/libretro/mame2003-plus-libretro"
mednafen_ngp:
- "https://github.com/libretro/beetle-ngp-libretro"
mednafen_pce_fast:
- "https://github.com/libretro/beetle-pce-fast-libretro"
mednafen_supafaust:
- "https://github.com/libretro/supafaust"
mednafen_vb:
- "https://github.com/libretro/beetle-vb-libretro"
mednafen_wswan:
- "https://github.com/libretro/beetle-wswan-libretro"
mgba:
- "https://github.com/libretro/mgba"
neocd:
- "https://github.com/libretro/neocd_libretro"
pcsx_rearmed:
- "https://github.com/libretro/pcsx_rearmed"
picodrive:
- "https://github.com/irixxxx/picodrive"
pokemini:
- "https://github.com/libretro/PokeMini"
prboom:
- "https://github.com/libretro/libretro-prboom"
prosystem:
- "https://github.com/libretro/prosystem-libretro"
puae2021:
- "https://github.com/libretro/libretro-uae"
snes9x2005_plus:
- "https://github.com/libretro/snes9x2005"
stella2014:
- "https://github.com/libretro/stella2014-libretro"
tyrquake:
- "https://github.com/libretro/tyrquake"
systems: {}
+399
View File
@@ -0,0 +1,399 @@
platform: NextUI
version: '2026-10-20'
homepage: "https://nextui.loveretro.games"
source: "https://github.com/LoveRetro/NextUI/tree/5291344b3aceae5fa197437a70824afb41e2d338"
base_destination: ''
hash_type: sha1
verification_mode: existence
system_directory_ref: "workspace/all/minarch/ma_core.c:76"
cores:
- a5200
- bluemsx
- cap32
- fake08
- fbneo
- fceumm
- gambatte
- gearcoleco
- gpsp
- handy
- mednafen_pce_fast
- mednafen_supafaust
- mednafen_vb
- mgba
- pcsx_rearmed
- picodrive
- pokemini
- prboom
- prosystem
- puae2021
- race
- snes9x
- stella2014
- vice_x128
- vice_x64
- vice_xpet
- vice_xplus4
- vice_xvic
core_system_dirs:
Bios/32X:
cores:
- picodrive
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/32X.pak/launch.sh:3"
Bios/A2600:
cores:
- stella2014
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/A2600.pak/launch.sh:3"
Bios/A5200:
cores:
- a5200
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/A5200.pak/launch.sh:3"
Bios/A7800:
cores:
- prosystem
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/A7800.pak/launch.sh:3"
Bios/C128:
cores:
- vice_x128
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/C128.pak/launch.sh:3"
Bios/C64:
cores:
- vice_x64
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/C64.pak/launch.sh:3"
Bios/COLECO:
cores:
- gearcoleco
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/COLECO.pak/launch.sh:3"
Bios/CPC:
cores:
- cap32
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/CPC.pak/launch.sh:3"
Bios/FBN:
cores:
- fbneo
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/FBN.pak/launch.sh:3"
Bios/FC:
cores:
- fceumm
devices:
- desktop
- h700
- tg5040
- tg5050
source_ref: "skeleton/SYSTEM/desktop/paks/Emus/FC.pak/launch.sh:3"
Bios/FDS:
cores:
- fceumm
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/FDS.pak/launch.sh:3"
Bios/GB:
cores:
- gambatte
devices:
- desktop
- h700
- tg5040
- tg5050
source_ref: "skeleton/SYSTEM/desktop/paks/Emus/GB.pak/launch.sh:3"
Bios/GBA:
cores:
- gpsp
devices:
- desktop
- h700
- tg5040
- tg5050
source_ref: "skeleton/SYSTEM/desktop/paks/Emus/GBA.pak/launch.sh:3"
Bios/GBC:
cores:
- gambatte
devices:
- desktop
- h700
- tg5040
- tg5050
source_ref: "skeleton/SYSTEM/desktop/paks/Emus/GBC.pak/launch.sh:3"
Bios/GG:
cores:
- picodrive
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/GG.pak/launch.sh:3"
Bios/LYNX:
cores:
- handy
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/LYNX.pak/launch.sh:3"
Bios/MD:
cores:
- picodrive
devices:
- desktop
- h700
- tg5040
- tg5050
source_ref: "skeleton/SYSTEM/desktop/paks/Emus/MD.pak/launch.sh:3"
Bios/MGBA:
cores:
- mgba
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/MGBA.pak/launch.sh:3"
Bios/MSX:
cores:
- bluemsx
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/MSX.pak/launch.sh:3"
Bios/NGP:
cores:
- race
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/NGP.pak/launch.sh:3"
Bios/NGPC:
cores:
- race
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/NGPC.pak/launch.sh:3"
Bios/P8:
cores:
- fake08
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/P8.pak/launch.sh:3"
Bios/PCE:
cores:
- mednafen_pce_fast
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/PCE.pak/launch.sh:3"
Bios/PET:
cores:
- vice_xpet
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/PET.pak/launch.sh:3"
Bios/PKM:
cores:
- pokemini
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/PKM.pak/launch.sh:3"
Bios/PLUS4:
cores:
- vice_xplus4
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/PLUS4.pak/launch.sh:3"
Bios/PRBOOM:
cores:
- prboom
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/PRBOOM.pak/launch.sh:7"
Bios/PS:
cores:
- pcsx_rearmed
devices:
- desktop
- h700
- tg5040
- tg5050
source_ref: "skeleton/SYSTEM/desktop/paks/Emus/PS.pak/launch.sh:3"
Bios/PUAE:
cores:
- puae2021
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/PUAE.pak/launch.sh:3"
Bios/SEGACD:
cores:
- picodrive
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/SEGACD.pak/launch.sh:3"
Bios/SFC:
cores:
- snes9x
devices:
- desktop
- h700
- tg5040
- tg5050
source_ref: "skeleton/SYSTEM/desktop/paks/Emus/SFC.pak/launch.sh:3"
Bios/SG1000:
cores:
- picodrive
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/SG1000.pak/launch.sh:3"
Bios/SGB:
cores:
- mgba
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/SGB.pak/launch.sh:3"
Bios/SMS:
cores:
- picodrive
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/SMS.pak/launch.sh:3"
Bios/SUPA:
cores:
- mednafen_supafaust
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/SUPA.pak/launch.sh:3"
Bios/VB:
cores:
- mednafen_vb
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/VB.pak/launch.sh:3"
Bios/VIC:
cores:
- vice_xvic
devices:
- h700
- tg5040
- tg5050
source_ref: "skeleton/EXTRAS/Emus/h700/VIC.pak/launch.sh:3"
core_repositories:
a5200:
- "https://github.com/libretro/a5200"
bluemsx:
- "https://github.com/libretro/blueMSX-libretro"
cap32:
- "https://github.com/libretro/libretro-cap32"
fake08:
- "https://github.com/jtothebell/fake-08"
fbneo:
- "https://github.com/libretro/FBNeo"
fceumm:
- "https://github.com/libretro/libretro-fceumm"
gambatte:
- "https://github.com/libretro/gambatte-libretro"
gearcoleco:
- "https://github.com/drhelius/Gearcoleco"
gpsp:
- "https://github.com/libretro/gpsp"
handy:
- "https://github.com/libretro/libretro-handy"
mednafen_pce_fast:
- "https://github.com/libretro/beetle-pce-fast-libretro"
mednafen_supafaust:
- "https://github.com/libretro/supafaust"
mednafen_vb:
- "https://github.com/libretro/beetle-vb-libretro"
mgba:
- "https://github.com/libretro/mgba"
pcsx_rearmed:
- "https://github.com/libretro/pcsx_rearmed"
picodrive:
- "https://github.com/irixxxx/picodrive"
pokemini:
- "https://github.com/libretro/PokeMini"
prboom:
- "https://github.com/libretro/libretro-prboom"
prosystem:
- "https://github.com/libretro/prosystem-libretro"
puae2021:
- "https://github.com/libretro/libretro-uae"
race:
- "https://github.com/libretro/race"
snes9x:
- "https://github.com/libretro/snes9x"
stella2014:
- "https://github.com/libretro/stella2014-libretro"
vice_x128:
- "https://github.com/libretro/vice-libretro"
vice_x64:
- "https://github.com/libretro/vice-libretro"
vice_xpet:
- "https://github.com/libretro/vice-libretro"
vice_xplus4:
- "https://github.com/libretro/vice-libretro"
vice_xvic:
- "https://github.com/libretro/vice-libretro"
systems: {}
+50
View File
@@ -53,6 +53,56 @@
"uniqueItems": true,
"description": "Emulators of standalone_cores the platform also launches as a libretro core: both layouts get their files"
},
"core_system_dirs": {
"type": "object",
"description": "The libretro system directory the platform hands each core, keyed by that directory relative to the pack root. A platform that declares it gives a core no other directory: the core's files are placed below each directory listed for it, and a core listed nowhere gets none (MinUI hands each launch tag its own Bios/<TAG>/)",
"additionalProperties": {
"type": "object",
"additionalProperties": false,
"required": [
"cores",
"source_ref"
],
"properties": {
"cores": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {
"type": "string"
},
"description": "Libretro cores the platform starts with this directory, by the name of their .so"
},
"devices": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string"
},
"description": "Devices whose build ships a launcher for this directory"
},
"source_ref": {
"type": "string",
"description": "file:line of the launcher that names the core, at the revision `source` pins"
}
}
}
},
"core_repositories": {
"type": "object",
"description": "Repositories each core of core_system_dirs is built from, read from the platform's build recipes",
"additionalProperties": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string"
}
}
},
"system_directory_ref": {
"type": "string",
"description": "file:line, at the revision `source` pins, of the code that sets the system directory core_system_dirs lists"
},
"homepage": {
"type": "string",
"format": "uri",
+72 -2
View File
@@ -1221,6 +1221,12 @@ def resolve_local_file(
"""
sha1 = file_entry.get("sha1")
name = file_entry.get("name", "")
# A platform that hands each core a system directory of its own (MinUI's
# Bios/<TAG>/) writes that folder in front of the core's path. The tail
# the collection indexes is the core's path alone.
system_dir = file_entry.get("system_dir")
if system_dir and dest_hint.startswith(f"{system_dir}/"):
dest_hint = dest_hint[len(system_dir) + 1:]
# A name written with its directory is the destination when no path is
# given: gam4980/E.BIN resolved by its basename took another model's OS
# in verify --emulator and the emulator pack, while the platform pack,
@@ -2106,6 +2112,11 @@ def group_identical_platforms(
# One archive cannot serve a filesystem that folds case and one
# that does not: Lakka keeps bios.col beside BIOS.col.
bool(config.get("case_insensitive_fs")),
# Where each core's files go: the same cores handed other
# folders make another pack.
sorted(
tag for tag in standalone_set(config) if tag.startswith(CORE_DIR_PREFIX)
),
]
fp = hashlib.sha1(f"{fp}|{emulators}".encode()).hexdigest()
if target_cores_cache:
@@ -2252,6 +2263,55 @@ DUAL_MODE_PREFIX = "dual:"
# Marks, in the same set, a whole directory the platform's launcher hands a
# standalone emulator (launchers.handed_directories), as JSON.
HANDED_PREFIX = "handed:"
# Marks, in the same set, a libretro core and the system directory the
# platform hands it (``core_system_dirs``), as JSON.
CORE_DIR_PREFIX = "coredir:"
def core_system_dirs(
emu_name: str, profile: dict, standalone_cores: Iterable[str]
) -> list[str] | None:
"""The system directories a platform hands this libretro core, or None.
None when the platform declares no per-core directory: the core reads
the one system directory every core shares. A platform that declares
them (MinUI hands each launch tag its own Bios/<TAG>/) gives a core it
lists no directory at all, so the list is then empty.
"""
pairs = [
json.loads(tag[len(CORE_DIR_PREFIX):])
for tag in standalone_cores if tag.startswith(CORE_DIR_PREFIX)
]
if not pairs:
return None
names = {emu_name, *(str(c) for c in profile.get("cores") or [])}
return sorted({directory for core, directory in pairs if core in names})
def system_dir_view(profile: dict, directory: str) -> dict:
"""The profile as a core reads it from one handed system directory.
Each file keeps its own path, relative to the system directory, and
carries the directory: destinations put it in front, resolution takes
it off again (resolve_local_file), so the collection is still searched
by the core's own path. A core's archive subfolder moves with it.
"""
view = {
**profile,
"files": [
{**f, "system_dir": directory} if isinstance(f, dict) else f
for f in profile.get("files") or []
],
}
if profile.get("archive_prefix"):
view["archive_prefix"] = f"{directory}/{profile['archive_prefix']}"
return view
def system_dir_path(entry: dict, path: str) -> str:
"""``path`` below the system directory the entry carries, if any."""
directory = entry.get("system_dir")
return f"{directory}/{path}" if directory else path
def standalone_set(config: dict) -> set[str]:
@@ -2263,7 +2323,14 @@ def standalone_set(config: dict) -> set[str]:
"""
names = {str(c) for c in config.get("standalone_cores") or []}
names |= {DUAL_MODE_PREFIX + str(c) for c in config.get("dual_mode_cores") or []}
if names:
# The system directory each core is handed travels the same way, so
# platform_layouts places a core's files below it for every reader.
names |= {
CORE_DIR_PREFIX + json.dumps([str(core), directory])
for directory, entry in (config.get("core_system_dirs") or {}).items()
for core in entry.get("cores") or []
}
if names - {n for n in names if n.startswith(CORE_DIR_PREFIX)}:
from launchers import handed_directories # noqa: PLC0415
names |= {
@@ -2328,7 +2395,10 @@ def platform_layouts(
(what the build reads from the system directory, where it reads it).
"""
if not runs_standalone(emu_name, profile, standalone_cores):
return [(profile, False)]
directories = core_system_dirs(emu_name, profile, standalone_cores)
if directories is None:
return [(profile, False)]
return [(system_dir_view(profile, d), False) for d in directories]
standalone = profile
root = standalone_root_for(emu_name, profile, standalone_cores)
if root:
+3 -2
View File
@@ -41,6 +41,7 @@ from common import (
require_yaml,
resolve_local_file,
platform_layouts,
system_dir_path,
standalone_set,
select_emulator_profiles,
)
@@ -652,7 +653,7 @@ def slot_key(f: dict, standalone: bool = False) -> tuple:
return (
fname,
f.get("archive"),
where or fname,
system_dir_path(f, where or fname),
f.get("system"),
f.get("variant_group"),
f.get("mode", "both"),
@@ -727,7 +728,7 @@ def _cross_reference_profile(
if is_standalone:
effective_path = f.get("standalone_path") or f.get("path") or fname
else:
effective_path = f.get("path") or fname
effective_path = system_dir_path(f, f.get("path") or fname)
slot = slot_key(f, is_standalone)
seen_key = slot
if len(layouts) > 1:
+1
View File
@@ -1157,6 +1157,7 @@ def generate_pack(
standalone_copies=(
_pack_registry.get(platform_name, {}).get("install", {}) or {}
).get("standalone_copies", []),
launch_folders=len(config.get("core_system_dirs") or {}),
)
_write_generated_member(zf, "README.txt", readme_text)
+19 -3
View File
@@ -22,6 +22,7 @@ sys.path.insert(0, os.path.dirname(__file__))
from common import (
compute_composition,
count_catalog_matched,
declared_root,
github_token,
list_registered_platforms,
load_database,
@@ -167,6 +168,19 @@ def extract_targets(platforms_dir: str) -> list[tuple[str, str]]:
return sorted(rows, key=lambda row: row[0].casefold())
def own_root(config: dict) -> str:
"""The folder a pack written from an empty base_destination starts with.
RetroDECK writes bios/ in front of its files, the MinUI family hands its
cores folders below Bios/.
"""
root = declared_root(config)
if root:
return root
tops = {directory.split("/", 1)[0] for directory in config.get("core_system_dirs") or {}}
return tops.pop() if len(tops) == 1 else "bios"
def extract_notes(platforms_dir: str) -> list[str]:
"""A pack whose entries carry their own root extracts above the BIOS folder."""
folders = dict(extract_targets(platforms_dir))
@@ -176,8 +190,8 @@ def extract_notes(platforms_dir: str) -> list[str]:
display = config.get("platform", key)
if config.get("base_destination") == "" and display in folders:
notes.append(
f"The {display} pack already carries its own `bios/` folder, so it"
f" extracts into `{folders[display]}` rather than into the BIOS folder."
f"The {display} pack already carries its own `{own_root(config)}/` folder,"
f" so it extracts into `{folders[display]}` rather than into the BIOS folder."
)
return notes
@@ -470,7 +484,9 @@ def generate_readme(db: dict, platforms_dir: str) -> str:
" a checksum, the fingerprint of a file's contents, which catches a"
" corrupt or unexpected copy. RetroArch, Lakka and RetroPie check"
" that the file is present under its name without reading its"
" contents (`path_is_valid` in RetroArch's `core_info.c`): the"
" contents (`path_is_valid` in RetroArch's `core_info.c`), and"
" MinUI, NextUI and MyMinUI hand each core its folder without"
" opening a file in it: the"
" Coverage table says which applies to you. Independently of that,"
" the collection records five fingerprints per file, and wherever"
" an emulator's code states an expected size or hash, that value is"
+31 -3
View File
@@ -2158,7 +2158,9 @@ def generate_platform_page(
f"| Contributed by | [@{username}](https://github.com/{username}):"
f" {contribution}{pr_link} |"
)
lines.extend(["", *_platform_install_block(name, display)])
detect = ((registry or {}).get(name, {}).get("install") or {}).get("detect") or []
on_card = any(rule.get("method") == "card_marker" for rule in detect)
lines.extend(["", *_platform_install_block(name, display, on_card)])
lines.extend(
[
"",
@@ -3565,9 +3567,35 @@ def _install_block(name: str, profile: dict) -> list[str]:
]
def _platform_install_block(name: str, display: str) -> list[str]:
def _platform_install_block(name: str, display: str, on_card: bool = False) -> list[str]:
"""The platform's pack, installed file by file: what the ZIP holds, with
resume, verification and only what is missing."""
resume, verification and only what is missing.
``on_card``: the files go on the frontend's SD card, installed from the
computer it is mounted on, whose root is named.
"""
if on_card:
return [
'???+ tip "Install with one command"',
"",
f" With the {display} SD card mounted, the installer downloads the files "
"of the pack that are missing or differ from it into `Bios/<TAG>/` on "
"the card, checks each one, and resumes where it stopped. `--dest` "
"names the card's root.",
"",
" Linux, macOS:",
"",
" ```sh",
f" {install_command('sh', f'--platform {name} --dest /path/to/card')}",
" ```",
"",
" Windows (PowerShell, E: being the card):",
"",
" ```powershell",
" " + install_command("powershell", f"--platform {name} --dest E:\\"),
" ```",
"",
]
return [
'???+ tip "Install with one command"',
"",
+81 -42
View File
@@ -15,7 +15,7 @@ from common import load_emulator_profiles
from common import resolution_is_hash_exact
from common import resolve_local_file
from common import resolve_platform_cores
from common import platform_layouts
from common import core_system_dirs, platform_layouts, system_dir_path
from common import standalone_set as platform_standalone_set
from common import sanitize_pack_path
from common import size_fits
@@ -331,7 +331,9 @@ def _extra_dest(u: dict) -> str:
the file's name after it ("cafeLibs/" + "snd_user.rpl").
"""
archive = u.get("archive")
raw = archive if archive else (u.get("path") or u["name"])
# An archive goes at the root of the core's system directory, which a
# platform handing each core its own folder writes in the path.
raw = (u.get("path") or archive) if archive else (u.get("path") or u["name"])
return f"{raw}{u['name']}" if raw.endswith("/") else raw
@@ -479,6 +481,8 @@ def _agnostic_scan_extras(
seed = f.get("path") or ""
if standalone:
seed = f.get("standalone_path") or seed
else:
seed = system_dir_path(f, seed or fname)
folder = seed.rsplit("/", 1)[0] if "/" in seed else ""
dest = f"{folder}/{scan_name}" if folder else scan_name
full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest
@@ -727,6 +731,7 @@ def _archive_prefix_extras(
extras_prefix: str,
by_name: dict,
taken: dict[str, dict] | None = None,
standalone_set: set[str] | None = None,
) -> list[dict]:
"""A second copy of an archive under the subdirectory a core reads.
@@ -744,45 +749,75 @@ def _archive_prefix_extras(
continue
if emu_name not in relevant:
continue
prefix = profile.get("archive_prefix", "")
if not prefix:
# A platform that hands each core its own system directory moves the
# subfolder with it, once per directory.
views = [profile]
if core_system_dirs(emu_name, profile, standalone_set or set()) is not None:
views = [
view for view, standalone
in platform_layouts(emu_name, profile, standalone_set or set())
if not standalone
]
for view in views:
extras.extend(_archive_prefix_copies(
emu_name, view, covered_names, seen_dests, extras_prefix,
by_name, taken,
))
return extras
def _archive_prefix_copies(
emu_name: str,
profile: dict,
covered_names: set,
seen_dests: set,
extras_prefix: str,
by_name: dict,
taken: dict[str, dict] | None,
) -> list[dict]:
"""The prefixed copies of one layout's archives."""
extras: list[dict] = []
prefix = profile.get("archive_prefix", "")
if not prefix:
return extras
profile_archives: dict[str, dict] = {}
for f in profile.get("files", []):
archive = f.get("archive", "")
if archive:
profile_archives.setdefault(archive, f)
for archive_name, archive_entry in sorted(profile_archives.items()):
if archive_name not in covered_names:
continue
profile_archives: dict[str, dict] = {}
for f in profile.get("files", []):
archive = f.get("archive", "")
if archive:
profile_archives.setdefault(archive, f)
for archive_name, archive_entry in sorted(profile_archives.items()):
if archive_name not in covered_names:
continue
dest = f"{prefix}/{archive_name}"
full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest
if full_dest in seen_dests and (taken is None or full_dest not in taken):
continue
if not by_name.get(archive_name):
continue
extra = {
"name": archive_name,
"destination": dest,
"required": True,
"hle_fallback": False,
"source_emulator": profile.get("emulator", emu_name),
"source_profile": emu_name,
# Keyed as the gap report keys an archive: name and path alike.
"source_name": archive_name,
"source_path": archive_name,
"category": archive_entry.get("category", "bios"),
"source_system": archive_entry.get("system"),
"source_systems": entry_system_ids(archive_entry, profile),
"region": archive_entry.get("region"),
"variant_group": archive_entry.get("variant_group"),
}
members = archive_members(archive_name, profile.get("files", []))
if members:
extra["members"] = members
extra["claim_order"] = (3, 0, emu_name)
if _take(extra, full_dest, seen_dests, taken):
extras.append(extra)
dest = f"{prefix}/{archive_name}"
full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest
if full_dest in seen_dests and (taken is None or full_dest not in taken):
continue
if not by_name.get(archive_name):
continue
extra = {
"name": archive_name,
"destination": dest,
"required": True,
"hle_fallback": False,
"source_emulator": profile.get("emulator", emu_name),
"source_profile": emu_name,
# Keyed as the gap report keys an archive: name and path alike.
"source_name": archive_name,
"source_path": system_dir_path(archive_entry, archive_name),
"category": archive_entry.get("category", "bios"),
"source_system": archive_entry.get("system"),
"source_systems": entry_system_ids(archive_entry, profile),
"region": archive_entry.get("region"),
"variant_group": archive_entry.get("variant_group"),
}
members = archive_members(archive_name, profile.get("files", []))
if members:
extra["members"] = members
if archive_entry.get("system_dir"):
extra["system_dir"] = archive_entry["system_dir"]
extra["claim_order"] = (3, 0, emu_name)
if _take(extra, full_dest, seen_dests, taken):
extras.append(extra)
return extras
@@ -1046,7 +1081,7 @@ def _collect_emulator_extras(
if is_standalone:
raw = f.get("standalone_path") or f.get("path") or fname
else:
raw = f.get("path") or fname
raw = system_dir_path(f, f.get("path") or fname)
dest = f"{raw}{fname}" if raw.endswith("/") else raw
# A bare name is a destination too: the core reads it at the root
# of the directory core files go to, which the platform may not
@@ -1087,6 +1122,8 @@ def _collect_emulator_extras(
extra[identity] = f[identity]
if f.get("contents"):
extra["contents"] = f["contents"]
if f.get("system_dir"):
extra["system_dir"] = f["system_dir"]
extra["claim_order"] = (2, 0, emu_name)
if _take(extra, full_dest, seen_dests, taken):
extras.append(extra)
@@ -1094,7 +1131,7 @@ def _collect_emulator_extras(
extras.extend(
_archive_prefix_extras(
profiles, relevant, covered_names, seen_dests, extras_prefix,
by_name, taken,
by_name, taken, standalone_set,
)
)
@@ -1211,6 +1248,8 @@ def _report_extra(u: dict, name: str, dest: str) -> dict:
}
if u.get("members"):
extra["members"] = u["members"]
if u.get("system_dir"):
extra["system_dir"] = u["system_dir"]
# The archive layout the core opens ranks same-named copies by what
# they hold. Without it the first copy indexed won.
if u.get("contents"):
+60 -7
View File
@@ -12,9 +12,46 @@ def _detect_systems(detect: list[dict]) -> list[str]:
"""The operating systems the registry's detection rules cover, in order."""
order = ("linux", "darwin", "windows")
seen = {str(rule.get("os", "")) for rule in detect}
if "any" in seen:
return list(order)
return [name for name in order if name in seen]
def _card_section(platform_name: str, display: str) -> str:
"""The one-line install for a frontend whose files live on its SD card.
The installer runs on the computer the card is mounted on, and the card
root is named: no folder on that computer is one the frontend reads.
"""
return (
" Option A: Automatic (recommended)\n"
" ---------------------------------\n"
" With the SD card mounted on a computer, name its root with --dest.\n"
" Linux and macOS: run this in a terminal:\n\n"
f" curl -fsSL {INSTALL_SH} | sh -s -- --platform {platform_name} --dest /path/to/card\n\n"
" Windows: run this in PowerShell (E: being the card):\n\n"
f" &([scriptblock]::Create((irm {INSTALL_PS1}))) --platform {platform_name} --dest E:\\\n\n"
f" The installer writes each file where {display} hands it to the core\n"
" and checks every file.\n\n"
)
def _card_manual(display: str) -> str:
"""Where the files of a per-tag Bios/ layout go, copied by hand."""
return (
" Option B: Manual\n"
" ----------------\n"
" 1. Put the SD card in your PC\n"
" 2. Extract all files from this archive directly into the root of the\n"
" card: the archive holds the Bios/ folder, with one subfolder per\n"
" launch tag (Bios/PS/, Bios/GBA/, ...)\n"
" 3. Overwrite if asked\n\n"
f" {display} hands each core the folder named after the tag in\n"
" parentheses of its Roms folder (\"Sony PlayStation (PS)\" reads\n"
" Bios/PS/). A Roms folder can be renamed as long as the tag stays.\n\n"
)
def _automatic_section(
display: str, detect: list[dict], standalone_copies: list[dict] | None,
) -> str:
@@ -234,24 +271,40 @@ def _build_readme(
system_filter: list[str] | None = None,
detect: list[dict] | None = None,
standalone_copies: list[dict] | None = None,
launch_folders: int = 0,
) -> str:
"""Build a personalized step-by-step README for each platform pack."""
"""Build a personalized step-by-step README for each platform pack.
``launch_folders`` counts the folders a platform that hands each core
its own system directory gives them, where it names no system.
"""
narrowings = narrowings or []
sep = "=" * 50
scope = (
f"{launch_folders} launch folders" if launch_folders and not num_systems
else f"{num_systems} systems"
)
header = (
f"{sep}\n"
f" RETROBIOS - {platform_display} BIOS Pack\n"
f" {total_files} files for {num_systems} systems\n"
f" {total_files} files for {scope}\n"
f"{sep}\n\n"
)
detect = list(detect or [])
bios_paths = [str(rule.get("bios_path", "")) for rule in detect]
guide = (
"INSTALLATION GUIDE\n\n"
+ _automatic_section(platform_display, detect, standalone_copies)
+ MANUAL_GUIDES.get(platform_name, _generic_manual(bios_paths))
)
if any(rule.get("method") == "card_marker" for rule in detect):
guide = (
"INSTALLATION GUIDE\n\n"
+ _card_section(platform_name, platform_display)
+ _card_manual(platform_display)
)
else:
guide = (
"INSTALLATION GUIDE\n\n"
+ _automatic_section(platform_display, detect, standalone_copies)
+ MANUAL_GUIDES.get(platform_name, _generic_manual(bios_paths))
)
if regions:
region_help = (
+348
View File
@@ -0,0 +1,348 @@
#!/usr/bin/env python3
"""Scraper for MinUI and the frontends built on it (NextUI, MyMinUI).
Source: the frontend's repository at a pinned commit
Format: one emulator pak per launch tag, ``skeleton/SYSTEM/<device>/paks/Emus/
<TAG>.pak/launch.sh`` (base install) or ``skeleton/EXTRAS/Emus/<device>/
<TAG>.pak/launch.sh`` (extras bundle), whose script names the libretro core
in ``EMU_EXE`` and starts it under minarch.
None of the three publishes a BIOS list in code: the frontend hands every
core the folder ``<SD>/Bios/<TAG>/`` as its libretro system directory
(``core.bios_dir``, returned for RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY) and
never opens or hashes a BIOS file itself. What the code does state is which
core each tag runs, and that tag decides the folder. So the platform file
carries no file entry: it maps each folder to the cores launched with it,
and the files are the ones the core profiles document, placed below that
folder.
Paks that start a standalone binary (NDS.pak runs DraStic, P8-NATIVE.pak
the PICO-8 executable) set no ``EMU_EXE`` and are not libretro launches:
they are counted and left out.
"""
from __future__ import annotations
import json
import re
import urllib.parse
from collections.abc import Callable, Mapping
from dataclasses import dataclass
try:
from .base_scraper import (
BaseScraper,
BiosRequirement,
fetch_branch_head,
fetch_bytes,
github_headers,
)
except ImportError:
from base_scraper import (
BaseScraper,
BiosRequirement,
fetch_branch_head,
fetch_bytes,
github_headers,
)
PLATFORM_NAME = "minui"
@dataclass(frozen=True)
class Variant:
"""One frontend of the family: where its code lives and what it is called."""
key: str
name: str
repo: str
branch: str
homepage: str
# The file that sets core.bios_dir.
minarch_source: str
VARIANTS: dict[str, Variant] = {
"minui": Variant(
key="minui",
name="MinUI",
repo="shauninman/MinUI",
branch="main",
homepage="https://github.com/shauninman/MinUI",
minarch_source="workspace/all/minarch/minarch.c",
),
"nextui": Variant(
key="nextui",
name="NextUI",
repo="LoveRetro/NextUI",
branch="main",
homepage="https://nextui.loveretro.games",
minarch_source="workspace/all/minarch/ma_core.c",
),
"myminui": Variant(
key="myminui",
name="MyMinUI",
repo="Turro75/MyMinUI",
branch="main",
homepage="https://github.com/Turro75/MyMinUI",
minarch_source="workspace/all/minarch/minarch.c",
),
}
# The folder minarch hands a core: SDCARD_PATH "/Bios/" and the launch tag.
BIOS_ROOT = "Bios"
_BIOS_DIR = re.compile(
r'sprintf\(\s*\(char\s*\*\)\s*core\.bios_dir\s*,\s*SDCARD_PATH\s*"/Bios/%s"\s*,\s*core\.tag\s*\)'
)
_PAK = re.compile(
r"^skeleton/(?:SYSTEM/(?P<base>[^/]+)/paks/Emus|EXTRAS/Emus/(?P<extra>[^/]+))"
r"/(?P<tag>[^/]+)\.pak/launch\.sh$"
)
_EMU_EXE = re.compile(r"^[ \t]*EMU_EXE=([A-Za-z0-9_.-]+)[ \t]*$", re.MULTILINE)
# Both ways a pak starts its core: minarch on the core's .so directly, or
# MyMinUI's launch_rom.sh, which runs minarch (or RetroArch with
# system_directory set to the same folder) on ${EMU_EXE}_libretro.so.
_STARTS_CORE = re.compile(
r'^[ \t]*(?:nice\s+-?\d+\s+)?minarch\.elf\s+"\$CORES_PATH/\$\{EMU_EXE\}_libretro\.so"'
r"|^[ \t]*launch_rom\.sh\s",
re.MULTILINE,
)
# Each device builds its cores from workspace/<device>/cores/makefile through
# the template in workspace/all/cores/makefile: `<X>_REPO ?=
# https://github.com/libretro/<X>` and the library `<X>_libretro.so` unless
# `<X>_CORE` names another.
_RECIPE = re.compile(r"^workspace/(?P<device>[^/]+)/cores/makefile$")
_CORES_LIST = re.compile(r"^CORES\s*\+?=(.*)$", re.MULTILINE)
_CORE_VAR = re.compile(r"^(\S+?)_(REPO|CORE)\s*\??=\s*(\S+)", re.MULTILINE)
# A library built from a project other than the libretro core of that name:
# MyMinUI builds dosbox-pure into dosbox_libretro.so. The core is the project
# the bytes come from.
BUILT_AS = {
"https://github.com/libretro/dosbox-pure": "dosbox_pure",
}
@dataclass(frozen=True)
class Pak:
"""One emulator pak as its launch script states it."""
tag: str
device: str
core: str
path: str
line: int
def read_pak(path: str, text: str) -> Pak | None:
"""The libretro launch a pak's script makes, or None for a standalone one.
A script that names a core but starts it in a way not read here raises:
the shape of the paks changed and the scrape must say so.
"""
match = _PAK.match(path)
if not match:
return None
exe = _EMU_EXE.search(text)
if not exe:
return None
if not _STARTS_CORE.search(text):
raise ValueError(f"{path}: EMU_EXE={exe.group(1)} but no minarch launch")
line = text.count("\n", 0, exe.start()) + 1
return Pak(
tag=match.group("tag"),
device=match.group("base") or match.group("extra"),
core=exe.group(1),
path=path,
line=line,
)
def normalize_repository(url: str) -> str:
"""A repository URL without the .git suffix or a trailing slash."""
url = url.strip().rstrip("/")
return url[: -len(".git")] if url.endswith(".git") else url
def read_recipes(text: str) -> dict[str, str]:
"""Library file name to source repository, for one device's core makefile."""
text = "\n".join(line.split("#", 1)[0] for line in text.splitlines())
names = [name for match in _CORES_LIST.finditer(text) for name in match.group(1).split()]
values: dict[tuple[str, str], str] = {
(match.group(1), match.group(2)): match.group(3) for match in _CORE_VAR.finditer(text)
}
return {
values.get((name, "CORE"), f"{name}_libretro.so"): normalize_repository(
values.get((name, "REPO"), f"https://github.com/libretro/{name}")
)
for name in names
}
def bios_dir_ref(source: str, text: str) -> str:
"""``file:line`` of the statement that sets the per-tag system directory."""
match = _BIOS_DIR.search(text)
if not match:
raise ValueError(f"{source}: core.bios_dir is no longer SDCARD_PATH/Bios/<tag>")
return f"{source}:{text.count(chr(10), 0, match.start()) + 1}"
def core_identity(library: str, repositories: Mapping[str, set[str]]) -> str:
"""The core a pak's library is, by the project it is built from."""
stem = library.removesuffix("_libretro.so")
built = sorted({BUILT_AS[r] for r in repositories.get(library, set()) if r in BUILT_AS})
if len(built) > 1:
raise ValueError(f"{library} is built from projects of {', '.join(built)}")
return built[0] if built else stem
def core_system_dirs(
paks: list[Pak], repositories: Mapping[str, set[str]] | None = None
) -> dict[str, dict]:
"""Each Bios/<TAG> folder with the cores its paks run and where they ship."""
repositories = repositories or {}
by_dir: dict[str, dict] = {}
for pak in sorted(paks, key=lambda p: (p.tag, p.device, p.path)):
entry = by_dir.setdefault(
f"{BIOS_ROOT}/{pak.tag}",
{"cores": [], "devices": [], "source_ref": f"{pak.path}:{pak.line}"},
)
core = core_identity(f"{pak.core}_libretro.so", repositories)
if core not in entry["cores"]:
entry["cores"].append(core)
if pak.device not in entry["devices"]:
entry["devices"].append(pak.device)
for entry in by_dir.values():
entry["cores"].sort()
entry["devices"].sort()
return dict(sorted(by_dir.items()))
def platform_config(
variant: Variant, revision: str, date: str, files: Mapping[str, str]
) -> dict:
"""The platform file for one frontend, from its sources at ``revision``.
``files`` maps repository paths to their text: every pak launch script,
every device's core makefile and the minarch source that sets the
system directory.
"""
if variant.minarch_source not in files:
raise ValueError(f"{variant.repo}@{revision}: no {variant.minarch_source}")
reference = bios_dir_ref(variant.minarch_source, files[variant.minarch_source])
paks = [pak for path, text in sorted(files.items()) if (pak := read_pak(path, text))]
if not paks:
raise ValueError(f"{variant.repo}@{revision}: no pak starts a libretro core")
repositories: dict[str, set[str]] = {}
for path, text in files.items():
if _RECIPE.match(path):
for library, repository in read_recipes(text).items():
repositories.setdefault(library, set()).add(repository)
dirs = core_system_dirs(paks, repositories)
built_from = {
core_identity(f"{pak.core}_libretro.so", repositories): sorted(
repositories.get(f"{pak.core}_libretro.so", set())
)
for pak in paks
}
return {
"platform": variant.name,
"version": date,
"homepage": variant.homepage,
"source": f"https://github.com/{variant.repo}/tree/{revision}",
# The card root: every core reads below Bios/<TAG>/ on it.
"base_destination": "",
"hash_type": "sha1",
# minarch hands the folder and never reads or hashes a file in it.
"verification_mode": "existence",
"system_directory_ref": reference,
"cores": sorted({core for entry in dirs.values() for core in entry["cores"]}),
"core_system_dirs": dirs,
# Where each core's library is built from, read from the device
# makefiles. A pak may ship a library no makefile builds.
"core_repositories": {core: repos for core, repos in sorted(built_from.items()) if repos},
"systems": {},
}
class Scraper(BaseScraper):
"""Pak scraper for one frontend of the MinUI family."""
lists_files = False
variant: Variant = VARIANTS["minui"]
def __init__(
self,
url: str | None = None,
fetch: Callable[[str, dict[str, str] | None], bytes] | None = None,
) -> None:
self._fetch = fetch or (lambda u, h=None: fetch_bytes(u, headers=h))
self.revision = ""
if url is None:
self.revision = fetch_branch_head(self.variant.repo, self.variant.branch)
url = f"https://github.com/{self.variant.repo}/tree/{self.revision}"
super().__init__(url=url)
self._config: dict | None = None
def _api(self, path: str) -> dict:
url = f"https://api.github.com/repos/{self.variant.repo}/{path}"
try:
return json.loads(self._fetch(url, github_headers()))
except (ConnectionError, json.JSONDecodeError) as exc:
raise RuntimeError(f"cannot read {url}: {exc}") from exc
def _raw(self, path: str) -> str:
url = (
f"https://raw.githubusercontent.com/{self.variant.repo}/{self.revision}/"
f"{urllib.parse.quote(path)}"
)
return self._fetch(url, None).decode("utf-8", errors="replace")
def config(self) -> dict:
"""The platform file, read once."""
if self._config is not None:
return self._config
if not self.revision:
self.revision = self.url.rstrip("/").rsplit("/", 1)[-1]
tree = self._api(f"git/trees/{self.revision}?recursive=1")
if tree.get("truncated"):
raise RuntimeError(f"{self.variant.repo}@{self.revision}: tree listing truncated")
paths = [
item["path"] for item in tree.get("tree", [])
if item.get("type") == "blob"
and (
_PAK.match(item["path"])
or _RECIPE.match(item["path"])
or item["path"] == self.variant.minarch_source
)
]
commit = self._api(f"commits/{self.revision}")
date = commit.get("commit", {}).get("committer", {}).get("date", "")[:10]
if not date:
raise RuntimeError(f"{self.variant.repo}@{self.revision}: commit has no date")
files = {path: self._raw(path) for path in paths}
self._config = platform_config(self.variant, self.revision, date, files)
return self._config
def fetch_requirements(self) -> list[BiosRequirement]:
"""None: the frontend names no BIOS file in code, its cores do."""
self.config()
return []
def validate_format(self, raw_data: str) -> bool:
return bool(self.config().get("core_system_dirs"))
def generate_platform_yaml(self) -> dict:
return self.config()
def main() -> None:
try:
from .base_scraper import scraper_cli
except ImportError:
from base_scraper import scraper_cli
scraper_cli(Scraper, "Scrape the MinUI pak list")
if __name__ == "__main__":
main()
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""Scraper for MyMinUI's pak list: the MinUI scraper on MyMinUI's repository."""
from __future__ import annotations
try:
from . import minui_scraper
except ImportError:
import minui_scraper
PLATFORM_NAME = "myminui"
class Scraper(minui_scraper.Scraper):
"""Pak scraper for MyMinUI."""
variant = minui_scraper.VARIANTS["myminui"]
def main() -> None:
try:
from .base_scraper import scraper_cli
except ImportError:
from base_scraper import scraper_cli
scraper_cli(Scraper, "Scrape the MyMinUI pak list")
if __name__ == "__main__":
main()
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""Scraper for NextUI's pak list: the MinUI scraper on NextUI's repository."""
from __future__ import annotations
try:
from . import minui_scraper
except ImportError:
import minui_scraper
PLATFORM_NAME = "nextui"
class Scraper(minui_scraper.Scraper):
"""Pak scraper for NextUI."""
variant = minui_scraper.VARIANTS["nextui"]
def main() -> None:
try:
from .base_scraper import scraper_cli
except ImportError:
from base_scraper import scraper_cli
scraper_cli(Scraper, "Scrape the NextUI pak list")
if __name__ == "__main__":
main()
+8 -3
View File
@@ -35,6 +35,7 @@ from common import (
resolve_platform_cores,
platform_layouts,
standalone_set as platform_standalone_set,
system_dir_path,
variant_base_name,
)
from packresolve import preferred_entries
@@ -227,7 +228,7 @@ def profile_claims(
dest = (
(entry.get("standalone_path") or entry.get("path"))
if is_standalone
else entry.get("path")
else system_dir_path(entry, entry.get("path") or entry.get("name") or "")
) or entry.get("name") or ""
if not dest:
continue
@@ -299,8 +300,12 @@ def _archive_claims(
members = entry.get("members")
if not members:
continue
system = next(f.get("system") for f in files if f.get("archive") == archive)
for dest in (archive, f"{prefix}/{archive}") if prefix else (archive,):
member = next(f for f in files if f.get("archive") == archive)
system = member.get("system")
if member.get("system_dir"):
entry = {**entry, "system_dir": member["system_dir"]}
root = system_dir_path(entry, archive)
for dest in (root, f"{prefix}/{archive}") if prefix else (root,):
if placement is None:
full = f"{base_dest}/{dest}" if base_dest else dest
else:
+8 -2
View File
@@ -76,6 +76,7 @@ from common import (
entry_system_ids,
sanitize_pack_path,
select_emulator_profiles,
system_dir_path,
)
yaml = require_yaml()
@@ -576,7 +577,7 @@ def find_undeclared_files(
if is_standalone:
dest = f.get("standalone_path") or f.get("path") or fname
else:
dest = f.get("path") or fname
dest = system_dir_path(f, f.get("path") or fname)
raw_regions = f.get("region") or []
region_key = tuple(
str(value) for value in (
@@ -615,6 +616,7 @@ def find_undeclared_files(
if archive:
archive_key = (
archive,
f.get("system_dir"),
f.get("system"),
f.get("variant_group"),
region_key,
@@ -634,7 +636,7 @@ def find_undeclared_files(
"variant_group": f.get("variant_group"),
"name": archive,
"archive": archive,
"path": archive,
"path": system_dir_path(f, archive),
"required": False,
"hle_fallback": False,
"category": f.get("category", "bios"),
@@ -647,6 +649,8 @@ def find_undeclared_files(
"archive_required_count": 0,
"members": archive_members(archive, profile.get("files", [])),
}
if f.get("system_dir"):
archive_entries[archive_key]["system_dir"] = f["system_dir"]
entry = archive_entries[archive_key]
entry["archive_file_count"] += 1
if f.get("required", False):
@@ -738,6 +742,8 @@ def find_undeclared_files(
}
if f.get("contents"):
entry["contents"] = f["contents"]
if f.get("system_dir"):
entry["system_dir"] = f["system_dir"]
members = with_archive_members(f, profile.get("files", [])).get("members")
if members:
entry["members"] = members
+24 -1
View File
@@ -56,6 +56,17 @@ from exporter.baseline import ( # noqa: E402
UPSTREAM_CACHE = REPO_ROOT / ".cache" / "upstream-native"
def _without_native_list() -> set[str]:
"""Registered platforms whose code names no BIOS file to hand back."""
import yaml # noqa: PLC0415
registry = yaml.safe_load((REPO_ROOT / "platforms" / "_registry.yml").read_text())
return {
name for name, entry in registry["platforms"].items()
if entry.get("native_list") == "none"
}
TRUTH = {
"systems": {
"sony-playstation": {
@@ -552,7 +563,19 @@ class ExporterRegistry(unittest.TestCase):
str(REPO_ROOT / "platforms"), include_archived=True
)
)
self.assertEqual(registered - set(exporters), set())
self.assertEqual(registered - set(exporters) - _without_native_list(), set())
def test_a_platform_without_a_native_list_declares_no_file(self):
"""`native_list: none` says the platform's code names no BIOS file
(MinUI hands each core a folder and opens nothing in it). A platform
whose file declares one has a list to hand back."""
for name in _without_native_list():
config = load_platform_config(name, str(REPO_ROOT / "platforms"))
with self.subTest(platform=name):
self.assertNotIn(name, discover_exporters())
self.assertFalse(any(
system.get("files") for system in (config.get("systems") or {}).values()
))
def test_no_exporter_is_registered_for_an_unregistered_platform(self):
"""A dead exporter is one nobody will notice has stopped working."""
+415
View File
@@ -0,0 +1,415 @@
"""MinUI, NextUI and MyMinUI hand each core a system directory of its own.
minarch sets ``core.bios_dir`` to SDCARD_PATH "/Bios/<TAG>", the tag being
the pak's folder name and the one in parentheses of the Roms folder, and
returns it for RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY. A core run from
GB.pak reads gb_bios.bin from Bios/GB/, the same core run from GBC.pak from
Bios/GBC/. The frontends name no BIOS file, so the platform file maps each
folder to the cores its paks start, and the files come from the core
profiles, placed below that folder.
"""
from __future__ import annotations
import hashlib
import json
import os
import shutil
import sys
import tempfile
import unittest
import zipfile
import zlib
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
sys.path.insert(0, str(REPO_ROOT))
from common import load_emulator_profiles, load_platform_config # noqa: E402
from scraper import minui_scraper # noqa: E402
MINARCH = (
"void Core_open(const char* core_path, const char* tag_name) {\n"
'\tsprintf((char*)core.saves_dir, SDCARD_PATH "/Saves/%s", core.tag);\n'
'\tsprintf((char*)core.bios_dir, SDCARD_PATH "/Bios/%s", core.tag);\n'
"}\n"
)
BASE_PAK = (
"#!/bin/sh\n\n"
"EMU_EXE=gambatte\n\n"
"###############################\n\n"
'EMU_TAG=$(basename "$(dirname "$0")" .pak)\n'
'mkdir -p "$BIOS_PATH/$EMU_TAG"\n'
'nice -20 minarch.elf "$CORES_PATH/${EMU_EXE}_libretro.so" "$ROM" &> "$LOGS_PATH/$EMU_TAG.txt"\n'
)
# MyMinUI's shape: the pak hands its folder to launch_rom.sh.
EXTRA_PAK = (
"#!/bin/sh\n\n"
"EMU_EXE=dosbox\n\n"
"RUN=minarch\nRUN2=minarch\n"
'CURDIR="$(dirname "$0")"\n'
'launch_rom.sh "$1" "$2" "$3" "$4" ${RUN} ${RUN2} "$CURDIR" "$EMU_EXE" $CPU_OC\n'
)
STANDALONE_PAK = (
"#!/bin/sh\n"
'if [ -f /mnt/SDCARD/Bios/NDS/drastic ]; then\n'
' /mnt/SDCARD/Bios/NDS/drastic "$1"\n'
"fi\n"
)
RECIPE = (
"CORES = gambatte dosbox # mesen\n"
"dosbox_REPO = https://github.com/libretro/dosbox-pure.git\n"
"dosbox_CORE = dosbox_libretro.so\n"
)
FIXTURE = {
"workspace/all/minarch/minarch.c": MINARCH,
"skeleton/SYSTEM/miyoomini/paks/Emus/GB.pak/launch.sh": BASE_PAK,
"skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/launch.sh": BASE_PAK,
"skeleton/EXTRAS/Emus/tg5040/DOS.pak/launch.sh": EXTRA_PAK,
"skeleton/EXTRAS/Emus/miyoomini/NDS.pak/launch.sh": STANDALONE_PAK,
"workspace/tg5040/cores/makefile": RECIPE,
}
class ScraperReadsThePaks(unittest.TestCase):
"""Two paks, one per bundle, and a standalone one left out."""
def config(self, files: dict[str, str] | None = None) -> dict:
return minui_scraper.platform_config(
minui_scraper.VARIANTS["minui"], "f" * 40, "2026-01-01", files or FIXTURE
)
def test_each_tag_is_a_folder_below_bios(self):
dirs = self.config()["core_system_dirs"]
self.assertEqual(sorted(dirs), ["Bios/DOS", "Bios/GB"])
self.assertEqual(dirs["Bios/GB"]["cores"], ["gambatte"])
self.assertEqual(dirs["Bios/GB"]["devices"], ["miyoomini", "tg5040"])
self.assertEqual(
dirs["Bios/GB"]["source_ref"],
"skeleton/SYSTEM/miyoomini/paks/Emus/GB.pak/launch.sh:3",
)
def test_the_core_is_the_project_the_library_is_built_from(self):
"""MyMinUI builds dosbox-pure into dosbox_libretro.so: the pak's
EMU_EXE=dosbox is DOSBox Pure, whose profile reads other files than
the dosbox core's."""
config = self.config()
self.assertEqual(config["core_system_dirs"]["Bios/DOS"]["cores"], ["dosbox_pure"])
self.assertEqual(
config["core_repositories"]["dosbox_pure"],
["https://github.com/libretro/dosbox-pure"],
)
self.assertEqual(config["cores"], ["dosbox_pure", "gambatte"])
def test_the_folder_rule_is_read_from_minarch(self):
config = self.config()
self.assertEqual(config["system_directory_ref"], "workspace/all/minarch/minarch.c:3")
self.assertEqual(config["base_destination"], "")
self.assertEqual(config["verification_mode"], "existence")
def test_a_standalone_pak_is_not_a_core(self):
dirs = self.config()["core_system_dirs"]
self.assertNotIn("Bios/NDS", dirs)
def test_a_named_core_started_another_way_raises(self):
files = {**FIXTURE, "skeleton/SYSTEM/miyoomini/paks/Emus/GB.pak/launch.sh":
"EMU_EXE=gambatte\nretroarch -L core.so\n"}
with self.assertRaises(ValueError):
self.config(files)
def test_a_moved_system_directory_raises(self):
files = {**FIXTURE, "workspace/all/minarch/minarch.c":
'sprintf((char*)core.bios_dir, SDCARD_PATH "/BIOS");\n'}
with self.assertRaises(ValueError):
self.config(files)
def test_the_scraper_reads_the_pinned_revision(self):
"""Tree, commit and file reads all name the same commit."""
revision = "a" * 40
asked: list[str] = []
def fetch(url: str, _headers: dict[str, str] | None = None) -> bytes:
asked.append(url)
if url.endswith(f"git/trees/{revision}?recursive=1"):
tree = [{"path": p, "type": "blob"} for p in FIXTURE]
tree.append({"path": "skeleton/BASE/README.txt", "type": "blob"})
return json.dumps({"tree": tree, "truncated": False}).encode()
if url.endswith(f"commits/{revision}"):
return json.dumps(
{"commit": {"committer": {"date": "2026-02-03T04:05:06Z"}}}
).encode()
prefix = f"https://raw.githubusercontent.com/shauninman/MinUI/{revision}/"
self.assertTrue(url.startswith(prefix), url)
return FIXTURE[url[len(prefix):]].encode()
scraper = minui_scraper.Scraper(
url=f"https://github.com/shauninman/MinUI/tree/{revision}", fetch=fetch
)
config = scraper.generate_platform_yaml()
self.assertEqual(config["version"], "2026-02-03")
self.assertEqual(config["source"], f"https://github.com/shauninman/MinUI/tree/{revision}")
self.assertNotIn(
f"https://raw.githubusercontent.com/shauninman/MinUI/{revision}/skeleton/BASE/README.txt",
asked,
)
def _crc(data: bytes) -> str:
return format(zlib.crc32(data) & 0xFFFFFFFF, "08x")
class PackPlacesCoreFilesUnderTheTag(unittest.TestCase):
"""The pack, the report and the manifest put a core's files in each
folder the platform hands it, and nowhere else."""
def setUp(self):
from common import _emulator_profiles_cache
_emulator_profiles_cache.clear()
self.root = tempfile.mkdtemp(dir=REPO_ROOT / "tmp" / "tests")
self.bios = os.path.join(self.root, "bios")
self.platforms = os.path.join(self.root, "platforms")
self.emulators = os.path.join(self.root, "emulators")
for folder in (self.bios, self.platforms, self.emulators):
os.makedirs(folder)
self.records: dict[str, dict] = {}
self._file("Nintendo/Game Boy/gb_bios.bin", b"DMG BOOT ROM")
# Two palettes of one name: the core's path, below its folder,
# must still choose its own.
self._file("Other/gambatte/palettes/default.pal", b"GAMBATTE PALETTE")
self._file("Other/elsewhere/default.pal", b"ANOTHER PALETTE")
self.member = b"ARCADE BIOS ROM"
buffer = os.path.join(self.root, "arc.zip")
with zipfile.ZipFile(buffer, "w") as archive:
archive.writestr("rom_a.bin", self.member)
self._file("Arcade/arc.zip", Path(buffer).read_bytes())
self._file("Other/unlisted/unlisted.bin", b"NOT HANDED")
self.db = self._database()
platform = {
"platform": "MinUI Test",
"base_destination": "",
"verification_mode": "existence",
"cores": ["gambatte", "arcadecore", "unlisted"],
"core_system_dirs": {
"Bios/GB": {"cores": ["gambatte"], "source_ref": "GB.pak/launch.sh:3"},
"Bios/GBC": {"cores": ["gambatte"], "source_ref": "GBC.pak/launch.sh:3"},
"Bios/ARC": {"cores": ["arcade_lib"], "source_ref": "ARC.pak/launch.sh:3"},
},
"systems": {},
}
Path(self.platforms, "test_minui.yml").write_text(yaml.safe_dump(platform))
profiles = {
"gambatte": {
"emulator": "Gambatte", "type": "libretro", "systems": ["nintendo-gb"],
"files": [
{"name": "gb_bios.bin", "required": False},
{"name": "default.pal", "path": "palettes/default.pal", "required": False},
],
},
"arcadecore": {
"emulator": "Arcade", "type": "libretro", "systems": ["arcade"],
"cores": ["arcade_lib"], "archive_prefix": "arcadecore",
"files": [
{"name": "rom_a.bin", "archive": "arc.zip", "required": True,
"crc32": _crc(self.member)},
],
},
"unlisted": {
"emulator": "Unlisted", "type": "libretro", "systems": ["other"],
"files": [{"name": "unlisted.bin", "required": True}],
},
}
for key, profile in profiles.items():
Path(self.emulators, f"{key}.yml").write_text(yaml.safe_dump(profile))
def tearDown(self):
shutil.rmtree(self.root)
def _file(self, relative: str, data: bytes) -> None:
path = os.path.join(self.bios, relative)
os.makedirs(os.path.dirname(path), exist_ok=True)
Path(path).write_bytes(data)
self.records[relative] = {
"path": path,
"name": os.path.basename(relative),
"sha1": hashlib.sha1(data).hexdigest(),
"md5": hashlib.md5(data).hexdigest(),
"crc32": _crc(data),
"size": len(data),
}
def _database(self) -> dict:
files, by_name, by_md5, by_crc32, by_suffix = {}, {}, {}, {}, {}
for relative, record in self.records.items():
sha1 = record["sha1"]
files[sha1] = {k: v for k, v in record.items() if k != "sha1"}
by_name.setdefault(record["name"], []).append(sha1)
by_md5[record["md5"]] = sha1
by_crc32[record["crc32"]] = sha1
parts = relative.split("/")
for start in range(len(parts) - 1):
by_suffix.setdefault("/".join(parts[start:]), []).append(sha1)
return {"files": files, "indexes": {
"by_name": by_name, "by_md5": by_md5, "by_crc32": by_crc32,
"by_path_suffix": by_suffix,
}}
def _pack(self) -> dict[str, bytes]:
from generate_pack import generate_pack
output = os.path.join(self.root, "dist")
os.makedirs(output)
zip_path = generate_pack(
"test_minui", self.platforms, self.db, self.bios, output,
emulators_dir=self.emulators, offline=True,
)
self.assertIsNotNone(zip_path)
with zipfile.ZipFile(zip_path) as archive:
return {
name: archive.read(name) for name in archive.namelist()
if name not in ("README.txt", "manifest.json")
}
def test_a_core_file_lands_in_every_folder_of_the_core(self):
packed = self._pack()
self.assertEqual(packed["Bios/GB/gb_bios.bin"], b"DMG BOOT ROM")
self.assertEqual(packed["Bios/GBC/gb_bios.bin"], b"DMG BOOT ROM")
def test_the_core_path_still_chooses_the_copy(self):
packed = self._pack()
self.assertEqual(packed["Bios/GB/palettes/default.pal"], b"GAMBATTE PALETTE")
def test_an_archive_and_its_core_subfolder_move_with_the_folder(self):
packed = self._pack()
self.assertIn("Bios/ARC/arc.zip", packed)
self.assertIn("Bios/ARC/arcadecore/arc.zip", packed)
def test_nothing_outside_a_handed_folder(self):
"""A core no pak starts gets no folder, so none of its files."""
packed = self._pack()
outside = [
name for name in packed
if "/".join(name.split("/")[:2]) not in ("Bios/GB", "Bios/GBC", "Bios/ARC")
]
self.assertEqual(outside, [])
def test_the_same_cores_in_other_folders_make_another_pack(self):
from common import group_identical_platforms
twin = yaml.safe_load(Path(self.platforms, "test_minui.yml").read_text())
twin["core_system_dirs"]["Bios/GB"]["cores"] = ["arcade_lib"]
Path(self.platforms, "test_twin.yml").write_text(yaml.safe_dump(twin))
groups = group_identical_platforms(["test_minui", "test_twin"], self.platforms)
self.assertEqual(len(groups), 2)
def test_the_report_and_the_manifest_agree_with_the_pack(self):
from generate_pack import generate_manifest
from verify import verify_platform
packed = set(self._pack())
config = load_platform_config("test_minui", self.platforms)
profiles = load_emulator_profiles(self.emulators)
report = verify_platform(config, self.db, self.emulators, profiles)
self.assertEqual(set(report["pack_destinations"]), packed)
manifest = generate_manifest(
"test_minui", self.platforms, self.db, self.bios,
os.path.join(self.platforms, "_registry.yml"), emulators_dir=self.emulators,
emu_profiles=profiles, offline=True,
)
self.assertEqual({entry["dest"] for entry in manifest["files"]}, packed)
class PublishedPlatformsHoldTheRule(unittest.TestCase):
"""The committed files of the family, read as the tools read them."""
FAMILY = ("minui", "nextui", "myminui")
def test_every_core_resolves_to_a_profile_of_its_project(self):
"""A core named in core_system_dirs is the profile built from the
repository the platform's recipe names: a library renamed from
another project (dosbox-pure as dosbox) would hand the wrong
profile's files to the folder."""
from common import resolve_platform_cores
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
def norm(url: str) -> str:
url = str(url).strip().rstrip("/").lower()
url = url.split("/tree/", 1)[0]
return url[: -len(".git")] if url.endswith(".git") else url
for platform in self.FAMILY:
config = load_platform_config(platform, str(REPO_ROOT / "platforms"))
resolved = resolve_platform_cores(config, profiles)
for core, repositories in config["core_repositories"].items():
matches = [
key for key in resolved
if (key == core or core in [str(c) for c in profiles[key].get("cores") or []])
and profiles[key].get("type") not in ("alias", "launcher")
]
with self.subTest(platform=platform, core=core):
self.assertTrue(matches, f"{core} resolves to no profile")
for key in matches:
declared = []
for field in ("source", "upstream"):
value = profiles[key].get(field)
declared += list(value.values()) if isinstance(value, dict) else [value]
self.assertTrue(
{norm(r) for r in repositories} & {norm(d) for d in declared if d},
f"{key} documents {declared}, {platform} builds {repositories}",
)
def test_the_registry_detects_the_card_the_installer_reads(self):
import install
registry = yaml.safe_load((REPO_ROOT / "platforms" / "_registry.yml").read_text())
rules = {
(name, rule["file"], rule["first_line_prefix"])
for name, entry in registry["platforms"].items()
for rule in (entry.get("install") or {}).get("detect", [])
if rule["method"] == "card_marker"
}
self.assertEqual(set(install._CARD_RULES), rules)
self.assertEqual({name for name, _f, _p in rules}, set(self.FAMILY))
class InstallerReadsTheCard(unittest.TestCase):
"""install.py runs on the computer the card is mounted on."""
def setUp(self):
self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp" / "tests"))
(self.root / ".system").mkdir()
def tearDown(self):
shutil.rmtree(self.root)
def test_the_release_name_names_the_frontend(self):
import install
for first_line, expected in (
("MinUI-20251127-0", "minui"),
("MyMinUI-20260809b-0", "myminui"),
("NextUI-20261020-1", "nextui"),
("Onion-4.3", None),
):
(self.root / ".system" / "version.txt").write_text(f"{first_line}\nabc123\n")
with self.subTest(first_line=first_line):
self.assertEqual(install.card_platform(self.root), expected)
def test_a_folder_inside_the_card_is_refused(self):
"""Bios/ given for the card would install into Bios/Bios/<TAG>/."""
import install
(self.root / ".system" / "version.txt").write_text("MinUI-20251127-0\n")
(self.root / "Bios").mkdir()
with self.assertRaises(SystemExit):
install._check_card_root("minui", self.root / "Bios")
install._check_card_root("minui", self.root)
if __name__ == "__main__":
unittest.main()
+28 -2
View File
@@ -151,7 +151,8 @@ platforms:
source_url: https://... # upstream data URL
source_format: json # json, xml, clrmamepro_dat, python_dict,
# bash_script+csv, csharp_firmware_database,
# github_component_manifests, mister_downloader_db
# github_component_manifests, mister_downloader_db,
# pak_launch_scripts
hash_type: md5 # primary hash in the upstream data
cores: # which emulator profiles apply
- core_a
@@ -194,6 +195,17 @@ profile's layout.
and `hatari`): both layouts are packed. A profile without a standalone build keeps
its libretro layout whatever the platform calls it.
A platform that hands each libretro core a system directory of its own declares
them in `core_system_dirs`, keyed by the directory relative to the pack root,
each with the `cores` started with it and the `source_ref` of the launcher that
names them. MinUI, NextUI and MyMinUI give every launch tag `Bios/<TAG>/`
(`system_directory_ref` cites the minarch statement). A core's files are then
placed below each directory listed for it, an archive's core subfolder
(`archive_prefix`) moves with it, and a core listed nowhere gets no file: the
platform hands it no folder. `platform_layouts` builds one view per directory,
so the builder, the manifest, verify and slots read the same placement, and the
resolver still looks the collection up by the core's own path.
### Optional fields
```yaml
@@ -205,6 +217,8 @@ its libretro layout whatever the platform calls it.
zip_digest: composite # the frontend hashes a .zip by its members only
firmware_folder_flat: true # the frontend lists each system folder without
# descending into it
native_list: none # the platform's code names no BIOS file, so no
# exporter hands one back
source_wiki: https://... # human-readable upstream reference
target_scraper: myplatform_targets # hardware target scraper name, null if static
target_source: https://... # target data source URL
@@ -220,6 +234,18 @@ its libretro layout whatever the platform calls it.
parse_key: bios_directory
```
A frontend whose BIOS live on its SD card is installed from the computer the
card is mounted on. Its rule is `os: any`, `method: card_marker`, with `file`
(a path below the card root) and `first_line_prefix`: `install.py` reads that
file under `--dest` and names the platform when its first line starts with the
prefix. MinUI, NextUI and MyMinUI write their release name to
`.system/version.txt`.
`native_list: none` marks a platform whose own code publishes no BIOS list
(the MinUI family hands each core a folder and names no file in it). The test
that every registered platform has an exporter exempts it, and a second test
holds that its platform YAML declares no file.
`zip_digest: composite` is set where the frontend's own check takes
`Zip::Md5Composite` for every `.zip` and never the container's MD5 (Recalbox,
`Bios.cpp:64-68`). Each `.zip` entry is then checked against its composite alone,
@@ -413,7 +439,7 @@ to use:
| Mode | Behavior | Example platforms |
|------|----------|-------------------|
| `existence` | File must exist, no hash check | RetroArch, Lakka, RetroPie |
| `existence` | File must exist, no hash check | RetroArch, Lakka, RetroPie, MinUI, NextUI, MyMinUI |
| `md5` | MD5 must match the declared hash | Batocera, Recalbox, RetroBat, EmuDeck, RetroDECK, RomM, ROCKNIX, KNULLI, AmberELEC, EmuELEC, MiSTer FPGA |
| `sha1` | SHA1 must match | BizHawk |
+1 -1
View File
@@ -221,7 +221,7 @@ that the file exists.
`untested` means the file exists on disk but its hash does not match the expected value. This happens on MD5 and SHA1 platforms (Batocera, Recalbox, BizHawk, ROCKNIX, MiSTer FPGA, and the rest) when the file is present but contains different data than what the platform declares.
On existence-mode platforms (RetroArch, Lakka, RetroPie), files are never `untested` because the platform only checks presence, not content. Those files show as `ok` if present, whatever they contain.
On existence-mode platforms (RetroArch, Lakka, RetroPie, MinUI, NextUI, MyMinUI), files are never `untested` because the platform only checks presence, not content. Those files show as `ok` if present, whatever they contain.
Running `verify.py --emulator <core> --verbose` shows the emulator-level ground truth, which can confirm whether the file's hash matches what the source code expects. The platform report applies the same check on its own: `verify.py --platform retroarch` prints a `DISCREPANCY` line for every file the platform accepts and a profiled emulator rejects.
+30 -3
View File
@@ -225,6 +225,33 @@ Files go under the per-core subdirectory the MiSTer BIOS database declares
lists with a download URL are in scope: the rest ship with the MiSTer
distribution and install themselves.
### MinUI, NextUI and MyMinUI
```
<SD card>/Bios/<TAG>/
```
These frontends give each core a folder of its own, named after the tag in
parentheses of the Roms folder the game sits in: a game in
`Roms/Sony PlayStation (PS)/` runs with `Bios/PS/` as its system directory, one
in `Roms/Game Boy (GB)/` with `Bios/GB/` (MinUI
`workspace/all/minarch/minarch.c:2943`, NextUI
`workspace/all/minarch/ma_core.c:76`, MyMinUI
`workspace/all/minarch/minarch.c:3532`). The pack holds the `Bios/` folder:
extract it at the root of the card. A core started from two tags (Gambatte for
GB and GBC) gets its files in both folders.
The installer runs on the computer the card is mounted on, with the card's root
as `--dest`:
```bash
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --platform minui --dest /media/$USER/CARD
```
Without `--platform`, it reads the release name the frontend writes in
`.system/version.txt` on the card. A pak that starts a standalone program
instead of a libretro core (DraStic for NDS, native PICO-8) is not covered.
### BizHawk
```
@@ -262,9 +289,9 @@ dump shows up.
The installer's `--check` hashes every file against the list on every
platform, so it catches a wrong revision even where the frontend would not:
RetroArch, Lakka and RetroPie only check that a file exists, while Batocera,
RetroBat, Recalbox, EmuDeck, RetroDECK, RomM, ROCKNIX, KNULLI, AmberELEC,
EmuELEC and MiSTer FPGA compare
RetroArch, Lakka, RetroPie, MinUI, NextUI and MyMinUI only check that a file
exists, while Batocera, RetroBat, Recalbox, EmuDeck, RetroDECK, RomM, ROCKNIX,
KNULLI, AmberELEC, EmuELEC and MiSTer FPGA compare
MD5 and BizHawk compares SHA1.
For a single system, or the files a single emulator core reads (`--list-systems`
+10 -1
View File
@@ -356,6 +356,11 @@ not already found that way.
| `%USERPROFILE%\BizHawk\Firmware` | BizHawk | the same |
| `EmuHawk.exe` or `EmuHawkMono.sh` in a `BizHawk*` folder of the home folder or one level down (`~/Games/BizHawk-2.11.1`), the newest | BizHawk | `Firmware` beside it |
A `--dest` given without `--platform` is first read as an SD card: the MinUI
family writes the release name as the first line of `.system/version.txt` at
the card root (`MinUI-`, `NextUI-`, `MyMinUI-`), and that line names the file
list.
Desktop installs are read from their own configuration, and several can be
found on one machine.
@@ -431,7 +436,11 @@ for EmuDeck (`C:\Emulation\bios` on Windows), `/storage/roms/bios` for ROCKNIX,
`C:\RetroBat\bios` for RetroBat, and RetroArch's own default system
directory for RetroArch. BizHawk has no such folder: it reads `Firmware`
beside its executable wherever the archive was extracted, so a forced
`--platform bizhawk` that finds no install stops and asks for `--dest`. On Android a forced `--platform retroarch` falls back to
`--platform bizhawk` that finds no install stops and asks for `--dest`. MinUI,
NextUI and MyMinUI keep their files on the SD card, which the installer reads
from a computer it is mounted on: a forced `--platform minui` stops and asks
for the card's root as `--dest`, and a `--dest` that is a folder inside such a
card (its `Bios/`) is refused with the root to name instead. On Android a forced `--platform retroarch` falls back to
`<shared storage>/RetroArch/system` instead, a home directory there belonging
to Termux rather than to the frontend. Pass `--dest` to say where instead of
relying on that.
+1 -1
View File
@@ -40,7 +40,7 @@ Every module in `tests/`, by the area it guards.
| Core regression suites | synthetic | `test_e2e`, `test_audit_regressions`, `test_tool_messages` |
| File resolution and identity | synthetic + collection | `test_adler32_resolution`, `test_agnostic_scan`, `test_agnostic_scan_mode`, `test_agnostic_scope`, `test_agnostic_substitute`, `test_archive_choice`, `test_bios_companions`, `test_bundled_identity`, `test_casefold_cache`, `test_db_aliases`, `test_declared_names`, `test_derivations`, `test_directory_entries`, `test_extra_identity`, `test_foreign_tree`, `test_foreign_tree_names`, `test_frontend_digest`, `test_game_data_homonyms`, `test_hashing`, `test_identity`, `test_md5_pack_context`, `test_name_tiebreak`, `test_nec_filing`, `test_owner_resolution`, `test_path_suffix`, `test_resolution_evidence`, `test_sega_filing`, `test_shipped_content`, `test_twin_declarations`, `test_validated_choice`, `test_variant_choice`, `test_variant_identity`, `test_zipped_member` |
| Verification, cross-reference and profile checks | synthetic + profiles | `test_case_insensitive_platforms`, `test_checks_have_values`, `test_core_file_fills_declaration`, `test_core_size_refusal`, `test_cross_reference_sources`, `test_crypto_aes`, `test_crypto_unique_data`, `test_crypto_verify`, `test_file_consumers`, `test_fileless_audit`, `test_flycast_layout`, `test_gap_settlement`, `test_hidden_extras`, `test_launcher_aliases`, `test_launcher_layer`, `test_native_mode`, `test_no_case_collisions`, `test_path_conflict_status`, `test_path_null_copies`, `test_profile_checks`, `test_profile_repositories`, `test_romm_hash_rule`, `test_shared_groups`, `test_standalone_links`, `test_standalone_mode`, `test_undeclared_dedup`, `test_unplaced_extras`, `test_validation_scope`, `test_verify_all_dir`, `test_verify_emulator_evidence`, `test_verify_listing`, `test_verify_pack_choice`, `test_verify_release_asset` |
| Packs and manifests | synthetic + real packs | `test_artifact_lock`, `test_atomic_artifacts`, `test_batocera_cores`, `test_deterministic_zip`, `test_emulator_pack_counts`, `test_finalize_pack_names`, `test_large_file_cache`, `test_libretro_layout`, `test_manifest_entry`, `test_pack_conformance_dirs`, `test_pack_core_extras`, `test_pack_counts`, `test_pack_destinations`, `test_pack_failures`, `test_pack_file_names`, `test_pack_grouping`, `test_pack_integrity`, `test_pack_member_identity`, `test_pack_readme`, `test_platform_inheritance`, `test_platform_keys`, `test_required_only_archives`, `test_required_only_twins`, `test_split_extras`, `test_split_names`, `test_split_pack`, `test_system_pack_extras`, `test_system_pack_parity`, `test_torrentzip`, `test_which_pack` |
| Packs and manifests | synthetic + real packs | `test_artifact_lock`, `test_atomic_artifacts`, `test_batocera_cores`, `test_deterministic_zip`, `test_emulator_pack_counts`, `test_finalize_pack_names`, `test_large_file_cache`, `test_libretro_layout`, `test_manifest_entry`, `test_minui_platform`, `test_pack_conformance_dirs`, `test_pack_core_extras`, `test_pack_counts`, `test_pack_destinations`, `test_pack_failures`, `test_pack_file_names`, `test_pack_grouping`, `test_pack_integrity`, `test_pack_member_identity`, `test_pack_readme`, `test_platform_inheritance`, `test_platform_keys`, `test_required_only_archives`, `test_required_only_twins`, `test_split_extras`, `test_split_names`, `test_split_pack`, `test_system_pack_extras`, `test_system_pack_parity`, `test_torrentzip`, `test_which_pack` |
| Slots and regions | synthetic + profiles | `test_region`, `test_region_agnostic_scan`, `test_region_groups`, `test_region_held`, `test_region_platform_index`, `test_region_required_only`, `test_slot`, `test_slots`, `test_slots_shipped_file` |
| Hardware targets and systems | synthetic + target files | `test_extract_targets`, `test_lakka_targets`, `test_libretro_slugs`, `test_system_ids`, `test_system_selection`, `test_target_names`, `test_target_scrapers`, `test_target_systems`, `test_target_verify` |
| Installer and downloads | loopback server | `test_check_release`, `test_download`, `test_install`, `test_install_cores`, `test_install_experience`, `test_install_selection` |
+2
View File
@@ -148,6 +148,7 @@ Verification modes per platform:
| EmuELEC | md5 | MD5 of each file under `/storage/roms`; `zippedFile` is never read |
| MiSTer FPGA | md5 | MD5 of the file at its destination, no ZIP inspection |
| BizHawk | sha1 | SHA1 per firmware from `FirmwareDatabase.cs` |
| MinUI, NextUI, MyMinUI | existence | none: minarch hands each core `Bios/<TAG>/` and opens no file |
Full details and severity mapping: [verification modes](verification-modes.md).
`--region` narrows the report to the file set a regional pack would carry, and a
@@ -854,6 +855,7 @@ stand alone.
| `emuelec_scraper` | batocera-systems script, es_systems.json (cores) | Python dict, JSON |
| `retropie_scraper` | RetroPie-Setup scriptmodules (`rp_module_id`, `rp_module_help`) and `retropie_packages.sh` (version) | Bash |
| `misterfpga_scraper` | BiosDB_MiSTer bios_db.json.zip | JSON in ZIP |
| `minui_scraper`, `nextui_scraper`, `myminui_scraper` | each pak's `launch.sh` (`EMU_EXE`, the tag from the pak folder), each device's `cores/makefile` (the repository a library is built from) and the minarch statement that sets `core.bios_dir` | Shell, Make, C |
| `coreinfo_scraper` | .info files from libretro-core-info | INI-like |
| `bizhawk_scraper` | FirmwareDatabase.cs | C# source |
| `mame_hash_scraper` | mamedev/mame source tree. `--release mameNNNN` writes the profile of one release a platform builds. `--devices --listxml FILE` adds the device ROM sets of the release's `-listxml` | C source (sparse clone), listxml |
+3
View File
@@ -69,6 +69,9 @@ Each platform expects BIOS files in a specific base directory:
- EmuDeck: `~/Emulation/bios/`
- RetroDECK: `~/retrodeck/bios/`
- RetroBat: `bios/` inside the RetroBat directory
- MinUI, NextUI, MyMinUI: `Bios/<TAG>/` at the root of the SD card, one folder
per tag in parentheses of the Roms folders (`Bios/PS/` for
`Roms/Sony PlayStation (PS)/`)
Some cores expect files in subdirectories (e.g. `dc/` for Dreamcast, `pcsx2/bios/`
for PlayStation 2). Check the `path:` field in the emulator profile for the exact
+9 -2
View File
@@ -6,9 +6,9 @@ of each platform so that verification results match what the platform itself wou
## Existence mode
**Platforms**: RetroArch, Lakka, RetroPie
**Platforms**: RetroArch, Lakka, RetroPie, MinUI, NextUI, MyMinUI
**Source**: RetroArch `core_info.c`, function `path_is_valid()`
**Source**: RetroArch `core_info.c`, function `path_is_valid()`; minarch for the MinUI family
The most straightforward mode. A file is OK if it exists at the expected path. No hash is checked.
Any file with the correct name passes, regardless of content.
@@ -25,6 +25,13 @@ upstream code: `core_info.c` only calls `path_is_valid()` and does not open or h
Lakka and RetroPie inherit this behavior through platform config inheritance
(`inherits: retroarch` in the platform YAML).
MinUI, NextUI and MyMinUI check nothing at all: minarch returns the folder
`Bios/<TAG>/` for `RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY` and never opens a
file in it (MinUI `workspace/all/minarch/minarch.c:1889-1893` and `:2943`). The
core decides what it loads, so the files are the ones its profile documents,
placed below each folder a pak hands it (`core_system_dirs` in the platform
YAML).
## MD5 mode