From 14a5f871ae07a765d0a01a643ebf98428ccd6ed9 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 15 Nov 2026 21:07:43 +0800 Subject: [PATCH] fix(verify): refuse the cross-architecture Ghidra lanes before allocating when their toolchain is missing (#11117) * fix(verify): refuse the cross-architecture Ghidra lanes before allocating when their toolchain is missing * fix(verify): probe the fixture toolchain through the owned command runner and check the tools' targets --- scripts/verify-real-ghidra-dol.mjs | 3 + .../verify-real-ghidra-mips-relocatable.mjs | 3 + scripts/verify-real-ghidra-mips-shared.mjs | 3 + scripts/verify-real-ghidra-mips.mjs | 3 + scripts/verify-real-ghidra-ppc.mjs | 3 + scripts/verify-real-ghidra-riscv.mjs | 3 + tests/conformance/ghidra/dol-checks.mjs | 3 +- tests/conformance/ghidra/dol-fixture.mjs | 3 +- .../ghidra/fixture-toolchain.d.mts | 16 ++ .../conformance/ghidra/fixture-toolchain.mjs | 217 ++++++++++++++++++ .../ghidra/fixture-toolchain.test.ts | 89 +++++++ tests/conformance/ghidra/mips-checks.mjs | 5 +- tests/conformance/ghidra/mips-fixture.mjs | 3 +- .../ghidra/mips-relocatable-checks.mjs | 3 +- .../ghidra/mips16-shared-fixture.mjs | 7 +- tests/conformance/ghidra/ppc-checks.mjs | 3 +- tests/conformance/ghidra/ppc-fixture.mjs | 3 +- tests/conformance/ghidra/riscv-checks.mjs | 3 +- tests/conformance/ghidra/riscv-fixture.mjs | 7 +- 19 files changed, 365 insertions(+), 15 deletions(-) create mode 100644 tests/conformance/ghidra/fixture-toolchain.d.mts create mode 100644 tests/conformance/ghidra/fixture-toolchain.mjs create mode 100644 tests/conformance/ghidra/fixture-toolchain.test.ts diff --git a/scripts/verify-real-ghidra-dol.mjs b/scripts/verify-real-ghidra-dol.mjs index ebfcb64b07..47be906b72 100644 --- a/scripts/verify-real-ghidra-dol.mjs +++ b/scripts/verify-real-ghidra-dol.mjs @@ -34,6 +34,7 @@ import { } from "./lib/verifier-run.mjs"; import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; import { VerifierFixture } from "./lib/verifier-fixture.mjs"; +import { requireLaneToolchain } from "../tests/conformance/ghidra/fixture-toolchain.mjs"; // Optional cross-target lane. No tool acquisition and no target execution. if (process.argv.length !== 2) @@ -59,6 +60,8 @@ if (installation.status !== "available") const execute = promisify(execFile); const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url)); const run = createVerifierRun(); +// Refuse a host without the fixture toolchain before allocating anything. +await requireLaneToolchain("dol"); const workspaceFixture = await VerifierFixture.open({ prefix: "rea-dol-proof-", // Ghidra children use the workspace as TMPDIR; a plain root admits them on every host. diff --git a/scripts/verify-real-ghidra-mips-relocatable.mjs b/scripts/verify-real-ghidra-mips-relocatable.mjs index 4762271640..1e8c2df151 100755 --- a/scripts/verify-real-ghidra-mips-relocatable.mjs +++ b/scripts/verify-real-ghidra-mips-relocatable.mjs @@ -21,6 +21,7 @@ import { import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; import { VerifierFixture } from "./lib/verifier-fixture.mjs"; +import { requireLaneToolchain } from "../tests/conformance/ghidra/fixture-toolchain.mjs"; // Optional cross-target lane for ET_REL objects. No tool acquisition, no // linking and no target execution. @@ -48,6 +49,8 @@ if (installation.status !== "available") const execute = promisify(execFile); const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url)); const run = createVerifierRun(); +// Refuse a host without the fixture toolchain before allocating anything. +await requireLaneToolchain("mips-relocatable"); const workspaceFixture = await VerifierFixture.open({ prefix: "rea-mips-rel-proof-", // Ghidra children use the workspace as TMPDIR; a plain root admits them on every host. diff --git a/scripts/verify-real-ghidra-mips-shared.mjs b/scripts/verify-real-ghidra-mips-shared.mjs index 8a60952c54..1dd9ebcc57 100644 --- a/scripts/verify-real-ghidra-mips-shared.mjs +++ b/scripts/verify-real-ghidra-mips-shared.mjs @@ -16,6 +16,7 @@ import { } from "./lib/mcp-verifier-results.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; import { VerifierFixture } from "./lib/verifier-fixture.mjs"; +import { requireLaneToolchain } from "../tests/conformance/ghidra/fixture-toolchain.mjs"; if ( process.env.GHIDRA_INSTALL_DIR === undefined || @@ -26,6 +27,8 @@ if ( ); const execute = promisify(execFile); const run = createVerifierRun(); +// Refuse a host without the fixture toolchain before allocating anything. +await requireLaneToolchain("mips-shared"); const workspaceFixture = await VerifierFixture.open({ prefix: "rea-mips-shared-proof-", // Ghidra children use the workspace as TMPDIR; a plain root admits them on every host. diff --git a/scripts/verify-real-ghidra-mips.mjs b/scripts/verify-real-ghidra-mips.mjs index 4208a01f1e..8e58ce6a51 100644 --- a/scripts/verify-real-ghidra-mips.mjs +++ b/scripts/verify-real-ghidra-mips.mjs @@ -46,6 +46,7 @@ import { import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; import { runOwnedCommand } from "../dist/process/OwnedCommand.js"; import { VerifierFixture } from "./lib/verifier-fixture.mjs"; +import { requireLaneToolchain } from "../tests/conformance/ghidra/fixture-toolchain.mjs"; // Optional cross-target lane. No tool acquisition and no target execution. const mode = process.argv[2]; @@ -76,6 +77,8 @@ if (installation.status !== "available") const execute = promisify(execFile); const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url)); const run = createVerifierRun(); +// Refuse a host without the fixture toolchain before allocating anything. +await requireLaneToolchain(psp ? "psp" : "mips"); const workspaceFixture = await VerifierFixture.open({ prefix: "rea-mips-proof-", // Ghidra children use the workspace as TMPDIR; a plain root admits them on every host. diff --git a/scripts/verify-real-ghidra-ppc.mjs b/scripts/verify-real-ghidra-ppc.mjs index aab8b8630f..6329618bad 100644 --- a/scripts/verify-real-ghidra-ppc.mjs +++ b/scripts/verify-real-ghidra-ppc.mjs @@ -23,6 +23,7 @@ import { buildPowerPcFixture } from "../tests/conformance/ghidra/ppc-fixture.mjs import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; import { VerifierFixture } from "./lib/verifier-fixture.mjs"; +import { requireLaneToolchain } from "../tests/conformance/ghidra/fixture-toolchain.mjs"; // Optional cross-target lane. No tool acquisition and no target execution. if (process.argv.length !== 2) @@ -47,6 +48,8 @@ if (installation.status !== "available") const execute = promisify(execFile); const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url)); const run = createVerifierRun(); +// Refuse a host without the fixture toolchain before allocating anything. +await requireLaneToolchain("ppc"); const workspaceFixture = await VerifierFixture.open({ prefix: "rea-ppc-proof-", // Ghidra children use the workspace as TMPDIR; a plain root admits them on every host. diff --git a/scripts/verify-real-ghidra-riscv.mjs b/scripts/verify-real-ghidra-riscv.mjs index 5ebc58881c..6345bdeebd 100644 --- a/scripts/verify-real-ghidra-riscv.mjs +++ b/scripts/verify-real-ghidra-riscv.mjs @@ -35,6 +35,7 @@ import { } from "./lib/mcp-verifier-results.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; import { VerifierFixture } from "./lib/verifier-fixture.mjs"; +import { requireLaneToolchain } from "../tests/conformance/ghidra/fixture-toolchain.mjs"; if (process.argv.length !== 2) throw new Error("Usage: node scripts/verify-real-ghidra-riscv.mjs"); @@ -60,6 +61,8 @@ assert.equal(installation.providerVersion, "12.1.4"); const execute = promisify(execFile); const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url)); const run = createVerifierRun(); +// Refuse a host without the fixture toolchain before allocating anything. +await requireLaneToolchain("riscv"); const workspaceFixture = await VerifierFixture.open({ prefix: "rea-riscv-proof-", // Ghidra children use the workspace as TMPDIR; a plain root admits them on every host. diff --git a/tests/conformance/ghidra/dol-checks.mjs b/tests/conformance/ghidra/dol-checks.mjs index 0dc5b66394..4ac29e969c 100644 --- a/tests/conformance/ghidra/dol-checks.mjs +++ b/tests/conformance/ghidra/dol-checks.mjs @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; +import { FIXTURE_TOOLS, fixtureCommand } from "./fixture-toolchain.mjs"; const execute = promisify(execFile); // Stock language is the default; the Gekko override is checked only when installed. @@ -104,7 +105,7 @@ export function parseReadelfDolSource(text) { /** Independent GNU readelf reader for the ELF the DOL was converted from. */ export async function inspectReadelfDolSource(elfPath) { - const command = process.env.REA_PPC_READELF ?? "readelf"; + const command = fixtureCommand(FIXTURE_TOOLS.ppcReadelf); const options = { env: { ...process.env, LC_ALL: "C" }, timeout: 10000, diff --git a/tests/conformance/ghidra/dol-fixture.mjs b/tests/conformance/ghidra/dol-fixture.mjs index 5daa6d26e3..80ee3ee9d7 100644 --- a/tests/conformance/ghidra/dol-fixture.mjs +++ b/tests/conformance/ghidra/dol-fixture.mjs @@ -4,6 +4,7 @@ import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; +import { FIXTURE_TOOLS, fixtureCommand } from "./fixture-toolchain.mjs"; const execute = promisify(execFile); @@ -103,7 +104,7 @@ export function convertElfToDol(elf, { bss }) { * The fixture is never executed. */ export async function buildGameCubeDolFixture(directory) { - const compiler = process.env.REA_PPC_CLANG ?? "clang"; + const compiler = fixtureCommand(FIXTURE_TOOLS.ppcClang); const target = "powerpc-unknown-linux-gnu"; const here = (path) => fileURLToPath(new URL(path, import.meta.url)); const sources = [ diff --git a/tests/conformance/ghidra/fixture-toolchain.d.mts b/tests/conformance/ghidra/fixture-toolchain.d.mts new file mode 100644 index 0000000000..fd89507d5c --- /dev/null +++ b/tests/conformance/ghidra/fixture-toolchain.d.mts @@ -0,0 +1,16 @@ +/** A host command a fixture runs, and the variable that selects another one. */ +export interface FixtureTool { + readonly variable?: string; + readonly fallback: string; +} + +export declare const FIXTURE_TOOLS: Readonly>; + +/** The command a fixture runs for `tool`. */ +export declare function fixtureCommand(tool: FixtureTool): string; + +/** + * Refuse a lane before it allocates a workspace when this host lacks a + * command its fixtures run, naming each missing capability. + */ +export declare function requireLaneToolchain(lane: string): Promise; diff --git a/tests/conformance/ghidra/fixture-toolchain.mjs b/tests/conformance/ghidra/fixture-toolchain.mjs new file mode 100644 index 0000000000..311ab92b1f --- /dev/null +++ b/tests/conformance/ghidra/fixture-toolchain.mjs @@ -0,0 +1,217 @@ +import { randomUUID } from "node:crypto"; +import { access } from "node:fs/promises"; +import { devNull } from "node:os"; +import { join } from "node:path"; + +import { importVerifierSource } from "../../../scripts/lib/verifier-source-loader.mjs"; + +/** + * The host commands the cross-architecture Ghidra fixtures build and inspect + * with. Each names the environment variable that selects it and the command + * used when that variable is unset; fixtures and checks resolve commands only + * through `fixtureCommand`, so the prerequisite check probes what they run. + */ +export const FIXTURE_TOOLS = Object.freeze({ + mipsClang: { variable: "REA_MIPS_CLANG", fallback: "clang" }, + mipsAssembler: { variable: "REA_MIPS_AS", fallback: "mips-linux-gnu-as" }, + mipsLinker: { variable: "REA_MIPS_LD", fallback: "mips-linux-gnu-ld" }, + mipsReadelf: { variable: "REA_MIPS_READELF", fallback: "readelf" }, + ppcClang: { variable: "REA_PPC_CLANG", fallback: "clang" }, + ppcReadelf: { variable: "REA_PPC_READELF", fallback: "readelf" }, + riscvClang: { variable: "REA_RISCV_CLANG", fallback: "clang" }, + riscvLinker: { variable: "REA_RISCV_LLD", fallback: "ld.lld" }, + riscvReadelf: { variable: "REA_RISCV_READELF", fallback: "llvm-readelf" }, + riscvObjdump: { variable: "REA_RISCV_OBJDUMP", fallback: "llvm-objdump" }, + // The PSPSDK build runs these by name; no variable selects them. + make: { fallback: "make" }, + pspConfig: { fallback: "psp-config" }, + pspCompiler: { fallback: "psp-gcc" }, + pspReadelf: { fallback: "psp-readelf" }, + pspObjdump: { fallback: "psp-objdump" }, +}); + +/** The command a fixture runs for `tool`. */ +export const fixtureCommand = (tool) => + (tool.variable === undefined ? undefined : process.env[tool.variable]) ?? + tool.fallback; + +const firstLine = (text) => + String(text ?? "") + .split("\n") + .find((line) => line.trim() !== "") + ?.trim(); + +/** + * A command that must answer `arguments` (default `--version`), with output + * matching `banner` when the fixture reads a particular tool's output. + */ +const runs = (tool, banner, args = ["--version"]) => ({ + tool, + need: banner === undefined ? "run" : `report ${String(banner)}`, + arguments: args, + accepts: (stdout) => + banner === undefined || banner.test(stdout) + ? undefined + : `${args.join(" ")} reported ${JSON.stringify(firstLine(stdout) ?? "")}`, +}); + +/** + * A Clang that generates code for `target`; with `lld` it also links it with + * ld.lld, which prints its version and exits without writing an output. + */ +const targets = (tool, target, { lld = false } = {}) => [ + { + tool, + need: lld + ? `generate and link ${target} code with -fuse-ld=lld` + : `generate ${target} code`, + arguments: [ + `--target=${target}`, + ...(lld ? ["-fuse-ld=lld", "-nostdlib", "-Wl,--version"] : ["-c"]), + "-x", + "c", + devNull, + "-o", + devNull, + ], + accepts: () => undefined, + }, +]; + +/** The PSPSDK root whose build rules the fixture Makefile includes. */ +const pspsdk = { + tool: FIXTURE_TOOLS.pspConfig, + need: "report a PSPSDK path with lib/build.mak", + arguments: ["--pspsdk-path"], + accepts: async (stdout) => { + const path = firstLine(stdout); + if (path === undefined) return "--pspsdk-path reported nothing"; + try { + await access(join(path, "lib", "build.mak")); + return undefined; + } catch { + return `--pspsdk-path reported ${JSON.stringify(path)}, which has no lib/build.mak`; + } + }, +}; + +const gnuReadelf = /^GNU readelf\b/u; +// The RISC-V fixture parses LLVM's attribute layout and disassembles one +// symbol with --disassemble-symbols, which GNU binutils do not provide. +const llvm = /\bLLVM\b/u; +const tools = FIXTURE_TOOLS; + +/** What each lane's fixtures and independent readers run. */ +export const LANE_TOOLCHAINS = Object.freeze({ + mips: [ + ...targets(tools.mipsClang, "mipsel-linux-gnu", { lld: true }), + ...targets(tools.mipsClang, "mips-linux-gnu", { lld: true }), + runs(tools.mipsReadelf, gnuReadelf), + ], + psp: [ + runs(tools.make), + pspsdk, + runs(tools.pspCompiler), + runs(tools.pspReadelf), + runs(tools.pspObjdump), + ], + "mips-relocatable": [ + ...targets(tools.mipsClang, "mipsel-linux-gnu"), + ...targets(tools.mipsClang, "mips-linux-gnu"), + runs(tools.mipsReadelf, gnuReadelf), + ], + "mips-shared": [ + // GNU tools name their configured target; a host-native assembler or + // linker answers --version but cannot build MIPS16 code. + runs(tools.mipsAssembler, /GNU assembler[\s\S]*target of .mips/u), + runs(tools.mipsLinker, /GNU ld[\s\S]*\belf32\w*mip/u, ["-V"]), + runs(tools.mipsReadelf), + ], + ppc: [ + ...targets(tools.ppcClang, "powerpc-unknown-linux-gnu", { lld: true }), + runs(tools.ppcReadelf, gnuReadelf), + ], + dol: [ + ...targets(tools.ppcClang, "powerpc-unknown-linux-gnu", { lld: true }), + runs(tools.ppcReadelf, gnuReadelf), + ], + riscv: [ + ...targets(tools.riscvClang, "riscv32-unknown-elf"), + ...targets(tools.riscvClang, "riscv64-unknown-elf"), + runs(tools.riscvLinker), + runs(tools.riscvReadelf, llvm), + runs(tools.riscvObjdump, llvm), + ], +}); + +let ownedCommand; +/** Probes run through the shared owned-command runner, which owns their process group. */ +const runOwnedCommand = async () => { + ownedCommand ??= importVerifierSource( + new URL("../../../src/process/OwnedCommand.ts", import.meta.url).href, + import.meta.url, + ); + return (await ownedCommand).runOwnedCommand; +}; + +/** Why `requirement` is unmet on this host, or undefined when it is met. */ +const unmet = async (requirement) => { + const run = await runOwnedCommand(); + let stdout; + try { + const snapshot = await run( + { + command: fixtureCommand(requirement.tool), + arguments: requirement.arguments, + runId: randomUUID(), + expectedCommand: null, + env: { ...process.env, LC_ALL: "C" }, + }, + { timeoutMs: 10_000, diagnosticBytes: 1024 * 1024 }, + ); + stdout = snapshot.stdout.text; + } catch (cause) { + // A probe whose process group could not be confirmed stopped is not a + // missing tool; that failure stays the caller's to report. + if (cause?.cleanupFailure != null) throw cause; + if (cause?.code === "ENOENT" || cause?.processError?.code === "ENOENT") + return "command not found"; + return ( + firstLine(cause?.snapshot?.stderr?.text) ?? + firstLine(cause?.snapshot?.stdout?.text) ?? + firstLine(cause?.message) ?? + "failed" + ); + } + return requirement.accepts(stdout); +}; + +/** + * Refuse a lane, before it allocates a workspace, when this host lacks a + * command its fixtures run. The refusal names each missing capability, the + * command probed and the variable that selects another one. + */ +export async function requireLaneToolchain(lane) { + const requirements = LANE_TOOLCHAINS[lane]; + if (requirements === undefined) + throw new Error(`No fixture toolchain is declared for lane ${lane}`); + const missing = []; + for (const requirement of requirements) { + const reason = await unmet(requirement); + if (reason === undefined) continue; + const command = fixtureCommand(requirement.tool); + const selected = + requirement.tool.variable === undefined + ? command + : `${requirement.tool.variable}=${command}`; + missing.push(`- ${selected} must ${requirement.need}: ${reason}`); + } + if (missing.length > 0) + throw new Error( + [ + `Missing prerequisite: the ${lane} lane's fixture toolchain (no workspace was created).`, + ...missing, + "Install the missing commands, or set each variable to a command that provides it.", + ].join("\n"), + ); +} diff --git a/tests/conformance/ghidra/fixture-toolchain.test.ts b/tests/conformance/ghidra/fixture-toolchain.test.ts new file mode 100644 index 0000000000..93b65cf52a --- /dev/null +++ b/tests/conformance/ghidra/fixture-toolchain.test.ts @@ -0,0 +1,89 @@ +import { chmod, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { afterEach, expect, it, vi } from "vitest"; + +import { requireLaneToolchain } from "./fixture-toolchain.mjs"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +afterEach(() => { + vi.unstubAllEnvs(); +}); + +/** A stand-in command that prints `banner` for any arguments. */ +const command = async (directory: string, name: string, banner: string) => { + const path = join(directory, name); + await writeFile( + path, + `#!${process.execPath}\nprocess.stdout.write(${JSON.stringify(`${banner}\n`)});\n`, + ); + await chmod(path, 0o700); + return path; +}; + +/** The refusal's lines that name `selected` (a `VARIABLE=command` pair). */ +const refusalFor = async (lane: string, selected: string) => { + const refusal = await requireLaneToolchain(lane).catch( + (cause: unknown) => cause, + ); + expect(refusal).toBeInstanceOf(Error); + expect((refusal as Error).message).toMatch(/^Missing prerequisite\b/u); + return (refusal as Error).message + .split("\n") + .filter((line) => line.includes(selected)); +}; + +const mipsBinutils = async (directory: string, target: string) => { + vi.stubEnv( + "REA_MIPS_AS", + await command( + directory, + "as", + `GNU assembler (GNU Binutils) 2.44\nThis assembler was configured for a target of \`${target}'.`, + ), + ); + vi.stubEnv( + "REA_MIPS_LD", + await command( + directory, + "ld", + `GNU ld (GNU Binutils) 2.44\n Supported emulations:\n ${target.startsWith("mips") ? "elf32btsmip" : "elf_x86_64"}`, + ), + ); + vi.stubEnv("REA_MIPS_READELF", await command(directory, "readelf", "x")); +}; + +it.skipIf(process.platform === "win32")( + "names each missing fixture command, its variable and the observed reason", + async () => { + const directory = await createTestTempDirectory("rea-fixture-toolchain-"); + const clang = join(directory, "missing-clang"); + const readelf = await command(directory, "readelf", "LLVM readelf 19"); + vi.stubEnv("REA_PPC_CLANG", clang); + vi.stubEnv("REA_PPC_READELF", readelf); + + const [compiler] = await refusalFor("ppc", `REA_PPC_CLANG=${clang}`); + expect(compiler).toContain("powerpc-unknown-linux-gnu"); + expect(compiler).toContain("command not found"); + const [reader] = await refusalFor("ppc", `REA_PPC_READELF=${readelf}`); + expect(reader).toContain("LLVM readelf 19"); + }, +); + +it.skipIf(process.platform === "win32")( + "admits MIPS binutils configured for MIPS and refuses host-native ones", + async () => { + const directory = await createTestTempDirectory("rea-fixture-toolchain-"); + await mipsBinutils(directory, "mips-linux-gnu"); + await expect(requireLaneToolchain("mips-shared")).resolves.toBeUndefined(); + + const native = await createTestTempDirectory("rea-fixture-toolchain-"); + await mipsBinutils(native, "x86_64-linux-gnu"); + expect( + await refusalFor("mips-shared", `REA_MIPS_AS=${join(native, "as")}`), + ).toHaveLength(1); + expect( + await refusalFor("mips-shared", `REA_MIPS_LD=${join(native, "ld")}`), + ).toHaveLength(1); + }, +); diff --git a/tests/conformance/ghidra/mips-checks.mjs b/tests/conformance/ghidra/mips-checks.mjs index 253013d4ca..0be938c379 100644 --- a/tests/conformance/ghidra/mips-checks.mjs +++ b/tests/conformance/ghidra/mips-checks.mjs @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; +import { FIXTURE_TOOLS, fixtureCommand } from "./fixture-toolchain.mjs"; const execute = promisify(execFile); const language = (byteOrder) => { @@ -194,7 +195,7 @@ export function parseMipsReadelf( /** Independent reader is a bounded test prerequisite, never an REA provider. */ export async function inspectMipsReadelf(path, byteOrder, floatAbi = "hard") { - const command = process.env.REA_MIPS_READELF ?? "readelf"; + const command = fixtureCommand(FIXTURE_TOOLS.mipsReadelf); const options = { env: { ...process.env, LC_ALL: "C" }, timeout: 10000, @@ -226,7 +227,7 @@ export async function inspectMipsReadelf(path, byteOrder, floatAbi = "hard") { /** Independent GNU readelf interpretation for the recordless legacy fixture. */ export async function inspectLegacyMipsReadelf(path, byteOrder) { - const command = process.env.REA_MIPS_READELF ?? "readelf"; + const command = fixtureCommand(FIXTURE_TOOLS.mipsReadelf); const options = { env: { ...process.env, LC_ALL: "C" }, timeout: 10000, diff --git a/tests/conformance/ghidra/mips-fixture.mjs b/tests/conformance/ghidra/mips-fixture.mjs index 5d6202d46a..66ae2f355b 100644 --- a/tests/conformance/ghidra/mips-fixture.mjs +++ b/tests/conformance/ghidra/mips-fixture.mjs @@ -4,6 +4,7 @@ import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; +import { FIXTURE_TOOLS, fixtureCommand } from "./fixture-toolchain.mjs"; const execute = promisify(execFile); @@ -196,7 +197,7 @@ async function compileMipsFixture( args, options = {}, ) { - const compiler = process.env.REA_MIPS_CLANG ?? "clang"; + const compiler = fixtureCommand(FIXTURE_TOOLS.mipsClang); const version = await execute(compiler, ["--version"], { timeout: 10000 }); await execute(compiler, args, { timeout: 60000 }); const bytes = await readFile(path); diff --git a/tests/conformance/ghidra/mips-relocatable-checks.mjs b/tests/conformance/ghidra/mips-relocatable-checks.mjs index e4c4f0abe2..294c6c9424 100644 --- a/tests/conformance/ghidra/mips-relocatable-checks.mjs +++ b/tests/conformance/ghidra/mips-relocatable-checks.mjs @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { parseMipsReadelf } from "./mips-checks.mjs"; +import { FIXTURE_TOOLS, fixtureCommand } from "./fixture-toolchain.mjs"; const execute = promisify(execFile); const RELOCATION_TYPES = new Map([ @@ -90,7 +91,7 @@ export function parseMipsRelocatableReadelf(text) { /** Independent reader is a bounded test prerequisite, never an REA provider. */ export async function inspectMipsRelocatableReadelf(path, byteOrder, floatAbi) { - const command = process.env.REA_MIPS_READELF ?? "readelf"; + const command = fixtureCommand(FIXTURE_TOOLS.mipsReadelf); const options = { env: { ...process.env, LC_ALL: "C" }, timeout: 10000, diff --git a/tests/conformance/ghidra/mips16-shared-fixture.mjs b/tests/conformance/ghidra/mips16-shared-fixture.mjs index 37ebddeabf..fce6ec48e8 100644 --- a/tests/conformance/ghidra/mips16-shared-fixture.mjs +++ b/tests/conformance/ghidra/mips16-shared-fixture.mjs @@ -5,6 +5,7 @@ import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; +import { FIXTURE_TOOLS, fixtureCommand } from "./fixture-toolchain.mjs"; const execute = promisify(execFile); const source = fileURLToPath(new URL("./mips16-shared.S", import.meta.url)); @@ -24,8 +25,8 @@ export async function buildMips16SharedFixture(directory, byteOrder, variant) { "section-relocations", ].includes(variant), ); - const assembler = process.env.REA_MIPS_AS ?? "mips-linux-gnu-as"; - const linker = process.env.REA_MIPS_LD ?? "mips-linux-gnu-ld"; + const assembler = fixtureCommand(FIXTURE_TOOLS.mipsAssembler); + const linker = fixtureCommand(FIXTURE_TOOLS.mipsLinker); const endian = byteOrder === "little" ? "-EL" : "-EB"; const prefix = join(directory, `${byteOrder}-${variant}`); const object = `${prefix}.o`; @@ -97,7 +98,7 @@ export async function buildMips16SharedFixture(directory, byteOrder, variant) { return u32(mappings[0] + 4) + address - u32(mappings[0] + 8); }; const readelf = await execute( - process.env.REA_MIPS_READELF ?? "readelf", + fixtureCommand(FIXTURE_TOOLS.mipsReadelf), ["-W", "-h", "-l", "-s", "-r", path], { timeout: 10000 }, ); diff --git a/tests/conformance/ghidra/ppc-checks.mjs b/tests/conformance/ghidra/ppc-checks.mjs index 1136f2602e..64f8afeeb0 100644 --- a/tests/conformance/ghidra/ppc-checks.mjs +++ b/tests/conformance/ghidra/ppc-checks.mjs @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; +import { FIXTURE_TOOLS, fixtureCommand } from "./fixture-toolchain.mjs"; const execute = promisify(execFile); // The lane binds this language; Ghidra's ELF loader alone selects e500. @@ -52,7 +53,7 @@ export function parsePowerPcReadelf(text) { /** Independent reader is a bounded test prerequisite, never an REA provider. */ export async function inspectPowerPcReadelf(path) { - const command = process.env.REA_PPC_READELF ?? "readelf"; + const command = fixtureCommand(FIXTURE_TOOLS.ppcReadelf); const options = { env: { ...process.env, LC_ALL: "C" }, timeout: 10000, diff --git a/tests/conformance/ghidra/ppc-fixture.mjs b/tests/conformance/ghidra/ppc-fixture.mjs index 202c0dacf6..d71dfdbd1b 100644 --- a/tests/conformance/ghidra/ppc-fixture.mjs +++ b/tests/conformance/ghidra/ppc-fixture.mjs @@ -4,12 +4,13 @@ import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; +import { FIXTURE_TOOLS, fixtureCommand } from "./fixture-toolchain.mjs"; const execute = promisify(execFile); /** Build a source-owned freestanding ELF; never execute its target code. */ export async function buildPowerPcFixture(directory) { - const compiler = process.env.REA_PPC_CLANG ?? "clang"; + const compiler = fixtureCommand(FIXTURE_TOOLS.ppcClang); const target = "powerpc-unknown-linux-gnu"; const source = fileURLToPath(new URL("../c/ppc.c", import.meta.url)); const probe = fileURLToPath(new URL("./ppc-probe.S", import.meta.url)); diff --git a/tests/conformance/ghidra/riscv-checks.mjs b/tests/conformance/ghidra/riscv-checks.mjs index 5770d3caa6..5056519b57 100644 --- a/tests/conformance/ghidra/riscv-checks.mjs +++ b/tests/conformance/ghidra/riscv-checks.mjs @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; +import { FIXTURE_TOOLS, fixtureCommand } from "./fixture-toolchain.mjs"; const execute = promisify(execFile); @@ -328,7 +329,7 @@ export function parseRiscvCallSites(listing) { /** Disassemble the typed fixture's caller with an independent LLVM tool. */ export async function inspectRiscvCallSites(path) { - const command = process.env.REA_RISCV_OBJDUMP ?? "llvm-objdump"; + const command = fixtureCommand(FIXTURE_TOOLS.riscvObjdump); const options = { env: { ...process.env, LC_ALL: "C" }, timeout: 10000, diff --git a/tests/conformance/ghidra/riscv-fixture.mjs b/tests/conformance/ghidra/riscv-fixture.mjs index 65636d171c..945cc45c67 100644 --- a/tests/conformance/ghidra/riscv-fixture.mjs +++ b/tests/conformance/ghidra/riscv-fixture.mjs @@ -5,6 +5,7 @@ import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; +import { FIXTURE_TOOLS, fixtureCommand } from "./fixture-toolchain.mjs"; const execute = promisify(execFile); @@ -96,9 +97,9 @@ export async function buildRiscvFixture(directory, bits, floatAbi = "double") { const build = BUILDS[floatAbi]; assert.ok(build !== undefined, `No ${floatAbi}-float RISC-V fixture`); assert.ok(floatAbi === "double" || bits === 32); - const compiler = process.env.REA_RISCV_CLANG ?? "clang"; - const linker = process.env.REA_RISCV_LLD ?? "ld.lld"; - const reader = process.env.REA_RISCV_READELF ?? "llvm-readelf"; + const compiler = fixtureCommand(FIXTURE_TOOLS.riscvClang); + const linker = fixtureCommand(FIXTURE_TOOLS.riscvLinker); + const reader = fixtureCommand(FIXTURE_TOOLS.riscvReadelf); const sources = build.sources.map((path) => fileURLToPath(new URL(path, import.meta.url)), );