feat: install only what the installed cores read

This commit is contained in:
Abdessamad Derraz committed 2026-10-16 05:02:50 +02:00
1 parent 058ac673c4
commit fbe432ccd0
23 files changed
+16680 -2185

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@ param(
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "95e9cf9c274d3ccf5a305ee4a32e74c5c173177b7cf012069835a096e7ff2b35"
$defaultInstallSha256 = "0aa7b10a9c6f295366f5d22df012d7205af4ccf4cc9d9cdc9cd90872cc4d6bf3"
$maximumInstallerBytes = 2MB
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
$temporary = $null
+332 -61
View File
@@ -427,6 +427,35 @@ def _expand_retroarch_path(value: str, app_dir: Path) -> Path:
return Path(os.path.expandvars(os.path.expanduser(value)))
def _retroarch_cfg_dir(
cfg_path: Path, key: str, app_dir: Path, default: str
) -> Path | None:
"""A directory setting of retroarch.cfg, as RetroArch expands it.
A value left empty or at `default` is the directory RetroArch falls back
on, `default` under app_dir. None when the config is unreadable or does
not set the key.
"""
try:
lines = cfg_path.read_text(encoding="utf-8", errors="replace").splitlines()
except OSError:
return None
for line in lines:
line = line.strip()
name, sep, value = line.partition("=")
if sep and name.strip() == key:
value = value.strip().strip('"').strip("'")
if not value or value == "default":
return app_dir / default
return _expand_retroarch_path(value, app_dir)
return None
# System directory -> the retroarch.cfg it was read from and the directory
# ':' expands to there, for finding the cores of the same installation.
_RETROARCH_CONFIGS: dict[Path, tuple[Path, Path]] = {}
def _parse_retroarch_system_dir(
cfg_path: Path, app_dir: Path | None = None
) -> Path | None:
@@ -439,18 +468,10 @@ def _parse_retroarch_system_dir(
if not cfg_path.exists():
return None
app_dir = app_dir or cfg_path.parent
try:
for line in cfg_path.read_text(encoding="utf-8", errors="replace").splitlines():
line = line.strip()
if line.startswith("system_directory"):
_, _, value = line.partition("=")
value = value.strip().strip('"').strip("'")
if not value or value == "default":
return app_dir / "system"
return _expand_retroarch_path(value, app_dir)
except OSError:
pass
return None
system_dir = _retroarch_cfg_dir(cfg_path, "system_directory", app_dir, "system")
if system_dir is not None:
_RETROARCH_CONFIGS.setdefault(system_dir, (cfg_path, app_dir))
return system_dir
def _shell_unquote(value: str) -> str:
@@ -920,6 +941,69 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]:
return found
# A libretro core as RetroArch loads it: <name>_libretro.so|dll|dylib, and
# <name>_libretro_android.so on Android.
_CORE_FILE_RE = re.compile(
r"^(?P<name>.+?)_libretro(?:_android)?\.(?:so|dll|dylib)$", re.IGNORECASE
)
CORE_PLATFORMS = ("retroarch", "lakka", "retropie")
# Where these systems install cores whatever retroarch.cfg says: RetroPie
# gives each package its own folder, Lakka keeps the image's cores and the
# ones the online updater fetches apart.
_FIXED_CORE_DIRS = {
"retropie": (Path("/opt/retropie/libretrocores"),),
"lakka": (Path("/usr/lib/libretro"), Path("/storage/cores"), Path("/tmp/cores")),
}
def core_dirs(plat: str, bios_path: Path) -> list[Path]:
"""The directories the RetroArch writing to bios_path loads cores from.
The config detection read for this system directory names them in
libretro_directory; without one, a retroarch.cfg beside the system
directory is the portable layout. Unset, the setting is cores/ under
RetroArch's own directory.
"""
found = _RETROARCH_CONFIGS.get(bios_path)
if found is None and (bios_path.parent / "retroarch.cfg").is_file():
found = (bios_path.parent / "retroarch.cfg", bios_path.parent)
dirs: list[Path] = []
if found is not None:
cfg, app_dir = found
dirs.append(
_retroarch_cfg_dir(cfg, "libretro_directory", app_dir, "cores")
or app_dir / "cores"
)
dirs.extend(_FIXED_CORE_DIRS.get(plat, ()))
return list(dict.fromkeys(dirs))
def installed_cores(dirs: list[Path]) -> list[str]:
"""The cores present in these directories, by the name the buildbot uses.
A folder inside is looked into once, which is how RetroPie installs
each package. An unreadable directory holds nothing that can be seen.
"""
names: set[str] = set()
for directory in dirs:
try:
children = list(directory.iterdir())
except OSError:
continue
for child in children:
candidates = [child]
if child.is_dir():
try:
candidates = list(child.iterdir())
except OSError:
continue
for path in candidates:
match = _CORE_FILE_RE.match(path.name)
if match and path.is_file():
names.add(match.group("name"))
return sorted(names)
def normalize_platform(name: str) -> str:
"""Return the canonical platform id for user-supplied input."""
plat = name.strip().lower()
@@ -1021,6 +1105,48 @@ def _check_tags(entry: dict, field: str, where: str) -> None:
raise ValueError(f"invalid {field} for {where}")
# What an alternative replaces on the entry it stands in for.
_COPY_FIELDS = ("sha1", "sha256", "size", "repo_path", "release_asset", "storage", "cores")
def _check_download(entry: dict, where: str, dest: str) -> int:
"""The size, hashes, source and cores of one downloadable copy; its size."""
size = entry.get("size")
if isinstance(size, bool) or not isinstance(size, int) or not (0 <= size <= MAX_DOWNLOAD_SIZE):
raise ValueError(f"invalid size for {dest}")
sha1 = entry.get("sha1", "")
sha256 = entry.get("sha256", "")
if sha1 and (not isinstance(sha1, str) or not _SHA1_RE.fullmatch(sha1)):
raise ValueError(f"invalid SHA1 for {dest}")
if sha256 and (
not isinstance(sha256, str) or not _SHA256_RE.fullmatch(sha256)
):
raise ValueError(f"invalid SHA256 for {dest}")
if not sha1 and not sha256:
raise ValueError(f"missing content hash for {dest}")
release_asset = entry.get("release_asset")
repo_path = entry.get("repo_path")
if release_asset:
asset = _safe_relative_path(release_asset, f"{where}.release_asset")
if len(asset.parts) != 1:
raise ValueError(f"release asset must be a basename: {release_asset}")
elif repo_path:
source = _safe_relative_path(repo_path, f"{where}.repo_path")
if source.parts[0] != "bios":
raise ValueError(f"repo_path outside bios/: {repo_path}")
else:
raise ValueError(f"no download source for {dest}")
cores = entry.get("cores")
if cores is not None and (
not isinstance(cores, list) or not all(isinstance(core, str) for core in cores)
):
raise ValueError(f"invalid cores list for {dest}")
return size
def _validate_manifest(data: object, plat: str) -> dict:
"""Validate the untrusted install-manifest boundary using stdlib only."""
if not isinstance(data, dict):
@@ -1035,6 +1161,7 @@ def _validate_manifest(data: object, plat: str) -> dict:
seen_destinations: set[str] = set()
total_size = 0
largest_total = 0
for index, entry in enumerate(files):
if not isinstance(entry, dict):
raise ValueError(f"files[{index}] must be an object")
@@ -1043,42 +1170,24 @@ def _validate_manifest(data: object, plat: str) -> dict:
raise ValueError(f"duplicate manifest destination: {dest}")
seen_destinations.add(dest)
size = entry.get("size")
if isinstance(size, bool) or not isinstance(size, int) or not (0 <= size <= MAX_DOWNLOAD_SIZE):
raise ValueError(f"invalid size for {dest}")
size = _check_download(entry, f"files[{index}]", dest)
total_size += size
if total_size > MAX_TOTAL_DOWNLOAD_SIZE:
alternatives = entry.get("alternatives", [])
if not isinstance(alternatives, list) or len(alternatives) > MAX_SELECTION_TAGS:
raise ValueError(f"invalid alternatives for {dest}")
for alt_index, alternative in enumerate(alternatives):
where = f"files[{index}].alternatives[{alt_index}]"
if (
not isinstance(alternative, dict)
or not alternative.get("cores")
or not set(alternative) <= set(_COPY_FIELDS)
):
raise ValueError(f"invalid alternative for {dest}")
# Narrowing can swap in any copy: the cap holds for the largest.
size = max(size, _check_download(alternative, where, dest))
largest_total += size
if largest_total > MAX_TOTAL_DOWNLOAD_SIZE:
raise ValueError("manifest total size exceeds safety limit")
sha1 = entry.get("sha1", "")
sha256 = entry.get("sha256", "")
if sha1 and (not isinstance(sha1, str) or not _SHA1_RE.fullmatch(sha1)):
raise ValueError(f"invalid SHA1 for {dest}")
if sha256 and (
not isinstance(sha256, str) or not _SHA256_RE.fullmatch(sha256)
):
raise ValueError(f"invalid SHA256 for {dest}")
if not sha1 and not sha256:
raise ValueError(f"missing content hash for {dest}")
release_asset = entry.get("release_asset")
repo_path = entry.get("repo_path")
if release_asset:
asset = _safe_relative_path(release_asset, f"files[{index}].release_asset")
if len(asset.parts) != 1:
raise ValueError(f"release asset must be a basename: {release_asset}")
elif repo_path:
source = _safe_relative_path(repo_path, f"files[{index}].repo_path")
if source.parts[0] != "bios":
raise ValueError(f"repo_path outside bios/: {repo_path}")
else:
raise ValueError(f"no download source for {dest}")
cores = entry.get("cores")
if cores is not None and (
not isinstance(cores, list) or not all(isinstance(core, str) for core in cores)
):
raise ValueError(f"invalid cores list for {dest}")
for field in SELECTION_FIELDS:
_check_tags(entry, field, dest)
@@ -1127,6 +1236,19 @@ def _validate_manifest(data: object, plat: str) -> dict:
):
raise ValueError("manifest total_omitted does not match omitted_files")
for field in ("core_names", "system_cores"):
mapping = data.get(field)
if mapping is None:
continue
if not isinstance(mapping, dict) or len(mapping) > MAX_MANIFEST_FILES:
raise ValueError(f"invalid {field}")
for key in mapping:
if not isinstance(key, str) or not 0 < len(key) <= 256:
raise ValueError(f"invalid {field} key")
_check_tags(mapping, key, field)
if mapping[key] is None:
raise ValueError(f"invalid {field} for {key}")
copies = data.get("standalone_copies", [])
if not isinstance(copies, list) or len(copies) > 10_000:
raise ValueError("invalid standalone_copies")
@@ -1241,19 +1363,95 @@ def fetch_targets(plat: str) -> "dict | None":
return None
def _for_cores(entry: dict, chosen: set[str]) -> dict | None:
"""The copy of a core file the chosen cores read, or None.
A destination holds one file. When several cores read it and disagree
on its content, the manifest carries the first core's copy and the
others as alternatives, in the order the pack builder tries them: a
pack built without the first core carries the next one's copy.
"""
if chosen.intersection(entry.get("cores") or []):
return entry
for alternative in entry.get("alternatives", []):
if chosen.intersection(alternative["cores"]):
kept = {
k: v for k, v in entry.items()
if k not in _COPY_FIELDS and k != "alternatives"
}
kept.update((k, alternative[k]) for k in _COPY_FIELDS if k in alternative)
return kept
return None
def _filter_by_target(
files: list[dict], target_cores: list[str]
) -> list[dict]:
"""Keep files where cores is None or overlaps with target_cores."""
"""Keep the platform's files, and the core files the given cores read."""
chosen = set(target_cores)
result: list[dict] = []
target_set = set(target_cores)
for f in files:
cores = f.get("cores")
if cores is None or any(c in target_set for c in cores):
if f.get("cores") is None:
result.append(f)
continue
kept = _for_cores(f, chosen)
if kept is not None:
result.append(kept)
return result
def designated_cores(manifest: dict, names: list[str]) -> set[str]:
"""The cores a list of names designates, the way the pack builder reads it.
A target list, or a core found installed, names a core the buildbot's
way (`mednafen_psx`); a file names the profile that reads it
(`beetle_psx`). `core_names` gives each profile its other names.
"""
wanted = set(names)
chosen = set(wanted)
for profile, aliases in (manifest.get("core_names") or {}).items():
if wanted.intersection(aliases):
chosen.add(profile)
return chosen
def narrow_to_cores(
manifest: dict,
names: list[str],
files: list[dict] | None = None,
omitted: list[dict] | None = None,
) -> tuple[list[dict], list[dict]]:
"""Keep what the named cores read, as a pack built for them carries.
A file a core loads stays when that core is named. A file of the
platform's own list stays when one of its systems does: a system goes
when none of the cores `system_cores` gives it is named, and a system
it does not list stays whatever is named. A manifest without the two
maps keeps the platform's list whole and matches cores by name.
"""
cores = designated_cores(manifest, names)
reach = manifest.get("system_cores") or {}
def kept(entries: list[dict]) -> list[dict]:
result: list[dict] = []
for entry in entries:
if entry.get("cores") is not None:
copy = _for_cores(entry, cores)
if copy is not None:
result.append(copy)
continue
systems = entry_systems(entry)
if not systems or any(
s not in reach or cores.intersection(reach[s]) for s in systems
):
result.append(entry)
return result
files = manifest.get("files", []) if files is None else files
omitted = manifest.get("omitted_files", []) if omitted is None else omitted
return kept(files), kept(omitted)
def canonical_region(raw: str) -> str:
"""One region name or alias, spelled the way manifests spell it."""
key = raw.strip().lower()
@@ -1393,7 +1591,11 @@ def available_choices(files: list[dict], omitted: list[dict]) -> dict[str, list[
ordered = sorted(systems)
if any(not entry_systems(f) for f in files):
ordered.append(OTHER_SYSTEMS)
cores = {c for f in files for c in (f.get("cores") or [])}
cores = {
c for f in files
for copy in [f, *f.get("alternatives", [])]
for c in (copy.get("cores") or [])
}
regions = {
r for e in files + omitted for r in (e.get("regions") or [])
if r != WORLD_REGION
@@ -2124,16 +2326,24 @@ def _pick(title: str, rows: list[tuple[str, list[dict]]], ordered: bool = False)
return [rows[number - 1][0] for number in chosen]
INSTALLED_ONLY = "installed"
def _prompt_custom_selection(
plat: str, files: list[dict], omitted: list[dict]
) -> tuple[list[str], list[str], list[str]] | None:
plat: str, files: list[dict], omitted: list[dict],
installed: tuple[list[dict], list[dict]] | None = None,
) -> tuple[list[str], list[str], list[str]] | str | None:
"""Offer to narrow the install by system, core and region.
Enter installs everything, the default most runs want. Returns None
then, or the three choices; each empty list keeps everything on its axis.
then, INSTALLED_ONLY for what `installed` (the files the cores found
installed read) holds, or the three choices; each empty list keeps
everything on its axis.
"""
print(f"\n{plat.capitalize()}: {_sized(files)}.")
print(" Enter) install everything")
if installed is not None:
print(f" i) only what the installed cores read: {_sized(installed[0])}")
print(" c) choose systems, cores and regions")
print(" q) quit")
while True:
@@ -2142,6 +2352,8 @@ def _prompt_custom_selection(
return None
if answer == "q":
sys.exit(0)
if answer == "i" and installed is not None:
return INSTALLED_ONLY
if answer == "c":
break
@@ -2161,7 +2373,8 @@ def _prompt_custom_selection(
print("\nThe platform's own files for these systems are always kept;")
print("the extra files each core loads can be narrowed.")
cores = _pick("Cores:", [
(name, [f for f in kept if name in (f.get("cores") or [])])
(name, _filter_by_target(
[f for f in kept if f.get("cores") is not None], [name]))
for name in core_names
])
kept, kept_omitted = narrow(kept, kept_omitted, [], cores, [])
@@ -2205,7 +2418,7 @@ def _print_choices(
held = (
[f for f in files if _chosen(f, {name})]
if systems
else [f for f in files if name in (f.get("cores") or [])]
else _filter_by_target([f for f in files if f.get("cores") is not None], [name])
)
print(f" {name:<{width}} {_sized(held)}")
@@ -2285,6 +2498,12 @@ def main() -> None:
action="store_true",
help="opt in to copies into detected standalone-emulator directories",
)
parser.add_argument(
"--installed-cores",
action="store_true",
help="install only what the cores already installed read "
"(RetroArch, Lakka, RetroPie)",
)
parser.add_argument(
"--system",
action="append",
@@ -2453,6 +2672,28 @@ def _install(args: argparse.Namespace, real_stdout) -> None:
failures: list[tuple[str, str]] = []
installed_paths: list[Path] = []
# Every check --installed-cores makes before anything is written: a
# refusal for the second platform must not follow an install of the first.
cores_found: dict[tuple[str, Path], list[str]] = {}
for plat_name, bios_path in platforms:
if plat_name in CORE_PLATFORMS:
searched = core_dirs(plat_name, bios_path)
cores_found[(plat_name, bios_path)] = installed_cores(searched)
elif args.installed_cores:
print(
f"Error: {plat_name} ships its cores with the system; "
f"--installed-cores applies to {', '.join(CORE_PLATFORMS)}",
file=sys.stderr,
)
sys.exit(1)
if args.installed_cores and not cores_found[(plat_name, bios_path)]:
print(
f"Error: no installed core found for {plat_name} in: "
f"{', '.join(map(str, searched)) or 'no known directory'}",
file=sys.stderr,
)
sys.exit(1)
for plat_name, bios_path in platforms:
if bios_path not in installed_paths:
installed_paths.append(bios_path)
@@ -2503,12 +2744,31 @@ def _install(args: argparse.Namespace, real_stdout) -> None:
"installing every file"
)
else:
target_cores = target_info["cores"]
before = len(files)
files = _filter_by_target(files, target_cores)
omitted_files = _filter_by_target(omitted_files, target_cores)
files, omitted_files = narrow_to_cores(
manifest, target_info["cores"], files, omitted_files
)
print(f" Filtered {before} -> {len(files)} files for target {args.target}")
found_cores = cores_found.get((plat_name, bios_path), [])
if "system_cores" not in manifest:
found_cores = []
elif found_cores:
print(f" {_plural(len(found_cores), 'core')} installed")
if args.installed_cores:
if "system_cores" not in manifest:
print(
f"Error: the {plat_name} file list does not say which cores "
"read which systems; --installed-cores needs a newer one",
file=sys.stderr,
)
sys.exit(1)
before = len(files)
files, omitted_files = narrow_to_cores(
manifest, found_cores, files, omitted_files
)
print(f" Narrowed {before} -> {len(files)} files to the installed cores")
selection = _requested_narrowing(args, plat_name, files, omitted_files)
if (
selection is None
@@ -2517,8 +2777,19 @@ def _install(args: argparse.Namespace, real_stdout) -> None:
and not listing
and records_selection(files)
):
selection = _prompt_custom_selection(plat_name, files, omitted_files)
if selection is not None:
installed = (
narrow_to_cores(manifest, found_cores, files, omitted_files)
if found_cores and not args.installed_cores else None
)
selection = _prompt_custom_selection(
plat_name, files, omitted_files, installed
)
if selection == INSTALLED_ONLY:
files, omitted_files = installed
print("\n Same choice without questions: "
f"--platform {plat_name} --installed-cores")
selection = None
elif selection is not None:
print("\n Same choice without questions: "
+ _same_choice_hint(plat_name, *selection))
if selection is not None:
+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="95e9cf9c274d3ccf5a305ee4a32e74c5c173177b7cf012069835a096e7ff2b35"
DEFAULT_INSTALL_SHA256="0aa7b10a9c6f295366f5d22df012d7205af4ccf4cc9d9cdc9cd90872cc4d6bf3"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+1259 -422
View File
File diff suppressed because it is too large. Load diff
+748 -26
View File
@@ -5,7 +5,7 @@
"platform": "bizhawk",
"display_name": "BizHawk",
"version": "1.0",
"generated": "2026-10-10T05:24:26Z",
"generated": "2026-10-15T21:45:48Z",
"base_destination": "Firmware",
"detect": [
{
@@ -26,6 +26,620 @@
"pack_size": 2585299183,
"total_omitted": 0,
"omitted_files": [],
"core_names": {
"3dsen": [
"3dsen-windows"
],
"beetle_gba": [
"mednafen_gba"
],
"beetle_lynx": [
"mednafen_lynx"
],
"beetle_pce": [
"mednafen_pce"
],
"beetle_pcfx": [
"mednafen_pcfx",
"pcfx"
],
"beetle_psx": [
"mednafen_psx",
"mednafen_psx_hw"
],
"beetle_saturn": [
"beetle-saturn",
"mednafen_saturn"
],
"bsnes2014": [
"bsnes2014_accuracy",
"bsnes2014_balanced",
"bsnes2014_performance"
],
"bsnes_hd_beta": [
"bsnes_hd",
"bsneshd"
],
"bsnes_mercury": [
"bsnes_mercury_accuracy",
"bsnes_mercury_balanced",
"bsnes_mercury_performance"
],
"c64-emu": [
"C64-EMU",
"C64.emu"
],
"doublecherrygb": [
"DoubleCherryGB"
],
"ep128emu": [
"ep128emu_core"
],
"ep128emu_core": [
"ep128emu-core"
],
"fpse-ng": [
"fpse64",
"fpseng"
],
"gba-emu": [
"GBA-EMU",
"GBA.emu"
],
"gbc-emu": [
"GBC-EMU",
"GBC.emu"
],
"genesis_plus_gx": [
"genesisplusgx",
"genesisplusgx-expanded",
"genplus"
],
"genesis_plus_gx_wide": [
"genesisplusgx-wide",
"genesisplusgxwide"
],
"higan_sfc_balanced": [
"nside_sfc_balanced"
],
"idtech4a++": [
"diii4a",
"idtech4amm"
],
"lime3ds": [
"lime-qt",
"lime3ds-gui"
],
"linkboy": [
"LINKBOY",
"Linkboy"
],
"lynx-emu": [
"LYNX-EMU",
"Lynx.emu"
],
"m64plus-fz": [
"M64PLUS-FZ",
"org.mupen64plusae.v3.fzurita",
"org.mupen64plusae.v3.fzurita.amazon",
"org.mupen64plusae.v3.fzurita.pro"
],
"mame": [
"imame4all"
],
"mame2016": [
"mess2016"
],
"mame4droid-2024": [
"MAME4DROID-2024",
"MAME4DROID-CURRENT",
"com.seleuco.mame4d2024"
],
"mandarine": [
"mandarine-qt"
],
"mastergear": [
"MASTERGEAR",
"com.fms.mg"
],
"md-emu": [
"MD-EMU",
"MD.emu"
],
"mednafen_snes": [
"beetle-bsnes",
"beetle_bsnes"
],
"mednafen_supafaust": [
"supafaust"
],
"melonds_ds": [
"melondsds"
],
"mesen": [
"mesens"
],
"mesen-s": [
"mesen_s"
],
"mesence": [
"mesen2"
],
"msx-emu": [
"MSX-EMU",
"MSX.emu"
],
"mupen64plus-ae": [
"MUPEN64PLUS-AE",
"org.mupen64plusae.v3.alpha"
],
"mupen64plus_next": [
"mupen64plus",
"mupen64plus-next",
"mupen64plus_next_develop",
"mupen64plus_next_gles2",
"mupen64plus_next_gles3"
],
"my-boy": [
"MY-BOY",
"My Boy!"
],
"my-oldboy": [
"MY-OLDBOY",
"My OldBoy!"
],
"nes-emu": [
"NES-EMU",
"NES.emu"
],
"nesoid": [
"NESOID",
"Nesoid"
],
"o2em": [
"o2em_libretro"
],
"parallel-launcher": [
"ca.parallel_launcher.ParallelLauncher"
],
"parallel_n64": [
"parallel_n64_debug"
],
"pce-emu": [
"PCE-EMU",
"PCE.emu",
"com.PceEmu",
"pceemu"
],
"pcsx_rearmed": [
"pcsx_rearmed_interpreter",
"pcsx_rearmed_neon"
],
"pizza-boy-gba": [
"PIZZA-BOY-GBA",
"Pizza Boy GBA"
],
"pizza-boy-gbc": [
"PIZZA-BOY-GBC",
"Pizza Boy GBC"
],
"pizza-boy-sc": [
"PIZZA-BOY-SC",
"Pizza Boy SC",
"it.dbtecno.pizzaboyscbasic",
"it.dbtecno.pizzaboyscpro"
],
"puae": [
"uae"
],
"real3doplayer": [
"REAL3DOPLAYER",
"Real3DOPlayer"
],
"rosalies-mupen-gui": [
"RMG",
"com.github.Rosalie241.RMG"
],
"saturn-emu": [
"SATURN-EMU",
"Saturn.emu",
"saturnemu"
],
"smsplus": [
"smsplus_gx"
],
"snes9x": [
"pocketsnes",
"snes9x_next"
],
"snes9x-explus": [
"SNES9X-EXPLUS",
"Snes9x EX+",
"Snes9xEXPlus"
],
"speccy": [
"SPECCY",
"com.fms.speccy",
"com.fms.speccy.deluxe"
],
"stella2023": [
"stella"
],
"superzsnes": [
"SUPERZSNES"
],
"tgbdual": [
"TGB Dual"
],
"vbam": [
"vba-m"
],
"vice": [
"vice_x128",
"vice_x64",
"vice_x64dtv",
"vice_x64sc",
"vice_xcbm2",
"vice_xcbm5x0",
"vice_xpet",
"vice_xplus4",
"vice_xscpu64",
"vice_xvic"
],
"vice_x64": [
"x64"
],
"yabasanshiro": [
"yabasanshiro-2"
]
},
"system_cores": {
"3do": [
"groovymame",
"mame",
"mame2015",
"mame2016",
"mame4droid-2024",
"opera",
"real3doplayer",
"ume2015"
],
"apple-ii": [
"applewin",
"clk",
"izapple2",
"kegs",
"linapple",
"mariani",
"mednafen",
"pom2"
],
"atari-7800": [
"a7800",
"prosystem"
],
"atari-lynx": [
"beetle_lynx",
"gearlynx",
"handy",
"holani",
"lynx-emu",
"mednafen"
],
"coleco-colecovision": [
"ares",
"bluemsx",
"clk",
"colem",
"fbneo",
"gearcoleco",
"jollycv",
"mesence",
"msx-emu",
"openmsx",
"smsplus",
"zesarux"
],
"commodore-amiga": [
"amiberry",
"clk",
"fsuae",
"puae",
"puae2021",
"uae4arm"
],
"commodore-c64": [
"c64-emu",
"frodo",
"vice",
"vice_x64",
"vice_x64sc",
"x64sdl"
],
"doom": [
"gzdoom",
"idtech4a++",
"prboom",
"prboom-plus"
],
"fairchild-channel-f": [
"fbneo",
"freechaf"
],
"gce-vectrex": [
"vecx"
],
"mattel-intellivision": [
"freeintv"
],
"microsoft-msx": [
"ares",
"bluemsx",
"clk",
"fbneo",
"fmsx",
"msx-emu",
"openmsx",
"zesarux"
],
"nec-pc-engine-cd": [
"ares",
"geargrafx",
"mednafen",
"mesence",
"pce-emu"
],
"nec-pc-fx": [
"beetle_pcfx",
"mednafen"
],
"nintendo-3ds": [
"azahar",
"azaharplus",
"citra",
"citra-mmj",
"citra2018",
"citra_canary",
"lime3ds",
"mandarine",
"panda3ds",
"trident"
],
"nintendo-64dd": [
"ares",
"m64plus-fz",
"mupen64plus-ae",
"mupen64plus_next",
"parallel-launcher",
"parallel_n64",
"project64",
"rosalies-mupen-gui",
"sixtyforce"
],
"nintendo-ds": [
"desmume",
"desmume2015",
"drastic",
"melonds",
"melonds-nightly",
"melonds_ds",
"noods",
"skyemu"
],
"nintendo-gb": [
"boytacean",
"doublecherrygb",
"emux_gb",
"gambatte",
"gbc-emu",
"gearboy",
"irogb",
"jgenesis",
"linkboy",
"mednafen",
"mesen-s",
"mesence",
"mgba",
"my-oldboy",
"pizza-boy-gbc",
"sameboy",
"skyemu",
"tgbdual",
"vbam"
],
"nintendo-gba": [
"beetle_gba",
"gba-emu",
"gpsp",
"jgenesis",
"linkboy",
"mednafen",
"mednafen_gba",
"mesence",
"meteor",
"mgba",
"mgba_splitscreen",
"my-boy",
"noods",
"pizza-boy-gba",
"skyemu",
"tempgba",
"vba_next",
"vbam"
],
"nintendo-gbc": [
"boytacean",
"doublecherrygb",
"fixgb",
"gambatte",
"gbc-emu",
"gearboy",
"irogb",
"jgenesis",
"linkboy",
"mednafen",
"mesen-s",
"mesence",
"mgba",
"my-oldboy",
"pizza-boy-gbc",
"sameboy",
"skyemu",
"tgbdual",
"vbam"
],
"nintendo-nes": [
"3dsen",
"bnes",
"emux_nes",
"fceumm",
"fixnes",
"ines",
"jgenesis",
"mednafen",
"mesen",
"mesence",
"nes",
"nes-emu",
"nesoid",
"nestopia",
"plastic",
"punes",
"quicknes",
"rustynes"
],
"nintendo-satellaview": [
"bsnes",
"bsnes-jg",
"bsnes2014",
"bsnes_cplusplus98",
"bsnes_hd_beta",
"bsnes_mercury",
"chimerasnes",
"mesen-s",
"mesence",
"snes9x",
"snes9x-explus",
"snes9x2002",
"snes9x2005",
"snes9x2005_plus",
"snes9x2010"
],
"nintendo-snes": [
"bsnes",
"bsnes-jg",
"bsnes2014",
"bsnes_cplusplus98",
"bsnes_hd_beta",
"bsnes_mercury",
"chimerasnes",
"fbneo",
"higan_sfc",
"higan_sfc_balanced",
"jgenesis",
"mednafen",
"mednafen_snes",
"mednafen_supafaust",
"mesen-s",
"mesence",
"snes9x",
"snes9x-explus",
"snes9x2002",
"snes9x2005",
"snes9x2005_plus",
"snes9x2010",
"superzsnes"
],
"philips-videopac": [
"o2em"
],
"sega-32x": [
"ares",
"jgenesis",
"picodrive"
],
"sega-game-gear": [
"ares",
"gearsystem",
"genesis_plus_gx",
"genesis_plus_gx_wide",
"jgenesis",
"mastergear",
"mednafen",
"mesence",
"picodrive",
"pizza-boy-sc",
"smsplus"
],
"sega-master-system": [
"ares",
"blastem",
"clk",
"gearsystem",
"genesis_plus_gx",
"genesis_plus_gx_wide",
"jgenesis",
"mastergear",
"md-emu",
"mednafen",
"mesence",
"picodrive",
"pizza-boy-sc",
"smsplus",
"zesarux"
],
"sega-mega-drive": [
"ares",
"blastem",
"clownmdemu",
"genesis_plus_gx",
"genesis_plus_gx_wide",
"jgenesis",
"md-emu",
"mednafen",
"picodrive",
"pizza-boy-sc"
],
"sega-saturn": [
"ares",
"beetle_saturn",
"kronos",
"mednafen",
"saturn-emu",
"ssf",
"yabasanshiro",
"yabause",
"ymir"
],
"sinclair-zx-spectrum": [
"81",
"ares",
"clk",
"cspect",
"ep128emu",
"ep128emu_core",
"fuse",
"speccy",
"zesarux"
],
"sony-playstation": [
"ares",
"armsx1",
"beetle_psx",
"duckstation",
"epsxe",
"fpse",
"fpse-ng",
"mednafen",
"pcsx1",
"pcsx_rearmed",
"rustation",
"swanstation"
],
"texas-instruments-ti-83": [
"numero"
]
},
"files": [
{
"dest": "panafz1.bin",
@@ -1379,7 +1993,8 @@
"size": 16384,
"repo_path": "bios/Nintendo/Game Boy Advance/GBA_bios.rom",
"cores": [
"ares"
"ares",
"mgba"
]
},
{
@@ -1389,7 +2004,8 @@
"size": 1024,
"repo_path": "bios/Sega/Game Gear/GG_majesco.gg",
"cores": [
"ares"
"ares",
"genesis_plus_gx"
],
"systems": [
"sega-game-gear"
@@ -1402,7 +2018,8 @@
"size": 8192,
"repo_path": "bios/Other/zesarux/sms.rom",
"cores": [
"ares"
"ares",
"genesis_plus_gx"
],
"systems": [
"sega-master-system"
@@ -1415,7 +2032,8 @@
"size": 8192,
"repo_path": "bios/Sega/Master System/MasterSystem/japanese-bios.sms",
"cores": [
"ares"
"ares",
"genesis_plus_gx"
],
"systems": [
"sega-master-system"
@@ -1428,7 +2046,8 @@
"size": 8192,
"repo_path": "bios/Other/zesarux/sms.rom",
"cores": [
"ares"
"ares",
"genesis_plus_gx"
],
"systems": [
"sega-master-system"
@@ -1441,7 +2060,9 @@
"size": 131072,
"repo_path": "bios/Sega/Mega CD/SegaCDBIOS9303.bin",
"cores": [
"ares"
"ares",
"genesis_plus_gx",
"picodrive"
]
},
{
@@ -1451,7 +2072,9 @@
"size": 131072,
"repo_path": "bios/Sega/Mega CD/jp_mcd2_921222.bin",
"cores": [
"ares"
"ares",
"genesis_plus_gx",
"picodrive"
]
},
{
@@ -1461,7 +2084,9 @@
"size": 131072,
"repo_path": "bios/Sega/Mega CD/eu_mcd2_9303.bin",
"cores": [
"ares"
"ares",
"genesis_plus_gx",
"picodrive"
]
},
{
@@ -1601,7 +2226,8 @@
"size": 262144,
"repo_path": "bios/NEC/PC Engine/syscard1.pce",
"cores": [
"ares"
"ares",
"beetle_pce"
],
"systems": [
"nec-pc-engine-cd"
@@ -1614,7 +2240,8 @@
"size": 262144,
"repo_path": "bios/NEC/PC Engine/syscard3.pce",
"cores": [
"ares"
"ares",
"beetle_pce"
],
"systems": [
"nec-pc-engine-cd"
@@ -1627,7 +2254,8 @@
"size": 262144,
"repo_path": "bios/NEC/PC Engine/syscard3u.pce",
"cores": [
"ares"
"ares",
"beetle_pce"
],
"systems": [
"nec-pc-engine-cd"
@@ -1683,7 +2311,8 @@
"size": 524288,
"repo_path": "bios/Sony/PlayStation/scph5501.bin",
"cores": [
"ares"
"ares",
"beetle_psx"
],
"systems": [
"sony-playstation"
@@ -1696,7 +2325,8 @@
"size": 524288,
"repo_path": "bios/Sony/PlayStation/scph5500.bin",
"cores": [
"ares"
"ares",
"beetle_psx"
],
"systems": [
"sony-playstation"
@@ -1709,7 +2339,8 @@
"size": 524288,
"repo_path": "bios/Sony/PlayStation/scph5502.bin",
"cores": [
"ares"
"ares",
"beetle_psx"
],
"systems": [
"sony-playstation"
@@ -1779,7 +2410,8 @@
"size": 262144,
"repo_path": "bios/Nintendo/Super Game Boy/SGB1.sfc/program.rom",
"cores": [
"ares"
"ares",
"bsnes"
]
},
{
@@ -1789,7 +2421,8 @@
"size": 524288,
"repo_path": "bios/Nintendo/Super Game Boy/SGB2.sfc/program.rom",
"cores": [
"ares"
"ares",
"bsnes"
]
},
{
@@ -1799,7 +2432,8 @@
"size": 262144,
"repo_path": "bios/Nintendo/SuFami Turbo/STBIOS.bin",
"cores": [
"ares"
"ares",
"snes9x"
]
},
{
@@ -1809,7 +2443,9 @@
"size": 1048576,
"repo_path": "bios/Nintendo/SNES/BS-X.bin",
"cores": [
"ares"
"ares",
"bsnes",
"snes9x"
]
},
{
@@ -2099,6 +2735,17 @@
"repo_path": "bios/Arcade/Arcade/stvbios.zip",
"cores": [
"beetle_saturn"
],
"alternatives": [
{
"sha1": "7783ab663bb1a8d62ff60d583674355c2af556b1",
"sha256": "38ceb2792c72770f56aadb1865cf66dc33a3017db1ee05e11c2b2dff397ad39e",
"size": 2955630,
"repo_path": "bios/Arcade/MAME/stvbios.zip",
"cores": [
"mame"
]
}
]
},
{
@@ -2368,7 +3015,9 @@
"size": 3145728,
"repo_path": "bios/Nintendo/3DS/Citra/sysdata/shared_font.bin",
"cores": [
"citra"
"citra",
"citra2018",
"citra_canary"
],
"systems": [
"nintendo-3ds"
@@ -3989,7 +4638,8 @@
"size": 256,
"repo_path": "bios/Nintendo/Game Boy/GB_dmg.bin",
"cores": [
"gambatte"
"gambatte",
"mgba"
],
"systems": [
"nintendo-gb"
@@ -4002,7 +4652,8 @@
"size": 2304,
"repo_path": "bios/Nintendo/Game Boy Color/GBC_cgb.bin",
"cores": [
"gambatte"
"gambatte",
"mgba"
],
"systems": [
"nintendo-gbc"
@@ -4107,6 +4758,17 @@
"repo_path": "bios/Arcade/MAME/neogeo.zip",
"cores": [
"mame"
],
"alternatives": [
{
"sha1": "deb62b0074b8cae4f162c257662136733cfc76ad",
"sha256": "bf05cc848e403f8ae457bfe26faa8135cf4725cfff956fa030f5c0a00a26adcc",
"size": 1859335,
"repo_path": "bios/Arcade/Arcade/neogeo.zip",
"cores": [
"fbneo"
]
}
]
},
{
@@ -4116,7 +4778,8 @@
"size": 2094636,
"repo_path": "bios/Arcade/MAME/pgm.zip",
"cores": [
"mame"
"mame",
"fbneo"
]
},
{
@@ -4321,6 +4984,17 @@
],
"systems": [
"3do"
],
"alternatives": [
{
"sha1": "f590ccf688b4c05fa1da5c5dd92c224545170c3b",
"sha256": "c481bfde0ae68e5fbe70a406af1698fcba5b76d7e23c860c2715cacf019efad4",
"size": 10207,
"repo_path": "bios/Arcade/Arcade/isgsm.zip",
"cores": [
"fbneo"
]
}
]
},
{
@@ -4344,6 +5018,17 @@
"repo_path": "bios/Arcade/MAME/decocass.zip",
"cores": [
"mame"
],
"alternatives": [
{
"sha1": "1b33cf0e730a6cde96479b0face108b6931d433e",
"sha256": "bd86d293abab2e816d307626cdd793e2da2ed69c4124bf92f7e7eca69f9a5096",
"size": 22379,
"repo_path": "bios/Arcade/Arcade/decocass.zip",
"cores": [
"fbneo"
]
}
]
},
{
@@ -4492,7 +5177,8 @@
"size": 7950,
"repo_path": "bios/Arcade/Arcade/bubsys.zip",
"cores": [
"mame"
"mame",
"fbneo"
],
"systems": [
"3do"
@@ -4695,6 +5381,17 @@
"repo_path": "bios/Arcade/MAME/skns.zip",
"cores": [
"mame"
],
"alternatives": [
{
"sha1": "4257bd14b541fafbd555cb98ba079a3416a45934",
"sha256": "6edf22c68e70be06c9b32741e0d1df1aa38cf6fc162c1027c15d6c1d882f621b",
"size": 924762,
"repo_path": "bios/Arcade/Arcade/skns.zip",
"cores": [
"fbneo"
]
}
]
},
{
@@ -4796,6 +5493,17 @@
],
"systems": [
"3do"
],
"alternatives": [
{
"sha1": "4fb1f00681d2540b476add2def2bbf07ca3e3993",
"sha256": "1cf61e6b54589ed9bf2b5f319c2b56de205384d6726b7a0fae79213071f3cfb5",
"size": 49385,
"repo_path": "bios/Arcade/Arcade/atarisy1.zip",
"cores": [
"fbneo"
]
}
]
},
{
@@ -5052,7 +5760,8 @@
"size": 6800,
"repo_path": "bios/Bally/Astrocade/astrocde.zip",
"cores": [
"mame"
"mame",
"fbneo"
]
},
{
@@ -5672,6 +6381,17 @@
],
"systems": [
"3do"
],
"alternatives": [
{
"sha1": "06fc753d015b43ca1787f4cfd9331b1674202e64",
"sha256": "187ede0f57d1439092a7ac50ea923533871ab7ee65e29baabc4dcdfa7ad4823f",
"size": 7609,
"repo_path": "bios/Arcade/Arcade/ym2608.zip",
"cores": [
"fbneo"
]
}
]
},
{
@@ -10869,7 +11589,9 @@
"size": 7357,
"repo_path": "bios/Nintendo/3DS/aes_keys.txt",
"cores": [
"citra"
"citra",
"citra2018",
"citra_canary"
],
"systems": [
"nintendo-3ds"
+374 -23
View File
@@ -5,7 +5,7 @@
"platform": "emudeck",
"display_name": "EmuDeck",
"version": "1.0",
"generated": "2026-10-10T05:24:26Z",
"generated": "2026-10-15T21:45:48Z",
"base_destination": "bios",
"detect": [
{
@@ -256,6 +256,324 @@
"pack_size": 3472458179,
"total_omitted": 0,
"omitted_files": [],
"core_names": {
"aethersx2": [
"aethersx2-sa"
],
"armsx2": [
"armsx2_libretro"
],
"beetle_psx": [
"mednafen_psx",
"mednafen_psx_hw"
],
"beetle_saturn": [
"beetle-saturn",
"mednafen_saturn"
],
"cxbx-reloaded": [
"cxbx"
],
"fbalpha2012": [
"fbalpha"
],
"flycast": [
"flycast_dev",
"flycastvl"
],
"fpse-ng": [
"fpse64",
"fpseng"
],
"genesis_plus_gx": [
"genesisplusgx",
"genesisplusgx-expanded",
"genplus"
],
"genesis_plus_gx_wide": [
"genesisplusgx-wide",
"genesisplusgxwide"
],
"kenji-nx": [
"KENJI-NX",
"kenjinx"
],
"lime3ds": [
"lime-qt",
"lime3ds-gui"
],
"lrps2": [
"pcsx2"
],
"m64plus-fz": [
"M64PLUS-FZ",
"org.mupen64plusae.v3.fzurita",
"org.mupen64plusae.v3.fzurita.amazon",
"org.mupen64plusae.v3.fzurita.pro"
],
"mame": [
"imame4all"
],
"mame2016": [
"mess2016"
],
"mame4droid-2024": [
"MAME4DROID-2024",
"MAME4DROID-CURRENT",
"com.seleuco.mame4d2024"
],
"mandarine": [
"mandarine-qt"
],
"md-emu": [
"MD-EMU",
"MD.emu"
],
"melonds_ds": [
"melondsds"
],
"mesence": [
"mesen2"
],
"msx-emu": [
"MSX-EMU",
"MSX.emu"
],
"mupen64plus-ae": [
"MUPEN64PLUS-AE",
"org.mupen64plusae.v3.alpha"
],
"mupen64plus_next": [
"mupen64plus",
"mupen64plus-next",
"mupen64plus_next_develop",
"mupen64plus_next_gles2",
"mupen64plus_next_gles3"
],
"nethersx2": [
"NETHERSX2",
"NetherSX2"
],
"nethersx2-turnip": [
"NETHERSX2-TURNIP",
"NetherSX2-Turnip"
],
"nethersx2-turnip-classic": [
"NETHERSX2-TURNIP-CLASSIC",
"NetherSX2-Turnip-Classic"
],
"parallel-launcher": [
"ca.parallel_launcher.ParallelLauncher"
],
"parallel_n64": [
"parallel_n64_debug"
],
"pcsx2-legacy": [
"PCSX2-LEGACY"
],
"pcsx_rearmed": [
"pcsx_rearmed_interpreter",
"pcsx_rearmed_neon"
],
"pizza-boy-sc": [
"PIZZA-BOY-SC",
"Pizza Boy SC",
"it.dbtecno.pizzaboyscbasic",
"it.dbtecno.pizzaboyscpro"
],
"play": [
"play!"
],
"real3doplayer": [
"REAL3DOPLAYER",
"Real3DOPlayer"
],
"rosalies-mupen-gui": [
"RMG",
"com.github.Rosalie241.RMG"
],
"saturn-emu": [
"SATURN-EMU",
"Saturn.emu",
"saturnemu"
],
"shadps4": [
"shadps4-gui"
],
"simple64": [
"io.github.simple64.simple64",
"simple64-gui"
],
"skyline": [
"SKYLINE"
],
"smsplus": [
"smsplus_gx"
],
"triforce": [
"dolphin-triforce"
],
"x1-box": [
"x1box"
],
"yabasanshiro": [
"yabasanshiro-2"
]
},
"system_cores": {
"coleco-colecovision": [
"ares",
"bluemsx",
"clk",
"colem",
"fbneo",
"gearcoleco",
"jollycv",
"mesence",
"msx-emu",
"openmsx",
"smsplus",
"zesarux"
],
"nintendo-3ds": [
"azahar",
"azaharplus",
"citra",
"citra-mmj",
"citra2018",
"citra_canary",
"lime3ds",
"mandarine",
"panda3ds",
"trident"
],
"nintendo-64": [
"ares",
"gopher64",
"m64plus-fz",
"mupen64plus-ae",
"mupen64plus_next",
"parallel-launcher",
"parallel_n64",
"project64",
"rosalies-mupen-gui",
"simple64",
"sixtyforce"
],
"nintendo-ds": [
"desmume",
"desmume2015",
"drastic",
"melonds",
"melonds-nightly",
"melonds_ds",
"noods",
"skyemu"
],
"nintendo-gamecube": [
"dolphin",
"dolphin-mmjr",
"dolphin-mmjr2",
"dolphin_launcher",
"ishiiruka",
"primehack",
"triforce"
],
"nintendo-switch": [
"citron",
"eden",
"kenji-nx",
"ryujinx",
"skyline",
"suyu",
"yuzu"
],
"nintendo-wii-u": [
"cemu"
],
"panasonic-3do": [
"groovymame",
"mame",
"mame2015",
"mame2016",
"mame4droid-2024",
"opera",
"real3doplayer",
"ume2015"
],
"sega-dreamcast": [
"demul",
"flycast",
"flycast-dojo",
"redream",
"retrodream"
],
"sega-mega-cd": [
"ares",
"clownmdemu",
"genesis_plus_gx",
"genesis_plus_gx_wide",
"jgenesis",
"md-emu",
"mednafen",
"picodrive",
"pizza-boy-sc"
],
"sega-saturn": [
"ares",
"beetle_saturn",
"kronos",
"mednafen",
"saturn-emu",
"ssf",
"yabasanshiro",
"yabause",
"ymir"
],
"snk-neogeo": [
"ares",
"fbalpha2012",
"fbalpha2012_neogeo"
],
"sony-playstation": [
"ares",
"armsx1",
"beetle_psx",
"duckstation",
"epsxe",
"fpse",
"fpse-ng",
"mednafen",
"pcsx1",
"pcsx_rearmed",
"rustation",
"swanstation"
],
"sony-playstation-2": [
"aethersx2",
"armsx2",
"emucorex",
"lrps2",
"nethersx2",
"nethersx2-turnip",
"nethersx2-turnip-classic",
"pcee2",
"pcsx2",
"pcsx2-legacy",
"play",
"yaps2"
],
"sony-psp": [
"ppsspp"
],
"xbox": [
"cxbx-reloaded",
"directxbox",
"hakux",
"x1-box",
"xemu"
]
},
"files": [
{
"dest": "colecovision.rom",
@@ -648,7 +966,8 @@
"size": 16384,
"repo_path": "bios/Nintendo/Game Boy Advance/GBA_bios.rom",
"cores": [
"ares"
"ares",
"mgba"
]
},
{
@@ -1236,7 +1555,11 @@
"size": 1224,
"repo_path": "bios/Nintendo/Switch/title.keys",
"cores": [
"citron"
"citron",
"eden",
"ryujinx",
"suyu",
"yuzu"
],
"systems": [
"nintendo-switch"
@@ -1249,7 +1572,11 @@
"size": 451,
"repo_path": "bios/Other/citron/switch/console.keys",
"cores": [
"citron"
"citron",
"eden",
"ryujinx",
"suyu",
"yuzu"
],
"systems": [
"nintendo-switch"
@@ -1262,7 +1589,10 @@
"size": 160,
"repo_path": "bios/Other/citron/switch/key_retail.bin",
"cores": [
"citron"
"citron",
"eden",
"suyu",
"yuzu"
],
"systems": [
"nintendo-switch"
@@ -1275,7 +1605,8 @@
"size": 8192,
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -1288,7 +1619,8 @@
"size": 4096,
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_coef.bin.c116d867",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -1301,7 +1633,8 @@
"size": 6478,
"repo_path": "bios/Other/dolphin-mmjr/font_western.bin",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -1314,7 +1647,8 @@
"size": 259626,
"repo_path": "bios/Other/dolphin-mmjr/font_japanese.bin",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -1337,7 +1671,8 @@
"size": 16384,
"repo_path": "bios/Nintendo/Game Boy Advance/GBA_bios.rom",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -1350,7 +1685,8 @@
"size": 16384,
"repo_path": "bios/Nintendo/Wii/SYSCONF",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -1363,7 +1699,8 @@
"size": 256,
"repo_path": "bios/Nintendo/Wii/setting.txt",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -1376,7 +1713,8 @@
"size": 134217728,
"repo_path": "bios/Nintendo/Wii/WiiSD.raw",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -1391,7 +1729,8 @@
"size": 1005,
"repo_path": "bios/Nintendo/Wii/clientca.pem",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -1404,7 +1743,8 @@
"size": 609,
"repo_path": "bios/Nintendo/Wii/clientcakey.pem",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -1417,7 +1757,8 @@
"size": 897,
"repo_path": "bios/Nintendo/Wii/rootca.pem",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -2636,7 +2977,8 @@
"size": 4194304,
"repo_path": "bios/Sony/PlayStation 2/ps2_scph50006.bin",
"cores": [
"duckstation"
"duckstation",
"pcsx2"
],
"systems": [
"sony-playstation"
@@ -2676,7 +3018,8 @@
"size": 4194304,
"repo_path": "bios/Sony/PlayStation 2/ps2_scph50008.bin",
"cores": [
"duckstation"
"duckstation",
"pcsx2"
],
"systems": [
"sony-playstation"
@@ -6534,7 +6877,11 @@
"size": 16025,
"repo_path": "bios/Nintendo/Switch/prod.keys",
"cores": [
"citron"
"citron",
"eden",
"ryujinx",
"suyu",
"yuzu"
],
"systems": [
"nintendo-switch"
@@ -6547,7 +6894,8 @@
"size": 2097152,
"repo_path": "bios/Nintendo/GameCube/GC/USA/IPL.bin",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -6566,7 +6914,8 @@
"size": 2097152,
"repo_path": "bios/Nintendo/GameCube/GC/EUR/IPL.bin",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -6585,7 +6934,8 @@
"size": 2097152,
"repo_path": "bios/Nintendo/GameCube/GC/JAP/IPL.bin",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -6604,7 +6954,8 @@
"size": 64,
"repo_path": "bios/Sony/PlayStation 2/eeprom.dat",
"cores": [
"armsx2"
"armsx2",
"pcsx2"
],
"systems": [
"sony-playstation-2"
+1908 -140
View File
File diff suppressed because it is too large. Load diff
+3 -1
View File
@@ -5,7 +5,7 @@
"platform": "misterfpga",
"display_name": "MiSTer FPGA",
"version": "1.0",
"generated": "2026-10-10T05:24:26Z",
"generated": "2026-10-15T21:45:48Z",
"base_destination": "games",
"detect": [
{
@@ -22,6 +22,8 @@
"pack_size": 29969609,
"total_omitted": 0,
"omitted_files": [],
"core_names": {},
"system_cores": {},
"files": [
{
"dest": "3DO/boot.rom",
+1726 -131
View File
File diff suppressed because it is too large. Load diff
+1908 -140
View File
File diff suppressed because it is too large. Load diff
+1301 -399
View File
File diff suppressed because it is too large. Load diff
+1407 -190
View File
File diff suppressed because it is too large. Load diff
+2047 -230
View File
File diff suppressed because it is too large. Load diff
+2055 -243
View File
File diff suppressed because it is too large. Load diff
+829 -17
View File
@@ -5,7 +5,7 @@
"platform": "romm",
"display_name": "RomM",
"version": "1.0",
"generated": "2026-10-10T05:24:26Z",
"generated": "2026-10-15T21:45:48Z",
"base_destination": "bios",
"detect": [
{
@@ -602,6 +602,793 @@
]
}
],
"core_names": {
"3dsen": [
"3dsen-windows"
],
"ace-dl": [
"acedl"
],
"aethersx2": [
"aethersx2-sa"
],
"armsx2": [
"armsx2_libretro"
],
"beetle_gba": [
"mednafen_gba"
],
"beetle_lynx": [
"mednafen_lynx"
],
"beetle_pce": [
"mednafen_pce"
],
"beetle_pcfx": [
"mednafen_pcfx",
"pcfx"
],
"beetle_psx": [
"mednafen_psx",
"mednafen_psx_hw"
],
"beetle_saturn": [
"beetle-saturn",
"mednafen_saturn"
],
"bsnes2014": [
"bsnes2014_accuracy",
"bsnes2014_balanced",
"bsnes2014_performance"
],
"bsnes_hd_beta": [
"bsnes_hd",
"bsneshd"
],
"bsnes_mercury": [
"bsnes_mercury_accuracy",
"bsnes_mercury_balanced",
"bsnes_mercury_performance"
],
"cap32": [
"caprice32"
],
"cxbx-reloaded": [
"cxbx"
],
"dosbox-x": [
"dosboxx"
],
"doublecherrygb": [
"DoubleCherryGB"
],
"ep128emu": [
"ep128emu_core"
],
"ep128emu_core": [
"ep128emu-core"
],
"fpse-ng": [
"fpse64",
"fpseng"
],
"gba-emu": [
"GBA-EMU",
"GBA.emu"
],
"gbc-emu": [
"GBC-EMU",
"GBC.emu"
],
"genesis_plus_gx": [
"genesisplusgx",
"genesisplusgx-expanded",
"genplus"
],
"genesis_plus_gx_wide": [
"genesisplusgx-wide",
"genesisplusgxwide"
],
"higan_sfc_balanced": [
"nside_sfc_balanced"
],
"idtech4a++": [
"diii4a",
"idtech4amm"
],
"j2me-loader": [
"J2ME-LOADER"
],
"jl-mod": [
"JL-MOD"
],
"kemulator": [
"KEMULATOR"
],
"linkboy": [
"LINKBOY",
"Linkboy"
],
"lrps2": [
"pcsx2"
],
"lynx-emu": [
"LYNX-EMU",
"Lynx.emu"
],
"m64plus-fz": [
"M64PLUS-FZ",
"org.mupen64plusae.v3.fzurita",
"org.mupen64plusae.v3.fzurita.amazon",
"org.mupen64plusae.v3.fzurita.pro"
],
"mame": [
"imame4all"
],
"mame2016": [
"mess2016"
],
"mame4droid-2024": [
"MAME4DROID-2024",
"MAME4DROID-CURRENT",
"com.seleuco.mame4d2024"
],
"mastergear": [
"MASTERGEAR",
"com.fms.mg"
],
"md-emu": [
"MD-EMU",
"MD.emu"
],
"mednafen_pce": [
"pce"
],
"mednafen_pce_fast": [
"beetle_pce_fast",
"pce_fast"
],
"mednafen_snes": [
"beetle-bsnes",
"beetle_bsnes"
],
"mednafen_supafaust": [
"supafaust"
],
"mednafen_supergrafx": [
"beetle_supergrafx"
],
"melonds_ds": [
"melondsds"
],
"mesen": [
"mesens"
],
"mesen-s": [
"mesen_s"
],
"mesence": [
"mesen2"
],
"msx-emu": [
"MSX-EMU",
"MSX.emu"
],
"mupen64plus-ae": [
"MUPEN64PLUS-AE",
"org.mupen64plusae.v3.alpha"
],
"mupen64plus_next": [
"mupen64plus",
"mupen64plus-next",
"mupen64plus_next_develop",
"mupen64plus_next_gles2",
"mupen64plus_next_gles3"
],
"my-boy": [
"MY-BOY",
"My Boy!"
],
"my-oldboy": [
"MY-OLDBOY",
"My OldBoy!"
],
"nes-emu": [
"NES-EMU",
"NES.emu"
],
"nesoid": [
"NESOID",
"Nesoid"
],
"nethersx2": [
"NETHERSX2",
"NetherSX2"
],
"nethersx2-turnip": [
"NETHERSX2-TURNIP",
"NetherSX2-Turnip"
],
"nethersx2-turnip-classic": [
"NETHERSX2-TURNIP-CLASSIC",
"NetherSX2-Turnip-Classic"
],
"o2em": [
"o2em_libretro"
],
"parallel-launcher": [
"ca.parallel_launcher.ParallelLauncher"
],
"parallel_n64": [
"parallel_n64_debug"
],
"pce-emu": [
"PCE-EMU",
"PCE.emu",
"com.PceEmu",
"pceemu"
],
"pcsx2-legacy": [
"PCSX2-LEGACY"
],
"pcsx_rearmed": [
"pcsx_rearmed_interpreter",
"pcsx_rearmed_neon"
],
"pizza-boy-gba": [
"PIZZA-BOY-GBA",
"Pizza Boy GBA"
],
"pizza-boy-gbc": [
"PIZZA-BOY-GBC",
"Pizza Boy GBC"
],
"pizza-boy-sc": [
"PIZZA-BOY-SC",
"Pizza Boy SC",
"it.dbtecno.pizzaboyscbasic",
"it.dbtecno.pizzaboyscpro"
],
"play": [
"play!"
],
"puae": [
"uae"
],
"real3doplayer": [
"REAL3DOPLAYER",
"Real3DOPlayer"
],
"rosalies-mupen-gui": [
"RMG",
"com.github.Rosalie241.RMG"
],
"saturn-emu": [
"SATURN-EMU",
"Saturn.emu",
"saturnemu"
],
"smsplus": [
"smsplus_gx"
],
"snes9x": [
"pocketsnes",
"snes9x_next"
],
"snes9x-explus": [
"SNES9X-EXPLUS",
"Snes9x EX+",
"Snes9xEXPlus"
],
"speccy": [
"SPECCY",
"com.fms.speccy",
"com.fms.speccy.deluxe"
],
"superzsnes": [
"SUPERZSNES"
],
"tgbdual": [
"TGB Dual"
],
"triforce": [
"dolphin-triforce"
],
"vbam": [
"vba-m"
],
"x1-box": [
"x1box"
],
"xm6pro68k": [
"XM6PRO68K",
"XM6PRO68K-WINDOWS",
"xm6pro"
],
"xm6typeg": [
"XM6TYPEG",
"XM6TYPEG-WINDOWS"
],
"yabasanshiro": [
"yabasanshiro-2"
]
},
"system_cores": {
"3do": [
"groovymame",
"mame",
"mame2015",
"mame2016",
"mame4droid-2024",
"opera",
"real3doplayer",
"ume2015"
],
"amstrad-cpc": [
"ace-dl",
"cap32",
"clk",
"cpcemu",
"crocods",
"ep128emu",
"ep128emu_core",
"zesarux"
],
"apple-macintosh-ii": [
"basiliskii",
"minivmac"
],
"atari-400-800": [
"altirra",
"atari800"
],
"atari-5200": [
"a5200",
"altirra",
"atari800"
],
"atari-7800": [
"a7800",
"prosystem"
],
"atari-lynx": [
"beetle_lynx",
"gearlynx",
"handy",
"holani",
"lynx-emu",
"mednafen"
],
"atari-st": [
"clk",
"hatari",
"hatari2014",
"hatarib"
],
"coleco-colecovision": [
"ares",
"bluemsx",
"clk",
"colem",
"fbneo",
"gearcoleco",
"jollycv",
"mesence",
"msx-emu",
"openmsx",
"smsplus",
"zesarux"
],
"commodore-amiga": [
"amiberry",
"clk",
"fsuae",
"puae",
"puae2021",
"uae4arm"
],
"doom": [
"gzdoom",
"idtech4a++",
"prboom",
"prboom-plus"
],
"enterprise-64-128": [
"clk"
],
"fairchild-channel-f": [
"fbneo",
"freechaf"
],
"j2me": [
"freej2me",
"freej2me_plus",
"j2me-loader",
"jl-mod",
"kemulator",
"squirreljme"
],
"magnavox-odyssey2": [
"o2em"
],
"mattel-intellivision": [
"freeintv"
],
"microsoft-msx": [
"ares",
"bluemsx",
"clk",
"fbneo",
"fmsx",
"msx-emu",
"openmsx",
"zesarux"
],
"microsoft-xbox": [
"cxbx-reloaded",
"directxbox",
"hakux",
"x1-box",
"xemu"
],
"nec-pc-98": [
"dosbox-x",
"nekop2",
"np2kai"
],
"nec-pc-engine": [
"ares",
"beetle_pce",
"geargrafx",
"jgenesis",
"mednafen",
"mednafen_pce",
"mednafen_pce_fast",
"mednafen_supergrafx",
"mesence",
"pce-emu"
],
"nec-pc-fx": [
"beetle_pcfx",
"mednafen"
],
"nintendo-64dd": [
"ares",
"m64plus-fz",
"mupen64plus-ae",
"mupen64plus_next",
"parallel-launcher",
"parallel_n64",
"project64",
"rosalies-mupen-gui",
"sixtyforce"
],
"nintendo-ds": [
"desmume",
"desmume2015",
"drastic",
"melonds",
"melonds-nightly",
"melonds_ds",
"noods",
"skyemu"
],
"nintendo-fds": [
"fbneo",
"fceumm",
"fixnes",
"ines",
"mednafen",
"mesen",
"mesence",
"nes-emu",
"nesoid",
"nestopia",
"punes",
"rustynes"
],
"nintendo-gamecube": [
"dolphin",
"dolphin-mmjr",
"dolphin-mmjr2",
"dolphin_launcher",
"ishiiruka",
"primehack",
"triforce"
],
"nintendo-gb": [
"boytacean",
"doublecherrygb",
"emux_gb",
"gambatte",
"gbc-emu",
"gearboy",
"irogb",
"jgenesis",
"linkboy",
"mednafen",
"mesen-s",
"mesence",
"mgba",
"my-oldboy",
"pizza-boy-gbc",
"sameboy",
"skyemu",
"tgbdual",
"vbam"
],
"nintendo-gba": [
"beetle_gba",
"dolphin",
"gba-emu",
"gpsp",
"jgenesis",
"linkboy",
"mednafen",
"mednafen_gba",
"mesence",
"meteor",
"mgba",
"mgba_splitscreen",
"my-boy",
"noods",
"pizza-boy-gba",
"primehack",
"skyemu",
"tempgba",
"triforce",
"vba_next",
"vbam"
],
"nintendo-gbc": [
"boytacean",
"doublecherrygb",
"fixgb",
"gambatte",
"gbc-emu",
"gearboy",
"irogb",
"jgenesis",
"linkboy",
"mednafen",
"mesen-s",
"mesence",
"mgba",
"my-oldboy",
"pizza-boy-gbc",
"sameboy",
"skyemu",
"tgbdual",
"vbam"
],
"nintendo-nes": [
"3dsen",
"bnes",
"emux_nes",
"fceumm",
"fixnes",
"ines",
"jgenesis",
"mednafen",
"mesen",
"mesence",
"nes",
"nes-emu",
"nesoid",
"nestopia",
"plastic",
"punes",
"quicknes",
"rustynes"
],
"nintendo-pokemon-mini": [
"pokemini"
],
"nintendo-satellaview": [
"bsnes",
"bsnes-jg",
"bsnes2014",
"bsnes_cplusplus98",
"bsnes_hd_beta",
"bsnes_mercury",
"chimerasnes",
"mesen-s",
"mesence",
"snes9x",
"snes9x-explus",
"snes9x2002",
"snes9x2005",
"snes9x2005_plus",
"snes9x2010"
],
"nintendo-sgb": [
"bsnes-jg",
"bsnes2014",
"bsnes_hd_beta",
"bsnes_mercury",
"higan_sfc",
"higan_sfc_balanced",
"linkboy",
"mgba",
"my-oldboy",
"pizza-boy-gbc",
"sameboy"
],
"nintendo-snes": [
"bsnes",
"bsnes-jg",
"bsnes2014",
"bsnes_cplusplus98",
"bsnes_hd_beta",
"bsnes_mercury",
"chimerasnes",
"fbneo",
"higan_sfc",
"higan_sfc_balanced",
"jgenesis",
"mednafen",
"mednafen_snes",
"mednafen_supafaust",
"mesen-s",
"mesence",
"snes9x",
"snes9x-explus",
"snes9x2002",
"snes9x2005",
"snes9x2005_plus",
"snes9x2010",
"superzsnes"
],
"nintendo-sufami-turbo": [
"bsnes-jg",
"bsnes2014",
"bsnes_cplusplus98",
"bsnes_hd_beta",
"bsnes_mercury",
"chimerasnes",
"mesence",
"snes9x",
"snes9x-explus",
"snes9x2002",
"snes9x2005",
"snes9x2005_plus",
"snes9x2010"
],
"philips-videopac": [
"o2em"
],
"scummvm": [
"scummvm"
],
"sega-game-gear": [
"ares",
"gearsystem",
"genesis_plus_gx",
"genesis_plus_gx_wide",
"jgenesis",
"mastergear",
"mednafen",
"mesence",
"picodrive",
"pizza-boy-sc",
"smsplus"
],
"sega-master-system": [
"ares",
"blastem",
"clk",
"gearsystem",
"genesis_plus_gx",
"genesis_plus_gx_wide",
"jgenesis",
"mastergear",
"md-emu",
"mednafen",
"mesence",
"picodrive",
"pizza-boy-sc",
"smsplus",
"zesarux"
],
"sega-mega-cd": [
"ares",
"clownmdemu",
"genesis_plus_gx",
"genesis_plus_gx_wide",
"jgenesis",
"md-emu",
"mednafen",
"picodrive",
"pizza-boy-sc"
],
"sega-mega-drive": [
"ares",
"blastem",
"clownmdemu",
"genesis_plus_gx",
"genesis_plus_gx_wide",
"jgenesis",
"md-emu",
"mednafen",
"picodrive",
"pizza-boy-sc"
],
"sega-saturn": [
"ares",
"beetle_saturn",
"kronos",
"mednafen",
"saturn-emu",
"ssf",
"yabasanshiro",
"yabause",
"ymir"
],
"sharp-x1": [
"x1"
],
"sharp-x68000": [
"px68k",
"xm6pro68k",
"xm6typeg"
],
"sinclair-zx-spectrum": [
"81",
"ares",
"clk",
"cspect",
"ep128emu",
"ep128emu_core",
"fuse",
"speccy",
"zesarux"
],
"snk-neogeo-cd": [
"fbneo",
"fbneo_neogeo",
"geolith",
"neocd"
],
"sony-playstation": [
"ares",
"armsx1",
"beetle_psx",
"duckstation",
"epsxe",
"fpse",
"fpse-ng",
"mednafen",
"pcsx1",
"pcsx_rearmed",
"rustation",
"swanstation"
],
"sony-playstation-2": [
"aethersx2",
"armsx2",
"emucorex",
"lrps2",
"nethersx2",
"nethersx2-turnip",
"nethersx2-turnip-classic",
"pcee2",
"pcsx2",
"pcsx2-legacy",
"play",
"yaps2"
],
"sony-psp": [
"ppsspp"
],
"videoton-tvc": [
"ep128emu",
"ep128emu_core"
],
"wolfenstein-3d": [
"ecwolf",
"idtech4a++"
]
},
"files": [
{
"dest": "3do/3do_arcade_saot.bin",
@@ -4892,7 +5679,8 @@
"size": 524288,
"repo_path": "bios/Sony/PlayStation/psxonpsp660.bin",
"cores": [
"beetle_psx"
"beetle_psx",
"pcsx_rearmed"
],
"systems": [
"sony-playstation"
@@ -6420,7 +7208,9 @@
"size": 2097152,
"repo_path": "bios/Nintendo/GameCube/GC/USA/IPL.bin",
"cores": [
"dolphin"
"dolphin",
"ishiiruka",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -6439,7 +7229,9 @@
"size": 2097152,
"repo_path": "bios/Nintendo/GameCube/GC/EUR/IPL.bin",
"cores": [
"dolphin"
"dolphin",
"ishiiruka",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -6458,7 +7250,9 @@
"size": 2097152,
"repo_path": "bios/Nintendo/GameCube/GC/JAP/IPL.bin",
"cores": [
"dolphin"
"dolphin",
"ishiiruka",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -6477,7 +7271,9 @@
"size": 8192,
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_rom.bin.3c6cc6e0",
"cores": [
"dolphin"
"dolphin",
"ishiiruka",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -6490,7 +7286,9 @@
"size": 4096,
"repo_path": "bios/Nintendo/GameCube/.variants/dsp_coef.bin.c116d867",
"cores": [
"dolphin"
"dolphin",
"ishiiruka",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -6503,7 +7301,9 @@
"size": 6478,
"repo_path": "bios/Other/dolphin-mmjr/font_western.bin",
"cores": [
"dolphin"
"dolphin",
"ishiiruka",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -6516,7 +7316,9 @@
"size": 259626,
"repo_path": "bios/Other/dolphin-mmjr/font_japanese.bin",
"cores": [
"dolphin"
"dolphin",
"ishiiruka",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -6539,7 +7341,9 @@
"size": 16384,
"repo_path": "bios/Nintendo/Wii/SYSCONF",
"cores": [
"dolphin"
"dolphin",
"ishiiruka",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -6552,7 +7356,9 @@
"size": 256,
"repo_path": "bios/Nintendo/Wii/setting.txt",
"cores": [
"dolphin"
"dolphin",
"ishiiruka",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -6565,7 +7371,8 @@
"size": 134217728,
"repo_path": "bios/Nintendo/Wii/WiiSD.raw",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -6580,7 +7387,8 @@
"size": 1005,
"repo_path": "bios/Nintendo/Wii/clientca.pem",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -6593,7 +7401,8 @@
"size": 609,
"repo_path": "bios/Nintendo/Wii/clientcakey.pem",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -6606,7 +7415,8 @@
"size": 897,
"repo_path": "bios/Nintendo/Wii/rootca.pem",
"cores": [
"dolphin"
"dolphin",
"primehack"
],
"systems": [
"nintendo-gamecube"
@@ -6619,7 +7429,8 @@
"size": 32768,
"repo_path": "bios/Enterprise/64-128/exdos14isdos10uk.rom",
"cores": [
"ep128emu"
"ep128emu",
"ep128emu_core"
]
},
{
@@ -6629,7 +7440,8 @@
"size": 32768,
"repo_path": "bios/Enterprise/64-128/epdos16f.rom",
"cores": [
"ep128emu"
"ep128emu",
"ep128emu_core"
]
},
{
+71
View File
@@ -96,6 +96,30 @@
"type": "integer",
"minimum": 0
},
"core_names": {
"description": "Each core a file names, with the other names it goes by upstream (the buildbot's mednafen_psx for beetle_psx).",
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
},
"uniqueItems": true
}
},
"system_cores": {
"description": "Each platform system with the cores whose presence keeps it; a system absent here is kept whatever cores are available.",
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
},
"uniqueItems": true
}
},
"omitted_files": {
"type": "array",
"items": {
@@ -377,6 +401,53 @@
"priority": {
"description": "The rank the code searches the file at, lower first: a world file withdraws an unmatched regional one only when it ranks no worse.",
"type": "integer"
},
"alternatives": {
"description": "Copies other cores read at this destination when they disagree on its content, in the order the pack builder tries them; a narrowing that drops the entry's cores takes the first whose cores remain.",
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"sha1",
"sha256",
"size",
"repo_path",
"cores"
],
"properties": {
"sha1": {
"type": "string",
"pattern": "^[a-f0-9]{40}$"
},
"sha256": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
},
"size": {
"type": "integer",
"minimum": 0
},
"repo_path": {
"type": "string"
},
"storage": {
"const": "release"
},
"release_asset": {
"type": "string",
"pattern": "^[^/\\\\]+$"
},
"cores": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"uniqueItems": true
}
}
}
}
},
"oneOf": [
+53 -54
View File
@@ -1518,15 +1518,7 @@ def resolve_platform_cores(
}
if target_cores is not None:
# Build reverse index: upstream name -> profile key
# Upstream sources (buildbot, es_systems) may use different names
# than our profile keys (e.g., mednafen_psx vs beetle_psx).
# The profiles' cores: field lists these alternate names.
index = upstream_profile_index(profiles, include_aliases=True)
expanded = {
profile for c in target_cores for profile in index.get(c, {c})
}
result = result & expanded
result = result & designated_profiles(profiles, target_cores)
return result
@@ -1617,40 +1609,39 @@ def _norm_system_id(sid: str) -> str:
return s.replace("-", "")
def filter_systems_by_target(
def designated_profiles(profiles: dict[str, dict], names: set[str]) -> set[str]:
"""The profiles a list of upstream core names designates.
The buildbot and es_systems name cores their own way (`mednafen_psx` for
beetle_psx); a profile's `cores:` lists those names. A name no profile
claims stands for itself.
"""
index = upstream_profile_index(profiles, include_aliases=True)
return {profile for c in names for profile in index.get(c, {c})}
def system_core_reach(
systems: dict[str, dict],
profiles: dict[str, dict],
target_cores: set[str] | None,
platform_cores: set[str] | None = None,
) -> dict[str, dict]:
"""Filter platform systems to only those reachable by target cores.
platform_cores: set[str] | None,
) -> dict[str, set[str] | None]:
"""For each platform system, the cores whose presence keeps it.
A system is reachable if at least one core that emulates it is available
on the target. Only considers cores relevant to the platform (from
platform_cores). Systems whose cores are all outside the platform's
scope are kept (no information to exclude them).
Returns the filtered systems dict (or all if no target).
A system is reachable if at least one core that emulates it is available.
Only cores relevant to the platform (platform_cores) can rule a system
out: a system whose cores are all outside the platform's scope maps to
None, kept whatever is available, there being no information to exclude
it.
"""
if target_cores is None:
return systems
# Build reverse index for target core name resolution
index = upstream_profile_index(profiles, include_aliases=True)
expanded_target = {
profile for c in target_cores for profile in index.get(c, {c})
}
_norm_sid = _norm_system_id
# Build normalized system -> cores from ALL profiles
# Normalized system -> cores, from ALL profiles
norm_system_cores: dict[str, set[str]] = {}
for name, p in profiles.items():
if p.get("type") == "alias":
continue
for sid in p.get("systems", []):
norm_key = _norm_sid(sid)
norm_system_cores.setdefault(norm_key, set()).add(name)
norm_system_cores.setdefault(_norm_sid(sid), set()).add(name)
# Platform-scoped mapping (for distinguishing "no info" from "known but off-target")
norm_plat_system_cores: dict[str, set[str]] = {}
@@ -1658,14 +1649,16 @@ def filter_systems_by_target(
for name in platform_cores:
p = profiles.get(name, {})
for sid in p.get("systems", []):
norm_key = _norm_sid(sid)
norm_plat_system_cores.setdefault(norm_key, set()).add(name)
norm_plat_system_cores.setdefault(_norm_sid(sid), set()).add(name)
# A platform can file a system under an id no profile uses: Batocera's
# enterprise-64-128 is ep128emu's enterprise-64 and enterprise-128, and
# only CLK, off most targets, carries the joined id. The destinations a
# system declares name the cores that read its files. A bare name proves
# nothing (bios.bin belongs to many systems), so only a path counts.
# The evidence only keeps: a path shared with an off-target core says
# nothing of the other files, and Batocera's bbc runs on MAME, which no
# profile ties to bbcb.zip, while CLK reads two of its ROMs.
dest_cores: dict[str, set[str]] = {}
for name, p in profiles.items():
if p.get("type") == "alias":
@@ -1681,9 +1674,12 @@ def filter_systems_by_target(
if "/" in dest:
dest_cores.setdefault(dest, set()).add(name)
filtered = {}
reach: dict[str, set[str] | None] = {}
for sys_id, sys_data in systems.items():
norm_key = _norm_sid(sys_id)
if not norm_plat_system_cores.get(norm_key):
reach[sys_id] = None
continue
file_cores = {
core
for fe in sys_data.get("files", [])
@@ -1692,26 +1688,29 @@ def filter_systems_by_target(
(),
)
}
all_cores = norm_system_cores.get(norm_key, set())
plat_cores_here = norm_plat_system_cores.get(norm_key, set())
reach[sys_id] = file_cores | norm_system_cores.get(norm_key, set())
return reach
if file_cores & expanded_target:
# An on-target core reads the system's files. The evidence only
# keeps: a path shared with an off-target core says nothing of the
# other files, and Batocera's bbc runs on MAME, which no profile
# ties to bbcb.zip, while CLK reads two of its ROMs.
filtered[sys_id] = sys_data
elif not all_cores and not plat_cores_here:
# No profile maps to this system -keep it
filtered[sys_id] = sys_data
elif all_cores & expanded_target:
# At least one core is on the target
filtered[sys_id] = sys_data
elif not plat_cores_here:
# Platform resolution didn't find cores for this system -keep it
filtered[sys_id] = sys_data
# else: known cores exist but none are on the target -exclude
return filtered
def filter_systems_by_target(
systems: dict[str, dict],
profiles: dict[str, dict],
target_cores: set[str] | None,
platform_cores: set[str] | None = None,
) -> dict[str, dict]:
"""Filter platform systems to only those reachable by target cores.
Returns the filtered systems dict (or all if no target).
"""
if target_cores is None:
return systems
available = designated_profiles(profiles, target_cores)
reach = system_core_reach(systems, profiles, platform_cores)
return {
sys_id: sys_data
for sys_id, sys_data in systems.items()
if reach[sys_id] is None or reach[sys_id] & available
}
def platform_declarations(
+84
View File
@@ -60,6 +60,8 @@ from common import (
resolve_local_file,
sanitize_pack_path,
select_emulator_profiles,
system_core_reach,
upstream_profile_index,
yaml_load,
)
import packresolve
@@ -3236,6 +3238,46 @@ def _manifest_core_entries(
pack_only_sizes.append(file_size)
continue
# Every other core that reads this destination, in the order the
# builder meets them: a pack built without the earlier cores carries
# the first remaining one's copy. A claim joins the copy just before
# it when the bytes agree, and starts an alternative otherwise; it
# never joins an earlier copy, which would let it win over a core
# the builder tries first. Only a core's first claim can win: its
# later ones are reached only when that one is.
claimed = set(entry["cores"])
for claim in fe.get("other_claims", []):
claim_emu = claim.get("source_profile") or claim.get("source_emulator", "")
if not claim_emu or claim_emu in claimed:
continue
claimed.add(claim_emu)
if required_only and claim.get("required") is False:
continue
claim_path, claim_status = resolve_file(
claim, db, bios_dir, zip_contents,
dest_hint=claim.get("destination", ""), offline=offline,
)
if claim_status in ("not_found", "external", "user_provided") or not claim_path:
continue
claim_path, _ = validated_choice(
claim, claim_path, db, validation_index or {}, bios_dir, None, dest, owners,
)
copy, _size = _manifest_entry(
claim_path, manifest_dest, [claim_emu], db, repo_root, {}
)
if copy is None:
continue
alternatives = entry.setdefault("alternatives", [])
last = alternatives[-1] if alternatives else entry
if copy["sha1"] == last["sha1"]:
if claim_emu not in last["cores"]:
last["cores"].append(claim_emu)
continue
del copy["dest"]
alternatives.append(copy)
if not entry.get("alternatives"):
entry.pop("alternatives", None)
manifest_files.append(entry)
omitted_by_destination.pop(full_dest, None)
total_size += file_size
@@ -3553,6 +3595,9 @@ def generate_manifest(
]
now = _build_timestamp(db)
core_names, system_cores = _manifest_core_index(
config, emu_profiles, manifest_files, omitted_by_destination.values()
)
result: dict = {
"manifest_version": 2,
@@ -3581,11 +3626,50 @@ def generate_manifest(
"omitted_files": sorted(
omitted_by_destination.values(), key=lambda entry: entry["dest"]
),
"core_names": core_names,
"system_cores": system_cores,
"files": manifest_files,
}
return result
def _manifest_core_index(
config: dict,
profiles: dict[str, dict],
files: list[dict],
omitted,
) -> tuple[dict[str, list[str]], dict[str, list[str]]]:
"""What the installer needs to narrow by a list of core names.
A target list, or the cores found installed, names cores the way the
buildbot does, and a file names the profile that reads it: `core_names`
gives each profile the other names it goes by. `system_cores` gives each
platform system the cores whose presence keeps it, as
`filter_systems_by_target` decides; a system absent from it is kept
whatever is available.
"""
reach = system_core_reach(
config.get("systems", {}), profiles, _platform_cores(config, profiles)
)
system_cores = {
sys_id: sorted(cores) for sys_id, cores in sorted(reach.items())
if cores is not None
}
named = {
c for entry in [*files, *omitted]
for copy in [entry, *entry.get("alternatives", [])]
for c in copy.get("cores") or []
}
named |= {c for cores in system_cores.values() for c in cores}
aliases: dict[str, set[str]] = {}
for name, claimants in upstream_profile_index(profiles, include_aliases=True).items():
for profile in claimants & named:
if name != profile:
aliases.setdefault(profile, set()).add(name)
core_names = {profile: sorted(names) for profile, names in sorted(aliases.items())}
return core_names, system_cores
# Post-generation pack verification + manifest + SHA256SUMS
+132 -80
View File
@@ -158,6 +158,7 @@ def _agnostic_scan_extras(
seen_dests: set,
extras_prefix: str,
standalone_set: set[str] | None = None,
taken: dict[str, dict] | None = None,
) -> list[dict]:
"""Every interchangeable candidate a filename-agnostic core accepts.
@@ -261,33 +262,34 @@ def _agnostic_scan_extras(
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
if full_dest in seen_dests:
if full_dest in seen_dests and (taken is None or full_dest not in taken):
continue
seen_dests.add(full_dest)
extras.append(
{
"name": scan_name,
"destination": dest,
# The scan already holds the entry it selected, so it
# names it by content. Emitting the filename alone
# sent the packing step back to a by-name lookup, and
# three PS2 resources came back from another
# emulator's copy of the same filename.
"sha1": sha1,
"required": False,
"hle_fallback": False,
"source_emulator": profile.get("emulator", emu_name),
"source_profile": emu_name,
"source_system": f.get("system"),
"source_systems": list(profile.get("systems", [])),
"region": f.get("region"),
"variant_group": f.get("variant_group"),
"agnostic_scan": True,
}
)
extra = {
"name": scan_name,
"destination": dest,
# The scan already holds the entry it selected, so it
# names it by content. Emitting the filename alone
# sent the packing step back to a by-name lookup, and
# three PS2 resources came back from another
# emulator's copy of the same filename.
"sha1": sha1,
"required": False,
"hle_fallback": False,
"source_emulator": profile.get("emulator", emu_name),
"source_profile": emu_name,
"source_system": f.get("system"),
"source_systems": list(profile.get("systems", [])),
"region": f.get("region"),
"variant_group": f.get("variant_group"),
"agnostic_scan": True,
}
extra["claim_order"] = (4, 0, emu_name)
if not _take(extra, full_dest, seen_dests, taken):
continue
extras.append(extra)
extras.extend(_companion_extras(
_companions(path, scan_name, _companion_extensions(profile, fname), files_db),
folder, extras_prefix, seen_dests, profile, emu_name, f,
folder, extras_prefix, seen_dests, profile, emu_name, f, taken,
))
return extras
@@ -300,16 +302,16 @@ def _companion_extras(
profile: dict,
emu_name: str,
seed: dict,
taken: dict[str, dict] | None = None,
) -> list[dict]:
"""Extras for the companions of one scanned image, placed beside it."""
extras: list[dict] = []
for companion_sha1, companion in companions:
companion_dest = f"{folder}/{companion['name']}" if folder else companion["name"]
companion_full = f"{extras_prefix}/{companion_dest}" if extras_prefix else companion_dest
if companion_full in seen_dests:
if companion_full in seen_dests and (taken is None or companion_full not in taken):
continue
seen_dests.add(companion_full)
extras.append({
extra = {
"name": companion["name"],
"destination": companion_dest,
"sha1": companion_sha1,
@@ -322,7 +324,10 @@ def _companion_extras(
"region": seed.get("region"),
"variant_group": seed.get("variant_group"),
"agnostic_scan": True,
})
}
extra["claim_order"] = (4, 0, emu_name)
if _take(extra, companion_full, seen_dests, taken):
extras.append(extra)
return extras
@@ -368,6 +373,7 @@ def _archive_prefix_extras(
seen_dests: set,
extras_prefix: str,
by_name: dict,
taken: dict[str, dict] | None = None,
) -> list[dict]:
"""A second copy of an archive under the subdirectory a core reads.
@@ -398,25 +404,25 @@ def _archive_prefix_extras(
continue
dest = f"{prefix}/{archive_name}"
full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest
if full_dest in seen_dests:
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
seen_dests.add(full_dest)
extras.append(
{
"name": archive_name,
"destination": dest,
"required": True,
"hle_fallback": False,
"source_emulator": profile.get("emulator", emu_name),
"source_profile": emu_name,
"source_system": archive_entry.get("system"),
"source_systems": list(profile.get("systems", [])),
"region": archive_entry.get("region"),
"variant_group": archive_entry.get("variant_group"),
}
)
extra = {
"name": archive_name,
"destination": dest,
"required": True,
"hle_fallback": False,
"source_emulator": profile.get("emulator", emu_name),
"source_profile": emu_name,
"source_system": archive_entry.get("system"),
"source_systems": list(profile.get("systems", [])),
"region": archive_entry.get("region"),
"variant_group": archive_entry.get("variant_group"),
}
extra["claim_order"] = (3, 0, emu_name)
if _take(extra, full_dest, seen_dests, taken):
extras.append(extra)
return extras
@@ -517,6 +523,8 @@ def _collect_emulator_extras(
)
extras = []
seen_dests: set[str] = set(seen)
# Destination -> the extra that took it, to record later claimants on.
taken: dict[str, dict] = {}
for u in undeclared:
if not u["in_repo"]:
continue
@@ -538,38 +546,18 @@ def _collect_emulator_extras(
claimants[(u.get("emulator", ""), u.get("name", ""), u.get("path") or "")] = (
sanitize_pack_path(dest)
)
if full_dest in seen_dests:
extra = _report_extra(u, name, dest)
claims = [_report_extra(c, name, dest) for c in u.get("other_claims", [])]
# Where the builder meets each claim: the report lists the profiles in
# order, archives after the loose files.
for item in (extra, *claims):
item["claim_order"] = (1, 1 if u.get("archive") else 0, item["source_profile"])
if not _take(extra, full_dest, seen_dests, taken):
for claim in claims:
_another_claim(taken.get(full_dest), claim)
continue
seen_dests.add(full_dest)
extra = {
"name": name,
"destination": dest,
"required": u.get("required", False),
"hle_fallback": u.get("hle_fallback", False),
"source_emulator": u.get("emulator", ""),
"source_profile": u.get("profile", ""),
# Identity of the report entry this extra came from. The name
# alone does not identify it: Dolphin declares three IPL.bin that
# differ only by path, and collapsing them onto one key withdraws
# the wrong regional variant from the report.
"source_name": u.get("name", ""),
"source_path": u.get("path") or "",
"source_system": u.get("system"),
"source_systems": u.get("systems", []),
"region": u.get("region"),
"variant_group": u.get("variant_group"),
}
# Keep every reproducible identity constraint carried by the source
# profile. Some profiles expose CRC32/SHA-256 only through their
# validation block; dropping those fields here would let a same-named
# but different payload enter a generated pack.
expected = u.get("expected") or {}
for identity in PROFILE_IDENTITY_FIELDS:
declared = u.get(identity)
if declared in (None, "", []):
declared = expected.get(identity)
if declared not in (None, "", []):
extra[identity] = declared
for claim in claims:
_another_claim(extra, claim)
extras.append(extra)
# Second pass: find alternative destinations for files already in the pack.
@@ -624,7 +612,7 @@ def _collect_emulator_extras(
if dest == fname:
continue # no alternative destination
full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest
if full_dest in seen_dests:
if full_dest in seen_dests and full_dest not in taken:
continue
# Check file exists in repo or data dirs
if not (
@@ -633,7 +621,6 @@ def _collect_emulator_extras(
or by_path_suffix.get(dest)
):
continue
seen_dests.add(full_dest)
extra = {
"name": fname,
"destination": dest,
@@ -652,24 +639,89 @@ def _collect_emulator_extras(
for identity in PROFILE_IDENTITY_FIELDS:
if f.get(identity) not in (None, "", []):
extra[identity] = f[identity]
extras.append(extra)
extra["claim_order"] = (2, 0, emu_name)
if _take(extra, full_dest, seen_dests, taken):
extras.append(extra)
extras.extend(
_archive_prefix_extras(
profiles, relevant, covered_names, seen_dests, extras_prefix,
by_name,
by_name, taken,
)
)
extras.extend(
_agnostic_scan_extras(
profiles, relevant, db, by_name, seen_dests, extras_prefix,
standalone_set,
standalone_set, taken,
)
)
return extras
def _report_extra(u: dict, name: str, dest: str) -> dict:
"""The extra a cross-reference entry asks for at its destination."""
extra = {
"name": name,
"destination": dest,
"required": u.get("required", False),
"hle_fallback": u.get("hle_fallback", False),
"source_emulator": u.get("emulator", ""),
"source_profile": u.get("profile", ""),
# Identity of the report entry this extra came from. The name
# alone does not identify it: Dolphin declares three IPL.bin that
# differ only by path, and collapsing them onto one key withdraws
# the wrong regional variant from the report.
"source_name": u.get("name", ""),
"source_path": u.get("path") or "",
"source_system": u.get("system"),
"source_systems": u.get("systems", []),
"region": u.get("region"),
"variant_group": u.get("variant_group"),
}
# Keep every reproducible identity constraint carried by the source
# profile. Some profiles expose CRC32/SHA-256 only through their
# validation block; dropping those fields here would let a same-named
# but different payload enter a generated pack.
expected = u.get("expected") or {}
for identity in PROFILE_IDENTITY_FIELDS:
declared = u.get(identity)
if declared in (None, "", []):
declared = expected.get(identity)
if declared not in (None, "", []):
extra[identity] = declared
return extra
def _take(
extra: dict, full_dest: str, seen_dests: set[str], taken: dict[str, dict] | None
) -> bool:
"""Whether an extra takes its destination; a taken one records the claim."""
if full_dest in seen_dests:
if taken is not None:
_another_claim(taken.get(full_dest), extra)
return False
seen_dests.add(full_dest)
if taken is not None:
taken[full_dest] = extra
return True
def _another_claim(extra: dict | None, claim: dict) -> None:
"""Record a core that reads a destination another core's extra took.
The pack ships one file per destination, so the first claimant's extra
stands; the others are kept on it so the manifest can name every core
the file serves. A pack built for a target without the first core packs
the next one's copy, and an installer narrowing by core must keep it.
"""
if extra is None or claim.get("source_profile") == extra.get("source_profile"):
return
claims = extra.setdefault("other_claims", [])
claims.append(claim)
claims.sort(key=lambda c: c["claim_order"])
def _extra_system_ids(extra: dict) -> list[str]:
"""Return the narrowest system ownership preserved on a core extra."""
explicit = extra.get("source_system")
+35 -24
View File
@@ -505,6 +505,10 @@ def find_undeclared_files(
standalone_set = set(str(c) for c in config.get("standalone_cores", []))
undeclared = []
seen_files: set[tuple] = set()
# Key -> the entry reported for it. A later profile reading the same
# requirement is kept on that entry rather than counted again: a pack
# built without the first profile still has to name the file.
reported: dict[tuple, dict] = {}
# Track archives: archive_name -> {in_repo, emulator, files: [...], ...}
archive_entries: dict[tuple, dict] = {}
@@ -549,7 +553,11 @@ def find_undeclared_files(
f.get("variant_group"),
region_key,
)
if not fname or seen_key in seen_files:
if not fname:
continue
first = reported.get(seen_key)
duplicate = seen_key in seen_files
if duplicate and (first is None or first["profile"] == emu_name):
continue
verdict = _candidate_verdict(
f, fname, is_standalone, include_all, declared_names, dest
@@ -644,29 +652,32 @@ def find_undeclared_files(
in_repo = False
checks = _parse_validation(f.get("validation"))
undeclared.append(
{
"profile": emu_name,
"emulator": profile.get("emulator", emu_name),
"systems": list(profile.get("systems", [])),
"system": f.get("system"),
"region": f.get("region"),
"variant_group": f.get("variant_group"),
"name": fname,
"path": dest,
"required": f.get("required", False),
"hle_fallback": f.get("hle_fallback", False),
"category": f.get("category", "bios"),
"in_repo": in_repo,
"note": f.get("note", ""),
"checks": sorted(checks) if checks else [],
"source_ref": f.get("source_ref"),
"expected": _build_expected(f, checks),
**_identity_of(f),
"sha1": f.get("sha1"),
"md5": f.get("md5"),
}
)
entry = {
"profile": emu_name,
"emulator": profile.get("emulator", emu_name),
"systems": list(profile.get("systems", [])),
"system": f.get("system"),
"region": f.get("region"),
"variant_group": f.get("variant_group"),
"name": fname,
"path": dest,
"required": f.get("required", False),
"hle_fallback": f.get("hle_fallback", False),
"category": f.get("category", "bios"),
"in_repo": in_repo,
"note": f.get("note", ""),
"checks": sorted(checks) if checks else [],
"source_ref": f.get("source_ref"),
"expected": _build_expected(f, checks),
**_identity_of(f),
"sha1": f.get("sha1"),
"md5": f.get("md5"),
}
if duplicate:
first.setdefault("other_claims", []).append(entry)
continue
reported[seen_key] = entry
undeclared.append(entry)
# Append grouped archive entries
for entry in sorted(archive_entries.values(), key=lambda e: e["name"]):
+297
View File
@@ -0,0 +1,297 @@
"""The one-liner narrows to the cores a RetroArch already has.
RetroArch keeps its cores in libretro_directory, named the buildbot's way
(`mednafen_psx_libretro.so`), while a file in the manifest names the profile
that reads it (`beetle_psx`). The manifest maps one to the other and says
which cores keep each system, the rule a pack built for a target follows.
"""
from __future__ import annotations
import contextlib
import io
import json
import shlex
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT))
sys.path.insert(0, str(REPO_ROOT / "tests"))
import install # noqa: E402
from test_install_selection import FakeRepository # noqa: E402
class CoreDirectories(unittest.TestCase):
def setUp(self):
self.tmp = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(lambda: __import__("shutil").rmtree(self.tmp, ignore_errors=True))
def test_the_config_detection_read_names_the_cores(self):
cfg = self.tmp / "retroarch.cfg"
cfg.write_text(
f'system_directory = "{self.tmp / "sys"}"\n'
f'libretro_directory = "{self.tmp / "my cores"}"\n'
)
system_dir = install._parse_retroarch_system_dir(cfg)
self.assertEqual(
install.core_dirs("retroarch", system_dir), [self.tmp / "my cores"]
)
def test_a_portable_layout_defaults_to_cores_beside_the_config(self):
(self.tmp / "retroarch.cfg").write_text('libretro_directory = "default"\n')
self.assertEqual(
install.core_dirs("retroarch", self.tmp / "system"), [self.tmp / "cores"]
)
(self.tmp / "retroarch.cfg").write_text('video_driver = "gl"\n')
self.assertEqual(
install.core_dirs("retroarch", self.tmp / "system"), [self.tmp / "cores"]
)
def test_retropie_and_lakka_keep_their_own_folders(self):
self.assertIn(
Path("/opt/retropie/libretrocores"),
install.core_dirs("retropie", self.tmp / "none"),
)
self.assertIn(Path("/storage/cores"), install.core_dirs("lakka", self.tmp / "none"))
def test_core_files_of_every_system_and_retropie_folders(self):
cores = self.tmp / "cores"
(cores / "lr-fceumm").mkdir(parents=True)
for name in (
"mednafen_psx_libretro.so", "snes9x_libretro.dll",
"mgba_libretro.dylib", "genesis_plus_gx_libretro_android.so",
"lr-fceumm/fceumm_libretro.so", "mgba_libretro.info", "readme.txt",
):
(cores / name).write_bytes(b"")
self.assertEqual(
install.installed_cores([cores, self.tmp / "missing"]),
["fceumm", "genesis_plus_gx", "mednafen_psx", "mgba", "snes9x"],
)
MANIFEST = {
"core_names": {"beetle_psx": ["mednafen_psx"]},
"system_cores": {"sony-playstation": ["beetle_psx", "pcsx_rearmed"],
"nintendo-gba": ["mgba"]},
"files": [
{"dest": "scph5501.bin", "cores": None, "systems": ["sony-playstation"]},
{"dest": "gba_bios.bin", "cores": None, "systems": ["nintendo-gba"]},
{"dest": "unlisted.bin", "cores": None, "systems": ["arcade"]},
{"dest": "psx/extra.bin", "cores": ["beetle_psx"], "systems": ["sony-playstation"]},
{"dest": "mgba/extra.bin", "cores": ["mgba"], "systems": ["nintendo-gba"]},
],
"omitted_files": [
{"dest": "gone.bin", "cores": ["mgba"], "systems": ["nintendo-gba"]},
],
}
class NarrowToCores(unittest.TestCase):
def test_a_buildbot_name_keeps_its_profile_and_its_systems(self):
files, omitted = install.narrow_to_cores(MANIFEST, ["mednafen_psx"])
self.assertEqual(
[f["dest"] for f in files],
["scph5501.bin", "unlisted.bin", "psx/extra.bin"],
)
self.assertEqual(omitted, [])
def test_an_older_list_keeps_the_platform_files(self):
older = {k: v for k, v in MANIFEST.items() if k in ("files", "omitted_files")}
files, _ = install.narrow_to_cores(older, ["mgba"])
self.assertEqual(
[f["dest"] for f in files],
["scph5501.bin", "gba_bios.bin", "unlisted.bin", "mgba/extra.bin"],
)
class TheBuildersOrder(unittest.TestCase):
"""RetroPie reads megaplay.zip as advancemame (copy A), mame (copy B),
then mame2003 (copy A). Without advancemame the builder ships mame's
copy; merging mame2003 into the first copy gave it A."""
ENTRY = {
"dest": "megaplay.zip", "sha1": "a" * 40, "size": 1,
"repo_path": "bios/a.zip", "cores": ["advancemame"], "systems": ["arcade"],
"alternatives": [
{"sha1": "b" * 40, "sha256": "", "size": 2, "repo_path": "bios/b.zip",
"cores": ["mame"]},
{"sha1": "a" * 40, "sha256": "", "size": 1, "repo_path": "bios/a.zip",
"cores": ["mame2003"]},
],
}
def test_the_first_chosen_claim_wins(self):
manifest = {"files": [self.ENTRY], "omitted_files": []}
for names, sha in ((["mame", "mame2003"], "b"), (["mame2003"], "a"),
(["advancemame", "mame"], "a")):
with self.subTest(names=names):
files, _ = install.narrow_to_cores(manifest, names)
self.assertEqual(files[0]["sha1"], sha * 40)
self.assertNotIn("alternatives", files[0] if sha == "b" else {})
def test_the_parity_covers_a_core_set(self):
"""A real build for mame and mame2003 on RetroPie, the shape
--installed-cores produces there."""
db_path = REPO_ROOT / "database.json"
if not db_path.exists():
self.skipTest("database.json is not built")
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import generate_pack as gp
from common import load_emulator_profiles
db = json.loads(db_path.read_text(encoding="utf-8"))
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
def manifest(cores):
return gp.generate_manifest(
"retropie", str(REPO_ROOT / "platforms"), db, str(REPO_ROOT / "bios"),
str(REPO_ROOT / "platforms" / "_registry.yml"),
emulators_dir=str(REPO_ROOT / "emulators"), emu_profiles=profiles,
target_cores=cores, offline=True,
)
names = ["mame", "mame2003"]
files, _ = install.narrow_to_cores(manifest(None), names)
built = manifest(set(names))
self.assertEqual(
{(f["dest"], f["sha1"]) for f in files},
{(f["dest"], f["sha1"]) for f in built["files"]},
)
class TheBoundaryBoundsAlternatives(unittest.TestCase):
def _manifest(self, alternative: dict) -> dict:
entry = {"dest": "a.bin", "sha1": "a" * 40, "size": 1,
"repo_path": "bios/a.bin", "cores": ["x"],
"alternatives": [alternative]}
return {"manifest_version": 2, "platform": "retroarch", "files": [entry]}
def test_an_alternative_carries_only_a_copy(self):
good = {"sha1": "b" * 40, "size": 1, "repo_path": "bios/b.bin", "cores": ["y"]}
install._validate_manifest(self._manifest(good), "retroarch")
for extra in ({"dest": "../../evil"}, {"regions": 5}, {"cores": []},
{"repo_path": "../x"}):
with self.subTest(extra=extra), self.assertRaises(ValueError):
install._validate_manifest(self._manifest({**good, **extra}), "retroarch")
def test_the_cap_counts_the_largest_copy(self):
big = {"sha1": "b" * 40, "size": install.MAX_DOWNLOAD_SIZE,
"repo_path": "bios/b.bin", "cores": ["y"]}
manifest = self._manifest(big)
manifest["files"] = [
{**manifest["files"][0], "dest": f"f{i}.bin"} for i in range(70)
]
with self.assertRaises(ValueError):
install._validate_manifest(manifest, "retroarch")
class RefusedBeforeAnyInstall(unittest.TestCase):
def test_a_platform_shipping_its_cores_stops_the_run_first(self):
tmp = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, tmp, True)
detected = [("retroarch", tmp / "system"), ("batocera", tmp / "bios")]
(tmp / "retroarch.cfg").write_text("")
(tmp / "cores").mkdir()
(tmp / "cores" / "mgba_libretro.so").write_bytes(b"")
with mock.patch.object(install, "detect_os", return_value="linux"), \
mock.patch.object(install, "detect_frontends", return_value=[]), \
mock.patch.object(install, "detect_platforms", return_value=detected), \
mock.patch.object(install, "fetch_manifest") as fetch, \
mock.patch.object(sys, "argv", ["install.py", "--installed-cores", "--no-input"]), \
contextlib.redirect_stdout(io.StringIO()), \
contextlib.redirect_stderr(io.StringIO()) as err:
with self.assertRaises(SystemExit) as stop:
install.main()
self.assertEqual(stop.exception.code, 1)
self.assertIn("batocera ships its cores", err.getvalue())
fetch.assert_not_called()
class _CoreRepository(FakeRepository):
"""The fake repository with the two maps and a portable RetroArch."""
def __enter__(self):
super().__enter__()
path = self.root / "install" / "retroarch.json"
manifest = json.loads(path.read_text())
manifest["core_names"] = {"pcsx_rearmed": ["pcsx_rearmed_neon"]}
manifest["system_cores"] = {
"sony-playstation": ["pcsx_rearmed"], "nintendo-gba": ["mgba"],
}
path.write_text(json.dumps(manifest))
(self.root / "retroarch.cfg").write_text('libretro_directory = ":/cores"\n')
(self.root / "cores").mkdir()
return self
def install_core(self, name: str) -> None:
(self.root / "cores" / f"{name}_libretro.so").write_bytes(b"")
class TheOneLinerReadsInstalledCores(unittest.TestCase):
def test_only_what_the_installed_cores_read(self):
with _CoreRepository() as repo:
repo.install_core("pcsx_rearmed_neon")
proc = repo.run("--installed-cores")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("1 core installed", proc.stdout)
self.assertEqual(
repo.installed(), ["pcsx/extra.bin", "scph5500.bin", "scph5501.bin"]
)
def test_no_core_found_is_an_error_not_everything(self):
with _CoreRepository() as repo:
proc = repo.run("--installed-cores")
self.assertEqual(proc.returncode, 1)
self.assertIn("no installed core found", proc.stderr)
self.assertEqual(repo.installed(), [])
def test_an_older_file_list_is_refused(self):
with FakeRepository() as repo:
(repo.root / "retroarch.cfg").write_text("")
(repo.root / "cores").mkdir()
(repo.root / "cores" / "mgba_libretro.so").write_bytes(b"")
proc = repo.run("--installed-cores")
self.assertEqual(proc.returncode, 1)
self.assertIn("needs a newer one", proc.stderr)
def test_a_platform_that_ships_its_cores_refuses_the_flag(self):
with _CoreRepository() as repo:
batocera = repo.root / "install" / "batocera.json"
manifest = json.loads((repo.root / "install" / "retroarch.json").read_text())
manifest["platform"] = "batocera"
batocera.write_text(json.dumps(manifest))
command = repo.command("--installed-cores")
command[command.index("retroarch")] = "batocera"
proc = subprocess.run(
command, env=repo.env(), capture_output=True, text=True,
timeout=60, check=False, stdin=subprocess.DEVNULL,
)
self.assertEqual(proc.returncode, 1)
self.assertIn("ships its cores with the system", proc.stderr)
@unittest.skipUnless(shutil.which("script"), "script(1) provides the terminal")
def test_the_prompt_offers_the_installed_cores(self):
with _CoreRepository() as repo:
repo.install_core("mgba")
command = " ".join(shlex.quote(part) for part in repo.command())
proc = subprocess.run(
["script", "-qec", command, "/dev/null"],
input="i\n", env=repo.env(), capture_output=True,
text=True, timeout=60, check=False,
)
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("i) only what the installed cores read: 2 files", proc.stdout)
self.assertIn("--platform retroarch --installed-cores", proc.stdout)
self.assertEqual(repo.installed(), ["gba_bios.bin", "mgba/extra.bin"])
if __name__ == "__main__":
unittest.main()
+75
View File
@@ -257,6 +257,19 @@ class TheBoundaryChecksTheNewFields(unittest.TestCase):
with self.subTest(field=bad), self.assertRaises(ValueError):
install._validate_manifest(self._manifest(**bad), "retroarch")
def test_the_core_maps(self):
good = self._manifest()
good.update(core_names={"beetle_psx": ["mednafen_psx"]},
system_cores={"sony-playstation": ["beetle_psx"]})
install._validate_manifest(good, "retroarch")
for field, bad in (("core_names", ["x"]), ("core_names", {"a": "b"}),
("system_cores", {"s": None}), ("system_cores", {"": ["a"]}),
("system_cores", {"s": [1]})):
manifest = self._manifest()
manifest[field] = bad
with self.subTest(field=field, value=bad), self.assertRaises(ValueError):
install._validate_manifest(manifest, "retroarch")
class _QuietHandler(http.server.SimpleHTTPRequestHandler):
def log_message(self, format, *args): # noqa: A002 - stdlib signature
@@ -555,5 +568,67 @@ class InstallMatchesASystemPack(unittest.TestCase):
)
class InstallMatchesATargetManifest(unittest.TestCase):
"""`install.py --target T` against the manifest the builder writes for T.
A target list names cores the way the buildbot does (`mednafen_psx`)
while a file names the profile that reads it (`beetle_psx`), and the
builder drops a platform system no available core reads. The installer
did neither: on RetroArch desktop it kept every system and lost the
extras of five cores. A destination two cores read was also filed under
the first alone, so a target without that core lost it, or got the
first core's copy where the other core wants different bytes
(megaplay.zip for mame and mame2003 on Switch).
"""
CASES = (
("retroarch", "desktop"),
("retroarch", "switch"),
("batocera", "x86_64"),
("batocera", "bcm2711"),
("retropie", "rpi4"),
("emudeck", "steamos"),
)
@classmethod
def setUpClass(cls):
db_path = REPO_ROOT / "database.json"
if not db_path.exists():
raise unittest.SkipTest("database.json is not built")
import generate_pack as gp
from common import load_emulator_profiles, load_target_config
cls.gp = gp
cls.load_target_config = staticmethod(load_target_config)
cls.db = json.loads(db_path.read_text(encoding="utf-8"))
cls.profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
def _manifest(self, platform, target_cores=None, target=None):
return self.gp.generate_manifest(
platform, str(REPO_ROOT / "platforms"), self.db,
str(REPO_ROOT / "bios"), str(REPO_ROOT / "platforms" / "_registry.yml"),
emulators_dir=str(REPO_ROOT / "emulators"), emu_profiles=self.profiles,
target_cores=target_cores, target_name=target, offline=True,
)
def test_each_target(self):
for platform, target in self.CASES:
with self.subTest(platform=platform, target=target):
cores = sorted(self.load_target_config(
platform, target, str(REPO_ROOT / "platforms")
))
full = self._manifest(platform)
built = self._manifest(platform, set(cores), target)
files, omitted = install.narrow_to_cores(full, cores)
self.assertEqual(
{(f["dest"], f["sha1"]) for f in files},
{(f["dest"], f["sha1"]) for f in built["files"]},
)
self.assertEqual(
{o["dest"] for o in omitted},
{o["dest"] for o in built["omitted_files"]},
)
if __name__ == "__main__":
unittest.main()
+34 -2
View File
@@ -118,10 +118,17 @@ after detection, it asks:
```
Retroarch: 5861 files, 6.0 GB.
Enter) install everything
i) only what the installed cores read: 102 files, 44.5 MB
c) choose systems, cores and regions
q) quit
```
`i` appears for RetroArch, Lakka and RetroPie when cores are found installed;
the line above is a RetroArch with Beetle PSX HW, Snes9x, mGBA, Genesis Plus GX
and FCEUmm.
It keeps what those cores read and drops the systems none of them emulates,
the rule `--installed-cores` applies.
`c` walks through three numbered lists, each answered with numbers such as
`1,4,7-9`, or Enter to keep everything on that axis.
@@ -149,6 +156,30 @@ python install.py --platform retroarch --system sony-playstation --core pcsx_rea
python install.py --platform recalbox --region us,eu,jp
```
### Installed cores
`--installed-cores` reads the cores a RetroArch already has and installs what
they need. The cores are the `<name>_libretro.so` (`.dll`, `.dylib`,
`_libretro_android.so`) files in the `libretro_directory` of the
`retroarch.cfg` detection found, `cores/` beside RetroArch when the setting is
unset, a `retroarch.cfg` next to the system directory for a portable install
given with `--dest`, and the folders RetroPie (`/opt/retropie/libretrocores`)
and Lakka (`/usr/lib/libretro`, `/storage/cores`) install into. A core file
is named the buildbot's way (`mednafen_psx`); the manifest maps each name to
the profile its files are filed under (`beetle_psx`).
What stays is what a pack built for exactly those cores carries, the rule
`--target` follows too: a platform system stays when an installed core
emulates it or reads its files, or when no core of the platform does; a
file a core loads stays when that core is installed. Where two cores read
one path and want different bytes, the copy of the first installed core in
the builder's order is the one installed.
Batocera, Recalbox and the other distributions ship their cores with the
system, so the option is refused there, as it is when no core is found or
when the file list predates the mapping: carrying on would install
everything while the option asked for less.
A name the platform does not have is refused with the list of the ones it
has, before anything is downloaded: carrying on would install something other
than what was asked. A system name that itself contains commas, as Recalbox's
@@ -173,7 +204,8 @@ what the options name, everything by default.
|--------|--------|
| `--platform NAME` | Install for this platform instead of the detected one. Unknown names are refused with the available list |
| `--dest PATH` | Destination directory, overriding detection. With `--dest` alone the file list is RetroArch's |
| `--target NAME` | Keep only the files the cores of that hardware target need. An unknown target is refused rather than ignored, since carrying on would install everything |
| `--target NAME` | Keep only the files the cores of that hardware target need, and the systems they emulate. An unknown target is refused rather than ignored, since carrying on would install everything |
| `--installed-cores` | Keep only what the cores already installed read (RetroArch, Lakka, RetroPie); see [Installed cores](#installed-cores) |
| `--system NAMES` | Install only these systems. Comma-separated or repeated; `--list-systems` prints the names, `other` included |
| `--core NAMES` | Keep the platform's own list and only the extra files these cores load; `--list-cores` prints the names |
| `--region LIST` | Preferred regions, best first (`us,eu,jp`): keep one BIOS per region where a system has several. Accepts the names and aliases of `generate_pack.py --region` |
@@ -379,7 +411,7 @@ followed.
## Exit status
`0` on success, `1` on failure: an unusable base URL, an unknown platform or
target, a manifest that fails validation, a fetch that never succeeded, or no
target, `--installed-cores` with no core found, a manifest that fails validation, a fetch that never succeeded, or no
platform to install for in a non-interactive run.
Failures that are per-file, such as a download exhausting its three attempts,