fix(ghidra): try keg JDKs after failed PATH launchers

This commit is contained in:
morluto
2026-10-24 02:32:51 +08:00
parent dfafc04f6d
commit 6ff682505c
3 changed files with 163 additions and 18 deletions
+4 -1
View File
@@ -996,7 +996,10 @@ selected formula's `libexec/openjdk.jdk/Contents/Home`, for example
`/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home` or
`/usr/local/opt/openjdk/libexec/openjdk.jdk/Contents/Home`. Use a JDK inside the
Java version bounds declared by your Ghidra installation; REA does not select
another installed JDK automatically. Doctor distinguishes an absent Java
another JDK when `JAVA_HOME` is explicit or the PATH JVM runs. On macOS,
when the PATH launcher cannot start a JVM, it tries executable Homebrew
`opt/openjdk` candidates, preferring the host architecture's prefix. Failed
discovery probes remain available in installation diagnostics. Doctor distinguishes an absent Java
command from a command that runs and fails, and retains bounded probe output
and any parsed properties when the selected runtime cannot be admitted.
+63
View File
@@ -1774,6 +1774,69 @@ it.each([
},
);
it.each([
"PATH succeeds",
"keg succeeds",
"both fail",
"explicit home",
] as const)(
"discovers a keg-only JDK after a failed PATH launcher: %s",
async (scenario) => {
const commands: string[] = [];
const keg =
"/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home/bin/java";
const selectedHome =
scenario === "explicit home" ? "/selected-jdk" : undefined;
const inspection = await inspectGhidraInstallation(
{
resourceOwner: inspectionResources.owner,
environment: { PATH: "/usr/bin" },
installDir: INSTALL,
platform: "darwin",
architecture: "arm64",
...(selectedHome === undefined ? {} : { javaHome: selectedHome }),
},
host({
executable: (candidate) =>
candidate === "/usr/bin/java" ||
candidate === keg ||
host().executable(candidate),
probeJava: (command) => {
commands.push(command);
return scenario === "PATH succeeds" ||
(scenario === "keg succeeds" && command === keg)
? javaProbeOk({ ...JAVA, osArch: "aarch64", osName: "Mac OS X" })
: missingJavaProbe;
},
}),
);
expect(commands).toEqual(
scenario === "explicit home"
? ["/selected-jdk/bin/java"]
: scenario === "PATH succeeds"
? ["java"]
: ["java", keg],
);
expect(inspection.status).toBe(
scenario === "PATH succeeds" || scenario === "keg succeeds"
? "available"
: "unavailable",
);
if (scenario === "keg succeeds" || scenario === "both fail") {
expect(ghidraInstallationDiagnostics(inspection)).toMatchObject({
java_discovery_failures: expect.arrayContaining([
{
command: "java",
code: "runtime_missing",
detail: expect.any(String),
},
]),
});
}
if (scenario === "both fail") expect(inspection.javaCommand).toBe("java");
},
);
it.each([
["darwin", undefined, "/selected-bin/java", "java"],
["darwin", "/selected-jdk", undefined, "/selected-jdk/bin/java"],
+96 -17
View File
@@ -201,6 +201,10 @@ interface GhidraInstallationObservation {
readonly configurationDirectoryName: string | null;
readonly nativeDecompilerPath: string | null;
readonly nativeDecompilerHeaderFailure?: AnalysisError;
readonly javaDiscoveryFailures?: readonly {
readonly command: string;
readonly failure: GhidraJavaProbeFailure;
}[];
readonly javaCommand: string;
readonly javaProbeFailure?: GhidraJavaProbeFailure;
/** Observed java.vm.name; omitted when the probe cannot establish VM identity. */
@@ -305,6 +309,10 @@ interface GhidraInstallationCoordinates {
AnalysisError
>;
readonly properties: GhidraApplicationProperties | undefined;
readonly javaDiscoveryFailures?: readonly {
readonly command: string;
readonly failure: GhidraJavaProbeFailure;
}[];
readonly javaCommand: string;
readonly java: GhidraJavaObservation | undefined;
readonly javaProbeFailure: GhidraJavaProbeFailure | undefined;
@@ -361,6 +369,9 @@ export const inspectGhidraInstallation = async (
nativeDecompilerHeaderFailure:
coordinates.nativeDecompilerHeader.error,
}),
...(coordinates.javaDiscoveryFailures === undefined
? {}
: { javaDiscoveryFailures: coordinates.javaDiscoveryFailures }),
javaCommand: coordinates.javaCommand,
...(coordinates.javaProbeFailure === undefined
? {}
@@ -502,7 +513,19 @@ const installationCoordinates = async (
nativeDecompilerHeader.error.cleanupIncomplete)
)
throw nativeDecompilerHeader.error;
const javaCommand = (() => {
const homebrewJavaCandidates = () =>
(architecture === "arm64"
? ["/opt/homebrew", "/usr/local"]
: ["/usr/local", "/opt/homebrew"]
)
.map((prefix) =>
path.join(
prefix,
"opt/openjdk/libexec/openjdk.jdk/Contents/Home/bin/java",
),
)
.filter((candidate) => host.executable(candidate));
let javaCommand = (() => {
if (options.javaHome !== undefined)
return path.join(
options.javaHome,
@@ -518,26 +541,12 @@ const installationCoordinates = async (
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"
);
return homebrewJavaCandidates()[0] ?? "java";
})();
// Java is a prerequisite of a selected installation. Without one, or with a
// relative JAVA_HOME, configuration validation refuses Ghidra before Java can
// matter, so callers such as setup and doctor must not execute a host JVM.
const javaProbe =
let javaProbe =
installDir !== null &&
(options.javaHome === undefined || path.isAbsolute(options.javaHome))
? await host.probeJava(
@@ -559,6 +568,55 @@ const installationCoordinates = async (
javaProbe.error instanceof AnalysisError
)
throw javaProbe.error;
const javaDiscoveryFailures: {
command: string;
failure: GhidraJavaProbeFailure;
}[] = [];
if (
platform === "darwin" &&
options.javaHome === undefined &&
javaCommand === "java" &&
javaProbe !== undefined &&
!javaProbe.ok &&
!(javaProbe.error instanceof AnalysisError) &&
javaProbe.error.code !== "version_unresolved" &&
javaProbe.error.cancellation === undefined &&
!options.signal?.aborted
) {
javaDiscoveryFailures.push({
command: javaCommand,
failure: javaProbe.error,
});
for (const candidate of homebrewJavaCandidates()) {
const fallback = await host.probeJava(
candidate,
ghidraJavaEnvironment(undefined, options.environment, platform),
{
resourceOwner: options.resourceOwner,
...(options.signal === undefined ? {} : { signal: options.signal }),
},
);
if (!fallback.ok && fallback.error instanceof AnalysisError)
throw fallback.error;
if (fallback.ok) {
javaCommand = candidate;
javaProbe = fallback;
break;
}
javaDiscoveryFailures.push({
command: candidate,
failure: fallback.error,
});
if (
fallback.error.cancellation !== undefined ||
options.signal?.aborted
) {
javaCommand = candidate;
javaProbe = fallback;
break;
}
}
}
return {
installDir,
applicationPropertiesPath,
@@ -569,6 +627,7 @@ const installationCoordinates = async (
properties === undefined
? undefined
: ghidraApplicationProperties(properties),
...(javaDiscoveryFailures.length === 0 ? {} : { javaDiscoveryFailures }),
javaCommand,
java: javaProbe?.ok === true ? javaProbe.value : undefined,
javaProbeFailure:
@@ -709,6 +768,26 @@ export const ghidraInstallationDiagnostics = (
: { output: { ...inspection.javaProbeFailure.output } }),
},
}),
...(inspection.javaDiscoveryFailures === undefined
? {}
: {
java_discovery_failures: inspection.javaDiscoveryFailures.map(
({ command, failure }) => ({
command,
code: failure.code,
detail: failure.detail,
...(failure.output === undefined
? {}
: { output: { ...failure.output } }),
...(failure.properties === undefined
? {}
: { properties: { ...failure.properties } }),
...(failure.cancellation === undefined
? {}
: { cancellation_observed: true }),
}),
),
}),
java_home: inspection.javaHome,
java_version: inspection.javaVersion,
java_vm_name: inspection.javaVmName ?? null,