Skip to content

Commit

Permalink
pythongh-94399: Restore PATH search behaviour of py.exe launcher
Browse files Browse the repository at this point in the history
  • Loading branch information
zooba committed Aug 2, 2022
1 parent e3b6ff1 commit 4d61fe3
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 1 deletion.
5 changes: 5 additions & 0 deletions Doc/using/windows.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
executable :envvar:`PATH` for a Python executable. This corresponds to the
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
--------------------------
Expand Down
17 changes: 16 additions & 1 deletion Lib/test/test_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,10 @@ def run_py(self, args, env=None, allow_fail=False, expect_returncode=0, argv=Non
ignore = {"VIRTUAL_ENV", "PY_PYTHON", "PY_PYTHON2", "PY_PYTHON3"}
env = {
**{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_NO_SEARCH_PATH": "1",
"PYLAUNCHER_DEBUG": "1",
"PYLAUNCHER_DRYRUN": "1",
**{k.upper(): v for k, v in (env or {}).items()},
}
if not argv:
argv = [self.py_exe, *args]
Expand Down Expand Up @@ -551,6 +552,20 @@ def test_py_shebang_short_argv0(self):
self.assertEqual("3.100", data["SearchInfo.tag"])
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={"PYLAUNCHER_NO_SEARCH_PATH": ""})
self.assertEqual(f"{sys.executable} -prearg {script} -postarg", data["stdout"].strip())

def test_recursive_search_path(self):
with self.py_ini(TEST_PY_COMMANDS):
with self.script("#! /usr/bin/env py") as script:
data = self.run_py([script], env={"PYLAUNCHER_NO_SEARCH_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):
data = self.run_py(["-V:3.10"], env={"PYLAUNCHER_ALWAYS_INSTALL": "1"}, expect_returncode=111)
cmd = data["stdout"].strip()
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
80 changes: 80 additions & 0 deletions PC/launcher2.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#define RC_DUPLICATE_ITEM 110
#define RC_INSTALLING 111
#define RC_NO_PYTHON_AT_ALL 112
#define RC_NO_SHEBANG 113

static FILE * log_fp = NULL;

Expand Down Expand Up @@ -750,6 +751,78 @@ _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 buffer[MAXLEN];
SetSearchPathMode(BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE);
int n = SearchPathW(NULL, filename, NULL, MAXLEN, buffer, NULL);

This comment has been minimized.

Copy link
@eryksun

eryksun Aug 3, 2022

The launcher could pass the value of PATH as lpPath. Only use the default search (i.e. NULL) when PATH is empty or undefined. This avoids searching the application directory and current directory, unless PATH explicitly contains them or a "." entry.

if (!n) {
if (GetLastError() == ERROR_FILE_NOT_FOUND) {
// 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
_readIni(const wchar_t *section, const wchar_t *settingName, wchar_t *buffer, int bufferLength)
{
Expand Down Expand Up @@ -885,6 +958,12 @@ checkShebang(SearchInfo *search)
}
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
const wchar_t *command;
int commandLength;
Expand All @@ -895,6 +974,7 @@ checkShebang(SearchInfo *search)
L"",
NULL
};

for (const wchar_t **tmpl = shebangTemplates; *tmpl; ++tmpl) {
if (_shebangStartsWith(shebang, shebangLength, *tmpl, &command)) {
commandLength = 0;
Expand Down

0 comments on commit 4d61fe3

Please sign in to comment.