diff --git a/src/managers/builtin/venvUtils.ts b/src/managers/builtin/venvUtils.ts index e22b90b3..fe1ad231 100644 --- a/src/managers/builtin/venvUtils.ts +++ b/src/managers/builtin/venvUtils.ts @@ -2,13 +2,7 @@ import * as fsapi from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; import { l10n, LogOutputChannel, ProgressLocation, QuickPickItem, QuickPickItemKind, ThemeIcon, Uri } from 'vscode'; -import { - EnvironmentManager, - PythonCommandRunConfiguration, - PythonEnvironment, - PythonEnvironmentApi, - PythonEnvironmentInfo, -} from '../../api'; +import { EnvironmentManager, PythonEnvironment, PythonEnvironmentApi, PythonEnvironmentInfo } from '../../api'; import { ENVS_EXTENSION_ID } from '../../common/constants'; import { Common, VenvManagerStrings } from '../../common/localize'; import { traceInfo } from '../../common/logging'; @@ -16,7 +10,6 @@ import { getWorkspacePersistentState } from '../../common/persistentState'; import { pickEnvironmentFrom } from '../../common/pickers/environments'; import { EventNames } from '../../common/telemetry/constants'; import { sendTelemetryEvent } from '../../common/telemetry/sender'; -import { isWindows } from '../../common/utils/platformUtils'; import { showErrorMessage, showInputBox, @@ -26,14 +19,13 @@ import { withProgress, } from '../../common/window.apis'; import { getConfiguration } from '../../common/workspace.apis'; -import { ShellConstants } from '../../features/common/shellConstants'; import { isNativeEnvInfo, NativeEnvInfo, NativePythonEnvironmentKind, NativePythonFinder, } from '../common/nativePythonFinder'; -import { pathForGitBash, shortVersion, sortEnvironments } from '../common/utils'; +import { getShellActivationCommands, shortVersion, sortEnvironments } from '../common/utils'; import { isUvInstalled, runPython, runUV } from './helpers'; import { getProjectInstallable, getWorkspacePackagesToInstall, PipPackages } from './pipUtils'; import { resolveSystemPythonEnvironmentPath } from './utils'; @@ -122,79 +114,7 @@ async function getPythonInfo(env: NativeEnvInfo): Promise const binDir = path.dirname(env.executable); - 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' }]); - } - - 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.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.KSH, [{ executable: '.', args: [path.join(binDir, `activate`)] }]); - shellDeactivation.set(ShellConstants.KSH, [{ executable: 'deactivate' }]); - - if (await fsapi.pathExists(path.join(binDir, 'Activate.ps1'))) { - shellActivation.set(ShellConstants.PWSH, [{ executable: '&', args: [path.join(binDir, `Activate.ps1`)] }]); - shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'deactivate' }]); - } else if (await fsapi.pathExists(path.join(binDir, 'activate.ps1'))) { - shellActivation.set(ShellConstants.PWSH, [{ executable: '&', args: [path.join(binDir, `activate.ps1`)] }]); - shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'deactivate' }]); - } - - if (await fsapi.pathExists(path.join(binDir, 'activate.bat'))) { - shellActivation.set(ShellConstants.CMD, [{ executable: path.join(binDir, `activate.bat`) }]); - shellDeactivation.set(ShellConstants.CMD, [{ executable: path.join(binDir, `deactivate.bat`) }]); - } - - if (await fsapi.pathExists(path.join(binDir, 'activate.csh'))) { - shellActivation.set(ShellConstants.CSH, [ - { executable: 'source', args: [path.join(binDir, `activate.csh`)] }, - ]); - shellDeactivation.set(ShellConstants.CSH, [{ executable: 'deactivate' }]); - - shellActivation.set(ShellConstants.FISH, [ - { executable: 'source', args: [path.join(binDir, `activate.csh`)] }, - ]); - shellDeactivation.set(ShellConstants.FISH, [{ executable: 'deactivate' }]); - } - - if (await fsapi.pathExists(path.join(binDir, 'activate.fish'))) { - shellActivation.set(ShellConstants.FISH, [ - { executable: 'source', args: [path.join(binDir, `activate.fish`)] }, - ]); - shellDeactivation.set(ShellConstants.FISH, [{ executable: 'deactivate' }]); - } - - if (await fsapi.pathExists(path.join(binDir, 'activate.xsh'))) { - shellActivation.set(ShellConstants.XONSH, [ - { executable: 'source', args: [path.join(binDir, `activate.xsh`)] }, - ]); - shellDeactivation.set(ShellConstants.XONSH, [{ executable: 'deactivate' }]); - } - - if (await fsapi.pathExists(path.join(binDir, 'activate.nu'))) { - shellActivation.set(ShellConstants.NU, [ - { executable: 'overlay', args: ['use', path.join(binDir, 'activate.nu')] }, - ]); - shellDeactivation.set(ShellConstants.NU, [{ executable: 'overlay', args: ['hide', 'activate'] }]); - } + const { shellActivation, shellDeactivation } = await getShellActivationCommands(binDir); return { name: name, diff --git a/src/managers/common/utils.ts b/src/managers/common/utils.ts index 5b2519aa..7bd6ff6e 100644 --- a/src/managers/common/utils.ts +++ b/src/managers/common/utils.ts @@ -1,5 +1,8 @@ -import { PythonEnvironment } from '../../api'; +import * as fs from 'fs-extra'; +import path from 'path'; +import { PythonCommandRunConfiguration, PythonEnvironment } from '../../api'; import { isWindows } from '../../common/utils/platformUtils'; +import { ShellConstants } from '../../features/common/shellConstants'; import { Installable } from './types'; export function noop() { @@ -112,3 +115,82 @@ export function compareVersions(version1: string, version2: string): number { return 0; } + +export async function getShellActivationCommands(binDir: string): Promise<{ + shellActivation: Map; + shellDeactivation: Map; +}> { + 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' }]); + } + + 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.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.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, [{ executable: '&', args: [path.join(binDir, `Activate.ps1`)] }]); + shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'deactivate' }]); + } else if (await fs.pathExists(path.join(binDir, 'activate.ps1'))) { + shellActivation.set(ShellConstants.PWSH, [{ executable: '&', args: [path.join(binDir, `activate.ps1`)] }]); + shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'deactivate' }]); + } + + if (await fs.pathExists(path.join(binDir, 'activate.bat'))) { + shellActivation.set(ShellConstants.CMD, [{ executable: path.join(binDir, `activate.bat`) }]); + shellDeactivation.set(ShellConstants.CMD, [{ executable: path.join(binDir, `deactivate.bat`) }]); + } + + if (await fs.pathExists(path.join(binDir, 'activate.csh'))) { + shellActivation.set(ShellConstants.CSH, [{ executable: 'source', args: [path.join(binDir, `activate.csh`)] }]); + shellDeactivation.set(ShellConstants.CSH, [{ executable: 'deactivate' }]); + + shellActivation.set(ShellConstants.FISH, [{ executable: 'source', args: [path.join(binDir, `activate.csh`)] }]); + shellDeactivation.set(ShellConstants.FISH, [{ executable: 'deactivate' }]); + } + + if (await fs.pathExists(path.join(binDir, 'activate.fish'))) { + shellActivation.set(ShellConstants.FISH, [ + { executable: 'source', args: [path.join(binDir, `activate.fish`)] }, + ]); + shellDeactivation.set(ShellConstants.FISH, [{ executable: 'deactivate' }]); + } + + if (await fs.pathExists(path.join(binDir, 'activate.xsh'))) { + shellActivation.set(ShellConstants.XONSH, [ + { executable: 'source', args: [path.join(binDir, `activate.xsh`)] }, + ]); + shellDeactivation.set(ShellConstants.XONSH, [{ executable: 'deactivate' }]); + } + + if (await fs.pathExists(path.join(binDir, 'activate.nu'))) { + shellActivation.set(ShellConstants.NU, [ + { executable: 'overlay', args: ['use', path.join(binDir, 'activate.nu')] }, + ]); + shellDeactivation.set(ShellConstants.NU, [{ executable: 'overlay', args: ['hide', 'activate'] }]); + } + return { + shellActivation, + shellDeactivation, + }; +} diff --git a/src/managers/poetry/main.ts b/src/managers/poetry/main.ts index 02d52c06..9e68abc8 100644 --- a/src/managers/poetry/main.ts +++ b/src/managers/poetry/main.ts @@ -4,10 +4,9 @@ import { traceInfo } from '../../common/logging'; import { showErrorMessage } from '../../common/window.apis'; import { getPythonApi } from '../../features/pythonApi'; import { NativePythonFinder } from '../common/nativePythonFinder'; -import { compareVersions } from '../common/utils'; import { PoetryManager } from './poetryManager'; import { PoetryPackageManager } from './poetryPackageManager'; -import { getPoetry, getPoetryVersion, isPoetryShellPluginInstalled } from './poetryUtils'; +import { getPoetry, getPoetryVersion } from './poetryUtils'; export async function registerPoetryFeatures( nativeFinder: NativePythonFinder, @@ -18,25 +17,16 @@ export async function registerPoetryFeatures( try { const poetryPath = await getPoetry(nativeFinder); - let shellSupported = true; if (poetryPath) { const version = await getPoetryVersion(poetryPath); if (!version) { showErrorMessage(l10n.t('Poetry version could not be determined.')); return; } - if (version && compareVersions(version, '2.0.0') >= 0) { - shellSupported = await isPoetryShellPluginInstalled(poetryPath); - if (!shellSupported) { - showErrorMessage( - l10n.t( - 'Poetry 2.0.0+ detected. The `shell` command is not available by default. Please install the shell plugin to enable shell activation. See [here](https://python-poetry.org/docs/managing-environments/#activating-the-environment), shell [plugin](https://github.com/python-poetry/poetry-plugin-shell)', - ), - ); - return; - } - } - + traceInfo( + 'The `shell` command is not available by default in Poetry versions 2.0.0 and above. Therefore all shell activation will be handled by calling `source `. If you face any problems with shell activation, please file an issue at https://github.com/microsoft/vscode-python-environments/issues to help us improve this implementation. Note the current version of Poetry is {0}.', + version, + ); const envManager = new PoetryManager(nativeFinder, api); const pkgManager = new PoetryPackageManager(api, outputChannel, envManager); diff --git a/src/managers/poetry/poetryUtils.ts b/src/managers/poetry/poetryUtils.ts index d1befcaf..38bfc1ed 100644 --- a/src/managers/poetry/poetryUtils.ts +++ b/src/managers/poetry/poetryUtils.ts @@ -2,19 +2,12 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import { Uri } from 'vscode'; import which from 'which'; -import { - EnvironmentManager, - PythonCommandRunConfiguration, - PythonEnvironment, - PythonEnvironmentApi, - PythonEnvironmentInfo, -} from '../../api'; +import { EnvironmentManager, PythonEnvironment, PythonEnvironmentApi, PythonEnvironmentInfo } from '../../api'; import { ENVS_EXTENSION_ID } from '../../common/constants'; import { traceError, traceInfo } from '../../common/logging'; import { getWorkspacePersistentState } from '../../common/persistentState'; import { getUserHomeDir, untildify } from '../../common/utils/pathUtils'; import { isWindows } from '../../common/utils/platformUtils'; -import { ShellConstants } from '../../features/common/shellConstants'; import { isNativeEnvInfo, NativeEnvInfo, @@ -22,7 +15,7 @@ import { NativePythonEnvironmentKind, NativePythonFinder, } from '../common/nativePythonFinder'; -import { shortVersion, sortEnvironments } from '../common/utils'; +import { getShellActivationCommands, shortVersion, sortEnvironments } from '../common/utils'; async function findPoetry(): Promise { try { @@ -229,62 +222,12 @@ export async function getPoetryVersion(poetry: string): Promise { - try { - const { stdout } = await exec(`"${poetry}" self show plugins`); - // Look for a line like: " - poetry-plugin-shell (1.0.1) Poetry plugin to run subshell..." - return /\s+-\s+poetry-plugin-shell\s+\(\d+\.\d+\.\d+\)/.test(stdout); - } catch { - return false; - } -} - -function createShellActivation( - poetry: string, - _prefix: string, -): Map | undefined { - const shellActivation: Map = new Map(); - - shellActivation.set(ShellConstants.BASH, [{ executable: poetry, args: ['shell'] }]); - shellActivation.set(ShellConstants.ZSH, [{ executable: poetry, args: ['shell'] }]); - shellActivation.set(ShellConstants.SH, [{ executable: poetry, args: ['shell'] }]); - shellActivation.set(ShellConstants.GITBASH, [{ executable: poetry, args: ['shell'] }]); - shellActivation.set(ShellConstants.FISH, [{ executable: poetry, args: ['shell'] }]); - shellActivation.set(ShellConstants.PWSH, [{ executable: poetry, args: ['shell'] }]); - if (isWindows()) { - shellActivation.set(ShellConstants.CMD, [{ executable: poetry, args: ['shell'] }]); - } - shellActivation.set(ShellConstants.NU, [{ executable: poetry, args: ['shell'] }]); - shellActivation.set('unknown', [{ executable: poetry, args: ['shell'] }]); - return shellActivation; -} - -function createShellDeactivation(): Map { - const shellDeactivation: Map = new Map(); - - // Poetry doesn't have a standard deactivation command like venv does - // The best approach is to exit the shell or start a new one - shellDeactivation.set('unknown', [{ executable: 'exit' }]); - - shellDeactivation.set(ShellConstants.BASH, [{ executable: 'exit' }]); - shellDeactivation.set(ShellConstants.ZSH, [{ executable: 'exit' }]); - shellDeactivation.set(ShellConstants.SH, [{ executable: 'exit' }]); - shellDeactivation.set(ShellConstants.GITBASH, [{ executable: 'exit' }]); - shellDeactivation.set(ShellConstants.FISH, [{ executable: 'exit' }]); - shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'exit' }]); - shellDeactivation.set(ShellConstants.CMD, [{ executable: 'exit' }]); - shellDeactivation.set(ShellConstants.NU, [{ executable: 'exit' }]); - - return shellDeactivation; -} - -function nativeToPythonEnv( +async function nativeToPythonEnv( info: NativeEnvInfo, api: PythonEnvironmentApi, manager: EnvironmentManager, _poetry: string, -): PythonEnvironment | undefined { +): Promise { if (!(info.prefix && info.executable && info.version)) { traceError(`Incomplete poetry environment info: ${JSON.stringify(info)}`); return undefined; @@ -294,9 +237,6 @@ function nativeToPythonEnv( const name = info.name || info.displayName || path.basename(info.prefix); const displayName = info.displayName || `poetry (${sv})`; - const shellActivation = createShellActivation(_poetry, info.prefix); - const shellDeactivation = createShellDeactivation(); - // Check if this is a global Poetry virtualenv by checking if it's in Poetry's virtualenvs directory // We need to use path.normalize() to ensure consistent path format comparison const normalizedPrefix = path.normalize(info.prefix); @@ -319,6 +259,10 @@ function nativeToPythonEnv( } } + // Get generic python environment info to access shell activation/deactivation commands following Poetry 2.0+ dropping the `shell` command + const binDir = path.dirname(info.executable); + const { shellActivation, shellDeactivation } = await getShellActivationCommands(binDir); + const environment: PythonEnvironmentInfo = { name: name, displayName: displayName, @@ -374,14 +318,16 @@ export async function refreshPoetry( const collection: PythonEnvironment[] = []; - envs.forEach((e) => { - if (poetry) { - const environment = nativeToPythonEnv(e, api, manager, poetry); - if (environment) { - collection.push(environment); + await Promise.all( + envs.map(async (e) => { + if (poetry) { + const environment = await nativeToPythonEnv(e, api, manager, poetry); + if (environment) { + collection.push(environment); + } } - } - }); + }), + ); return sortEnvironments(collection); }