462 lines
16 KiB
Bash
462 lines
16 KiB
Bash
#!/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 <path>] [--python <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" ]]; then
|
|
echo "Removing .pth files as requested..." >&2
|
|
for p in "${pths[@]}"; do rm -f "$p"; 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"; 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 <<EOF
|
|
Usage: $0 [options]
|
|
Options:
|
|
-e, --editable Install project in editable mode (pip -e .)
|
|
-p, --venv <path> Venv path (default: .venv)
|
|
--python <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 <list> 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 <path>" >&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
|
|
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."
|
|
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
|
|
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" <<EOF
|
|
[Desktop Entry]
|
|
Name=Medeia-Macina
|
|
Comment=Launch Medeia-Macina
|
|
Exec=$EXEC_PATH
|
|
Terminal=false
|
|
Type=Application
|
|
Categories=Utility;
|
|
EOF
|
|
chmod +x "$DESKTOP_FILE" || true
|
|
if [[ -d "$HOME/Desktop" ]]; then
|
|
cp "$DESKTOP_FILE" "$HOME/Desktop/"
|
|
chmod +x "$HOME/Desktop/$(basename "$DESKTOP_FILE")" || true
|
|
fi
|
|
echo "Desktop launcher created: $DESKTOP_FILE"
|
|
fi
|
|
|
|
# Install a global 'mm' launcher so it can be invoked from any shell.
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
USER_BIN="${XDG_BIN_HOME:-$HOME/.local/bin}"
|
|
mkdir -p "$USER_BIN"
|
|
if [[ -f "$USER_BIN/mm" ]]; then
|
|
echo "Backing up existing $USER_BIN/mm to $USER_BIN/mm.bak.$(date +%s)"
|
|
mv "$USER_BIN/mm" "$USER_BIN/mm.bak.$(date +%s)"
|
|
fi
|
|
cat > "$USER_BIN/mm" <<EOF
|
|
#!/usr/bin/env bash
|
|
REPO="$REPO"
|
|
VENV="$REPO/.venv"
|
|
if [ -x "$VENV/bin/mm" ]; then
|
|
exec "$VENV/bin/mm" "$@"
|
|
elif [ -x "$VENV/bin/python" ]; then
|
|
exec "$VENV/bin/python" -m medeia_macina.cli_entry "$@"
|
|
else
|
|
exec python -m medeia_macina.cli_entry "$@"
|
|
fi
|
|
EOF
|
|
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
|
|
echo "Warning: Global 'mm' launcher failed to run in this shell. Ensure $USER_BIN is on your PATH and the venv is installed; try: $VENV/bin/python -m medeia_macina.cli_entry --help" >&2
|
|
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" <<PROFILE_SNIPPET
|
|
# Added by Medeia-Macina setup: ensure user local bin is on PATH
|
|
if [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
|
|
PATH="$HOME/.local/bin:$PATH"
|
|
fi
|
|
PROFILE_SNIPPET
|
|
echo "Added $USER_BIN export to $PROFILE; restart your shell or source $PROFILE to use 'mm' from anywhere"
|
|
fi
|
|
fi
|
|
|
|
cat <<EOF
|
|
|
|
Bootstrap complete.
|
|
To activate the virtualenv:
|
|
source $VENV_PATH/bin/activate
|
|
To run the app:
|
|
$VENV_PATH/bin/mm # if installed as a console script
|
|
$VENV_PY -m medeia_macina.cli_entry # alternative
|
|
|
|
Global launcher installed: $USER_BIN/mm
|
|
EOF
|