move_detect/deploy/bundle/build_offline_bundle.sh
유창욱 36b1f94571 fix(install): arch guard + CSI device access (P5-2, P5-5)
- P5-2 [H] build_offline_bundle.sh native branch hard-fails when uname -m != TARGET_ARCH
  (an x86 build was producing an aarch64-named tarball of x86_64 wheels).
- P5-5 [H] systemd unit adds DeviceAllow char-media + /dev/dma_heap for the CSI camera —
  picamera2/libcamera on Pi 5 needs them or the unit denies its own camera. OFF-HW: verify on Pi.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 09:38:18 +09:00

104 lines
4.9 KiB
Bash
Executable file

#!/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
# Guard the ARCH too: a native build on an x86_64 workstation would download x86_64 wheels
# while the tarball is still named/declared aarch64 (line 27/86/92) — a bundle that installs
# then fails to import on the Pi. numpy/opencv/onnxruntime are per-arch, so this is fatal.
HOST_ARCH="$(uname -m)"
if [ "$HOST_ARCH" != "$TARGET_ARCH" ]; then
echo "ERROR: native build host arch is $HOST_ARCH but TARGET_ARCH is $TARGET_ARCH." >&2
echo " Build natively ON an $TARGET_ARCH host (a Pi 5 / arm64 CI), or use --cross." >&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."