From 3d9d4db0cbf2dc5b8cdb0893cdef6a114581a87c Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 25 Sep 2024 17:32:05 +0200 Subject: [PATCH] feat: select path for runner when installing (#1238) Closes #1144 ### Summary of Changes Users must now select a path for the runner during installation, since the path given by VS Code might not be writable. --- .eslintrc.cjs | 2 + .../src/extension/actions/installRunner.ts | 55 ++++++++++++------- .../src/extension/actions/updateRunner.ts | 2 +- .../src/extension/mainClient.ts | 4 +- 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index cafa984d1..29a564cd8 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -7,6 +7,8 @@ module.exports = { extends: '@lars-reimann/svelte', rules: { + '@typescript-eslint/no-throw-literal': 'off', + '@typescript-eslint/only-throw-error': 'error', 'import/extensions': 'off', 'import/no-extraneous-dependencies': 'off', 'svelte/valid-compile': 'off', diff --git a/packages/safe-ds-vscode/src/extension/actions/installRunner.ts b/packages/safe-ds-vscode/src/extension/actions/installRunner.ts index 224b5be0a..59248c393 100644 --- a/packages/safe-ds-vscode/src/extension/actions/installRunner.ts +++ b/packages/safe-ds-vscode/src/extension/actions/installRunner.ts @@ -1,4 +1,4 @@ -import vscode, { ExtensionContext, Uri } from 'vscode'; +import vscode, { Uri } from 'vscode'; import child_process from 'node:child_process'; import semver from 'semver'; import { dependencies, rpc } from '@safe-ds/lang'; @@ -11,7 +11,7 @@ const LOWEST_SUPPORTED_PYTHON_VERSION = '3.11.0'; const LOWEST_UNSUPPORTED_PYTHON_VERSION = '3.13.0'; const npmVersionRange = `>=${LOWEST_SUPPORTED_PYTHON_VERSION} <${LOWEST_UNSUPPORTED_PYTHON_VERSION}`; -export const installRunner = (context: ExtensionContext, client: LanguageClient) => { +export const installRunner = (client: LanguageClient) => { return async () => { // If the runner is already started, do nothing if (await client.sendRequest(rpc.IsRunnerReadyRequest.type)) { @@ -19,8 +19,20 @@ export const installRunner = (context: ExtensionContext, client: LanguageClient) return; } + // Ask the user where the virtual environment should be created + const runnerVirtualEnvironmentUris = await vscode.window.showOpenDialog({ + canSelectFolders: true, + canSelectMany: false, + title: 'Location for the runner installation', + }); + + if (!runnerVirtualEnvironmentUris || runnerVirtualEnvironmentUris.length === 0) { + return; + } + const runnerVirtualEnvironmentUri = runnerVirtualEnvironmentUris[0]!; + // Install the runner if it is not already installed - const success = await doInstallRunner(context); + const success = await doInstallRunner(runnerVirtualEnvironmentUri); if (!success) { return; } @@ -28,7 +40,11 @@ export const installRunner = (context: ExtensionContext, client: LanguageClient) // Set the runner command in the configuration await vscode.workspace .getConfiguration() - .update('safe-ds.runner.command', getRunnerCommand(context), vscode.ConfigurationTarget.Global); + .update( + 'safe-ds.runner.command', + getRunnerCommand(runnerVirtualEnvironmentUri), + vscode.ConfigurationTarget.Global, + ); // Start the runner (needed if the configuration did not change, so no event is fired) await client.sendNotification(rpc.StartRunnerNotification.type); @@ -41,7 +57,7 @@ export const installRunner = (context: ExtensionContext, client: LanguageClient) /** * Installs the runner in a virtual environment. Returns true if the installation was successful. */ -const doInstallRunner = async (context: ExtensionContext): Promise => { +const doInstallRunner = async (runnerVirtualEnvironmentUri: Uri): Promise => { // Check if a matching Python interpreter is available const pythonCommand = await getPythonCommand(); if (!pythonCommand) { @@ -58,7 +74,7 @@ const doInstallRunner = async (context: ExtensionContext): Promise => { }, async () => { try { - await createRunnerVirtualEnvironment(context, pythonCommand); + await createRunnerVirtualEnvironment(runnerVirtualEnvironmentUri, pythonCommand); return true; } catch (error) { vscode.window.showErrorMessage('Failed to create a virtual environment.'); @@ -79,7 +95,7 @@ const doInstallRunner = async (context: ExtensionContext): Promise => { }, async () => { try { - await installRunnerInVirtualEnvironment(getPipCommand(context)); + await installRunnerInVirtualEnvironment(getPipCommand(runnerVirtualEnvironmentUri)); return true; } catch (error) { vscode.window.showErrorMessage('Failed to install the runner.'); @@ -116,9 +132,12 @@ const isMatchingPython = async (pythonCommand: string): Promise => { }); }; -const createRunnerVirtualEnvironment = async (context: ExtensionContext, pythonCommand: string): Promise => { +const createRunnerVirtualEnvironment = async ( + runnerVirtualEnvironmentUri: Uri, + pythonCommand: string, +): Promise => { return new Promise((resolve, reject) => { - child_process.exec(`${pythonCommand} -m venv ${runnerVirtualEnvironmentUri(context).fsPath}`, (error) => { + child_process.exec(`"${pythonCommand}" -m venv "${runnerVirtualEnvironmentUri.fsPath}"`, (error) => { if (error) { reject(error); } else { @@ -130,7 +149,7 @@ const createRunnerVirtualEnvironment = async (context: ExtensionContext, pythonC export const installRunnerInVirtualEnvironment = async (pipCommand: string): Promise => { return new Promise((resolve, reject) => { - const installCommand = `${pipCommand} install --upgrade "safe-ds-runner${dependencies['safe-ds-runner'].pipVersionRange}"`; + const installCommand = `"${pipCommand}" install --upgrade "safe-ds-runner${dependencies['safe-ds-runner'].pipVersionRange}"`; const process = child_process.spawn(installCommand, { shell: true }); process.stdout.on('data', (data: Buffer) => { @@ -153,22 +172,18 @@ export const installRunnerInVirtualEnvironment = async (pipCommand: string): Pro }); }; -const getPipCommand = (context: ExtensionContext): string => { +const getPipCommand = (runnerVirtualEnvironmentUri: Uri): string => { if (process.platform === 'win32') { - return `${runnerVirtualEnvironmentUri(context).fsPath}\\Scripts\\pip.exe`; + return `${runnerVirtualEnvironmentUri.fsPath}\\Scripts\\pip.exe`; } else { - return `${runnerVirtualEnvironmentUri(context).fsPath}/bin/pip`; + return `${runnerVirtualEnvironmentUri.fsPath}/bin/pip`; } }; -const getRunnerCommand = (context: ExtensionContext): string => { +const getRunnerCommand = (runnerVirtualEnvironmentUri: Uri): string => { if (process.platform === 'win32') { - return `${runnerVirtualEnvironmentUri(context).fsPath}\\Scripts\\safe-ds-runner.exe`; + return `${runnerVirtualEnvironmentUri.fsPath}\\Scripts\\safe-ds-runner.exe`; } else { - return `${runnerVirtualEnvironmentUri(context).fsPath}/bin/safe-ds-runner`; + return `${runnerVirtualEnvironmentUri.fsPath}/bin/safe-ds-runner`; } }; - -const runnerVirtualEnvironmentUri = (context: ExtensionContext): Uri => { - return vscode.Uri.joinPath(context.globalStorageUri, 'runnerVenv'); -}; diff --git a/packages/safe-ds-vscode/src/extension/actions/updateRunner.ts b/packages/safe-ds-vscode/src/extension/actions/updateRunner.ts index 5bf0f5705..90362da70 100644 --- a/packages/safe-ds-vscode/src/extension/actions/updateRunner.ts +++ b/packages/safe-ds-vscode/src/extension/actions/updateRunner.ts @@ -17,7 +17,7 @@ export const updateRunner = (context: ExtensionContext, client: LanguageClient) // If the runner executable cannot be found at all, install it from scratch if (!fs.existsSync(await getRunnerCommand())) { - await installRunner(context, client)(); + await installRunner(client)(); return; } diff --git a/packages/safe-ds-vscode/src/extension/mainClient.ts b/packages/safe-ds-vscode/src/extension/mainClient.ts index da0791a9b..ecd1ce35d 100644 --- a/packages/safe-ds-vscode/src/extension/mainClient.ts +++ b/packages/safe-ds-vscode/src/extension/mainClient.ts @@ -98,7 +98,7 @@ const createLanguageClient = function (context: vscode.ExtensionContext): Langua const registerNotificationListeners = function (context: vscode.ExtensionContext) { context.subscriptions.push( client.onNotification(rpc.InstallRunnerNotification.type, async () => { - await installRunner(context, client)(); + await installRunner(client)(); }), client.onNotification(rpc.RunnerStartedNotification.type, async ({ port }: rpc.RunnerStartedParams) => { await services.runtime.PythonServer.connectToPort(port); @@ -114,7 +114,7 @@ const registerCommands = function (context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('safe-ds.dumpDiagnostics', dumpDiagnostics(context)), vscode.commands.registerCommand('safe-ds.exploreTable', exploreTable(context)), - vscode.commands.registerCommand('safe-ds.installRunner', installRunner(context, client)), + vscode.commands.registerCommand('safe-ds.installRunner', installRunner(client)), vscode.commands.registerCommand('safe-ds.openDiagnosticsDumps', openDiagnosticsDumps(context)), vscode.commands.registerCommand('safe-ds.refreshWebview', refreshWebview(context)), vscode.commands.registerCommand('safe-ds.updateRunner', updateRunner(context, client)),