8000 gh-94399: Restore PATH search behaviour of py.exe launcher for '/usr/… · python/cpython@67840ed · GitHub
[go: up one dir, main page]

Skip to content

Commit 67840ed

Browse files
authored
gh-94399: Restore PATH search behaviour of py.exe launcher for '/usr/bin/env' shebang lines (GH-95582)
1 parent b53aed7 commit 67840ed

File tree

4 files changed

+159
-8
lines changed

4 files changed

+159
-8
lines changed

Doc/using/windows.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,11 @@ The ``/usr/bin/env`` form of shebang line has one further special property.
868868
Before looking for installed Python interpreters, this form will search the
869869
executable :envvar:`PATH` for a Python executable. This corresponds to the
870870
behaviour of the Unix ``env`` program, which performs a :envvar:`PATH` search.
871+
If an executable matching the first argument after the ``env`` command cannot
872+
be found, it will be handled as described below. Additionally, the environment
873+
variable :envvar:`PYLAUNCHER_NO_SEARCH_PATH` may be set (to any value) to skip
874+
this additional search.
875+
871876

872877
Arguments in shebang lines
873878
--------------------------

Lib/test/test_launcher.py

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -187,16 +187,21 @@ def find_py(cls):
187187
)
188188
return py_exe
189189

190+
def get_py_exe(self):
191+
if not self.py_exe:
192+
self.py_exe = self.find_py()
193+
return self.py_exe
194+
190195
def run_py(self, args, env=None, allow_fail=False, expect_returncode=0, argv=None):
191196
if not self.py_exe:
192197
self.py_exe = self.find_py()
193198

194199
ignore = {"VIRTUAL_ENV", "PY_PYTHON", "PY_PYTHON2", "PY_PYTHON3"}
195200
env = {
196201
**{k.upper(): v for k, v in os.environ.items() if k.upper() not in ignore},
197-
**{k.upper(): v for k, v in (env or {}).items()},
198202
"PYLAUNCHER_DEBUG": "1",
199203
"PYLAUNCHER_DRYRUN": "1",
204+
**{k.upper(): v for k, v in (env or {}).items()},
200205
}
201206
if not argv:
202207
argv = [self.py_exe, *args]
@@ -496,61 +501,93 @@ def test_virtualenv_with_env(self):
496501

497502
def test_py_shebang(self):
498503
with self.py_ini(TEST_PY_COMMANDS):
499-
with self.script("#! /usr/bin/env python -prearg") as script:
504+
with self.script("#! /usr/bin/python -prearg") as script:
500505
data = self.run_py([script, "-postarg"])
501506
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
502507
self.assertEqual("3.100", data["SearchInfo.tag"])
503508
self.assertEqual(f"X.Y.exe -prearg {script} -postarg", data["stdout"].strip())
504509

505510
def test_py2_shebang(self):
506511
with self.py_ini(TEST_PY_COMMANDS):
507-
with self.script("#! /usr/bin/env python2 -prearg") as script:
512+
with self.script("#! /usr/bin/python2 -prearg") as script:
508513
data = self.run_py([script, "-postarg"])
509514
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
510515
self.assertEqual("3.100-32", data["SearchInfo.tag"])
511516
self.assertEqual(f"X.Y-32.exe -prearg {script} -postarg", data["stdout"].strip())
512517

513518
def test_py3_shebang(self):
514519
with self.py_ini(TEST_PY_COMMANDS):
515-
with self.script("#! /usr/bin/env python3 -prearg") as script:
520+
with self.script("#! /usr/bin/python3 -prearg") as script:
516521
data = self.run_py([script, "-postarg"])
517522
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
518523
self.assertEqual("3.100-arm64", data["SearchInfo.tag"])
519524
self.assertEqual(f"X.Y-arm64.exe -X fake_arg_for_test -prearg {script} -postarg", data["stdout"].strip())
520525

521526
def test_py_shebang_nl(self):
522527 17AE
with self.py_ini(TEST_PY_COMMANDS):
523-
with self.script("#! /usr/bin/env python -prearg\n") as script:
528+
with self.script("#! /usr/bin/python -prearg\n") as script:
524529
data = self.run_py([script, "-postarg"])
525530
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
526531
self.assertEqual("3.100", data["SearchInfo.tag"])
527532
self.assertEqual(f"X.Y.exe -prearg {script} -postarg", data["stdout"].strip())
528533

529534
def test_py2_shebang_nl(self):
530535
with self.py_ini(TEST_PY_COMMANDS):
531-
with self.script("#! /usr/bin/env python2 -prearg\n") as script:
536+
with self.script("#! /usr/bin/python2 -prearg\n") as script:
532537
data = self.run_py([script, "-postarg"])
533538
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
534539
self.assertEqual("3.100-32", data["SearchInfo.tag"])
535540
self.assertEqual(f"X.Y-32.exe -prearg {script} -postarg", data["stdout"].strip())
536541

537542
def test_py3_shebang_nl(self):
538543
with self.py_ini(TEST_PY_COMMANDS):
539-
with self.script("#! /usr/bin/env python3 -prearg\n") as script:
544+
with self.script("#! /usr/bin/python3 -prearg\n") as script:
540545
data = self.run_py([script, "-postarg"])
541546
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
542547
self.assertEqual("3.100-arm64", data["SearchInfo.tag"])
543548
self.assertEqual(f"X.Y-arm64.exe -X fake_arg_for_test -prearg {script} -postarg", data["stdout"].strip())
544549

545550
def test_py_shebang_short_argv0(self):
546551
with self.py_ini(TEST_PY_COMMANDS):
547-
with self.script("#! /usr/bin/env python -prearg") as script:
552+
with self.script("#! /usr/bin/python -prearg") as script:
548553
# Override argv to only pass "py.exe" as the command
549554
data = self.run_py([script, "-postarg"], argv=f'"py.exe" "{script}" -postarg')
550555
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
551556
self.assertEqual("3.100", data["SearchInfo.tag"])
552557
self.assertEqual(f'X.Y.exe -prearg "{script}" -postarg', data["stdout"].strip())
553558

559+
def test_search_path(self):
560+
stem = Path(sys.executable).stem
561+
with self.py_ini(TEST_PY_COMMANDS):
562+
with self.script(f"#! /usr/bin/env {stem} -prearg") as script:
563+
data = self.run_py(
564+
[script, "-postarg"],
565+
env={"PATH": f"{Path(sys.executable).parent};{os.getenv('PATH')}"},
566+
)
567+
self.assertEqual(f"{sys.executable} -prearg {script} -postarg", data["stdout"].strip())
568+
569+
def test_search_path_exe(self):
570+
# Leave the .exe on the name to ensure we don't add it a second time
571+
name = Path(sys.executable).name
572+
with self.py_ini(TEST_PY_COMMANDS):
573+
with self.script(f"#! /usr/bin/env {name} -prearg") as script:
574+
data = self.run_py(
575+
[script, "-postarg"],
576+
env={"PATH": f"{Path(sys.executable).parent};{os.getenv('PATH')}"},
577+
)
578+
self.assertEqual(f"{sys.executable} -prearg {script} -postarg", data["stdout"].strip())
579+
580+
def test_recursive_search_path(self):
581+
stem = self.get_py_exe().stem
582+
with self.py_ini(TEST_PY_COMMANDS):
583+
with self.script(f"#! /usr/bin/env {stem}") as script:
584+
data = self.run_py(
585+
[script],
586+
env={"PATH": f"{self.get_py_exe().parent};{os.getenv('PATH')}"},
587+
)
588+
# The recursive search is ignored and we get normal "py" behavior
589+
self.assertEqual(f"X.Y.exe {script}", data["stdout"].strip())
590+
554591
def test_install(self):
555592
data = self.run_py(["-V:3.10"], env={"PYLAUNCHER_ALWAYS_INSTALL": "1"}, expect_returncode=111)
556593
cmd = data["stdout"].strip()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Restores the behaviour of :ref:`launcher` for ``/usr/bin/env`` shebang
2+
lines, which will now search :envvar:`PATH` for an executable matching the
3+
given command. If none is found, the usual search process is used.

PC/launcher2.c

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#define RC_DUPLICATE_ITEM 110
3737
#define RC_INSTALLING 111
3838
#define RC_NO_PYTHON_AT_ALL 112
39+
#define RC_NO_SHEBANG 113
3940

4041
static FILE * log_fp = NULL;
4142

@@ -750,6 +751,88 @@ _shebangStartsWith(const wchar_t *buffer, int bufferLength, const wchar_t *prefi
750751
}
751752

752753

754+
int
755+
searchPath(SearchInfo *search, const wchar_t *shebang, int shebangLength)
756+
{
757+
if (isEnvVarSet(L"PYLAUNCHER_NO_SEARCH_PATH")) {
758+
return RC_NO_SHEBANG;
759+
}
760+
761+
wchar_t *command;
762+
if (!_shebangStartsWith(shebang, shebangLength, L"/usr/bin/env ", &command)) {
763+
return RC_NO_SHEBANG;
764+
}
765+
766+
wchar_t filename[MAXLEN];
767+
int lastDot = 0;
768+
int commandLength = 0;
769+
while (commandLength < MAXLEN && command[commandLength] && !isspace(command[commandLength])) {
770+
if (command[commandLength] == L'.') {
771+
lastDot = commandLength;
772+
}
773+
filename[commandLength] = command[commandLength];
774+
commandLength += 1;
775+
}
776+
777+
if (!commandLength || commandLength == MAXLEN) {
778+
return RC_BAD_VIRTUAL_PATH;
779+
}
780+
781+
filename[commandLength] = L'\0';
782+
783+
const wchar_t *ext = L".exe";
784+
// If the command already has an extension, we do not want to add it again
785+
if (!lastDot || _comparePath(&filename[lastDot], -1, ext, -1)) {
786+
if (wcscat_s(filename, MAXLEN, L".exe")) {
787+
return RC_BAD_VIRTUAL_PATH;
788+
}
789+
}
790+
791+
wchar_t pathVariable[MAXLEN];
792+
int n = GetEnvironmentVariableW(L"PATH", pathVariable, MAXLEN);
793+
if (!n) {
794+
if (GetLastError() == ERROR_ENVVAR_NOT_FOUND) {
795+
return RC_NO_SHEBANG;
796+
}
797+
winerror(0, L"Failed to read PATH\n", filename);
798+
return RC_INTERNAL_ERROR;
799+
}
800+
801+
wchar_t buffer[MAXLEN];
802+
n = SearchPathW(pathVariable, filename, NULL, MAXLEN, buffer, NULL);
803+
if (!n) {
804+
if (GetLastError() == ERROR_FILE_NOT_FOUND) {
805+
debug(L"# Did not find %s on PATH\n", filename);
806+
// If we didn't find it on PATH, let normal handling take over
807+
return RC_NO_SHEBANG;
808+
}
809+
// Other errors should cause us to break
810+
winerror(0, L"Failed to find %s on PATH\n", filename);
811+
return RC_BAD_VIRTUAL_PATH;
812+
}
813+
814+
// Check that we aren't going to call ourselves again
815+
// If we are, pretend there was no shebang and let normal handling take over
816+
if (GetModuleFileNameW(NULL, filename, MAXLEN) &&
817+
0 == _comparePath(filename, -1, buffer, -1)) {
818+
debug(L"# ignoring recursive shebang command\n");
819+
return RC_NO_SHEBANG;
820+
}
821+
822+
wchar_t *buf = allocSearchInfoBuffer(search, n + 1);
823+
if (!buf || wcscpy_s(buf, n + 1, buffer)) {
824+
return RC_NO_MEMORY;
825+
}
826+
827+
search->executablePath = buf;
828+
search->executableArgs = &command[commandLength];
829+
search->executableArgsLength = shebangLength - commandLength;
830+
debug(L"# Found %s on PATH\n", buf);
831+
832+
return 0;
833+
}
834+
835+
753836
int
754837
_readIni(const wchar_t *section, const wchar_t *settingName, wchar_t *buffer, int bufferLength)
755838
{
@@ -885,6 +968,12 @@ checkShebang(SearchInfo *search)
885968
}
886969
debug(L"Shebang: %s\n", shebang);
887970

971+
// Handle shebangs that we should search PATH for
972+
exitCode = searchPath(search, shebang, shebangLength);
973+
if (exitCode != RC_NO_SHEBANG) {
974+
return exitCode;
975+
}
976+
888977
// Handle some known, case-sensitive shebang templates
889978
const wchar_t *command;
890979
int commandLength;
@@ -895,6 +984,7 @@ checkShebang(SearchInfo *search)
895984
L"",
896985
NULL
897986
};
987+
898988
for (const wchar_t **tmpl = shebangTemplates; *tmpl; ++tmpl) {
899989
if (_shebangStartsWith(shebang, shebangLength, *tmpl, &command)) {
900990
commandLength = 0;
@@ -910,6 +1000,22 @@ checkShebang(SearchInfo *search)
9101000
} else if (_shebangStartsWith(command, commandLength, L"python", NULL)) {
9111001
search->tag = &command[6];
9121002
search->tagLength = commandLength - 6;
1003+
// If we had 'python3.12.exe' then we want to strip the suffix
1004+
// off of the tag
1005+
if (search->tagLength > 4) {
1006+
const wchar_t *suffix = &search->tag[search->tagLength - 4];
1007+
if (0 == _comparePath(suffix, 4, L".exe", -1)) {
1008+
search->tagLength -= 4;
1009+
}
1010+
}
1011+
// If we had 'python3_d' then we want to strip the '_d' (any
1012+
// '.exe' is already gone)
1013+
if (search->tagLength > 2) {
1014+
const wchar_t *suffix = &search->tag[search->tagLength - 2];
1015+
if (0 == _comparePath(suffix, 2, L"_d", -1)) {
1016+
search->tagLength -= 2;
1017+
}
1018+
}
9131019
search->oldStyleTag = true;
9141020
search->executableArgs = &command[commandLength];
9151021
search->executableArgsLength = shebangLength - commandLength;

0 commit comments

Comments
 (0)
0