diff --git a/config/packaged-runtime-node-modules.cjs b/config/packaged-runtime-node-modules.cjs index 8854f866f..71ca780d1 100644 --- a/config/packaged-runtime-node-modules.cjs +++ b/config/packaged-runtime-node-modules.cjs @@ -44,6 +44,14 @@ const PARCEL_WATCHER_PLATFORM_PREFIX_BY_PLATFORM = { linux: 'watcher-linux', win32: 'watcher-win32' } +const ELECTRON_ARCHITECTURE_BY_ENUM = { + 0: 'ia32', + 1: 'x64', + 2: 'arm', + 3: 'arm64', + 4: 'universal' +} +const PACKAGED_NATIVE_ARCHITECTURES = new Set(['ia32', 'x64', 'arm', 'arm64']) const TYPE_DECLARATION_ARTIFACT_RE = /\.d\.(?:c|m)?ts(?:\.map)?$/ const VERSIONED_ONNXRUNTIME_DYLIB_RE = /^libonnxruntime\.\d[\d.]*\.dylib$/ @@ -170,7 +178,7 @@ function collectPackagedRuntimePackages(electronPlatformName = process.platform) // optionalDependency (e.g. @parcel/watcher-linux-x64-glibc) that the // dependencies graph above never reaches. Include the ones installed for the // build's supported architectures; afterPack pruning trims non-target - // platforms. Without this the packaged main bundle's import of + // platform/architecture variants. Without this the packaged main bundle's import of // '@parcel/watcher' resolves at runtime but throws loading its binary. const parcelWatcherDir = packages.get('@parcel/watcher') if (parcelWatcherDir) { @@ -246,13 +254,44 @@ function verifyPackagedMainRuntimeDeps(resourcesDir, asar = require('@electron/a } function normalizeNodePtyWindowsArch(electronArch) { - if (electronArch === 'x64' || electronArch === 1) { - return 'x64' + const architecture = normalizeElectronArchitecture(electronArch) + if (architecture !== 'x64' && architecture !== 'arm64') { + throw new Error(`Unsupported packaged node-pty Windows architecture: ${architecture}`) } - if (electronArch === 'arm64' || electronArch === 3) { - return 'arm64' + return architecture +} + +function normalizeElectronArchitecture(electronArch) { + const architecture = + typeof electronArch === 'number' + ? ELECTRON_ARCHITECTURE_BY_ENUM[electronArch] + : electronArch === 'armv7l' + ? 'arm' + : electronArch + if (!PACKAGED_NATIVE_ARCHITECTURES.has(architecture)) { + throw new Error(`Unsupported packaged runtime architecture: ${String(electronArch)}`) + } + return architecture +} + +function pruneNodePtyNativeDirectories(directory, platformPrefix, electronArch, allowsSuffix) { + if (!existsSync(directory)) { + return + } + const architecture = normalizeElectronArchitecture(electronArch) + const targetPrefix = `${platformPrefix}${architecture}` + const platformPrefixes = Object.values(NODE_PTY_PREBUILD_PREFIX_BY_PLATFORM) + for (const entry of readdirSync(directory, { withFileTypes: true })) { + if (!entry.isDirectory() || !platformPrefixes.some((prefix) => entry.name.startsWith(prefix))) { + continue + } + const matchesTarget = + entry.name.startsWith(platformPrefix) && + (entry.name === targetPrefix || (allowsSuffix && entry.name.startsWith(`${targetPrefix}-`))) + if (!matchesTarget) { + rmSync(join(directory, entry.name), { recursive: true, force: true }) + } } - return process.arch === 'arm64' ? 'arm64' : 'x64' } function findNodePtyConptySourceDir(nodePtyDir, windowsArch) { @@ -308,14 +347,19 @@ function prunePackagedNodePty(resourcesDir, electronPlatformName, electronArch) const allowedPrebuildPrefix = NODE_PTY_PREBUILD_PREFIX_BY_PLATFORM[electronPlatformName] if (allowedPrebuildPrefix) { - const prebuildsDir = join(nodePtyDir, 'prebuilds') - if (existsSync(prebuildsDir)) { - for (const entry of readdirSync(prebuildsDir, { withFileTypes: true })) { - if (entry.isDirectory() && !entry.name.startsWith(allowedPrebuildPrefix)) { - rmSync(join(prebuildsDir, entry.name), { recursive: true, force: true }) - } - } - } + pruneNodePtyNativeDirectories( + join(nodePtyDir, 'prebuilds'), + allowedPrebuildPrefix, + electronArch, + false + ) + // Why: sequential cross-arch rebuilds accumulate ABI-tagged outputs here. + pruneNodePtyNativeDirectories( + join(nodePtyDir, 'bin'), + allowedPrebuildPrefix, + electronArch, + true + ) } if (electronPlatformName === 'win32') { @@ -328,7 +372,7 @@ function prunePackagedNodePty(resourcesDir, electronPlatformName, electronArch) } } -function prunePackagedParcelWatcher(resourcesDir, electronPlatformName) { +function prunePackagedParcelWatcher(resourcesDir, electronPlatformName, electronArch) { const parcelDir = join(resourcesDir, 'node_modules', '@parcel') if (!existsSync(parcelDir)) { return @@ -336,9 +380,11 @@ function prunePackagedParcelWatcher(resourcesDir, electronPlatformName) { // Why: we package every installed @parcel/watcher- optional // subpackage (supportedArchitectures fetches all), but each build only needs - // its own platform's binary. Keep the core package and the matching platform - // subpackages; drop the rest so a Linux serve doesn't ship macOS/Windows .node. + // its own platform/architecture binaries. Keep the core package and matching + // native variants; drop the rest. const keepPrefix = PARCEL_WATCHER_PLATFORM_PREFIX_BY_PLATFORM[electronPlatformName] + const architecture = normalizeElectronArchitecture(electronArch) + const targetPrefix = keepPrefix ? `${keepPrefix}-${architecture}` : null for (const entry of readdirSync(parcelDir, { withFileTypes: true })) { if (!entry.isDirectory() || entry.name === 'watcher') { continue @@ -348,7 +394,11 @@ function prunePackagedParcelWatcher(resourcesDir, electronPlatformName) { if (!entry.name.startsWith('watcher-')) { continue } - if (keepPrefix && entry.name.startsWith(keepPrefix)) { + if ( + keepPrefix && + entry.name.startsWith(keepPrefix) && + (entry.name === targetPrefix || entry.name.startsWith(`${targetPrefix}-`)) + ) { continue } rmSync(join(parcelDir, entry.name), { recursive: true, force: true }) @@ -395,8 +445,9 @@ function prunePackagedZodSources(resourcesDir) { } function prunePackagedRuntimeNodeModules(resourcesDir, electronPlatformName, electronArch) { - prunePackagedNodePty(resourcesDir, electronPlatformName, electronArch) - prunePackagedParcelWatcher(resourcesDir, electronPlatformName) + const architecture = normalizeElectronArchitecture(electronArch) + prunePackagedNodePty(resourcesDir, electronPlatformName, architecture) + prunePackagedParcelWatcher(resourcesDir, electronPlatformName, architecture) prunePackagedRuntimeTypeDeclarations(resourcesDir) prunePackagedSherpaOnnx(resourcesDir, electronPlatformName) prunePackagedZodSources(resourcesDir) diff --git a/config/scripts/electron-builder-config.test.mjs b/config/scripts/electron-builder-config.test.mjs index 5166fdf19..3227d957b 100644 --- a/config/scripts/electron-builder-config.test.mjs +++ b/config/scripts/electron-builder-config.test.mjs @@ -420,40 +420,42 @@ describe('electron-builder config', () => { expect(findAsarEntry(['/out/main/index.js'], 'out/main/index.js')).toBe('/out/main/index.js') }) - it('prunes non-target node-pty prebuilds from packaged runtime resources', async () => { + it('prunes non-target node-pty architecture outputs from packaged runtime resources', async () => { const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-node-pty-prune-')) try { - const prebuildsDir = join(resourcesDir, 'node_modules', 'node-pty', 'prebuilds') + const nodePtyDir = join(resourcesDir, 'node_modules', 'node-pty') + const prebuildsDir = join(nodePtyDir, 'prebuilds') + const binDir = join(nodePtyDir, 'bin') await mkdir(join(prebuildsDir, 'darwin-arm64'), { recursive: true }) await mkdir(join(prebuildsDir, 'darwin-x64'), { recursive: true }) await mkdir(join(prebuildsDir, 'linux-x64'), { recursive: true }) await mkdir(join(prebuildsDir, 'win32-x64'), { recursive: true }) - await mkdir(join(resourcesDir, 'node_modules', 'node-pty', 'third_party', 'conpty'), { - recursive: true - }) - await mkdir(join(resourcesDir, 'node_modules', 'node-pty', 'deps', 'winpty'), { + await mkdir(join(binDir, 'darwin-arm64-148'), { recursive: true }) + await mkdir(join(binDir, 'darwin-x64-148'), { recursive: true }) + await mkdir(join(nodePtyDir, 'third_party', 'conpty'), { recursive: true }) + await mkdir(join(nodePtyDir, 'deps', 'winpty'), { recursive: true }) - prunePackagedNodePty(resourcesDir, 'darwin') + prunePackagedNodePty(resourcesDir, 'darwin', 3) - await expect(readdir(prebuildsDir).then((entries) => entries.sort())).resolves.toEqual([ - 'darwin-arm64', - 'darwin-x64' - ]) - await expect( - readdir(join(resourcesDir, 'node_modules', 'node-pty', 'third_party')) - ).resolves.toEqual([]) - await expect( - readdir(join(resourcesDir, 'node_modules', 'node-pty', 'deps')) - ).resolves.toEqual([]) + await expect(readdir(prebuildsDir)).resolves.toEqual(['darwin-arm64']) + await expect(readdir(binDir)).resolves.toEqual(['darwin-arm64-148']) + await expect(readdir(join(nodePtyDir, 'third_party'))).resolves.toEqual([]) + await expect(readdir(join(nodePtyDir, 'deps'))).resolves.toEqual([]) + expect(() => prunePackagedNodePty(resourcesDir, 'darwin', 4)).toThrow( + 'Unsupported packaged runtime architecture: 4' + ) } finally { await rm(resourcesDir, { recursive: true, force: true }) } }) it('copies the Windows node-pty ConPTY runtime beside the rebuilt addon', async () => { - for (const arch of ['x64', 'arm64']) { + for (const [arch, electronArch] of [ + ['x64', 1], + ['arm64', 3] + ]) { const resourcesDir = await mkdtemp(join(tmpdir(), `orca-node-pty-conpty-${arch}-`)) try { const nodePtyDir = join(resourcesDir, 'node_modules', 'node-pty') @@ -472,7 +474,7 @@ describe('electron-builder config', () => { ) } - prunePackagedNodePty(resourcesDir, 'win32', arch) + prunePackagedNodePty(resourcesDir, 'win32', electronArch) await expect(readFile(join(releaseDir, 'conpty', 'conpty.dll'), 'utf8')).resolves.toBe( `dll payload ${arch}` @@ -500,7 +502,7 @@ describe('electron-builder config', () => { ).toBe(true) }) - it('prunes non-target @parcel/watcher platform subpackages from packaged runtime resources', async () => { + it('prunes non-target @parcel/watcher architecture subpackages', async () => { const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-parcel-watcher-prune-')) try { const parcelDir = join(resourcesDir, 'node_modules', '@parcel') @@ -511,13 +513,15 @@ describe('electron-builder config', () => { await mkdir(join(parcelDir, 'watcher-linux-arm64-glibc'), { recursive: true }) await mkdir(join(parcelDir, 'watcher-win32-x64'), { recursive: true }) - prunePackagedParcelWatcher(resourcesDir, 'linux') + prunePackagedParcelWatcher(resourcesDir, 'linux', 'arm64') await expect(readdir(parcelDir).then((entries) => entries.sort())).resolves.toEqual([ 'watcher', - 'watcher-linux-arm64-glibc', - 'watcher-linux-x64-glibc' + 'watcher-linux-arm64-glibc' ]) + expect(() => prunePackagedParcelWatcher(resourcesDir, 'linux', 'universal')).toThrow( + 'Unsupported packaged runtime architecture: universal' + ) } finally { await rm(resourcesDir, { recursive: true, force: true }) } @@ -533,7 +537,7 @@ describe('electron-builder config', () => { // A hypothetical future @parcel/* runtime dep that is NOT a watcher subpackage. await mkdir(join(parcelDir, 'transformer-js'), { recursive: true }) - prunePackagedParcelWatcher(resourcesDir, 'linux') + prunePackagedParcelWatcher(resourcesDir, 'linux', 1) await expect(readdir(parcelDir).then((entries) => entries.sort())).resolves.toEqual([ 'transformer-js', @@ -653,7 +657,8 @@ describe('electron-builder config', () => { await electronBuilderConfig.afterPack({ appOutDir: join(root, 'linux-unpacked'), - electronPlatformName: 'linux' + electronPlatformName: 'linux', + arch: 1 }) expect((await stat(launcherPath)).mode & 0o111).not.toBe(0)