-
-
Save B-Ricey763/43800e789c340b7803d2bfb8d0e4b9ce to your computer and use it in GitHub Desktop.
vk - camera firmware build/deploy helper script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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