fix(native): share otool build tool parsing

This commit is contained in:
morluto
2026-10-27 11:52:21 +08:00
parent a6a69ec63e
commit 8aa6df2d86
2 changed files with 76 additions and 13 deletions
+18 -13
View File
@@ -308,7 +308,7 @@ const parseLoadCommand = (block: string, state: ParsedLoadCommands): void => {
fields,
});
collectEntrypoint(kind, fields, state);
collectBuild(kind, block, fields, state);
collectBuild(kind, parsed, state);
collectDependency(kind, fields, state);
if (PAYLOAD_DEPENDENCY_COMMANDS.has(kind))
state.omittedDependencyCommands.push(kind);
@@ -387,12 +387,12 @@ const collectEntrypoint = (
const collectBuild = (
kind: string,
block: string,
fields: Readonly<Record<string, string | number | null>>,
parsed: ParsedFields,
state: ParsedLoadCommands,
): void => {
const fields = parsed.values;
if (kind === "LC_BUILD_VERSION") {
const tools = parseBuildTools(block);
const tools = parseBuildTools(parsed.rows);
if (numberField(fields, "ntools") !== tools.length)
throw new TypeError("otool build-tool count does not match ntools");
state.builds.push({
@@ -517,8 +517,6 @@ const parseField = (rawLine: string): readonly [string, string] | undefined => {
// exactly, including surrounding spaces, under otool's ordinal label.
const option = /^\s*(string #[1-9]\d*)(?: (.*))?$/u.exec(rawLine);
if (option?.[1] !== undefined) return [option[1], option[2] ?? ""];
const fixedName = /^\s*(segname|sectname) (.*)$/u.exec(rawLine);
if (fixedName?.[1] !== undefined) return [fixedName[1], fixedName[2] ?? ""];
const line = rawLine.trim();
// These are the compound field labels used by otool. Keeping this list
// explicit prevents a one-word field value such as `sectname version` from
@@ -586,16 +584,23 @@ const parseSections = (block: string): ParsedSection[] =>
};
});
/** Project tool/version pairs from the same ordered facts as command fields. */
const parseBuildTools = (
block: string,
rows: ParsedFields["rows"],
): Array<{ name: string; version: string }> => {
const tools: Array<{ name: string; version: string }> = [];
const lines = block.split(/\r?\n/u).map((line) => line.trim());
for (let index = 0; index < lines.length - 1; index += 1) {
const tool = /^tool\s+(.+)$/u.exec(lines[index] ?? "")?.[1];
const version = /^version\s+(.+)$/u.exec(lines[index + 1] ?? "")?.[1];
if (tool !== undefined && version !== undefined)
tools.push({ name: BUILD_TOOL_NAMES.get(tool) ?? tool, version });
for (let index = 0; index < rows.length; index++) {
const field = rows[index];
if (field?.[0] === "version")
throw new TypeError("otool build-tool version has no preceding tool");
if (field?.[0] !== "tool") continue;
const version = rows[++index];
if (field[1] === "" || version?.[0] !== "version" || version[1] === "")
throw new TypeError("otool build tool is missing its name or version");
tools.push({
name: BUILD_TOOL_NAMES.get(field[1]) ?? field[1],
version: version[1],
});
}
return tools;
};
@@ -595,3 +595,61 @@ it.each([
);
expect(load.builds[0]?.tools).toEqual([{ name, version: "1.0" }]);
});
it("projects build tools from the retained ordered field facts", () => {
const load = parseOtoolLoadCommands(
withOtoolMachHeader(
[
"Load command 0",
"cmd LC_BUILD_VERSION",
"cmdsize 40",
"platform 1",
"minos 1.0",
"sdk 1.0",
"ntools 2",
"tool 1",
"",
"version 001",
"tool 99",
"version 0x10",
"",
].join("\n"),
),
);
expect(load.builds[0]?.tools).toEqual([
{ name: "CLANG", version: "001" },
{ name: "99", version: "0x10" },
]);
expect(load.commands[0]?.fields).toMatchObject({
"tool #1": 1,
"tool #2": 99,
"version #1": 1,
"version #2": 16,
});
});
it.each([
["version 9.0", "tool 1", "version 1.0"],
["tool 1", "version 1.0", "tool 2"],
["tool 1", "version 1.0", "version 2.0"],
["tool 1", "unexpected 2", "version 1.0"],
["tool 1", "version"],
])("rejects unpaired build tool/version facts: %j", (...toolRows) => {
expect(() =>
parseOtoolLoadCommands(
withOtoolMachHeader(
[
"Load command 0",
"cmd LC_BUILD_VERSION",
"cmdsize 32",
"platform 1",
"minos 1.0",
"sdk 1.0",
"ntools 1",
...toolRows,
"",
].join("\n"),
),
),
).toThrow(/otool build.tool/u);
});