mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
feat: accept platform system names in --system
The manifest now carries system_names, each system's native_system names from the platform's own file, and --system, its suggestions and --list-systems read them: --platform batocera --system psx installs sony-playstation.
This commit is contained in:
1 parent
10c94369df
commit
5e4d9199c2
7 files changed
+145
-16
No files matched your search
+1
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
$defaultInstallSha256 = "86c4e8b2a67b5721020dd3629212c741a67a3b3180e8f62fb61bba333303a5b6"
|
||||
$defaultInstallSha256 = "c462ff5c1c755b269b680f6e1c4c22ef58b7ae9b92679444634c35d248cc4267"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+49
-12
@@ -1559,17 +1559,23 @@ def _name_key(name: str) -> str:
|
||||
SHORT_LIST = 12
|
||||
|
||||
|
||||
def did_you_mean(typed: list[str], known: list[str], listing: str) -> str:
|
||||
def did_you_mean(
|
||||
typed: list[str], known: list[str], listing: str,
|
||||
aliases: dict[str, str] | None = None,
|
||||
) -> str:
|
||||
"""The closest known names to what was typed, and the option listing them all.
|
||||
|
||||
A typo was answered with every name the platform has, up to 400 on one
|
||||
line, with no hint that playstation is sony-playstation.
|
||||
line, with no hint that playstation is sony-playstation. An alias close
|
||||
to what was typed suggests the name it stands for.
|
||||
"""
|
||||
import difflib # noqa: PLC0415
|
||||
|
||||
by_key: dict[str, str] = {}
|
||||
for name in known:
|
||||
by_key.setdefault(_name_key(name), name)
|
||||
for alias, name in (aliases or {}).items():
|
||||
by_key.setdefault(_name_key(alias), name)
|
||||
close: list[str] = []
|
||||
for word in typed:
|
||||
key = _name_key(word)
|
||||
@@ -1983,7 +1989,7 @@ 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"):
|
||||
for field in ("core_names", "system_cores", "system_names"):
|
||||
mapping = data.get(field)
|
||||
if mapping is None:
|
||||
continue
|
||||
@@ -2532,21 +2538,31 @@ def available_choices(files: list[dict], omitted: list[dict]) -> dict[str, list[
|
||||
}
|
||||
|
||||
|
||||
def resolve_choices(values: list[str], known: list[str], label: str) -> list[str]:
|
||||
def resolve_choices(
|
||||
values: list[str], known: list[str], label: str,
|
||||
aliases: dict[str, str] | None = None,
|
||||
) -> list[str]:
|
||||
"""Names given on the command line, matched without regard to case.
|
||||
|
||||
Each value is one name or a comma-separated list. A whole value that is
|
||||
itself a name wins over splitting it: Recalbox names one system
|
||||
`msx1,msx2,msxturbor`. An unknown name is refused, never ignored, since
|
||||
carrying on would install something other than what was asked.
|
||||
aliases maps another name of a known one to it, as Batocera's `psx`
|
||||
stands for sony-playstation. A known name always wins over an alias.
|
||||
"""
|
||||
aliases = {alias: name for alias, name in (aliases or {}).items() if name in known}
|
||||
by_lower = {name.lower(): name for name in known}
|
||||
for alias, name in aliases.items():
|
||||
by_lower.setdefault(alias.lower(), name)
|
||||
# Spelled as the site writes it (Sony PlayStation), a name is still one
|
||||
# name when no other one reads the same once case and spacing go.
|
||||
by_key: dict[str, str | None] = {}
|
||||
for name in known:
|
||||
key = _name_key(name)
|
||||
by_key[key] = None if key in by_key else name
|
||||
for alias, name in aliases.items():
|
||||
by_key.setdefault(_name_key(alias), name)
|
||||
chosen: list[str] = []
|
||||
unknown: list[str] = []
|
||||
for value in values:
|
||||
@@ -2566,7 +2582,8 @@ def resolve_choices(values: list[str], known: list[str], label: str) -> list[str
|
||||
listing = {"system": "--list-systems", "emulator": "--list-emulators"}.get(
|
||||
label, "--list-cores")
|
||||
raise ValueError(
|
||||
f"unknown {label}: {', '.join(unknown)}. {did_you_mean(unknown, known, listing)}"
|
||||
f"unknown {label}: {', '.join(unknown)}. "
|
||||
f"{did_you_mean(unknown, known, listing, aliases)}"
|
||||
)
|
||||
if values and not chosen:
|
||||
raise ValueError(f"--{label} needs at least one name")
|
||||
@@ -4120,8 +4137,24 @@ def _same_choice_hint(plat: str, systems: list[str], cores: list[str], regions:
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def system_aliases(manifest: dict | None) -> dict[str, str]:
|
||||
"""The platform's own names for its systems, each with the system id."""
|
||||
return {
|
||||
alias: system
|
||||
for system, names in ((manifest or {}).get("system_names") or {}).items()
|
||||
for alias in names
|
||||
}
|
||||
|
||||
|
||||
def _system_label(name: str, manifest: dict | None) -> str:
|
||||
"""A system id with the names the platform gives it: nintendo-gba (gba)."""
|
||||
names = ((manifest or {}).get("system_names") or {}).get(name)
|
||||
return f"{name} ({', '.join(names)})" if names else name
|
||||
|
||||
|
||||
def _print_choices(
|
||||
plat: str, files: list[dict], omitted: list[dict], systems: bool
|
||||
plat: str, files: list[dict], omitted: list[dict], systems: bool,
|
||||
manifest: dict | None = None,
|
||||
) -> None:
|
||||
"""What --system or --core accepts for a platform, with what each holds."""
|
||||
choices = available_choices(files, omitted)
|
||||
@@ -4129,14 +4162,15 @@ def _print_choices(
|
||||
if not names:
|
||||
print(f" {plat} records no {'systems' if systems else 'cores'}")
|
||||
return
|
||||
width = min(max(len(name) for name in names), 40)
|
||||
labels = {name: _system_label(name, manifest) if systems else name for name in names}
|
||||
width = min(max(len(label) for label in labels.values()), 40)
|
||||
for name in names:
|
||||
held = (
|
||||
[f for f in files if _chosen(f, {name})]
|
||||
if systems
|
||||
else _filter_by_target([f for f in files if f.get("cores") is not None], [name])
|
||||
)
|
||||
print(f" {name:<{width}} {_sized(held)}")
|
||||
print(f" {labels[name]:<{width}} {_sized(held)}")
|
||||
|
||||
|
||||
def emulator_names(manifest: dict, files: list[dict], omitted: list[dict]) -> dict[str, str]:
|
||||
@@ -4326,7 +4360,8 @@ def _narrow_to_emulators(
|
||||
|
||||
|
||||
def _requested_narrowing(
|
||||
args: argparse.Namespace, plat: str, files: list[dict], omitted: list[dict]
|
||||
args: argparse.Namespace, plat: str, files: list[dict], omitted: list[dict],
|
||||
manifest: dict | None = None,
|
||||
) -> tuple[list[str], list[str], list[str]] | None:
|
||||
"""The systems, cores and regions named on the command line, checked."""
|
||||
if not (args.system or args.core or args.region):
|
||||
@@ -4343,7 +4378,9 @@ def _requested_narrowing(
|
||||
sys.exit(1)
|
||||
choices = available_choices(files, omitted)
|
||||
try:
|
||||
systems = resolve_choices(args.system or [], choices["systems"], "system")
|
||||
systems = resolve_choices(
|
||||
args.system or [], choices["systems"], "system", system_aliases(manifest)
|
||||
)
|
||||
cores = resolve_choices(args.core or [], choices["cores"], "core")
|
||||
regions = resolve_regions(args.region or [])
|
||||
except ValueError as exc:
|
||||
@@ -4887,7 +4924,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
args.emulator, manifest, plat_name, files, omitted_files
|
||||
)
|
||||
|
||||
selection = _requested_narrowing(args, plat_name, files, omitted_files)
|
||||
selection = _requested_narrowing(args, plat_name, files, omitted_files, manifest)
|
||||
if (
|
||||
selection is None
|
||||
and interactive
|
||||
@@ -4924,7 +4961,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
print(f" Narrowed {before} -> {len(files)} files")
|
||||
|
||||
if args.list_systems or args.list_cores:
|
||||
_print_choices(plat_name, files, omitted_files, args.list_systems)
|
||||
_print_choices(plat_name, files, omitted_files, args.list_systems, manifest)
|
||||
continue
|
||||
|
||||
total_size = sum(f.get("size", 0) for f in files)
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ esac
|
||||
TEMP_INSTALLER=""
|
||||
TEMP_DIRECTORY=""
|
||||
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
DEFAULT_INSTALL_SHA256="86c4e8b2a67b5721020dd3629212c741a67a3b3180e8f62fb61bba333303a5b6"
|
||||
DEFAULT_INSTALL_SHA256="c462ff5c1c755b269b680f6e1c4c22ef58b7ae9b92679444634c35d248cc4267"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -120,6 +120,18 @@
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
"system_names": {
|
||||
"description": "Each platform system with the names the platform's own file gives it (native_system), which the installer accepts for --system.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
"omitted_files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
@@ -74,7 +74,7 @@ from common import (
|
||||
import packresolve
|
||||
from refresh_data_dirs import cache_lock, hold_data_dirs
|
||||
import region as region_mod
|
||||
from manifestselect import SelectionIndex
|
||||
from manifestselect import SelectionIndex, native_system_names
|
||||
import slot as slot_mod
|
||||
import slots
|
||||
import split_pack
|
||||
@@ -4465,6 +4465,9 @@ def generate_manifest(
|
||||
),
|
||||
"core_names": core_names,
|
||||
"system_cores": system_cores,
|
||||
# The names the platform itself gives its systems (Batocera's psx),
|
||||
# which --system accepts beside the ids.
|
||||
"system_names": native_system_names(config, list(pack_systems)),
|
||||
"files": manifest_files,
|
||||
}
|
||||
if data_directories:
|
||||
|
||||
@@ -17,8 +17,10 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import re
|
||||
|
||||
import region as region_mod
|
||||
from common import _norm_system_id, sanitize_pack_path
|
||||
from common import _norm_system_id, native_layer, sanitize_pack_path
|
||||
from packextras import _extra_system_ids, _kept, extra_region_groups, system_spellings
|
||||
|
||||
|
||||
@@ -118,3 +120,36 @@ class SelectionIndex:
|
||||
if priority is not None:
|
||||
out["priority"] = priority
|
||||
return out
|
||||
|
||||
|
||||
def _name_key(name: str) -> str:
|
||||
"""Letters and digits only, as the installer compares names."""
|
||||
return re.sub(r"[^a-z0-9]", "", name.lower())
|
||||
|
||||
|
||||
def native_system_names(config: dict, systems: list[str]) -> dict[str, list[str]]:
|
||||
"""Per platform system, the names the platform's own file gives it.
|
||||
|
||||
A Batocera user knows PlayStation as `psx`, the folder under roms/, and
|
||||
the platform YAML records that name as `native_system` on each file it
|
||||
transcribes. Only the platform's own entries count (`native_layer`), a
|
||||
name spelled like the system id adds nothing, and a name the platform
|
||||
gives to two systems, or that reads as another system's id, names
|
||||
neither.
|
||||
"""
|
||||
native = native_layer(config) or {}
|
||||
by_name: dict[str, set[str]] = {}
|
||||
for sys_id, system in native.get("systems", {}).items():
|
||||
if sys_id not in systems:
|
||||
continue
|
||||
for entry in system.get("files", []):
|
||||
name = entry.get("native_system")
|
||||
if isinstance(name, str) and name.strip():
|
||||
by_name.setdefault(name.strip(), set()).add(sys_id)
|
||||
ids = {_name_key(sys_id) for sys_id in systems}
|
||||
out: dict[str, list[str]] = {}
|
||||
for name, owners in by_name.items():
|
||||
if len(owners) != 1 or _name_key(name) in ids:
|
||||
continue
|
||||
out.setdefault(next(iter(owners)), []).append(name)
|
||||
return {sys_id: sorted(names) for sys_id, names in sorted(out.items())}
|
||||
@@ -746,5 +746,47 @@ class AListingNeedsNoInstall(unittest.TestCase):
|
||||
self.assertIn("nintendo-gba", proc.stdout)
|
||||
|
||||
|
||||
class ThePlatformsOwnSystemNamesAreAccepted(unittest.TestCase):
|
||||
"""A Batocera user who typed `--system psx`, the folder under roms/,
|
||||
was told the name is unknown with no suggestion, while the platform
|
||||
file records psx as the native_system of sony-playstation."""
|
||||
|
||||
def test_the_names_come_from_the_platform_file(self):
|
||||
from common import load_platform_config # noqa: PLC0415
|
||||
from manifestselect import native_system_names # noqa: PLC0415
|
||||
|
||||
config = load_platform_config("batocera", str(REPO_ROOT / "platforms"))
|
||||
names = native_system_names(config, list(config["systems"]))
|
||||
self.assertEqual(names["sony-playstation"], ["psx"])
|
||||
self.assertEqual(names["nintendo-gba"], ["gba"])
|
||||
# A name spelled like the system id adds nothing.
|
||||
self.assertTrue(all(
|
||||
install._name_key(alias) != install._name_key(system)
|
||||
for system, aliases in names.items() for alias in aliases
|
||||
))
|
||||
|
||||
def test_the_builder_writes_them_into_the_manifest(self):
|
||||
source = (REPO_ROOT / "scripts" / "generate_pack.py").read_text(encoding="utf-8")
|
||||
self.assertIn('"system_names": native_system_names(config, list(pack_systems))',
|
||||
source)
|
||||
|
||||
def test_a_native_name_narrows_and_lists(self):
|
||||
with FakeRepository() as repo:
|
||||
path = repo.root / "install" / "retroarch.json"
|
||||
manifest = json.loads(path.read_text())
|
||||
manifest["system_names"] = {"sony-playstation": ["psx"],
|
||||
"nintendo-gba": ["gba"]}
|
||||
path.write_text(json.dumps(manifest))
|
||||
proc = repo.run("--system", "psx", "--no-input")
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertEqual(repo.installed(),
|
||||
["pcsx/extra.bin", "scph5500.bin", "scph5501.bin"])
|
||||
listing = repo.run("--list-systems", "--no-input")
|
||||
self.assertIn("nintendo-gba (gba)", listing.stdout)
|
||||
typo = repo.run("--system", "psxx", "--no-input")
|
||||
self.assertEqual(typo.returncode, 1)
|
||||
self.assertIn("Did you mean: sony-playstation", typo.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user