Files
esh-pfi-infrastructure/scripts/blender-extensions
T
vh 83dc497b40 feat(blender): pinned extension set in a read-only System repo, for the GUI and blender-run --extensions
Blender is now a mandatory stage in draupnir's pipeline (Prime, 2026-09-28), and draupnir asked
for eight add-ons from extensions.blender.org: SurfacePsycho 0.10.4, CAD Sketcher 0.32.1,
3D-Print Toolbox 1.4.1, STEP Importer 1.2.1, Bool Tool 2.1.0, LoopTools 4.7.7, MeasureIt 1.8.4,
3MF Import/Export 2.7.7.

- stacks/blender/extensions.lock pins each by version and archive sha256.
- scripts/blender-extensions sync builds fv-ml1:/tank/blender-extensions/5.2/system with Blender's
  own install-file, pre-warms and byte-compiles it, checks a read-only enable, then swaps it in.
  It refuses while the GUI or a blender-run job holds the old directory.
- conf/scripts/startup/fleet_extensions.py enables every package in the System repo: in a timer
  in the GUI (after the prefs load), and as --python ahead of the caller's args in
  blender-run --extensions (a failed enable exits 1 before the caller's script).
- It also patches SurfacePsycho's sp_overwrite_segment_selection from eval() to literal_eval():
  the eval walked past MCP safe mode (control: unpatched ran code, patched refuses).
- blender-run: --extensions (bind mounts via --mount so a missing source fails instead of being
  created); USER/LOGNAME set, which CAD Sketcher's getpass needs.
- compose.yaml mounts the repo read-only and the hook into the GUI container. NOT yet deployed.
- scripts/blender-probes/extensions_acceptance.py: one operator run per add-on, safe-mode
  compliant. Headless 8/9 online and with --network none; CAD Sketcher sketching is GUI-only.
  A Python audit hook saw no network/process events (positive control fired).
2026-09-28 12:50:57 -07:00

111 lines
6.7 KiB
Bash
Executable File

#!/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