mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(ghidra): preserve stable JDK registration paths
This commit is contained in:
@@ -1027,7 +1027,11 @@ cached across launches. `JAVA_TOOL_OPTIONS`
|
||||
and `_JAVA_OPTIONS` do not expand `@file` tokens, so they remain
|
||||
literal-token checks.
|
||||
When Java is found through `PATH`, setup records its observed JDK home so GUI
|
||||
MCP clients do not depend on an incidental shell path. Setup shows every exact
|
||||
MCP clients do not depend on an incidental shell path. A caller-selected
|
||||
`JAVA_HOME` spelling is preserved when it resolves to that observed JDK. On
|
||||
macOS, a discovered Homebrew Cellar home uses the formula's stable `opt` alias
|
||||
only when both resolve to the same JDK. The Java check retains the JVM-reported
|
||||
home as evidence. Setup shows every exact
|
||||
environment entry in its plan, writes only after approval, and never downloads,
|
||||
installs, upgrades, or modifies Ghidra or Java.
|
||||
|
||||
|
||||
@@ -9,7 +9,15 @@ import {
|
||||
parseGhidraJavaOutput,
|
||||
type GhidraJavaObservation,
|
||||
} from "./GhidraJavaProbe.js";
|
||||
import { chmod, mkdir, rm, symlink, unlink, writeFile } from "node:fs/promises";
|
||||
import {
|
||||
chmod,
|
||||
mkdir,
|
||||
realpath,
|
||||
rm,
|
||||
symlink,
|
||||
unlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -153,6 +161,87 @@ it.skipIf(process.platform === "win32")(
|
||||
},
|
||||
);
|
||||
|
||||
it
|
||||
.skipIf(process.platform === "win32")
|
||||
.each([
|
||||
"selected alias",
|
||||
"selected foreign alias",
|
||||
"selected missing alias",
|
||||
"discovered matching opt",
|
||||
"discovered foreign opt",
|
||||
"discovered missing opt",
|
||||
] as const)("keeps JDK registration identity for %s", async (scenario) => {
|
||||
const temporaryRoot = await createTestTempDirectory("rea-java-home-");
|
||||
const prefix = await realpath(temporaryRoot);
|
||||
const suffix = "libexec/openjdk.jdk/Contents/Home";
|
||||
const firstKeg = join(prefix, "Cellar", "openjdk@21", "21.0.12");
|
||||
const secondKeg = join(prefix, "Cellar", "openjdk@21", "21.0.13");
|
||||
const observedHome = join(firstKeg, suffix);
|
||||
const optAlias = join(prefix, "opt", "openjdk@21");
|
||||
const selectedAlias = join(prefix, "selected-jdk");
|
||||
await mkdir(observedHome, { recursive: true });
|
||||
await mkdir(join(secondKeg, suffix), { recursive: true });
|
||||
await mkdir(join(prefix, "opt"));
|
||||
const selected = scenario.startsWith("selected");
|
||||
const missing = scenario.includes("missing");
|
||||
const foreign = scenario.includes("foreign");
|
||||
if (!missing)
|
||||
await symlink(
|
||||
selected
|
||||
? foreign
|
||||
? join(secondKeg, suffix)
|
||||
: observedHome
|
||||
: foreign
|
||||
? secondKeg
|
||||
: firstKeg,
|
||||
selected ? selectedAlias : optAlias,
|
||||
);
|
||||
const inspection = await inspectGhidraInstallation(
|
||||
{
|
||||
resourceOwner: inspectionResources.owner,
|
||||
environment: {},
|
||||
installDir: INSTALL,
|
||||
platform: process.platform,
|
||||
architecture: "x64",
|
||||
...(selected ? { javaHome: selectedAlias } : {}),
|
||||
},
|
||||
host({
|
||||
probeJava: () =>
|
||||
javaProbeOk({
|
||||
...JAVA,
|
||||
home: observedHome,
|
||||
osName: process.platform === "darwin" ? "Mac OS X" : "Linux",
|
||||
}),
|
||||
executable: (path) =>
|
||||
path === HEADLESS ||
|
||||
path === LINUX_DECOMPILER ||
|
||||
path ===
|
||||
`${INSTALL}/Ghidra/Features/Decompiler/os/mac_x86_64/decompile`,
|
||||
}),
|
||||
);
|
||||
expect(inspection.status).toBe("available");
|
||||
const stable =
|
||||
!missing && !foreign && (selected || process.platform === "darwin");
|
||||
const expected = stable
|
||||
? selected
|
||||
? selectedAlias
|
||||
: join(optAlias, suffix)
|
||||
: observedHome;
|
||||
expect(inspection.javaHome).toBe(expected);
|
||||
expect(
|
||||
projectGhidraDoctorInspection(inspection).registrationEnvironment.JAVA_HOME,
|
||||
).toBe(expected);
|
||||
expect(
|
||||
inspection.checks.find(({ name }) => name === "java")?.detail,
|
||||
).toContain(observedHome);
|
||||
if (stable && !selected) {
|
||||
await unlink(optAlias);
|
||||
await symlink(secondKeg, optAlias);
|
||||
await rm(firstKeg, { recursive: true });
|
||||
expect(await realpath(expected)).toBe(join(secondKeg, suffix));
|
||||
}
|
||||
});
|
||||
|
||||
it.skipIf(process.platform !== "darwin")(
|
||||
"uses a same-installation Homebrew opt alias for doctor registration and survives keg rotation",
|
||||
async () => {
|
||||
|
||||
@@ -391,15 +391,16 @@ export const inspectGhidraInstallation = async (
|
||||
throw new AnalysisCancelledError("ghidra:installation", {
|
||||
cause: options.signal.reason,
|
||||
diagnostics: ghidraInstallationDiagnostics(
|
||||
availableInstallation(coordinates, observation),
|
||||
availableInstallation(coordinates, observation, options.javaHome),
|
||||
),
|
||||
});
|
||||
return availableInstallation(coordinates, observation);
|
||||
return availableInstallation(coordinates, observation, options.javaHome);
|
||||
};
|
||||
|
||||
const availableInstallation = (
|
||||
coordinates: GhidraInstallationCoordinates,
|
||||
observation: GhidraInstallationObservation,
|
||||
selectedJavaHome: string | undefined,
|
||||
): AvailableGhidraInstallation => {
|
||||
const providerVersion = coordinates.properties?.providerVersion;
|
||||
if (
|
||||
@@ -420,7 +421,11 @@ const availableInstallation = (
|
||||
analyzeHeadlessPath: coordinates.analyzeHeadlessPath,
|
||||
applicationPropertiesPath: coordinates.applicationPropertiesPath,
|
||||
providerVersion,
|
||||
javaHome: coordinates.java.home,
|
||||
javaHome: stableJavaHome(
|
||||
coordinates.java.home,
|
||||
selectedJavaHome,
|
||||
observation.platform,
|
||||
),
|
||||
javaVersion: coordinates.java.version,
|
||||
};
|
||||
};
|
||||
@@ -1081,6 +1086,52 @@ const stableGhidraInstallationRoot = (
|
||||
}
|
||||
};
|
||||
|
||||
/** Preserve a proven caller alias, or an active Homebrew keg alias. */
|
||||
const stableJavaHome = (
|
||||
observedHome: string,
|
||||
selectedHome: string | undefined,
|
||||
platform: NodeJS.Platform,
|
||||
): string => {
|
||||
// Compare paths only with the matching host filesystem semantics.
|
||||
if (platform !== process.platform) return observedHome;
|
||||
const sameHome = (candidate: string): boolean => {
|
||||
try {
|
||||
return realpathSync(candidate) === realpathSync(observedHome);
|
||||
} catch (cause: unknown) {
|
||||
// Missing or unresolved aliases do not prove identity.
|
||||
void cause;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if (selectedHome !== undefined)
|
||||
return sameHome(selectedHome) ? selectedHome : observedHome;
|
||||
if (platform !== "darwin") return observedHome;
|
||||
const parsed = posix.parse(observedHome);
|
||||
const segments = observedHome.slice(parsed.root.length).split(posix.sep);
|
||||
const cellarIndex = segments.length - 7;
|
||||
const formula = segments[cellarIndex + 1];
|
||||
const version = segments[cellarIndex + 2];
|
||||
if (
|
||||
cellarIndex < 0 ||
|
||||
segments[cellarIndex] !== "Cellar" ||
|
||||
formula === undefined ||
|
||||
formula.length === 0 ||
|
||||
version === undefined ||
|
||||
version.length === 0 ||
|
||||
segments.slice(cellarIndex + 3).join("/") !==
|
||||
"libexec/openjdk.jdk/Contents/Home"
|
||||
)
|
||||
return observedHome;
|
||||
const prefix = posix.join(parsed.root, ...segments.slice(0, cellarIndex));
|
||||
const candidate = posix.join(
|
||||
prefix,
|
||||
"opt",
|
||||
formula,
|
||||
"libexec/openjdk.jdk/Contents/Home",
|
||||
);
|
||||
return sameHome(candidate) ? candidate : observedHome;
|
||||
};
|
||||
|
||||
const executablePath = (path: string): boolean => {
|
||||
try {
|
||||
accessSync(path, constants.X_OK);
|
||||
|
||||
Reference in New Issue
Block a user