diff --git a/plugins/platforms/teams/adapter.py b/plugins/platforms/teams/adapter.py index 3cb96ffc72..578d250cce 100644 --- a/plugins/platforms/teams/adapter.py +++ b/plugins/platforms/teams/adapter.py @@ -1475,22 +1475,19 @@ def _install_hint() -> str: """Build the Teams install hint from the canonical LAZY_DEPS pins. Derived (not hardcoded) so a pin bump in ``tools/lazy_deps.py`` — aiohttp - is CVE-pinned, so bumps happen — never leaves this string stale, and - ``sys.executable -m pip`` targets the actual Hermes venv in every layout - (default install, ``HERMES_HOME`` override, profile installs) instead of - a hardcoded ``~/.hermes`` path. Also sidesteps Ubuntu 24.04's PEP 668 - ``externally-managed-environment`` failure that a bare ``pip install`` - hint invites. + is CVE-pinned, so bumps happen — never leaves this string stale. + ``feature_install_command(venv_pip=True)`` targets the actual Hermes + venv in every layout and sidesteps Ubuntu 24.04's PEP 668 failure that + a bare ``pip install`` hint invites. """ try: - from tools.lazy_deps import feature_specs - specs = " ".join(f"'{s}'" for s in feature_specs("platform.teams")) + from tools.lazy_deps import feature_install_command + cmd = feature_install_command("platform.teams", venv_pip=True) except Exception: # pragma: no cover — defensive - specs = "'microsoft-teams-apps' 'aiohttp'" - return ( - "Teams SDK missing — restart the gateway to auto-install, or run: " - f"{sys.executable} -m pip install {specs}" - ) + cmd = None + if not cmd: + cmd = f"{sys.executable} -m pip install microsoft-teams-apps aiohttp" + return f"Teams SDK missing — restart the gateway to auto-install, or run: {cmd}" def register(ctx) -> None: diff --git a/tests/tools/test_lazy_deps.py b/tests/tools/test_lazy_deps.py index 61b1dcb790..dd23b16cee 100644 --- a/tests/tools/test_lazy_deps.py +++ b/tests/tools/test_lazy_deps.py @@ -83,6 +83,20 @@ class TestAllowlist: def test_feature_install_command_unknown(self): assert ld.feature_install_command("not.real") is None + assert ld.feature_install_command("not.real", venv_pip=True) is None + + def test_feature_install_command_venv_pip_targets_interpreter(self): + # venv_pip=True must target the running interpreter's pip (correct in + # every install layout, immune to PEP 668) and carry the same specs + # as the default uv form. + import sys as _sys + default = ld.feature_install_command("platform.teams") + venv = ld.feature_install_command("platform.teams", venv_pip=True) + assert default is not None and venv is not None + assert venv.startswith(f"{_sys.executable} -m pip install ") + assert default.startswith("uv pip install ") + # Same spec tail on both forms. + assert venv.split(" -m pip install ", 1)[1] == default.split("uv pip install ", 1)[1] # --------------------------------------------------------------------------- diff --git a/tools/lazy_deps.py b/tools/lazy_deps.py index babfa3c044..a7f20b748e 100644 --- a/tools/lazy_deps.py +++ b/tools/lazy_deps.py @@ -971,12 +971,23 @@ def is_available(feature: str) -> bool: return not feature_missing(feature) -def feature_install_command(feature: str) -> Optional[str]: - """Return the ``pip install`` command a user could run manually, or None.""" +def feature_install_command(feature: str, *, venv_pip: bool = False) -> Optional[str]: + """Return the ``pip install`` command a user could run manually, or None. + + ``venv_pip=True`` targets the running interpreter's pip + (``{sys.executable} -m pip install …``) — correct in every layout + (default install, ``HERMES_HOME`` overrides, profile installs) and + immune to Ubuntu 24.04's PEP 668 ``externally-managed-environment`` + failure that a bare/system ``pip install`` hint invites. The default + ``uv pip install`` form is kept for contexts that document uv usage. + """ if feature not in LAZY_DEPS: return None specs = LAZY_DEPS[feature] - return "uv pip install " + " ".join(repr(s) for s in specs) + joined = " ".join(repr(s) for s in specs) + if venv_pip: + return f"{sys.executable} -m pip install {joined}" + return "uv pip install " + joined @dataclass