From c29c456206a7081d9bec40d18a679eb0ece73252 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:04:54 +0200 Subject: [PATCH] fix: name the python install command per system Without Python the bootstrap told every system to run sudo apt install python3. It now reads Termux, macOS and /etc/os-release to name pkg, xcode-select, apt, dnf, pacman, zypper or apk, and in Termux offers to run pkg install python, which needs no root. --- install.sh | 64 +++++++++++++++++++++++++++++++++++++------ tests/test_install.py | 37 +++++++++++++++++++++++++ wiki/first-install.md | 8 ++++-- 3 files changed, 99 insertions(+), 10 deletions(-) diff --git a/install.sh b/install.sh index 82aea29f4..8da70dda8 100755 --- a/install.sh +++ b/install.sh @@ -118,17 +118,65 @@ if [ -z "$INSTALLER" ] || [ ! -f "$INSTALLER" ]; then INSTALLER="$TEMP_INSTALLER" fi -PYTHON="" -for command_name in python3 python; do - if command -v "$command_name" >/dev/null 2>&1 \ - && "$command_name" -c 'import sys; raise SystemExit(sys.version_info < (3, 8))' 2>/dev/null; then - PYTHON=$command_name - break +find_python() { + PYTHON="" + for command_name in python3 python; do + if command -v "$command_name" >/dev/null 2>&1 \ + && "$command_name" -c 'import sys; raise SystemExit(sys.version_info < (3, 8))' 2>/dev/null; then + PYTHON=$command_name + return 0 + fi + done + return 1 +} + +# The command that installs Python here, named for this system: Termux has +# no sudo and installs with pkg, macOS has no apt. +python_install_command() { + case "${TERMUX_VERSION:-}:${PREFIX:-}" in + ?*:*|*com.termux*) echo "pkg install python"; return ;; + esac + if [ "$(uname -s 2>/dev/null)" = "Darwin" ]; then + echo "xcode-select --install" + return fi -done + os_ids="" + if [ -r /etc/os-release ]; then + os_ids=$(sed -n 's/^ID\(_LIKE\)\{0,1\}=//p' /etc/os-release | tr -d '"' | tr '\n' ' ') + fi + for os_id in $os_ids; do + case "$os_id" in + debian|ubuntu) echo "sudo apt install python3"; return ;; + fedora|rhel|centos) echo "sudo dnf install python3"; return ;; + arch) echo "sudo pacman -S python"; return ;; + opensuse*|suse) echo "sudo zypper install python3"; return ;; + alpine) echo "apk add python3"; return ;; + esac + done + echo "" +} + +if ! find_python; then + install_command=$(python_install_command) + # Termux installs without root: at a terminal, the bootstrap offers to run it. + if [ "$install_command" = "pkg install python" ] && (: /dev/null; then + printf 'Python 3.8 or newer is required. Install it now with "%s"? [Y/n] ' \ + "$install_command" >/dev/tty + read -r answer &2 - echo " Install it, then run this command again. On Debian or Ubuntu: sudo apt install python3" >&2 + if [ -n "$install_command" ]; then + echo " Install it with: $install_command" >&2 + echo " Then run this command again." >&2 + else + echo " Install it with this system's package manager, then run this command again." >&2 + fi exit 1 fi diff --git a/tests/test_install.py b/tests/test_install.py index 68802f453..255676341 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -2378,6 +2378,43 @@ class RetroArchWhereItReallyLives(unittest.TestCase): self.assertIn(("retroarch", support / "system"), found) +class NoPythonNamesThisSystemsCommand(unittest.TestCase): + """Without Python the bootstrap said `sudo apt install python3` on every + system: Termux has no sudo and installs with `pkg install python`, and + macOS has no apt.""" + + def _run(self, extra_env: dict, uname: str = "Linux") -> subprocess.CompletedProcess: + root = Path(_tempdir(self)) + fake_bin = root / "bin" + fake_bin.mkdir() + for tool in ("sed", "tr", "dirname", "cat"): + found = shutil.which(tool) + if found: + (fake_bin / tool).symlink_to(found) + (fake_bin / "uname").write_text(f"#!/bin/sh\necho {uname}\n", encoding="utf-8") + (fake_bin / "uname").chmod(0o755) + # No terminal: the Termux offer is a question asked on /dev/tty. + return subprocess.run( + ["/bin/sh", str(REPO_ROOT / "install.sh")], cwd=root, + env={"PATH": str(fake_bin), "HOME": str(root), **extra_env}, + capture_output=True, text=True, timeout=30, check=False, + start_new_session=True, + ) + + def test_termux_is_told_pkg(self): + proc = self._run({"TERMUX_VERSION": "0.118", + "PREFIX": "/data/data/com.termux/files/usr"}) + self.assertEqual(proc.returncode, 1) + self.assertIn("Install it with: pkg install python", proc.stderr) + self.assertNotIn("sudo", proc.stderr) + + def test_macos_is_told_xcode_select(self): + proc = self._run({}, uname="Darwin") + self.assertEqual(proc.returncode, 1) + self.assertIn("Install it with: xcode-select --install", proc.stderr) + self.assertNotIn("apt", proc.stderr) + + class ThePowerShellWrapperPassesTheStatusOn(unittest.TestCase): """Every non-zero exit became a red "installer failed" exception and code 1: a --check that found work read as a crash, 2 and 130 were lost.""" diff --git a/wiki/first-install.md b/wiki/first-install.md index 71f39e5a6..c5675632f 100644 --- a/wiki/first-install.md +++ b/wiki/first-install.md @@ -154,8 +154,12 @@ its BIOS folder, see [Getting started](getting-started.md). ### Error: Python 3.8 or newer is required. No Python 3.8 or newer was found: Python is missing, or older. The message -names the command that installs it, `sudo apt install python3` on Debian and -Ubuntu. On a handheld running Batocera or Recalbox it is already there, so +names the command that installs it on the system it runs on: `sudo apt install +python3` on Debian and Ubuntu, `sudo dnf install python3` on Fedora, `sudo +pacman -S python` on Arch, `xcode-select --install` on macOS and `pkg install +python` in Termux, where it offers to run that command itself (Termux installs +without root). On Windows, `install.ps1` offers to run `winget install +Python.Python.3.12`. On a handheld running Batocera or Recalbox it is already there, so this message usually means the command ran on the computer rather than on the console.