diff --git a/src/client/common/installer/condaInstaller.ts b/src/client/common/installer/condaInstaller.ts index 774cade34457..a20b35e0f110 100644 --- a/src/client/common/installer/condaInstaller.ts +++ b/src/client/common/installer/condaInstaller.ts @@ -5,6 +5,7 @@ import { inject, injectable } from 'inversify'; import { ICondaService, IComponentAdapter } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; +import { getEnvPath } from '../../pythonEnvironments/base/info/env'; import { ModuleInstallerType } from '../../pythonEnvironments/info'; import { ExecutionInfo, IConfigurationService, Product } from '../types'; import { isResource } from '../utils/misc'; @@ -79,7 +80,7 @@ export class CondaInstaller extends ModuleInstaller { const pythonPath = isResource(resource) ? this.serviceContainer.get(IConfigurationService).getSettings(resource).pythonPath - : resource.id ?? ''; + : getEnvPath(resource.path, resource.envPath).path ?? ''; const condaLocatorService = this.serviceContainer.get(IComponentAdapter); const info = await condaLocatorService.getCondaEnvironment(pythonPath); const args = [flags & ModuleInstallFlags.upgrade ? 'update' : 'install']; @@ -132,7 +133,7 @@ export class CondaInstaller extends ModuleInstaller { const condaService = this.serviceContainer.get(IComponentAdapter); const pythonPath = isResource(resource) ? this.serviceContainer.get(IConfigurationService).getSettings(resource).pythonPath - : resource.id ?? ''; + : getEnvPath(resource.path, resource.envPath).path ?? ''; return condaService.isCondaEnvironment(pythonPath); } } diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 5f40fcf263db..1b710a888c99 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -304,7 +304,7 @@ export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment const { path } = getEnvPath(env.executable.filename, env.location); const resolvedEnv: ResolvedEnvironment = { path, - id: getEnvID(path), + id: env.id!, executable: { uri: env.executable.filename === 'python' ? undefined : Uri.file(env.executable.filename), bitness: convertBitness(env.arch), diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts index 9340792a4f4b..2527f18202cd 100644 --- a/src/client/pythonEnvironments/base/info/env.ts +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -198,6 +198,7 @@ function getMinimalPartialInfo(env: string | PythonEnvInfo | BasicEnvInfo): Part return undefined; } return { + id: '', executable: { filename: env, sysPrefix: '', @@ -208,6 +209,7 @@ function getMinimalPartialInfo(env: string | PythonEnvInfo | BasicEnvInfo): Part } if ('executablePath' in env) { return { + id: '', executable: { filename: env.executablePath, sysPrefix: '', @@ -235,7 +237,7 @@ export function getEnvPath(interpreterPath: string, envFolderPath?: string): Env } /** - * Gets unique identifier for an environment. + * Gets general unique identifier for most environments. */ export function getEnvID(interpreterPath: string, envFolderPath?: string): string { return normCasePath(getEnvPath(interpreterPath, envFolderPath).path); @@ -266,7 +268,15 @@ export function areSameEnv( const leftFilename = leftInfo.executable!.filename; const rightFilename = rightInfo.executable!.filename; + if (leftInfo.id && leftInfo.id === rightInfo.id) { + // In case IDs are available, use it. + return true; + } + if (getEnvID(leftFilename, leftInfo.location) === getEnvID(rightFilename, rightInfo.location)) { + // Otherwise use ID function to get the ID. Note ID returned by function may itself change if executable of + // an environment changes, for eg. when conda installs python into the env. So only use it as a fallback if + // ID is not available. return true; } diff --git a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts index c0506d4a06ba..4bfcfac7fc87 100644 --- a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts +++ b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -15,7 +15,12 @@ import { import { buildEnvInfo, comparePythonVersionSpecificity, setEnvDisplayString, getEnvID } from '../../info/env'; import { getEnvironmentDirFromPath, getPythonVersionFromPath } from '../../../common/commonUtils'; import { arePathsSame, getFileInfo, isParentPath } from '../../../common/externalDependencies'; -import { AnacondaCompanyName, Conda, isCondaEnvironment } from '../../../common/environmentManagers/conda'; +import { + AnacondaCompanyName, + Conda, + getCondaInterpreterPath, + isCondaEnvironment, +} from '../../../common/environmentManagers/conda'; import { getPyenvVersionsDir, parsePyenvVersion } from '../../../common/environmentManagers/pyenv'; import { Architecture, getOSType, OSType } from '../../../../common/utils/platform'; import { getPythonVersionFromPath as parsePythonVersionFromPath, parseVersion } from '../../info/pythonVersion'; @@ -57,7 +62,6 @@ export async function resolveBasicEnv(env: BasicEnvInfo): Promise await updateEnvUsingRegistry(resolvedEnv); } setEnvDisplayString(resolvedEnv); - resolvedEnv.id = getEnvID(resolvedEnv.executable.filename, resolvedEnv.location); const { ctime, mtime } = await getFileInfo(resolvedEnv.executable.filename); resolvedEnv.executable.ctime = ctime; resolvedEnv.executable.mtime = mtime; @@ -189,6 +193,13 @@ async function resolveCondaEnv(env: BasicEnvInfo): Promise { if (name) { info.name = name; } + if (env.envPath && path.basename(executable) === executable) { + // For environments without python, set ID using the predicted executable path after python is installed. + // Another alternative could've been to set ID of all conda environments to the environment path, as that + // remains constant even after python installation. + const predictedExecutable = getCondaInterpreterPath(env.envPath); + info.id = getEnvID(predictedExecutable, env.envPath); + } return info; } diff --git a/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/src/client/pythonEnvironments/common/environmentManagers/conda.ts index bdbcb7ea3ac5..0e19c8e94801 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/conda.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -226,14 +226,11 @@ export async function getPythonVersionFromConda(interpreterPath: string): Promis /** * Return the interpreter's filename for the given environment. */ -async function getInterpreterPath(condaEnvironmentPath: string): Promise { +export function getCondaInterpreterPath(condaEnvironmentPath: string): string { // where to find the Python binary within a conda env. const relativePath = getOSType() === OSType.Windows ? 'python.exe' : path.join('bin', 'python'); const filePath = path.join(condaEnvironmentPath, relativePath); - if (await pathExists(filePath)) { - return filePath; - } - return undefined; + return filePath; } // Minimum version number of conda required to be able to use 'conda run' with '--no-capture-output' flag. @@ -494,8 +491,8 @@ export class Conda { */ // eslint-disable-next-line class-methods-use-this public async getInterpreterPathForEnvironment(condaEnv: CondaEnvInfo | { prefix: string }): Promise { - const executablePath = await getInterpreterPath(condaEnv.prefix); - if (executablePath) { + const executablePath = getCondaInterpreterPath(condaEnv.prefix); + if (await pathExists(executablePath)) { traceVerbose('Found executable within conda env', JSON.stringify(condaEnv)); return executablePath; } diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index 80db62f4814b..5dfa54492c6b 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -252,6 +252,7 @@ suite('Proposed Extension API', () => { test('environments: python found', async () => { const expectedEnvs = [ { + id: normCasePath('this/is/a/test/python/path1'), executable: { filename: 'this/is/a/test/python/path1', ctime: 1, @@ -273,6 +274,7 @@ suite('Proposed Extension API', () => { }, }, { + id: normCasePath('this/is/a/test/python/path2'), executable: { filename: 'this/is/a/test/python/path2', ctime: 1, @@ -297,6 +299,7 @@ suite('Proposed Extension API', () => { const envs = [ ...expectedEnvs, { + id: normCasePath('this/is/a/test/python/path3'), executable: { filename: 'this/is/a/test/python/path3', ctime: 1, @@ -343,6 +346,7 @@ suite('Proposed Extension API', () => { searchLocation: Uri.file(workspacePath), }), { + id: normCasePath('this/is/a/test/python/path1'), executable: { filename: 'this/is/a/test/python/path1', ctime: 1, @@ -364,6 +368,7 @@ suite('Proposed Extension API', () => { }, }, { + id: normCasePath('this/is/a/test/python/path2'), executable: { filename: 'this/is/a/test/python/path2', ctime: 1,