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>
This commit is contained in:
VADLACHARYandmorluto authored and GitHub committed 2026-10-17 06:41:38 +08:00
1 parent 360626434d
commit 8267edc0ef
2 files changed
+109 -10

No files matched your search

+77
View File
@@ -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);
},
);
+32 -10
View File
@@ -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)