mirror of
https://github.com/python/cpython.git
synced 2025-08-30 21:48:47 +00:00

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.
2666 lines
81 KiB
C
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
|