Files
hermes-agent/scripts/_activation.py
ethernet b4d5092d3f refactor(activate): split interactive activation from running a command in the env
`activate` doubled as the thing scripts sourced non-interactively, so it
carried `--` argument guards and every caller had to tolerate its prompt and
`hermes` function. Composition (setup sync, bootstrap Python, pm env) now
lives in scripts/_activation.sh, and there are two consumers:

- `activate`: interactive, reversible (`deactivate`).
- new `scripts/run-in-hermes-env CMD...`: applies the env to one process and
  execs into it. Replaces `scripts/_hermes-python`; every Python script's
  shebang and `scripts/run_tests.sh` (which re-executes itself under it,
  however it was launched) use it.

`pm.environments --format sh` emits the env as shell exports, so the inline
JSON-to-export Python is gone from `activate`. An install that did not build
the test environment now reads as stale for every consumer.

Tests are split along the same seams: activation support fixtures, bash/ps1
activation, env-script quoting, and the runner's staleness.
2026-10-02 18:15:08 -04:00

72 lines
2.6 KiB
Python

"""Activation guard for repository scripts.
Every script in this checkout assumes it runs under the PM-activated
environment (``source ./activate`` on POSIX, ``. .\\activate.ps1`` on Windows),
which is what puts the pinned interpreter and its dependency tree on
``PATH``/``PYTHONPATH``. A script run with a bare system Python instead fails
much later with a confusing ``ModuleNotFoundError``. Call
:func:`require_activation` first and it fails immediately, naming the exact
command for the caller's shell.
Import it by its bare name: ``scripts/`` is the running script's own directory,
so it is on ``sys.path`` whether or not the shell is activated::
from _activation import require_activation
require_activation()
This module must stay stdlib-only and side-effect free at import — it has to
load before the environment it checks for exists.
Activating from the shebang
---------------------------
A POSIX script can run itself in the environment, so ``./scripts/foo.py`` works
from any cwd with no manual ``source``: its shebang hands the file to
``scripts/run-in-hermes-env`` (the line, staleness rules and portability limits
are documented there). Windows keeps using ``python scripts\\foo.py`` with the
guard.
"""
from __future__ import annotations
import os
import sys
ACTIVATION_ENV_VAR = "__HERMES_ACTIVATED"
POSIX_COMMAND = "source ./activate"
WINDOWS_COMMAND = ". .\\activate.ps1"
def activation_command() -> str:
"""The exact command that activates this checkout in the caller's shell."""
if os.name != "nt":
return POSIX_COMMAND
# A bash-family shell on Windows (Git Bash, MSYS, WSL interop) sources the
# POSIX script; only a native host gets the PowerShell one.
if os.environ.get("MSYSTEM") or os.path.basename(os.environ.get("SHELL", "")) in {"bash", "sh", "zsh"}:
return POSIX_COMMAND
return WINDOWS_COMMAND
def require_activation() -> None:
"""Exit the process unless the launching shell sourced the activate script.
Only tests the sentinel for non-emptiness: its value is the installed-state
path the environment was built from (and earlier revisions wrote a bare
``1``), so any value means "an ancestor shell activated".
"""
if os.environ.get(ACTIVATION_ENV_VAR):
return
script = os.path.basename(sys.argv[0]) or "this script"
print(
f"{script}: the Hermes environment is not activated.\n"
"From the repository root, run:\n"
"\n"
f" {activation_command()}\n"
"\n"
"then re-run this script.",
file=sys.stderr,
)
raise SystemExit(1)