fix(packaging): prune non-target native binaries (#12174)

This commit is contained in:
OrcaWin 2026-08-03 10:54:00 -07:00 committed by GitHub
parent e08eba674c
commit 128e3e335e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 101 additions and 45 deletions

View File

@ -44,6 +44,14 @@ const PARCEL_WATCHER_PLATFORM_PREFIX_BY_PLATFORM = {
linux: 'watcher-linux', linux: 'watcher-linux',
win32: 'watcher-win32' 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 TYPE_DECLARATION_ARTIFACT_RE = /\.d\.(?:c|m)?ts(?:\.map)?$/
const VERSIONED_ONNXRUNTIME_DYLIB_RE = /^libonnxruntime\.\d[\d.]*\.dylib$/ 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 // optionalDependency (e.g. @parcel/watcher-linux-x64-glibc) that the
// dependencies graph above never reaches. Include the ones installed for the // dependencies graph above never reaches. Include the ones installed for the
// build's supported architectures; afterPack pruning trims non-target // 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. // '@parcel/watcher' resolves at runtime but throws loading its binary.
const parcelWatcherDir = packages.get('@parcel/watcher') const parcelWatcherDir = packages.get('@parcel/watcher')
if (parcelWatcherDir) { if (parcelWatcherDir) {
@ -246,13 +254,44 @@ function verifyPackagedMainRuntimeDeps(resourcesDir, asar = require('@electron/a
} }
function normalizeNodePtyWindowsArch(electronArch) { function normalizeNodePtyWindowsArch(electronArch) {
if (electronArch === 'x64' || electronArch === 1) { const architecture = normalizeElectronArchitecture(electronArch)
return 'x64' if (architecture !== 'x64' && architecture !== 'arm64') {
throw new Error(`Unsupported packaged node-pty Windows architecture: ${architecture}`)
} }
if (electronArch === 'arm64' || electronArch === 3) { return architecture
return 'arm64' }
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) { function findNodePtyConptySourceDir(nodePtyDir, windowsArch) {
@ -308,14 +347,19 @@ function prunePackagedNodePty(resourcesDir, electronPlatformName, electronArch)
const allowedPrebuildPrefix = NODE_PTY_PREBUILD_PREFIX_BY_PLATFORM[electronPlatformName] const allowedPrebuildPrefix = NODE_PTY_PREBUILD_PREFIX_BY_PLATFORM[electronPlatformName]
if (allowedPrebuildPrefix) { if (allowedPrebuildPrefix) {
const prebuildsDir = join(nodePtyDir, 'prebuilds') pruneNodePtyNativeDirectories(
if (existsSync(prebuildsDir)) { join(nodePtyDir, 'prebuilds'),
for (const entry of readdirSync(prebuildsDir, { withFileTypes: true })) { allowedPrebuildPrefix,
if (entry.isDirectory() && !entry.name.startsWith(allowedPrebuildPrefix)) { electronArch,
rmSync(join(prebuildsDir, entry.name), { recursive: true, force: true }) false
} )
} // Why: sequential cross-arch rebuilds accumulate ABI-tagged outputs here.
} pruneNodePtyNativeDirectories(
join(nodePtyDir, 'bin'),
allowedPrebuildPrefix,
electronArch,
true
)
} }
if (electronPlatformName === 'win32') { 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') const parcelDir = join(resourcesDir, 'node_modules', '@parcel')
if (!existsSync(parcelDir)) { if (!existsSync(parcelDir)) {
return return
@ -336,9 +380,11 @@ function prunePackagedParcelWatcher(resourcesDir, electronPlatformName) {
// Why: we package every installed @parcel/watcher-<platform> optional // Why: we package every installed @parcel/watcher-<platform> optional
// subpackage (supportedArchitectures fetches all), but each build only needs // subpackage (supportedArchitectures fetches all), but each build only needs
// its own platform's binary. Keep the core package and the matching platform // its own platform/architecture binaries. Keep the core package and matching
// subpackages; drop the rest so a Linux serve doesn't ship macOS/Windows .node. // native variants; drop the rest.
const keepPrefix = PARCEL_WATCHER_PLATFORM_PREFIX_BY_PLATFORM[electronPlatformName] 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 })) { for (const entry of readdirSync(parcelDir, { withFileTypes: true })) {
if (!entry.isDirectory() || entry.name === 'watcher') { if (!entry.isDirectory() || entry.name === 'watcher') {
continue continue
@ -348,7 +394,11 @@ function prunePackagedParcelWatcher(resourcesDir, electronPlatformName) {
if (!entry.name.startsWith('watcher-')) { if (!entry.name.startsWith('watcher-')) {
continue continue
} }
if (keepPrefix && entry.name.startsWith(keepPrefix)) { if (
keepPrefix &&
entry.name.startsWith(keepPrefix) &&
(entry.name === targetPrefix || entry.name.startsWith(`${targetPrefix}-`))
) {
continue continue
} }
rmSync(join(parcelDir, entry.name), { recursive: true, force: true }) rmSync(join(parcelDir, entry.name), { recursive: true, force: true })
@ -395,8 +445,9 @@ function prunePackagedZodSources(resourcesDir) {
} }
function prunePackagedRuntimeNodeModules(resourcesDir, electronPlatformName, electronArch) { function prunePackagedRuntimeNodeModules(resourcesDir, electronPlatformName, electronArch) {
prunePackagedNodePty(resourcesDir, electronPlatformName, electronArch) const architecture = normalizeElectronArchitecture(electronArch)
prunePackagedParcelWatcher(resourcesDir, electronPlatformName) prunePackagedNodePty(resourcesDir, electronPlatformName, architecture)
prunePackagedParcelWatcher(resourcesDir, electronPlatformName, architecture)
prunePackagedRuntimeTypeDeclarations(resourcesDir) prunePackagedRuntimeTypeDeclarations(resourcesDir)
prunePackagedSherpaOnnx(resourcesDir, electronPlatformName) prunePackagedSherpaOnnx(resourcesDir, electronPlatformName)
prunePackagedZodSources(resourcesDir) prunePackagedZodSources(resourcesDir)

View File

@ -420,40 +420,42 @@ describe('electron-builder config', () => {
expect(findAsarEntry(['/out/main/index.js'], 'out/main/index.js')).toBe('/out/main/index.js') 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-')) const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-node-pty-prune-'))
try { 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-arm64'), { recursive: true })
await mkdir(join(prebuildsDir, 'darwin-x64'), { recursive: true }) await mkdir(join(prebuildsDir, 'darwin-x64'), { recursive: true })
await mkdir(join(prebuildsDir, 'linux-x64'), { recursive: true }) await mkdir(join(prebuildsDir, 'linux-x64'), { recursive: true })
await mkdir(join(prebuildsDir, 'win32-x64'), { recursive: true }) await mkdir(join(prebuildsDir, 'win32-x64'), { recursive: true })
await mkdir(join(resourcesDir, 'node_modules', 'node-pty', 'third_party', 'conpty'), { await mkdir(join(binDir, 'darwin-arm64-148'), { recursive: true })
recursive: true await mkdir(join(binDir, 'darwin-x64-148'), { recursive: true })
}) await mkdir(join(nodePtyDir, 'third_party', 'conpty'), {
await mkdir(join(resourcesDir, 'node_modules', 'node-pty', 'deps', 'winpty'), {
recursive: true 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([ await expect(readdir(prebuildsDir)).resolves.toEqual(['darwin-arm64'])
'darwin-arm64', await expect(readdir(binDir)).resolves.toEqual(['darwin-arm64-148'])
'darwin-x64' await expect(readdir(join(nodePtyDir, 'third_party'))).resolves.toEqual([])
]) await expect(readdir(join(nodePtyDir, 'deps'))).resolves.toEqual([])
await expect( expect(() => prunePackagedNodePty(resourcesDir, 'darwin', 4)).toThrow(
readdir(join(resourcesDir, 'node_modules', 'node-pty', 'third_party')) 'Unsupported packaged runtime architecture: 4'
).resolves.toEqual([]) )
await expect(
readdir(join(resourcesDir, 'node_modules', 'node-pty', 'deps'))
).resolves.toEqual([])
} finally { } finally {
await rm(resourcesDir, { recursive: true, force: true }) await rm(resourcesDir, { recursive: true, force: true })
} }
}) })
it('copies the Windows node-pty ConPTY runtime beside the rebuilt addon', async () => { 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}-`)) const resourcesDir = await mkdtemp(join(tmpdir(), `orca-node-pty-conpty-${arch}-`))
try { try {
const nodePtyDir = join(resourcesDir, 'node_modules', 'node-pty') 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( await expect(readFile(join(releaseDir, 'conpty', 'conpty.dll'), 'utf8')).resolves.toBe(
`dll payload ${arch}` `dll payload ${arch}`
@ -500,7 +502,7 @@ describe('electron-builder config', () => {
).toBe(true) ).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-')) const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-parcel-watcher-prune-'))
try { try {
const parcelDir = join(resourcesDir, 'node_modules', '@parcel') 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-linux-arm64-glibc'), { recursive: true })
await mkdir(join(parcelDir, 'watcher-win32-x64'), { 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([ await expect(readdir(parcelDir).then((entries) => entries.sort())).resolves.toEqual([
'watcher', 'watcher',
'watcher-linux-arm64-glibc', 'watcher-linux-arm64-glibc'
'watcher-linux-x64-glibc'
]) ])
expect(() => prunePackagedParcelWatcher(resourcesDir, 'linux', 'universal')).toThrow(
'Unsupported packaged runtime architecture: universal'
)
} finally { } finally {
await rm(resourcesDir, { recursive: true, force: true }) 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. // A hypothetical future @parcel/* runtime dep that is NOT a watcher subpackage.
await mkdir(join(parcelDir, 'transformer-js'), { recursive: true }) 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([ await expect(readdir(parcelDir).then((entries) => entries.sort())).resolves.toEqual([
'transformer-js', 'transformer-js',
@ -653,7 +657,8 @@ describe('electron-builder config', () => {
await electronBuilderConfig.afterPack({ await electronBuilderConfig.afterPack({
appOutDir: join(root, 'linux-unpacked'), appOutDir: join(root, 'linux-unpacked'),
electronPlatformName: 'linux' electronPlatformName: 'linux',
arch: 1
}) })
expect((await stat(launcherPath)).mode & 0o111).not.toBe(0) expect((await stat(launcherPath)).mode & 0o111).not.toBe(0)