mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
refactor(pm): pm owns the dependency-environment layout and interpreter paths
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).
This commit is contained in:
1 parent
ad53be7826
commit
bbec973514
101 files changed
+525
-493
No files matched your search
@@ -38,7 +38,7 @@ import json, os, re, shutil, subprocess, sys, tomllib, venv
|
||||
from pathlib import Path
|
||||
root, temp = map(Path, sys.argv[1:])
|
||||
sys.path.insert(0, str(root))
|
||||
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, site_packages
|
||||
from pm.environments import install_state_dir, runtime_facts_path, site_packages
|
||||
seed = temp / 'seed'
|
||||
venv.EnvBuilder(with_pip=False).create(seed)
|
||||
dependencies = temp / 'dependencies'
|
||||
|
||||
@@ -57,7 +57,7 @@ def main() -> None:
|
||||
from pm import paths
|
||||
from pm.lock import Facts
|
||||
from pm.store import current_target, tree_digest
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from pm.environments import selected_venv
|
||||
from hermes_cli._launchers import ensure_install_launchers
|
||||
|
||||
worker = root / "pm" / "worker.py"
|
||||
|
||||
@@ -131,7 +131,7 @@ def _windows_cron_python_invocation(python_exe: str) -> tuple[str, dict[str, str
|
||||
interpreter = sibling
|
||||
|
||||
from hermes_cli._launchers import resolve_store_python
|
||||
from hermes_cli.runtime_paths import selected_venv, site_packages as dependency_site
|
||||
from pm.environments import selected_venv, site_packages as dependency_site
|
||||
|
||||
repo = Path(__file__).resolve().parents[1]
|
||||
managed_python = resolve_store_python(repo)
|
||||
|
||||
+1
-1
@@ -311,7 +311,7 @@ suppress_platform_ver_console()
|
||||
|
||||
# Every entry point imports this module before its dependency graph.
|
||||
from pathlib import Path
|
||||
from hermes_cli.runtime_paths import activate_dependencies
|
||||
from pm.environments import activate_dependencies
|
||||
from hermes_cli._early_recovery import recover_if_needed
|
||||
|
||||
from hermes_cli._parser import command_argv
|
||||
|
||||
@@ -181,7 +181,7 @@ def recover_if_needed(project_root: Path | None = None, argv: list[str] | None =
|
||||
args = command_argv(sys.argv[1:] if argv is None else argv)
|
||||
if not explicit and args[:1] == ["pm"]:
|
||||
return False # PM's command boundary owns the explicit repair.
|
||||
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, selected_venv, site_packages
|
||||
from pm.environments import install_state_dir, runtime_facts_path, selected_venv, site_packages
|
||||
|
||||
missing_marker = install_state_dir(root) / ".repair-incomplete"
|
||||
marker_paths = (root / ".update-incomplete", root / ".lazy-refresh-incomplete", missing_marker)
|
||||
@@ -249,7 +249,7 @@ _EARLY_CORE_INSTALL_MAX_ATTEMPTS = 3
|
||||
|
||||
def _claim_recovery_lock(root: Path) -> int | None:
|
||||
"""Hold a kernel lock in writable state; process exit releases it."""
|
||||
from hermes_cli.runtime_paths import install_state_dir
|
||||
from pm.environments import install_state_dir
|
||||
from hermes_cli.runtime_state import _lock
|
||||
|
||||
state = install_state_dir(root)
|
||||
|
||||
@@ -281,7 +281,8 @@ def migrate_windows_bin_path(
|
||||
root = Path(root)
|
||||
|
||||
# Same per-machine anchor as ensure_windows_bin_launchers (see there).
|
||||
from hermes_constants import get_default_hermes_root, venv_bin_dir
|
||||
from hermes_constants import get_default_hermes_root
|
||||
from pm.environments import venv_bin_dir
|
||||
|
||||
try:
|
||||
home = Path(get_default_hermes_root())
|
||||
|
||||
@@ -21,7 +21,7 @@ if __name__ == "__main__":
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from hermes_constants import get_hermes_home
|
||||
from hermes_cli.runtime_paths import dependency_home_root, store_root
|
||||
from pm.environments import dependency_home_root, store_root
|
||||
|
||||
|
||||
def runtime_command(repo_root: Path, args=(), *, module: str = "hermes_cli.main",
|
||||
|
||||
@@ -21,7 +21,7 @@ def prepare(request: dict) -> tuple[Path, dict[str, str]]:
|
||||
from pm.client import ensure, sync_venv, venv_is_current
|
||||
from pm.lock import Lockfile
|
||||
from pm.registry import source_install_packages
|
||||
from hermes_cli.runtime_paths import activation_environment, install_state_dir, runtime_facts_path
|
||||
from pm.environments import activation_environment, install_state_dir, runtime_facts_path
|
||||
from hermes_cli._launchers import resolve_store_python
|
||||
from hermes_cli.venv_sync import publish_launchers
|
||||
|
||||
@@ -68,7 +68,7 @@ def main() -> int:
|
||||
# Historical atexit cleanup may run after the update's result is fixed.
|
||||
# It must reuse that installation, never start a second update/repair.
|
||||
from hermes_cli._launchers import resolve_store_python
|
||||
from hermes_cli.runtime_paths import activation_environment
|
||||
from pm.environments import activation_environment
|
||||
|
||||
root = Path(request["root"])
|
||||
python = resolve_store_python(root)
|
||||
|
||||
@@ -9,6 +9,7 @@ import threading
|
||||
from pathlib import Path
|
||||
from hermes_cli import source_check
|
||||
# Historical updater import (tests/compat/old_updater_surface.json). In-tree callers use the owner.
|
||||
from hermes_cli.source_check import _github_compare_behind # noqa: F401
|
||||
from hermes_constants import get_hermes_home
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ an explicit post-update run). PM owns runtime diagnosis, not this record.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from hermes_cli.runtime_paths import install_state_dir
|
||||
from pm.environments import install_state_dir
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -17,7 +17,7 @@ class DataRemovalPlan:
|
||||
|
||||
def plan_data_removal(home: Path, project: Path, userdata: Path | None = None) -> DataRemovalPlan:
|
||||
from hermes_constants import get_default_hermes_root
|
||||
from hermes_cli.runtime_paths import base_venv, installs_root, store_root
|
||||
from pm.environments import base_venv, installs_root, store_root
|
||||
from hermes_cli.steward import is_bundled_payload
|
||||
from tools.checkpoint_pruning import store_lock_path
|
||||
|
||||
|
||||
@@ -464,7 +464,7 @@ def _check_python_environment(should_fix: bool, f: Finding) -> None:
|
||||
# sys.path. Neither sys.prefix nor a stale PYTHONPATH proves activation.
|
||||
staged = _staged_venv_dir()
|
||||
if staged is not None:
|
||||
from hermes_cli.runtime_paths import site_packages
|
||||
from pm.environments import site_packages
|
||||
|
||||
selected_site = site_packages(staged).resolve()
|
||||
running_here = selected_site.is_dir() and any(Path(entry).resolve() == selected_site for entry in sys.path)
|
||||
@@ -537,7 +537,7 @@ def _check_command_installation(should_fix: bool, f: Finding) -> None:
|
||||
f.manual_issues.append(_python_repair_hint())
|
||||
return
|
||||
from hermes_cli._launchers import resolve_store_python
|
||||
from hermes_cli.runtime_paths import base_venv, selected_venv
|
||||
from pm.environments import base_venv, selected_venv
|
||||
|
||||
try:
|
||||
selected = selected_venv(PROJECT_ROOT)
|
||||
|
||||
@@ -6811,7 +6811,7 @@ def _pm_runtime_venv_dir(project_root: Path | None = None) -> Path | None:
|
||||
selection raises — fail closed, never a silently wrong venv.
|
||||
"""
|
||||
root = Path(project_root) if project_root is not None else PROJECT_ROOT
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from pm.environments import selected_venv
|
||||
|
||||
venv = selected_venv(root) # a malformed committed selection raises: fail closed
|
||||
return venv if venv.is_dir() else None
|
||||
|
||||
@@ -23,7 +23,7 @@ import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from hermes_constants import venv_python_path
|
||||
from pm.environments import venv_python
|
||||
from utils import atomic_write_text
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -60,7 +60,7 @@ def _is_uv_macos_store(path: str) -> bool:
|
||||
|
||||
def _venv_dir(project_root: Path | None = None) -> Path | None:
|
||||
root = Path(project_root) if project_root is not None else Path(__file__).resolve().parents[1]
|
||||
return next((root / n for n in ("venv", ".venv") if _present(venv_python_path(root / n))), None)
|
||||
return next((root / n for n in ("venv", ".venv") if _present(venv_python(root / n))), None)
|
||||
|
||||
|
||||
def _present(path: Path) -> bool:
|
||||
@@ -85,7 +85,7 @@ def _interpreter_file(src: str | Path) -> Path | None:
|
||||
|
||||
def _interpreter_source(venv_dir: Path) -> str | None:
|
||||
"""Return the interpreter file the venv currently resolves to (symlink target or pyvenv.cfg home)."""
|
||||
venv_py = venv_python_path(venv_dir)
|
||||
venv_py = venv_python(venv_dir)
|
||||
if venv_py.is_symlink():
|
||||
try:
|
||||
return str(venv_py.resolve(strict=False))
|
||||
@@ -113,7 +113,7 @@ def _managed_venv(project_root: Path | None) -> tuple[Path, Path, str] | str:
|
||||
source = _interpreter_source(venv_dir)
|
||||
if source is None or not _is_uv_macos_store(source):
|
||||
return "interpreter not uv-managed (stable path)"
|
||||
return venv_dir, venv_python_path(venv_dir), source
|
||||
return venv_dir, venv_python(venv_dir), source
|
||||
|
||||
|
||||
def _anchor_marker(venv_bin: Path) -> Path:
|
||||
@@ -283,7 +283,7 @@ def _install_anchor(venv_dir: Path, source_file: Path) -> None:
|
||||
"""Replace ``bin/python`` with a signed copy, gated on a real boot."""
|
||||
from hermes_cli.macos_signing import sign_managed_python
|
||||
|
||||
venv_py = venv_python_path(venv_dir)
|
||||
venv_py = venv_python(venv_dir)
|
||||
venv_bin = venv_py.parent
|
||||
venv_bin.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
@@ -93,7 +93,8 @@ def _is_windows() -> bool:
|
||||
def _venv_scripts_dir() -> Path | None:
|
||||
"""Return the venv Scripts directory if we're running inside the project venv."""
|
||||
from hermes_cli.main import PROJECT_ROOT
|
||||
from hermes_constants import project_venv_dir, venv_bin_dir
|
||||
from hermes_constants import project_venv_dir
|
||||
from pm.environments import venv_bin_dir
|
||||
venv_dir = project_venv_dir(PROJECT_ROOT)
|
||||
if venv_dir is None:
|
||||
return None
|
||||
|
||||
@@ -85,7 +85,7 @@ def prepare_memory_provider_dependencies(provider_name: str) -> tuple[dict, str
|
||||
if not inputs:
|
||||
return meta, None
|
||||
pm.sync_venv(explicit=True, **inputs)
|
||||
from hermes_cli.runtime_paths import selected_venv, site_packages
|
||||
from pm.environments import selected_venv, site_packages
|
||||
from pm.paths import repo_root
|
||||
|
||||
selected = site_packages(selected_venv(repo_root())).resolve()
|
||||
|
||||
+11
-218
@@ -1,220 +1,13 @@
|
||||
"""Dependency-environment paths shared by PM and pre-import launchers.
|
||||
"""Frozen updater surface: pre-PM updaters import these names from here.
|
||||
|
||||
Only stdlib and hermes_constants: environment selection must work before
|
||||
any dependency from that environment has been imported.
|
||||
The implementation moved to ``pm.environments``; in-tree code imports it from there.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from hermes_constants import get_default_hermes_root, project_venv_dir
|
||||
|
||||
|
||||
def install_key(project_root: Path) -> str:
|
||||
canonical = str(Path(project_root).resolve())
|
||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:16]
|
||||
|
||||
|
||||
def dependency_home_root() -> Path:
|
||||
"""Scope dependency state like a process launched in the active home."""
|
||||
from hermes_constants import get_default_hermes_root, get_hermes_home_override
|
||||
|
||||
override = get_hermes_home_override()
|
||||
return get_default_hermes_root(home=override) if override else get_default_hermes_root()
|
||||
|
||||
|
||||
def installs_root() -> Path:
|
||||
return dependency_home_root() / "installs"
|
||||
|
||||
|
||||
def install_state_dir(project_root: Path) -> Path:
|
||||
return installs_root() / install_key(project_root)
|
||||
|
||||
|
||||
def runtime_facts_path(project_root: Path) -> Path:
|
||||
return install_state_dir(project_root) / "facts.json"
|
||||
|
||||
|
||||
def base_venv(project_root: Path) -> Path:
|
||||
root = Path(project_root).resolve()
|
||||
manifest_path = root.parent / "manifest.json"
|
||||
if manifest_path.is_file():
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
|
||||
if (root.parent / manifest.get("repo", "")).resolve() == root:
|
||||
venv = (root.parent / manifest["venv"]).resolve()
|
||||
if not venv.is_relative_to(root.parent):
|
||||
raise RuntimeError("payload environment escapes its root")
|
||||
return venv
|
||||
return project_venv_dir(root) or root / "venv"
|
||||
|
||||
|
||||
def store_root(project_root: Path) -> Path:
|
||||
"""Resolve a payload-relative or stamped store before PM imports."""
|
||||
override = os.environ.get("HERMES_RUNTIME_DIR")
|
||||
if override:
|
||||
return Path(override).resolve()
|
||||
root = Path(project_root).resolve()
|
||||
manifest_path = root.parent / "manifest.json"
|
||||
if manifest_path.is_file():
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
|
||||
if (root.parent / manifest.get("repo", "")).resolve() == root:
|
||||
store = (root.parent / manifest["store"]).resolve()
|
||||
if not store.is_relative_to(root.parent):
|
||||
raise RuntimeError("payload store escapes its root")
|
||||
return store
|
||||
for directory in (root, *root.parents):
|
||||
stamp = directory / "install-stamp.json"
|
||||
if stamp.is_file():
|
||||
try:
|
||||
data = json.loads(stamp.read_text(encoding="utf-8-sig"))
|
||||
except (OSError, ValueError):
|
||||
return get_default_hermes_root() / "tools"
|
||||
value = data.get("runtimeDir") if isinstance(data, dict) else None
|
||||
return Path(value).resolve() if value else get_default_hermes_root() / "tools"
|
||||
return get_default_hermes_root() / "tools"
|
||||
|
||||
|
||||
def selected_venv(project_root: Path) -> Path:
|
||||
"""Use the committed environment, or the original install before first sync.
|
||||
|
||||
A broken committed selection is an error, not permission to load an older
|
||||
dependency set silently. Reading this function never creates user state.
|
||||
"""
|
||||
path = runtime_facts_path(project_root)
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except FileNotFoundError:
|
||||
return base_venv(project_root)
|
||||
except (OSError, ValueError) as exc:
|
||||
raise RuntimeError(f"cannot read dependency environment: {path}") from exc
|
||||
try:
|
||||
fact = data.get("packages", {}).get("venv", {})
|
||||
value = fact.get("environment")
|
||||
except AttributeError as exc:
|
||||
raise RuntimeError(f"invalid dependency environment record: {path}") from exc
|
||||
if value is None:
|
||||
return base_venv(project_root)
|
||||
if not isinstance(value, str):
|
||||
raise RuntimeError("invalid dependency environment path")
|
||||
environment = Path(value).resolve()
|
||||
generations = install_state_dir(project_root) / "environments"
|
||||
if not environment.is_relative_to(generations.resolve()) or not (environment / "pyvenv.cfg").is_file():
|
||||
raise RuntimeError(f"dependency environment is missing or outside this install: {environment}")
|
||||
return environment
|
||||
|
||||
|
||||
def venv_python_version(venv: Path) -> tuple[int, int] | None:
|
||||
"""The interpreter version a POSIX venv actually holds, or ``None``.
|
||||
|
||||
``site_packages`` must not date the tree from the CALLER's ``sys.version_info``:
|
||||
an update can rebuild the dependency environment with a different Python than
|
||||
the launcher that later imports it. Observed on an app-driven upgrade -- PM
|
||||
built the environment with CPython 3.14 while the PATH shim ran 3.11, so the
|
||||
shim composed ``lib/python3.11/site-packages`` inside a 3.14 venv, found no
|
||||
tree, and failed *after* a successful update.
|
||||
"""
|
||||
try:
|
||||
for line in (venv / "pyvenv.cfg").read_text(encoding="utf-8").splitlines():
|
||||
key, _, value = line.partition("=")
|
||||
if key.strip() != "version":
|
||||
continue
|
||||
major, _, rest = value.strip().partition(".")
|
||||
minor, _, _ = rest.partition(".")
|
||||
if major.isdigit() and minor.isdigit():
|
||||
return int(major), int(minor)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
candidates = sorted((venv / "lib").glob("python3*"))
|
||||
except OSError:
|
||||
return None
|
||||
for candidate in candidates:
|
||||
major, _, rest = candidate.name.removeprefix("python").partition(".")
|
||||
minor, _, _ = rest.partition(".")
|
||||
if major.isdigit() and minor.isdigit():
|
||||
return int(major), int(minor)
|
||||
return None
|
||||
|
||||
|
||||
def site_packages(venv: Path) -> Path:
|
||||
import sys
|
||||
|
||||
if os.name == "nt":
|
||||
return venv / "Lib/site-packages"
|
||||
version = venv_python_version(venv) or (sys.version_info.major, sys.version_info.minor)
|
||||
return venv / f"lib/python{version[0]}.{version[1]}/site-packages"
|
||||
|
||||
|
||||
def activate_dependencies(project_root: Path) -> None:
|
||||
"""Select the committed tree at process boot, before third-party imports.
|
||||
|
||||
A process with no extension selection keeps its original launch contract.
|
||||
Already-running processes are never switched after a dependency install.
|
||||
"""
|
||||
import sys
|
||||
|
||||
state = install_state_dir(project_root)
|
||||
if state.is_dir():
|
||||
from hermes_cli.runtime_state import runtime_lock, recover_publication, lease_generation
|
||||
# The lock's holder may be another profile's backend running a full dependency rebuild;
|
||||
# this process only reads the committed selection, so it proceeds without waiting rather
|
||||
# than leaving the backend unbound (see runtime_lock).
|
||||
with runtime_lock(project_root) as held:
|
||||
if held:
|
||||
recover_publication(project_root)
|
||||
environment = selected_venv(project_root)
|
||||
lease_generation(environment)
|
||||
selected = site_packages(environment)
|
||||
if not selected.is_dir() and not runtime_facts_path(project_root).is_file():
|
||||
return
|
||||
else:
|
||||
# Older installs and sealed payloads still select once, before imports.
|
||||
# Never consult VIRTUAL_ENV: it can describe the invoking shell's Python.
|
||||
environment = base_venv(project_root)
|
||||
selected = site_packages(environment)
|
||||
if not selected.is_dir():
|
||||
return # External/Nix interpreter owns its original sys.path.
|
||||
if not selected.is_dir():
|
||||
raise RuntimeError(f"dependency environment has no site-packages: {selected}")
|
||||
import site
|
||||
|
||||
sys.path[:] = [entry for entry in sys.path
|
||||
if Path(entry).name not in ("site-packages", "dist-packages")
|
||||
and Path(entry).resolve() != project_root.resolve()]
|
||||
# uv editable members are activated by .pth files, not by sys.path alone.
|
||||
site.addsitedir(str(selected))
|
||||
sys.path[:] = [str(project_root.resolve()), str(selected),
|
||||
*[entry for entry in sys.path if Path(entry).resolve() != selected.resolve()]]
|
||||
os.environ["PYTHONPATH"] = os.pathsep.join([str(project_root.resolve()), str(selected)])
|
||||
os.environ.pop("VIRTUAL_ENV", None)
|
||||
executable_dir = environment / ("Scripts" if os.name == "nt" else "bin")
|
||||
if executable_dir.is_dir():
|
||||
os.environ["PATH"] = os.pathsep.join([str(executable_dir), os.environ.get("PATH", "")])
|
||||
|
||||
|
||||
def activation_environment(project_root: Path) -> dict[str, str]:
|
||||
"""Read the installed PM environment; do not provision or switch imports."""
|
||||
from pm.ensure import env_for
|
||||
from pm.registry import all_packages
|
||||
|
||||
env = env_for(*all_packages())
|
||||
selected = site_packages(selected_venv(project_root))
|
||||
env.pop("PYTHONHOME", None)
|
||||
env.pop("VIRTUAL_ENV", None)
|
||||
env["PYTHONPATH"] = os.pathsep.join([str(project_root.resolve()), str(selected)])
|
||||
# The child-process sentinel. Its VALUE is the installed-state file this
|
||||
# environment was composed against, so a consumer gets three things for
|
||||
# free: that it inherited an activated shell, which checkout/profile that
|
||||
# shell came from, and a staleness stamp — uv.lock / pyproject.toml /
|
||||
# pm/lock.json newer than this file means the shell's environment predates
|
||||
# its inputs. pm rewrites it on every real sync and no-ops otherwise, so a
|
||||
# `-nt` comparison settles back to "current" after one re-activation.
|
||||
env["__HERMES_ACTIVATED"] = str(runtime_facts_path(project_root))
|
||||
return env
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps(activation_environment(Path(__file__).resolve().parents[1])))
|
||||
from pm.environments import ( # noqa: F401
|
||||
activation_environment,
|
||||
dependency_home_root,
|
||||
install_state_dir,
|
||||
runtime_facts_path,
|
||||
selected_venv,
|
||||
site_packages,
|
||||
store_root,
|
||||
)
|
||||
@@ -15,7 +15,7 @@ import tempfile
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from hermes_cli.runtime_paths import dependency_home_root, install_state_dir, runtime_facts_path
|
||||
from pm.environments import dependency_home_root, install_state_dir, runtime_facts_path
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
@@ -205,7 +205,7 @@ def lease_generation(environment: Path) -> None:
|
||||
|
||||
def collect_generations(project: Path, *, min_age_seconds: float = 86400) -> list[Path]:
|
||||
"""Remove unselected lease-managed generations after their readers exit."""
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from pm.environments import selected_venv
|
||||
removed = []
|
||||
root = install_state_dir(project)
|
||||
if not root.exists():
|
||||
|
||||
@@ -63,10 +63,9 @@ def complete_source_checkout(
|
||||
|
||||
def _bootstrap_command(root: Path, argv: list[str]) -> list[str]:
|
||||
"""Re-enter the checkout on PM's selected interpreter with its environment."""
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from hermes_constants import venv_python_path
|
||||
from pm.environments import project_python
|
||||
|
||||
return [str(venv_python_path(selected_venv(root))), "-I", "-B", "-u",
|
||||
return [str(project_python(root)), "-I", "-B", "-u",
|
||||
str(Path(__file__).resolve()), "--source", str(root), *argv, _PREPARED]
|
||||
|
||||
|
||||
@@ -91,7 +90,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
if prepared:
|
||||
# Dependencies are selected before any application import: this is the
|
||||
# same ordering the update completion guarantees.
|
||||
from hermes_cli.runtime_paths import activate_dependencies
|
||||
from pm.environments import activate_dependencies
|
||||
|
||||
activate_dependencies(root)
|
||||
if args.finish_update:
|
||||
@@ -111,7 +110,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
)
|
||||
return 0 if ok else 1
|
||||
|
||||
from hermes_cli.runtime_paths import activation_environment
|
||||
from pm.environments import activation_environment
|
||||
|
||||
# This interpreter is a bootstrap one: the work happens in the re-exec, so
|
||||
# every flag that decides WHICH tail runs has to survive into it. Losing
|
||||
|
||||
@@ -31,7 +31,7 @@ installers and boot paths read it before the full config machinery loads.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from hermes_cli.runtime_paths import install_key, installs_root
|
||||
from pm.environments import install_key, installs_root
|
||||
from hermes_cli.release_channels import validate_name
|
||||
from contextlib import contextmanager
|
||||
import logging
|
||||
|
||||
@@ -135,8 +135,7 @@ def _read_terminal_receipt(request: dict) -> dict | None:
|
||||
def _prepare(request: dict, request_path: Path, result_path: Path) -> int:
|
||||
import pm
|
||||
from pm import receipt
|
||||
from hermes_cli.runtime_paths import activation_environment, selected_venv
|
||||
from hermes_constants import venv_python_path
|
||||
from pm.environments import activation_environment, project_python
|
||||
|
||||
root = Path(request["source"])
|
||||
update_id = request["receipt"]["update_id"]
|
||||
@@ -146,7 +145,7 @@ def _prepare(request: dict, request_path: Path, result_path: Path) -> int:
|
||||
finally:
|
||||
request["pm_receipt"] = receipt.last_for_update(update_id)
|
||||
_write_json(request_path, request)
|
||||
command = [str(venv_python_path(selected_venv(root))),
|
||||
command = [str(project_python(root)),
|
||||
"-I", "-S", "-u", "-X", f"pycache_prefix={request['bytecode_cache']}",
|
||||
str(root / "hermes_cli/update_completion.py"),
|
||||
str(request_path), str(result_path), "--prepared"]
|
||||
@@ -262,7 +261,7 @@ def main() -> int:
|
||||
if "--prepared" in sys.argv[3:]:
|
||||
# Claim the selected generation's lease and process its .pth files only
|
||||
# after PM selection, before importing any application dependencies.
|
||||
from hermes_cli.runtime_paths import activate_dependencies
|
||||
from pm.environments import activate_dependencies
|
||||
activate_dependencies(root)
|
||||
return _finish(request, result_path)
|
||||
try:
|
||||
|
||||
@@ -54,11 +54,12 @@ def post_swap_python() -> Path:
|
||||
if sys.platform != "win32":
|
||||
return Path(sys.executable)
|
||||
from hermes_cli._launchers import _is_windows
|
||||
from hermes_constants import project_venv_dir, venv_python_path
|
||||
from hermes_constants import project_venv_dir
|
||||
from pm.environments import venv_python
|
||||
|
||||
venv_dir = project_venv_dir(Path(__file__).resolve().parents[1])
|
||||
if venv_dir is not None and _is_windows():
|
||||
candidate = venv_python_path(venv_dir, windows=True)
|
||||
candidate = venv_python(venv_dir, windows=True)
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
return Path(sys.executable)
|
||||
|
||||
@@ -16,7 +16,7 @@ def main(context: Path, result: Path) -> int:
|
||||
handled = True
|
||||
return 0
|
||||
sys.path.insert(0, str(root))
|
||||
from hermes_cli.runtime_paths import activate_dependencies
|
||||
from pm.environments import activate_dependencies
|
||||
activate_dependencies(root)
|
||||
from hermes_cli.dashboard_procs import _filter_dashboard_respawn_candidates
|
||||
from hermes_cli.main_dashboard import _respawn_dashboard_processes
|
||||
|
||||
@@ -156,7 +156,7 @@ def prepare_launch(project_root: Path, argv: list[str]) -> Path | None:
|
||||
|
||||
import pm
|
||||
from hermes_cli._launchers import resolve_store_python
|
||||
from hermes_cli.runtime_paths import activation_environment, runtime_facts_path
|
||||
from pm.environments import activation_environment, runtime_facts_path
|
||||
|
||||
current = pm.venv_is_current(project_root=root)
|
||||
if not current:
|
||||
|
||||
+7
-15
@@ -973,19 +973,10 @@ AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh/v1"
|
||||
|
||||
|
||||
def venv_bin_dir(venv_dir, *, windows: bool | None = None) -> Path:
|
||||
"""Venv executable dir (``Scripts``/``bin``); *windows* lets tests exercise Windows paths on Linux.
|
||||
"""Frozen updater surface: pre-PM updaters import this name; pm.environments owns it."""
|
||||
from pm.environments import venv_bin_dir as resolve
|
||||
|
||||
Returned unconditionally — callers differ on whether a missing venv is an error.
|
||||
|
||||
Canonical helper for venv layout. This was open-coded in seven places across four ``hermes_cli`` modules
|
||||
using three different Windows predicates (``platform.system()``, ``is_windows()``, ``_is_windows()``);
|
||||
each new call site had to re-derive it, and #76091 shipped an eighth copy because the correct behaviour
|
||||
lived 2400 lines away in another function. A few sites outside ``hermes_cli``
|
||||
(``tools/code_execution_tool.py``, ``agent/lsp/install.py``, ``agent/lsp/servers.py``) still hand-roll
|
||||
it — convert them as they are touched.
|
||||
"""
|
||||
windows = sys.platform == "win32" if windows is None else windows
|
||||
return Path(venv_dir) / ("Scripts" if windows else "bin")
|
||||
return resolve(venv_dir, windows=windows)
|
||||
|
||||
|
||||
def project_venv_dir(project_root) -> Path | None:
|
||||
@@ -1001,9 +992,10 @@ def project_venv_dir(project_root) -> Path | None:
|
||||
|
||||
|
||||
def venv_python_path(venv_dir, *, windows: bool | None = None) -> Path:
|
||||
"""Path to the Python interpreter inside *venv_dir* (may not exist)."""
|
||||
bin_dir = venv_bin_dir(venv_dir, windows=windows)
|
||||
return bin_dir / ("python.exe" if bin_dir.name == "Scripts" else "python")
|
||||
"""Frozen updater surface: pre-PM updaters import this name; pm.environments owns it."""
|
||||
from pm.environments import venv_python
|
||||
|
||||
return venv_python(venv_dir, windows=windows)
|
||||
|
||||
|
||||
# First-party roots: an ImportError naming one means our own tree is inconsistent. The
|
||||
|
||||
+41
-63
@@ -9,70 +9,48 @@ file (facts.json, per install) says what is actually on this machine.
|
||||
ensure(name) makes the installed state match the lockfile and returns a
|
||||
Runner with the composed environment. env_for(*names) composes already-installed
|
||||
packages' env without installing anything.
|
||||
|
||||
The facade resolves lazily (PEP 562): ``pm.environments`` runs at process boot,
|
||||
before any dependency is importable, and must not drag the downloader/ensure
|
||||
machinery in with it.
|
||||
"""
|
||||
|
||||
from pm.ensure import (
|
||||
activate,
|
||||
check,
|
||||
drift,
|
||||
enabled_extras,
|
||||
env_for,
|
||||
is_installed,
|
||||
installed_package,
|
||||
lazy_installs_allowed,
|
||||
)
|
||||
from pm.client import (
|
||||
ensure, sync_venv, build_environment, lock_project, stage_manager_runtime,
|
||||
ensure_environment, ensure_python_tool, venv_is_current,
|
||||
check_project_lock, export_requirements, build_requirements_environment, prune_cache, stage_tools, prepare_tools,
|
||||
)
|
||||
from pm.operations import environment_python, python_tool
|
||||
from pm.extras import available, ensure_import
|
||||
from pm.lock import Facts, Lockfile
|
||||
from pm.package import InstallError, Package, Runner, compose_env
|
||||
from pm.registry import all_packages, get_package, register, walk
|
||||
from pm.store import Store, current_target
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
"ensure",
|
||||
"env_for",
|
||||
"is_installed",
|
||||
"installed_package",
|
||||
"check",
|
||||
"drift",
|
||||
"activate",
|
||||
"sync_venv",
|
||||
"available",
|
||||
"ensure_import",
|
||||
"enabled_extras",
|
||||
"lazy_installs_allowed",
|
||||
"build_environment",
|
||||
"lock_project",
|
||||
"stage_manager_runtime",
|
||||
"stage_tools",
|
||||
"prepare_tools",
|
||||
"ensure_environment",
|
||||
"environment_python",
|
||||
"ensure_python_tool",
|
||||
"python_tool",
|
||||
"venv_is_current",
|
||||
"check_project_lock",
|
||||
"export_requirements",
|
||||
"build_requirements_environment",
|
||||
"prune_cache",
|
||||
"Facts",
|
||||
"Lockfile",
|
||||
"InstallError",
|
||||
"Package",
|
||||
"Runner",
|
||||
"compose_env",
|
||||
"all_packages",
|
||||
"get_package",
|
||||
"register",
|
||||
"walk",
|
||||
"Store",
|
||||
"current_target",
|
||||
]
|
||||
import importlib
|
||||
|
||||
_EXPORTS = {
|
||||
"pm.ensure": (
|
||||
"activate", "check", "drift", "enabled_extras", "env_for", "is_installed",
|
||||
"installed_package", "lazy_installs_allowed",
|
||||
),
|
||||
"pm.client": (
|
||||
"ensure", "sync_venv", "build_environment", "lock_project", "stage_manager_runtime",
|
||||
"ensure_environment", "ensure_python_tool", "venv_is_current", "check_project_lock",
|
||||
"export_requirements", "build_requirements_environment", "prune_cache", "stage_tools",
|
||||
"prepare_tools",
|
||||
),
|
||||
"pm.operations": ("environment_python", "python_tool"),
|
||||
"pm.extras": ("available", "ensure_import"),
|
||||
"pm.lock": ("Facts", "Lockfile"),
|
||||
"pm.package": ("InstallError", "Package", "Runner", "compose_env"),
|
||||
"pm.registry": ("all_packages", "get_package", "register", "walk"),
|
||||
"pm.store": ("Store", "current_target"),
|
||||
}
|
||||
_HOME = {name: module for module, names in _EXPORTS.items() for name in names}
|
||||
|
||||
__all__ = list(_HOME)
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
module = _HOME.get(name)
|
||||
if module is None:
|
||||
raise AttributeError(f"module 'pm' has no attribute {name!r}")
|
||||
value = getattr(importlib.import_module(module), name)
|
||||
globals()[name] = value
|
||||
return value
|
||||
|
||||
|
||||
def __dir__():
|
||||
return sorted(set(globals()) | set(_HOME))
|
||||
|
||||
import pm.packages # noqa: E402,F401 (registers the built-in definitions)
|
||||
import pm.security_packages # noqa: E402,F401
|
||||
+2
-2
@@ -496,7 +496,7 @@ def env_for(*names: str, base_env: Optional[dict] = None) -> dict[str, str]:
|
||||
def _runtime_state_matches(fact: dict, stamp: str, *, project_root: Path | None = None) -> bool:
|
||||
if not isinstance(fact, dict) or fact.get("stamp") != stamp:
|
||||
return False
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from pm.environments import selected_venv
|
||||
|
||||
try:
|
||||
environment = selected_venv(paths.repo_root() if project_root is None else project_root)
|
||||
@@ -511,7 +511,7 @@ def _runtime_state_matches(fact: dict, stamp: str, *, project_root: Path | None
|
||||
def venv_is_current(*, extras: list[str] | None = None, plugin_dirs=None, extra_plugin_dirs=(),
|
||||
project_root: Path | None = None) -> bool:
|
||||
"""Probe the requested union without changing recorded dependency state."""
|
||||
from hermes_cli.runtime_paths import runtime_facts_path
|
||||
from pm.environments import runtime_facts_path
|
||||
from pm.packages import Venv
|
||||
|
||||
root = paths.repo_root() if project_root is None else Path(project_root).absolute()
|
||||
|
||||
+3
-1
@@ -172,7 +172,9 @@ class PythonEnvironment:
|
||||
|
||||
@property
|
||||
def executable(self) -> Path:
|
||||
return self.destination / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
|
||||
from pm.environments import venv_python
|
||||
|
||||
return venv_python(self.destination)
|
||||
|
||||
def _run(self, args: list[str], *, cwd: Path, timeout: int) -> subprocess.CompletedProcess:
|
||||
# Explicit index credentials survive, but cannot redirect the project,
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
"""Dependency-environment layout: where a project's venv generations live, which one
|
||||
is selected, and the interpreter inside any venv. Shared by PM and pre-import launchers.
|
||||
|
||||
Only stdlib and hermes_constants: environment selection must work before
|
||||
any dependency from that environment has been imported.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from hermes_constants import get_default_hermes_root, project_venv_dir
|
||||
|
||||
|
||||
def install_key(project_root: Path) -> str:
|
||||
canonical = str(Path(project_root).resolve())
|
||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:16]
|
||||
|
||||
|
||||
def dependency_home_root() -> Path:
|
||||
"""Scope dependency state like a process launched in the active home."""
|
||||
from hermes_constants import get_default_hermes_root, get_hermes_home_override
|
||||
|
||||
override = get_hermes_home_override()
|
||||
return get_default_hermes_root(home=override) if override else get_default_hermes_root()
|
||||
|
||||
|
||||
def installs_root() -> Path:
|
||||
return dependency_home_root() / "installs"
|
||||
|
||||
|
||||
def install_state_dir(project_root: Path) -> Path:
|
||||
return installs_root() / install_key(project_root)
|
||||
|
||||
|
||||
def runtime_facts_path(project_root: Path) -> Path:
|
||||
return install_state_dir(project_root) / "facts.json"
|
||||
|
||||
|
||||
def base_venv(project_root: Path) -> Path:
|
||||
root = Path(project_root).resolve()
|
||||
manifest_path = root.parent / "manifest.json"
|
||||
if manifest_path.is_file():
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
|
||||
if (root.parent / manifest.get("repo", "")).resolve() == root:
|
||||
venv = (root.parent / manifest["venv"]).resolve()
|
||||
if not venv.is_relative_to(root.parent):
|
||||
raise RuntimeError("payload environment escapes its root")
|
||||
return venv
|
||||
return project_venv_dir(root) or root / "venv"
|
||||
|
||||
|
||||
def store_root(project_root: Path) -> Path:
|
||||
"""Resolve a payload-relative or stamped store before PM imports."""
|
||||
override = os.environ.get("HERMES_RUNTIME_DIR")
|
||||
if override:
|
||||
return Path(override).resolve()
|
||||
root = Path(project_root).resolve()
|
||||
manifest_path = root.parent / "manifest.json"
|
||||
if manifest_path.is_file():
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
|
||||
if (root.parent / manifest.get("repo", "")).resolve() == root:
|
||||
store = (root.parent / manifest["store"]).resolve()
|
||||
if not store.is_relative_to(root.parent):
|
||||
raise RuntimeError("payload store escapes its root")
|
||||
return store
|
||||
for directory in (root, *root.parents):
|
||||
stamp = directory / "install-stamp.json"
|
||||
if stamp.is_file():
|
||||
try:
|
||||
data = json.loads(stamp.read_text(encoding="utf-8-sig"))
|
||||
except (OSError, ValueError):
|
||||
return get_default_hermes_root() / "tools"
|
||||
value = data.get("runtimeDir") if isinstance(data, dict) else None
|
||||
return Path(value).resolve() if value else get_default_hermes_root() / "tools"
|
||||
return get_default_hermes_root() / "tools"
|
||||
|
||||
|
||||
def selected_venv(project_root: Path) -> Path:
|
||||
"""Use the committed environment, or the original install before first sync.
|
||||
|
||||
A broken committed selection is an error, not permission to load an older
|
||||
dependency set silently. Reading this function never creates user state.
|
||||
"""
|
||||
path = runtime_facts_path(project_root)
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except FileNotFoundError:
|
||||
return base_venv(project_root)
|
||||
except (OSError, ValueError) as exc:
|
||||
raise RuntimeError(f"cannot read dependency environment: {path}") from exc
|
||||
try:
|
||||
fact = data.get("packages", {}).get("venv", {})
|
||||
value = fact.get("environment")
|
||||
except AttributeError as exc:
|
||||
raise RuntimeError(f"invalid dependency environment record: {path}") from exc
|
||||
if value is None:
|
||||
return base_venv(project_root)
|
||||
if not isinstance(value, str):
|
||||
raise RuntimeError("invalid dependency environment path")
|
||||
environment = Path(value).resolve()
|
||||
generations = install_state_dir(project_root) / "environments"
|
||||
if not environment.is_relative_to(generations.resolve()) or not (environment / "pyvenv.cfg").is_file():
|
||||
raise RuntimeError(f"dependency environment is missing or outside this install: {environment}")
|
||||
return environment
|
||||
|
||||
|
||||
def venv_bin_dir(venv: Path, *, windows: bool | None = None) -> Path:
|
||||
"""``Scripts`` on Windows, ``bin`` elsewhere. Returned unconditionally — callers
|
||||
differ on whether a missing venv is an error. *windows* lets a POSIX process
|
||||
reason about a Windows layout (update hand-off, launcher repair)."""
|
||||
if windows is None:
|
||||
windows = os.name == "nt"
|
||||
return Path(venv) / ("Scripts" if windows else "bin")
|
||||
|
||||
|
||||
def venv_python(venv: Path, *, windows: bool | None = None) -> Path:
|
||||
"""The interpreter inside *venv* (may not exist)."""
|
||||
bin_dir = venv_bin_dir(venv, windows=windows)
|
||||
return bin_dir / ("python.exe" if bin_dir.name == "Scripts" else "python")
|
||||
|
||||
|
||||
def project_python(project_root: Path) -> Path:
|
||||
"""The interpreter of the committed dependency environment for *project_root*."""
|
||||
return venv_python(selected_venv(project_root))
|
||||
|
||||
|
||||
def venv_python_version(venv: Path) -> tuple[int, int] | None:
|
||||
"""The interpreter version a POSIX venv actually holds, or ``None``.
|
||||
|
||||
``site_packages`` must not date the tree from the CALLER's ``sys.version_info``:
|
||||
an update can rebuild the dependency environment with a different Python than
|
||||
the launcher that later imports it. Observed on an app-driven upgrade -- PM
|
||||
built the environment with CPython 3.14 while the PATH shim ran 3.11, so the
|
||||
shim composed ``lib/python3.11/site-packages`` inside a 3.14 venv, found no
|
||||
tree, and failed *after* a successful update.
|
||||
"""
|
||||
try:
|
||||
for line in (venv / "pyvenv.cfg").read_text(encoding="utf-8").splitlines():
|
||||
key, _, value = line.partition("=")
|
||||
if key.strip() != "version":
|
||||
continue
|
||||
major, _, rest = value.strip().partition(".")
|
||||
minor, _, _ = rest.partition(".")
|
||||
if major.isdigit() and minor.isdigit():
|
||||
return int(major), int(minor)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
candidates = sorted((venv / "lib").glob("python3*"))
|
||||
except OSError:
|
||||
return None
|
||||
for candidate in candidates:
|
||||
major, _, rest = candidate.name.removeprefix("python").partition(".")
|
||||
minor, _, _ = rest.partition(".")
|
||||
if major.isdigit() and minor.isdigit():
|
||||
return int(major), int(minor)
|
||||
return None
|
||||
|
||||
|
||||
def site_packages(venv: Path) -> Path:
|
||||
import sys
|
||||
|
||||
if os.name == "nt":
|
||||
return venv / "Lib/site-packages"
|
||||
version = venv_python_version(venv) or (sys.version_info.major, sys.version_info.minor)
|
||||
return venv / f"lib/python{version[0]}.{version[1]}/site-packages"
|
||||
|
||||
|
||||
def activate_dependencies(project_root: Path) -> None:
|
||||
"""Select the committed tree at process boot, before third-party imports.
|
||||
|
||||
A process with no extension selection keeps its original launch contract.
|
||||
Already-running processes are never switched after a dependency install.
|
||||
"""
|
||||
import sys
|
||||
|
||||
state = install_state_dir(project_root)
|
||||
if state.is_dir():
|
||||
from hermes_cli.runtime_state import runtime_lock, recover_publication, lease_generation
|
||||
# The lock's holder may be another profile's backend running a full dependency rebuild;
|
||||
# this process only reads the committed selection, so it proceeds without waiting rather
|
||||
# than leaving the backend unbound (see runtime_lock).
|
||||
with runtime_lock(project_root) as held:
|
||||
if held:
|
||||
recover_publication(project_root)
|
||||
environment = selected_venv(project_root)
|
||||
lease_generation(environment)
|
||||
selected = site_packages(environment)
|
||||
if not selected.is_dir() and not runtime_facts_path(project_root).is_file():
|
||||
return
|
||||
else:
|
||||
# Older installs and sealed payloads still select once, before imports.
|
||||
# Never consult VIRTUAL_ENV: it can describe the invoking shell's Python.
|
||||
environment = base_venv(project_root)
|
||||
selected = site_packages(environment)
|
||||
if not selected.is_dir():
|
||||
return # External/Nix interpreter owns its original sys.path.
|
||||
if not selected.is_dir():
|
||||
raise RuntimeError(f"dependency environment has no site-packages: {selected}")
|
||||
import site
|
||||
|
||||
sys.path[:] = [entry for entry in sys.path
|
||||
if Path(entry).name not in ("site-packages", "dist-packages")
|
||||
and Path(entry).resolve() != project_root.resolve()]
|
||||
# uv editable members are activated by .pth files, not by sys.path alone.
|
||||
site.addsitedir(str(selected))
|
||||
sys.path[:] = [str(project_root.resolve()), str(selected),
|
||||
*[entry for entry in sys.path if Path(entry).resolve() != selected.resolve()]]
|
||||
os.environ["PYTHONPATH"] = os.pathsep.join([str(project_root.resolve()), str(selected)])
|
||||
os.environ.pop("VIRTUAL_ENV", None)
|
||||
executable_dir = venv_bin_dir(environment)
|
||||
if executable_dir.is_dir():
|
||||
os.environ["PATH"] = os.pathsep.join([str(executable_dir), os.environ.get("PATH", "")])
|
||||
|
||||
|
||||
def activation_environment(project_root: Path) -> dict[str, str]:
|
||||
"""Read the installed PM environment; do not provision or switch imports."""
|
||||
from pm.ensure import env_for
|
||||
from pm.registry import all_packages
|
||||
|
||||
env = env_for(*all_packages())
|
||||
selected = site_packages(selected_venv(project_root))
|
||||
env.pop("PYTHONHOME", None)
|
||||
env.pop("VIRTUAL_ENV", None)
|
||||
env["PYTHONPATH"] = os.pathsep.join([str(project_root.resolve()), str(selected)])
|
||||
# The child-process sentinel. Its VALUE is the installed-state file this
|
||||
# environment was composed against, so a consumer gets three things for
|
||||
# free: that it inherited an activated shell, which checkout/profile that
|
||||
# shell came from, and a staleness stamp — uv.lock / pyproject.toml /
|
||||
# pm/lock.json newer than this file means the shell's environment predates
|
||||
# its inputs. pm rewrites it on every real sync and no-ops otherwise, so a
|
||||
# `-nt` comparison settles back to "current" after one re-activation.
|
||||
env["__HERMES_ACTIVATED"] = str(runtime_facts_path(project_root))
|
||||
return env
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps(activation_environment(Path(__file__).resolve().parents[1])))
|
||||
+1
-1
@@ -178,7 +178,7 @@ def ensure_import(extra: str) -> None:
|
||||
sync_venv([extra])
|
||||
# Activation is a process-boot operation. Never mix a newly resolved
|
||||
# dependency tree with libraries already imported by this process.
|
||||
from hermes_cli.runtime_paths import selected_venv, site_packages
|
||||
from pm.environments import selected_venv, site_packages
|
||||
from pm.paths import repo_root, runtime_facts_path
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
+3
-1
@@ -120,7 +120,9 @@ def _environment_root(name: str, root: Path | None) -> Path:
|
||||
|
||||
|
||||
def _python(environment: Path) -> Path:
|
||||
return environment / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
|
||||
from pm.environments import venv_python
|
||||
|
||||
return venv_python(environment)
|
||||
|
||||
|
||||
def _selection(root: Path) -> dict:
|
||||
|
||||
+2
-2
@@ -358,7 +358,7 @@ class Venv(StatePackage):
|
||||
return repo_root() if self._project_root is None else self._project_root
|
||||
|
||||
def venv_dir(self) -> Path:
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from pm.environments import selected_venv
|
||||
|
||||
return selected_venv(self.project_root())
|
||||
|
||||
@@ -387,7 +387,7 @@ class Venv(StatePackage):
|
||||
def apply(self, extras: list[str], *, plugin_dirs=None, repair: bool = False, explicit: bool = False) -> dict:
|
||||
"""Prepare one complete environment; the caller commits its selection."""
|
||||
import uuid
|
||||
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path
|
||||
from pm.environments import install_state_dir, runtime_facts_path
|
||||
from pm.environment import managed_environment
|
||||
from pm.lock import Facts
|
||||
from pm.workspace import enabled_member_dirs, lock_and_sync
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ def lockfile_path() -> Path:
|
||||
|
||||
|
||||
def store_root() -> Path:
|
||||
from hermes_cli.runtime_paths import store_root as resolve
|
||||
from pm.environments import store_root as resolve
|
||||
|
||||
return resolve(repo_root())
|
||||
|
||||
@@ -47,6 +47,6 @@ def writable_store_root() -> Path:
|
||||
|
||||
|
||||
def runtime_facts_path() -> Path:
|
||||
from hermes_cli.runtime_paths import runtime_facts_path as resolve
|
||||
from pm.environments import runtime_facts_path as resolve
|
||||
|
||||
return resolve(repo_root())
|
||||
+2
-2
@@ -11,7 +11,7 @@ from typing import Any, Optional
|
||||
|
||||
def _profiles_root() -> Path:
|
||||
# Plugin discovery and dependency publication must use the same home root.
|
||||
from hermes_cli.runtime_paths import dependency_home_root
|
||||
from pm.environments import dependency_home_root
|
||||
|
||||
return dependency_home_root() / "profiles"
|
||||
|
||||
@@ -88,7 +88,7 @@ def _is_directory(path: Path) -> bool:
|
||||
|
||||
def _all_homes() -> list[Path]:
|
||||
"""Enumerate the complete union or refuse; a partial scan cannot remove members."""
|
||||
from hermes_cli.runtime_paths import dependency_home_root
|
||||
from pm.environments import dependency_home_root
|
||||
|
||||
homes = [dependency_home_root()]
|
||||
root = _profiles_root()
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from hermes_cli.runtime_paths import dependency_home_root, install_state_dir, runtime_facts_path
|
||||
from pm.environments import dependency_home_root, install_state_dir, runtime_facts_path
|
||||
from hermes_cli.runtime_state import _atomic_bytes, _bytes, _digest
|
||||
from pm.workspace import enabled_plugin_dirs, _is_member_candidate
|
||||
|
||||
|
||||
+22
-5
@@ -14,6 +14,20 @@ from pm.package import InstallError, Package
|
||||
_packages: dict[str, Package] = {}
|
||||
|
||||
|
||||
def _builtins_loaded() -> dict[str, Package]:
|
||||
"""Register the built-in definitions on first read, not on ``import pm``.
|
||||
|
||||
Process boot imports ``pm.environments`` before any dependency is importable;
|
||||
pulling the whole package catalogue (downloader, network) in with it would
|
||||
cost every launch ~40ms and break the stripped payloads that ship only the
|
||||
pre-import files.
|
||||
"""
|
||||
if "pm.packages" not in sys.modules:
|
||||
import pm.packages # noqa: F401 (registers the built-in definitions)
|
||||
import pm.security_packages # noqa: F401
|
||||
return _packages
|
||||
|
||||
|
||||
def register(cls):
|
||||
instance = cls()
|
||||
if not instance.name:
|
||||
@@ -23,13 +37,14 @@ def register(cls):
|
||||
|
||||
|
||||
def get_package(name: str) -> Package:
|
||||
if name not in _packages:
|
||||
packages = _builtins_loaded()
|
||||
if name not in packages:
|
||||
raise KeyError(f"unknown package: {name}")
|
||||
return _packages[name]
|
||||
return packages[name]
|
||||
|
||||
|
||||
def all_packages() -> list[str]:
|
||||
return sorted(_packages)
|
||||
return sorted(_builtins_loaded())
|
||||
|
||||
|
||||
def source_install_packages(names: list[str]) -> list[str]:
|
||||
@@ -45,7 +60,7 @@ def package_definitions(names: list[str] | None = None) -> list[dict[str, Any]]:
|
||||
source bodies, or the application's import path. ``names`` selects a closure.
|
||||
"""
|
||||
definitions = []
|
||||
for package in walk(names) if names is not None else list(_packages.values()):
|
||||
for package in walk(names) if names is not None else list(_builtins_loaded().values()):
|
||||
cls = type(package)
|
||||
if cls.__module__ == "pm.packages":
|
||||
continue
|
||||
@@ -122,7 +137,9 @@ def load_package_definitions(definitions: list[dict[str, Any]]) -> None:
|
||||
definition["name"], f"cannot load package definition {module_name}.{definition['qualname']}: {exc}",
|
||||
"keep the package definition and its dependencies importable in the isolated PM runtime",
|
||||
) from exc
|
||||
# Import-time decorators must not override the caller's final selection.
|
||||
# Import-time decorators must not override the caller's final selection:
|
||||
# load the built-ins first so a restored definition wins.
|
||||
_builtins_loaded()
|
||||
_packages.update(restored)
|
||||
|
||||
|
||||
|
||||
+4
-2
@@ -31,7 +31,9 @@ def runtime_environment() -> dict[str, str]:
|
||||
|
||||
|
||||
def _python(environment: Path) -> Path:
|
||||
return environment / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
|
||||
from pm.environments import venv_python
|
||||
|
||||
return venv_python(environment)
|
||||
|
||||
|
||||
def _inputs(project: Path, python: Path) -> str:
|
||||
@@ -147,7 +149,7 @@ def runtime_python(*, bootstrap: bool = True, cache: Path | None = None) -> Path
|
||||
"""Resolve PM without selecting, repairing, or importing the app environment."""
|
||||
if is_runtime():
|
||||
return Path(sys.executable)
|
||||
from hermes_cli.runtime_paths import install_state_dir
|
||||
from pm.environments import install_state_dir
|
||||
from pm._uv import _toolchain
|
||||
from pm.paths import repo_root
|
||||
|
||||
|
||||
+2
-6
@@ -105,13 +105,9 @@ def current_target() -> str:
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
import hashlib
|
||||
from pm.downloader import _sha256_file
|
||||
|
||||
digest = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for block in iter(lambda: f.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
return _sha256_file(path)
|
||||
|
||||
|
||||
def hash_url(url: str) -> str:
|
||||
|
||||
@@ -35,7 +35,7 @@ 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 ``hermes_cli.runtime_paths.activation_environment``), so the
|
||||
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
|
||||
|
||||
@@ -182,7 +182,7 @@ def dependencies(args) -> None:
|
||||
return
|
||||
import tomllib
|
||||
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from pm.environments import selected_venv
|
||||
from pm import build_environment, check_project_lock, sync_venv
|
||||
from pm.paths import repo_root
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ class TestRunJobScript:
|
||||
monkeypatch.setenv("HERMES_RUNTIME_DIR", str(store))
|
||||
# The supplied payload launcher is older than the committed extension
|
||||
# generation. It must not override the installation's current selection.
|
||||
from hermes_cli.runtime_paths import install_state_dir, site_packages as dependency_site
|
||||
from pm.environments import install_state_dir, site_packages as dependency_site
|
||||
repo = Path(sched_script.__file__).resolve().parents[1]
|
||||
state = install_state_dir(repo)
|
||||
selected = state / "environments" / "selected" / "venv"
|
||||
|
||||
@@ -92,7 +92,7 @@ class PluginWorld:
|
||||
plugins_command(parser.parse_args(argv))
|
||||
|
||||
def selected(self) -> Path:
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from pm.environments import selected_venv
|
||||
return selected_venv(self.core)
|
||||
|
||||
def enabled(self) -> list[str]:
|
||||
|
||||
@@ -41,7 +41,7 @@ def layout(tmp_path, monkeypatch):
|
||||
@pytest.mark.parametrize("mode", ["confirmed", "cancel", "dry-run"])
|
||||
def test_data_only_preserves_runtime_and_sibling_homes(layout, monkeypatch, mode):
|
||||
import json
|
||||
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, selected_venv
|
||||
from pm.environments import install_state_dir, runtime_facts_path, selected_venv
|
||||
|
||||
monkeypatch.delattr(Path, "is_junction", raising=False)
|
||||
home, witnesses, data = layout
|
||||
|
||||
@@ -10,7 +10,7 @@ import sys
|
||||
import pytest
|
||||
|
||||
from hermes_cli import _launchers, doctor, doctor_platform
|
||||
from hermes_cli.runtime_paths import install_state_dir, site_packages
|
||||
from pm.environments import install_state_dir, site_packages
|
||||
|
||||
|
||||
def _tree(tmp_path, monkeypatch):
|
||||
@@ -46,7 +46,7 @@ def _pm_source(project, home):
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
for relative in (
|
||||
"hermes", "hermes_bootstrap.py", "hermes_constants.py", "hermes_cli/__init__.py",
|
||||
"hermes_cli/runtime_paths.py", "hermes_cli/runtime_state.py",
|
||||
"pm/environments.py", "hermes_cli/runtime_state.py",
|
||||
"hermes_cli/_early_recovery.py", "hermes_cli/_parser.py",
|
||||
"hermes_cli/venv_sync.py", "hermes_cli/steward.py", "hermes_cli/stderr_timestamp.py",
|
||||
):
|
||||
|
||||
@@ -90,7 +90,7 @@ def test_recovery_obeys_live_owner_and_single_flight(tmp_path, monkeypatch):
|
||||
|
||||
|
||||
def test_missing_environment_cannot_write_a_retry_marker_without_lock(tmp_path, monkeypatch):
|
||||
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path
|
||||
from pm.environments import install_state_dir, runtime_facts_path
|
||||
from pm.lock import Facts
|
||||
|
||||
root = _project(tmp_path)
|
||||
|
||||
@@ -1286,7 +1286,7 @@ def _seed_pm_environment(tmp_path, monkeypatch, with_venv_fact=True):
|
||||
facts.json whose venv fact names a committed environment generation under
|
||||
the install state (pyvenv.cfg included). Returns
|
||||
``(project_root, environment_dir)``."""
|
||||
from hermes_cli.runtime_paths import install_state_dir
|
||||
from pm.environments import install_state_dir
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path)) # installs_root() under the test root
|
||||
project_root = tmp_path / "payload" / "hermes-agent"
|
||||
|
||||
@@ -14,7 +14,7 @@ import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, site_packages
|
||||
from pm.environments import install_state_dir, runtime_facts_path, site_packages
|
||||
|
||||
_HOLDER = """
|
||||
import sys
|
||||
@@ -69,7 +69,7 @@ def test_runtime_lock_reports_a_lost_race(locked_install):
|
||||
def test_boot_activation_proceeds_while_the_install_is_locked(locked_install, monkeypatch):
|
||||
"""The issue's symptom, inverted: the backend reaches its dependency environment and can bind
|
||||
while a sibling holds the lock. Recovery belongs to whoever holds it, so it is skipped."""
|
||||
import hermes_cli.runtime_paths as runtime_paths
|
||||
import pm.environments as runtime_paths
|
||||
import hermes_cli.runtime_state as runtime_state
|
||||
|
||||
repo, site = locked_install
|
||||
|
||||
@@ -8,24 +8,27 @@ import sys
|
||||
import pytest
|
||||
|
||||
from hermes_cli import _launchers
|
||||
from hermes_cli.runtime_paths import install_state_dir, site_packages
|
||||
from pm.environments import install_state_dir, site_packages
|
||||
|
||||
|
||||
@pytest.mark.platforms("windows")
|
||||
def test_minted_launcher_reads_current_selection_and_editable_members(tmp_path, monkeypatch):
|
||||
from hermes_cli import runtime_paths, runtime_state
|
||||
from pm import environments as runtime_paths, runtime_state
|
||||
import hermes_constants
|
||||
|
||||
root = tmp_path / "repo"
|
||||
package = root / "hermes_cli"
|
||||
package.mkdir(parents=True)
|
||||
(package / "__init__.py").write_text("")
|
||||
pm_package = root / "pm"
|
||||
pm_package.mkdir()
|
||||
(pm_package / "__init__.py").write_text("")
|
||||
# Real selection code, with a fixture entry point rather than a live CLI.
|
||||
(package / "runtime_paths.py").write_bytes(Path(runtime_paths.__file__).read_bytes())
|
||||
(pm_package / "environments.py").write_bytes(Path(runtime_paths.__file__).read_bytes())
|
||||
(package / "runtime_state.py").write_bytes(Path(runtime_state.__file__).read_bytes())
|
||||
(root / "hermes_constants.py").write_bytes(Path(hermes_constants.__file__).read_bytes())
|
||||
(root / "hermes_bootstrap.py").write_text(
|
||||
"from pathlib import Path\nfrom hermes_cli.runtime_paths import activate_dependencies\n"
|
||||
"from pathlib import Path\nfrom pm.environments import activate_dependencies\n"
|
||||
"activate_dependencies(Path(__file__).resolve().parent)\n"
|
||||
)
|
||||
(package / "main.py").write_text(
|
||||
|
||||
@@ -26,7 +26,7 @@ import pytest
|
||||
|
||||
import hermes_cli.doctor as doctor
|
||||
import hermes_cli.macos_tcc_anchor as tcc
|
||||
from hermes_constants import venv_python_path
|
||||
from pm.environments import venv_python
|
||||
from hermes_cli import doctor_platform
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ class TestEnsureTccAnchorNonMacos:
|
||||
# The host check gates before any filesystem access, so a plain
|
||||
# regular-file interpreter fixture is enough: ensure must return None
|
||||
# and leave the file byte-identical. Assert the fixture's actual path
|
||||
# (venv_python_path is host-shaped: Scripts/python.exe on Windows).
|
||||
# (venv_python is host-shaped: Scripts/python.exe on Windows).
|
||||
venv_py = tmp_path / "checkout" / ".venv" / "bin" / "python"
|
||||
venv_py.parent.mkdir(parents=True)
|
||||
venv_py.write_bytes(b"#!fake interpreter")
|
||||
@@ -170,7 +170,7 @@ class TestEnsureTccAnchor:
|
||||
store_py.write_bytes(b"#!fake generation interpreter")
|
||||
store_py.chmod(0o755)
|
||||
root = _build_checkout(tmp_path, store_bin=store_bin)
|
||||
venv_py = venv_python_path(root / ".venv")
|
||||
venv_py = venv_python(root / ".venv")
|
||||
assert venv_py.is_symlink()
|
||||
|
||||
anchored = tcc.ensure_tcc_anchor(root)
|
||||
@@ -182,7 +182,7 @@ class TestEnsureTccAnchor:
|
||||
def test_anchors_uv_managed_interpreter(self, tmp_path):
|
||||
store_bin = _build_store(tmp_path)
|
||||
root = _build_checkout(tmp_path, store_bin=store_bin)
|
||||
venv_py = venv_python_path(root / ".venv")
|
||||
venv_py = venv_python(root / ".venv")
|
||||
assert venv_py.is_symlink()
|
||||
|
||||
anchored = tcc.ensure_tcc_anchor(root)
|
||||
@@ -202,7 +202,7 @@ class TestEnsureTccAnchor:
|
||||
def test_idempotent(self, tmp_path):
|
||||
store_bin = _build_store(tmp_path)
|
||||
root = _build_checkout(tmp_path, store_bin=store_bin, anchored=True)
|
||||
venv_py = venv_python_path(root / ".venv")
|
||||
venv_py = venv_python(root / ".venv")
|
||||
marker = venv_py.parent / ".tcc-anchor-source"
|
||||
before = marker.read_text(encoding="utf-8")
|
||||
|
||||
@@ -229,7 +229,7 @@ class TestEnsureTccAnchor:
|
||||
def test_reanchors_after_patch_bump(self, tmp_path):
|
||||
old_bin = _build_store(tmp_path, version="3.11.15")
|
||||
root = _build_checkout(tmp_path, store_bin=old_bin, anchored=True)
|
||||
venv_py = venv_python_path(root / ".venv")
|
||||
venv_py = venv_python(root / ".venv")
|
||||
|
||||
new_bin = _build_store(tmp_path, version="3.11.16")
|
||||
new_py = new_bin / "python3.11"
|
||||
@@ -250,7 +250,7 @@ class TestEnsureTccAnchor:
|
||||
|
||||
def test_skips_homebrew_interpreter(self, tmp_path):
|
||||
root = _build_checkout(tmp_path, homebrew=True)
|
||||
venv_py = venv_python_path(root / ".venv")
|
||||
venv_py = venv_python(root / ".venv")
|
||||
|
||||
assert tcc.ensure_tcc_anchor(root) is None
|
||||
assert venv_py.is_symlink()
|
||||
@@ -282,7 +282,7 @@ class TestEnsureTccAnchor:
|
||||
def test_boot_gate_refusal_leaves_venv_untouched(self, tmp_path, monkeypatch):
|
||||
store_bin = _build_store(tmp_path)
|
||||
root = _build_checkout(tmp_path, store_bin=store_bin)
|
||||
venv_py = venv_python_path(root / ".venv")
|
||||
venv_py = venv_python(root / ".venv")
|
||||
monkeypatch.setattr(tcc, "_passes_boot_gate", lambda *a, **k: False)
|
||||
|
||||
assert tcc.ensure_tcc_anchor(root) is None
|
||||
@@ -296,7 +296,7 @@ class TestEnsureTccAnchor:
|
||||
# The next ensure retries the whole install.
|
||||
store_bin = _build_store(tmp_path)
|
||||
root = _build_checkout(tmp_path, store_bin=store_bin)
|
||||
venv_py = venv_python_path(root / ".venv")
|
||||
venv_py = venv_python(root / ".venv")
|
||||
monkeypatch.setattr(tcc, "_copy_alias", lambda *a, **k: False)
|
||||
|
||||
import logging
|
||||
@@ -477,7 +477,7 @@ class TestTccAnchorState:
|
||||
|
||||
status, detail = tcc.tcc_anchor_state(root)
|
||||
assert status == "missing"
|
||||
assert str(venv_python_path(root / ".venv")) in detail
|
||||
assert str(venv_python(root / ".venv")) in detail
|
||||
|
||||
tcc.ensure_tcc_anchor(root)
|
||||
|
||||
@@ -498,7 +498,7 @@ class TestTccAnchorState:
|
||||
status, _ = tcc.tcc_anchor_state(root)
|
||||
assert status == "stale"
|
||||
anchored = tcc.ensure_tcc_anchor(root)
|
||||
assert anchored == venv_python_path(root / ".venv")
|
||||
assert anchored == venv_python(root / ".venv")
|
||||
assert (root / ".venv" / "bin" / "python").read_bytes() == (
|
||||
new_bin / "python3.11"
|
||||
).read_bytes()
|
||||
|
||||
@@ -12,7 +12,7 @@ import pytest
|
||||
import hermes_yaml as yaml
|
||||
|
||||
from hermes_cli import memory_setup
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from pm.environments import selected_venv
|
||||
from pm import paths
|
||||
from tests.pm._fixtures import _wheel
|
||||
|
||||
|
||||
@@ -317,7 +317,10 @@ def test_completed_serve_token_is_acknowledged_without_preparation(tmp_path, enc
|
||||
# request without importing PM or launching any completed backend twice.
|
||||
(package / "_launchers.py").write_text(
|
||||
"import sys\nresolve_store_python = lambda root: sys.executable\n", encoding="utf-8")
|
||||
(package / "runtime_paths.py").write_text(
|
||||
pm_package = root / "pm"
|
||||
pm_package.mkdir()
|
||||
(pm_package / "__init__.py").write_text("", encoding="utf-8")
|
||||
(pm_package / "environments.py").write_text(
|
||||
"import os\nactivation_environment = lambda root: dict(os.environ)\n", encoding="utf-8")
|
||||
context, result_path = tmp_path / "request.json", tmp_path / "result.json"
|
||||
context.write_text(json.dumps({"root": str(root), "stopped_serves": {"pending": False}},
|
||||
|
||||
@@ -30,7 +30,7 @@ from pathlib import Path
|
||||
import os,sys
|
||||
from pm.publication import StagedPlugin
|
||||
from pm.store import tree_digest
|
||||
from hermes_cli.runtime_paths import runtime_facts_path
|
||||
from pm.environments import runtime_facts_path
|
||||
from pm.lock import Facts
|
||||
project,staged,target = map(Path,sys.argv[1:4])
|
||||
StagedPlugin({"staged": str(staged), "target": str(target), "target_digest": tree_digest(target),
|
||||
|
||||
@@ -76,7 +76,7 @@ def installed(admission_env, monkeypatch, request):
|
||||
@pytest.mark.parametrize("failure", ["dependencies", "version", "publication", "manifest"])
|
||||
def test_failed_update_keeps_code_metadata_config_and_environment(installed, monkeypatch, failure):
|
||||
from hermes_cli import plugins_cmd
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from pm.environments import selected_venv
|
||||
from pm import paths
|
||||
from pm.lock import Facts
|
||||
|
||||
@@ -108,7 +108,7 @@ def test_failed_update_keeps_code_metadata_config_and_environment(installed, mon
|
||||
def test_successful_update_publishes_matching_code_and_durable_workspace(installed):
|
||||
import tomllib
|
||||
from hermes_cli import plugins_cmd
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from pm.environments import selected_venv
|
||||
from pm import paths
|
||||
from pm.lock import Facts
|
||||
from pm.packages import Venv
|
||||
|
||||
@@ -5,7 +5,7 @@ import os
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.runtime_paths import site_packages, store_root, venv_python_version
|
||||
from pm.environments import site_packages, store_root, venv_python_version
|
||||
from hermes_constants import get_default_hermes_root
|
||||
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ import sys
|
||||
import pytest
|
||||
|
||||
from hermes_cli import _launchers
|
||||
from hermes_cli.runtime_paths import install_state_dir, site_packages
|
||||
from pm.environments import install_state_dir, site_packages
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BOOT_FILES = (
|
||||
"hermes_bootstrap.py", "hermes_constants.py", "hermes_cli/__init__.py", "hermes_cli/_launchers.py",
|
||||
"hermes_cli/runtime_paths.py", "hermes_cli/runtime_state.py",
|
||||
"pm/environments.py", "hermes_cli/runtime_state.py",
|
||||
"hermes_cli/_early_recovery.py", "hermes_cli/_parser.py",
|
||||
"hermes_cli/venv_sync.py", "hermes_cli/steward.py",
|
||||
"hermes_cli/stderr_timestamp.py",
|
||||
|
||||
@@ -20,8 +20,9 @@ def transition(tmp_path):
|
||||
package = root / "hermes_cli"
|
||||
package.mkdir()
|
||||
(package / "__init__.py").write_text("")
|
||||
(root / "pm").mkdir()
|
||||
(root / "pm/__init__.py").write_text("OLD_API = True\n")
|
||||
pm_package = root / "pm"
|
||||
pm_package.mkdir()
|
||||
(pm_package / "__init__.py").write_text("OLD_API = True\n")
|
||||
|
||||
def git(*args):
|
||||
return subprocess.run(["git", *args], cwd=root, text=True, capture_output=True, check=True).stdout.strip()
|
||||
@@ -52,21 +53,20 @@ def transition(tmp_path):
|
||||
" with pathlib.Path('events.jsonl').open('a') as f:\n"
|
||||
" f.write(json.dumps(dict(name=name, pid=os.getpid(), python=sys.executable, **values)) + '\\n')\n"
|
||||
)
|
||||
(package / "runtime_paths.py").write_text(
|
||||
selected = tmp_path / "selected-python"
|
||||
venv.EnvBuilder(with_pip=False).create(selected)
|
||||
selected_python = selected / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
|
||||
(pm_package / "environments.py").write_text(
|
||||
"import os, sys\n"
|
||||
"from pathlib import Path\n"
|
||||
"selected_venv = lambda root: Path(sys.executable).parent.parent\n"
|
||||
f"project_python = lambda root: Path({str(selected_python)!r})\n"
|
||||
"activation_environment = lambda root: {**os.environ, 'PYTHONPATH': str(root)}\n"
|
||||
"def activate_dependencies(root):\n"
|
||||
" from hermes_cli.probe import event\n"
|
||||
" event('activate')\n"
|
||||
)
|
||||
selected = tmp_path / "selected-python"
|
||||
venv.EnvBuilder(with_pip=False).create(selected)
|
||||
selected_python = selected / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
|
||||
(root / "hermes_constants.py").write_text(
|
||||
f"venv_python_path = lambda root: {str(selected_python)!r}\n"
|
||||
)
|
||||
(root / "hermes_constants.py").write_text("")
|
||||
(package / "venv_sync.py").write_text(
|
||||
"from hermes_cli.probe import event\n"
|
||||
"publish_launchers = lambda root: event('launchers')\n"
|
||||
|
||||
@@ -31,7 +31,7 @@ def _put(root, name, content):
|
||||
@pytest.fixture
|
||||
def completion(tmp_path, monkeypatch):
|
||||
from hermes_cli.config_defaults import DEFAULT_CONFIG
|
||||
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, site_packages
|
||||
from pm.environments import install_state_dir, runtime_facts_path, site_packages
|
||||
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
@@ -215,7 +215,7 @@ def test_failure_preserves_original_receipt_before_build(completion, fault):
|
||||
context.write_text(json.dumps(request), encoding="utf-8")
|
||||
message = "application import failed before build"
|
||||
if fault == "activation":
|
||||
from hermes_cli.runtime_paths import selected_venv, site_packages
|
||||
from pm.environments import selected_venv, site_packages
|
||||
|
||||
shutil.rmtree(site_packages(selected_venv(source)))
|
||||
message = "dependency environment has no site-packages"
|
||||
|
||||
@@ -10,7 +10,7 @@ import sys
|
||||
import pytest
|
||||
|
||||
from hermes_cli import venv_sync
|
||||
from hermes_cli.runtime_paths import runtime_facts_path
|
||||
from pm.environments import runtime_facts_path
|
||||
|
||||
|
||||
def test_first_launch_syncs_without_marker_then_uses_completion_fact(tmp_path, monkeypatch):
|
||||
|
||||
@@ -15,8 +15,8 @@ import subprocess
|
||||
import pytest
|
||||
|
||||
import pm
|
||||
from hermes_cli.runtime_paths import activation_environment, runtime_facts_path, selected_venv, site_packages
|
||||
from hermes_constants import venv_python_path
|
||||
from pm.environments import activation_environment, runtime_facts_path, selected_venv, site_packages
|
||||
from pm.environments import venv_python
|
||||
from tests.pm._fixtures import isolated_python # noqa: F401
|
||||
from tests.pm.test_source_update_launch import source_launch # noqa: F401
|
||||
|
||||
@@ -52,7 +52,7 @@ def configured_update(source_launch, tmp_path, monkeypatch):
|
||||
f"source_build.build_update_products(Path({str(root)!r}), desktop=False)\n"
|
||||
"print('TARGET=' + json.dumps({'prefix': sys.prefix, 'python': sys.executable}))\n"
|
||||
)
|
||||
subprocess.run([str(venv_python_path(selected)), "-c", script],
|
||||
subprocess.run([str(venv_python(selected)), "-c", script],
|
||||
cwd=root, env=activation_environment(root), check=True)
|
||||
home = tmp_path / "home"
|
||||
config = home / "config.yaml"
|
||||
|
||||
@@ -308,9 +308,9 @@ def test_zip_refuses_non_main_before_transport(zip_update, monkeypatch, capsys):
|
||||
@pytest.mark.parametrize("windows,folder,executable", [(True, "Scripts", "python.exe"), (False, "bin", "python")])
|
||||
def test_venv_layout_explicit_and_native(tmp_path, windows, folder, executable):
|
||||
import os
|
||||
from hermes_constants import venv_bin_dir, venv_python_path
|
||||
from pm.environments import venv_bin_dir, venv_python
|
||||
|
||||
assert venv_bin_dir(tmp_path, windows=windows) == tmp_path / folder
|
||||
assert venv_python_path(str(tmp_path), windows=windows) == tmp_path / folder / executable
|
||||
assert venv_python(str(tmp_path), windows=windows) == tmp_path / folder / executable
|
||||
if windows == (os.name == "nt"):
|
||||
assert venv_python_path(tmp_path) == tmp_path / folder / executable
|
||||
assert venv_python(tmp_path) == tmp_path / folder / executable
|
||||
@@ -8,7 +8,7 @@ import subprocess
|
||||
import hermes_yaml as yaml
|
||||
|
||||
from hermes_cli import venv_sync
|
||||
from hermes_cli.runtime_paths import install_state_dir, selected_venv
|
||||
from pm.environments import install_state_dir, selected_venv
|
||||
from pm import paths
|
||||
from pm.lock import Lockfile
|
||||
from tests.pm.test_plugin_survival_contract import admission_env # noqa: F401
|
||||
|
||||
@@ -15,8 +15,8 @@ from hermes_cli.web_routers import memory_providers as mp
|
||||
@pytest.mark.parametrize("declaration", ["pyproject", "python_dependencies", "pip_dependencies"])
|
||||
def test_setup_admits_real_provider_union_and_keeps_selection_on_failure(tmp_path, monkeypatch, surface, declaration):
|
||||
import pm
|
||||
from hermes_constants import venv_python_path
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from pm.environments import venv_python
|
||||
from pm.environments import selected_venv
|
||||
from tests.pm._fixtures import _wheel
|
||||
from hermes_cli import memory_setup
|
||||
from hermes_cli.web_server_memory import _memory_provider_setup_info
|
||||
@@ -89,7 +89,7 @@ def test_setup_admits_real_provider_union_and_keeps_selection_on_failure(tmp_pat
|
||||
success = prepare()
|
||||
if surface == "dashboard":
|
||||
assert success[0]["status"] == "restart_required", success
|
||||
python = venv_python_path(selected_venv(core))
|
||||
python = venv_python(selected_venv(core))
|
||||
result = subprocess.run([str(python), "-I", "-c", "import existing_dep, provider_dep; print('both')"],
|
||||
check=True, capture_output=True, text=True, timeout=30)
|
||||
assert result.stdout.strip() == "both"
|
||||
|
||||
@@ -57,7 +57,7 @@ def probe_pm(root: Path, desktop: str, command: list[str]) -> None:
|
||||
# bootstrap: that would complete dependencies or recover markers for it.
|
||||
sys.path.insert(0, str(root))
|
||||
from hermes_cli._launchers import runtime_command
|
||||
from hermes_cli.runtime_paths import selected_venv, site_packages
|
||||
from pm.environments import selected_venv, site_packages
|
||||
|
||||
if command != runtime_command(root):
|
||||
raise RuntimeError("published launcher belongs to another installation or Python")
|
||||
|
||||
@@ -17,7 +17,7 @@ def publish_fixture_launcher(root: Path, main_source: str) -> Path:
|
||||
# are real. The interpreter is external to the checkout, like PM's store.
|
||||
(root / "hermes_bootstrap.py").write_text("", encoding="utf-8")
|
||||
(root / "pm").mkdir(exist_ok=True)
|
||||
for relative in ("hermes_constants.py", "hermes_cli/_launchers.py", "hermes_cli/runtime_paths.py"):
|
||||
for relative in ("hermes_constants.py", "hermes_cli/_launchers.py", "pm/environments.py"):
|
||||
shutil.copyfile(repository / relative, root / relative)
|
||||
out = root / ".hermes" / "bin"
|
||||
out.mkdir(parents=True)
|
||||
|
||||
@@ -24,9 +24,9 @@ def side_root(tmp_path, monkeypatch):
|
||||
|
||||
def test_ensure_sideenv_uses_pm_selected_python(side_root, monkeypatch):
|
||||
import pm
|
||||
from hermes_constants import venv_python_path
|
||||
from pm.environments import venv_python
|
||||
|
||||
python = venv_python_path(side_root / "selected")
|
||||
python = venv_python(side_root / "selected")
|
||||
seen = {}
|
||||
|
||||
def ensure(name, requirements, **kwargs):
|
||||
@@ -44,9 +44,9 @@ def test_ensure_sideenv_uses_pm_selected_python(side_root, monkeypatch):
|
||||
|
||||
def test_sideenv_python_is_pm_passive_selection(side_root, monkeypatch):
|
||||
import pm
|
||||
from hermes_constants import venv_python_path
|
||||
from pm.environments import venv_python
|
||||
|
||||
python = venv_python_path(side_root / "selected")
|
||||
python = venv_python(side_root / "selected")
|
||||
seen = []
|
||||
monkeypatch.setattr(pm, "environment_python",
|
||||
lambda name, **kw: seen.append((name, kw)) or python)
|
||||
@@ -61,8 +61,8 @@ def side_python(side_root, monkeypatch, tmp_path):
|
||||
|
||||
subprocess.run([sys.executable, "-m", "venv", "--without-pip", str(side_root)],
|
||||
check=True, capture_output=True, timeout=30)
|
||||
from hermes_constants import venv_python_path
|
||||
python = venv_python_path(side_root)
|
||||
from pm.environments import venv_python
|
||||
python = venv_python(side_root)
|
||||
site = Path(subprocess.check_output(
|
||||
[str(python), "-c", "import sysconfig; print(sysconfig.get_path('purelib'))"],
|
||||
text=True, timeout=10,
|
||||
|
||||
@@ -156,7 +156,7 @@ def _isolated_checkout(tmp_path: Path) -> Path:
|
||||
shutil.copytree(REPO_ROOT / "pm", root / "pm", ignore=shutil.ignore_patterns("__pycache__"))
|
||||
(root / "hermes_cli").mkdir()
|
||||
for relative in ("activate", "activate.ps1", "hermes_constants.py", "hermes_cli/__init__.py",
|
||||
"hermes_cli/runtime_paths.py", "hermes_cli/runtime_state.py"):
|
||||
"pm/environments.py", "hermes_cli/runtime_state.py"):
|
||||
shutil.copy2(REPO_ROOT / relative, root / relative)
|
||||
# Environment-only tests do not exercise provisioning; the runtime tests
|
||||
# replace these stubs with a publisher that records and applies each sync.
|
||||
|
||||
@@ -22,7 +22,7 @@ def _sync_checkout(tmp_path: Path):
|
||||
# no-op and activation must still call it, rather than cache its own answer.
|
||||
(root / "sync.py").write_text(textwrap.dedent('''\
|
||||
import json, os, pathlib, shutil, sys
|
||||
from hermes_cli.runtime_paths import runtime_facts_path, site_packages
|
||||
from pm.environments import runtime_facts_path, site_packages
|
||||
root = pathlib.Path(__file__).parent
|
||||
record = {"argv": sys.argv[1:], "python_env": {
|
||||
key: os.environ.get(key) for key in ("PYTHONHOME", "PYTHONPATH", "VIRTUAL_ENV")}}
|
||||
|
||||
@@ -13,7 +13,7 @@ def test_minimal_bootstrap_closure_reaches_pm_paths_and_locks(tmp_path):
|
||||
shutil.copytree(repo / "pm", stage / "pm", ignore=shutil.ignore_patterns("__pycache__"))
|
||||
shutil.copy2(repo / "hermes_constants.py", stage / "hermes_constants.py")
|
||||
(stage / "hermes_cli").mkdir()
|
||||
for name in ("__init__.py", "runtime_paths.py", "runtime_state.py"):
|
||||
for name in ("__init__.py", "runtime_state.py"):
|
||||
shutil.copy2(repo / "hermes_cli" / name, stage / "hermes_cli" / name)
|
||||
store = stage / "tools"
|
||||
env = dict(os.environ, HERMES_HOME=str(tmp_path / "home"),
|
||||
|
||||
@@ -187,7 +187,7 @@ assert importlib.util.find_spec('idna') is None
|
||||
report_code = """
|
||||
import json
|
||||
from pathlib import Path
|
||||
from hermes_cli.runtime_paths import install_state_dir, selected_venv, runtime_facts_path
|
||||
from pm.environments import install_state_dir, selected_venv, runtime_facts_path
|
||||
root = Path(sys.path[0])
|
||||
state = install_state_dir(root)
|
||||
print(json.dumps({'state': str(state), 'app': str(selected_venv(root)),
|
||||
@@ -225,7 +225,7 @@ print(json.dumps({{'yaml': ruamel.yaml.__file__, 'prefix': sys.prefix}}))
|
||||
app_code = """
|
||||
import json
|
||||
from pathlib import Path
|
||||
from hermes_cli.runtime_paths import activate_dependencies, selected_venv
|
||||
from pm.environments import activate_dependencies, selected_venv
|
||||
root = Path(sys.path[0])
|
||||
activate_dependencies(root)
|
||||
import ruamel.yaml, idna
|
||||
|
||||
@@ -146,7 +146,7 @@ def test_public_build_installs_all_extras_at_explicit_destination(installable_pr
|
||||
assert executable.parent.parent == tmp_path / "native environment"
|
||||
assert not (tmp_path / "wrong-environment").exists()
|
||||
assert "root_app" not in sys.modules
|
||||
from hermes_cli.runtime_paths import site_packages
|
||||
from pm.environments import site_packages
|
||||
|
||||
site = site_packages(executable.parent.parent)
|
||||
assert (site / "_virtualenv.pth").exists() is not sealed
|
||||
@@ -203,7 +203,7 @@ def test_group_only_build_excludes_application_dependencies(locked_project, tmp_
|
||||
@pytest.mark.parametrize("lazy", [False, True])
|
||||
def test_first_bundle_extension_preserves_shipped_extras(locked_project, build_worker, tmp_path, monkeypatch, lazy):
|
||||
import pm
|
||||
from hermes_cli.runtime_paths import selected_venv, runtime_facts_path
|
||||
from pm.environments import selected_venv, runtime_facts_path
|
||||
from pm import paths
|
||||
from pm.features import write_features
|
||||
from pm.lock import Facts
|
||||
@@ -258,7 +258,7 @@ def test_first_bundle_extension_preserves_shipped_extras(locked_project, build_w
|
||||
|
||||
def test_worker_sync_reuses_unions_and_reports_real_lock_drift(locked_project, build_worker, tmp_path, monkeypatch):
|
||||
import pm
|
||||
from hermes_cli.runtime_paths import selected_venv, runtime_facts_path
|
||||
from pm.environments import selected_venv, runtime_facts_path
|
||||
from pm.lock import Facts, Lockfile
|
||||
from pm import paths
|
||||
|
||||
@@ -689,7 +689,7 @@ def test_real_sync_retains_selection_until_commit(locked_project, tmp_path, monk
|
||||
import pm.extras as extras
|
||||
from pm import paths
|
||||
from pm.lock import Facts
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from pm.environments import selected_venv
|
||||
|
||||
source, uv, env = locked_project
|
||||
monkeypatch.setattr(paths, "repo_root", lambda: source)
|
||||
@@ -729,7 +729,7 @@ def test_real_sync_retains_selection_until_commit(locked_project, tmp_path, monk
|
||||
|
||||
|
||||
def test_live_apply_keeps_selection_on_failed_union(locked_project, tmp_path, monkeypatch):
|
||||
from hermes_cli.runtime_paths import runtime_facts_path, selected_venv
|
||||
from pm.environments import runtime_facts_path, selected_venv
|
||||
from pm.lock import Facts
|
||||
from pm.packages import Venv
|
||||
import pm.paths
|
||||
|
||||
@@ -10,7 +10,7 @@ import pytest
|
||||
|
||||
import pm.extras as extras
|
||||
import pm.features as features
|
||||
from hermes_cli.runtime_paths import site_packages
|
||||
from pm.environments import site_packages
|
||||
|
||||
|
||||
def test_inventory_uses_the_target_and_requires_every_anchor(tmp_path, monkeypatch):
|
||||
|
||||
@@ -66,7 +66,7 @@ def test_sync_venv_allows_frozen_extras_when_lazy_off(rooted, monkeypatch):
|
||||
import sys
|
||||
from pm import paths
|
||||
from pm.lock import Facts
|
||||
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path
|
||||
from pm.environments import install_state_dir, runtime_facts_path
|
||||
|
||||
ensure_mod = sys.modules["pm.ensure"]
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ def test_state_fact_in_shared_facts_file_is_not_a_store_install(pm_env):
|
||||
"""``venv`` is recorded via ``record_state`` (stamp + extras, no entry) into
|
||||
the same facts.json as tool facts. Reading the composed env over every
|
||||
registered package must skip it, not KeyError on ``fact["entry"]`` —
|
||||
that crashed ``activate`` (``hermes_cli.runtime_paths``) after a sync."""
|
||||
that crashed ``activate`` (``pm.environments``) after a sync."""
|
||||
from pm.ensure import env_for
|
||||
from pm.lock import Facts
|
||||
from pm.registry import all_packages
|
||||
|
||||
@@ -189,7 +189,7 @@ def test_conflicting_candidate_refused_unenabled_and_unimported(admission_env):
|
||||
plug_a, plug_b, *_ = _local_conflict_members(home)
|
||||
_write_enabled(home, [], provider="plug-a")
|
||||
admission.admit_plugin_set_change(set(), set(), active_plugins_dir=home / "plugins")
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from pm.environments import selected_venv
|
||||
working = selected_venv(tmp_path / "core")
|
||||
config_before = (home / "config.yaml").read_bytes()
|
||||
tree_before = {p: sorted(str(f) for f in p.rglob("*")) for p in (plug_a, plug_b)}
|
||||
@@ -250,7 +250,7 @@ def test_conflicting_candidate_refused_unenabled_and_unimported(admission_env):
|
||||
assert marker.read_bytes() == before
|
||||
child = subprocess.run([str(sidecar_python), "-c", "import sys; print(sys.prefix)"], check=True, capture_output=True, text=True, timeout=30)
|
||||
assert Path(child.stdout.strip()) == sidecar
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from pm.environments import selected_venv
|
||||
selected = selected_venv(tmp_path / "core")
|
||||
assert selected.is_dir() and selected != sidecar
|
||||
# A declared version range remains a member across the next managed rebuild.
|
||||
|
||||
@@ -29,7 +29,7 @@ def test_startup_validation_checks_real_ruamel_dependency(tmp_path, failure):
|
||||
|
||||
import ruamel.yaml
|
||||
|
||||
from hermes_cli.runtime_paths import site_packages
|
||||
from pm.environments import site_packages
|
||||
from pm.package import InstallError
|
||||
from pm.recovery import validate_environment
|
||||
|
||||
@@ -86,7 +86,7 @@ def recovery_graph(tmp_path):
|
||||
def test_repair_restores_recorded_plugin_dependencies_without_config(tmp_path, monkeypatch, recovery_graph, failure):
|
||||
import pm.paths as paths
|
||||
import pm.workspace as workspace
|
||||
from hermes_cli.runtime_paths import selected_venv, site_packages
|
||||
from pm.environments import selected_venv, site_packages
|
||||
|
||||
engine = importlib.import_module("pm.ensure")
|
||||
uv = shutil.which("uv")
|
||||
@@ -166,7 +166,7 @@ def test_repair_restores_recorded_plugin_dependencies_without_config(tmp_path, m
|
||||
def test_uncertain_profile_selection_refuses_sync_but_not_recorded_repair(tmp_path, monkeypatch, recovery_graph):
|
||||
import pm.paths as paths
|
||||
from hermes_cli.plugins_admission import AdmissionRefused, admit_plugin_set_change
|
||||
from hermes_cli.runtime_paths import install_state_dir, selected_venv, site_packages
|
||||
from pm.environments import install_state_dir, selected_venv, site_packages
|
||||
|
||||
engine = importlib.import_module("pm.ensure")
|
||||
# Use the same engine for admission and repair with the offline uv fixture.
|
||||
|
||||
@@ -6,7 +6,7 @@ import sys
|
||||
|
||||
|
||||
def test_bootstrap_lease_survives_selection_change(tmp_path, monkeypatch):
|
||||
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, site_packages
|
||||
from pm.environments import install_state_dir, runtime_facts_path, site_packages
|
||||
from hermes_cli.runtime_state import collect_generations
|
||||
|
||||
repo = tmp_path / "repo"
|
||||
@@ -30,7 +30,7 @@ def test_bootstrap_lease_survives_selection_change(tmp_path, monkeypatch):
|
||||
code = '''
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from hermes_cli.runtime_paths import activate_dependencies
|
||||
from pm.environments import activate_dependencies
|
||||
activate_dependencies(Path(sys.argv[1]))
|
||||
print("ready", flush=True)
|
||||
sys.stdin.readline()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Context-only homes use the same dependency state as their own process."""
|
||||
|
||||
from hermes_cli import runtime_paths
|
||||
from pm import environments as runtime_paths
|
||||
from pm.publication import PluginSelection
|
||||
from hermes_cli.runtime_state import recover_publication, runtime_lock
|
||||
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
|
||||
|
||||
@@ -6,7 +6,7 @@ def test_recovery_refuses_to_replace_newer_config(tmp_path, monkeypatch):
|
||||
from hermes_cli.runtime_state import recover_publication, runtime_lock
|
||||
from pm.publication import PluginSelection
|
||||
import pm.paths as paths
|
||||
from hermes_cli.runtime_paths import install_state_dir
|
||||
from pm.environments import install_state_dir
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
repo = tmp_path / "repo"
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.runtime_paths import install_state_dir
|
||||
from pm.environments import install_state_dir
|
||||
from hermes_cli.runtime_state import recover_publication, runtime_lock
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import pytest
|
||||
|
||||
|
||||
def test_install_runtime_selection_is_scoped_and_read_only(tmp_path, monkeypatch):
|
||||
from hermes_cli import runtime_paths
|
||||
from pm import environments as runtime_paths
|
||||
|
||||
home = tmp_path / "home"
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
@@ -33,7 +33,7 @@ def test_boot_uses_one_selected_dependency_tree_in_fresh_process(tmp_path, monke
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from hermes_cli import runtime_paths
|
||||
from pm import environments as runtime_paths
|
||||
|
||||
root = tmp_path / "repo"
|
||||
base = root / "venv"
|
||||
@@ -53,7 +53,7 @@ def test_boot_uses_one_selected_dependency_tree_in_fresh_process(tmp_path, monke
|
||||
"venv": {"environment": str(selected)}
|
||||
}}))
|
||||
code = (
|
||||
"import sys; from pathlib import Path; from hermes_cli.runtime_paths import activate_dependencies; "
|
||||
"import sys; from pathlib import Path; from pm.environments import activate_dependencies; "
|
||||
"sys.path.insert(0, sys.argv[2]); activate_dependencies(Path(sys.argv[1])); "
|
||||
"import probe_package, importlib.util; print(probe_package.version); "
|
||||
"print(importlib.util.find_spec('base_only') is None)"
|
||||
@@ -70,7 +70,7 @@ def test_broken_environment_keeps_explicit_repair_entry_reachable(tmp_path, monk
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from hermes_cli.runtime_paths import runtime_facts_path
|
||||
from pm.environments import runtime_facts_path
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
|
||||
repo = Path(__file__).resolve().parents[2]
|
||||
@@ -90,7 +90,7 @@ def test_manual_repair_bypasses_damaged_generation_activation(tmp_path, monkeypa
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, site_packages
|
||||
from pm.environments import install_state_dir, runtime_facts_path, site_packages
|
||||
|
||||
repo = Path(__file__).resolve().parents[2]
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
|
||||
@@ -112,7 +112,7 @@ def test_manual_repair_bypasses_damaged_generation_activation(tmp_path, monkeypa
|
||||
|
||||
@pytest.mark.parametrize("data", [[], {"packages": []}, {"packages": {"venv": []}}])
|
||||
def test_malformed_selection_has_actionable_error(tmp_path, monkeypatch, data):
|
||||
from hermes_cli import runtime_paths
|
||||
from pm import environments as runtime_paths
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
|
||||
record = runtime_paths.runtime_facts_path(tmp_path / "repo")
|
||||
record.parent.mkdir(parents=True)
|
||||
@@ -123,7 +123,7 @@ def test_malformed_selection_has_actionable_error(tmp_path, monkeypatch, data):
|
||||
|
||||
@pytest.mark.parametrize("bad_path", ["outside", "missing"])
|
||||
def test_invalid_selected_environment_never_silently_falls_back(tmp_path, monkeypatch, bad_path):
|
||||
from hermes_cli import runtime_paths
|
||||
from pm import environments as runtime_paths
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
|
||||
root = tmp_path / "repo"
|
||||
|
||||
@@ -19,7 +19,7 @@ import pytest
|
||||
|
||||
import pm
|
||||
from hermes_cli import venv_sync
|
||||
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, selected_venv, site_packages
|
||||
from pm.environments import install_state_dir, runtime_facts_path, selected_venv, site_packages
|
||||
from pm import paths
|
||||
from pm.lock import Facts
|
||||
from pm.package import InstallError
|
||||
@@ -313,7 +313,7 @@ def test_real_bootstrap_reexecs_before_app_imports(source_launch, tmp_path, isol
|
||||
"import hermes_bootstrap\n"
|
||||
"import json, sys\n"
|
||||
"from pathlib import Path\n"
|
||||
"from hermes_cli.runtime_paths import selected_venv, site_packages\n"
|
||||
"from pm.environments import selected_venv, site_packages\n"
|
||||
"from hermes_cli.venv_sync import prepare_launch\n"
|
||||
"root = Path(__file__).parent\n"
|
||||
"selected = selected_venv(root)\n"
|
||||
@@ -342,7 +342,7 @@ def test_real_bootstrap_reexecs_before_app_imports(source_launch, tmp_path, isol
|
||||
activation_probe = subprocess.run(
|
||||
[str(isolated_python), "-I", "-c",
|
||||
f"import sys; sys.path.insert(0, {str(repository)!r}); "
|
||||
"from pathlib import Path; from hermes_cli.runtime_paths import activate_dependencies; "
|
||||
"from pathlib import Path; from pm.environments import activate_dependencies; "
|
||||
f"activate_dependencies(Path({str(root)!r}))"],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ def isolated_machine_home(tmp_path, monkeypatch):
|
||||
@pytest.mark.parametrize("marker_name", [".update-incomplete", ".lazy-refresh-incomplete", None, "manual", "baseline"])
|
||||
def test_bootstrap_repairs_before_dependency_activation(tmp_path, monkeypatch, marker_name):
|
||||
import pm.paths as paths
|
||||
from hermes_cli.runtime_paths import selected_venv, site_packages
|
||||
from pm.environments import selected_venv, site_packages
|
||||
|
||||
engine = importlib.import_module("pm.ensure")
|
||||
repo = Path(__file__).resolve().parents[2]
|
||||
@@ -49,7 +49,7 @@ def test_bootstrap_repairs_before_dependency_activation(tmp_path, monkeypatch, m
|
||||
cli.mkdir()
|
||||
# Include the real preimport protocol, including its ownership check. Do
|
||||
# not stub prepare_launch: the same files are also saved in PM's workspace.
|
||||
for name in ("__init__.py", "runtime_paths.py", "runtime_state.py", "_early_recovery.py",
|
||||
for name in ("__init__.py", "runtime_state.py", "_early_recovery.py",
|
||||
"_parser.py", "venv_sync.py", "steward.py"):
|
||||
shutil.copy2(repo / "hermes_cli" / name, cli / name)
|
||||
wheels = tmp_path / "wheels"
|
||||
|
||||
@@ -66,7 +66,7 @@ def test_fresh_takeover_prepares_generation_and_runs_selected_python(tmp_path):
|
||||
"import json, sys\nfrom pathlib import Path\n"
|
||||
"request=json.loads(Path(sys.argv[1]).read_text())\n"
|
||||
"sys.path.insert(0,request['root'])\n"
|
||||
"from hermes_cli.runtime_paths import activate_dependencies\n"
|
||||
"from pm.environments import activate_dependencies\n"
|
||||
"activate_dependencies(Path(request['root']))\nimport takeover_dep\n"
|
||||
"assert request['pm_receipt']['update_id']==request['update_id']\n"
|
||||
"Path(sys.argv[2]).write_text(json.dumps({'python':sys.executable,'dep':takeover_dep.__file__}))\n",
|
||||
|
||||
@@ -49,7 +49,7 @@ def test_refused_or_already_paused_install_does_not_acquire_runtime(client, monk
|
||||
|
||||
|
||||
def _current_environment(tmp_path, monkeypatch, members):
|
||||
from hermes_cli.runtime_paths import install_state_dir
|
||||
from pm.environments import install_state_dir
|
||||
from pm.lock import Facts
|
||||
from pm.packages import Venv
|
||||
|
||||
@@ -71,7 +71,7 @@ def _current_environment(tmp_path, monkeypatch, members):
|
||||
def test_currency_probe_preserves_union_and_candidate_inputs(client, tmp_path, monkeypatch, isolated_python,
|
||||
member_shape, route):
|
||||
import json
|
||||
from hermes_cli.runtime_paths import runtime_facts_path, selected_venv
|
||||
from pm.environments import runtime_facts_path, selected_venv
|
||||
from pm.lock import Facts
|
||||
from pm.packages import Venv
|
||||
|
||||
@@ -158,7 +158,7 @@ def test_currency_probe_preserves_union_and_candidate_inputs(client, tmp_path, m
|
||||
|
||||
|
||||
def _assert_worker_holds_lock(repo):
|
||||
from hermes_cli.runtime_paths import install_state_dir
|
||||
from pm.environments import install_state_dir
|
||||
from hermes_cli.runtime_state import _lock
|
||||
|
||||
with (install_state_dir(repo) / ".install.lock").open("a+b") as lock:
|
||||
@@ -237,7 +237,7 @@ def test_lazy_disabled_sync_does_not_bootstrap_tools(client, tmp_path, monkeypat
|
||||
|
||||
def test_invalid_selection_waits_for_failed_receipt_and_lock_release(client, tmp_path, monkeypatch):
|
||||
import json
|
||||
from hermes_cli.runtime_paths import install_state_dir
|
||||
from pm.environments import install_state_dir
|
||||
from hermes_cli.runtime_state import _lock
|
||||
|
||||
repo = _current_environment(tmp_path, monkeypatch, [])
|
||||
@@ -463,7 +463,7 @@ def test_resolution_conflict_survives_worker_and_receipt(client, tmp_path, monke
|
||||
|
||||
def test_failed_facts_write_restores_exact_config_before_reporting(client, tmp_path, monkeypatch, isolated_python):
|
||||
from tests.pm._fixtures import worker_toolchain
|
||||
from hermes_cli.runtime_paths import install_state_dir
|
||||
from pm.environments import install_state_dir
|
||||
|
||||
repo = _current_environment(tmp_path, monkeypatch, [])
|
||||
home = tmp_path / "home"
|
||||
@@ -502,7 +502,7 @@ def test_worker_death_reports_transport_failure(client, monkeypatch, isolated_py
|
||||
|
||||
def test_foreign_checkout_sync_uses_its_own_pm_generation(client, tmp_path, monkeypatch, isolated_python):
|
||||
from pm import venv_is_current
|
||||
from hermes_cli.runtime_paths import selected_venv, runtime_facts_path
|
||||
from pm.environments import selected_venv, runtime_facts_path
|
||||
|
||||
from tests.pm._fixtures import worker_toolchain
|
||||
worker_toolchain(client, monkeypatch, isolated_python)
|
||||
|
||||
@@ -15,7 +15,7 @@ from tests.pm._fixtures import worker_toolchain
|
||||
|
||||
|
||||
def test_worker_publishes_selection_even_when_dependencies_are_current(client, tmp_path, monkeypatch):
|
||||
from hermes_cli.runtime_paths import install_state_dir
|
||||
from pm.environments import install_state_dir
|
||||
from pm import receipt
|
||||
|
||||
home = tmp_path / "home"
|
||||
@@ -47,7 +47,7 @@ def test_worker_publishes_selection_even_when_dependencies_are_current(client, t
|
||||
def test_staged_plugin_publication_uses_installed_identity_and_local_dependencies(
|
||||
client, tmp_path, monkeypatch, isolated_python, active, missing,
|
||||
):
|
||||
from hermes_cli.runtime_paths import install_state_dir, selected_venv
|
||||
from pm.environments import install_state_dir, selected_venv
|
||||
from pm.store import tree_digest
|
||||
from tests.pm.test_environment_build import _wheel
|
||||
from pm import paths
|
||||
@@ -105,7 +105,7 @@ def test_staged_plugin_publication_uses_installed_identity_and_local_dependencie
|
||||
|
||||
@pytest.mark.parametrize("mutation", ["sibling", "active"])
|
||||
def test_selection_refuses_config_edits_during_preparation(client, tmp_path, monkeypatch, isolated_python, mutation):
|
||||
from hermes_cli.runtime_paths import install_state_dir
|
||||
from pm.environments import install_state_dir
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
config = home / "config.yaml"
|
||||
@@ -178,7 +178,7 @@ def test_memory_setup_sends_candidate_paths_instead_of_discovery_callbacks(tmp_p
|
||||
def test_worker_death_recovers_at_each_durable_publication_boundary(
|
||||
client, tmp_path, monkeypatch, isolated_python, kind, rebuild, phase,
|
||||
):
|
||||
from hermes_cli.runtime_paths import install_state_dir, selected_venv
|
||||
from pm.environments import install_state_dir, selected_venv
|
||||
from pm import paths
|
||||
from pm.package import InstallError
|
||||
from pm.store import tree_digest
|
||||
@@ -243,7 +243,7 @@ def test_worker_death_recovers_at_each_durable_publication_boundary(
|
||||
assert (state / "publication.json").exists()
|
||||
source = Path(client.__file__).resolve().parent.parent
|
||||
program = (f"import sys; sys.path.insert(0, {str(source)!r}); from pathlib import Path; "
|
||||
"from hermes_cli.runtime_paths import activate_dependencies; "
|
||||
"from pm.environments import activate_dependencies; "
|
||||
f"project = Path({str(project)!r})\n"
|
||||
"activate_dependencies(project)\nactivate_dependencies(project)\n")
|
||||
recovery = subprocess.run([sys.executable, "-I", "-S", "-c", program], capture_output=True, text=True,
|
||||
@@ -266,7 +266,7 @@ def test_worker_death_recovers_at_each_durable_publication_boundary(
|
||||
|
||||
@pytest.mark.parametrize("mutation", ["metadata", "target", "staged", "sibling-manifest"])
|
||||
def test_staged_publication_refuses_concurrent_input_edits(client, tmp_path, monkeypatch, isolated_python, mutation):
|
||||
from hermes_cli.runtime_paths import install_state_dir
|
||||
from pm.environments import install_state_dir
|
||||
from pm.store import tree_digest
|
||||
repo = _current_environment(tmp_path, monkeypatch, [])
|
||||
home = tmp_path / "home"
|
||||
@@ -342,7 +342,7 @@ def test_selection_preserves_yaml11_values_and_quotes_plugin_names(client, tmp_p
|
||||
|
||||
def test_explicit_publication_keeps_its_intent_through_tool_acquisition(client, tmp_path, monkeypatch, isolated_python):
|
||||
from pm import paths
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from pm.environments import selected_venv
|
||||
project = tmp_path / "project"
|
||||
project.mkdir()
|
||||
monkeypatch.setattr(paths, "repo_root", lambda: project)
|
||||
|
||||
@@ -23,7 +23,7 @@ def test_bundle_stages_git_tree_and_runs_native_children_before_manifest(tmp_pat
|
||||
import importlib
|
||||
import inspect
|
||||
|
||||
from hermes_cli.runtime_paths import site_packages
|
||||
from pm.environments import site_packages
|
||||
from pm.lock import Facts
|
||||
from pm.registry import get_package
|
||||
from pm.store import tree_digest
|
||||
@@ -55,7 +55,7 @@ def test_bundle_stages_git_tree_and_runs_native_children_before_manifest(tmp_pat
|
||||
source = Path(__file__).resolve().parents[2]
|
||||
shutil.copytree(source / "pm", repo / "pm", ignore=shutil.ignore_patterns("__pycache__"))
|
||||
(repo / "hermes_cli").mkdir()
|
||||
for name in ("__init__.py", "runtime_paths.py", "runtime_state.py"):
|
||||
for name in ("__init__.py", "runtime_state.py"):
|
||||
shutil.copy2(source / "hermes_cli" / name, repo / "hermes_cli" / name)
|
||||
shutil.copy2(source / "hermes_constants.py", repo / "hermes_constants.py")
|
||||
wheels = repo / "wheels"
|
||||
|
||||
@@ -15,7 +15,7 @@ def test_stamp_uses_built_commit_even_with_dispatch_sha_and_refuses_mismatch(tmp
|
||||
repo.mkdir()
|
||||
for relative in ('scripts/write_install_stamp.py', 'scripts/releases/commit_build.py',
|
||||
'hermes_cli/__init__.py', 'hermes_cli/update_channel.py', 'hermes_cli/release_channels.py',
|
||||
'hermes_cli/runtime_paths.py', 'hermes_cli/steward.py', 'hermes_constants.py'):
|
||||
'pm/environments.py', 'hermes_cli/steward.py', 'hermes_constants.py'):
|
||||
dest = repo / relative
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(ROOT / relative, dest)
|
||||
|
||||
@@ -139,7 +139,7 @@ def test_bootstrap_real_pm_resolves_only_build_owned_state(tmp_path):
|
||||
probe = (
|
||||
"import json, sys; from pathlib import Path; sys.path.insert(0, sys.argv[1]); "
|
||||
"from pm import paths; from pm.packages import uv_cache_dir; "
|
||||
"from hermes_cli.runtime_paths import install_state_dir; "
|
||||
"from pm.environments import install_state_dir; "
|
||||
"print(json.dumps([str(paths.store_root()), str(paths.partials_root()), "
|
||||
"str(uv_cache_dir()), str(install_state_dir(Path(sys.argv[1])))]))"
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ def payload_tree(tmp_path: Path):
|
||||
|
||||
# Exercise the real bootstrap before the fixture entry point.
|
||||
for relative in ("hermes_bootstrap.py", "hermes_constants.py", "hermes_cli/__init__.py",
|
||||
"hermes_cli/runtime_paths.py", "hermes_cli/runtime_state.py",
|
||||
"pm/environments.py", "hermes_cli/runtime_state.py",
|
||||
"hermes_cli/_early_recovery.py", "hermes_cli/_parser.py"):
|
||||
shutil.copy2(_REPO / relative, repo / relative)
|
||||
(repo / "hermes_cli" / "main.py").write_text(
|
||||
|
||||
@@ -22,7 +22,7 @@ def _exercise_relocated_pm_runtime(tmp_path, monkeypatch):
|
||||
source = Path(__file__).resolve().parents[2]
|
||||
shutil.copytree(source / "pm", repo / "pm", ignore=shutil.ignore_patterns("__pycache__", ".hermes-tmp.*"))
|
||||
(repo / "hermes_cli").mkdir()
|
||||
for name in ("__init__.py", "runtime_paths.py", "runtime_state.py"):
|
||||
for name in ("__init__.py", "runtime_state.py"):
|
||||
shutil.copy2(source / "hermes_cli" / name, repo / "hermes_cli" / name)
|
||||
shutil.copy2(source / "hermes_constants.py", repo / "hermes_constants.py")
|
||||
# Copy the base executable, not a venv's launcher. The test host provides
|
||||
|
||||
@@ -35,7 +35,7 @@ def fixture_repo(tmp_path):
|
||||
'scripts/releases/r2.py', 'scripts/releases/r2_scope.py', 'scripts/release-content-types.json',
|
||||
'hermes_cli/__init__.py', 'hermes_cli/update_channel.py',
|
||||
'hermes_cli/release_channels.py',
|
||||
'hermes_cli/runtime_paths.py', 'hermes_constants.py'):
|
||||
'pm/environments.py', 'hermes_constants.py'):
|
||||
dest = repo / relative
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(ROOT / relative, dest)
|
||||
|
||||
@@ -56,7 +56,7 @@ def test_development_setup_keeps_test_groups_out_of_the_runtime(tmp_path, monkey
|
||||
assert result.stdout.strip() == str("dev" in extras)
|
||||
assert Path(outputs["venv"]).is_relative_to(home)
|
||||
assert not (core / ".venv").exists()
|
||||
from hermes_cli.runtime_paths import runtime_facts_path
|
||||
from pm.environments import runtime_facts_path
|
||||
assert runtime_facts_path(core).exists() == ("dev" not in extras)
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ def _stamp_probe(tmp_path, shell):
|
||||
repo = tmp_path / "installed source"
|
||||
for relative in (
|
||||
"scripts/write_install_stamp.py", "hermes_cli/__init__.py",
|
||||
"hermes_cli/update_channel.py", "hermes_cli/runtime_paths.py",
|
||||
"hermes_cli/update_channel.py", "pm/environments.py",
|
||||
"hermes_cli/steward.py", "hermes_constants.py",
|
||||
):
|
||||
dest = repo / relative
|
||||
|
||||
@@ -146,7 +146,7 @@ root, store, node, deps = map(Path, sys.argv[1:])
|
||||
sys.path.insert(0, str(root))
|
||||
from pm.lock import Facts, Lockfile
|
||||
from pm.packages import Venv
|
||||
from hermes_cli.runtime_paths import install_state_dir, runtime_facts_path, site_packages
|
||||
from pm.environments import install_state_dir, runtime_facts_path, site_packages
|
||||
from hermes_cli._launchers import ensure_install_launchers
|
||||
lock = Lockfile(root / 'pm/lock.json')
|
||||
lock.set_pin('node', 'fixture', {})
|
||||
|
||||
@@ -64,7 +64,7 @@ def test_candidate_member_dirs_preserves_proposed_home_order_and_extras(isolated
|
||||
|
||||
@pytest.fixture
|
||||
def publication(isolated_home, tmp_path):
|
||||
from hermes_cli.runtime_paths import install_state_dir
|
||||
from pm.environments import install_state_dir
|
||||
|
||||
project = tmp_path / "checkout"
|
||||
project.mkdir()
|
||||
@@ -115,7 +115,7 @@ plugins_transaction.recover_plugin_publication(
|
||||
|
||||
@pytest.mark.parametrize("commit", ["rollback", "explicit", "facts-changed"])
|
||||
def test_old_publication_recovers_in_stdlib_using_supplied_row_and_journal(publication, commit):
|
||||
from hermes_cli.runtime_paths import runtime_facts_path
|
||||
from pm.environments import runtime_facts_path
|
||||
|
||||
project, row, journal, canonical = publication
|
||||
target, backup, metadata = (Path(row[key]) for key in ("target", "backup", "metadata"))
|
||||
|
||||
@@ -7,7 +7,7 @@ import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.runtime_paths import install_state_dir, site_packages
|
||||
from pm.environments import install_state_dir, site_packages
|
||||
from pm.lock import Lockfile
|
||||
from tests.hermes_cli.test_source_launcher_publication import fixture_tree
|
||||
|
||||
|
||||
@@ -282,7 +282,7 @@ def test_launcher_alias_provenance(child_env, monkeypatch, link_at, profile):
|
||||
|
||||
@pytest.mark.parametrize("has_facts", [True, False])
|
||||
def test_runtime_provenance_is_independent_of_aliases_and_virtual_env(child_env, monkeypatch, has_facts):
|
||||
from hermes_cli.runtime_paths import runtime_facts_path
|
||||
from pm.environments import runtime_facts_path
|
||||
payload = child_env / "payload"
|
||||
runtime = payload / "state/environments/candidate/venv"
|
||||
site = runtime / ("Lib/site-packages" if os.name == "nt" else
|
||||
@@ -291,7 +291,7 @@ def test_runtime_provenance_is_independent_of_aliases_and_virtual_env(child_env,
|
||||
(runtime / "pyvenv.cfg").write_text("version = 3.14\n", encoding="utf-8")
|
||||
(payload / "tools").mkdir()
|
||||
(payload / "manifest.json").write_text("{}", encoding="utf-8")
|
||||
monkeypatch.setattr("hermes_cli.runtime_paths.install_state_dir", lambda repo: payload / "state")
|
||||
monkeypatch.setattr("pm.environments.install_state_dir", lambda repo: payload / "state")
|
||||
if has_facts:
|
||||
facts = runtime_facts_path(Path(__file__).resolve().parents[2])
|
||||
facts.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -123,7 +123,7 @@ def test_warm_sdk_uses_pm_availability_and_refuses_inactive_generation(tmp_path,
|
||||
import pm.client
|
||||
import pm.paths
|
||||
import pm.extras
|
||||
from hermes_cli.runtime_paths import runtime_facts_path
|
||||
from pm.environments import runtime_facts_path
|
||||
|
||||
calls = []
|
||||
root = tmp_path / "repo"
|
||||
|
||||
@@ -21,8 +21,9 @@ from hermes_cli._subprocess_compat import windows_hide_flags
|
||||
from tools.computer_use.backend import ActionResult, ComputerUseBackend
|
||||
from tools.computer_use.cua_backend_capture import _CaptureMixin
|
||||
from tools.computer_use.cua_backend_daemon import _EmbeddedCuaDaemon
|
||||
from tools.computer_use.cua_backend_driver import (
|
||||
_CUA_DRIVER_CMD_ENV, cua_driver_binary_available, cua_driver_runtime_contract_status)
|
||||
from tools.computer_use.cua_backend_driver import ( # noqa: F401 — resolve_cua_driver_cmd: frozen updater surface
|
||||
_CUA_DRIVER_CMD_ENV, cua_driver_binary_available, cua_driver_runtime_contract_status,
|
||||
resolve_cua_driver_cmd)
|
||||
from tools.computer_use.cua_backend_input import _InputMixin
|
||||
from tools.computer_use.cua_backend_parse import _action_result_from
|
||||
from tools.computer_use.cua_backend_session import _AsyncBridge, _CuaDriverSession
|
||||
|
||||
@@ -74,7 +74,7 @@ def _validated_runtime_venv(env: dict) -> Path | None:
|
||||
"""Producer-owned runtime venv identified by VIRTUAL_ENV, or None. The variable
|
||||
alone is not provenance (users carry unrelated venvs): require the legacy Windows
|
||||
base-Python producer's exact ``<repo>/venv`` layout AND a real ``pyvenv.cfg``."""
|
||||
from hermes_cli.runtime_paths import runtime_facts_path, selected_venv
|
||||
from pm.environments import runtime_facts_path, selected_venv
|
||||
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
if runtime_facts_path(root).is_file():
|
||||
@@ -111,7 +111,7 @@ def _get_hermes_site_packages(env: dict) -> list[Path]:
|
||||
|
||||
runtime_venv = _validated_runtime_venv(env)
|
||||
if runtime_venv is not None:
|
||||
from hermes_cli.runtime_paths import site_packages
|
||||
from pm.environments import site_packages
|
||||
runtime_site_packages = site_packages(runtime_venv)
|
||||
if not any(_same_path(runtime_site_packages, existing) for existing in result):
|
||||
result.append(runtime_site_packages)
|
||||
|
||||
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user