Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add locator for pixi environments #22968

Merged
merged 13 commits into from
Jun 20, 2024
Merged
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,12 @@
"scope": "machine-overridable",
"type": "string"
},
"python.pixiToolPath": {
"default": "pixi",
"description": "%python.pixiToolPath.description%",
"scope": "machine-overridable",
"type": "string"
},
"python.tensorBoard.logDirectory": {
"default": "",
"description": "%python.tensorBoard.logDirectory.description%",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"python.locator.description": "[Experimental] Select implementation of environment locators. This is an experimental setting while we test native environment location.",
"python.pipenvPath.description": "Path to the pipenv executable to use for activation.",
"python.poetryPath.description": "Path to the poetry executable.",
"python.pixiToolPath.description": "Path to the pixi executable.",
"python.EnableREPLSmartSend.description": "Toggle Smart Send for the Python REPL. Smart Send enables sending the smallest runnable block of code to the REPL on Shift+Enter and moves the cursor accordingly.",
"python.REPL.sendToNativeREPL.description": "Toggle to send code to Python REPL instead of the terminal on execution. Turning this on will change the behavior for both Smart Send and Run Selection/Line in the Context Menu.",
"python.tensorBoard.logDirectory.description": "Set this setting to your preferred TensorBoard log directory to skip log directory prompt when starting TensorBoard.",
Expand Down
1 change: 1 addition & 0 deletions resources/report_issue_user_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"condaPath": "placeholder",
"pipenvPath": "placeholder",
"poetryPath": "placeholder",
"pixiToolPath": "placeholder",
"devOptions": false,
"globalModuleInstallation": false,
"languageServer": true,
Expand Down
5 changes: 5 additions & 0 deletions src/client/common/configSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ export class PythonSettings implements IPythonSettings {

public poetryPath = '';

public pixiToolPath = '';

public devOptions: string[] = [];

public autoComplete!: IAutoCompleteSettings;
Expand Down Expand Up @@ -260,6 +262,9 @@ export class PythonSettings implements IPythonSettings {
this.pipenvPath = pipenvPath && pipenvPath.length > 0 ? getAbsolutePath(pipenvPath, workspaceRoot) : pipenvPath;
const poetryPath = systemVariables.resolveAny(pythonSettings.get<string>('poetryPath'))!;
this.poetryPath = poetryPath && poetryPath.length > 0 ? getAbsolutePath(poetryPath, workspaceRoot) : poetryPath;
const pixiToolPath = systemVariables.resolveAny(pythonSettings.get<string>('pixiToolPath'))!;
this.pixiToolPath =
pixiToolPath && pixiToolPath.length > 0 ? getAbsolutePath(pixiToolPath, workspaceRoot) : pixiToolPath;

this.interpreter = pythonSettings.get<IInterpreterSettings>('interpreter') ?? {
infoVisibility: 'onPythonRelated',
Expand Down
81 changes: 81 additions & 0 deletions src/client/common/installer/pixiInstaller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-disable class-methods-use-this */
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { inject, injectable } from 'inversify';
import { IInterpreterService } from '../../interpreter/contracts';
import { IServiceContainer } from '../../ioc/types';
import { getEnvPath } from '../../pythonEnvironments/base/info/env';
import { EnvironmentType, ModuleInstallerType } from '../../pythonEnvironments/info';
import { ExecutionInfo, IConfigurationService } from '../types';
import { isResource } from '../utils/misc';
import { ModuleInstaller } from './moduleInstaller';
import { InterpreterUri } from './types';
import { getPixiEnvironmentFromInterpreter } from '../../pythonEnvironments/common/environmentManagers/pixi';

/**
* A Python module installer for a pixi project.
*/
@injectable()
export class PixiInstaller extends ModuleInstaller {
constructor(
@inject(IServiceContainer) serviceContainer: IServiceContainer,
@inject(IConfigurationService) private readonly configurationService: IConfigurationService,
) {
super(serviceContainer);
}

public get name(): string {
return 'Pixi';
}

public get displayName(): string {
return 'pixi';
}

public get type(): ModuleInstallerType {
return ModuleInstallerType.Pixi;
}

public get priority(): number {
return 20;
}

public async isSupported(resource?: InterpreterUri): Promise<boolean> {
if (isResource(resource)) {
const interpreter = await this.serviceContainer
.get<IInterpreterService>(IInterpreterService)
.getActiveInterpreter(resource);
if (!interpreter || interpreter.envType !== EnvironmentType.Pixi) {
return false;
}

const pixiEnv = await getPixiEnvironmentFromInterpreter(interpreter.path);
return pixiEnv !== undefined;
}
return resource.envType === EnvironmentType.Pixi;
}

/**
* Return the commandline args needed to install the module.
*/
protected async getExecutionInfo(moduleName: string, resource?: InterpreterUri): Promise<ExecutionInfo> {
const pythonPath = isResource(resource)
? this.configurationService.getSettings(resource).pythonPath
: getEnvPath(resource.path, resource.envPath).path ?? '';

const pixiEnv = await getPixiEnvironmentFromInterpreter(pythonPath);
const execPath = pixiEnv?.pixi.command;

let args = ['add', moduleName];
const manifestPath = pixiEnv?.manifestPath;
if (manifestPath !== undefined) {
args = args.concat(['--manifest-path', manifestPath]);
}

return {
args,
execPath,
};
}
}
2 changes: 1 addition & 1 deletion src/client/common/installer/productInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export { Product } from '../types';
// Installer implementations can check this to determine a suitable installation channel for a product
// This is temporary and can be removed when https://github.com/microsoft/vscode-jupyter/issues/5034 is unblocked
const UnsupportedChannelsForProduct = new Map<Product, Set<EnvironmentType>>([
[Product.torchProfilerInstallName, new Set([EnvironmentType.Conda])],
[Product.torchProfilerInstallName, new Set([EnvironmentType.Conda, EnvironmentType.Pixi])],
]);

abstract class BaseInstaller implements IBaseInstaller {
Expand Down
2 changes: 2 additions & 0 deletions src/client/common/installer/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import { InstallationChannelManager } from './channelManager';
import { CondaInstaller } from './condaInstaller';
import { PipEnvInstaller } from './pipEnvInstaller';
import { PipInstaller } from './pipInstaller';
import { PixiInstaller } from './pixiInstaller';
import { PoetryInstaller } from './poetryInstaller';
import { DataScienceProductPathService, TestFrameworkProductPathService } from './productPath';
import { ProductService } from './productService';
import { IInstallationChannelManager, IModuleInstaller, IProductPathService, IProductService } from './types';

export function registerTypes(serviceManager: IServiceManager) {
serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PixiInstaller);
serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, CondaInstaller);
serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PipInstaller);
serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PipEnvInstaller);
Expand Down
18 changes: 18 additions & 0 deletions src/client/common/process/pythonEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { isTestExecution } from '../constants';
import { IFileSystem } from '../platform/types';
import * as internalPython from './internal/python';
import { ExecutionResult, IProcessService, IPythonEnvironment, ShellOptions, SpawnOptions } from './types';
import { PixiEnvironmentInfo } from '../../pythonEnvironments/common/environmentManagers/pixi';

const cachedExecutablePath: Map<string, Promise<string | undefined>> = new Map<string, Promise<string | undefined>>();

Expand Down Expand Up @@ -173,6 +174,23 @@ export async function createCondaEnv(
return new PythonEnvironment(interpreterPath, deps);
}

export async function createPixiEnv(
pixiEnv: PixiEnvironmentInfo,
// These are used to generate the deps.
procs: IProcessService,
fs: IFileSystem,
): Promise<PythonEnvironment | undefined> {
const pythonArgv = pixiEnv.pixi.getRunPythonArgs(pixiEnv.manifestPath, pixiEnv.envName);
const deps = createDeps(
async (filename) => fs.pathExists(filename),
pythonArgv,
pythonArgv,
(file, args, opts) => procs.exec(file, args, opts),
(command, opts) => procs.shellExec(command, opts),
);
return new PythonEnvironment(pixiEnv.interpreterPath, deps);
}

export function createMicrosoftStoreEnv(
pythonPath: string,
// These are used to generate the deps.
Expand Down
30 changes: 29 additions & 1 deletion src/client/common/process/pythonExecutionFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { EventName } from '../../telemetry/constants';
import { IFileSystem } from '../platform/types';
import { IConfigurationService, IDisposableRegistry, IInterpreterPathService } from '../types';
import { ProcessService } from './proc';
import { createCondaEnv, createPythonEnv, createMicrosoftStoreEnv } from './pythonEnvironment';
import { createCondaEnv, createPythonEnv, createMicrosoftStoreEnv, createPixiEnv } from './pythonEnvironment';
import { createPythonProcessService } from './pythonProcess';
import {
ExecutionFactoryCreateWithEnvironmentOptions,
Expand All @@ -25,6 +25,7 @@ import {
import { IInterpreterAutoSelectionService } from '../../interpreter/autoSelection/types';
import { sleep } from '../utils/async';
import { traceError } from '../../logging';
import { getPixiEnvironmentFromInterpreter } from '../../pythonEnvironments/common/environmentManagers/pixi';

@injectable()
export class PythonExecutionFactory implements IPythonExecutionFactory {
Expand Down Expand Up @@ -79,6 +80,11 @@ export class PythonExecutionFactory implements IPythonExecutionFactory {
}
const processService: IProcessService = await this.processServiceFactory.create(options.resource);

const pixiExecutionService = await this.createPixiExecutionService(pythonPath, processService);
if (pixiExecutionService) {
return pixiExecutionService;
}

const condaExecutionService = await this.createCondaExecutionService(pythonPath, processService);
if (condaExecutionService) {
return condaExecutionService;
Expand Down Expand Up @@ -116,6 +122,11 @@ export class PythonExecutionFactory implements IPythonExecutionFactory {
processService.on('exec', this.logger.logProcess.bind(this.logger));
this.disposables.push(processService);

const pixiExecutionService = await this.createPixiExecutionService(pythonPath, processService);
if (pixiExecutionService) {
return pixiExecutionService;
}

const condaExecutionService = await this.createCondaExecutionService(pythonPath, processService);
if (condaExecutionService) {
return condaExecutionService;
Expand All @@ -139,6 +150,23 @@ export class PythonExecutionFactory implements IPythonExecutionFactory {
}
return createPythonService(processService, env);
}

public async createPixiExecutionService(
pythonPath: string,
processService: IProcessService,
): Promise<IPythonExecutionService | undefined> {
const pixiEnvironment = await getPixiEnvironmentFromInterpreter(pythonPath);
if (!pixiEnvironment) {
return undefined;
}

const env = await createPixiEnv(pixiEnvironment, processService, this.fileSystem);
if (!env) {
return undefined;
}

return createPythonService(processService, env);
}
}

function createPythonService(procService: IProcessService, env: IPythonEnvironment): IPythonExecutionService {
Expand Down
6 changes: 6 additions & 0 deletions src/client/common/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import { ContextKeyManager } from './application/contextKeyManager';
import { CreatePythonFileCommandHandler } from './application/commands/createPythonFile';
import { RequireJupyterPrompt } from '../jupyter/requireJupyterPrompt';
import { isWindows } from './platform/platformService';
import { PixiActivationCommandProvider } from './terminal/environmentActivationProviders/pixiActivationProvider';

export function registerTypes(serviceManager: IServiceManager): void {
serviceManager.addSingletonInstance<boolean>(IsWindows, isWindows());
Expand Down Expand Up @@ -161,6 +162,11 @@ export function registerTypes(serviceManager: IServiceManager): void {
CondaActivationCommandProvider,
TerminalActivationProviders.conda,
);
serviceManager.addSingleton<ITerminalActivationCommandProvider>(
ITerminalActivationCommandProvider,
PixiActivationCommandProvider,
TerminalActivationProviders.pixi,
);
serviceManager.addSingleton<ITerminalActivationCommandProvider>(
ITerminalActivationCommandProvider,
PipEnvActivationCommandProvider,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/* eslint-disable class-methods-use-this */
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { inject, injectable } from 'inversify';
import { Uri } from 'vscode';
import { IInterpreterService } from '../../../interpreter/contracts';
import { ITerminalActivationCommandProvider, TerminalShellType } from '../types';
import { traceError } from '../../../logging';
import {
getPixiEnvironmentFromInterpreter,
isNonDefaultPixiEnvironmentName,
} from '../../../pythonEnvironments/common/environmentManagers/pixi';
import { exec } from '../../../pythonEnvironments/common/externalDependencies';
import { splitLines } from '../../stringUtils';

@injectable()
export class PixiActivationCommandProvider implements ITerminalActivationCommandProvider {
constructor(@inject(IInterpreterService) private readonly interpreterService: IInterpreterService) {}

// eslint-disable-next-line class-methods-use-this
public isShellSupported(targetShell: TerminalShellType): boolean {
return shellTypeToPixiShell(targetShell) !== undefined;
}

public async getActivationCommands(
resource: Uri | undefined,
targetShell: TerminalShellType,
): Promise<string[] | undefined> {
const interpreter = await this.interpreterService.getActiveInterpreter(resource);
if (!interpreter) {
return undefined;
}

return this.getActivationCommandsForInterpreter(interpreter.path, targetShell);
}

public async getActivationCommandsForInterpreter(
pythonPath: string,
targetShell: TerminalShellType,
): Promise<string[] | undefined> {
const pixiEnv = await getPixiEnvironmentFromInterpreter(pythonPath);
if (!pixiEnv) {
return undefined;
}

const command = ['shell-hook', '--manifest-path', pixiEnv.manifestPath];
if (isNonDefaultPixiEnvironmentName(pixiEnv.envName)) {
command.push('--environment');
command.push(pixiEnv.envName);
}

const pixiTargetShell = shellTypeToPixiShell(targetShell);
if (pixiTargetShell) {
command.push('--shell');
command.push(pixiTargetShell);
}

const shellHookOutput = await exec(pixiEnv.pixi.command, command, {
throwOnStdErr: false,
}).catch(traceError);
if (!shellHookOutput) {
return undefined;
}

return splitLines(shellHookOutput.stdout, {
removeEmptyEntries: true,
trim: true,
});
}
}

/**
* Returns the name of a terminal shell type within Pixi.
*/
function shellTypeToPixiShell(targetShell: TerminalShellType): string | undefined {
switch (targetShell) {
case TerminalShellType.powershell:
case TerminalShellType.powershellCore:
return 'powershell';
case TerminalShellType.commandPrompt:
return 'cmd';

case TerminalShellType.zsh:
return 'zsh';

case TerminalShellType.fish:
return 'fish';

case TerminalShellType.nushell:
return 'nushell';

case TerminalShellType.xonsh:
return 'xonsh';

case TerminalShellType.cshell:
// Explicitly unsupported
return undefined;

case TerminalShellType.gitbash:
case TerminalShellType.bash:
case TerminalShellType.wsl:
case TerminalShellType.tcshell:
case TerminalShellType.other:
default:
return 'bash';
}
}
Loading
Loading