diff --git a/extension/commands/run.ts b/extension/commands/run.ts index dd1014f..e8979bf 100644 --- a/extension/commands/run.ts +++ b/extension/commands/run.ts @@ -9,11 +9,10 @@ import * as vscode from 'vscode'; import { DisposableContext } from '../utils/disposableContext'; import * as path from 'path'; import * as config from '../utils/config'; -import { MAXSDK } from '../sdk/sdk'; -import { MAXSDKManager } from '../sdk/sdkManager'; import { MojoDebugConfiguration } from '../debug/debug'; import md5 from 'md5'; import { Optional } from '../types'; +import { PythonEnvironmentManager, SDK } from '../pyenv'; type FileArgs = { runArgs: string[]; @@ -24,13 +23,16 @@ type FileArgs = { * This class provides a manager for executing and debugging mojo files. */ class ExecutionManager extends DisposableContext { - readonly sdkManager: MAXSDKManager; + readonly envManager: PythonEnvironmentManager; private context: vscode.ExtensionContext; - constructor(sdkManager: MAXSDKManager, context: vscode.ExtensionContext) { + constructor( + sdkManager: PythonEnvironmentManager, + context: vscode.ExtensionContext, + ) { super(); - this.sdkManager = sdkManager; + this.envManager = sdkManager; this.context = context; this.activateRunCommands(); } @@ -150,7 +152,7 @@ class ExecutionManager extends DisposableContext { } // Find the config for processing this file. - const sdk = await this.sdkManager.findSDK(/*hideRepeatedErrors=*/ false); + const sdk = await this.envManager.getActiveSDK(); if (!sdk) { return; @@ -161,7 +163,7 @@ class ExecutionManager extends DisposableContext { terminal.show(); terminal.sendText( quote([ - sdk.config.mojoDriverPath, + sdk.mojoPath, 'run', ...this.getBuildArgs(doc.fileName), doc.fileName, @@ -212,8 +214,8 @@ class ExecutionManager extends DisposableContext { /** * Get a terminal to use for the given file. */ - getTerminalForFile(doc: vscode.TextDocument, sdk: MAXSDK): vscode.Terminal { - const fullId = `${doc.fileName} · ${sdk.config.modularHomePath}`; + getTerminalForFile(doc: vscode.TextDocument, sdk: SDK): vscode.Terminal { + const fullId = `${doc.fileName} · ${sdk.homePath}`; // We have to keep the full terminal name short so that VS Code renders it nicely, // and we have to keep it unique among other files. const terminalName = `Mojo: ${path.basename(doc.fileName)} · ${md5(fullId).substring(0, 5)}`; @@ -285,8 +287,8 @@ class ExecutionManager extends DisposableContext { * commands. */ export function activateRunCommands( - sdkManager: MAXSDKManager, + envManager: PythonEnvironmentManager, context: vscode.ExtensionContext, ): vscode.Disposable { - return new ExecutionManager(sdkManager, context); + return new ExecutionManager(envManager, context); } diff --git a/extension/debug/debug.ts b/extension/debug/debug.ts index 530de51..ea9c912 100644 --- a/extension/debug/debug.ts +++ b/extension/debug/debug.ts @@ -11,13 +11,13 @@ import { DisposableContext } from '../utils/disposableContext'; import { getAllOpenMojoFiles, WorkspaceAwareFile } from '../utils/files'; import { activatePickProcessToAttachCommand } from './attachQuickPick'; import { initializeInlineLocalVariablesProvider } from './inlineVariables'; -import { MAXSDK } from '../sdk/sdk'; import { MojoExtension } from '../extension'; -import { MAXSDKManager } from '../sdk/sdkManager'; import { quote } from 'shell-quote'; import * as util from 'util'; import { execFile as execFileBase } from 'child_process'; import { Optional } from '../types'; +import { PythonEnvironmentManager, SDK, SDKKind } from '../pyenv'; +import { Logger } from '../logging'; const execFile = util.promisify(execFileBase); /** @@ -76,12 +76,15 @@ const DEBUG_TYPE: string = 'mojo-lldb'; */ async function findSDKForDebugConfiguration( config: MojoDebugConfiguration, - sdkManager: MAXSDKManager, -): Promise> { + envManager: PythonEnvironmentManager, +): Promise> { if (config.modularHomePath !== undefined) { - return sdkManager.createAdHocSDKAndShowError(config.modularHomePath); + return envManager.createSDKFromHomePath( + SDKKind.Custom, + config.modularHomePath, + ); } - return sdkManager.findSDK(/*hideRepeatedErrors=*/ false); + return envManager.getActiveSDK(); } /** * This class defines a factory used to find the lldb-vscode binary to use @@ -90,10 +93,12 @@ async function findSDKForDebugConfiguration( class MojoDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory { - private sdkManager: MAXSDKManager; + private envManager: PythonEnvironmentManager; + private logger: Logger; - constructor(sdkManager: MAXSDKManager) { - this.sdkManager = sdkManager; + constructor(envManager: PythonEnvironmentManager, logger: Logger) { + this.envManager = envManager; + this.logger = logger; } async createDebugAdapterDescriptor( @@ -102,54 +107,47 @@ class MojoDebugAdapterDescriptorFactory ): Promise> { const sdk = await findSDKForDebugConfiguration( session.configuration, - this.sdkManager, + this.envManager, ); // We don't need to show error messages here because // `findSDKConfigForDebugSession` does that. if (!sdk) { - this.sdkManager.logger.error( - "Couldn't find an SDK for the debug session", - ); + this.logger.error("Couldn't find an SDK for the debug session"); return undefined; } - this.sdkManager.logger.info( - `Using the SDK ${sdk.config.version.toString()} for the debug session`, - ); - if (sdk.config.modularHomePath.endsWith('.derived')) { + this.logger.info(`Using the SDK ${sdk.version} for the debug session`); + if (sdk.homePath.endsWith('.derived')) { // Debug adapters from dev sdks tend to be corrupted because dependencies // might need to be rebuilt, so we run a simple verification. try { - await execFile(sdk.config.mojoLLDBVSCodePath, ['--help']); + await execFile(sdk.dapPath, ['--help']); } catch (ex: any) { const { stderr, stdout } = ex; - this.sdkManager.logger.main.outputChannel.appendLine( + this.logger.main.outputChannel.appendLine( '\n\n\n===== LLDB Debug Adapter verification =====', ); - this.sdkManager.logger.error( - 'Unable to execute the LLDB Debug Adapter.', - ex, - ); + this.logger.error('Unable to execute the LLDB Debug Adapter.', ex); if (stdout) { - this.sdkManager.logger.info('stdout: ' + stdout); + this.logger.info('stdout: ' + stdout); } if (stderr) { - this.sdkManager.logger.info('stderr: ' + stderr); + this.logger.info('stderr: ' + stderr); } - this.sdkManager.logger.main.outputChannel.show(); + this.logger.main.outputChannel.show(); - this.sdkManager.showBazelwRunInstallPrompt( - 'The LLDB Debug Adapter seems to be corrupted.', - sdk.config.modularHomePath, - ); + // this.envManager.showBazelwRunInstallPrompt( + // 'The LLDB Debug Adapter seems to be corrupted.', + // sdk.homePath, + // ); } } - return new vscode.DebugAdapterExecutable(sdk.config.mojoLLDBVSCodePath, [ + return new vscode.DebugAdapterExecutable(sdk.dapPath, [ '--repl-mode', 'variable', '--pre-init-command', - `?!plugin load '${sdk.config.mojoLLDBPluginPath}'`, + `?!plugin load '${sdk.lldbPluginPath}'`, ]); } } @@ -178,10 +176,12 @@ class MojoCudaGdbDebugAdapterDescriptorFactory class MojoDebugConfigurationResolver implements vscode.DebugConfigurationProvider { - private sdkManager: MAXSDKManager; + private envManager: PythonEnvironmentManager; + private logger: Logger; - constructor(sdkManager: MAXSDKManager) { - this.sdkManager = sdkManager; + constructor(envManager: PythonEnvironmentManager, logger: Logger) { + this.envManager = envManager; + this.logger = logger; } async resolveDebugConfigurationWithSubstitutedVariables?( @@ -191,7 +191,7 @@ class MojoDebugConfigurationResolver ): Promise { const sdk = await findSDKForDebugConfiguration( debugConfiguration, - this.sdkManager, + this.envManager, ); // We don't need to show error messages here because // `findSDKConfigForDebugSession` does that. @@ -211,7 +211,7 @@ class MojoDebugConfigurationResolver const message = `Mojo Debug error: the file '${ debugConfiguration.mojoFile }' doesn't have the .🔥 or .mojo extension.`; - this.sdkManager.logger.error(message); + this.logger.error(message); vscode.window.showErrorMessage(message); return undefined; } @@ -224,7 +224,7 @@ class MojoDebugConfigurationResolver debugConfiguration.mojoFile, ...(debugConfiguration.args || []), ]; - debugConfiguration.program = sdk.config.mojoDriverPath; + debugConfiguration.program = sdk.mojoPath; } // We give preference to the init commands specified by the user. @@ -277,7 +277,7 @@ class MojoDebugConfigurationResolver // Pull in the additional visualizers within the lldb-visualizers dir. if (await sdk.lldbHasPythonScriptingSupport()) { - const visualizersDir = sdk.config.mojoLLDBVisualizersPath; + const visualizersDir = sdk.visualizersPath; const visualizers = await vscode.workspace.fs.readDirectory( vscode.Uri.file(visualizersDir), ); @@ -291,7 +291,7 @@ class MojoDebugConfigurationResolver `LLDB_VSCODE_RIT_TIMEOUT_IN_MS=${initializationTimeoutSec * 1000}`, // runInTerminal initialization timeout. ]; - env.push(`MODULAR_HOME=${sdk.config.modularHomePath}`); + env.push(`MODULAR_HOME=${sdk.homePath}`); debugConfiguration.env = [...env, ...(debugConfiguration.env || [])]; return debugConfiguration as vscode.DebugConfiguration; @@ -324,10 +324,12 @@ class MojoDebugConfigurationResolver class MojoCudaGdbDebugConfigurationResolver implements vscode.DebugConfigurationProvider { - private sdkManager: MAXSDKManager; + private envManager: PythonEnvironmentManager; + private logger: Logger; - constructor(sdkManager: MAXSDKManager) { - this.sdkManager = sdkManager; + constructor(envManager: PythonEnvironmentManager, logger: Logger) { + this.envManager = envManager; + this.logger = logger; } async resolveDebugConfigurationWithSubstitutedVariables?( @@ -335,7 +337,7 @@ class MojoCudaGdbDebugConfigurationResolver debugConfigIn: MojoCudaGdbDebugConfiguration, _token?: vscode.CancellationToken, ): Promise { - const maybeErrorMessage = await checkNsightInstall(this.sdkManager.logger); + const maybeErrorMessage = await checkNsightInstall(this.logger); if (maybeErrorMessage) { return undefined; } @@ -346,7 +348,7 @@ class MojoCudaGdbDebugConfigurationResolver const sdk = await findSDKForDebugConfiguration( debugConfigIn as vscode.DebugConfiguration, - this.sdkManager, + this.envManager, ); // We don't need to show error messages here because // `findSDKConfigForDebugSession` does that. @@ -355,7 +357,7 @@ class MojoCudaGdbDebugConfigurationResolver } // If we have a mojoFile config, translate it to program plus args. if (debugConfigIn.mojoFile) { - debugConfig.program = sdk.config.mojoDriverPath; + debugConfig.program = sdk.mojoPath; args = [ 'run', '--no-optimization', @@ -376,7 +378,7 @@ class MojoCudaGdbDebugConfigurationResolver // cuda-gdb takes environment as a list of objects like: // [{"name": "HOME", "value": "/home/ubuntu"}] let env = []; - env.push(`MODULAR_HOME=${sdk.config.modularHomePath}`); + env.push(`MODULAR_HOME=${sdk.homePath}`); env = [...env, ...(debugConfigIn.env || [])]; debugConfig.environment = env.map((envStr: string) => { const split = envStr.split('='); @@ -423,17 +425,20 @@ class MojoDebugDynamicConfigurationProvider * mojo debugging. */ export class MojoDebugManager extends DisposableContext { - private sdkManager: MAXSDKManager; + private envManager: PythonEnvironmentManager; - constructor(extension: MojoExtension, sdkManager: MAXSDKManager) { + constructor(extension: MojoExtension, envManager: PythonEnvironmentManager) { super(); - this.sdkManager = sdkManager; + this.envManager = envManager; // Register the lldb-vscode debug adapter. this.pushSubscription( vscode.debug.registerDebugAdapterDescriptorFactory( DEBUG_TYPE, - new MojoDebugAdapterDescriptorFactory(this.sdkManager), + new MojoDebugAdapterDescriptorFactory( + this.envManager, + extension.logger, + ), ), ); @@ -457,7 +462,7 @@ export class MojoDebugManager extends DisposableContext { this.pushSubscription( vscode.debug.registerDebugConfigurationProvider( DEBUG_TYPE, - new MojoDebugConfigurationResolver(sdkManager), + new MojoDebugConfigurationResolver(envManager, extension.logger), ), ); @@ -498,7 +503,7 @@ export class MojoDebugManager extends DisposableContext { this.pushSubscription( vscode.debug.registerDebugConfigurationProvider( 'mojo-cuda-gdb', - new MojoCudaGdbDebugConfigurationResolver(sdkManager), + new MojoCudaGdbDebugConfigurationResolver(envManager, extension.logger), ), ); } diff --git a/extension/extension.ts b/extension/extension.ts index 543690b..5ede02b 100644 --- a/extension/extension.ts +++ b/extension/extension.ts @@ -7,7 +7,6 @@ import * as vscode from 'vscode'; import { Logger } from './logging'; -import { MAXSDKManager } from './sdk/sdkManager'; import { MojoLSPManager } from './lsp/lsp'; import * as configWatcher from './utils/configWatcher'; import { DisposableContext } from './utils/disposableContext'; @@ -18,6 +17,7 @@ import { MojoDecoratorManager } from './decorations'; import { RpcServer } from './server/RpcServer'; import { Mutex } from 'async-mutex'; import { TelemetryReporter } from './telemetry'; +import { PythonEnvironmentManager } from './pyenv'; /** * Returns if the given extension context is a nightly build. @@ -35,6 +35,7 @@ export class MojoExtension extends DisposableContext { public readonly extensionContext: vscode.ExtensionContext; public lspManager?: MojoLSPManager; public readonly isNightly: boolean; + public pyenvManager?: PythonEnvironmentManager; private activateMutex = new Mutex(); private reporter: TelemetryReporter; @@ -77,12 +78,12 @@ Activating the Mojo Extension ============================= `); - const sdkManager = new MAXSDKManager( + this.pyenvManager = new PythonEnvironmentManager( this.logger, - this.isNightly, - this.extensionContext, + this.reporter, ); - this.pushSubscription(sdkManager); + this.pushSubscription(this.pyenvManager); + await this.pyenvManager.init(); this.pushSubscription( await configWatcher.activate({ @@ -98,14 +99,14 @@ Activating the Mojo Extension ); // Initialize the formatter. - this.pushSubscription(registerFormatter(sdkManager)); + this.pushSubscription(registerFormatter(this.pyenvManager, this.logger)); // Initialize the debugger support. - this.pushSubscription(new MojoDebugManager(this, sdkManager)); + this.pushSubscription(new MojoDebugManager(this, this.pyenvManager)); // Initialize the execution commands. this.pushSubscription( - activateRunCommands(sdkManager, this.extensionContext), + activateRunCommands(this.pyenvManager, this.extensionContext), ); // Initialize the decorations. @@ -113,8 +114,9 @@ Activating the Mojo Extension // Initialize the LSPs this.lspManager = new MojoLSPManager( - sdkManager, + this.pyenvManager, this.extensionContext, + this.logger, this.reporter, ); await this.lspManager.activate(); diff --git a/extension/formatter.ts b/extension/formatter.ts index 79be487..56ac6fd 100644 --- a/extension/formatter.ts +++ b/extension/formatter.ts @@ -7,10 +7,14 @@ import { execFile } from 'child_process'; import * as vscode from 'vscode'; -import { MAXSDKManager } from './sdk/sdkManager'; import { get } from './utils/config'; +import { PythonEnvironmentManager } from './pyenv'; +import { Logger } from './logging'; -export function registerFormatter(maxSDKManager: MAXSDKManager) { +export function registerFormatter( + envManager: PythonEnvironmentManager, + logger: Logger, +) { return vscode.languages.registerDocumentFormattingEditProvider('mojo', { async provideDocumentFormattingEdits(document, _options) { const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); @@ -18,27 +22,24 @@ export function registerFormatter(maxSDKManager: MAXSDKManager) { const cwd = workspaceFolder?.uri?.fsPath || backupFolder?.uri.fsPath; const args = get('formatting.args', workspaceFolder, []); - // We use 'hideRepeatedErrors' because this action is often automated. - const sdk = await maxSDKManager.findSDK(/*hideRepeatedErrors=*/ true); + const sdk = await envManager.getActiveSDK(); if (!sdk) { return []; } - const env = sdk.getProcessEnv(); - return new Promise(function (resolve, reject) { const originalDocumentText = document.getText(); const process = execFile( - sdk.config.mojoMBlackPath, + sdk.mblackPath, ['--fast', '--preview', '--quiet', '-t', 'mojo', ...args, '-'], - { cwd, env }, + { cwd, env: sdk.getProcessEnv() }, (error, stdout, stderr) => { // Process any errors/warnings during formatting. These aren't all // necessarily fatal, so this doesn't prevent edits from being // applied. if (error) { - maxSDKManager.logger.error(`Formatting error:\n${stderr}`); + logger.error(`Formatting error:\n${stderr}`); reject(error); return; } diff --git a/extension/lsp/lsp.ts b/extension/lsp/lsp.ts index 61fb560..c5eceb3 100644 --- a/extension/lsp/lsp.ts +++ b/extension/lsp/lsp.ts @@ -9,15 +9,14 @@ import * as vscode from 'vscode'; import * as vscodelc from 'vscode-languageclient/node'; import { TransportKind } from 'vscode-languageclient/node'; -import { MAXSDK } from '../sdk/sdk'; import * as config from '../utils/config'; import { DisposableContext } from '../utils/disposableContext'; import { Subject } from 'rxjs'; import { Logger } from '../logging'; -import { MAXSDKManager } from '../sdk/sdkManager'; import { TelemetryReporter } from '../telemetry'; import { LSPRecorder } from './recorder'; import { Optional } from '../types'; +import { PythonEnvironmentManager, SDK } from '../pyenv'; /** * This type represents the initialization options send by the extension to the @@ -42,8 +41,8 @@ export interface InitializationOptions { * This class manages the LSP clients. */ export class MojoLSPManager extends DisposableContext { - private sdkManager: MAXSDKManager; private extensionContext: vscode.ExtensionContext; + private envManager: PythonEnvironmentManager; public lspClient: Optional; public lspClientChanges = new Subject>(); private logger: Logger; @@ -53,15 +52,16 @@ export class MojoLSPManager extends DisposableContext { private attachDebugger: boolean = false; constructor( - sdkManager: MAXSDKManager, + envManager: PythonEnvironmentManager, extensionContext: vscode.ExtensionContext, + logger: Logger, reporter: TelemetryReporter, ) { super(); - this.sdkManager = sdkManager; + this.envManager = envManager; this.extensionContext = extensionContext; - this.logger = sdkManager.logger; + this.logger = logger; this.reporter = reporter; } @@ -188,8 +188,9 @@ export class MojoLSPManager extends DisposableContext { this.tryStartLanguageClient(doc), ), ); - this.pushRxjsSubscription( - this.sdkManager.onActiveSDKChanged.subscribe(() => { + + this.pushSubscription( + this.envManager.onEnvironmentChange(() => { vscode.commands.executeCommand('mojo.lsp.restart'); }), ); @@ -200,9 +201,10 @@ export class MojoLSPManager extends DisposableContext { return; } - const sdk = await this.sdkManager.findSDK(/*hideRepeatedErrors=*/ true); + const sdk = await this.envManager.getActiveSDK(); if (!sdk) { + await this.envManager.showInstallWarning(); return; } @@ -232,7 +234,7 @@ export class MojoLSPManager extends DisposableContext { * Create a new language server. */ activateLanguageClient( - sdk: MAXSDK, + sdk: SDK, includeDirs: string[], ): vscodelc.LanguageClient { this.logger.lsp.info('Activating language client'); @@ -250,7 +252,7 @@ export class MojoLSPManager extends DisposableContext { const initializationOptions: InitializationOptions = { serverArgs: serverArgs, serverEnv: sdk.getProcessEnv(), - serverPath: sdk.config.mojoLanguageServerPath, + serverPath: sdk.lspPath, }; const module = this.extensionContext.asAbsolutePath( @@ -326,7 +328,7 @@ export class MojoLSPManager extends DisposableContext { this.pushSubscription( languageClient.onNotification('mojo/lspRestart', () => { this.reporter.sendTelemetryEvent('lspRestart', { - mojoSDKVersion: sdk.config.version.toString(), + mojoSDKVersion: sdk.version, mojoSDKKind: sdk.kind, }); }), diff --git a/extension/pyenv.ts b/extension/pyenv.ts new file mode 100644 index 0000000..a436ab7 --- /dev/null +++ b/extension/pyenv.ts @@ -0,0 +1,181 @@ +//===----------------------------------------------------------------------===// +// +// This file is Modular Inc proprietary. +// +//===----------------------------------------------------------------------===// + +import * as vscode from 'vscode'; +import * as ini from 'ini'; +import { DisposableContext } from './utils/disposableContext'; +import { PythonExtension } from '@vscode/python-extension'; +import assert from 'assert'; +import { Logger } from './logging'; +import path from 'path'; +import * as util from 'util'; +import { execFile as callbackExecFile } from 'child_process'; +import { Memoize } from 'typescript-memoize'; +import { TelemetryReporter } from './telemetry'; +const execFile = util.promisify(callbackExecFile); + +export enum SDKKind { + Environment = 'environment', + Custom = 'custom', +} + +/// Represents a usable instance of the MAX SDK. +export class SDK { + constructor( + private logger: Logger, + /// What kind of SDK this is. Primarily used for logging and context hinting. + readonly kind: SDKKind, + /// The unparsed version string of the SDK. + readonly version: string, + /// The home path of the SDK. This is always a directory containing a modular.cfg file. + readonly homePath: string, + /// The path to the language server executable. + readonly lspPath: string, + /// The path to the mblack executable. + readonly mblackPath: string, + /// The path to the Mojo LLDB plugin. + readonly lldbPluginPath: string, + /// The path to the DAP server executable. + readonly dapPath: string, + /// The path to the Mojo executable. + readonly mojoPath: string, + /// The path to the directory containing LLDB debug visualizers. + 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. + public async lldbHasPythonScriptingSupport(): Promise { + try { + let { stdout, stderr } = await execFile(this.lldbPath, [ + '-b', + '-o', + 'script print(100+1)', + ]); + stdout = (stdout || '') as string; + stderr = (stderr || '') as string; + + if (stdout.indexOf('101') != -1) { + this.logger.info('Python scripting support in LLDB found.'); + return true; + } else { + this.logger.info( + `Python scripting support in LLDB not found. The test script returned:\n${ + stdout + }\n${stderr}`, + ); + } + } catch (e) { + this.logger.error( + 'Python scripting support in LLDB not found. The test script failed with', + e, + ); + } + return false; + } + + /// Gets an appropriate environment to spawn subprocesses from this SDK. + public getProcessEnv(withTelemetry: boolean = true) { + return { + MODULAR_HOME: this.homePath, + MODULAR_TELEMETRY_ENABLED: withTelemetry ? 'true' : 'false', + }; + } +} + +export class PythonEnvironmentManager extends DisposableContext { + private api: PythonExtension | undefined = undefined; + private logger: Logger; + private reporter: TelemetryReporter; + public onEnvironmentChange: vscode.Event; + private envChangeEmitter: vscode.EventEmitter; + + constructor(logger: Logger, reporter: TelemetryReporter) { + super(); + this.logger = logger; + this.reporter = reporter; + this.envChangeEmitter = new vscode.EventEmitter(); + this.onEnvironmentChange = this.envChangeEmitter.event; + } + + public async init() { + this.api = await PythonExtension.api(); + this.pushSubscription( + this.api.environments.onDidChangeActiveEnvironmentPath((_) => + this.envChangeEmitter.fire(), + ), + ); + } + + /// Inform the user that they need to install the MAX SDK. + public async showInstallWarning() { + await vscode.window.showErrorMessage( + 'The MAX SDK is not installed in your current Python environment. Please install the MAX SDK or select a Python environment with MAX installed.', + ); + } + + /// Retrieves the active SDK from the currently active Python virtual environment, or undefined if one is not present. + public async getActiveSDK(): Promise { + assert(this.api !== undefined); + const envPath = this.api.environments.getActiveEnvironmentPath(); + const env = await this.api.environments.resolveEnvironment(envPath); + this.logger.info('Loading MAX SDK information from Python venv'); + + if (!env) { + return undefined; + } + + if (!env.environment) { + return undefined; + } + + this.logger.info(`Found Python environment at ${envPath.path}`); + + const homePath = path.join(env.executable.sysPrefix, 'share', 'max'); + return this.createSDKFromHomePath(SDKKind.Environment, homePath); + } + + /// Attempts to create a SDK from a home path. Returns undefined if creation failed. + public async createSDKFromHomePath( + kind: SDKKind, + homePath: string, + ): Promise { + const modularCfgPath = path.join(homePath, 'modular.cfg'); + try { + const decoder = new TextDecoder(); + const bytes = await vscode.workspace.fs.readFile( + vscode.Uri.file(modularCfgPath), + ); + const contents = decoder.decode(bytes); + const config = ini.parse(contents); + this.logger.info(`Found SDK with version ${config.max.version}`); + + this.reporter.sendTelemetryEvent('sdkLoaded', { + version: config.max.version, + kind, + }); + + return new SDK( + this.logger, + kind, + config.max.version, + homePath, + config['mojo-max']['lsp_server_path'], + config['mojo-max']['mblack_path'], + config['mojo-max']['lldb_plugin_path'], + config['mojo-max']['lldb_vscode_path'], + config['mojo-max']['driver_path'], + config['mojo-max']['lldb_visualizers_path'], + config['mojo-max']['lldb_path'], + ); + } catch (e) { + this.logger.error('Error loading SDK', e); + return undefined; + } + } +} diff --git a/extension/sdk/magicSdk.ts b/extension/sdk/magicSdk.ts deleted file mode 100644 index aec0dfd..0000000 --- a/extension/sdk/magicSdk.ts +++ /dev/null @@ -1,474 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This file is Modular Inc proprietary. -// -//===----------------------------------------------------------------------===// - -import * as vscode from 'vscode'; -import { MAXSDKSpec } from './types'; -import * as path from 'path'; -import { mkdirp, chmod, rm, createWriteStream } from 'fs-extra'; -import * as util from 'util'; -import { directoryExists, readFile } from '../utils/files'; -import { lock } from 'proper-lockfile'; -import axios from 'axios'; -import { Logger } from '../logging'; -import { execFile } from 'child_process'; -const execFileSync = util.promisify(execFile); -import { MAXSDKVersion as MaxSDKVersion } from './sdkVersion'; -import { Optional } from '../types'; - -const SDK_INSTALLATION_CANCELLATION_MSG = 'SDK installation cancelled'; -type MagicInstallationResult = 'succeeded' | 'failed' | 'cancelled'; - -async function downloadFile( - url: string, - outputPath: string, - timeoutMins: number, - errorMessage: string, - logger: Logger, -): Promise { - const writer = createWriteStream(outputPath); - - const response = await axios({ - url, - method: 'GET', - responseType: 'stream', - timeout: timeoutMins * 60 * 1000, - }); - - response.data.pipe(writer); - - try { - await new Promise((resolve, reject) => { - writer.on('finish', resolve); - writer.on('error', reject); - }); - return true; - } catch (ex: any) { - vscode.window.showErrorMessage(errorMessage, ex.message); - logger.error(errorMessage, ex); - return false; - } -} - -function getMagicUrl(): Optional { - let platform: string; - if (process.platform === 'linux') { - platform = 'unknown-linux-musl'; - } else if (process.platform === 'darwin') { - platform = 'apple-darwin'; - } else if (process.platform === 'win32') { - platform = 'pc-windows-msvc'; - } else { - vscode.window.showErrorMessage( - `The MAX SDK is not supported in this platform: ${process.platform}`, - ); - return undefined; - } - let arch: string; - if (process.arch === 'x64') { - arch = 'x86_64'; - } else if (process.arch === 'arm64') { - arch = 'aarch64'; - } else { - arch = process.arch; - } - return `https://dl.modular.com/public/magic/raw/versions/latest/magic-${arch}-${platform}`; -} - -type DownloadSpec = { - privateDir: string; - magicDataHome: string; - magicPath: string; - doneDirectory: string; - versionDoneDirParent: string; - versionDoneDir: string; - magicUrl: string; - version: string; - major: string; - minor: string; - patch: string; -}; - -/** - * @returns 1 if version1 > version2, -1 if version1 < version2, 0 otherwise. - */ -function compareNightlyMAXVersions(version1: string, version2: string): number { - const [M1_, m1_, p1_, d1] = version1.split('.'); - const [M2_, m2_, p2_, d2] = version2.split('.'); - const M1 = parseInt(M1_); - const m1 = parseInt(m1_); - const p1 = parseInt(p1_); - const M2 = parseInt(M2_); - const m2 = parseInt(m2_); - const p2 = parseInt(p2_); - const compare = (a: any, b: any) => { - return a === b ? 0 : a < b ? -1 : 1; - }; - for (const [a, b] of [ - [M1, M2], - [m1, m2], - [p1, p2], - [d1, d2], - ]) { - const comp = compare(a, b); - if (comp !== 0) { - return comp; - } - } - return 0; -} - -/** - * @returns undefined or a non-empty list of versions sorted ascending semantically. - */ -async function getAllNightlyMAXVersions( - logger: Logger, - privateDir: string, -): Promise> { - const repodataDir = path.join(privateDir, 'repodata'); - const now = new Date(); - const repodataFile = path.join( - privateDir, - 'repodata', - `${now.getFullYear()}-${now.getMonth()}-${now.getDay()}`, - ); - let contents = await readFile(repodataFile); - // If the repodata for today is not present, then we download it and we delete any previous repodata files. - if (!contents) { - await rm(repodataDir, { recursive: true, force: true }); - await mkdirp(repodataDir); - - const repodataUrl = - 'https://conda.modular.com/max-nightly/noarch/repodata.json'; - logger.info(`Will download ${repodataUrl} into ${repodataFile}`); - if ( - !(await downloadFile( - repodataUrl, - repodataFile, - /*timeoutMins=*/ 1, - "Couldn't download " + repodataUrl, - logger, - )) - ) { - return undefined; - } - logger.info('Successfully downloaded'); - } - contents = await readFile(repodataFile); - if (!contents) { - return undefined; - } - const jsonContents = JSON.parse(contents); - const packages = jsonContents['packages.conda']; - const versions: string[] = []; - for (const packageName in packages) { - if (packageName.startsWith('max-')) { - versions.push(packages[packageName].version); - } - } - versions.sort(compareNightlyMAXVersions); - return versions.length === 0 ? undefined : versions; -} - -async function getLatestNightlyMAXVersion( - logger: Logger, - privateDir: string, -): Promise> { - const versions = await getAllNightlyMAXVersions(logger, privateDir); - if (versions === undefined) { - return undefined; - } - return versions[versions.length - 1]; -} - -async function findVersionToDownload( - context: vscode.ExtensionContext, - extVersion: string, - isNightly: boolean, - logger: Logger, - privateDir: string, -): Promise> { - const nightlyMaxVersionToComponents = ( - nightlyVersion: Optional, - ): Optional<[string, string, string]> => { - if (nightlyVersion === undefined) { - return undefined; - } - const [major, minor, patch, dev] = nightlyVersion.split('.'); - return [major, minor, patch + `.${dev}`]; - }; - - if (extVersion === '0.0.0') { - // If this is a dev version of the extension, we can figure out dynamically - // what's the latest version of the sdk. - return nightlyMaxVersionToComponents( - await getLatestNightlyMAXVersion(logger, privateDir), - ); - } - if (isNightly) { - return nightlyMaxVersionToComponents( - context.extension.packageJSON.sdkVersion, - ); - } - // stable - const [major, minor, patch] = - context.extension.packageJSON.sdkVersion.split('.'); - return [major, minor, patch]; -} - -async function createDownloadSpec( - context: vscode.ExtensionContext, - isNightly: boolean, - logger: Logger, -): Promise> { - const privateDir = context.globalStorageUri.fsPath; - const magicDataHome = path.join(privateDir, 'magic-data-home'); - const magicPath = path.join(privateDir, 'magic'); - const doneDirectory = path.join(privateDir, 'done'); - const magicUrl = getMagicUrl(); - if (!magicUrl) { - return undefined; - } - isNightly = true; - const extVersion = context.extension.packageJSON.version as string; - const versionToDownload = await findVersionToDownload( - context, - extVersion, - isNightly, - logger, - privateDir, - ); - if (!versionToDownload) { - return undefined; - } - const [major, minor, patch] = versionToDownload; - const version = `${major}.${minor}.${patch}`; - const versionDoneDirParent = path.join(privateDir, 'versionDone'); - const versionDoneDir = path.join(versionDoneDirParent, version); - return { - privateDir, - magicDataHome, - magicPath, - doneDirectory, - versionDoneDir, - versionDoneDirParent, - magicUrl, - version, - major, - minor, - patch, - }; -} - -async function doInstallMagicAndMAXSDK( - downloadSpec: DownloadSpec, - logger: Logger, - isNightly: boolean, - token: vscode.CancellationToken, -): Promise { - isNightly = true; - await rm(downloadSpec.doneDirectory, { recursive: true, force: true }); - - logger.info( - `Will download ${downloadSpec.magicUrl} into ${downloadSpec.magicPath}`, - ); - if (token.isCancellationRequested) { - throw new Error(SDK_INSTALLATION_CANCELLATION_MSG); - } - - await downloadFile( - downloadSpec.magicUrl, - downloadSpec.magicPath, - /*timeoutMins=*/ 5, - "Couldn't download magic", - logger, - ); - logger.info('Successfully downloaded magic.'); - await chmod(downloadSpec.magicPath, 0o755); - logger.info( - `The permissions for ${downloadSpec.magicPath} have been changed and it's now executable.`, - ); - - logger.info(`Will prepare the MAX SDK installation.`); - const env = { ...process.env }; - env['MAGIC_DATA_HOME'] = downloadSpec.magicDataHome; - // We remove data home before installing again in case another process is - // trying to write to it for some weird reason. - logger.info(`Removing magic-data-home`); - await rm(downloadSpec.magicDataHome, { - recursive: true, - force: true, - }); - - const downloadOverride = process.env['MOJO_VSCODE_MAGIC_SOURCE']; - const downloadSource = - downloadOverride ?? - 'https://conda.modular.com/max' + (isNightly ? '-nightly' : ''); - - logger.debug(`Downloading MAX from ${downloadOverride}.`); - - const args = [ - 'global', - 'install', - '-c', - downloadSource, - '-c', - 'conda-forge', - `max==${downloadSpec.version}`, - 'python>=3.11,<3.12', - ]; - logger.info(`Installing the MAX SDK.`); - - if (token.isCancellationRequested) { - logger.info('SDK installation was cancelled.'); - throw new Error(SDK_INSTALLATION_CANCELLATION_MSG); - } - - const controller = new AbortController(); - const { signal } = controller; - const child = execFileSync(downloadSpec.magicPath, args, { env, signal }); - token.onCancellationRequested(() => { - controller.abort(); - }); - await child; - - logger.info(`Successfully installed MAX.`); - - await mkdirp(downloadSpec.doneDirectory); - await mkdirp(downloadSpec.versionDoneDir); -} - -/** - * @returns a string with an error message if the download didn't succeed. - */ -async function installMagicAndMAXSDKWithProgress( - downloadSpec: DownloadSpec, - logger: Logger, - isNightly: boolean, - reinstall: boolean, -): Promise> { - isNightly = true; - if (!reinstall && (await directoryExists(downloadSpec.versionDoneDir))) { - logger.info('Magic SDK present. Skipping installation.'); - return undefined; - } - await rm(downloadSpec.versionDoneDirParent, { - recursive: true, - force: true, - }); - return await vscode.window.withProgress( - { - title: 'Installing the MAX SDK for VS Code', - location: vscode.ProgressLocation.Notification, - cancellable: true, - }, - async (_progress, token: vscode.CancellationToken) => { - try { - await doInstallMagicAndMAXSDK(downloadSpec, logger, isNightly, token); - return undefined; - } catch (e: any) { - logger.error("Couldn't install the MAX SDK for VS Code", e); - return e.message; - } - }, - ); -} - -async function acquireLockIfNeeded( - logger: Logger, - useLock: boolean, - downloadSpec: DownloadSpec, -): Promise<() => Promise> { - if (!useLock) { - return async () => {}; - } - logger.info('Trying to acquire installation lock...'); - const releaseLock = await lock(downloadSpec.privateDir, { retries: 10 }); - logger.info('Lock acquired...'); - return releaseLock; -} - -export async function installMagicSDK( - withLock: boolean, - context: vscode.ExtensionContext, - logger: Logger, - isNightly: boolean, - reinstall: boolean = false, -): Promise { - isNightly = true; - const downloadSpec = await createDownloadSpec(context, isNightly, logger); - if (downloadSpec === undefined) { - return 'failed'; - } - await mkdirp(downloadSpec.magicDataHome); - - let success = false; - let errorMessage: string | undefined = ''; - try { - logger.info('Trying to acquire installation lock...'); - const releaseLock = await acquireLockIfNeeded( - logger, - withLock, - downloadSpec, - ); - errorMessage = await installMagicAndMAXSDKWithProgress( - downloadSpec, - logger, - isNightly, - reinstall, - ); - if (errorMessage === undefined) { - success = true; - } - await releaseLock(); - } catch (e: any) { - logger.error( - 'Error while handling the lock for the MAX SDK for VS Code', - e, - ); - } - if (!success) { - const displayErrorMessage = errorMessage ? `\n${errorMessage}.` : ''; - vscode.window.showErrorMessage( - `Couldn't install the MAX SDK for VS Code.${displayErrorMessage}`, - ); - if (errorMessage === SDK_INSTALLATION_CANCELLATION_MSG) { - return 'cancelled'; - } - return 'failed'; - } - return 'succeeded'; -} - -export async function findMagicSDKSpec( - context: vscode.ExtensionContext, - logger: Logger, - isNightly: boolean, -): Promise> { - isNightly = true; - const downloadSpec = await createDownloadSpec(context, isNightly, logger); - if (downloadSpec === undefined) { - return undefined; - } - const modularHomePath = path.join( - downloadSpec.magicDataHome, - 'envs', - 'max', - 'share', - 'max', - ); - - return { - kind: 'magic', - modularHomePath, - version: new MaxSDKVersion( - 'MAX SDK ' + (isNightly ? '(nightly) ' : '(stable)'), - downloadSpec.major, - downloadSpec.minor, - downloadSpec.patch, - modularHomePath, - ), - }; -} diff --git a/extension/sdk/sdk.ts b/extension/sdk/sdk.ts deleted file mode 100644 index 90ba955..0000000 --- a/extension/sdk/sdk.ts +++ /dev/null @@ -1,85 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This file is Modular Inc proprietary. -// -//===----------------------------------------------------------------------===// - -import { Logger } from '../logging'; -import { MAXSDKConfig } from './sdkConfig'; -import { Memoize } from 'typescript-memoize'; -import * as util from 'util'; -import { MAXSDKKind as MAXSDKKind } from './types'; -import { execFile as execFileBase } from 'child_process'; -const execFile = util.promisify(execFileBase); - -/** - * Class that represents an SDK in the system. - */ -export class MAXSDK { - public readonly config: MAXSDKConfig; - public readonly kind: MAXSDKKind; - private logger: Logger; - - constructor(config: MAXSDKConfig, kind: MAXSDKKind, logger: Logger) { - this.config = config; - this.kind = kind; - this.logger = logger; - } - - /** - * Determine whether python scripting is functional in LLDB. As there - * are many reasons why python scripting would fail (e.g. disabled in the build system, - * wrong SDK installation, etc.), it's more effective to just execute a - * minimal script to confirm it's operative. - * - * @returns true if and only if the LLDB binary in this SDK has a working - * python scripting feature. - */ - @Memoize() - public async lldbHasPythonScriptingSupport(): Promise { - try { - let { stdout, stderr } = await execFile(this.config.lldbPath, [ - '-b', - '-o', - 'script print(100+1)', - ]); - stdout = (stdout || '') as string; - stderr = (stderr || '') as string; - - if (stdout.indexOf('101') != -1) { - this.logger.info('Python scripting support in LLDB found.'); - return true; - } else { - this.logger.info( - `Python scripting support in LLDB not found. The test script returned:\n${ - stdout - }\n${stderr}`, - ); - } - } catch (e) { - this.logger.error( - 'Python scripting support in LLDB not found. The test script failed with', - e, - ); - } - return false; - } - - /** - * Returns a process environment to be used when executing SDK - * binaries. - */ - public getProcessEnv(withTelemetry: boolean = true): NodeJS.ProcessEnv { - const env = { ...process.env }; - - // If we had modular home provided somewhere, make sure that - // gets propagated. - if (this.config.modularHomePath) { - env.MODULAR_HOME = this.config.modularHomePath; - } - if (!withTelemetry) { - env.MODULAR_TELEMETRY_ENABLED = 'false'; - } - return env; - } -} diff --git a/extension/sdk/sdkConfig.ts b/extension/sdk/sdkConfig.ts deleted file mode 100644 index 15f1105..0000000 --- a/extension/sdk/sdkConfig.ts +++ /dev/null @@ -1,124 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This file is Modular Inc proprietary. -// -//===----------------------------------------------------------------------===// - -import { Logger } from '../logging'; -import { MAXSDKVersion } from './sdkVersion'; -import * as util from 'util'; -import { execFile as execFileBase } from 'child_process'; -import { Optional } from '../types'; -const execFile = util.promisify(execFileBase); - -/** - * This class represents a subset of the Modular config object used by extension - * for interacting with mojo. It should be handled a POD object. - */ -export class MAXSDKConfig { - /** - * The version of the SDK. - */ - readonly version: MAXSDKVersion; - - /** - * The MODULAR_HOME path containing the SDK. - */ - readonly modularHomePath: string; - - /** - * The path to the mojo driver within the SDK installation. - */ - readonly mojoDriverPath: string; - - /** - * The path to mblack. - */ - readonly mojoMBlackPath: string; - - /** - * The path to the LLDB vscode debug adapter. - */ - readonly mojoLLDBVSCodePath: string; - - /** - * The path to the LLDB visualizers. - */ - readonly mojoLLDBVisualizersPath: string; - - /** - * The path the mojo language server within the SDK installation. - */ - readonly mojoLanguageServerPath: string; - - /** - * The path to the mojo LLDB plugin. - */ - readonly mojoLLDBPluginPath: string; - - /** - * The path to the LLDB binary. - */ - readonly lldbPath: string; - - public constructor( - version: MAXSDKVersion, - modularPath: string, - rawConfig: { [key: string]: any }, - ) { - this.version = version; - this.modularHomePath = modularPath; - this.mojoLLDBVSCodePath = rawConfig.lldb_vscode_path; - this.mojoLLDBVisualizersPath = rawConfig.lldb_visualizers_path; - this.mojoDriverPath = rawConfig.driver_path; - this.mojoMBlackPath = rawConfig.mblack_path; - this.mojoLanguageServerPath = rawConfig.lsp_server_path; - this.mojoLLDBPluginPath = rawConfig.lldb_plugin_path; - this.lldbPath = rawConfig.lldb_path; - } - - /** - * Parse a version number from the given mojo driver. - */ - public static async parseVersionFromDriver( - logger: Logger, - driverPath: string, - configSection: string, - ): Promise> { - try { - const { stdout, stderr } = await execFile(driverPath, ['--version'], { - env: { ...process.env }, - encoding: 'utf-8', - }); - logger.info(`${driverPath} --version results\n` + stderr + '\n' + stdout); - - if (stderr) { - return undefined; - } - - const match = stdout.toString().match(/mojo\s+([0-9]+)\.([0-9]+)\.(.*)/); - - if (!match) { - return undefined; - } - - // Build the title of the version based on the config key. - let title = 'Mojo'; - - if (configSection.includes('max')) { - title += ' Max'; - } - - return new MAXSDKVersion( - title, - `${match[1]}`, - `${match[2]}`, - `${match[3]}`, - driverPath, - ); - } catch (e) { - logger.error('Unable to parse version from `mojo` driver: ', e); - return undefined; - } - } -} diff --git a/extension/sdk/sdkManager.ts b/extension/sdk/sdkManager.ts deleted file mode 100644 index 5e0c3f7..0000000 --- a/extension/sdk/sdkManager.ts +++ /dev/null @@ -1,492 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This file is Modular Inc proprietary. -// -//===----------------------------------------------------------------------===// - -import * as ini from 'ini'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import * as config from '../utils/config'; -import { realpath } from 'fs-extra'; -import { Logger } from '../logging'; -import { DisposableContext } from '../utils/disposableContext'; -import { MAXSDKConfig } from './sdkConfig'; -import { MAXSDK } from './sdk'; -import { Mutex } from 'async-mutex'; -import { - directoryExists, - getAllOpenMojoFiles, - isMojoFile, - moveUpUntil, - readFile, -} from '../utils/files'; -import { MAXSDKVersion } from './sdkVersion'; -import { findMagicSDKSpec, installMagicSDK } from './magicSdk'; -import { Expected, MAXSDKSpec } from './types'; -import { Subject } from 'rxjs'; -import { Optional } from '../types'; - -type NotYetSelectedSDK = { - state: 'not-yet-selected'; -}; - -type SelectedSDK = { - state: 'selected'; - sdkSpec: Optional; - errorMessage?: Optional; -}; - -type SDKSelection = NotYetSelectedSDK | SelectedSDK; - -/** - * This class manages the active SDK, switching SDKs, and other related ad hoc actions. - * - * There are two public APIs: - * - `findSDK` is the way to get the active SDK and it's protected by a mutex. - * - `createAdHocSDKAndShowError` is used for actions that force the use of a given SDK. - * This function doesn't have side effects. - * - * Caching should be minimized to capture the current state of the SDKs in the filesystem. - */ -export class MAXSDKManager extends DisposableContext { - public logger: Logger; - private statusBarItem: vscode.StatusBarItem; - private _activeSDK: SDKSelection = { state: 'not-yet-selected' }; - private set activeSDK(newSDK: SDKSelection) { - this._activeSDK = newSDK; - this.refreshStatusBarItemVisibility(); - this.onActiveSDKChanged.next(); - } - private get activeSDK() { - return this._activeSDK; - } - - private findSDKMutex = new Mutex(); - private isNightly: boolean; - private extensionContext: vscode.ExtensionContext; - public onActiveSDKChanged = new Subject(); - - constructor( - logger: Logger, - isNightly: boolean, - extensionContext: vscode.ExtensionContext, - ) { - super(); - this.logger = logger; - this.isNightly = isNightly; - this.extensionContext = extensionContext; - this.statusBarItem = vscode.window.createStatusBarItem( - 'mojo.selected-sdk', - vscode.StatusBarAlignment.Right, - /*priority=*/ 100, - ); - this.pushSubscription(this.statusBarItem); - this.statusBarItem.command = 'mojo.sdk.select-default'; - this.statusBarItem.name = 'MAX SDK'; - this.pushSubscription( - vscode.window.onDidChangeVisibleTextEditors((_editors) => { - this.refreshStatusBarItemVisibility(); - }), - ); - - this.pushSubscription( - vscode.commands.registerCommand('mojo.sdk.select-default', async () => { - const allSDKSpecs = await this.findAllSDKs(); - if (allSDKSpecs.length === 0) { - vscode.window.showErrorMessage('No MAX SDKs were found.'); - return; - } - const sdkNames = allSDKSpecs.map((spec) => spec.version.toString()); - const selectedName = await vscode.window.showQuickPick(sdkNames, { - ignoreFocusOut: true, - title: 'Select the default MAX SDK to use', - placeHolder: - this.activeSDK.state === 'selected' && this.activeSDK.sdkSpec - ? `Currently using ${this.activeSDK.sdkSpec.version.toString()}` - : 'Select an SDK or cancel', - }); - const selectedSDK = allSDKSpecs.find( - (spec) => spec.version.toString() === selectedName, - ); - if (selectedSDK !== undefined) { - this.extensionContext.globalState.update( - 'mojo.defaultSDKModularHomePath', - selectedSDK.modularHomePath, - ); - const finalSDK: SelectedSDK = { - state: 'selected', - sdkSpec: selectedSDK, - }; - await this.createSDKAndShowError( - finalSDK, - /*hireRepeatedErrors=*/ false, - /*allowInstallation=*/ true, - ); - this.activeSDK = finalSDK; - } - }), - ); - this.pushSubscription( - vscode.commands.registerCommand('mojo.sdk.reinstall', async () => { - const result = await installMagicSDK( - /*withLock=*/ false, - this.extensionContext, - this.logger, - this.isNightly, - /*reinstall=*/ true, - ); - if ( - this.activeSDK.state === 'selected' && - this.activeSDK.sdkSpec?.kind === 'magic' && - this.activeSDK.errorMessage !== undefined && - result === 'succeeded' - ) { - this.activeSDK = { - state: 'selected', - sdkSpec: this.activeSDK.sdkSpec, - }; - } - }), - ); - } - - public async findSDK(hideRepeatedErrors: boolean): Promise> { - const doWork = async () => { - if (this.activeSDK.state === 'selected') { - return this.createSDKAndShowError( - this.activeSDK, - hideRepeatedErrors, - /*allowInstallation=*/ false, - ); - } else { - // This is invoked only once per extension activation. - const sdkSpec = await this.selectSDK(); - const sdkSelection: SDKSelection = { state: 'selected', sdkSpec }; - const sdk = await this.createSDKAndShowError( - sdkSelection, - /*hideRepeatedErrors=*/ false, - /*allowInstallation=*/ true, - ); - this.activeSDK = sdkSelection; - return sdk; - } - }; - - return this.findSDKMutex.runExclusive(() => doWork()); - } - - public async createAdHocSDKAndShowError( - modularHomePath: string, - ): Promise> { - const hideRepeatedErrors = false; - const allowInstallation = false; - - const devSDKSpec = await this.findDevSDKSpecFromSubPath(modularHomePath); - if (devSDKSpec !== undefined) { - return this.createSDKAndShowError( - { state: 'selected', sdkSpec: devSDKSpec }, - hideRepeatedErrors, - /*allowInstallation=*/ false, - ); - } - if ( - this.activeSDK.state === 'selected' && - this.activeSDK.sdkSpec?.modularHomePath === modularHomePath - ) { - return this.createSDKAndShowError( - this.activeSDK, - hideRepeatedErrors, - allowInstallation, - ); - } - const sdkSpec: MAXSDKSpec = { - kind: 'custom', - modularHomePath, - version: new MAXSDKVersion( - modularHomePath, - '0', - '0', - '0', - modularHomePath, - ), - }; - return this.createSDKAndShowError( - { state: 'selected', sdkSpec }, - hideRepeatedErrors, - allowInstallation, - ); - } - - private async createSDKAndShowError( - selectedSDK: SelectedSDK, - hideRepeatedErrors: boolean, - allowInstallation: boolean, - ): Promise> { - const result = await this.doCreateSDK(selectedSDK, allowInstallation); - if (result.errorMessage !== undefined) { - if ( - hideRepeatedErrors && - selectedSDK.errorMessage === result.errorMessage - ) { - return undefined; - } - let errorMessage = result.errorMessage; - selectedSDK.errorMessage = result.errorMessage; - - if (selectedSDK.sdkSpec?.kind === 'dev') { - this.showBazelwRunInstallPrompt( - errorMessage, - selectedSDK.sdkSpec.modularHomePath, - ); - } else if (selectedSDK.sdkSpec?.kind === 'magic') { - errorMessage += '\nPlease reinstall the MAX SDK for VS Code.'; - vscode.window - .showErrorMessage(errorMessage, 'Reinstall') - .then(async (value) => { - if (value === 'Reinstall') { - vscode.commands.executeCommand('mojo.sdk.reinstall'); - } - }); - } else if (selectedSDK.sdkSpec?.kind === 'custom') { - errorMessage += `\nPlease reinstall or rebuild the SDK given by ${selectedSDK.sdkSpec.modularHomePath}.`; - vscode.window.showErrorMessage(errorMessage); - } - this.logger.error(errorMessage); - return undefined; - } - return result.value; - } - - private refreshStatusBarItemVisibility(): void { - if (isMojoFile(vscode.window.activeTextEditor?.document.uri)) { - const activeSDK = this.activeSDK; - if (activeSDK.state === 'selected' && activeSDK.sdkSpec !== undefined) { - this.statusBarItem.text = `MAX SDK: ${activeSDK.sdkSpec.version.toTinyString()}`; - this.statusBarItem.show(); - return; - } - } - this.statusBarItem.hide(); - } - - public showBazelwRunInstallPrompt( - errorMessage: string, - modularHomePath: string, - ): void { - const action = 'Run ./bazelw run //:install'; - vscode.window.showErrorMessage(errorMessage, action).then((value) => { - if (value === action) { - const repo = path.dirname(modularHomePath); - const terminal = - vscode.window.activeTerminal || - vscode.window.createTerminal({ - name: repo, - }); - terminal.sendText(`(cd '${repo}' && ./bazelw run //:install)`); - terminal.show(); - } - }); - } - - private async doCreateSDK( - selectedSDK: SelectedSDK, - allowInstallation: boolean, - ): Promise> { - const spec = selectedSDK.sdkSpec; - if (spec === undefined) { - return { - errorMessage: 'The Mojo🔥 development environment was not found.', - }; - } - if (selectedSDK.sdkSpec?.kind === 'magic') { - if (allowInstallation) { - await installMagicSDK( - /*withLock=*/ true, - this.extensionContext, - this.logger, - this.isNightly, - ); - } - } - const modularConfigPath = path.join(spec.modularHomePath, 'modular.cfg'); - const modularConfigContents = await readFile(modularConfigPath); - if (modularConfigContents === undefined) { - return { - errorMessage: `The modular config file '${modularConfigPath}' can't be read.`, - }; - } - const modularConfig = ini.parse(modularConfigContents); - this.logger.info(`'${modularConfigPath}' with contents`, modularConfig); - const mojoConfig = modularConfig['mojo-max']; - if (!mojoConfig) { - return { - errorMessage: `The modular config file '${modularConfigPath}' doesn't have the expected section 'mojo-max'`, - }; - } - const sdkConfig = new MAXSDKConfig( - spec.version, - spec.modularHomePath, - mojoConfig, - ); - if (!sdkConfig) { - return { - errorMessage: `Unable to determine the MAX SDK version.`, - }; - } - return { value: new MAXSDK(sdkConfig, spec.kind, this.logger) }; - } - - private async selectSDK(): Promise> { - const allSDKSpecs = await this.findAllSDKs(); - if (allSDKSpecs.length === 0) { - return undefined; - } - if (allSDKSpecs.length === 1) { - return allSDKSpecs[0]; - } - const defaultSDKModularHomePath = this.extensionContext.globalState.get< - Optional - >('mojo.defaultSDKModularHomePath'); - const selectedDefaultSDKSpec = allSDKSpecs.find( - (spec) => spec.modularHomePath === defaultSDKModularHomePath, - )!; - if (selectedDefaultSDKSpec !== undefined) { - return selectedDefaultSDKSpec; - } - - return allSDKSpecs[0]; - } - - private async findAllSDKs(): Promise { - // If we're only going to use the release SDK specs, don't bother looking for others. - if (process.env['MOJO_EXTENSION_FORCE_MAGIC'] !== undefined) { - const releaseSDKSpecs = await this.findMagicSDKSpecs(); - this.logger.info( - 'MOJO_EXTENSION_FORCE_MAGIC is set; using release SDK spec(s)', - releaseSDKSpecs, - ); - return releaseSDKSpecs; - } - - // This list has to be returned in a specific order, as the default SDK - // in new sessions will be the first one from this list. - const [devSDKSpecs, releaseSDKSpecs, userProvidedSDKSpecs] = - await Promise.all([ - this.findUserProvidedSDKSpecs(), - this.findDevSDKSpecs(), - this.findMagicSDKSpecs(), - ]); - - return [...devSDKSpecs, ...releaseSDKSpecs, ...userProvidedSDKSpecs]; - } - - private async findUserProvidedSDKSpecs(): Promise { - const additionalRoots = config.get( - 'SDK.additionalSDKs', - undefined, - [], - ); - const specs = await Promise.all( - additionalRoots.map(async (modularHomePath) => { - const modularConfig = path.join(modularHomePath, 'modular.cfg'); - const contents = await readFile(modularConfig); - if (contents === undefined) { - this.logger.error( - `Unable to read ${modularConfig}. Skipping the user-provided SDK ${modularHomePath}`, - ); - return undefined; - } - const spec: MAXSDKSpec = { - kind: 'custom', - modularHomePath, - version: new MAXSDKVersion( - 'MAX SDK', - '-1', - '-1', - '-1', - modularHomePath, - ), - }; - return spec; - }), - ); - return specs.filter((spec): spec is MAXSDKSpec => spec !== undefined); - } - - private async findDevSDKSpecs(): Promise { - const visiblePaths = []; - const [activeMojoFile, otherOpenMojoFiles] = getAllOpenMojoFiles(); - - if (activeMojoFile) { - visiblePaths.push(activeMojoFile.uri.fsPath); - } - for (const file of otherOpenMojoFiles) { - visiblePaths.push(file.uri.fsPath); - } - for (const workspaceFolder of vscode.workspace.workspaceFolders || []) { - visiblePaths.push(workspaceFolder.uri.fsPath); - } - return this.findDevSDKSpecsFromSubPaths(visiblePaths); - } - - private async findDevSDKSpecsFromSubPaths( - paths: string[], - ): Promise { - const candidateSDKSpecs = ( - await Promise.all( - paths.map((path) => this.findDevSDKSpecFromSubPath(path)), - ) - ).filter((spec): spec is MAXSDKSpec => spec !== undefined); - const uniqueSDKSpecs = new Map(); - candidateSDKSpecs.forEach((spec) => - uniqueSDKSpecs.set(spec.modularHomePath, spec), - ); - return [...uniqueSDKSpecs.values()]; - } - - private async findDevSDKSpecFromSubPath( - fsPath: string, - ): Promise> { - const repoRoot = await moveUpUntil(fsPath, (p) => - directoryExists(path.join(p, '.git')), - ); - if (!repoRoot) { - return undefined; - } - const bazelPath = path.join(repoRoot, 'MODULE.bazel'); - const bazelContents = await readFile(bazelPath); - if (!bazelContents) { - return undefined; - } - if (!bazelContents.includes('module(name = "modular-internal")')) { - return undefined; - } - // It is possible to clone the monorepo and run the extension without ever creating .derived. - if (!directoryExists(path.join(repoRoot, '.derived'))) { - return undefined; - } - const modularHomePath = await realpath(path.join(repoRoot, '.derived')); - const spec: MAXSDKSpec = { - kind: 'dev', - modularHomePath, - version: new MAXSDKVersion( - 'Modular Repo', - '0', - '0', - '0', - modularHomePath, - ), - }; - return spec; - } - - private async findMagicSDKSpecs(): Promise { - const spec = await findMagicSDKSpec( - this.extensionContext, - this.logger, - true, - ); - return spec ? [spec] : []; - } -} diff --git a/extension/sdk/sdkVersion.ts b/extension/sdk/sdkVersion.ts deleted file mode 100644 index 2fd7820..0000000 --- a/extension/sdk/sdkVersion.ts +++ /dev/null @@ -1,73 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This file is Modular Inc proprietary. -// -//===----------------------------------------------------------------------===// - -import * as path from 'path'; -/** - * This class represents a MAX SDK version. - */ -export class MAXSDKVersion { - constructor( - title: string, - major: string, - minor: string, - patch: string, - modularHomePath: string, - ) { - this.title = title; - this.minor = minor; - this.major = major; - this.patch = patch; - this.modularHomePath = modularHomePath; - } - - /** - * Return if this is a dev version. - */ - isDev(): boolean { - return this.minor === '0' && this.major === '0' && this.patch === '0'; - } - - /** - * Return if this is a user-provided spec - */ - isUserProvided(): boolean { - return this.minor === '-1' && this.major === '-1' && this.patch === '-1'; - } - - /** - * Convert the version into a human readable string. - */ - toString(): string { - // If this is a dev build, format the title differently. - if (this.isDev()) { - // We include the path to the modular repo, which is three levels up from - // the mojo driver path. - return `${this.title} (dev) - ${this.modularHomePath}`; - } - if (this.isUserProvided()) { - return `${this.title} (user-provided) - ${this.modularHomePath}`; - } - - // Otherwise, just format the version number. - return `${this.title} - ${this.major}.${this.minor}.${this.patch}`; - } - - /** - * @returns a tiny representation of this version for small displays. - */ - toTinyString(): string { - if (this.isDev() || this.isUserProvided()) { - return `${path.dirname(this.modularHomePath)}`; - } - return `${this.title}`; - } - - title: string; - minor: string; - major: string; - patch: string; - modularHomePath: string; -} diff --git a/extension/sdk/types.ts b/extension/sdk/types.ts deleted file mode 100644 index cacfc16..0000000 --- a/extension/sdk/types.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { MAXSDKVersion } from './sdkVersion'; - -export type MAXSDKKind = 'dev' | 'magic' | 'custom'; - -/** - * A MAX SDK Spec represents an SDK somewhere in the file system, but it's not - * guaranteed to exist or even have a valid modular.cfg file. - */ -export type MAXSDKSpec = { - kind: MAXSDKKind; - modularHomePath: string; - version: MAXSDKVersion; -}; - -export type Expected = - | { - errorMessage: string; - } - | { - value: T; - errorMessage?: undefined; - }; diff --git a/extension/testing/testing.ts b/extension/testing/testing.ts index 1287e2a..da5c817 100644 --- a/extension/testing/testing.ts +++ b/extension/testing/testing.ts @@ -6,13 +6,12 @@ import { execFile } from 'child_process'; import * as vscode from 'vscode'; -import { MAXSDK } from '../sdk/sdk'; import * as config from '../utils/config'; import { DisposableContext } from '../utils/disposableContext'; import * as path from 'path'; -import { MAXSDKManager } from '../sdk/sdkManager'; import { Logger } from '../logging'; import { Optional } from '../types'; +import { PythonEnvironmentManager, SDK } from '../pyenv'; /** * An interface defining a source range for a mojo test. @@ -53,7 +52,7 @@ interface MojoTestExecutionResult { * mojo testing. */ export class MojoTestManager extends DisposableContext { - private sdkManager: MAXSDKManager; + private envManager: PythonEnvironmentManager; private controller: vscode.TestController; private logger: Logger; @@ -61,9 +60,9 @@ export class MojoTestManager extends DisposableContext { private docTestTag = new vscode.TestTag('docTest'); private unitTestTag = new vscode.TestTag('unitTest'); - constructor(sdkManager: MAXSDKManager, logger: Logger) { + constructor(envManager: PythonEnvironmentManager, logger: Logger) { super(); - this.sdkManager = sdkManager; + this.envManager = envManager; this.logger = logger; // Register the mojo test controller. @@ -268,7 +267,7 @@ export class MojoTestManager extends DisposableContext { }; // Grab the sdk for the execution context. - const sdk = await this.sdkManager.findSDK(/*hideRepeatedErrors=*/ false); + const sdk = await this.envManager.getActiveSDK(); if (!sdk) { this.controller.items.delete(test.uri!.fsPath); return; @@ -343,7 +342,7 @@ export class MojoTestManager extends DisposableContext { * arguments. Returns the json output of running the command. */ async runMojoTestCommand( - sdk: MAXSDK, + sdk: SDK, testId: string, workspaceFolder: Optional, args: string[] = [], @@ -362,7 +361,7 @@ export class MojoTestManager extends DisposableContext { return new Promise>(function (resolve, _reject) { execFile( - sdk.config.mojoDriverPath, + sdk.mojoPath, ['test', '--diagnostic-format', 'json', testId, ...args], { env }, (_error, stdout, _stderr) => { @@ -392,7 +391,7 @@ export class MojoTestManager extends DisposableContext { // Invoke the mojo tool to discover tests in the document. // We use 'hideRepeatedErrors' because this action is automated. - const sdk = await this.sdkManager.findSDK(/*hideRepeatedErrors=*/ true); + const sdk = await this.envManager.getActiveSDK(); if (!sdk) { this.controller.items.delete(document.uri.fsPath); this.logger.debug(`No SDK present, clearing tests for ${document.uri}`); diff --git a/package-lock.json b/package-lock.json index 6f06869..ec3eab6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@vscode/extension-telemetry": "^0.9.7", + "@vscode/python-extension": "^1.0.5", "async-mutex": "0.5.0", "axios": "1.8.2", "chokidar": "^3.6.0", @@ -1569,6 +1570,16 @@ "vscode": "^1.75.0" } }, + "node_modules/@vscode/python-extension": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@vscode/python-extension/-/python-extension-1.0.5.tgz", + "integrity": "sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==", + "license": "MIT", + "engines": { + "node": ">=16.17.1", + "vscode": "^1.78.0" + } + }, "node_modules/@vscode/test-cli": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.10.tgz", diff --git a/package.json b/package.json index 517bb43..fde84a4 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,9 @@ "onUri", "onStartupFinished" ], + "extensionDependencies": [ + "ms-python.python" + ], "main": "./out/extension.js", "scripts": { "vscode:prepublish": "npm run typecheck && npm run bundle", @@ -34,7 +37,7 @@ "ci": "npm ci && cd ./lsp-proxy && npm ci", "format": "npx prettier . --write --ignore-path .gitignore", "publish": "vsce publish", - "test": "npm run build && vscode-test" + "test": "rm -r out/ && npm run build && vscode-test" }, "devDependencies": { "@eslint/js": "^9.30.1", @@ -58,6 +61,7 @@ }, "dependencies": { "@vscode/extension-telemetry": "^0.9.7", + "@vscode/python-extension": "^1.0.5", "async-mutex": "0.5.0", "axios": "1.8.2", "chokidar": "^3.6.0",