From b3f8f64395654073fc8e588aee064d736a546494 Mon Sep 17 00:00:00 2001
From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com>
Date: Fri, 6 Nov 2026 08:33:01 +0100
Subject: [PATCH] fix: refuse options a listing does not read
Two listing options printed one list, and --list-platforms, --list-targets and --list-emulators printed the same output with --system, --region or --core as without, code 0. A run now prints one list, and an option a listing ignores is refused.
---
install.ps1 | 2 +-
install.py | 25 +++++++++++++++++++-
install.sh | 2 +-
install/launchers/index.json | 16 ++++++-------
tests/test_install.py | 44 +++++++++++++++++++++++++++++++++++
tests/test_install_offline.py | 7 +++++-
wiki/installer.md | 10 ++++----
7 files changed, 89 insertions(+), 17 deletions(-)
diff --git a/install.ps1 b/install.ps1
index 57aca4f16..dab7b3426 100644
--- a/install.ps1
+++ b/install.ps1
@@ -16,7 +16,7 @@
# and Invoke-WebRequest throws on a cut transfer before the hash is read:
# the site's build rewrites this line there (scripts/sitebootstrap.py).
$mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
- $defaultInstallSha256 = "482a7710467d988f3878a5d8ebe5f885dfee35c3a1aac0df05077b6b062a7610"
+ $defaultInstallSha256 = "7622c5e1a9ec7a76bf3c325b1fd356a22b53aefe0b625d6c12447afee9414300"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
diff --git a/install.py b/install.py
index baa21de99..429dffa35 100644
--- a/install.py
+++ b/install.py
@@ -13429,6 +13429,29 @@ def main() -> None:
if (args.list_platforms or args.list_targets or args.list_systems
or args.list_cores or args.list_emulators):
parser.error("--for-games narrows an install, not a listing")
+ # Applied or refused, never ignored: a run prints one list, and the
+ # narrowing options apply to --list-systems and --list-cores only, past
+ # --target and --installed-cores, which --list-emulators also reads.
+ listings = [flag for flag, given in (
+ ("--list-platforms", args.list_platforms), ("--list-targets", args.list_targets),
+ ("--list-systems", args.list_systems), ("--list-cores", args.list_cores),
+ ("--list-emulators", args.list_emulators)) if given]
+ if len(listings) > 1:
+ parser.error(f"{listings[0]} and {listings[1]} each print a list: run them "
+ "one at a time")
+ if listings and listings[0] in ("--list-platforms", "--list-targets",
+ "--list-emulators"):
+ unread = (("--system", args.system), ("--core", args.core),
+ ("--region", args.region), ("--emulator", args.emulator))
+ if listings[0] != "--list-emulators":
+ unread += (("--target", args.target), ("--every-board", args.every_board),
+ ("--installed-cores", args.installed_cores))
+ if listings[0] == "--list-platforms":
+ unread += (("--platform", args.platform),)
+ given = next((flag for flag, value in unread if value), "")
+ if given:
+ parser.error(f"{listings[0]} prints the whole list, which {given} "
+ "does not narrow")
if args.declared_only:
# Applied or refused, never ignored.
for given, reason in (
@@ -15176,7 +15199,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
# --installed-cores is below: EmuDeck installed 3.2 GB and only then was
# steamos refused for RetroArch, with no summary of what had landed.
target_lists: dict[str, dict] = {}
- if args.target and not args.list_targets:
+ if args.target:
for plat_name, _bios_path in platforms:
targets = fetch_targets(plat_name)
if targets is None:
diff --git a/install.sh b/install.sh
index 4f1d02bbf..cbdaed395 100755
--- a/install.sh
+++ b/install.sh
@@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins
# curl or wget stop on a cut transfer before the hash is read: the site's
# build rewrites this line there (scripts/sitebootstrap.py).
MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
-DEFAULT_INSTALL_SHA256="482a7710467d988f3878a5d8ebe5f885dfee35c3a1aac0df05077b6b062a7610"
+DEFAULT_INSTALL_SHA256="7622c5e1a9ec7a76bf3c325b1fd356a22b53aefe0b625d6c12447afee9414300"
MAX_INSTALLER_BYTES=2097152
cleanup() {
diff --git a/install/launchers/index.json b/install/launchers/index.json
index 6f8f8ff49..2be9a4f89 100644
--- a/install/launchers/index.json
+++ b/install/launchers/index.json
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
- "size": 1104874,
- "sha256": "482a7710467d988f3878a5d8ebe5f885dfee35c3a1aac0df05077b6b062a7610",
+ "size": 1106280,
+ "sha256": "7622c5e1a9ec7a76bf3c325b1fd356a22b53aefe0b625d6c12447afee9414300",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
- "size": 1158501,
- "sha256": "0ecf443039c136286322a57b33f1374dd52de0b5f2a772fbae93c1f98c19b62d"
+ "size": 1159907,
+ "sha256": "6460af3bc1a9d27c7373b42cf45f19bf8619b8e55daac9892af9f57df21b258e"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
- "size": 1104874,
- "sha256": "482a7710467d988f3878a5d8ebe5f885dfee35c3a1aac0df05077b6b062a7610",
+ "size": 1106280,
+ "sha256": "7622c5e1a9ec7a76bf3c325b1fd356a22b53aefe0b625d6c12447afee9414300",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
- "size": 1104874,
- "sha256": "482a7710467d988f3878a5d8ebe5f885dfee35c3a1aac0df05077b6b062a7610",
+ "size": 1106280,
+ "sha256": "7622c5e1a9ec7a76bf3c325b1fd356a22b53aefe0b625d6c12447afee9414300",
"executable": false
},
{
diff --git a/tests/test_install.py b/tests/test_install.py
index 7efea9660..658385670 100644
--- a/tests/test_install.py
+++ b/tests/test_install.py
@@ -4475,6 +4475,50 @@ def _install_run(env: dict, *argv: str, **kwargs) -> subprocess.CompletedProcess
)
+class AListingAppliesItsOptionsOrRefusesThem(unittest.TestCase):
+ """--list-systems --list-cores printed the systems alone, and
+ --list-platforms, --list-targets and --list-emulators printed the same
+ list with --system, --region or --core as without, code 0."""
+
+ def test_two_listings_or_an_option_a_listing_ignores_are_refused(self):
+ env = dict(os.environ, RETROBIOS_OS="linux")
+ given = ["--platform", "retroarch", "--offline", "--no-input",
+ "--manifest", "install/retroarch.json"]
+ for argv, words in (
+ ([*given, "--list-systems", "--list-cores"],
+ "--list-systems and --list-cores each print a list"),
+ ([*given, "--list-cores", "--list-emulators"],
+ "--list-cores and --list-emulators each print a list"),
+ (["--list-platforms", "--list-systems", "--platform", "retroarch"],
+ "--list-platforms and --list-systems each print a list"),
+ (["--list-platforms", "--region", "us"],
+ "--list-platforms prints the whole list, which --region does not narrow"),
+ (["--list-platforms", "--platform", "retroarch"],
+ "--list-platforms prints the whole list, which --platform does not narrow"),
+ ([*given, "--list-targets", "--system", "sony-playstation"],
+ "--list-targets prints the whole list, which --system does not narrow"),
+ ([*given, "--list-targets", "--target", "switch"],
+ "--list-targets prints the whole list, which --target does not narrow"),
+ ([*given, "--list-emulators", "--system", "sony-playstation"],
+ "--list-emulators prints the whole list, which --system does not narrow"),
+ ([*given, "--list-emulators", "--core", "beetle_psx"],
+ "--list-emulators prints the whole list, which --core does not narrow"),
+ ):
+ with self.subTest(argv=argv):
+ proc = _install_run(env, *argv)
+ self.assertEqual(proc.returncode, 2, proc.stdout + proc.stderr)
+ self.assertIn(words, proc.stderr)
+
+ def test_the_options_a_listing_reads_still_apply(self):
+ env = dict(os.environ, RETROBIOS_OS="linux")
+ given = ["--platform", "retroarch", "--offline", "--no-input",
+ "--manifest", "install/retroarch.json"]
+ whole = _install_run(env, *given, "--list-cores")
+ narrowed = _install_run(env, *given, "--list-cores", "--system", "nintendo-gba")
+ self.assertEqual((whole.returncode, narrowed.returncode), (0, 0), narrowed.stderr)
+ self.assertLess(len(narrowed.stdout), len(whole.stdout))
+
+
class AMissingPackNamesTheOptionAndThePacksBeside(unittest.TestCase):
"""A typo in --from-pack said "Error: X does not exist": not the option,
not the pack sitting next to the name typed."""
diff --git a/tests/test_install_offline.py b/tests/test_install_offline.py
index b46ea29be..08d9194d2 100644
--- a/tests/test_install_offline.py
+++ b/tests/test_install_offline.py
@@ -778,7 +778,12 @@ class OfflineMakesNoRequest(Scratch):
self.assertIn("--offline", proc.stderr)
def test_a_listing_and_a_pack_need_no_network_anyway(self):
- proc = self.run_installer("--offline", "--list-platforms")
+ # --platform alone with it is refused: the listing names every one.
+ proc = subprocess.run(
+ [sys.executable, str(REPO_ROOT / "install.py"), "--offline", "--list-platforms"],
+ env=_offline_env(), capture_output=True, text=True, timeout=120,
+ check=False, stdin=subprocess.DEVNULL,
+ )
self.assertEqual(proc.returncode, 0, proc.stderr)
diff --git a/wiki/installer.md b/wiki/installer.md
index 88da4c7b0..f66a8eb54 100644
--- a/wiki/installer.md
+++ b/wiki/installer.md
@@ -976,9 +976,9 @@ what the options name, everything by default.
| `--required-only` | With `--for-games`, only the files the games cannot start without |
| `--no-game-data` | Leave out game data (a game's own files, not firmware) and install the firmware only. What it leaves out is named. See [Firmware without game data](#firmware-without-game-data) |
| `--declared-only` | Keep only the files the platform's own list names (`cores: null` in the manifest), not the extra files cores load. With `--check`, the answer is about the list the platform itself checks. Refused with `--core`, the listings it does not change, `--standalone-copies` and `--tui` |
-| `--list-systems` | Print a platform's systems with the files and size each holds, the files other systems share too counted apart |
-| `--list-cores` | Print the cores whose extra files a platform installs |
-| `--list-emulators` | Print every name `--emulator` accepts, each buildbot name beside the emulator it stands for |
+| `--list-systems` | Print a platform's systems with the files and size each holds, the files other systems share too counted apart. `--system`, `--core`, `--region`, `--target` and `--installed-cores` narrow it, as they narrow an install. A run prints one list: two listing options are refused |
+| `--list-cores` | Print the cores whose extra files a platform installs. The options that narrow `--list-systems` narrow it too |
+| `--list-emulators` | Print every name `--emulator` accepts, each buildbot name beside the emulator it stands for. `--target` and `--installed-cores` narrow it, and `--system`, `--core`, `--region` and `--emulator` are refused |
| `--romm-firmware TEMPLATE` | RomM: where each platform's firmware goes in the library, as `filesystem.structure.firmware` in RomM's `config.yml` says (`{platform}/bios` for a library laid out as `{platform}/roms`). Read from `config.yml` beside the library when it is there. See [RomM firmware layout](#romm-firmware-layout) |
| `--no-input` | Never ask a question, even at a terminal. A run whose output goes to a file or a pipe asks nothing either, and says so on stderr. It finds the setups and the mounted cards the screen finds: one is taken, as Enter takes it, two stop the run with an `error` event (`stage` `detect`, `reason` `conflict`) naming each with its `--platform` and `--dest`, and none stops it too, with `reason` `not_found` when a file list or a pack names the platform |
| `--check` | Report and exit without writing |
@@ -994,8 +994,8 @@ what the options name, everything by default.
| `--tui` | Open the full-screen interface, or stop with the reason it cannot open here (code 2) instead of falling back to the questions. Wins over `RETROBIOS_PLAIN` and `ACCESSIBLE`. See [The full-screen interface](#the-full-screen-interface) |
| `--plain` | Ask the questions line by line instead of the full-screen interface, the mode for a screen reader. See [The full-screen interface](#the-full-screen-interface) |
| `--version` | Print the installer's identity, the first 12 hex digits of the SHA-256 the bootstraps pin |
-| `--list-platforms` | Print the supported platforms and what was detected here |
-| `--list-targets` | Print the hardware targets a platform publishes, with the core count each carries |
+| `--list-platforms` | Print the supported platforms and what was detected here. An option that narrows a list (`--platform`, `--system`, `--core`, `--region`, `--emulator`, `--target`, `--every-board`, `--installed-cores`) is refused |
+| `--list-targets` | Print the hardware targets a platform publishes, with the core count each carries. `--system`, `--core`, `--region`, `--emulator`, `--target`, `--every-board` and `--installed-cores` are refused |
| `--jobs N`, `-j N` | Parallel downloads, 1 to 32, default 8 |
| `--verbose`, `-v` | Print per-attempt failures and hash mismatches |
| `--standalone-copies` | Also copy into detected standalone emulator directories |