summaryrefslogtreecommitdiffstats
path: root/PC/launcher2.c
diff options
context:
space:
mode:
authorSteve Dower <steve.dower@python.org>2022-03-28 23:21:08 (GMT)
committerGitHub <noreply@github.com>2022-03-28 23:21:08 (GMT)
commitbad86a621af61f383b9f06fe4a08f66245df99e2 (patch)
tree0d52b1d9356c9cd73947db5a63d9829f9c5af2f4 /PC/launcher2.c
parent5c30388f3c586ba2f33e349e22e5949cb92de621 (diff)
downloadcpython-bad86a621af61f383b9f06fe4a08f66245df99e2.zip
cpython-bad86a621af61f383b9f06fe4a08f66245df99e2.tar.gz
cpython-bad86a621af61f383b9f06fe4a08f66245df99e2.tar.bz2
bpo-46566: Add new py.exe launcher implementation (GH-32062)
Diffstat (limited to 'PC/launcher2.c')
-rw-r--r--PC/launcher2.c2264
1 files changed, 2264 insertions, 0 deletions
diff --git a/PC/launcher2.c b/PC/launcher2.c
new file mode 100644
index 0000000..8735381
--- /dev/null
+++ b/PC/launcher2.c
@@ -0,0 +1,2264 @@
+/*
+ * 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>
+
+#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
+
+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 (xLen == 0) {
+ return yLen == 0 ? 0 : -1;
+ } else if (yLen == 0) {
+ 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 (xLen == 0) {
+ return yLen == 0 ? 0 : -1;
+ } else if (yLen == 0) {
+ 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 (xLen == 0) {
+ return yLen == 0 ? 0 : -1;
+ } else if (yLen == 0) {
+ 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)
+{
+ 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)
+{
+ 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, we had an old-style tag with '-64' suffix, and so do not
+ // want to match tags like '3.x-32'
+ bool exclude32Bit;
+ // if true, we had an old-style tag with '-32' suffix, and so *only*
+ // want to match tags like '3.x-32'
+ bool only32Bit;
+ // 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;
+ }
+
+#define DEBUGNAME(s) L"SearchInfo." ## s
+#define DEBUG(s) debug(DEBUGNAME(#s) L": %s\n", (search->s) ? (search->s) : L"(null)")
+#define DEBUG_2(s, sl) _debugStringAndLength((search->s), (search->sl), DEBUGNAME(#s))
+#define DEBUG_BOOL(s) debug(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(exclude32Bit);
+ DEBUG_BOOL(only32Bit);
+ 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
+findArgumentLength(const wchar_t *buffer, int bufferLength)
+{
+ if (bufferLength < 0) {
+ bufferLength = (int)wcsnlen_s(buffer, MAXLEN);
+ }
+ if (bufferLength == 0) {
+ return 0;
+ }
+ const wchar_t *end;
+ int i;
+
+ if (buffer[0] != L'"') {
+ end = wcschr(buffer, L' ');
+ if (!end) {
+ return bufferLength;
+ }
+ i = (int)(end - buffer);
+ return i < bufferLength ? i : bufferLength;
+ }
+
+ i = 0;
+ while (i < bufferLength) {
+ end = wcschr(&buffer[i + 1], L'"');
+ if (!end) {
+ return bufferLength;
+ }
+
+ i = (int)(end - buffer);
+ if (i >= bufferLength) {
+ return bufferLength;
+ }
+
+ int j = i;
+ while (j > 1 && buffer[--j] == L'\\') {
+ if (j > 0 && buffer[--j] == L'\\') {
+ // Even number, so back up and keep counting
+ } else {
+ // Odd number, so it's escaped and we want to keep searching
+ continue;
+ }
+ }
+
+ // Non-escaped quote with space after it - end of the argument!
+ if (i + 1 >= bufferLength || isspace(buffer[i + 1])) {
+ return i + 1;
+ }
+ }
+
+ return bufferLength;
+}
+
+
+const wchar_t *
+findArgumentEnd(const wchar_t *buffer, int bufferLength)
+{
+ return &buffer[findArgumentLength(buffer, bufferLength)];
+}
+
+
+/******************************************************************************\
+ *** COMMAND-LINE PARSING ***
+\******************************************************************************/
+
+
+int
+parseCommandLine(SearchInfo *search)
+{
+ if (!search || !search->originalCmdLine) {
+ return RC_NO_COMMANDLINE;
+ }
+
+ const wchar_t *tail = findArgumentEnd(search->originalCmdLine, -1);
+ const wchar_t *end = tail;
+ search->restOfCmdLine = tail;
+ while (--tail != search->originalCmdLine) {
+ if (*tail == L'.' && end == search->restOfCmdLine) {
+ end = tail;
+ } else if (*tail == L'\\' || *tail == L'/') {
+ ++tail;
+ break;
+ }
+ }
+ // 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;
+ // If the tag ends with -64, we want to exclude 32-bit runtimes
+ // (If the tag ends with -32, it will be filtered later)
+ if (argLen > 3) {
+ if (0 == _compareArgument(&arg[argLen - 3], 3, L"-64", 3)) {
+ search->tagLength -= 3;
+ search->exclude32Bit = true;
+ } else if (0 == _compareArgument(&arg[argLen - 3], 3, L"-32", 3)) {
+ search->tagLength -= 3;
+ search->only32Bit = true;
+ }
+ }
+ } 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->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 prefixLength = (int)wcsnlen_s(prefix, MAXLEN);
+ if (bufferLength < prefixLength) {
+ return false;
+ }
+ if (rest) {
+ *rest = &buffer[prefixLength];
+ }
+ return _startsWithArgument(buffer, bufferLength, prefix, prefixLength);
+}
+
+
+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 true;
+ } 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
+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;
+ int exitCode = _decodeShebang(search, start, (int)(b - start + 1), onlyUtf8, &shebang, &shebangLength);
+ if (exitCode) {
+ return exitCode;
+ }
+ debug(L"Shebang: %s\n", shebang);
+
+ // Handle some known, case-sensitive shebang templates
+ const wchar_t *command;
+ int commandLength;
+ static const wchar_t *shebangTemplates[] = {
+ L"/usr/bin/env ",
+ L"/usr/bin/",
+ L"/usr/local/bin/",
+ L"",
+ NULL
+ };
+ for (const wchar_t **tmpl = shebangTemplates; *tmpl; ++tmpl) {
+ if (_shebangStartsWith(shebang, shebangLength, *tmpl, &command)) {
+ commandLength = 0;
+ while (command[commandLength] && !isspace(command[commandLength])) {
+ commandLength += 1;
+ }
+ if (!commandLength) {
+ } else if (_findCommand(search, command, commandLength)) {
+ search->executableArgs = &command[commandLength];
+ search->executableArgsLength = shebangLength - commandLength;
+ debug(L"# Treating shebang command '%.*s' as %s\n",
+ commandLength, command, search->executablePath);
+ } else if (_shebangStartsWith(command, commandLength, L"python", NULL)) {
+ search->tag = &command[6];
+ search->tagLength = commandLength - 6;
+ search->oldStyleTag = true;
+ search->executableArgs = &command[commandLength];
+ search->executableArgsLength = shebangLength - commandLength;
+ debug(L"# Treating shebang command '%.*s' as 'py -%.*s'\n",
+ commandLength, command, search->tagLength, search->tag);
+ } else {
+ debug(L"# Found shebang command but could not execute it: %.*s\n",
+ commandLength, command);
+ }
+ break;
+ }
+ }
+
+ return 0;
+}
+
+
+int
+checkDefaults(SearchInfo *search)
+{
+ if (!search->allowDefaults || search->list || search->listPaths) {
+ return 0;
+ }
+
+ wchar_t buffer[MAXLEN];
+ int n;
+
+ // If no tag was provided at all, look for an active virtual environment
+ if (!search->tag || !search->tagLength) {
+ n = GetEnvironmentVariableW(L"VIRTUAL_ENV", buffer, MAXLEN);
+ if (n && join(buffer, MAXLEN, L"Scripts") && join(buffer, MAXLEN, search->executable)) {
+ if (INVALID_FILE_ATTRIBUTES != GetFileAttributesW(buffer)) {
+ n = (int)wcsnlen_s(buffer, MAXLEN) + 1;
+ wchar_t *path = allocSearchInfoBuffer(search, n);
+ if (!path) {
+ return RC_NO_MEMORY;
+ }
+ search->executablePath = path;
+ wcscpy_s(path, n, buffer);
+ return 0;
+ } else {
+ debug(L"Python executable %s missing from virtual env\n", buffer);
+ }
+ }
+ }
+
+ // 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 *settingName = NULL;
+ if (!search->tag || !search->tagLength) {
+ settingName = L"py_python";
+ } else if (0 == wcsncmp(search->tag, L"3", search->tagLength)) {
+ settingName = L"py_python3";
+ } else if (0 == wcsncmp(search->tag, L"2", search->tagLength)) {
+ settingName = 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
+ n = GetEnvironmentVariableW(settingName, buffer, MAXLEN);
+
+ // If none found, check in our two .ini files instead
+ if (!n) {
+ n = _readIni(L"defaults", settingName, 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;
+ } else {
+ search->company = tag;
+ search->companyLength = (int)(slash - tag);
+ search->tag = slash + 1;
+ search->tagLength = n - (search->companyLength + 1);
+ search->oldStyleTag = false;
+ }
+ }
+
+ 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;
+} 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)
+{
+ 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)
+{
+ // 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 *node)
+{
+ EnvironmentInfo *r = *root;
+ if (!r) {
+ *root = node;
+ node->parent = NULL;
+ return 0;
+ }
+ // Sort by company name
+ switch (_compareCompany(node->company, r->company)) {
+ case -1:
+ return addEnvironmentInfo(&r->prev, node);
+ case 1:
+ return addEnvironmentInfo(&r->next, node);
+ case 0:
+ break;
+ }
+ // Then by tag (descending)
+ switch (_compareTag(node->tag, r->tag)) {
+ case -1:
+ return addEnvironmentInfo(&r->next, node);
+ case 1:
+ return addEnvironmentInfo(&r->prev, node);
+ case 0:
+ break;
+ }
+ // Then keep the one with the lowest internal sort key
+ if (r->internalSortKey < node->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;
+ }
+ }
+ node->next = r->next;
+ node->prev = r->prev;
+ } else {
+ debug(L"# not adding %s/%s/%i to tree\n", node->company, node->tag, node->internalSortKey);
+ freeEnvironmentInfo(node);
+ 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);
+}
+
+
+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 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;
+ }
+
+ exitCode = _registryReadString(&env->architecture, root, NULL, L"SysArchitecture");
+ if (exitCode) {
+ return exitCode;
+ }
+
+ exitCode = _registryReadString(&env->displayName, root, NULL, L"DisplayName");
+ if (exitCode) {
+ return exitCode;
+ }
+
+ // Only PythonCore entries will infer executablePath from installDir and architecture from the binary
+ if (0 == _compare(env->company, -1, L"PythonCore", -1)) {
+ if (!env->executablePath) {
+ exitCode = _combineWithInstallDir(
+ &env->executablePath,
+ env->installDir,
+ search->executable,
+ search->executableLength
+ );
+ if (exitCode) {
+ return exitCode;
+ }
+ }
+ if (!env->architecture && env->executablePath && fallbackArch) {
+ copyWstr(&env->architecture, fallbackArch);
+ }
+ }
+
+ if (!env->executablePath) {
+ debug(L"# %s/%s has no executable path\n", env->company, env->tag);
+ return RC_NO_PYTHON;
+ }
+
+ 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, env);
+ if (exitCode == RC_DUPLICATE_ITEM) {
+ exitCode = 0;
+ } else if (exitCode) {
+ freeEnvironmentInfo(env);
+ }
+ }
+ }
+ }
+ 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, env);
+ if (exitCode == RC_DUPLICATE_ITEM) {
+ exitCode = 0;
+ } else if (exitCode) {
+ freeEnvironmentInfo(env);
+ }
+
+
+ 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.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.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;
+ EnvironmentInfo *env = NULL;
+ if (!result) {
+ return RC_INTERNAL_ERROR;
+ }
+ *result = NULL;
+
+ if (search->executablePath) {
+ env = newEnvironmentInfo(NULL, NULL);
+ return copyWstr(&env->executablePath, search->executablePath);
+ }
+
+ HKEY root;
+
+ 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.10 */ L"9PJPW5LDXLZ5" },
+ { 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 (!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 (_startsWith(env->tag, -1, search->tag, search->tagLength)) {
+ if (search->exclude32Bit && _is32Bit(env)) {
+ debug(L"# Excluding %s/%s because it looks like 32bit\n", env->company, env->tag);
+ } else if (search->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, bool *isDefault)
+{
+ wchar_t buffer[256];
+ while (env) {
+ int exitCode = _listAllEnvironments(env->prev, out, showPath, isDefault);
+ 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, sizeof(buffer) / sizeof(buffer[0]), L"-V:%s", env->tag);
+ } else {
+ swprintf_s(buffer, sizeof(buffer) / sizeof(buffer[0]), L"-V:%s/%s", env->company, env->tag);
+ }
+ if (buffer[0]) {
+ if (*isDefault) {
+ wcscat_s(buffer, sizeof(buffer) / sizeof(buffer[0]), L" *");
+ *isDefault = false;
+ }
+ exitCode = _printEnvironment(env, out, showPath, buffer);
+ if (exitCode) {
+ return exitCode;
+ }
+ }
+
+ env = env->next;
+ }
+ return 0;
+}
+
+
+int
+listEnvironments(EnvironmentInfo *env, FILE * out, bool showPath)
+{
+ bool isDefault = true;
+
+ 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, &isDefault);
+ 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);
+ if (exitCode) {
+ 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;
+ 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;
+ }
+ }
+
+ // List all environments, then exit
+ if (search.list || search.listPaths) {
+ exitCode = listEnvironments(envs, stdout, search.listPaths);
+ goto abort;
+ }
+
+ // When debugging, list all discovered environments anyway
+ if (log_fp) {
+ exitCode = listEnvironments(envs, log_fp, true);
+ if (exitCode) {
+ goto abort;
+ }
+ }
+
+ // Select best environment
+ env = NULL;
+ if (search.executablePath == NULL) {
+ exitCode = selectEnvironment(&search, envs, &env);
+ // 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
+ exitCode = performSearch(&search, &envs);
+ if (exitCode) {
+ goto abort;
+ }
+ env = NULL;
+ 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