From 84f4f290ac0a1922d492a6cd229723557402d783 Mon Sep 17 00:00:00 2001 From: Shubham-Padkonde Date: Sat, 19 Sep 2026 20:07:37 +0530 Subject: [PATCH] fix: skip terminal activation when there is no activate script getShellActivationCommands() emitted `source /activate` for sh, bash, zsh, Git Bash, ksh and the "unknown" shell without checking that the script exists, unlike the other shells. For base interpreters that share the layout but are not virtual environments (for example `uv python install` toolchains), every new terminal then ran a failing `source` command. Only add these entries when `activate` exists, and treat an environment with an empty shell activation map as not activatable. Fixes #1775 Co-Authored-By: Claude Opus 5 --- src/features/common/activation.ts | 2 +- src/managers/common/utils.ts | 42 +++++++------ .../features/common/activation.unit.test.ts | 26 ++++++++ ...ls.getShellActivationCommands.unit.test.ts | 62 ++++++++++++++++++- 4 files changed, 110 insertions(+), 22 deletions(-) create mode 100644 src/test/features/common/activation.unit.test.ts diff --git a/src/features/common/activation.ts b/src/features/common/activation.ts index c44a685cf..e4b07a023 100644 --- a/src/features/common/activation.ts +++ b/src/features/common/activation.ts @@ -9,7 +9,7 @@ import { import { identifyTerminalShell } from './shellDetector'; export function isActivatableEnvironment(environment: PythonEnvironment): boolean { - return !!environment.execInfo?.activation || !!environment.execInfo?.shellActivation; + return !!environment.execInfo?.activation || (environment.execInfo?.shellActivation?.size ?? 0) > 0; } export function isActivatedRunAvailable(environment: PythonEnvironment): boolean { diff --git a/src/managers/common/utils.ts b/src/managers/common/utils.ts index 60ccf9473..6358bf54e 100644 --- a/src/managers/common/utils.ts +++ b/src/managers/common/utils.ts @@ -115,30 +115,34 @@ export async function getShellActivationCommands(binDir: string): Promise<{ const shellActivation: Map = new Map(); const shellDeactivation: Map = new Map(); - if (isWindows()) { - shellActivation.set('unknown', [{ executable: path.join(binDir, `activate`) }]); - shellDeactivation.set('unknown', [{ executable: path.join(binDir, `deactivate`) }]); - } else { - shellActivation.set('unknown', [{ executable: 'source', args: [path.join(binDir, `activate`)] }]); - shellDeactivation.set('unknown', [{ executable: 'deactivate' }]); - } + // Base interpreters (for example `uv python install` toolchains) share this layout but have no + // activation scripts, so only offer activation for scripts that exist, like the shells below. + if (await fs.pathExists(path.join(binDir, 'activate'))) { + if (isWindows()) { + shellActivation.set('unknown', [{ executable: path.join(binDir, `activate`) }]); + shellDeactivation.set('unknown', [{ executable: path.join(binDir, `deactivate`) }]); + } else { + shellActivation.set('unknown', [{ executable: 'source', args: [path.join(binDir, `activate`)] }]); + shellDeactivation.set('unknown', [{ executable: 'deactivate' }]); + } - shellActivation.set(ShellConstants.SH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]); - shellDeactivation.set(ShellConstants.SH, [{ executable: 'deactivate' }]); + shellActivation.set(ShellConstants.SH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]); + shellDeactivation.set(ShellConstants.SH, [{ executable: 'deactivate' }]); - shellActivation.set(ShellConstants.BASH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]); - shellDeactivation.set(ShellConstants.BASH, [{ executable: 'deactivate' }]); + shellActivation.set(ShellConstants.BASH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]); + shellDeactivation.set(ShellConstants.BASH, [{ executable: 'deactivate' }]); - shellActivation.set(ShellConstants.GITBASH, [ - { executable: 'source', args: [pathForGitBash(path.join(binDir, `activate`))] }, - ]); - shellDeactivation.set(ShellConstants.GITBASH, [{ executable: 'deactivate' }]); + shellActivation.set(ShellConstants.GITBASH, [ + { executable: 'source', args: [pathForGitBash(path.join(binDir, `activate`))] }, + ]); + shellDeactivation.set(ShellConstants.GITBASH, [{ executable: 'deactivate' }]); - shellActivation.set(ShellConstants.ZSH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]); - shellDeactivation.set(ShellConstants.ZSH, [{ executable: 'deactivate' }]); + shellActivation.set(ShellConstants.ZSH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]); + shellDeactivation.set(ShellConstants.ZSH, [{ executable: 'deactivate' }]); - shellActivation.set(ShellConstants.KSH, [{ executable: '.', args: [path.join(binDir, `activate`)] }]); - shellDeactivation.set(ShellConstants.KSH, [{ executable: 'deactivate' }]); + shellActivation.set(ShellConstants.KSH, [{ executable: '.', args: [path.join(binDir, `activate`)] }]); + shellDeactivation.set(ShellConstants.KSH, [{ executable: 'deactivate' }]); + } if (await fs.pathExists(path.join(binDir, 'Activate.ps1'))) { shellActivation.set(ShellConstants.PWSH, buildPwshActivationCommands(path.join(binDir, 'Activate.ps1'))); diff --git a/src/test/features/common/activation.unit.test.ts b/src/test/features/common/activation.unit.test.ts new file mode 100644 index 000000000..a8822bd2a --- /dev/null +++ b/src/test/features/common/activation.unit.test.ts @@ -0,0 +1,26 @@ +import assert from 'assert'; +import { PythonEnvironment } from '../../../api'; +import { isActivatableEnvironment } from '../../../features/common/activation'; + +suite('isActivatableEnvironment', () => { + function env(execInfo: Partial): PythonEnvironment { + return { execInfo: { run: { executable: 'python' }, ...execInfo } } as PythonEnvironment; + } + + test('Environment without activation is not activatable', () => { + assert.strictEqual(isActivatableEnvironment(env({})), false); + }); + + test('Environment with an empty shell activation map is not activatable', () => { + assert.strictEqual(isActivatableEnvironment(env({ shellActivation: new Map() })), false); + }); + + test('Environment with shell activation commands is activatable', () => { + const shellActivation = new Map([['bash', [{ executable: 'source', args: ['activate'] }]]]); + assert.strictEqual(isActivatableEnvironment(env({ shellActivation })), true); + }); + + test('Environment with an activation command is activatable', () => { + assert.strictEqual(isActivatableEnvironment(env({ activation: [{ executable: 'activate' }] })), true); + }); +}); diff --git a/src/test/managers/common/utils.getShellActivationCommands.unit.test.ts b/src/test/managers/common/utils.getShellActivationCommands.unit.test.ts index 9f21bf618..33d7d0a21 100644 --- a/src/test/managers/common/utils.getShellActivationCommands.unit.test.ts +++ b/src/test/managers/common/utils.getShellActivationCommands.unit.test.ts @@ -89,7 +89,11 @@ suite('getShellActivationCommands', () => { const pwshActivation = result.shellActivation.get(ShellConstants.PWSH); assert.ok(pwshActivation, 'PowerShell activation should be defined'); - assert.strictEqual(pwshActivation.length, 1, 'Should have only 1 command: activate (no Set-ExecutionPolicy)'); + assert.strictEqual( + pwshActivation.length, + 1, + 'Should have only 1 command: activate (no Set-ExecutionPolicy)', + ); assert.strictEqual(pwshActivation[0].executable, '&'); assert.ok(pwshActivation[0].args); assert.ok( @@ -106,7 +110,11 @@ suite('getShellActivationCommands', () => { const pwshActivation = result.shellActivation.get(ShellConstants.PWSH); assert.ok(pwshActivation, 'PowerShell activation should be defined'); - assert.strictEqual(pwshActivation.length, 1, 'Should have only 1 command: activate (no Set-ExecutionPolicy)'); + assert.strictEqual( + pwshActivation.length, + 1, + 'Should have only 1 command: activate (no Set-ExecutionPolicy)', + ); assert.strictEqual(pwshActivation[0].executable, '&'); assert.ok(pwshActivation[0].args); assert.ok( @@ -131,6 +139,7 @@ suite('getShellActivationCommands', () => { suite('Other shells are not affected by execution policy change', () => { test('Bash activation does not include Set-ExecutionPolicy', async () => { isWindowsStub.returns(false); + await fs.writeFile(path.join(tmpDir, 'activate'), ''); const result = await getShellActivationCommands(tmpDir); const bashActivation = result.shellActivation.get(ShellConstants.BASH); @@ -156,6 +165,7 @@ suite('getShellActivationCommands', () => { suite('Windows unknown shell fallback', () => { test('Windows unknown shell uses activate without Set-ExecutionPolicy', async () => { isWindowsStub.returns(true); + await fs.writeFile(path.join(tmpDir, 'activate'), ''); const result = await getShellActivationCommands(tmpDir); const unknownActivation = result.shellActivation.get('unknown'); @@ -165,4 +175,52 @@ suite('getShellActivationCommands', () => { assert.ok(unknownActivation[0].executable.endsWith('activate')); }); }); + + suite('No activation without activation scripts', () => { + test('No POSIX shell activation when bin has no activate script (e.g. uv python toolchain)', async () => { + isWindowsStub.returns(false); + await fs.writeFile(path.join(tmpDir, 'python3'), ''); + + const result = await getShellActivationCommands(tmpDir); + + for (const shell of [ + 'unknown', + ShellConstants.SH, + ShellConstants.BASH, + ShellConstants.GITBASH, + ShellConstants.ZSH, + ShellConstants.KSH, + ]) { + assert.strictEqual(result.shellActivation.get(shell), undefined, `${shell} should not be activated`); + assert.strictEqual( + result.shellDeactivation.get(shell), + undefined, + `${shell} should not be deactivated`, + ); + } + assert.strictEqual(result.shellActivation.size, 0); + }); + + test('No Windows unknown shell activation when Scripts has no activate script', async () => { + isWindowsStub.returns(true); + + const result = await getShellActivationCommands(tmpDir); + + assert.strictEqual(result.shellActivation.get('unknown'), undefined); + assert.strictEqual(result.shellActivation.size, 0); + }); + + test('POSIX shells are activated when the activate script exists', async () => { + isWindowsStub.returns(false); + await fs.writeFile(path.join(tmpDir, 'activate'), ''); + + const result = await getShellActivationCommands(tmpDir); + + for (const shell of [ShellConstants.SH, ShellConstants.BASH, ShellConstants.ZSH]) { + assert.deepStrictEqual(result.shellActivation.get(shell), [ + { executable: 'source', args: [path.join(tmpDir, 'activate')] }, + ]); + } + }); + }); });