From 879cc15a88a6366f5fda1f7441f2976fb6e5f9eb Mon Sep 17 00:00:00 2001 From: stealthdigital Date: Tue, 11 Aug 2026 11:56:28 -0400 Subject: [PATCH] Resolve python3 in the installers instead of bare python install.sh and uninstall.sh invoked bare `python` for the Gemini loader edit. macOS has shipped no `python` since Monterey removed the Python 2 stub, so `./install.sh --target all` exited 127 after copying five skill trees, leaving a half-installed surface and no loader. uninstall.sh failed at the same point and stranded the loader block in GEMINI.md. Both scripts now resolve an interpreter lazily, preferring python3 and falling back to python only when it is Python 3, so --help still works where no interpreter exists at all. CI could not catch this: actions/setup-python puts a `python` on PATH, as does any active virtualenv. The added regression test builds a sanitized PATH carrying python3 and the utilities the scripts call, with no `python` under any name, and round-trips install and uninstall through it. It fails with "line 80: python: command not found" against the unpatched scripts. Co-Authored-By: Claude Opus 5 (1M context) --- anti-slop-brain/install.sh | 23 +++++++++++++- anti-slop-brain/tests/test_pipeline.py | 44 ++++++++++++++++++++++++++ anti-slop-brain/uninstall.sh | 24 +++++++++++++- 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/anti-slop-brain/install.sh b/anti-slop-brain/install.sh index 77ed107..ae83ca6 100755 --- a/anti-slop-brain/install.sh +++ b/anti-slop-brain/install.sh @@ -72,12 +72,33 @@ install_one() { echo "Anti-Slop Brain installed to ${dest}" } +# The Gemini loader edit is the only step that needs an interpreter, so resolve +# one lazily rather than at startup: --help must still work on a machine with no +# Python at all. Prefer python3. macOS has not shipped a bare `python` since +# Monterey removed the Python 2 stub, so hardcoding it fails there with 127. +resolve_python() { + if [ -n "${PYTHON:-}" ]; then + return + fi + local candidate + for candidate in python3 python; do + if command -v "${candidate}" >/dev/null 2>&1 && + "${candidate}" -c 'import sys; sys.exit(0 if sys.version_info[0] == 3 else 1)' >/dev/null 2>&1; then + PYTHON="${candidate}" + return + fi + done + echo "ERROR: no Python 3 interpreter on PATH. Looked for python3, then python." >&2 + exit 1 +} + install_gemini() { local dest="${base_home}/.gemini/anti-slop-brain" local loader="${base_home}/.gemini/GEMINI.md" install_one "${dest}" mkdir -p "$(dirname "${loader}")" - python - "${loader}" "@./anti-slop-brain/GEMINI.md" <<'PY' + resolve_python + "${PYTHON}" - "${loader}" "@./anti-slop-brain/GEMINI.md" <<'PY' from __future__ import annotations import re diff --git a/anti-slop-brain/tests/test_pipeline.py b/anti-slop-brain/tests/test_pipeline.py index bb6c985..6fd32af 100644 --- a/anti-slop-brain/tests/test_pipeline.py +++ b/anti-slop-brain/tests/test_pipeline.py @@ -4,6 +4,7 @@ import hashlib import json import os +import shutil import subprocess import sys import tempfile @@ -95,6 +96,48 @@ def test_demo_build_is_reproducible() -> None: ) +def test_installers_run_without_a_bare_python() -> None: + """The installers must not assume a `python` on PATH. + + Regression: `install.sh` and `uninstall.sh` invoked bare `python` for the + Gemini loader edit. macOS has shipped no `python` since Monterey removed + the Python 2 stub, so `./install.sh --target all` exited 127 after it had + already copied five skill trees, leaving a half-installed surface and no + loader. `uninstall.sh` failed at the same point and stranded the loader + block inside `GEMINI.md`, which the next install then had to repair. + + The ambient-PATH run in `main` cannot catch this. `actions/setup-python` + puts a `python` on PATH, so CI is green, and so is any developer machine + with a virtualenv active. This test builds a sanitized PATH carrying + `python3` and the utilities the scripts shell out to, and no `python` under + any name, which is the arrangement that actually failed. + """ + with tempfile.TemporaryDirectory(prefix="anti-slop-brain-nopython-") as tmp: + root = Path(tmp) + bin_dir = root / "bin" + bin_dir.mkdir() + for tool in ("bash", "cp", "mkdir", "rm", "chmod", "dirname", "find"): + resolved = shutil.which(tool) + assert resolved, f"cannot build a sanitized PATH without {tool}" + (bin_dir / tool).symlink_to(resolved) + # The interpreter is present under its versioned name only. + (bin_dir / "python3").symlink_to(PY) + assert shutil.which("python", path=str(bin_dir)) is None, "sanitized PATH still exposes a bare python" + + env = {"PATH": str(bin_dir), "ANTI_SLOP_BRAIN_INSTALL_HOME": str(root / "home")} + loader = root / "home" / ".gemini" / "GEMINI.md" + # subprocess resolves argv[0] against the parent PATH rather than the + # env passed here, so bash is named absolutely. Everything the script + # itself looks up goes through the sanitized PATH. + bash = str(bin_dir / "bash") + + run_cmd([bash, "install.sh", "--target", "gemini"], env=env) + assert "anti-slop-brain-install:start" in loader.read_text(encoding="utf-8"), "install wrote no loader block" + + run_cmd([bash, "uninstall.sh", "--target", "gemini"], env=env) + assert not loader.exists(), "uninstall left the loader block behind" + + def main() -> int: run(["-m", "compileall", "scripts", "anti_slop_brain", "tests"]) run(["scripts/lint_vault.py", "--vault", "assets/template-brain", "--template"]) @@ -145,6 +188,7 @@ def main() -> int: assert not (Path(tmp) / ".gemini" / "GEMINI.md").exists() run_cmd(["bash", "uninstall.sh", "--target", "custom", "--path", str(custom_root)], env=env) assert not (custom_root / "anti-slop-brain").exists() + test_installers_run_without_a_bare_python() print("Pipeline tests passed") return 0 diff --git a/anti-slop-brain/uninstall.sh b/anti-slop-brain/uninstall.sh index 56d0a11..e5a4a15 100755 --- a/anti-slop-brain/uninstall.sh +++ b/anti-slop-brain/uninstall.sh @@ -40,12 +40,34 @@ remove_one() { fi } +# The Gemini loader cleanup is the only step that needs an interpreter, so +# resolve one lazily rather than at startup: --help must still work on a machine +# with no Python at all. Prefer python3. macOS has not shipped a bare `python` +# since Monterey removed the Python 2 stub, so hardcoding it fails with 127, +# which would strand the loader block in GEMINI.md after an uninstall. +resolve_python() { + if [ -n "${PYTHON:-}" ]; then + return + fi + local candidate + for candidate in python3 python; do + if command -v "${candidate}" >/dev/null 2>&1 && + "${candidate}" -c 'import sys; sys.exit(0 if sys.version_info[0] == 3 else 1)' >/dev/null 2>&1; then + PYTHON="${candidate}" + return + fi + done + echo "ERROR: no Python 3 interpreter on PATH. Looked for python3, then python." >&2 + exit 1 +} + remove_gemini() { local dir="${base_home}/.gemini/anti-slop-brain" local loader="${base_home}/.gemini/GEMINI.md" remove_one "${dir}" if [ -f "${loader}" ]; then - python - "${loader}" <<'PY' + resolve_python + "${PYTHON}" - "${loader}" <<'PY' from __future__ import annotations import re