From 7523696fb64b989f469fe230d006d87edfb6fa20 Mon Sep 17 00:00:00 2001 From: Daniel Bader Date: Thu, 24 Sep 2026 23:31:49 -0400 Subject: [PATCH] fix(pm): keep a workspace member's own uv.lock in the build snapshot `_copy_core_inputs` listed `uv.lock` in the names its copytree `ignore` drops, and copytree applies that callback at every depth, so the member lock `pm/uv.lock` never reached the generation workspace. `pm/runtime.py::_inputs()` hashes `/pm/{pyproject.toml,uv.lock}` to key the PM runtime, so every `hermes pm` command run by the managed environment's `hermes` died at preflight: FileNotFoundError: .../workspace/pm/uv.lock Drop `uv.lock` from the exclusion set. The root lock is unaffected: the root pass copies only the explicit `files` set (never `uv.lock`), and `lock_and_sync` seeds or resolves the root lock itself. Refs #124075 --- pm/workspace.py | 4 +++- tests/pm/test_workspace_build_inputs.py | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/pm/workspace.py b/pm/workspace.py index 18316036e1..9d7a652162 100644 --- a/pm/workspace.py +++ b/pm/workspace.py @@ -82,7 +82,9 @@ def _copy_core_inputs(source: Path, destination: Path) -> None: files.update(str(p.relative_to(source)) for p in source.glob(pattern)) files.update(p.name for p in source.glob("*.py")) - excluded = {".git", ".venv", "venv", "node_modules", "__pycache__", "build", "dist", "release", "uv.lock"} + # uv.lock is not excluded: the root lock is never copied (only ``files`` are; lock_and_sync + # seeds or resolves it), and pm/uv.lock is the PM runtime's input (pm/runtime.py::_inputs). + excluded = {".git", ".venv", "venv", "node_modules", "__pycache__", "build", "dist", "release"} def ignore(directory, names): return [name for name in names if name in excluded or name.startswith(".") or name.endswith(".egg-info") or (Path(directory) / name).is_symlink()] diff --git a/tests/pm/test_workspace_build_inputs.py b/tests/pm/test_workspace_build_inputs.py index 70ac8c6c43..468a7b0d04 100644 --- a/tests/pm/test_workspace_build_inputs.py +++ b/tests/pm/test_workspace_build_inputs.py @@ -240,3 +240,24 @@ def test_plugin_can_move_compatible_transitive_but_not_exact_requirement(tmp_pat capture_output=True, text=True, check=True, timeout=30) assert result.stdout.strip() == "1.0 1.3" assert (baseline / "uv.lock").read_bytes() == first_lock + + +def test_member_uv_lock_travels_with_its_member(tmp_path): + """pm/runtime.py::_inputs reads /pm/uv.lock; the snapshot must carry it.""" + core = tmp_path / "core" + member = core / "pm" + member.mkdir(parents=True) + (core / "pyproject.toml").write_text( + '[project]\nname="core"\nversion="1"\nrequires-python=">=3.11"\n' + '[tool.setuptools.packages.find]\ninclude=["pm"]\n', + encoding="utf-8", + ) + (core / "uv.lock").write_text("version = 1\n# root lock\n", encoding="utf-8") + (member / "pyproject.toml").write_text('[project]\nname="pm"\nversion="1"\n', encoding="utf-8") + (member / "uv.lock").write_text("version = 1\n# member lock\n", encoding="utf-8") + + destination = tmp_path / "stage" + workspace._copy_core_inputs(core, destination) + + assert (destination / "pm/uv.lock").read_text(encoding="utf-8") == "version = 1\n# member lock\n" + assert not (destination / "uv.lock").exists(), "the root lock is seeded by lock_and_sync, not copied"