diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b777f514..c88094bac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,8 @@ jobs: steps: - uses: actions/checkout@v2 - uses: coursier/setup-action@v1 + with: + jvm: temurin:17 - uses: actions/setup-node@v2 with: node-version: "16" diff --git a/packages/metals-languageclient/src/__tests__/getJavaHome.test.ts b/packages/metals-languageclient/src/__tests__/getJavaHome.test.ts index 4e3b5e6d6..d3d35930b 100644 --- a/packages/metals-languageclient/src/__tests__/getJavaHome.test.ts +++ b/packages/metals-languageclient/src/__tests__/getJavaHome.test.ts @@ -1,4 +1,14 @@ import path from "path"; +import { OutputChannel } from "../interfaces/OutputChannel"; + +class MockOutput implements OutputChannel { + append(value: string): void { + console.log(value); + } + appendLine(value: string): void { + console.log(value); + } +} const exampleJavaVersionString = `openjdk "17.0.1" 2021-10-19 OpenJDK Runtime Environment (build 17.0.1+12-39) @@ -26,6 +36,28 @@ describe("getJavaHome", () => { const javaHome = await require("../getJavaHome").getJavaHome("17"); expect(javaHome).toBe(JAVA_HOME); }); + + // needs to run on a machine with an actual JAVA_HOME set up + it("reads from real JAVA_HOME", async () => { + process.env = { ...originalEnv }; + delete process.env.PATH; + const javaHome = await require("../getJavaHome").getJavaHome( + "17", + new MockOutput() + ); + expect(javaHome).toBeDefined(); + }); + + // needs to run on a machine with an actual java on PATH set up + it("reads from real PATH", async () => { + process.env = { ...originalEnv }; + delete process.env.JAVA_HOME; + const javaHome = await require("../getJavaHome").getJavaHome( + "17", + new MockOutput() + ); + expect(javaHome).toBeDefined(); + }); }); function mockExistsFs(javaLinks: { binPath: String }[]): void { diff --git a/packages/metals-languageclient/src/__tests__/setupCoursier.test.ts b/packages/metals-languageclient/src/__tests__/setupCoursier.test.ts index 860a2bf0e..36879640e 100644 --- a/packages/metals-languageclient/src/__tests__/setupCoursier.test.ts +++ b/packages/metals-languageclient/src/__tests__/setupCoursier.test.ts @@ -33,7 +33,8 @@ describe("setupCoursier", () => { }); it("should not find coursier if not present in PATH", async () => { - expect(await validateCoursier("path/fake")).toBeUndefined(); + process.env = {}; + expect(await validateCoursier()).toBeUndefined(); }); it("should fetch coursier correctly", async () => { diff --git a/packages/metals-languageclient/src/getJavaHome.ts b/packages/metals-languageclient/src/getJavaHome.ts index 3805ffda1..1513e7378 100644 --- a/packages/metals-languageclient/src/getJavaHome.ts +++ b/packages/metals-languageclient/src/getJavaHome.ts @@ -1,6 +1,8 @@ import path from "path"; import { spawn } from "promisify-child-process"; import { OutputChannel } from "./interfaces/OutputChannel"; +import { findOnPath } from "./util"; +import { realpathSync } from "fs"; export type JavaVersion = "11" | "17" | "21"; @@ -19,7 +21,7 @@ export async function getJavaHome( outputChannel: OutputChannel ): Promise { const fromEnvValue = await fromEnv(javaVersion, outputChannel); - return fromEnvValue; + return fromEnvValue || (await fromPath(javaVersion, outputChannel)); } const versionRegex = /\"\d\d/; @@ -52,6 +54,26 @@ async function validateJavaVersion( return false; } +export async function fromPath( + javaVersion: JavaVersion, + outputChannel: OutputChannel +): Promise { + let javaExecutable = findOnPath(["java"]); + if (javaExecutable) { + let realJavaPath = realpathSync(javaExecutable); + outputChannel.appendLine( + `Found java executable under ${javaExecutable} that resolves to ${realJavaPath}` + ); + const possibleJavaHome = path.dirname(path.dirname(realJavaPath)); + const isValid = await validateJavaVersion( + possibleJavaHome, + javaVersion, + outputChannel + ); + if (isValid) return possibleJavaHome; + } +} + export async function fromEnv( javaVersion: JavaVersion, outputChannel: OutputChannel diff --git a/packages/metals-languageclient/src/setupCoursier.ts b/packages/metals-languageclient/src/setupCoursier.ts index 858d42f15..8fa054d95 100644 --- a/packages/metals-languageclient/src/setupCoursier.ts +++ b/packages/metals-languageclient/src/setupCoursier.ts @@ -8,6 +8,7 @@ import { JavaVersion, getJavaHome } from "./getJavaHome"; import { OutputChannel } from "./interfaces/OutputChannel"; import path from "path"; import fs from "fs"; +import { findOnPath } from "./util"; const coursierVersion = "v2.1.8"; // https://github.com/coursier/launchers contains only launchers with the most recent version @@ -25,13 +26,11 @@ export async function setupCoursier( }; const resolveCoursier = async () => { - const envPath = process.env["PATH"]; const isWindows = process.platform === "win32"; const defaultCoursier = isWindows ? path.resolve(coursierFetchPath, "cs.exe") : path.resolve(coursierFetchPath, "cs"); const possibleCoursier: string | undefined = await validateCoursier( - envPath, defaultCoursier ); @@ -86,10 +85,8 @@ export async function setupCoursier( } export async function validateCoursier( - pathEnv?: string | undefined, defaultCoursier?: string | undefined ): Promise { - const isWindows = process.platform === "win32"; const validate = async (coursier: string) => { try { const coursierVersion = await spawn(coursier, ["version"]); @@ -111,33 +108,11 @@ export async function validateCoursier( } }; - if (pathEnv) { - 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")) - ); - - return ( - (possibleCoursier && (await validate(possibleCoursier))) || - (await validateDefault()) - ); - } - - return validateDefault(); + const possibleCoursier = findOnPath(["cs", "coursier"]); + return ( + (possibleCoursier && (await validate(possibleCoursier))) || + (await validateDefault()) + ); } export async function fetchCoursier( diff --git a/packages/metals-languageclient/src/util.ts b/packages/metals-languageclient/src/util.ts index d940af288..ff094b27f 100644 --- a/packages/metals-languageclient/src/util.ts +++ b/packages/metals-languageclient/src/util.ts @@ -1,6 +1,8 @@ import { TaskEither } from "fp-ts/lib/TaskEither"; import { pipe } from "fp-ts/lib/function"; import * as E from "fp-ts/lib/Either"; +import path from "path"; +import fs from "fs"; export function toPromise(te: TaskEither): Promise { return te().then( @@ -8,3 +10,34 @@ export function toPromise(te: TaskEither): Promise { new Promise((resolve, reject) => pipe(res, E.fold(reject, resolve))) ); } + +export function findOnPath(names: string[]) { + const envPath = process.env["PATH"] || process.env["Path"]; + const isWindows = process.platform === "win32"; + + if (envPath) { + const possibleExecutable = envPath + .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) => { + if (isWindows) { + return names.some( + (name) => + p.endsWith(path.sep + name + ".bat") || + p.endsWith(path.sep + name + ".exe") + ); + } else { + return names.some((name) => p.endsWith(path.sep + name)); + } + }); + return possibleExecutable; + } +}