cpython/PC/launcher2.c
Steve Dower 737d367b1f
gh-77532: Minor tweaks to allow compiling with PlatformToolset=ClangCL on Windows (GH-101352)
To use this, ensure that clang support was selected in Visual Studio Installer, then set the PlatformToolset environment variable to "ClangCL" and build as normal from the command line.
It remains unsupported, but at least is possible now for experimentation.
2023-01-27 14:45:08 +00:00

2666 lines
81 KiB
C

/*
* Rewritten Python launcher for Windows
*
* This new rewrite properly handles PEP 514 and allows any registered Python
* runtime to be launched. It also enables auto-install of versions when they
* are requested but no installation can be found.
*/
#define __STDC_WANT_LIB_EXT1__ 1
#include <windows.h>
#include <pathcch.h>
#include <fcntl.h>
#include <io.h>
#include <shlobj.h>
#include <stdio.h>
#include <stdbool.h>
#include <tchar.h>
#include <assert.h>
#define MS_WINDOWS
#include "patchlevel.h"
#define MAXLEN PATHCCH_MAX_CCH
#define MSGSIZE 1024
#define RC_NO_STD_HANDLES 100
#define RC_CREATE_PROCESS 101
#define RC_BAD_VIRTUAL_PATH 102
#define RC_NO_PYTHON 103
#define RC_NO_MEMORY 104
#define RC_NO_SCRIPT 105
#define RC_NO_VENV_CFG 106
#define RC_BAD_VENV_CFG 107
#define RC_NO_COMMANDLINE 108
#define RC_INTERNAL_ERROR 109
#define RC_DUPLICATE_ITEM 110
#define RC_INSTALLING 111
#define RC_NO_PYTHON_AT_ALL 112
#define RC_NO_SHEBANG 113
#define RC_RECURSIVE_SHEBANG 114
static FILE * log_fp = NULL;
void
debug(wchar_t * format, ...)
{
va_list va;
if (log_fp != NULL) {
wchar_t buffer[MAXLEN];
int r = 0;
va_start(va, format);
r = vswprintf_s(buffer, MAXLEN, format, va);
va_end(va);
if (r <= 0) {
return;
}
fputws(buffer, log_fp);
while (r && isspace(buffer[r])) {
buffer[r--] = L'\0';
}
if (buffer[0]) {
OutputDebugStringW(buffer);
}
}
}
void
formatWinerror(int rc, wchar_t * message, int size)
{
FormatMessageW(
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, rc, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
message, size, NULL);
}
void
winerror(int err, wchar_t * format, ... )
{
va_list va;
wchar_t message[MSGSIZE];
wchar_t win_message[MSGSIZE];
int len;
if (err == 0) {
err = GetLastError();
}
va_start(va, format);
len = _vsnwprintf_s(message, MSGSIZE, _TRUNCATE, format, va);
va_end(va);
formatWinerror(err, win_message, MSGSIZE);
if (len >= 0) {
_snwprintf_s(&message[len], MSGSIZE - len, _TRUNCATE, L": %s",
win_message);
}
#if !defined(_WINDOWS)
fwprintf(stderr, L"%s\n", message);
#else
MessageBoxW(NULL, message, L"Python Launcher is sorry to say ...",
MB_OK);
#endif
}
void
error(wchar_t * format, ... )
{
va_list va;
wchar_t message[MSGSIZE];
va_start(va, format);
_vsnwprintf_s(message, MSGSIZE, _TRUNCATE, format, va);
va_end(va);
#if !defined(_WINDOWS)
fwprintf(stderr, L"%s\n", message);
#else
MessageBoxW(NULL, message, L"Python Launcher is sorry to say ...",
MB_OK);
#endif
}
typedef BOOL (*PIsWow64Process2)(HANDLE, USHORT*, USHORT*);
USHORT
_getNativeMachine()
{
static USHORT _nativeMachine = IMAGE_FILE_MACHINE_UNKNOWN;
if (_nativeMachine == IMAGE_FILE_MACHINE_UNKNOWN) {
USHORT processMachine;
HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");
PIsWow64Process2 IsWow64Process2 = kernel32 ?
(PIsWow64Process2)GetProcAddress(kernel32, "IsWow64Process2") :
NULL;
if (!IsWow64Process2) {
BOOL wow64Process;
if (!IsWow64Process(NULL, &wow64Process)) {
winerror(0, L"Checking process type");
} else if (wow64Process) {
// We should always be a 32-bit executable, so if running
// under emulation, it must be a 64-bit host.
_nativeMachine = IMAGE_FILE_MACHINE_AMD64;
} else {
// Not running under emulation, and an old enough OS to not
// have IsWow64Process2, so assume it's x86.
_nativeMachine = IMAGE_FILE_MACHINE_I386;
}
} else if (!IsWow64Process2(NULL, &processMachine, &_nativeMachine)) {
winerror(0, L"Checking process type");
}
}
return _nativeMachine;
}
bool
isAMD64Host()
{
return _getNativeMachine() == IMAGE_FILE_MACHINE_AMD64;
}
bool
isARM64Host()
{
return _getNativeMachine() == IMAGE_FILE_MACHINE_ARM64;
}
bool
isEnvVarSet(const wchar_t *name)
{
/* only looking for non-empty, which means at least one character
and the null terminator */
return GetEnvironmentVariableW(name, NULL, 0) >= 2;
}
bool
join(wchar_t *buffer, size_t bufferLength, const wchar_t *fragment)
{
if (SUCCEEDED(PathCchCombineEx(buffer, bufferLength, buffer, fragment, PATHCCH_ALLOW_LONG_PATHS))) {
return true;
}
return false;
}
int
_compare(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
{
// Empty strings sort first
if (!x || !xLen) {
return (!y || !yLen) ? 0 : -1;
} else if (!y || !yLen) {
return 1;
}
switch (CompareStringEx(
LOCALE_NAME_INVARIANT, NORM_IGNORECASE | SORT_DIGITSASNUMBERS,
x, xLen, y, yLen,
NULL, NULL, 0
)) {
case CSTR_LESS_THAN:
return -1;
case CSTR_EQUAL:
return 0;
case CSTR_GREATER_THAN:
return 1;
default:
winerror(0, L"Error comparing '%.*s' and '%.*s' (compare)", xLen, x, yLen, y);
return -1;
}
}
int
_compareArgument(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
{
// Empty strings sort first
if (!x || !xLen) {
return (!y || !yLen) ? 0 : -1;
} else if (!y || !yLen) {
return 1;
}
switch (CompareStringEx(
LOCALE_NAME_INVARIANT, 0,
x, xLen, y, yLen,
NULL, NULL, 0
)) {
case CSTR_LESS_THAN:
return -1;
case CSTR_EQUAL:
return 0;
case CSTR_GREATER_THAN:
return 1;
default:
winerror(0, L"Error comparing '%.*s' and '%.*s' (compareArgument)", xLen, x, yLen, y);
return -1;
}
}
int
_comparePath(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
{
// Empty strings sort first
if (!x || !xLen) {
return !y || !yLen ? 0 : -1;
} else if (!y || !yLen) {
return 1;
}
switch (CompareStringOrdinal(x, xLen, y, yLen, TRUE)) {
case CSTR_LESS_THAN:
return -1;
case CSTR_EQUAL:
return 0;
case CSTR_GREATER_THAN:
return 1;
default:
winerror(0, L"Error comparing '%.*s' and '%.*s' (comparePath)", xLen, x, yLen, y);
return -1;
}
}
bool
_startsWith(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
{
if (!x || !y) {
return false;
}
yLen = yLen < 0 ? (int)wcsnlen_s(y, MAXLEN) : yLen;
xLen = xLen < 0 ? (int)wcsnlen_s(x, MAXLEN) : xLen;
return xLen >= yLen && 0 == _compare(x, yLen, y, yLen);
}
bool
_startsWithArgument(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
{
if (!x || !y) {
return false;
}
yLen = yLen < 0 ? (int)wcsnlen_s(y, MAXLEN) : yLen;
xLen = xLen < 0 ? (int)wcsnlen_s(x, MAXLEN) : xLen;
return xLen >= yLen && 0 == _compareArgument(x, yLen, y, yLen);
}
/******************************************************************************\
*** HELP TEXT ***
\******************************************************************************/
int
showHelpText(wchar_t ** argv)
{
// The help text is stored in launcher-usage.txt, which is compiled into
// the launcher and loaded at runtime if needed.
//
// The file must be UTF-8. There are two substitutions:
// %ls - PY_VERSION (as wchar_t*)
// %ls - argv[0] (as wchar_t*)
HRSRC res = FindResourceExW(NULL, L"USAGE", MAKEINTRESOURCE(1), MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL));
HGLOBAL resData = res ? LoadResource(NULL, res) : NULL;
const char *usage = resData ? (const char*)LockResource(resData) : NULL;
if (usage == NULL) {
winerror(0, L"Unable to load usage text");
return RC_INTERNAL_ERROR;
}
DWORD cbData = SizeofResource(NULL, res);
DWORD cchUsage = MultiByteToWideChar(CP_UTF8, 0, usage, cbData, NULL, 0);
if (!cchUsage) {
winerror(0, L"Unable to preprocess usage text");
return RC_INTERNAL_ERROR;
}
cchUsage += 1;
wchar_t *wUsage = (wchar_t*)malloc(cchUsage * sizeof(wchar_t));
cchUsage = MultiByteToWideChar(CP_UTF8, 0, usage, cbData, wUsage, cchUsage);
if (!cchUsage) {
winerror(0, L"Unable to preprocess usage text");
free((void *)wUsage);
return RC_INTERNAL_ERROR;
}
// Ensure null termination
wUsage[cchUsage] = L'\0';
fwprintf(stdout, wUsage, (L"" PY_VERSION), argv[0]);
fflush(stdout);
free((void *)wUsage);
return 0;
}
/******************************************************************************\
*** SEARCH INFO ***
\******************************************************************************/
struct _SearchInfoBuffer {
struct _SearchInfoBuffer *next;
wchar_t buffer[0];
};
typedef struct {
// the original string, managed by the OS
const wchar_t *originalCmdLine;
// pointer into the cmdline to mark what we've consumed
const wchar_t *restOfCmdLine;
// if known/discovered, the full executable path of our runtime
const wchar_t *executablePath;
// pointer and length into cmdline for the file to check for a
// shebang line, if any. Length can be -1 if the string is null
// terminated.
const wchar_t *scriptFile;
int scriptFileLength;
// pointer and length into cmdline or a static string with the
// name of the target executable. Length can be -1 if the string
// is null terminated.
const wchar_t *executable;
int executableLength;
// pointer and length into a string with additional interpreter
// arguments to include before restOfCmdLine. Length can be -1 if
// the string is null terminated.
const wchar_t *executableArgs;
int executableArgsLength;
// pointer and length into cmdline or a static string with the
// company name for PEP 514 lookup. Length can be -1 if the string
// is null terminated.
const wchar_t *company;
int companyLength;
// pointer and length into cmdline or a static string with the
// tag for PEP 514 lookup. Length can be -1 if the string is
// null terminated.
const wchar_t *tag;
int tagLength;
// if true, treats 'tag' as a non-PEP 514 filter
bool oldStyleTag;
// if true, ignores 'tag' when a high priority environment is found
// gh-92817: This is currently set when a tag is read from configuration or
// the environment, rather than the command line or a shebang line, and the
// only currently possible high priority environment is an active virtual
// environment
bool lowPriorityTag;
// if true, allow PEP 514 lookup to override 'executable'
bool allowExecutableOverride;
// if true, allow a nearby pyvenv.cfg to locate the executable
bool allowPyvenvCfg;
// if true, allow defaults (env/py.ini) to clarify/override tags
bool allowDefaults;
// if true, prefer windowed (console-less) executable
bool windowed;
// if true, only list detected runtimes without launching
bool list;
// if true, only list detected runtimes with paths without launching
bool listPaths;
// if true, display help message before contiuning
bool help;
// dynamically allocated buffers to free later
struct _SearchInfoBuffer *_buffer;
} SearchInfo;
wchar_t *
allocSearchInfoBuffer(SearchInfo *search, int wcharCount)
{
struct _SearchInfoBuffer *buffer = (struct _SearchInfoBuffer*)malloc(
sizeof(struct _SearchInfoBuffer) +
wcharCount * sizeof(wchar_t)
);
if (!buffer) {
return NULL;
}
buffer->next = search->_buffer;
search->_buffer = buffer;
return buffer->buffer;
}
void
freeSearchInfo(SearchInfo *search)
{
struct _SearchInfoBuffer *b = search->_buffer;
search->_buffer = NULL;
while (b) {
struct _SearchInfoBuffer *nextB = b->next;
free((void *)b);
b = nextB;
}
}
void
_debugStringAndLength(const wchar_t *s, int len, const wchar_t *name)
{
if (!s) {
debug(L"%s: (null)\n", name);
} else if (len == 0) {
debug(L"%s: (empty)\n", name);
} else if (len < 0) {
debug(L"%s: %s\n", name, s);
} else {
debug(L"%s: %.*ls\n", name, len, s);
}
}
void
dumpSearchInfo(SearchInfo *search)
{
if (!log_fp) {
return;
}
#ifdef __clang__
#define DEBUGNAME(s) L # s
#else
#define DEBUGNAME(s) # s
#endif
#define DEBUG(s) debug(L"SearchInfo." DEBUGNAME(s) L": %s\n", (search->s) ? (search->s) : L"(null)")
#define DEBUG_2(s, sl) _debugStringAndLength((search->s), (search->sl), L"SearchInfo." DEBUGNAME(s))
#define DEBUG_BOOL(s) debug(L"SearchInfo." DEBUGNAME(s) L": %s\n", (search->s) ? L"True" : L"False")
DEBUG(originalCmdLine);
DEBUG(restOfCmdLine);
DEBUG(executablePath);
DEBUG_2(scriptFile, scriptFileLength);
DEBUG_2(executable, executableLength);
DEBUG_2(executableArgs, executableArgsLength);
DEBUG_2(company, companyLength);
DEBUG_2(tag, tagLength);
DEBUG_BOOL(oldStyleTag);
DEBUG_BOOL(lowPriorityTag);
DEBUG_BOOL(allowDefaults);
DEBUG_BOOL(allowExecutableOverride);
DEBUG_BOOL(windowed);
DEBUG_BOOL(list);
DEBUG_BOOL(listPaths);
DEBUG_BOOL(help);
#undef DEBUG_BOOL
#undef DEBUG_2
#undef DEBUG
#undef DEBUGNAME
}
int
findArgv0Length(const wchar_t *buffer, int bufferLength)
{
// Note: this implements semantics that are only valid for argv0.
// Specifically, there is no escaping of quotes, and quotes within
// the argument have no effect. A quoted argv0 must start and end
// with a double quote character; otherwise, it ends at the first
// ' ' or '\t'.
int quoted = buffer[0] == L'"';
for (int i = 1; bufferLength < 0 || i < bufferLength; ++i) {
switch (buffer[i]) {
case L'\0':
return i;
case L' ':
case L'\t':
if (!quoted) {
return i;
}
break;
case L'"':
if (quoted) {
return i + 1;
}
break;
}
}
return bufferLength;
}
const wchar_t *
findArgv0End(const wchar_t *buffer, int bufferLength)
{
return &buffer[findArgv0Length(buffer, bufferLength)];
}
/******************************************************************************\
*** COMMAND-LINE PARSING ***
\******************************************************************************/
int
parseCommandLine(SearchInfo *search)
{
if (!search || !search->originalCmdLine) {
return RC_NO_COMMANDLINE;
}
const wchar_t *argv0End = findArgv0End(search->originalCmdLine, -1);
const wchar_t *tail = argv0End; // will be start of the executable name
const wchar_t *end = argv0End; // will be end of the executable name
search->restOfCmdLine = argv0End; // will be first space after argv0
while (--tail != search->originalCmdLine) {
if (*tail == L'"' && end == argv0End) {
// Move the "end" up to the quote, so we also allow moving for
// a period later on.
end = argv0End = tail;
} else if (*tail == L'.' && end == argv0End) {
end = tail;
} else if (*tail == L'\\' || *tail == L'/') {
++tail;
break;
}
}
if (tail == search->originalCmdLine && tail[0] == L'"') {
++tail;
}
// Without special cases, we can now fill in the search struct
int tailLen = (int)(end ? (end - tail) : wcsnlen_s(tail, MAXLEN));
search->executableLength = -1;
// Our special cases are as follows
#define MATCHES(s) (0 == _comparePath(tail, tailLen, (s), -1))
#define STARTSWITH(s) _startsWith(tail, tailLen, (s), -1)
if (MATCHES(L"py")) {
search->executable = L"python.exe";
search->allowExecutableOverride = true;
search->allowDefaults = true;
} else if (MATCHES(L"pyw")) {
search->executable = L"pythonw.exe";
search->allowExecutableOverride = true;
search->allowDefaults = true;
search->windowed = true;
} else if (MATCHES(L"py_d")) {
search->executable = L"python_d.exe";
search->allowExecutableOverride = true;
search->allowDefaults = true;
} else if (MATCHES(L"pyw_d")) {
search->executable = L"pythonw_d.exe";
search->allowExecutableOverride = true;
search->allowDefaults = true;
search->windowed = true;
} else if (STARTSWITH(L"python3")) {
search->executable = L"python.exe";
search->tag = &tail[6];
search->tagLength = tailLen - 6;
search->allowExecutableOverride = true;
search->oldStyleTag = true;
search->allowPyvenvCfg = true;
} else if (STARTSWITH(L"pythonw3")) {
search->executable = L"pythonw.exe";
search->tag = &tail[7];
search->tagLength = tailLen - 7;
search->allowExecutableOverride = true;
search->oldStyleTag = true;
search->allowPyvenvCfg = true;
search->windowed = true;
} else {
search->executable = tail;
search->executableLength = tailLen;
search->allowPyvenvCfg = true;
}
#undef STARTSWITH
#undef MATCHES
// First argument might be one of our options. If so, consume it,
// update flags and then set restOfCmdLine.
const wchar_t *arg = search->restOfCmdLine;
while(*arg && isspace(*arg)) { ++arg; }
#define MATCHES(s) (0 == _compareArgument(arg, argLen, (s), -1))
#define STARTSWITH(s) _startsWithArgument(arg, argLen, (s), -1)
if (*arg && *arg == L'-' && *++arg) {
tail = arg;
while (*tail && !isspace(*tail)) { ++tail; }
int argLen = (int)(tail - arg);
if (argLen > 0) {
if (STARTSWITH(L"2") || STARTSWITH(L"3")) {
// All arguments starting with 2 or 3 are assumed to be version tags
search->tag = arg;
search->tagLength = argLen;
search->oldStyleTag = true;
search->restOfCmdLine = tail;
} else if (STARTSWITH(L"V:") || STARTSWITH(L"-version:")) {
// Arguments starting with 'V:' specify company and/or tag
const wchar_t *argStart = wcschr(arg, L':') + 1;
const wchar_t *tagStart = wcschr(argStart, L'/') ;
if (tagStart) {
search->company = argStart;
search->companyLength = (int)(tagStart - argStart);
search->tag = tagStart + 1;
} else {
search->tag = argStart;
}
search->tagLength = (int)(tail - search->tag);
search->allowDefaults = false;
search->restOfCmdLine = tail;
} else if (MATCHES(L"0") || MATCHES(L"-list")) {
search->list = true;
search->restOfCmdLine = tail;
} else if (MATCHES(L"0p") || MATCHES(L"-list-paths")) {
search->listPaths = true;
search->restOfCmdLine = tail;
} else if (MATCHES(L"h") || MATCHES(L"-help")) {
search->help = true;
// Do not update restOfCmdLine so that we trigger the help
// message from whichever interpreter we select
}
}
}
#undef STARTSWITH
#undef MATCHES
// Might have a script filename. If it looks like a filename, add
// it to the SearchInfo struct for later reference.
arg = search->restOfCmdLine;
while(*arg && isspace(*arg)) { ++arg; }
if (*arg && *arg != L'-') {
search->scriptFile = arg;
if (*arg == L'"') {
++search->scriptFile;
while (*++arg && *arg != L'"') { }
} else {
while (*arg && !isspace(*arg)) { ++arg; }
}
search->scriptFileLength = (int)(arg - search->scriptFile);
}
return 0;
}
int
_decodeShebang(SearchInfo *search, const char *buffer, int bufferLength, bool onlyUtf8, wchar_t **decoded, int *decodedLength)
{
DWORD cp = CP_UTF8;
int wideLen = MultiByteToWideChar(cp, MB_ERR_INVALID_CHARS, buffer, bufferLength, NULL, 0);
if (!wideLen) {
cp = CP_ACP;
wideLen = MultiByteToWideChar(cp, MB_ERR_INVALID_CHARS, buffer, bufferLength, NULL, 0);
if (!wideLen) {
debug(L"# Failed to decode shebang line (0x%08X)\n", GetLastError());
return RC_BAD_VIRTUAL_PATH;
}
}
wchar_t *b = allocSearchInfoBuffer(search, wideLen + 1);
if (!b) {
return RC_NO_MEMORY;
}
wideLen = MultiByteToWideChar(cp, 0, buffer, bufferLength, b, wideLen + 1);
if (!wideLen) {
debug(L"# Failed to decode shebang line (0x%08X)\n", GetLastError());
return RC_BAD_VIRTUAL_PATH;
}
b[wideLen] = L'\0';
*decoded = b;
*decodedLength = wideLen;
return 0;
}
bool
_shebangStartsWith(const wchar_t *buffer, int bufferLength, const wchar_t *prefix, const wchar_t **rest, int *firstArgumentLength)
{
int prefixLength = (int)wcsnlen_s(prefix, MAXLEN);
if (bufferLength < prefixLength || !_startsWithArgument(buffer, bufferLength, prefix, prefixLength)) {
return false;
}
if (rest) {
*rest = &buffer[prefixLength];
}
if (firstArgumentLength) {
int i = prefixLength;
while (i < bufferLength && !isspace(buffer[i])) {
i += 1;
}
*firstArgumentLength = i - prefixLength;
}
return true;
}
int
searchPath(SearchInfo *search, const wchar_t *shebang, int shebangLength)
{
if (isEnvVarSet(L"PYLAUNCHER_NO_SEARCH_PATH")) {
return RC_NO_SHEBANG;
}
wchar_t *command;
int commandLength;
if (!_shebangStartsWith(shebang, shebangLength, L"/usr/bin/env ", &command, &commandLength)) {
return RC_NO_SHEBANG;
}
if (!commandLength || commandLength == MAXLEN) {
return RC_BAD_VIRTUAL_PATH;
}
int lastDot = commandLength;
while (lastDot > 0 && command[lastDot] != L'.') {
lastDot -= 1;
}
if (!lastDot) {
lastDot = commandLength;
}
wchar_t filename[MAXLEN];
if (wcsncpy_s(filename, MAXLEN, command, lastDot)) {
return RC_BAD_VIRTUAL_PATH;
}
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_RECURSIVE_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)
{
wchar_t iniPath[MAXLEN];
int n;
if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, iniPath)) &&
join(iniPath, MAXLEN, L"py.ini")) {
debug(L"# Reading from %s for %s/%s\n", iniPath, section, settingName);
n = GetPrivateProfileStringW(section, settingName, NULL, buffer, bufferLength, iniPath);
if (n) {
debug(L"# Found %s in %s\n", settingName, iniPath);
return n;
} else if (GetLastError() == ERROR_FILE_NOT_FOUND) {
debug(L"# Did not find file %s\n", iniPath);
} else {
winerror(0, L"Failed to read from %s\n", iniPath);
}
}
if (GetModuleFileNameW(NULL, iniPath, MAXLEN) &&
SUCCEEDED(PathCchRemoveFileSpec(iniPath, MAXLEN)) &&
join(iniPath, MAXLEN, L"py.ini")) {
debug(L"# Reading from %s for %s/%s\n", iniPath, section, settingName);
n = GetPrivateProfileStringW(section, settingName, NULL, buffer, MAXLEN, iniPath);
if (n) {
debug(L"# Found %s in %s\n", settingName, iniPath);
return n;
} else if (GetLastError() == ERROR_FILE_NOT_FOUND) {
debug(L"# Did not find file %s\n", iniPath);
} else {
winerror(0, L"Failed to read from %s\n", iniPath);
}
}
return 0;
}
bool
_findCommand(SearchInfo *search, const wchar_t *command, int commandLength)
{
wchar_t commandBuffer[MAXLEN];
wchar_t buffer[MAXLEN];
wcsncpy_s(commandBuffer, MAXLEN, command, commandLength);
int n = _readIni(L"commands", commandBuffer, buffer, MAXLEN);
if (!n) {
return false;
}
wchar_t *path = allocSearchInfoBuffer(search, n + 1);
if (!path) {
return false;
}
wcscpy_s(path, n + 1, buffer);
search->executablePath = path;
return true;
}
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
checkShebang(SearchInfo *search)
{
// Do not check shebang if a tag was provided or if no script file
// was found on the command line.
if (search->tag || !search->scriptFile) {
return 0;
}
if (search->scriptFileLength < 0) {
search->scriptFileLength = (int)wcsnlen_s(search->scriptFile, MAXLEN);
}
wchar_t *scriptFile = (wchar_t*)malloc(sizeof(wchar_t) * (search->scriptFileLength + 1));
if (!scriptFile) {
return RC_NO_MEMORY;
}
wcsncpy_s(scriptFile, search->scriptFileLength + 1,
search->scriptFile, search->scriptFileLength);
HANDLE hFile = CreateFileW(scriptFile, GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
debug(L"# Failed to open %s for shebang parsing (0x%08X)\n",
scriptFile, GetLastError());
free(scriptFile);
return 0;
}
DWORD bytesRead = 0;
char buffer[4096];
if (!ReadFile(hFile, buffer, sizeof(buffer), &bytesRead, NULL)) {
debug(L"# Failed to read %s for shebang parsing (0x%08X)\n",
scriptFile, GetLastError());
free(scriptFile);
return 0;
}
CloseHandle(hFile);
debug(L"# Read %d bytes from %s to find shebang line\n", bytesRead, scriptFile);
free(scriptFile);
char *b = buffer;
bool onlyUtf8 = false;
if (bytesRead > 3 && *b == 0xEF) {
if (*++b == 0xBB && *++b == 0xBF) {
// Allow a UTF-8 BOM
++b;
bytesRead -= 3;
onlyUtf8 = true;
} else {
debug(L"# Invalid BOM in shebang line");
return 0;
}
}
if (bytesRead <= 2 || b[0] != '#' || b[1] != '!') {
// No shebang (#!) at start of line
debug(L"# No valid shebang line");
return 0;
}
++b;
--bytesRead;
while (--bytesRead > 0 && isspace(*++b)) { }
char *start = b;
while (--bytesRead > 0 && *++b != '\r' && *b != '\n') { }
wchar_t *shebang;
int shebangLength;
// We add 1 when bytesRead==0, as in that case we hit EOF and b points
// to the last character in the file, not the newline
int exitCode = _decodeShebang(search, start, (int)(b - start + (bytesRead == 0)), onlyUtf8, &shebang, &shebangLength);
if (exitCode) {
return exitCode;
}
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 shebangs
const wchar_t *command;
int commandLength;
// Each template must end with "python"
static const wchar_t *shebangTemplates[] = {
L"/usr/bin/env python",
L"/usr/bin/python",
L"/usr/local/bin/python",
L"python",
NULL
};
for (const wchar_t **tmpl = shebangTemplates; *tmpl; ++tmpl) {
// Just to make sure we don't mess this up in the future
assert(0 == wcscmp(L"python", (*tmpl) + wcslen(*tmpl) - 6));
if (_shebangStartsWith(shebang, shebangLength, *tmpl, &command, &commandLength)) {
// Search for "python{command}" overrides. All templates end with
// "python", so we prepend it by jumping back 6 characters
if (_findCommand(search, &command[-6], commandLength + 6)) {
search->executableArgs = &command[commandLength];
search->executableArgsLength = shebangLength - commandLength;
debug(L"# Treating shebang command '%.*s' as %s\n",
commandLength + 6, &command[-6], search->executablePath);
return 0;
}
search->tag = command;
search->tagLength = commandLength;
// 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->executableArgs = &command[commandLength];
search->executableArgsLength = shebangLength - commandLength;
if (search->tag && search->tagLength) {
debug(L"# Treating shebang command '%.*s' as 'py -%.*s'\n",
commandLength, command, search->tagLength, search->tag);
} else {
debug(L"# Treating shebang command '%.*s' as 'py'\n",
commandLength, command);
}
return 0;
}
}
// Unrecognised executables are first tried as command aliases
commandLength = 0;
while (commandLength < shebangLength && !isspace(shebang[commandLength])) {
commandLength += 1;
}
if (_findCommand(search, shebang, commandLength)) {
search->executableArgs = &shebang[commandLength];
search->executableArgsLength = shebangLength - commandLength;
debug(L"# Treating shebang command '%.*s' as %s\n",
commandLength, shebang, search->executablePath);
return 0;
}
// Unrecognised commands are joined to the script's directory and treated
// as the executable path
return _useShebangAsExecutable(search, shebang, shebangLength);
}
int
checkDefaults(SearchInfo *search)
{
if (!search->allowDefaults) {
return 0;
}
// Only resolve old-style (or absent) tags to defaults
if (search->tag && search->tagLength && !search->oldStyleTag) {
return 0;
}
// If tag is only a major version number, expand it from the environment
// or an ini file
const wchar_t *iniSettingName = NULL;
const wchar_t *envSettingName = NULL;
if (!search->tag || !search->tagLength) {
iniSettingName = L"python";
envSettingName = L"py_python";
} else if (0 == wcsncmp(search->tag, L"3", search->tagLength)) {
iniSettingName = L"python3";
envSettingName = L"py_python3";
} else if (0 == wcsncmp(search->tag, L"2", search->tagLength)) {
iniSettingName = L"python2";
envSettingName = L"py_python2";
} else {
debug(L"# Cannot select defaults for tag '%.*s'\n", search->tagLength, search->tag);
return 0;
}
// First, try to read an environment variable
wchar_t buffer[MAXLEN];
int n = GetEnvironmentVariableW(envSettingName, buffer, MAXLEN);
// If none found, check in our two .ini files instead
if (!n) {
n = _readIni(L"defaults", iniSettingName, buffer, MAXLEN);
}
if (n) {
wchar_t *tag = allocSearchInfoBuffer(search, n + 1);
if (!tag) {
return RC_NO_MEMORY;
}
wcscpy_s(tag, n + 1, buffer);
wchar_t *slash = wcschr(tag, L'/');
if (!slash) {
search->tag = tag;
search->tagLength = n;
search->oldStyleTag = true;
} else {
search->company = tag;
search->companyLength = (int)(slash - tag);
search->tag = slash + 1;
search->tagLength = n - (search->companyLength + 1);
search->oldStyleTag = false;
}
// gh-92817: allow a high priority env to be selected even if it
// doesn't match the tag
search->lowPriorityTag = true;
}
return 0;
}
/******************************************************************************\
*** ENVIRONMENT SEARCH ***
\******************************************************************************/
typedef struct EnvironmentInfo {
/* We use a binary tree and sort on insert */
struct EnvironmentInfo *prev;
struct EnvironmentInfo *next;
/* parent is only used when constructing */
struct EnvironmentInfo *parent;
const wchar_t *company;
const wchar_t *tag;
int internalSortKey;
const wchar_t *installDir;
const wchar_t *executablePath;
const wchar_t *executableArgs;
const wchar_t *architecture;
const wchar_t *displayName;
bool highPriority;
} EnvironmentInfo;
int
copyWstr(const wchar_t **dest, const wchar_t *src)
{
if (!dest) {
return RC_NO_MEMORY;
}
if (!src) {
*dest = NULL;
return 0;
}
size_t n = wcsnlen_s(src, MAXLEN - 1) + 1;
wchar_t *buffer = (wchar_t*)malloc(n * sizeof(wchar_t));
if (!buffer) {
return RC_NO_MEMORY;
}
wcsncpy_s(buffer, n, src, n - 1);
*dest = (const wchar_t*)buffer;
return 0;
}
EnvironmentInfo *
newEnvironmentInfo(const wchar_t *company, const wchar_t *tag)
{
EnvironmentInfo *env = (EnvironmentInfo *)malloc(sizeof(EnvironmentInfo));
if (!env) {
return NULL;
}
memset(env, 0, sizeof(EnvironmentInfo));
int exitCode = copyWstr(&env->company, company);
if (exitCode) {
free((void *)env);
return NULL;
}
exitCode = copyWstr(&env->tag, tag);
if (exitCode) {
free((void *)env->company);
free((void *)env);
return NULL;
}
return env;
}
void
freeEnvironmentInfo(EnvironmentInfo *env)
{
if (env) {
free((void *)env->company);
free((void *)env->tag);
free((void *)env->installDir);
free((void *)env->executablePath);
free((void *)env->executableArgs);
free((void *)env->displayName);
freeEnvironmentInfo(env->prev);
env->prev = NULL;
freeEnvironmentInfo(env->next);
env->next = NULL;
free((void *)env);
}
}
/* Specific string comparisons for sorting the tree */
int
_compareCompany(const wchar_t *x, const wchar_t *y)
{
if (!x && !y) {
return 0;
} else if (!x) {
return -1;
} else if (!y) {
return 1;
}
bool coreX = 0 == _compare(x, -1, L"PythonCore", -1);
bool coreY = 0 == _compare(y, -1, L"PythonCore", -1);
if (coreX) {
return coreY ? 0 : -1;
} else if (coreY) {
return 1;
}
return _compare(x, -1, y, -1);
}
int
_compareTag(const wchar_t *x, const wchar_t *y)
{
if (!x && !y) {
return 0;
} else if (!x) {
return -1;
} else if (!y) {
return 1;
}
// Compare up to the first dash. If not equal, that's our sort order
const wchar_t *xDash = wcschr(x, L'-');
const wchar_t *yDash = wcschr(y, L'-');
int xToDash = xDash ? (int)(xDash - x) : -1;
int yToDash = yDash ? (int)(yDash - y) : -1;
int r = _compare(x, xToDash, y, yToDash);
if (r) {
return r;
}
// If we're equal up to the first dash, we want to sort one with
// no dash *after* one with a dash. Otherwise, a reversed compare.
// This works out because environments are sorted in descending tag
// order, so that higher versions (probably) come first.
// For PythonCore, our "X.Y" structure ensures that higher versions
// come first. Everyone else will just have to deal with it.
if (xDash && yDash) {
return _compare(yDash, -1, xDash, -1);
} else if (xDash) {
return -1;
} else if (yDash) {
return 1;
}
return 0;
}
int
addEnvironmentInfo(EnvironmentInfo **root, EnvironmentInfo* parent, EnvironmentInfo *node)
{
EnvironmentInfo *r = *root;
if (!r) {
*root = node;
node->parent = parent;
return 0;
}
// Sort by company name
switch (_compareCompany(node->company, r->company)) {
case -1:
return addEnvironmentInfo(&r->prev, r, node);
case 1:
return addEnvironmentInfo(&r->next, r, node);
case 0:
break;
}
// Then by tag (descending)
switch (_compareTag(node->tag, r->tag)) {
case -1:
return addEnvironmentInfo(&r->next, r, node);
case 1:
return addEnvironmentInfo(&r->prev, r, node);
case 0:
break;
}
// Then keep the one with the lowest internal sort key
if (node->internalSortKey < r->internalSortKey) {
// Replace the current node
node->parent = r->parent;
if (node->parent) {
if (node->parent->prev == r) {
node->parent->prev = node;
} else if (node->parent->next == r) {
node->parent->next = node;
} else {
debug(L"# Inconsistent parent value in tree\n");
freeEnvironmentInfo(node);
return RC_INTERNAL_ERROR;
}
} else {
// If node has no parent, then it is the root.
*root = node;
}
node->next = r->next;
node->prev = r->prev;
debug(L"# replaced %s/%s/%i in tree\n", node->company, node->tag, node->internalSortKey);
freeEnvironmentInfo(r);
} else {
debug(L"# not adding %s/%s/%i to tree\n", node->company, node->tag, node->internalSortKey);
return RC_DUPLICATE_ITEM;
}
return 0;
}
/******************************************************************************\
*** REGISTRY SEARCH ***
\******************************************************************************/
int
_registryReadString(const wchar_t **dest, HKEY root, const wchar_t *subkey, const wchar_t *value)
{
// Note that this is bytes (hence 'cb'), not characters ('cch')
DWORD cbData = 0;
DWORD flags = RRF_RT_REG_SZ | RRF_RT_REG_EXPAND_SZ;
if (ERROR_SUCCESS != RegGetValueW(root, subkey, value, flags, NULL, NULL, &cbData)) {
return 0;
}
wchar_t *buffer = (wchar_t*)malloc(cbData);
if (!buffer) {
return RC_NO_MEMORY;
}
if (ERROR_SUCCESS == RegGetValueW(root, subkey, value, flags, NULL, buffer, &cbData)) {
*dest = buffer;
} else {
free((void *)buffer);
}
return 0;
}
int
_combineWithInstallDir(const wchar_t **dest, const wchar_t *installDir, const wchar_t *fragment, int fragmentLength)
{
wchar_t buffer[MAXLEN];
wchar_t fragmentBuffer[MAXLEN];
if (wcsncpy_s(fragmentBuffer, MAXLEN, fragment, fragmentLength)) {
return RC_NO_MEMORY;
}
if (FAILED(PathCchCombineEx(buffer, MAXLEN, installDir, fragmentBuffer, PATHCCH_ALLOW_LONG_PATHS))) {
return RC_NO_MEMORY;
}
return copyWstr(dest, buffer);
}
bool
_isLegacyVersion(EnvironmentInfo *env)
{
// Check if backwards-compatibility is required.
// Specifically PythonCore versions 2.X and 3.0 - 3.5 do not implement PEP 514.
if (0 != _compare(env->company, -1, L"PythonCore", -1)) {
return false;
}
int versionMajor, versionMinor;
int n = swscanf_s(env->tag, L"%d.%d", &versionMajor, &versionMinor);
if (n != 2) {
debug(L"# %s/%s has an invalid version tag\n", env->company, env->tag);
return false;
}
return versionMajor == 2
|| (versionMajor == 3 && versionMinor >= 0 && versionMinor <= 5);
}
int
_registryReadLegacyEnvironment(const SearchInfo *search, HKEY root, EnvironmentInfo *env, const wchar_t *fallbackArch)
{
// Backwards-compatibility for PythonCore versions which do not implement PEP 514.
int exitCode = _combineWithInstallDir(
&env->executablePath,
env->installDir,
search->executable,
search->executableLength
);
if (exitCode) {
return exitCode;
}
if (search->windowed) {
exitCode = _registryReadString(&env->executableArgs, root, L"InstallPath", L"WindowedExecutableArguments");
}
else {
exitCode = _registryReadString(&env->executableArgs, root, L"InstallPath", L"ExecutableArguments");
}
if (exitCode) {
return exitCode;
}
if (fallbackArch) {
copyWstr(&env->architecture, fallbackArch);
} else {
DWORD binaryType;
BOOL success = GetBinaryTypeW(env->executablePath, &binaryType);
if (!success) {
return RC_NO_PYTHON;
}
switch (binaryType) {
case SCS_32BIT_BINARY:
copyWstr(&env->architecture, L"32bit");
break;
case SCS_64BIT_BINARY:
copyWstr(&env->architecture, L"64bit");
break;
default:
return RC_NO_PYTHON;
}
}
if (0 == _compare(env->architecture, -1, L"32bit", -1)) {
size_t tagLength = wcslen(env->tag);
if (tagLength <= 3 || 0 != _compare(&env->tag[tagLength - 3], 3, L"-32", 3)) {
const wchar_t *rawTag = env->tag;
wchar_t *realTag = (wchar_t*) malloc(sizeof(wchar_t) * (tagLength + 4));
if (!realTag) {
return RC_NO_MEMORY;
}
int count = swprintf_s(realTag, tagLength + 4, L"%s-32", env->tag);
if (count == -1) {
free(realTag);
return RC_INTERNAL_ERROR;
}
env->tag = realTag;
free((void*)rawTag);
}
}
wchar_t buffer[MAXLEN];
if (swprintf_s(buffer, MAXLEN, L"Python %s", env->tag)) {
copyWstr(&env->displayName, buffer);
}
return 0;
}
int
_registryReadEnvironment(const SearchInfo *search, HKEY root, EnvironmentInfo *env, const wchar_t *fallbackArch)
{
int exitCode = _registryReadString(&env->installDir, root, L"InstallPath", NULL);
if (exitCode) {
return exitCode;
}
if (!env->installDir) {
return RC_NO_PYTHON;
}
if (_isLegacyVersion(env)) {
return _registryReadLegacyEnvironment(search, root, env, fallbackArch);
}
// If pythonw.exe requested, check specific value
if (search->windowed) {
exitCode = _registryReadString(&env->executablePath, root, L"InstallPath", L"WindowedExecutablePath");
if (!exitCode && env->executablePath) {
exitCode = _registryReadString(&env->executableArgs, root, L"InstallPath", L"WindowedExecutableArguments");
}
}
if (exitCode) {
return exitCode;
}
// Missing windowed path or non-windowed request means we use ExecutablePath
if (!env->executablePath) {
exitCode = _registryReadString(&env->executablePath, root, L"InstallPath", L"ExecutablePath");
if (!exitCode && env->executablePath) {
exitCode = _registryReadString(&env->executableArgs, root, L"InstallPath", L"ExecutableArguments");
}
}
if (exitCode) {
return exitCode;
}
if (!env->executablePath) {
debug(L"# %s/%s has no executable path\n", env->company, env->tag);
return RC_NO_PYTHON;
}
exitCode = _registryReadString(&env->architecture, root, NULL, L"SysArchitecture");
if (exitCode) {
return exitCode;
}
exitCode = _registryReadString(&env->displayName, root, NULL, L"DisplayName");
if (exitCode) {
return exitCode;
}
return 0;
}
int
_registrySearchTags(const SearchInfo *search, EnvironmentInfo **result, HKEY root, int sortKey, const wchar_t *company, const wchar_t *fallbackArch)
{
wchar_t buffer[256];
int err = 0;
int exitCode = 0;
for (int i = 0; exitCode == 0; ++i) {
DWORD cchBuffer = sizeof(buffer) / sizeof(buffer[0]);
err = RegEnumKeyExW(root, i, buffer, &cchBuffer, NULL, NULL, NULL, NULL);
if (err) {
if (err != ERROR_NO_MORE_ITEMS) {
winerror(0, L"Failed to read installs (tags) from the registry");
}
break;
}
HKEY subkey;
if (ERROR_SUCCESS == RegOpenKeyExW(root, buffer, 0, KEY_READ, &subkey)) {
EnvironmentInfo *env = newEnvironmentInfo(company, buffer);
env->internalSortKey = sortKey;
exitCode = _registryReadEnvironment(search, subkey, env, fallbackArch);
RegCloseKey(subkey);
if (exitCode == RC_NO_PYTHON) {
freeEnvironmentInfo(env);
exitCode = 0;
} else if (!exitCode) {
exitCode = addEnvironmentInfo(result, NULL, env);
if (exitCode) {
freeEnvironmentInfo(env);
if (exitCode == RC_DUPLICATE_ITEM) {
exitCode = 0;
}
}
}
}
}
return exitCode;
}
int
registrySearch(const SearchInfo *search, EnvironmentInfo **result, HKEY root, int sortKey, const wchar_t *fallbackArch)
{
wchar_t buffer[256];
int err = 0;
int exitCode = 0;
for (int i = 0; exitCode == 0; ++i) {
DWORD cchBuffer = sizeof(buffer) / sizeof(buffer[0]);
err = RegEnumKeyExW(root, i, buffer, &cchBuffer, NULL, NULL, NULL, NULL);
if (err) {
if (err != ERROR_NO_MORE_ITEMS) {
winerror(0, L"Failed to read distributors (company) from the registry");
}
break;
}
HKEY subkey;
if (ERROR_SUCCESS == RegOpenKeyExW(root, buffer, 0, KEY_READ, &subkey)) {
exitCode = _registrySearchTags(search, result, subkey, sortKey, buffer, fallbackArch);
RegCloseKey(subkey);
}
}
return exitCode;
}
/******************************************************************************\
*** APP PACKAGE SEARCH ***
\******************************************************************************/
int
appxSearch(const SearchInfo *search, EnvironmentInfo **result, const wchar_t *packageFamilyName, const wchar_t *tag, int sortKey)
{
wchar_t realTag[32];
wchar_t buffer[MAXLEN];
const wchar_t *exeName = search->executable;
if (!exeName || search->allowExecutableOverride) {
exeName = search->windowed ? L"pythonw.exe" : L"python.exe";
}
if (FAILED(SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, buffer)) ||
!join(buffer, MAXLEN, L"Microsoft\\WindowsApps") ||
!join(buffer, MAXLEN, packageFamilyName) ||
!join(buffer, MAXLEN, exeName)) {
return RC_INTERNAL_ERROR;
}
if (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(buffer)) {
return RC_NO_PYTHON;
}
// Assume packages are native architecture, which means we need to append
// the '-arm64' on ARM64 host.
wcscpy_s(realTag, 32, tag);
if (isARM64Host()) {
wcscat_s(realTag, 32, L"-arm64");
}
EnvironmentInfo *env = newEnvironmentInfo(L"PythonCore", realTag);
if (!env) {
return RC_NO_MEMORY;
}
env->internalSortKey = sortKey;
if (isAMD64Host()) {
copyWstr(&env->architecture, L"64bit");
} else if (isARM64Host()) {
copyWstr(&env->architecture, L"ARM64");
}
copyWstr(&env->executablePath, buffer);
if (swprintf_s(buffer, MAXLEN, L"Python %s (Store)", tag)) {
copyWstr(&env->displayName, buffer);
}
int exitCode = addEnvironmentInfo(result, NULL, env);
if (exitCode) {
freeEnvironmentInfo(env);
if (exitCode == RC_DUPLICATE_ITEM) {
exitCode = 0;
}
}
return exitCode;
}
/******************************************************************************\
*** OVERRIDDEN EXECUTABLE PATH ***
\******************************************************************************/
int
explicitOverrideSearch(const SearchInfo *search, EnvironmentInfo **result)
{
if (!search->executablePath) {
return 0;
}
EnvironmentInfo *env = newEnvironmentInfo(NULL, NULL);
if (!env) {
return RC_NO_MEMORY;
}
env->internalSortKey = 10;
int exitCode = copyWstr(&env->executablePath, search->executablePath);
if (exitCode) {
goto abort;
}
exitCode = copyWstr(&env->displayName, L"Explicit override");
if (exitCode) {
goto abort;
}
exitCode = addEnvironmentInfo(result, NULL, env);
if (exitCode) {
goto abort;
}
return 0;
abort:
freeEnvironmentInfo(env);
if (exitCode == RC_DUPLICATE_ITEM) {
exitCode = 0;
}
return exitCode;
}
/******************************************************************************\
*** ACTIVE VIRTUAL ENVIRONMENT SEARCH ***
\******************************************************************************/
int
virtualenvSearch(const SearchInfo *search, EnvironmentInfo **result)
{
int exitCode = 0;
EnvironmentInfo *env = NULL;
wchar_t buffer[MAXLEN];
int n = GetEnvironmentVariableW(L"VIRTUAL_ENV", buffer, MAXLEN);
if (!n || !join(buffer, MAXLEN, L"Scripts") || !join(buffer, MAXLEN, search->executable)) {
return 0;
}
if (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(buffer)) {
debug(L"Python executable %s missing from virtual env\n", buffer);
return 0;
}
env = newEnvironmentInfo(NULL, NULL);
if (!env) {
return RC_NO_MEMORY;
}
env->highPriority = true;
env->internalSortKey = 20;
exitCode = copyWstr(&env->displayName, L"Active venv");
if (exitCode) {
goto abort;
}
exitCode = copyWstr(&env->executablePath, buffer);
if (exitCode) {
goto abort;
}
exitCode = addEnvironmentInfo(result, NULL, env);
if (exitCode) {
goto abort;
}
return 0;
abort:
freeEnvironmentInfo(env);
if (exitCode == RC_DUPLICATE_ITEM) {
return 0;
}
return exitCode;
}
/******************************************************************************\
*** COLLECT ENVIRONMENTS ***
\******************************************************************************/
struct RegistrySearchInfo {
// Registry subkey to search
const wchar_t *subkey;
// Registry hive to search
HKEY hive;
// Flags to use when opening the subkey
DWORD flags;
// Internal sort key to select between "identical" environments discovered
// through different methods
int sortKey;
// Fallback value to assume for PythonCore entries missing a SysArchitecture value
const wchar_t *fallbackArch;
};
struct RegistrySearchInfo REGISTRY_SEARCH[] = {
{
L"Software\\Python",
HKEY_CURRENT_USER,
KEY_READ,
1,
NULL
},
{
L"Software\\Python",
HKEY_LOCAL_MACHINE,
KEY_READ | KEY_WOW64_64KEY,
3,
L"64bit"
},
{
L"Software\\Python",
HKEY_LOCAL_MACHINE,
KEY_READ | KEY_WOW64_32KEY,
4,
L"32bit"
},
{ NULL, 0, 0, 0, NULL }
};
struct AppxSearchInfo {
// The package family name. Can be found for an installed package using the
// Powershell "Get-AppxPackage" cmdlet
const wchar_t *familyName;
// The tag to treat the installation as
const wchar_t *tag;
// Internal sort key to select between "identical" environments discovered
// through different methods
int sortKey;
};
struct AppxSearchInfo APPX_SEARCH[] = {
// Releases made through the Store
{ L"PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0", L"3.12", 10 },
{ L"PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0", L"3.11", 10 },
{ L"PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0", L"3.10", 10 },
{ L"PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0", L"3.9", 10 },
{ L"PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0", L"3.8", 10 },
// Side-loadable releases. Note that the publisher ID changes whenever we
// renew our code-signing certificate, so the newer ID has a higher
// priority (lower sortKey)
{ L"PythonSoftwareFoundation.Python.3.12_3847v3x7pw1km", L"3.12", 11 },
{ L"PythonSoftwareFoundation.Python.3.11_3847v3x7pw1km", L"3.11", 11 },
{ L"PythonSoftwareFoundation.Python.3.11_hd69rhyc2wevp", L"3.11", 12 },
{ L"PythonSoftwareFoundation.Python.3.10_3847v3x7pw1km", L"3.10", 11 },
{ L"PythonSoftwareFoundation.Python.3.10_hd69rhyc2wevp", L"3.10", 12 },
{ L"PythonSoftwareFoundation.Python.3.9_3847v3x7pw1km", L"3.9", 11 },
{ L"PythonSoftwareFoundation.Python.3.9_hd69rhyc2wevp", L"3.9", 12 },
{ L"PythonSoftwareFoundation.Python.3.8_hd69rhyc2wevp", L"3.8", 12 },
{ NULL, NULL, 0 }
};
int
collectEnvironments(const SearchInfo *search, EnvironmentInfo **result)
{
int exitCode = 0;
HKEY root;
EnvironmentInfo *env = NULL;
if (!result) {
return RC_INTERNAL_ERROR;
}
*result = NULL;
exitCode = explicitOverrideSearch(search, result);
if (exitCode) {
return exitCode;
}
exitCode = virtualenvSearch(search, result);
if (exitCode) {
return exitCode;
}
// If we aren't collecting all items to list them, we can exit now.
if (env && !(search->list || search->listPaths)) {
return 0;
}
for (struct RegistrySearchInfo *info = REGISTRY_SEARCH; info->subkey; ++info) {
if (ERROR_SUCCESS == RegOpenKeyExW(info->hive, info->subkey, 0, info->flags, &root)) {
exitCode = registrySearch(search, result, root, info->sortKey, info->fallbackArch);
RegCloseKey(root);
}
if (exitCode) {
return exitCode;
}
}
for (struct AppxSearchInfo *info = APPX_SEARCH; info->familyName; ++info) {
exitCode = appxSearch(search, result, info->familyName, info->tag, info->sortKey);
if (exitCode && exitCode != RC_NO_PYTHON) {
return exitCode;
}
}
return 0;
}
/******************************************************************************\
*** INSTALL ON DEMAND ***
\******************************************************************************/
struct StoreSearchInfo {
// The tag a user is looking for
const wchar_t *tag;
// The Store ID for a package if it can be installed from the Microsoft
// Store. These are obtained from the dashboard at
// https://partner.microsoft.com/dashboard
const wchar_t *storeId;
};
struct StoreSearchInfo STORE_SEARCH[] = {
{ L"3", /* 3.11 */ L"9NRWMJP3717K" },
{ L"3.12", L"9NCVDN91XZQP" },
{ L"3.11", L"9NRWMJP3717K" },
{ L"3.10", L"9PJPW5LDXLZ5" },
{ L"3.9", L"9P7QFQMJRFP7" },
{ L"3.8", L"9MSSZTT1N39L" },
{ NULL, NULL }
};
int
_installEnvironment(const wchar_t *command, const wchar_t *arguments)
{
SHELLEXECUTEINFOW siw = {
sizeof(SHELLEXECUTEINFOW),
SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE,
NULL, NULL,
command, arguments, NULL,
SW_SHOWNORMAL
};
debug(L"# Installing with %s %s\n", command, arguments);
if (isEnvVarSet(L"PYLAUNCHER_DRYRUN")) {
debug(L"# Exiting due to PYLAUNCHER_DRYRUN\n");
fflush(stdout);
int mode = _setmode(_fileno(stdout), _O_U8TEXT);
if (arguments) {
fwprintf_s(stdout, L"\"%s\" %s\n", command, arguments);
} else {
fwprintf_s(stdout, L"\"%s\"\n", command);
}
fflush(stdout);
if (mode >= 0) {
_setmode(_fileno(stdout), mode);
}
return RC_INSTALLING;
}
if (!ShellExecuteExW(&siw)) {
return RC_NO_PYTHON;
}
if (!siw.hProcess) {
return RC_INSTALLING;
}
WaitForSingleObjectEx(siw.hProcess, INFINITE, FALSE);
DWORD exitCode = 0;
if (GetExitCodeProcess(siw.hProcess, &exitCode) && exitCode == 0) {
return 0;
}
return RC_INSTALLING;
}
const wchar_t *WINGET_COMMAND = L"Microsoft\\WindowsApps\\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\\winget.exe";
const wchar_t *WINGET_ARGUMENTS = L"install -q %s --exact --accept-package-agreements --source msstore";
const wchar_t *MSSTORE_COMMAND = L"ms-windows-store://pdp/?productid=%s";
int
installEnvironment(const SearchInfo *search)
{
// No tag? No installing
if (!search->tag || !search->tagLength) {
debug(L"# Cannot install Python with no tag specified\n");
return RC_NO_PYTHON;
}
// PEP 514 tag but not PythonCore? No installing
if (!search->oldStyleTag &&
search->company && search->companyLength &&
0 != _compare(search->company, search->companyLength, L"PythonCore", -1)) {
debug(L"# Cannot install for company %.*s\n", search->companyLength, search->company);
return RC_NO_PYTHON;
}
const wchar_t *storeId = NULL;
for (struct StoreSearchInfo *info = STORE_SEARCH; info->tag; ++info) {
if (0 == _compare(search->tag, search->tagLength, info->tag, -1)) {
storeId = info->storeId;
break;
}
}
if (!storeId) {
return RC_NO_PYTHON;
}
int exitCode;
wchar_t command[MAXLEN];
wchar_t arguments[MAXLEN];
if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, command)) &&
join(command, MAXLEN, WINGET_COMMAND) &&
swprintf_s(arguments, MAXLEN, WINGET_ARGUMENTS, storeId)) {
if (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(command)) {
formatWinerror(GetLastError(), arguments, MAXLEN);
debug(L"# Skipping %s: %s\n", command, arguments);
} else {
fputws(L"Launching winget to install Python. The following output is from the install process\n\
***********************************************************************\n", stdout);
exitCode = _installEnvironment(command, arguments);
if (exitCode == RC_INSTALLING) {
fputws(L"***********************************************************************\n\
Please check the install status and run your command again.", stderr);
return exitCode;
} else if (exitCode) {
return exitCode;
}
fputws(L"***********************************************************************\n\
Install appears to have succeeded. Searching for new matching installs.\n", stdout);
return 0;
}
}
if (swprintf_s(command, MAXLEN, MSSTORE_COMMAND, storeId)) {
fputws(L"Opening the Microsoft Store to install Python. After installation, "
L"please run your command again.\n", stderr);
exitCode = _installEnvironment(command, NULL);
if (exitCode) {
return exitCode;
}
return 0;
}
return RC_NO_PYTHON;
}
/******************************************************************************\
*** ENVIRONMENT SELECT ***
\******************************************************************************/
bool
_companyMatches(const SearchInfo *search, const EnvironmentInfo *env)
{
if (!search->company || !search->companyLength) {
return true;
}
return 0 == _compare(env->company, -1, search->company, search->companyLength);
}
bool
_tagMatches(const SearchInfo *search, const EnvironmentInfo *env)
{
if (!search->tag || !search->tagLength) {
return true;
}
return _startsWith(env->tag, -1, search->tag, search->tagLength);
}
bool
_is32Bit(const EnvironmentInfo *env)
{
if (env->architecture) {
return 0 == _compare(env->architecture, -1, L"32bit", -1);
}
return false;
}
int
_selectEnvironment(const SearchInfo *search, EnvironmentInfo *env, EnvironmentInfo **best)
{
int exitCode = 0;
while (env) {
exitCode = _selectEnvironment(search, env->prev, best);
if (exitCode && exitCode != RC_NO_PYTHON) {
return exitCode;
} else if (!exitCode && *best) {
return 0;
}
if (env->highPriority && search->lowPriorityTag) {
// This environment is marked high priority, and the search allows
// it to be selected even though a tag is specified, so select it
// gh-92817: this allows an active venv to be selected even when a
// default tag has been found in py.ini or the environment
*best = env;
return 0;
}
if (!search->oldStyleTag) {
if (_companyMatches(search, env) && _tagMatches(search, env)) {
// Because of how our sort tree is set up, we will walk up the
// "prev" side and implicitly select the "best" best. By
// returning straight after a match, we skip the entire "next"
// branch and won't ever select a "worse" best.
*best = env;
return 0;
}
} else if (0 == _compare(env->company, -1, L"PythonCore", -1)) {
// Old-style tags can only match PythonCore entries
// If the tag ends with -64, we want to exclude 32-bit runtimes
// (If the tag ends with -32, it will be filtered later)
int tagLength = search->tagLength;
bool exclude32Bit = false, only32Bit = false;
if (tagLength > 3) {
if (0 == _compareArgument(&search->tag[tagLength - 3], 3, L"-64", 3)) {
tagLength -= 3;
exclude32Bit = true;
} else if (0 == _compareArgument(&search->tag[tagLength - 3], 3, L"-32", 3)) {
tagLength -= 3;
only32Bit = true;
}
}
if (_startsWith(env->tag, -1, search->tag, tagLength)) {
if (exclude32Bit && _is32Bit(env)) {
debug(L"# Excluding %s/%s because it looks like 32bit\n", env->company, env->tag);
} else if (only32Bit && !_is32Bit(env)) {
debug(L"# Excluding %s/%s because it doesn't look 32bit\n", env->company, env->tag);
} else {
*best = env;
return 0;
}
}
}
env = env->next;
}
return RC_NO_PYTHON;
}
int
selectEnvironment(const SearchInfo *search, EnvironmentInfo *root, EnvironmentInfo **best)
{
if (!best) {
return RC_INTERNAL_ERROR;
}
if (!root) {
*best = NULL;
return RC_NO_PYTHON_AT_ALL;
}
if (!root->next && !root->prev) {
*best = root;
return 0;
}
EnvironmentInfo *result = NULL;
int exitCode = _selectEnvironment(search, root, &result);
if (!exitCode) {
*best = result;
}
return exitCode;
}
/******************************************************************************\
*** LIST ENVIRONMENTS ***
\******************************************************************************/
#define TAGWIDTH 16
int
_printEnvironment(const EnvironmentInfo *env, FILE *out, bool showPath, const wchar_t *argument)
{
if (showPath) {
if (env->executablePath && env->executablePath[0]) {
if (env->executableArgs && env->executableArgs[0]) {
fwprintf(out, L" %-*s %s %s\n", TAGWIDTH, argument, env->executablePath, env->executableArgs);
} else {
fwprintf(out, L" %-*s %s\n", TAGWIDTH, argument, env->executablePath);
}
} else if (env->installDir && env->installDir[0]) {
fwprintf(out, L" %-*s %s\n", TAGWIDTH, argument, env->installDir);
} else {
fwprintf(out, L" %s\n", argument);
}
} else if (env->displayName) {
fwprintf(out, L" %-*s %s\n", TAGWIDTH, argument, env->displayName);
} else {
fwprintf(out, L" %s\n", argument);
}
return 0;
}
int
_listAllEnvironments(EnvironmentInfo *env, FILE * out, bool showPath, EnvironmentInfo *defaultEnv)
{
wchar_t buffer[256];
const int bufferSize = 256;
while (env) {
int exitCode = _listAllEnvironments(env->prev, out, showPath, defaultEnv);
if (exitCode) {
return exitCode;
}
if (!env->company || !env->tag) {
buffer[0] = L'\0';
} else if (0 == _compare(env->company, -1, L"PythonCore", -1)) {
swprintf_s(buffer, bufferSize, L"-V:%s", env->tag);
} else {
swprintf_s(buffer, bufferSize, L"-V:%s/%s", env->company, env->tag);
}
if (env == defaultEnv) {
wcscat_s(buffer, bufferSize, L" *");
}
if (buffer[0]) {
exitCode = _printEnvironment(env, out, showPath, buffer);
if (exitCode) {
return exitCode;
}
}
env = env->next;
}
return 0;
}
int
listEnvironments(EnvironmentInfo *env, FILE * out, bool showPath, EnvironmentInfo *defaultEnv)
{
if (!env) {
fwprintf_s(stdout, L"No installed Pythons found!\n");
return 0;
}
/* TODO: Do we want to display these?
In favour, helps users see that '-3' is a good option
Against, repeats the next line of output
SearchInfo majorSearch;
EnvironmentInfo *major;
int exitCode;
if (showPath) {
memset(&majorSearch, 0, sizeof(majorSearch));
majorSearch.company = L"PythonCore";
majorSearch.companyLength = -1;
majorSearch.tag = L"3";
majorSearch.tagLength = -1;
majorSearch.oldStyleTag = true;
major = NULL;
exitCode = selectEnvironment(&majorSearch, env, &major);
if (!exitCode && major) {
exitCode = _printEnvironment(major, out, showPath, L"-3 *");
isDefault = false;
if (exitCode) {
return exitCode;
}
}
majorSearch.tag = L"2";
major = NULL;
exitCode = selectEnvironment(&majorSearch, env, &major);
if (!exitCode && major) {
exitCode = _printEnvironment(major, out, showPath, L"-2");
if (exitCode) {
return exitCode;
}
}
}
*/
int mode = _setmode(_fileno(out), _O_U8TEXT);
int exitCode = _listAllEnvironments(env, out, showPath, defaultEnv);
fflush(out);
if (mode >= 0) {
_setmode(_fileno(out), mode);
}
return exitCode;
}
/******************************************************************************\
*** INTERPRETER LAUNCH ***
\******************************************************************************/
int
calculateCommandLine(const SearchInfo *search, const EnvironmentInfo *launch, wchar_t *buffer, int bufferLength)
{
int exitCode = 0;
const wchar_t *executablePath = NULL;
// Construct command line from a search override, or else the selected
// environment's executablePath
if (search->executablePath) {
executablePath = search->executablePath;
} else if (launch && launch->executablePath) {
executablePath = launch->executablePath;
}
// If we have an executable path, put it at the start of the command, but
// only if the search allowed an override.
// Otherwise, use the environment's installDir and the search's default
// executable name.
if (executablePath && search->allowExecutableOverride) {
if (wcschr(executablePath, L' ') && executablePath[0] != L'"') {
buffer[0] = L'"';
exitCode = wcscpy_s(&buffer[1], bufferLength - 1, executablePath);
if (!exitCode) {
exitCode = wcscat_s(buffer, bufferLength, L"\"");
}
} else {
exitCode = wcscpy_s(buffer, bufferLength, executablePath);
}
} else if (launch) {
if (!launch->installDir) {
fwprintf_s(stderr, L"Cannot launch %s %s because no install directory was specified",
launch->company, launch->tag);
exitCode = RC_NO_PYTHON;
} else if (!search->executable || !search->executableLength) {
fwprintf_s(stderr, L"Cannot launch %s %s because no executable name is available",
launch->company, launch->tag);
exitCode = RC_NO_PYTHON;
} else {
wchar_t executable[256];
wcsncpy_s(executable, 256, search->executable, search->executableLength);
if ((wcschr(launch->installDir, L' ') && launch->installDir[0] != L'"') ||
(wcschr(executable, L' ') && executable[0] != L'"')) {
buffer[0] = L'"';
exitCode = wcscpy_s(&buffer[1], bufferLength - 1, launch->installDir);
if (!exitCode) {
exitCode = join(buffer, bufferLength, executable) ? 0 : RC_NO_MEMORY;
}
if (!exitCode) {
exitCode = wcscat_s(buffer, bufferLength, L"\"");
}
} else {
exitCode = wcscpy_s(buffer, bufferLength, launch->installDir);
if (!exitCode) {
exitCode = join(buffer, bufferLength, executable) ? 0 : RC_NO_MEMORY;
}
}
}
} else {
exitCode = RC_NO_PYTHON;
}
if (!exitCode && launch && launch->executableArgs) {
exitCode = wcscat_s(buffer, bufferLength, L" ");
if (!exitCode) {
exitCode = wcscat_s(buffer, bufferLength, launch->executableArgs);
}
}
if (!exitCode && search->executableArgs) {
if (search->executableArgsLength < 0) {
exitCode = wcscat_s(buffer, bufferLength, search->executableArgs);
} else if (search->executableArgsLength > 0) {
int end = (int)wcsnlen_s(buffer, MAXLEN);
if (end < bufferLength - (search->executableArgsLength + 1)) {
exitCode = wcsncpy_s(&buffer[end], bufferLength - end,
search->executableArgs, search->executableArgsLength);
}
}
}
if (!exitCode && search->restOfCmdLine) {
exitCode = wcscat_s(buffer, bufferLength, search->restOfCmdLine);
}
return exitCode;
}
BOOL
_safeDuplicateHandle(HANDLE in, HANDLE * pout, const wchar_t *nameForError)
{
BOOL ok;
HANDLE process = GetCurrentProcess();
DWORD rc;
*pout = NULL;
ok = DuplicateHandle(process, in, process, pout, 0, TRUE,
DUPLICATE_SAME_ACCESS);
if (!ok) {
rc = GetLastError();
if (rc == ERROR_INVALID_HANDLE) {
debug(L"DuplicateHandle returned ERROR_INVALID_HANDLE\n");
ok = TRUE;
}
else {
winerror(0, L"Failed to duplicate %s handle", nameForError);
}
}
return ok;
}
BOOL WINAPI
ctrl_c_handler(DWORD code)
{
return TRUE; /* We just ignore all control events. */
}
int
launchEnvironment(const SearchInfo *search, const EnvironmentInfo *launch, wchar_t *launchCommand)
{
HANDLE job;
JOBOBJECT_EXTENDED_LIMIT_INFORMATION info;
DWORD rc;
BOOL ok;
STARTUPINFOW si;
PROCESS_INFORMATION pi;
// If this is a dryrun, do not actually launch
if (isEnvVarSet(L"PYLAUNCHER_DRYRUN")) {
debug(L"LaunchCommand: %s\n", launchCommand);
debug(L"# Exiting due to PYLAUNCHER_DRYRUN variable\n");
fflush(stdout);
int mode = _setmode(_fileno(stdout), _O_U8TEXT);
fwprintf(stdout, L"%s\n", launchCommand);
fflush(stdout);
if (mode >= 0) {
_setmode(_fileno(stdout), mode);
}
return 0;
}
#if defined(_WINDOWS)
/*
When explorer launches a Windows (GUI) application, it displays
the "app starting" (the "pointer + hourglass") cursor for a number
of seconds, or until the app does something UI-ish (eg, creating a
window, or fetching a message). As this launcher doesn't do this
directly, that cursor remains even after the child process does these
things. We avoid that by doing a simple post+get message.
See http://bugs.python.org/issue17290 and
https://bitbucket.org/vinay.sajip/pylauncher/issue/20/busy-cursor-for-a-long-time-when-running
*/
MSG msg;
PostMessage(0, 0, 0, 0);
GetMessage(&msg, 0, 0, 0);
#endif
debug(L"# about to run: %s\n", launchCommand);
job = CreateJobObject(NULL, NULL);
ok = QueryInformationJobObject(job, JobObjectExtendedLimitInformation,
&info, sizeof(info), &rc);
if (!ok || (rc != sizeof(info)) || !job) {
winerror(0, L"Failed to query job information");
return RC_CREATE_PROCESS;
}
info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE |
JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
ok = SetInformationJobObject(job, JobObjectExtendedLimitInformation, &info,
sizeof(info));
if (!ok) {
winerror(0, L"Failed to update job information");
return RC_CREATE_PROCESS;
}
memset(&si, 0, sizeof(si));
GetStartupInfoW(&si);
if (!_safeDuplicateHandle(GetStdHandle(STD_INPUT_HANDLE), &si.hStdInput, L"stdin") ||
!_safeDuplicateHandle(GetStdHandle(STD_OUTPUT_HANDLE), &si.hStdOutput, L"stdout") ||
!_safeDuplicateHandle(GetStdHandle(STD_ERROR_HANDLE), &si.hStdError, L"stderr")) {
return RC_NO_STD_HANDLES;
}
ok = SetConsoleCtrlHandler(ctrl_c_handler, TRUE);
if (!ok) {
winerror(0, L"Failed to update Control-C handler");
return RC_NO_STD_HANDLES;
}
si.dwFlags = STARTF_USESTDHANDLES;
ok = CreateProcessW(NULL, launchCommand, NULL, NULL, TRUE,
0, NULL, NULL, &si, &pi);
if (!ok) {
winerror(0, L"Unable to create process using '%s'", launchCommand);
return RC_CREATE_PROCESS;
}
AssignProcessToJobObject(job, pi.hProcess);
CloseHandle(pi.hThread);
WaitForSingleObjectEx(pi.hProcess, INFINITE, FALSE);
ok = GetExitCodeProcess(pi.hProcess, &rc);
if (!ok) {
winerror(0, L"Failed to get exit code of process");
return RC_CREATE_PROCESS;
}
debug(L"child process exit code: %d\n", rc);
return rc;
}
/******************************************************************************\
*** PROCESS CONTROLLER ***
\******************************************************************************/
int
performSearch(SearchInfo *search, EnvironmentInfo **envs)
{
// First parse the command line for options
int exitCode = parseCommandLine(search);
if (exitCode) {
return exitCode;
}
// Check for a shebang line in our script file
// (or return quickly if no script file was specified)
exitCode = checkShebang(search);
switch (exitCode) {
case 0:
case RC_NO_SHEBANG:
case RC_RECURSIVE_SHEBANG:
break;
default:
return exitCode;
}
// Resolve old-style tags (possibly from a shebang) against py.ini entries
// and environment variables.
exitCode = checkDefaults(search);
if (exitCode) {
return exitCode;
}
// If debugging is enabled, list our search criteria
dumpSearchInfo(search);
// Find all matching environments
exitCode = collectEnvironments(search, envs);
if (exitCode) {
return exitCode;
}
return 0;
}
int
process(int argc, wchar_t ** argv)
{
int exitCode = 0;
int searchExitCode = 0;
SearchInfo search = {0};
EnvironmentInfo *envs = NULL;
EnvironmentInfo *env = NULL;
wchar_t launchCommand[MAXLEN];
memset(launchCommand, 0, sizeof(launchCommand));
if (isEnvVarSet(L"PYLAUNCHER_DEBUG")) {
setvbuf(stderr, (char *)NULL, _IONBF, 0);
log_fp = stderr;
debug(L"argv0: %s\nversion: %S\n", argv[0], PY_VERSION);
}
search.originalCmdLine = GetCommandLineW();
exitCode = performSearch(&search, &envs);
if (exitCode) {
goto abort;
}
// Display the help text, but only exit on error
if (search.help) {
exitCode = showHelpText(argv);
if (exitCode) {
goto abort;
}
}
// Select best environment
// This is early so that we can show the default when listing, but all
// responses to any errors occur later.
searchExitCode = selectEnvironment(&search, envs, &env);
// List all environments, then exit
if (search.list || search.listPaths) {
exitCode = listEnvironments(envs, stdout, search.listPaths, env);
goto abort;
}
// When debugging, list all discovered environments anyway
if (log_fp) {
exitCode = listEnvironments(envs, log_fp, true, NULL);
if (exitCode) {
goto abort;
}
}
// We searched earlier, so if we didn't find anything, now we react
exitCode = searchExitCode;
// If none found, and if permitted, install it
if (exitCode == RC_NO_PYTHON && isEnvVarSet(L"PYLAUNCHER_ALLOW_INSTALL") ||
isEnvVarSet(L"PYLAUNCHER_ALWAYS_INSTALL")) {
exitCode = installEnvironment(&search);
if (!exitCode) {
// Successful install, so we need to re-scan and select again
env = NULL;
exitCode = performSearch(&search, &envs);
if (exitCode) {
goto abort;
}
exitCode = selectEnvironment(&search, envs, &env);
}
}
if (exitCode == RC_NO_PYTHON) {
fputws(L"No suitable Python runtime found\n", stderr);
fputws(L"Pass --list (-0) to see all detected environments on your machine\n", stderr);
if (!isEnvVarSet(L"PYLAUNCHER_ALLOW_INSTALL") && search.oldStyleTag) {
fputws(L"or set environment variable PYLAUNCHER_ALLOW_INSTALL to use winget\n"
L"or open the Microsoft Store to the requested version.\n", stderr);
}
goto abort;
}
if (exitCode == RC_NO_PYTHON_AT_ALL) {
fputws(L"No installed Python found!\n", stderr);
goto abort;
}
if (exitCode) {
goto abort;
}
if (env) {
debug(L"env.company: %s\nenv.tag: %s\n", env->company, env->tag);
} else {
debug(L"env.company: (null)\nenv.tag: (null)\n");
}
exitCode = calculateCommandLine(&search, env, launchCommand, sizeof(launchCommand) / sizeof(launchCommand[0]));
if (exitCode) {
goto abort;
}
// Launch selected runtime
exitCode = launchEnvironment(&search, env, launchCommand);
abort:
freeSearchInfo(&search);
freeEnvironmentInfo(envs);
return exitCode;
}
#if defined(_WINDOWS)
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPWSTR lpstrCmd, int nShow)
{
return process(__argc, __wargv);
}
#else
int cdecl wmain(int argc, wchar_t ** argv)
{
return process(argc, argv);
}
#endif