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).
111 lines
6.7 KiB
Bash
Executable File
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
|