329 lines
14 KiB
Bash
Executable File
329 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# bootstrap_sources — source-anchored dependency build into /mnt/AI.
|
|
#
|
|
# Every Thicket dependency is mirrored as a git checkout under
|
|
# <AI_ROOT>/distfiles/git/ and built into THE venv at
|
|
# <AI_ROOT>/runtime/thicket-venv — one environment, always, outside
|
|
# the project tree so the checkout stays clean for git and archives.
|
|
# Launchers land in <AI_ROOT>/tools/bin and ~/.local/bin.
|
|
# Idempotent: re-run to update.
|
|
#
|
|
# Methods (per package, table below):
|
|
# source — pip install from the local git checkout (pure Python)
|
|
# wheel — pip install the release wheel; the git checkout is still
|
|
# mirrored for audit. Native-heavy builds (faiss, lancedb,
|
|
# duckdb, fastembed/onnxruntime, PySide6/Qt) produce the
|
|
# same code as the wheel at a fraction of the build cost.
|
|
# --force-source builds everything from checkouts.
|
|
#
|
|
# Repository sync is state-aware:
|
|
# absent -> atomic clone (staged in a temp dir, moved into place)
|
|
# corrupt -> dir without .git is moved aside, then re-cloned
|
|
# dirty -> local modifications are never destroyed; the package
|
|
# is skipped with the offending files listed
|
|
# moved -> fast-forward to FETCH_HEAD only when HEAD differs;
|
|
# unchanged checkouts are NOT reinstalled (the venv is
|
|
# probed for the importable module first)
|
|
# Step-down: a failed source build falls back to the wheel (logged),
|
|
# so one broken upstream never wedges the whole bootstrap.
|
|
#
|
|
# Usage:
|
|
# scripts/bootstrap_sources.sh # asks which projects
|
|
# (Enter = all) in a
|
|
# terminal; installs all
|
|
# when non-interactive
|
|
# scripts/bootstrap_sources.sh --only a,b # subset, no prompt
|
|
# scripts/bootstrap_sources.sh --force-source # build native too
|
|
# scripts/bootstrap_sources.sh --update # git pull + reinstall
|
|
#
|
|
# Native build deps (auto-installed via pacman when missing, sudo):
|
|
# git gcc make cmake rustup postgresql-libs
|
|
|
|
set -euo pipefail
|
|
|
|
AI_ROOT="${THICKET_AI_ROOT:-/mnt/AI}"
|
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
GIT_MIRROR="$AI_ROOT/distfiles/git"
|
|
VENV_DIR="$AI_ROOT/runtime/thicket-venv"
|
|
BIN_DIR="$AI_ROOT/tools/bin"
|
|
LOG_DIR="$AI_ROOT/logs"
|
|
LOG="$LOG_DIR/thicket-bootstrap.log"
|
|
|
|
FORCE_SOURCE=0
|
|
UPDATE_ONLY=0
|
|
ONLY=""
|
|
mode="${1:-}"
|
|
case "$mode" in
|
|
--force-source) FORCE_SOURCE=1 ;;
|
|
--update) UPDATE_ONLY=1 ;;
|
|
--only) ONLY="${2:-}"; [ -n "$ONLY" ] || { echo "--only needs a list"; exit 2; } ;;
|
|
"") ;;
|
|
*) echo "unknown option: $mode"; exit 2 ;;
|
|
esac
|
|
|
|
mkdir -p "$GIT_MIRROR" "$VENV_DIR" "$BIN_DIR" "$LOG_DIR"
|
|
exec > >(tee -a "$LOG") 2>&1
|
|
|
|
log() { printf '\033[1;32m>>>\033[0m %s\n' "$*"; }
|
|
warn() { printf '\033[1;33m>>> WARN\033[0m %s\n' "$*"; }
|
|
|
|
# ── name | git url | pip name | extras | method ─────────────────────────
|
|
# Pure-python projects install from their checkout; the native-heavy
|
|
# set keeps a mirrored checkout but installs the release wheel.
|
|
PACKAGES=(
|
|
"qdrant-client|https://github.com/qdrant/qdrant-client.git|qdrant-client||source|qdrant_client|vector target: qdrant (service)"
|
|
"chromadb|https://github.com/chroma-core/chroma.git|chromadb||source|chromadb|vector target: chroma (embedded)"
|
|
"lancedb|https://github.com/lancedb/lancedb.git|lancedb||wheel|lancedb|vector target: lancedb (embedded)"
|
|
"faiss|https://github.com/facebookresearch/faiss.git|faiss-cpu||wheel|faiss|vector target: faiss (file index)"
|
|
"pymilvus|https://github.com/milvus-io/pymilvus.git|pymilvus|[milvus_lite]|source|pymilvus|vector target: milvus lite"
|
|
"weaviate-client|https://github.com/weaviate/weaviate-python-client.git|weaviate-client||source|weaviate|vector target: weaviate (service)"
|
|
"psycopg|https://github.com/psycopg/psycopg.git|psycopg|[binary]|wheel|psycopg|vector target: pgvector - postgres driver"
|
|
"pgvector|https://github.com/pgvector/pgvector-python.git|pgvector||source|pgvector|vector target: pgvector - python bindings"
|
|
"duckdb|https://github.com/duckdb/duckdb.git|duckdb||wheel|duckdb|vector target: duckdb (embedded)"
|
|
"sqlite-vec|https://github.com/asg017/sqlite-vec.git|sqlite-vec||source|sqlite_vec|vector target: sqlite-vec (embedded)"
|
|
"PyMySQL|https://github.com/PyMySQL/PyMySQL.git|PyMySQL||source|pymysql|vector target: mariadb driver"
|
|
"graphify|https://github.com/Graphify-Labs/graphify.git|graphifyy|[ollama]|source|graphify|graph engine: graph.json + HTML report"
|
|
"vanna|https://github.com/vanna-ai/vanna.git|vanna|[ollama,postgres,mysql]|source|vanna|ask: natural-language SQL over corpora"
|
|
"minio-py|https://github.com/minio/minio-py.git|minio||source|minio|stage: MinIO object archive"
|
|
"LightRAG|https://github.com/HKUDS/LightRAG.git|lightrag-hku||source|lightrag|graph engine: merged entity graph"
|
|
"ebooklib|https://github.com/aerkalov/ebooklib.git|EbookLib||source|ebooklib|parser: EPUB"
|
|
"BeautifulSoup4|https://github.com/wention/BeautifulSoup4.git|beautifulsoup4||source|bs4|parser: HTML inside EPUB"
|
|
"pypdf|https://github.com/py-pdf/pypdf.git|pypdf||source|pypdf|parser: PDF"
|
|
"python-slugify|https://github.com/un33k/python-slugify.git|python-slugify||source|slugify|vault note filenames"
|
|
"fastembed|https://github.com/qdrant/fastembed.git|fastembed||wheel|fastembed|local ONNX embeddings"
|
|
"PySide6|https://code.qt.io/pyside/pyside-setup.git|PySide6||wheel|PySide6|the console GUI toolkit"
|
|
)
|
|
|
|
# ── native build tools via pacman (skipped when present) ─────────────
|
|
required_tools=(git gcc make)
|
|
missing=()
|
|
for tool in "${required_tools[@]}"; do
|
|
command -v "$tool" >/dev/null 2>&1 || missing+=("$tool")
|
|
done
|
|
if [ "${#missing[@]}" -gt 0 ]; then
|
|
log "installing build tools via pacman: ${missing[*]}"
|
|
sudo pacman -S --needed "${missing[@]}"
|
|
fi
|
|
|
|
# ── project selection ─────────────────────────────────────────────────
|
|
# Interactive in a terminal; --only or non-TTY stdin bypasses the menu.
|
|
select_packages() {
|
|
if [ -n "$ONLY" ]; then
|
|
return
|
|
fi
|
|
if [ ! -t 0 ] && [ "${THICKET_BOOTSTRAP_INTERACTIVE:-0}" != "1" ]; then
|
|
return # everything, no prompt
|
|
fi
|
|
|
|
local -a names=() roles=()
|
|
local row
|
|
for row in "${PACKAGES[@]}"; do
|
|
names+=("$(cut -d'|' -f1 <<<"$row")")
|
|
roles+=("$(cut -d'|' -f7 <<<"$row")")
|
|
done
|
|
|
|
printf 'Thicket projects:\n'
|
|
local i
|
|
for i in "${!names[@]}"; do
|
|
printf ' %2d) %-18s %-34s %s\n' \
|
|
"$((i + 1))" "${names[$i]}" \
|
|
"$(cut -d'|' -f5 <<<"${PACKAGES[$i]}")" "${roles[$i]}"
|
|
done
|
|
|
|
local answer token from to picked valid
|
|
while :; do
|
|
read -r -p $'Install which? (numbers/names/ranges, "all", Enter = all): ' answer || return
|
|
[ -z "$answer" ] && return
|
|
[ "$answer" = "all" ] && return
|
|
picked=""
|
|
for token in ${answer//,/ }; do
|
|
valid=""
|
|
if [[ "$token" =~ ^[0-9]+$ ]] && [ "$token" -ge 1 ] \
|
|
&& [ "$token" -le "${#names[@]}" ]; then
|
|
picked+=",${names[$((token - 1))]}"; valid=1
|
|
elif [[ "$token" =~ ^([0-9]+)-([0-9]+)$ ]]; then
|
|
from="${BASH_REMATCH[1]}"; to="${BASH_REMATCH[2]}"
|
|
if [ "$from" -ge 1 ] && [ "$to" -le "${#names[@]}" ] \
|
|
&& [ "$from" -le "$to" ]; then
|
|
for ((i = from; i <= to; i++)); do
|
|
picked+=",${names[$((i - 1))]}"
|
|
done
|
|
valid=1
|
|
fi
|
|
else
|
|
for i in "${!names[@]}"; do
|
|
if [ "${names[$i],,}" = "${token,,}" ]; then
|
|
picked+=",${names[$i]}"; valid=1; break
|
|
fi
|
|
done
|
|
fi
|
|
[ -n "$valid" ] || warn "ignored unknown selection: $token"
|
|
done
|
|
if [ -n "$picked" ]; then
|
|
ONLY="${picked#,}"
|
|
log "selected: ${ONLY//,/ }"
|
|
return
|
|
fi
|
|
done
|
|
}
|
|
select_packages
|
|
|
|
# ── the venv ──────────────────────────────────────────────────────────
|
|
if [ ! -x "$VENV_DIR/bin/python" ]; then
|
|
log "creating venv at $VENV_DIR"
|
|
python3 -m venv "$VENV_DIR"
|
|
fi
|
|
PIP=("$VENV_DIR/bin/pip")
|
|
"${PIP[@]}" install -q --upgrade pip setuptools wheel
|
|
|
|
# ── repository sync: absent | corrupt | dirty | unchanged | moved ────
|
|
# Prints the resulting state; never destroys uncommitted work.
|
|
sync_repo() {
|
|
local name="$1" url="$2"
|
|
local dir="$GIT_MIRROR/$name"
|
|
|
|
if [ ! -e "$dir" ]; then
|
|
local stage="$dir.clone.$$"
|
|
if git clone --quiet --depth 1 "$url" "$stage"; then
|
|
mv "$stage" "$dir"
|
|
echo "cloned"
|
|
else
|
|
rm -rf "$stage"
|
|
echo "clone-failed"
|
|
fi
|
|
return
|
|
fi
|
|
|
|
if [ ! -d "$dir/.git" ]; then
|
|
local aside="$dir.corrupt-$(date +%s)"
|
|
warn "[$name] not a git checkout — moved aside to $(basename "$aside")"
|
|
mv "$dir" "$aside"
|
|
local stage="$dir.clone.$$"
|
|
if git clone --quiet --depth 1 "$url" "$stage"; then
|
|
mv "$stage" "$dir"
|
|
echo "recloned"
|
|
else
|
|
rm -rf "$stage"
|
|
echo "clone-failed"
|
|
fi
|
|
return
|
|
fi
|
|
|
|
local origin_url
|
|
origin_url="$(git -C "$dir" remote get-url origin 2>/dev/null || true)"
|
|
if [ "$origin_url" != "$url" ]; then
|
|
warn "[$name] remote is '$origin_url', table says '$url' — skipping"
|
|
echo "url-mismatch"
|
|
return
|
|
fi
|
|
|
|
if [ -n "$(git -C "$dir" status --porcelain 2>/dev/null)" ]; then
|
|
warn "[$name] local modifications — not touching them:"
|
|
git -C "$dir" status --porcelain | sed 's/^/ /'
|
|
echo "dirty"
|
|
return
|
|
fi
|
|
|
|
local branch refspec
|
|
branch="$(git -C "$dir" rev-parse --abbrev-ref HEAD)"
|
|
if [ "$branch" = "HEAD" ]; then
|
|
refspec="HEAD" # detached shallow clone
|
|
else
|
|
refspec="$branch"
|
|
fi
|
|
if ! git -C "$dir" fetch --quiet --depth 1 origin "$refspec"; then
|
|
warn "[$name] fetch failed (offline?)"
|
|
echo "unchanged"
|
|
return
|
|
fi
|
|
|
|
local head fetched
|
|
head="$(git -C "$dir" rev-parse HEAD)"
|
|
fetched="$(git -C "$dir" rev-parse FETCH_HEAD)"
|
|
if [ "$head" = "$fetched" ]; then
|
|
echo "unchanged"
|
|
return
|
|
fi
|
|
git -C "$dir" reset --hard --quiet "$fetched"
|
|
echo "moved"
|
|
}
|
|
|
|
module_importable() {
|
|
"$VENV_DIR/bin/python" -c "import $1" >/dev/null 2>&1
|
|
}
|
|
|
|
# ── per-package install with step-down ────────────────────────────────
|
|
install_one() {
|
|
local name="$1" url="$2" pipname="$3" extras="$4" method="$5" module="$6"
|
|
local dir="$GIT_MIRROR/$name"
|
|
|
|
if [ -n "$ONLY" ]; then
|
|
case ",$ONLY," in *",$name,"*) ;; *) return 0 ;; esac
|
|
fi
|
|
|
|
local state
|
|
state="$(sync_repo "$name" "$url")"
|
|
case "$state" in
|
|
clone-failed)
|
|
warn "[$name] clone failed — skipping"
|
|
return 0 ;;
|
|
url-mismatch | dirty)
|
|
return 0 ;; # never clobber; report above
|
|
esac
|
|
|
|
if [ "$UPDATE_ONLY" -eq 1 ]; then
|
|
log "[$name] $state"
|
|
return 0
|
|
fi
|
|
|
|
# Unchanged checkout + already importable: nothing to do.
|
|
if [ "$state" = "unchanged" ] && module_importable "$module"; then
|
|
log "[$name] up to date ($(git -C "$dir" rev-parse --short HEAD))"
|
|
return 0
|
|
fi
|
|
|
|
if [ "$method" = "source" ] || [ "$FORCE_SOURCE" -eq 1 ]; then
|
|
log "[$name] $state — installing from source checkout"
|
|
# Local-path extras: pip accepts "<dir>[extra1,extra2]".
|
|
if "${PIP[@]}" install -q "${dir}${extras}"; then
|
|
return 0
|
|
fi
|
|
warn "[$name] source build failed — stepping down to wheel"
|
|
fi
|
|
log "[$name] $state — installing ${pipname}${extras} (wheel)"
|
|
"${PIP[@]}" install -q "${pipname}${extras}"
|
|
}
|
|
|
|
for row in "${PACKAGES[@]}"; do
|
|
IFS='|' read -r name url pipname extras method module _ <<<"$row"
|
|
install_one "$name" "$url" "$pipname" "$extras" "$method" "$module"
|
|
done
|
|
|
|
# ── thicket itself, editable from this checkout ───────────────────────
|
|
log "[thicket] installing project (editable)"
|
|
"${PIP[@]}" install -q -e "$PROJECT_ROOT"
|
|
|
|
# ── launcher in the AI tree ───────────────────────────────────────────
|
|
cat > "$BIN_DIR/thicket" <<WRAPPER
|
|
#!/usr/bin/env bash
|
|
# Thicket launcher — /mnt/AI venv, project checkout, one command.
|
|
exec "$VENV_DIR/bin/python" "$PROJECT_ROOT/thicket.py" "\$@"
|
|
WRAPPER
|
|
chmod +x "$BIN_DIR/thicket"
|
|
|
|
# PATH command (~/.local/bin) — same venv, same interpreter.
|
|
# Nothing is ever written inside the project checkout.
|
|
mkdir -p "$HOME/.local/bin"
|
|
cat > "$HOME/.local/bin/thicket" <<USERWRAPPER
|
|
#!/usr/bin/env bash
|
|
# thicket — console/CLI with the project venv's interpreter.
|
|
exec "$VENV_DIR/bin/python" "$PROJECT_ROOT/thicket.py" "\$@"
|
|
USERWRAPPER
|
|
chmod +x "$HOME/.local/bin/thicket"
|
|
|
|
log "bootstrap complete"
|
|
log " venv : $VENV_DIR"
|
|
log " launcher : $BIN_DIR/thicket"
|
|
log " mirror : $GIT_MIRROR ($(ls "$GIT_MIRROR" | wc -l) checkouts)"
|
|
log "verify : $BIN_DIR/thicket --dry-run"
|