af293d7036
The macOS install harness caught this on its first real run: a throwaway account gets `bot-bottle install: error: python3 3.11 or newer is required` and stops. A fresh account's PATH is just /etc/paths, which excludes /opt/homebrew/bin, so `python3` resolves to the Command Line Tools stub — still 3.9.6 on macOS 26. Homebrew's shellenv line lives in the *installing* user's ~/.zprofile and is inherited by nobody. The documented `curl … | sh` path therefore dead-ends for anyone whose profile isn't already set up, which is every new user, launchd job, and CI runner. So look past PATH before giving up: try `python3`, then python3.11-3.14, then /opt/homebrew/bin, /usr/local/bin, ~/.local/bin, and python.org framework builds, and say which one was picked when it isn't the obvious one. On this host that turns the failure into a successful install. The chosen interpreter is now threaded through everything downstream — the pip probe, the PEP 668 check, the pip --user fallback, and the user-scripts-dir lookup — which were all still hardcoding `python3` and would otherwise have run against the 3.9 stub we just rejected. `pipx install` gains `--python`, since pipx otherwise builds the venv with whichever interpreter pipx itself was installed with, not the one that passed the version check. When nothing usable is found the error is now actionable: what was found and why it's insufficient, where else it looked, a platform-appropriate install command, and BOT_BOTTLE_PYTHON to point at an interpreter directly. An explicit BOT_BOTTLE_PYTHON that is too old or unusable is an error rather than a silent fallback to a different interpreter than the caller asked for. Three existing tests asserted on incidental literals rather than the behaviour they describe, and are narrowed to their actual intent: * `never_uses_sudo` matched the word anywhere, including the new "sudo apt install python3.12" remediation *advice*. It now strips string literals and comments first, so it still catches sudo as a bare command, in a pipeline, and in a command substitution — verified by mutation — while allowing the script to print the word. * `checks_pip_usable_before_fallback` pinned the literal `python3 -m pip`. * `resolves_user_scripts_dir_not_hardcoded` banned ".local/bin" script-wide; it's now scoped to the USER_SCRIPTS assignment it exists to guard, since ~/.local/bin is a legitimate place to *find an interpreter*. README grows the install command and a Requirements section it never had, leading with the Python floor and why a working `python3` in your own shell says nothing about what a fresh account sees. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WEfZZhakx13bxTfXcZCoS5
211 lines
8.1 KiB
Bash
Executable File
211 lines
8.1 KiB
Bash
Executable File
#!/bin/sh
|
|
# bot-bottle quick installer.
|
|
#
|
|
# Usage:
|
|
# curl -fsSL https://gitea.dideric.is/didericis/bot-bottle/raw/branch/main/install.sh | sh
|
|
#
|
|
# Python-native users can skip this entirely:
|
|
# pipx install bot-bottle # from a checkout or a published index
|
|
# uv tool install bot-bottle
|
|
#
|
|
# This script is a thin bootstrapper: it finds a Python 3.11+ interpreter,
|
|
# installs the package with pipx (falling back to pip --user), creates the
|
|
# config dir, and runs `bot-bottle doctor`. It is idempotent (safe to re-run)
|
|
# and never uses sudo. It does NOT install Docker or a VM backend for you —
|
|
# `doctor` reports what's missing after install.
|
|
#
|
|
# Env:
|
|
# BOT_BOTTLE_PYTHON interpreter to install with (skips the search)
|
|
# BOT_BOTTLE_INSTALL_SPEC pip/git spec to install instead of the default
|
|
set -eu
|
|
|
|
PACKAGE_SPEC="${BOT_BOTTLE_INSTALL_SPEC:-git+https://gitea.dideric.is/didericis/bot-bottle.git}"
|
|
MIN_PYTHON_MAJOR=3
|
|
MIN_PYTHON_MINOR=11
|
|
|
|
say() {
|
|
printf 'bot-bottle install: %s\n' "$*" >&2
|
|
}
|
|
|
|
die() {
|
|
say "error: $*"
|
|
exit 1
|
|
}
|
|
|
|
# --- prerequisites: find an interpreter new enough ----------------------------
|
|
|
|
# Is $1 an interpreter that exists and meets the floor?
|
|
python_ok() {
|
|
[ -n "${1:-}" ] || return 1
|
|
command -v "$1" >/dev/null 2>&1 || return 1
|
|
"$1" - "$MIN_PYTHON_MAJOR" "$MIN_PYTHON_MINOR" <<'PY' >/dev/null 2>&1
|
|
import sys
|
|
|
|
want = (int(sys.argv[1]), int(sys.argv[2]))
|
|
raise SystemExit(0 if sys.version_info[:2] >= want else 1)
|
|
PY
|
|
}
|
|
|
|
python_version() {
|
|
"$1" -c 'import sys; print("%d.%d.%d" % sys.version_info[:3])' 2>/dev/null
|
|
}
|
|
|
|
# `python3` on PATH is often NOT the newest interpreter installed, and on macOS
|
|
# it is usually the oldest: a fresh login shell's PATH is just /etc/paths, so
|
|
# python3 resolves to the Command Line Tools stub (3.9.x) while the usable
|
|
# 3.11+ build sits in /opt/homebrew/bin or a python.org framework directory,
|
|
# reachable only via a line in the *installing* user's shell profile. A new
|
|
# account inherits none of that. Look past PATH before giving up, so the common
|
|
# case installs instead of dead-ending on a version error.
|
|
find_python() {
|
|
for candidate in \
|
|
"${BOT_BOTTLE_PYTHON:-}" \
|
|
python3 \
|
|
python3.14 python3.13 python3.12 python3.11 \
|
|
/opt/homebrew/bin/python3 \
|
|
/usr/local/bin/python3 \
|
|
"${HOME}/.local/bin/python3" \
|
|
/Library/Frameworks/Python.framework/Versions/*/bin/python3
|
|
do
|
|
# An unmatched glob arrives here literally; python_ok rejects it.
|
|
if python_ok "$candidate"; then
|
|
command -v "$candidate"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# An explicit choice that doesn't work is an error, not a reason to quietly
|
|
# search elsewhere and install somewhere the caller didn't ask for.
|
|
if [ -n "${BOT_BOTTLE_PYTHON:-}" ] && ! python_ok "${BOT_BOTTLE_PYTHON}"; then
|
|
if command -v "${BOT_BOTTLE_PYTHON}" >/dev/null 2>&1; then
|
|
die "BOT_BOTTLE_PYTHON=${BOT_BOTTLE_PYTHON} is $(python_version "${BOT_BOTTLE_PYTHON}"), "\
|
|
"below the ${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR} floor. Unset it to search for a newer one."
|
|
fi
|
|
die "BOT_BOTTLE_PYTHON=${BOT_BOTTLE_PYTHON} is not an executable interpreter."
|
|
fi
|
|
|
|
PYTHON="$(find_python || true)"
|
|
|
|
if [ -z "${PYTHON}" ]; then
|
|
path_python="$(command -v python3 2>/dev/null || true)"
|
|
if [ -n "${path_python}" ]; then
|
|
found="the python3 on your PATH is ${path_python} ($(python_version "${path_python}")), which is too old"
|
|
else
|
|
found="no python3 was found on your PATH"
|
|
fi
|
|
case "$(uname -s)" in
|
|
Darwin) fix=" brew install python@3.12
|
|
# or install from https://www.python.org/downloads/macos/
|
|
# macOS itself ships only /usr/bin/python3, which is too old" ;;
|
|
*) fix=" sudo apt install python3.12 # Debian/Ubuntu
|
|
sudo dnf install python3.12 # Fedora/RHEL" ;;
|
|
esac
|
|
die "bot-bottle needs python3 ${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR} or newer, and none was found.
|
|
${found}.
|
|
Also checked: python3.11-3.14, /opt/homebrew/bin, /usr/local/bin,
|
|
~/.local/bin, and python.org framework builds.
|
|
|
|
Install a newer Python, then re-run this installer:
|
|
${fix}
|
|
|
|
Already have one somewhere? Point at it directly:
|
|
BOT_BOTTLE_PYTHON=/path/to/python3 sh install.sh"
|
|
fi
|
|
|
|
# Be explicit when the interpreter isn't the obvious one, so nobody is left
|
|
# wondering which Python their install ended up on.
|
|
path_python="$(command -v python3 2>/dev/null || true)"
|
|
if [ "${PYTHON}" != "${path_python}" ]; then
|
|
say "using ${PYTHON} ($(python_version "${PYTHON}"))"
|
|
if [ -n "${path_python}" ]; then
|
|
say "note: 'python3' on your PATH is ${path_python} ($(python_version "${path_python}")), which is below the ${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR} floor"
|
|
fi
|
|
fi
|
|
|
|
# Installing a `git+` spec (the default) shells out to git under the hood,
|
|
# whether via pipx or pip. Fail early with a clear message rather than deep
|
|
# inside the installer's output.
|
|
case "${PACKAGE_SPEC}" in
|
|
git+*|*.git)
|
|
command -v git >/dev/null 2>&1 || die \
|
|
"git is required to install from '${PACKAGE_SPEC}'. Install git, or set "\
|
|
"BOT_BOTTLE_INSTALL_SPEC to a non-git spec (e.g. a wheel path or a package index name)."
|
|
;;
|
|
esac
|
|
|
|
# The pip fallback needs a usable pip. Externally-managed interpreters
|
|
# (PEP 668, common on Debian/Ubuntu/Homebrew) reject `pip install --user`;
|
|
# pipx sidesteps that, so recommend it when pip can't be used.
|
|
if ! command -v pipx >/dev/null 2>&1; then
|
|
"${PYTHON}" -m pip --version >/dev/null 2>&1 || die \
|
|
"neither pipx nor a usable '${PYTHON} -m pip' was found. Install pipx "\
|
|
"(recommended): '${PYTHON} -m pip install --user pipx' or your OS package manager."
|
|
if "${PYTHON}" - <<'PY'
|
|
import os
|
|
import sys
|
|
import sysconfig
|
|
|
|
# PEP 668: an EXTERNALLY-MANAGED marker in the stdlib dir means pip refuses
|
|
# to install into this interpreter without --break-system-packages.
|
|
marker = os.path.join(sysconfig.get_path("stdlib"), "EXTERNALLY-MANAGED")
|
|
raise SystemExit(0 if os.path.exists(marker) else 1)
|
|
PY
|
|
then
|
|
die "${PYTHON} is externally managed (PEP 668), so 'pip install --user' is "\
|
|
"blocked. Install pipx and re-run: '${PYTHON} -m pip install --user --break-system-packages pipx', "\
|
|
"then 'pipx ensurepath'."
|
|
fi
|
|
fi
|
|
|
|
# --- config directories ------------------------------------------------------
|
|
|
|
mkdir -p \
|
|
"${HOME}/.bot-bottle/agents" \
|
|
"${HOME}/.bot-bottle/bottles" \
|
|
"${HOME}/.bot-bottle/contrib"
|
|
|
|
# --- install -----------------------------------------------------------------
|
|
|
|
if command -v pipx >/dev/null 2>&1; then
|
|
# --python pins the venv to the interpreter we vetted. Without it pipx uses
|
|
# whichever Python it was itself installed with, which is not necessarily
|
|
# the one that passed the version check above.
|
|
say "installing with pipx (python: ${PYTHON})"
|
|
pipx install --python "${PYTHON}" --force "${PACKAGE_SPEC}"
|
|
else
|
|
say "pipx not found; installing with '${PYTHON} -m pip install --user'"
|
|
"${PYTHON}" -m pip install --user --upgrade "${PACKAGE_SPEC}"
|
|
fi
|
|
|
|
# --- locate the entry point --------------------------------------------------
|
|
|
|
# The pip --user scripts directory is platform-specific: ~/.local/bin on Linux,
|
|
# but ~/Library/Python/<X.Y>/bin on a python.org macOS interpreter. Ask the
|
|
# interpreter for its own user-scheme scripts dir instead of hardcoding.
|
|
USER_SCRIPTS="$("${PYTHON}" - <<'PY'
|
|
import sysconfig
|
|
print(sysconfig.get_path("scripts", sysconfig.get_preferred_scheme("user")))
|
|
PY
|
|
)"
|
|
|
|
if command -v bot-bottle >/dev/null 2>&1; then
|
|
BOT_BOTTLE_BIN="bot-bottle"
|
|
elif [ -n "${USER_SCRIPTS}" ] && [ -x "${USER_SCRIPTS}/bot-bottle" ]; then
|
|
BOT_BOTTLE_BIN="${USER_SCRIPTS}/bot-bottle"
|
|
say "note: add ${USER_SCRIPTS} to your PATH to run 'bot-bottle' directly"
|
|
else
|
|
die "bot-bottle was installed but is not on PATH; add ${USER_SCRIPTS:-your user scripts dir} to PATH and re-run"
|
|
fi
|
|
|
|
# --- verify ------------------------------------------------------------------
|
|
|
|
say "running '${BOT_BOTTLE_BIN} doctor'"
|
|
if "${BOT_BOTTLE_BIN}" doctor; then
|
|
say "done. Run '${BOT_BOTTLE_BIN} --help' to get started."
|
|
else
|
|
say "install completed, but 'doctor' reported unmet prerequisites (see above)."
|
|
say "resolve them, then re-run '${BOT_BOTTLE_BIN} doctor'."
|
|
fi
|