#!/usr/bin/env bash # Build a fully self-contained, offline-installable bundle for the move-detect # edge daemon (air-gapped / 폐쇄망 target — see rules/common/build-deployment.md). # # Run this on a BUILD machine that HAS internet. The resulting tarball carries # every runtime wheel + the pose model + the edge source + the systemd unit, so # the air-gapped Pi installs with ZERO network fetches (see install_offline.sh). # # RELIABILITY NOTE: onnxruntime and opencv publish per-architecture wheels. The # most reliable build is a NATIVE one — run this ON an aarch64 host (a Pi 5, or # an arm64 CI runner) so pip downloads wheels for the exact target. Cross-downloading # aarch64 wheels from an x86 box (--cross) is best-effort and can miss a wheel whose # tags don't match; if it fails, build natively. # # Usage: # deploy/bundle/build_offline_bundle.sh # native (host arch) # deploy/bundle/build_offline_bundle.sh --cross # cross to aarch64/cp311 # TARGET_ARCH=aarch64 PYVER=3.11 deploy/bundle/build_offline_bundle.sh --cross set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" OUT_DIR="${OUT_DIR:-$REPO_ROOT/dist}" STAGE="$OUT_DIR/move-detect-edge-bundle" WHEELHOUSE="$STAGE/wheelhouse" PYVER="${PYVER:-3.11}" # Pi OS Bookworm ships CPython 3.11 PYABI="cp${PYVER//./}" # cp311 TARGET_ARCH="${TARGET_ARCH:-aarch64}" RUNTIME_PKGS=(numpy requests onnxruntime opencv-python-headless) MODE="native" [ "${1:-}" = "--cross" ] && MODE="cross" echo ">> build mode : $MODE" echo ">> python target : $PYVER ($PYABI)" echo ">> target arch : $TARGET_ARCH" echo ">> output : $OUT_DIR" rm -rf "$STAGE" mkdir -p "$WHEELHOUSE" "$STAGE/src" "$STAGE/models" "$STAGE/deploy/systemd" "$STAGE/docs" echo ">> [1/5] downloading runtime wheels -> wheelhouse/" if [ "$MODE" = "cross" ]; then # Best-effort cross download. Try the common manylinux aarch64 tags. python3 -m pip download \ --only-binary=:all: \ --python-version "$PYVER" --implementation cp --abi "$PYABI" \ --platform "manylinux2014_${TARGET_ARCH}" \ --platform "manylinux_2_28_${TARGET_ARCH}" \ -d "$WHEELHOUSE" "${RUNTIME_PKGS[@]}" else # Native: wheels for THIS host's interpreter/arch (run on the Pi / arm64 CI). # Guard the ABI: numpy/onnxruntime ship version-specific cpXY wheels, so a build # host whose python3 != target PYVER produces a wheelhouse the Pi's cp311 venv # cannot install offline. Fail loudly instead of shipping a broken bundle. HOST_PYVER="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" if [ "$HOST_PYVER" != "$PYVER" ]; then echo "ERROR: native build host python3 is $HOST_PYVER but target PYVER is $PYVER." >&2 echo " Run with python$PYVER (e.g. 'PYVER=$HOST_PYVER ...' only if the Pi runs $HOST_PYVER)," >&2 echo " or use --cross to pin aarch64/$PYABI wheels explicitly." >&2 exit 1 fi python3 -m pip download -d "$WHEELHOUSE" "${RUNTIME_PKGS[@]}" fi echo ">> [2/5] fetching MoveNet ONNX model (build-machine only)" ( cd "$REPO_ROOT" && python3 -m edge.tools.fetch_movenet ) cp "$REPO_ROOT/models/movenet_lightning.onnx" "$STAGE/models/" echo ">> [3/5] staging edge source (package + pyproject)" cp -r "$REPO_ROOT/edge" "$STAGE/src/edge" cp "$REPO_ROOT/pyproject.toml" "$STAGE/src/" # do not ship compiled caches find "$STAGE/src" -name '__pycache__' -type d -prune -exec rm -rf {} + echo ">> [4/5] staging deploy assets + install script" cp "$REPO_ROOT/deploy/systemd/move-detect-edge.service" "$STAGE/deploy/systemd/" cp "$REPO_ROOT/deploy/systemd/move-detect-edge.env.example" "$STAGE/deploy/systemd/" cp "$REPO_ROOT/deploy/bundle/install_offline.sh" "$STAGE/" cp "$REPO_ROOT/docs/pilot-runbook.md" "$STAGE/docs/" 2>/dev/null || true chmod +x "$STAGE/install_offline.sh" { echo "bundle: move-detect-edge" echo "built_mode: $MODE" echo "python: $PYVER ($PYABI)" echo "target_arch: $TARGET_ARCH" echo "wheels:" ls -1 "$WHEELHOUSE" } > "$STAGE/MANIFEST.txt" echo ">> [5/5] packaging tarball" TARBALL="$OUT_DIR/move-detect-edge-bundle-${TARGET_ARCH}-${PYABI}.tar.gz" tar -C "$OUT_DIR" -czf "$TARBALL" "move-detect-edge-bundle" echo ">> DONE: $TARBALL" echo ">> transfer this single file across the air gap, then run install_offline.sh on the Pi."