From dedcf321927e7f6bb4b3b25a1bdd33b44e27bce9 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sun, 18 Oct 2026 07:38:52 +0200 Subject: [PATCH] fix: report a failed bootstrap download plainly --- install.ps1 | 22 +++++++++++++++++++--- install.sh | 20 +++++++++++++++++--- tests/test_install.py | 35 +++++++++++++++++++++++++++++++++++ wiki/installer.md | 2 +- 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/install.ps1 b/install.ps1 index dc685b85..98f34278 100644 --- a/install.ps1 +++ b/install.ps1 @@ -33,7 +33,14 @@ try { [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } $temporary = Join-Path ([IO.Path]::GetTempPath()) ("retrobios-install-{0}.py" -f [Guid]::NewGuid()) - Invoke-WebRequest -Uri $uri -OutFile $temporary -UseBasicParsing + try { + Invoke-WebRequest -Uri $uri -OutFile $temporary -UseBasicParsing + } catch { + # The raw WebException read as a crash and named no next step. + throw ("The installer could not be downloaded from {0}: {1} " + + "Check the network connection and any proxy, then run the same command again. " + + "Nothing was run and nothing was written.") -f $url, $_.Exception.Message + } if ((Get-Item -LiteralPath $temporary).Length -gt $maximumInstallerBytes) { throw "Downloaded installer exceeds the size limit." } @@ -54,11 +61,14 @@ try { $python = Get-Command python -ErrorAction SilentlyContinue } } + $needPython = "Python 3.8 or newer is required. Install it with " + + "'winget install Python.Python.3.12' or from https://www.python.org/downloads/ " + + "(tick 'Add python.exe to PATH'), then open a new PowerShell window and run this command again." if (-not $python) { - throw "Python 3.8 or newer is required." + throw $needPython } & $python.Source @launcher -c "import sys; raise SystemExit(sys.version_info < (3, 8))" - if ($LASTEXITCODE -ne 0) { throw "Python 3.8 or newer is required." } + if ($LASTEXITCODE -ne 0) { throw $needPython } & $python.Source @launcher $installer @InstallerArguments # The installer has already said what happened: its status is passed on # as it is (1 for files left to fetch under --check, 2 when nothing was @@ -66,6 +76,12 @@ try { # as a crash and flattens every code to one. $installerExit = $LASTEXITCODE } +catch { + # One line saying what failed and what to do, rather than the red + # exception block PowerShell prints for an uncaught throw. + [Console]::Error.WriteLine("Error: " + $_.Exception.Message) + $installerExit = 1 +} finally { if ($temporary -and (Test-Path -LiteralPath $temporary)) { Remove-Item -LiteralPath $temporary -Force diff --git a/install.sh b/install.sh index 5441786c..fe8db511 100755 --- a/install.sh +++ b/install.sh @@ -50,16 +50,30 @@ if [ -z "$INSTALLER" ] || [ ! -f "$INSTALLER" ]; then fi TEMP_DIRECTORY=$(mktemp -d) TEMP_INSTALLER="$TEMP_DIRECTORY/install.py" + # Quiet but for the error line: a progress table followed by + # "curl: (6) ..." and curl's own exit code told nobody what failed. if command -v curl >/dev/null 2>&1; then - curl --fail --location --proto '=https' --tlsv1.2 \ - "$install_url" --output "$TEMP_INSTALLER" + fetch() { + curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \ + --connect-timeout 30 --retry 2 "$install_url" --output "$TEMP_INSTALLER" + } elif command -v wget >/dev/null 2>&1; then - wget --https-only --output-document="$TEMP_INSTALLER" "$install_url" + fetch() { + wget --quiet --https-only --timeout=30 --tries=3 \ + --output-document="$TEMP_INSTALLER" "$install_url" + } else echo "Error: curl or wget is required to download the installer." >&2 echo " On Debian or Ubuntu: sudo apt install curl" >&2 exit 1 fi + if ! fetch_error=$(fetch 2>&1); then + echo "Error: the installer could not be downloaded from $install_url" >&2 + echo " $(printf '%s\n' "$fetch_error" | tail -n 1)" >&2 + echo " Check the network connection, and HTTPS_PROXY if this network needs a proxy," >&2 + echo " then run the same command again. Nothing was run and nothing was written." >&2 + exit 1 + fi actual_size=$(wc -c < "$TEMP_INSTALLER" | tr -d ' ') if [ "$actual_size" -gt "$MAX_INSTALLER_BYTES" ]; then echo "Error: downloaded installer exceeds the size limit." >&2 diff --git a/tests/test_install.py b/tests/test_install.py index 35e3cf10..e87ef7d8 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -1895,6 +1895,41 @@ class TestAvailablePlatforms(unittest.TestCase): self.assertTrue(marker.is_file(), "piped bootstrap did not download install.py") self.assertIn("retroarch", proc.stdout) + def test_a_failed_download_says_so_in_one_message(self): + """Offline, the one-liner printed curl's progress table and + "curl: (6) ...", then exited 6, a code the table does not list.""" + scratch = REPO_ROOT / "tmp" / "tests" + scratch.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(dir=scratch) as directory: + root = Path(directory) + fake_bin = root / "bin" + fake_bin.mkdir() + (fake_bin / "curl").write_text( + "#!/bin/sh\necho 'curl: (6) Could not resolve host: x' >&2\nexit 6\n", + encoding="utf-8", + ) + (fake_bin / "curl").chmod(0o755) + env = os.environ.copy() + env.update( + PATH=f"{fake_bin}{os.pathsep}{env['PATH']}", + TMPDIR=str(root), + RETROBIOS_INSTALL_URL="https://example.invalid/install.py", + ) + proc = subprocess.run( + ["sh", "-s"], cwd=root, env=env, + input=(REPO_ROOT / "install.sh").read_text(encoding="utf-8"), + capture_output=True, text=True, timeout=30, check=False, + ) + self.assertEqual(proc.returncode, 1, proc.stderr) + self.assertIn("could not be downloaded", proc.stderr) + self.assertIn("Nothing was run and nothing was written", proc.stderr) + self.assertEqual(proc.stderr.count("curl: (6)"), 1) + + def test_powershell_turns_a_failure_into_one_line(self): + source = (REPO_ROOT / "install.ps1").read_text(encoding="utf-8") + self.assertIn('[Console]::Error.WriteLine("Error: "', source) + self.assertIn("winget install Python", source) + @unittest.skipUnless(shutil.which("script"), "script(1) provides the terminal") def test_piped_one_liner_hands_the_terminal_to_the_installer(self): """curl | sh gives the installer the script as stdin, so isatty() diff --git a/wiki/installer.md b/wiki/installer.md index 73f69022..de9ea1d8 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -420,7 +420,7 @@ followed. | Code | Meaning | |------|---------| | `0` | everything asked for is installed or already up to date | -| `1` | a failure: an unusable base URL, an unknown platform or target, `--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 while others installed, or `--check` found files to fetch | +| `1` | a failure: the one-liner could not download or verify `install.py`, or found no Python 3.8 (nothing was run), an unusable base URL, an unknown platform or target, `--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 while others installed, or `--check` found files to fetch | | `2` | nothing was installed: every file the run tried failed (a BIOS folder that cannot be written, for one) | | `130` | stopped with Ctrl+C; run the same command again to carry on | | `141` | the output was piped to a reader that closed early (`--list \| head`) |