Skip to content

Commit ae58f22

Browse files
authored
Fix No such file or directory on conda activate (#18989)
1 parent f37690e commit ae58f22

6 files changed

Lines changed: 128 additions & 13 deletions

File tree

news/2 Fixes/18989.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix error `No such file or directory` on conda activate, and simplify the environment activation code.

src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,35 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman
8181
if (versionInfo.minor >= CondaRequiredMinor) {
8282
// New version.
8383
const interpreterPath = await this.condaService.getInterpreterPathForEnvironment(envInfo);
84-
const condaPath = await this.condaService.getCondaFileFromInterpreter(interpreterPath, envInfo.name);
85-
if (condaPath) {
86-
const activatePath = path
87-
.join(path.dirname(condaPath), 'activate')
88-
.fileToCommandArgumentForPythonExt();
89-
const firstActivate = this.platform.isWindows ? activatePath : `source ${activatePath}`;
90-
return [firstActivate, `conda activate ${condaEnv.toCommandArgumentForPythonExt()}`];
84+
const activatePath = await this.condaService.getActivationScriptFromInterpreter(
85+
interpreterPath,
86+
envInfo.name,
87+
);
88+
// eslint-disable-next-line camelcase
89+
if (activatePath?.path) {
90+
if (this.platform.isWindows) {
91+
return [activatePath.path, `conda activate ${condaEnv.toCommandArgumentForPythonExt()}`];
92+
}
93+
94+
const condaInfo = await this.condaService.getCondaInfo();
95+
96+
if (
97+
activatePath.type !== 'global' ||
98+
// eslint-disable-next-line camelcase
99+
condaInfo?.conda_shlvl === undefined ||
100+
condaInfo.conda_shlvl === -1
101+
) {
102+
// activatePath is not the global activate path, or we don't have a shlvl, or it's -1(conda never sourced).
103+
// and we need to source the activate path.
104+
if (activatePath.path === 'activate') {
105+
return [
106+
`source ${activatePath.path}`,
107+
`conda activate ${condaEnv.toCommandArgumentForPythonExt()}`,
108+
];
109+
}
110+
return [`source ${activatePath.path} ${condaEnv.toCommandArgumentForPythonExt()}`];
111+
}
112+
return [`conda activate ${condaEnv.toCommandArgumentForPythonExt()}`];
91113
}
92114
}
93115
}

src/client/interpreter/contracts.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
PythonLocatorQuery,
99
TriggerRefreshOptions,
1010
} from '../pythonEnvironments/base/locator';
11-
import { CondaEnvironmentInfo } from '../pythonEnvironments/common/environmentManagers/conda';
11+
import { CondaEnvironmentInfo, CondaInfo } from '../pythonEnvironments/common/environmentManagers/conda';
1212
import { EnvironmentType, PythonEnvironment } from '../pythonEnvironments/info';
1313

1414
export type PythonEnvironmentsChangedEvent = {
@@ -59,10 +59,15 @@ export const ICondaService = Symbol('ICondaService');
5959
*/
6060
export interface ICondaService {
6161
getCondaFile(forShellExecution?: boolean): Promise<string>;
62+
getCondaInfo(): Promise<CondaInfo | undefined>;
6263
isCondaAvailable(): Promise<boolean>;
6364
getCondaVersion(): Promise<SemVer | undefined>;
6465
getInterpreterPathForEnvironment(condaEnv: CondaEnvironmentInfo): Promise<string | undefined>;
6566
getCondaFileFromInterpreter(interpreterPath?: string, envName?: string): Promise<string | undefined>;
67+
getActivationScriptFromInterpreter(
68+
interpreterPath?: string,
69+
envName?: string,
70+
): Promise<{ path: string | undefined; type: 'local' | 'global' } | undefined>;
6671
}
6772

6873
export const IInterpreterService = Symbol('IInterpreterService');

src/client/pythonEnvironments/common/environmentManagers/conda.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export type CondaInfo = {
4343
default_prefix?: string; // eslint-disable-line camelcase
4444
root_prefix?: string; // eslint-disable-line camelcase
4545
conda_version?: string; // eslint-disable-line camelcase
46+
conda_shlvl?: number; // eslint-disable-line camelcase
4647
};
4748

4849
type CondaEnvInfo = {

src/client/pythonEnvironments/common/environmentManagers/condaService.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,39 @@ export class CondaService implements ICondaService {
1919
@inject(IFileSystem) private fileSystem: IFileSystem,
2020
) {}
2121

22+
public async getActivationScriptFromInterpreter(
23+
interpreterPath?: string,
24+
envName?: string,
25+
): Promise<{ path: string | undefined; type: 'local' | 'global' } | undefined> {
26+
const condaPath = await this.getCondaFileFromInterpreter(interpreterPath, envName);
27+
28+
const activatePath = (condaPath
29+
? path.join(path.dirname(condaPath), 'activate')
30+
: 'activate'
31+
).fileToCommandArgumentForPythonExt(); // maybe global activate?
32+
33+
// try to find the activate script in the global conda root prefix.
34+
if (this.platform.isLinux || this.platform.isMac) {
35+
const condaInfo = await this.getCondaInfo();
36+
// eslint-disable-next-line camelcase
37+
if (condaInfo?.root_prefix) {
38+
const globalActivatePath = path
39+
// eslint-disable-next-line camelcase
40+
.join(condaInfo.root_prefix, this.platform.virtualEnvBinName, 'activate')
41+
.fileToCommandArgumentForPythonExt();
42+
43+
if (activatePath === globalActivatePath || !(await this.fileSystem.fileExists(activatePath))) {
44+
return {
45+
path: globalActivatePath,
46+
type: 'global',
47+
};
48+
}
49+
}
50+
}
51+
52+
return { path: activatePath, type: 'local' }; // return the default activate script wether it exists or not.
53+
}
54+
2255
/**
2356
* Return the path to the "conda file".
2457
*/
@@ -111,7 +144,7 @@ export class CondaService implements ICondaService {
111144
*/
112145
@cache(60_000)
113146
// eslint-disable-next-line class-methods-use-this
114-
public async _getCondaInfo(): Promise<CondaInfo | undefined> {
147+
public async getCondaInfo(): Promise<CondaInfo | undefined> {
115148
const conda = await Conda.getConda();
116149
return conda?.getInfo();
117150
}

src/test/common/terminals/activation.conda.unit.test.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,17 @@ suite('Terminal Environment Activation conda', () => {
210210
const interpreterPath = path.join('path', 'to', 'interpreter');
211211
const environmentName = 'Env';
212212
const environmentNameHasSpaces = 'Env with spaces';
213-
const testsForActivationUsingInterpreterPath = [
213+
const testsForActivationUsingInterpreterPath: {
214+
testName: string;
215+
envName: string;
216+
condaScope?: 'global' | 'local';
217+
condaInfo?: {
218+
// eslint-disable-next-line camelcase
219+
conda_shlvl?: number;
220+
};
221+
expectedResult: string[];
222+
isWindows: boolean;
223+
}[] = [
214224
{
215225
testName:
216226
'Activation provides correct activation commands (windows) after 4.4.0 given interpreter path is provided, with no spaces in env name',
@@ -222,7 +232,7 @@ suite('Terminal Environment Activation conda', () => {
222232
testName:
223233
'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, with no spaces in env name',
224234
envName: environmentName,
225-
expectedResult: ['source path/to/activate', 'conda activate Env'],
235+
expectedResult: ['source path/to/activate Env'],
226236
isWindows: false,
227237
},
228238
{
@@ -236,7 +246,7 @@ suite('Terminal Environment Activation conda', () => {
236246
testName:
237247
'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, with spaces in env name',
238248
envName: environmentNameHasSpaces,
239-
expectedResult: ['source path/to/activate', 'conda activate "Env with spaces"'],
249+
expectedResult: ['source path/to/activate "Env with spaces"'],
240250
isWindows: false,
241251
},
242252
{
@@ -250,7 +260,37 @@ suite('Terminal Environment Activation conda', () => {
250260
testName:
251261
'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, and no env name',
252262
envName: '',
253-
expectedResult: ['source path/to/activate', `conda activate .`],
263+
expectedResult: ['source path/to/activate .'],
264+
isWindows: false,
265+
},
266+
{
267+
testName:
268+
'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, global conda, conda not sourced and with no spaces in env name',
269+
envName: environmentName,
270+
expectedResult: ['source path/to/activate Env'],
271+
condaScope: 'global',
272+
isWindows: false,
273+
},
274+
{
275+
testName:
276+
'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, global conda, conda sourced and with no spaces in env name',
277+
envName: environmentName,
278+
expectedResult: ['conda activate Env'],
279+
condaInfo: {
280+
conda_shlvl: 1,
281+
},
282+
condaScope: 'global',
283+
isWindows: false,
284+
},
285+
{
286+
testName:
287+
'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, local conda, conda sourced and with no spaces in env name',
288+
envName: environmentName,
289+
expectedResult: ['source path/to/activate Env'],
290+
condaInfo: {
291+
conda_shlvl: 1,
292+
},
293+
condaScope: 'local',
254294
isWindows: false,
255295
},
256296
];
@@ -272,13 +312,26 @@ suite('Terminal Environment Activation conda', () => {
272312
condaService
273313
.setup((c) => c.getCondaFileFromInterpreter(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
274314
.returns(() => Promise.resolve(interpreterPath));
315+
condaService
316+
.setup((c) => c.getActivationScriptFromInterpreter(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
317+
.returns(() =>
318+
Promise.resolve({
319+
path: path.join(path.dirname(interpreterPath), 'activate').fileToCommandArgumentForPythonExt(),
320+
type: testParams.condaScope ?? 'local',
321+
}),
322+
);
323+
324+
condaService.setup((c) => c.getCondaInfo()).returns(() => Promise.resolve(testParams.condaInfo));
325+
326+
// getActivationScriptFromInterpreter
275327

276328
const provider = new CondaActivationCommandProvider(
277329
condaService.object,
278330
platformService.object,
279331
configService.object,
280332
componentAdapter.object,
281333
);
334+
282335
const activationCommands = await provider.getActivationCommands(undefined, TerminalShellType.bash);
283336

284337
expect(activationCommands).to.deep.equal(testParams.expectedResult, 'Incorrect Activation command');

0 commit comments

Comments
 (0)