diff --git a/src/features/common/activation.ts b/src/features/common/activation.ts index c44a685c..e4b07a02 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 60ccf947..6358bf54 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 00000000..a8822bd2 --- /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 9f21bf61..33d7d0a2 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')] }, + ]); + } + }); + }); });