source ./activate and . .\activate.ps1 always run setup, and setup always runs `python -m pm.cli install`. An explicit install re-hashes every published tool entry so it can repair a corrupted one. On this machine that hash reads about 550 MiB and takes 6.0 of the 6.6 seconds, even when nothing changed. Shell activation now passes --trust-recorded and trusts the digest the install recorded, the same check startup already uses. A missing tool is still installed and a stale venv is still rebuilt. `hermes pm install` and `hermes update` keep the byte check, and the flag refuses names, --extra, and --target so it cannot narrow an install someone asked for by name. Measured on this machine, already up to date: the activation path drops from 6.6 s to 0.9 s. A bare `python -m pm.cli install` stays at 7.2 s. The remaining 0.9 s is process startup and imports, not hashing.
185 lines
7.7 KiB
Bash
Executable File
185 lines
7.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ============================================================================
|
|
# venv-style activation for the Hermes dev environment (pm-managed tools).
|
|
#
|
|
# source ./activate (bash / zsh, repo root)
|
|
# .\activate.ps1 (PowerShell)
|
|
#
|
|
# Emits the composed pm env (PATH + tool vars) into the CURRENT shell, with
|
|
# save/restore: `deactivate` undoes exactly what activation changed.
|
|
#
|
|
# `hermes` becomes a shell function for this checkout. It runs only while the
|
|
# shell is inside this worktree and refuses outside it, so a sibling worktree
|
|
# keeps its own command. A prompt prefix names the worktree.
|
|
#
|
|
# Sync through setup before selecting the environment. PM owns freshness;
|
|
# activation does not maintain a second dependency stamp. It trusts the
|
|
# recorded tool digest instead of re-hashing every entry. `hermes pm
|
|
# install` and `hermes update` keep that check.
|
|
# ============================================================================
|
|
_HERMES_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# Setup runs in a child: failures must not exit or partially activate the
|
|
# caller's shell, and activation must not republish launchers or shell config.
|
|
if ! (
|
|
unset PYTHONHOME PYTHONPATH VIRTUAL_ENV
|
|
bash "$_HERMES_REPO/setup-hermes.sh" --runtime-only
|
|
) >&2; then
|
|
printf '%s\n' 'activate: setup failed; shell environment unchanged' >&2
|
|
return 1 2>/dev/null || exit 1
|
|
fi
|
|
|
|
# Guard against double-sourcing: venv activate precedent (re-activating is a
|
|
# no-op re-save; we keep it idempotent by deactivating first). Key on the
|
|
# function, not the sentinel: child shells inherit __HERMES_ACTIVATED from an
|
|
# activated parent but never the function, and there is nothing to undo there.
|
|
if [ "$(type -t deactivate 2>/dev/null)" = "function" ]; then
|
|
deactivate
|
|
fi
|
|
|
|
# Bootstrap Python only emits the environment; it does not install anything.
|
|
_hermes_repo="$_HERMES_REPO"
|
|
_hermes_py=""
|
|
for _hermes_candidate in "$_hermes_repo/.venv/bin/python" "$_hermes_repo/.venv/Scripts/python.exe" \
|
|
"$_hermes_repo/venv/bin/python" "$_hermes_repo/venv/Scripts/python.exe"; do
|
|
[ -x "$_hermes_candidate" ] && { _hermes_py="$_hermes_candidate"; break; }
|
|
done
|
|
if [ -z "$_hermes_py" ]; then
|
|
for _hermes_store in "${HERMES_RUNTIME_DIR:-}" "$_hermes_repo/../tools" "${HERMES_HOME:-$HOME/.hermes}/tools"; do
|
|
[ -n "$_hermes_store" ] || continue
|
|
for _hermes_candidate in "$_hermes_store"/python-*/bin/python3 "$_hermes_store"/python-*/python.exe "$_hermes_store"/python-*/bin/python "$_hermes_store"/python-*/bin/python.exe; do
|
|
[ -x "$_hermes_candidate" ] && { _hermes_py="$_hermes_candidate"; break; }
|
|
done
|
|
[ -n "$_hermes_py" ] && break
|
|
done
|
|
fi
|
|
if [ -z "$_hermes_py" ]; then
|
|
echo "activate: no bootstrap Python found; run setup-hermes.sh" >&2
|
|
return 1 2>/dev/null || exit 1
|
|
fi
|
|
|
|
# --- emit export lines from `pm env` ---
|
|
_hermes_json="$(PYTHONHOME= PYTHONPATH="$_hermes_repo" "$_hermes_py" -m pm.environments)"
|
|
if [ -z "$_hermes_json" ] || [ "${_hermes_json#*{}" = "$_hermes_json" ]; then
|
|
echo "activate: could not read pm env (run ./setup-hermes.sh first)" >&2
|
|
unset _hermes_repo _hermes_os _hermes_arch _hermes_target _hermes_store _hermes_py _hermes_json
|
|
return 1 2>/dev/null || exit 1
|
|
fi
|
|
|
|
_hermes_keys="$(printf '%s' "$_hermes_json" | "$_hermes_py" -c 'import json,sys; print(" ".join(json.load(sys.stdin)))' \
|
|
| tr ' ' '\n' | grep -E '^[A-Za-z_][A-Za-z0-9_]*$' | tr '\n' ' ')"
|
|
[ -n "$_hermes_keys" ] || _hermes_keys="PATH"
|
|
|
|
# --- snapshot what we are about to change (deactivate restores this) ---
|
|
# __HERMES_ACTIVATED is part of the composed env below (it carries the
|
|
# installed-state path this environment was built from), so it is snapshotted
|
|
# and exported with every other key — no separate assignment here.
|
|
__HERMES_SAVED_PATH="$PATH"
|
|
for _hermes_k in $_hermes_keys; do
|
|
eval "__HERMES_SAVED_$_hermes_k=\"\${$_hermes_k+set}\""
|
|
eval "__HERMES_PRIOR_$_hermes_k=\${$_hermes_k-__HERMES_UNSET__}"
|
|
done
|
|
|
|
eval "$(printf '%s' "$_hermes_json" | "$_hermes_py" -c '
|
|
import json, sys, shlex, re
|
|
for k, v in json.load(sys.stdin).items():
|
|
# Windows carries names like ProgramFiles(ARM) that are not valid
|
|
# bash identifiers — skip them (they pass through untouched).
|
|
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", k):
|
|
continue
|
|
print("export %s=%s" % (k, shlex.quote(str(v))))')"
|
|
|
|
# This checkout, not whichever `hermes` PATH finds. A function beats PATH, an
|
|
# alias, and a hashed command, including the MSIX execution alias.
|
|
__HERMES_WORKTREE="$_hermes_repo"
|
|
# The branch names the worktree. A checkout cannot share a branch with another.
|
|
__HERMES_WORKTREE_NAME="$(git -C "$_hermes_repo" rev-parse --abbrev-ref HEAD 2>/dev/null || :)"
|
|
if [ -z "$__HERMES_WORKTREE_NAME" ] || [ "$__HERMES_WORKTREE_NAME" = HEAD ]; then
|
|
__HERMES_WORKTREE_NAME="$(basename "$_hermes_repo")"
|
|
fi
|
|
__HERMES_SAVED_PS1="${PS1-__HERMES_UNSET__}"
|
|
__HERMES_SAVED_PROMPT_COMMAND="${PROMPT_COMMAND-__HERMES_UNSET__}"
|
|
|
|
_hermes_worktree_here() {
|
|
_hermes_top="$(git rev-parse --show-toplevel 2>/dev/null)" || return 1
|
|
[ "$(cd "$_hermes_top" && pwd -P)" = "$(cd "$__HERMES_WORKTREE" && pwd -P)" ]
|
|
}
|
|
|
|
hermes() {
|
|
_hermes_worktree_here || {
|
|
printf '%s\n' "hermes: $(pwd) is outside $__HERMES_WORKTREE; refusing (the installed command is hidden while this checkout is active)" >&2
|
|
return 1
|
|
}
|
|
(
|
|
cd "$__HERMES_WORKTREE" || exit 1
|
|
if [ -n "${PYTHON-}" ]; then
|
|
exec "$PYTHON" hermes "$@"
|
|
elif [ -x .venv/Scripts/python.exe ]; then
|
|
exec .venv/Scripts/python.exe hermes "$@"
|
|
elif [ -x .venv/bin/python ]; then
|
|
exec .venv/bin/python hermes "$@"
|
|
elif [ -x venv/Scripts/python.exe ]; then
|
|
exec venv/Scripts/python.exe hermes "$@"
|
|
elif [ -x venv/bin/python ]; then
|
|
exec venv/bin/python hermes "$@"
|
|
else
|
|
exec python hermes "$@"
|
|
fi
|
|
)
|
|
}
|
|
|
|
_hermes_prompt() {
|
|
if [ "$__HERMES_SAVED_PS1" = "__HERMES_UNSET__" ]; then
|
|
_hermes_base_ps1=""
|
|
else
|
|
_hermes_base_ps1="$__HERMES_SAVED_PS1"
|
|
fi
|
|
if _hermes_worktree_here; then
|
|
PS1="($__HERMES_WORKTREE_NAME) $_hermes_base_ps1"
|
|
else
|
|
PS1="$_hermes_base_ps1"
|
|
fi
|
|
}
|
|
|
|
if [ -n "${PROMPT_COMMAND-}" ]; then
|
|
PROMPT_COMMAND="_hermes_prompt; ${PROMPT_COMMAND}"
|
|
else
|
|
PROMPT_COMMAND=_hermes_prompt
|
|
fi
|
|
_hermes_prompt
|
|
|
|
deactivate() {
|
|
export PATH="$__HERMES_SAVED_PATH"
|
|
for _hermes_k in $__HERMES_KEY_LIST; do
|
|
eval "_hermes_was=\"\$__HERMES_SAVED_$_hermes_k\""
|
|
eval "_hermes_old=\"\$__HERMES_PRIOR_$_hermes_k\""
|
|
if [ "$_hermes_was" != "set" ]; then
|
|
unset "$_hermes_k"
|
|
else
|
|
export "$_hermes_k=$_hermes_old"
|
|
fi
|
|
unset "__HERMES_SAVED_$_hermes_k" "__HERMES_PRIOR_$_hermes_k"
|
|
done
|
|
if [ "$__HERMES_SAVED_PS1" = "__HERMES_UNSET__" ]; then
|
|
unset PS1
|
|
else
|
|
PS1="$__HERMES_SAVED_PS1"
|
|
fi
|
|
if [ "$__HERMES_SAVED_PROMPT_COMMAND" = "__HERMES_UNSET__" ]; then
|
|
unset PROMPT_COMMAND
|
|
else
|
|
PROMPT_COMMAND="$__HERMES_SAVED_PROMPT_COMMAND"
|
|
fi
|
|
unset __HERMES_SAVED_PATH __HERMES_KEY_LIST __HERMES_ACTIVATED \
|
|
__HERMES_WORKTREE __HERMES_WORKTREE_NAME \
|
|
__HERMES_SAVED_PS1 __HERMES_SAVED_PROMPT_COMMAND
|
|
unset -f deactivate hermes _hermes_worktree_here _hermes_prompt
|
|
unset _hermes_k _hermes_was _hermes_old _hermes_top
|
|
}
|
|
|
|
# remember the key list for deactivate (kept out of the loop vars above)
|
|
__HERMES_KEY_LIST="$_hermes_keys"
|
|
|
|
unset _hermes_repo _hermes_os _hermes_arch _hermes_target _hermes_store \
|
|
_hermes_py _hermes_json _hermes_keys _hermes_k _hermes_entry _hermes_venvpy \
|
|
_hermes_winarch _hermes_top
|