diff --git a/install.ps1 b/install.ps1 index fb834c753..36a10ee24 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 = "9d9768d0d0a9cd702d1eefb60146ae96fc343e1699ce8780afca57b4fb4dd5ac" + $defaultInstallSha256 = "8a276fc96f3172fc0f447e57f8ff6aab53614982043197edcb435d54b1903411" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 6a4e48611..03c789a58 100644 --- a/install.py +++ b/install.py @@ -7712,7 +7712,7 @@ def _install_standalone_folders( if args.check: if pending: print("\n" + _check_fetch_line(pending, "its folder")) - sys.exit(1) + sys.exit(3) return True if args.dry_run: return True @@ -16320,9 +16320,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: skipped=_skipped(omitted_seen), left_out=_left_out_events(left_out_seen), kept=KEPT.summary(), interrupted=total_interrupted, ) - # A check scripted as `--check && launch` reads the exit code only. + # A check scripted as `--check && launch` reads the exit code only. Its + # own code, 3: with 1, a check that found files missing could not be + # told from a check that never ran (a list refused, a --from missing). if args.check and check_pending: - sys.exit(1) + sys.exit(3) if not args.check and not listing and not args.dry_run: _report_outcome( installed_paths, diff --git a/install.sh b/install.sh index afb8f61e7..168567117 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="9d9768d0d0a9cd702d1eefb60146ae96fc343e1699ce8780afca57b4fb4dd5ac" +DEFAULT_INSTALL_SHA256="8a276fc96f3172fc0f447e57f8ff6aab53614982043197edcb435d54b1903411" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/install/launchers/index.json b/install/launchers/index.json index 14be142b6..59a3cb477 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": 1120968, - "sha256": "9d9768d0d0a9cd702d1eefb60146ae96fc343e1699ce8780afca57b4fb4dd5ac", + "size": 1121120, + "sha256": "8a276fc96f3172fc0f447e57f8ff6aab53614982043197edcb435d54b1903411", "executable": false }, { @@ -71,8 +71,8 @@ "muos": { "archive": { "dest": "ARCHIVE/RetroBIOS.muxapp", - "size": 1174595, - "sha256": "0f65a01eaa506007e580d81d8687a91f32da4406ef7a95feb3dac992edf02c86" + "size": 1174747, + "sha256": "a92c48887550f3f68e2528f533c0acc49be77263f6da3e2cb50571d8d97a2d9a" }, "members": [ { @@ -92,8 +92,8 @@ { "name": "RetroBIOS/install.py", "repo_path": "install.py", - "size": 1120968, - "sha256": "9d9768d0d0a9cd702d1eefb60146ae96fc343e1699ce8780afca57b4fb4dd5ac", + "size": 1121120, + "sha256": "8a276fc96f3172fc0f447e57f8ff6aab53614982043197edcb435d54b1903411", "executable": false }, { @@ -166,8 +166,8 @@ { "dest": ".config/modules/retrobios/install.py", "repo_path": "install.py", - "size": 1120968, - "sha256": "9d9768d0d0a9cd702d1eefb60146ae96fc343e1699ce8780afca57b4fb4dd5ac", + "size": 1121120, + "sha256": "8a276fc96f3172fc0f447e57f8ff6aab53614982043197edcb435d54b1903411", "executable": false }, { diff --git a/scripts/generate_installer_doc.py b/scripts/generate_installer_doc.py index 7fd134d65..bd391ccce 100644 --- a/scripts/generate_installer_doc.py +++ b/scripts/generate_installer_doc.py @@ -443,9 +443,9 @@ class Sandbox: (system / "system" / "gba_bios.bin").write_bytes(data["gba_bios.bin"]) with zipfile.ZipFile(system / "PSX BIOS.zip", "w") as archive: archive.writestr("extra.bin", data["pcsx/extra.bin"]) - # A check that flags a file ends on 1, as documented. + # A check that flags a file ends on 3, as documented. return self.run("retroarch", "--check", "--manifest", str(listed), home=home, - codes=(1,)) + codes=(3,)) old = home / "old" old.mkdir() for dest in ("scph5500.bin", "scph5501.bin"): diff --git a/tests/test_install.py b/tests/test_install.py index 90008eab2..66e30d8db 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -1509,7 +1509,7 @@ class InstallerStaysOnPython38AndTheStdlib(unittest.TestCase): cwd=REPO_ROOT, env=env, capture_output=True, text=True, timeout=300, check=False, ) - self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr) + self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr) self.assertIn("to download:", proc.stdout) def test_no_syntax_a_312_parser_lets_through(self): @@ -4533,6 +4533,24 @@ class AFullDriveIsSaidAsOne(unittest.TestCase): self.assertEqual(len(attempts), 1) +class ACheckThatFindsFilesMissingIsNoFailure(unittest.TestCase): + """--check ended on 1 for files missing as for a list refused or a + --from folder that does not exist: a scheduled check could not tell a + BIOS folder to complete from a tool that never ran.""" + + def test_missing_is_3_and_a_failure_stays_1(self): + from test_install_selection import FakeRepository # noqa: PLC0415 + + with FakeRepository() as repo: + missing = repo.run("--no-input", "--check") + nowhere = repo.run("--no-input", "--check", "--from", str(repo.root / "nope")) + repo.run("--no-input") + complete = repo.run("--no-input", "--check") + self.assertEqual(missing.returncode, 3, missing.stdout + missing.stderr) + self.assertEqual(nowhere.returncode, 1, nowhere.stdout + nowhere.stderr) + self.assertEqual(complete.returncode, 0, complete.stdout + complete.stderr) + + class OneEmulatorsFilesInOneCommand(unittest.TestCase): """An emulator page offers one command for that emulator's files. --core kept the platform's whole list, and narrowing to cores kept the @@ -4921,7 +4939,7 @@ class TheInstallerSaysWhatItFound(unittest.TestCase): self.assertEqual(self._run("--dest", str(dest), "--system", "atari-lynx", "--yes").returncode, 0) (dest / "lynxboot.img").write_bytes(b"corrupt") proc = self._run("--dest", str(dest), "--system", "atari-lynx", "--check") - self.assertEqual(proc.returncode, 1) + self.assertEqual(proc.returncode, 3) self.assertIn("wrong copy: lynxboot.img", proc.stdout) self.assertIn("without --check", proc.stdout) @@ -5607,7 +5625,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun): (dest / "scph5501.bin").write_bytes(b"junk") proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(dest), "--check", "--no-input") - self.assertEqual(proc.returncode, 1, proc.stderr) + self.assertEqual(proc.returncode, 3, proc.stderr) listing = proc.stdout.split("to download:\n", 1)[1].splitlines() self.assertEqual(listing[0].strip(), "wrong copy: scph5501.bin") @@ -5639,7 +5657,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun): shutil.copy(REPO_ROOT / repo_path, dest / "PPSSPP") proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(dest), "--system", "sony-psp", "--check", "--no-input") - self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr) + self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr) self.assertNotIn("Everything is in place", proc.stdout) found = re.search(r"(\d+) in place, (\d+) missing", proc.stdout) self.assertIsNotNone(found, proc.stdout) @@ -5653,7 +5671,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun): proc = _install_run({**self.env, "RETROBIOS_INSTALL_OFFLINE": "1"}, "--platform", "retroarch", "--dest", str(dest), "--check", "--no-input", "--manifest", str(REPO_ROOT / "install" / "retroarch.json")) - self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr) + self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr) missing = re.search(r"0 in place, (\d+) missing", proc.stdout).group(1) self.assertIn(f"\n {missing} files not in place:\n", proc.stdout) self.assertIn(f" {missing} files need the internet", proc.stdout) @@ -6764,13 +6782,13 @@ class EventsDescribeTheRun(unittest.TestCase, _SyntheticTree): def test_a_check_run(self): proc, events = self._run("--check") - self.assertEqual(proc.returncode, 1) + self.assertEqual(proc.returncode, 3) installer = events[0]["installer"] self.assertEqual(events, self._head("check", installer) + [ {"event": "summary", "mode": "check", "installed": 0, "up_to_date": 1, "pending": 3, "failed": [], "skipped": self.SKIPPED, "left_out": [], "kept": [], "interrupted": False}, - {"event": "end", "code": 1}, + {"event": "end", "code": 3}, ]) self.assertIn("1 in place, 2 missing, 1 wrong copy", proc.stderr) self.assertFalse((self.bios / "c.bin").exists()) @@ -6986,7 +7004,7 @@ class TheInstallerPageIsWrittenFromTheCode(unittest.TestCase): self.scratch.read_text(encoding="utf-8")) def test_the_exit_codes_are_the_ones_install_py_exits_with(self): - self.assertEqual(self.generator.exit_codes(), [0, 1, 2, 130, 141]) + self.assertEqual(self.generator.exit_codes(), [0, 1, 2, 3, 130, 141]) def test_a_missing_marker_stops_it(self): page = self.scratch.read_text(encoding="utf-8") diff --git a/tests/test_install_experience.py b/tests/test_install_experience.py index 2ea164a30..addee3a91 100644 --- a/tests/test_install_experience.py +++ b/tests/test_install_experience.py @@ -546,7 +546,7 @@ class AFolderThisUserCannotEnter(unittest.TestCase): finally: os.chmod(locked, 0o755) self.assertNotIn("Traceback", proc.stderr) - self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr) + self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr) self.assertIn("wrong copy: pcsx/extra.bin", proc.stdout) self.assertIn("could not be read (Permission denied)", proc.stdout) self.assertIn("missing: scph5501.bin", proc.stdout) diff --git a/tests/test_install_from.py b/tests/test_install_from.py index ff28c52c7..cd2cf473a 100644 --- a/tests/test_install_from.py +++ b/tests/test_install_from.py @@ -219,7 +219,7 @@ class CopyFromAnOldCard(FromFolder): self.keep("gba_bios.bin", GBA) proc = self.run_installer("--check", "--manifest", str(self.list), "--from", str(self.old)) - self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr) + self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr) self.assertIn(f"{self.old} holds 2 of these files: a run without --check " "copies them.", proc.stdout) self.assertIn(f"in {self.old}: BIOS/SCPH5501.BIN", proc.stdout) @@ -605,7 +605,7 @@ class UnreadableCopies(FromFolder): for options in (("--check",), ("--dry-run",), ()): with self.subTest(options=options): proc = self.install(*options) - self.assertEqual(proc.returncode, 1 if options != ("--dry-run",) else 0, + self.assertEqual(proc.returncode, {("--check",): 3, ("--dry-run",): 0}.get(options, 1), proc.stdout + proc.stderr) self.assertIn(line, proc.stdout) self.assertNotIn("notes.txt", proc.stdout) diff --git a/tests/test_install_offline.py b/tests/test_install_offline.py index 08d9194d2..56bc25a37 100644 --- a/tests/test_install_offline.py +++ b/tests/test_install_offline.py @@ -118,7 +118,7 @@ class Scratch(unittest.TestCase): def check(self) -> str: proc = self.run_installer("--check", "--manifest", str(self.list), "--verbose") - self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr) + self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr) return proc.stdout def complete(self, *skip: str) -> None: @@ -1035,7 +1035,7 @@ class OneOfflineVocabulary(Scratch): """"Run the same command without --check to fetch them".""" proc = self.run_installer("--check", "--offline", "--manifest", str(self.list)) out = proc.stdout + proc.stderr - self.assertEqual(proc.returncode, 1, out) + self.assertEqual(proc.returncode, 3, out) self.assertNotIn("to fetch them", out) self.assertIn("4 files need the internet, which --offline does not use.", out) self.assertIn("Copy them from your old card or a folder (--from)", out) diff --git a/tests/test_install_selection.py b/tests/test_install_selection.py index d0cc9dac3..9f50aec90 100644 --- a/tests/test_install_selection.py +++ b/tests/test_install_selection.py @@ -1726,7 +1726,7 @@ class SomethingElseAtADestination(unittest.TestCase): with FakeRepository() as repo: self.prepare(repo) proc = repo.run("--check") - self.assertEqual(proc.returncode, 1, proc.stderr) + self.assertEqual(proc.returncode, 3, proc.stderr) self.assertNotIn("Traceback", proc.stderr) self.assertIn(" wrong copy: scph5500.bin\n a folder sits where this " "file goes: move it away, then run again", proc.stdout) diff --git a/wiki/installer.md b/wiki/installer.md index dbb48e664..a997afe89 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -577,7 +577,7 @@ Checking existing files... `Everything is in place.` is said only when the data folders are complete too. With the BIOS files in place and data files missing (PPSSPP's assets, Dolphin's `Sys`), the line counts those instead (`169 data files to fetch`), -and `--check` exits with code 1. The files the collection does not hold are +and `--check` exits with code 3. The files the collection does not hold are listed after it again, as long as they are absent from the folder. A number before `wrong copies` means a file is in place with contents the platform does not accept. Running the install again replaces it and keeps the old one, see @@ -1011,8 +1011,8 @@ what the options name, everything by default. -`--check` exits with code 1 when a file is missing or does not match, and 0 -when every one does. What it lists is under +`--check` exits with code 3 when a file is missing or does not match, 0 +when every one does, and 1 when the check itself failed. What it lists is under [Why a file is refused](#why-a-file-is-refused). An install leaves a file with the expected hash untouched and replaces any other, the old one kept beside the BIOS folder. Files the list does not name are never read, moved or deleted. @@ -1768,8 +1768,9 @@ fields, the installer refuses to narrow rather than install everything. | Code | Meaning | |------|---------| | `0` | everything asked for is installed or already in place | -| `1` | a failure: the one-liner could not download or verify `install.py`, or found no Python 3.8 with its ssl, unicodedata and zlib modules (nothing was run), an unusable base URL, `--installed-cores` with no core found, a manifest that fails validation, a fetch that never succeeded, no platform to install for in a non-interactive run, some files failed or were left out by `--offline` while others installed, `--check` found files to fetch, a `--from-pack` pack that cannot be read, lacks a part or is for another platform, a `--from` folder that does not exist or cannot be read, a BIOS folder that is a file or lies under one, a `--dest` that is not the card of the system named (another system's card, a folder inside one, a drive with none of its files where its card always has them, or a folder that does not exist), `--restore` could not put a file back, or `--add-app` could not put every file of the app on the card | +| `1` | a failure: the one-liner could not download or verify `install.py`, or found no Python 3.8 with its ssl, unicodedata and zlib modules (nothing was run), an unusable base URL, `--installed-cores` with no core found, a manifest that fails validation, a fetch that never succeeded, no platform to install for in a non-interactive run, some files failed or were left out by `--offline` while others installed, a `--from-pack` pack that cannot be read, lacks a part or is for another platform, a `--from` folder that does not exist or cannot be read, a BIOS folder that is a file or lies under one, a `--dest` that is not the card of the system named (another system's card, a folder inside one, a drive with none of its files where its card always has them, or a folder that does not exist), `--restore` could not put a file back, or `--add-app` could not put every file of the app on the card | | `2` | nothing was installed: every file the run tried failed (a BIOS folder that cannot be written, for one), or `--offline` left every file out, the drive has too little free space for the download (checked before any folder is created), or the question about a folder that does not exist, named with `--dest` or typed at the folder question, was answered no. Also an option the installer refused before any download: an unknown option, `--jobs` outside 1 to 32, `--target` with `--every-board`, `--target` with `--from-pack` or `--offline` (or `RETROBIOS_INSTALL_OFFLINE` set) when no core list per board is at hand (the pack carries none and none is beside `--manifest` as `targets/.json`), or for a platform that publishes none, which is said to have no hardware targets, `--list-targets` with `--offline` and neither, `--add-app` with `--check`, `--dry-run`, a listing, `--from-pack` or `--offline`. The message then starts with `install.py: error:`. Also an unknown platform, target, system, core or emulator, said in an `Error:` or `Unknown platform` line and an `error` event whose `stage` is `option`. Also `--add-app` for a folder on no card that takes the app, said in an `Error:` line and an `app` event before anything is written. With `--restore`, a folder that is not a dated folder inside `retrobios-backup`. Also a quit chosen before the install, as `gh` ends a cancelled prompt: `q` at a question, `q` or **Quit** on the full screen, which then keeps `RetroBIOS: quit. Nothing was installed.`, and the full screen closed with no button pressed (`RETROBIOS_IDLE_EXIT`). No code but `0` closes a Windows Terminal tab started on the installer itself, so such a tab stays open on a quit | +| `3` | `--check` ran and found files to fetch or wrong copies, nothing having failed. A failure takes `1` as before: a manifest refused, a `--from` folder that does not exist. Until November 2026 this case also ended on `1`, so a script that reads any code but `0` as "not ready" needs no change | | `130` | stopped with Ctrl+C, or with Ctrl+C or Ctrl+D at a question, the terminal keeping `RetroBIOS: stopped. Nothing was installed.` before an install. Run the same command again to carry on | | `141` | the output was piped to a reader that closed early (`--list-platforms \| head`) | @@ -1792,9 +1793,9 @@ in the session. Failures that are per-file, such as a download exhausting its three attempts, are counted and reported at the end. -To tell missing files from a failure, read the `check` event of -[Machine-readable events](#machine-readable-events): `missing` and `wrong_hash` -count what is left. +`--check` tells missing files (`3`) from a failure (`1`) by its code. The +`check` event of [Machine-readable events](#machine-readable-events) counts what +is left in `missing` and `wrong_hash`. ## Machine-readable events