Skip to content

Commit 0bd3547

Browse files
committed
bug fix: enhance conda environment management with sourcing status and updated shell activation support
1 parent 6173146 commit 0bd3547

File tree

4 files changed

+520
-188
lines changed

4 files changed

+520
-188
lines changed

src/managers/conda/condaEnvManager.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { traceError } from '../../common/logging';
2424
import { createDeferred, Deferred } from '../../common/utils/deferred';
2525
import { showErrorMessage, withProgress } from '../../common/window.apis';
2626
import { NativePythonFinder } from '../common/nativePythonFinder';
27+
import { CondaSourcingStatus } from './condaSourcingUtils';
2728
import {
2829
checkForNoPythonCondaEnvironment,
2930
clearCondaCache,
@@ -52,6 +53,8 @@ export class CondaEnvManager implements EnvironmentManager, Disposable {
5253
private readonly _onDidChangeEnvironments = new EventEmitter<DidChangeEnvironmentsEventArgs>();
5354
public readonly onDidChangeEnvironments = this._onDidChangeEnvironments.event;
5455

56+
public sourcingInformation: CondaSourcingStatus | undefined;
57+
5558
constructor(
5659
private readonly nativeFinder: NativePythonFinder,
5760
private readonly api: PythonEnvironmentApi,
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import * as fse from 'fs-extra';
5+
import * as path from 'path';
6+
import { traceError, traceInfo, traceVerbose } from '../../common/logging';
7+
import { isWindows } from '../../common/utils/platformUtils';
8+
9+
/**
10+
* Represents the status of conda sourcing in the current environment
11+
*/
12+
export class CondaSourcingStatus {
13+
/**
14+
* Creates a new CondaSourcingStatus instance
15+
* @param condaPath Path to the conda installation
16+
* @param condaFolder Path to the conda installation folder (derived from condaPath)
17+
* @param isActiveOnLaunch Whether conda was activated before VS Code launch
18+
* @param globalSourcingScript Path to the global sourcing script (if exists)
19+
* @param shellSourcingScripts List of paths to shell-specific sourcing scripts
20+
*/
21+
constructor(
22+
public readonly condaPath: string,
23+
public readonly condaFolder: string,
24+
public isActiveOnLaunch?: boolean,
25+
public globalSourcingScript?: string,
26+
public shellSourcingScripts?: string[],
27+
) {}
28+
29+
/**
30+
* Returns a formatted string representation of the conda sourcing status
31+
*/
32+
toString(): string {
33+
const lines: string[] = [];
34+
lines.push('Conda Sourcing Status:');
35+
lines.push(`├─ Conda Path: ${this.condaPath}`);
36+
lines.push(`├─ Conda Folder: ${this.condaFolder}`);
37+
lines.push(`├─ Active on Launch: ${this.isActiveOnLaunch ?? 'false'}`);
38+
39+
if (this.globalSourcingScript) {
40+
lines.push(`├─ Global Sourcing Script: ${this.globalSourcingScript}`);
41+
}
42+
43+
if (this.shellSourcingScripts?.length) {
44+
lines.push('└─ Shell-specific Sourcing Scripts:');
45+
this.shellSourcingScripts.forEach((script, index, array) => {
46+
const isLast = index === array.length - 1;
47+
if (script) {
48+
// Only include scripts that exist
49+
lines.push(` ${isLast ? '└─' : '├─'} ${script}`);
50+
}
51+
});
52+
} else {
53+
lines.push('└─ No Shell-specific Sourcing Scripts Found');
54+
}
55+
56+
return lines.join('\n');
57+
}
58+
}
59+
60+
/**
61+
* Constructs the conda sourcing status for a given conda installation
62+
* @param condaPath The path to the conda executable
63+
* @returns A CondaSourcingStatus object containing:
64+
* - Whether conda was active when VS Code launched
65+
* - Path to global sourcing script (if found)
66+
* - Paths to shell-specific sourcing scripts (if found)
67+
*
68+
* This function checks:
69+
* 1. If conda is already active in the current shell (CONDA_SHLVL)
70+
* 2. Location of the global activation script
71+
* 3. Location of shell-specific activation scripts
72+
*/
73+
export async function constructCondaSourcingStatus(condaPath: string): Promise<CondaSourcingStatus> {
74+
const condaFolder = path.dirname(path.dirname(condaPath));
75+
let sourcingStatus = new CondaSourcingStatus(condaPath, condaFolder);
76+
77+
// The `conda_shlvl` value indicates whether conda is properly initialized in the current shell:
78+
// - `-1`: Conda has never been sourced
79+
// - `undefined`: No shell level information available
80+
// - `0 or higher`: Conda is properly sourced in the shell
81+
const condaShlvl = process.env.CONDA_SHLVL;
82+
if (condaShlvl && parseInt(condaShlvl) >= 0) {
83+
sourcingStatus.isActiveOnLaunch = true;
84+
// if activation already occurred, no need to find further scripts
85+
return sourcingStatus;
86+
}
87+
88+
// Attempt to find the GLOBAL conda sourcing script
89+
const globalSourcingScript: string | undefined = await findGlobalSourcingScript(sourcingStatus.condaFolder);
90+
if (globalSourcingScript) {
91+
sourcingStatus.globalSourcingScript = globalSourcingScript;
92+
// TODO: determine if we want to exit here or continue to generate all the other activation scripts
93+
}
94+
95+
// find and save all of the shell specific sourcing scripts
96+
sourcingStatus.shellSourcingScripts = await findShellSourcingScripts(sourcingStatus);
97+
98+
return sourcingStatus;
99+
}
100+
101+
/**
102+
* Finds the global conda activation script for the given conda installation
103+
* @param condaPath The path to the conda executable
104+
* @returns The path to the global activation script if it exists, undefined otherwise
105+
*
106+
* On Windows, this will look for 'Scripts/activate.bat'
107+
* On Unix systems, this will look for 'bin/activate'
108+
*/
109+
export async function findGlobalSourcingScript(condaFolder: string): Promise<string | undefined> {
110+
const sourcingScript = isWindows()
111+
? path.join(condaFolder, 'Scripts', 'activate.bat')
112+
: path.join(condaFolder, 'bin', 'activate');
113+
114+
traceVerbose(`Checking for global conda sourcing script at: ${sourcingScript}`);
115+
if (await fse.pathExists(sourcingScript)) {
116+
traceInfo(`Found global conda sourcing script at: ${sourcingScript}`);
117+
return sourcingScript;
118+
} else {
119+
traceInfo(`No global conda sourcing script found.`);
120+
return undefined;
121+
}
122+
}
123+
124+
export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStatus): Promise<string[]> {
125+
// Search for conda-hook.ps1 in the expected locations
126+
const ps1Script: string | undefined = await getCondaHookPs1Path(sourcingStatus.condaFolder);
127+
traceVerbose(`Conda hook script search completed found: ${ps1Script}`);
128+
129+
// Search for conda.sh in the expected locations
130+
const shScript: string | undefined = await getCondaShPath(sourcingStatus.condaFolder);
131+
traceVerbose(`Conda shell script search completed found: ${shScript}`);
132+
133+
// Search for the Windows batch activation file (activate.bat)
134+
const cmdActivate: string | undefined = await getCondaBatActivationFile(sourcingStatus.condaPath);
135+
traceVerbose(`Conda command script search completed found: ${cmdActivate}`);
136+
137+
return [ps1Script, shScript, cmdActivate] as string[];
138+
}
139+
140+
/**
141+
* Returns the best guess path to conda-hook.ps1 given a conda executable path.
142+
*
143+
* Searches for conda-hook.ps1 in these locations (relative to the conda root):
144+
* - shell/condabin/
145+
* - Library/shell/condabin/
146+
* - condabin/
147+
* - etc/profile.d/
148+
*/
149+
async function getCondaHookPs1Path(condaFolder: string): Promise<string | undefined> {
150+
// Check cache first
151+
152+
// Create the promise for finding the hook path
153+
const hookPathPromise = (async () => {
154+
const condaRootCandidates: string[] = [
155+
path.join(condaFolder, 'shell', 'condabin'),
156+
path.join(condaFolder, 'Library', 'shell', 'condabin'),
157+
path.join(condaFolder, 'condabin'),
158+
path.join(condaFolder, 'etc', 'profile.d'),
159+
];
160+
161+
const checks = condaRootCandidates.map(async (hookSearchDir) => {
162+
const candidate = path.join(hookSearchDir, 'conda-hook.ps1');
163+
if (await fse.pathExists(candidate)) {
164+
traceInfo(`Conda hook found at: ${candidate}`);
165+
return candidate;
166+
}
167+
return undefined;
168+
});
169+
const results = await Promise.all(checks);
170+
const found = results.find(Boolean);
171+
if (found) {
172+
return found as string;
173+
}
174+
return undefined;
175+
})();
176+
177+
// Store in cache and return
178+
return hookPathPromise;
179+
}
180+
181+
/**
182+
* Helper function that checks for a file in a list of locations.
183+
* Returns the first location where the file exists, or undefined if not found.
184+
*/
185+
async function findFileInLocations(locations: string[], description: string): Promise<string | undefined> {
186+
for (const location of locations) {
187+
if (await fse.pathExists(location)) {
188+
traceInfo(`${description} found in ${location}`);
189+
return location;
190+
}
191+
}
192+
return undefined;
193+
}
194+
195+
/**
196+
* Returns the path to conda.sh given a conda executable path.
197+
*
198+
* Searches for conda.sh in these locations (relative to the conda root):
199+
* - etc/profile.d/conda.sh
200+
* - shell/etc/profile.d/conda.sh
201+
* - Library/etc/profile.d/conda.sh
202+
* - lib/pythonX.Y/site-packages/conda/shell/etc/profile.d/conda.sh
203+
* - site-packages/conda/shell/etc/profile.d/conda.sh
204+
* Also checks some system-level locations
205+
*/
206+
async function getCondaShPath(condaFolder: string): Promise<string | undefined> {
207+
// Create the promise for finding the conda.sh path
208+
const shPathPromise = (async () => {
209+
// First try standard conda installation locations
210+
const standardLocations = [
211+
path.join(condaFolder, 'etc', 'profile.d', 'conda.sh'),
212+
path.join(condaFolder, 'shell', 'etc', 'profile.d', 'conda.sh'),
213+
path.join(condaFolder, 'Library', 'etc', 'profile.d', 'conda.sh'),
214+
];
215+
216+
// Check standard locations first
217+
const standardLocation = await findFileInLocations(standardLocations, 'conda.sh');
218+
if (standardLocation) {
219+
return standardLocation;
220+
}
221+
222+
// If not found in standard locations, try pip install locations
223+
// First, find all python* directories in lib
224+
let pythonDirs: string[] = [];
225+
const libPath = path.join(condaFolder, 'lib');
226+
try {
227+
const dirs = await fse.readdir(libPath);
228+
pythonDirs = dirs.filter((dir) => dir.startsWith('python'));
229+
} catch (err) {
230+
traceVerbose(`No lib directory found at ${libPath}, ${err}`);
231+
}
232+
233+
const pipInstallLocations = [
234+
...pythonDirs.map((ver) =>
235+
path.join(condaFolder, 'lib', ver, 'site-packages', 'conda', 'shell', 'etc', 'profile.d', 'conda.sh'),
236+
),
237+
path.join(condaFolder, 'site-packages', 'conda', 'shell', 'etc', 'profile.d', 'conda.sh'),
238+
];
239+
240+
// Check pip install locations
241+
const pipLocation = await findFileInLocations(pipInstallLocations, 'conda.sh');
242+
if (pipLocation) {
243+
traceError(
244+
'WARNING: conda.sh was found in a pip install location. ' +
245+
'This is not a supported configuration and may be deprecated in the future. ' +
246+
'Please install conda in a standard location. ' +
247+
'See https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html for proper installation instructions.',
248+
);
249+
return pipLocation;
250+
}
251+
return undefined;
252+
})();
253+
254+
return shPathPromise;
255+
}
256+
257+
/**
258+
* Returns the path to the Windows batch activation file (activate.bat) for conda
259+
* @param condaPath The path to the conda executable
260+
* @returns The path to activate.bat if it exists in the same directory as conda.exe, undefined otherwise
261+
*
262+
* This file is used specifically for CMD.exe activation on Windows systems.
263+
* It should be located in the same directory as the conda executable.
264+
*/
265+
async function getCondaBatActivationFile(condaPath: string): Promise<string | undefined> {
266+
const cmdActivate = path.join(path.dirname(condaPath), 'activate.bat');
267+
if (await fse.pathExists(cmdActivate)) {
268+
return cmdActivate;
269+
}
270+
return undefined;
271+
}
272+
273+
/**
274+
* Returns the path to the local conda activation script
275+
* @param condaPath The path to the conda executable
276+
* @returns Promise that resolves to:
277+
* - The path to the local 'activate' script if it exists in the same directory as conda
278+
* - undefined if the script is not found
279+
*
280+
* This function checks for a local 'activate' script in the same directory as the conda executable.
281+
* This script is used for direct conda activation without shell-specific configuration.
282+
*/
283+
export async function getLocalActivationScript(condaPath: string): Promise<string | undefined> {
284+
const activatePath = path.join(path.dirname(condaPath), 'activate');
285+
traceVerbose(`Checking for local activation script at: ${activatePath}`);
286+
287+
if (!condaPath) {
288+
traceVerbose('No conda path provided, cannot find local activation script');
289+
return undefined;
290+
}
291+
292+
try {
293+
const exists = await fse.pathExists(activatePath);
294+
if (exists) {
295+
traceInfo(`Found local activation script at: ${activatePath}`);
296+
return activatePath;
297+
} else {
298+
traceVerbose(`No local activation script found at: ${activatePath}`);
299+
return undefined;
300+
}
301+
} catch (err) {
302+
traceError(`Error checking for local activation script: ${err}`);
303+
return undefined;
304+
}
305+
}

0 commit comments

Comments
 (0)