fix(dashboard): compare and spawn the venv interpreter by UNRESOLVED path
Follow-up on the cherry-picked #90030: candidate.resolve() breaks the fix on the standard Linux venv layout, where venv/bin/python is a symlink to the base interpreter. Resolving makes the venv python compare equal to the dependency-less base (so the swap never happens), and returning the resolved target would spawn the bare base interpreter, bypassing pyvenv.cfg — the fix would silently not fix #90026 on the exact platform it was reported from. Compare and return normalized UNRESOLVED paths: the venv path IS the interpreter's identity. Adds the symlink-layout regression test; live-E2E'd with a real dependency-less base runtime.
This commit is contained in:
@@ -4604,12 +4604,22 @@ def _dashboard_spawn_executable() -> str:
|
||||
for rel in ("venv/bin/python", "venv/Scripts/python.exe"):
|
||||
candidate = PROJECT_ROOT / rel
|
||||
if candidate.is_file():
|
||||
resolved = candidate.resolve()
|
||||
# Same interpreter → keep sys.executable (preserves the
|
||||
# docstring's console-ownership behavior verbatim).
|
||||
if resolved == exe.resolve():
|
||||
# docstring's console-ownership behavior verbatim). Compare
|
||||
# UNRESOLVED normalized paths: a venv's bin/python is
|
||||
# typically a SYMLINK to the base interpreter, so resolving
|
||||
# both sides makes the venv python and the dependency-less
|
||||
# base compare equal — exactly the SSH-runtime case this
|
||||
# function exists to fix. The unresolved path IS the venv's
|
||||
# identity (pyvenv.cfg discovery keys off argv0's location).
|
||||
if os.path.normcase(os.path.normpath(str(candidate))) == (
|
||||
os.path.normcase(os.path.normpath(str(exe)))
|
||||
):
|
||||
return sys.executable
|
||||
return str(resolved)
|
||||
# Return the candidate UNRESOLVED for the same reason:
|
||||
# invoking the resolved target would bypass pyvenv.cfg and
|
||||
# run the bare base interpreter again.
|
||||
return str(candidate)
|
||||
except OSError:
|
||||
pass
|
||||
return sys.executable
|
||||
|
||||
@@ -67,3 +67,24 @@ class TestDashboardSpawnExecutable:
|
||||
patch.object(sys, "executable", str(base_interp)),
|
||||
):
|
||||
assert web_server._dashboard_spawn_executable() == str(base_interp)
|
||||
|
||||
def test_venv_symlink_to_base_is_still_preferred_unresolved(self, tmp_path):
|
||||
"""The Linux-standard layout: venv/bin/python is a SYMLINK to the
|
||||
base interpreter. The chooser must return the UNRESOLVED venv path —
|
||||
resolving it would compare equal to the base interpreter (missing
|
||||
the swap) or spawn the base directly (bypassing pyvenv.cfg). This is
|
||||
the exact layout of the #90026 report."""
|
||||
base = tmp_path / "uv-base" / "python"
|
||||
base.parent.mkdir(parents=True)
|
||||
base.touch()
|
||||
venv_py = tmp_path / "venv" / "bin" / "python"
|
||||
venv_py.parent.mkdir(parents=True)
|
||||
venv_py.symlink_to(base)
|
||||
with (
|
||||
patch.object(web_server, "PROJECT_ROOT", tmp_path),
|
||||
patch.object(sys, "executable", str(base)),
|
||||
):
|
||||
chosen = web_server._dashboard_spawn_executable()
|
||||
assert chosen == str(venv_py), (
|
||||
"must return the unresolved venv path, not the symlink target"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user