#!/usr/bin/env bash # Bootstrap script for POSIX (Linux/macOS) to create a Python venv and install the project. # Usage: scripts/bootstrap.sh [--editable] [--venv ] [--python ] [--desktop] [--no-install] # Ensure script is running under Bash. Some users invoke this script with `sh` (dash) # which does not support the Bash features used below (e.g., [[ ]], arrays, read -p). # If not running under Bash, re-exec using a discovered bash binary. if [ -z "${BASH_VERSION:-}" ]; then if command -v bash >/dev/null 2>&1; then echo "This script requires Bash; re-execing as 'bash $0'..." exec bash "$0" "$@" else echo "ERROR: This script requires Bash. Please run with 'bash $0' or install Bash." >&2 exit 2 fi fi set -euo pipefail VENV_PATH=".venv" EDITABLE=false DESKTOP=false PYTHON_CMD="" NOINSTALL=false FORCE=false QUIET=false FIX_URLLIB3=false # Playwright options PLAYWRIGHT_BROWSERS="chromium" # comma-separated (chromium,firefox,webkit) or 'all' NO_PLAYWRIGHT=false REMOVE_PTH=false attempt_fix_urllib3() { local venv_py="$1" echo "Attempting automatic urllib3 fix in venv: $venv_py" >&2 # Temporarily disable set -e to inspect return codes ourselves set +e "$venv_py" -m pip uninstall urllib3-future -y >/dev/null 2>&1 || true "$venv_py" -m pip install --upgrade --force-reinstall urllib3 local pip_ret=$? # Best-effort install/update for niquests; non-fatal "$venv_py" -m pip install niquests -U >/dev/null 2>&1 || true set -e if [[ $pip_ret -ne 0 ]]; then echo "ERROR: pip failed to reinstall urllib3 (exit $pip_ret)" >&2 return 2 fi # Verify fix if "$venv_py" -c 'from SYS.env_check import check_urllib3_compat; ok,msg = check_urllib3_compat(); import sys; sys.exit(0 if ok else 2)'; then echo "Success: urllib3 issues resolved" >&2 return 0 fi echo "ERROR: urllib3 fix attempt incomplete (import check failed)" >&2 # Detect potential interfering .pth files in site-packages echo "Searching for interfering .pth files in the venv site-packages..." >&2 local site_pkgs site_pkgs=$("$venv_py" - <<'PY' import json, site, sysconfig paths = [] try: paths.extend(site.getsitepackages()) except Exception: pass try: p = sysconfig.get_paths().get('purelib') if p: paths.append(p) except Exception: pass seen = [] out = [] for s in paths: if s and s not in seen: seen.append(s) out.append(s) print("\n".join(out)) PY ) local pths=() if [[ -n "$site_pkgs" ]]; then while IFS= read -r sp; do if [[ -d "$sp" ]]; then while IFS= read -r f; do if [[ -n "$f" ]]; then if grep -qi 'urllib3_future' "$f" >/dev/null 2>&1 || grep -qi 'urllib3-future' "$f" >/dev/null 2>&1; then pths+=("$f") fi fi done < <(find "$sp" -maxdepth 1 -type f -name '*.pth' -print 2>/dev/null) fi done <<< "$site_pkgs" fi if [[ ${#pths[@]} -eq 0 ]]; then echo "No obvious interfering .pth files found in site-packages. Manual inspection recommended." >&2 return 3 fi echo "Found the following potentially interfering .pth files:" >&2 for p in "${pths[@]}"; do echo " - $p" >&2; done if [[ "$REMOVE_PTH" == "true" || "$FIX_URLLIB3" == "true" ]]; then echo "Removing .pth files (automatic removal due to --fix-urllib3)..." >&2 for p in "${pths[@]}"; do rm -f "$p" echo "Removed: $p" >&2 done else if [[ "$QUIET" == "true" ]]; then echo "Detected interfering .pth files but cannot prompt in quiet mode. Re-run with --remove-pth to remove them automatically." >&2 return 3 fi read -p "Remove these files now? (y/N) " resp if [[ "$resp" != "y" && "$resp" != "Y" ]]; then echo "User declined to remove .pth files. Aborting." >&2 return 3 fi for p in "${pths[@]}"; do rm -f "$p" echo "Removed: $p" >&2 done fi # Re-run reinstall & verify echo "Reinstalling urllib3 after .pth removal..." >&2 set +e "$venv_py" -m pip install --upgrade --force-reinstall urllib3 local pip_ret2=$? set -e if [[ $pip_ret2 -ne 0 ]]; then echo "ERROR: pip reinstall failed after .pth removal (exit $pip_ret2)" >&2 return 2 fi if "$venv_py" -c 'from SYS.env_check import check_urllib3_compat; ok,msg = check_urllib3_compat(); import sys; sys.exit(0 if ok else 2)'; then echo "Success: urllib3 issues resolved after .pth removal" >&2 return 0 fi echo "ERROR: urllib3 still not importable after .pth removal" >&2 return 3 } usage() { cat < Venv path (default: .venv) --python Python executable to use (e.g. python3) -d, --desktop Create a desktop launcher (~/.local/share/applications and ~/Desktop) -n, --no-install Skip pip install --no-playwright Skip installing Playwright browsers (default: install chromium) --playwright-browsers Comma-separated list of browsers to install (default: chromium) -q, --quiet Quiet / non-interactive mode; abort on errors instead of prompting -F, --fix-urllib3 Attempt to automatically fix known urllib3 issues in the venv if detected -R, --remove-pth When fixing urllib3, automatically remove interfering .pth files (like urllib3_future.pth) -f, --force Overwrite existing venv without prompting -h, --help Show this help EOF } while [[ $# -gt 0 ]]; do case "$1" in -e|--editable) EDITABLE=true; shift;; -p|--venv) VENV_PATH="$2"; shift 2;; --python) PYTHON_CMD="$2"; shift 2;; -d|--desktop) DESKTOP=true; shift;; -n|--no-install) NOINSTALL=true; shift;; -f|--force) FORCE=true; shift;; -F|--fix-urllib3) FIX_URLLIB3=true; shift;; -R|--remove-pth) REMOVE_PTH=true; shift;; -q|--quiet) QUIET=true; shift;; -h|--help) usage; exit 0;; --no-playwright) NO_PLAYWRIGHT=true; shift;; --playwright-browsers) PLAYWRIGHT_BROWSERS="$2"; shift 2;; *) echo "Unknown option: $1"; usage; exit 1;; esac done if [[ -n "$PYTHON_CMD" ]]; then PY="$PYTHON_CMD" elif command -v python3 >/dev/null 2>&1; then PY=python3 elif command -v python >/dev/null 2>&1; then PY=python else echo "ERROR: No python executable found; install Python or pass --python " >&2 exit 2 fi echo "Using Python: $PY" # Operate from the repository root (parent of the scripts dir) so relative # operations (like creating the venv and pip install) act on the project root # regardless of where this script was invoked from. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO="$(cd "$SCRIPT_DIR/.." && pwd)" if ! cd "$REPO"; then echo "ERROR: Failed to change to repo root: $REPO" >&2 exit 2 fi echo "Operating from repo root: $REPO" if [[ -d "$VENV_PATH" ]]; then # Detect whether the existing venv has a working python executable VENV_PY="" for cand in "$VENV_PATH/bin/python" "$VENV_PATH/bin/python3" "$VENV_PATH/Scripts/python.exe"; do if [[ -x "$cand" ]]; then VENV_PY="$cand" break fi done if [[ "$FORCE" == "true" ]]; then echo "Removing existing venv $VENV_PATH" rm -rf "$VENV_PATH" else if [[ -z "$VENV_PY" ]]; then if [[ "$QUIET" == "true" ]]; then echo "ERROR: Existing venv appears incomplete or broken (no python executable). Use --force to recreate." >&2 exit 4 fi read -p "$VENV_PATH exists but appears invalid (no python executable). Overwrite to recreate? (y/N) " REPLY if [[ "$REPLY" != "y" && "$REPLY" != "Y" ]]; then echo "Aborted."; exit 4 fi rm -rf "$VENV_PATH" else if [[ "$QUIET" == "true" ]]; then echo "Using existing venv at $VENV_PATH (quiet mode)" else read -p "$VENV_PATH already exists. Overwrite? (y/N) (default: use existing venv) " REPLY if [[ "$REPLY" == "y" || "$REPLY" == "Y" ]]; then echo "Removing existing venv $VENV_PATH" rm -rf "$VENV_PATH" else echo "Continuing using existing venv at $VENV_PATH" fi fi fi fi fi if [[ -d "$VENV_PATH" && -n "${VENV_PY:-}" && -x "${VENV_PY:-}" ]]; then echo "Using existing venv at $VENV_PATH" else echo "Creating venv at $VENV_PATH" $PY -m venv "$VENV_PATH" VENV_PY="$VENV_PATH/bin/python" fi if [[ ! -x "$VENV_PY" ]]; then echo "ERROR: venv python not found at $VENV_PY" >&2 exit 3 fi if [[ "$NOINSTALL" != "true" ]]; then # If not explicitly requested, suggest editable install for development checkouts if [[ "$EDITABLE" != "true" ]]; then if [[ -d "$REPO/.git" ]] || git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then if [[ "$QUIET" != "true" && -t 0 ]]; then read -p "It looks like this is a development checkout (git repo). Install project in editable mode for development? (Y/n) " devans if [[ -z "$devans" || "$devans" == "y" || "$devans" == "Y" ]]; then EDITABLE=true echo "Selected: editable install for development" fi fi fi fi echo "Upgrading pip, setuptools, wheel..." "$VENV_PY" -m pip install -U pip setuptools wheel if [[ "$EDITABLE" == "true" ]]; then echo "Installing project in editable mode..." "$VENV_PY" -m pip install -e "$REPO" else echo "Installing project..." "$VENV_PY" -m pip install "$REPO" fi # Verify the installed CLI module can be imported. This helps catch packaging # or installation problems early (e.g., missing modules or mispackaged project). echo "Verifying installed CLI import..." if "$VENV_PY" -c 'import importlib; importlib.import_module("medeia_macina.cli_entry")' >/dev/null 2>&1; then echo "OK: 'medeia_macina.cli_entry' is importable in the venv." # Additional compatibility check: top-level 'CLI' module may be required by # older entrypoints or direct imports when running from a development checkout. if ! "$VENV_PY" -c 'import importlib,sys; importlib.import_module("CLI")' >/dev/null 2>&1; then echo "Note: top-level 'CLI' module not importable; some entrypoints expect it." >&2 # If this appears to be a development checkout, offer to install editable mode if [[ -d "$REPO/.git" ]] || git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then if [[ "$EDITABLE" == "true" ]]; then echo "Installing project in editable mode to provide top-level 'CLI'..." "$VENV_PY" -m pip install -e "$REPO" || { echo "Editable install failed" >&2; exit 6; } else if [[ "$QUIET" != "true" && -t 0 ]]; then read -p "Top-level 'CLI' not importable; install project in editable mode now? (Y/n) " devans2 if [[ -z "$devans2" || "$devans2" == "y" || "$devans2" == "Y" ]]; then "$VENV_PY" -m pip install -e "$REPO" || { echo "Editable install failed" >&2; exit 6; } else echo "Continuing without editable install; 'mm' may not work as expected." >&2 fi else echo "Top-level 'CLI' not found and not interactive; re-run bootstrap with --editable or run: $VENV_PY -m pip install -e ." >&2 exit 6 fi fi # Re-check after editable install if "$VENV_PY" -c 'import importlib; importlib.import_module("CLI")' >/dev/null 2>&1; then echo "Success: top-level 'CLI' now importable." >&2 else echo "ERROR: top-level 'CLI' still not importable after editable install; aborting." >&2 exit 6 fi else echo "Note: not a development checkout (no .git) — if you need top-level 'CLI' run: $VENV_PY -m pip install -e ." >&2 fi fi else echo "WARNING: Could not import 'medeia_macina.cli_entry' from the venv." >&2 # Check if legacy top-level module is present; if so, inform the user to prefer the packaged entrypoint if "$VENV_PY" -c 'import importlib; importlib.import_module("medeia_entry")' >/dev/null 2>&1; then echo "Note: 'medeia_entry' top-level module is present. It's recommended to install the project so 'medeia_macina.cli_entry' is available." >&2 else echo "Action: Try running: $VENV_PY -m pip install -e . or inspect the venv site-packages to verify the installation." >&2 fi fi echo "Verifying environment for known issues (urllib3 compatibility)..." if ! "$VENV_PY" -c 'from SYS.env_check import check_urllib3_compat; ok,msg = check_urllib3_compat(); print(msg); import sys; sys.exit(0 if ok else 2)'; then echo "ERROR: Bootstrap detected a potentially broken 'urllib3' installation. See message above." >&2 echo "You can attempt to fix with:" >&2 echo " $VENV_PY -m pip uninstall urllib3-future -y" >&2 echo " $VENV_PY -m pip install --upgrade --force-reinstall urllib3" >&2 echo " $VENV_PY -m pip install niquests -U" >&2 echo "" >&2 if [[ "$FIX_URLLIB3" == "true" ]]; then echo "Attempting automatic fix (--fix-urllib3 requested)..." >&2 if attempt_fix_urllib3 "$VENV_PY"; then echo "Success: urllib3 issues resolved; continuing..." >&2 else echo "ERROR: Automatic fix did not resolve the issue; inspect .pth files in site-packages (e.g., urllib3_future.pth) and remove them, then re-run with --fix-urllib3" >&2 exit 7 fi else if [[ "$QUIET" == "true" ]]; then echo "ERROR: Bootstrap detected a potentially broken 'urllib3' installation. Use --fix-urllib3 to attempt an automatic fix." >&2 exit 7 fi read -p "Attempt automatic fix now? (y/N) " REPLY if [[ "$REPLY" == "y" || "$REPLY" == "Y" ]]; then AUTOFIX_INTERACTIVE=1 if attempt_fix_urllib3 "$VENV_PY"; then echo "Success: urllib3 issues resolved; continuing..." >&2 else echo "ERROR: Automatic fix did not resolve the issue; aborting." >&2 exit 7 fi else echo "Aborting bootstrap. Re-run with --fix-urllib3 to attempt an automatic fix or run the commands above." >&2 exit 7 fi fi fi # Install Playwright browsers (default: chromium) unless explicitly disabled if [[ "$NO_PLAYWRIGHT" != "true" && "$NOINSTALL" != "true" ]]; then echo "Ensuring Playwright browsers are installed (browsers=$PLAYWRIGHT_BROWSERS)..." # Install package if missing in venv if ! "$VENV_PY" -c 'import importlib, sys; importlib.import_module("playwright")' >/dev/null 2>&1; then echo "'playwright' package not found in venv; installing via pip..." "$VENV_PY" -m pip install playwright fi # Compute install behavior: 'all' means install all engines, otherwise split comma list if [[ "$PLAYWRIGHT_BROWSERS" == "all" ]]; then echo "Installing all Playwright browsers..." "$VENV_PY" -m playwright install || echo "Warning: Playwright browser install failed" >&2 else IFS=',' read -ra PWB <<< "$PLAYWRIGHT_BROWSERS" for b in "${PWB[@]}"; do b_trimmed=$(echo "$b" | tr -d '[:space:]') if [[ -n "$b_trimmed" ]]; then echo "Installing Playwright browser: $b_trimmed" "$VENV_PY" -m playwright install "$b_trimmed" || echo "Warning: Playwright install for $b_trimmed failed" >&2 fi done fi fi else echo "Skipping install (--no-install)" fi # Install Deno (official installer) - installed automatically if command -v deno >/dev/null 2>&1; then echo "Deno already installed: $(deno --version | head -n 1)" else echo "Installing Deno via official installer (https://deno.land)..." if command -v curl >/dev/null 2>&1; then curl -fsSL https://deno.land/install.sh | sh elif command -v wget >/dev/null 2>&1; then wget -qO- https://deno.land/install.sh | sh else echo "ERROR: curl or wget is required to install Deno automatically; please install Deno manually." >&2 fi export DENO_INSTALL="${DENO_INSTALL:-$HOME/.deno}" export PATH="$DENO_INSTALL/bin:$PATH" if command -v deno >/dev/null 2>&1; then echo "Deno installed: $(deno --version | head -n 1)" else echo "Warning: Deno installer completed but 'deno' not found on PATH; add $HOME/.deno/bin to your PATH or restart your shell." >&2 fi fi if [[ "$DESKTOP" == "true" ]]; then echo "Creating desktop launcher..." EXEC_PATH="$VENV_PATH/bin/mm" if [[ ! -x "$EXEC_PATH" ]]; then # fallback to python -m EXEC_PATH="$VENV_PY -m medeia_macina.cli_entry" fi APPDIR="$HOME/.local/share/applications" mkdir -p "$APPDIR" DESKTOP_FILE="$APPDIR/medeia-macina.desktop" cat > "$DESKTOP_FILE" < "$USER_BIN/mm" <<'MM' #!/usr/bin/env bash set -e # REPO is injected at install time; if it doesn't look like a project, try to # find the repo by walking up from the current working directory. REPO="__REPO__" # If the embedded REPO does not contain a canonical project marker, search # upward from the current working directory for a project root. Use only # explicit project markers (CLI.py or pyproject.toml) to avoid false positives # from subdirectories like 'scripts' which may contain their own setup.py. if [ ! -f "$REPO/CLI.py" ] && [ ! -f "$REPO/pyproject.toml" ]; then CUR="$(pwd -P)" while [ "$CUR" != "/" ] && [ "$CUR" != "" ]; do if [ -f "$CUR/CLI.py" ] || [ -f "$CUR/pyproject.toml" ]; then REPO="$CUR" break fi CUR="$(dirname "$CUR")" done fi VENV="$REPO/.venv" # Packaged console script in the venv if available if [ -x "$VENV/bin/mm" ]; then exec "$VENV/bin/mm" "$@" fi # Prefer venv's python3, then venv's python if [ -x "$VENV/bin/python3" ]; then exec "$VENV/bin/python3" -m medeia_macina.cli_entry "$@" fi if [ -x "$VENV/bin/python" ]; then exec "$VENV/bin/python" -m medeia_macina.cli_entry "$@" fi # Fallback to system python3, then system python (only if it's Python 3) if command -v python3 >/dev/null 2>&1; then exec python3 -m medeia_macina.cli_entry "$@" fi if command -v python >/dev/null 2>&1; then if python -c 'import sys; sys.exit(0 if sys.version_info[0] >= 3 else 1)'; then exec python -m medeia_macina.cli_entry "$@" fi fi printf "Error: no suitable Python 3 interpreter found. Activate the venv with 'source %s/bin/activate' or install Python 3.\n" "$VENV" >&2 exit 127 MM # Inject absolute repo path into the installed script so global launcher prefers the project venv escaped_repo=$(printf '%s' "$REPO" | sed -e 's/[\/&]/\\&/g') sed -i "s|__REPO__|$escaped_repo|g" "$USER_BIN/mm" || true chmod +x "$USER_BIN/mm" # Quick verification of the global launcher; helps catch packaging issues early. if "$USER_BIN/mm" --help >/dev/null 2>&1; then echo "Global 'mm' launcher verified: $USER_BIN/mm runs correctly." else # Capture error output to detect permission issues err=$( { "$USER_BIN/mm" --help >/dev/null 2>&1 || true; } 2>&1 || true ) echo "Warning: Global 'mm' launcher failed to run in this shell. Error: ${err:-unknown}" >&2 # If we detected a permission denied (e.g., filesystem mounted noexec), fall back to user-local bin if echo "${err}" | grep -qi "Permission denied" || [[ ${err:-} == *"Permission denied"* ]]; then FALLBACK_BIN="$HOME/.local/bin" mkdir -p "$FALLBACK_BIN" # Backup the problematic file and copy to fallback (use a single timestamp variable) backup="$USER_BIN/mm.broken.$(date +%s)" if mv "$USER_BIN/mm" "$backup" 2>/dev/null; then echo "Moved non-executable launcher to backup: $backup" >&2 cp "$backup" "$FALLBACK_BIN/mm" 2>/dev/null || true chmod +x "$FALLBACK_BIN/mm" 2>/dev/null || true USER_BIN="$FALLBACK_BIN" echo "Installed launcher to fallback location: $USER_BIN/mm" >&2 echo "Ensure '$USER_BIN' is on your PATH (e.g. add 'export PATH=\"$USER_BIN:\$PATH\"' to your shell rc)." >&2 else echo "Failed to move the launcher for fallback installation; manual inspection recommended: $USER_BIN/mm" >&2 fi fi fi # Ensure the user's bin is on PATH for future sessions by adding to ~/.profile if needed if ! echo ":$PATH:" | grep -q ":$USER_BIN:"; then PROFILE="$HOME/.profile" if [ ! -f "$PROFILE" ]; then if [ -f "$HOME/.bash_profile" ]; then PROFILE="$HOME/.bash_profile" elif [ -f "$HOME/.bashrc" ]; then PROFILE="$HOME/.bashrc" elif [ -f "$HOME/.zshrc" ]; then PROFILE="$HOME/.zshrc" else PROFILE="$HOME/.profile" fi fi if ! grep -q "ensure user local bin is on PATH" "$PROFILE" 2>/dev/null; then cat >> "$PROFILE" <