fix(artifacts): infer legacy Mach-O simulator platforms
This commit is contained in:
parent
5751f5caac
commit
5aa200285a
|
|
@ -11,9 +11,14 @@ export const LC = {
|
|||
DYLD_ENVIRONMENT: 0x27,
|
||||
CODE_SIGNATURE: 0x1d,
|
||||
BUILD_VERSION: 0x32,
|
||||
VERSION_MIN_MACOSX: 0x24,
|
||||
VERSION_MIN_IPHONEOS: 0x25,
|
||||
VERSION_MIN_TVOS: 0x2f,
|
||||
VERSION_MIN_WATCHOS: 0x30,
|
||||
} as const;
|
||||
|
||||
export const CPU = {
|
||||
i386: { type: 7, subtype: 3 },
|
||||
arm64: { type: 0x0100000c, subtype: 0 },
|
||||
arm64e: { type: 0x0100000c, subtype: 0x80000002 },
|
||||
x86_64: { type: 0x01000007, subtype: 3 },
|
||||
|
|
@ -86,6 +91,16 @@ export const buildVersionCommand = (platform: number): Uint8Array => {
|
|||
return bytes;
|
||||
};
|
||||
|
||||
/** `version_min_command` carrying `LC_VERSION_MIN_*` semantics. */
|
||||
export const versionMinCommand = (command: number): Uint8Array => {
|
||||
const bytes = new Uint8Array(16);
|
||||
const view = new DataView(bytes.buffer);
|
||||
view.setUint32(0, command, true);
|
||||
view.setUint32(4, bytes.length, true);
|
||||
view.setUint32(8, 0x00090000, true);
|
||||
return bytes;
|
||||
};
|
||||
|
||||
/** A 64-bit (or 32-bit) little-endian Mach-O image made of the given commands. */
|
||||
export const machoImage = (options: {
|
||||
readonly cpu?: { readonly type: number; readonly subtype: number };
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import { createHash } from "node:crypto";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
|
|
@ -13,16 +17,124 @@ import {
|
|||
machoImage,
|
||||
readerOf,
|
||||
rpathCommand,
|
||||
versionMinCommand,
|
||||
} from "./MachoImage.fixture.js";
|
||||
import {
|
||||
MAX_LOAD_COMMAND_BYTES,
|
||||
hasMachoMagic,
|
||||
readMachoImage,
|
||||
} from "./MachoLoadCommandReader.js";
|
||||
import { traceDylibResolution } from "./DylibResolutionReader.js";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
|
||||
const read = (bytes: Uint8Array) =>
|
||||
readMachoImage(readerOf(bytes), bytes.length);
|
||||
|
||||
// Legacy `LC_VERSION_MIN_*` commands carry no simulator flag: per dyld
|
||||
// rules Intel images are simulator, ARM images are device (tvOS simulator
|
||||
// is x86_64 only). A legacy simulator executable must not fail resolution
|
||||
// against a modern simulator dylib.
|
||||
it.each([
|
||||
{ command: LC.VERSION_MIN_IPHONEOS, cpu: CPU.i386, platform: 7 },
|
||||
{ command: LC.VERSION_MIN_IPHONEOS, cpu: CPU.x86_64, platform: 7 },
|
||||
{ command: LC.VERSION_MIN_IPHONEOS, cpu: CPU.arm64, platform: 2 },
|
||||
{ command: LC.VERSION_MIN_TVOS, cpu: CPU.x86_64, platform: 8 },
|
||||
{ command: LC.VERSION_MIN_TVOS, cpu: CPU.arm64, platform: 3 },
|
||||
{ command: LC.VERSION_MIN_WATCHOS, cpu: CPU.i386, platform: 9 },
|
||||
{ command: LC.VERSION_MIN_WATCHOS, cpu: CPU.arm64, platform: 4 },
|
||||
{ command: LC.VERSION_MIN_MACOSX, cpu: CPU.x86_64, platform: 1 },
|
||||
] as const)(
|
||||
"classifies legacy command $command on $cpu.type as platform $platform",
|
||||
async ({ command, cpu, platform }) => {
|
||||
const facts = await read(
|
||||
machoImage({ cpu, commands: [versionMinCommand(command)] }),
|
||||
);
|
||||
if (facts.status !== "parsed") throw new Error(facts.status);
|
||||
expect(facts.slices[0]).toMatchObject({ platform, platforms: [platform] });
|
||||
},
|
||||
);
|
||||
|
||||
it("uses legacy simulator and device platform facts when resolving dylibs", async () => {
|
||||
const root = await createTestTempDirectory("rea-macho-legacy-platform-");
|
||||
const dylibPath = "Frameworks/libThing.dylib";
|
||||
const installName = "@rpath/libThing.dylib";
|
||||
const intelExecutable = machoImage({
|
||||
cpu: CPU.x86_64,
|
||||
commands: [
|
||||
versionMinCommand(LC.VERSION_MIN_IPHONEOS),
|
||||
rpathCommand("@executable_path/Frameworks"),
|
||||
dylibCommand(LC.LOAD_DYLIB, installName),
|
||||
],
|
||||
});
|
||||
const armExecutable = machoImage({
|
||||
cpu: CPU.arm64,
|
||||
commands: [
|
||||
versionMinCommand(LC.VERSION_MIN_IPHONEOS),
|
||||
rpathCommand("@executable_path/Frameworks"),
|
||||
dylibCommand(LC.LOAD_DYLIB, installName),
|
||||
],
|
||||
});
|
||||
const simulatorDylib = fatImage([
|
||||
{
|
||||
cpu: CPU.x86_64,
|
||||
bytes: machoImage({
|
||||
cpu: CPU.x86_64,
|
||||
fileType: FILE_TYPE.dylib,
|
||||
commands: [
|
||||
buildVersionCommand(7),
|
||||
dylibCommand(LC.ID_DYLIB, installName),
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
cpu: CPU.arm64,
|
||||
bytes: machoImage({
|
||||
cpu: CPU.arm64,
|
||||
fileType: FILE_TYPE.dylib,
|
||||
commands: [
|
||||
buildVersionCommand(7),
|
||||
dylibCommand(LC.ID_DYLIB, installName),
|
||||
],
|
||||
}),
|
||||
},
|
||||
]);
|
||||
await mkdir(join(root, "Frameworks"), { recursive: true });
|
||||
await writeFile(join(root, "Intel"), intelExecutable);
|
||||
await writeFile(join(root, "Arm"), armExecutable);
|
||||
await writeFile(join(root, dylibPath), simulatorDylib);
|
||||
|
||||
const trace = async (target: string, architecture: "arm64" | "x86_64") => {
|
||||
const bytes = target === "Intel" ? intelExecutable : armExecutable;
|
||||
return traceDylibResolution({
|
||||
rootPath: root,
|
||||
targetPath: join(root, target),
|
||||
targetSha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
enumerateRoots: false,
|
||||
parameters: { roots: [target], architecture },
|
||||
});
|
||||
};
|
||||
const intelTrace = await trace("Intel", "x86_64");
|
||||
const armTrace = await trace("Arm", "arm64");
|
||||
|
||||
expect(intelTrace.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
loader: "Intel",
|
||||
resolution: expect.objectContaining({
|
||||
status: "resolved",
|
||||
image: dylibPath,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(armTrace.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
loader: "Arm",
|
||||
candidates: expect.arrayContaining([
|
||||
expect.objectContaining({ outcome: "platform-mismatch" }),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe("Mach-O load command reader: decodes thin-image load commands", () => {
|
||||
it("decodes dylib loading commands of a thin image", async () => {
|
||||
const image = machoImage({
|
||||
|
|
|
|||
|
|
@ -45,6 +45,47 @@ const LC_VERSION_MIN_TVOS = 0x2f;
|
|||
const LC_VERSION_MIN_WATCHOS = 0x30;
|
||||
const LC_BUILD_VERSION = 0x32;
|
||||
|
||||
// `mach-o/loader.h` `PLATFORM_*` numbers observed via Apple linker output.
|
||||
const PLATFORM_MACOS = 1;
|
||||
const PLATFORM_IOS = 2;
|
||||
const PLATFORM_TVOS = 3;
|
||||
const PLATFORM_WATCHOS = 4;
|
||||
const PLATFORM_IOSSIMULATOR = 7;
|
||||
const PLATFORM_TVOSSIMULATOR = 8;
|
||||
const PLATFORM_WATCHOSSIMULATOR = 9;
|
||||
|
||||
const CPU_TYPE_I386 = 7;
|
||||
const CPU_TYPE_X86_64 = 0x01000007;
|
||||
|
||||
/**
|
||||
* Legacy `LC_VERSION_MIN_*` commands predate explicit simulator platform
|
||||
* IDs. Per Apple's dyld rules the image CPU type distinguishes simulator
|
||||
* from device: Intel images are simulator, ARM images are device
|
||||
* (tvOS simulator applies to x86_64 only). `LC_BUILD_VERSION` platform
|
||||
* numbers are preserved verbatim; unknown future numbers stay unknown
|
||||
* downstream, never mapped to nearest.
|
||||
*/
|
||||
const legacyVersionMinPlatform = (
|
||||
command: number,
|
||||
cpuType: number,
|
||||
): number | undefined => {
|
||||
const isIntel = cpuType === CPU_TYPE_I386 || cpuType === CPU_TYPE_X86_64;
|
||||
switch (command) {
|
||||
case LC_VERSION_MIN_MACOSX:
|
||||
return PLATFORM_MACOS;
|
||||
case LC_VERSION_MIN_IPHONEOS:
|
||||
return isIntel ? PLATFORM_IOSSIMULATOR : PLATFORM_IOS;
|
||||
case LC_VERSION_MIN_TVOS:
|
||||
return cpuType === CPU_TYPE_X86_64
|
||||
? PLATFORM_TVOSSIMULATOR
|
||||
: PLATFORM_TVOS;
|
||||
case LC_VERSION_MIN_WATCHOS:
|
||||
return isIntel ? PLATFORM_WATCHOSSIMULATOR : PLATFORM_WATCHOS;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/** `mach-o/loader.h` `dylib_use_command`: `nameoff == 28` and this marker. */
|
||||
const DYLIB_USE_MARKER = 0x1a741800;
|
||||
const DYLIB_USE_WEAK_LINK = 0x1;
|
||||
|
|
@ -377,12 +418,7 @@ const decodeCommand = (
|
|||
slice.platforms.push(platform);
|
||||
return;
|
||||
}
|
||||
const legacyPlatform = new Map<number, number>([
|
||||
[LC_VERSION_MIN_MACOSX, 1],
|
||||
[LC_VERSION_MIN_IPHONEOS, 2],
|
||||
[LC_VERSION_MIN_TVOS, 3],
|
||||
[LC_VERSION_MIN_WATCHOS, 4],
|
||||
]).get(command);
|
||||
const legacyPlatform = legacyVersionMinPlatform(command, slice.cpu_type);
|
||||
if (legacyPlatform !== undefined) {
|
||||
if (body.byteLength < 16)
|
||||
throw new MachoFormatIssue(
|
||||
|
|
|
|||
Loading…
Reference in New Issue