build.sh 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. #!/usr/bin/env bash
  2. #
  3. # packaging/build.sh — Top-level build orchestrator for smartbotic-vectorapi
  4. #
  5. # Supports two modes:
  6. # Default: Docker build targeting Debian 13 (production .debs)
  7. # --local: Native build on current system (development/testing).
  8. # .deb packaging only happens on a Debian-family host; elsewhere
  9. # (e.g. Void Linux) it compiles and tests only - packages for
  10. # release always come from the Docker/Debian-13 path.
  11. #
  12. set -euo pipefail
  13. # ---------------------------------------------------------------------------
  14. # Paths
  15. # ---------------------------------------------------------------------------
  16. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  17. PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
  18. BASE_IMAGE_NAME="smartbotic-vectorapi-build-base:debian13"
  19. # Client version the server deb is compiled against. Must equal the version
  20. # installed on the deploy target: every 2.x client shares one SONAME, so a
  21. # mismatch installs cleanly and only segfaults on the next service restart.
  22. DB_CLIENT_VERSION="${DB_CLIENT_VERSION:-2.11.3-1}"
  23. BUILD_JOBS="${BUILD_JOBS:-$(nproc)}"
  24. DEB_REPO_DIR="${DEB_REPO_DIR:-/data/dev/smartbotics/smartbotics-deb-repo}"
  25. # Output directories — separate to prevent local/Docker overwriting each other
  26. # dist/local/ — native build for current system
  27. # dist/debian13/ — Docker build for Debian 13 production
  28. DIST_BASE="${PROJECT_DIR}/dist"
  29. VERSION="$(cat "$PROJECT_DIR/VERSION")"
  30. GIT_COMMIT="$(git -C "$PROJECT_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown")"
  31. # ---------------------------------------------------------------------------
  32. # Defaults
  33. # ---------------------------------------------------------------------------
  34. LOCAL_MODE=0
  35. INSTALL_AFTER=0
  36. SKIP_TESTS=0
  37. DEB_REVISION=1
  38. REBUILD_BASE=0
  39. NO_CACHE=0
  40. DO_REPO=0
  41. SUITE=""
  42. DO_SYNC=0
  43. MAKE_DEBS=1
  44. # ---------------------------------------------------------------------------
  45. # Colored logging
  46. # ---------------------------------------------------------------------------
  47. log_info() { printf '\033[1;34m[INFO]\033[0m %s\n' "$*"; }
  48. log_success() { printf '\033[1;32m[OK]\033[0m %s\n' "$*"; }
  49. log_warn() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*"; }
  50. log_error() { printf '\033[1;31m[ERROR]\033[0m %s\n' "$*"; }
  51. # ---------------------------------------------------------------------------
  52. # Host detection
  53. # ---------------------------------------------------------------------------
  54. # .debs are only meaningful when produced on a Debian-family host: a package
  55. # linked against another distro's glibc/abseil/gRPC will not install (or will
  56. # crash-loop) on Debian 13. Everything we ship comes from the Docker path.
  57. host_is_debian() {
  58. [[ -r /etc/os-release ]] || return 1
  59. local ID="" ID_LIKE=""
  60. # shellcheck disable=SC1091
  61. . /etc/os-release
  62. [[ "$ID" == "debian" || "$ID" == "ubuntu" || "$ID_LIKE" == *debian* ]]
  63. }
  64. host_name() {
  65. if [[ -r /etc/os-release ]]; then
  66. ( . /etc/os-release; echo "${PRETTY_NAME:-${NAME:-unknown}}" )
  67. else
  68. echo "unknown"
  69. fi
  70. }
  71. # ---------------------------------------------------------------------------
  72. # Usage
  73. # ---------------------------------------------------------------------------
  74. usage() {
  75. cat <<EOF
  76. Usage: packaging/build.sh [OPTIONS]
  77. Build modes:
  78. (default) Docker build targeting Debian 13 (production)
  79. --local Native compile + tests for current system (development).
  80. Builds .debs only on a Debian-family host; on any other
  81. distro packaging is refused - use the Docker mode.
  82. Options:
  83. --install Install .debs locally after build (--local, Debian host only)
  84. --skip-tests Skip building and running the test suite (--local only)
  85. --deb-revision N Debian revision suffix (default: 1)
  86. --rebuild-base Force rebuild Docker base image
  87. --no-cache Build without Docker caches
  88. --repo Add packages to local APT repository
  89. --suite NAME Distribution suite (e.g., trixie)
  90. --sync Add to repo and upload to repository.smartbotics.ai
  91. Environment variables:
  92. BUILD_JOBS Parallel build jobs (default: nproc)
  93. DEB_REPO_DIR Path to smartbotics-deb-repo (default: /data/dev/smartbotics/smartbotics-deb-repo)
  94. DB_CLIENT_VERSION libsmartbotic-db-client version to build against; must match
  95. the deploy target (default: 2.11.3-1)
  96. EOF
  97. exit 0
  98. }
  99. # ---------------------------------------------------------------------------
  100. # Argument parsing
  101. # ---------------------------------------------------------------------------
  102. while [[ $# -gt 0 ]]; do
  103. case "$1" in
  104. --local) LOCAL_MODE=1; shift ;;
  105. --install) INSTALL_AFTER=1; shift ;;
  106. --skip-tests) SKIP_TESTS=1; shift ;;
  107. --deb-revision) DEB_REVISION="$2"; shift 2 ;;
  108. --rebuild-base) REBUILD_BASE=1; shift ;;
  109. --no-cache) NO_CACHE=1; shift ;;
  110. --repo) DO_REPO=1; shift ;;
  111. --suite) SUITE="$2"; shift 2 ;;
  112. --sync) DO_SYNC=1; shift ;;
  113. -h|--help) usage ;;
  114. *)
  115. log_error "Unknown option: $1"
  116. usage
  117. ;;
  118. esac
  119. done
  120. # Validate option combinations
  121. if [[ $INSTALL_AFTER -eq 1 && $LOCAL_MODE -eq 0 ]]; then
  122. log_error "--install is only supported with --local"
  123. exit 1
  124. fi
  125. if [[ $DO_SYNC -eq 1 && $DO_REPO -eq 0 ]]; then
  126. DO_REPO=1
  127. fi
  128. if [[ $DO_REPO -eq 1 && -z "$SUITE" ]]; then
  129. log_error "--repo requires --suite NAME"
  130. exit 1
  131. fi
  132. # Native packaging is only allowed on a Debian-family host. On anything else
  133. # (Void Linux on zeus, for example) --local compiles and tests, but must not
  134. # emit .debs: they would be linked against the wrong libc/abseil/gRPC.
  135. if [[ $LOCAL_MODE -eq 1 ]] && ! host_is_debian; then
  136. MAKE_DEBS=0
  137. log_warn "Non-Debian host detected ($(host_name)): --local will compile and test only."
  138. log_warn "Release packages must be built with the Docker mode: packaging/build.sh"
  139. if [[ $INSTALL_AFTER -eq 1 ]]; then
  140. log_error "--install needs .deb packages, which are not built on a non-Debian host."
  141. exit 1
  142. fi
  143. if [[ $DO_REPO -eq 1 ]]; then
  144. log_error "--repo/--sync need .deb packages; run the Docker mode (drop --local)."
  145. exit 1
  146. fi
  147. fi
  148. # ---------------------------------------------------------------------------
  149. # Set output directory based on mode
  150. # ---------------------------------------------------------------------------
  151. if [[ $LOCAL_MODE -eq 1 ]]; then
  152. OUTPUT_DIR="${DIST_BASE}/local"
  153. else
  154. OUTPUT_DIR="${DIST_BASE}/debian13"
  155. fi
  156. if [[ $MAKE_DEBS -eq 1 ]]; then
  157. mkdir -p "$OUTPUT_DIR"
  158. fi
  159. # ---------------------------------------------------------------------------
  160. # Banner
  161. # ---------------------------------------------------------------------------
  162. log_info "smartbotic-vectorapi build v${VERSION} (${GIT_COMMIT})"
  163. log_info "Build jobs: ${BUILD_JOBS}"
  164. if [[ $LOCAL_MODE -eq 1 ]]; then
  165. log_info "Mode: local (native, $(host_name))"
  166. else
  167. log_info "Mode: Docker (Debian 13)"
  168. fi
  169. if [[ $MAKE_DEBS -eq 1 ]]; then
  170. log_info "Output: ${OUTPUT_DIR}"
  171. else
  172. log_info "Output: ${PROJECT_DIR}/build (no packages on this host)"
  173. fi
  174. # ---------------------------------------------------------------------------
  175. # Local build
  176. # ---------------------------------------------------------------------------
  177. build_local() {
  178. local tests="ON"
  179. if [[ $SKIP_TESTS -eq 1 ]]; then
  180. tests="OFF"
  181. fi
  182. log_info "Configuring CMake (Release, BUILD_TESTS=${tests})..."
  183. cmake -B "$PROJECT_DIR/build" -G Ninja \
  184. -DCMAKE_BUILD_TYPE=Release \
  185. -DBUILD_WEBUI=ON \
  186. -DBUILD_TESTS="$tests" \
  187. -S "$PROJECT_DIR"
  188. log_info "Building with ${BUILD_JOBS} jobs..."
  189. cmake --build "$PROJECT_DIR/build" -j"$BUILD_JOBS"
  190. log_success "Build completed"
  191. if [[ $tests == "ON" ]]; then
  192. log_info "Running tests..."
  193. # Integration tests GTEST_SKIP() when smartbotic-database is unreachable.
  194. ctest --test-dir "$PROJECT_DIR/build" --output-on-failure
  195. log_success "Tests passed"
  196. fi
  197. if [[ $MAKE_DEBS -eq 0 ]]; then
  198. log_success "Native build verified: $PROJECT_DIR/build/src/smartbotic-vectorapi"
  199. return 0
  200. fi
  201. log_info "Creating .deb packages..."
  202. OUTPUT_DIR="$OUTPUT_DIR" BUILD_DEB_REVISION="$DEB_REVISION" "$PROJECT_DIR/packaging/deb/create-debs.sh"
  203. log_success "Packages created"
  204. if [[ $INSTALL_AFTER -eq 1 ]]; then
  205. log_info "Installing .deb packages..."
  206. sudo dpkg -i \
  207. "$OUTPUT_DIR"/smartbotic-vectorapi_*.deb \
  208. "$OUTPUT_DIR"/smartbotic-vectorapi-webui_*.deb \
  209. || sudo apt-get install -f -y
  210. log_success "Packages installed"
  211. fi
  212. }
  213. # ---------------------------------------------------------------------------
  214. # Docker build
  215. # ---------------------------------------------------------------------------
  216. build_docker() {
  217. if ! command -v docker &>/dev/null; then
  218. log_error "Docker is not installed or not in PATH"
  219. exit 1
  220. fi
  221. # Read repo credentials from the .env file (key names: RepoUser / RepoPass)
  222. local env_file="${DEB_REPO_DIR}/.env"
  223. if [[ ! -f "$env_file" ]]; then
  224. log_error "Repository .env not found: $env_file"
  225. exit 1
  226. fi
  227. local REPO_USER REPO_PASS
  228. REPO_USER="$(grep '^RepoUser:' "$env_file" | awk '{print $2}')"
  229. REPO_PASS="$(grep '^RepoPass:' "$env_file" | awk '{print $2}')"
  230. if [[ -z "$REPO_USER" || -z "$REPO_PASS" ]]; then
  231. log_error "Could not parse RepoUser/RepoPass from $env_file"
  232. exit 1
  233. fi
  234. # Check / build base image
  235. if [[ $REBUILD_BASE -eq 1 ]] || ! docker image inspect "$BASE_IMAGE_NAME" &>/dev/null; then
  236. if [[ $REBUILD_BASE -eq 1 ]]; then
  237. log_info "Rebuilding base image (--rebuild-base)..."
  238. else
  239. log_info "Base image not found, building ${BASE_IMAGE_NAME}..."
  240. fi
  241. # Publishing re-indexes the apt pool, so the pinned client version is
  242. # usually gone from the repo index. Vendor the .debs from the local
  243. # deb-repo staging dir when they are there; otherwise the image falls
  244. # back to installing that version from the repo.
  245. local vendor_dir="$PROJECT_DIR/packaging/vendor"
  246. rm -f "$vendor_dir"/*.deb
  247. local found=0
  248. for pkg in libsmartbotic-db-client libsmartbotic-db-client-dev; do
  249. local deb="$DEB_REPO_DIR/dist/${pkg}_${DB_CLIENT_VERSION}_amd64.deb"
  250. if [[ -f "$deb" ]]; then cp "$deb" "$vendor_dir/"; found=$((found + 1)); fi
  251. done
  252. if [[ $found -eq 2 ]]; then
  253. log_info "Pinning db client ${DB_CLIENT_VERSION} from ${DEB_REPO_DIR}/dist"
  254. else
  255. log_warn "db client ${DB_CLIENT_VERSION} not found locally; the image will try the apt repo"
  256. fi
  257. local base_cache_args=()
  258. if [[ $NO_CACHE -eq 1 ]]; then
  259. base_cache_args+=("--no-cache")
  260. fi
  261. docker buildx build \
  262. -f "$PROJECT_DIR/packaging/Dockerfile.base" \
  263. -t "$BASE_IMAGE_NAME" \
  264. --build-arg "REPO_USER=${REPO_USER}" \
  265. --build-arg "REPO_PASS=${REPO_PASS}" \
  266. --build-arg "DB_CLIENT_VERSION=${DB_CLIENT_VERSION}" \
  267. "${base_cache_args[@]+"${base_cache_args[@]}"}" \
  268. "$PROJECT_DIR"
  269. log_success "Base image built: ${BASE_IMAGE_NAME}"
  270. else
  271. log_info "Using existing base image: ${BASE_IMAGE_NAME}"
  272. fi
  273. # Build packages
  274. log_info "Building packages in Docker..."
  275. local build_cache_args=()
  276. if [[ $NO_CACHE -eq 1 ]]; then
  277. build_cache_args+=("--no-cache")
  278. fi
  279. docker buildx build \
  280. -f "$PROJECT_DIR/packaging/Dockerfile.build" \
  281. --build-arg "BASE_IMAGE=${BASE_IMAGE_NAME}" \
  282. --build-arg "BUILD_VERSION=${VERSION}" \
  283. --build-arg "BUILD_DEB_REVISION=${DEB_REVISION}" \
  284. --build-arg "BUILD_JOBS=${BUILD_JOBS}" \
  285. --build-arg "BUILD_GIT_COMMIT=${GIT_COMMIT}" \
  286. --target packages \
  287. --output "type=local,dest=$OUTPUT_DIR" \
  288. "${build_cache_args[@]+"${build_cache_args[@]}"}" \
  289. "$PROJECT_DIR"
  290. log_success "Docker build completed"
  291. }
  292. # ---------------------------------------------------------------------------
  293. # Main
  294. # ---------------------------------------------------------------------------
  295. if [[ $LOCAL_MODE -eq 1 ]]; then
  296. build_local
  297. else
  298. build_docker
  299. fi
  300. # ---------------------------------------------------------------------------
  301. # List created packages
  302. # ---------------------------------------------------------------------------
  303. if [[ $MAKE_DEBS -eq 1 ]]; then
  304. echo ""
  305. log_info "Created packages:"
  306. ls -lh "$OUTPUT_DIR"/*.deb 2>/dev/null || {
  307. log_error "No .deb files found in $OUTPUT_DIR"
  308. exit 1
  309. }
  310. fi
  311. # ---------------------------------------------------------------------------
  312. # Repository operations
  313. # ---------------------------------------------------------------------------
  314. if [[ $DO_REPO -eq 1 ]]; then
  315. echo ""
  316. log_info "Adding packages to repository (suite: ${SUITE})..."
  317. if [[ ! -x "$DEB_REPO_DIR/scripts/add-packages.sh" ]]; then
  318. log_error "Repository tool not found: $DEB_REPO_DIR/scripts/add-packages.sh"
  319. exit 1
  320. fi
  321. "$DEB_REPO_DIR/scripts/add-packages.sh" "$OUTPUT_DIR"
  322. log_success "Packages added to staging"
  323. "$DEB_REPO_DIR/scripts/create-repo.sh" --suite "$SUITE"
  324. log_success "Repository created for suite: ${SUITE}"
  325. if [[ $DO_SYNC -eq 1 ]]; then
  326. log_info "Syncing repository to repository.smartbotics.ai..."
  327. "$DEB_REPO_DIR/scripts/sync-repo.sh"
  328. log_success "Repository synced"
  329. fi
  330. fi
  331. # ---------------------------------------------------------------------------
  332. # Next steps
  333. # ---------------------------------------------------------------------------
  334. echo ""
  335. if [[ $MAKE_DEBS -eq 1 ]]; then
  336. log_success "Build complete! (v${VERSION}-${DEB_REVISION})"
  337. else
  338. log_success "Native build complete! (v${VERSION}, ${GIT_COMMIT})"
  339. fi
  340. echo ""
  341. if [[ $MAKE_DEBS -eq 0 ]]; then
  342. log_info "To run the server in place:"
  343. echo " SMARTBOTIC_VECTORAPI_KEY=dev ${PROJECT_DIR}/build/src/smartbotic-vectorapi \\"
  344. echo " --config ${PROJECT_DIR}/config/config.json \\"
  345. echo " --share-dir ${PROJECT_DIR}/api \\"
  346. echo " --webui-dir ${PROJECT_DIR}/build/webui/dist"
  347. echo ""
  348. fi
  349. if [[ $MAKE_DEBS -eq 1 && $LOCAL_MODE -eq 1 && $INSTALL_AFTER -eq 0 ]]; then
  350. log_info "To install locally:"
  351. echo " sudo dpkg -i ${OUTPUT_DIR}/*.deb || sudo apt-get install -f -y"
  352. echo ""
  353. fi
  354. if [[ $DO_REPO -eq 0 ]]; then
  355. log_info "To build release packages and publish:"
  356. echo " packaging/build.sh --repo --suite trixie"
  357. echo ""
  358. fi
  359. if [[ $DO_REPO -eq 1 && $DO_SYNC -eq 0 ]]; then
  360. log_info "To sync repository to remote:"
  361. echo " packaging/build.sh --repo --suite ${SUITE} --sync"
  362. echo ""
  363. fi