Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
131 changes: 130 additions & 1 deletion src/managers/conda/condaSourcingUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

import * as fse from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import { traceError, traceInfo, traceVerbose } from '../../common/logging';
import { isWindows } from '../../common/utils/platformUtils';
Expand All @@ -17,6 +18,20 @@ export interface ShellSourcingScripts {
sh?: string;
/** Windows CMD batch file (activate.bat) */
cmd?: string;
/** Fish shell initialization script (conda.fish) */
fish?: string;
}

/**
* Tracks whether `conda init <shell>` has been run for each shell type.
* When true, the shell's profile/config file contains the conda initialization block,
* meaning bare `conda` will be available as a shell function when that shell starts.
*/
export interface ShellCondaInitStatus {
bash?: boolean;
zsh?: boolean;
fish?: boolean;
pwsh?: boolean;
}

/**
Expand All @@ -37,6 +52,7 @@ export class CondaSourcingStatus {
public isActiveOnLaunch?: boolean,
public globalSourcingScript?: string,
public shellSourcingScripts?: ShellSourcingScripts,
public shellInitStatus?: ShellCondaInitStatus,
) {}

/**
Expand All @@ -59,6 +75,7 @@ export class CondaSourcingStatus {
scripts.ps1 && `PowerShell: ${scripts.ps1}`,
scripts.sh && `Bash/sh: ${scripts.sh}`,
scripts.cmd && `CMD: ${scripts.cmd}`,
scripts.fish && `Fish: ${scripts.fish}`,
].filter(Boolean);

if (entries.length > 0) {
Expand All @@ -74,6 +91,13 @@ export class CondaSourcingStatus {
lines.push('└─ No Shell-specific Sourcing Scripts Found');
}

if (this.shellInitStatus) {
const initEntries = (['bash', 'zsh', 'fish', 'pwsh'] as const)
.map((s) => `${s}: ${this.shellInitStatus![s] ? '✓' : '✗'}`)
.join(', ');
lines.push(`├─ Shell conda init status: ${initEntries}`);
}

return lines.join('\n');
}
}
Expand Down Expand Up @@ -116,6 +140,9 @@ export async function constructCondaSourcingStatus(condaPath: string): Promise<C
// find and save all of the shell specific sourcing scripts
sourcingStatus.shellSourcingScripts = await findShellSourcingScripts(sourcingStatus);

// check shell profile files to see if `conda init <shell>` has been run
sourcingStatus.shellInitStatus = await checkCondaInitInShellProfiles();

return sourcingStatus;
}

Expand Down Expand Up @@ -148,6 +175,7 @@ export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStat
let ps1Script: string | undefined;
let shScript: string | undefined;
let cmdActivate: string | undefined;
let fishScript: string | undefined;

try {
// Search for PowerShell hook script (conda-hook.ps1)
Expand Down Expand Up @@ -178,20 +206,103 @@ export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStat
} catch (err) {
logs.push(` Error during CMD script search: ${err instanceof Error ? err.message : 'Unknown error'}`);
}

// Search for Fish shell script (conda.fish)
logs.push('\nSearching for Fish shell script...');
try {
fishScript = await getCondaFishPath(sourcingStatus.condaFolder);
logs.push(` Path: ${fishScript ?? '✗ Not found'}`);
} catch (err) {
logs.push(` Error during Fish 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(` Fish: ${fishScript ? '✓' : '✗'}`);
logs.push('============================');

// Log everything at once
traceVerbose(logs.join('\n'));
}

return { ps1: ps1Script, sh: shScript, cmd: cmdActivate };
return { ps1: ps1Script, sh: shScript, cmd: cmdActivate, fish: fishScript };
}

/**
* Checks shell profile/config files to determine if `conda init <shell>` has been run.
*
* When `conda init <shell>` is run, it adds a `# >>> conda initialize >>>` block to the
* shell's profile. If that block is present, then any new terminal of that shell type will
* have `conda` available as a shell function, and bare `conda activate` will work.
*
* For Fish, `conda init fish` may either modify `config.fish` or drop a file in
* `~/.config/fish/conf.d/`, so both locations are checked.
*
* @param homeDir Optional home directory override (defaults to os.homedir(), useful for testing)
* @returns Status object indicating which shells have conda initialized
*/
export async function checkCondaInitInShellProfiles(homeDir?: string): Promise<ShellCondaInitStatus> {
const home = homeDir ?? os.homedir();
const status: ShellCondaInitStatus = {};
const logs: string[] = ['=== Checking shell profiles for conda init ==='];

const checks: Array<{ shell: keyof ShellCondaInitStatus; files: string[] }> = [
{
shell: 'bash',
files: [path.join(home, '.bashrc'), path.join(home, '.bash_profile')],
},
{
shell: 'zsh',
files: [path.join(home, '.zshrc')],
},
{
shell: 'fish',
files: [
path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'fish', 'config.fish'),
path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'fish', 'conf.d', 'conda.fish'),
],
},
{
shell: 'pwsh',
files: [
path.join(
process.env.XDG_CONFIG_HOME || path.join(home, '.config'),
'powershell',
'Microsoft.PowerShell_profile.ps1',
),
path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'powershell', 'profile.ps1'),
],
},
];

await Promise.all(
checks.map(async ({ shell, files }) => {
for (const filePath of files) {
try {
if (await fse.pathExists(filePath)) {
const content = await fse.readFile(filePath, 'utf-8');
if (content.includes('conda initialize')) {
status[shell] = true;
logs.push(` ${shell}: ✓ conda init found in ${filePath}`);
return;
}
}
} catch {
// File not readable, skip
}
}
logs.push(` ${shell}: ✗ conda init not found`);
}),
);

logs.push('============================');
traceVerbose(logs.join('\n'));

return status;
}

/**
Expand Down Expand Up @@ -308,6 +419,24 @@ async function getCondaShPath(condaFolder: string): Promise<string | undefined>
return shPathPromise;
}

/**
* Returns the path to conda.fish given a conda installation folder.
*
* Searches for conda.fish in these locations (relative to the conda root):
* - etc/fish/conf.d/conda.fish
* - shell/etc/fish/conf.d/conda.fish
* - Library/etc/fish/conf.d/conda.fish
*/
export async function getCondaFishPath(condaFolder: string): Promise<string | undefined> {
const locations = [
path.join(condaFolder, 'etc', 'fish', 'conf.d', 'conda.fish'),
path.join(condaFolder, 'shell', 'etc', 'fish', 'conf.d', 'conda.fish'),
path.join(condaFolder, 'Library', 'etc', 'fish', 'conf.d', 'conda.fish'),
];

return findFileInLocations(locations, 'conda.fish');
}

/**
* Returns the path to the Windows batch activation file (activate.bat) for conda
* @param condaPath The path to the conda executable
Expand Down
118 changes: 111 additions & 7 deletions src/managers/conda/condaUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import { selectFromCommonPackagesToInstall } from '../common/pickers';
import { Installable } from '../common/types';
import { shortVersion, sortEnvironments } from '../common/utils';
import { CondaEnvManager } from './condaEnvManager';
import { getCondaHookPs1Path, getLocalActivationScript } from './condaSourcingUtils';
import { getCondaHookPs1Path, getLocalActivationScript, ShellCondaInitStatus } from './condaSourcingUtils';
import { createStepBasedCondaFlow } from './condaStepBasedFlow';

export const CONDA_PATH_KEY = `${ENVS_EXTENSION_ID}:conda:CONDA_PATH`;
Expand Down Expand Up @@ -526,12 +526,21 @@ async function buildShellActivationMapForConda(
return shellMaps;
}

logs.push('✓ Using source command with preferred path');
const condaSourcingPathFirst = {
executable: 'source',
args: [preferredSourcingPath, envIdentifier],
};
shellMaps = await generateShellActivationMapFromConfig([condaSourcingPathFirst], [condaCommonDeactivate]);
logs.push('✓ Using shell-specific activation commands');
const condaShPath = envManager.sourcingInformation.shellSourcingScripts?.sh;
const condaFishPath = envManager.sourcingInformation.shellSourcingScripts?.fish;
const condaPs1Path = envManager.sourcingInformation.shellSourcingScripts?.ps1;

shellMaps = nonWindowsGenerateConfig(
preferredSourcingPath,
envIdentifier,
condaCommonDeactivate,
envManager.sourcingInformation.condaPath,
condaShPath,
condaFishPath,
condaPs1Path,
envManager.sourcingInformation.shellInitStatus,
);
return shellMaps;
} catch (error) {
logs.push(
Expand Down Expand Up @@ -579,6 +588,9 @@ async function generateShellActivationMapFromConfig(
shellActivation.set(ShellConstants.PWSH, activate);
shellDeactivation.set(ShellConstants.PWSH, deactivate);

shellActivation.set(ShellConstants.FISH, activate);
shellDeactivation.set(ShellConstants.FISH, deactivate);

return { shellActivation, shellDeactivation };
}

Expand Down Expand Up @@ -648,6 +660,98 @@ export async function windowsExceptionGenerateConfig(
return { shellActivation, shellDeactivation };
}

/**
* Generates shell-specific activation configuration for non-Windows (Linux/macOS).
* Uses conda.sh for bash-like shells, conda.fish for Fish, and conda-hook.ps1 for PowerShell.
* Falls back to `source <activate-script> <env>` for bash-like shells when conda.sh is unavailable.
*
* For each shell, when the shell-specific sourcing script is not found, checks whether
* `conda init <shell>` has been run (via shellInitStatus). If it has, bare `conda` is used
* since the shell will set up the conda function on startup. Otherwise, the full conda path
* is used as the executable.
*
* @internal Exported for testing
*/
export function nonWindowsGenerateConfig(
sourceInitPath: string,
envIdentifier: string,
condaCommonDeactivate: PythonCommandRunConfiguration,
condaPath: string,
condaShPath?: string,
condaFishPath?: string,
condaPs1Path?: string,
shellInitStatus?: ShellCondaInitStatus,
): ShellCommandMaps {
const shellActivation: Map<string, PythonCommandRunConfiguration[]> = new Map();
const shellDeactivation: Map<string, PythonCommandRunConfiguration[]> = new Map();
const deactivate = [condaCommonDeactivate];

// Helper: determine the conda executable for a given shell based on init status.
// If `conda init <shell>` has been run, the shell profile sets up `conda` as a shell
// function on startup, so bare `conda` works. Otherwise, use the full path to the
// conda binary (the user will see an actionable error if hooks aren't set up).
const condaExe = (shell: keyof ShellCondaInitStatus): string => (shellInitStatus?.[shell] ? 'conda' : condaPath);

// Bash-like shells: use conda.sh if available, otherwise fall back to source activate
let bashActivate: PythonCommandRunConfiguration[];
if (condaShPath) {
bashActivate = [
{ executable: 'source', args: [condaShPath] },
{ executable: 'conda', args: ['activate', envIdentifier] },
];
} else {
bashActivate = [{ executable: 'source', args: [sourceInitPath, envIdentifier] }];
}

shellActivation.set(ShellConstants.BASH, bashActivate);
shellDeactivation.set(ShellConstants.BASH, deactivate);

shellActivation.set(ShellConstants.SH, bashActivate);
shellDeactivation.set(ShellConstants.SH, deactivate);

shellActivation.set(ShellConstants.ZSH, bashActivate);
shellDeactivation.set(ShellConstants.ZSH, deactivate);

shellActivation.set(ShellConstants.GITBASH, bashActivate);
shellDeactivation.set(ShellConstants.GITBASH, deactivate);
Comment on lines +709 to +716
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For ShellConstants.SH, the activation command uses source ..., but POSIX sh (e.g., dash) typically does not support source (it uses .). This will cause activation to fail when the user’s integrated shell is sh. Consider generating a separate activation sequence for sh that uses . (dot) instead of source (both for sourcing conda.sh and for the activate fallback).

Copilot uses AI. Check for mistakes.

// Fish shell: use conda.fish if available. Otherwise, check if `conda init fish`
// was run — if so, bare `conda` works; if not, use the full conda path.
let fishActivate: PythonCommandRunConfiguration[];
if (condaFishPath) {
fishActivate = [
{ executable: 'source', args: [condaFishPath] },
{ executable: 'conda', args: ['activate', envIdentifier] },
];
} else {
fishActivate = [{ executable: condaExe('fish'), args: ['activate', envIdentifier] }];
}

shellActivation.set(ShellConstants.FISH, fishActivate);
shellDeactivation.set(ShellConstants.FISH, deactivate);

// PowerShell: use conda-hook.ps1 if available. Otherwise, check if `conda init powershell`
// was run — if so, bare `conda` works; if not, use the full conda path.
let pwshActivate: PythonCommandRunConfiguration[];
if (condaPs1Path) {
pwshActivate = [{ executable: condaPs1Path }, { executable: 'conda', args: ['activate', envIdentifier] }];
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PowerShell activation runs the hook script by placing the .ps1 path directly in executable. If the path contains spaces, the command builder will quote it, and in pwsh a quoted path is treated as a string (not invoked) unless prefixed with the call operator &. To make this robust, consider emitting the first PowerShell command as & <condaPs1Path> (e.g., executable: '&', args: [condaPs1Path]) rather than executable: condaPs1Path.

Suggested change
pwshActivate = [{ executable: condaPs1Path }, { executable: 'conda', args: ['activate', envIdentifier] }];
pwshActivate = [
{ executable: '&', args: [condaPs1Path] },
{ executable: 'conda', args: ['activate', envIdentifier] },
];

Copilot uses AI. Check for mistakes.
} else {
pwshActivate = [{ executable: condaExe('pwsh'), args: ['activate', envIdentifier] }];
}

shellActivation.set(ShellConstants.PWSH, pwshActivate);
shellDeactivation.set(ShellConstants.PWSH, deactivate);

traceVerbose(
`Non-Windows activation commands:
Bash: ${JSON.stringify(bashActivate)},
Fish: ${JSON.stringify(fishActivate)},
PowerShell: ${JSON.stringify(pwshActivate)}`,
);

return { shellActivation, shellDeactivation };
}

function getCondaWithoutPython(name: string, prefix: string, conda: string): PythonEnvironmentInfo {
return {
name: name,
Expand Down
Loading
Loading