"""Stable macOS TCC anchor for the uv-managed Python interpreter (#95596). Re-land of the interpreter anchor reverted in #95563. macOS keys TCC grants to the resolved absolute path of the client binary. Hermes' interpreter is managed by uv and lives at a versioned store path; every patch bump orphans every prior grant (#85345). The first landing copied the interpreter into ``venv/bin/python`` but left two holes that bricked real Macs: * Dynamically-linked builds look up ``libpython`` via ``@executable_path/../lib``. That resolved into ``venv/lib/``, which had no dylib — every hermes command, including update/doctor, died in dyld (#95425). * Alias names (``python3``, ``python3.N``) were re-pointed at the copy as *symlinks*. Invoking the copied interpreter through a symlink makes CPython getpath lose the venv prefix on affected python-build-standalone builds — startup dies with ``ModuleNotFoundError: encodings`` and the stdlib resolves to the build-time ``/install`` prefix (#95541). Console scripts exec ``python3``, so the entire CLI surface died. This re-land keeps the copy + identifier-pinned signature (TCC attribution stays on the stable venv path) and closes both holes: 1. Aliases are materialized as real-file copies of the anchor, never symlinks. 2. If the store ships ``libpython*``, it is hardlinked into ``venv/lib/`` (copy if the store is on another device). Existing ``LC_RPATH`` already points at ``@executable_path/../lib`` — no rewrite. 3. A pre-install boot gate actually launches the staged copy and demands ``import encodings`` plus ``sys.prefix == ``. Failure rolls the staging file back and leaves the live interpreter untouched (a surplus provisioned dylib in ``venv/lib/`` may remain — harmless), so a bad anchor can never brick update/doctor again. All functions are no-ops on non-macOS and for interpreters that are not uv-managed. Best-effort: never raises to callers. """ from __future__ import annotations import errno import logging import os import platform import re import shutil import subprocess import tempfile from pathlib import Path from hermes_constants import venv_python_path from hermes_cli.managed_uv import _RUNTIME_DIR_NAME from utils import atomic_write_text logger = logging.getLogger(__name__) _MARKER_NAME = ".tcc-anchor-source" _STORE_COMMON_MARKERS = ("cpython-", "-macos-") # The runtime-store marker is derived from managed_uv so a rename of the # repair-generation directory cannot silently stop the anchor from matching. _STORE_ROOT_MARKERS = ("/uv/python/", f"/{_RUNTIME_DIR_NAME}/python/") class _BootGateFailed(Exception): """Staged copy refused to boot; the live venv must stay untouched.""" def _marker_value(source_file: Path) -> str: """Canonical marker value: fully resolved so symlinked spellings of the same store binary (``cpython-3.11-macos-*`` → ``cpython-3.11.15-macos-*``) compare equal.""" return os.path.realpath(str(source_file)) def is_macos() -> bool: return platform.system() == "Darwin" def _sibling_names() -> tuple[str, ...]: """Alias names uv creates inside the venv bin dir.""" import sys as _sys return ("python3", f"python3.{_sys.version_info.minor}") def _store_bin_names() -> tuple[str, ...]: """Preferred interpreter file names inside a store ``bin`` dir.""" import sys as _sys return (f"python3.{_sys.version_info.minor}", "python3", "python") def _is_uv_macos_store(path: str) -> bool: normalized = path.replace("\\", "/") if not all(marker in normalized for marker in _STORE_COMMON_MARKERS): return False return any(marker in normalized for marker in _STORE_ROOT_MARKERS) 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] ) for name in ("venv", ".venv"): candidate = root / name venv_py = venv_python_path(candidate) if venv_py.is_file() or venv_py.is_symlink(): return candidate return None def _interpreter_file(src: str | Path) -> Path | None: """Return the interpreter binary file at/inside *src*.""" p = Path(src) if p.is_file(): return p if not p.is_dir(): return None for name in _store_bin_names(): candidate = p / name if candidate.is_file(): return candidate try: for candidate in sorted(p.glob("python3.*")): if candidate.is_file() and not candidate.name.endswith((".dSYM", ".txt")): return candidate except OSError: return None return None def _interpreter_source(venv_dir: Path) -> str | None: """Return the interpreter file the venv currently resolves to.""" venv_py = venv_python_path(venv_dir) if venv_py.is_symlink(): try: resolved = venv_py.resolve(strict=False) except OSError: return None return str(resolved) cfg = venv_dir / "pyvenv.cfg" if not cfg.is_file(): return None home = "" try: for line in cfg.read_text(encoding="utf-8").splitlines(): if line.lower().startswith("home"): _, _, home = line.partition("=") home = home.strip() break except OSError: return None if not home: return None interp = _interpreter_file(home) return str(interp) if interp is not None else None def _anchor_marker(venv_bin: Path) -> Path: return venv_bin / _MARKER_NAME def _write_marker(venv_bin: Path, source_file: Path) -> None: """Write the anchor marker atomically via the shared helper. A concurrent ensure (update + doctor --fix) must never observe a partially-written marker: a torn read would compare unequal and trigger a spurious reinstall, and ``write_text`` alone is not atomic. """ atomic_write_text( _anchor_marker(venv_bin), _marker_value(source_file), tmp_prefix=f"{_MARKER_NAME}.", ) def _store_root(source_file: Path) -> Path: # .../cpython--macos-*/bin/python3.N → store root return source_file.resolve(strict=False).parent.parent def _provision_libpython( venv_dir: Path, source_file: Path, *, refresh: bool = False ) -> None: """Hardlink (else copy) store ``libpython*`` into ``venv/lib/``. Provision-if-present: a surplus hardlink on a statically-linked build is free; a missed detection is the only way #95425 returns. """ src_lib = _store_root(source_file) / "lib" if not src_lib.is_dir(): return dst_lib = venv_dir / "lib" try: dst_lib.mkdir(parents=True, exist_ok=True) for src in src_lib.glob("libpython*"): if not src.is_file(): continue dst = dst_lib / src.name if dst.exists() or dst.is_symlink(): if not refresh: continue try: dst.unlink() except OSError: continue try: os.link(src, dst) except OSError: try: shutil.copy2(src, dst) except OSError: logger.debug("libpython provision failed for %s", src, exc_info=True) except OSError: logger.debug("libpython provision skipped", exc_info=True) def _copy_alias(venv_bin: Path, name: str, anchor: Path) -> bool: """Materialize *name* as a real-file copy of *anchor* (atomic rename). Returns False (and warns) on failure: a leftover alias *symlink* to the anchor is the exact #95541 crash shape, so callers must know when the alias set is incomplete. The staging name is unique (mkstemp) so a concurrent ensure (update + doctor --fix) cannot promote a truncated interim copy. """ tmp_path: Path | None = None try: fd, tmp_name = tempfile.mkstemp(prefix=f".{name}.tcc-", dir=str(venv_bin)) os.close(fd) tmp_path = Path(tmp_name) shutil.copy2(anchor, tmp_path) os.chmod(tmp_path, anchor.stat().st_mode | 0o111) os.replace(tmp_path, venv_bin / name) return True except OSError as exc: logger.warning("TCC anchor alias %s not materialized: %s", name, exc) if tmp_path is not None: try: tmp_path.unlink(missing_ok=True) except OSError: pass return False def _materialize_aliases( venv_bin: Path, anchor: Path, *, refresh: bool = False ) -> bool: """Materialize uv alias names as real-file copies of the anchor. Returns True only when every alias that needed materializing succeeded. """ names = set(_sibling_names()) try: names.update( p.name for p in venv_bin.glob("python3*") if re.fullmatch(r"python3(\.\d+)?", p.name) ) except OSError: pass ok = True for name in sorted(names): alias = venv_bin / name try: if refresh or alias.is_symlink() or not alias.exists(): ok = _copy_alias(venv_bin, name, anchor) and ok except OSError: ok = False continue return ok def _passes_boot_gate(staged: Path, venv_dir: Path) -> bool: """Launch *staged* and demand encodings + the venv prefix. The probe runs with ``PYTHONHOME``/``PYTHONPATH`` scrubbed: an inherited ``PYTHONHOME`` papers over exactly the prefix-resolution failure the gate exists to catch. ``OSError`` is split by errno — ``ENOENT``/``ENOEXEC`` (fixture binaries, foreign-arch images) means the binary cannot run here at all, so the symlinked venv was equally dead and installing cannot make things worse: skip. Anything else (notably ``EACCES`` after our own chmod) is a broken install about to go live: refuse. """ env = { k: v for k, v in os.environ.items() if k not in ("PYTHONHOME", "PYTHONPATH", "PYTHONSTARTUP", "__PYVENV_LAUNCHER__") } try: proc = subprocess.run( [str(staged), "-c", "import encodings, sys; print(sys.prefix)"], capture_output=True, text=True, timeout=30, env=env, ) except OSError as exc: if exc.errno in (errno.ENOENT, errno.ENOEXEC): logger.debug("boot gate skipped: cannot execute %s (%s)", staged, exc) return True logger.warning("boot gate: staged copy not executable: %s", exc) return False except subprocess.TimeoutExpired: return False if proc.returncode != 0: return False printed = (proc.stdout or "").strip().splitlines() if not printed: return False try: return Path(printed[-1]).resolve() == venv_dir.resolve() except OSError: return str(venv_dir) in printed[-1] def _install_anchor(venv_dir: Path, source_file: Path) -> None: """Replace ``bin/python`` with a signed copy, gated on a real boot.""" venv_py = venv_python_path(venv_dir) venv_bin = venv_py.parent venv_bin.mkdir(parents=True, exist_ok=True) _provision_libpython(venv_dir, source_file, refresh=True) fd, tmp_name = tempfile.mkstemp(prefix=".python-tcc-", dir=str(venv_bin)) os.close(fd) tmp_path = Path(tmp_name) try: shutil.copy2(source_file, tmp_path) os.chmod(tmp_path, source_file.stat().st_mode | 0o111) try: from hermes_cli.managed_uv import _macos_sign_managed_python _macos_sign_managed_python(tmp_path) except Exception: # pragma: no cover - never block the anchor logger.debug("anchor copy signing skipped", exc_info=True) if not _passes_boot_gate(tmp_path, venv_dir): raise _BootGateFailed( f"staged copy at {tmp_path} failed encodings/prefix probe" ) os.replace(tmp_path, venv_py) aliases_ok = _materialize_aliases(venv_bin, venv_py, refresh=True) if aliases_ok: # Marker last, atomically: it asserts the WHOLE layout (anchor + # aliases) is complete. A partially-materialized alias set (the # #95541 crash shape when an alias stays a symlink) must not read # "active" in doctor — leaving the marker absent makes the next # ensure retry the install. _write_marker(venv_bin, source_file) else: logger.warning( "TCC anchor installed but alias materialization was " "incomplete; leaving anchor unmarked so the next run retries" ) except Exception: try: tmp_path.unlink(missing_ok=True) except OSError: pass raise def ensure_tcc_anchor(project_root: Path | None = None) -> Path | None: """Pin a dylib-complete interpreter anchor for macOS TCC (#95596). No-op (returns None) on non-macOS, when no venv interpreter exists, or when the interpreter is not uv-managed. Idempotent. Best-effort — returns None (and logs) if the copy or boot-gate fails; callers must never depend on success. """ if not is_macos(): return None venv_dir = _venv_dir(project_root) if venv_dir is None: return None venv_py = venv_python_path(venv_dir) if not (venv_py.is_file() or venv_py.is_symlink()): return None source = _interpreter_source(venv_dir) if source is None or not _is_uv_macos_store(source): return None source_file = _interpreter_file(source) if source_file is None: return None if not venv_py.is_symlink(): marker = _anchor_marker(venv_py.parent) try: if marker.is_file() and marker.read_text(encoding="utf-8").strip() == ( _marker_value(source_file) ): _provision_libpython(venv_dir, source_file, refresh=False) if _passes_boot_gate(venv_py, venv_dir): _materialize_aliases(venv_py.parent, venv_py) return venv_py except OSError: pass try: _install_anchor(venv_dir, source_file) except _BootGateFailed as exc: logger.warning("macOS TCC anchor boot-gate refused install: %s", exc) return None except Exception as exc: # best-effort: never break update/doctor logger.warning("macOS TCC anchor install failed: %s", exc) return None return venv_py def tcc_anchor_state(project_root: Path | None = None) -> tuple[str, str]: """Report the anchor state for ``hermes doctor``. Returns ``(status, detail)`` with status one of: - ``"skip"`` — not applicable (non-macOS, no venv, or not uv-managed) - ``"active"`` — venv interpreter is pinned at a stable real-file anchor - ``"stale"`` — pinned but the interpreter changed since the last copy - ``"missing"`` — uv-managed interpreter with no stable anchor installed """ if not is_macos(): return "skip", "not macOS" venv_dir = _venv_dir(project_root) if venv_dir is None: return "skip", "no venv interpreter" venv_py = venv_python_path(venv_dir) if not (venv_py.is_file() or venv_py.is_symlink()): return "skip", "no venv interpreter" source = _interpreter_source(venv_dir) if source is None or not _is_uv_macos_store(source): return "skip", "interpreter not uv-managed (stable path)" if not venv_py.is_symlink(): marker = _anchor_marker(venv_py.parent) source_file = _interpreter_file(source) expected = _marker_value(source_file) if source_file is not None else source try: if marker.is_file() and marker.read_text(encoding="utf-8").strip() == expected: return "active", str(venv_py) except OSError: pass return "stale", str(venv_py) return "missing", str(venv_py)