Skip to content

Instantly share code, notes, and snippets.

@B-Ricey763
Created March 13, 2026 18:20
Show Gist options
  • Select an option

  • Save B-Ricey763/43800e789c340b7803d2bfb8d0e4b9ce to your computer and use it in GitHub Desktop.

Select an option

Save B-Ricey763/43800e789c340b7803d2bfb8d0e4b9ce to your computer and use it in GitHub Desktop.
vk - camera firmware build/deploy helper script
#!/usr/bin/env bash
set -euo pipefail
# --- Config ---
declare -A VK_BINARY_MAP=()
declare -A VK_SOURCE_MAP=()
find_repo_root() {
git rev-parse --show-toplevel 2>/dev/null
}
load_config() {
# Save any env overrides before sourcing files
local env_machine="${VK_MACHINE:-}"
local env_camera="${VK_CAMERA:-}"
# Global defaults first
[[ -f "$HOME/scripts/vk.conf" ]] && source "$HOME/scripts/vk.conf" || true
# Per-repo overrides
VK_REPO=$(find_repo_root) || { echo "error: not in a git repo"; exit 1; }
[[ -f "$VK_REPO/.vk.conf" ]] && source "$VK_REPO/.vk.conf" || true
# Env overrides win
[[ -n "$env_machine" ]] && VK_MACHINE="$env_machine"
[[ -n "$env_camera" ]] && VK_CAMERA="$env_camera"
return 0
}
ensure_config() {
load_config
if [[ -z "${VK_MACHINE:-}" ]]; then
echo "error: VK_MACHINE not set. Run 'vk init' or set VK_MACHINE."
exit 1
fi
VK_CAMERA="${VK_CAMERA:-${VK_MACHINE%%-*}}"
CACHE_DIR="$HOME/.cache/vk-build/$(echo "$VK_REPO" | md5sum | cut -c1-12)"
mkdir -p "$CACHE_DIR"
}
# --- Helpers ---
notify() {
printf '\033Ptmux;\033\033]9;%s\033\\\033\\' "$1" > /dev/tty 2>/dev/null || true
}
ensure_ssh_agent() {
[[ -f "$HOME/.keychain/off-the-wall-sh" ]] && source "$HOME/.keychain/off-the-wall-sh" || true
}
check_cf_auth() {
# Pre-trigger cloudflare tunnel auth so it doesn't silently hang during build.
# The build script starts cloudflared in the background where the auth prompt
# gets lost. By doing it here first, the prompt shows in the foreground.
command -v cloudflared >/dev/null 2>&1 || return 0
local port=19876 # ephemeral port, just for the auth check
local hostname="athens.prod1.cf.verkada.com"
echo "Checking Cloudflare tunnel auth..."
cloudflared access tcp --hostname "$hostname" --url "127.0.0.1:$port" &
local cf_pid=$!
sleep 2
# Trigger auth — this is what causes the browser prompt if needed
curl -k -I -s -L --max-time 10 "https://localhost.verkada.com:$port/" >/dev/null 2>&1 || true
kill "$cf_pid" 2>/dev/null
wait "$cf_pid" 2>/dev/null || true
}
check_aws_creds() {
if ! aws sts get-caller-identity --profile verkada-camerafw >/dev/null 2>&1; then
echo "AWS credentials expired. Running build_logins..."
if command -v build_logins >/dev/null 2>&1; then
build_logins
else
echo "error: AWS creds expired and build_logins not found. Log in manually."
exit 1
fi
fi
}
machine_underscored() {
echo "${VK_MACHINE}" | tr '-' '_'
}
get_source_state() {
{ git -C "$VK_REPO" rev-parse HEAD
git -C "$VK_REPO" diff
git -C "$VK_REPO" diff --cached
} | md5sum | cut -c1-32
}
get_recipe_state() {
local paths="$1"
{ git -C "$VK_REPO" rev-parse HEAD
git -C "$VK_REPO" diff -- $paths
git -C "$VK_REPO" diff --cached -- $paths
} | md5sum | cut -c1-32
}
get_recipe_source_paths() {
local recipe="$1"
if [[ -n "${VK_SOURCE_MAP[$recipe]:-}" ]]; then
echo "${VK_SOURCE_MAP[$recipe]}"
else
echo "verkada/layers/meta-verkada/meta-camera/recipes-verkada/$recipe/"
fi
}
needs_rebuild_full() {
local saved="$CACHE_DIR/last-build-state"
[[ ! -f "$saved" ]] && return 0
local current
current=$(get_source_state)
[[ "$current" != "$(cat "$saved")" ]]
}
needs_rebuild_recipe() {
local recipe="$1"
local saved="$CACHE_DIR/last-build-recipe-${recipe}"
[[ ! -f "$saved" ]] && return 0
local paths current
paths=$(get_recipe_source_paths "$recipe")
current=$(get_recipe_state "$paths")
[[ "$current" != "$(cat "$saved")" ]]
}
find_binary() {
local recipe="$1"
local machine_us
machine_us=$(machine_underscored)
local binary_name="${VK_BINARY_MAP[$recipe]:-$recipe}"
local work_dir="$VK_REPO/build/tmp-glibc/work/${machine_us}-vlnx-linux/${recipe}"
# Glob for version dir (usually 1.0-r0 or 1.0.0-r0)
local version_dir
version_dir=$(ls -d "$work_dir"/*/ 2>/dev/null | head -1)
if [[ -z "$version_dir" ]]; then
echo "error: no build output found for $recipe at $work_dir" >&2
return 1
fi
local binary="$version_dir$binary_name"
if [[ ! -f "$binary" ]]; then
echo "error: binary not found at $binary" >&2
return 1
fi
echo "$binary"
}
extract_version() {
local log="$1"
grep "new version ready:" "$log" | tail -1 | sed 's/.*new version ready: //; s/\x1b\[[0-9;]*m//g' | tr -d ' \n\r'
}
analyze_failure() {
local log="$1"
echo ""
echo "--- Error Summary ---"
grep -iE "error:|ERROR |fatal|FAILED|: undefined reference|No such file" "$log" | tail -20 || true
echo ""
echo "Tip: use /analyze-build-failure in Claude Code for detailed analysis"
}
# --- Commands ---
vk_build_full() {
ensure_config
check_aws_creds
check_cf_auth
local log="$CACHE_DIR/build.log"
echo "Building $VK_MACHINE (full firmware)..."
notify "vk: building $VK_MACHINE..."
if cd "$VK_REPO" && ./scripts/build "$VK_MACHINE" --with-ssh --build-dir build/ 2>&1 | tee "$log"; then
local version
version=$(extract_version "$log")
if [[ -z "$version" ]]; then
echo "warning: could not extract version from build log"
else
echo "$version" > "$CACHE_DIR/last-build-version"
echo ""
echo "Version: $version"
fi
get_source_state > "$CACHE_DIR/last-build-state"
notify "vk: build OK ${version:-}"
echo "Build succeeded."
else
notify "vk: BUILD FAILED"
analyze_failure "$log"
exit 1
fi
}
vk_build_recipe() {
local recipe="$1"
local clean="${2:-}"
ensure_config
check_cf_auth
local log="$CACHE_DIR/build-${recipe}.log"
if [[ "$clean" == "clean" ]]; then
echo "Cleaning $recipe..."
cd "$VK_REPO" && ./scripts/build "$VK_MACHINE" --build-dir build/ -- "$recipe" -c cleansstate -f 2>&1 | tee "$log"
fi
echo "Building $recipe for $VK_MACHINE..."
notify "vk: building $recipe..."
if cd "$VK_REPO" && ./scripts/build "$VK_MACHINE" --build-dir build/ -- "$recipe" -c compile -f 2>&1 | tee "$log"; then
local paths
paths=$(get_recipe_source_paths "$recipe")
get_recipe_state "$paths" > "$CACHE_DIR/last-build-recipe-${recipe}"
notify "vk: $recipe built"
local binary
if binary=$(find_binary "$recipe"); then
echo ""
echo "Binary: $binary"
echo "Deploy: vk deploy $recipe"
fi
echo "Build succeeded."
else
notify "vk: $recipe FAILED"
analyze_failure "$log"
exit 1
fi
}
vk_deploy_full() {
ensure_config
# Build if needed
if needs_rebuild_full; then
echo "Changes detected, rebuilding..."
vk_build_full
else
echo "No changes since last build, skipping rebuild."
fi
local version
if [[ -f "$CACHE_DIR/last-build-version" ]]; then
version=$(cat "$CACHE_DIR/last-build-version")
else
echo "error: no build version found. Run 'vk build' first."
exit 1
fi
echo "Deploying $version to $VK_CAMERA..."
notify "vk: deploying to $VK_CAMERA..."
ensure_ssh_agent
ssh "$VK_CAMERA" "verkada_upgrade_auto $version && reboot"
notify "vk: deployed to $VK_CAMERA"
echo "Deployment complete. Camera will reboot (~3 min)."
echo "Verify: ssh $VK_CAMERA 'cat /etc/verkada_version'"
}
vk_deploy_recipe() {
local recipe="$1"
ensure_config
# Build if needed
if needs_rebuild_recipe "$recipe"; then
echo "Changes detected in $recipe, rebuilding..."
vk_build_recipe "$recipe"
else
echo "No changes since last build of $recipe, skipping rebuild."
fi
local binary
binary=$(find_binary "$recipe") || exit 1
local binary_name
binary_name=$(basename "$binary")
echo "Deploying $recipe to $VK_CAMERA..."
notify "vk: deploying $recipe to $VK_CAMERA..."
ensure_ssh_agent
ssh "$VK_CAMERA" "killall -9 $binary_name 2>/dev/null; true"
scp -O "$binary" "$VK_CAMERA:/mnt/approot-data/$binary_name"
ssh "$VK_CAMERA" "ls -lh /mnt/approot-data/$binary_name"
notify "vk: $recipe deployed to $VK_CAMERA"
echo "Deployed. Run on camera:"
echo " ssh $VK_CAMERA 'sv stop $recipe && /mnt/approot-data/$binary_name'"
}
vk_push() {
local file="${1:?Usage: vk push <file> [dest]}"
local dest="${2:-/mnt/approot-data}"
ensure_config
ensure_ssh_agent
echo "Pushing $file to $VK_CAMERA:$dest..."
scp -O "$file" "$VK_CAMERA:$dest"
echo "Done."
}
vk_status() {
load_config
echo "=== vk status ==="
echo "Repo: ${VK_REPO:-<not in repo>}"
echo "Machine: ${VK_MACHINE:-<not set>}"
echo "Camera: ${VK_CAMERA:-${VK_MACHINE:+${VK_MACHINE%%-*}}}"
if [[ -n "${VK_MACHINE:-}" ]]; then
VK_CAMERA="${VK_CAMERA:-${VK_MACHINE%%-*}}"
CACHE_DIR="$HOME/.cache/vk-build/$(echo "$VK_REPO" | md5sum | cut -c1-12)"
echo ""
if [[ -f "$CACHE_DIR/last-build-version" ]]; then
echo "Last build version: $(cat "$CACHE_DIR/last-build-version")"
else
echo "Last build version: (never built)"
fi
if [[ -f "$CACHE_DIR/last-build-state" ]]; then
if needs_rebuild_full; then
echo "Full build: STALE (changes detected)"
else
echo "Full build: UP TO DATE"
fi
else
echo "Full build: (never built)"
fi
# Show per-recipe states
for f in "$CACHE_DIR"/last-build-recipe-*; do
[[ -f "$f" ]] || continue
local recipe="${f##*last-build-recipe-}"
if needs_rebuild_recipe "$recipe"; then
echo "Recipe $recipe: STALE"
else
echo "Recipe $recipe: UP TO DATE"
fi
done
fi
if [[ -f "$VK_REPO/.vk.conf" ]]; then
echo ""
echo "Config: $VK_REPO/.vk.conf"
else
echo ""
echo "Config: none (run 'vk init')"
fi
}
vk_init() {
local repo_root
repo_root=$(find_repo_root) || { echo "error: not in a git repo"; exit 1; }
local conf="$repo_root/.vk.conf"
if [[ -f "$conf" ]]; then
echo "$conf already exists:"
cat "$conf"
echo ""
read -rp "Overwrite? [y/N] " yn
[[ "$yn" == [yY]* ]] || { echo "Aborted."; exit 0; }
fi
local machine camera
read -rp "Machine [nemo-4k-secure]: " machine
machine="${machine:-nemo-4k-secure}"
read -rp "Camera [${machine%%-*}]: " camera
camera="${camera:-${machine%%-*}}"
cat > "$conf" <<EOF
VK_MACHINE="$machine"
VK_CAMERA="$camera"
EOF
echo "Created $conf"
cat "$conf"
# Add to local git exclude (doesn't modify tracked files)
local exclude="$repo_root/.git/info/exclude"
if [[ -f "$exclude" ]] && ! grep -qxF '.vk.conf' "$exclude"; then
echo '.vk.conf' >> "$exclude"
echo "Added .vk.conf to .git/info/exclude"
fi
}
vk_usage() {
cat <<'EOF'
vk — camera firmware build & deploy tool
Usage:
vk build Build full firmware (--with-ssh)
vk build <recipe> Build single recipe (-c compile -f)
vk build -c <recipe> Clean recipe state, then rebuild
vk deploy Build-if-changed + deploy full firmware
vk deploy <recipe> Build-if-changed + deploy single recipe binary
vk push <file> [dest] SCP file to camera (default: /mnt/approot-data)
vk status Show config, build state, change detection
vk init Create .vk.conf in current repo
Environment overrides:
VK_MACHINE=omaha-4k-secure vk build
VK_CAMERA=omaha vk deploy
EOF
}
# --- Main ---
cmd="${1:-}"
shift || true
case "$cmd" in
build)
clean=""
recipe=""
while [[ $# -gt 0 ]]; do
case "$1" in
--clean|-c) clean="clean"; shift ;;
*) recipe="$1"; shift ;;
esac
done
if [[ -n "$recipe" ]]; then
vk_build_recipe "$recipe" "$clean"
elif [[ -n "$clean" ]]; then
echo "error: -c requires a recipe name (e.g., vk build -c dory-controller)"
exit 1
else
vk_build_full
fi
;;
deploy)
if [[ -n "${1:-}" ]]; then
vk_deploy_recipe "$1"
else
vk_deploy_full
fi
;;
push)
vk_push "$@"
;;
status)
vk_status
;;
init)
vk_init
;;
help|--help|-h)
vk_usage
;;
"")
vk_usage
;;
*)
echo "error: unknown command '$cmd'"
echo ""
vk_usage
exit 1
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment