diff --git a/src/client/pythonEnvironments/common/environmentIdentifier.ts b/src/client/pythonEnvironments/common/environmentIdentifier.ts index 2834b5642cfa..b56c84e2b73e 100644 --- a/src/client/pythonEnvironments/common/environmentIdentifier.ts +++ b/src/client/pythonEnvironments/common/environmentIdentifier.ts @@ -3,6 +3,7 @@ import { isCondaEnvironment } from '../discovery/locators/services/condaLocator'; import { isPipenvEnvironment } from '../discovery/locators/services/pipEnvHelper'; +import { isPyenvEnvironment } from '../discovery/locators/services/pyenvLocator'; import { isVenvEnvironment } from '../discovery/locators/services/venvLocator'; import { isVirtualenvEnvironment } from '../discovery/locators/services/virtualenvLocator'; import { isVirtualenvwrapperEnvironment } from '../discovery/locators/services/virtualenvwrapperLocator'; @@ -45,6 +46,10 @@ export async function identifyEnvironment(interpreterPath: string): Promise { + // Check if the pyenv environment variables exist: PYENV on Windows, PYENV_ROOT on Unix. + // They contain the path to pyenv's installation folder. + // If they don't exist, use the default path: ~/.pyenv/pyenv-win on Windows, ~/.pyenv on Unix. + // If the interpreter path starts with the path to the pyenv folder, then it is a pyenv environment. + // See https://github.com/pyenv/pyenv#locating-the-python-installation for general usage, + // And https://github.com/pyenv-win/pyenv-win for Windows specifics. + const isWindows = getOSType() === OSType.Windows; + const envVariable = isWindows ? 'PYENV' : 'PYENV_ROOT'; + + let pyenvDir = getEnvironmentVariable(envVariable); + let pathToCheck = interpreterPath; + + if (!pyenvDir) { + const homeDir = getUserHomeDir() || ''; + pyenvDir = isWindows ? path.join(homeDir, '.pyenv', 'pyenv-win') : path.join(homeDir, '.pyenv'); + } + + if (!await pathExists(pyenvDir)) { + return false; + } + + if (!pyenvDir.endsWith(path.sep)) { + pyenvDir += path.sep; + } + + if (getOSType() === OSType.Windows) { + pyenvDir = pyenvDir.toUpperCase(); + pathToCheck = pathToCheck.toUpperCase(); + } + + return pathToCheck.startsWith(pyenvDir); +} diff --git a/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts index 6f6bd4cbc818..a2f00aa4d3e9 100644 --- a/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts @@ -133,6 +133,92 @@ suite('Environment Identifier', () => { }); }); + suite('Pyenv', () => { + let getEnvVarStub: sinon.SinonStub; + let getOsTypeStub: sinon.SinonStub; + let getUserHomeDirStub: sinon.SinonStub; + + suiteSetup(() => { + getEnvVarStub = sinon.stub(platformApis, 'getEnvironmentVariable'); + getOsTypeStub = sinon.stub(platformApis, 'getOSType'); + getUserHomeDirStub = sinon.stub(platformApis, 'getUserHomeDir'); + }); + + suiteTeardown(() => { + getEnvVarStub.restore(); + getOsTypeStub.restore(); + getUserHomeDirStub.restore(); + }); + + test('PYENV_ROOT is not set on non-Windows, fallback to the default value ~/.pyenv', async function () { + if (getOSTypeForTest() === OSType.Windows) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + + const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'pyenv1', '.pyenv', 'versions', '3.6.9', 'bin', 'python'); + + getUserHomeDirStub.returns(path.join(TEST_LAYOUT_ROOT, 'pyenv1')); + getEnvVarStub.withArgs('PYENV_ROOT').returns(undefined); + + const envType: EnvironmentType = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, EnvironmentType.Pyenv); + + return undefined; + }); + + test('PYENV is not set on Windows, fallback to the default value %USERPROFILE%\\.pyenv\\pyenv-win', async function () { + if (getOSTypeForTest() !== OSType.Windows) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + + const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'pyenv2', '.pyenv', 'pyenv-win', 'versions', '3.6.9', 'bin', 'python.exe'); + + getUserHomeDirStub.returns(path.join(TEST_LAYOUT_ROOT, 'pyenv2')); + getEnvVarStub.withArgs('PYENV').returns(undefined); + getOsTypeStub.returns(platformApis.OSType.Windows); + + const envType: EnvironmentType = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, EnvironmentType.Pyenv); + + return undefined; + }); + + test('PYENV_ROOT is set to a custom value on non-Windows', async function () { + if (getOSTypeForTest() === OSType.Windows) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + + const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'pyenv3', 'versions', '3.6.9', 'bin', 'python'); + + getEnvVarStub.withArgs('PYENV_ROOT').returns(path.join(TEST_LAYOUT_ROOT, 'pyenv3')); + + const envType: EnvironmentType = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, EnvironmentType.Pyenv); + + return undefined; + }); + + test('PYENV is set to a custom value on Windows', async function () { + if (getOSTypeForTest() !== OSType.Windows) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + + const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'pyenv3', 'versions', '3.6.9', 'bin', 'python.exe'); + + getEnvVarStub.withArgs('PYENV').returns(path.join(TEST_LAYOUT_ROOT, 'pyenv3')); + getOsTypeStub.returns(platformApis.OSType.Windows); + + const envType: EnvironmentType = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, EnvironmentType.Pyenv); + + return undefined; + }); + }); + suite('Venv', () => { test('Pyvenv.cfg is in the same directory as the interpreter', async () => { const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'venv1', 'python'); diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenv1/.pyenv/versions/3.6.9/bin/python b/src/test/pythonEnvironments/common/envlayouts/pyenv1/.pyenv/versions/3.6.9/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenv2/.pyenv/pyenv-win/versions/3.6.9/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/pyenv2/.pyenv/pyenv-win/versions/3.6.9/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python b/src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/discovery/locators/pyenvLocator.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/pyenvLocator.unit.test.ts new file mode 100644 index 000000000000..7cf2614f14b2 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/pyenvLocator.unit.test.ts @@ -0,0 +1,106 @@ +// 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 platformUtils from '../../../../client/common/utils/platform'; +import * as fileUtils from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { isPyenvEnvironment } from '../../../../client/pythonEnvironments/discovery/locators/services/pyenvLocator'; + +suite('Pyenv Locator Tests', () => { + const home = platformUtils.getUserHomeDir() || ''; + let getEnvVariableStub: sinon.SinonStub; + let pathExistsStub:sinon.SinonStub; + let getOsTypeStub: sinon.SinonStub; + + setup(() => { + getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable'); + getOsTypeStub = sinon.stub(platformUtils, 'getOSType'); + pathExistsStub = sinon.stub(fileUtils, 'pathExists'); + }); + + teardown(() => { + getEnvVariableStub.restore(); + pathExistsStub.restore(); + getOsTypeStub.restore(); + }); + + type PyenvUnitTestData = { + testTitle: string, + interpreterPath: string, + pyenvEnvVar?: string, + osType: platformUtils.OSType, + }; + + const testData: PyenvUnitTestData[] = [ + { + testTitle: 'undefined', + interpreterPath: path.join(home, '.pyenv', 'versions', '3.8.0', 'bin', 'python'), + osType: platformUtils.OSType.Linux, + }, + { + testTitle: 'undefined', + interpreterPath: path.join(home, '.pyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'), + osType: platformUtils.OSType.Windows, + }, + { + testTitle: 'its default value', + interpreterPath: path.join(home, '.pyenv', 'versions', '3.8.0', 'bin', 'python'), + pyenvEnvVar: path.join(home, '.pyenv'), + osType: platformUtils.OSType.Linux, + }, + { + testTitle: 'its default value', + interpreterPath: path.join(home, '.pyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'), + pyenvEnvVar: path.join(home, '.pyenv', 'pyenv-win'), + osType: platformUtils.OSType.Windows, + }, + { + testTitle: 'a custom value', + interpreterPath: path.join('path', 'to', 'mypyenv', 'versions', '3.8.0', 'bin', 'python'), + pyenvEnvVar: path.join('path', 'to', 'mypyenv'), + osType: platformUtils.OSType.Linux, + }, + { + testTitle: 'a custom value', + interpreterPath: path.join('path', 'to', 'mypyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'), + pyenvEnvVar: path.join('path', 'to', 'mypyenv', 'pyenv-win'), + osType: platformUtils.OSType.Windows, + }, + ]; + + testData.forEach(({ + testTitle, interpreterPath, pyenvEnvVar, osType, + }) => { + test(`The environment variable is set to ${testTitle} on ${osType}, and the interpreter path is in a subfolder of the pyenv folder`, async () => { + getEnvVariableStub.withArgs('PYENV_ROOT').returns(pyenvEnvVar); + getEnvVariableStub.withArgs('PYENV').returns(pyenvEnvVar); + getOsTypeStub.returns(osType); + pathExistsStub.resolves(true); + + const result = await isPyenvEnvironment(interpreterPath); + + assert.strictEqual(result, true); + }); + }); + + test('The pyenv directory does not exist', async () => { + const interpreterPath = path.join('path', 'to', 'python'); + + pathExistsStub.resolves(false); + + const result = await isPyenvEnvironment(interpreterPath); + + assert.strictEqual(result, false); + }); + + test('The interpreter path is not in a subfolder of the pyenv folder', async () => { + const interpreterPath = path.join('path', 'to', 'python'); + + pathExistsStub.resolves(true); + + const result = await isPyenvEnvironment(interpreterPath); + + assert.strictEqual(result, false); + }); +});