Add self-deploying setup scripts for all sub-projects (P1-P8)
- Root setup.sh orchestrator with per-phase dispatch (./setup.sh p1..p8 | all | status) - Makefile convenience targets (make infra, make llm, make status, etc.) - scripts/common.sh: shared bash library for OS detection, Docker helpers, service management (launchd/systemd), package install, env management - .env.example + .gitignore: shared config template and secret exclusions P1 (homeai-infra): full implementation - docker-compose.yml: Uptime Kuma, code-server, n8n - Note: Home Assistant, Portainer, Gitea are pre-existing instances - setup.sh: Docker install, homeai network, container health checks P2 (homeai-llm): full implementation - Ollama native install with CUDA/ROCm/Metal auto-detection - launchd plist (macOS) + systemd service (Linux) for auto-start - scripts/pull-models.sh: idempotent model puller from manifest - scripts/benchmark.sh: tokens/sec measurement per model - Open WebUI on port 3030 (avoids Gitea :3000 conflict) P3-P8: working stubs with prerequisite checks and TODO sections Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
331
scripts/common.sh
Normal file
331
scripts/common.sh
Normal file
@@ -0,0 +1,331 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/common.sh — Shared bash library for HomeAI setup scripts
|
||||
# Source this file: source "$(dirname "$0")/../scripts/common.sh"
|
||||
# Do NOT execute directly.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ─── Colours ───────────────────────────────────────────────────────────────────
|
||||
if [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'; YELLOW='\033[0;33m'; GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
|
||||
else
|
||||
RED=''; YELLOW=''; GREEN=''; BLUE=''; CYAN=''; BOLD=''; RESET=''
|
||||
fi
|
||||
|
||||
# ─── Logging ───────────────────────────────────────────────────────────────────
|
||||
log_info() { echo -e "${BLUE}[INFO]${RESET} $*"; }
|
||||
log_success() { echo -e "${GREEN}[OK]${RESET} $*"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
|
||||
log_section() { echo -e "\n${BOLD}${CYAN}══ $* ══${RESET}"; }
|
||||
log_step() { echo -e "${CYAN} →${RESET} $*"; }
|
||||
|
||||
die() { log_error "$*"; exit 1; }
|
||||
|
||||
# ─── OS & Architecture Detection ───────────────────────────────────────────────
|
||||
detect_os() {
|
||||
case "$(uname -s)" in
|
||||
Linux*) OS_TYPE=linux ;;
|
||||
Darwin*) OS_TYPE=macos ;;
|
||||
*) die "Unsupported OS: $(uname -s)" ;;
|
||||
esac
|
||||
export OS_TYPE
|
||||
}
|
||||
|
||||
detect_arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) ARCH=amd64 ;;
|
||||
arm64|aarch64) ARCH=arm64 ;;
|
||||
*) ARCH=unknown ;;
|
||||
esac
|
||||
export ARCH
|
||||
}
|
||||
|
||||
detect_distro() {
|
||||
DISTRO=unknown
|
||||
if [[ "$OS_TYPE" == "linux" ]]; then
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
DISTRO="${ID:-unknown}"
|
||||
fi
|
||||
fi
|
||||
export DISTRO
|
||||
}
|
||||
|
||||
detect_platform() {
|
||||
detect_os
|
||||
detect_arch
|
||||
detect_distro
|
||||
log_info "Platform: ${OS_TYPE}/${ARCH} (${DISTRO})"
|
||||
}
|
||||
|
||||
# ─── GPU Detection ─────────────────────────────────────────────────────────────
|
||||
detect_gpu() {
|
||||
GPU_TYPE=none
|
||||
if [[ "$OS_TYPE" == "macos" && "$ARCH" == "arm64" ]]; then
|
||||
GPU_TYPE=metal
|
||||
elif command -v nvidia-smi &>/dev/null && nvidia-smi &>/dev/null; then
|
||||
GPU_TYPE=cuda
|
||||
GPU_INFO=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -1 || true)
|
||||
elif command -v rocm-smi &>/dev/null; then
|
||||
GPU_TYPE=rocm
|
||||
GPU_INFO=$(rocm-smi --showproductname 2>/dev/null | head -1 || true)
|
||||
fi
|
||||
export GPU_TYPE GPU_INFO
|
||||
log_info "GPU: ${GPU_TYPE}${GPU_INFO:+ — ${GPU_INFO}}"
|
||||
}
|
||||
|
||||
# ─── Dependency Checking ───────────────────────────────────────────────────────
|
||||
require_command() {
|
||||
local cmd="$1"
|
||||
local hint="${2:-install $cmd}"
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
die "'$cmd' is required but not found. Hint: $hint"
|
||||
fi
|
||||
}
|
||||
|
||||
command_exists() { command -v "$1" &>/dev/null; }
|
||||
|
||||
require_min_version() {
|
||||
local cmd="$1" required="$2"
|
||||
local actual
|
||||
actual="$("$cmd" --version 2>&1 | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1)"
|
||||
if [[ "$(printf '%s\n' "$required" "$actual" | sort -V | head -1)" != "$required" ]]; then
|
||||
die "$cmd version $required+ required, found $actual"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Package Management ────────────────────────────────────────────────────────
|
||||
install_package() {
|
||||
local pkg="$1"
|
||||
log_step "Installing $pkg..."
|
||||
case "$OS_TYPE" in
|
||||
linux)
|
||||
case "$DISTRO" in
|
||||
ubuntu|debian|linuxmint|pop)
|
||||
sudo apt-get install -y -qq "$pkg" ;;
|
||||
fedora|rhel|centos|rocky|almalinux)
|
||||
sudo dnf install -y -q "$pkg" ;;
|
||||
arch|manjaro|endeavouros)
|
||||
sudo pacman -S --noconfirm --quiet "$pkg" ;;
|
||||
opensuse*)
|
||||
sudo zypper install -y -q "$pkg" ;;
|
||||
*)
|
||||
die "Unknown distro '$DISTRO' — install $pkg manually" ;;
|
||||
esac ;;
|
||||
macos)
|
||||
if ! command_exists brew; then
|
||||
die "Homebrew not found. Install from https://brew.sh"
|
||||
fi
|
||||
brew install "$pkg" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
update_package_index() {
|
||||
if [[ "$OS_TYPE" == "linux" ]]; then
|
||||
case "$DISTRO" in
|
||||
ubuntu|debian|linuxmint|pop) sudo apt-get update -qq ;;
|
||||
fedora|rhel|centos|rocky|almalinux) sudo dnf check-update -q || true ;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Docker ────────────────────────────────────────────────────────────────────
|
||||
check_docker_installed() {
|
||||
command_exists docker
|
||||
}
|
||||
|
||||
check_docker_running() {
|
||||
docker info &>/dev/null
|
||||
}
|
||||
|
||||
install_docker() {
|
||||
if check_docker_installed; then
|
||||
log_success "Docker already installed: $(docker --version)"
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "Installing Docker..."
|
||||
|
||||
if [[ "$OS_TYPE" == "macos" ]]; then
|
||||
die "On macOS, install Docker Desktop manually: https://www.docker.com/products/docker-desktop/"
|
||||
fi
|
||||
|
||||
# Linux: use the official convenience script
|
||||
curl -fsSL https://get.docker.com -o /tmp/get-docker.sh
|
||||
sudo sh /tmp/get-docker.sh
|
||||
rm /tmp/get-docker.sh
|
||||
|
||||
# Add current user to docker group
|
||||
sudo usermod -aG docker "$USER"
|
||||
log_warn "Added $USER to docker group. Log out and back in (or run 'newgrp docker') for it to take effect."
|
||||
|
||||
# Start and enable Docker
|
||||
sudo systemctl enable --now docker
|
||||
log_success "Docker installed: $(docker --version)"
|
||||
}
|
||||
|
||||
ensure_docker_running() {
|
||||
if ! check_docker_running; then
|
||||
if [[ "$OS_TYPE" == "linux" ]]; then
|
||||
log_info "Starting Docker..."
|
||||
sudo systemctl start docker
|
||||
sleep 2
|
||||
else
|
||||
die "Docker Desktop is not running. Start it from your Applications folder."
|
||||
fi
|
||||
fi
|
||||
if ! check_docker_running; then
|
||||
die "Docker is not running. Start Docker and retry."
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_docker_network() {
|
||||
local network="${1:-homeai}"
|
||||
if ! docker network inspect "$network" &>/dev/null; then
|
||||
log_step "Creating Docker network '$network'..."
|
||||
docker network create "$network"
|
||||
log_success "Network '$network' created."
|
||||
else
|
||||
log_info "Docker network '$network' already exists."
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Docker Compose ────────────────────────────────────────────────────────────
|
||||
# Handles both `docker compose` (v2) and `docker-compose` (v1)
|
||||
docker_compose() {
|
||||
if docker compose version &>/dev/null 2>&1; then
|
||||
docker compose "$@"
|
||||
elif command_exists docker-compose; then
|
||||
docker-compose "$@"
|
||||
else
|
||||
die "docker compose not found. Ensure Docker is up to date."
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Service Management ────────────────────────────────────────────────────────
|
||||
install_service() {
|
||||
# Usage: install_service <name> <systemd_unit_file> <launchd_plist_file>
|
||||
local name="$1"
|
||||
local systemd_file="$2"
|
||||
local launchd_file="$3"
|
||||
|
||||
if [[ "$OS_TYPE" == "linux" ]]; then
|
||||
if [[ ! -f "$systemd_file" ]]; then
|
||||
log_warn "No systemd unit file at $systemd_file — skipping service install."
|
||||
return
|
||||
fi
|
||||
log_step "Installing systemd service: $name"
|
||||
sudo cp "$systemd_file" "/etc/systemd/system/${name}.service"
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now "$name"
|
||||
log_success "Service '$name' enabled and started."
|
||||
|
||||
elif [[ "$OS_TYPE" == "macos" ]]; then
|
||||
if [[ ! -f "$launchd_file" ]]; then
|
||||
log_warn "No launchd plist at $launchd_file — skipping service install."
|
||||
return
|
||||
fi
|
||||
local plist_dest="${HOME}/Library/LaunchAgents/$(basename "$launchd_file")"
|
||||
log_step "Installing launchd agent: $name"
|
||||
cp "$launchd_file" "$plist_dest"
|
||||
launchctl load -w "$plist_dest"
|
||||
log_success "LaunchAgent '$name' installed and loaded."
|
||||
fi
|
||||
}
|
||||
|
||||
uninstall_service() {
|
||||
local name="$1"
|
||||
local plist_label="${2:-$name}"
|
||||
|
||||
if [[ "$OS_TYPE" == "linux" ]]; then
|
||||
sudo systemctl disable --now "$name" 2>/dev/null || true
|
||||
sudo rm -f "/etc/systemd/system/${name}.service"
|
||||
sudo systemctl daemon-reload
|
||||
elif [[ "$OS_TYPE" == "macos" ]]; then
|
||||
local plist_path="${HOME}/Library/LaunchAgents/${plist_label}.plist"
|
||||
launchctl unload -w "$plist_path" 2>/dev/null || true
|
||||
rm -f "$plist_path"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Environment Files ─────────────────────────────────────────────────────────
|
||||
load_env() {
|
||||
local env_file="${1:-.env}"
|
||||
if [[ -f "$env_file" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
set -a; source "$env_file"; set +a
|
||||
fi
|
||||
}
|
||||
|
||||
load_env_services() {
|
||||
local services_file="${HOME}/.env.services"
|
||||
if [[ -f "$services_file" ]]; then
|
||||
set -a; source "$services_file"; set +a
|
||||
fi
|
||||
}
|
||||
|
||||
write_env_service() {
|
||||
# Append or update KEY=VALUE in ~/.env.services
|
||||
local key="$1" value="$2"
|
||||
local file="${HOME}/.env.services"
|
||||
touch "$file"
|
||||
if grep -q "^${key}=" "$file" 2>/dev/null; then
|
||||
# Update existing
|
||||
sed -i "s|^${key}=.*|${key}=${value}|" "$file"
|
||||
else
|
||||
echo "${key}=${value}" >> "$file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Bootstrap .env from .env.example if not present
|
||||
bootstrap_env() {
|
||||
local dir="${1:-.}"
|
||||
if [[ ! -f "${dir}/.env" && -f "${dir}/.env.example" ]]; then
|
||||
cp "${dir}/.env.example" "${dir}/.env"
|
||||
log_warn "Created ${dir}/.env from .env.example — fill in secrets before continuing."
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Network Helpers ───────────────────────────────────────────────────────────
|
||||
wait_for_http() {
|
||||
local url="$1" name="${2:-service}" timeout="${3:-60}"
|
||||
log_step "Waiting for $name at $url (up to ${timeout}s)..."
|
||||
local elapsed=0
|
||||
while ! curl -sf "$url" -o /dev/null; do
|
||||
sleep 3; elapsed=$((elapsed + 3))
|
||||
if [[ $elapsed -ge $timeout ]]; then
|
||||
log_warn "$name did not respond within ${timeout}s. It may still be starting."
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
log_success "$name is up."
|
||||
}
|
||||
|
||||
check_port_free() {
|
||||
local port="$1"
|
||||
if ss -tlnp "sport = :${port}" 2>/dev/null | grep -q ":${port}"; then
|
||||
return 1 # port in use
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ─── Utility ───────────────────────────────────────────────────────────────────
|
||||
confirm() {
|
||||
local msg="${1:-Continue?}"
|
||||
read -rp "$(echo -e "${YELLOW}${msg} [y/N]${RESET} ")" response
|
||||
[[ "${response,,}" == "y" ]]
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
local title="$1"; shift
|
||||
echo ""
|
||||
log_section "$title"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
local key="$1" val="$2"; shift 2
|
||||
printf " ${BOLD}%-24s${RESET} %s\n" "$key" "$val"
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
Reference in New Issue
Block a user