mirror of https://github.com/python/cpython
gh-94399: Restore PATH search behaviour of py.exe launcher for '/usr/bin/env' shebang lines (GH-95582)
This commit is contained in:
parent
b53aed76d2
commit
67840edb28
|
@ -868,6 +868,11 @@ The ``/usr/bin/env`` form of shebang line has one further special property.
|
||||||
Before looking for installed Python interpreters, this form will search the
|
Before looking for installed Python interpreters, this form will search the
|
||||||
executable :envvar:`PATH` for a Python executable. This corresponds to the
|
executable :envvar:`PATH` for a Python executable. This corresponds to the
|
||||||
behaviour of the Unix ``env`` program, which performs a :envvar:`PATH` search.
|
behaviour of the Unix ``env`` program, which performs a :envvar:`PATH` search.
|
||||||
|
If an executable matching the first argument after the ``env`` command cannot
|
||||||
|
be found, it will be handled as described below. Additionally, the environment
|
||||||
|
variable :envvar:`PYLAUNCHER_NO_SEARCH_PATH` may be set (to any value) to skip
|
||||||
|
this additional search.
|
||||||
|
|
||||||
|
|
||||||
Arguments in shebang lines
|
Arguments in shebang lines
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
|
@ -187,6 +187,11 @@ class RunPyMixin:
|
||||||
)
|
)
|
||||||
return py_exe
|
return py_exe
|
||||||
|
|
||||||
|
def get_py_exe(self):
|
||||||
|
if not self.py_exe:
|
||||||
|
self.py_exe = self.find_py()
|
||||||
|
return self.py_exe
|
||||||
|
|
||||||
def run_py(self, args, env=None, allow_fail=False, expect_returncode=0, argv=None):
|
def run_py(self, args, env=None, allow_fail=False, expect_returncode=0, argv=None):
|
||||||
if not self.py_exe:
|
if not self.py_exe:
|
||||||
self.py_exe = self.find_py()
|
self.py_exe = self.find_py()
|
||||||
|
@ -194,9 +199,9 @@ class RunPyMixin:
|
||||||
ignore = {"VIRTUAL_ENV", "PY_PYTHON", "PY_PYTHON2", "PY_PYTHON3"}
|
ignore = {"VIRTUAL_ENV", "PY_PYTHON", "PY_PYTHON2", "PY_PYTHON3"}
|
||||||
env = {
|
env = {
|
||||||
**{k.upper(): v for k, v in os.environ.items() if k.upper() not in ignore},
|
**{k.upper(): v for k, v in os.environ.items() if k.upper() not in ignore},
|
||||||
**{k.upper(): v for k, v in (env or {}).items()},
|
|
||||||
"PYLAUNCHER_DEBUG": "1",
|
"PYLAUNCHER_DEBUG": "1",
|
||||||
"PYLAUNCHER_DRYRUN": "1",
|
"PYLAUNCHER_DRYRUN": "1",
|
||||||
|
**{k.upper(): v for k, v in (env or {}).items()},
|
||||||
}
|
}
|
||||||
if not argv:
|
if not argv:
|
||||||
argv = [self.py_exe, *args]
|
argv = [self.py_exe, *args]
|
||||||
|
@ -496,7 +501,7 @@ class TestLauncher(unittest.TestCase, RunPyMixin):
|
||||||
|
|
||||||
def test_py_shebang(self):
|
def test_py_shebang(self):
|
||||||
with self.py_ini(TEST_PY_COMMANDS):
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
with self.script("#! /usr/bin/env python -prearg") as script:
|
with self.script("#! /usr/bin/python -prearg") as script:
|
||||||
data = self.run_py([script, "-postarg"])
|
data = self.run_py([script, "-postarg"])
|
||||||
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
||||||
self.assertEqual("3.100", data["SearchInfo.tag"])
|
self.assertEqual("3.100", data["SearchInfo.tag"])
|
||||||
|
@ -504,7 +509,7 @@ class TestLauncher(unittest.TestCase, RunPyMixin):
|
||||||
|
|
||||||
def test_py2_shebang(self):
|
def test_py2_shebang(self):
|
||||||
with self.py_ini(TEST_PY_COMMANDS):
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
with self.script("#! /usr/bin/env python2 -prearg") as script:
|
with self.script("#! /usr/bin/python2 -prearg") as script:
|
||||||
data = self.run_py([script, "-postarg"])
|
data = self.run_py([script, "-postarg"])
|
||||||
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
||||||
self.assertEqual("3.100-32", data["SearchInfo.tag"])
|
self.assertEqual("3.100-32", data["SearchInfo.tag"])
|
||||||
|
@ -512,7 +517,7 @@ class TestLauncher(unittest.TestCase, RunPyMixin):
|
||||||
|
|
||||||
def test_py3_shebang(self):
|
def test_py3_shebang(self):
|
||||||
with self.py_ini(TEST_PY_COMMANDS):
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
with self.script("#! /usr/bin/env python3 -prearg") as script:
|
with self.script("#! /usr/bin/python3 -prearg") as script:
|
||||||
data = self.run_py([script, "-postarg"])
|
data = self.run_py([script, "-postarg"])
|
||||||
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
||||||
self.assertEqual("3.100-arm64", data["SearchInfo.tag"])
|
self.assertEqual("3.100-arm64", data["SearchInfo.tag"])
|
||||||
|
@ -520,7 +525,7 @@ class TestLauncher(unittest.TestCase, RunPyMixin):
|
||||||
|
|
||||||
def test_py_shebang_nl(self):
|
def test_py_shebang_nl(self):
|
||||||
with self.py_ini(TEST_PY_COMMANDS):
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
with self.script("#! /usr/bin/env python -prearg\n") as script:
|
with self.script("#! /usr/bin/python -prearg\n") as script:
|
||||||
data = self.run_py([script, "-postarg"])
|
data = self.run_py([script, "-postarg"])
|
||||||
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
||||||
self.assertEqual("3.100", data["SearchInfo.tag"])
|
self.assertEqual("3.100", data["SearchInfo.tag"])
|
||||||
|
@ -528,7 +533,7 @@ class TestLauncher(unittest.TestCase, RunPyMixin):
|
||||||
|
|
||||||
def test_py2_shebang_nl(self):
|
def test_py2_shebang_nl(self):
|
||||||
with self.py_ini(TEST_PY_COMMANDS):
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
with self.script("#! /usr/bin/env python2 -prearg\n") as script:
|
with self.script("#! /usr/bin/python2 -prearg\n") as script:
|
||||||
data = self.run_py([script, "-postarg"])
|
data = self.run_py([script, "-postarg"])
|
||||||
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
||||||
self.assertEqual("3.100-32", data["SearchInfo.tag"])
|
self.assertEqual("3.100-32", data["SearchInfo.tag"])
|
||||||
|
@ -536,7 +541,7 @@ class TestLauncher(unittest.TestCase, RunPyMixin):
|
||||||
|
|
||||||
def test_py3_shebang_nl(self):
|
def test_py3_shebang_nl(self):
|
||||||
with self.py_ini(TEST_PY_COMMANDS):
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
with self.script("#! /usr/bin/env python3 -prearg\n") as script:
|
with self.script("#! /usr/bin/python3 -prearg\n") as script:
|
||||||
data = self.run_py([script, "-postarg"])
|
data = self.run_py([script, "-postarg"])
|
||||||
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
||||||
self.assertEqual("3.100-arm64", data["SearchInfo.tag"])
|
self.assertEqual("3.100-arm64", data["SearchInfo.tag"])
|
||||||
|
@ -544,13 +549,45 @@ class TestLauncher(unittest.TestCase, RunPyMixin):
|
||||||
|
|
||||||
def test_py_shebang_short_argv0(self):
|
def test_py_shebang_short_argv0(self):
|
||||||
with self.py_ini(TEST_PY_COMMANDS):
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
with self.script("#! /usr/bin/env python -prearg") as script:
|
with self.script("#! /usr/bin/python -prearg") as script:
|
||||||
# Override argv to only pass "py.exe" as the command
|
# Override argv to only pass "py.exe" as the command
|
||||||
data = self.run_py([script, "-postarg"], argv=f'"py.exe" "{script}" -postarg')
|
data = self.run_py([script, "-postarg"], argv=f'"py.exe" "{script}" -postarg')
|
||||||
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
|
||||||
self.assertEqual("3.100", data["SearchInfo.tag"])
|
self.assertEqual("3.100", data["SearchInfo.tag"])
|
||||||
self.assertEqual(f'X.Y.exe -prearg "{script}" -postarg', data["stdout"].strip())
|
self.assertEqual(f'X.Y.exe -prearg "{script}" -postarg', data["stdout"].strip())
|
||||||
|
|
||||||
|
def test_search_path(self):
|
||||||
|
stem = Path(sys.executable).stem
|
||||||
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
|
with self.script(f"#! /usr/bin/env {stem} -prearg") as script:
|
||||||
|
data = self.run_py(
|
||||||
|
[script, "-postarg"],
|
||||||
|
env={"PATH": f"{Path(sys.executable).parent};{os.getenv('PATH')}"},
|
||||||
|
)
|
||||||
|
self.assertEqual(f"{sys.executable} -prearg {script} -postarg", data["stdout"].strip())
|
||||||
|
|
||||||
|
def test_search_path_exe(self):
|
||||||
|
# Leave the .exe on the name to ensure we don't add it a second time
|
||||||
|
name = Path(sys.executable).name
|
||||||
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
|
with self.script(f"#! /usr/bin/env {name} -prearg") as script:
|
||||||
|
data = self.run_py(
|
||||||
|
[script, "-postarg"],
|
||||||
|
env={"PATH": f"{Path(sys.executable).parent};{os.getenv('PATH')}"},
|
||||||
|
)
|
||||||
|
self.assertEqual(f"{sys.executable} -prearg {script} -postarg", data["stdout"].strip())
|
||||||
|
|
||||||
|
def test_recursive_search_path(self):
|
||||||
|
stem = self.get_py_exe().stem
|
||||||
|
with self.py_ini(TEST_PY_COMMANDS):
|
||||||
|
with self.script(f"#! /usr/bin/env {stem}") as script:
|
||||||
|
data = self.run_py(
|
||||||
|
[script],
|
||||||
|
env={"PATH": f"{self.get_py_exe().parent};{os.getenv('PATH')}"},
|
||||||
|
)
|
||||||
|
# The recursive search is ignored and we get normal "py" behavior
|
||||||
|
self.assertEqual(f"X.Y.exe {script}", data["stdout"].strip())
|
||||||
|
|
||||||
def test_install(self):
|
def test_install(self):
|
||||||
data = self.run_py(["-V:3.10"], env={"PYLAUNCHER_ALWAYS_INSTALL": "1"}, expect_returncode=111)
|
data = self.run_py(["-V:3.10"], env={"PYLAUNCHER_ALWAYS_INSTALL": "1"}, expect_returncode=111)
|
||||||
cmd = data["stdout"].strip()
|
cmd = data["stdout"].strip()
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
Restores the behaviour of :ref:`launcher` for ``/usr/bin/env`` shebang
|
||||||
|
lines, which will now search :envvar:`PATH` for an executable matching the
|
||||||
|
given command. If none is found, the usual search process is used.
|
106
PC/launcher2.c
106
PC/launcher2.c
|
@ -36,6 +36,7 @@
|
||||||
#define RC_DUPLICATE_ITEM 110
|
#define RC_DUPLICATE_ITEM 110
|
||||||
#define RC_INSTALLING 111
|
#define RC_INSTALLING 111
|
||||||
#define RC_NO_PYTHON_AT_ALL 112
|
#define RC_NO_PYTHON_AT_ALL 112
|
||||||
|
#define RC_NO_SHEBANG 113
|
||||||
|
|
||||||
static FILE * log_fp = NULL;
|
static FILE * log_fp = NULL;
|
||||||
|
|
||||||
|
@ -750,6 +751,88 @@ _shebangStartsWith(const wchar_t *buffer, int bufferLength, const wchar_t *prefi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
searchPath(SearchInfo *search, const wchar_t *shebang, int shebangLength)
|
||||||
|
{
|
||||||
|
if (isEnvVarSet(L"PYLAUNCHER_NO_SEARCH_PATH")) {
|
||||||
|
return RC_NO_SHEBANG;
|
||||||
|
}
|
||||||
|
|
||||||
|
wchar_t *command;
|
||||||
|
if (!_shebangStartsWith(shebang, shebangLength, L"/usr/bin/env ", &command)) {
|
||||||
|
return RC_NO_SHEBANG;
|
||||||
|
}
|
||||||
|
|
||||||
|
wchar_t filename[MAXLEN];
|
||||||
|
int lastDot = 0;
|
||||||
|
int commandLength = 0;
|
||||||
|
while (commandLength < MAXLEN && command[commandLength] && !isspace(command[commandLength])) {
|
||||||
|
if (command[commandLength] == L'.') {
|
||||||
|
lastDot = commandLength;
|
||||||
|
}
|
||||||
|
filename[commandLength] = command[commandLength];
|
||||||
|
commandLength += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!commandLength || commandLength == MAXLEN) {
|
||||||
|
return RC_BAD_VIRTUAL_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
filename[commandLength] = L'\0';
|
||||||
|
|
||||||
|
const wchar_t *ext = L".exe";
|
||||||
|
// If the command already has an extension, we do not want to add it again
|
||||||
|
if (!lastDot || _comparePath(&filename[lastDot], -1, ext, -1)) {
|
||||||
|
if (wcscat_s(filename, MAXLEN, L".exe")) {
|
||||||
|
return RC_BAD_VIRTUAL_PATH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wchar_t pathVariable[MAXLEN];
|
||||||
|
int n = GetEnvironmentVariableW(L"PATH", pathVariable, MAXLEN);
|
||||||
|
if (!n) {
|
||||||
|
if (GetLastError() == ERROR_ENVVAR_NOT_FOUND) {
|
||||||
|
return RC_NO_SHEBANG;
|
||||||
|
}
|
||||||
|
winerror(0, L"Failed to read PATH\n", filename);
|
||||||
|
return RC_INTERNAL_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
wchar_t buffer[MAXLEN];
|
||||||
|
n = SearchPathW(pathVariable, filename, NULL, MAXLEN, buffer, NULL);
|
||||||
|
if (!n) {
|
||||||
|
if (GetLastError() == ERROR_FILE_NOT_FOUND) {
|
||||||
|
debug(L"# Did not find %s on PATH\n", filename);
|
||||||
|
// If we didn't find it on PATH, let normal handling take over
|
||||||
|
return RC_NO_SHEBANG;
|
||||||
|
}
|
||||||
|
// Other errors should cause us to break
|
||||||
|
winerror(0, L"Failed to find %s on PATH\n", filename);
|
||||||
|
return RC_BAD_VIRTUAL_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we aren't going to call ourselves again
|
||||||
|
// If we are, pretend there was no shebang and let normal handling take over
|
||||||
|
if (GetModuleFileNameW(NULL, filename, MAXLEN) &&
|
||||||
|
0 == _comparePath(filename, -1, buffer, -1)) {
|
||||||
|
debug(L"# ignoring recursive shebang command\n");
|
||||||
|
return RC_NO_SHEBANG;
|
||||||
|
}
|
||||||
|
|
||||||
|
wchar_t *buf = allocSearchInfoBuffer(search, n + 1);
|
||||||
|
if (!buf || wcscpy_s(buf, n + 1, buffer)) {
|
||||||
|
return RC_NO_MEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
search->executablePath = buf;
|
||||||
|
search->executableArgs = &command[commandLength];
|
||||||
|
search->executableArgsLength = shebangLength - commandLength;
|
||||||
|
debug(L"# Found %s on PATH\n", buf);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
int
|
int
|
||||||
_readIni(const wchar_t *section, const wchar_t *settingName, wchar_t *buffer, int bufferLength)
|
_readIni(const wchar_t *section, const wchar_t *settingName, wchar_t *buffer, int bufferLength)
|
||||||
{
|
{
|
||||||
|
@ -885,6 +968,12 @@ checkShebang(SearchInfo *search)
|
||||||
}
|
}
|
||||||
debug(L"Shebang: %s\n", shebang);
|
debug(L"Shebang: %s\n", shebang);
|
||||||
|
|
||||||
|
// Handle shebangs that we should search PATH for
|
||||||
|
exitCode = searchPath(search, shebang, shebangLength);
|
||||||
|
if (exitCode != RC_NO_SHEBANG) {
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle some known, case-sensitive shebang templates
|
// Handle some known, case-sensitive shebang templates
|
||||||
const wchar_t *command;
|
const wchar_t *command;
|
||||||
int commandLength;
|
int commandLength;
|
||||||
|
@ -895,6 +984,7 @@ checkShebang(SearchInfo *search)
|
||||||
L"",
|
L"",
|
||||||
NULL
|
NULL
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const wchar_t **tmpl = shebangTemplates; *tmpl; ++tmpl) {
|
for (const wchar_t **tmpl = shebangTemplates; *tmpl; ++tmpl) {
|
||||||
if (_shebangStartsWith(shebang, shebangLength, *tmpl, &command)) {
|
if (_shebangStartsWith(shebang, shebangLength, *tmpl, &command)) {
|
||||||
commandLength = 0;
|
commandLength = 0;
|
||||||
|
@ -910,6 +1000,22 @@ checkShebang(SearchInfo *search)
|
||||||
} else if (_shebangStartsWith(command, commandLength, L"python", NULL)) {
|
} else if (_shebangStartsWith(command, commandLength, L"python", NULL)) {
|
||||||
search->tag = &command[6];
|
search->tag = &command[6];
|
||||||
search->tagLength = commandLength - 6;
|
search->tagLength = commandLength - 6;
|
||||||
|
// If we had 'python3.12.exe' then we want to strip the suffix
|
||||||
|
// off of the tag
|
||||||
|
if (search->tagLength > 4) {
|
||||||
|
const wchar_t *suffix = &search->tag[search->tagLength - 4];
|
||||||
|
if (0 == _comparePath(suffix, 4, L".exe", -1)) {
|
||||||
|
search->tagLength -= 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we had 'python3_d' then we want to strip the '_d' (any
|
||||||
|
// '.exe' is already gone)
|
||||||
|
if (search->tagLength > 2) {
|
||||||
|
const wchar_t *suffix = &search->tag[search->tagLength - 2];
|
||||||
|
if (0 == _comparePath(suffix, 2, L"_d", -1)) {
|
||||||
|
search->tagLength -= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
search->oldStyleTag = true;
|
search->oldStyleTag = true;
|
||||||
search->executableArgs = &command[commandLength];
|
search->executableArgs = &command[commandLength];
|
||||||
search->executableArgsLength = shebangLength - commandLength;
|
search->executableArgsLength = shebangLength - commandLength;
|
||||||
|
|
Loading…
Reference in New Issue