From 87d0faddedf92f1a5726d83cedc4c0ad2a673ba9 Mon Sep 17 00:00:00 2001 From: ethernet Date: Thu, 24 Sep 2026 11:08:35 -0400 Subject: [PATCH] fix(pm): resolve the install stamp in pm.paths so sealed stages can read it b207701287 routed PM's stamp reads through hermes_cli.steward.install_stamp_path. The Docker runtime base runs PM before hermes_cli ships (only __init__ and runtime_state are copied), so ensure() died with No module named 'hermes_cli.steward'. The resolver is an install-root path built only on pm.paths.install_root/repo_root; move it there and point every reader at it. --- hermes_cli/post_update.py | 2 +- hermes_cli/steward.py | 20 ++----------------- hermes_cli/venv_sync.py | 3 ++- hermes_cli/version_info.py | 5 ++--- pm/environments.py | 2 +- pm/paths.py | 16 +++++++++++++++ pm/runtime.py | 2 +- .../hermes_cli/test_doctor_legacy_checkout.py | 2 +- .../hermes_cli/test_uninstall_install_kind.py | 2 +- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/hermes_cli/post_update.py b/hermes_cli/post_update.py index 17e36d591c..25ffb566d4 100644 --- a/hermes_cli/post_update.py +++ b/hermes_cli/post_update.py @@ -186,7 +186,7 @@ def step_adopt_blessed_checkout(project_root: Path | None = None) -> dict: if not (root / ".git").exists(): return {"ok": True, "skipped": "not-a-checkout"} - from hermes_cli.steward import install_stamp_path + from pm.paths import install_stamp_path stamp_path = install_stamp_path(root) if stamp_path.exists(): return {"ok": True, "skipped": "already-stamped"} diff --git a/hermes_cli/steward.py b/hermes_cli/steward.py index 95d9668960..5db757f0e2 100644 --- a/hermes_cli/steward.py +++ b/hermes_cli/steward.py @@ -24,7 +24,6 @@ import sys from pathlib import Path from typing import Optional -BUILD_INFO_NAME = "install-stamp.json" UPDATE_MECHANISMS = ("self", "app-installer", "electron-updater", "external", "microsoft-store") STEWARD_DESKTOP = "desktop-app" @@ -177,23 +176,6 @@ def steward_uninstall_message(steward: str, platform: "str | None" = None) -> st return _STEWARD_UNINSTALL_FALLBACK.format(steward=steward) -def install_stamp_path(project_root: Path) -> Path: - """THE stamp location for ``project_root``, shared by every stamp reader. - - Beside the code in checkouts, Docker and desktop payloads. A Nix package - bakes the stamp outside the store's package dir and its wrapper carries - ``HERMES_INSTALL_ROOT`` for the executing tree only — so the executing - tree resolves through pm.paths.install_root, any other tree is taken - literally. Two resolvers here once classified Nix as "unknown". - """ - from pm.paths import install_root, repo_root - - root = Path(project_root) - if root.resolve() == repo_root(): - root = install_root() - return root / BUILD_INFO_NAME - - def read_install_stamp(project_root: Path) -> dict: """The build stamp of ``project_root``, or ``{}``. @@ -201,6 +183,8 @@ def read_install_stamp(project_root: Path) -> dict: path on a malformed stamp — a tree we cannot prove is ours still refuses (see :func:`sealed_steward`), so garbage degrades safely. """ + from pm.paths import install_stamp_path + try: data = json.loads(install_stamp_path(project_root).read_text(encoding="utf-8-sig")) except (OSError, ValueError): diff --git a/hermes_cli/venv_sync.py b/hermes_cli/venv_sync.py index 2cfe4c79e9..9dd35f8515 100644 --- a/hermes_cli/venv_sync.py +++ b/hermes_cli/venv_sync.py @@ -32,7 +32,8 @@ def _is_sealed(project_root: Path) -> bool: """ if (project_root / ".git").exists(): return False - from hermes_cli.steward import install_stamp_path, read_install_stamp + from hermes_cli.steward import read_install_stamp + from pm.paths import install_stamp_path stamp_path = install_stamp_path(project_root) data = read_install_stamp(project_root) if not data: diff --git a/hermes_cli/version_info.py b/hermes_cli/version_info.py index 6fb7d32200..bd76778708 100644 --- a/hermes_cli/version_info.py +++ b/hermes_cli/version_info.py @@ -109,9 +109,8 @@ def _calver_release_version(repo_dir: Path) -> tuple[str, int] | None: # --- Install stamp reader --------------------------------------------------- def _resolve_stamp_file() -> Path | None: - """The executing tree's stamp (steward.install_stamp_path owns the location).""" - from hermes_cli.steward import install_stamp_path - from pm.paths import repo_root + """The executing tree's stamp (pm.paths.install_stamp_path owns the location).""" + from pm.paths import install_stamp_path, repo_root p = install_stamp_path(repo_root()) return p if p.is_file() else None diff --git a/pm/environments.py b/pm/environments.py index 0c6c4afcf3..6095b238fa 100644 --- a/pm/environments.py +++ b/pm/environments.py @@ -107,7 +107,7 @@ def store_root(project_root: Path) -> Path: if not store.is_relative_to(root.parent): raise RuntimeError("payload store escapes its root") return store - from hermes_cli.steward import install_stamp_path + from pm.paths import install_stamp_path for directory in (root, *root.parents): stamp = install_stamp_path(directory) diff --git a/pm/paths.py b/pm/paths.py index 2904023a28..52d4f0a882 100644 --- a/pm/paths.py +++ b/pm/paths.py @@ -18,6 +18,22 @@ def install_root() -> Path: return Path(env) if env else repo_root() +def install_stamp_path(project_root: Path) -> Path: + """THE stamp location for ``project_root``, shared by every stamp reader. + + Beside the code in checkouts, Docker and desktop payloads. A Nix package + bakes the stamp outside the store's package dir and its wrapper carries + ``HERMES_INSTALL_ROOT`` for the executing tree only — so the executing + tree resolves through install_root, any other tree is taken literally. + PM reads it in sealed stages (the Docker runtime base) before hermes_cli + ships, so it lives here rather than beside the stewards. + """ + root = Path(project_root) + if root.resolve() == repo_root(): + root = install_root() + return root / "install-stamp.json" + + def lockfile_path() -> Path: return Path(__file__).resolve().parent / "lock.json" diff --git a/pm/runtime.py b/pm/runtime.py index ce5fdb9a4c..5ac889d2bd 100644 --- a/pm/runtime.py +++ b/pm/runtime.py @@ -61,7 +61,7 @@ def _resident_runtime() -> tuple[Path, Path] | None: if payload is not None: runtime = payload / "pm-runtime" else: - from hermes_cli.steward import install_stamp_path + from pm.paths import install_stamp_path stamp_path = install_stamp_path(project) try: diff --git a/tests/hermes_cli/test_doctor_legacy_checkout.py b/tests/hermes_cli/test_doctor_legacy_checkout.py index 274bdd68a5..330254f05b 100644 --- a/tests/hermes_cli/test_doctor_legacy_checkout.py +++ b/tests/hermes_cli/test_doctor_legacy_checkout.py @@ -23,7 +23,7 @@ def embedded_context(tmp_path, monkeypatch): """Running tree = sealed desktop-app; legacy checkout at the managed root.""" bundle = tmp_path / "bundle" / "repo" bundle.mkdir(parents=True) - # The code-scoped stamp (hermes_cli.steward.BUILD_INFO_NAME). + # The code-scoped stamp (pm.paths.install_stamp_path). (bundle / "install-stamp.json").write_text( json.dumps({"commit": "a" * 40, "distribution": "desktop-app", "updateMechanism": "electron-updater"}) ) diff --git a/tests/hermes_cli/test_uninstall_install_kind.py b/tests/hermes_cli/test_uninstall_install_kind.py index 8da30658c8..f77d9471b1 100644 --- a/tests/hermes_cli/test_uninstall_install_kind.py +++ b/tests/hermes_cli/test_uninstall_install_kind.py @@ -68,7 +68,7 @@ def _fake_project_root(monkeypatch, tmp_path: Path, *, git: bool, distribution: if git: (root / ".git").mkdir() if distribution is not None: - # The code-scoped stamp (see hermes_cli.steward.BUILD_INFO_NAME). + # The code-scoped stamp (see pm.paths.install_stamp_path). (root / "install-stamp.json").write_text( json.dumps({"distribution": distribution, "updateMechanism": "external"}) , encoding="utf-8")