Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/managers/conda/condaEnvManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { traceError } from '../../common/logging';
import { createDeferred, Deferred } from '../../common/utils/deferred';
import { showErrorMessage, withProgress } from '../../common/window.apis';
import { NativePythonFinder } from '../common/nativePythonFinder';
import { CondaSourcingStatus } from './condaSourcingUtils';
import {
checkForNoPythonCondaEnvironment,
clearCondaCache,
Expand Down Expand Up @@ -52,6 +53,8 @@ export class CondaEnvManager implements EnvironmentManager, Disposable {
private readonly _onDidChangeEnvironments = new EventEmitter<DidChangeEnvironmentsEventArgs>();
public readonly onDidChangeEnvironments = this._onDidChangeEnvironments.event;

public sourcingInformation: CondaSourcingStatus | undefined;

constructor(
private readonly nativeFinder: NativePythonFinder,
private readonly api: PythonEnvironmentApi,
Expand Down
355 changes: 355 additions & 0 deletions src/managers/conda/condaSourcingUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import * as fse from 'fs-extra';
import * as path from 'path';
import { traceError, traceInfo, traceVerbose } from '../../common/logging';
import { isWindows } from '../../common/utils/platformUtils';

/**
* Represents the status of conda sourcing in the current environment
*/
export class CondaSourcingStatus {
/**
* Creates a new CondaSourcingStatus instance
* @param condaPath Path to the conda installation
* @param condaFolder Path to the conda installation folder (derived from condaPath)
* @param isActiveOnLaunch Whether conda was activated before VS Code launch
* @param globalSourcingScript Path to the global sourcing script (if exists)
* @param shellSourcingScripts List of paths to shell-specific sourcing scripts
*/
constructor(
public readonly condaPath: string,
public readonly condaFolder: string,
public isActiveOnLaunch?: boolean,
public globalSourcingScript?: string,
public shellSourcingScripts?: string[],
) {}

/**
* Returns a formatted string representation of the conda sourcing status
*/
toString(): string {
const lines: string[] = [];
lines.push('Conda Sourcing Status:');
lines.push(`├─ Conda Path: ${this.condaPath}`);
lines.push(`├─ Conda Folder: ${this.condaFolder}`);
lines.push(`├─ Active on Launch: ${this.isActiveOnLaunch ?? 'false'}`);

if (this.globalSourcingScript) {
lines.push(`├─ Global Sourcing Script: ${this.globalSourcingScript}`);
}

if (this.shellSourcingScripts?.length) {
lines.push('└─ Shell-specific Sourcing Scripts:');
this.shellSourcingScripts.forEach((script, index, array) => {
const isLast = index === array.length - 1;
if (script) {
// Only include scripts that exist
lines.push(` ${isLast ? '└─' : '├─'} ${script}`);
}
});
} else {
lines.push('└─ No Shell-specific Sourcing Scripts Found');
}

return lines.join('\n');
}
}

/**
* Constructs the conda sourcing status for a given conda installation
* @param condaPath The path to the conda executable
* @returns A CondaSourcingStatus object containing:
* - Whether conda was active when VS Code launched
* - Path to global sourcing script (if found)
* - Paths to shell-specific sourcing scripts (if found)
*
* This function checks:
* 1. If conda is already active in the current shell (CONDA_SHLVL)
* 2. Location of the global activation script
* 3. Location of shell-specific activation scripts
*/
export async function constructCondaSourcingStatus(condaPath: string): Promise<CondaSourcingStatus> {
const condaFolder = path.dirname(path.dirname(condaPath));
let sourcingStatus = new CondaSourcingStatus(condaPath, condaFolder);

// The `conda_shlvl` value indicates whether conda is properly initialized in the current shell:
// - `-1`: Conda has never been sourced
// - `undefined`: No shell level information available
// - `0 or higher`: Conda is properly sourced in the shell
const condaShlvl = process.env.CONDA_SHLVL;
if (condaShlvl && parseInt(condaShlvl) >= 0) {
sourcingStatus.isActiveOnLaunch = true;
// if activation already occurred, no need to find further scripts
return sourcingStatus;
}

// Attempt to find the GLOBAL conda sourcing script
const globalSourcingScript: string | undefined = await findGlobalSourcingScript(sourcingStatus.condaFolder);
if (globalSourcingScript) {
sourcingStatus.globalSourcingScript = globalSourcingScript;
// note: future iterations could decide to exit here instead of continuing to generate all the other activation scripts
}

// find and save all of the shell specific sourcing scripts
sourcingStatus.shellSourcingScripts = await findShellSourcingScripts(sourcingStatus);

return sourcingStatus;
}

/**
* Finds the global conda activation script for the given conda installation
* @param condaPath The path to the conda executable
* @returns The path to the global activation script if it exists, undefined otherwise
*
* On Windows, this will look for 'Scripts/activate.bat'
* On Unix systems, this will look for 'bin/activate'
*/
export async function findGlobalSourcingScript(condaFolder: string): Promise<string | undefined> {
const sourcingScript = isWindows()
? path.join(condaFolder, 'Scripts', 'activate.bat')
: path.join(condaFolder, 'bin', 'activate');

if (await fse.pathExists(sourcingScript)) {
traceInfo(`Found global conda sourcing script at: ${sourcingScript}`);
return sourcingScript;
} else {
traceInfo(`No global conda sourcing script found. at: ${sourcingScript}`);
return undefined;
}
}

export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStatus): Promise<string[]> {
const logs: string[] = [];
logs.push('=== Conda Sourcing Shell Script Search ===');

let ps1Script: string | undefined;
let shScript: string | undefined;
let cmdActivate: string | undefined;

try {
// Search for PowerShell hook script (conda-hook.ps1)
logs.push('Searching for PowerShell hook script...');
try {
ps1Script = await getCondaHookPs1Path(sourcingStatus.condaFolder);
logs.push(` Path: ${ps1Script ?? '✗ Not found'}`);
} catch (err) {
logs.push(
` Error during PowerShell script search: ${err instanceof Error ? err.message : 'Unknown error'}`,
);
}

// Search for Shell script (conda.sh)
logs.push('\nSearching for Shell script...');
try {
shScript = await getCondaShPath(sourcingStatus.condaFolder);
logs.push(` Path: ${shScript ?? '✗ Not found'}`);
} catch (err) {
logs.push(` Error during Shell script search: ${err instanceof Error ? err.message : 'Unknown error'}`);
}

// Search for Windows CMD script (activate.bat)
logs.push('\nSearching for Windows CMD script...');
try {
cmdActivate = await getCondaBatActivationFile(sourcingStatus.condaPath);
logs.push(` Path: ${cmdActivate ?? '✗ Not found'}`);
} catch (err) {
logs.push(` Error during CMD script search: ${err instanceof Error ? err.message : 'Unknown error'}`);
}
} catch (error) {
logs.push(`\nCritical error during script search: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
logs.push('\nSearch Summary:');
logs.push(` PowerShell: ${ps1Script ? '✓' : '✗'}`);
logs.push(` Shell: ${shScript ? '✓' : '✗'}`);
logs.push(` CMD: ${cmdActivate ? '✓' : '✗'}`);
logs.push('============================');

// Log everything at once
traceVerbose(logs.join('\n'));
}

return [ps1Script, shScript, cmdActivate] as string[];
}

/**
* Returns the best guess path to conda-hook.ps1 given a conda executable path.
*
* Searches for conda-hook.ps1 in these locations (relative to the conda root):
* - shell/condabin/
* - Library/shell/condabin/
* - condabin/
* - etc/profile.d/
*/
export async function getCondaHookPs1Path(condaFolder: string): Promise<string | undefined> {
// Create the promise for finding the hook path
const hookPathPromise = (async () => {
const condaRootCandidates: string[] = [
path.join(condaFolder, 'shell', 'condabin'),
path.join(condaFolder, 'Library', 'shell', 'condabin'),
path.join(condaFolder, 'condabin'),
path.join(condaFolder, 'etc', 'profile.d'),
];

const checks = condaRootCandidates.map(async (hookSearchDir) => {
const candidate = path.join(hookSearchDir, 'conda-hook.ps1');
if (await fse.pathExists(candidate)) {
traceInfo(`Conda hook found at: ${candidate}`);
return candidate;
}
return undefined;
});
const results = await Promise.all(checks);
const found = results.find(Boolean);
if (found) {
return found as string;
}
return undefined;
})();

return hookPathPromise;
}

/**
* Helper function that checks for a file in a list of locations.
* Returns the first location where the file exists, or undefined if not found.
*/
async function findFileInLocations(locations: string[], description: string): Promise<string | undefined> {
for (const location of locations) {
if (await fse.pathExists(location)) {
traceInfo(`${description} found in ${location}`);
return location;
}
}
return undefined;
}

/**
* Returns the path to conda.sh given a conda executable path.
*
* Searches for conda.sh in these locations (relative to the conda root):
* - etc/profile.d/conda.sh
* - shell/etc/profile.d/conda.sh
* - Library/etc/profile.d/conda.sh
* - lib/pythonX.Y/site-packages/conda/shell/etc/profile.d/conda.sh
* - site-packages/conda/shell/etc/profile.d/conda.sh
* Also checks some system-level locations
*/
async function getCondaShPath(condaFolder: string): Promise<string | undefined> {
// Create the promise for finding the conda.sh path
const shPathPromise = (async () => {
// First try standard conda installation locations
const standardLocations = [
path.join(condaFolder, 'etc', 'profile.d', 'conda.sh'),
path.join(condaFolder, 'shell', 'etc', 'profile.d', 'conda.sh'),
path.join(condaFolder, 'Library', 'etc', 'profile.d', 'conda.sh'),
];

// Check standard locations first
const standardLocation = await findFileInLocations(standardLocations, 'conda.sh');
if (standardLocation) {
return standardLocation;
}

// If not found in standard locations, try pip install locations
// First, find all python* directories in lib
let pythonDirs: string[] = [];
const libPath = path.join(condaFolder, 'lib');
try {
const dirs = await fse.readdir(libPath);
pythonDirs = dirs.filter((dir) => dir.startsWith('python'));
} catch (err) {
traceVerbose(`No lib directory found at ${libPath}, ${err}`);
}

const pipInstallLocations = [
...pythonDirs.map((ver) =>
path.join(condaFolder, 'lib', ver, 'site-packages', 'conda', 'shell', 'etc', 'profile.d', 'conda.sh'),
),
path.join(condaFolder, 'site-packages', 'conda', 'shell', 'etc', 'profile.d', 'conda.sh'),
];

// Check pip install locations
const pipLocation = await findFileInLocations(pipInstallLocations, 'conda.sh');
if (pipLocation) {
traceError(
'WARNING: conda.sh was found in a pip install location. ' +
'This is not a supported configuration and may be deprecated in the future. ' +
'Please install conda in a standard location. ' +
'See https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html for proper installation instructions.',
);
return pipLocation;
}
return undefined;
})();

return shPathPromise;
}

/**
* Returns the path to the Windows batch activation file (activate.bat) for conda
* @param condaPath The path to the conda executable
* @returns The path to activate.bat if it exists in the same directory as conda.exe, undefined otherwise
*
* This file is used specifically for CMD.exe activation on Windows systems.
* It should be located in the same directory as the conda executable.
*/
async function getCondaBatActivationFile(condaPath: string): Promise<string | undefined> {
const cmdActivate = path.join(path.dirname(condaPath), 'activate.bat');
if (await fse.pathExists(cmdActivate)) {
return cmdActivate;
}
return undefined;
}

/**
* Returns the path to the local conda activation script
* @param condaPath The path to the conda executable
* @returns Promise that resolves to:
* - The path to the local 'activate' script if it exists in the same directory as conda
* - undefined if the script is not found
*
* This function checks for a local 'activate' script in the same directory as the conda executable.
* This script is used for direct conda activation without shell-specific configuration.
*/

const knownSourcingScriptCache: string[] = [];
export async function getLocalActivationScript(condaPath: string): Promise<string | undefined> {
// Define all possible paths to check
const paths = [
// Direct path
isWindows() ? path.join(condaPath, 'Scripts', 'activate') : path.join(condaPath, 'bin', 'activate'),
// One level up
isWindows()
? path.join(path.dirname(condaPath), 'Scripts', 'activate')
: path.join(path.dirname(condaPath), 'bin', 'activate'),
// Two levels up
isWindows()
? path.join(path.dirname(path.dirname(condaPath)), 'Scripts', 'activate')
: path.join(path.dirname(path.dirname(condaPath)), 'bin', 'activate'),
];

// Check each path in sequence
for (const sourcingScript of paths) {
// Check if any of the paths are in the cache
if (knownSourcingScriptCache.includes(sourcingScript)) {
traceVerbose(`Found local activation script in cache at: ${sourcingScript}`);
return sourcingScript;
}
try {
const exists = await fse.pathExists(sourcingScript);
if (exists) {
traceInfo(`Found local activation script at: ${sourcingScript}, adding to cache.`);
knownSourcingScriptCache.push(sourcingScript);
return sourcingScript;
}
} catch (err) {
traceError(`Error checking for local activation script at ${sourcingScript}: ${err}`);
continue;
}
}

traceVerbose('No local activation script found in any of the expected locations');
return undefined;
}
Loading
Loading