build.sh 13 KB

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