forked from DonJayamanne/pythonVSCode
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Identify conda and windows store environments from given interpreter path #13589
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
b8cf6d7
Initial commit
karthiknadig 5bfb4fa
Implement Windows store identifier
karthiknadig 1a50b5c
Fix test errors.
karthiknadig 109c0b3
Add more notes on windows store identifier.
karthiknadig 15b74f2
minor tweaks.
karthiknadig dd118ec
Address comments.
karthiknadig 69ba610
Clean up
karthiknadig 0341c10
Missed suggestions.
karthiknadig File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
164 changes: 164 additions & 0 deletions
164
src/client/pythonEnvironments/common/environmentIdentifier.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
import * as fsapi from 'fs-extra'; | ||
import * as path from 'path'; | ||
import { traceWarning } from '../../common/logger'; | ||
import { createDeferred } from '../../common/utils/async'; | ||
import { getEnvironmentVariable } from '../../common/utils/platform'; | ||
import { EnvironmentType } from '../info'; | ||
|
||
function pathExists(absPath: string): Promise<boolean> { | ||
karrtikr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const deferred = createDeferred<boolean>(); | ||
fsapi.exists(absPath, (result) => { | ||
deferred.resolve(result); | ||
}); | ||
return deferred.promise; | ||
} | ||
|
||
function or(...arr: boolean[]): boolean { | ||
return arr.includes(true); | ||
} | ||
|
||
/** | ||
* Checks if the given interpreter path belongs to a conda environment. Using | ||
* known folder layout, and presence of 'conda-meta' directory. | ||
* @param {string} interpreterPath: Absolute path to any python interpreter. | ||
* | ||
* Remarks: This is what we will use to begin with. Another approach we can take | ||
* here is to parse ~/.conda/environments.txt. This file will have list of conda | ||
* environments. We can compare the interpreter path against the paths in that file. | ||
* We don't want to rely on this file because it is an implementation detail of | ||
* conda. If it turns out that the layout based identification is not sufficient | ||
* that is the next alternative that is cheap. | ||
* | ||
* sample content of the ~/.conda/environments.txt: | ||
* C:\envs\\myenv | ||
* C:\ProgramData\Miniconda3 | ||
* | ||
* Yet another approach is to use `conda env list --json` and compare the returned env | ||
* list to see if the given interpreter path belongs to any of the returned environments. | ||
* This approach is heavy, and involves running a binary. For now we decided not to | ||
* take this approach, since it does not look like we need it. | ||
* | ||
* sample output from `conda env list --json`: | ||
* conda env list --json | ||
* { | ||
* "envs": [ | ||
* "C:\\envs\\myenv", | ||
* "C:\\ProgramData\\Miniconda3" | ||
* ] | ||
* } | ||
*/ | ||
async function isCondaEnvironment(interpreterPath: string): Promise<boolean> { | ||
const condaMetaDir = 'conda-meta'; | ||
|
||
// Check if the conda-meta directory is in the same directory as the interpreter. | ||
// This layout is common in Windows. | ||
// env | ||
// |__ conda-meta <--- check if this directory exists | ||
// |__ python.exe <--- interpreterPath | ||
const conda_env_dir_1 = path.join(path.dirname(interpreterPath), condaMetaDir); | ||
karthiknadig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Check if the conda-meta directory is in the parent directory relative to the interpreter. | ||
// This layout is common on linux/Mac. | ||
// env | ||
// |__ conda-meta <--- check if this directory exists | ||
// |__ bin | ||
// |__ python <--- interpreterPath | ||
const conda_env_dir_2 = path.join(path.dirname(path.dirname(interpreterPath)), condaMetaDir); | ||
karthiknadig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return or(await pathExists(conda_env_dir_1), await pathExists(conda_env_dir_2)); | ||
karthiknadig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/** | ||
* Checks if the given interpreter belongs to Windows Store Python environment. | ||
* @param interpreterPath: Absolute path to any python interpreter. | ||
* | ||
* Remarks: | ||
* 1. Checking if the path includes 'Microsoft\WindowsApps`, `Program Files\WindowsApps`, is | ||
* NOT enough. In WSL, /mnt/c/users/user/AppData/Local/Microsoft/WindowsApps is available as a search | ||
* path. It is possible to get a false positive for that path. So the comparison should check if the | ||
* absolute path to 'WindowsApps' directory is present in the given interpreter path. The WSL path to | ||
* 'WindowsApps' is not a valid path to access, Windows Store Python. | ||
* | ||
* 2. 'startsWith' comparison may not be right, user can provide '\\?\C:\users\' style long paths in windows. | ||
* | ||
* 3. A limitation of the checks here is that they don't handle 8.3 style windows paths. | ||
* For example, | ||
* C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE | ||
* is the shortened form of | ||
* C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe | ||
* | ||
* The correct way to compare these would be to always convert given paths to long path (or to short path). | ||
* For either approach to work correctly you need actual file to exist, and accessible from the user's | ||
* account. | ||
* | ||
* To convert to short path without using N-API in node would be to use this command. This is very expensive: | ||
* > cmd /c for %A in ("C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe") do @echo %~sA | ||
karthiknadig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* The above command will print out this: | ||
* C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE | ||
* | ||
* If we go down the N-API route, use node-ffi and either call GetShortPathNameW or GetLongPathNameW from, | ||
karthiknadig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* Kernel32 to convert between the two path variants. | ||
* | ||
*/ | ||
async function isWindowsStoreEnvironment(interpreterPath: string): Promise<boolean> { | ||
const pythonPathToCompare = path.normalize(interpreterPath).toUpperCase(); | ||
const localAppDataStorePath = path | ||
.join(getEnvironmentVariable('LOCALAPPDATA') || '', 'Microsoft', 'WindowsApps') | ||
.normalize() | ||
.toUpperCase(); | ||
if (pythonPathToCompare.includes(localAppDataStorePath)) { | ||
return true; | ||
} | ||
|
||
// Program Files store path is a forbidden path. Only admins and system has access this path. | ||
// We should never have to look at this path or even execute python from this path. | ||
const programFilesStorePath = path | ||
.join(getEnvironmentVariable('ProgramFiles') || 'Program Files', 'WindowsApps') | ||
.normalize() | ||
.toUpperCase(); | ||
if (pythonPathToCompare.includes(programFilesStorePath)) { | ||
traceWarning('isWindowsStoreEnvironment called with Program Files store path.'); | ||
return true; | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* Returns environment type. | ||
* @param {string} interpreterPath : Absolute path to the python interpreter binary. | ||
* @returns {EnvironmentType} | ||
* | ||
* Remarks: This is the order of detection based on how the various distributions and tools | ||
* configure the environment, and the fall back for identification. | ||
* Top level we have the following environment types, since they leave a unique signature | ||
* in the environment or * use a unique path for the environments they create. | ||
* 1. Conda | ||
* 2. Windows Store | ||
* 3. PipEnv | ||
* 4. Pyenv | ||
* 5. Poetry | ||
* | ||
* Next level we have the following virtual environment tools. The are here because they | ||
* are consumed by the tools above, and can also be used independently. | ||
* 1. venv | ||
* 2. virtualenvwrapper | ||
* 3. virtualenv | ||
* | ||
* Last category is globally installed python, or system python. | ||
*/ | ||
export async function identifyEnvironment(interpreterPath: string): Promise<EnvironmentType> { | ||
if (await isCondaEnvironment(interpreterPath)) { | ||
return EnvironmentType.Conda; | ||
karthiknadig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
if (await isWindowsStoreEnvironment(interpreterPath)) { | ||
return EnvironmentType.WindowsStore; | ||
} | ||
|
||
// additional identifiers go here | ||
|
||
return EnvironmentType.Unknown; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
import * as assert from 'assert'; | ||
import * as path from 'path'; | ||
import * as sinon from 'sinon'; | ||
import * as platformApis from '../../../client/common/utils/platform'; | ||
import { identifyEnvironment } from '../../../client/pythonEnvironments/common/environmentIdentifier'; | ||
import { EnvironmentType } from '../../../client/pythonEnvironments/info'; | ||
|
||
suite('Environment Identifier', () => { | ||
const testLayoutsRoot = path.join( | ||
__dirname, | ||
'..', | ||
'..', | ||
'..', | ||
'..', | ||
'src', | ||
'test', | ||
'pythonEnvironments', | ||
'common', | ||
'envlayouts' | ||
); | ||
suite('Conda', () => { | ||
test('Conda layout with conda-meta and python binary in the same directory', async () => { | ||
const interpreterPath: string = path.join(testLayoutsRoot, 'conda1', 'python.exe'); | ||
const envType: EnvironmentType = await identifyEnvironment(interpreterPath); | ||
assert.deepEqual(envType, EnvironmentType.Conda); | ||
}); | ||
test('Conda layout with conda-meta and python binary in a sub directory', async () => { | ||
const interpreterPath: string = path.join(testLayoutsRoot, 'conda2', 'bin', 'python'); | ||
const envType: EnvironmentType = await identifyEnvironment(interpreterPath); | ||
assert.deepEqual(envType, EnvironmentType.Conda); | ||
}); | ||
}); | ||
|
||
suite('Windows Store', () => { | ||
let getEnvVar: sinon.SinonStub; | ||
const fakeLocalAppDataPath = 'X:\\users\\user\\AppData\\Local'; | ||
const fakeProgramFilesPath = 'X:\\Program Files'; | ||
const executable = ['python.exe', 'python3.exe', 'python3.8.exe']; | ||
suiteSetup(() => { | ||
getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); | ||
getEnvVar.withArgs('LOCALAPPDATA').returns(fakeLocalAppDataPath); | ||
getEnvVar.withArgs('ProgramFiles').returns(fakeProgramFilesPath); | ||
}); | ||
suiteTeardown(() => { | ||
getEnvVar.restore(); | ||
}); | ||
executable.forEach((exe) => { | ||
test(`Path to local app data windows store interpreter (${exe})`, async () => { | ||
const interpreterPath = path.join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe); | ||
const envType: EnvironmentType = await identifyEnvironment(interpreterPath); | ||
assert.deepEqual(envType, EnvironmentType.WindowsStore); | ||
}); | ||
test(`Path to local app data windows store interpreter app sub-directory (${exe})`, async () => { | ||
const interpreterPath = path.join( | ||
fakeLocalAppDataPath, | ||
'Microsoft', | ||
'WindowsApps', | ||
'PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0', | ||
exe | ||
); | ||
const envType: EnvironmentType = await identifyEnvironment(interpreterPath); | ||
assert.deepEqual(envType, EnvironmentType.WindowsStore); | ||
}); | ||
test(`Path to program files windows store interpreter app sub-directory (${exe})`, async () => { | ||
const interpreterPath = path.join( | ||
fakeProgramFilesPath, | ||
'WindowsApps', | ||
'PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0', | ||
exe | ||
); | ||
const envType: EnvironmentType = await identifyEnvironment(interpreterPath); | ||
assert.deepEqual(envType, EnvironmentType.WindowsStore); | ||
}); | ||
test(`Local app data not set (${exe})`, async () => { | ||
getEnvVar.withArgs('LOCALAPPDATA').returns(undefined); | ||
const interpreterPath = path.join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe); | ||
const envType: EnvironmentType = await identifyEnvironment(interpreterPath); | ||
assert.deepEqual(envType, EnvironmentType.WindowsStore); | ||
}); | ||
test(`Program files app data not set (${exe})`, async () => { | ||
getEnvVar.withArgs('ProgramFiles').returns(undefined); | ||
const interpreterPath = path.join( | ||
fakeProgramFilesPath, | ||
'WindowsApps', | ||
'PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0', | ||
exe | ||
); | ||
const envType: EnvironmentType = await identifyEnvironment(interpreterPath); | ||
assert.deepEqual(envType, EnvironmentType.WindowsStore); | ||
}); | ||
test(`Path using forward slashes (${exe})`, async () => { | ||
const interpreterPath = path | ||
.join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe) | ||
.replace('\\', '/'); | ||
const envType: EnvironmentType = await identifyEnvironment(interpreterPath); | ||
assert.deepEqual(envType, EnvironmentType.WindowsStore); | ||
}); | ||
test(`Path using long path style slashes (${exe})`, async () => { | ||
const interpreterPath = path | ||
.join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe) | ||
.replace('\\', '/'); | ||
const envType: EnvironmentType = await identifyEnvironment(`\\\\?\\${interpreterPath}`); | ||
assert.deepEqual(envType, EnvironmentType.WindowsStore); | ||
}); | ||
}); | ||
}); | ||
}); |
1 change: 1 addition & 0 deletions
1
src/test/pythonEnvironments/common/envlayouts/conda1/conda-meta/history
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Usually contains command that was used to create or update the conda environment with time stamps. |
1 change: 1 addition & 0 deletions
1
src/test/pythonEnvironments/common/envlayouts/conda1/python.exe
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Not real python exe |
1 change: 1 addition & 0 deletions
1
src/test/pythonEnvironments/common/envlayouts/conda2/bin/python
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Not a real python binary |
1 change: 1 addition & 0 deletions
1
src/test/pythonEnvironments/common/envlayouts/conda2/conda-meta/history
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Usually contains command that was used to create or update the conda environment with time stamps. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.