-
Notifications
You must be signed in to change notification settings - Fork 31
bug fix: enhance conda environment management with sourcing status and updated shell activation support #693
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
0bd3547
bug fix: enhance conda environment management with sourcing status an…
eleanorjboyd bbb17fd
update local sourcing path preference
eleanorjboyd 39331a6
improve logging and update local activation paths checked
eleanorjboyd 203f09d
address comments
eleanorjboyd ba39d40
fix up
eleanorjboyd 429a009
Merge branch 'main' into everything-bagel
eleanorjboyd 1f9b370
updates based on comments
eleanorjboyd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.