From 3ac27f3a2593d02b6dbc50064abb56a468ffbd9c Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Fri, 14 Mar 2025 09:51:09 -0700 Subject: [PATCH 01/10] feat: package manager permissions when using API --- package.json | 20 ++ package.nls.json | 4 +- src/api.ts | 2 +- src/common/command.api.ts | 5 + src/common/localize.ts | 7 + src/common/utils/frameUtils.ts | 68 ++++-- src/common/utils/pathUtils.ts | 15 +- src/common/window.apis.ts | 18 ++ src/extension.ts | 29 ++- src/features/envCommands.ts | 13 +- .../permissions/packageManagerPermissions.ts | 193 ++++++++++++++++++ src/features/permissions/pickers.ts | 31 +++ src/features/pythonApi.ts | 23 ++- 13 files changed, 391 insertions(+), 37 deletions(-) create mode 100644 src/features/permissions/packageManagerPermissions.ts create mode 100644 src/features/permissions/pickers.ts diff --git a/package.json b/package.json index ce1be620..31f8eaa3 100644 --- a/package.json +++ b/package.json @@ -231,6 +231,18 @@ "title": "%python-envs.copyProjectPath.title%", "category": "Python Envs", "icon": "$(copy)" + }, + { + "command": "python-envs.permissions", + "title": "%python-envs.permissions.title%", + "category": "Python Envs", + "icon": "$(shield)" + }, + { + "command": "python-envs.resetPermissions", + "title": "%python-envs.resetPermissions.title%", + "category": "Python Envs", + "icon": "$(sync)" } ], "menus": { @@ -418,9 +430,17 @@ }, { "command": "python-envs.refreshAllManagers", + "when": "view == env-managers" + }, + { + "command": "python-envs.permissions", "group": "navigation", "when": "view == env-managers" }, + { + "command": "python-envs.resetPermissions", + "when": "view == env-managers" + }, { "command": "python-envs.terminal.activate", "group": "navigation", diff --git a/package.nls.json b/package.nls.json index f7547a6e..aa99a384 100644 --- a/package.nls.json +++ b/package.nls.json @@ -28,5 +28,7 @@ "python-envs.runAsTask.title": "Run as Task", "python-envs.terminal.activate.title": "Activate Environment in Current Terminal", "python-envs.terminal.deactivate.title": "Deactivate Environment in Current Terminal", - "python-envs.uninstallPackage.title": "Uninstall Package" + "python-envs.uninstallPackage.title": "Uninstall Package", + "python-envs.permissions.title": "Package Manager Permissions", + "python-envs.resetPermissions.title": "Reset Package Manager Permissions" } diff --git a/src/api.ts b/src/api.ts index e7d2245d..55768917 100644 --- a/src/api.ts +++ b/src/api.ts @@ -918,7 +918,7 @@ export interface PythonPackageManagementApi { * @param environment The Python Environment from which packages are to be uninstalled. * @param packages The packages to uninstall. */ - uninstallPackages(environment: PythonEnvironment, packages: PackageInfo[] | string[]): Promise; + uninstallPackages(environment: PythonEnvironment, packages: string[]): Promise; } export interface PythonPackageManagerApi diff --git a/src/common/command.api.ts b/src/common/command.api.ts index 3fece44c..f107b78f 100644 --- a/src/common/command.api.ts +++ b/src/common/command.api.ts @@ -1,6 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { commands } from 'vscode'; +import { Disposable } from 'vscode-jsonrpc'; export function executeCommand(command: string, ...rest: any[]): Thenable { return commands.executeCommand(command, ...rest); } + +export function registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable { + return commands.registerCommand(command, callback, thisArg); +} diff --git a/src/common/localize.ts b/src/common/localize.ts index f9864f7c..0cd0a353 100644 --- a/src/common/localize.ts +++ b/src/common/localize.ts @@ -144,3 +144,10 @@ export namespace EnvViewStrings { export const selectedGlobalTooltip = l10n.t('This environment is selected for non-workspace files'); export const selectedWorkspaceTooltip = l10n.t('This environment is selected for workspace files'); } + +export namespace PermissionsCommon { + export const allow = l10n.t('Allow'); + export const deny = l10n.t('Deny'); + export const ask = l10n.t('Ask'); + export const setPermissions = l10n.t('Set Permissions'); +} diff --git a/src/common/utils/frameUtils.ts b/src/common/utils/frameUtils.ts index f0b32d4d..cdf70f5a 100644 --- a/src/common/utils/frameUtils.ts +++ b/src/common/utils/frameUtils.ts @@ -1,7 +1,9 @@ +import * as path from 'path'; +import { Uri } from 'vscode'; import { ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID } from '../constants'; import { parseStack } from '../errors/utils'; import { allExtensions, getExtension } from '../extension.apis'; - +import { normalizePath } from './pathUtils'; interface FrameData { filePath: string; functionName: string; @@ -15,38 +17,64 @@ function getFrameData(): FrameData[] { })); } +function getPathFromFrame(frame: FrameData): string { + if (frame.filePath && frame.filePath.startsWith('file://')) { + return Uri.parse(frame.filePath).fsPath; + } + return frame.filePath; +} + export function getCallingExtension(): string { const pythonExts = [ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID]; - + const execPath = normalizePath(path.dirname(process.execPath)); const extensions = allExtensions(); const otherExts = extensions.filter((ext) => !pythonExts.includes(ext.id)); - const frames = getFrameData().filter((frame) => !!frame.filePath); + const frames = getFrameData(); + const filePaths: string[] = []; for (const frame of frames) { - const filename = frame.filePath; - if (filename) { - const ext = otherExts.find((ext) => filename.includes(ext.id)); - if (ext) { - return ext.id; - } + if (!frame || !frame.filePath) { + continue; + } + const filePath = normalizePath(getPathFromFrame(frame)); + if (!filePath) { + continue; + } + + if (filePath.startsWith(execPath) && filePath.endsWith('extensionhostprocess.js')) { + continue; + } + + if (filePath.startsWith('node:')) { + continue; + } + + filePaths.push(filePath); + + const ext = otherExts.find((ext) => filePath.includes(ext.id)); + if (ext) { + return ext.id; } } // `ms-python.vscode-python-envs` extension in Development mode - const candidates = frames.filter((frame) => otherExts.some((s) => frame.filePath.includes(s.extensionPath))); - const envsExtPath = getExtension(ENVS_EXTENSION_ID)?.extensionPath; - if (!envsExtPath) { + const candidates = filePaths.filter((filePath) => + otherExts.some((s) => filePath.includes(normalizePath(s.extensionPath))), + ); + const envExt = getExtension(ENVS_EXTENSION_ID); + + if (!envExt) { throw new Error('Something went wrong with feature registration'); } - - if (candidates.length === 0 && frames.every((frame) => frame.filePath.startsWith(envsExtPath))) { + const envsExtPath = normalizePath(envExt.extensionPath); + if (candidates.length === 0 && filePaths.every((filePath) => filePath.startsWith(envsExtPath))) { return PYTHON_EXTENSION_ID; - } - - // 3rd party extension in Development mode - const candidateExt = otherExts.find((ext) => candidates[0].filePath.includes(ext.extensionPath)); - if (candidateExt) { - return candidateExt.id; + } else if (candidates.length > 0) { + // 3rd party extension in Development mode + const candidateExt = otherExts.find((ext) => candidates[0].includes(ext.extensionPath)); + if (candidateExt) { + return candidateExt.id; + } } throw new Error('Unable to determine calling extension id, registration failed'); diff --git a/src/common/utils/pathUtils.ts b/src/common/utils/pathUtils.ts index ac9f4cc0..09a3ef29 100644 --- a/src/common/utils/pathUtils.ts +++ b/src/common/utils/pathUtils.ts @@ -1,10 +1,9 @@ -import * as path from 'path'; +import { isWindows } from '../../managers/common/utils'; -export function areSamePaths(a: string, b: string): boolean { - return path.resolve(a) === path.resolve(b); -} - -export function isParentPath(parent: string, child: string): boolean { - const relative = path.relative(path.resolve(parent), path.resolve(child)); - return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); +export function normalizePath(path: string): string { + const path1 = path.replace(/\\/g, '/'); + if (isWindows()) { + return path1.toLowerCase(); + } + return path1; } diff --git a/src/common/window.apis.ts b/src/common/window.apis.ts index bdbfa8a9..003176b2 100644 --- a/src/common/window.apis.ts +++ b/src/common/window.apis.ts @@ -7,6 +7,8 @@ import { InputBox, InputBoxOptions, LogOutputChannel, + MessageItem, + MessageOptions, OpenDialogOptions, OutputChannel, Progress, @@ -288,6 +290,22 @@ export function showWarningMessage(message: string, ...items: string[]): Thenabl return window.showWarningMessage(message, ...items); } +export function showInformationMessage(message: string, ...items: T[]): Thenable; +export function showInformationMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showInformationMessage(message: string, ...items: T[]): Thenable; +export function showInformationMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showInformationMessage(message: string, ...items: any[]): Thenable { + return window.showInformationMessage(message, ...items); +} + export function showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable { return window.showInputBox(options, token); } diff --git a/src/extension.ts b/src/extension.ts index ac5a1b34..231289dc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -23,6 +23,7 @@ import { runInDedicatedTerminalCommand, handlePackageUninstall, copyPathToClipboard, + getUninstallPackages, } from './features/envCommands'; import { registerCondaFeatures } from './managers/conda/main'; import { registerSystemPythonFeatures } from './managers/builtin/main'; @@ -57,6 +58,11 @@ import { registerTools } from './common/lm.apis'; import { GetPackagesTool } from './features/copilotTools'; import { TerminalActivationImpl } from './features/terminal/terminalActivationState'; import { getEnvironmentForTerminal } from './features/terminal/utils'; +import { + checkPackageManagementPermissions, + handlePermissionsCommand, + PackageManagerPermissionsImpl, +} from './features/permissions/packageManagerPermissions'; export async function activate(context: ExtensionContext): Promise { const start = new StopWatch(); @@ -93,7 +99,9 @@ export async function activate(context: ExtensionContext): Promise { + await handlePermissionsCommand(pkgPerm); + }), + commands.registerCommand('python-envs.resetPermissions', async () => { + await pkgPerm.resetPermissions(); + }), commands.registerCommand('python-envs.viewLogs', () => outputChannel.show()), commands.registerCommand('python-envs.refreshManager', async (item) => { await refreshManagerCommand(item); @@ -138,9 +152,20 @@ export async function activate(context: ExtensionContext): Promise { + const result = await checkPackageManagementPermissions(pkgPerm, 'uninstall', getUninstallPackages(context)); + if (!result) { + return; + } + await handlePackageUninstall(context, envManagers); }), commands.registerCommand('python-envs.set', async (item) => { diff --git a/src/features/envCommands.ts b/src/features/envCommands.ts index 6d0c4e4c..fe38d0d2 100644 --- a/src/features/envCommands.ts +++ b/src/features/envCommands.ts @@ -181,8 +181,8 @@ export async function removeEnvironmentCommand(context: unknown, managers: Envir } export async function handlePackagesCommand( - packageManager: InternalPackageManager, environment: PythonEnvironment, + packageManager: InternalPackageManager, ): Promise { const action = await pickPackageOptions(); @@ -194,12 +194,21 @@ export async function handlePackagesCommand( } } catch (ex) { if (ex === QuickInputButtons.Back) { - return handlePackagesCommand(packageManager, environment); + return handlePackagesCommand(environment, packageManager); } throw ex; } } +export function getUninstallPackages(context: unknown): string[] | undefined { + if (context instanceof PackageTreeItem) { + return [(context as PackageTreeItem).pkg.name]; + } else if (context instanceof ProjectPackage) { + return [(context as ProjectPackage).pkg.name]; + } + return undefined; +} + export async function handlePackageUninstall(context: unknown, em: EnvironmentManagers) { if (context instanceof PackageTreeItem || context instanceof ProjectPackage) { const moduleName = context.pkg.name; diff --git a/src/features/permissions/packageManagerPermissions.ts b/src/features/permissions/packageManagerPermissions.ts new file mode 100644 index 00000000..b321a044 --- /dev/null +++ b/src/features/permissions/packageManagerPermissions.ts @@ -0,0 +1,193 @@ +import { l10n, SecretStorage } from 'vscode'; +import { pickExtension } from './pickers'; +import { showInformationMessage, showWarningMessage } from '../../common/window.apis'; +import { PermissionsCommon } from '../../common/localize'; +import { traceLog } from '../../common/logging'; +import { allExtensions } from '../../common/extension.apis'; +import { getCallingExtension } from '../../common/utils/frameUtils'; + +type PermissionType = 'Ask' | 'Allow' | 'Deny'; +function validatePermissionType(value: string): value is PermissionType { + return ['Ask', 'Allow', 'Deny'].includes(value); +} + +export interface PermissionsManager { + getPermissions(extensionId: string): Promise; + setPermissions(extensionId: string, permissions: T | undefined): Promise; + resetPermissions(): Promise; +} + +export interface PackageManagerPermissions extends PermissionsManager {} +export class PackageManagerPermissionsImpl implements PackageManagerPermissions { + constructor(private readonly secretStore: SecretStorage) {} + + async getPermissions(extensionId: string): Promise { + const permission: string | undefined = await this.secretStore.get( + `python-envs.permissions.packageManagement.${extensionId}`, + ); + if (permission) { + if (validatePermissionType(permission)) { + return permission as PermissionType; + } + } + // else if (extensionId === PYTHON_EXTENSION_ID || extensionId === ENVS_EXTENSION_ID) { + // // Default to allow for the Python extension and the Envs extension + // return 'Allow'; + // } + return undefined; + } + + async setPermissions(extensionId: string, permissions: PermissionType): Promise { + await this.secretStore.store(`python-envs.permissions.packageManagement.${extensionId}`, permissions); + } + + async resetPermissions(): Promise { + const ids = allExtensions().map((e) => `python-envs.permissions.packageManagement.${e.id}`); + await Promise.all(ids.map((id) => this.secretStore.delete(id))); + traceLog('All package management permissions have been reset.'); + } +} + +function getPackageListAsString(packages: string[]): string { + const maxStrLength = 100; + let result = ''; + let count = 0; + + for (const pkg of packages) { + if (result.length + pkg.length + (result ? 2 : 0) > maxStrLength) { + break; + } + result += (result ? ', ' : '') + pkg; + count++; + } + + const remaining = packages.length - count; + if (remaining > 0) { + result += l10n.t('... and {0} others', remaining); + } + + return result; +} + +async function configureFirstTimePermissions(extensionId: string, pm: PackageManagerPermissions) { + const response = await showInformationMessage( + l10n.t( + 'The extension {0} wants to install, upgrade, or uninstall packages from your Python environments', + extensionId, + ), + { modal: true }, + { + title: PermissionsCommon.ask, + isCloseAffordance: true, + }, + { title: PermissionsCommon.allow }, + { title: PermissionsCommon.deny }, + ); + if (response?.title === PermissionsCommon.ask) { + await pm.setPermissions(extensionId, 'Ask'); + traceLog('Package management permissions set to "ask" for extension: ', extensionId); + return true; + } else if (response?.title === PermissionsCommon.allow) { + await pm.setPermissions(extensionId, 'Allow'); + traceLog('Package management permissions set to "allow" for extension: ', extensionId); + return true; + } else if (response?.title === PermissionsCommon.deny) { + await pm.setPermissions(extensionId, 'Deny'); + traceLog('Package management permissions set to "deny" for extension: ', extensionId); + return false; + } else { + traceLog('Package management permissions not changed for extension: ', extensionId); + return false; + } +} + +export async function checkPackageManagementPermissions( + pm: PackageManagerPermissions, + mode: 'install' | 'uninstall' | 'changes', + packages?: string[], +): Promise { + const extensionId = getCallingExtension(); + + const currentPermission = await pm.getPermissions(extensionId); + if (currentPermission === 'Allow') { + return true; + } else if (currentPermission === 'Deny') { + traceLog(`Package management permissions denied for extension: ${extensionId}`); + setImmediate(async () => { + const response = await showWarningMessage( + l10n.t( + 'The extension `{0}` is not allowed to {1} packages into your Python environment.', + extensionId, + mode, + ), + PermissionsCommon.setPermissions, + ); + if (response === PermissionsCommon.setPermissions) { + handlePermissionsCommand(pm, extensionId); + } + }); + return false; + } else if (currentPermission === undefined) { + return await configureFirstTimePermissions(extensionId, pm); + } + + // Below handles Permission level is 'Ask' + let message = l10n.t('The extension `{0}` wants to install packages into your Python environment.', extensionId); + if (mode === 'uninstall') { + message = l10n.t('The extension `{0}` wants to uninstall packages from your Python environment.', extensionId); + } else if (mode === 'changes') { + message = l10n.t('The extension `{0}` wants to make changes to your Python environment.', extensionId); + } + + const response = await showInformationMessage( + message, + { + modal: true, + detail: packages ? l10n.t('Packages: {0}', getPackageListAsString(packages)) : undefined, + }, + { title: PermissionsCommon.allow }, + { title: PermissionsCommon.deny, isCloseAffordance: true }, + ); + if (response?.title === PermissionsCommon.allow) { + traceLog(`Package management permissions granted for extension: ${extensionId}`); + return true; + } + traceLog(`Package management permissions denied for extension: ${extensionId}`); + return false; +} + +export async function handlePermissionsCommand(pm: PermissionsManager, extensionId?: string) { + extensionId = extensionId ?? (await pickExtension()); + if (!extensionId) { + return; + } + + const currentPermission = await pm.getPermissions(extensionId); + + const response = await showInformationMessage( + l10n.t( + 'Set permissions for the extension {0} to install, upgrade, or uninstall packages from your Python environments', + extensionId, + ), + { + modal: true, + detail: currentPermission ? l10n.t('Current permission: {0}', currentPermission) : undefined, + }, + PermissionsCommon.ask, + PermissionsCommon.allow, + PermissionsCommon.deny, + ); + + if (response === PermissionsCommon.ask) { + await pm.setPermissions(extensionId, 'Ask'); + traceLog('Package management permissions set to "ask" for extension: ', extensionId); + } else if (response === PermissionsCommon.allow) { + await pm.setPermissions(extensionId, 'Allow'); + traceLog('Package management permissions set to "allow" for extension: ', extensionId); + } else if (response === PermissionsCommon.deny) { + await pm.setPermissions(extensionId, 'Deny'); + traceLog('Package management permissions set to "deny" for extension: ', extensionId); + } else { + traceLog('Package management permissions not changed for extension: ', extensionId); + } +} diff --git a/src/features/permissions/pickers.ts b/src/features/permissions/pickers.ts new file mode 100644 index 00000000..9a202097 --- /dev/null +++ b/src/features/permissions/pickers.ts @@ -0,0 +1,31 @@ +import { Extension, QuickPickItem } from 'vscode'; +import { allExtensions } from '../../common/extension.apis'; +import { showQuickPick } from '../../common/window.apis'; + +function getExtensionName(ext: Extension): string { + try { + return ext.packageJSON.name; + } catch { + return ''; + } +} + +function getExtensionItems(): QuickPickItem[] { + const extensions = allExtensions(); + return extensions.map((ext) => { + return { + description: ext.id, + label: getExtensionName(ext), + }; + }); +} + +export async function pickExtension(): Promise { + const items = getExtensionItems(); + + const result = await showQuickPick(items, { + ignoreFocusOut: true, + }); + + return result?.description; +} diff --git a/src/features/pythonApi.ts b/src/features/pythonApi.ts index 780413ef..c64a7b72 100644 --- a/src/features/pythonApi.ts +++ b/src/features/pythonApi.ts @@ -46,6 +46,7 @@ import { runAsTask } from './execution/runAsTask'; import { runInTerminal } from './terminal/runInTerminal'; import { runInBackground } from './execution/runInBackground'; import { EnvVarManager } from './execution/envVariableManager'; +import { checkPackageManagementPermissions, PackageManagerPermissions } from './permissions/packageManagerPermissions'; class PythonEnvironmentApiImpl implements PythonEnvironmentApi { private readonly _onDidChangeEnvironments = new EventEmitter(); @@ -60,6 +61,7 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { private readonly projectCreators: ProjectCreators, private readonly terminalManager: TerminalManager, private readonly envVarManager: EnvVarManager, + private readonly pkgPerm: PackageManagerPermissions, private readonly disposables: Disposable[] = [], ) { this.disposables.push( @@ -216,14 +218,28 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { } return new Disposable(() => disposables.forEach((d) => d.dispose())); } - installPackages(context: PythonEnvironment, packages: string[], options: PackageInstallOptions): Promise { + async installPackages( + context: PythonEnvironment, + packages: string[], + options: PackageInstallOptions, + ): Promise { + const result = await checkPackageManagementPermissions(this.pkgPerm, 'install', packages); + if (!result) { + return Promise.reject(new Error('Permission denied')); + } + const manager = this.envManagers.getPackageManager(context); if (!manager) { return Promise.reject(new Error('No package manager found')); } return manager.install(context, packages, options); } - uninstallPackages(context: PythonEnvironment, packages: Package[] | string[]): Promise { + async uninstallPackages(context: PythonEnvironment, packages: string[]): Promise { + const result = await checkPackageManagementPermissions(this.pkgPerm, 'uninstall', packages); + if (!result) { + return Promise.reject(new Error('Permission denied')); + } + const manager = this.envManagers.getPackageManager(context); if (!manager) { return Promise.reject(new Error('No package manager found')); @@ -324,9 +340,10 @@ export function setPythonApi( projectCreators: ProjectCreators, terminalManager: TerminalManager, envVarManager: EnvVarManager, + pkgPerm: PackageManagerPermissions, ) { _deferred.resolve( - new PythonEnvironmentApiImpl(envMgr, projectMgr, projectCreators, terminalManager, envVarManager), + new PythonEnvironmentApiImpl(envMgr, projectMgr, projectCreators, terminalManager, envVarManager, pkgPerm), ); } From 1a2a6ddca02703f97de7a053cce71585ce7e062a Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 20 Mar 2025 10:28:38 -0700 Subject: [PATCH 02/10] fix: address comments --- src/common/localize.ts | 4 +- .../permissions/packageManagerPermissions.ts | 39 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/common/localize.ts b/src/common/localize.ts index 0cd0a353..222a7ea7 100644 --- a/src/common/localize.ts +++ b/src/common/localize.ts @@ -148,6 +148,6 @@ export namespace EnvViewStrings { export namespace PermissionsCommon { export const allow = l10n.t('Allow'); export const deny = l10n.t('Deny'); - export const ask = l10n.t('Ask'); - export const setPermissions = l10n.t('Set Permissions'); + export const confirmEachTime = l10n.t('Confirm each time'); + export const updatePermissions = l10n.t('Update permissions'); } diff --git a/src/features/permissions/packageManagerPermissions.ts b/src/features/permissions/packageManagerPermissions.ts index b321a044..813e6208 100644 --- a/src/features/permissions/packageManagerPermissions.ts +++ b/src/features/permissions/packageManagerPermissions.ts @@ -71,19 +71,16 @@ function getPackageListAsString(packages: string[]): string { async function configureFirstTimePermissions(extensionId: string, pm: PackageManagerPermissions) { const response = await showInformationMessage( - l10n.t( - 'The extension {0} wants to install, upgrade, or uninstall packages from your Python environments', - extensionId, - ), + l10n.t('The {0} extension wants to make changes to packages in your Python environments', extensionId), { modal: true }, { - title: PermissionsCommon.ask, + title: PermissionsCommon.confirmEachTime, isCloseAffordance: true, }, { title: PermissionsCommon.allow }, { title: PermissionsCommon.deny }, ); - if (response?.title === PermissionsCommon.ask) { + if (response?.title === PermissionsCommon.confirmEachTime) { await pm.setPermissions(extensionId, 'Ask'); traceLog('Package management permissions set to "ask" for extension: ', extensionId); return true; @@ -114,15 +111,23 @@ export async function checkPackageManagementPermissions( } else if (currentPermission === 'Deny') { traceLog(`Package management permissions denied for extension: ${extensionId}`); setImmediate(async () => { - const response = await showWarningMessage( - l10n.t( - 'The extension `{0}` is not allowed to {1} packages into your Python environment.', - extensionId, - mode, - ), - PermissionsCommon.setPermissions, + let message = l10n.t( + 'The extension `{0}` is not permitted to install packages into your Python environment.', + extensionId, ); - if (response === PermissionsCommon.setPermissions) { + if (mode === 'uninstall') { + message = l10n.t( + 'The extension `{0}` is not permitted to uninstall packages from your Python environment.', + extensionId, + ); + } else if (mode === 'changes') { + message = l10n.t( + 'The extension `{0}` is not permitted to make changes to your Python environment.', + extensionId, + ); + } + const response = await showWarningMessage(message, PermissionsCommon.updatePermissions); + if (response === PermissionsCommon.updatePermissions) { handlePermissionsCommand(pm, extensionId); } }); @@ -166,19 +171,19 @@ export async function handlePermissionsCommand(pm: PermissionsManager Date: Thu, 20 Mar 2025 11:05:34 -0700 Subject: [PATCH 03/10] chore: some refactoring --- .../permissions/packageManagerPermissions.ts | 108 ++++++++++-------- 1 file changed, 63 insertions(+), 45 deletions(-) diff --git a/src/features/permissions/packageManagerPermissions.ts b/src/features/permissions/packageManagerPermissions.ts index 813e6208..520cc99a 100644 --- a/src/features/permissions/packageManagerPermissions.ts +++ b/src/features/permissions/packageManagerPermissions.ts @@ -69,7 +69,10 @@ function getPackageListAsString(packages: string[]): string { return result; } -async function configureFirstTimePermissions(extensionId: string, pm: PackageManagerPermissions) { +async function configureFirstTimePermissions( + extensionId: string, + pm: PackageManagerPermissions, +): Promise { const response = await showInformationMessage( l10n.t('The {0} extension wants to make changes to packages in your Python environments', extensionId), { modal: true }, @@ -83,66 +86,55 @@ async function configureFirstTimePermissions(extensionId: string, pm: PackageMan if (response?.title === PermissionsCommon.confirmEachTime) { await pm.setPermissions(extensionId, 'Ask'); traceLog('Package management permissions set to "ask" for extension: ', extensionId); - return true; + return 'Ask'; } else if (response?.title === PermissionsCommon.allow) { await pm.setPermissions(extensionId, 'Allow'); traceLog('Package management permissions set to "allow" for extension: ', extensionId); - return true; + return 'Allow'; } else if (response?.title === PermissionsCommon.deny) { await pm.setPermissions(extensionId, 'Deny'); traceLog('Package management permissions set to "deny" for extension: ', extensionId); - return false; + return 'Deny'; } else { - traceLog('Package management permissions not changed for extension: ', extensionId); - return false; + traceLog('Package management permissions not set (default: ask) for extension: ', extensionId); + return 'Ask'; } } -export async function checkPackageManagementPermissions( - pm: PackageManagerPermissions, - mode: 'install' | 'uninstall' | 'changes', - packages?: string[], -): Promise { - const extensionId = getCallingExtension(); - - const currentPermission = await pm.getPermissions(extensionId); - if (currentPermission === 'Allow') { - return true; - } else if (currentPermission === 'Deny') { - traceLog(`Package management permissions denied for extension: ${extensionId}`); - setImmediate(async () => { - let message = l10n.t( - 'The extension `{0}` is not permitted to install packages into your Python environment.', - extensionId, - ); - if (mode === 'uninstall') { - message = l10n.t( - 'The extension `{0}` is not permitted to uninstall packages from your Python environment.', - extensionId, - ); - } else if (mode === 'changes') { - message = l10n.t( - 'The extension `{0}` is not permitted to make changes to your Python environment.', - extensionId, - ); - } - const response = await showWarningMessage(message, PermissionsCommon.updatePermissions); - if (response === PermissionsCommon.updatePermissions) { - handlePermissionsCommand(pm, extensionId); - } - }); - return false; - } else if (currentPermission === undefined) { - return await configureFirstTimePermissions(extensionId, pm); +async function notifyPermissionsDenied(pm: PackageManagerPermissions, extensionId: string, mode: string) { + let message = l10n.t( + 'The extension `{0}` is not permitted to install packages into your Python environment.', + extensionId, + ); + if (mode === 'uninstall') { + message = l10n.t( + 'The extension `{0}` is not permitted to uninstall packages from your Python environment.', + extensionId, + ); + } else if (mode === 'changes') { + message = l10n.t( + 'The extension `{0}` is not permitted to make changes to your Python environment.', + extensionId, + ); + } + const response = await showWarningMessage(message, PermissionsCommon.updatePermissions); + if (response === PermissionsCommon.updatePermissions) { + handlePermissionsCommand(pm, extensionId); } +} - // Below handles Permission level is 'Ask' +async function handleAskForPermissions(extensionId: string, mode: string, packages?: string[]) { let message = l10n.t('The extension `{0}` wants to install packages into your Python environment.', extensionId); if (mode === 'uninstall') { message = l10n.t('The extension `{0}` wants to uninstall packages from your Python environment.', extensionId); } else if (mode === 'changes') { message = l10n.t('The extension `{0}` wants to make changes to your Python environment.', extensionId); } + traceLog( + `Asking for package management permissions for extension ${extensionId} to ${mode}: ${ + packages ?? 'no packages listed' + }`, + ); const response = await showInformationMessage( message, @@ -154,13 +146,39 @@ export async function checkPackageManagementPermissions( { title: PermissionsCommon.deny, isCloseAffordance: true }, ); if (response?.title === PermissionsCommon.allow) { - traceLog(`Package management permissions granted for extension: ${extensionId}`); + traceLog(`Package management permissions granted for extension this time: ${extensionId}`); return true; } - traceLog(`Package management permissions denied for extension: ${extensionId}`); + traceLog(`Package management permissions denied for extension this time: ${extensionId}`); return false; } +export async function checkPackageManagementPermissions( + pm: PackageManagerPermissions, + mode: 'install' | 'uninstall' | 'changes', + packages?: string[], +): Promise { + const extensionId = getCallingExtension(); + + let currentPermission = await pm.getPermissions(extensionId); + if (currentPermission === undefined) { + currentPermission = await configureFirstTimePermissions(extensionId, pm); + } + + if (currentPermission === 'Allow') { + return true; + } else if (currentPermission === 'Deny') { + traceLog(`Package management permissions denied for extension: ${extensionId}`); + setImmediate(async () => { + await notifyPermissionsDenied(pm, extensionId, mode); + }); + return false; + } + + const result = await handleAskForPermissions(extensionId, mode, packages); + return result; +} + export async function handlePermissionsCommand(pm: PermissionsManager, extensionId?: string) { extensionId = extensionId ?? (await pickExtension()); if (!extensionId) { From 42122c99b4ee049968295bfcb588bb555fd7f7d2 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Fri, 21 Mar 2025 15:11:12 -0700 Subject: [PATCH 04/10] fix: reduce extension list for permissions check to installed extensions --- src/common/extension.apis.ts | 11 +++++++++++ src/features/permissions/packageManagerPermissions.ts | 8 ++++---- src/features/permissions/pickers.ts | 6 +++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/common/extension.apis.ts b/src/common/extension.apis.ts index c77594be..15af19a3 100644 --- a/src/common/extension.apis.ts +++ b/src/common/extension.apis.ts @@ -8,3 +8,14 @@ export function getExtension(extensionId: string): Extension | undef export function allExtensions(): readonly Extension[] { return extensions.all; } + +export function allExternalExtensions(): readonly Extension[] { + return allExtensions().filter((extension) => { + try { + return extension.packageJSON.publisher !== 'vscode'; + } catch { + // No publisher + return false; + } + }); +} diff --git a/src/features/permissions/packageManagerPermissions.ts b/src/features/permissions/packageManagerPermissions.ts index 520cc99a..735c98eb 100644 --- a/src/features/permissions/packageManagerPermissions.ts +++ b/src/features/permissions/packageManagerPermissions.ts @@ -1,9 +1,9 @@ import { l10n, SecretStorage } from 'vscode'; -import { pickExtension } from './pickers'; +import { pickExtensionForPermissions } from './pickers'; import { showInformationMessage, showWarningMessage } from '../../common/window.apis'; import { PermissionsCommon } from '../../common/localize'; import { traceLog } from '../../common/logging'; -import { allExtensions } from '../../common/extension.apis'; +import { allExternalExtensions } from '../../common/extension.apis'; import { getCallingExtension } from '../../common/utils/frameUtils'; type PermissionType = 'Ask' | 'Allow' | 'Deny'; @@ -42,7 +42,7 @@ export class PackageManagerPermissionsImpl implements PackageManagerPermissions } async resetPermissions(): Promise { - const ids = allExtensions().map((e) => `python-envs.permissions.packageManagement.${e.id}`); + const ids = allExternalExtensions().map((e) => `python-envs.permissions.packageManagement.${e.id}`); await Promise.all(ids.map((id) => this.secretStore.delete(id))); traceLog('All package management permissions have been reset.'); } @@ -180,7 +180,7 @@ export async function checkPackageManagementPermissions( } export async function handlePermissionsCommand(pm: PermissionsManager, extensionId?: string) { - extensionId = extensionId ?? (await pickExtension()); + extensionId = extensionId ?? (await pickExtensionForPermissions()); if (!extensionId) { return; } diff --git a/src/features/permissions/pickers.ts b/src/features/permissions/pickers.ts index 9a202097..b9d73a97 100644 --- a/src/features/permissions/pickers.ts +++ b/src/features/permissions/pickers.ts @@ -1,5 +1,5 @@ import { Extension, QuickPickItem } from 'vscode'; -import { allExtensions } from '../../common/extension.apis'; +import { allExternalExtensions } from '../../common/extension.apis'; import { showQuickPick } from '../../common/window.apis'; function getExtensionName(ext: Extension): string { @@ -11,7 +11,7 @@ function getExtensionName(ext: Extension): string { } function getExtensionItems(): QuickPickItem[] { - const extensions = allExtensions(); + const extensions = allExternalExtensions(); return extensions.map((ext) => { return { description: ext.id, @@ -20,7 +20,7 @@ function getExtensionItems(): QuickPickItem[] { }); } -export async function pickExtension(): Promise { +export async function pickExtensionForPermissions(): Promise { const items = getExtensionItems(); const result = await showQuickPick(items, { From e9f5f9a97da67bfea8c29b5182bbb1def3956e9a Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Fri, 21 Mar 2025 11:55:45 -0700 Subject: [PATCH 05/10] fix: use vsceTarget to rustTarget conversion when pulling `pet` (#225) --- build/azure-pipeline.pre-release.yml | 28 +++++++++++++++++++++++++++- build/azure-pipeline.stable.yml | 28 +++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index b4e4e951..3b6a9180 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -90,6 +90,32 @@ extends: chmod +x $(Build.SourcesDirectory)/python-env-tools/bin displayName: Make Directory for python-env-tool binary + - bash: | + if [ "$(vsceTarget)" == "win32-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "win32-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "linux-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "linux-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "linux-armhf" ]; then + echo "##vso[task.setvariable variable=buildTarget]armv7-unknown-linux-gnueabihf" + elif [ "$(vsceTarget)" == "darwin-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-apple-darwin" + elif [ "$(vsceTarget)" == "darwin-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-apple-darwin" + elif [ "$(vsceTarget)" == "alpine-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "alpine-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "web" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + else + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + fi + displayName: Set buildTarget variable + - task: DownloadPipelineArtifact@2 inputs: buildType: 'specific' @@ -98,7 +124,7 @@ extends: buildVersionToDownload: 'latest' branchName: 'refs/heads/main' targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' - artifactName: 'bin-$(vsceTarget)' + artifactName: 'bin-$(buildTarget)' itemPattern: | pet.exe pet diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 517be175..6d880681 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -80,6 +80,32 @@ extends: chmod +x $(Build.SourcesDirectory)/python-env-tools/bin displayName: Make Directory for python-env-tool binary + - bash: | + if [ "$(vsceTarget)" == "win32-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "win32-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "linux-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "linux-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "linux-armhf" ]; then + echo "##vso[task.setvariable variable=buildTarget]armv7-unknown-linux-gnueabihf" + elif [ "$(vsceTarget)" == "darwin-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-apple-darwin" + elif [ "$(vsceTarget)" == "darwin-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-apple-darwin" + elif [ "$(vsceTarget)" == "alpine-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "alpine-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "web" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + else + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + fi + displayName: Set buildTarget variable + - task: DownloadPipelineArtifact@2 inputs: buildType: 'specific' @@ -88,7 +114,7 @@ extends: buildVersionToDownload: 'latestFromBranch' branchName: 'refs/heads/release/2024.18' targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' - artifactName: 'bin-$(vsceTarget)' + artifactName: 'bin-$(buildTarget)' itemPattern: | pet.exe pet From 1fcb7b929842243ffc3be6e253fd771e6c7cc9b9 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Fri, 21 Mar 2025 11:59:32 -0700 Subject: [PATCH 06/10] fix: UX when installing packages (#222) * ensure skip install is only shown when relevant * ensure delete env message is modal fixes https://github.com/microsoft/vscode-python-environments/issues/220 --- src/api.ts | 5 ++++ src/common/window.apis.ts | 14 +++++++++- src/features/envCommands.ts | 2 +- src/managers/builtin/pipManager.ts | 2 +- src/managers/builtin/pipUtils.ts | 29 +++++++++++--------- src/managers/builtin/venvUtils.ts | 15 ++++++++--- src/managers/conda/condaPackageManager.ts | 2 +- src/managers/conda/condaUtils.ts | 32 +++++++++++++---------- 8 files changed, 66 insertions(+), 35 deletions(-) diff --git a/src/api.ts b/src/api.ts index 55768917..9e4b35cd 100644 --- a/src/api.ts +++ b/src/api.ts @@ -721,6 +721,11 @@ export interface PackageInstallOptions { * Upgrade the packages if it is already installed. */ upgrade?: boolean; + + /** + * Show option to skip package installation + */ + showSkipOption?: boolean; } export interface PythonProcess { diff --git a/src/common/window.apis.ts b/src/common/window.apis.ts index 003176b2..888d2f46 100644 --- a/src/common/window.apis.ts +++ b/src/common/window.apis.ts @@ -286,7 +286,19 @@ export async function showInputBoxWithButtons( } } -export function showWarningMessage(message: string, ...items: string[]): Thenable { +export function showWarningMessage(message: string, ...items: T[]): Thenable; +export function showWarningMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showWarningMessage(message: string, ...items: T[]): Thenable; +export function showWarningMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showWarningMessage(message: string, ...items: any[]): Thenable { return window.showWarningMessage(message, ...items); } diff --git a/src/features/envCommands.ts b/src/features/envCommands.ts index fe38d0d2..950f38b1 100644 --- a/src/features/envCommands.ts +++ b/src/features/envCommands.ts @@ -188,7 +188,7 @@ export async function handlePackagesCommand( try { if (action === Common.install) { - await packageManager.install(environment); + await packageManager.install(environment, undefined, { showSkipOption: false }); } else if (action === Common.uninstall) { await packageManager.uninstall(environment); } diff --git a/src/managers/builtin/pipManager.ts b/src/managers/builtin/pipManager.ts index d4c5a736..b06edd6d 100644 --- a/src/managers/builtin/pipManager.ts +++ b/src/managers/builtin/pipManager.ts @@ -54,7 +54,7 @@ export class PipPackageManager implements PackageManager, Disposable { if (selected.length === 0) { const projects = this.venv.getProjectsByEnvironment(environment); - selected = (await getWorkspacePackagesToInstall(this.api, projects)) ?? []; + selected = (await getWorkspacePackagesToInstall(this.api, options, projects)) ?? []; } if (selected.length === 0) { diff --git a/src/managers/builtin/pipUtils.ts b/src/managers/builtin/pipUtils.ts index 259e5ceb..c3d1175f 100644 --- a/src/managers/builtin/pipUtils.ts +++ b/src/managers/builtin/pipUtils.ts @@ -4,7 +4,7 @@ import * as tomljs from '@iarna/toml'; import { LogOutputChannel, ProgressLocation, QuickInputButtons, Uri } from 'vscode'; import { showQuickPickWithButtons, withProgress } from '../../common/window.apis'; import { PackageManagement, Pickers, VenvManagerStrings } from '../../common/localize'; -import { PythonEnvironmentApi, PythonProject } from '../../api'; +import { PackageInstallOptions, PythonEnvironmentApi, PythonProject } from '../../api'; import { findFiles } from '../../common/workspace.apis'; import { EXTENSION_ROOT_DIR } from '../../common/constants'; import { Installable, selectFromCommonPackagesToInstall, selectFromInstallableToInstall } from '../common/pickers'; @@ -75,6 +75,7 @@ async function getCommonPackages(): Promise { async function selectWorkspaceOrCommon( installable: Installable[], common: Installable[], + showSkipOption: boolean, ): Promise { if (installable.length === 0 && common.length === 0) { return undefined; @@ -95,19 +96,20 @@ async function selectWorkspaceOrCommon( }); } - if (items.length > 0) { + if (showSkipOption && items.length > 0) { items.push({ label: PackageManagement.skipPackageInstallation }); - } else { - return undefined; } - const selected = await showQuickPickWithButtons(items, { - placeHolder: Pickers.Packages.selectOption, - ignoreFocusOut: true, - showBackButton: true, - matchOnDescription: false, - matchOnDetail: false, - }); + const selected = + items.length === 1 + ? items[0] + : await showQuickPickWithButtons(items, { + placeHolder: Pickers.Packages.selectOption, + ignoreFocusOut: true, + showBackButton: true, + matchOnDescription: false, + matchOnDetail: false, + }); if (selected && !Array.isArray(selected)) { try { @@ -122,7 +124,7 @@ async function selectWorkspaceOrCommon( // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (ex: any) { if (ex === QuickInputButtons.Back) { - return selectWorkspaceOrCommon(installable, common); + return selectWorkspaceOrCommon(installable, common, showSkipOption); } } } @@ -131,11 +133,12 @@ async function selectWorkspaceOrCommon( export async function getWorkspacePackagesToInstall( api: PythonEnvironmentApi, + options?: PackageInstallOptions, project?: PythonProject[], ): Promise { const installable = (await getProjectInstallable(api, project)) ?? []; const common = await getCommonPackages(); - return selectWorkspaceOrCommon(installable, common); + return selectWorkspaceOrCommon(installable, common, !!options?.showSkipOption); } export async function getProjectInstallable( diff --git a/src/managers/builtin/venvUtils.ts b/src/managers/builtin/venvUtils.ts index 5e43d54c..e3460746 100644 --- a/src/managers/builtin/venvUtils.ts +++ b/src/managers/builtin/venvUtils.ts @@ -400,7 +400,11 @@ export async function createPythonVenv( os.platform() === 'win32' ? path.join(envPath, 'Scripts', 'python.exe') : path.join(envPath, 'bin', 'python'); const project = api.getPythonProject(venvRoot); - const packages = await getWorkspacePackagesToInstall(api, project ? [project] : undefined); + const packages = await getWorkspacePackagesToInstall( + api, + { showSkipOption: true }, + project ? [project] : undefined, + ); return await withProgress( { @@ -455,10 +459,13 @@ export async function removeVenv(environment: PythonEnvironment, log: LogOutputC const confirm = await showWarningMessage( l10n.t('Are you sure you want to remove {0}?', envPath), - Common.yes, - Common.no, + { + modal: true, + }, + { title: Common.yes }, + { title: Common.no, isCloseAffordance: true }, ); - if (confirm === Common.yes) { + if (confirm?.title === Common.yes) { await withProgress( { location: ProgressLocation.Notification, diff --git a/src/managers/conda/condaPackageManager.ts b/src/managers/conda/condaPackageManager.ts index fe775081..03b6d94f 100644 --- a/src/managers/conda/condaPackageManager.ts +++ b/src/managers/conda/condaPackageManager.ts @@ -56,7 +56,7 @@ export class CondaPackageManager implements PackageManager, Disposable { let selected: string[] = packages ?? []; if (selected.length === 0) { - selected = (await getCommonCondaPackagesToInstall()) ?? []; + selected = (await getCommonCondaPackagesToInstall(options)) ?? []; } if (selected.length === 0) { diff --git a/src/managers/conda/condaUtils.ts b/src/managers/conda/condaUtils.ts index 49d822b8..c240fbf8 100644 --- a/src/managers/conda/condaUtils.ts +++ b/src/managers/conda/condaUtils.ts @@ -761,7 +761,10 @@ async function getCommonPackages(): Promise { } } -async function selectCommonPackagesOrSkip(common: Installable[]): Promise { +async function selectCommonPackagesOrSkip( + common: Installable[], + showSkipOption: boolean, +): Promise { if (common.length === 0) { return undefined; } @@ -774,19 +777,20 @@ async function selectCommonPackagesOrSkip(common: Installable[]): Promise 0) { + if (showSkipOption && items.length > 0) { items.push({ label: PackageManagement.skipPackageInstallation }); - } else { - return undefined; } - const selected = await showQuickPickWithButtons(items, { - placeHolder: Pickers.Packages.selectOption, - ignoreFocusOut: true, - showBackButton: true, - matchOnDescription: false, - matchOnDetail: false, - }); + const selected = + items.length === 1 + ? items[0] + : await showQuickPickWithButtons(items, { + placeHolder: Pickers.Packages.selectOption, + ignoreFocusOut: true, + showBackButton: true, + matchOnDescription: false, + matchOnDetail: false, + }); if (selected && !Array.isArray(selected)) { try { @@ -799,15 +803,15 @@ async function selectCommonPackagesOrSkip(common: Installable[]): Promise { +export async function getCommonCondaPackagesToInstall(options?: PackageInstallOptions): Promise { const common = await getCommonPackages(); - const selected = await selectCommonPackagesOrSkip(common); + const selected = await selectCommonPackagesOrSkip(common, !!options?.showSkipOption); return selected; } From 8f4cd45acf134619e130d9b7f0417d0a38667a0e Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 26 Mar 2025 09:20:49 -0700 Subject: [PATCH 07/10] fix: package refresh issue when installed externally (#253) --- src/managers/builtin/pipManager.ts | 8 +++++++- src/managers/conda/condaPackageManager.ts | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/managers/builtin/pipManager.ts b/src/managers/builtin/pipManager.ts index b06edd6d..986299df 100644 --- a/src/managers/builtin/pipManager.ts +++ b/src/managers/builtin/pipManager.ts @@ -135,7 +135,13 @@ export class PipPackageManager implements PackageManager, Disposable { title: 'Refreshing packages', }, async () => { - this.packages.set(environment.envId.id, await refreshPackages(environment, this.api, this)); + const before = this.packages.get(environment.envId.id) ?? []; + const after = await refreshPackages(environment, this.api, this); + const changes = getChanges(before, after); + this.packages.set(environment.envId.id, after); + if (changes.length > 0) { + this._onDidChangePackages.fire({ environment, manager: this, changes }); + } }, ); } diff --git a/src/managers/conda/condaPackageManager.ts b/src/managers/conda/condaPackageManager.ts index 03b6d94f..7125dfe5 100644 --- a/src/managers/conda/condaPackageManager.ts +++ b/src/managers/conda/condaPackageManager.ts @@ -131,14 +131,20 @@ export class CondaPackageManager implements PackageManager, Disposable { }, ); } - async refresh(context: PythonEnvironment): Promise { + async refresh(environment: PythonEnvironment): Promise { await withProgress( { location: ProgressLocation.Window, title: CondaStrings.condaRefreshingPackages, }, async () => { - this.packages.set(context.envId.id, await refreshPackages(context, this.api, this)); + const before = this.packages.get(environment.envId.id) ?? []; + const after = await refreshPackages(environment, this.api, this); + const changes = getChanges(before, after); + this.packages.set(environment.envId.id, after); + if (changes.length > 0) { + this._onDidChangePackages.fire({ environment, manager: this, changes }); + } }, ); } From d65b796137301e1cd1891c44c34f5d78253a6f59 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 07:44:58 -0700 Subject: [PATCH 08/10] Bump tar-fs from 2.1.1 to 2.1.2 (#255) Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 2.1.1 to 2.1.2.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tar-fs&package-manager=npm_and_yarn&previous-version=2.1.1&new-version=2.1.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python-environments/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 315b2d8c..65755ba0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4810,10 +4810,11 @@ } }, "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "chownr": "^1.1.1", @@ -8930,9 +8931,9 @@ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" }, "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", "dev": true, "optional": true, "requires": { From 62a894ee889c759fd493365ee0614c825d94ec5b Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Fri, 14 Mar 2025 09:51:09 -0700 Subject: [PATCH 09/10] feat: package manager permissions when using API --- src/common/localize.ts | 4 +- .../permissions/packageManagerPermissions.ts | 123 +++++++----------- src/features/permissions/pickers.ts | 6 +- 3 files changed, 55 insertions(+), 78 deletions(-) diff --git a/src/common/localize.ts b/src/common/localize.ts index 222a7ea7..0cd0a353 100644 --- a/src/common/localize.ts +++ b/src/common/localize.ts @@ -148,6 +148,6 @@ export namespace EnvViewStrings { export namespace PermissionsCommon { export const allow = l10n.t('Allow'); export const deny = l10n.t('Deny'); - export const confirmEachTime = l10n.t('Confirm each time'); - export const updatePermissions = l10n.t('Update permissions'); + export const ask = l10n.t('Ask'); + export const setPermissions = l10n.t('Set Permissions'); } diff --git a/src/features/permissions/packageManagerPermissions.ts b/src/features/permissions/packageManagerPermissions.ts index 735c98eb..b321a044 100644 --- a/src/features/permissions/packageManagerPermissions.ts +++ b/src/features/permissions/packageManagerPermissions.ts @@ -1,9 +1,9 @@ import { l10n, SecretStorage } from 'vscode'; -import { pickExtensionForPermissions } from './pickers'; +import { pickExtension } from './pickers'; import { showInformationMessage, showWarningMessage } from '../../common/window.apis'; import { PermissionsCommon } from '../../common/localize'; import { traceLog } from '../../common/logging'; -import { allExternalExtensions } from '../../common/extension.apis'; +import { allExtensions } from '../../common/extension.apis'; import { getCallingExtension } from '../../common/utils/frameUtils'; type PermissionType = 'Ask' | 'Allow' | 'Deny'; @@ -42,7 +42,7 @@ export class PackageManagerPermissionsImpl implements PackageManagerPermissions } async resetPermissions(): Promise { - const ids = allExternalExtensions().map((e) => `python-envs.permissions.packageManagement.${e.id}`); + const ids = allExtensions().map((e) => `python-envs.permissions.packageManagement.${e.id}`); await Promise.all(ids.map((id) => this.secretStore.delete(id))); traceLog('All package management permissions have been reset.'); } @@ -69,72 +69,75 @@ function getPackageListAsString(packages: string[]): string { return result; } -async function configureFirstTimePermissions( - extensionId: string, - pm: PackageManagerPermissions, -): Promise { +async function configureFirstTimePermissions(extensionId: string, pm: PackageManagerPermissions) { const response = await showInformationMessage( - l10n.t('The {0} extension wants to make changes to packages in your Python environments', extensionId), + l10n.t( + 'The extension {0} wants to install, upgrade, or uninstall packages from your Python environments', + extensionId, + ), { modal: true }, { - title: PermissionsCommon.confirmEachTime, + title: PermissionsCommon.ask, isCloseAffordance: true, }, { title: PermissionsCommon.allow }, { title: PermissionsCommon.deny }, ); - if (response?.title === PermissionsCommon.confirmEachTime) { + if (response?.title === PermissionsCommon.ask) { await pm.setPermissions(extensionId, 'Ask'); traceLog('Package management permissions set to "ask" for extension: ', extensionId); - return 'Ask'; + return true; } else if (response?.title === PermissionsCommon.allow) { await pm.setPermissions(extensionId, 'Allow'); traceLog('Package management permissions set to "allow" for extension: ', extensionId); - return 'Allow'; + return true; } else if (response?.title === PermissionsCommon.deny) { await pm.setPermissions(extensionId, 'Deny'); traceLog('Package management permissions set to "deny" for extension: ', extensionId); - return 'Deny'; + return false; } else { - traceLog('Package management permissions not set (default: ask) for extension: ', extensionId); - return 'Ask'; + traceLog('Package management permissions not changed for extension: ', extensionId); + return false; } } -async function notifyPermissionsDenied(pm: PackageManagerPermissions, extensionId: string, mode: string) { - let message = l10n.t( - 'The extension `{0}` is not permitted to install packages into your Python environment.', - extensionId, - ); - if (mode === 'uninstall') { - message = l10n.t( - 'The extension `{0}` is not permitted to uninstall packages from your Python environment.', - extensionId, - ); - } else if (mode === 'changes') { - message = l10n.t( - 'The extension `{0}` is not permitted to make changes to your Python environment.', - extensionId, - ); - } - const response = await showWarningMessage(message, PermissionsCommon.updatePermissions); - if (response === PermissionsCommon.updatePermissions) { - handlePermissionsCommand(pm, extensionId); +export async function checkPackageManagementPermissions( + pm: PackageManagerPermissions, + mode: 'install' | 'uninstall' | 'changes', + packages?: string[], +): Promise { + const extensionId = getCallingExtension(); + + const currentPermission = await pm.getPermissions(extensionId); + if (currentPermission === 'Allow') { + return true; + } else if (currentPermission === 'Deny') { + traceLog(`Package management permissions denied for extension: ${extensionId}`); + setImmediate(async () => { + const response = await showWarningMessage( + l10n.t( + 'The extension `{0}` is not allowed to {1} packages into your Python environment.', + extensionId, + mode, + ), + PermissionsCommon.setPermissions, + ); + if (response === PermissionsCommon.setPermissions) { + handlePermissionsCommand(pm, extensionId); + } + }); + return false; + } else if (currentPermission === undefined) { + return await configureFirstTimePermissions(extensionId, pm); } -} -async function handleAskForPermissions(extensionId: string, mode: string, packages?: string[]) { + // Below handles Permission level is 'Ask' let message = l10n.t('The extension `{0}` wants to install packages into your Python environment.', extensionId); if (mode === 'uninstall') { message = l10n.t('The extension `{0}` wants to uninstall packages from your Python environment.', extensionId); } else if (mode === 'changes') { message = l10n.t('The extension `{0}` wants to make changes to your Python environment.', extensionId); } - traceLog( - `Asking for package management permissions for extension ${extensionId} to ${mode}: ${ - packages ?? 'no packages listed' - }`, - ); const response = await showInformationMessage( message, @@ -146,41 +149,15 @@ async function handleAskForPermissions(extensionId: string, mode: string, packag { title: PermissionsCommon.deny, isCloseAffordance: true }, ); if (response?.title === PermissionsCommon.allow) { - traceLog(`Package management permissions granted for extension this time: ${extensionId}`); + traceLog(`Package management permissions granted for extension: ${extensionId}`); return true; } - traceLog(`Package management permissions denied for extension this time: ${extensionId}`); + traceLog(`Package management permissions denied for extension: ${extensionId}`); return false; } -export async function checkPackageManagementPermissions( - pm: PackageManagerPermissions, - mode: 'install' | 'uninstall' | 'changes', - packages?: string[], -): Promise { - const extensionId = getCallingExtension(); - - let currentPermission = await pm.getPermissions(extensionId); - if (currentPermission === undefined) { - currentPermission = await configureFirstTimePermissions(extensionId, pm); - } - - if (currentPermission === 'Allow') { - return true; - } else if (currentPermission === 'Deny') { - traceLog(`Package management permissions denied for extension: ${extensionId}`); - setImmediate(async () => { - await notifyPermissionsDenied(pm, extensionId, mode); - }); - return false; - } - - const result = await handleAskForPermissions(extensionId, mode, packages); - return result; -} - export async function handlePermissionsCommand(pm: PermissionsManager, extensionId?: string) { - extensionId = extensionId ?? (await pickExtensionForPermissions()); + extensionId = extensionId ?? (await pickExtension()); if (!extensionId) { return; } @@ -189,19 +166,19 @@ export async function handlePermissionsCommand(pm: PermissionsManager): string { @@ -11,7 +11,7 @@ function getExtensionName(ext: Extension): string { } function getExtensionItems(): QuickPickItem[] { - const extensions = allExternalExtensions(); + const extensions = allExtensions(); return extensions.map((ext) => { return { description: ext.id, @@ -20,7 +20,7 @@ function getExtensionItems(): QuickPickItem[] { }); } -export async function pickExtensionForPermissions(): Promise { +export async function pickExtension(): Promise { const items = getExtensionItems(); const result = await showQuickPick(items, { From e79795ff0a3a82444a768c0429718829bb091d63 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 2 Apr 2025 10:02:38 -0700 Subject: [PATCH 10/10] fix to remove application from paths list --- src/common/utils/frameUtils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/common/utils/frameUtils.ts b/src/common/utils/frameUtils.ts index cdf70f5a..057fc5b2 100644 --- a/src/common/utils/frameUtils.ts +++ b/src/common/utils/frameUtils.ts @@ -1,4 +1,3 @@ -import * as path from 'path'; import { Uri } from 'vscode'; import { ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID } from '../constants'; import { parseStack } from '../errors/utils'; @@ -26,7 +25,6 @@ function getPathFromFrame(frame: FrameData): string { export function getCallingExtension(): string { const pythonExts = [ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID]; - const execPath = normalizePath(path.dirname(process.execPath)); const extensions = allExtensions(); const otherExts = extensions.filter((ext) => !pythonExts.includes(ext.id)); const frames = getFrameData(); @@ -41,7 +39,7 @@ export function getCallingExtension(): string { continue; } - if (filePath.startsWith(execPath) && filePath.endsWith('extensionhostprocess.js')) { + if (filePath.toLowerCase().endsWith('extensionhostprocess.js')) { continue; }