diff --git a/apps/desktop/scripts/stage-native-deps.mjs b/apps/desktop/scripts/stage-native-deps.mjs index 8966fece6cb4d..660880374ca75 100644 --- a/apps/desktop/scripts/stage-native-deps.mjs +++ b/apps/desktop/scripts/stage-native-deps.mjs @@ -63,15 +63,20 @@ function copyGlobByExt(srcDir, destDir, extensions) { * lib/unixTerminal.js requires at a fixed relative path. Filtering this * directory by ['.node'] silently drops it — the package then looks * fine, ships fine, and crashes the first time a terminal is spawned. - * Directories are copied wholesale to also cover any nested native - * payload (e.g. a conpty/ subfolder some build layouts produce). + * Directories are copied wholesale except for Windows conpty payloads, + * which are walked file by file because Node's recursive cpSync can fail + * with EIO on that directory even when copying each file succeeds. */ function copyBuildRelease(srcDir, destDir) { if (!existsSync(srcDir)) return mkdirSync(destDir, { recursive: true }) for (const entry of readdirSync(srcDir, { withFileTypes: true })) { if (entry.isDirectory()) { - cpSync(join(srcDir, entry.name), join(destDir, entry.name), { recursive: true }) + if (entry.name === 'conpty') { + copyGlobByExt(join(srcDir, entry.name), join(destDir, entry.name), ['.dll', '.exe']) + } else { + cpSync(join(srcDir, entry.name), join(destDir, entry.name), { recursive: true }) + } continue } if (entry.name === 'spawn-helper' || /\.(node|dll|exe)$/.test(entry.name)) { @@ -231,7 +236,7 @@ export function stageNodePtyInto(srcRoot, destRoot, { platform = process.platfor mkdirSync(destPrebuild, { recursive: true }) for (const entry of readdirSync(prebuildDir, { withFileTypes: true })) { if (entry.name === 'conpty' && entry.isDirectory()) { - cpSync(join(prebuildDir, 'conpty'), join(destPrebuild, 'conpty'), { recursive: true }) + copyGlobByExt(join(prebuildDir, 'conpty'), join(destPrebuild, 'conpty'), ['.dll', '.exe']) continue } if (entry.isFile() && /\.(node|dll|exe)$/.test(entry.name)) { diff --git a/apps/desktop/scripts/stage-native-deps.test.mjs b/apps/desktop/scripts/stage-native-deps.test.mjs index 65e1bb5d3022c..d7f0d9778a570 100644 --- a/apps/desktop/scripts/stage-native-deps.test.mjs +++ b/apps/desktop/scripts/stage-native-deps.test.mjs @@ -220,6 +220,28 @@ test('cross-target: matching prebuild IS staged for a foreign target', () => { } }) +test('win32 prebuild: complete conpty payload is staged', () => { + const tmp = fs.mkdtempSync(join(os.tmpdir(), 'hermes-stage-')) + try { + const srcRoot = join(tmp, 'node-pty') + const destRoot = join(tmp, 'dest') + + makeFakeNodePty(srcRoot, { prebuildPlatform: 'win32', prebuildArch: 'x64' }) + const conptyDir = join(srcRoot, 'prebuilds', 'win32-x64', 'conpty') + fs.mkdirSync(conptyDir, { recursive: true }) + fs.writeFileSync(join(conptyDir, 'conpty.dll'), 'conpty-dll') + fs.writeFileSync(join(conptyDir, 'OpenConsole.exe'), 'open-console') + + stageNodePtyInto(srcRoot, destRoot, { platform: 'win32', arch: 'x64' }) + + const stagedConpty = join(destRoot, 'prebuilds', 'win32-x64', 'conpty') + assert.equal(fs.readFileSync(join(stagedConpty, 'conpty.dll'), 'utf8'), 'conpty-dll') + assert.equal(fs.readFileSync(join(stagedConpty, 'OpenConsole.exe'), 'utf8'), 'open-console') + } finally { + fs.rmSync(tmp, { recursive: true, force: true }) + } +}) + test('cross-target: foreign target with no prebuild throws (fail closed)', () => { const tmp = fs.mkdtempSync(join(os.tmpdir(), 'hermes-stage-')) try { @@ -262,6 +284,30 @@ test('host-target: host build/Release IS staged for a matching target', () => { } }) +test('host-target: nested conpty build payload is staged', () => { + const tmp = fs.mkdtempSync(join(os.tmpdir(), 'hermes-stage-')) + try { + const srcRoot = join(tmp, 'node-pty') + const destRoot = join(tmp, 'dest') + + makeFakeNodePty(srcRoot) + const buildReleaseDir = join(srcRoot, 'build', 'Release') + makeFakeNode(join(buildReleaseDir, 'pty.node'), process.platform) + const conptyDir = join(buildReleaseDir, 'conpty') + fs.mkdirSync(conptyDir, { recursive: true }) + fs.writeFileSync(join(conptyDir, 'conpty.dll'), 'conpty-dll') + fs.writeFileSync(join(conptyDir, 'OpenConsole.exe'), 'open-console') + + stageNodePtyInto(srcRoot, destRoot, { platform: process.platform, arch: process.arch }) + + const stagedConpty = join(destRoot, 'build', 'Release', 'conpty') + assert.equal(fs.readFileSync(join(stagedConpty, 'conpty.dll'), 'utf8'), 'conpty-dll') + assert.equal(fs.readFileSync(join(stagedConpty, 'OpenConsole.exe'), 'utf8'), 'open-console') + } finally { + fs.rmSync(tmp, { recursive: true, force: true }) + } +}) + test('validation rejects a staged binary with the wrong platform magic', () => { const tmp = fs.mkdtempSync(join(os.tmpdir(), 'hermes-stage-')) try {