diff --git a/extension/extension.ts b/extension/extension.ts index f8df845..39f9672 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,12 @@ 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 8690826..0601513 100644 --- a/extension/pyenv.ts +++ b/extension/pyenv.ts @@ -59,7 +59,7 @@ export class SDK { readonly visualizersPath: string, /// The path to the LLDB executor. readonly lldbPath: string, - ) {} + ) { } @Memoize() /// Checks if the version of LLDB shipped with this SDK supports Python scripting. @@ -78,8 +78,7 @@ export class SDK { return true; } else { this.logger.info( - `Python scripting support in LLDB not found. The test script returned:\n${ - stdout + `Python scripting support in LLDB not found. The test script returned:\n${stdout }\n${stderr}`, ); } @@ -315,6 +314,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..ff1ded4 --- /dev/null +++ b/extension/utils/pixiSetup.ts @@ -0,0 +1,146 @@ +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); + } + }); + }); +} \ No newline at end of file diff --git a/package.json b/package.json index dbad5c9..5398eb2 100644 --- a/package.json +++ b/package.json @@ -188,6 +188,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",