bpo-38133: Allow py.exe launcher to locate installations from the Microsoft Store (GH-16025)

This commit is contained in:
Steve Dower 2019-09-12 18:16:50 +01:00 committed by GitHub
parent 2b2ead7438
commit ed93a8852d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 211 additions and 101 deletions

View file

@ -0,0 +1,2 @@
Allow py.exe launcher to locate installations from the Microsoft Store and
improve display of active virtual environments.

View file

@ -162,12 +162,13 @@ static wchar_t * get_env(wchar_t * key)
#endif
#endif
#define MAX_VERSION_SIZE 4
#define MAX_VERSION_SIZE 8
typedef struct {
wchar_t version[MAX_VERSION_SIZE]; /* m.n */
int bits; /* 32 or 64 */
wchar_t executable[MAX_PATH];
wchar_t exe_display[MAX_PATH];
} INSTALLED_PYTHON;
/*
@ -185,10 +186,18 @@ static size_t num_installed_pythons = 0;
* The version name can be longer than MAX_VERSION_SIZE, but will be
* truncated to just X.Y for comparisons.
*/
#define IP_BASE_SIZE 40
#define IP_BASE_SIZE 80
#define IP_VERSION_SIZE 8
#define IP_SIZE (IP_BASE_SIZE + IP_VERSION_SIZE)
#define CORE_PATH L"SOFTWARE\\Python\\PythonCore"
/*
* Installations from the Microsoft Store will set the same registry keys,
* but because of a limitation in Windows they cannot be enumerated normally
* (unless you have no other Python installations... which is probably false
* because that's the most likely way to get this launcher!)
* This key is under HKEY_LOCAL_MACHINE
*/
#define LOOKASIDE_PATH L"SOFTWARE\\Microsoft\\AppModel\\Lookaside\\user\\Software\\Python\\PythonCore"
static wchar_t * location_checks[] = {
L"\\",
@ -201,7 +210,7 @@ static wchar_t * location_checks[] = {
};
static INSTALLED_PYTHON *
find_existing_python(wchar_t * path)
find_existing_python(const wchar_t * path)
{
INSTALLED_PYTHON * result = NULL;
size_t i;
@ -216,15 +225,32 @@ find_existing_python(wchar_t * path)
return result;
}
static INSTALLED_PYTHON *
find_existing_python2(int bits, const wchar_t * version)
{
INSTALLED_PYTHON * result = NULL;
size_t i;
INSTALLED_PYTHON * ip;
for (i = 0, ip = installed_pythons; i < num_installed_pythons; i++, ip++) {
if (bits == ip->bits && _wcsicmp(version, ip->version) == 0) {
result = ip;
break;
}
}
return result;
}
static void
locate_pythons_for_key(HKEY root, REGSAM flags)
_locate_pythons_for_key(HKEY root, LPCWSTR subkey, REGSAM flags, int bits,
int display_name_only)
{
HKEY core_root, ip_key;
LSTATUS status = RegOpenKeyExW(root, CORE_PATH, 0, flags, &core_root);
LSTATUS status = RegOpenKeyExW(root, subkey, 0, flags, &core_root);
wchar_t message[MSGSIZE];
DWORD i;
size_t n;
BOOL ok;
BOOL ok, append_name;
DWORD type, data_size, attrs;
INSTALLED_PYTHON * ip, * pip;
wchar_t ip_version[IP_VERSION_SIZE];
@ -252,8 +278,16 @@ locate_pythons_for_key(HKEY root, REGSAM flags)
else {
wcsncpy_s(ip->version, MAX_VERSION_SIZE, ip_version,
MAX_VERSION_SIZE-1);
/* Still treating version as "x.y" rather than sys.winver
* When PEP 514 tags are properly used, we shouldn't need
* to strip this off here.
*/
check = wcsrchr(ip->version, L'-');
if (check && !wcscmp(check, L"-32")) {
*check = L'\0';
}
_snwprintf_s(ip_path, IP_SIZE, _TRUNCATE,
L"%ls\\%ls\\InstallPath", CORE_PATH, ip_version);
L"%ls\\%ls\\InstallPath", subkey, ip_version);
status = RegOpenKeyExW(root, ip_path, 0, flags, &ip_key);
if (status != ERROR_SUCCESS) {
winerror(status, message, MSGSIZE);
@ -262,42 +296,57 @@ locate_pythons_for_key(HKEY root, REGSAM flags)
continue;
}
data_size = sizeof(ip->executable) - 1;
status = RegQueryValueExW(ip_key, NULL, NULL, &type,
append_name = FALSE;
status = RegQueryValueExW(ip_key, L"ExecutablePath", NULL, &type,
(LPBYTE)ip->executable, &data_size);
if (status != ERROR_SUCCESS || type != REG_SZ || !data_size) {
append_name = TRUE;
data_size = sizeof(ip->executable) - 1;
status = RegQueryValueExW(ip_key, NULL, NULL, &type,
(LPBYTE)ip->executable, &data_size);
if (status != ERROR_SUCCESS) {
winerror(status, message, MSGSIZE);
debug(L"%ls\\%ls: %ls\n", key_name, ip_path, message);
RegCloseKey(ip_key);
continue;
}
}
RegCloseKey(ip_key);
if (status != ERROR_SUCCESS) {
winerror(status, message, MSGSIZE);
debug(L"%ls\\%ls: %ls\n", key_name, ip_path, message);
if (type != REG_SZ) {
continue;
}
if (type == REG_SZ) {
data_size = data_size / sizeof(wchar_t) - 1; /* for NUL */
if (ip->executable[data_size - 1] == L'\\')
--data_size; /* reg value ended in a backslash */
/* ip->executable is data_size long */
for (checkp = location_checks; *checkp; ++checkp) {
check = *checkp;
data_size = data_size / sizeof(wchar_t) - 1; /* for NUL */
if (ip->executable[data_size - 1] == L'\\')
--data_size; /* reg value ended in a backslash */
/* ip->executable is data_size long */
for (checkp = location_checks; *checkp; ++checkp) {
check = *checkp;
if (append_name) {
_snwprintf_s(&ip->executable[data_size],
MAX_PATH - data_size,
MAX_PATH - data_size,
L"%ls%ls", check, PYTHON_EXECUTABLE);
attrs = GetFileAttributesW(ip->executable);
if (attrs == INVALID_FILE_ATTRIBUTES) {
winerror(GetLastError(), message, MSGSIZE);
debug(L"locate_pythons_for_key: %ls: %ls",
ip->executable, message);
}
else if (attrs & FILE_ATTRIBUTE_DIRECTORY) {
debug(L"locate_pythons_for_key: '%ls' is a \
directory\n",
ip->executable, attrs);
}
else if (find_existing_python(ip->executable)) {
debug(L"locate_pythons_for_key: %ls: already \
found\n", ip->executable);
}
else {
/* check the executable type. */
}
attrs = GetFileAttributesW(ip->executable);
if (attrs == INVALID_FILE_ATTRIBUTES) {
winerror(GetLastError(), message, MSGSIZE);
debug(L"locate_pythons_for_key: %ls: %ls",
ip->executable, message);
}
else if (attrs & FILE_ATTRIBUTE_DIRECTORY) {
debug(L"locate_pythons_for_key: '%ls' is a directory\n",
ip->executable, attrs);
}
else if (find_existing_python(ip->executable)) {
debug(L"locate_pythons_for_key: %ls: already found\n",
ip->executable);
}
else {
/* check the executable type. */
if (bits) {
ip->bits = bits;
} else {
ok = GetBinaryTypeW(ip->executable, &attrs);
if (!ok) {
debug(L"Failure getting binary type: %ls\n",
@ -310,32 +359,48 @@ found\n", ip->executable);
ip->bits = 32;
else
ip->bits = 0;
if (ip->bits == 0) {
debug(L"locate_pythons_for_key: %ls: \
}
}
if (ip->bits == 0) {
debug(L"locate_pythons_for_key: %ls: \
invalid binary type: %X\n",
ip->executable, attrs);
ip->executable, attrs);
}
else {
if (display_name_only) {
/* display just the executable name. This is
* primarily for the Store installs */
const wchar_t *name = wcsrchr(ip->executable, L'\\');
if (name) {
wcscpy_s(ip->exe_display, MAX_PATH, name+1);
}
else {
if (wcschr(ip->executable, L' ') != NULL) {
/* has spaces, so quote */
n = wcslen(ip->executable);
memmove(&ip->executable[1],
ip->executable, n * sizeof(wchar_t));
ip->executable[0] = L'\"';
ip->executable[n + 1] = L'\"';
ip->executable[n + 2] = L'\0';
}
debug(L"locate_pythons_for_key: %ls \
}
if (wcschr(ip->executable, L' ') != NULL) {
/* has spaces, so quote, and set original as
* the display name */
if (!ip->exe_display[0]) {
wcscpy_s(ip->exe_display, MAX_PATH, ip->executable);
}
n = wcslen(ip->executable);
memmove(&ip->executable[1],
ip->executable, n * sizeof(wchar_t));
ip->executable[0] = L'\"';
ip->executable[n + 1] = L'\"';
ip->executable[n + 2] = L'\0';
}
debug(L"locate_pythons_for_key: %ls \
is a %dbit executable\n",
ip->executable, ip->bits);
++num_installed_pythons;
pip = ip++;
if (num_installed_pythons >=
MAX_INSTALLED_PYTHONS)
break;
/* Copy over the attributes for the next */
*ip = *pip;
}
ip->executable, ip->bits);
if (find_existing_python2(ip->bits, ip->version)) {
debug(L"locate_pythons_for_key: %ls-%i: already \
found\n", ip->version, ip->bits);
}
else {
++num_installed_pythons;
pip = ip++;
if (num_installed_pythons >=
MAX_INSTALLED_PYTHONS)
break;
}
}
}
@ -359,9 +424,63 @@ compare_pythons(const void * p1, const void * p2)
return result;
}
static void
locate_pythons_for_key(HKEY root, REGSAM flags)
{
_locate_pythons_for_key(root, CORE_PATH, flags, 0, FALSE);
}
static void
locate_store_pythons()
{
#if defined(_M_X64)
/* 64bit process, so look in native registry */
_locate_pythons_for_key(HKEY_LOCAL_MACHINE, LOOKASIDE_PATH,
KEY_READ, 64, TRUE);
#else
/* 32bit process, so check that we're on 64bit OS */
BOOL f64 = FALSE;
if (IsWow64Process(GetCurrentProcess(), &f64) && f64) {
_locate_pythons_for_key(HKEY_LOCAL_MACHINE, LOOKASIDE_PATH,
KEY_READ | KEY_WOW64_64KEY, 64, TRUE);
}
#endif
}
static void
locate_venv_python()
{
static wchar_t venv_python[MAX_PATH];
INSTALLED_PYTHON * ip;
wchar_t *virtual_env = get_env(L"VIRTUAL_ENV");
DWORD attrs;
/* Check for VIRTUAL_ENV environment variable */
if (virtual_env == NULL || virtual_env[0] == L'\0') {
return;
}
/* Check for a python executable in the venv */
debug(L"Checking for Python executable in virtual env '%ls'\n", virtual_env);
_snwprintf_s(venv_python, MAX_PATH, _TRUNCATE,
L"%ls\\Scripts\\%ls", virtual_env, PYTHON_EXECUTABLE);
attrs = GetFileAttributesW(venv_python);
if (attrs == INVALID_FILE_ATTRIBUTES) {
debug(L"Python executable %ls missing from virtual env\n", venv_python);
return;
}
ip = &installed_pythons[num_installed_pythons++];
wcscpy_s(ip->executable, MAX_PATH, venv_python);
ip->bits = 0;
wcscpy_s(ip->version, MAX_VERSION_SIZE, L"venv");
}
static void
locate_all_pythons()
{
/* venv Python is highest priority */
locate_venv_python();
#if defined(_M_X64)
/* If we are a 64bit process, first hit the 32bit keys. */
debug(L"locating Pythons in 32bit registry\n");
@ -380,6 +499,8 @@ locate_all_pythons()
debug(L"locating Pythons in native registry\n");
locate_pythons_for_key(HKEY_CURRENT_USER, KEY_READ);
locate_pythons_for_key(HKEY_LOCAL_MACHINE, KEY_READ);
/* Store-installed Python is lowest priority */
locate_store_pythons();
qsort(installed_pythons, num_installed_pythons, sizeof(INSTALLED_PYTHON),
compare_pythons);
}
@ -416,31 +537,6 @@ find_python_by_version(wchar_t const * wanted_ver)
}
static wchar_t *
find_python_by_venv()
{
static wchar_t venv_python[MAX_PATH];
wchar_t *virtual_env = get_env(L"VIRTUAL_ENV");
DWORD attrs;
/* Check for VIRTUAL_ENV environment variable */
if (virtual_env == NULL || virtual_env[0] == L'\0') {
return NULL;
}
/* Check for a python executable in the venv */
debug(L"Checking for Python executable in virtual env '%ls'\n", virtual_env);
_snwprintf_s(venv_python, MAX_PATH, _TRUNCATE,
L"%ls\\Scripts\\%ls", virtual_env, PYTHON_EXECUTABLE);
attrs = GetFileAttributesW(venv_python);
if (attrs == INVALID_FILE_ATTRIBUTES) {
debug(L"Python executable %ls missing from virtual env\n", venv_python);
return NULL;
}
return venv_python;
}
static wchar_t appdata_ini_path[MAX_PATH];
static wchar_t launcher_ini_path[MAX_PATH];
@ -523,9 +619,12 @@ locate_python(wchar_t * wanted_ver, BOOL from_shebang)
}
else {
*last_char = L'\0'; /* look for an overall default */
configured_value = get_configured_value(config_key);
if (configured_value)
result = find_python_by_version(configured_value);
result = find_python_by_version(L"venv");
if (result == NULL) {
configured_value = get_configured_value(config_key);
if (configured_value)
result = find_python_by_version(configured_value);
}
/* Not found a value yet - try by major version.
* If we're looking for an interpreter specified in a shebang line,
* we want to try Python 2 first, then Python 3 (for Unix and backward
@ -1443,7 +1542,8 @@ show_python_list(wchar_t ** argv)
INSTALLED_PYTHON * defpy = locate_python(L"", FALSE);
size_t i = 0;
wchar_t *p = argv[1];
wchar_t *fmt = L"\n -%ls-%d"; /* print VER-BITS */
wchar_t *ver_fmt = L"-%ls-%d";
wchar_t *fmt = L"\n %ls";
wchar_t *defind = L" *"; /* Default indicator */
/*
@ -1452,8 +1552,8 @@ show_python_list(wchar_t ** argv)
*/
fwprintf(stderr,
L"Installed Pythons found by %s Launcher for Windows", argv[0]);
if (!_wcsicmp(p, L"-0p") || !_wcsicmp(p, L"--list-paths")) /* Show path? */
fmt = L"\n -%ls-%d\t%ls"; /* print VER-BITS path */
if (!_wcsicmp(p, L"-0p") || !_wcsicmp(p, L"--list-paths"))
fmt = L"\n %-15ls%ls"; /* include path */
if (num_installed_pythons == 0) /* We have somehow got here without searching for pythons */
locate_all_pythons(); /* Find them, Populates installed_pythons */
@ -1463,9 +1563,22 @@ show_python_list(wchar_t ** argv)
else
{
for (i = 0; i < num_installed_pythons; i++, ip++) {
fwprintf(stdout, fmt, ip->version, ip->bits, ip->executable);
wchar_t version[BUFSIZ];
if (wcscmp(ip->version, L"venv") == 0) {
wcscpy_s(version, BUFSIZ, L"(venv)");
}
else {
swprintf_s(version, BUFSIZ, ver_fmt, ip->version, ip->bits);
}
if (ip->exe_display[0]) {
fwprintf(stdout, fmt, version, ip->exe_display);
}
else {
fwprintf(stdout, fmt, version, ip->executable);
}
/* If there is a default indicate it */
if ((defpy != NULL) && !_wcsicmp(ip->executable, defpy->executable))
if (defpy == ip)
fwprintf(stderr, defind);
}
}
@ -1850,16 +1963,11 @@ installed, use -0 for available pythons", &p[1]);
executable = NULL; /* Info call only */
}
else {
/* Look for an active virtualenv */
executable = find_python_by_venv();
/* If we didn't find one, look for the default Python */
if (executable == NULL) {
ip = locate_python(L"", FALSE);
if (ip == NULL)
error(RC_NO_PYTHON, L"Can't find a default Python.");
executable = ip->executable;
}
/* look for the default Python */
ip = locate_python(L"", FALSE);
if (ip == NULL)
error(RC_NO_PYTHON, L"Can't find a default Python.");
executable = ip->executable;
}
}
if (executable != NULL)