#!/usr/bin/env bash # # packaging/build.sh — Top-level build orchestrator for smartbotic-vectorapi # # Supports two modes: # Default: Docker build targeting Debian 13 (production .debs) # --local: Native build on current system (development/testing). # .deb packaging only happens on a Debian-family host; elsewhere # (e.g. Void Linux) it compiles and tests only - packages for # release always come from the Docker/Debian-13 path. # set -euo pipefail # --------------------------------------------------------------------------- # Paths # --------------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" BASE_IMAGE_NAME="smartbotic-vectorapi-build-base:debian13" # Client version the server deb is compiled against. Must equal the version # installed on the deploy target: every 2.x client shares one SONAME, so a # mismatch installs cleanly and only segfaults on the next service restart. DB_CLIENT_VERSION="${DB_CLIENT_VERSION:-2.11.3-1}" BUILD_JOBS="${BUILD_JOBS:-$(nproc)}" DEB_REPO_DIR="${DEB_REPO_DIR:-/data/dev/smartbotics/smartbotics-deb-repo}" # Output directories — separate to prevent local/Docker overwriting each other # dist/local/ — native build for current system # dist/debian13/ — Docker build for Debian 13 production DIST_BASE="${PROJECT_DIR}/dist" VERSION="$(cat "$PROJECT_DIR/VERSION")" GIT_COMMIT="$(git -C "$PROJECT_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown")" # --------------------------------------------------------------------------- # Defaults # --------------------------------------------------------------------------- LOCAL_MODE=0 INSTALL_AFTER=0 SKIP_TESTS=0 DEB_REVISION=1 REBUILD_BASE=0 NO_CACHE=0 DO_REPO=0 SUITE="" DO_SYNC=0 MAKE_DEBS=1 # --------------------------------------------------------------------------- # Colored logging # --------------------------------------------------------------------------- log_info() { printf '\033[1;34m[INFO]\033[0m %s\n' "$*"; } log_success() { printf '\033[1;32m[OK]\033[0m %s\n' "$*"; } log_warn() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*"; } log_error() { printf '\033[1;31m[ERROR]\033[0m %s\n' "$*"; } # --------------------------------------------------------------------------- # Host detection # --------------------------------------------------------------------------- # .debs are only meaningful when produced on a Debian-family host: a package # linked against another distro's glibc/abseil/gRPC will not install (or will # crash-loop) on Debian 13. Everything we ship comes from the Docker path. host_is_debian() { [[ -r /etc/os-release ]] || return 1 local ID="" ID_LIKE="" # shellcheck disable=SC1091 . /etc/os-release [[ "$ID" == "debian" || "$ID" == "ubuntu" || "$ID_LIKE" == *debian* ]] } host_name() { if [[ -r /etc/os-release ]]; then ( . /etc/os-release; echo "${PRETTY_NAME:-${NAME:-unknown}}" ) else echo "unknown" fi } # --------------------------------------------------------------------------- # Usage # --------------------------------------------------------------------------- usage() { cat </dev/null; then log_error "Docker is not installed or not in PATH" exit 1 fi # Read repo credentials from the .env file (key names: RepoUser / RepoPass) local env_file="${DEB_REPO_DIR}/.env" if [[ ! -f "$env_file" ]]; then log_error "Repository .env not found: $env_file" exit 1 fi local REPO_USER REPO_PASS REPO_USER="$(grep '^RepoUser:' "$env_file" | awk '{print $2}')" REPO_PASS="$(grep '^RepoPass:' "$env_file" | awk '{print $2}')" if [[ -z "$REPO_USER" || -z "$REPO_PASS" ]]; then log_error "Could not parse RepoUser/RepoPass from $env_file" exit 1 fi # Check / build base image if [[ $REBUILD_BASE -eq 1 ]] || ! docker image inspect "$BASE_IMAGE_NAME" &>/dev/null; then if [[ $REBUILD_BASE -eq 1 ]]; then log_info "Rebuilding base image (--rebuild-base)..." else log_info "Base image not found, building ${BASE_IMAGE_NAME}..." fi # Publishing re-indexes the apt pool, so the pinned client version is # usually gone from the repo index. Vendor the .debs from the local # deb-repo staging dir when they are there; otherwise the image falls # back to installing that version from the repo. local vendor_dir="$PROJECT_DIR/packaging/vendor" rm -f "$vendor_dir"/*.deb local found=0 for pkg in libsmartbotic-db-client libsmartbotic-db-client-dev; do local deb="$DEB_REPO_DIR/dist/${pkg}_${DB_CLIENT_VERSION}_amd64.deb" if [[ -f "$deb" ]]; then cp "$deb" "$vendor_dir/"; found=$((found + 1)); fi done if [[ $found -eq 2 ]]; then log_info "Pinning db client ${DB_CLIENT_VERSION} from ${DEB_REPO_DIR}/dist" else log_warn "db client ${DB_CLIENT_VERSION} not found locally; the image will try the apt repo" fi local base_cache_args=() if [[ $NO_CACHE -eq 1 ]]; then base_cache_args+=("--no-cache") fi docker buildx build \ -f "$PROJECT_DIR/packaging/Dockerfile.base" \ -t "$BASE_IMAGE_NAME" \ --build-arg "REPO_USER=${REPO_USER}" \ --build-arg "REPO_PASS=${REPO_PASS}" \ --build-arg "DB_CLIENT_VERSION=${DB_CLIENT_VERSION}" \ "${base_cache_args[@]+"${base_cache_args[@]}"}" \ "$PROJECT_DIR" log_success "Base image built: ${BASE_IMAGE_NAME}" else log_info "Using existing base image: ${BASE_IMAGE_NAME}" fi # Build packages log_info "Building packages in Docker..." local build_cache_args=() if [[ $NO_CACHE -eq 1 ]]; then build_cache_args+=("--no-cache") fi docker buildx build \ -f "$PROJECT_DIR/packaging/Dockerfile.build" \ --build-arg "BASE_IMAGE=${BASE_IMAGE_NAME}" \ --build-arg "BUILD_VERSION=${VERSION}" \ --build-arg "BUILD_DEB_REVISION=${DEB_REVISION}" \ --build-arg "BUILD_JOBS=${BUILD_JOBS}" \ --build-arg "BUILD_GIT_COMMIT=${GIT_COMMIT}" \ --target packages \ --output "type=local,dest=$OUTPUT_DIR" \ "${build_cache_args[@]+"${build_cache_args[@]}"}" \ "$PROJECT_DIR" log_success "Docker build completed" } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- if [[ $LOCAL_MODE -eq 1 ]]; then build_local else build_docker fi # --------------------------------------------------------------------------- # List created packages # --------------------------------------------------------------------------- if [[ $MAKE_DEBS -eq 1 ]]; then echo "" log_info "Created packages:" ls -lh "$OUTPUT_DIR"/*.deb 2>/dev/null || { log_error "No .deb files found in $OUTPUT_DIR" exit 1 } fi # --------------------------------------------------------------------------- # Repository operations # --------------------------------------------------------------------------- if [[ $DO_REPO -eq 1 ]]; then echo "" log_info "Adding packages to repository (suite: ${SUITE})..." if [[ ! -x "$DEB_REPO_DIR/scripts/add-packages.sh" ]]; then log_error "Repository tool not found: $DEB_REPO_DIR/scripts/add-packages.sh" exit 1 fi "$DEB_REPO_DIR/scripts/add-packages.sh" "$OUTPUT_DIR" log_success "Packages added to staging" "$DEB_REPO_DIR/scripts/create-repo.sh" --suite "$SUITE" log_success "Repository created for suite: ${SUITE}" if [[ $DO_SYNC -eq 1 ]]; then log_info "Syncing repository to repository.smartbotics.ai..." "$DEB_REPO_DIR/scripts/sync-repo.sh" log_success "Repository synced" fi fi # --------------------------------------------------------------------------- # Next steps # --------------------------------------------------------------------------- echo "" if [[ $MAKE_DEBS -eq 1 ]]; then log_success "Build complete! (v${VERSION}-${DEB_REVISION})" else log_success "Native build complete! (v${VERSION}, ${GIT_COMMIT})" fi echo "" if [[ $MAKE_DEBS -eq 0 ]]; then log_info "To run the server in place:" echo " SMARTBOTIC_VECTORAPI_KEY=dev ${PROJECT_DIR}/build/src/smartbotic-vectorapi \\" echo " --config ${PROJECT_DIR}/config/config.json \\" echo " --share-dir ${PROJECT_DIR}/api \\" echo " --webui-dir ${PROJECT_DIR}/build/webui/dist" echo "" fi if [[ $MAKE_DEBS -eq 1 && $LOCAL_MODE -eq 1 && $INSTALL_AFTER -eq 0 ]]; then log_info "To install locally:" echo " sudo dpkg -i ${OUTPUT_DIR}/*.deb || sudo apt-get install -f -y" echo "" fi if [[ $DO_REPO -eq 0 ]]; then log_info "To build release packages and publish:" echo " packaging/build.sh --repo --suite trixie" echo "" fi if [[ $DO_REPO -eq 1 && $DO_SYNC -eq 0 ]]; then log_info "To sync repository to remote:" echo " packaging/build.sh --repo --suite ${SUITE} --sync" echo "" fi