From 1a0c7883007a30d7b2c3a73119dfbd382bf791ec Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 24 Jan 2025 14:04:03 +0100 Subject: [PATCH 1/4] =?UTF-8?q?Handle=20all=20shells=E2=80=99=20de/activat?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/utils/unsafeEntries.ts | 5 ++ src/features/common/shellDetector.ts | 32 +++++++------ src/managers/builtin/venvUtils.ts | 72 +++++++++++++++------------- 3 files changed, 61 insertions(+), 48 deletions(-) create mode 100644 src/common/utils/unsafeEntries.ts diff --git a/src/common/utils/unsafeEntries.ts b/src/common/utils/unsafeEntries.ts new file mode 100644 index 00000000..5c4bfeea --- /dev/null +++ b/src/common/utils/unsafeEntries.ts @@ -0,0 +1,5 @@ +/** Converts an object from a trusted source (i.e. without unknown entries) to a typed array */ +export default function entries(o: T): [keyof T, T[K]][] { + return Object.entries(o) as [keyof T, T[K]][] +} + \ No newline at end of file diff --git a/src/features/common/shellDetector.ts b/src/features/common/shellDetector.ts index 5d5acb43..fe5644ec 100644 --- a/src/features/common/shellDetector.ts +++ b/src/features/common/shellDetector.ts @@ -3,6 +3,7 @@ import { isWindows } from '../../managers/common/utils'; import * as os from 'os'; import { vscodeShell } from '../../common/vscodeEnv.apis'; import { getConfiguration } from '../../common/workspace.apis'; +import unsafeEntries from '../../common/utils/unsafeEntries'; import { TerminalShellType } from '../../api'; /* @@ -30,20 +31,23 @@ const IS_TCSHELL = /(tcsh$)/i; const IS_NUSHELL = /(nu$)/i; const IS_XONSH = /(xonsh$)/i; -const detectableShells = new Map(); -detectableShells.set(TerminalShellType.powershell, IS_POWERSHELL); -detectableShells.set(TerminalShellType.gitbash, IS_GITBASH); -detectableShells.set(TerminalShellType.bash, IS_BASH); -detectableShells.set(TerminalShellType.wsl, IS_WSL); -detectableShells.set(TerminalShellType.zsh, IS_ZSH); -detectableShells.set(TerminalShellType.ksh, IS_KSH); -detectableShells.set(TerminalShellType.commandPrompt, IS_COMMAND); -detectableShells.set(TerminalShellType.fish, IS_FISH); -detectableShells.set(TerminalShellType.tcshell, IS_TCSHELL); -detectableShells.set(TerminalShellType.cshell, IS_CSHELL); -detectableShells.set(TerminalShellType.nushell, IS_NUSHELL); -detectableShells.set(TerminalShellType.powershellCore, IS_POWERSHELL_CORE); -detectableShells.set(TerminalShellType.xonsh, IS_XONSH); +type KnownShellType = Exclude; +const detectableShells = new Map(unsafeEntries({ + [TerminalShellType.powershell]: IS_POWERSHELL, + [TerminalShellType.gitbash]: IS_GITBASH, + [TerminalShellType.bash]: IS_BASH, + [TerminalShellType.wsl]: IS_WSL, + [TerminalShellType.zsh]: IS_ZSH, + [TerminalShellType.ksh]: IS_KSH, + [TerminalShellType.commandPrompt]: IS_COMMAND, + [TerminalShellType.fish]: IS_FISH, + [TerminalShellType.tcshell]: IS_TCSHELL, + [TerminalShellType.cshell]: IS_CSHELL, + [TerminalShellType.nushell]: IS_NUSHELL, + [TerminalShellType.powershellCore]: IS_POWERSHELL_CORE, + [TerminalShellType.xonsh]: IS_XONSH, +// This `satisfies` makes sure all shells are covered +} satisfies Record)); function identifyShellFromShellPath(shellPath: string): TerminalShellType { // Remove .exe extension so shells can be more consistently detected diff --git a/src/managers/builtin/venvUtils.ts b/src/managers/builtin/venvUtils.ts index f3a31409..4fbe5ad9 100644 --- a/src/managers/builtin/venvUtils.ts +++ b/src/managers/builtin/venvUtils.ts @@ -31,6 +31,7 @@ import { } from '../../common/window.apis'; import { showErrorMessage } from '../../common/errors/utils'; import { Common, VenvManagerStrings } from '../../common/localize'; +import unsafeEntries from '../../common/utils/unsafeEntries'; import { isUvInstalled, runUV, runPython } from './helpers'; import { getWorkspacePackagesToInstall } from './pipUtils'; @@ -117,44 +118,47 @@ async function getPythonInfo(env: NativeEnvInfo): Promise const name = `${venvName} (${sv})`; const binDir = path.dirname(env.executable); + + /** Venv activation/deactivation for a POSIXy shell */ + const venvMgr = (suffix = '', executable = 'source') => ({ + activate: { executable, args: [path.join(binDir, `activate${suffix}`)] }, + deactivate: { executable: 'deactivate' }, + }); + // A Record to make sure all shells are covered + const venvManagers: Record< + Exclude, + {activate: PythonCommandRunConfiguration, deactivate: PythonCommandRunConfiguration} + > = { + [TerminalShellType.bash]: venvMgr(), + [TerminalShellType.gitbash]: venvMgr(), + [TerminalShellType.zsh]: venvMgr(), + [TerminalShellType.wsl]: venvMgr(), + [TerminalShellType.ksh]: venvMgr('', '.'), + [TerminalShellType.cshell]: venvMgr('.csh'), + [TerminalShellType.tcshell]: venvMgr('.csh'), + [TerminalShellType.powershell]: venvMgr('.ps1', '&'), + [TerminalShellType.powershellCore]: venvMgr('.ps1', '&'), + [TerminalShellType.fish]: venvMgr('.fish'), + [TerminalShellType.commandPrompt]: { + activate: { executable: path.join(binDir, 'activate.bat') }, + deactivate: { executable: path.join(binDir, 'deactivate.bat') }, + }, + [TerminalShellType.xonsh]: venvMgr('.xsh'), + [TerminalShellType.nushell]: { + activate: { executable: 'overlay', args: ['use', path.join(binDir, 'activate.nu')] }, + deactivate: { executable: 'overlay', args: ['hide', 'activate'] }, + }, + }; const shellActivation: Map = new Map(); const shellDeactivation: Map = new Map(); - // Commands for bash - shellActivation.set(TerminalShellType.bash, [{ executable: 'source', args: [path.join(binDir, 'activate')] }]); - shellDeactivation.set(TerminalShellType.bash, [{ executable: 'deactivate' }]); - - // Commands for csh - shellActivation.set(TerminalShellType.cshell, [ - { executable: 'source', args: [path.join(binDir, 'activate')] }, - ]); - shellDeactivation.set(TerminalShellType.cshell, [{ executable: 'deactivate' }]); - - // Commands for zsh - shellActivation.set(TerminalShellType.zsh, [{ executable: 'source', args: [path.join(binDir, 'activate')] }]); - shellDeactivation.set(TerminalShellType.zsh, [{ executable: 'deactivate' }]); - - // Commands for powershell - shellActivation.set(TerminalShellType.powershell, [ - { executable: '&', args: [path.join(binDir, 'Activate.ps1')] }, - ]); - shellActivation.set(TerminalShellType.powershellCore, [ - { executable: '&', args: [path.join(binDir, 'Activate.ps1')] }, - ]); - shellDeactivation.set(TerminalShellType.powershell, [{ executable: 'deactivate' }]); - shellDeactivation.set(TerminalShellType.powershellCore, [{ executable: 'deactivate' }]); - - // Commands for command prompt - shellActivation.set(TerminalShellType.commandPrompt, [{ executable: path.join(binDir, 'activate.bat') }]); - shellDeactivation.set(TerminalShellType.commandPrompt, [{ executable: path.join(binDir, 'deactivate.bat') }]); - - // Commands for fish - if (await fsapi.pathExists(path.join(binDir, 'activate.fish'))) { - shellActivation.set(TerminalShellType.fish, [ - { executable: 'source', args: [path.join(binDir, 'activate.fish')] }, - ]); - shellDeactivation.set(TerminalShellType.fish, [{ executable: 'deactivate' }]); + for (const [shell, {activate, deactivate}] of unsafeEntries(venvManagers)) { + if (shell === TerminalShellType.fish && activate.args && !await fsapi.pathExists(activate?.args[0])) { + continue; + } + shellActivation.set(shell, [activate]); + shellDeactivation.set(shell, [deactivate]); } // Commands for unknown cases From 9bbe73ecffc076644e754b3813d7ce44886753a6 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 24 Jan 2025 14:25:26 +0100 Subject: [PATCH 2/4] add unknown to Record --- src/managers/builtin/venvUtils.ts | 50 +++++++++++++------------------ 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/src/managers/builtin/venvUtils.ts b/src/managers/builtin/venvUtils.ts index 4fbe5ad9..8c28c615 100644 --- a/src/managers/builtin/venvUtils.ts +++ b/src/managers/builtin/venvUtils.ts @@ -119,35 +119,38 @@ async function getPythonInfo(env: NativeEnvInfo): Promise const binDir = path.dirname(env.executable); + /** Venv activation/deactivation using a command */ + const cmdMgr = (suffix = '') => ({ + activate: { executable: path.join(binDir, `activate${suffix}`) }, + deactivate: { executable: path.join(binDir, `deactivate${suffix}`) }, + }); /** Venv activation/deactivation for a POSIXy shell */ - const venvMgr = (suffix = '', executable = 'source') => ({ + const sourceMgr = (suffix = '', executable = 'source') => ({ activate: { executable, args: [path.join(binDir, `activate${suffix}`)] }, deactivate: { executable: 'deactivate' }, }); - // A Record to make sure all shells are covered + // satisfies `Record` to make sure all shells are covered const venvManagers: Record< - Exclude, + TerminalShellType, {activate: PythonCommandRunConfiguration, deactivate: PythonCommandRunConfiguration} > = { - [TerminalShellType.bash]: venvMgr(), - [TerminalShellType.gitbash]: venvMgr(), - [TerminalShellType.zsh]: venvMgr(), - [TerminalShellType.wsl]: venvMgr(), - [TerminalShellType.ksh]: venvMgr('', '.'), - [TerminalShellType.cshell]: venvMgr('.csh'), - [TerminalShellType.tcshell]: venvMgr('.csh'), - [TerminalShellType.powershell]: venvMgr('.ps1', '&'), - [TerminalShellType.powershellCore]: venvMgr('.ps1', '&'), - [TerminalShellType.fish]: venvMgr('.fish'), - [TerminalShellType.commandPrompt]: { - activate: { executable: path.join(binDir, 'activate.bat') }, - deactivate: { executable: path.join(binDir, 'deactivate.bat') }, - }, - [TerminalShellType.xonsh]: venvMgr('.xsh'), + [TerminalShellType.bash]: sourceMgr(), + [TerminalShellType.gitbash]: sourceMgr(), + [TerminalShellType.zsh]: sourceMgr(), + [TerminalShellType.wsl]: sourceMgr(), + [TerminalShellType.ksh]: sourceMgr('', '.'), + [TerminalShellType.cshell]: sourceMgr('.csh'), + [TerminalShellType.tcshell]: sourceMgr('.csh'), + [TerminalShellType.powershell]: sourceMgr('.ps1', '&'), + [TerminalShellType.powershellCore]: sourceMgr('.ps1', '&'), + [TerminalShellType.fish]: sourceMgr('.fish'), + [TerminalShellType.commandPrompt]: cmdMgr('.bat'), + [TerminalShellType.xonsh]: sourceMgr('.xsh'), [TerminalShellType.nushell]: { activate: { executable: 'overlay', args: ['use', path.join(binDir, 'activate.nu')] }, deactivate: { executable: 'overlay', args: ['hide', 'activate'] }, }, + [TerminalShellType.unknown]: isWindows() ? cmdMgr() : sourceMgr(), }; const shellActivation: Map = new Map(); @@ -161,17 +164,6 @@ async function getPythonInfo(env: NativeEnvInfo): Promise shellDeactivation.set(shell, [deactivate]); } - // Commands for unknown cases - if (isWindows()) { - shellActivation.set(TerminalShellType.unknown, [{ executable: path.join(binDir, 'activate') }]); - shellDeactivation.set(TerminalShellType.unknown, [{ executable: 'deactivate' }]); - } else { - shellActivation.set(TerminalShellType.unknown, [ - { executable: 'source', args: [path.join(binDir, 'activate')] }, - ]); - shellDeactivation.set(TerminalShellType.unknown, [{ executable: 'deactivate' }]); - } - return { name: name, displayName: name, From 007f37e6aaffef572d302870da53c7ae19239ae6 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Sat, 25 Jan 2025 11:39:18 +0100 Subject: [PATCH 3/4] check all virtualenv shells --- src/common/utils/unsafeEntries.ts | 4 +-- src/managers/builtin/venvUtils.ts | 44 ++++++++++++++++++++----------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/common/utils/unsafeEntries.ts b/src/common/utils/unsafeEntries.ts index 5c4bfeea..9aef5ad6 100644 --- a/src/common/utils/unsafeEntries.ts +++ b/src/common/utils/unsafeEntries.ts @@ -1,5 +1,5 @@ /** Converts an object from a trusted source (i.e. without unknown entries) to a typed array */ -export default function entries(o: T): [keyof T, T[K]][] { - return Object.entries(o) as [keyof T, T[K]][] +export default function unsafeEntries(o: T): [keyof T, T[K]][] { + return Object.entries(o) as [keyof T, T[K]][]; } \ No newline at end of file diff --git a/src/managers/builtin/venvUtils.ts b/src/managers/builtin/venvUtils.ts index 8c28c615..e16f0b62 100644 --- a/src/managers/builtin/venvUtils.ts +++ b/src/managers/builtin/venvUtils.ts @@ -119,50 +119,64 @@ async function getPythonInfo(env: NativeEnvInfo): Promise const binDir = path.dirname(env.executable); + interface VenvManager { + activate: PythonCommandRunConfiguration, + deactivate: PythonCommandRunConfiguration, + /// true if created by the builtin `venv` module and not just the `virtualenv` package. + supportsStdlib: boolean, + } + /** Venv activation/deactivation using a command */ - const cmdMgr = (suffix = '') => ({ + const cmdMgr = (suffix = ''): VenvManager => ({ activate: { executable: path.join(binDir, `activate${suffix}`) }, deactivate: { executable: path.join(binDir, `deactivate${suffix}`) }, + supportsStdlib: true, }); /** Venv activation/deactivation for a POSIXy shell */ - const sourceMgr = (suffix = '', executable = 'source') => ({ + const sourceMgr = (suffix = '', executable = 'source'): VenvManager => ({ activate: { executable, args: [path.join(binDir, `activate${suffix}`)] }, deactivate: { executable: 'deactivate' }, + supportsStdlib: ['', '.ps1'].includes(suffix) }); // satisfies `Record` to make sure all shells are covered - const venvManagers: Record< - TerminalShellType, - {activate: PythonCommandRunConfiguration, deactivate: PythonCommandRunConfiguration} - > = { + const venvManagers: Record = { + // Shells supported by the builtin `venv` module [TerminalShellType.bash]: sourceMgr(), [TerminalShellType.gitbash]: sourceMgr(), [TerminalShellType.zsh]: sourceMgr(), [TerminalShellType.wsl]: sourceMgr(), [TerminalShellType.ksh]: sourceMgr('', '.'), - [TerminalShellType.cshell]: sourceMgr('.csh'), - [TerminalShellType.tcshell]: sourceMgr('.csh'), [TerminalShellType.powershell]: sourceMgr('.ps1', '&'), [TerminalShellType.powershellCore]: sourceMgr('.ps1', '&'), - [TerminalShellType.fish]: sourceMgr('.fish'), [TerminalShellType.commandPrompt]: cmdMgr('.bat'), + // Shells supported by the `virtualenv` package + [TerminalShellType.cshell]: sourceMgr('.csh'), + [TerminalShellType.tcshell]: sourceMgr('.csh'), + [TerminalShellType.fish]: sourceMgr('.fish'), [TerminalShellType.xonsh]: sourceMgr('.xsh'), [TerminalShellType.nushell]: { activate: { executable: 'overlay', args: ['use', path.join(binDir, 'activate.nu')] }, deactivate: { executable: 'overlay', args: ['hide', 'activate'] }, + supportsStdlib: false, }, + // Fallback [TerminalShellType.unknown]: isWindows() ? cmdMgr() : sourceMgr(), }; const shellActivation: Map = new Map(); const shellDeactivation: Map = new Map(); - for (const [shell, {activate, deactivate}] of unsafeEntries(venvManagers)) { - if (shell === TerminalShellType.fish && activate.args && !await fsapi.pathExists(activate?.args[0])) { - continue; + await Promise.all(unsafeEntries(venvManagers).map(async ([shell, mgr]) => { + if ( + !mgr.supportsStdlib && + mgr.activate.args && + !await fsapi.pathExists(mgr.activate.args[mgr.activate.args.length - 1]) + ) { + return; } - shellActivation.set(shell, [activate]); - shellDeactivation.set(shell, [deactivate]); - } + shellActivation.set(shell, [mgr.activate]); + shellDeactivation.set(shell, [mgr.deactivate]); + })); return { name: name, From 316cd2d9ebe7331f2ceec1543df9878063d5d52f Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Sat, 25 Jan 2025 11:40:31 +0100 Subject: [PATCH 4/4] more explicit --- src/managers/builtin/venvUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/managers/builtin/venvUtils.ts b/src/managers/builtin/venvUtils.ts index e16f0b62..4d94e861 100644 --- a/src/managers/builtin/venvUtils.ts +++ b/src/managers/builtin/venvUtils.ts @@ -130,13 +130,13 @@ async function getPythonInfo(env: NativeEnvInfo): Promise const cmdMgr = (suffix = ''): VenvManager => ({ activate: { executable: path.join(binDir, `activate${suffix}`) }, deactivate: { executable: path.join(binDir, `deactivate${suffix}`) }, - supportsStdlib: true, + supportsStdlib: ['', '.bat'].includes(suffix), }); /** Venv activation/deactivation for a POSIXy shell */ const sourceMgr = (suffix = '', executable = 'source'): VenvManager => ({ activate: { executable, args: [path.join(binDir, `activate${suffix}`)] }, deactivate: { executable: 'deactivate' }, - supportsStdlib: ['', '.ps1'].includes(suffix) + supportsStdlib: ['', '.ps1'].includes(suffix), }); // satisfies `Record` to make sure all shells are covered const venvManagers: Record = {