|
| 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 | + // note: future iterations could decide to exit here instead of continuing 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 | + if (await fse.pathExists(sourcingScript)) { |
| 115 | + traceInfo(`Found global conda sourcing script at: ${sourcingScript}`); |
| 116 | + return sourcingScript; |
| 117 | + } else { |
| 118 | + traceInfo(`No global conda sourcing script found. at: ${sourcingScript}`); |
| 119 | + return undefined; |
| 120 | + } |
| 121 | +} |
| 122 | + |
| 123 | +export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStatus): Promise<string[]> { |
| 124 | + const logs: string[] = []; |
| 125 | + logs.push('=== Conda Sourcing Shell Script Search ==='); |
| 126 | + |
| 127 | + let ps1Script: string | undefined; |
| 128 | + let shScript: string | undefined; |
| 129 | + let cmdActivate: string | undefined; |
| 130 | + |
| 131 | + try { |
| 132 | + // Search for PowerShell hook script (conda-hook.ps1) |
| 133 | + logs.push('Searching for PowerShell hook script...'); |
| 134 | + try { |
| 135 | + ps1Script = await getCondaHookPs1Path(sourcingStatus.condaFolder); |
| 136 | + logs.push(` Path: ${ps1Script ?? '✗ Not found'}`); |
| 137 | + } catch (err) { |
| 138 | + logs.push( |
| 139 | + ` Error during PowerShell script search: ${err instanceof Error ? err.message : 'Unknown error'}`, |
| 140 | + ); |
| 141 | + } |
| 142 | + |
| 143 | + // Search for Shell script (conda.sh) |
| 144 | + logs.push('\nSearching for Shell script...'); |
| 145 | + try { |
| 146 | + shScript = await getCondaShPath(sourcingStatus.condaFolder); |
| 147 | + logs.push(` Path: ${shScript ?? '✗ Not found'}`); |
| 148 | + } catch (err) { |
| 149 | + logs.push(` Error during Shell script search: ${err instanceof Error ? err.message : 'Unknown error'}`); |
| 150 | + } |
| 151 | + |
| 152 | + // Search for Windows CMD script (activate.bat) |
| 153 | + logs.push('\nSearching for Windows CMD script...'); |
| 154 | + try { |
| 155 | + cmdActivate = await getCondaBatActivationFile(sourcingStatus.condaPath); |
| 156 | + logs.push(` Path: ${cmdActivate ?? '✗ Not found'}`); |
| 157 | + } catch (err) { |
| 158 | + logs.push(` Error during CMD script search: ${err instanceof Error ? err.message : 'Unknown error'}`); |
| 159 | + } |
| 160 | + } catch (error) { |
| 161 | + logs.push(`\nCritical error during script search: ${error instanceof Error ? error.message : 'Unknown error'}`); |
| 162 | + } finally { |
| 163 | + logs.push('\nSearch Summary:'); |
| 164 | + logs.push(` PowerShell: ${ps1Script ? '✓' : '✗'}`); |
| 165 | + logs.push(` Shell: ${shScript ? '✓' : '✗'}`); |
| 166 | + logs.push(` CMD: ${cmdActivate ? '✓' : '✗'}`); |
| 167 | + logs.push('============================'); |
| 168 | + |
| 169 | + // Log everything at once |
| 170 | + traceVerbose(logs.join('\n')); |
| 171 | + } |
| 172 | + |
| 173 | + return [ps1Script, shScript, cmdActivate] as string[]; |
| 174 | +} |
| 175 | + |
| 176 | +/** |
| 177 | + * Returns the best guess path to conda-hook.ps1 given a conda executable path. |
| 178 | + * |
| 179 | + * Searches for conda-hook.ps1 in these locations (relative to the conda root): |
| 180 | + * - shell/condabin/ |
| 181 | + * - Library/shell/condabin/ |
| 182 | + * - condabin/ |
| 183 | + * - etc/profile.d/ |
| 184 | + */ |
| 185 | +export async function getCondaHookPs1Path(condaFolder: string): Promise<string | undefined> { |
| 186 | + // Create the promise for finding the hook path |
| 187 | + const hookPathPromise = (async () => { |
| 188 | + const condaRootCandidates: string[] = [ |
| 189 | + path.join(condaFolder, 'shell', 'condabin'), |
| 190 | + path.join(condaFolder, 'Library', 'shell', 'condabin'), |
| 191 | + path.join(condaFolder, 'condabin'), |
| 192 | + path.join(condaFolder, 'etc', 'profile.d'), |
| 193 | + ]; |
| 194 | + |
| 195 | + const checks = condaRootCandidates.map(async (hookSearchDir) => { |
| 196 | + const candidate = path.join(hookSearchDir, 'conda-hook.ps1'); |
| 197 | + if (await fse.pathExists(candidate)) { |
| 198 | + traceInfo(`Conda hook found at: ${candidate}`); |
| 199 | + return candidate; |
| 200 | + } |
| 201 | + return undefined; |
| 202 | + }); |
| 203 | + const results = await Promise.all(checks); |
| 204 | + const found = results.find(Boolean); |
| 205 | + if (found) { |
| 206 | + return found as string; |
| 207 | + } |
| 208 | + return undefined; |
| 209 | + })(); |
| 210 | + |
| 211 | + return hookPathPromise; |
| 212 | +} |
| 213 | + |
| 214 | +/** |
| 215 | + * Helper function that checks for a file in a list of locations. |
| 216 | + * Returns the first location where the file exists, or undefined if not found. |
| 217 | + */ |
| 218 | +async function findFileInLocations(locations: string[], description: string): Promise<string | undefined> { |
| 219 | + for (const location of locations) { |
| 220 | + if (await fse.pathExists(location)) { |
| 221 | + traceInfo(`${description} found in ${location}`); |
| 222 | + return location; |
| 223 | + } |
| 224 | + } |
| 225 | + return undefined; |
| 226 | +} |
| 227 | + |
| 228 | +/** |
| 229 | + * Returns the path to conda.sh given a conda executable path. |
| 230 | + * |
| 231 | + * Searches for conda.sh in these locations (relative to the conda root): |
| 232 | + * - etc/profile.d/conda.sh |
| 233 | + * - shell/etc/profile.d/conda.sh |
| 234 | + * - Library/etc/profile.d/conda.sh |
| 235 | + * - lib/pythonX.Y/site-packages/conda/shell/etc/profile.d/conda.sh |
| 236 | + * - site-packages/conda/shell/etc/profile.d/conda.sh |
| 237 | + * Also checks some system-level locations |
| 238 | + */ |
| 239 | +async function getCondaShPath(condaFolder: string): Promise<string | undefined> { |
| 240 | + // Create the promise for finding the conda.sh path |
| 241 | + const shPathPromise = (async () => { |
| 242 | + // First try standard conda installation locations |
| 243 | + const standardLocations = [ |
| 244 | + path.join(condaFolder, 'etc', 'profile.d', 'conda.sh'), |
| 245 | + path.join(condaFolder, 'shell', 'etc', 'profile.d', 'conda.sh'), |
| 246 | + path.join(condaFolder, 'Library', 'etc', 'profile.d', 'conda.sh'), |
| 247 | + ]; |
| 248 | + |
| 249 | + // Check standard locations first |
| 250 | + const standardLocation = await findFileInLocations(standardLocations, 'conda.sh'); |
| 251 | + if (standardLocation) { |
| 252 | + return standardLocation; |
| 253 | + } |
| 254 | + |
| 255 | + // If not found in standard locations, try pip install locations |
| 256 | + // First, find all python* directories in lib |
| 257 | + let pythonDirs: string[] = []; |
| 258 | + const libPath = path.join(condaFolder, 'lib'); |
| 259 | + try { |
| 260 | + const dirs = await fse.readdir(libPath); |
| 261 | + pythonDirs = dirs.filter((dir) => dir.startsWith('python')); |
| 262 | + } catch (err) { |
| 263 | + traceVerbose(`No lib directory found at ${libPath}, ${err}`); |
| 264 | + } |
| 265 | + |
| 266 | + const pipInstallLocations = [ |
| 267 | + ...pythonDirs.map((ver) => |
| 268 | + path.join(condaFolder, 'lib', ver, 'site-packages', 'conda', 'shell', 'etc', 'profile.d', 'conda.sh'), |
| 269 | + ), |
| 270 | + path.join(condaFolder, 'site-packages', 'conda', 'shell', 'etc', 'profile.d', 'conda.sh'), |
| 271 | + ]; |
| 272 | + |
| 273 | + // Check pip install locations |
| 274 | + const pipLocation = await findFileInLocations(pipInstallLocations, 'conda.sh'); |
| 275 | + if (pipLocation) { |
| 276 | + traceError( |
| 277 | + 'WARNING: conda.sh was found in a pip install location. ' + |
| 278 | + 'This is not a supported configuration and may be deprecated in the future. ' + |
| 279 | + 'Please install conda in a standard location. ' + |
| 280 | + 'See https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html for proper installation instructions.', |
| 281 | + ); |
| 282 | + return pipLocation; |
| 283 | + } |
| 284 | + return undefined; |
| 285 | + })(); |
| 286 | + |
| 287 | + return shPathPromise; |
| 288 | +} |
| 289 | + |
| 290 | +/** |
| 291 | + * Returns the path to the Windows batch activation file (activate.bat) for conda |
| 292 | + * @param condaPath The path to the conda executable |
| 293 | + * @returns The path to activate.bat if it exists in the same directory as conda.exe, undefined otherwise |
| 294 | + * |
| 295 | + * This file is used specifically for CMD.exe activation on Windows systems. |
| 296 | + * It should be located in the same directory as the conda executable. |
| 297 | + */ |
| 298 | +async function getCondaBatActivationFile(condaPath: string): Promise<string | undefined> { |
| 299 | + const cmdActivate = path.join(path.dirname(condaPath), 'activate.bat'); |
| 300 | + if (await fse.pathExists(cmdActivate)) { |
| 301 | + return cmdActivate; |
| 302 | + } |
| 303 | + return undefined; |
| 304 | +} |
| 305 | + |
| 306 | +/** |
| 307 | + * Returns the path to the local conda activation script |
| 308 | + * @param condaPath The path to the conda executable |
| 309 | + * @returns Promise that resolves to: |
| 310 | + * - The path to the local 'activate' script if it exists in the same directory as conda |
| 311 | + * - undefined if the script is not found |
| 312 | + * |
| 313 | + * This function checks for a local 'activate' script in the same directory as the conda executable. |
| 314 | + * This script is used for direct conda activation without shell-specific configuration. |
| 315 | + */ |
| 316 | + |
| 317 | +const knownSourcingScriptCache: string[] = []; |
| 318 | +export async function getLocalActivationScript(condaPath: string): Promise<string | undefined> { |
| 319 | + // Define all possible paths to check |
| 320 | + const paths = [ |
| 321 | + // Direct path |
| 322 | + isWindows() ? path.join(condaPath, 'Scripts', 'activate') : path.join(condaPath, 'bin', 'activate'), |
| 323 | + // One level up |
| 324 | + isWindows() |
| 325 | + ? path.join(path.dirname(condaPath), 'Scripts', 'activate') |
| 326 | + : path.join(path.dirname(condaPath), 'bin', 'activate'), |
| 327 | + // Two levels up |
| 328 | + isWindows() |
| 329 | + ? path.join(path.dirname(path.dirname(condaPath)), 'Scripts', 'activate') |
| 330 | + : path.join(path.dirname(path.dirname(condaPath)), 'bin', 'activate'), |
| 331 | + ]; |
| 332 | + |
| 333 | + // Check each path in sequence |
| 334 | + for (const sourcingScript of paths) { |
| 335 | + // Check if any of the paths are in the cache |
| 336 | + if (knownSourcingScriptCache.includes(sourcingScript)) { |
| 337 | + traceVerbose(`Found local activation script in cache at: ${sourcingScript}`); |
| 338 | + return sourcingScript; |
| 339 | + } |
| 340 | + try { |
| 341 | + const exists = await fse.pathExists(sourcingScript); |
| 342 | + if (exists) { |
| 343 | + traceInfo(`Found local activation script at: ${sourcingScript}, adding to cache.`); |
| 344 | + knownSourcingScriptCache.push(sourcingScript); |
| 345 | + return sourcingScript; |
| 346 | + } |
| 347 | + } catch (err) { |
| 348 | + traceError(`Error checking for local activation script at ${sourcingScript}: ${err}`); |
| 349 | + continue; |
| 350 | + } |
| 351 | + } |
| 352 | + |
| 353 | + traceVerbose('No local activation script found in any of the expected locations'); |
| 354 | + return undefined; |
| 355 | +} |
0 commit comments