Skip to content

Commit afb0a7f

Browse files
committed
fix: use shell-specific conda activation and check conda init status (Fixes #1313)
1 parent 0ab87b1 commit afb0a7f

File tree

4 files changed

+621
-8
lines changed

4 files changed

+621
-8
lines changed

src/managers/conda/condaSourcingUtils.ts

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
import * as fse from 'fs-extra';
5+
import * as os from 'os';
56
import * as path from 'path';
67
import { traceError, traceInfo, traceVerbose } from '../../common/logging';
78
import { isWindows } from '../../common/utils/platformUtils';
@@ -17,6 +18,20 @@ export interface ShellSourcingScripts {
1718
sh?: string;
1819
/** Windows CMD batch file (activate.bat) */
1920
cmd?: string;
21+
/** Fish shell initialization script (conda.fish) */
22+
fish?: string;
23+
}
24+
25+
/**
26+
* Tracks whether `conda init <shell>` has been run for each shell type.
27+
* When true, the shell's profile/config file contains the conda initialization block,
28+
* meaning bare `conda` will be available as a shell function when that shell starts.
29+
*/
30+
export interface ShellCondaInitStatus {
31+
bash?: boolean;
32+
zsh?: boolean;
33+
fish?: boolean;
34+
pwsh?: boolean;
2035
}
2136

2237
/**
@@ -37,6 +52,7 @@ export class CondaSourcingStatus {
3752
public isActiveOnLaunch?: boolean,
3853
public globalSourcingScript?: string,
3954
public shellSourcingScripts?: ShellSourcingScripts,
55+
public shellInitStatus?: ShellCondaInitStatus,
4056
) {}
4157

4258
/**
@@ -59,6 +75,7 @@ export class CondaSourcingStatus {
5975
scripts.ps1 && `PowerShell: ${scripts.ps1}`,
6076
scripts.sh && `Bash/sh: ${scripts.sh}`,
6177
scripts.cmd && `CMD: ${scripts.cmd}`,
78+
scripts.fish && `Fish: ${scripts.fish}`,
6279
].filter(Boolean);
6380

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

94+
if (this.shellInitStatus) {
95+
const initEntries = (['bash', 'zsh', 'fish', 'pwsh'] as const)
96+
.map((s) => `${s}: ${this.shellInitStatus![s] ? '✓' : '✗'}`)
97+
.join(', ');
98+
lines.push(`├─ Shell conda init status: ${initEntries}`);
99+
}
100+
77101
return lines.join('\n');
78102
}
79103
}
@@ -116,6 +140,9 @@ export async function constructCondaSourcingStatus(condaPath: string): Promise<C
116140
// find and save all of the shell specific sourcing scripts
117141
sourcingStatus.shellSourcingScripts = await findShellSourcingScripts(sourcingStatus);
118142

143+
// check shell profile files to see if `conda init <shell>` has been run
144+
sourcingStatus.shellInitStatus = await checkCondaInitInShellProfiles();
145+
119146
return sourcingStatus;
120147
}
121148

@@ -148,6 +175,7 @@ export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStat
148175
let ps1Script: string | undefined;
149176
let shScript: string | undefined;
150177
let cmdActivate: string | undefined;
178+
let fishScript: string | undefined;
151179

152180
try {
153181
// Search for PowerShell hook script (conda-hook.ps1)
@@ -178,20 +206,103 @@ export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStat
178206
} catch (err) {
179207
logs.push(` Error during CMD script search: ${err instanceof Error ? err.message : 'Unknown error'}`);
180208
}
209+
210+
// Search for Fish shell script (conda.fish)
211+
logs.push('\nSearching for Fish shell script...');
212+
try {
213+
fishScript = await getCondaFishPath(sourcingStatus.condaFolder);
214+
logs.push(` Path: ${fishScript ?? '✗ Not found'}`);
215+
} catch (err) {
216+
logs.push(` Error during Fish script search: ${err instanceof Error ? err.message : 'Unknown error'}`);
217+
}
181218
} catch (error) {
182219
logs.push(`\nCritical error during script search: ${error instanceof Error ? error.message : 'Unknown error'}`);
183220
} finally {
184221
logs.push('\nSearch Summary:');
185222
logs.push(` PowerShell: ${ps1Script ? '✓' : '✗'}`);
186223
logs.push(` Shell: ${shScript ? '✓' : '✗'}`);
187224
logs.push(` CMD: ${cmdActivate ? '✓' : '✗'}`);
225+
logs.push(` Fish: ${fishScript ? '✓' : '✗'}`);
188226
logs.push('============================');
189227

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

194-
return { ps1: ps1Script, sh: shScript, cmd: cmdActivate };
232+
return { ps1: ps1Script, sh: shScript, cmd: cmdActivate, fish: fishScript };
233+
}
234+
235+
/**
236+
* Checks shell profile/config files to determine if `conda init <shell>` has been run.
237+
*
238+
* When `conda init <shell>` is run, it adds a `# >>> conda initialize >>>` block to the
239+
* shell's profile. If that block is present, then any new terminal of that shell type will
240+
* have `conda` available as a shell function, and bare `conda activate` will work.
241+
*
242+
* For Fish, `conda init fish` may either modify `config.fish` or drop a file in
243+
* `~/.config/fish/conf.d/`, so both locations are checked.
244+
*
245+
* @param homeDir Optional home directory override (defaults to os.homedir(), useful for testing)
246+
* @returns Status object indicating which shells have conda initialized
247+
*/
248+
export async function checkCondaInitInShellProfiles(homeDir?: string): Promise<ShellCondaInitStatus> {
249+
const home = homeDir ?? os.homedir();
250+
const status: ShellCondaInitStatus = {};
251+
const logs: string[] = ['=== Checking shell profiles for conda init ==='];
252+
253+
const checks: Array<{ shell: keyof ShellCondaInitStatus; files: string[] }> = [
254+
{
255+
shell: 'bash',
256+
files: [path.join(home, '.bashrc'), path.join(home, '.bash_profile')],
257+
},
258+
{
259+
shell: 'zsh',
260+
files: [path.join(home, '.zshrc')],
261+
},
262+
{
263+
shell: 'fish',
264+
files: [
265+
path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'fish', 'config.fish'),
266+
path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'fish', 'conf.d', 'conda.fish'),
267+
],
268+
},
269+
{
270+
shell: 'pwsh',
271+
files: [
272+
path.join(
273+
process.env.XDG_CONFIG_HOME || path.join(home, '.config'),
274+
'powershell',
275+
'Microsoft.PowerShell_profile.ps1',
276+
),
277+
path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'powershell', 'profile.ps1'),
278+
],
279+
},
280+
];
281+
282+
await Promise.all(
283+
checks.map(async ({ shell, files }) => {
284+
for (const filePath of files) {
285+
try {
286+
if (await fse.pathExists(filePath)) {
287+
const content = await fse.readFile(filePath, 'utf-8');
288+
if (content.includes('conda initialize')) {
289+
status[shell] = true;
290+
logs.push(` ${shell}: ✓ conda init found in ${filePath}`);
291+
return;
292+
}
293+
}
294+
} catch {
295+
// File not readable, skip
296+
}
297+
}
298+
logs.push(` ${shell}: ✗ conda init not found`);
299+
}),
300+
);
301+
302+
logs.push('============================');
303+
traceVerbose(logs.join('\n'));
304+
305+
return status;
195306
}
196307

197308
/**
@@ -308,6 +419,24 @@ async function getCondaShPath(condaFolder: string): Promise<string | undefined>
308419
return shPathPromise;
309420
}
310421

422+
/**
423+
* Returns the path to conda.fish given a conda installation folder.
424+
*
425+
* Searches for conda.fish in these locations (relative to the conda root):
426+
* - etc/fish/conf.d/conda.fish
427+
* - shell/etc/fish/conf.d/conda.fish
428+
* - Library/etc/fish/conf.d/conda.fish
429+
*/
430+
export async function getCondaFishPath(condaFolder: string): Promise<string | undefined> {
431+
const locations = [
432+
path.join(condaFolder, 'etc', 'fish', 'conf.d', 'conda.fish'),
433+
path.join(condaFolder, 'shell', 'etc', 'fish', 'conf.d', 'conda.fish'),
434+
path.join(condaFolder, 'Library', 'etc', 'fish', 'conf.d', 'conda.fish'),
435+
];
436+
437+
return findFileInLocations(locations, 'conda.fish');
438+
}
439+
311440
/**
312441
* Returns the path to the Windows batch activation file (activate.bat) for conda
313442
* @param condaPath The path to the conda executable

src/managers/conda/condaUtils.ts

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import { selectFromCommonPackagesToInstall } from '../common/pickers';
5555
import { Installable } from '../common/types';
5656
import { shortVersion, sortEnvironments } from '../common/utils';
5757
import { CondaEnvManager } from './condaEnvManager';
58-
import { getCondaHookPs1Path, getLocalActivationScript } from './condaSourcingUtils';
58+
import { getCondaHookPs1Path, getLocalActivationScript, ShellCondaInitStatus } from './condaSourcingUtils';
5959
import { createStepBasedCondaFlow } from './condaStepBasedFlow';
6060

6161
export const CONDA_PATH_KEY = `${ENVS_EXTENSION_ID}:conda:CONDA_PATH`;
@@ -526,12 +526,21 @@ async function buildShellActivationMapForConda(
526526
return shellMaps;
527527
}
528528

529-
logs.push('✓ Using source command with preferred path');
530-
const condaSourcingPathFirst = {
531-
executable: 'source',
532-
args: [preferredSourcingPath, envIdentifier],
533-
};
534-
shellMaps = await generateShellActivationMapFromConfig([condaSourcingPathFirst], [condaCommonDeactivate]);
529+
logs.push('✓ Using shell-specific activation commands');
530+
const condaShPath = envManager.sourcingInformation.shellSourcingScripts?.sh;
531+
const condaFishPath = envManager.sourcingInformation.shellSourcingScripts?.fish;
532+
const condaPs1Path = envManager.sourcingInformation.shellSourcingScripts?.ps1;
533+
534+
shellMaps = nonWindowsGenerateConfig(
535+
preferredSourcingPath,
536+
envIdentifier,
537+
condaCommonDeactivate,
538+
envManager.sourcingInformation.condaPath,
539+
condaShPath,
540+
condaFishPath,
541+
condaPs1Path,
542+
envManager.sourcingInformation.shellInitStatus,
543+
);
535544
return shellMaps;
536545
} catch (error) {
537546
logs.push(
@@ -579,6 +588,9 @@ async function generateShellActivationMapFromConfig(
579588
shellActivation.set(ShellConstants.PWSH, activate);
580589
shellDeactivation.set(ShellConstants.PWSH, deactivate);
581590

591+
shellActivation.set(ShellConstants.FISH, activate);
592+
shellDeactivation.set(ShellConstants.FISH, deactivate);
593+
582594
return { shellActivation, shellDeactivation };
583595
}
584596

@@ -648,6 +660,98 @@ export async function windowsExceptionGenerateConfig(
648660
return { shellActivation, shellDeactivation };
649661
}
650662

663+
/**
664+
* Generates shell-specific activation configuration for non-Windows (Linux/macOS).
665+
* Uses conda.sh for bash-like shells, conda.fish for Fish, and conda-hook.ps1 for PowerShell.
666+
* Falls back to `source <activate-script> <env>` for bash-like shells when conda.sh is unavailable.
667+
*
668+
* For each shell, when the shell-specific sourcing script is not found, checks whether
669+
* `conda init <shell>` has been run (via shellInitStatus). If it has, bare `conda` is used
670+
* since the shell will set up the conda function on startup. Otherwise, the full conda path
671+
* is used as the executable.
672+
*
673+
* @internal Exported for testing
674+
*/
675+
export function nonWindowsGenerateConfig(
676+
sourceInitPath: string,
677+
envIdentifier: string,
678+
condaCommonDeactivate: PythonCommandRunConfiguration,
679+
condaPath: string,
680+
condaShPath?: string,
681+
condaFishPath?: string,
682+
condaPs1Path?: string,
683+
shellInitStatus?: ShellCondaInitStatus,
684+
): ShellCommandMaps {
685+
const shellActivation: Map<string, PythonCommandRunConfiguration[]> = new Map();
686+
const shellDeactivation: Map<string, PythonCommandRunConfiguration[]> = new Map();
687+
const deactivate = [condaCommonDeactivate];
688+
689+
// Helper: determine the conda executable for a given shell based on init status.
690+
// If `conda init <shell>` has been run, the shell profile sets up `conda` as a shell
691+
// function on startup, so bare `conda` works. Otherwise, use the full path to the
692+
// conda binary (the user will see an actionable error if hooks aren't set up).
693+
const condaExe = (shell: keyof ShellCondaInitStatus): string => (shellInitStatus?.[shell] ? 'conda' : condaPath);
694+
695+
// Bash-like shells: use conda.sh if available, otherwise fall back to source activate
696+
let bashActivate: PythonCommandRunConfiguration[];
697+
if (condaShPath) {
698+
bashActivate = [
699+
{ executable: 'source', args: [condaShPath] },
700+
{ executable: 'conda', args: ['activate', envIdentifier] },
701+
];
702+
} else {
703+
bashActivate = [{ executable: 'source', args: [sourceInitPath, envIdentifier] }];
704+
}
705+
706+
shellActivation.set(ShellConstants.BASH, bashActivate);
707+
shellDeactivation.set(ShellConstants.BASH, deactivate);
708+
709+
shellActivation.set(ShellConstants.SH, bashActivate);
710+
shellDeactivation.set(ShellConstants.SH, deactivate);
711+
712+
shellActivation.set(ShellConstants.ZSH, bashActivate);
713+
shellDeactivation.set(ShellConstants.ZSH, deactivate);
714+
715+
shellActivation.set(ShellConstants.GITBASH, bashActivate);
716+
shellDeactivation.set(ShellConstants.GITBASH, deactivate);
717+
718+
// Fish shell: use conda.fish if available. Otherwise, check if `conda init fish`
719+
// was run — if so, bare `conda` works; if not, use the full conda path.
720+
let fishActivate: PythonCommandRunConfiguration[];
721+
if (condaFishPath) {
722+
fishActivate = [
723+
{ executable: 'source', args: [condaFishPath] },
724+
{ executable: 'conda', args: ['activate', envIdentifier] },
725+
];
726+
} else {
727+
fishActivate = [{ executable: condaExe('fish'), args: ['activate', envIdentifier] }];
728+
}
729+
730+
shellActivation.set(ShellConstants.FISH, fishActivate);
731+
shellDeactivation.set(ShellConstants.FISH, deactivate);
732+
733+
// PowerShell: use conda-hook.ps1 if available. Otherwise, check if `conda init powershell`
734+
// was run — if so, bare `conda` works; if not, use the full conda path.
735+
let pwshActivate: PythonCommandRunConfiguration[];
736+
if (condaPs1Path) {
737+
pwshActivate = [{ executable: condaPs1Path }, { executable: 'conda', args: ['activate', envIdentifier] }];
738+
} else {
739+
pwshActivate = [{ executable: condaExe('pwsh'), args: ['activate', envIdentifier] }];
740+
}
741+
742+
shellActivation.set(ShellConstants.PWSH, pwshActivate);
743+
shellDeactivation.set(ShellConstants.PWSH, deactivate);
744+
745+
traceVerbose(
746+
`Non-Windows activation commands:
747+
Bash: ${JSON.stringify(bashActivate)},
748+
Fish: ${JSON.stringify(fishActivate)},
749+
PowerShell: ${JSON.stringify(pwshActivate)}`,
750+
);
751+
752+
return { shellActivation, shellDeactivation };
753+
}
754+
651755
function getCondaWithoutPython(name: string, prefix: string, conda: string): PythonEnvironmentInfo {
652756
return {
653757
name: name,

0 commit comments

Comments
 (0)