From 7f6ca14a5f6739cdf8269293268cdb1770e68d8a Mon Sep 17 00:00:00 2001 From: liuhao1024 Date: Mon, 3 Aug 2026 14:03:40 +0530 Subject: [PATCH] fix(lazy-deps): skip the install ladder on package-manager installs Salvage of #48637 (Fixes #48628). On a NixOS-style install the venv's site-packages lives in the read-only store, so ensure()'s uv -> pip -> ensurepip ladder spends ~15s bootstrapping ensurepip only to fail against a target it can never write. Fail fast with an actionable message pointing at the system package manager. Retargeted onto current main (the PR's base predates the durable-target subsystem by ~8.1K commits) with two corrections to the original: - Gate on _lazy_install_target() is None. The container deployment sets HERMES_MANAGED=true AND HERMES_LAZY_INSTALL_TARGET (a writable volume); the original guard would have blocked installs that path legitimately satisfies, breaking the NixOS-container mode. - Reason string starts with 'unsupported ' because refresh_active_features classifies FeatureUnavailable by that prefix; the original wording made 'hermes update' report a hard failure instead of a skip. Placed after _unsupported_feature_reason so a platform-specific reason (more actionable) wins, and so ensure() agrees with refresh_active_features, which pre-checks that same function. --- tests/tools/test_lazy_deps_managed.py | 120 ++++++++++++++++++++++++++ tools/lazy_deps.py | 29 +++++++ 2 files changed, 149 insertions(+) create mode 100644 tests/tools/test_lazy_deps_managed.py diff --git a/tests/tools/test_lazy_deps_managed.py b/tests/tools/test_lazy_deps_managed.py new file mode 100644 index 0000000000000..bd756d59eb069 --- /dev/null +++ b/tests/tools/test_lazy_deps_managed.py @@ -0,0 +1,120 @@ +"""Managed-install guard in :func:`tools.lazy_deps.ensure` (#48628). + +A package-manager install (NixOS, and anything else shipping Hermes from a +read-only store) cannot receive lazy pip installs: the venv's site-packages +lives in the store, so the uv -> pip -> ensurepip ladder burns ~15s +bootstrapping ensurepip only to fail. ``ensure()`` must fail fast instead. +""" + +import pytest + +from tools import lazy_deps +from tools.lazy_deps import FeatureUnavailable + + +FEATURE = "provider.anthropic" + + +@pytest.fixture(autouse=True) +def _missing_and_installable(monkeypatch): + """Reach the guard: deps missing, installs allowed, no durable target. + + ``_allow_lazy_installs`` is patched explicitly so the suite does not + depend on the host's ~/.hermes/config.yaml (a local + ``allow_lazy_installs: false`` otherwise short-circuits with a different + rejection reason). + """ + monkeypatch.setattr(lazy_deps, "feature_missing", lambda _f: ("some-pkg==1.0",)) + monkeypatch.setattr(lazy_deps, "_allow_lazy_installs", lambda: True) + monkeypatch.setattr(lazy_deps, "_lazy_install_target", lambda: None) + + +def _no_installer(monkeypatch): + """Fail loudly if the guard lets execution reach the install ladder.""" + def _boom(*_a, **_kw): + raise AssertionError("guard let execution reach the install ladder") + + monkeypatch.setattr(lazy_deps.subprocess, "run", _boom) + + +def test_nixos_install_fails_fast_without_touching_the_installer(monkeypatch): + monkeypatch.setattr("hermes_cli.config.get_managed_system", lambda: "nixos") + _no_installer(monkeypatch) + + with pytest.raises(FeatureUnavailable) as excinfo: + lazy_deps.ensure(FEATURE, prompt=False) + + assert "nixos" in excinfo.value.reason + # refresh_active_features classifies by this prefix — anything else is + # reported to the user as a hard failure instead of a skip. + assert excinfo.value.reason.startswith("unsupported ") + + +def test_reason_is_classified_as_skipped_not_failed(monkeypatch): + """The wording contract with refresh_active_features, pinned directly.""" + monkeypatch.setattr("hermes_cli.config.get_managed_system", lambda: "nixos") + + with pytest.raises(FeatureUnavailable) as excinfo: + lazy_deps.ensure(FEATURE, prompt=False) + + assert excinfo.value.reason.startswith("unsupported "), ( + "refresh_active_features would report this as failed: rather than skipped:" + ) + + +def test_unmanaged_install_is_not_blocked_by_the_guard(monkeypatch): + """On a normal pip install the guard must be transparent.""" + monkeypatch.setattr("hermes_cli.config.get_managed_system", lambda: None) + + with pytest.raises(FeatureUnavailable) as excinfo: + lazy_deps.ensure(FEATURE, prompt=False) + + # Whatever stops the install here, it must NOT be the managed guard. + assert "managed installs" not in excinfo.value.reason + + +def test_durable_install_target_overrides_the_guard(monkeypatch, tmp_path): + """The container deployment sets HERMES_MANAGED *and* a writable target. + + Dockerfile sets HERMES_LAZY_INSTALL_TARGET and the NixOS container module + passes HERMES_MANAGED=true; blocking there would break that deployment. + """ + monkeypatch.setattr("hermes_cli.config.get_managed_system", lambda: "nixos") + monkeypatch.setattr(lazy_deps, "_lazy_install_target", lambda: tmp_path) + + with pytest.raises(FeatureUnavailable) as excinfo: + lazy_deps.ensure(FEATURE, prompt=False) + + assert "nixos" not in excinfo.value.reason.lower(), ( + "durable-target installs must not be blocked by the managed guard" + ) + + +def test_platform_unsupported_takes_precedence(monkeypatch): + """A platform-specific reason is more actionable than 'managed install'. + + Also required for consistency: refresh_active_features pre-checks + _unsupported_feature_reason before calling ensure(). + """ + monkeypatch.setattr("hermes_cli.config.get_managed_system", lambda: "nixos") + monkeypatch.setattr( + lazy_deps, "_unsupported_feature_reason", lambda _f: "unsupported on win32" + ) + + with pytest.raises(FeatureUnavailable) as excinfo: + lazy_deps.ensure(FEATURE, prompt=False) + + assert excinfo.value.reason == "unsupported on win32" + + +def test_unreadable_config_fails_open(monkeypatch): + """A broken config must not block installs on a normal pip install.""" + def _raise(): + raise RuntimeError("config unreadable") + + monkeypatch.setattr("hermes_cli.config.get_managed_system", _raise) + + with pytest.raises(FeatureUnavailable) as excinfo: + lazy_deps.ensure(FEATURE, prompt=False) + + assert "managed" not in excinfo.value.reason.lower() diff --git a/tools/lazy_deps.py b/tools/lazy_deps.py index 15992f7006b9f..926aa0d868bbd 100644 --- a/tools/lazy_deps.py +++ b/tools/lazy_deps.py @@ -847,6 +847,35 @@ def ensure(feature: str, *, prompt: bool = True) -> None: if unsupported: raise FeatureUnavailable(feature, missing, unsupported) + # Package-manager installs (NixOS, and any other distro that ships Hermes + # from a read-only store) cannot receive lazy pip installs: the venv's + # site-packages lives in the store, so the uv -> pip -> ensurepip ladder + # below burns ~15s bootstrapping ensurepip only to fail on a read-only + # target. Fail fast with an actionable message instead. + # + # Skipped when a durable install target is configured: the container + # deployment sets HERMES_MANAGED=true *and* HERMES_LAZY_INSTALL_TARGET + # (a writable volume), where lazy installs legitimately work. + # + # The reason string starts with "unsupported " on purpose: + # refresh_active_features classifies FeatureUnavailable by that prefix and + # reports anything else as a hard failure rather than a skip. + if _lazy_install_target() is None: + try: + from hermes_cli.config import get_managed_system + + managed_by = get_managed_system() + except Exception: + managed_by = "" # config unreadable — proceed with the install + if managed_by: + raise FeatureUnavailable( + feature, missing, + f"unsupported on {managed_by}-managed installs: this build's " + f"packages come from {managed_by}, so Hermes cannot install " + f"them at runtime. Add the dependencies for {feature!r} via " + f"{managed_by} (or run a pip/uv install of Hermes instead)." + ) + # Validate every spec against the allowlist + safety regex. Belt and # braces — the keys-in-LAZY_DEPS check above already constrains this. for spec in missing: