diff --git a/apps/desktop/scripts/stage-native-deps.mjs b/apps/desktop/scripts/stage-native-deps.mjs index f06f5bef5..8966fece6 100644 --- a/apps/desktop/scripts/stage-native-deps.mjs +++ b/apps/desktop/scripts/stage-native-deps.mjs @@ -86,11 +86,20 @@ function copyBuildRelease(srcDir, destDir) { // format. By reading the first few bytes (magic) we can determine which // platform a given .node was compiled for, without shelling out to `file`. // -// ELF (\x7fELF) → linux -// Mach-O 32-bit (feedface) → darwin -// Mach-O 64-bit (feedfacf) → darwin -// Fat/Universal (cafebabe) → darwin -// PE (MZ DOS header) → win32 +// ELF (\x7fELF) → linux +// Mach-O 32-bit BE (feedface) → darwin +// Mach-O 64-bit BE (feedfacf) → darwin +// Mach-O 32-bit LE (cefaedfe — CIGAM) → darwin +// Mach-O 64-bit LE (cffaedfe — CIGAM_64) → darwin +// Fat/Universal BE (cafebabe) → darwin +// Fat/Universal LE (bebafeca — FAT_CIGAM) → darwin +// PE (MZ DOS header) → win32 +// +// Mach-O and Fat binaries are stored on disk in the host's native byte +// order. On x64/arm64 Darwin (every Apple Silicon + every Intel Mac that +// ships node-pty prebuilds) that is little-endian, so the on-disk magic is +// the CIGAM byte-swapped form, NOT the big-endian MH_MAGIC form. Checking +// only the BE constants misclassifies every real Darwin prebuild as unknown. // // Exported for unit testing. @@ -112,18 +121,30 @@ export function classifyNativeBinary(filePath) { if (buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46) { return 'linux' } - // Mach-O 32-bit: feedface + // Mach-O 32-bit (big-endian / MH_MAGIC): feedface if (buf[0] === 0xfe && buf[1] === 0xed && buf[2] === 0xfa && buf[3] === 0xce) { return 'darwin' } - // Mach-O 64-bit: feedfacf + // Mach-O 64-bit (big-endian / MH_MAGIC_64): feedfacf if (buf[0] === 0xfe && buf[1] === 0xed && buf[2] === 0xfa && buf[3] === 0xcf) { return 'darwin' } - // Fat/Universal binary: cafebabe + // Mach-O 32-bit (little-endian / MH_CIGAM): cefaedfe + if (buf[0] === 0xce && buf[1] === 0xfa && buf[2] === 0xed && buf[3] === 0xfe) { + return 'darwin' + } + // Mach-O 64-bit (little-endian / MH_CIGAM_64): cffaedfe + if (buf[0] === 0xcf && buf[1] === 0xfa && buf[2] === 0xed && buf[3] === 0xfe) { + return 'darwin' + } + // Fat/Universal binary (big-endian / FAT_MAGIC): cafebabe if (buf[0] === 0xca && buf[1] === 0xfe && buf[2] === 0xba && buf[3] === 0xbe) { return 'darwin' } + // Fat/Universal binary (little-endian / FAT_CIGAM): bebafeca + if (buf[0] === 0xbe && buf[1] === 0xba && buf[2] === 0xfe && buf[3] === 0xca) { + return 'darwin' + } // PE: MZ DOS header if (buf[0] === 0x4d && buf[1] === 0x5a) { return 'win32' diff --git a/apps/desktop/scripts/stage-native-deps.test.mjs b/apps/desktop/scripts/stage-native-deps.test.mjs index 7e0b82288..65e1bb5d3 100644 --- a/apps/desktop/scripts/stage-native-deps.test.mjs +++ b/apps/desktop/scripts/stage-native-deps.test.mjs @@ -21,7 +21,9 @@ const { join } = path function makeFakeNode(filePath, platform) { const headers = { linux: Buffer.from([0x7f, 0x45, 0x4c, 0x46, 0x00, 0x00, 0x00, 0x00]), // ELF - darwin: Buffer.from([0xfe, 0xed, 0xfa, 0xcf, 0x00, 0x00, 0x00, 0x00]), // Mach-O 64-bit + // On x64/arm64 Darwin, Mach-O binaries are stored little-endian on disk + // (MH_CIGAM_64 = cffaedfe). This is the form node-pty's prebuilds ship in. + darwin: Buffer.from([0xcf, 0xfa, 0xed, 0xfe, 0x00, 0x00, 0x00, 0x00]), // Mach-O 64-bit LE (CIGAM_64) win32: Buffer.from([0x4d, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), // MZ (PE) } fs.mkdirSync(path.dirname(filePath), { recursive: true }) @@ -54,7 +56,7 @@ test('classifyNativeBinary detects ELF as linux', () => { } }) -test('classifyNativeBinary detects Mach-O 64-bit as darwin', () => { +test('classifyNativeBinary detects Mach-O 64-bit BE as darwin', () => { const tmp = fs.mkdtempSync(join(os.tmpdir(), 'hermes-stage-')) try { const f = join(tmp, 'test.node') @@ -65,7 +67,18 @@ test('classifyNativeBinary detects Mach-O 64-bit as darwin', () => { } }) -test('classifyNativeBinary detects Mach-O 32-bit as darwin', () => { +test('classifyNativeBinary detects Mach-O 64-bit LE (CIGAM_64) as darwin', () => { + const tmp = fs.mkdtempSync(join(os.tmpdir(), 'hermes-stage-')) + try { + const f = join(tmp, 'test.node') + fs.writeFileSync(f, Buffer.from([0xcf, 0xfa, 0xed, 0xfe, 0x00, 0x00])) + assert.equal(classifyNativeBinary(f), 'darwin') + } finally { + fs.rmSync(tmp, { recursive: true, force: true }) + } +}) + +test('classifyNativeBinary detects Mach-O 32-bit BE as darwin', () => { const tmp = fs.mkdtempSync(join(os.tmpdir(), 'hermes-stage-')) try { const f = join(tmp, 'test.node') @@ -76,6 +89,39 @@ test('classifyNativeBinary detects Mach-O 32-bit as darwin', () => { } }) +test('classifyNativeBinary detects Mach-O 32-bit LE (CIGAM) as darwin', () => { + const tmp = fs.mkdtempSync(join(os.tmpdir(), 'hermes-stage-')) + try { + const f = join(tmp, 'test.node') + fs.writeFileSync(f, Buffer.from([0xce, 0xfa, 0xed, 0xfe, 0x00, 0x00])) + assert.equal(classifyNativeBinary(f), 'darwin') + } finally { + fs.rmSync(tmp, { recursive: true, force: true }) + } +}) + +test('classifyNativeBinary detects Fat/Universal BE (cafebabe) as darwin', () => { + const tmp = fs.mkdtempSync(join(os.tmpdir(), 'hermes-stage-')) + try { + const f = join(tmp, 'test.node') + fs.writeFileSync(f, Buffer.from([0xca, 0xfe, 0xba, 0xbe, 0x00, 0x00])) + assert.equal(classifyNativeBinary(f), 'darwin') + } finally { + fs.rmSync(tmp, { recursive: true, force: true }) + } +}) + +test('classifyNativeBinary detects Fat/Universal LE (bebafeca / FAT_CIGAM) as darwin', () => { + const tmp = fs.mkdtempSync(join(os.tmpdir(), 'hermes-stage-')) + try { + const f = join(tmp, 'test.node') + fs.writeFileSync(f, Buffer.from([0xbe, 0xba, 0xfe, 0xca, 0x00, 0x00])) + assert.equal(classifyNativeBinary(f), 'darwin') + } finally { + fs.rmSync(tmp, { recursive: true, force: true }) + } +}) + test('classifyNativeBinary detects PE (MZ) as win32', () => { const tmp = fs.mkdtempSync(join(os.tmpdir(), 'hermes-stage-')) try {