Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
305 changes: 305 additions & 0 deletions src/managers/conda/condaSourcingUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
// 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;
// TODO: determine if we want to exit here or continue 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');

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

export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStatus): Promise<string[]> {
// Search for conda-hook.ps1 in the expected locations
const ps1Script: string | undefined = await getCondaHookPs1Path(sourcingStatus.condaFolder);
traceVerbose(`Conda hook script search completed found: ${ps1Script}`);

// Search for conda.sh in the expected locations
const shScript: string | undefined = await getCondaShPath(sourcingStatus.condaFolder);
traceVerbose(`Conda shell script search completed found: ${shScript}`);

// Search for the Windows batch activation file (activate.bat)
const cmdActivate: string | undefined = await getCondaBatActivationFile(sourcingStatus.condaPath);
traceVerbose(`Conda command script search completed found: ${cmdActivate}`);

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/
*/
async function getCondaHookPs1Path(condaFolder: string): Promise<string | undefined> {
// Check cache first

// 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;
})();

// Store in cache and return
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.
*/
export async function getLocalActivationScript(condaPath: string): Promise<string | undefined> {
const activatePath = path.join(path.dirname(condaPath), 'activate');
traceVerbose(`Checking for local activation script at: ${activatePath}`);

if (!condaPath) {
traceVerbose('No conda path provided, cannot find local activation script');
return undefined;
}

try {
const exists = await fse.pathExists(activatePath);
if (exists) {
traceInfo(`Found local activation script at: ${activatePath}`);
return activatePath;
} else {
traceVerbose(`No local activation script found at: ${activatePath}`);
return undefined;
}
} catch (err) {
traceError(`Error checking for local activation script: ${err}`);
return undefined;
}
}
Loading
Loading