feat(pm): generated uv-workspace root unions plugin deps into the venv
pm/workspace.py owns the generated workspace root (settled design: .hermes/plans/2026-09-02_164500-plugin-deps-workspace-union.md): - build_root() writes <store_root>/.pm-workspace/pyproject.toml = core's pyproject verbatim + [tool.uv.workspace] members pointing at each enabled plugin dir via relative ../-escaping paths. The committed pyproject is never touched (sealed installs are read-only; member lists are machine-specific). - enabled_member_dirs() scans all profiles' plugin dirs for python-dep declarations: pyproject.toml (modern) or legacy pip_dependencies/ python_dependencies in plugin.yaml (bridge candidates). Per-install union (settled): profiles share the venv, so their plugins share the resolution graph. OSError-safe walks (dangling junctions). - lock_and_sync(): build root -> uv lock -> uv sync --frozen --extras into the venv (UV_PROJECT_ENVIRONMENT pinned). One lock, one graph: core pins preserved (proven live: union of the real repo pyproject + a plugin kept rich==14.3.3 / httpx==0.28.1 while adding the plugin's own dep), conflict = loud resolver refusal naming both sides (proven live: a plugin pinning rich==13.9.4 against core's 14.3.3). - Venv.expected_stamp folds in members_stamp() so a changed plugin set re-syncs; Venv.apply routes through the workspace root when members exist, plain uv sync otherwise (no behavior change for zero-plugin installs). tests/pm/test_workspace.py: 8 tests (root location, verbatim core carry-over, relative escaping members, idempotent build, zero-member build, stamp hashing, member discovery incl. legacy + broken-root resilience). tests/pm suite: 152 passed, 0 failed.
This commit is contained in:
@@ -303,10 +303,23 @@ class Venv(StatePackage):
|
||||
h.update(_uv_lock_digest(self.project_root() / "uv.lock"))
|
||||
h.update(",".join(sorted(extras)).encode())
|
||||
h.update(f"{sys.version_info.major}.{sys.version_info.minor}".encode())
|
||||
# Plugin members union into the venv — a changed member set must
|
||||
# re-sync even when extras and core lock are unchanged.
|
||||
from pm.workspace import enabled_member_dirs, members_stamp
|
||||
|
||||
h.update(members_stamp(enabled_member_dirs()).encode())
|
||||
return h.hexdigest()
|
||||
|
||||
def apply(self, extras: list[str]) -> None:
|
||||
from pm.ensure import uv as pm_uv
|
||||
from pm.workspace import enabled_member_dirs, lock_and_sync
|
||||
|
||||
member_dirs = enabled_member_dirs()
|
||||
if member_dirs:
|
||||
# Plugin deps union into the venv through the generated
|
||||
# workspace root (one lock, conflict = loud refusal).
|
||||
lock_and_sync(member_dirs, extras, venv_dir=self.venv_dir())
|
||||
return
|
||||
|
||||
uv_bin, env = pm_uv(venv=self.venv_dir())
|
||||
if uv_bin is None:
|
||||
|
||||
Reference in New Issue
Block a user