bpy-default.sh and bpy-dev.sh were saved 'pixi shell-hook' output from another host, with PIXI_EXE and PATH pointing into a personal home that doesn't exist on the consoles. bpy-env.sh now runs the hook at runtime (--as-is, ~0.2 s) for its shared-env fallback instead of sourcing bpy-default.sh; nothing else used either file. Scripts that call pixi now prefer the system /usr/bin/pixi and fall back to /sf/bernina/bin/pixi (e.g. on ra, which has no system pixi).
282 lines
12 KiB
Bash
Executable File
282 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
# Keep a local, per-host mirror of the shared bernina pixi python
|
|
# environment in sync, so day-to-day python/pixi startup doesn't pay the
|
|
# cost of running straight off the shared project share.
|
|
#
|
|
# Unlike pixi_rebuild_local (which runs `pixi update` to produce a NEW
|
|
# lock file for testing), this script mirrors SOURCE_DIR's
|
|
# pixi.toml/pixi.lock (+pixi_dev.toml) EXACTLY into LOCAL_DIR and runs
|
|
# `pixi install --all` against that lock -- it never regenerates it. That
|
|
# keeps every host's local copy identical to whatever was last promoted
|
|
# at SOURCE_DIR.
|
|
#
|
|
# Meant to be launched once, detached, from startup_bernina_terminal (same
|
|
# nohup/disown pattern as kerberos_ticket_renew). With no flags it loops
|
|
# forever, so a single invocation covers:
|
|
# - first-time install of LOCAL_DIR if it doesn't exist yet
|
|
# - picking up any change to the shared lock file on every terminal
|
|
# restart (the loop's first iteration runs immediately)
|
|
# - noticing further upstream changes while the terminal stays open,
|
|
# checked every CHECK_INTERVAL seconds
|
|
#
|
|
# Change detection hashes SOURCE_DIR's pixi.toml/pixi.lock/pixi_dev.toml --
|
|
# cheap enough to do every minute. The (re)install itself only runs when
|
|
# that hash actually changed, or on first install. Before a first install,
|
|
# and before any (re)install if free space looks tight, the user is
|
|
# asked/warned via a zenity popup so a multi-GB download never happens
|
|
# silently without headroom.
|
|
#
|
|
# Safe to run from multiple terminals on the same host: a flock singleton
|
|
# means only one loop actually checks/installs per host at a time.
|
|
set -uo pipefail # no -e: one failed check/install must not kill the loop
|
|
|
|
SOURCE_DIR="/sf/bernina/applications/python"
|
|
PIXI_BIN="/usr/bin/pixi" # console (system) pixi first
|
|
[ -x "$PIXI_BIN" ] || PIXI_BIN="/sf/bernina/bin/pixi" # fallback, e.g. on ra (RHEL8)
|
|
LOCAL_DIR="${PIXI_LOCAL_DIR:-$HOME/bpy}"
|
|
export PIXI_CACHE_DIR="/sf/bernina/misc/pixi-cache"
|
|
|
|
CHECK_INTERVAL="${PIXI_LOCAL_SYNC_INTERVAL:-60}" # seconds between checks
|
|
EST_INSTALL_GB="${PIXI_LOCAL_SYNC_EST_GB:-15}" # estimated size of a fresh install
|
|
UPDATE_MIN_FREE_GB="${PIXI_LOCAL_SYNC_UPDATE_MIN_GB:-5}" # floor required for a resync
|
|
POPUP_THROTTLE_S=1800 # don't repeat resync error popups more often than this
|
|
ONCE=0
|
|
AUTO_YES=0
|
|
|
|
usage() {
|
|
cat >&2 <<EOF
|
|
Usage: $(basename "$0") [-1] [-y] [-i seconds]
|
|
|
|
Mirrors $SOURCE_DIR/pixi.{toml,lock} (+pixi_dev.toml) into
|
|
\$PIXI_LOCAL_DIR (default: \$HOME/bpy) and runs 'pixi install --all'
|
|
there, so this host has its own local copy of the shared python env.
|
|
|
|
-1 check/sync once and exit, instead of looping forever
|
|
-y never prompt before the first install (assume yes)
|
|
-i seconds between checks when looping (default: 60)
|
|
EOF
|
|
exit 1
|
|
}
|
|
|
|
while getopts "1yi:h" opt; do
|
|
case "$opt" in
|
|
1) ONCE=1 ;;
|
|
y) AUTO_YES=1 ;;
|
|
i) CHECK_INTERVAL="$OPTARG" ;;
|
|
h|*) usage ;;
|
|
esac
|
|
done
|
|
|
|
command -v "$PIXI_BIN" >/dev/null 2>&1 || PIXI_BIN="pixi"
|
|
command -v "$PIXI_BIN" >/dev/null 2>&1 || { echo "error: pixi not found" >&2; exit 1; }
|
|
|
|
# Only RHEL9+ consoles are supported: the shared pixi.lock's builds
|
|
# aren't verified against RHEL8's older system libraries, and bernina
|
|
# consoles are mid-migration from RHEL8 to RHEL9. On anything older,
|
|
# clean up any local mirror a pre-this-check attempt may have left
|
|
# behind and do nothing else -- no install, no loop.
|
|
os_id="" os_major=""
|
|
if [ -r /etc/os-release ]; then
|
|
os_id=$(. /etc/os-release 2>/dev/null; echo "$ID")
|
|
os_major=$(. /etc/os-release 2>/dev/null; echo "${VERSION_ID%%.*}")
|
|
fi
|
|
if [ "$os_id" != "rhel" ] || [ -z "$os_major" ] || [ "$os_major" -lt 9 ] 2>/dev/null; then
|
|
if [ -d "$LOCAL_DIR" ]; then
|
|
echo "$(date '+%F %T') $(hostname) is not RHEL9+ (detected: ${os_id:-unknown} ${os_major:-?}) -- removing local pixi env remnant at $LOCAL_DIR"
|
|
rm -rf "$LOCAL_DIR"
|
|
command -v notify-send >/dev/null 2>&1 && notify-send -u normal "Local python env (bpy)" \
|
|
"Removed $LOCAL_DIR: this host (${os_id:-unknown} ${os_major:-?}) isn't RHEL9+, so it isn't supported." 2>/dev/null
|
|
else
|
|
echo "$(date '+%F %T') $(hostname) is not RHEL9+ (detected: ${os_id:-unknown} ${os_major:-?}) -- nothing to do"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# Singleton per host+user: startup_bernina_terminal launches this on every
|
|
# terminal open, and it's also handy to run by hand -- only one loop
|
|
# should ever actually be checking/installing at a time.
|
|
LOCKFILE="${XDG_RUNTIME_DIR:-/tmp}/pixi_local_sync.lock"
|
|
exec 9>"$LOCKFILE"
|
|
flock -n 9 || { echo "already running (lock held: $LOCKFILE)" >&2; exit 0; }
|
|
|
|
STATE_DIR="$LOCAL_DIR/.pixi_local_sync"
|
|
|
|
notify() {
|
|
local urgency="$1" title="$2" body="$3"
|
|
echo "$(date '+%F %T') $title: $body"
|
|
command -v notify-send >/dev/null 2>&1 && notify-send -u "$urgency" "$title" "$body" 2>/dev/null
|
|
}
|
|
|
|
have_popup() {
|
|
[ -n "${DISPLAY:-}" ] && command -v zenity >/dev/null 2>&1
|
|
}
|
|
|
|
# free space under $HOME, in whole GB
|
|
avail_gb() {
|
|
df -Pk "$HOME" | awk 'NR==2 {printf "%d", $4/1024/1024}'
|
|
}
|
|
|
|
# marker-file throttle: true if $1 was touched less than $2 seconds ago
|
|
recently() {
|
|
local marker="$1" within_s="$2" now then
|
|
[ -f "$marker" ] || return 1
|
|
now=$(date +%s)
|
|
then=$(cat "$marker" 2>/dev/null || echo 0)
|
|
[ -n "$then" ] || then=0
|
|
(( now - then < within_s ))
|
|
}
|
|
mark_now() { date +%s > "$1"; }
|
|
|
|
# Stop trying a first install for the rest of THIS running instance:
|
|
# either the user said no, or there isn't enough space for it right now.
|
|
# Popping the same question/warning every minute forever is pointless --
|
|
# but deliberately in-memory, not a state file: a fresh invocation of
|
|
# this script (next login/terminal, i.e. a new process) should always
|
|
# check again from scratch, since free space can change and a decline
|
|
# only speaks for the moment it was given, not forever.
|
|
GAVE_UP_THIS_RUN=0
|
|
give_up() {
|
|
local reason="$1"
|
|
GAVE_UP_THIS_RUN=1
|
|
notify normal "Local python env (bpy)" \
|
|
"Not installing $LOCAL_DIR for now ($reason). Won't ask again this run -- next login/terminal will check again."
|
|
}
|
|
|
|
source_hash() {
|
|
{ cat "$SOURCE_DIR/pixi.toml" "$SOURCE_DIR/pixi.lock" 2>/dev/null
|
|
[ -f "$SOURCE_DIR/pixi_dev.toml" ] && cat "$SOURCE_DIR/pixi_dev.toml"
|
|
} | sha256sum | awk '{print $1}'
|
|
}
|
|
|
|
check_space() {
|
|
local need_gb="$1" first_install="$2" have msg
|
|
have=$(avail_gb)
|
|
if [ "$have" -lt "$need_gb" ]; then
|
|
if [ "$first_install" -eq 1 ]; then
|
|
# not enough room for the very first install -- nagging every
|
|
# minute won't change that within the same run, so stop
|
|
# asking for now; a later run (next login) checks fresh.
|
|
if have_popup; then
|
|
printf -v msg 'Not enough free space in $HOME to install the local pixi environment at\n%s\n\nAvailable now: %s GB\nNeeded (approx.): %s GB\n\nFree up space; this will be checked again next time a terminal starts.' \
|
|
"$LOCAL_DIR" "$have" "$need_gb"
|
|
zenity --error --title="Local python env (bpy): not enough space" --text="$msg" 2>/dev/null &
|
|
disown
|
|
fi
|
|
give_up "not enough space at first install: ${have}GB free, ~${need_gb}GB needed"
|
|
elif ! recently "$STATE_DIR/.space_warned" "$POPUP_THROTTLE_S"; then
|
|
notify critical "Local python env (bpy)" \
|
|
"Not enough space in \$HOME to resync $LOCAL_DIR: ${have}GB free, ~${need_gb}GB needed. Free up space -- will retry automatically."
|
|
mkdir -p "$STATE_DIR"
|
|
mark_now "$STATE_DIR/.space_warned"
|
|
fi
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
ask_permission() {
|
|
local have projected msg
|
|
have=$(avail_gb)
|
|
projected=$(( have - EST_INSTALL_GB ))
|
|
[ "$projected" -lt 0 ] && projected=0
|
|
|
|
[ "$AUTO_YES" -eq 1 ] && return 0
|
|
|
|
if ! have_popup; then
|
|
echo "$(date '+%F %T') no graphical session/zenity available -- skipping first install of $LOCAL_DIR for now"
|
|
return 1
|
|
fi
|
|
|
|
printf -v msg 'No local copy of the shared bernina python environment was found at:\n%s\n\nInstalling one here speeds up python/pixi startup on this host by\navoiding the network project share.\n\nFree space in $HOME now: %s GB\nEstimated space needed: ~%s GB\nProjected free space after: ~%s GB\n\nInstall it now? (runs in the background, will not block this terminal)' \
|
|
"$LOCAL_DIR" "$have" "$EST_INSTALL_GB" "$projected"
|
|
|
|
if zenity --question --title="Set up local python env (bpy)?" --width=420 --text="$msg" 2>/dev/null; then
|
|
return 0
|
|
else
|
|
give_up "declined by user"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
do_sync() {
|
|
local first_install=0 hash
|
|
|
|
[ -d "$LOCAL_DIR/.pixi/envs" ] || first_install=1
|
|
|
|
# Already gave up this run (declined, or no space at first install) --
|
|
# stay silent for the rest of this process; a fresh invocation of the
|
|
# script starts with GAVE_UP_THIS_RUN=0 again and checks from scratch.
|
|
if [ "$first_install" -eq 1 ] && [ "$GAVE_UP_THIS_RUN" -eq 1 ]; then
|
|
return
|
|
fi
|
|
|
|
hash=$(source_hash)
|
|
[ -n "$hash" ] || { echo "$(date '+%F %T') $SOURCE_DIR not readable, skipping"; return; }
|
|
|
|
if [ "$first_install" -eq 0 ] && [ -f "$STATE_DIR/.synced_hash" ] \
|
|
&& [ "$(cat "$STATE_DIR/.synced_hash")" = "$hash" ]; then
|
|
return # nothing changed -- the common case, stay quiet and fast
|
|
fi
|
|
|
|
if [ "$first_install" -eq 1 ]; then
|
|
check_space "$EST_INSTALL_GB" 1 || return
|
|
ask_permission || return
|
|
notify normal "Local python env (bpy)" "Installing local copy in $LOCAL_DIR ..."
|
|
else
|
|
check_space "$UPDATE_MIN_FREE_GB" 0 || return
|
|
echo "$(date '+%F %T') shared env changed, resyncing $LOCAL_DIR"
|
|
fi
|
|
|
|
mkdir -p "$LOCAL_DIR" "$STATE_DIR"
|
|
|
|
if (
|
|
umask 002
|
|
# Exclusive lock for the whole copy+install window: bpy-env.sh takes
|
|
# a non-blocking SHARED lock on this same file before trusting
|
|
# the local mirror, so it falls back to the shared NFS env
|
|
# instead of activating a half-copied/half-linked one. Using
|
|
# flock (not a marker file) means a crash here still releases it
|
|
# automatically -- nothing can get permanently stuck "busy".
|
|
exec 8>"$STATE_DIR/.install.lock"
|
|
flock -x 8
|
|
mkdir -p "$PIXI_CACHE_DIR" 2>/dev/null || true
|
|
chmod 2775 "$PIXI_CACHE_DIR" 2>/dev/null || true
|
|
cp "$SOURCE_DIR/pixi.toml" "$LOCAL_DIR/" || exit 1
|
|
[ -f "$SOURCE_DIR/pixi.lock" ] && cp "$SOURCE_DIR/pixi.lock" "$LOCAL_DIR/"
|
|
[ -f "$SOURCE_DIR/pixi_dev.toml" ] && cp "$SOURCE_DIR/pixi_dev.toml" "$LOCAL_DIR/"
|
|
# --frozen: never let this mirror resolve anything itself (e.g.
|
|
# live git/PyPI access for a dependency the shared pixi.lock
|
|
# hasn't caught up with yet, as happened with the eco package) --
|
|
# only ever install exactly what's already pinned in the lock we
|
|
# just copied. Fails loudly instead of silently drifting if
|
|
# SOURCE_DIR's pixi.toml and pixi.lock are ever inconsistent;
|
|
# fix that with pixi_rebuild_local + promote, not by letting
|
|
# this script improvise.
|
|
cd "$LOCAL_DIR" && "$PIXI_BIN" install --all --frozen
|
|
); then
|
|
echo "$hash" > "$STATE_DIR/.synced_hash"
|
|
rm -f "$STATE_DIR/.space_warned"
|
|
if [ "$first_install" -eq 1 ]; then
|
|
notify normal "Local python env (bpy)" "Install finished: $LOCAL_DIR is ready ($(avail_gb)GB free in \$HOME now)."
|
|
else
|
|
notify low "Local python env (bpy)" "Resynced $LOCAL_DIR with the latest shared env."
|
|
fi
|
|
else
|
|
notify critical "Local python env (bpy)" "pixi install failed in $LOCAL_DIR -- will retry automatically."
|
|
fi
|
|
}
|
|
|
|
# small jitter on the very first check so a mass terminal restart across
|
|
# many hosts doesn't all hit the shared project dir/cache at once
|
|
[ "$ONCE" -eq 0 ] && sleep $(( RANDOM % 20 ))
|
|
|
|
if [ "$ONCE" -eq 1 ]; then
|
|
do_sync
|
|
exit 0
|
|
fi
|
|
|
|
while true; do
|
|
do_sync
|
|
sleep "$CHECK_INTERVAL"
|
|
done
|