|  | 
|  | 1 | +// Copyright (c) Microsoft Corporation. All rights reserved. | 
|  | 2 | +// Licensed under the MIT License. | 
|  | 3 | + | 
|  | 4 | +import * as fsapi from 'fs-extra'; | 
|  | 5 | +import * as path from 'path'; | 
|  | 6 | +import { traceWarning } from '../../common/logger'; | 
|  | 7 | +import { createDeferred } from '../../common/utils/async'; | 
|  | 8 | +import { getEnvironmentVariable } from '../../common/utils/platform'; | 
|  | 9 | +import { EnvironmentType } from '../info'; | 
|  | 10 | + | 
|  | 11 | +function pathExists(absPath: string): Promise<boolean> { | 
|  | 12 | + const deferred = createDeferred<boolean>(); | 
|  | 13 | + fsapi.exists(absPath, (result) => { | 
|  | 14 | + deferred.resolve(result); | 
|  | 15 | + }); | 
|  | 16 | + return deferred.promise; | 
|  | 17 | +} | 
|  | 18 | + | 
|  | 19 | +/** | 
|  | 20 | + * Checks if the given interpreter path belongs to a conda environment. Using | 
|  | 21 | + * known folder layout, and presence of 'conda-meta' directory. | 
|  | 22 | + * @param {string} interpreterPath: Absolute path to any python interpreter. | 
|  | 23 | + * | 
|  | 24 | + * Remarks: This is what we will use to begin with. Another approach we can take | 
|  | 25 | + * here is to parse ~/.conda/environments.txt. This file will have list of conda | 
|  | 26 | + * environments. We can compare the interpreter path against the paths in that file. | 
|  | 27 | + * We don't want to rely on this file because it is an implementation detail of | 
|  | 28 | + * conda. If it turns out that the layout based identification is not sufficient | 
|  | 29 | + * that is the next alternative that is cheap. | 
|  | 30 | + * | 
|  | 31 | + * sample content of the ~/.conda/environments.txt: | 
|  | 32 | + * C:\envs\\myenv | 
|  | 33 | + * C:\ProgramData\Miniconda3 | 
|  | 34 | + * | 
|  | 35 | + * Yet another approach is to use `conda env list --json` and compare the returned env | 
|  | 36 | + * list to see if the given interpreter path belongs to any of the returned environments. | 
|  | 37 | + * This approach is heavy, and involves running a binary. For now we decided not to | 
|  | 38 | + * take this approach, since it does not look like we need it. | 
|  | 39 | + * | 
|  | 40 | + * sample output from `conda env list --json`: | 
|  | 41 | + * conda env list --json | 
|  | 42 | + * { | 
|  | 43 | + * "envs": [ | 
|  | 44 | + * "C:\\envs\\myenv", | 
|  | 45 | + * "C:\\ProgramData\\Miniconda3" | 
|  | 46 | + * ] | 
|  | 47 | + * } | 
|  | 48 | + */ | 
|  | 49 | +async function isCondaEnvironment(interpreterPath: string): Promise<boolean> { | 
|  | 50 | + const condaMetaDir = 'conda-meta'; | 
|  | 51 | + | 
|  | 52 | + // Check if the conda-meta directory is in the same directory as the interpreter. | 
|  | 53 | + // This layout is common in Windows. | 
|  | 54 | + // env | 
|  | 55 | + // |__ conda-meta <--- check if this directory exists | 
|  | 56 | + // |__ python.exe <--- interpreterPath | 
|  | 57 | + const condaEnvDir1 = path.join(path.dirname(interpreterPath), condaMetaDir); | 
|  | 58 | + | 
|  | 59 | + // Check if the conda-meta directory is in the parent directory relative to the interpreter. | 
|  | 60 | + // This layout is common on linux/Mac. | 
|  | 61 | + // env | 
|  | 62 | + // |__ conda-meta <--- check if this directory exists | 
|  | 63 | + // |__ bin | 
|  | 64 | + // |__ python <--- interpreterPath | 
|  | 65 | + const condaEnvDir2 = path.join(path.dirname(path.dirname(interpreterPath)), condaMetaDir); | 
|  | 66 | + | 
|  | 67 | + return [await pathExists(condaEnvDir1), await pathExists(condaEnvDir2)].includes(true); | 
|  | 68 | +} | 
|  | 69 | + | 
|  | 70 | +/** | 
|  | 71 | + * Checks if the given interpreter belongs to Windows Store Python environment. | 
|  | 72 | + * @param interpreterPath: Absolute path to any python interpreter. | 
|  | 73 | + * | 
|  | 74 | + * Remarks: | 
|  | 75 | + * 1. Checking if the path includes 'Microsoft\WindowsApps`, `Program Files\WindowsApps`, is | 
|  | 76 | + * NOT enough. In WSL, /mnt/c/users/user/AppData/Local/Microsoft/WindowsApps is available as a search | 
|  | 77 | + * path. It is possible to get a false positive for that path. So the comparison should check if the | 
|  | 78 | + * absolute path to 'WindowsApps' directory is present in the given interpreter path. The WSL path to | 
|  | 79 | + * 'WindowsApps' is not a valid path to access, Windows Store Python. | 
|  | 80 | + * | 
|  | 81 | + * 2. 'startsWith' comparison may not be right, user can provide '\\?\C:\users\' style long paths in windows. | 
|  | 82 | + * | 
|  | 83 | + * 3. A limitation of the checks here is that they don't handle 8.3 style windows paths. | 
|  | 84 | + * For example, | 
|  | 85 | + * C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE | 
|  | 86 | + * is the shortened form of | 
|  | 87 | + * C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe | 
|  | 88 | + * | 
|  | 89 | + * The correct way to compare these would be to always convert given paths to long path (or to short path). | 
|  | 90 | + * For either approach to work correctly you need actual file to exist, and accessible from the user's | 
|  | 91 | + * account. | 
|  | 92 | + * | 
|  | 93 | + * To convert to short path without using N-API in node would be to use this command. This is very expensive: | 
|  | 94 | + * > cmd /c for %A in ("C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe") do @echo %~sA | 
|  | 95 | + * The above command will print out this: | 
|  | 96 | + * C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE | 
|  | 97 | + * | 
|  | 98 | + * If we go down the N-API route, use node-ffi and either call GetShortPathNameW or GetLongPathNameW from, | 
|  | 99 | + * Kernel32 to convert between the two path variants. | 
|  | 100 | + * | 
|  | 101 | + */ | 
|  | 102 | +async function isWindowsStoreEnvironment(interpreterPath: string): Promise<boolean> { | 
|  | 103 | + const pythonPathToCompare = path.normalize(interpreterPath).toUpperCase(); | 
|  | 104 | + const localAppDataStorePath = path | 
|  | 105 | + .join(getEnvironmentVariable('LOCALAPPDATA') || '', 'Microsoft', 'WindowsApps') | 
|  | 106 | + .normalize() | 
|  | 107 | + .toUpperCase(); | 
|  | 108 | + if (pythonPathToCompare.includes(localAppDataStorePath)) { | 
|  | 109 | + return true; | 
|  | 110 | + } | 
|  | 111 | + | 
|  | 112 | + // Program Files store path is a forbidden path. Only admins and system has access this path. | 
|  | 113 | + // We should never have to look at this path or even execute python from this path. | 
|  | 114 | + const programFilesStorePath = path | 
|  | 115 | + .join(getEnvironmentVariable('ProgramFiles') || 'Program Files', 'WindowsApps') | 
|  | 116 | + .normalize() | 
|  | 117 | + .toUpperCase(); | 
|  | 118 | + if (pythonPathToCompare.includes(programFilesStorePath)) { | 
|  | 119 | + traceWarning('isWindowsStoreEnvironment called with Program Files store path.'); | 
|  | 120 | + return true; | 
|  | 121 | + } | 
|  | 122 | + return false; | 
|  | 123 | +} | 
|  | 124 | + | 
|  | 125 | +/** | 
|  | 126 | + * Returns environment type. | 
|  | 127 | + * @param {string} interpreterPath : Absolute path to the python interpreter binary. | 
|  | 128 | + * @returns {EnvironmentType} | 
|  | 129 | + * | 
|  | 130 | + * Remarks: This is the order of detection based on how the various distributions and tools | 
|  | 131 | + * configure the environment, and the fall back for identification. | 
|  | 132 | + * Top level we have the following environment types, since they leave a unique signature | 
|  | 133 | + * in the environment or * use a unique path for the environments they create. | 
|  | 134 | + * 1. Conda | 
|  | 135 | + * 2. Windows Store | 
|  | 136 | + * 3. PipEnv | 
|  | 137 | + * 4. Pyenv | 
|  | 138 | + * 5. Poetry | 
|  | 139 | + * | 
|  | 140 | + * Next level we have the following virtual environment tools. The are here because they | 
|  | 141 | + * are consumed by the tools above, and can also be used independently. | 
|  | 142 | + * 1. venv | 
|  | 143 | + * 2. virtualenvwrapper | 
|  | 144 | + * 3. virtualenv | 
|  | 145 | + * | 
|  | 146 | + * Last category is globally installed python, or system python. | 
|  | 147 | + */ | 
|  | 148 | +export async function identifyEnvironment(interpreterPath: string): Promise<EnvironmentType> { | 
|  | 149 | + if (await isCondaEnvironment(interpreterPath)) { | 
|  | 150 | + return EnvironmentType.Conda; | 
|  | 151 | + } | 
|  | 152 | + | 
|  | 153 | + if (await isWindowsStoreEnvironment(interpreterPath)) { | 
|  | 154 | + return EnvironmentType.WindowsStore; | 
|  | 155 | + } | 
|  | 156 | + | 
|  | 157 | + // additional identifiers go here | 
|  | 158 | + | 
|  | 159 | + return EnvironmentType.Unknown; | 
|  | 160 | +} | 
0 commit comments