hermes_cli.runtime_paths (venv generations, selection, activation) moves to pm.environments, and gains venv_bin_dir / venv_python / project_python. Every in-tree caller asks pm for an interpreter now; pm no longer reaches back into hermes_cli for its own environment layout (pm.packages, pm.extras, pm.ensure, pm.paths imported hermes_cli.runtime_paths). The three open-coded "Scripts/python.exe or bin/python" ladders in pm collapse onto venv_python. hermes_constants.venv_python_path / venv_bin_dir and hermes_cli.runtime_paths stay as frozen-updater-surface shims only (tests/compat/old_updater_surface.json). To keep the boot path light, pm/__init__ resolves its facade lazily (PEP 562) and pm.registry loads the built-in package definitions on first read instead of at import: `import hermes_bootstrap` now loads pm + pm.environments only (25ms, was 37ms with the eager facade dragging in the downloader). The stripped-payload fixtures that ship only pre-import files keep working for the same reason. Also restores two frozen-surface re-exports the F401 sweep dropped (banner._github_compare_behind, cua_backend.resolve_cua_driver_cmd).
93 lines
3.7 KiB
Python
93 lines
3.7 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 activate for itself, so ``./scripts/foo.py`` works from any
|
|
cwd with no manual ``source``. The shebang points at ``scripts/_hermes-python``,
|
|
a real repo script that sources ``activate`` and execs the interpreter on the
|
|
same file::
|
|
|
|
#!/usr/bin/env -S bash -c 'exec "$BASH" "$(dirname "$0")/_hermes-python" "$0" "$@"'
|
|
|
|
The kernel appends the invoking script as the last argument, and ``bash -c``
|
|
binds it to ``$0`` — so the prologue finds the target relative to itself and
|
|
the shebang stays independent of the cwd. ``$BASH`` for the second hop keeps it
|
|
free of both the exec bit and a ``PATH`` lookup.
|
|
|
|
Activation is not re-run when the inherited environment is still current: the
|
|
sentinel's value is the installed-state file the environment was composed
|
|
against (see ``pm.environments.activation_environment``), so the
|
|
prologue compares it against the inputs that decide the dependency set. Any of
|
|
``uv.lock``, ``pyproject.toml`` or ``pm/lock.json`` being newer than that file
|
|
means the inherited environment predates its inputs, and the prologue
|
|
re-activates once. ``[ -nt ]`` is a bash builtin, so the check costs no process
|
|
spawn; pm rewrites that file on every real sync and no-ops otherwise, so it
|
|
settles back to current rather than re-syncing on every run.
|
|
|
|
Two constraints worth knowing: the line is 83 bytes (a shebang must stay under
|
|
127), and ``/usr/bin/env -S`` is GNU and newer-BSD — verify it if an older
|
|
macOS or BSD is a target. 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)
|