diff --git a/package-lock.json b/package-lock.json index a63600b9..f538aa1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "4.0.0", "license": "MIT", "dependencies": { + "@vscode/python-extension": "1.0.5", "glob": "^11.1.0", "semver": "^7.7.3", "vscode-languageclient": "^8.1.0", @@ -22,7 +23,7 @@ "@types/mocha": "^10.0.6", "@types/node": "^22.13.0", "@types/semver": "^7.7.1", - "@types/vscode": "^1.67.0", + "@types/vscode": "^1.78.0", "@types/which": "^3.0.0", "@typescript-eslint/eslint-plugin": "^8.52.0", "@typescript-eslint/parser": "^8.29.1", @@ -48,7 +49,7 @@ "webpack-cli": "^6.0.1" }, "engines": { - "vscode": "^1.67.0" + "vscode": "^1.78.0" } }, "node_modules/@azu/format-text": { @@ -1528,6 +1529,16 @@ "node": ">=20.0.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.12", "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.12.tgz", diff --git a/package.json b/package.json index e05cb66a..ac1137f3 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ } ], "engines": { - "vscode": "^1.67.0" + "vscode": "^1.78.0" }, "icon": "assets/png/icon.png", "homepage": "https://github.com/fortran-lang/vscode-fortran-support#readme", @@ -763,7 +763,7 @@ "@types/mocha": "^10.0.6", "@types/node": "^22.13.0", "@types/semver": "^7.7.1", - "@types/vscode": "^1.67.0", + "@types/vscode": "^1.78.0", "@types/which": "^3.0.0", "@typescript-eslint/eslint-plugin": "^8.52.0", "@typescript-eslint/parser": "^8.29.1", @@ -796,6 +796,7 @@ ] }, "dependencies": { + "@vscode/python-extension": "1.0.5", "glob": "^11.1.0", "semver": "^7.7.3", "vscode-languageclient": "^8.1.0", diff --git a/src/util/python-installer.ts b/src/util/python-installer.ts new file mode 100644 index 00000000..1afbdc3c --- /dev/null +++ b/src/util/python-installer.ts @@ -0,0 +1,77 @@ +import * as vscode from 'vscode'; + +import { spawnAsPromise } from './tools'; + +const PYTHON_EXTENSION_ID = 'ms-python.python'; +// TODO: add pretty output Log channel or maybe progress notification? +// TODO: Creates a schism between installation and detection/usage of binaries +// the detection/usage should also leverage the python extension +// TODO: All of these functions watch for change events in the python extension +/** + * Get Python executable from ms-python.python extension if available, otherwise fallback. + * @param resource Optional URI to determine workspace-specific environment + * @returns Python executable path or undefined + */ +async function getPythonExecutable(resource?: vscode.Uri): Promise { + const pyExt = vscode.extensions.getExtension(PYTHON_EXTENSION_ID); + if (!pyExt) return undefined; + + if (!pyExt.isActive) { + await pyExt.activate(); + } + + // Import type only when extension is available + const { PythonExtension } = await import('@vscode/python-extension'); + const api = await PythonExtension.api(); + + const envPath = api.environments.getActiveEnvironmentPath(resource); + if (!envPath) return undefined; + + const env = await api.environments.resolveEnvironment(envPath); + return env?.executable.uri?.fsPath; +} + +/** + * Install Python package using pip with ms-python.python integration. + * Falls back to system Python if extension unavailable. + * @param pyPackage name of python package in PyPi + * @param resource Optional URI for workspace-specific environment selection + * @returns Success message + */ +export async function installPythonPackage( + pyPackage: string, + resource?: vscode.Uri +): Promise { + let pythonExe = await getPythonExecutable(resource); + + // Fallback to system Python + if (!pythonExe) { + const candidates = ['python3', 'py', 'python']; + for (const cmd of candidates) { + try { + await spawnAsPromise(cmd, ['--version'], { windowsHide: true }); + pythonExe = cmd; + break; + } catch { + continue; + } + } + } + + if (!pythonExe) { + throw new Error('No Python interpreter found'); + } + + const args = ['-m', 'pip', 'install', '--upgrade', pyPackage]; + + try { + // Virtual environments do not support user installs + // the first attempt is "global" + await spawnAsPromise(pythonExe, args, { windowsHide: true }); + } catch { + // Retry with --user for permission-restricted environments + await spawnAsPromise(pythonExe, [...args, '--user'], { windowsHide: true }); + } + // TODO: needs better returns, strings are lazy programming + return `Successfully installed ${pyPackage}`; +} diff --git a/src/util/tools.ts b/src/util/tools.ts index 8398f118..766f5c18 100644 --- a/src/util/tools.ts +++ b/src/util/tools.ts @@ -141,6 +141,7 @@ export async function promptForMissingTool( * A wrapper around a call to `pip` for installing external tools. * Does not explicitly check if `pip` is installed. * + * @deprecated Use `installPythonPackage` from python-installer.ts for better virtual environment support * @param pyPackage name of python package in PyPi */ export async function pipInstall(pyPackage: string): Promise {