From b665fdcefed5d945e38620ffcbe2a86ac47fb9cb Mon Sep 17 00:00:00 2001 From: Tomasz Godzik Date: Tue, 11 Apr 2023 18:21:08 +0200 Subject: [PATCH] feature: Run local coursier if available to avoid using bootstraped one This might help out in corporate environements since it will require installing coursier only once and Metals will be able to use it. In later steps I will try to automatically download coursier or native image via metals if not available. --- .github/workflows/ci.yml | 1 + .vscode/launch.json | 7 +- .../src/__tests__/fetchMetals.test.ts | 14 ++ .../metals-languageclient/src/fetchMetals.ts | 142 +++++++++++++----- packages/metals-vscode/src/extension.ts | 109 ++++++++------ 5 files changed, 182 insertions(+), 91 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cadc55db7..2b777f514 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,7 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@v2 + - uses: coursier/setup-action@v1 - uses: actions/setup-node@v2 with: node-version: "16" diff --git a/.vscode/launch.json b/.vscode/launch.json index a7caf6f6c..2ce88134f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,10 +7,11 @@ "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", - "args": ["--extensionDevelopmentPath=${workspaceRoot}"], - "stopOnEntry": false, + "args": [ + "--extensionDevelopmentPath=${workspaceRoot}/packages/metals-vscode" + ], "sourceMaps": true, - "outFiles": ["${workspaceRoot}/out/**/*.js"], + "outFiles": ["${workspaceRoot}/packages/metals-vscode/out/**/*.js"], "preLaunchTask": "npm: watch" } ] diff --git a/packages/metals-languageclient/src/__tests__/fetchMetals.test.ts b/packages/metals-languageclient/src/__tests__/fetchMetals.test.ts index a3f79236f..34a480b55 100644 --- a/packages/metals-languageclient/src/__tests__/fetchMetals.test.ts +++ b/packages/metals-languageclient/src/__tests__/fetchMetals.test.ts @@ -1,4 +1,5 @@ import { calcServerDependency } from "../fetchMetals"; +import { validateCoursier } from "../fetchMetals"; describe("fetchMetals", () => { describe("calcServerDependency", () => { @@ -34,4 +35,17 @@ describe("fetchMetals", () => { expect(calcServerDependency(customVersion)).toBe(customVersion); }); }); + + it("should find coursier in PATH", async () => { + const pathEnv = process.env["PATH"]; + if (pathEnv) { + expect(await validateCoursier(pathEnv)).toBeDefined(); + } else { + fail("PATH environment variable is not defined"); + } + }); + + it("should not find coursier if not present in PATH", async () => { + expect(await validateCoursier("path/fake")).toBeUndefined(); + }); }); diff --git a/packages/metals-languageclient/src/fetchMetals.ts b/packages/metals-languageclient/src/fetchMetals.ts index 1092a42c2..74cf42674 100644 --- a/packages/metals-languageclient/src/fetchMetals.ts +++ b/packages/metals-languageclient/src/fetchMetals.ts @@ -1,6 +1,9 @@ import * as semver from "semver"; +import path from "path"; +import fs from "fs"; import { ChildProcessPromise, spawn } from "promisify-child-process"; import { JavaConfig } from "./getJavaConfig"; +import { OutputChannel } from "./interfaces/OutputChannel"; interface FetchMetalsOptions { serverVersion: string; @@ -8,49 +11,112 @@ interface FetchMetalsOptions { javaConfig: JavaConfig; } -export function fetchMetals({ - serverVersion, - serverProperties, - javaConfig: { javaPath, javaOptions, extraEnv, coursierPath }, -}: FetchMetalsOptions): ChildProcessPromise { +/** + * Without the additional object, the promises will be flattened and + * we will not be able to stream the output. + */ +interface PackedChildPromise { + promise: ChildProcessPromise; +} + +export async function fetchMetals( + { + serverVersion, + serverProperties, + javaConfig: { javaPath, javaOptions, extraEnv, coursierPath }, + }: FetchMetalsOptions, + output: OutputChannel +): Promise { const fetchProperties = serverProperties.filter( (p) => !p.startsWith("-agentlib") ); - const serverDependency = calcServerDependency(serverVersion); - return spawn( - javaPath, - [ - ...javaOptions, - ...fetchProperties, - "-Dfile.encoding=UTF-8", - "-jar", - coursierPath, - "fetch", - "-p", - "--ttl", - // Use infinite ttl to avoid redunant "Checking..." logs when using SNAPSHOT - // versions. Metals SNAPSHOT releases are effectively immutable since we - // never publish the same version twice. - "Inf", - serverDependency, - "-r", - "bintray:scalacenter/releases", - "-r", - "sonatype:public", - "-r", - "sonatype:snapshots", - "-p", - ], - { - env: { - COURSIER_NO_TERM: "true", - ...extraEnv, - ...process.env, - }, - stdio: ["ignore"], // Due to Issue: #219 + + const coursierArgs = [ + "fetch", + "-p", + "--ttl", + // Use infinite ttl to avoid redunant "Checking..." logs when using SNAPSHOT + // versions. Metals SNAPSHOT releases are effectively immutable since we + // never publish the same version twice. + "Inf", + serverDependency, + "-r", + "bintray:scalacenter/releases", + "-r", + "sonatype:public", + "-r", + "sonatype:snapshots", + "-p", + ]; + + const path = process.env["PATH"]; + let possibleCoursier: string | undefined; + if (path) { + possibleCoursier = await validateCoursier(path); + } + + function spawnDefault(): ChildProcessPromise { + return spawn( + javaPath, + [ + ...javaOptions, + ...fetchProperties, + "-Dfile.encoding=UTF-8", + "-jar", + coursierPath, + ].concat(coursierArgs), + { + env: { + COURSIER_NO_TERM: "true", + ...extraEnv, + ...process.env, + }, + } + ); + } + + if (possibleCoursier) { + const coursier: string = possibleCoursier; + output.appendLine(`Using coursier located at ${coursier}`); + return { + promise: spawn(coursier, coursierArgs), + }; + } else { + return { promise: spawnDefault() }; + } +} + +export async function validateCoursier( + pathEnv: string +): Promise { + const isWindows = process.platform === "win32"; + const possibleCoursier = pathEnv + .split(path.delimiter) + .flatMap((p) => { + try { + if (fs.statSync(p).isDirectory()) { + return fs.readdirSync(p).map((sub) => path.resolve(p, sub)); + } else return [p]; + } catch (e) { + return []; + } + }) + .find( + (p) => + (!isWindows && p.endsWith(path.sep + "cs")) || + (!isWindows && p.endsWith(path.sep + "coursier")) || + (isWindows && p.endsWith(path.sep + "cs.bat")) || + (isWindows && p.endsWith(path.sep + "cs.exe")) + ); + if (possibleCoursier) { + const coursierVersion = await spawn(possibleCoursier, ["version"]); + if (coursierVersion.code !== 0) { + return undefined; + } else { + return possibleCoursier; } - ); + } } export function calcServerDependency(serverVersion: string): string { diff --git a/packages/metals-vscode/src/extension.ts b/packages/metals-vscode/src/extension.ts index 788f781c5..75ce54c0d 100644 --- a/packages/metals-vscode/src/extension.ts +++ b/packages/metals-vscode/src/extension.ts @@ -183,62 +183,71 @@ function fetchAndLaunchMetals( extensionPath: context.extensionPath, }); - const fetchProcess = fetchMetals({ - serverVersion, - serverProperties, - javaConfig, - }); + const fetchProcess = fetchMetals( + { + serverVersion, + serverProperties, + javaConfig, + }, + outputChannel + ); const title = `Downloading Metals v${serverVersion}`; - return trackDownloadProgress(title, outputChannel, fetchProcess).then( - (classpath) => { - return launchMetals( - outputChannel, - context, - classpath, - serverProperties, - javaConfig, - serverVersion - ); - }, - (reason) => { - if (reason instanceof Error) { - outputChannel.appendLine( - "Downloading Metals failed with the following:" - ); - outputChannel.appendLine(reason.message); - } - const msg = (() => { - const proxy = - `See https://scalameta.org/metals/docs/editors/vscode/#http-proxy for instructions ` + - `if you are using an HTTP proxy.`; - if (process.env.FLATPAK_SANDBOX_DIR) { - return ( - `Failed to download Metals. It seems you are running Visual Studio Code inside the ` + - `Flatpak sandbox, which is known to interfere with the download of Metals. ` + - `Please, try running Visual Studio Code without Flatpak.` + return fetchProcess + .then((childProcess) => { + return trackDownloadProgress(title, outputChannel, childProcess.promise); + }) + .then( + (classpath) => { + if (classpath) { + return launchMetals( + outputChannel, + context, + classpath, + serverProperties, + javaConfig, + serverVersion ); - } else { - return ( - `Failed to download Metals, make sure you have an internet connection, ` + - `the Metals version '${serverVersion}' is correct and the Java Home '${javaHome}' is valid. ` + - `You can configure the Metals version and Java Home in the settings.` + - proxy + } + }, + (reason) => { + if (reason instanceof Error) { + outputChannel.appendLine( + "Downloading Metals failed with the following:" ); + outputChannel.appendLine(reason.message); } - })(); - outputChannel.show(); - window - .showErrorMessage(msg, openSettingsAction, downloadJava) - .then((choice) => { - if (choice === openSettingsAction) { - commands.executeCommand(workbenchCommands.openSettings); - } else if (choice === downloadJava) { - showInstallJavaAction(outputChannel); + const msg = (() => { + const proxy = + `See https://scalameta.org/metals/docs/editors/vscode/#http-proxy for instructions ` + + `if you are using an HTTP proxy.`; + if (process.env.FLATPAK_SANDBOX_DIR) { + return ( + `Failed to download Metals. It seems you are running Visual Studio Code inside the ` + + `Flatpak sandbox, which is known to interfere with the download of Metals. ` + + `Please, try running Visual Studio Code without Flatpak.` + ); + } else { + return ( + `Failed to download Metals, make sure you have an internet connection, ` + + `the Metals version '${serverVersion}' is correct and the Java Home '${javaHome}' is valid. ` + + `You can configure the Metals version and Java Home in the settings.` + + proxy + ); } - }); - } - ); + })(); + outputChannel.show(); + window + .showErrorMessage(msg, openSettingsAction, downloadJava) + .then((choice) => { + if (choice === openSettingsAction) { + commands.executeCommand(workbenchCommands.openSettings); + } else if (choice === downloadJava) { + showInstallJavaAction(outputChannel); + } + }); + } + ); } function launchMetals(