mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(doctor): retain host uncertainty and resolved Java selection
This commit is contained in:
1 parent
82fe820060
commit
37a7c2d652
5 files changed
+105
-43
No files matched your search
@@ -495,6 +495,37 @@ describe("doctor runtime check", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([undefined, "", "not-a-version", "14garbage"])(
|
||||
"retains unknown host support for macOS probe value %s",
|
||||
async (version) => {
|
||||
const result = await runDoctor(
|
||||
undefined,
|
||||
host({ macosVersion: () => Promise.resolve(version) }),
|
||||
);
|
||||
expect(result.checks).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "host",
|
||||
ok: false,
|
||||
classification: "unknown",
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform !== "darwin")(
|
||||
"reads the macOS version without PATH utilities",
|
||||
async () => {
|
||||
const system = systemDoctorHost({
|
||||
environment: { PATH: "/nonexistent" },
|
||||
});
|
||||
try {
|
||||
expect(await system.macosVersion()).toMatch(/^\d+(?:\.\d+)*$/u);
|
||||
} finally {
|
||||
await system.close?.();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
describe("doctor", () => {
|
||||
it("returns exact recovery for every failed diagnostic", async () => {
|
||||
const result = await runDoctor(
|
||||
@@ -518,22 +549,19 @@ describe("doctor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["11.7", undefined])(
|
||||
"rejects unsupported macOS version %s",
|
||||
async (version) => {
|
||||
const result = await runDoctor(
|
||||
undefined,
|
||||
host({ macosVersion: () => Promise.resolve(version) }),
|
||||
);
|
||||
expect(result.checks).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "host",
|
||||
ok: false,
|
||||
classification: "unsupported_host",
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
it.each(["11.7"])("rejects unsupported macOS version %s", async (version) => {
|
||||
const result = await runDoctor(
|
||||
undefined,
|
||||
host({ macosVersion: () => Promise.resolve(version) }),
|
||||
);
|
||||
expect(result.checks).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "host",
|
||||
ok: false,
|
||||
classification: "unsupported_host",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("admits only the experimental Windows x64 host boundary", async () => {
|
||||
const windows = await runDoctor(
|
||||
|
||||
@@ -764,7 +764,9 @@ const readMacosVersion = async (
|
||||
options: { readonly env: NodeJS.ProcessEnv },
|
||||
): Promise<string | undefined> => {
|
||||
try {
|
||||
return (await run("sw_vers", ["-productVersion"], options)).stdout.trim();
|
||||
return (
|
||||
await run("/usr/bin/sw_vers", ["-productVersion"], options)
|
||||
).stdout.trim();
|
||||
} catch (cause: unknown) {
|
||||
// best-effort cleanup: optional OS version probing; failure means unknown.
|
||||
void cause;
|
||||
|
||||
@@ -129,8 +129,25 @@ const hostCheck = async (host: DoctorHost): Promise<DoctorCheck> => {
|
||||
host.platform === "darwin" ? await host.macosVersion() : undefined;
|
||||
const linuxDistribution =
|
||||
host.platform === "linux" ? await host.linuxDistribution() : undefined;
|
||||
const macosMajor =
|
||||
macosVersion !== undefined && /^\d+(?:\.\d+)*$/u.test(macosVersion)
|
||||
? Number(macosVersion.split(".")[0])
|
||||
: undefined;
|
||||
if (host.platform === "darwin" && macosMajor === undefined)
|
||||
return {
|
||||
name: "host",
|
||||
ok: false,
|
||||
classification: "unknown",
|
||||
detail:
|
||||
macosVersion === undefined
|
||||
? "The macOS version could not be determined."
|
||||
: `The macOS version probe returned an unrecognized value: ${JSON.stringify(macosVersion)}.`,
|
||||
remediation: "Check /usr/bin/sw_vers -productVersion and rerun doctor.",
|
||||
};
|
||||
const supported =
|
||||
(host.platform === "darwin" && parseMajor(macosVersion) >= 12) ||
|
||||
(host.platform === "darwin" &&
|
||||
macosMajor !== undefined &&
|
||||
macosMajor >= 12) ||
|
||||
(host.platform === "linux" && supportsReaHost(linuxDistribution)) ||
|
||||
(host.platform === "win32" && host.architecture === "x64");
|
||||
const detail =
|
||||
@@ -359,8 +376,5 @@ const unsupportedHopperRemediation = (configured: boolean): string =>
|
||||
? "Check HOPPER_LAUNCHER_PATH and its publisher verification evidence. Run rea setup to install and retain an authenticated official Hopper package for offline use."
|
||||
: "Run rea setup to install and retain an authenticated official Hopper package for offline use.";
|
||||
|
||||
const parseMajor = (version: string | undefined): number =>
|
||||
Number.parseInt(version?.split(".")[0] ?? "0", 10);
|
||||
|
||||
const providerCheckName = (providerId: string, name: string): boolean =>
|
||||
name === providerId || name.startsWith(`${providerId}-`);
|
||||
@@ -1714,7 +1714,7 @@ it("explains the macOS keg-only JDK case without probing an unselected installat
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(probed).toEqual(["java"]);
|
||||
expect(probed).toEqual(["/usr/bin/java"]);
|
||||
expect(inspection.checks.find(({ name }) => name === "java")).toMatchObject({
|
||||
status: "failed",
|
||||
code: "runtime_missing",
|
||||
@@ -1813,8 +1813,8 @@ it.each([
|
||||
scenario === "explicit home"
|
||||
? ["/selected-jdk/bin/java"]
|
||||
: scenario === "PATH succeeds"
|
||||
? ["java"]
|
||||
: ["java", keg],
|
||||
? ["/usr/bin/java"]
|
||||
: ["/usr/bin/java", keg],
|
||||
);
|
||||
expect(inspection.status).toBe(
|
||||
scenario === "PATH succeeds" || scenario === "keg succeeds"
|
||||
@@ -1825,19 +1825,21 @@ it.each([
|
||||
expect(ghidraInstallationDiagnostics(inspection)).toMatchObject({
|
||||
java_discovery_failures: expect.arrayContaining([
|
||||
{
|
||||
command: "java",
|
||||
command: "/usr/bin/java",
|
||||
code: "runtime_missing",
|
||||
detail: expect.any(String),
|
||||
},
|
||||
]),
|
||||
});
|
||||
}
|
||||
if (scenario === "both fail") expect(inspection.javaCommand).toBe("java");
|
||||
if (scenario === "both fail")
|
||||
expect(inspection.javaCommand).toBe("/usr/bin/java");
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["darwin", undefined, "/selected-bin/java", "java"],
|
||||
["darwin", undefined, "/selected-bin/java", "/selected-bin/java"],
|
||||
["darwin", undefined, "/linked/../jdk/java", "/linked/../jdk/java"],
|
||||
["darwin", "/selected-jdk", undefined, "/selected-jdk/bin/java"],
|
||||
["linux", undefined, undefined, "java"],
|
||||
["win32", undefined, undefined, "java.exe"],
|
||||
@@ -1849,7 +1851,9 @@ it.each([
|
||||
const inspection = await inspectGhidraInstallation(
|
||||
{
|
||||
resourceOwner: inspectionResources.owner,
|
||||
environment: { PATH: "/selected-bin" },
|
||||
environment: {
|
||||
PATH: `:relative-bin:${pathJava === undefined ? "/selected-bin" : pathJava.slice(0, -"/java".length)}`,
|
||||
},
|
||||
installDir: INSTALL,
|
||||
platform,
|
||||
architecture: "arm64",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ok, type Result } from "../domain/result.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { readGhidraExecutableHeader } from "./GhidraExecutableHeader.js";
|
||||
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import {
|
||||
readGhidraInstallationProperties,
|
||||
type GhidraInstallationFileOptions,
|
||||
@@ -524,24 +525,37 @@ const installationCoordinates = async (
|
||||
),
|
||||
)
|
||||
.filter((candidate) => host.executable(candidate));
|
||||
let javaCommand = (() => {
|
||||
const selectedJava: {
|
||||
command: string;
|
||||
source: "configured" | "path" | "homebrew" | "default";
|
||||
} = (() => {
|
||||
if (options.javaHome !== undefined)
|
||||
return path.join(
|
||||
options.javaHome,
|
||||
"bin",
|
||||
platform === "win32" ? "java.exe" : "java",
|
||||
);
|
||||
return {
|
||||
source: "configured",
|
||||
command: path.join(
|
||||
options.javaHome,
|
||||
"bin",
|
||||
platform === "win32" ? "java.exe" : "java",
|
||||
),
|
||||
};
|
||||
if (platform !== "darwin")
|
||||
return platform === "win32" ? "java.exe" : "java";
|
||||
return {
|
||||
source: "default",
|
||||
command: 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";
|
||||
return homebrewJavaCandidates()[0] ?? "java";
|
||||
const javaOnPath = options.environment.PATH?.split(":")
|
||||
.filter((directory) => path.isAbsolute(directory))
|
||||
.map((directory) => absoluteCallerPath(`${directory}/java`, { platform }))
|
||||
.find((candidate) => host.executable(candidate));
|
||||
if (javaOnPath !== undefined)
|
||||
return { source: "path", command: javaOnPath };
|
||||
const homebrew = homebrewJavaCandidates()[0];
|
||||
return homebrew === undefined
|
||||
? { source: "default", command: "/usr/bin/java" }
|
||||
: { source: "homebrew", command: homebrew };
|
||||
})();
|
||||
let javaCommand = selectedJava.command;
|
||||
// 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.
|
||||
@@ -574,7 +588,7 @@ const installationCoordinates = async (
|
||||
if (
|
||||
platform === "darwin" &&
|
||||
options.javaHome === undefined &&
|
||||
javaCommand === "java" &&
|
||||
(selectedJava.source === "path" || selectedJava.source === "default") &&
|
||||
javaProbe !== undefined &&
|
||||
!javaProbe.ok &&
|
||||
!(javaProbe.error instanceof AnalysisError) &&
|
||||
|
||||
Reference in new issue
Block a user