xref: /third_party/python/PC/launcher2.c (revision 7db96d56)
1/*
2 * Rewritten Python launcher for Windows
3 *
4 * This new rewrite properly handles PEP 514 and allows any registered Python
5 * runtime to be launched. It also enables auto-install of versions when they
6 * are requested but no installation can be found.
7 */
8
9#define __STDC_WANT_LIB_EXT1__ 1
10
11#include <windows.h>
12#include <pathcch.h>
13#include <fcntl.h>
14#include <io.h>
15#include <shlobj.h>
16#include <stdio.h>
17#include <stdbool.h>
18#include <tchar.h>
19#include <assert.h>
20
21#define MS_WINDOWS
22#include "patchlevel.h"
23
24#define MAXLEN PATHCCH_MAX_CCH
25#define MSGSIZE 1024
26
27#define RC_NO_STD_HANDLES   100
28#define RC_CREATE_PROCESS   101
29#define RC_BAD_VIRTUAL_PATH 102
30#define RC_NO_PYTHON        103
31#define RC_NO_MEMORY        104
32#define RC_NO_SCRIPT        105
33#define RC_NO_VENV_CFG      106
34#define RC_BAD_VENV_CFG     107
35#define RC_NO_COMMANDLINE   108
36#define RC_INTERNAL_ERROR   109
37#define RC_DUPLICATE_ITEM   110
38#define RC_INSTALLING       111
39#define RC_NO_PYTHON_AT_ALL 112
40#define RC_NO_SHEBANG       113
41#define RC_RECURSIVE_SHEBANG 114
42
43static FILE * log_fp = NULL;
44
45void
46debug(wchar_t * format, ...)
47{
48    va_list va;
49
50    if (log_fp != NULL) {
51        wchar_t buffer[MAXLEN];
52        int r = 0;
53        va_start(va, format);
54        r = vswprintf_s(buffer, MAXLEN, format, va);
55        va_end(va);
56
57        if (r <= 0) {
58            return;
59        }
60        fputws(buffer, log_fp);
61        while (r && isspace(buffer[r])) {
62            buffer[r--] = L'\0';
63        }
64        if (buffer[0]) {
65            OutputDebugStringW(buffer);
66        }
67    }
68}
69
70
71void
72formatWinerror(int rc, wchar_t * message, int size)
73{
74    FormatMessageW(
75        FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
76        NULL, rc, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
77        message, size, NULL);
78}
79
80
81void
82winerror(int err, wchar_t * format, ... )
83{
84    va_list va;
85    wchar_t message[MSGSIZE];
86    wchar_t win_message[MSGSIZE];
87    int len;
88
89    if (err == 0) {
90        err = GetLastError();
91    }
92
93    va_start(va, format);
94    len = _vsnwprintf_s(message, MSGSIZE, _TRUNCATE, format, va);
95    va_end(va);
96
97    formatWinerror(err, win_message, MSGSIZE);
98    if (len >= 0) {
99        _snwprintf_s(&message[len], MSGSIZE - len, _TRUNCATE, L": %s",
100                     win_message);
101    }
102
103#if !defined(_WINDOWS)
104    fwprintf(stderr, L"%s\n", message);
105#else
106    MessageBoxW(NULL, message, L"Python Launcher is sorry to say ...",
107               MB_OK);
108#endif
109}
110
111
112void
113error(wchar_t * format, ... )
114{
115    va_list va;
116    wchar_t message[MSGSIZE];
117
118    va_start(va, format);
119    _vsnwprintf_s(message, MSGSIZE, _TRUNCATE, format, va);
120    va_end(va);
121
122#if !defined(_WINDOWS)
123    fwprintf(stderr, L"%s\n", message);
124#else
125    MessageBoxW(NULL, message, L"Python Launcher is sorry to say ...",
126               MB_OK);
127#endif
128}
129
130
131typedef BOOL (*PIsWow64Process2)(HANDLE, USHORT*, USHORT*);
132
133
134USHORT
135_getNativeMachine()
136{
137    static USHORT _nativeMachine = IMAGE_FILE_MACHINE_UNKNOWN;
138    if (_nativeMachine == IMAGE_FILE_MACHINE_UNKNOWN) {
139        USHORT processMachine;
140        HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");
141        PIsWow64Process2 IsWow64Process2 = kernel32 ?
142            (PIsWow64Process2)GetProcAddress(kernel32, "IsWow64Process2") :
143            NULL;
144        if (!IsWow64Process2) {
145            BOOL wow64Process;
146            if (!IsWow64Process(NULL, &wow64Process)) {
147                winerror(0, L"Checking process type");
148            } else if (wow64Process) {
149                // We should always be a 32-bit executable, so if running
150                // under emulation, it must be a 64-bit host.
151                _nativeMachine = IMAGE_FILE_MACHINE_AMD64;
152            } else {
153                // Not running under emulation, and an old enough OS to not
154                // have IsWow64Process2, so assume it's x86.
155                _nativeMachine = IMAGE_FILE_MACHINE_I386;
156            }
157        } else if (!IsWow64Process2(NULL, &processMachine, &_nativeMachine)) {
158            winerror(0, L"Checking process type");
159        }
160    }
161    return _nativeMachine;
162}
163
164
165bool
166isAMD64Host()
167{
168    return _getNativeMachine() == IMAGE_FILE_MACHINE_AMD64;
169}
170
171
172bool
173isARM64Host()
174{
175    return _getNativeMachine() == IMAGE_FILE_MACHINE_ARM64;
176}
177
178
179bool
180isEnvVarSet(const wchar_t *name)
181{
182    /* only looking for non-empty, which means at least one character
183       and the null terminator */
184    return GetEnvironmentVariableW(name, NULL, 0) >= 2;
185}
186
187
188bool
189join(wchar_t *buffer, size_t bufferLength, const wchar_t *fragment)
190{
191    if (SUCCEEDED(PathCchCombineEx(buffer, bufferLength, buffer, fragment, PATHCCH_ALLOW_LONG_PATHS))) {
192        return true;
193    }
194    return false;
195}
196
197
198int
199_compare(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
200{
201    // Empty strings sort first
202    if (!x || !xLen) {
203        return (!y || !yLen) ? 0 : -1;
204    } else if (!y || !yLen) {
205        return 1;
206    }
207    switch (CompareStringEx(
208        LOCALE_NAME_INVARIANT, NORM_IGNORECASE | SORT_DIGITSASNUMBERS,
209        x, xLen, y, yLen,
210        NULL, NULL, 0
211    )) {
212    case CSTR_LESS_THAN:
213        return -1;
214    case CSTR_EQUAL:
215        return 0;
216    case CSTR_GREATER_THAN:
217        return 1;
218    default:
219        winerror(0, L"Error comparing '%.*s' and '%.*s' (compare)", xLen, x, yLen, y);
220        return -1;
221    }
222}
223
224
225int
226_compareArgument(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
227{
228    // Empty strings sort first
229    if (!x || !xLen) {
230        return (!y || !yLen) ? 0 : -1;
231    } else if (!y || !yLen) {
232        return 1;
233    }
234    switch (CompareStringEx(
235        LOCALE_NAME_INVARIANT, 0,
236        x, xLen, y, yLen,
237        NULL, NULL, 0
238    )) {
239    case CSTR_LESS_THAN:
240        return -1;
241    case CSTR_EQUAL:
242        return 0;
243    case CSTR_GREATER_THAN:
244        return 1;
245    default:
246        winerror(0, L"Error comparing '%.*s' and '%.*s' (compareArgument)", xLen, x, yLen, y);
247        return -1;
248    }
249}
250
251int
252_comparePath(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
253{
254    // Empty strings sort first
255    if (!x || !xLen) {
256        return !y || !yLen ? 0 : -1;
257    } else if (!y || !yLen) {
258        return 1;
259    }
260    switch (CompareStringOrdinal(x, xLen, y, yLen, TRUE)) {
261    case CSTR_LESS_THAN:
262        return -1;
263    case CSTR_EQUAL:
264        return 0;
265    case CSTR_GREATER_THAN:
266        return 1;
267    default:
268        winerror(0, L"Error comparing '%.*s' and '%.*s' (comparePath)", xLen, x, yLen, y);
269        return -1;
270    }
271}
272
273
274bool
275_startsWith(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
276{
277    if (!x || !y) {
278        return false;
279    }
280    yLen = yLen < 0 ? (int)wcsnlen_s(y, MAXLEN) : yLen;
281    xLen = xLen < 0 ? (int)wcsnlen_s(x, MAXLEN) : xLen;
282    return xLen >= yLen && 0 == _compare(x, yLen, y, yLen);
283}
284
285
286bool
287_startsWithArgument(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
288{
289    if (!x || !y) {
290        return false;
291    }
292    yLen = yLen < 0 ? (int)wcsnlen_s(y, MAXLEN) : yLen;
293    xLen = xLen < 0 ? (int)wcsnlen_s(x, MAXLEN) : xLen;
294    return xLen >= yLen && 0 == _compareArgument(x, yLen, y, yLen);
295}
296
297
298// Unlike regular startsWith, this function requires that the following
299// character is either NULL (that is, the entire string matches) or is one of
300// the characters in 'separators'.
301bool
302_startsWithSeparated(const wchar_t *x, int xLen, const wchar_t *y, int yLen, const wchar_t *separators)
303{
304    if (!x || !y) {
305        return false;
306    }
307    yLen = yLen < 0 ? (int)wcsnlen_s(y, MAXLEN) : yLen;
308    xLen = xLen < 0 ? (int)wcsnlen_s(x, MAXLEN) : xLen;
309    if (xLen < yLen) {
310        return false;
311    }
312    if (xLen == yLen) {
313        return 0 == _compare(x, xLen, y, yLen);
314    }
315    return separators &&
316        0 == _compare(x, yLen, y, yLen) &&
317        wcschr(separators, x[yLen]) != NULL;
318}
319
320
321
322/******************************************************************************\
323 ***                               HELP TEXT                                ***
324\******************************************************************************/
325
326
327int
328showHelpText(wchar_t ** argv)
329{
330    // The help text is stored in launcher-usage.txt, which is compiled into
331    // the launcher and loaded at runtime if needed.
332    //
333    // The file must be UTF-8. There are two substitutions:
334    //  %ls - PY_VERSION (as wchar_t*)
335    //  %ls - argv[0] (as wchar_t*)
336    HRSRC res = FindResourceExW(NULL, L"USAGE", MAKEINTRESOURCE(1), MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL));
337    HGLOBAL resData = res ? LoadResource(NULL, res) : NULL;
338    const char *usage = resData ? (const char*)LockResource(resData) : NULL;
339    if (usage == NULL) {
340        winerror(0, L"Unable to load usage text");
341        return RC_INTERNAL_ERROR;
342    }
343
344    DWORD cbData = SizeofResource(NULL, res);
345    DWORD cchUsage = MultiByteToWideChar(CP_UTF8, 0, usage, cbData, NULL, 0);
346    if (!cchUsage) {
347        winerror(0, L"Unable to preprocess usage text");
348        return RC_INTERNAL_ERROR;
349    }
350
351    cchUsage += 1;
352    wchar_t *wUsage = (wchar_t*)malloc(cchUsage * sizeof(wchar_t));
353    cchUsage = MultiByteToWideChar(CP_UTF8, 0, usage, cbData, wUsage, cchUsage);
354    if (!cchUsage) {
355        winerror(0, L"Unable to preprocess usage text");
356        free((void *)wUsage);
357        return RC_INTERNAL_ERROR;
358    }
359    // Ensure null termination
360    wUsage[cchUsage] = L'\0';
361
362    fwprintf(stdout, wUsage, (L"" PY_VERSION), argv[0]);
363    fflush(stdout);
364
365    free((void *)wUsage);
366
367    return 0;
368}
369
370
371/******************************************************************************\
372 ***                              SEARCH INFO                               ***
373\******************************************************************************/
374
375
376struct _SearchInfoBuffer {
377    struct _SearchInfoBuffer *next;
378    wchar_t buffer[0];
379};
380
381
382typedef struct {
383    // the original string, managed by the OS
384    const wchar_t *originalCmdLine;
385    // pointer into the cmdline to mark what we've consumed
386    const wchar_t *restOfCmdLine;
387    // if known/discovered, the full executable path of our runtime
388    const wchar_t *executablePath;
389    // pointer and length into cmdline for the file to check for a
390    // shebang line, if any. Length can be -1 if the string is null
391    // terminated.
392    const wchar_t *scriptFile;
393    int scriptFileLength;
394    // pointer and length into cmdline or a static string with the
395    // name of the target executable. Length can be -1 if the string
396    // is null terminated.
397    const wchar_t *executable;
398    int executableLength;
399    // pointer and length into a string with additional interpreter
400    // arguments to include before restOfCmdLine. Length can be -1 if
401    // the string is null terminated.
402    const wchar_t *executableArgs;
403    int executableArgsLength;
404    // pointer and length into cmdline or a static string with the
405    // company name for PEP 514 lookup. Length can be -1 if the string
406    // is null terminated.
407    const wchar_t *company;
408    int companyLength;
409    // pointer and length into cmdline or a static string with the
410    // tag for PEP 514 lookup. Length can be -1 if the string is
411    // null terminated.
412    const wchar_t *tag;
413    int tagLength;
414    // if true, treats 'tag' as a non-PEP 514 filter
415    bool oldStyleTag;
416    // if true, ignores 'tag' when a high priority environment is found
417    // gh-92817: This is currently set when a tag is read from configuration or
418    // the environment, rather than the command line or a shebang line, and the
419    // only currently possible high priority environment is an active virtual
420    // environment
421    bool lowPriorityTag;
422    // if true, allow PEP 514 lookup to override 'executable'
423    bool allowExecutableOverride;
424    // if true, allow a nearby pyvenv.cfg to locate the executable
425    bool allowPyvenvCfg;
426    // if true, allow defaults (env/py.ini) to clarify/override tags
427    bool allowDefaults;
428    // if true, prefer windowed (console-less) executable
429    bool windowed;
430    // if true, only list detected runtimes without launching
431    bool list;
432    // if true, only list detected runtimes with paths without launching
433    bool listPaths;
434    // if true, display help message before contiuning
435    bool help;
436    // if set, limits search to registry keys with the specified Company
437    // This is intended for debugging and testing only
438    const wchar_t *limitToCompany;
439    // dynamically allocated buffers to free later
440    struct _SearchInfoBuffer *_buffer;
441} SearchInfo;
442
443
444wchar_t *
445allocSearchInfoBuffer(SearchInfo *search, int wcharCount)
446{
447    struct _SearchInfoBuffer *buffer = (struct _SearchInfoBuffer*)malloc(
448        sizeof(struct _SearchInfoBuffer) +
449        wcharCount * sizeof(wchar_t)
450    );
451    if (!buffer) {
452        return NULL;
453    }
454    buffer->next = search->_buffer;
455    search->_buffer = buffer;
456    return buffer->buffer;
457}
458
459
460void
461freeSearchInfo(SearchInfo *search)
462{
463    struct _SearchInfoBuffer *b = search->_buffer;
464    search->_buffer = NULL;
465    while (b) {
466        struct _SearchInfoBuffer *nextB = b->next;
467        free((void *)b);
468        b = nextB;
469    }
470}
471
472
473void
474_debugStringAndLength(const wchar_t *s, int len, const wchar_t *name)
475{
476    if (!s) {
477        debug(L"%s: (null)\n", name);
478    } else if (len == 0) {
479        debug(L"%s: (empty)\n", name);
480    } else if (len < 0) {
481        debug(L"%s: %s\n", name, s);
482    } else {
483        debug(L"%s: %.*ls\n", name, len, s);
484    }
485}
486
487
488void
489dumpSearchInfo(SearchInfo *search)
490{
491    if (!log_fp) {
492        return;
493    }
494
495#define DEBUGNAME(s) L"SearchInfo." ## s
496#define DEBUG(s) debug(DEBUGNAME(#s) L": %s\n", (search->s) ? (search->s) : L"(null)")
497#define DEBUG_2(s, sl) _debugStringAndLength((search->s), (search->sl), DEBUGNAME(#s))
498#define DEBUG_BOOL(s) debug(DEBUGNAME(#s) L": %s\n", (search->s) ? L"True" : L"False")
499    DEBUG(originalCmdLine);
500    DEBUG(restOfCmdLine);
501    DEBUG(executablePath);
502    DEBUG_2(scriptFile, scriptFileLength);
503    DEBUG_2(executable, executableLength);
504    DEBUG_2(executableArgs, executableArgsLength);
505    DEBUG_2(company, companyLength);
506    DEBUG_2(tag, tagLength);
507    DEBUG_BOOL(oldStyleTag);
508    DEBUG_BOOL(lowPriorityTag);
509    DEBUG_BOOL(allowDefaults);
510    DEBUG_BOOL(allowExecutableOverride);
511    DEBUG_BOOL(windowed);
512    DEBUG_BOOL(list);
513    DEBUG_BOOL(listPaths);
514    DEBUG_BOOL(help);
515    DEBUG(limitToCompany);
516#undef DEBUG_BOOL
517#undef DEBUG_2
518#undef DEBUG
519#undef DEBUGNAME
520}
521
522
523int
524findArgv0Length(const wchar_t *buffer, int bufferLength)
525{
526    // Note: this implements semantics that are only valid for argv0.
527    // Specifically, there is no escaping of quotes, and quotes within
528    // the argument have no effect. A quoted argv0 must start and end
529    // with a double quote character; otherwise, it ends at the first
530    // ' ' or '\t'.
531    int quoted = buffer[0] == L'"';
532    for (int i = 1; bufferLength < 0 || i < bufferLength; ++i) {
533        switch (buffer[i]) {
534        case L'\0':
535            return i;
536        case L' ':
537        case L'\t':
538            if (!quoted) {
539                return i;
540            }
541            break;
542        case L'"':
543            if (quoted) {
544                return i + 1;
545            }
546            break;
547        }
548    }
549    return bufferLength;
550}
551
552
553const wchar_t *
554findArgv0End(const wchar_t *buffer, int bufferLength)
555{
556    return &buffer[findArgv0Length(buffer, bufferLength)];
557}
558
559
560/******************************************************************************\
561 ***                          COMMAND-LINE PARSING                          ***
562\******************************************************************************/
563
564
565int
566parseCommandLine(SearchInfo *search)
567{
568    if (!search || !search->originalCmdLine) {
569        return RC_NO_COMMANDLINE;
570    }
571
572    const wchar_t *argv0End = findArgv0End(search->originalCmdLine, -1);
573    const wchar_t *tail = argv0End; // will be start of the executable name
574    const wchar_t *end = argv0End;  // will be end of the executable name
575    search->restOfCmdLine = argv0End;   // will be first space after argv0
576    while (--tail != search->originalCmdLine) {
577        if (*tail == L'"' && end == argv0End) {
578            // Move the "end" up to the quote, so we also allow moving for
579            // a period later on.
580            end = argv0End = tail;
581        } else if (*tail == L'.' && end == argv0End) {
582            end = tail;
583        } else if (*tail == L'\\' || *tail == L'/') {
584            ++tail;
585            break;
586        }
587    }
588    if (tail == search->originalCmdLine && tail[0] == L'"') {
589        ++tail;
590    }
591    // Without special cases, we can now fill in the search struct
592    int tailLen = (int)(end ? (end - tail) : wcsnlen_s(tail, MAXLEN));
593    search->executableLength = -1;
594
595    // Our special cases are as follows
596#define MATCHES(s) (0 == _comparePath(tail, tailLen, (s), -1))
597#define STARTSWITH(s) _startsWith(tail, tailLen, (s), -1)
598    if (MATCHES(L"py")) {
599        search->executable = L"python.exe";
600        search->allowExecutableOverride = true;
601        search->allowDefaults = true;
602    } else if (MATCHES(L"pyw")) {
603        search->executable = L"pythonw.exe";
604        search->allowExecutableOverride = true;
605        search->allowDefaults = true;
606        search->windowed = true;
607    } else if (MATCHES(L"py_d")) {
608        search->executable = L"python_d.exe";
609        search->allowExecutableOverride = true;
610        search->allowDefaults = true;
611    } else if (MATCHES(L"pyw_d")) {
612        search->executable = L"pythonw_d.exe";
613        search->allowExecutableOverride = true;
614        search->allowDefaults = true;
615        search->windowed = true;
616    } else if (STARTSWITH(L"python3")) {
617        search->executable = L"python.exe";
618        search->tag = &tail[6];
619        search->tagLength = tailLen - 6;
620        search->allowExecutableOverride = true;
621        search->oldStyleTag = true;
622        search->allowPyvenvCfg = true;
623    } else if (STARTSWITH(L"pythonw3")) {
624        search->executable = L"pythonw.exe";
625        search->tag = &tail[7];
626        search->tagLength = tailLen - 7;
627        search->allowExecutableOverride = true;
628        search->oldStyleTag = true;
629        search->allowPyvenvCfg = true;
630        search->windowed = true;
631    } else {
632        search->executable = tail;
633        search->executableLength = tailLen;
634        search->allowPyvenvCfg = true;
635    }
636#undef STARTSWITH
637#undef MATCHES
638
639    // First argument might be one of our options. If so, consume it,
640    // update flags and then set restOfCmdLine.
641    const wchar_t *arg = search->restOfCmdLine;
642    while(*arg && isspace(*arg)) { ++arg; }
643#define MATCHES(s) (0 == _compareArgument(arg, argLen, (s), -1))
644#define STARTSWITH(s) _startsWithArgument(arg, argLen, (s), -1)
645    if (*arg && *arg == L'-' && *++arg) {
646        tail = arg;
647        while (*tail && !isspace(*tail)) { ++tail; }
648        int argLen = (int)(tail - arg);
649        if (argLen > 0) {
650            if (STARTSWITH(L"2") || STARTSWITH(L"3")) {
651                // All arguments starting with 2 or 3 are assumed to be version tags
652                search->tag = arg;
653                search->tagLength = argLen;
654                search->oldStyleTag = true;
655                search->restOfCmdLine = tail;
656            } else if (STARTSWITH(L"V:") || STARTSWITH(L"-version:")) {
657                // Arguments starting with 'V:' specify company and/or tag
658                const wchar_t *argStart = wcschr(arg, L':') + 1;
659                const wchar_t *tagStart = wcschr(argStart, L'/') ;
660                if (tagStart) {
661                    search->company = argStart;
662                    search->companyLength = (int)(tagStart - argStart);
663                    search->tag = tagStart + 1;
664                } else {
665                    search->tag = argStart;
666                }
667                search->tagLength = (int)(tail - search->tag);
668                search->allowDefaults = false;
669                search->restOfCmdLine = tail;
670            } else if (MATCHES(L"0") || MATCHES(L"-list")) {
671                search->list = true;
672                search->restOfCmdLine = tail;
673            } else if (MATCHES(L"0p") || MATCHES(L"-list-paths")) {
674                search->listPaths = true;
675                search->restOfCmdLine = tail;
676            } else if (MATCHES(L"h") || MATCHES(L"-help")) {
677                search->help = true;
678                // Do not update restOfCmdLine so that we trigger the help
679                // message from whichever interpreter we select
680            }
681        }
682    }
683#undef STARTSWITH
684#undef MATCHES
685
686    // Might have a script filename. If it looks like a filename, add
687    // it to the SearchInfo struct for later reference.
688    arg = search->restOfCmdLine;
689    while(*arg && isspace(*arg)) { ++arg; }
690    if (*arg && *arg != L'-') {
691        search->scriptFile = arg;
692        if (*arg == L'"') {
693            ++search->scriptFile;
694            while (*++arg && *arg != L'"') { }
695        } else {
696            while (*arg && !isspace(*arg)) { ++arg; }
697        }
698        search->scriptFileLength = (int)(arg - search->scriptFile);
699    }
700
701    return 0;
702}
703
704
705int
706_decodeShebang(SearchInfo *search, const char *buffer, int bufferLength, bool onlyUtf8, wchar_t **decoded, int *decodedLength)
707{
708    DWORD cp = CP_UTF8;
709    int wideLen = MultiByteToWideChar(cp, MB_ERR_INVALID_CHARS, buffer, bufferLength, NULL, 0);
710    if (!wideLen) {
711        cp = CP_ACP;
712        wideLen = MultiByteToWideChar(cp, MB_ERR_INVALID_CHARS, buffer, bufferLength, NULL, 0);
713        if (!wideLen) {
714            debug(L"# Failed to decode shebang line (0x%08X)\n", GetLastError());
715            return RC_BAD_VIRTUAL_PATH;
716        }
717    }
718    wchar_t *b = allocSearchInfoBuffer(search, wideLen + 1);
719    if (!b) {
720        return RC_NO_MEMORY;
721    }
722    wideLen = MultiByteToWideChar(cp, 0, buffer, bufferLength, b, wideLen + 1);
723    if (!wideLen) {
724        debug(L"# Failed to decode shebang line (0x%08X)\n", GetLastError());
725        return RC_BAD_VIRTUAL_PATH;
726    }
727    b[wideLen] = L'\0';
728    *decoded = b;
729    *decodedLength = wideLen;
730    return 0;
731}
732
733
734bool
735_shebangStartsWith(const wchar_t *buffer, int bufferLength, const wchar_t *prefix, const wchar_t **rest, int *firstArgumentLength)
736{
737    int prefixLength = (int)wcsnlen_s(prefix, MAXLEN);
738    if (bufferLength < prefixLength || !_startsWithArgument(buffer, bufferLength, prefix, prefixLength)) {
739        return false;
740    }
741    if (rest) {
742        *rest = &buffer[prefixLength];
743    }
744    if (firstArgumentLength) {
745        int i = prefixLength;
746        while (i < bufferLength && !isspace(buffer[i])) {
747            i += 1;
748        }
749        *firstArgumentLength = i - prefixLength;
750    }
751    return true;
752}
753
754
755int
756searchPath(SearchInfo *search, const wchar_t *shebang, int shebangLength)
757{
758    if (isEnvVarSet(L"PYLAUNCHER_NO_SEARCH_PATH")) {
759        return RC_NO_SHEBANG;
760    }
761
762    wchar_t *command;
763    int commandLength;
764    if (!_shebangStartsWith(shebang, shebangLength, L"/usr/bin/env ", &command, &commandLength)) {
765        return RC_NO_SHEBANG;
766    }
767
768    if (!commandLength || commandLength == MAXLEN) {
769        return RC_BAD_VIRTUAL_PATH;
770    }
771
772    int lastDot = commandLength;
773    while (lastDot > 0 && command[lastDot] != L'.') {
774        lastDot -= 1;
775    }
776    if (!lastDot) {
777        lastDot = commandLength;
778    }
779
780    wchar_t filename[MAXLEN];
781    if (wcsncpy_s(filename, MAXLEN, command, lastDot)) {
782        return RC_BAD_VIRTUAL_PATH;
783    }
784
785    const wchar_t *ext = L".exe";
786    // If the command already has an extension, we do not want to add it again
787    if (!lastDot || _comparePath(&filename[lastDot], -1, ext, -1)) {
788        if (wcscat_s(filename, MAXLEN, L".exe")) {
789            return RC_BAD_VIRTUAL_PATH;
790        }
791    }
792
793    wchar_t pathVariable[MAXLEN];
794    int n = GetEnvironmentVariableW(L"PATH", pathVariable, MAXLEN);
795    if (!n) {
796        if (GetLastError() == ERROR_ENVVAR_NOT_FOUND) {
797            return RC_NO_SHEBANG;
798        }
799        winerror(0, L"Failed to read PATH\n", filename);
800        return RC_INTERNAL_ERROR;
801    }
802
803    wchar_t buffer[MAXLEN];
804    n = SearchPathW(pathVariable, filename, NULL, MAXLEN, buffer, NULL);
805    if (!n) {
806        if (GetLastError() == ERROR_FILE_NOT_FOUND) {
807            debug(L"# Did not find %s on PATH\n", filename);
808            // If we didn't find it on PATH, let normal handling take over
809            return RC_NO_SHEBANG;
810        }
811        // Other errors should cause us to break
812        winerror(0, L"Failed to find %s on PATH\n", filename);
813        return RC_BAD_VIRTUAL_PATH;
814    }
815
816    // Check that we aren't going to call ourselves again
817    // If we are, pretend there was no shebang and let normal handling take over
818    if (GetModuleFileNameW(NULL, filename, MAXLEN) &&
819        0 == _comparePath(filename, -1, buffer, -1)) {
820        debug(L"# ignoring recursive shebang command\n");
821        return RC_RECURSIVE_SHEBANG;
822    }
823
824    wchar_t *buf = allocSearchInfoBuffer(search, n + 1);
825    if (!buf || wcscpy_s(buf, n + 1, buffer)) {
826        return RC_NO_MEMORY;
827    }
828
829    search->executablePath = buf;
830    search->executableArgs = &command[commandLength];
831    search->executableArgsLength = shebangLength - commandLength;
832    debug(L"# Found %s on PATH\n", buf);
833
834    return 0;
835}
836
837
838int
839_readIni(const wchar_t *section, const wchar_t *settingName, wchar_t *buffer, int bufferLength)
840{
841    wchar_t iniPath[MAXLEN];
842    int n;
843    if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, iniPath)) &&
844        join(iniPath, MAXLEN, L"py.ini")) {
845        debug(L"# Reading from %s for %s/%s\n", iniPath, section, settingName);
846        n = GetPrivateProfileStringW(section, settingName, NULL, buffer, bufferLength, iniPath);
847        if (n) {
848            debug(L"# Found %s in %s\n", settingName, iniPath);
849            return n;
850        } else if (GetLastError() == ERROR_FILE_NOT_FOUND) {
851            debug(L"# Did not find file %s\n", iniPath);
852        } else {
853            winerror(0, L"Failed to read from %s\n", iniPath);
854        }
855    }
856    if (GetModuleFileNameW(NULL, iniPath, MAXLEN) &&
857        SUCCEEDED(PathCchRemoveFileSpec(iniPath, MAXLEN)) &&
858        join(iniPath, MAXLEN, L"py.ini")) {
859        debug(L"# Reading from %s for %s/%s\n", iniPath, section, settingName);
860        n = GetPrivateProfileStringW(section, settingName, NULL, buffer, MAXLEN, iniPath);
861        if (n) {
862            debug(L"# Found %s in %s\n", settingName, iniPath);
863            return n;
864        } else if (GetLastError() == ERROR_FILE_NOT_FOUND) {
865            debug(L"# Did not find file %s\n", iniPath);
866        } else {
867            winerror(0, L"Failed to read from %s\n", iniPath);
868        }
869    }
870    return 0;
871}
872
873
874bool
875_findCommand(SearchInfo *search, const wchar_t *command, int commandLength)
876{
877    wchar_t commandBuffer[MAXLEN];
878    wchar_t buffer[MAXLEN];
879    wcsncpy_s(commandBuffer, MAXLEN, command, commandLength);
880    int n = _readIni(L"commands", commandBuffer, buffer, MAXLEN);
881    if (!n) {
882        return false;
883    }
884    wchar_t *path = allocSearchInfoBuffer(search, n + 1);
885    if (!path) {
886        return false;
887    }
888    wcscpy_s(path, n + 1, buffer);
889    search->executablePath = path;
890    return true;
891}
892
893
894int
895_useShebangAsExecutable(SearchInfo *search, const wchar_t *shebang, int shebangLength)
896{
897    wchar_t buffer[MAXLEN];
898    wchar_t script[MAXLEN];
899    wchar_t command[MAXLEN];
900
901    int commandLength = 0;
902    int inQuote = 0;
903
904    if (!shebang || !shebangLength) {
905        return 0;
906    }
907
908    wchar_t *pC = command;
909    for (int i = 0; i < shebangLength; ++i) {
910        wchar_t c = shebang[i];
911        if (isspace(c) && !inQuote) {
912            commandLength = i;
913            break;
914        } else if (c == L'"') {
915            inQuote = !inQuote;
916        } else if (c == L'/' || c == L'\\') {
917            *pC++ = L'\\';
918        } else {
919            *pC++ = c;
920        }
921    }
922    *pC = L'\0';
923
924    if (!GetCurrentDirectoryW(MAXLEN, buffer) ||
925        wcsncpy_s(script, MAXLEN, search->scriptFile, search->scriptFileLength) ||
926        FAILED(PathCchCombineEx(buffer, MAXLEN, buffer, script,
927                                PATHCCH_ALLOW_LONG_PATHS)) ||
928        FAILED(PathCchRemoveFileSpec(buffer, MAXLEN)) ||
929        FAILED(PathCchCombineEx(buffer, MAXLEN, buffer, command,
930                                PATHCCH_ALLOW_LONG_PATHS))
931    ) {
932        return RC_NO_MEMORY;
933    }
934
935    int n = (int)wcsnlen(buffer, MAXLEN);
936    wchar_t *path = allocSearchInfoBuffer(search, n + 1);
937    if (!path) {
938        return RC_NO_MEMORY;
939    }
940    wcscpy_s(path, n + 1, buffer);
941    search->executablePath = path;
942    if (commandLength) {
943        search->executableArgs = &shebang[commandLength];
944        search->executableArgsLength = shebangLength - commandLength;
945    }
946    return 0;
947}
948
949
950int
951checkShebang(SearchInfo *search)
952{
953    // Do not check shebang if a tag was provided or if no script file
954    // was found on the command line.
955    if (search->tag || !search->scriptFile) {
956        return 0;
957    }
958
959    if (search->scriptFileLength < 0) {
960        search->scriptFileLength = (int)wcsnlen_s(search->scriptFile, MAXLEN);
961    }
962
963    wchar_t *scriptFile = (wchar_t*)malloc(sizeof(wchar_t) * (search->scriptFileLength + 1));
964    if (!scriptFile) {
965        return RC_NO_MEMORY;
966    }
967
968    wcsncpy_s(scriptFile, search->scriptFileLength + 1,
969              search->scriptFile, search->scriptFileLength);
970
971    HANDLE hFile = CreateFileW(scriptFile, GENERIC_READ,
972        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
973        NULL, OPEN_EXISTING, 0, NULL);
974
975    if (hFile == INVALID_HANDLE_VALUE) {
976        debug(L"# Failed to open %s for shebang parsing (0x%08X)\n",
977              scriptFile, GetLastError());
978        free(scriptFile);
979        return 0;
980    }
981
982    DWORD bytesRead = 0;
983    char buffer[4096];
984    if (!ReadFile(hFile, buffer, sizeof(buffer), &bytesRead, NULL)) {
985        debug(L"# Failed to read %s for shebang parsing (0x%08X)\n",
986              scriptFile, GetLastError());
987        free(scriptFile);
988        return 0;
989    }
990
991    CloseHandle(hFile);
992    debug(L"# Read %d bytes from %s to find shebang line\n", bytesRead, scriptFile);
993    free(scriptFile);
994
995
996    char *b = buffer;
997    bool onlyUtf8 = false;
998    if (bytesRead > 3 && *b == 0xEF) {
999        if (*++b == 0xBB && *++b == 0xBF) {
1000            // Allow a UTF-8 BOM
1001            ++b;
1002            bytesRead -= 3;
1003            onlyUtf8 = true;
1004        } else {
1005            debug(L"# Invalid BOM in shebang line");
1006            return 0;
1007        }
1008    }
1009    if (bytesRead <= 2 || b[0] != '#' || b[1] != '!') {
1010        // No shebang (#!) at start of line
1011        debug(L"# No valid shebang line");
1012        return 0;
1013    }
1014    ++b;
1015    --bytesRead;
1016    while (--bytesRead > 0 && isspace(*++b)) { }
1017    char *start = b;
1018    while (--bytesRead > 0 && *++b != '\r' && *b != '\n') { }
1019    wchar_t *shebang;
1020    int shebangLength;
1021    // We add 1 when bytesRead==0, as in that case we hit EOF and b points
1022    // to the last character in the file, not the newline
1023    int exitCode = _decodeShebang(search, start, (int)(b - start + (bytesRead == 0)), onlyUtf8, &shebang, &shebangLength);
1024    if (exitCode) {
1025        return exitCode;
1026    }
1027    debug(L"Shebang: %s\n", shebang);
1028
1029    // Handle shebangs that we should search PATH for
1030    exitCode = searchPath(search, shebang, shebangLength);
1031    if (exitCode != RC_NO_SHEBANG) {
1032        return exitCode;
1033    }
1034
1035    // Handle some known, case-sensitive shebangs
1036    const wchar_t *command;
1037    int commandLength;
1038    // Each template must end with "python"
1039    static const wchar_t *shebangTemplates[] = {
1040        L"/usr/bin/env python",
1041        L"/usr/bin/python",
1042        L"/usr/local/bin/python",
1043        L"python",
1044        NULL
1045    };
1046
1047    for (const wchar_t **tmpl = shebangTemplates; *tmpl; ++tmpl) {
1048        // Just to make sure we don't mess this up in the future
1049        assert(0 == wcscmp(L"python", (*tmpl) + wcslen(*tmpl) - 6));
1050
1051        if (_shebangStartsWith(shebang, shebangLength, *tmpl, &command, &commandLength)) {
1052            // Search for "python{command}" overrides. All templates end with
1053            // "python", so we prepend it by jumping back 6 characters
1054            if (_findCommand(search, &command[-6], commandLength + 6)) {
1055                search->executableArgs = &command[commandLength];
1056                search->executableArgsLength = shebangLength - commandLength;
1057                debug(L"# Treating shebang command '%.*s' as %s\n",
1058                    commandLength + 6, &command[-6], search->executablePath);
1059                return 0;
1060            }
1061
1062            search->tag = command;
1063            search->tagLength = commandLength;
1064            // If we had 'python3.12.exe' then we want to strip the suffix
1065            // off of the tag
1066            if (search->tagLength > 4) {
1067                const wchar_t *suffix = &search->tag[search->tagLength - 4];
1068                if (0 == _comparePath(suffix, 4, L".exe", -1)) {
1069                    search->tagLength -= 4;
1070                }
1071            }
1072            // If we had 'python3_d' then we want to strip the '_d' (any
1073            // '.exe' is already gone)
1074            if (search->tagLength > 2) {
1075                const wchar_t *suffix = &search->tag[search->tagLength - 2];
1076                if (0 == _comparePath(suffix, 2, L"_d", -1)) {
1077                    search->tagLength -= 2;
1078                }
1079            }
1080            search->oldStyleTag = true;
1081            search->executableArgs = &command[commandLength];
1082            search->executableArgsLength = shebangLength - commandLength;
1083            if (search->tag && search->tagLength) {
1084                debug(L"# Treating shebang command '%.*s' as 'py -%.*s'\n",
1085                    commandLength, command, search->tagLength, search->tag);
1086            } else {
1087                debug(L"# Treating shebang command '%.*s' as 'py'\n",
1088                    commandLength, command);
1089            }
1090            return 0;
1091        }
1092    }
1093
1094    // Unrecognised executables are first tried as command aliases
1095    commandLength = 0;
1096    while (commandLength < shebangLength && !isspace(shebang[commandLength])) {
1097        commandLength += 1;
1098    }
1099    if (_findCommand(search, shebang, commandLength)) {
1100        search->executableArgs = &shebang[commandLength];
1101        search->executableArgsLength = shebangLength - commandLength;
1102        debug(L"# Treating shebang command '%.*s' as %s\n",
1103            commandLength, shebang, search->executablePath);
1104        return 0;
1105    }
1106
1107    // Unrecognised commands are joined to the script's directory and treated
1108    // as the executable path
1109    return _useShebangAsExecutable(search, shebang, shebangLength);
1110}
1111
1112
1113int
1114checkDefaults(SearchInfo *search)
1115{
1116    if (!search->allowDefaults) {
1117        return 0;
1118    }
1119
1120    // Only resolve old-style (or absent) tags to defaults
1121    if (search->tag && search->tagLength && !search->oldStyleTag) {
1122        return 0;
1123    }
1124
1125    // If tag is only a major version number, expand it from the environment
1126    // or an ini file
1127    const wchar_t *iniSettingName = NULL;
1128    const wchar_t *envSettingName = NULL;
1129    if (!search->tag || !search->tagLength) {
1130        iniSettingName = L"python";
1131        envSettingName = L"py_python";
1132    } else if (0 == wcsncmp(search->tag, L"3", search->tagLength)) {
1133        iniSettingName = L"python3";
1134        envSettingName = L"py_python3";
1135    } else if (0 == wcsncmp(search->tag, L"2", search->tagLength)) {
1136        iniSettingName = L"python2";
1137        envSettingName = L"py_python2";
1138    } else {
1139        debug(L"# Cannot select defaults for tag '%.*s'\n", search->tagLength, search->tag);
1140        return 0;
1141    }
1142
1143    // First, try to read an environment variable
1144    wchar_t buffer[MAXLEN];
1145    int n = GetEnvironmentVariableW(envSettingName, buffer, MAXLEN);
1146
1147    // If none found, check in our two .ini files instead
1148    if (!n) {
1149        n = _readIni(L"defaults", iniSettingName, buffer, MAXLEN);
1150    }
1151
1152    if (n) {
1153        wchar_t *tag = allocSearchInfoBuffer(search, n + 1);
1154        if (!tag) {
1155            return RC_NO_MEMORY;
1156        }
1157        wcscpy_s(tag, n + 1, buffer);
1158        wchar_t *slash = wcschr(tag, L'/');
1159        if (!slash) {
1160            search->tag = tag;
1161            search->tagLength = n;
1162            search->oldStyleTag = true;
1163        } else {
1164            search->company = tag;
1165            search->companyLength = (int)(slash - tag);
1166            search->tag = slash + 1;
1167            search->tagLength = n - (search->companyLength + 1);
1168            search->oldStyleTag = false;
1169        }
1170        // gh-92817: allow a high priority env to be selected even if it
1171        // doesn't match the tag
1172        search->lowPriorityTag = true;
1173    }
1174
1175    return 0;
1176}
1177
1178/******************************************************************************\
1179 ***                          ENVIRONMENT SEARCH                            ***
1180\******************************************************************************/
1181
1182typedef struct EnvironmentInfo {
1183    /* We use a binary tree and sort on insert */
1184    struct EnvironmentInfo *prev;
1185    struct EnvironmentInfo *next;
1186    /* parent is only used when constructing */
1187    struct EnvironmentInfo *parent;
1188    const wchar_t *company;
1189    const wchar_t *tag;
1190    int internalSortKey;
1191    const wchar_t *installDir;
1192    const wchar_t *executablePath;
1193    const wchar_t *executableArgs;
1194    const wchar_t *architecture;
1195    const wchar_t *displayName;
1196    bool highPriority;
1197} EnvironmentInfo;
1198
1199
1200int
1201copyWstr(const wchar_t **dest, const wchar_t *src)
1202{
1203    if (!dest) {
1204        return RC_NO_MEMORY;
1205    }
1206    if (!src) {
1207        *dest = NULL;
1208        return 0;
1209    }
1210    size_t n = wcsnlen_s(src, MAXLEN - 1) + 1;
1211    wchar_t *buffer = (wchar_t*)malloc(n * sizeof(wchar_t));
1212    if (!buffer) {
1213        return RC_NO_MEMORY;
1214    }
1215    wcsncpy_s(buffer, n, src, n - 1);
1216    *dest = (const wchar_t*)buffer;
1217    return 0;
1218}
1219
1220
1221EnvironmentInfo *
1222newEnvironmentInfo(const wchar_t *company, const wchar_t *tag)
1223{
1224    EnvironmentInfo *env = (EnvironmentInfo *)malloc(sizeof(EnvironmentInfo));
1225    if (!env) {
1226        return NULL;
1227    }
1228    memset(env, 0, sizeof(EnvironmentInfo));
1229    int exitCode = copyWstr(&env->company, company);
1230    if (exitCode) {
1231        free((void *)env);
1232        return NULL;
1233    }
1234    exitCode = copyWstr(&env->tag, tag);
1235    if (exitCode) {
1236        free((void *)env->company);
1237        free((void *)env);
1238        return NULL;
1239    }
1240    return env;
1241}
1242
1243
1244void
1245freeEnvironmentInfo(EnvironmentInfo *env)
1246{
1247    if (env) {
1248        free((void *)env->company);
1249        free((void *)env->tag);
1250        free((void *)env->installDir);
1251        free((void *)env->executablePath);
1252        free((void *)env->executableArgs);
1253        free((void *)env->displayName);
1254        freeEnvironmentInfo(env->prev);
1255        env->prev = NULL;
1256        freeEnvironmentInfo(env->next);
1257        env->next = NULL;
1258        free((void *)env);
1259    }
1260}
1261
1262
1263/* Specific string comparisons for sorting the tree */
1264
1265int
1266_compareCompany(const wchar_t *x, const wchar_t *y)
1267{
1268    if (!x && !y) {
1269        return 0;
1270    } else if (!x) {
1271        return -1;
1272    } else if (!y) {
1273        return 1;
1274    }
1275
1276    bool coreX = 0 == _compare(x, -1, L"PythonCore", -1);
1277    bool coreY = 0 == _compare(y, -1, L"PythonCore", -1);
1278    if (coreX) {
1279        return coreY ? 0 : -1;
1280    } else if (coreY) {
1281        return 1;
1282    }
1283    return _compare(x, -1, y, -1);
1284}
1285
1286
1287int
1288_compareTag(const wchar_t *x, const wchar_t *y)
1289{
1290    if (!x && !y) {
1291        return 0;
1292    } else if (!x) {
1293        return -1;
1294    } else if (!y) {
1295        return 1;
1296    }
1297
1298    // Compare up to the first dash. If not equal, that's our sort order
1299    const wchar_t *xDash = wcschr(x, L'-');
1300    const wchar_t *yDash = wcschr(y, L'-');
1301    int xToDash = xDash ? (int)(xDash - x) : -1;
1302    int yToDash = yDash ? (int)(yDash - y) : -1;
1303    int r = _compare(x, xToDash, y, yToDash);
1304    if (r) {
1305        return r;
1306    }
1307    // If we're equal up to the first dash, we want to sort one with
1308    // no dash *after* one with a dash. Otherwise, a reversed compare.
1309    // This works out because environments are sorted in descending tag
1310    // order, so that higher versions (probably) come first.
1311    // For PythonCore, our "X.Y" structure ensures that higher versions
1312    // come first. Everyone else will just have to deal with it.
1313    if (xDash && yDash) {
1314        return _compare(yDash, -1, xDash, -1);
1315    } else if (xDash) {
1316        return -1;
1317    } else if (yDash) {
1318        return 1;
1319    }
1320    return 0;
1321}
1322
1323
1324int
1325addEnvironmentInfo(EnvironmentInfo **root, EnvironmentInfo* parent, EnvironmentInfo *node)
1326{
1327    EnvironmentInfo *r = *root;
1328    if (!r) {
1329        *root = node;
1330        node->parent = parent;
1331        return 0;
1332    }
1333    // Sort by company name
1334    switch (_compareCompany(node->company, r->company)) {
1335    case -1:
1336        return addEnvironmentInfo(&r->prev, r, node);
1337    case 1:
1338        return addEnvironmentInfo(&r->next, r, node);
1339    case 0:
1340        break;
1341    }
1342    // Then by tag (descending)
1343    switch (_compareTag(node->tag, r->tag)) {
1344    case -1:
1345        return addEnvironmentInfo(&r->next, r, node);
1346    case 1:
1347        return addEnvironmentInfo(&r->prev, r, node);
1348    case 0:
1349        break;
1350    }
1351    // Then keep the one with the lowest internal sort key
1352    if (node->internalSortKey < r->internalSortKey) {
1353        // Replace the current node
1354        node->parent = r->parent;
1355        if (node->parent) {
1356            if (node->parent->prev == r) {
1357                node->parent->prev = node;
1358            } else if (node->parent->next == r) {
1359                node->parent->next = node;
1360            } else {
1361                debug(L"# Inconsistent parent value in tree\n");
1362                freeEnvironmentInfo(node);
1363                return RC_INTERNAL_ERROR;
1364            }
1365        } else {
1366            // If node has no parent, then it is the root.
1367            *root = node;
1368        }
1369
1370        node->next = r->next;
1371        node->prev = r->prev;
1372
1373        debug(L"# replaced %s/%s/%i in tree\n", node->company, node->tag, node->internalSortKey);
1374        freeEnvironmentInfo(r);
1375    } else {
1376        debug(L"# not adding %s/%s/%i to tree\n", node->company, node->tag, node->internalSortKey);
1377        return RC_DUPLICATE_ITEM;
1378    }
1379    return 0;
1380}
1381
1382
1383/******************************************************************************\
1384 ***                            REGISTRY SEARCH                             ***
1385\******************************************************************************/
1386
1387
1388int
1389_registryReadString(const wchar_t **dest, HKEY root, const wchar_t *subkey, const wchar_t *value)
1390{
1391    // Note that this is bytes (hence 'cb'), not characters ('cch')
1392    DWORD cbData = 0;
1393    DWORD flags = RRF_RT_REG_SZ | RRF_RT_REG_EXPAND_SZ;
1394
1395    if (ERROR_SUCCESS != RegGetValueW(root, subkey, value, flags, NULL, NULL, &cbData)) {
1396        return 0;
1397    }
1398
1399    wchar_t *buffer = (wchar_t*)malloc(cbData);
1400    if (!buffer) {
1401        return RC_NO_MEMORY;
1402    }
1403
1404    if (ERROR_SUCCESS == RegGetValueW(root, subkey, value, flags, NULL, buffer, &cbData)) {
1405        *dest = buffer;
1406    } else {
1407        free((void *)buffer);
1408    }
1409    return 0;
1410}
1411
1412
1413int
1414_combineWithInstallDir(const wchar_t **dest, const wchar_t *installDir, const wchar_t *fragment, int fragmentLength)
1415{
1416    wchar_t buffer[MAXLEN];
1417    wchar_t fragmentBuffer[MAXLEN];
1418    if (wcsncpy_s(fragmentBuffer, MAXLEN, fragment, fragmentLength)) {
1419        return RC_NO_MEMORY;
1420    }
1421
1422    if (FAILED(PathCchCombineEx(buffer, MAXLEN, installDir, fragmentBuffer, PATHCCH_ALLOW_LONG_PATHS))) {
1423        return RC_NO_MEMORY;
1424    }
1425
1426    return copyWstr(dest, buffer);
1427}
1428
1429
1430bool
1431_isLegacyVersion(EnvironmentInfo *env)
1432{
1433    // Check if backwards-compatibility is required.
1434    // Specifically PythonCore versions 2.X and 3.0 - 3.5 do not implement PEP 514.
1435    if (0 != _compare(env->company, -1, L"PythonCore", -1)) {
1436        return false;
1437    }
1438
1439    int versionMajor, versionMinor;
1440    int n = swscanf_s(env->tag, L"%d.%d", &versionMajor, &versionMinor);
1441    if (n != 2) {
1442        debug(L"# %s/%s has an invalid version tag\n", env->company, env->tag);
1443        return false;
1444    }
1445
1446    return versionMajor == 2
1447        || (versionMajor == 3 && versionMinor >= 0 && versionMinor <= 5);
1448}
1449
1450int
1451_registryReadLegacyEnvironment(const SearchInfo *search, HKEY root, EnvironmentInfo *env, const wchar_t *fallbackArch)
1452{
1453    // Backwards-compatibility for PythonCore versions which do not implement PEP 514.
1454    int exitCode = _combineWithInstallDir(
1455        &env->executablePath,
1456        env->installDir,
1457        search->executable,
1458        search->executableLength
1459    );
1460    if (exitCode) {
1461        return exitCode;
1462    }
1463
1464    if (search->windowed) {
1465        exitCode = _registryReadString(&env->executableArgs, root, L"InstallPath", L"WindowedExecutableArguments");
1466    }
1467    else {
1468        exitCode = _registryReadString(&env->executableArgs, root, L"InstallPath", L"ExecutableArguments");
1469    }
1470    if (exitCode) {
1471        return exitCode;
1472    }
1473
1474    if (fallbackArch) {
1475        copyWstr(&env->architecture, fallbackArch);
1476    } else {
1477        DWORD binaryType;
1478        BOOL success = GetBinaryTypeW(env->executablePath, &binaryType);
1479        if (!success) {
1480            return RC_NO_PYTHON;
1481        }
1482
1483        switch (binaryType) {
1484        case SCS_32BIT_BINARY:
1485            copyWstr(&env->architecture, L"32bit");
1486            break;
1487        case SCS_64BIT_BINARY:
1488            copyWstr(&env->architecture, L"64bit");
1489            break;
1490        default:
1491            return RC_NO_PYTHON;
1492        }
1493    }
1494
1495    if (0 == _compare(env->architecture, -1, L"32bit", -1)) {
1496        size_t tagLength = wcslen(env->tag);
1497        if (tagLength <= 3 || 0 != _compare(&env->tag[tagLength - 3], 3, L"-32", 3)) {
1498            const wchar_t *rawTag = env->tag;
1499            wchar_t *realTag = (wchar_t*) malloc(sizeof(wchar_t) * (tagLength + 4));
1500            if (!realTag) {
1501                return RC_NO_MEMORY;
1502            }
1503
1504            int count = swprintf_s(realTag, tagLength + 4, L"%s-32", env->tag);
1505            if (count == -1) {
1506                free(realTag);
1507                return RC_INTERNAL_ERROR;
1508            }
1509
1510            env->tag = realTag;
1511            free((void*)rawTag);
1512        }
1513    }
1514
1515    wchar_t buffer[MAXLEN];
1516    if (swprintf_s(buffer, MAXLEN, L"Python %s", env->tag)) {
1517        copyWstr(&env->displayName, buffer);
1518    }
1519
1520    return 0;
1521}
1522
1523
1524int
1525_registryReadEnvironment(const SearchInfo *search, HKEY root, EnvironmentInfo *env, const wchar_t *fallbackArch)
1526{
1527    int exitCode = _registryReadString(&env->installDir, root, L"InstallPath", NULL);
1528    if (exitCode) {
1529        return exitCode;
1530    }
1531    if (!env->installDir) {
1532        return RC_NO_PYTHON;
1533    }
1534
1535    if (_isLegacyVersion(env)) {
1536        return _registryReadLegacyEnvironment(search, root, env, fallbackArch);
1537    }
1538
1539    // If pythonw.exe requested, check specific value
1540    if (search->windowed) {
1541        exitCode = _registryReadString(&env->executablePath, root, L"InstallPath", L"WindowedExecutablePath");
1542        if (!exitCode && env->executablePath) {
1543            exitCode = _registryReadString(&env->executableArgs, root, L"InstallPath", L"WindowedExecutableArguments");
1544        }
1545    }
1546    if (exitCode) {
1547        return exitCode;
1548    }
1549
1550    // Missing windowed path or non-windowed request means we use ExecutablePath
1551    if (!env->executablePath) {
1552        exitCode = _registryReadString(&env->executablePath, root, L"InstallPath", L"ExecutablePath");
1553        if (!exitCode && env->executablePath) {
1554            exitCode = _registryReadString(&env->executableArgs, root, L"InstallPath", L"ExecutableArguments");
1555        }
1556    }
1557    if (exitCode) {
1558        return exitCode;
1559    }
1560
1561    if (!env->executablePath) {
1562        debug(L"# %s/%s has no executable path\n", env->company, env->tag);
1563        return RC_NO_PYTHON;
1564    }
1565
1566    exitCode = _registryReadString(&env->architecture, root, NULL, L"SysArchitecture");
1567    if (exitCode) {
1568        return exitCode;
1569    }
1570
1571    exitCode = _registryReadString(&env->displayName, root, NULL, L"DisplayName");
1572    if (exitCode) {
1573        return exitCode;
1574    }
1575
1576    return 0;
1577}
1578
1579int
1580_registrySearchTags(const SearchInfo *search, EnvironmentInfo **result, HKEY root, int sortKey, const wchar_t *company, const wchar_t *fallbackArch)
1581{
1582    wchar_t buffer[256];
1583    int err = 0;
1584    int exitCode = 0;
1585    for (int i = 0; exitCode == 0; ++i) {
1586        DWORD cchBuffer = sizeof(buffer) / sizeof(buffer[0]);
1587        err = RegEnumKeyExW(root, i, buffer, &cchBuffer, NULL, NULL, NULL, NULL);
1588        if (err) {
1589            if (err != ERROR_NO_MORE_ITEMS) {
1590                winerror(0, L"Failed to read installs (tags) from the registry");
1591            }
1592            break;
1593        }
1594        HKEY subkey;
1595        if (ERROR_SUCCESS == RegOpenKeyExW(root, buffer, 0, KEY_READ, &subkey)) {
1596            EnvironmentInfo *env = newEnvironmentInfo(company, buffer);
1597            env->internalSortKey = sortKey;
1598            exitCode = _registryReadEnvironment(search, subkey, env, fallbackArch);
1599            RegCloseKey(subkey);
1600            if (exitCode == RC_NO_PYTHON) {
1601                freeEnvironmentInfo(env);
1602                exitCode = 0;
1603            } else if (!exitCode) {
1604                exitCode = addEnvironmentInfo(result, NULL, env);
1605                if (exitCode) {
1606                    freeEnvironmentInfo(env);
1607                    if (exitCode == RC_DUPLICATE_ITEM) {
1608                        exitCode = 0;
1609                    }
1610                }
1611            }
1612        }
1613    }
1614    return exitCode;
1615}
1616
1617
1618int
1619registrySearch(const SearchInfo *search, EnvironmentInfo **result, HKEY root, int sortKey, const wchar_t *fallbackArch)
1620{
1621    wchar_t buffer[256];
1622    int err = 0;
1623    int exitCode = 0;
1624    for (int i = 0; exitCode == 0; ++i) {
1625        DWORD cchBuffer = sizeof(buffer) / sizeof(buffer[0]);
1626        err = RegEnumKeyExW(root, i, buffer, &cchBuffer, NULL, NULL, NULL, NULL);
1627        if (err) {
1628            if (err != ERROR_NO_MORE_ITEMS) {
1629                winerror(0, L"Failed to read distributors (company) from the registry");
1630            }
1631            break;
1632        }
1633        if (search->limitToCompany && 0 != _compare(search->limitToCompany, -1, buffer, cchBuffer)) {
1634            debug(L"# Skipping %s due to PYLAUNCHER_LIMIT_TO_COMPANY\n", buffer);
1635            continue;
1636        }
1637        HKEY subkey;
1638        if (ERROR_SUCCESS == RegOpenKeyExW(root, buffer, 0, KEY_READ, &subkey)) {
1639            exitCode = _registrySearchTags(search, result, subkey, sortKey, buffer, fallbackArch);
1640            RegCloseKey(subkey);
1641        }
1642    }
1643    return exitCode;
1644}
1645
1646
1647/******************************************************************************\
1648 ***                            APP PACKAGE SEARCH                          ***
1649\******************************************************************************/
1650
1651int
1652appxSearch(const SearchInfo *search, EnvironmentInfo **result, const wchar_t *packageFamilyName, const wchar_t *tag, int sortKey)
1653{
1654    wchar_t realTag[32];
1655    wchar_t buffer[MAXLEN];
1656    const wchar_t *exeName = search->executable;
1657    if (!exeName || search->allowExecutableOverride) {
1658        exeName = search->windowed ? L"pythonw.exe" : L"python.exe";
1659    }
1660
1661    if (FAILED(SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, buffer)) ||
1662        !join(buffer, MAXLEN, L"Microsoft\\WindowsApps") ||
1663        !join(buffer, MAXLEN, packageFamilyName) ||
1664        !join(buffer, MAXLEN, exeName)) {
1665        return RC_INTERNAL_ERROR;
1666    }
1667
1668    if (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(buffer)) {
1669        return RC_NO_PYTHON;
1670    }
1671
1672    // Assume packages are native architecture, which means we need to append
1673    // the '-arm64' on ARM64 host.
1674    wcscpy_s(realTag, 32, tag);
1675    if (isARM64Host()) {
1676        wcscat_s(realTag, 32, L"-arm64");
1677    }
1678
1679    EnvironmentInfo *env = newEnvironmentInfo(L"PythonCore", realTag);
1680    if (!env) {
1681        return RC_NO_MEMORY;
1682    }
1683    env->internalSortKey = sortKey;
1684    if (isAMD64Host()) {
1685        copyWstr(&env->architecture, L"64bit");
1686    } else if (isARM64Host()) {
1687        copyWstr(&env->architecture, L"ARM64");
1688    }
1689
1690    copyWstr(&env->executablePath, buffer);
1691
1692    if (swprintf_s(buffer, MAXLEN, L"Python %s (Store)", tag)) {
1693        copyWstr(&env->displayName, buffer);
1694    }
1695
1696    int exitCode = addEnvironmentInfo(result, NULL, env);
1697    if (exitCode) {
1698        freeEnvironmentInfo(env);
1699        if (exitCode == RC_DUPLICATE_ITEM) {
1700            exitCode = 0;
1701        }
1702    }
1703
1704
1705    return exitCode;
1706}
1707
1708
1709/******************************************************************************\
1710 ***                      OVERRIDDEN EXECUTABLE PATH                        ***
1711\******************************************************************************/
1712
1713
1714int
1715explicitOverrideSearch(const SearchInfo *search, EnvironmentInfo **result)
1716{
1717    if (!search->executablePath) {
1718        return 0;
1719    }
1720
1721    EnvironmentInfo *env = newEnvironmentInfo(NULL, NULL);
1722    if (!env) {
1723        return RC_NO_MEMORY;
1724    }
1725    env->internalSortKey = 10;
1726    int exitCode = copyWstr(&env->executablePath, search->executablePath);
1727    if (exitCode) {
1728        goto abort;
1729    }
1730    exitCode = copyWstr(&env->displayName, L"Explicit override");
1731    if (exitCode) {
1732        goto abort;
1733    }
1734    exitCode = addEnvironmentInfo(result, NULL, env);
1735    if (exitCode) {
1736        goto abort;
1737    }
1738    return 0;
1739
1740abort:
1741    freeEnvironmentInfo(env);
1742    if (exitCode == RC_DUPLICATE_ITEM) {
1743        exitCode = 0;
1744    }
1745    return exitCode;
1746}
1747
1748
1749/******************************************************************************\
1750 ***                   ACTIVE VIRTUAL ENVIRONMENT SEARCH                    ***
1751\******************************************************************************/
1752
1753int
1754virtualenvSearch(const SearchInfo *search, EnvironmentInfo **result)
1755{
1756    int exitCode = 0;
1757    EnvironmentInfo *env = NULL;
1758    wchar_t buffer[MAXLEN];
1759    int n = GetEnvironmentVariableW(L"VIRTUAL_ENV", buffer, MAXLEN);
1760    if (!n || !join(buffer, MAXLEN, L"Scripts") || !join(buffer, MAXLEN, search->executable)) {
1761        return 0;
1762    }
1763
1764    if (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(buffer)) {
1765        debug(L"Python executable %s missing from virtual env\n", buffer);
1766        return 0;
1767    }
1768
1769    env = newEnvironmentInfo(NULL, NULL);
1770    if (!env) {
1771        return RC_NO_MEMORY;
1772    }
1773    env->highPriority = true;
1774    env->internalSortKey = 20;
1775    exitCode = copyWstr(&env->displayName, L"Active venv");
1776    if (exitCode) {
1777        goto abort;
1778    }
1779    exitCode = copyWstr(&env->executablePath, buffer);
1780    if (exitCode) {
1781        goto abort;
1782    }
1783    exitCode = addEnvironmentInfo(result, NULL, env);
1784    if (exitCode) {
1785        goto abort;
1786    }
1787    return 0;
1788
1789abort:
1790    freeEnvironmentInfo(env);
1791    if (exitCode == RC_DUPLICATE_ITEM) {
1792        return 0;
1793    }
1794    return exitCode;
1795}
1796
1797/******************************************************************************\
1798 ***                           COLLECT ENVIRONMENTS                         ***
1799\******************************************************************************/
1800
1801
1802struct RegistrySearchInfo {
1803    // Registry subkey to search
1804    const wchar_t *subkey;
1805    // Registry hive to search
1806    HKEY hive;
1807    // Flags to use when opening the subkey
1808    DWORD flags;
1809    // Internal sort key to select between "identical" environments discovered
1810    // through different methods
1811    int sortKey;
1812    // Fallback value to assume for PythonCore entries missing a SysArchitecture value
1813    const wchar_t *fallbackArch;
1814};
1815
1816
1817struct RegistrySearchInfo REGISTRY_SEARCH[] = {
1818    {
1819        L"Software\\Python",
1820        HKEY_CURRENT_USER,
1821        KEY_READ,
1822        1,
1823        NULL
1824    },
1825    {
1826        L"Software\\Python",
1827        HKEY_LOCAL_MACHINE,
1828        KEY_READ | KEY_WOW64_64KEY,
1829        3,
1830        L"64bit"
1831    },
1832    {
1833        L"Software\\Python",
1834        HKEY_LOCAL_MACHINE,
1835        KEY_READ | KEY_WOW64_32KEY,
1836        4,
1837        L"32bit"
1838    },
1839    { NULL, 0, 0, 0, NULL }
1840};
1841
1842
1843struct AppxSearchInfo {
1844    // The package family name. Can be found for an installed package using the
1845    // Powershell "Get-AppxPackage" cmdlet
1846    const wchar_t *familyName;
1847    // The tag to treat the installation as
1848    const wchar_t *tag;
1849    // Internal sort key to select between "identical" environments discovered
1850    // through different methods
1851    int sortKey;
1852};
1853
1854
1855struct AppxSearchInfo APPX_SEARCH[] = {
1856    // Releases made through the Store
1857    { L"PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0", L"3.12", 10 },
1858    { L"PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0", L"3.11", 10 },
1859    { L"PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0", L"3.10", 10 },
1860    { L"PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0", L"3.9", 10 },
1861    { L"PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0", L"3.8", 10 },
1862
1863    // Side-loadable releases. Note that the publisher ID changes whenever we
1864    // renew our code-signing certificate, so the newer ID has a higher
1865    // priority (lower sortKey)
1866    { L"PythonSoftwareFoundation.Python.3.12_3847v3x7pw1km", L"3.12", 11 },
1867    { L"PythonSoftwareFoundation.Python.3.11_3847v3x7pw1km", L"3.11", 11 },
1868    { L"PythonSoftwareFoundation.Python.3.11_hd69rhyc2wevp", L"3.11", 12 },
1869    { L"PythonSoftwareFoundation.Python.3.10_3847v3x7pw1km", L"3.10", 11 },
1870    { L"PythonSoftwareFoundation.Python.3.10_hd69rhyc2wevp", L"3.10", 12 },
1871    { L"PythonSoftwareFoundation.Python.3.9_3847v3x7pw1km", L"3.9", 11 },
1872    { L"PythonSoftwareFoundation.Python.3.9_hd69rhyc2wevp", L"3.9", 12 },
1873    { L"PythonSoftwareFoundation.Python.3.8_hd69rhyc2wevp", L"3.8", 12 },
1874    { NULL, NULL, 0 }
1875};
1876
1877
1878int
1879collectEnvironments(const SearchInfo *search, EnvironmentInfo **result)
1880{
1881    int exitCode = 0;
1882    HKEY root;
1883    EnvironmentInfo *env = NULL;
1884
1885    if (!result) {
1886        return RC_INTERNAL_ERROR;
1887    }
1888    *result = NULL;
1889
1890    exitCode = explicitOverrideSearch(search, result);
1891    if (exitCode) {
1892        return exitCode;
1893    }
1894
1895    exitCode = virtualenvSearch(search, result);
1896    if (exitCode) {
1897        return exitCode;
1898    }
1899
1900    // If we aren't collecting all items to list them, we can exit now.
1901    if (env && !(search->list || search->listPaths)) {
1902        return 0;
1903    }
1904
1905    for (struct RegistrySearchInfo *info = REGISTRY_SEARCH; info->subkey; ++info) {
1906        if (ERROR_SUCCESS == RegOpenKeyExW(info->hive, info->subkey, 0, info->flags, &root)) {
1907            exitCode = registrySearch(search, result, root, info->sortKey, info->fallbackArch);
1908            RegCloseKey(root);
1909        }
1910        if (exitCode) {
1911            return exitCode;
1912        }
1913    }
1914
1915    if (search->limitToCompany) {
1916        debug(L"# Skipping APPX search due to PYLAUNCHER_LIMIT_TO_COMPANY\n");
1917        return 0;
1918    }
1919
1920    for (struct AppxSearchInfo *info = APPX_SEARCH; info->familyName; ++info) {
1921        exitCode = appxSearch(search, result, info->familyName, info->tag, info->sortKey);
1922        if (exitCode && exitCode != RC_NO_PYTHON) {
1923            return exitCode;
1924        }
1925    }
1926
1927    return 0;
1928}
1929
1930
1931/******************************************************************************\
1932 ***                           INSTALL ON DEMAND                            ***
1933\******************************************************************************/
1934
1935struct StoreSearchInfo {
1936    // The tag a user is looking for
1937    const wchar_t *tag;
1938    // The Store ID for a package if it can be installed from the Microsoft
1939    // Store. These are obtained from the dashboard at
1940    // https://partner.microsoft.com/dashboard
1941    const wchar_t *storeId;
1942};
1943
1944
1945struct StoreSearchInfo STORE_SEARCH[] = {
1946    { L"3", /* 3.11 */ L"9NRWMJP3717K" },
1947    { L"3.12", L"9NCVDN91XZQP" },
1948    { L"3.11", L"9NRWMJP3717K" },
1949    { L"3.10", L"9PJPW5LDXLZ5" },
1950    { L"3.9", L"9P7QFQMJRFP7" },
1951    { L"3.8", L"9MSSZTT1N39L" },
1952    { NULL, NULL }
1953};
1954
1955
1956int
1957_installEnvironment(const wchar_t *command, const wchar_t *arguments)
1958{
1959    SHELLEXECUTEINFOW siw = {
1960        sizeof(SHELLEXECUTEINFOW),
1961        SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE,
1962        NULL, NULL,
1963        command, arguments, NULL,
1964        SW_SHOWNORMAL
1965    };
1966
1967    debug(L"# Installing with %s %s\n", command, arguments);
1968    if (isEnvVarSet(L"PYLAUNCHER_DRYRUN")) {
1969        debug(L"# Exiting due to PYLAUNCHER_DRYRUN\n");
1970        fflush(stdout);
1971        int mode = _setmode(_fileno(stdout), _O_U8TEXT);
1972        if (arguments) {
1973            fwprintf_s(stdout, L"\"%s\" %s\n", command, arguments);
1974        } else {
1975            fwprintf_s(stdout, L"\"%s\"\n", command);
1976        }
1977        fflush(stdout);
1978        if (mode >= 0) {
1979            _setmode(_fileno(stdout), mode);
1980        }
1981        return RC_INSTALLING;
1982    }
1983
1984    if (!ShellExecuteExW(&siw)) {
1985        return RC_NO_PYTHON;
1986    }
1987
1988    if (!siw.hProcess) {
1989        return RC_INSTALLING;
1990    }
1991
1992    WaitForSingleObjectEx(siw.hProcess, INFINITE, FALSE);
1993    DWORD exitCode = 0;
1994    if (GetExitCodeProcess(siw.hProcess, &exitCode) && exitCode == 0) {
1995        return 0;
1996    }
1997    return RC_INSTALLING;
1998}
1999
2000
2001const wchar_t *WINGET_COMMAND = L"Microsoft\\WindowsApps\\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\\winget.exe";
2002const wchar_t *WINGET_ARGUMENTS = L"install -q %s --exact --accept-package-agreements --source msstore";
2003
2004const wchar_t *MSSTORE_COMMAND = L"ms-windows-store://pdp/?productid=%s";
2005
2006int
2007installEnvironment(const SearchInfo *search)
2008{
2009    // No tag? No installing
2010    if (!search->tag || !search->tagLength) {
2011        debug(L"# Cannot install Python with no tag specified\n");
2012        return RC_NO_PYTHON;
2013    }
2014
2015    // PEP 514 tag but not PythonCore? No installing
2016    if (!search->oldStyleTag &&
2017        search->company && search->companyLength &&
2018        0 != _compare(search->company, search->companyLength, L"PythonCore", -1)) {
2019        debug(L"# Cannot install for company %.*s\n", search->companyLength, search->company);
2020        return RC_NO_PYTHON;
2021    }
2022
2023    const wchar_t *storeId = NULL;
2024    for (struct StoreSearchInfo *info = STORE_SEARCH; info->tag; ++info) {
2025        if (0 == _compare(search->tag, search->tagLength, info->tag, -1)) {
2026            storeId = info->storeId;
2027            break;
2028        }
2029    }
2030
2031    if (!storeId) {
2032        return RC_NO_PYTHON;
2033    }
2034
2035    int exitCode;
2036    wchar_t command[MAXLEN];
2037    wchar_t arguments[MAXLEN];
2038    if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, command)) &&
2039        join(command, MAXLEN, WINGET_COMMAND) &&
2040        swprintf_s(arguments, MAXLEN, WINGET_ARGUMENTS, storeId)) {
2041        if (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(command)) {
2042            formatWinerror(GetLastError(), arguments, MAXLEN);
2043            debug(L"# Skipping %s: %s\n", command, arguments);
2044        } else {
2045            fputws(L"Launching winget to install Python. The following output is from the install process\n\
2046***********************************************************************\n", stdout);
2047            exitCode = _installEnvironment(command, arguments);
2048            if (exitCode == RC_INSTALLING) {
2049                fputws(L"***********************************************************************\n\
2050Please check the install status and run your command again.", stderr);
2051                return exitCode;
2052            } else if (exitCode) {
2053                return exitCode;
2054            }
2055            fputws(L"***********************************************************************\n\
2056Install appears to have succeeded. Searching for new matching installs.\n", stdout);
2057            return 0;
2058        }
2059    }
2060
2061    if (swprintf_s(command, MAXLEN, MSSTORE_COMMAND, storeId)) {
2062        fputws(L"Opening the Microsoft Store to install Python. After installation, "
2063               L"please run your command again.\n", stderr);
2064        exitCode = _installEnvironment(command, NULL);
2065        if (exitCode) {
2066            return exitCode;
2067        }
2068        return 0;
2069    }
2070
2071    return RC_NO_PYTHON;
2072}
2073
2074/******************************************************************************\
2075 ***                           ENVIRONMENT SELECT                           ***
2076\******************************************************************************/
2077
2078bool
2079_companyMatches(const SearchInfo *search, const EnvironmentInfo *env)
2080{
2081    if (!search->company || !search->companyLength) {
2082        return true;
2083    }
2084    return 0 == _compare(env->company, -1, search->company, search->companyLength);
2085}
2086
2087
2088bool
2089_tagMatches(const SearchInfo *search, const EnvironmentInfo *env, int searchTagLength)
2090{
2091    if (searchTagLength < 0) {
2092        searchTagLength = search->tagLength;
2093    }
2094    if (!search->tag || !searchTagLength) {
2095        return true;
2096    }
2097    return _startsWithSeparated(env->tag, -1, search->tag, searchTagLength, L".-");
2098}
2099
2100
2101bool
2102_is32Bit(const EnvironmentInfo *env)
2103{
2104    if (env->architecture) {
2105        return 0 == _compare(env->architecture, -1, L"32bit", -1);
2106    }
2107    return false;
2108}
2109
2110
2111int
2112_selectEnvironment(const SearchInfo *search, EnvironmentInfo *env, EnvironmentInfo **best)
2113{
2114    int exitCode = 0;
2115    while (env) {
2116        exitCode = _selectEnvironment(search, env->prev, best);
2117
2118        if (exitCode && exitCode != RC_NO_PYTHON) {
2119            return exitCode;
2120        } else if (!exitCode && *best) {
2121            return 0;
2122        }
2123
2124        if (env->highPriority && search->lowPriorityTag) {
2125            // This environment is marked high priority, and the search allows
2126            // it to be selected even though a tag is specified, so select it
2127            // gh-92817: this allows an active venv to be selected even when a
2128            // default tag has been found in py.ini or the environment
2129            *best = env;
2130            return 0;
2131        }
2132
2133        if (!search->oldStyleTag) {
2134            if (_companyMatches(search, env) && _tagMatches(search, env, -1)) {
2135                // Because of how our sort tree is set up, we will walk up the
2136                // "prev" side and implicitly select the "best" best. By
2137                // returning straight after a match, we skip the entire "next"
2138                // branch and won't ever select a "worse" best.
2139                *best = env;
2140                return 0;
2141            }
2142        } else if (0 == _compare(env->company, -1, L"PythonCore", -1)) {
2143            // Old-style tags can only match PythonCore entries
2144
2145            // If the tag ends with -64, we want to exclude 32-bit runtimes
2146            // (If the tag ends with -32, it will be filtered later)
2147            int tagLength = search->tagLength;
2148            bool exclude32Bit = false, only32Bit = false;
2149            if (tagLength > 3) {
2150                if (0 == _compareArgument(&search->tag[tagLength - 3], 3, L"-64", 3)) {
2151                    tagLength -= 3;
2152                    exclude32Bit = true;
2153                } else if (0 == _compareArgument(&search->tag[tagLength - 3], 3, L"-32", 3)) {
2154                    tagLength -= 3;
2155                    only32Bit = true;
2156                }
2157            }
2158
2159            if (_tagMatches(search, env, tagLength)) {
2160                if (exclude32Bit && _is32Bit(env)) {
2161                    debug(L"# Excluding %s/%s because it looks like 32bit\n", env->company, env->tag);
2162                } else if (only32Bit && !_is32Bit(env)) {
2163                    debug(L"# Excluding %s/%s because it doesn't look 32bit\n", env->company, env->tag);
2164                } else {
2165                    *best = env;
2166                    return 0;
2167                }
2168            }
2169        }
2170
2171        env = env->next;
2172    }
2173    return RC_NO_PYTHON;
2174}
2175
2176int
2177selectEnvironment(const SearchInfo *search, EnvironmentInfo *root, EnvironmentInfo **best)
2178{
2179    if (!best) {
2180        return RC_INTERNAL_ERROR;
2181    }
2182    if (!root) {
2183        *best = NULL;
2184        return RC_NO_PYTHON_AT_ALL;
2185    }
2186
2187    EnvironmentInfo *result = NULL;
2188    int exitCode = _selectEnvironment(search, root, &result);
2189    if (!exitCode) {
2190        *best = result;
2191    }
2192
2193    return exitCode;
2194}
2195
2196
2197/******************************************************************************\
2198 ***                            LIST ENVIRONMENTS                           ***
2199\******************************************************************************/
2200
2201#define TAGWIDTH 16
2202
2203int
2204_printEnvironment(const EnvironmentInfo *env, FILE *out, bool showPath, const wchar_t *argument)
2205{
2206    if (showPath) {
2207        if (env->executablePath && env->executablePath[0]) {
2208            if (env->executableArgs && env->executableArgs[0]) {
2209                fwprintf(out, L" %-*s %s %s\n", TAGWIDTH, argument, env->executablePath, env->executableArgs);
2210            } else {
2211                fwprintf(out, L" %-*s %s\n", TAGWIDTH, argument, env->executablePath);
2212            }
2213        } else if (env->installDir && env->installDir[0]) {
2214            fwprintf(out, L" %-*s %s\n", TAGWIDTH, argument, env->installDir);
2215        } else {
2216            fwprintf(out, L" %s\n", argument);
2217        }
2218    } else if (env->displayName) {
2219        fwprintf(out, L" %-*s %s\n", TAGWIDTH, argument, env->displayName);
2220    } else {
2221        fwprintf(out, L" %s\n", argument);
2222    }
2223    return 0;
2224}
2225
2226
2227int
2228_listAllEnvironments(EnvironmentInfo *env, FILE * out, bool showPath, EnvironmentInfo *defaultEnv)
2229{
2230    wchar_t buffer[256];
2231    const int bufferSize = 256;
2232    while (env) {
2233        int exitCode = _listAllEnvironments(env->prev, out, showPath, defaultEnv);
2234        if (exitCode) {
2235            return exitCode;
2236        }
2237
2238        if (!env->company || !env->tag) {
2239            buffer[0] = L'\0';
2240        } else if (0 == _compare(env->company, -1, L"PythonCore", -1)) {
2241            swprintf_s(buffer, bufferSize, L"-V:%s", env->tag);
2242        } else {
2243            swprintf_s(buffer, bufferSize, L"-V:%s/%s", env->company, env->tag);
2244        }
2245
2246        if (env == defaultEnv) {
2247            wcscat_s(buffer, bufferSize, L" *");
2248        }
2249
2250        if (buffer[0]) {
2251            exitCode = _printEnvironment(env, out, showPath, buffer);
2252            if (exitCode) {
2253                return exitCode;
2254            }
2255        }
2256
2257        env = env->next;
2258    }
2259    return 0;
2260}
2261
2262
2263int
2264listEnvironments(EnvironmentInfo *env, FILE * out, bool showPath, EnvironmentInfo *defaultEnv)
2265{
2266    if (!env) {
2267        fwprintf_s(stdout, L"No installed Pythons found!\n");
2268        return 0;
2269    }
2270
2271    /* TODO: Do we want to display these?
2272       In favour, helps users see that '-3' is a good option
2273       Against, repeats the next line of output
2274    SearchInfo majorSearch;
2275    EnvironmentInfo *major;
2276    int exitCode;
2277
2278    if (showPath) {
2279        memset(&majorSearch, 0, sizeof(majorSearch));
2280        majorSearch.company = L"PythonCore";
2281        majorSearch.companyLength = -1;
2282        majorSearch.tag = L"3";
2283        majorSearch.tagLength = -1;
2284        majorSearch.oldStyleTag = true;
2285        major = NULL;
2286        exitCode = selectEnvironment(&majorSearch, env, &major);
2287        if (!exitCode && major) {
2288            exitCode = _printEnvironment(major, out, showPath, L"-3 *");
2289            isDefault = false;
2290            if (exitCode) {
2291                return exitCode;
2292            }
2293        }
2294        majorSearch.tag = L"2";
2295        major = NULL;
2296        exitCode = selectEnvironment(&majorSearch, env, &major);
2297        if (!exitCode && major) {
2298            exitCode = _printEnvironment(major, out, showPath, L"-2");
2299            if (exitCode) {
2300                return exitCode;
2301            }
2302        }
2303    }
2304    */
2305
2306    int mode = _setmode(_fileno(out), _O_U8TEXT);
2307    int exitCode = _listAllEnvironments(env, out, showPath, defaultEnv);
2308    fflush(out);
2309    if (mode >= 0) {
2310        _setmode(_fileno(out), mode);
2311    }
2312    return exitCode;
2313}
2314
2315
2316/******************************************************************************\
2317 ***                           INTERPRETER LAUNCH                           ***
2318\******************************************************************************/
2319
2320
2321int
2322calculateCommandLine(const SearchInfo *search, const EnvironmentInfo *launch, wchar_t *buffer, int bufferLength)
2323{
2324    int exitCode = 0;
2325    const wchar_t *executablePath = NULL;
2326
2327    // Construct command line from a search override, or else the selected
2328    // environment's executablePath
2329    if (search->executablePath) {
2330        executablePath = search->executablePath;
2331    } else if (launch && launch->executablePath) {
2332        executablePath = launch->executablePath;
2333    }
2334
2335    // If we have an executable path, put it at the start of the command, but
2336    // only if the search allowed an override.
2337    // Otherwise, use the environment's installDir and the search's default
2338    // executable name.
2339    if (executablePath && search->allowExecutableOverride) {
2340        if (wcschr(executablePath, L' ') && executablePath[0] != L'"') {
2341            buffer[0] = L'"';
2342            exitCode = wcscpy_s(&buffer[1], bufferLength - 1, executablePath);
2343            if (!exitCode) {
2344                exitCode = wcscat_s(buffer, bufferLength, L"\"");
2345            }
2346        } else {
2347            exitCode = wcscpy_s(buffer, bufferLength, executablePath);
2348        }
2349    } else if (launch) {
2350        if (!launch->installDir) {
2351            fwprintf_s(stderr, L"Cannot launch %s %s because no install directory was specified",
2352                       launch->company, launch->tag);
2353            exitCode = RC_NO_PYTHON;
2354        } else if (!search->executable || !search->executableLength) {
2355            fwprintf_s(stderr, L"Cannot launch %s %s because no executable name is available",
2356                       launch->company, launch->tag);
2357            exitCode = RC_NO_PYTHON;
2358        } else {
2359            wchar_t executable[256];
2360            wcsncpy_s(executable, 256, search->executable, search->executableLength);
2361            if ((wcschr(launch->installDir, L' ') && launch->installDir[0] != L'"') ||
2362                (wcschr(executable, L' ') && executable[0] != L'"')) {
2363                buffer[0] = L'"';
2364                exitCode = wcscpy_s(&buffer[1], bufferLength - 1, launch->installDir);
2365                if (!exitCode) {
2366                    exitCode = join(buffer, bufferLength, executable) ? 0 : RC_NO_MEMORY;
2367                }
2368                if (!exitCode) {
2369                    exitCode = wcscat_s(buffer, bufferLength, L"\"");
2370                }
2371            } else {
2372                exitCode = wcscpy_s(buffer, bufferLength, launch->installDir);
2373                if (!exitCode) {
2374                    exitCode = join(buffer, bufferLength, executable) ? 0 : RC_NO_MEMORY;
2375                }
2376            }
2377        }
2378    } else {
2379        exitCode = RC_NO_PYTHON;
2380    }
2381
2382    if (!exitCode && launch && launch->executableArgs) {
2383        exitCode = wcscat_s(buffer, bufferLength, L" ");
2384        if (!exitCode) {
2385            exitCode = wcscat_s(buffer, bufferLength, launch->executableArgs);
2386        }
2387    }
2388
2389    if (!exitCode && search->executableArgs) {
2390        if (search->executableArgsLength < 0) {
2391            exitCode = wcscat_s(buffer, bufferLength, search->executableArgs);
2392        } else if (search->executableArgsLength > 0) {
2393            int end = (int)wcsnlen_s(buffer, MAXLEN);
2394            if (end < bufferLength - (search->executableArgsLength + 1)) {
2395                exitCode = wcsncpy_s(&buffer[end], bufferLength - end,
2396                    search->executableArgs, search->executableArgsLength);
2397            }
2398        }
2399    }
2400
2401    if (!exitCode && search->restOfCmdLine) {
2402        exitCode = wcscat_s(buffer, bufferLength, search->restOfCmdLine);
2403    }
2404
2405    return exitCode;
2406}
2407
2408
2409
2410BOOL
2411_safeDuplicateHandle(HANDLE in, HANDLE * pout, const wchar_t *nameForError)
2412{
2413    BOOL ok;
2414    HANDLE process = GetCurrentProcess();
2415    DWORD rc;
2416
2417    *pout = NULL;
2418    ok = DuplicateHandle(process, in, process, pout, 0, TRUE,
2419                         DUPLICATE_SAME_ACCESS);
2420    if (!ok) {
2421        rc = GetLastError();
2422        if (rc == ERROR_INVALID_HANDLE) {
2423            debug(L"DuplicateHandle returned ERROR_INVALID_HANDLE\n");
2424            ok = TRUE;
2425        }
2426        else {
2427            winerror(0, L"Failed to duplicate %s handle", nameForError);
2428        }
2429    }
2430    return ok;
2431}
2432
2433BOOL WINAPI
2434ctrl_c_handler(DWORD code)
2435{
2436    return TRUE;    /* We just ignore all control events. */
2437}
2438
2439
2440int
2441launchEnvironment(const SearchInfo *search, const EnvironmentInfo *launch, wchar_t *launchCommand)
2442{
2443    HANDLE job;
2444    JOBOBJECT_EXTENDED_LIMIT_INFORMATION info;
2445    DWORD rc;
2446    BOOL ok;
2447    STARTUPINFOW si;
2448    PROCESS_INFORMATION pi;
2449
2450    // If this is a dryrun, do not actually launch
2451    if (isEnvVarSet(L"PYLAUNCHER_DRYRUN")) {
2452        debug(L"LaunchCommand: %s\n", launchCommand);
2453        debug(L"# Exiting due to PYLAUNCHER_DRYRUN variable\n");
2454        fflush(stdout);
2455        int mode = _setmode(_fileno(stdout), _O_U8TEXT);
2456        fwprintf(stdout, L"%s\n", launchCommand);
2457        fflush(stdout);
2458        if (mode >= 0) {
2459            _setmode(_fileno(stdout), mode);
2460        }
2461        return 0;
2462    }
2463
2464#if defined(_WINDOWS)
2465    /*
2466    When explorer launches a Windows (GUI) application, it displays
2467    the "app starting" (the "pointer + hourglass") cursor for a number
2468    of seconds, or until the app does something UI-ish (eg, creating a
2469    window, or fetching a message).  As this launcher doesn't do this
2470    directly, that cursor remains even after the child process does these
2471    things.  We avoid that by doing a simple post+get message.
2472    See http://bugs.python.org/issue17290 and
2473    https://bitbucket.org/vinay.sajip/pylauncher/issue/20/busy-cursor-for-a-long-time-when-running
2474    */
2475    MSG msg;
2476
2477    PostMessage(0, 0, 0, 0);
2478    GetMessage(&msg, 0, 0, 0);
2479#endif
2480
2481    debug(L"# about to run: %s\n", launchCommand);
2482    job = CreateJobObject(NULL, NULL);
2483    ok = QueryInformationJobObject(job, JobObjectExtendedLimitInformation,
2484                                  &info, sizeof(info), &rc);
2485    if (!ok || (rc != sizeof(info)) || !job) {
2486        winerror(0, L"Failed to query job information");
2487        return RC_CREATE_PROCESS;
2488    }
2489    info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE |
2490                                             JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
2491    ok = SetInformationJobObject(job, JobObjectExtendedLimitInformation, &info,
2492                                 sizeof(info));
2493    if (!ok) {
2494        winerror(0, L"Failed to update job information");
2495        return RC_CREATE_PROCESS;
2496    }
2497    memset(&si, 0, sizeof(si));
2498    GetStartupInfoW(&si);
2499    if (!_safeDuplicateHandle(GetStdHandle(STD_INPUT_HANDLE), &si.hStdInput, L"stdin") ||
2500        !_safeDuplicateHandle(GetStdHandle(STD_OUTPUT_HANDLE), &si.hStdOutput, L"stdout") ||
2501        !_safeDuplicateHandle(GetStdHandle(STD_ERROR_HANDLE), &si.hStdError, L"stderr")) {
2502        return RC_NO_STD_HANDLES;
2503    }
2504
2505    ok = SetConsoleCtrlHandler(ctrl_c_handler, TRUE);
2506    if (!ok) {
2507        winerror(0, L"Failed to update Control-C handler");
2508        return RC_NO_STD_HANDLES;
2509    }
2510
2511    si.dwFlags = STARTF_USESTDHANDLES;
2512    ok = CreateProcessW(NULL, launchCommand, NULL, NULL, TRUE,
2513                        0, NULL, NULL, &si, &pi);
2514    if (!ok) {
2515        winerror(0, L"Unable to create process using '%s'", launchCommand);
2516        return RC_CREATE_PROCESS;
2517    }
2518    AssignProcessToJobObject(job, pi.hProcess);
2519    CloseHandle(pi.hThread);
2520    WaitForSingleObjectEx(pi.hProcess, INFINITE, FALSE);
2521    ok = GetExitCodeProcess(pi.hProcess, &rc);
2522    if (!ok) {
2523        winerror(0, L"Failed to get exit code of process");
2524        return RC_CREATE_PROCESS;
2525    }
2526    debug(L"child process exit code: %d\n", rc);
2527    return rc;
2528}
2529
2530
2531/******************************************************************************\
2532 ***                           PROCESS CONTROLLER                           ***
2533\******************************************************************************/
2534
2535
2536int
2537performSearch(SearchInfo *search, EnvironmentInfo **envs)
2538{
2539    // First parse the command line for options
2540    int exitCode = parseCommandLine(search);
2541    if (exitCode) {
2542        return exitCode;
2543    }
2544
2545    // Check for a shebang line in our script file
2546    // (or return quickly if no script file was specified)
2547    exitCode = checkShebang(search);
2548    switch (exitCode) {
2549    case 0:
2550    case RC_NO_SHEBANG:
2551    case RC_RECURSIVE_SHEBANG:
2552        break;
2553    default:
2554        return exitCode;
2555    }
2556
2557    // Resolve old-style tags (possibly from a shebang) against py.ini entries
2558    // and environment variables.
2559    exitCode = checkDefaults(search);
2560    if (exitCode) {
2561        return exitCode;
2562    }
2563
2564    // If debugging is enabled, list our search criteria
2565    dumpSearchInfo(search);
2566
2567    // Find all matching environments
2568    exitCode = collectEnvironments(search, envs);
2569    if (exitCode) {
2570        return exitCode;
2571    }
2572
2573    return 0;
2574}
2575
2576
2577int
2578process(int argc, wchar_t ** argv)
2579{
2580    int exitCode = 0;
2581    int searchExitCode = 0;
2582    SearchInfo search = {0};
2583    EnvironmentInfo *envs = NULL;
2584    EnvironmentInfo *env = NULL;
2585    wchar_t launchCommand[MAXLEN];
2586
2587    memset(launchCommand, 0, sizeof(launchCommand));
2588
2589    if (isEnvVarSet(L"PYLAUNCHER_DEBUG")) {
2590        setvbuf(stderr, (char *)NULL, _IONBF, 0);
2591        log_fp = stderr;
2592        debug(L"argv0: %s\nversion: %S\n", argv[0], PY_VERSION);
2593    }
2594
2595    DWORD len = GetEnvironmentVariableW(L"PYLAUNCHER_LIMIT_TO_COMPANY", NULL, 0);
2596    if (len > 1) {
2597        wchar_t *limitToCompany = allocSearchInfoBuffer(&search, len);
2598        search.limitToCompany = limitToCompany;
2599        if (0 == GetEnvironmentVariableW(L"PYLAUNCHER_LIMIT_TO_COMPANY", limitToCompany, len)) {
2600            exitCode = RC_INTERNAL_ERROR;
2601            winerror(0, L"Failed to read PYLAUNCHER_LIMIT_TO_COMPANY variable");
2602            goto abort;
2603        }
2604    }
2605
2606    search.originalCmdLine = GetCommandLineW();
2607
2608    exitCode = performSearch(&search, &envs);
2609    if (exitCode) {
2610        goto abort;
2611    }
2612
2613    // Display the help text, but only exit on error
2614    if (search.help) {
2615        exitCode = showHelpText(argv);
2616        if (exitCode) {
2617            goto abort;
2618        }
2619    }
2620
2621    // Select best environment
2622    // This is early so that we can show the default when listing, but all
2623    // responses to any errors occur later.
2624    searchExitCode = selectEnvironment(&search, envs, &env);
2625
2626    // List all environments, then exit
2627    if (search.list || search.listPaths) {
2628        exitCode = listEnvironments(envs, stdout, search.listPaths, env);
2629        goto abort;
2630    }
2631
2632    // When debugging, list all discovered environments anyway
2633    if (log_fp) {
2634        exitCode = listEnvironments(envs, log_fp, true, NULL);
2635        if (exitCode) {
2636            goto abort;
2637        }
2638    }
2639
2640    // We searched earlier, so if we didn't find anything, now we react
2641    exitCode = searchExitCode;
2642    // If none found, and if permitted, install it
2643    if (exitCode == RC_NO_PYTHON && isEnvVarSet(L"PYLAUNCHER_ALLOW_INSTALL") ||
2644        isEnvVarSet(L"PYLAUNCHER_ALWAYS_INSTALL")) {
2645        exitCode = installEnvironment(&search);
2646        if (!exitCode) {
2647            // Successful install, so we need to re-scan and select again
2648            env = NULL;
2649            exitCode = performSearch(&search, &envs);
2650            if (exitCode) {
2651                goto abort;
2652            }
2653            exitCode = selectEnvironment(&search, envs, &env);
2654        }
2655    }
2656    if (exitCode == RC_NO_PYTHON) {
2657        fputws(L"No suitable Python runtime found\n", stderr);
2658        fputws(L"Pass --list (-0) to see all detected environments on your machine\n", stderr);
2659        if (!isEnvVarSet(L"PYLAUNCHER_ALLOW_INSTALL") && search.oldStyleTag) {
2660            fputws(L"or set environment variable PYLAUNCHER_ALLOW_INSTALL to use winget\n"
2661                   L"or open the Microsoft Store to the requested version.\n", stderr);
2662        }
2663        goto abort;
2664    }
2665    if (exitCode == RC_NO_PYTHON_AT_ALL) {
2666        fputws(L"No installed Python found!\n", stderr);
2667        goto abort;
2668    }
2669    if (exitCode) {
2670        goto abort;
2671    }
2672
2673    if (env) {
2674        debug(L"env.company: %s\nenv.tag: %s\n", env->company, env->tag);
2675    } else {
2676        debug(L"env.company: (null)\nenv.tag: (null)\n");
2677    }
2678
2679    exitCode = calculateCommandLine(&search, env, launchCommand, sizeof(launchCommand) / sizeof(launchCommand[0]));
2680    if (exitCode) {
2681        goto abort;
2682    }
2683
2684    // Launch selected runtime
2685    exitCode = launchEnvironment(&search, env, launchCommand);
2686
2687abort:
2688    freeSearchInfo(&search);
2689    freeEnvironmentInfo(envs);
2690    return exitCode;
2691}
2692
2693
2694#if defined(_WINDOWS)
2695
2696int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
2697                   LPWSTR lpstrCmd, int nShow)
2698{
2699    return process(__argc, __wargv);
2700}
2701
2702#else
2703
2704int cdecl wmain(int argc, wchar_t ** argv)
2705{
2706    return process(argc, argv);
2707}
2708
2709#endif
2710