fix(integrations): skip Windows Store python3 alias stub in resolve_python_interpreter (#3385)

* fix(integrations): skip Windows Store python3 alias stub in resolve_python_interpreter

On stock Windows, python3 on PATH is the Microsoft Store App Execution
Alias stub: it exists but only prints an installer hint and exits
non-zero, so generated {SCRIPT} invocations for the py script type were
broken. Verify the found interpreter actually runs before accepting it,
on Windows only, mirroring the parse-success-not-availability approach
of #3312/#3320 for the sh scripts. POSIX keeps the plain existence
check. sys.executable remains the fallback and is always live.

Fixes #3383

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(integrations): probe interpreter isolated and without site, discard I/O

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: pin POSIX platform in PATH-resolution tests

The tests fake shutil.which with POSIX paths; on Windows CI the real
sys.platform made the stub probe run against those fake paths and
fall through to sys.executable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Marsel Safin
2026-07-09 15:14:29 +02:00
committed by GitHub
parent 8e2e2d2f25
commit 15ac745e8d
2 changed files with 107 additions and 3 deletions

View File

@@ -17,6 +17,7 @@ import os
import re
import shlex
import shutil
import subprocess
import sys
from abc import ABC
from dataclasses import dataclass
@@ -604,10 +605,42 @@ class IntegrationBase(ABC):
if candidate.exists():
return relative
for name in ("python3", "python"):
if shutil.which(name):
return name
found = shutil.which(name)
if not found:
continue
# On Windows, python3/python on PATH may be the Microsoft
# Store App Execution Alias stub: it exists but only prints
# an installer hint and exits non-zero, so existence is not
# enough (see #3304 for the same defect in the sh scripts).
if sys.platform == "win32" and not IntegrationBase._interpreter_runs(
found
):
continue
return name
return sys.executable or "python3"
@staticmethod
def _interpreter_runs(path: str) -> bool:
"""Return True when *path* executes as a Python interpreter.
Runs isolated (``-I``) without ``site`` (``-S``) and discards
I/O so the probe is a fast liveness check that cannot trigger
``sitecustomize``/user startup hooks.
"""
try:
return (
subprocess.run(
[path, "-I", "-S", "-c", ""],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=15,
).returncode
== 0
)
except (OSError, subprocess.SubprocessError):
return False
@staticmethod
def process_template(
content: str,
@@ -1089,7 +1122,6 @@ class TomlIntegration(IntegrationBase):
# YamlIntegration — YAML-format agents (Goose)
# ---------------------------------------------------------------------------
class YamlIntegration(IntegrationBase):
"""Concrete base for integrations that use YAML recipe format.

View File

@@ -306,9 +306,12 @@ class TestResolveCommandRefs:
class TestResolvePythonInterpreter:
def test_returns_python_on_path(self, monkeypatch):
# Positive: when python3 is on PATH it is preferred over python.
# Pin a POSIX platform so the Windows stub probe (tested separately
# below) does not reject the fake PATH entries on Windows CI.
def fake_which(name):
return f"/usr/bin/{name}" if name in ("python3", "python") else None
monkeypatch.setattr("specify_cli.integrations.base.sys.platform", "linux")
monkeypatch.setattr(
"specify_cli.integrations.base.shutil.which", fake_which
)
@@ -318,6 +321,7 @@ class TestResolvePythonInterpreter:
def fake_which(name):
return "/usr/bin/python" if name == "python" else None
monkeypatch.setattr("specify_cli.integrations.base.sys.platform", "linux")
monkeypatch.setattr(
"specify_cli.integrations.base.shutil.which", fake_which
)
@@ -369,12 +373,79 @@ class TestResolvePythonInterpreter:
def test_ignores_missing_venv(self, monkeypatch, tmp_path):
# Negative: no venv directory -> PATH resolution is used instead.
monkeypatch.setattr("specify_cli.integrations.base.sys.platform", "linux")
monkeypatch.setattr(
"specify_cli.integrations.base.shutil.which",
lambda name: "/usr/bin/python3" if name == "python3" else None,
)
assert IntegrationBase.resolve_python_interpreter(tmp_path) == "python3"
def test_windows_skips_store_alias_stub(self, monkeypatch):
# On Windows, python3 on PATH may be the Microsoft Store App
# Execution Alias stub: it exists but only prints an installer
# hint and exits non-zero. Existence is not enough; the
# interpreter must actually run (mirrors #3304 for the CLI).
monkeypatch.setattr("specify_cli.integrations.base.sys.platform", "win32")
monkeypatch.setattr(
"specify_cli.integrations.base.shutil.which",
lambda name: f"C:\\WindowsApps\\{name}.exe"
if name in ("python3", "python")
else None,
)
monkeypatch.setattr(
IntegrationBase, "_interpreter_runs", staticmethod(lambda path: False)
)
monkeypatch.setattr(
"specify_cli.integrations.base.sys.executable", "C:\\Python\\python.exe"
)
result = IntegrationBase.resolve_python_interpreter()
assert result == "C:\\Python\\python.exe"
def test_windows_keeps_working_interpreter(self, monkeypatch):
# Positive: a real python3 on Windows PATH passes the run check.
monkeypatch.setattr("specify_cli.integrations.base.sys.platform", "win32")
monkeypatch.setattr(
"specify_cli.integrations.base.shutil.which",
lambda name: f"C:\\Python\\{name}.exe" if name == "python3" else None,
)
monkeypatch.setattr(
IntegrationBase, "_interpreter_runs", staticmethod(lambda path: True)
)
assert IntegrationBase.resolve_python_interpreter() == "python3"
def test_windows_stub_python3_falls_through_to_working_python(self, monkeypatch):
# python3 is the stub but python is a real install: pick python.
monkeypatch.setattr("specify_cli.integrations.base.sys.platform", "win32")
monkeypatch.setattr(
"specify_cli.integrations.base.shutil.which",
lambda name: f"C:\\somewhere\\{name}.exe"
if name in ("python3", "python")
else None,
)
monkeypatch.setattr(
IntegrationBase,
"_interpreter_runs",
staticmethod(lambda path: path.endswith("python.exe")),
)
assert IntegrationBase.resolve_python_interpreter() == "python"
def test_posix_does_not_spawn_run_check(self, monkeypatch):
# Non-Windows platforms have no App Execution Alias; existence
# on PATH stays sufficient and no subprocess is spawned.
monkeypatch.setattr("specify_cli.integrations.base.sys.platform", "linux")
monkeypatch.setattr(
"specify_cli.integrations.base.shutil.which",
lambda name: "/usr/bin/python3" if name == "python3" else None,
)
def boom(path):
raise AssertionError("run check must not execute on POSIX")
monkeypatch.setattr(
IntegrationBase, "_interpreter_runs", staticmethod(boom)
)
assert IntegrationBase.resolve_python_interpreter() == "python3"
class TestProcessTemplatePyScriptType:
CONTENT = (
@@ -390,6 +461,7 @@ class TestProcessTemplatePyScriptType:
def test_py_prefixes_interpreter(self, monkeypatch):
# Positive: py script type prefixes a resolved interpreter and the
# script path is rewritten to the .specify location.
monkeypatch.setattr("specify_cli.integrations.base.sys.platform", "linux")
monkeypatch.setattr(
"specify_cli.integrations.base.shutil.which",
lambda name: "/usr/bin/python3" if name == "python3" else None,