#!/usr/bin/env bash
# blender-extensions — fv-ml1's pinned Blender extension set (stacks/blender, README "Extensions").
#
#   scripts/blender-extensions sync     rebuild fv-ml1:/tank/blender-extensions/5.2/system from
#                                       stacks/blender/extensions.lock
#   scripts/blender-extensions status   what is installed, against the lock
#
# The set lives in Blender's System extension repository, which is a plain directory. The GUI
# container and `blender-run --extensions` both mount it READ-ONLY at /blender/5.2/extensions/system,
# and conf/scripts/startup/fleet_extensions.py enables everything in it. Agents never install
# anything: MCP safe mode blocks it, and the mount is read-only.
#
# sync:
#   1. downloads each pinned archive into /tank/blender-extensions/zips/ (kept as a cache) and checks
#      its sha256 against the lock. A mismatch stops everything.
#   2. installs each with Blender's own `--command extension install-file`, which validates the
#      manifest against this Blender and platform, into a staging directory.
#   3. pre-warms the staging copy: enables everything once while it is still WRITABLE, then
#      byte-compiles it. 3D-Print Toolbox writes a translation cache into its own package dir on
#      first import, and that fails on the read-only mount (found 2026-09-28).
#   4. checks that the staging copy enables READ-ONLY, then swaps it in and records the lock it was
#      built from as /tank/blender-extensions/5.2/installed.lock.
# ⚠ It refuses while the GUI container or any blender-run job is running. They hold the old
#   directory through a bind mount, and the swap would pull it out from under them.
set -euo pipefail

HOST=${BLENDER_SSH_HOST:-infra-ops@10.251.50.54}
HERE=$(cd "$(dirname "$0")" && pwd)
LOCK=$HERE/../stacks/blender/extensions.lock
HOOK=$HERE/../stacks/blender/conf/scripts/startup/fleet_extensions.py

case "${1:-}" in
  sync)
    rows=$(grep -vE '^\s*(#|$)' "$LOCK")
    echo "$rows" | awk 'NF != 4 || $3 !~ /^[0-9a-f]{64}$/ || $1 !~ /^[A-Za-z0-9_]+$/ { bad=1; print "bad lock row: " $0 > "/dev/stderr" } END { exit bad }'
    ssh -n -o BatchMode=yes "$HOST" "mkdir -p /tank/blender-extensions/zips /tank/blender-extensions/5.2"
    scp -q "$LOCK" "$HOST:/tank/blender-extensions/5.2/staging.lock"
    scp -q "$HOOK" "$HOST:/tank/blender-extensions/5.2/staging-hook.py"
    ssh -o BatchMode=yes "$HOST" bash -s <<'REMOTE'
set -euo pipefail
cd /tank/blender-extensions
if [ -n "$(docker ps -q --filter name='^blender$' --filter name='^blender-run-')" ]; then
  echo "blender-extensions: the GUI container or a blender-run job is running; stop it first (scripts/blender-mcp down)" >&2
  exit 3
fi
IMG=$(grep '^IMAGE=' /opt/docker/compose/blender/.env | cut -d= -f2)
# Fixed, literal staging path: it is emptied before every build.
rm -rf /tank/blender-extensions/5.2/staging
mkdir -p 5.2/staging/system
LOG=5.2/staging/sync.log
blender() {  # a throwaway Blender with the staging repo at $1 (rw|ro) and HOME in the container
  local mode=$1; shift
  docker run --rm --user 1002:1003 -e HOME=/tmp -e USER=infra-ops \
    -v /tank/blender-extensions/5.2/staging/system:/blender/5.2/extensions/system:"$mode" \
    -v /tank/blender-extensions/zips:/zips:ro \
    -v /tank/blender-extensions/5.2/staging-hook.py:/fleet/fleet_extensions.py:ro \
    --entrypoint /blender/blender "$IMG" "$@"
}
grep -vE '^\s*(#|$)' 5.2/staging.lock | while read -r id ver sha url; do
  zip=zips/$id-$ver.zip
  if ! echo "$sha  $zip" | sha256sum -c --status 2>/dev/null; then
    curl -fsSL --retry 3 -o "$zip.part" "$url"
    mv "$zip.part" "$zip"
  fi
  echo "$sha  $zip" | sha256sum -c --status || { echo "blender-extensions: sha256 MISMATCH for $id $ver; nothing installed" >&2; exit 4; }
  # install-file only targets user repos, so the staging dir is mounted as user_default for this step.
  docker run --rm --user 1002:1003 -e HOME=/tmp -e USER=infra-ops \
    -v /tank/blender-extensions/5.2/staging/system:/tmp/.config/blender/5.2/extensions/user_default \
    -v /tank/blender-extensions/zips:/zips:ro \
    --entrypoint /blender/blender "$IMG" --factory-startup \
    -c extension install-file -r user_default --no-prefs "/zips/$id-$ver.zip" >>"$LOG" 2>&1
  [ -f "5.2/staging/system/$id/blender_manifest.toml" ] || { echo "blender-extensions: install of $id failed; see $PWD/$LOG" >&2; exit 5; }
  echo "installed $id $ver"
done
# Pre-warm (writable), then byte-compile with Blender's own Python.
blender rw -b --factory-startup --python-exit-code 1 --python /fleet/fleet_extensions.py >>"$LOG" 2>&1 \
  || { echo "blender-extensions: pre-warm failed; see $PWD/$LOG" >&2; exit 6; }
docker run --rm --user 1002:1003 -v /tank/blender-extensions/5.2/staging/system:/s --entrypoint /blender/5.2/python/bin/python3.13 "$IMG" \
  -m compileall -q /s >>"$LOG" 2>&1 || true
# Must enable from the read-only mount before it goes live.
blender ro -b --factory-startup --python-exit-code 1 --python /fleet/fleet_extensions.py >>"$LOG" 2>&1 \
  || { echo "blender-extensions: read-only enable failed; see $PWD/$LOG" >&2; exit 7; }
grep 'fleet_extensions: enabled' "$LOG" | tail -1
# Swap in. Literal paths only.
rm -rf /tank/blender-extensions/5.2/system.prev
if [ -d /tank/blender-extensions/5.2/system ]; then mv /tank/blender-extensions/5.2/system /tank/blender-extensions/5.2/system.prev; fi
mv /tank/blender-extensions/5.2/staging/system /tank/blender-extensions/5.2/system
mv /tank/blender-extensions/5.2/staging.lock /tank/blender-extensions/5.2/installed.lock
mv /tank/blender-extensions/5.2/staging/sync.log /tank/blender-extensions/5.2/sync.log
rm -rf /tank/blender-extensions/5.2/system.prev /tank/blender-extensions/5.2/staging /tank/blender-extensions/5.2/staging-hook.py
echo "live: /tank/blender-extensions/5.2/system ($(du -sh /tank/blender-extensions/5.2/system | cut -f1))"
REMOTE
    "$HERE/ops-log" record --host fv-ml1 --action extensions-sync --target blender \
      --detail "rebuilt /tank/blender-extensions/5.2/system from extensions.lock ($(echo "$rows" | awk '{printf "%s %s, ", $1, $2}' | sed 's/, $//'))" ;;
  status)
    echo "lock (repo):"
    grep -vE '^\s*(#|$)' "$LOCK" | awk '{printf "  %-16s %s\n", $1, $2}'
    echo "installed on fv-ml1:"
    ssh -n -o BatchMode=yes "$HOST" 'for m in /tank/blender-extensions/5.2/system/*/blender_manifest.toml; do
        [ -f "$m" ] || { echo "  (nothing installed)"; break; }
        printf "  %-16s %s\n" "$(sed -n "s/^id = \"\(.*\)\"/\1/p" "$m")" "$(sed -n "s/^version = \"\(.*\)\"/\1/p" "$m")"
      done'
    if ssh -n -o BatchMode=yes "$HOST" cat /tank/blender-extensions/5.2/installed.lock 2>/dev/null | cmp -s - "$LOCK"; then
      echo "installed.lock matches the repo lock"
    else
      echo "installed.lock DIFFERS from the repo lock (or is missing): run '$0 sync'"
    fi ;;
  *)
    sed -n 2,6p "$0" >&2; exit 2 ;;
esac
