-
Notifications
You must be signed in to change notification settings - Fork 30
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 1 commit
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,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; | ||
| } | ||
| } | ||
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.