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 `<workspace>/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
This commit is contained in:
Daniel Bader
2026-09-24 23:31:49 -04:00
committed by Teknium
parent 95fdcf92b0
commit 7523696fb6
2 changed files with 24 additions and 1 deletions

View File

@@ -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()]

View File

@@ -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 <workspace>/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"