#!/usr/bin/env bash # # Verify prerequisites for every recipe. For each missing item, prints the # EXACT command to install it on the detected platform. Exits non-zero if # any hard requirement is missing; warns but continues for PATH hygiene # and the web-browser check (headless systems can still use login-headless). set -euo pipefail here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" # shellcheck disable=SC1091 . "$here/common.sh" # --------------------------------------------------------------------------- # Platform detection # --------------------------------------------------------------------------- uname_s="$(uname -s 2>/dev/null || echo unknown)" pm="" # one of: apt dnf pacman zypper brew "" case "$uname_s" in Linux) if [ -r /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release id_all=" ${ID:-} ${ID_LIKE:-} " case "$id_all" in *' debian '*|*' ubuntu '*) pm=apt ;; *' fedora '*|*' rhel '*|*' centos '*) pm=dnf ;; *' arch '*) pm=pacman ;; *' opensuse '*|*' suse '*) pm=zypper ;; esac fi if [ -z "$pm" ]; then for candidate in apt-get dnf pacman zypper; do if command -v "$candidate" >/dev/null 2>&1; then case "$candidate" in apt-get) pm=apt ;; dnf) pm=dnf ;; pacman) pm=pacman ;; zypper) pm=zypper ;; esac break fi done fi ;; Darwin) pm=brew ;; esac # --------------------------------------------------------------------------- # Render a package-install command for the current platform. # pkg_cmd # --------------------------------------------------------------------------- pkg_cmd() { local apt_pkg="$1" dnf_pkg="$2" pac_pkg="$3" brew_pkg="$4" url="$5" case "$pm" in apt) echo "sudo apt-get update && sudo apt-get install -y $apt_pkg" ;; dnf) echo "sudo dnf install -y $dnf_pkg" ;; pacman) echo "sudo pacman -S --noconfirm $pac_pkg" ;; zypper) echo "sudo zypper install -y $dnf_pkg" ;; brew) echo "brew install $brew_pkg" ;; *) echo "# install manually from: $url" ;; esac } # --------------------------------------------------------------------------- # Accumulators # --------------------------------------------------------------------------- missing=0 fix_commands=() record_miss() { local name="$1" detail="$2" fix="$3" printf ' %s %-12s -> %s\n' "$(_fc_tag "$_FC_RED" miss)" "$name" "$detail" printf ' fix: %s\n' "$fix" fix_commands+=("$fix") missing=$((missing + 1)) } check_cmd() { local cmd="$1" fix="$2" if command -v "$cmd" >/dev/null 2>&1; then printf ' %s %-12s -> %s\n' "$(_fc_tag "$_FC_GREEN" ok)" "$cmd" "$(command -v "$cmd")" else record_miss "$cmd" "not found on PATH" "$fix" fi } info 'checking prerequisites...' # --------------------------------------------------------------------------- # Required tools # --------------------------------------------------------------------------- check_cmd git "$(pkg_cmd git git git git https://git-scm.com/)" check_cmd bash "$(pkg_cmd bash bash bash bash https://www.gnu.org/software/bash/)" check_cmd curl "$(pkg_cmd curl curl curl curl https://curl.se/)" check_cmd install "$(pkg_cmd coreutils coreutils coreutils coreutils https://www.gnu.org/software/coreutils/)" check_cmd python3 "$(pkg_cmd python3 python3 python python3 https://www.python.org/)" # just and uv do not ship in most package managers, so prefer the upstream # installers (no sudo, land binaries in ~/.local/bin / ~/.cargo/bin). check_cmd just 'curl --proto "=https" --tlsv1.2 -LsSf https://just.systems/install.sh | bash -s -- --to "$HOME/.local/bin"' check_cmd uv 'curl -LsSf https://astral.sh/uv/install.sh | sh' # --------------------------------------------------------------------------- # Web browser (warning only: headless is supported via login-headless) # --------------------------------------------------------------------------- if command -v xdg-open >/dev/null 2>&1 || command -v open >/dev/null 2>&1; then printf ' %s %-12s -> (detected)\n' "$(_fc_tag "$_FC_GREEN" ok)" 'web browser' else printf ' %s %-12s -> no xdg-open/open on PATH.\n' "$(_fc_tag "$_FC_YELLOW" warn)" 'web browser' printf ' headless mode: just login-headless\n' case "$pm" in apt) printf ' otherwise install: sudo apt-get install -y xdg-utils\n' ;; dnf) printf ' otherwise install: sudo dnf install -y xdg-utils\n' ;; pacman) printf ' otherwise install: sudo pacman -S --noconfirm xdg-utils\n' ;; esac fi # --------------------------------------------------------------------------- # Python 3.11+ : accepted via system python3 OR uv-managed interpreter # --------------------------------------------------------------------------- py_ok=0 py_via="" if command -v python3 >/dev/null 2>&1; then if python3 - <<'PY' >/dev/null 2>&1 import sys sys.exit(0 if sys.version_info >= (3, 11) else 1) PY then py_ok=1 py_via="$(python3 -V 2>&1)" fi fi if [ "$py_ok" -eq 0 ] && command -v uv >/dev/null 2>&1; then if uv_py_path="$(uv python find '>=3.11' 2>/dev/null)" && [ -n "$uv_py_path" ]; then py_ok=1 py_via="$uv_py_path (via uv)" fi fi if [ "$py_ok" -eq 1 ]; then printf ' %s %-12s -> %s\n' "$(_fc_tag "$_FC_GREEN" ok)" 'python>=3.11' "$py_via" else detail='python3 missing' if command -v python3 >/dev/null 2>&1; then detail="python3 is $(python3 -V 2>&1 | awk '{print $2}'); need 3.11+" fi # Primary fix: uv-managed Python. Works on every platform, no sudo, and # the orchestrator (a uv project) will resolve it automatically via # `uv run` / `uv sync`. py_fix='uv python install 3.11' # Per-distro alternative (commented so users can copy one or the other): case "$pm" in apt) py_alt='sudo add-apt-repository -y ppa:deadsnakes/ppa && sudo apt-get update && sudo apt-get install -y python3.11 python3.11-venv' ;; dnf) py_alt='sudo dnf install -y python3.11' ;; pacman) py_alt='sudo pacman -S --noconfirm python' ;; zypper) py_alt='sudo zypper install -y python311' ;; brew) py_alt='brew install python@3.11' ;; *) py_alt='' ;; esac printf ' %s %-12s -> %s\n' "$(_fc_tag "$_FC_RED" miss)" 'python>=3.11' "$detail" printf ' fix: %s\n' "$py_fix" if [ -n "$py_alt" ]; then printf ' alt: %s\n' "$py_alt" fi fix_commands+=("$py_fix") missing=$((missing + 1)) fi # --------------------------------------------------------------------------- # PATH hygiene: warning, not a hard miss. # --------------------------------------------------------------------------- case ":$PATH:" in *":$LOCAL_BIN:"*) printf ' %s %-12s -> %s is on PATH\n' "$(_fc_tag "$_FC_GREEN" ok)" 'PATH' "$LOCAL_BIN" ;; *) rc='~/.bashrc' [ -n "${ZSH_VERSION:-}" ] && rc='~/.zshrc' printf ' %s %-12s -> %s is NOT on PATH.\n' "$(_fc_tag "$_FC_YELLOW" warn)" 'PATH' "$LOCAL_BIN" # shellcheck disable=SC2016 printf " fix: echo 'export PATH=\"%s:\$PATH\"' >> %s && exec \$SHELL -l\n" "$LOCAL_BIN" "$rc" ;; esac # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- if [ "$missing" -gt 0 ]; then printf '\n' warn "$missing prerequisite(s) missing. Run the following to fix them:" printf '\n' # De-duplicate while preserving order. seen='' for c in "${fix_commands[@]}"; do case "$seen" in *"<<$c>>"*) continue ;; esac seen="$seen<<$c>>" printf ' %s\n' "$c" done printf '\n' warn 're-run: just doctor' exit 1 fi info 'all prerequisites present'