|
| 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 | + // TODO: determine if we want to exit here or continue 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 | + traceVerbose(`Checking for global conda sourcing script at: ${sourcingScript}`); |
| 115 | + if (await fse.pathExists(sourcingScript)) { |
| 116 | + traceInfo(`Found global conda sourcing script at: ${sourcingScript}`); |
| 117 | + return sourcingScript; |
| 118 | + } else { |
| 119 | + traceInfo(`No global conda sourcing script found.`); |
| 120 | + return undefined; |
| 121 | + } |
| 122 | +} |
| 123 | + |
| 124 | +export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStatus): Promise<string[]> { |
| 125 | + // Search for conda-hook.ps1 in the expected locations |
| 126 | + const ps1Script: string | undefined = await getCondaHookPs1Path(sourcingStatus.condaFolder); |
| 127 | + traceVerbose(`Conda hook script search completed found: ${ps1Script}`); |
| 128 | + |
| 129 | + // Search for conda.sh in the expected locations |
| 130 | + const shScript: string | undefined = await getCondaShPath(sourcingStatus.condaFolder); |
| 131 | + traceVerbose(`Conda shell script search completed found: ${shScript}`); |
| 132 | + |
| 133 | + // Search for the Windows batch activation file (activate.bat) |
| 134 | + const cmdActivate: string | undefined = await getCondaBatActivationFile(sourcingStatus.condaPath); |
| 135 | + traceVerbose(`Conda command script search completed found: ${cmdActivate}`); |
| 136 | + |
| 137 | + return [ps1Script, shScript, cmdActivate] as string[]; |
| 138 | +} |
| 139 | + |
| 140 | +/** |
| 141 | + * Returns the best guess path to conda-hook.ps1 given a conda executable path. |
| 142 | + * |
| 143 | + * Searches for conda-hook.ps1 in these locations (relative to the conda root): |
| 144 | + * - shell/condabin/ |
| 145 | + * - Library/shell/condabin/ |
| 146 | + * - condabin/ |
| 147 | + * - etc/profile.d/ |
| 148 | + */ |
| 149 | +async function getCondaHookPs1Path(condaFolder: string): Promise<string | undefined> { |
| 150 | + // Check cache first |
| 151 | + |
| 152 | + // Create the promise for finding the hook path |
| 153 | + const hookPathPromise = (async () => { |
| 154 | + const condaRootCandidates: string[] = [ |
| 155 | + path.join(condaFolder, 'shell', 'condabin'), |
| 156 | + path.join(condaFolder, 'Library', 'shell', 'condabin'), |
| 157 | + path.join(condaFolder, 'condabin'), |
| 158 | + path.join(condaFolder, 'etc', 'profile.d'), |
| 159 | + ]; |
| 160 | + |
| 161 | + const checks = condaRootCandidates.map(async (hookSearchDir) => { |
| 162 | + const candidate = path.join(hookSearchDir, 'conda-hook.ps1'); |
| 163 | + if (await fse.pathExists(candidate)) { |
| 164 | + traceInfo(`Conda hook found at: ${candidate}`); |
| 165 | + return candidate; |
| 166 | + } |
| 167 | + return undefined; |
| 168 | + }); |
| 169 | + const results = await Promise.all(checks); |
| 170 | + const found = results.find(Boolean); |
| 171 | + if (found) { |
| 172 | + return found as string; |
| 173 | + } |
| 174 | + return undefined; |
| 175 | + })(); |
| 176 | + |
| 177 | + // Store in cache and return |
| 178 | + return hookPathPromise; |
| 179 | +} |
| 180 | + |
| 181 | +/** |
| 182 | + * Helper function that checks for a file in a list of locations. |
| 183 | + * Returns the first location where the file exists, or undefined if not found. |
| 184 | + */ |
| 185 | +async function findFileInLocations(locations: string[], description: string): Promise<string | undefined> { |
| 186 | + for (const location of locations) { |
| 187 | + if (await fse.pathExists(location)) { |
| 188 | + traceInfo(`${description} found in ${location}`); |
| 189 | + return location; |
| 190 | + } |
| 191 | + } |
| 192 | + return undefined; |
| 193 | +} |
| 194 | + |
| 195 | +/** |
| 196 | + * Returns the path to conda.sh given a conda executable path. |
| 197 | + * |
| 198 | + * Searches for conda.sh in these locations (relative to the conda root): |
| 199 | + * - etc/profile.d/conda.sh |
| 200 | + * - shell/etc/profile.d/conda.sh |
| 201 | + * - Library/etc/profile.d/conda.sh |
| 202 | + * - lib/pythonX.Y/site-packages/conda/shell/etc/profile.d/conda.sh |
| 203 | + * - site-packages/conda/shell/etc/profile.d/conda.sh |
| 204 | + * Also checks some system-level locations |
| 205 | + */ |
| 206 | +async function getCondaShPath(condaFolder: string): Promise<string | undefined> { |
| 207 | + // Create the promise for finding the conda.sh path |
| 208 | + const shPathPromise = (async () => { |
| 209 | + // First try standard conda installation locations |
| 210 | + const standardLocations = [ |
| 211 | + path.join(condaFolder, 'etc', 'profile.d', 'conda.sh'), |
| 212 | + path.join(condaFolder, 'shell', 'etc', 'profile.d', 'conda.sh'), |
| 213 | + path.join(condaFolder, 'Library', 'etc', 'profile.d', 'conda.sh'), |
| 214 | + ]; |
| 215 | + |
| 216 | + // Check standard locations first |
| 217 | + const standardLocation = await findFileInLocations(standardLocations, 'conda.sh'); |
| 218 | + if (standardLocation) { |
| 219 | + return standardLocation; |
| 220 | + } |
| 221 | + |
| 222 | + // If not found in standard locations, try pip install locations |
| 223 | + // First, find all python* directories in lib |
| 224 | + let pythonDirs: string[] = []; |
| 225 | + const libPath = path.join(condaFolder, 'lib'); |
| 226 | + try { |
| 227 | + const dirs = await fse.readdir(libPath); |
| 228 | + pythonDirs = dirs.filter((dir) => dir.startsWith('python')); |
| 229 | + } catch (err) { |
| 230 | + traceVerbose(`No lib directory found at ${libPath}, ${err}`); |
| 231 | + } |
| 232 | + |
| 233 | + const pipInstallLocations = [ |
| 234 | + ...pythonDirs.map((ver) => |
| 235 | + path.join(condaFolder, 'lib', ver, 'site-packages', 'conda', 'shell', 'etc', 'profile.d', 'conda.sh'), |
| 236 | + ), |
| 237 | + path.join(condaFolder, 'site-packages', 'conda', 'shell', 'etc', 'profile.d', 'conda.sh'), |
| 238 | + ]; |
| 239 | + |
| 240 | + // Check pip install locations |
| 241 | + const pipLocation = await findFileInLocations(pipInstallLocations, 'conda.sh'); |
| 242 | + if (pipLocation) { |
| 243 | + traceError( |
| 244 | + 'WARNING: conda.sh was found in a pip install location. ' + |
| 245 | + 'This is not a supported configuration and may be deprecated in the future. ' + |
| 246 | + 'Please install conda in a standard location. ' + |
| 247 | + 'See https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html for proper installation instructions.', |
| 248 | + ); |
| 249 | + return pipLocation; |
| 250 | + } |
| 251 | + return undefined; |
| 252 | + })(); |
| 253 | + |
| 254 | + return shPathPromise; |
| 255 | +} |
| 256 | + |
| 257 | +/** |
| 258 | + * Returns the path to the Windows batch activation file (activate.bat) for conda |
| 259 | + * @param condaPath The path to the conda executable |
| 260 | + * @returns The path to activate.bat if it exists in the same directory as conda.exe, undefined otherwise |
| 261 | + * |
| 262 | + * This file is used specifically for CMD.exe activation on Windows systems. |
| 263 | + * It should be located in the same directory as the conda executable. |
| 264 | + */ |
| 265 | +async function getCondaBatActivationFile(condaPath: string): Promise<string | undefined> { |
| 266 | + const cmdActivate = path.join(path.dirname(condaPath), 'activate.bat'); |
| 267 | + if (await fse.pathExists(cmdActivate)) { |
| 268 | + return cmdActivate; |
| 269 | + } |
| 270 | + return undefined; |
| 271 | +} |
| 272 | + |
| 273 | +/** |
| 274 | + * Returns the path to the local conda activation script |
| 275 | + * @param condaPath The path to the conda executable |
| 276 | + * @returns Promise that resolves to: |
| 277 | + * - The path to the local 'activate' script if it exists in the same directory as conda |
| 278 | + * - undefined if the script is not found |
| 279 | + * |
| 280 | + * This function checks for a local 'activate' script in the same directory as the conda executable. |
| 281 | + * This script is used for direct conda activation without shell-specific configuration. |
| 282 | + */ |
| 283 | +export async function getLocalActivationScript(condaPath: string): Promise<string | undefined> { |
| 284 | + const activatePath = path.join(path.dirname(condaPath), 'activate'); |
| 285 | + traceVerbose(`Checking for local activation script at: ${activatePath}`); |
| 286 | + |
| 287 | + if (!condaPath) { |
| 288 | + traceVerbose('No conda path provided, cannot find local activation script'); |
| 289 | + return undefined; |
| 290 | + } |
| 291 | + |
| 292 | + try { |
| 293 | + const exists = await fse.pathExists(activatePath); |
| 294 | + if (exists) { |
| 295 | + traceInfo(`Found local activation script at: ${activatePath}`); |
| 296 | + return activatePath; |
| 297 | + } else { |
| 298 | + traceVerbose(`No local activation script found at: ${activatePath}`); |
| 299 | + return undefined; |
| 300 | + } |
| 301 | + } catch (err) { |
| 302 | + traceError(`Error checking for local activation script: ${err}`); |
| 303 | + return undefined; |
| 304 | + } |
| 305 | +} |
0 commit comments