#!/usr/bin/env bash set -Eeuo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_go_version() { sed -n 's/^go //p' "$SCRIPT_DIR/go.mod" 2>/dev/null | head -n 1 } repo_node_version() { sed -n 's/.*"node": ">= *\([^"]*\)".*/\1/p' "$SCRIPT_DIR/package.json" 2>/dev/null | head -n 1 } repo_pnpm_version() { sed -n 's/.*"packageManager": "pnpm@\([^"]*\)".*/\1/p' "$SCRIPT_DIR/package.json" 2>/dev/null | head -n 1 } GO_VERSION="${GO_VERSION:-$(repo_go_version)}" GO_VERSION="${GO_VERSION:-1.26.2}" NODE_VERSION="${NODE_VERSION:-$(repo_node_version)}" NODE_VERSION="${NODE_VERSION:-22.18.0}" PNPM_VERSION="${PNPM_VERSION:-$(repo_pnpm_version)}" PNPM_VERSION="${PNPM_VERSION:-10.33.0}" NVM_VERSION="${NVM_VERSION:-v0.40.3}" INSTALL_SYSTEM=1 INSTALL_GO=1 INSTALL_NODE=1 INSTALL_FRONTEND_DEPS=1 VERIFY_ONLY=0 WITH_CROSS_CGO=0 SHOW_MENU=0 VERIFY_REPORT_ONLY=0 VERIFY_HAD_ISSUES=0 usage() { cat < %s\n' "$*" } warn() { printf 'WARNING: %s\n' "$*" >&2 } die() { printf 'ERROR: %s\n' "$*" >&2 exit 1 } command_exists() { command -v "$1" >/dev/null 2>&1 } repo_has_git_lfs_hooks() { local hook for hook in pre-push post-checkout post-commit post-merge; do [ -f "$SCRIPT_DIR/.git/hooks/$hook" ] || continue if grep -q 'git-lfs' "$SCRIPT_DIR/.git/hooks/$hook"; then return 0 fi done return 1 } repo_requires_git_lfs() { repo_has_git_lfs_hooks } run_as_root() { if [ "$(id -u)" -eq 0 ]; then "$@" elif command_exists sudo; then sudo "$@" else die "This step needs root privileges. Install sudo or run the script as root." fi } fetch_to() { local url="$1" local output="$2" if command_exists curl; then curl -fL --retry 3 --retry-delay 2 -o "$output" "$url" elif command_exists wget; then wget -O "$output" "$url" else die "curl or wget is required to download $url" fi } version_ge() { local actual="$1" local required="$2" [ "$(printf '%s\n%s\n' "$required" "$actual" | sort -V | head -n 1)" = "$required" ] } append_profile_line() { local file="$1" local line="$2" touch "$file" if ! grep -qxF "$line" "$file"; then printf '\n%s\n' "$line" >>"$file" fi } ensure_go_path() { activate_go_path append_profile_line "$HOME/.profile" 'export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"' append_profile_line "$HOME/.bashrc" 'export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"' } activate_go_path() { export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH" } install_system_packages() { log "Installing system packages" if command_exists apt-get; then local -a packages=( bash build-essential ca-certificates coreutils curl findutils gawk git gzip libpam0g-dev libsqlite3-dev make openssh-client perl pkg-config python3 sed sqlite3 tar unzip wget xz-utils zip ) repo_requires_git_lfs && packages+=(git-lfs) if [ "$WITH_CROSS_CGO" -eq 1 ]; then packages+=( gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 ) fi run_as_root apt-get update run_as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y "${packages[@]}" return fi if command_exists dnf; then local -a packages=( bash ca-certificates coreutils curl findutils gawk gcc gcc-c++ git gzip make openssh-clients pam-devel perl-core pkgconf-pkg-config python3 sed sqlite sqlite-devel tar unzip wget xz zip ) repo_requires_git_lfs && packages+=(git-lfs) run_as_root dnf install -y "${packages[@]}" [ "$WITH_CROSS_CGO" -eq 0 ] || warn "Cross-CGO package setup is only automated for apt-based systems." return fi if command_exists yum; then local -a packages=( bash ca-certificates coreutils curl findutils gawk gcc gcc-c++ git gzip make openssh-clients pam-devel perl pkgconfig python3 sed sqlite sqlite-devel tar unzip wget xz zip ) repo_requires_git_lfs && packages+=(git-lfs) run_as_root yum install -y "${packages[@]}" [ "$WITH_CROSS_CGO" -eq 0 ] || warn "Cross-CGO package setup is only automated for apt-based systems." return fi if command_exists pacman; then local -a packages=( base-devel bash ca-certificates coreutils curl findutils gawk git gzip make openssh pam perl pkgconf python sed sqlite tar unzip wget xz zip ) repo_requires_git_lfs && packages+=(git-lfs) run_as_root pacman -Sy --needed --noconfirm "${packages[@]}" [ "$WITH_CROSS_CGO" -eq 0 ] || warn "Cross-CGO package setup is only automated for apt-based systems." return fi if command_exists zypper; then local -a packages=( bash ca-certificates coreutils curl findutils gawk gcc gcc-c++ git gzip make openssh pam-devel perl pkg-config python3 sed sqlite3 sqlite3-devel tar unzip wget xz zip ) repo_requires_git_lfs && packages+=(git-lfs) run_as_root zypper install -y "${packages[@]}" [ "$WITH_CROSS_CGO" -eq 0 ] || warn "Cross-CGO package setup is only automated for apt-based systems." return fi die "Unsupported package manager. Install git, make, gcc/g++, pkg-config, sqlite3 headers, curl/wget, tar, unzip, xz, python3, and then rerun with --skip-system." } go_download_arch() { case "$(uname -m)" in x86_64 | amd64) printf 'amd64' ;; aarch64 | arm64) printf 'arm64' ;; i386 | i686) printf '386' ;; armv6l | armv7l) printf 'armv6l' ;; *) die "Unsupported CPU architecture for automatic Go install: $(uname -m)" ;; esac } current_go_version() { if command_exists go; then go version | awk '{print $3}' | sed 's/^go//' fi } install_go() { log "Configuring Go $GO_VERSION" ensure_go_path if [ "$(current_go_version)" = "$GO_VERSION" ]; then printf 'Go %s is already installed.\n' "$GO_VERSION" return fi local arch arch="$(go_download_arch)" local archive="/tmp/go${GO_VERSION}.linux-${arch}.tar.gz" local url="https://go.dev/dl/go${GO_VERSION}.linux-${arch}.tar.gz" fetch_to "$url" "$archive" run_as_root rm -rf /usr/local/go run_as_root tar -C /usr/local -xzf "$archive" ensure_go_path [ "$(current_go_version)" = "$GO_VERSION" ] || die "Go install finished, but go version is not $GO_VERSION." } load_nvm() { export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" # shellcheck disable=SC1091 [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" } install_nvm() { export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" if [ -s "$NVM_DIR/nvm.sh" ]; then load_nvm return fi log "Installing NVM $NVM_VERSION" local installer="/tmp/nvm-install-${NVM_VERSION}.sh" fetch_to "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" "$installer" bash "$installer" load_nvm command_exists nvm || die "NVM could not be loaded after installation." } current_node_version() { if command_exists node; then node -p 'process.versions.node' fi } current_pnpm_version() { if command_exists pnpm; then pnpm --version fi } install_node_and_pnpm() { log "Configuring Node $NODE_VERSION and pnpm $PNPM_VERSION" install_nvm nvm install "$NODE_VERSION" nvm alias default "$NODE_VERSION" >/dev/null nvm use "$NODE_VERSION" >/dev/null hash -r if command_exists corepack; then corepack enable corepack prepare "pnpm@${PNPM_VERSION}" --activate hash -r fi if ! command_exists pnpm || ! version_ge "$(current_pnpm_version)" "$PNPM_VERSION"; then npm install -g "pnpm@${PNPM_VERSION}" hash -r fi if ! version_ge "$(current_node_version)" "$NODE_VERSION"; then die "Node $(current_node_version) is older than required $NODE_VERSION." fi if ! version_ge "$(current_pnpm_version)" "$PNPM_VERSION"; then die "pnpm $(current_pnpm_version) is older than required $PNPM_VERSION." fi } install_frontend_deps() { log "Installing frontend dependencies" cd "$SCRIPT_DIR" pnpm install --frozen-lockfile } configure_git_lfs() { repo_requires_git_lfs || return log "Configuring Git LFS" command_exists git-lfs || die "git-lfs is required for this repository but is not installed." cd "$SCRIPT_DIR" git lfs install --local >/dev/null } configure_cross_cgo() { [ "$WITH_CROSS_CGO" -eq 1 ] || return log "Configuring Makefile.local for cross-CGO sqlite builds" cd "$SCRIPT_DIR" if [ -f Makefile.local ] && grep -qF '# BEGIN configure.sh cross-cgo' Makefile.local; then printf 'Makefile.local already contains configure.sh cross-CGO settings.\n' return fi cat >>Makefile.local <<'EOF' # BEGIN configure.sh cross-cgo ifeq ($(GOOS)/$(GOARCH),windows/amd64) CC := x86_64-w64-mingw32-gcc CXX := x86_64-w64-mingw32-g++ export CC export CXX endif ifeq ($(GOOS)/$(GOARCH)/$(GOARM),linux/arm/7) CC := arm-linux-gnueabihf-gcc CXX := arm-linux-gnueabihf-g++ export CC export CXX endif # END configure.sh cross-cgo EOF } verify_environment() { log "Verifying build environment" activate_go_path load_nvm || true hash -r local -a missing=() local -a problems=() local command_name for command_name in git make go node pnpm gcc pkg-config; do if ! command_exists "$command_name"; then missing+=("$command_name") fi done if repo_requires_git_lfs && ! command_exists git-lfs; then missing+=("git-lfs") fi if command_exists go; then local found_go found_go="$(current_go_version)" [ "$found_go" = "$GO_VERSION" ] || problems+=("Go $found_go found, but $GO_VERSION is required.") fi if command_exists node; then local found_node found_node="$(current_node_version)" version_ge "$found_node" "$NODE_VERSION" || problems+=("Node $found_node found, but >= $NODE_VERSION is required.") fi if command_exists pnpm; then local found_pnpm found_pnpm="$(current_pnpm_version)" version_ge "$found_pnpm" "$PNPM_VERSION" || problems+=("pnpm $found_pnpm found, but >= $PNPM_VERSION is required.") fi cd "$SCRIPT_DIR" if command_exists make && ! make help >/dev/null; then problems+=("make help failed.") fi if [ "$WITH_CROSS_CGO" -eq 1 ]; then for command_name in arm-linux-gnueabihf-gcc x86_64-w64-mingw32-gcc; do if ! command_exists "$command_name"; then missing+=("$command_name") fi done fi if [ ! -d "$SCRIPT_DIR/node_modules" ]; then problems+=("node_modules is missing; frontend dependencies are not installed.") fi if [ "${#missing[@]}" -gt 0 ] || [ "${#problems[@]}" -gt 0 ]; then VERIFY_HAD_ISSUES=1 if [ "${#missing[@]}" -gt 0 ]; then printf '\nMissing commands:\n' for command_name in "${missing[@]}"; do printf ' - %s\n' "$command_name" done fi if [ "${#problems[@]}" -gt 0 ]; then printf '\nConfiguration issues:\n' local problem for problem in "${problems[@]}"; do printf ' - %s\n' "$problem" done fi printf '\nTo fix the standard build environment, run ./configure.sh and choose 1) Normal.\n' printf 'For sqlite multi-arch builds, choose 2) With cross cgo.\n' if [ "$VERIFY_REPORT_ONLY" -eq 1 ]; then printf '\nVerify only completed; no changes were made.\n' return 0 fi return 1 fi printf 'Go: %s\n' "$(go version)" printf 'Node: %s\n' "$(node --version)" printf 'pnpm: %s\n' "$(pnpm --version)" printf 'gcc: %s\n' "$(gcc --version | head -n 1)" if repo_requires_git_lfs; then printf 'git-lfs: %s\n' "$(git lfs version)" fi if [ -d "$SCRIPT_DIR/node_modules" ]; then printf 'node_modules: present\n' else printf 'node_modules: missing; run ./configure.sh or pnpm install --frozen-lockfile\n' fi } if [ "$#" -eq 0 ] && [ -t 0 ]; then SHOW_MENU=1 fi while [ "$#" -gt 0 ]; do case "$1" in --with-cross-cgo) WITH_CROSS_CGO=1 ;; --skip-system) INSTALL_SYSTEM=0 ;; --skip-go) INSTALL_GO=0 ;; --skip-node) INSTALL_NODE=0 ;; --skip-frontend-deps) INSTALL_FRONTEND_DEPS=0 ;; --verify-only) VERIFY_ONLY=1 INSTALL_SYSTEM=0 INSTALL_GO=0 INSTALL_NODE=0 INSTALL_FRONTEND_DEPS=0 ;; --menu) SHOW_MENU=1 ;; -h | --help) usage exit 0 ;; *) usage die "Unknown option: $1" ;; esac shift done if [ "$SHOW_MENU" -eq 1 ]; then interactive_menu fi if [ "$(id -u)" -eq 0 ]; then warn "You are running as root. NVM/Node will be configured for root, not for your normal user." fi if [ "$VERIFY_ONLY" -eq 0 ]; then [ "$INSTALL_SYSTEM" -eq 0 ] || install_system_packages configure_git_lfs [ "$INSTALL_GO" -eq 0 ] || install_go [ "$INSTALL_NODE" -eq 0 ] || install_node_and_pnpm configure_cross_cgo [ "$INSTALL_FRONTEND_DEPS" -eq 0 ] || install_frontend_deps fi verify_environment if [ "$VERIFY_HAD_ISSUES" -eq 1 ]; then cat <