#!/usr/bin/env bash # Installs everything owpengram-server.sh checks for, so a fresh machine needs # one command instead of a shopping list: Go, Python 3 (+ the panel's packages # in a venv), Docker and OpenSSL. # # ./scripts/install-prereqs.sh install whatever is missing # ./scripts/install-prereqs.sh --dry-run only report what it would install # ./scripts/install-prereqs.sh --yes no confirmation prompt # # Supported: Arch (pacman) and Ubuntu/Debian (apt). Anything else is reported # rather than guessed at -- a wrong package manager on a production box is a # worse outcome than a clear "install these yourself". # # Root is taken once, up front, and held for the whole run: a sudo prompt # appearing ten minutes in, after the user walked away, is how half-installed # machines happen. set -euo pipefail cd "$(dirname "$0")/.." REPO_ROOT="$PWD" ok() { printf '[ok] %s\n' "$*"; } info() { printf '[..] %s\n' "$*"; } warn() { printf '[WARN] %s\n' "$*"; } die() { printf '[ERROR] %s\n' "$*" >&2; exit 1; } DRY_RUN=0 ASSUME_YES=0 for arg in "$@"; do case "$arg" in --dry-run) DRY_RUN=1 ;; --yes|-y) ASSUME_YES=1 ;; -h|--help) sed -n '2,14p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) die "unknown argument: $arg" ;; esac done # --- distro ------------------------------------------------------------------ # ID_LIKE is what makes derivatives work without listing every one of them: # EndeavourOS says ID_LIKE=arch, Pop!_OS and Mint say ID_LIKE=ubuntu/debian. FAMILY="" DISTRO_NAME="unknown" if [[ -r /etc/os-release ]]; then # shellcheck disable=SC1091 . /etc/os-release DISTRO_NAME="${PRETTY_NAME:-$ID}" case " ${ID:-} ${ID_LIKE:-} " in *" arch "*) FAMILY="arch" ;; *" ubuntu "*|*" debian "*) FAMILY="debian" ;; esac fi [[ -n "$FAMILY" ]] || die "unsupported distribution: ${DISTRO_NAME}. Supported: Arch and Ubuntu/Debian. Install Go 1.25+, Python 3, Docker and OpenSSL by hand, then run ./owpengram-server.sh" # --- what is missing --------------------------------------------------------- have() { command -v "$1" >/dev/null 2>&1; } # Go's own version gate: go.mod asks for 1.25, and a too-old toolchain fails at # build time with a message that does not obviously point back here. GO_MIN_MINOR=25 go_is_recent_enough() { have go || return 1 local v minor v="$(go env GOVERSION 2>/dev/null || true)" # e.g. go1.25.7 minor="${v#go1.}" minor="${minor%%.*}" [[ "$minor" =~ ^[0-9]+$ ]] && (( minor >= GO_MIN_MINOR )) } venv_python() { printf '%s/.venv/bin/python' "$REPO_ROOT"; } # Same precedence owpengram-server.sh uses when it picks an interpreter: the # venv when one exists, otherwise whatever python3 is on PATH. Checking only the # venv would report the packages missing on a machine that already has them # installed system-wide, and build a venv nobody asked for. pydeps_satisfied() { local py if [[ -x "$(venv_python)" ]]; then py="$(venv_python)" elif have python3; then py="python3" else return 1 fi [[ -z "$("$py" tui-panel/check_deps.py 2>/dev/null)" ]] } NEEDED=() go_is_recent_enough || NEEDED+=("go") have python3 || NEEDED+=("python") have docker || NEEDED+=("docker") have openssl || NEEDED+=("openssl") # The venv is built with python3, so it can only be settled after Python is. pydeps_satisfied || NEEDED+=("pydeps") if [[ ${#NEEDED[@]} -eq 0 ]]; then ok "All prerequisites are already installed." exit 0 fi echo "== Missing prerequisites on ${DISTRO_NAME} ==" for item in "${NEEDED[@]}"; do case "$item" in go) echo " - Go 1.${GO_MIN_MINOR}+ (builds owpengram-server and the admin panel)" ;; python) echo " - Python 3 (runs the server-panel TUI)" ;; pydeps) echo " - Python packages: textual, psutil, cryptography (into ./.venv)" ;; docker) echo " - Docker (runs PostgreSQL, Redis and MinIO)" ;; openssl) echo " - OpenSSL (exports the server's RSA public key for clients)" ;; esac done echo if [[ "$DRY_RUN" -eq 1 ]]; then ok "Dry run -- nothing was installed." exit 0 fi if [[ "$ASSUME_YES" -ne 1 ]]; then # Without a terminal there is nobody to answer, and `read` would block for as # long as the caller is willing to wait -- which for a CI job or a wrapping # script is forever. Say so instead of hanging. if [[ ! -t 0 ]]; then die "no terminal to confirm on -- re-run with --yes to install without asking, or --dry-run to only look" fi read -r -p "Install these now? This needs root. [Y/n] " answer case "${answer:-y}" in [Yy]|[Yy][Ee][Ss]|"") ;; *) die "cancelled" ;; esac fi # Ask for the password once, then refresh the timestamp in the background so a # long Go download or apt run does not hit a second prompt mid-way. if [[ $EUID -ne 0 ]]; then have sudo || die "sudo is not installed and this is not running as root" sudo -v || die "could not acquire root" while true; do sudo -n true 2>/dev/null; sleep 50; done & SUDO_KEEPALIVE=$! trap 'kill "$SUDO_KEEPALIVE" 2>/dev/null || true' EXIT SUDO="sudo" else SUDO="" fi needs() { local x; for x in "${NEEDED[@]}"; do [[ "$x" == "$1" ]] && return 0; done; return 1; } # Tools this script itself leans on, installed before anything tries to use # them. A minimal Ubuntu has no curl, and the Go tarball download would die on # its first call -- after root was already taken and the first package was # already installed, which is the worst place to stop. Arch's base always has # curl (pacman links against it), so in practice this is the Debian path. ensure_installer_tools() { local wanted=() have curl || wanted+=("curl") # Without the CA bundle every https download fails certificate verification. if [[ "$FAMILY" == "debian" && ! -e /etc/ssl/certs/ca-certificates.crt ]]; then wanted+=("ca-certificates") fi [[ ${#wanted[@]} -eq 0 ]] && return 0 info "Installing what this script needs first: ${wanted[*]}" pkg_install "${wanted[@]}" } # --- package manager --------------------------------------------------------- APT_UPDATED=0 apt_update_once() { if [[ "$APT_UPDATED" -eq 0 ]]; then $SUDO apt-get update -qq APT_UPDATED=1 fi } pkg_install() { case "$FAMILY" in arch) $SUDO pacman -S --needed --noconfirm "$@" ;; debian) apt_update_once; $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "$@" ;; esac } ensure_installer_tools # --- Python + OpenSSL -------------------------------------------------------- if needs python; then info "Installing Python 3" case "$FAMILY" in arch) pkg_install python ;; # python3-venv is separate on Debian/Ubuntu and is what the panel's # dependencies go into: both distros mark the system Python # externally-managed, so pip into it is refused by design. debian) pkg_install python3 python3-venv python3-pip ;; esac ok "Python installed: $(python3 --version)" fi if needs openssl; then info "Installing OpenSSL" pkg_install openssl ok "OpenSSL installed: $(openssl version)" fi # --- Go ---------------------------------------------------------------------- # Arch ships a current Go; Debian/Ubuntu do not (24.04 is still on 1.22, below # what go.mod needs), so there the official tarball is the only sane source. install_go_tarball() { local version arch tarball url tmp expected actual version="$(curl -fsSL 'https://go.dev/VERSION?m=text' | head -n1)" [[ "$version" == go* ]] || die "could not determine the latest Go version" case "$(uname -m)" in x86_64) arch="amd64" ;; aarch64) arch="arm64" ;; armv7l) arch="armv6l" ;; *) die "unsupported CPU architecture for the Go tarball: $(uname -m)" ;; esac tarball="${version}.linux-${arch}.tar.gz" url="https://go.dev/dl/${tarball}" tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' RETURN info "Downloading ${tarball}" curl -fsSL "$url" -o "$tmp/$tarball" # The published checksum lives in go.dev's release index, so the download is # verified against the site's metadata rather than trusted on arrival. expected="$(curl -fsSL 'https://go.dev/dl/?mode=json&include=all' \ | python3 -c 'import json,sys want = sys.argv[1] for release in json.load(sys.stdin): for f in release.get("files", []): if f.get("filename") == want: print(f.get("sha256", "")) raise SystemExit ' "$tarball")" [[ -n "$expected" ]] || die "no published checksum for ${tarball}" actual="$(sha256sum "$tmp/$tarball" | cut -d' ' -f1)" [[ "$actual" == "$expected" ]] || die "checksum mismatch for ${tarball}: expected ${expected}, got ${actual}" ok "Checksum verified" $SUDO rm -rf /usr/local/go $SUDO tar -C /usr/local -xzf "$tmp/$tarball" # /usr/local/go/bin is not on a default PATH; drop a profile snippet so new # shells find it. The current shell still will not, hence the note at the end. printf 'export PATH=$PATH:/usr/local/go/bin\n' | $SUDO tee /etc/profile.d/go.sh >/dev/null $SUDO chmod 0644 /etc/profile.d/go.sh export PATH="$PATH:/usr/local/go/bin" } GO_NEEDS_NEW_SHELL=0 if needs go; then info "Installing Go" case "$FAMILY" in arch) pkg_install go ;; debian) install_go_tarball; GO_NEEDS_NEW_SHELL=1 ;; esac ok "Go installed: $(go version)" fi # --- Docker ------------------------------------------------------------------ DOCKER_NEEDS_RELOGIN=0 install_docker_debian() { local codename repo_distro # Derivatives (Mint, Pop!_OS) carry their own codename that Docker's repo # does not publish; UBUNTU_CODENAME is the upstream one it does. # shellcheck disable=SC1091 . /etc/os-release codename="${UBUNTU_CODENAME:-${VERSION_CODENAME:-}}" [[ -n "$codename" ]] || die "could not determine the distribution codename for Docker's repository" case " ${ID:-} ${ID_LIKE:-} " in *" ubuntu "*) repo_distro="ubuntu" ;; *) repo_distro="debian" ;; esac pkg_install ca-certificates curl gnupg $SUDO install -m 0755 -d /etc/apt/keyrings $SUDO curl -fsSL "https://download.docker.com/linux/${repo_distro}/gpg" -o /etc/apt/keyrings/docker.asc $SUDO chmod a+r /etc/apt/keyrings/docker.asc printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/%s %s stable\n' \ "$(dpkg --print-architecture)" "$repo_distro" "$codename" \ | $SUDO tee /etc/apt/sources.list.d/docker.list >/dev/null APT_UPDATED=0 # the new repository has to be picked up pkg_install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin } if needs docker; then info "Installing Docker" case "$FAMILY" in arch) pkg_install docker docker-compose ;; debian) install_docker_debian ;; esac if have systemctl; then $SUDO systemctl enable --now docker ok "Docker service started" else warn "systemd not found -- start the Docker daemon yourself before running ./owpengram-server.sh" fi # Without this every docker call needs sudo, which the panel does not use. if [[ $EUID -ne 0 ]] && ! id -nG "$USER" | tr ' ' '\n' | grep -qx docker; then $SUDO usermod -aG docker "$USER" DOCKER_NEEDS_RELOGIN=1 fi ok "Docker installed: $(docker --version)" fi # --- the panel's Python packages --------------------------------------------- # Always a venv, never the system interpreter: Arch and Debian both refuse pip # into it (PEP 668), and owpengram-server.sh already prefers ./.venv when present. if needs pydeps; then info "Installing the panel's Python packages into ./.venv" # Debian/Ubuntu ship venv separately from python3, so an interpreter that was # already installed (and therefore skipped the Python step above) can still be # missing ensurepip -- `python3 -m venv` then fails halfway through, leaving a # broken .venv behind. The package is named for the interpreter's version; # the unversioned metapackage is the fallback for Debian releases that have it. if [[ "$FAMILY" == "debian" ]] && ! python3 -c 'import ensurepip' 2>/dev/null; then PYVER="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" info "Installing python${PYVER}-venv" pkg_install "python${PYVER}-venv" 2>/dev/null || pkg_install python3-venv fi # A previous failed attempt leaves a .venv without an interpreter; starting # over is the only way out of that, and it costs nothing when it is absent. [[ -x "$(venv_python)" ]] || rm -rf "$REPO_ROOT/.venv" [[ -x "$(venv_python)" ]] || python3 -m venv "$REPO_ROOT/.venv" "$(venv_python)" -m pip install --quiet --upgrade pip "$(venv_python)" -m pip install --quiet -r tui-panel/requirements-panel.txt ok "Python packages installed" fi echo ok "Prerequisites are ready." if [[ "$GO_NEEDS_NEW_SHELL" -eq 1 ]]; then echo " Go was installed to /usr/local/go. Open a new shell, or run:" echo " export PATH=\$PATH:/usr/local/go/bin" fi if [[ "$DOCKER_NEEDS_RELOGIN" -eq 1 ]]; then echo " You were added to the 'docker' group. Already-running shells keep the" echo " old one -- owpengram-server.sh re-enters itself with 'sg docker' so this" echo " run works anyway, but log out and back in before using docker elsewhere." fi echo " Then start the server with: ./owpengram-server.sh"