diff --git a/src/ghidra/GhidraInstallation.test.ts b/src/ghidra/GhidraInstallation.test.ts index fffa5d6e56..2dcec5b2b3 100644 --- a/src/ghidra/GhidraInstallation.test.ts +++ b/src/ghidra/GhidraInstallation.test.ts @@ -1410,3 +1410,80 @@ it("explains the macOS keg-only JDK case without probing an unselected installat "/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home", ); }); + +it.each([ + ["arm64", "/opt/homebrew"], + ["x64", "/usr/local"], +] as const)("discovers keg-only Java for macOS %s", (architecture, prefix) => { + const probed: string[] = []; + const checked: string[] = []; + const inspection = inspectGhidraInstallation( + { + environment: { PATH: ":relative-bin:/empty-bin" }, + installDir: INSTALL, + platform: "darwin", + architecture, + }, + host({ + executable: (candidate) => { + checked.push(candidate); + return ( + candidate.includes("/opt/openjdk/") || host().executable(candidate) + ); + }, + probeJava: (command) => { + probed.push(command); + return javaProbeOk({ + ...JAVA, + home: `${prefix}/opt/openjdk/libexec/openjdk.jdk/Contents/Home`, + osArch: architecture === "arm64" ? "aarch64" : "x86_64", + osName: "Mac OS X", + }); + }, + }), + ); + expect(probed).toEqual([ + `${prefix}/opt/openjdk/libexec/openjdk.jdk/Contents/Home/bin/java`, + ]); + expect(checked).not.toContain("java"); + expect(checked).not.toContain("relative-bin/java"); + expect(inspection.checks.find(({ name }) => name === "java")).toMatchObject({ + status: "passed", + }); +}); + +it.each([ + ["darwin", undefined, "/selected-bin/java", "java"], + ["darwin", "/selected-jdk", undefined, "/selected-jdk/bin/java"], + ["linux", undefined, undefined, "java"], + ["win32", undefined, undefined, "java.exe"], +] as const)( + "preserves selected Java on %s with JAVA_HOME %s", + (platform, javaHome, pathJava, expected) => { + const checked: string[] = []; + const probed: string[] = []; + inspectGhidraInstallation( + { + environment: { PATH: "/selected-bin" }, + installDir: INSTALL, + platform, + architecture: "arm64", + ...(javaHome === undefined ? {} : { javaHome }), + }, + host({ + executable: (candidate) => { + checked.push(candidate); + return candidate === pathJava || host().executable(candidate); + }, + probeJava: (command) => { + probed.push(command); + return missingJavaProbe; + }, + }), + ); + expect(probed).toEqual([expected]); + expect( + checked.some((candidate) => candidate.includes("/opt/openjdk/")), + ).toBe(false); + }, +); diff --git a/src/ghidra/GhidraInstallation.ts b/src/ghidra/GhidraInstallation.ts index 4437dbe2a1..d9a3b3b059 100644 --- a/src/ghidra/GhidraInstallation.ts +++ b/src/ghidra/GhidraInstallation.ts @@ -446,16 +446,38 @@ const installationCoordinates = ( applicationPropertiesPath === null ? undefined : host.readText(applicationPropertiesPath); - const javaCommand = - options.javaHome === undefined - ? platform === "win32" - ? "java.exe" - : "java" - : path.join( - options.javaHome, - "bin", - platform === "win32" ? "java.exe" : "java", - ); + const javaCommand = (() => { + if (options.javaHome !== undefined) + return path.join( + options.javaHome, + "bin", + platform === "win32" ? "java.exe" : "java", + ); + if (platform !== "darwin") + return platform === "win32" ? "java.exe" : "java"; + // Check the caller's PATH, never a file named java in the working directory. + const javaOnPath = options.environment.PATH?.split(":").some( + (directory) => + path.isAbsolute(directory) && + host.executable(path.join(directory, "java")), + ); + if (javaOnPath) return "java"; + // Prefer the Homebrew prefix matching the process architecture. + const prefixes = + architecture === "arm64" + ? ["/opt/homebrew", "/usr/local"] + : ["/usr/local", "/opt/homebrew"]; + return ( + prefixes + .map((prefix) => + path.join( + prefix, + "opt/openjdk/libexec/openjdk.jdk/Contents/Home/bin/java", + ), + ) + .find((candidate) => host.executable(candidate)) ?? "java" + ); + })(); // Relative JAVA_HOME is refused by configuration validation without execution. const javaProbe = options.javaHome === undefined || path.isAbsolute(options.javaHome)