fix(scripts): probe _pick_python candidates through env so make dev starts the frontend on Windows (#5181)

* bugfix #5179

* test: cover the env-aware _pick_python fallback from #5179

Follow the test_serve_nginx_stop.py extraction pattern: drive the real
_pick_python from serve.sh against a stub-only PATH plus a mocked env.

- python3 succeeds directly but fails through env -> python selected
  (red on main, green on this branch)
- env rejects every candidate -> nonzero exit (also red on main)
- healthy PATH with the real env -> python3 preferred, guarding against
  over-rejection

MSYS/Git Bash hosts need the stub dir as an MSYS-style (/c/...) PATH
entry, and bash diagnostics may arrive in the console code page, so the
runner decodes output with errors="replace".
This commit is contained in:
pclin 2026-09-04 23:32:40 +08:00 committed by GitHub
parent 4791e94a73
commit fcb1c88e5e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 129 additions and 1 deletions

View File

@ -0,0 +1,120 @@
"""Regression coverage for #5179: on Windows/Git Bash the Microsoft Store
python alias stubs pass Bash's own PATH lookup but cannot be exec'd through
/usr/bin/env, so `make dev` never started the frontend."""
from __future__ import annotations
import shlex
import shutil
import subprocess
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
SERVE_SH = REPO_ROOT / "scripts" / "serve.sh"
def _extract_shell_function(name: str) -> str:
text = SERVE_SH.read_text(encoding="utf-8")
marker = f"{name}() {{"
start = text.index(marker)
depth = 0
chunks: list[str] = []
for line in text[start:].splitlines(keepends=True):
chunks.append(line)
depth += line.count("{") - line.count("}")
if depth == 0:
return "".join(chunks)
raise AssertionError(f"Could not extract shell function {name}")
# Shells out to bash with a stub-only PATH plus an optional `env` override,
# exactly the split that broke Windows: the candidate stubs succeed when
# invoked directly, while the mocked `env` mimics /usr/bin/env resolving the
# WindowsApps alias (exit 127 = cannot exec).
_SCRIPT_TEMPLATE = r"""
set -u
BIN=__BIN__
mkdir -p "$BIN"
for name in __STUBS__; do
printf '#!/bin/sh
exit 0
' > "$BIN/$name"
chmod +x "$BIN/$name"
done
export PATH="$BIN:$PATH"
__ENV_MOCK__
__FUNCTION__
_pick_python
"""
def _to_bash_path(path: Path) -> str:
"""Return `path` in the form the target bash understands.
MSYS/Git Bash needs an MSYS-style path (/c/...) on PATH: a drive-letter
style segment (C:/...) is resolved inconsistently between bash's own
lookup and /usr/bin/env. POSIX hosts pass through unchanged.
"""
posix = path.resolve().as_posix()
if len(posix) > 1 and posix[1] == ":":
return "/" + posix[0].lower() + posix[2:]
return posix
def _run_pick_python(tmp_path: Path, *, env_mock: str = "") -> subprocess.CompletedProcess:
bash = shutil.which("bash")
if bash is None:
pytest.skip("bash is required to exercise serve.sh helpers")
script = _SCRIPT_TEMPLATE.replace("__BIN__", shlex.quote(_to_bash_path(tmp_path / "bin"))).replace("__STUBS__", "python3 python py").replace("__ENV_MOCK__", env_mock).replace("__FUNCTION__", _extract_shell_function("_pick_python"))
# errors="replace": bash's diagnostics may arrive in the console's code
# page (GBK on a Chinese Windows host) while the selection output is ASCII.
return subprocess.run(
[bash, "-c", script],
capture_output=True,
encoding="utf-8",
errors="replace",
check=False,
)
def test_pick_python_prefers_python3_when_env_agrees(tmp_path):
# Real env: all stubs are ordinary executables, so python3 wins.
result = _run_pick_python(tmp_path)
assert result.returncode == 0
assert result.stdout.strip() == "python3"
def test_pick_python_skips_candidate_that_env_cannot_exec(tmp_path):
# Store-alias world: python3 execs fine directly but env fails on it;
# python works through both paths. Must select python, not python3.
env_mock = """
env() {
case "$1" in
python3) return 127 ;;
esac
return 0
}
"""
result = _run_pick_python(tmp_path, env_mock=env_mock)
assert result.returncode == 0
assert result.stdout.strip() == "python"
def test_pick_python_fails_when_no_candidate_survives_env_probe(tmp_path):
env_mock = """
env() { return 127; }
"""
result = _run_pick_python(tmp_path, env_mock=env_mock)
assert result.returncode == 1
assert result.stdout.strip() == ""

View File

@ -42,7 +42,15 @@ fi
_pick_python() {
local candidate
for candidate in python3 python py; do
if command -v "$candidate" >/dev/null 2>&1 && "$candidate" -c 'import sys; raise SystemExit(0 if sys.version_info.major >= 3 else 1)' >/dev/null 2>&1; then
# Probe through `env` as well: the frontend is launched as
# `env PORT=3000 "$DEERFLOW_PNPM_PYTHON" ...` (FRONTEND_CMD below), and on
# Windows/Git Bash the Microsoft Store python aliases under WindowsApps
# are skipped by Bash's own PATH lookup yet still resolved (and fail to
# exec) inside /usr/bin/env. A bare "$candidate" probe passes while the
# real launch dies with: env: 'python3': No such file or directory
if command -v "$candidate" >/dev/null 2>&1 \
&& "$candidate" -c 'import sys; raise SystemExit(0 if sys.version_info.major >= 3 else 1)' >/dev/null 2>&1 \
&& env "$candidate" -c 'import sys; raise SystemExit(0 if sys.version_info.major >= 3 else 1)' >/dev/null 2>&1; then
printf '%s\n' "$candidate"
return 0
fi