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
43 static FILE * log_fp = NULL;
44
45 void
debug(wchar_t * format,...)46 debug(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
71 void
formatWinerror(int rc,wchar_t * message,int size)72 formatWinerror(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
81 void
winerror(int err,wchar_t * format,...)82 winerror(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
112 void
error(wchar_t * format,...)113 error(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
131 typedef BOOL (*PIsWow64Process2)(HANDLE, USHORT*, USHORT*);
132
133
134 USHORT
_getNativeMachine()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
165 bool
isAMD64Host()166 isAMD64Host()
167 {
168 return _getNativeMachine() == IMAGE_FILE_MACHINE_AMD64;
169 }
170
171
172 bool
isARM64Host()173 isARM64Host()
174 {
175 return _getNativeMachine() == IMAGE_FILE_MACHINE_ARM64;
176 }
177
178
179 bool
isEnvVarSet(const wchar_t * name)180 isEnvVarSet(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
188 bool
join(wchar_t * buffer,size_t bufferLength,const wchar_t * fragment)189 join(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
198 int
_compare(const wchar_t * x,int xLen,const wchar_t * y,int yLen)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
225 int
_compareArgument(const wchar_t * x,int xLen,const wchar_t * y,int yLen)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
251 int
_comparePath(const wchar_t * x,int xLen,const wchar_t * y,int yLen)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
274 bool
_startsWith(const wchar_t * x,int xLen,const wchar_t * y,int yLen)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
286 bool
_startsWithArgument(const wchar_t * x,int xLen,const wchar_t * y,int yLen)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'.
301 bool
_startsWithSeparated(const wchar_t * x,int xLen,const wchar_t * y,int yLen,const wchar_t * separators)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
327 int
showHelpText(wchar_t ** argv)328 showHelpText(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
376 struct _SearchInfoBuffer {
377 struct _SearchInfoBuffer *next;
378 wchar_t buffer[0];
379 };
380
381
382 typedef 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
444 wchar_t *
allocSearchInfoBuffer(SearchInfo * search,int wcharCount)445 allocSearchInfoBuffer(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
460 void
freeSearchInfo(SearchInfo * search)461 freeSearchInfo(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
473 void
_debugStringAndLength(const wchar_t * s,int len,const wchar_t * name)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
488 void
dumpSearchInfo(SearchInfo * search)489 dumpSearchInfo(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
523 int
findArgv0Length(const wchar_t * buffer,int bufferLength)524 findArgv0Length(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
553 const wchar_t *
findArgv0End(const wchar_t * buffer,int bufferLength)554 findArgv0End(const wchar_t *buffer, int bufferLength)
555 {
556 return &buffer[findArgv0Length(buffer, bufferLength)];
557 }
558
559
560 /******************************************************************************\
561 *** COMMAND-LINE PARSING ***
562 \******************************************************************************/
563
564
565 int
parseCommandLine(SearchInfo * search)566 parseCommandLine(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
705 int
_decodeShebang(SearchInfo * search,const char * buffer,int bufferLength,bool onlyUtf8,wchar_t ** decoded,int * decodedLength)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
734 bool
_shebangStartsWith(const wchar_t * buffer,int bufferLength,const wchar_t * prefix,const wchar_t ** rest,int * firstArgumentLength)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
755 int
searchPath(SearchInfo * search,const wchar_t * shebang,int shebangLength)756 searchPath(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
838 int
_readIni(const wchar_t * section,const wchar_t * settingName,wchar_t * buffer,int bufferLength)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
874 bool
_findCommand(SearchInfo * search,const wchar_t * command,int commandLength)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
894 int
_useShebangAsExecutable(SearchInfo * search,const wchar_t * shebang,int shebangLength)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
950 int
checkShebang(SearchInfo * search)951 checkShebang(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
1113 int
checkDefaults(SearchInfo * search)1114 checkDefaults(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
1182 typedef 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
1200 int
copyWstr(const wchar_t ** dest,const wchar_t * src)1201 copyWstr(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
1221 EnvironmentInfo *
newEnvironmentInfo(const wchar_t * company,const wchar_t * tag)1222 newEnvironmentInfo(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
1244 void
freeEnvironmentInfo(EnvironmentInfo * env)1245 freeEnvironmentInfo(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
1265 int
_compareCompany(const wchar_t * x,const wchar_t * y)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
1287 int
_compareTag(const wchar_t * x,const wchar_t * y)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
1324 int
addEnvironmentInfo(EnvironmentInfo ** root,EnvironmentInfo * parent,EnvironmentInfo * node)1325 addEnvironmentInfo(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
1388 int
_registryReadString(const wchar_t ** dest,HKEY root,const wchar_t * subkey,const wchar_t * value)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
1413 int
_combineWithInstallDir(const wchar_t ** dest,const wchar_t * installDir,const wchar_t * fragment,int fragmentLength)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
1430 bool
_isLegacyVersion(EnvironmentInfo * env)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
1450 int
_registryReadLegacyEnvironment(const SearchInfo * search,HKEY root,EnvironmentInfo * env,const wchar_t * fallbackArch)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
1524 int
_registryReadEnvironment(const SearchInfo * search,HKEY root,EnvironmentInfo * env,const wchar_t * fallbackArch)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
1579 int
_registrySearchTags(const SearchInfo * search,EnvironmentInfo ** result,HKEY root,int sortKey,const wchar_t * company,const wchar_t * fallbackArch)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
1618 int
registrySearch(const SearchInfo * search,EnvironmentInfo ** result,HKEY root,int sortKey,const wchar_t * fallbackArch)1619 registrySearch(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
1651 int
appxSearch(const SearchInfo * search,EnvironmentInfo ** result,const wchar_t * packageFamilyName,const wchar_t * tag,int sortKey)1652 appxSearch(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
1714 int
explicitOverrideSearch(const SearchInfo * search,EnvironmentInfo ** result)1715 explicitOverrideSearch(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
1740 abort:
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
1753 int
virtualenvSearch(const SearchInfo * search,EnvironmentInfo ** result)1754 virtualenvSearch(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
1789 abort:
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
1802 struct 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
1817 struct 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
1843 struct 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
1855 struct 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
1878 int
collectEnvironments(const SearchInfo * search,EnvironmentInfo ** result)1879 collectEnvironments(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
1935 struct 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
1945 struct 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
1956 int
_installEnvironment(const wchar_t * command,const wchar_t * arguments)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
2001 const wchar_t *WINGET_COMMAND = L"Microsoft\\WindowsApps\\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\\winget.exe";
2002 const wchar_t *WINGET_ARGUMENTS = L"install -q %s --exact --accept-package-agreements --source msstore";
2003
2004 const wchar_t *MSSTORE_COMMAND = L"ms-windows-store://pdp/?productid=%s";
2005
2006 int
installEnvironment(const SearchInfo * search)2007 installEnvironment(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\
2050 Please 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\
2056 Install 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
2078 bool
_companyMatches(const SearchInfo * search,const EnvironmentInfo * env)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
2088 bool
_tagMatches(const SearchInfo * search,const EnvironmentInfo * env,int searchTagLength)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
2101 bool
_is32Bit(const EnvironmentInfo * env)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
2111 int
_selectEnvironment(const SearchInfo * search,EnvironmentInfo * env,EnvironmentInfo ** best)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
2176 int
selectEnvironment(const SearchInfo * search,EnvironmentInfo * root,EnvironmentInfo ** best)2177 selectEnvironment(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
2203 int
_printEnvironment(const EnvironmentInfo * env,FILE * out,bool showPath,const wchar_t * argument)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
2227 int
_listAllEnvironments(EnvironmentInfo * env,FILE * out,bool showPath,EnvironmentInfo * defaultEnv)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
2263 int
listEnvironments(EnvironmentInfo * env,FILE * out,bool showPath,EnvironmentInfo * defaultEnv)2264 listEnvironments(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
2321 int
calculateCommandLine(const SearchInfo * search,const EnvironmentInfo * launch,wchar_t * buffer,int bufferLength)2322 calculateCommandLine(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
2410 BOOL
_safeDuplicateHandle(HANDLE in,HANDLE * pout,const wchar_t * nameForError)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
2433 BOOL WINAPI
ctrl_c_handler(DWORD code)2434 ctrl_c_handler(DWORD code)
2435 {
2436 return TRUE; /* We just ignore all control events. */
2437 }
2438
2439
2440 int
launchEnvironment(const SearchInfo * search,const EnvironmentInfo * launch,wchar_t * launchCommand)2441 launchEnvironment(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
2536 int
performSearch(SearchInfo * search,EnvironmentInfo ** envs)2537 performSearch(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
2577 int
process(int argc,wchar_t ** argv)2578 process(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
2687 abort:
2688 freeSearchInfo(&search);
2689 freeEnvironmentInfo(envs);
2690 return exitCode;
2691 }
2692
2693
2694 #if defined(_WINDOWS)
2695
wWinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPWSTR lpstrCmd,int nShow)2696 int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
2697 LPWSTR lpstrCmd, int nShow)
2698 {
2699 return process(__argc, __wargv);
2700 }
2701
2702 #else
2703
wmain(int argc,wchar_t ** argv)2704 int cdecl wmain(int argc, wchar_t ** argv)
2705 {
2706 return process(argc, argv);
2707 }
2708
2709 #endif
2710