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:
Teknium
2026-08-26 03:44:30 -07:00
parent ad8f995bcf
commit 19fde8a450
2 changed files with 35 additions and 4 deletions
+14 -4
View File
@@ -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"
)