From bcedd22a9e2b55ae4e5ab235c437a7007dc40adb Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:53:49 +0100 Subject: [PATCH] fix: pick a Python that can do HTTPS --- install.ps1 | 2 +- install.sh | 16 ++++++++-- install/launchers/index.json | 16 +++++----- ports/RetroBIOS/retrobios/screens.txt | 2 +- ports/shared/tui.txt | 12 ++++++- tests/test_handheld_launchers.py | 38 ++++++++++++++++++++++ tests/test_install.py | 45 +++++++++++++++++++++++++++ 7 files changed, 118 insertions(+), 13 deletions(-) diff --git a/install.ps1 b/install.ps1 index cebb3b2b3..f44c7a75c 100644 --- a/install.ps1 +++ b/install.ps1 @@ -84,7 +84,7 @@ param($candidate) if (-not $candidate) { return $false } $arguments = $candidate[1] - & $candidate[0].Source @arguments -c "import sys; raise SystemExit(sys.version_info < (3, 8))" | Out-Null + & $candidate[0].Source @arguments -c "import sys, ssl, unicodedata, zlib; raise SystemExit(sys.version_info < (3, 8))" 2>$null | Out-Null return ($LASTEXITCODE -eq 0) } $needPython = "Python 3.8 or newer is required. Install it with " + diff --git a/install.sh b/install.sh index 4c5668448..9594bc91d 100755 --- a/install.sh +++ b/install.sh @@ -121,14 +121,21 @@ if [ -z "$INSTALLER" ] || [ ! -f "$INSTALLER" ]; then INSTALLER="$TEMP_INSTALLER" fi +# A Python can be built without ssl, unicodedata or zlib, and the installer +# needs all three: a python3 lacking ssl was chosen over a full python and +# every download then failed as if the network were down. find_python() { PYTHON="" + PYTHON_LACKING="" 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 + command -v "$command_name" >/dev/null 2>&1 || continue + "$command_name" -c 'import sys; raise SystemExit(sys.version_info < (3, 8))' \ + 2>/dev/null || continue + if "$command_name" -c 'import ssl, unicodedata, zlib' 2>/dev/null; then PYTHON=$command_name return 0 fi + PYTHON_LACKING=${PYTHON_LACKING:-$command_name} done return 1 } @@ -172,6 +179,11 @@ if ! find_python; then esac fi fi +if [ -z "$PYTHON" ] && [ -n "$PYTHON_LACKING" ]; then + echo "Error: $PYTHON_LACKING is built without a module the installer needs" >&2 + echo " (ssl, unicodedata or zlib). Run it with a full Python 3.8 or newer." >&2 + exit 1 +fi if [ -z "$PYTHON" ]; then echo "Error: Python 3.8 or newer is required." >&2 if [ -n "$install_command" ]; then diff --git a/install/launchers/index.json b/install/launchers/index.json index 40796b8ed..dc01820d9 100644 --- a/install/launchers/index.json +++ b/install/launchers/index.json @@ -62,8 +62,8 @@ { "dest": "roms/ports/retrobios/tui.txt", "repo_path": "ports/shared/tui.txt", - "size": 35984, - "sha256": "7291b11eef5b5c2df8d2befab10614a2d515bffdf96dc208529b5dee9e6df0f1", + "size": 36434, + "sha256": "fa05432a9a4325f3fd3220eec0548ad932dffc1888b93a08af2047cc78aa4d0c", "executable": false } ] @@ -71,8 +71,8 @@ "muos": { "archive": { "dest": "ARCHIVE/RetroBIOS.muxapp", - "size": 1134145, - "sha256": "9a837360311366ecb4a65c4c4eff5388319ff0ade4f6acad3709cd4e63249a59" + "size": 1134595, + "sha256": "72ef1c1054aaff03491127694455e27d25edcc5565ace9e490bb5623c2a62840" }, "members": [ { @@ -141,8 +141,8 @@ { "name": "RetroBIOS/tui.txt", "repo_path": "ports/shared/tui.txt", - "size": 35984, - "sha256": "7291b11eef5b5c2df8d2befab10614a2d515bffdf96dc208529b5dee9e6df0f1", + "size": 36434, + "sha256": "fa05432a9a4325f3fd3220eec0548ad932dffc1888b93a08af2047cc78aa4d0c", "executable": false } ] @@ -208,8 +208,8 @@ { "dest": ".config/modules/retrobios/tui.txt", "repo_path": "ports/shared/tui.txt", - "size": 35984, - "sha256": "7291b11eef5b5c2df8d2befab10614a2d515bffdf96dc208529b5dee9e6df0f1", + "size": 36434, + "sha256": "fa05432a9a4325f3fd3220eec0548ad932dffc1888b93a08af2047cc78aa4d0c", "executable": false } ] diff --git a/ports/RetroBIOS/retrobios/screens.txt b/ports/RetroBIOS/retrobios/screens.txt index b486cb87c..5e5d16bd2 100644 --- a/ports/RetroBIOS/retrobios/screens.txt +++ b/ports/RetroBIOS/retrobios/screens.txt @@ -43,7 +43,7 @@ rb_fetch_runtime() { fi PortMasterDialogInit "no-check" PortMasterDialog "messages_begin" - PortMasterDialog "message" "This system's Python is older than 3.8. Downloading PortMaster's $RB_RUNTIME runtime." + PortMasterDialog "message" "This system's Python is older than 3.8 or lacks a module install.py needs. Downloading PortMaster's $RB_RUNTIME runtime." answer=$(rb_answer "check_runtime" "$RB_RUNTIME.squashfs") PortMasterDialog "messages_end" PortMasterDialogExit diff --git a/ports/shared/tui.txt b/ports/shared/tui.txt index 2ad8c88c9..c6bf99867 100644 --- a/ports/shared/tui.txt +++ b/ports/shared/tui.txt @@ -193,10 +193,17 @@ rb_open_log() { # --- Python --------------------------------------------------------------- -rb_python_ok() { +# The modules install.py needs that a Python can be built without: a python3 +# lacking ssl was chosen over PortMaster's runtime, and every download failed +# as if the network were down. +rb_python_new() { "$@" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 8) else 1)' >/dev/null 2>&1 } +rb_python_ok() { + rb_python_new "$@" && "$@" -c 'import ssl, unicodedata, zlib' >/dev/null 2>&1 +} + # The OS python3 when it is 3.8 or newer, else PortMaster's python_3.11 # runtime on aarch64, mounted as PortMaster's own Python ports do. A launcher # that can download the runtime defines rb_fetch_runtime. Sets @@ -252,6 +259,9 @@ rb_release_python() { rb_python_text() { local found found=$(python3 --version 2>&1 | awk '{print $2}') + if command -v python3 >/dev/null 2>&1 && rb_python_new python3; then + found="$found, built without a module install.py needs (ssl, unicodedata or zlib)" + fi case "$RB_PYTHON_PROBLEM" in old) printf '%s\n' "Python 3.8 or newer is needed. This system has ${found:-none}, and PortMaster's Python runtime exists for aarch64 only, and this device is $(rb_arch). $RB_PC_ROUTE" ;; runtime_offline) printf '%s\n' "Python 3.8 or newer is needed. This system has ${found:-none}, and PortMaster's $RB_RUNTIME runtime needs a connection to download. $RB_PC_ROUTE" ;; diff --git a/tests/test_handheld_launchers.py b/tests/test_handheld_launchers.py index 835376c61..99607be54 100644 --- a/tests/test_handheld_launchers.py +++ b/tests/test_handheld_launchers.py @@ -1080,6 +1080,44 @@ class ThePortMasterPort(unittest.TestCase): self.assertIn("--dest", screen) self.assertEqual(device.calls(), []) + def _python_without_ssl(self, device) -> None: + """A python3 3.8 or newer whose ssl import fails, as on a CFW image + built without OpenSSL.""" + real = shutil.which("python3.8") or sys.executable + shadow = device.root / "no-ssl" + shadow.mkdir() + (shadow / "ssl.py").write_text('raise ImportError("built without ssl")\n') + device.tool("python3", f'#!/bin/sh\nPYTHONPATH="{shadow}" exec {real} "$@"\n') + + def test_a_python_without_ssl_is_told_on_the_console(self): + """Only the version was probed: a python3 without ssl was chosen and + every download failed as if the network were down.""" + device = self.device() + self._python_without_ssl(device) + device.tool("uname", "#!/bin/sh\necho armv7l\n") + proc = device.run(["bash", self.script]) + self.assertEqual(proc.returncode, 1) + screen = " ".join(device.screen.split()) + self.assertIn("built without a module install.py needs", screen) + self.assertEqual(device.calls(), []) + + def test_a_python_without_ssl_gives_way_to_the_runtime(self): + device = self.device() + self._python_without_ssl(device) + real = shutil.which("python3.8") or sys.executable + device.tool("uname", "#!/bin/sh\necho aarch64\n") + device.tool("mount", '#!/bin/sh\nprintf "mount %s\\n" "$*" >> "$FAKE_STATE/tools.log"\n' + 'mkdir -p "$2/bin"\n' + f'printf \'#!/bin/sh\\nunset PYTHONHOME\\nexec {real} "$@"\\n\' > "$2/bin/python3"\n' + 'chmod +x "$2/bin/python3"\n') + device.tool("umount", FAKE_RECORDER) + (self.control / "libs").mkdir() + (self.control / "libs" / "python_3.11.squashfs").write_bytes(b"") + proc = device.run(["bash", self.script]) + self.assertEqual(proc.returncode, 0, proc.stderr.decode()) + [call] = device.tui_calls() + self.assertTrue(any(t.startswith("mount") for t in device.tools())) + def test_the_runtime_is_mounted_then_unmounted(self): """ArkOS's Python 3.7: PortMaster's python_3.11 runtime, mounted for the run and unmounted after it.""" diff --git a/tests/test_install.py b/tests/test_install.py index 52353b6e7..8556425fa 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -2575,6 +2575,51 @@ class NoPythonNamesThisSystemsCommand(unittest.TestCase): self.assertNotIn("apt", proc.stderr) +class ABootstrapPythonCanDoHttps(unittest.TestCase): + """find_python checked the version only: a python3 built without ssl + was chosen over a full python, and every download then failed.""" + + def _bin(self) -> tuple[Path, Path]: + root = Path(_tempdir(self)) + fake_bin = root / "bin" + fake_bin.mkdir() + for tool in ("sed", "tr", "dirname", "cat", "awk", "uname"): + found = shutil.which(tool) + if found: + (fake_bin / tool).symlink_to(found) + shadow = root / "no-ssl" + shadow.mkdir() + (shadow / "ssl.py").write_text('raise ImportError("built without ssl")\n') + (fake_bin / "python3").write_text( + f'#!/bin/sh\nPYTHONPATH="{shadow}" exec {shlex.quote(sys.executable)} "$@"\n') + (fake_bin / "python3").chmod(0o755) + (root / "install.py").write_text("import ssl; print('installer ran')\n") + shutil.copy(REPO_ROOT / "install.sh", root / "install.sh") + return root, fake_bin + + def _run(self, root: Path, fake_bin: Path) -> subprocess.CompletedProcess: + return subprocess.run( + ["/bin/sh", str(root / "install.sh")], cwd=root, + env={"PATH": str(fake_bin), "HOME": str(root)}, + capture_output=True, text=True, timeout=30, check=False, + stdin=subprocess.DEVNULL, start_new_session=True, + ) + + def test_a_full_python_is_preferred(self): + root, fake_bin = self._bin() + (fake_bin / "python").symlink_to(sys.executable) + proc = self._run(root, fake_bin) + self.assertEqual(proc.returncode, 0, proc.stderr) + self.assertIn("installer ran", proc.stdout) + + def test_the_missing_module_is_named(self): + root, fake_bin = self._bin() + proc = self._run(root, fake_bin) + self.assertEqual(proc.returncode, 1) + self.assertNotIn("installer ran", proc.stdout) + self.assertIn("python3 is built without a module the installer needs", proc.stderr) + + @unittest.skipUnless(shutil.which("pwsh") and shutil.which("script"), "needs PowerShell and a terminal") class WindowsWithoutPythonIsOfferedWinget(unittest.TestCase): """A Windows machine without Python was told to install it, open a new