diff --git a/extension/extension.ts b/extension/extension.ts index f8df845..2238e88 100644 --- a/extension/extension.ts +++ b/extension/extension.ts @@ -25,6 +25,7 @@ import { RpcServer } from './server/RpcServer'; import { Mutex } from 'async-mutex'; import { TelemetryReporter } from './telemetry'; import { PythonEnvironmentManager } from './pyenv'; +import { setupWorkspaceWithPixi } from './utils/pixiSetup'; /** * This class provides an entry point for the Mojo extension, managing the @@ -85,6 +86,15 @@ Activating the Mojo Extension }), ); + this.pushSubscription( + vscode.commands.registerCommand( + 'mojo.init.pixi.project.nightly', + async () => { + await setupWorkspaceWithPixi(logger, this.pyenvManager); + }, + ), + ); + // Initialize the formatter. this.pushSubscription(registerFormatter(this.pyenvManager, this.logger)); diff --git a/extension/pyenv.ts b/extension/pyenv.ts index aa4d792..888f6bb 100644 --- a/extension/pyenv.ts +++ b/extension/pyenv.ts @@ -325,6 +325,13 @@ export class PythonEnvironmentManager extends DisposableContext { ); } + /// Updates the active Python environment path if the provided path differs. + public setPythonEnv(path: string) { + if (path !== this.api?.environments.getActiveEnvironmentPath().path) { + this.api?.environments.updateActiveEnvironmentPath(path); + } + } + /// Attempts to create a SDK from a home path. Returns undefined if creation failed. public async createSDKFromHomePath( kind: SDKKind, diff --git a/extension/utils/pixiSetup.ts b/extension/utils/pixiSetup.ts new file mode 100644 index 0000000..0d56bb3 --- /dev/null +++ b/extension/utils/pixiSetup.ts @@ -0,0 +1,143 @@ +import * as vscode from 'vscode'; + +import { Logger } from './../logging'; +import { quote } from 'shell-quote'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { PythonEnvironmentManager } from '../pyenv'; + +const execAsync = promisify(exec); + +/** + * Sets up a workspace with Pixi package manager for Mojo development. + * + * This function performs the following steps: + * 1. Checks if Pixi is installed, installs it if missing + * 2. Initializes a Pixi project with Mojo-compatible channels if pixi.toml doesn't exist + * 3. Adds the Mojo package if not already present + * 4. Configures the Python environment to use Pixi's Python interpreter + * + * @param logger - Logger instance for debugging and tracking progress + * @param pyenvManager - Optional Python environment manager to configure the interpreter path + * @returns Promise that resolves when setup is complete + * + * @example + * ```typescript + * await setupWorkspaceWithPixi(logger, pyenvManager); + * ``` + */ +export async function setupWorkspaceWithPixi( + logger: Logger, + pyenvManager?: PythonEnvironmentManager, +): Promise { + logger.debug('Init pixi project'); + + if ((await isPixiInstalled()) === false) { + logger.debug('Need to install pixi first'); + await runTaskAndWait( + 'curl -fsSL https://pixi.sh/install.sh | bash', + 'Install Pixi', + ); + } + + if ((await vscode.workspace.findFiles('pixi.toml')).length === 0) { + await runTaskAndWait( + quote([ + 'pixi', + 'init', + '-c', + 'https://conda.modular.com/max-nightly/', + '-c', + 'conda-forge', + ]), + 'Pixi init', + ); + } + + if ((await vscode.workspace.findFiles('.pixi/**/mojo')).length === 0) { + await runTaskAndWait(quote(['pixi', 'add', 'mojo']), 'Adding Mojo'); + } + + const pythonInterpreterPaths = + await vscode.workspace.findFiles('.pixi/**/python'); + if (pythonInterpreterPaths.length === 1) { + pyenvManager?.setPythonEnv(pythonInterpreterPaths[0].fsPath); + } +} + +/** + * Checks if Pixi package manager is installed on the system. + * + * Attempts to execute `pixi --version` to verify installation. + * + * @returns Promise resolving to true if Pixi is installed, false otherwise + * + * @example + * ```typescript + * if (await isPixiInstalled()) { + * console.log('Pixi is available'); + * } + * ``` + */ +async function isPixiInstalled(): Promise { + try { + // Try to run pixie with a version or help flag + await execAsync('pixi --version', { + shell: '/bin/bash', + env: { + ...process.env, + PATH: `${process.env.HOME}/.pixi/bin:${process.env.PATH}`, + }, + }); + return true; + } catch (error) { + return false; + } +} + +/** + * Executes a shell command as a VS Code task and waits for completion. + * + * Creates and runs a workspace-scoped shell task, monitoring it until completion. + * The task is visible in VS Code's task output panel. + * + * @param command - The shell command to execute + * @param name - Display name for the task in VS Code's task list + * @returns Promise resolving to true if the task succeeded (exit code 0), false otherwise + * + * @example + * ```typescript + * const success = await runTaskAndWait('npm install', 'Install Dependencies'); + * if (success) { + * console.log('Installation completed successfully'); + * } + * ``` + */ +async function runTaskAndWait(command: string, name: string): Promise { + const env = { + ...process.env, + PATH: `${process.env.HOME}/.pixi/bin:${process.env.PATH}`, + }; + const task = new vscode.Task( + { type: 'shell' }, + vscode.TaskScope.Workspace, + name, + 'Mojo Extension', + new vscode.ShellExecution(command, { + env: env, + executable: '/bin/bash', // Explicitly use bash + shellArgs: ['-c'], + }), + ); + + const execution = await vscode.tasks.executeTask(task); + + return new Promise((resolve) => { + const disposable = vscode.tasks.onDidEndTaskProcess((e) => { + if (e.execution === execution) { + disposable.dispose(); + resolve(e.exitCode === 0); + } + }); + }); +} diff --git a/package.json b/package.json index cc34602..81fa6ca 100644 --- a/package.json +++ b/package.json @@ -194,6 +194,11 @@ "command": "mojo.lsp.stopRecord", "title": "Stop recording requests and notifications sent to the Mojo language server." }, + { + "category": "Mojo", + "command": "mojo.init.pixi.project.nightly", + "title": "Setup workspace with pixi (nightly)" + }, { "category": "Developer", "command": "mojo.lsp.debug",