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..aa67b071e 100644 --- a/packages/metals-vscode/src/extension.ts +++ b/packages/metals-vscode/src/extension.ts @@ -183,62 +183,69 @@ 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:" + return fetchProcess + .then((childProcess) => { + return trackDownloadProgress(title, outputChannel, childProcess.promise); + }) + .then( + (classpath) => { + return launchMetals( + outputChannel, + context, + classpath, + serverProperties, + javaConfig, + serverVersion ); - 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.` - ); - } 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(