build(uno-q): one-command cross-build + deploy helper for the board

Adds deploy/uno-q/build-deploy.sh: cross-compiles ZeroClaw for the Uno Q
(aarch64) on macOS via cargo-zigbuild and deploys it to the board over
adb as a proper drop-in for /home/arduino/zeroclaw.

Bakes in the two things that bit us:
- `--features hardware` is MANDATORY, else the build silently drops the
  10 Uno Q peripheral tools (GPIO bridge, flash, sysfs_led, camera,
  network, i2cdetect). The script fails fast if the artifact lacks them.
- each adb step runs with `</dev/null` so the shell returns (a trailing
  `setsid … &` otherwise hangs the adb call).

Flow: build (hardware, musl static) → verify tools present → push +
sha-check → stop supervisor + kill daemon → swap (keeps .prev.bak) →
relaunch supervisor → poll :8080/health. Also --build-only and
--rollback. Verified: the hardware cross-build produces a static aarch64
binary with the peripheral+flash tools and the streaming-fallback fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-05 08:41:09 -07:00
co-authored by Claude Opus 4.8
parent 42c4225425
commit 5d53149da2
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# build-deploy.sh — cross-build ZeroClaw for the Arduino Uno Q (aarch64) on this
# Mac and deploy it to the board over adb, as a proper drop-in replacement for
# /home/arduino/zeroclaw. One command for the "make a fix → get it on the board"
# loop.
#
# WHY THE FLAGS (learned the hard way — see memory zeroclaw-streaming-vs-nonstreaming-fallback):
# --features hardware MANDATORY. Without it the build silently drops the 10
# Uno Q peripheral tools (GPIO bridge, FLASH, sysfs_led,
# camera, network, i2cdetect). Symptom: agent load log
# shows "before:48" tools instead of "before:58".
# musl target the deployed binary is static (glibc-independent).
# cargo-zigbuild cross-links from macOS via zig (no cross toolchain).
#
# PREREQS (already set up on this Mac): rustup targets aarch64-unknown-linux-musl,
# cargo-zigbuild + zig on PATH, adb, the board on USB.
#
# Usage:
# deploy/uno-q/build-deploy.sh # build + deploy + verify
# ZC_REPO=/path/to/zeroclaw deploy/uno-q/build-deploy.sh
# SERIAL=65301572 deploy/uno-q/build-deploy.sh
# deploy/uno-q/build-deploy.sh --build-only # build + verify, no deploy
# deploy/uno-q/build-deploy.sh --rollback # restore the previous binary from backup
set -euo pipefail
ZC_REPO="${ZC_REPO:-$HOME/projects/zeroclaw}"
SERIAL="${SERIAL:-65301572}"
TARGET="aarch64-unknown-linux-musl"
BIN="$ZC_REPO/target/$TARGET/release/zeroclaw"
REMOTE="/home/arduino/zeroclaw"
SUPERVISOR="/home/arduino/zeroclaw-supervisor.sh"
adbsh() { adb -s "$SERIAL" shell "$1" </dev/null; } # </dev/null so adb always returns (avoids the setsid-hang)
require_device() {
[ "$(adb -s "$SERIAL" get-state 2>/dev/null)" = "device" ] || { echo "✗ board $SERIAL not connected (adb get-state != device)"; exit 1; }
}
rollback() {
require_device
echo "→ rolling back to $REMOTE.prev.bak"
adbsh "test -f $REMOTE.prev.bak" || { echo "✗ no backup at $REMOTE.prev.bak"; exit 1; }
adbsh "pkill -f '[z]eroclaw-supervisor'; sleep 1"
adbsh "pkill -9 -f 'zeroclaw daemon'; sleep 1"
adbsh "mv -f $REMOTE $REMOTE.rolledback.bak; mv -f $REMOTE.prev.bak $REMOTE; chmod +x $REMOTE"
adbsh "setsid sh -c 'exec $SUPERVISOR' >/home/arduino/zeroclaw-supervisor.log 2>&1 </dev/null &"
echo "→ rolled back; supervisor relaunched. sha:$(adbsh "sha256sum $REMOTE | cut -c1-16")"
exit 0
}
[ "${1:-}" = "--rollback" ] && rollback
echo "=== 1/4 cross-build (aarch64 musl, --features hardware) ==="
( cd "$ZC_REPO" && cargo zigbuild --target "$TARGET" --release --locked --features hardware --bin zeroclaw )
echo "=== 2/4 verify artifact ==="
file "$BIN" | grep -q "aarch64" || { echo "✗ not an aarch64 binary"; exit 1; }
strings "$BIN" | grep -qm1 "Uno Q tools added" || { echo "✗ peripheral tools MISSING — did the hardware feature build?"; exit 1; }
strings "$BIN" | grep -qm1 "uno_q_flash" || { echo "✗ flash tool missing"; exit 1; }
echo "✓ $(ls -la "$BIN" | awk '{print $5}') bytes, peripheral+flash tools present, sha:$(shasum -a256 "$BIN" | cut -c1-16)"
[ "${1:-}" = "--build-only" ] && { echo "build-only: done (artifact at $BIN)"; exit 0; }
require_device
echo "=== 3/4 push + swap (backup kept at $REMOTE.prev.bak) ==="
adb -s "$SERIAL" push "$BIN" "$REMOTE.new"
L=$(shasum -a256 "$BIN" | cut -d' ' -f1); R=$(adbsh "sha256sum $REMOTE.new | cut -d' ' -f1")
[ "$L" = "$R" ] || { echo "✗ sha mismatch after push ($L != $R)"; exit 1; }
adbsh "pkill -f '[z]eroclaw-supervisor'; sleep 1"
adbsh "pkill -9 -f 'zeroclaw daemon'; sleep 1"
adbsh "cp -f $REMOTE $REMOTE.prev.bak; mv -f $REMOTE.new $REMOTE; chmod +x $REMOTE"
adbsh "setsid sh -c 'exec $SUPERVISOR' >/home/arduino/zeroclaw-supervisor.log 2>&1 </dev/null &"
echo "=== 4/4 wait for daemon health (:8080 via host forward) ==="
adb -s "$SERIAL" forward tcp:8080 tcp:8080 >/dev/null 2>&1 || true
for i in $(seq 1 30); do
[ "$(curl -s -m 3 -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/health 2>/dev/null)" = "200" ] && { echo "✓ daemon healthy on new binary (sha:$(adbsh "sha256sum $REMOTE | cut -c1-16"))"; exit 0; }
sleep 3
done
echo "✗ daemon did not report healthy in time — check the board; roll back with: $0 --rollback"
exit 1