feat: add minui, nextui and myminui platforms

minarch hands every core SDCARD_PATH/Bios/<TAG> as its system directory
and opens no file in it. The scrapers read each pak's launch.sh (core,
tag, devices), each device's core makefile (the project a library is
built from: MyMinUI's dosbox_libretro.so is dosbox-pure) and the minarch
statement, at a pinned commit. The installer recognises the card from
.system/version.txt and refuses a folder inside it.
This commit is contained in:
Abdessamad Derraz committed 2026-10-22 04:04:36 +02:00
1 parent d0802c5360
commit 43bdf83973
17 files changed
+2292 -17

No files matched your search

+3
View File
@@ -44,6 +44,9 @@ body:
- ROCKNIX
- 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 = "fe8de4eda0919c268d4d2c0d571c7d0dc1cf675d7604263766f5f99a35aa3724"
$defaultInstallSha256 = "526b405a2bef8e7ae181fc5554225a09e74afa25396db9084a15fb3a51e4b7eb"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+67 -1
View File
@@ -185,7 +185,7 @@ OTHER_SYSTEMS = "other"
AVAILABLE_PLATFORMS = (
"retroarch", "batocera", "recalbox", "retrobat", "emudeck",
"lakka", "retrodeck", "rocknix", "romm", "bizhawk", "misterfpga",
"retropie",
"retropie", "minui", "nextui", "myminui",
)
# The names the frontends give themselves, as each manifest's display_name
@@ -197,8 +197,23 @@ PLATFORM_NAMES = {
"retrobat": "RetroBat", "emudeck": "EmuDeck", "lakka": "Lakka",
"retrodeck": "RetroDECK", "rocknix": "ROCKNIX", "romm": "RomM",
"bizhawk": "BizHawk", "misterfpga": "MiSTer FPGA", "retropie": "RetroPie",
"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/"
@@ -4830,6 +4845,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.
@@ -4839,6 +4887,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"
@@ -4896,6 +4949,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...")
@@ -4908,6 +4963,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="fe8de4eda0919c268d4d2c0d571c7d0dc1cf675d7604263766f5f99a35aa3724"
DEFAULT_INSTALL_SHA256="526b405a2bef8e7ae181fc5554225a09e74afa25396db9084a15fb3a51e4b7eb"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+83
View File
@@ -650,3 +650,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: {}
+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"',
"",
+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()
+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()