gh-98692: Enable treating shebang lines as executables in py.exe launcher (GH-98732)

This commit is contained in:
Steve Dower 2022-10-31 21:05:50 +00:00 committed by GitHub
parent 4702552885
commit 88297e2a8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 124 additions and 4 deletions

View File

@ -866,7 +866,6 @@ minor version. I.e. ``/usr/bin/python3.7-32`` will request usage of the
not provably i386/32-bit". To request a specific environment, use the new not provably i386/32-bit". To request a specific environment, use the new
``-V:<TAG>`` argument with the complete tag. ``-V:<TAG>`` argument with the complete tag.
The ``/usr/bin/env`` form of shebang line has one further special property. 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
@ -876,6 +875,13 @@ 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 variable :envvar:`PYLAUNCHER_NO_SEARCH_PATH` may be set (to any value) to skip
this additional search. this additional search.
Shebang lines that do not match any of these patterns are treated as **Windows**
paths that are absolute or relative to the directory containing the script file.
This is a convenience for Windows-only scripts, such as those generated by an
installer, since the behavior is not compatible with Unix-style shells.
These paths may be quoted, and may include multiple arguments, after which the
path to the script and any additional arguments will be appended.
Arguments in shebang lines Arguments in shebang lines
-------------------------- --------------------------

View File

@ -516,6 +516,14 @@ class TestLauncher(unittest.TestCase, RunPyMixin):
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_python_shebang(self):
with self.py_ini(TEST_PY_COMMANDS):
with self.script("#! python -prearg") as script:
data = self.run_py([script, "-postarg"])
self.assertEqual("PythonTestSuite", data["SearchInfo.company"])
self.assertEqual("3.100", data["SearchInfo.tag"])
self.assertEqual(f"X.Y.exe -prearg {script} -postarg", data["stdout"].strip())
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/python2 -prearg") as script: with self.script("#! /usr/bin/python2 -prearg") as script:
@ -617,3 +625,42 @@ class TestLauncher(unittest.TestCase, RunPyMixin):
self.assertIn("winget.exe", cmd) self.assertIn("winget.exe", cmd)
# Both command lines include the store ID # Both command lines include the store ID
self.assertIn("9PJPW5LDXLZ5", cmd) self.assertIn("9PJPW5LDXLZ5", cmd)
def test_literal_shebang_absolute(self):
with self.script(f"#! C:/some_random_app -witharg") as script:
data = self.run_py([script])
self.assertEqual(
f"C:\\some_random_app -witharg {script}",
data["stdout"].strip(),
)
def test_literal_shebang_relative(self):
with self.script(f"#! ..\\some_random_app -witharg") as script:
data = self.run_py([script])
self.assertEqual(
f"{script.parent.parent}\\some_random_app -witharg {script}",
data["stdout"].strip(),
)
def test_literal_shebang_quoted(self):
with self.script(f'#! "some random app" -witharg') as script:
data = self.run_py([script])
self.assertEqual(
f'"{script.parent}\\some random app" -witharg {script}',
data["stdout"].strip(),
)
with self.script(f'#! some" random "app -witharg') as script:
data = self.run_py([script])
self.assertEqual(
f'"{script.parent}\\some random app" -witharg {script}',
data["stdout"].strip(),
)
def test_literal_shebang_quoted_escape(self):
with self.script(f'#! some\\" random "app -witharg') as script:
data = self.run_py([script])
self.assertEqual(
f'"{script.parent}\\some\\ random app" -witharg {script}',
data["stdout"].strip(),
)

View File

@ -0,0 +1,2 @@
Fix the :ref:`launcher` ignoring unrecognized shebang lines instead of
treating them as local paths

View File

@ -871,6 +871,62 @@ _findCommand(SearchInfo *search, const wchar_t *command, int commandLength)
} }
int
_useShebangAsExecutable(SearchInfo *search, const wchar_t *shebang, int shebangLength)
{
wchar_t buffer[MAXLEN];
wchar_t script[MAXLEN];
wchar_t command[MAXLEN];
int commandLength = 0;
int inQuote = 0;
if (!shebang || !shebangLength) {
return 0;
}
wchar_t *pC = command;
for (int i = 0; i < shebangLength; ++i) {
wchar_t c = shebang[i];
if (isspace(c) && !inQuote) {
commandLength = i;
break;
} else if (c == L'"') {
inQuote = !inQuote;
} else if (c == L'/' || c == L'\\') {
*pC++ = L'\\';
} else {
*pC++ = c;
}
}
*pC = L'\0';
if (!GetCurrentDirectoryW(MAXLEN, buffer) ||
wcsncpy_s(script, MAXLEN, search->scriptFile, search->scriptFileLength) ||
FAILED(PathCchCombineEx(buffer, MAXLEN, buffer, script,
PATHCCH_ALLOW_LONG_PATHS)) ||
FAILED(PathCchRemoveFileSpec(buffer, MAXLEN)) ||
FAILED(PathCchCombineEx(buffer, MAXLEN, buffer, command,
PATHCCH_ALLOW_LONG_PATHS))
) {
return RC_NO_MEMORY;
}
int n = (int)wcsnlen(buffer, MAXLEN);
wchar_t *path = allocSearchInfoBuffer(search, n + 1);
if (!path) {
return RC_NO_MEMORY;
}
wcscpy_s(path, n + 1, buffer);
search->executablePath = path;
if (commandLength) {
search->executableArgs = &shebang[commandLength];
search->executableArgsLength = shebangLength - commandLength;
}
return 0;
}
int int
checkShebang(SearchInfo *search) checkShebang(SearchInfo *search)
{ {
@ -963,13 +1019,19 @@ checkShebang(SearchInfo *search)
L"/usr/bin/env ", L"/usr/bin/env ",
L"/usr/bin/", L"/usr/bin/",
L"/usr/local/bin/", L"/usr/local/bin/",
L"", L"python",
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;
// Normally "python" is the start of the command, but we also need it
// as a shebang prefix for back-compat. We move the command marker back
// if we match on that one.
if (0 == wcscmp(*tmpl, L"python")) {
command -= 6;
}
while (command[commandLength] && !isspace(command[commandLength])) { while (command[commandLength] && !isspace(command[commandLength])) {
commandLength += 1; commandLength += 1;
} }
@ -1012,11 +1074,14 @@ checkShebang(SearchInfo *search)
debug(L"# Found shebang command but could not execute it: %.*s\n", debug(L"# Found shebang command but could not execute it: %.*s\n",
commandLength, command); commandLength, command);
} }
break; // search is done by this point
return 0;
} }
} }
return 0; // Unrecognised commands are joined to the script's directory and treated
// as the executable path
return _useShebangAsExecutable(search, shebang, shebangLength);
} }