Respect PATHEXT when locating JS runtime on Windows (#15117)

Fixes #15043

Authored by: Grub4K
This commit is contained in:
Simon Sawicki 2025-11-24 01:56:43 +01:00 committed by GitHub
parent 715af0c636
commit e564b4a808
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 60 additions and 14 deletions

View file

@ -1,21 +1,61 @@
from __future__ import annotations
import abc
import dataclasses
import functools
import os.path
import sys
from ._utils import _get_exe_version_output, detect_exe_version, int_or_none
# NOT public API
def runtime_version_tuple(v):
def _runtime_version_tuple(v):
# NB: will return (0,) if `v` is an invalid version string
return tuple(int_or_none(x, default=0) for x in v.split('.'))
_FALLBACK_PATHEXT = ('.COM', '.EXE', '.BAT', '.CMD')
def _find_exe(basename: str) -> str:
if os.name != 'nt':
return basename
paths: list[str] = []
# binary dir
if getattr(sys, 'frozen', False):
paths.append(os.path.dirname(sys.executable))
# cwd
paths.append(os.getcwd())
# PATH items
if path := os.environ.get('PATH'):
paths.extend(filter(None, path.split(os.path.pathsep)))
pathext = os.environ.get('PATHEXT')
if pathext is None:
exts = _FALLBACK_PATHEXT
else:
exts = tuple(ext for ext in pathext.split(os.pathsep) if ext)
visited = []
for path in map(os.path.realpath, paths):
normed = os.path.normcase(path)
if normed in visited:
continue
visited.append(normed)
for ext in exts:
binary = os.path.join(path, f'{basename}{ext}')
if os.access(binary, os.F_OK | os.X_OK) and not os.path.isdir(binary):
return binary
return basename
def _determine_runtime_path(path, basename):
if not path:
return basename
return _find_exe(basename)
if os.path.isdir(path):
return os.path.join(path, basename)
return path
@ -52,7 +92,7 @@ class DenoJsRuntime(JsRuntime):
if not out:
return None
version = detect_exe_version(out, r'^deno (\S+)', 'unknown')
vt = runtime_version_tuple(version)
vt = _runtime_version_tuple(version)
return JsRuntimeInfo(
name='deno', path=path, version=version, version_tuple=vt,
supported=vt >= self.MIN_SUPPORTED_VERSION)
@ -67,7 +107,7 @@ class BunJsRuntime(JsRuntime):
if not out:
return None
version = detect_exe_version(out, r'^(\S+)', 'unknown')
vt = runtime_version_tuple(version)
vt = _runtime_version_tuple(version)
return JsRuntimeInfo(
name='bun', path=path, version=version, version_tuple=vt,
supported=vt >= self.MIN_SUPPORTED_VERSION)
@ -82,7 +122,7 @@ class NodeJsRuntime(JsRuntime):
if not out:
return None
version = detect_exe_version(out, r'^v(\S+)', 'unknown')
vt = runtime_version_tuple(version)
vt = _runtime_version_tuple(version)
return JsRuntimeInfo(
name='node', path=path, version=version, version_tuple=vt,
supported=vt >= self.MIN_SUPPORTED_VERSION)
@ -100,7 +140,7 @@ class QuickJsRuntime(JsRuntime):
is_ng = 'QuickJS-ng' in out
version = detect_exe_version(out, r'^QuickJS(?:-ng)?\s+version\s+(\S+)', 'unknown')
vt = runtime_version_tuple(version.replace('-', '.'))
vt = _runtime_version_tuple(version.replace('-', '.'))
if is_ng:
return JsRuntimeInfo(
name='quickjs-ng', path=path, version=version, version_tuple=vt,

View file

@ -876,13 +876,19 @@ class Popen(subprocess.Popen):
kwargs.setdefault('encoding', 'utf-8')
kwargs.setdefault('errors', 'replace')
if shell and os.name == 'nt' and kwargs.get('executable') is None:
if not isinstance(args, str):
args = shell_quote(args, shell=True)
shell = False
# Set variable for `cmd.exe` newline escaping (see `utils.shell_quote`)
env['='] = '"^\n\n"'
args = f'{self.__comspec()} /Q /S /D /V:OFF /E:ON /C "{args}"'
if os.name == 'nt' and kwargs.get('executable') is None:
# Must apply shell escaping if we are trying to run a batch file
# These conditions should be very specific to limit impact
if not shell and isinstance(args, list) and args and args[0].lower().endswith(('.bat', '.cmd')):
shell = True
if shell:
if not isinstance(args, str):
args = shell_quote(args, shell=True)
shell = False
# Set variable for `cmd.exe` newline escaping (see `utils.shell_quote`)
env['='] = '"^\n\n"'
args = f'{self.__comspec()} /Q /S /D /V:OFF /E:ON /C "{args}"'
super().__init__(args, *remaining, env=env, shell=shell, **kwargs, startupinfo=self._startupinfo)