From e041fac351ebf2ef73963f48ed833ada10ff3dc6 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:55:09 +0200 Subject: [PATCH] feat: suggest systems by short name --system gba, psx or ps1 was refused with no suggestion on RetroArch. Short names now suggest the ids they stand for, never taken in their place, and two aliases of one system name it once. --platform muos and the other systems --list-platforms names stand for the file list they get, and the run says which. --- install.ps1 | 2 +- install.py | 72 ++++++++++++++++++++++++++++++--- install.sh | 2 +- tests/test_install.py | 15 +++++++ tests/test_install_selection.py | 19 +++++++++ wiki/installer.md | 4 +- 6 files changed, 104 insertions(+), 10 deletions(-) diff --git a/install.ps1 b/install.ps1 index f466cfb7..31dd7476 100644 --- a/install.ps1 +++ b/install.ps1 @@ -11,7 +11,7 @@ $ErrorActionPreference = "Stop" $defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" - $defaultInstallSha256 = "853300c169f08b2554a421aac33da1b5004d3eedd70ae9dec1d732766d06b1ca" + $defaultInstallSha256 = "50afca134ab44a140421e3da43ab3a74fbbb35ba7c524adf688b8af5909dcc38" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 56e4d075..fb0d8f3b 100644 --- a/install.py +++ b/install.py @@ -2236,15 +2236,58 @@ def _name_key(name: str) -> str: SHORT_LIST = 12 +# Names people type for a system, each with its id. Only ever suggested: +# --system gba, psx and ps1 were refused with no hint on RetroArch, whose +# list names no system that way, and taking one in silence could install +# another console than the one meant. +SYSTEM_SHORT_NAMES = { + **dict.fromkeys(("psx", "ps1", "ps", "playstation1"), "sony-playstation"), + "ps2": "sony-playstation-2", "ps3": "sony-playstation-3", "psp": "sony-psp", + **dict.fromkeys(("vita", "psvita"), "sony-playstation-vita"), + "gb": "nintendo-gb", "gbc": "nintendo-gbc", "gba": "nintendo-gba", + **dict.fromkeys(("nes", "famicom", "fc"), "nintendo-nes"), "fds": "nintendo-fds", + **dict.fromkeys(("snes", "sfc", "superfamicom"), "nintendo-snes"), + "n64": "nintendo-64", "n64dd": "nintendo-64dd", "sgb": "nintendo-sgb", + **dict.fromkeys(("nds", "ds"), "nintendo-ds"), "dsi": "nintendo-dsi", + "3ds": "nintendo-3ds", "wiiu": "nintendo-wii-u", "switch": "nintendo-switch", + **dict.fromkeys(("gc", "ngc", "gcn", "gamecube"), "nintendo-gamecube"), + "pokemini": "nintendo-pokemon-mini", + **dict.fromkeys(("md", "gen", "genesis", "megadrive"), "sega-mega-drive"), + **dict.fromkeys(("scd", "segacd", "megacd"), "sega-mega-cd"), + "32x": "sega-32x", **dict.fromkeys(("sms", "mastersystem"), "sega-master-system"), + **dict.fromkeys(("gg", "gamegear"), "sega-game-gear"), + **dict.fromkeys(("ss", "saturn"), "sega-saturn"), + **dict.fromkeys(("dc", "dreamcast"), "sega-dreamcast"), + **dict.fromkeys(("naomi", "atomiswave"), "sega-dreamcast-arcade"), + **dict.fromkeys(("pce", "tg16", "turbografx", "turbografx16", "pcengine", + "supergrafx"), "nec-pc-engine"), + **dict.fromkeys(("pcecd", "tgcd", "turbografxcd"), "nec-pc-engine-cd"), + "pcfx": "nec-pc-fx", + **dict.fromkeys(("mvs", "aes"), "snk-neogeo"), + **dict.fromkeys(("neocd", "neogeocd"), "snk-neogeo-cd"), + **dict.fromkeys(("msx", "msx2"), "microsoft-msx"), + "c64": "commodore-c64", "c128": "commodore-c128", + **dict.fromkeys(("amiga", "cd32"), "commodore-amiga"), + "x68k": "sharp-x68000", **dict.fromkeys(("jag", "jaguar"), "atari-jaguar"), + **dict.fromkeys(("st", "atarist"), "atari-st"), "a800": "atari-400-800", + **dict.fromkeys(("coleco", "colecovision"), "coleco-colecovision"), + **dict.fromkeys(("intv", "intellivision"), "mattel-intellivision"), + "o2": "magnavox-odyssey2", "channelf": "fairchild-channel-f", + **dict.fromkeys(("zx", "spectrum", "zxspectrum"), "sinclair-zx-spectrum"), + "cpc": "amstrad-cpc", "bbc": "acorn-bbc-micro", "apple2": "apple-ii", +} + + def did_you_mean( typed: list[str], known: list[str], listing: str, - aliases: dict[str, str] | None = None, + aliases: dict[str, str] | None = None, inside_from: int = 4, ) -> 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. An alias close - to what was typed suggests the name it stands for. + to what was typed suggests the name it stands for. A name holding what + was typed, inside_from letters or more, is suggested too. """ import difflib # noqa: PLC0415 @@ -2259,9 +2302,12 @@ def did_you_mean( if not key: continue matches = difflib.get_close_matches(key, list(by_key), n=3, cutoff=0.6) - if len(key) >= 4: + if len(key) >= inside_from: matches += sorted(k for k in by_key if key in k and k not in matches) - close += [by_key[k] for k in matches if by_key[k] not in close] + for k in matches: + # Two aliases of one system (psx, ps1) name it once. + if by_key[k] not in close: + close.append(by_key[k]) if close: return f"Did you mean: {', '.join(close[:3])}? {listing} lists all {len(known)}." if len(known) <= SHORT_LIST: @@ -2279,6 +2325,13 @@ def normalize_platform(name: str) -> str: for plat in AVAILABLE_PLATFORMS: if key and key in (_name_key(plat), _name_key(display_name(plat))): return plat + # --list-platforms prints `muOS: retroarch`, and --platform muos was + # refused: a system named there stands for the list it gets. + for rule in _EMBEDDED_RULES: + if key and rule.manifest and key == _name_key(rule.name): + print(f" {rule.name} gets the {display_name(rule.manifest)} file list: " + f"--platform {rule.manifest}.") + return rule.manifest print(f"Unknown platform '{name.strip()}'. " + did_you_mean([name], list(AVAILABLE_PLATFORMS), "--list-platforms"), file=sys.stderr) @@ -3423,9 +3476,15 @@ def resolve_choices( raise ValueError(f"unknown {label}: {', '.join(unknown)}. This list records none.") listing = {"system": "--list-systems", "emulator": "--list-emulators"}.get( label, "--list-cores") + hints, inside_from = aliases, 4 + if label == "system": + # gba is the end of nintendo-gba: three letters are enough here. + hints = {**{short: name for short, name in SYSTEM_SHORT_NAMES.items() + if name in known}, **aliases} + inside_from = 3 raise ValueError( f"unknown {label}: {', '.join(unknown)}. " - f"{did_you_mean(unknown, known, listing, aliases)}" + f"{did_you_mean(unknown, known, listing, hints, inside_from)}" ) if values and not chosen: raise ValueError(f"--{label} needs at least one name") @@ -7139,7 +7198,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: print("Available platforms:") for p in AVAILABLE_PLATFORMS: print(f" {p}") - print("\nSystems recognized by their own files:") + print("\nSystems recognized by their own files, and the file list each gets " + "(--platform takes either name):") for line in _system_lines(): print(f" {line}") detected = detect_platforms(os_type) diff --git a/install.sh b/install.sh index 63aa076e..bf2e8f60 100755 --- a/install.sh +++ b/install.sh @@ -18,7 +18,7 @@ esac TEMP_INSTALLER="" TEMP_DIRECTORY="" DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -DEFAULT_INSTALL_SHA256="853300c169f08b2554a421aac33da1b5004d3eedd70ae9dec1d732766d06b1ca" +DEFAULT_INSTALL_SHA256="50afca134ab44a140421e3da43ab3a74fbbb35ba7c524adf688b8af5909dcc38" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/tests/test_install.py b/tests/test_install.py index c2b30a04..4b06a9f3 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -3759,6 +3759,21 @@ class ATypoGetsTheClosestName(unittest.TestCase): self.assertEqual(install.normalize_platform("RetroArch "), "retroarch") self.assertEqual(install.normalize_platform("RetroDECK"), "retrodeck") + def test_a_system_name_stands_for_the_list_it_gets(self): + """--list-platforms printed `muOS: retroarch`, and --platform muos + answered "Unknown platform".""" + for typed, plat in (("muos", "retroarch"), ("muOS", "retroarch"), + ("ArkOS", "retroarch"), ("PAN4ELEC", "amberelec"), + ("JELOS", "retroarch")): + out = io.StringIO() + with self.subTest(typed=typed), contextlib.redirect_stdout(out): + self.assertEqual(install.normalize_platform(typed), plat) + self.assertIn(f"--platform {plat}", out.getvalue()) + + def test_a_system_that_gets_no_list_is_still_refused(self): + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit): + install.normalize_platform("TrimUI") + def test_a_platform_typo_is_answered_with_one_name(self): err = io.StringIO() with contextlib.redirect_stderr(err), self.assertRaises(SystemExit) as raised: diff --git a/tests/test_install_selection.py b/tests/test_install_selection.py index 14de3476..8f81f83b 100644 --- a/tests/test_install_selection.py +++ b/tests/test_install_selection.py @@ -13,6 +13,7 @@ import functools import hashlib import http.server import importlib.util +import io import json import os import random @@ -311,6 +312,24 @@ class CommandLineChoices(unittest.TestCase): self.assertIn("sega-saturn", str(ctx.exception)) self.assertIn("nintendo-gba", str(ctx.exception)) + def test_a_short_name_is_suggested_never_taken(self): + """`--system gba`, `psx` and `ps1` were refused on RetroArch with no + suggestion, though nintendo-gba holds the very letters.""" + known = [f"system-{i}" for i in range(40)] + [ + "nintendo-gb", "nintendo-gba", "nintendo-gbc", "sony-playstation", + "sony-playstation-2", "sega-mega-drive"] + for typed, meant in (("gba", "nintendo-gba"), ("psx", "sony-playstation"), + ("ps1", "sony-playstation"), ("PS2", "sony-playstation-2"), + ("genesis", "sega-mega-drive"), ("gb", "nintendo-gb")): + with self.subTest(typed=typed), self.assertRaises(ValueError) as ctx: + install.resolve_choices([typed], known, "system") + self.assertIn(f"Did you mean: {meant}", str(ctx.exception)) + + def test_a_short_name_for_a_system_the_list_lacks_is_not_suggested(self): + with self.assertRaises(ValueError) as ctx: + install.resolve_choices(["psx"], [f"system-{i}" for i in range(40)], "system") + self.assertNotIn("sony-playstation", str(ctx.exception)) + def test_an_empty_list_is_refused(self): with self.assertRaises(ValueError): install.resolve_regions([" , "]) diff --git a/wiki/installer.md b/wiki/installer.md index e34a97d7..b718e7e8 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -493,14 +493,14 @@ what the options name, everything by default. | Option | Effect | |--------|--------| -| `--platform NAME` | Install for this platform instead of the detected one. Unknown names are refused with the available list | +| `--platform NAME` | Install for this platform instead of the detected one. A system `--list-platforms` names under its own files (`muos`, `arkos`) stands for the file list it gets, and the run says which. Unknown names are refused with the closest ones | | `--dest PATH` | Destination directory, overriding detection. Without `--platform`, the file list is the platform detected here: at a terminal the installer asks, Enter taking the one found, and otherwise it uses the one found, RetroArch's when nothing is found, and refuses when several are. The run prints whose list it is. A folder that does not exist is named with the nearest one that does, then created. When more than its last level is missing (a mistyped path), a run at a terminal asks first, and the answer defaults to no | | `--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. On Batocera, Recalbox, ROCKNIX and Lakka the board is read from the system (`batocera.arch`, `recalbox.arch`, `HW_DEVICE`, `LIBREELEC_DEVICE` or `LIBREELEC_PROJECT`) and applied when no `--target` is given | | `--every-board` | Install the files of every board instead of the one the system names, for a card that moves between devices | | `--installed-cores` | Keep only what the cores already installed read (RetroArch, Lakka, RetroPie). See [Installed cores](#installed-cores) | | `--cores-dir PATH` | The folder RetroArch loads its cores from, for `--installed-cores` when it is not found. Implies `--installed-cores` | | `--save-dir PATH` | The frontend's save directory (RetroArch's `savefile_directory`), for the files some cores read there, when the run cannot find it. The core's own folder is still added below it when the frontend sorts saves by core | -| `--system NAMES` | Install only these systems. Comma-separated or repeated. `--list-systems` prints the names, `other` included | +| `--system NAMES` | Install only these systems. Comma-separated or repeated. `--list-systems` prints the names, `other` included. A short name the platform does not use (`gba`, `psx`) is refused with the ids it may stand for, never taken in their place | | `--core NAMES` | Keep the platform's own list and only the extra files these cores load. `--list-cores` prints the names | | `--emulator NAMES` | Install what these emulators read on the detected platform, and nothing else: their own files and the platform's files of the systems they run. Each emulator page on the site gives this command. `--list-emulators` prints the names, buildbot names such as `mednafen_psx` included | | `--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` |