From 8267edc0ef8c49f617e8935df475edc8e496202c Mon Sep 17 00:00:00 2001 From: VADLACHARY <76141959+vadlachary2002@users.noreply.github.com> Date: Sat, 17 Oct 2026 04:11:38 +0530 Subject: [PATCH] fix(ghidra): resolve keg-only Homebrew openjdk on macOS (#4556) (#4557) * fix(ghidra): resolve keg-only Homebrew openjdk on macOS (#4556) Fixes an issue where the `ghidra-java` check fails when OpenJDK is installed via Homebrew on macOS (where it is keg-only and not added to `PATH` or resolved by `java_home`). - **Problem:** When `options.javaHome` is undefined, the provider defaulted to a bare `"java"` command, causing valid Homebrew OpenJDK installations to be reported as missing dependencies. - **Solution:** Replaced the single fallback with an absolute candidate list (`javaCandidates`) that probes standard Homebrew OpenJDK paths (`/opt/homebrew/opt/openjdk/...` and `/usr/local/opt/openjdk/...`) before falling back to `"java"`. All candidates are absolute, preserving existing constraints and current behavior. * fix(ghidra): fix javaHome type narrowing in java candidate list (#4556) Fixes a TypeScript compilation error (`TS2322` and `TS2345`) in `src/ghidra/GhidraInstallation.ts` where `options.javaHome` was inferred as `string | undefined` inside `path.join()`. - **Problem:** The ternary operator structure prevented TypeScript from narrowing `options.javaHome` to `string` in the `path.join()` call when `options.javaHome` was defined, causing CI compile checks to fail. - **Solution:** Refactored the `javaCandidates` assignment using an immediately invoked function expression (IIFE) with an explicit `if (options.javaHome === undefined)` check. This properly narrows `options.javaHome` to `string` in the non-undefined branch while keeping the same logic and Homebrew OpenJDK paths intact. * fix(ghidra): order homebrew java candidates by host architecture (#4556) Updates the Homebrew OpenJDK candidate ordering in `src/ghidra/GhidraInstallation.ts` to be architecture-aware, addressing review feedback regarding cross-architecture runs (e.g., x64 under Rosetta on Apple Silicon). - **Problem:** Previously, `/opt/homebrew` was always checked before `/usr/local`. When running REA as an x64 process under Rosetta on Apple Silicon, this could incorrectly select an ARM64 JDK over an Intel/x64 one, triggering a `nativeDecompilerCheck` failure and missing the valid fallback. - **Solution:** Dynamically orders `javaCandidates` based on the runtime `architecture`. On `arm64`, `/opt/homebrew` is prioritized; on `x64`, `/usr/local` is prioritized. Bare `"java"` remains first in both cases to preserve standard PATH resolution. --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- src/ghidra/GhidraInstallation.test.ts | 77 +++++++++++++++++++++++++++ src/ghidra/GhidraInstallation.ts | 42 +++++++++++---- 2 files changed, 109 insertions(+), 10 deletions(-) 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)