From 5bad1bfbbc04907509095b360f4ab8ebb09119b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Jun 2026 21:45:49 +0000 Subject: [PATCH 1/5] fix: fall back to musl binary when gnu prebuild fails to load Before: the napi-rs-generated loader selects between the gnu and musl binaries purely by libc *family* (`isMusl()`), with no glibc-version check and no cross-family fallback. On a glibc (gnu) Linux host whose system glibc is older than what the gnu prebuild was linked against, `require('./index.linux--gnu.node')` throws (e.g. `GLIBC_2.33 not found`); the loader records the error and gives up, hard-failing install even though we also ship a statically-linked musl binary that runs fine on glibc hosts. After: the binding post-processor injects a fallback into the linux gnu (x64 and arm64) branches so that, when the gnu `.node` require fails, the loader also tries the bundled `./index.linux--musl.node` before giving up. The musl require is wrapped in its own try/catch and pushes to `loadErrors` on failure, matching the existing generated style. The change lives in scripts/strip-binding-fallbacks.js (which already post-processes the generated binding.js) so it survives regeneration, rather than being a hand-edit of generated output that would be clobbered on the next build. --- scripts/strip-binding-fallbacks.js | 97 ++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 17 deletions(-) diff --git a/scripts/strip-binding-fallbacks.js b/scripts/strip-binding-fallbacks.js index 0174022..6d97541 100644 --- a/scripts/strip-binding-fallbacks.js +++ b/scripts/strip-binding-fallbacks.js @@ -10,6 +10,17 @@ // This keeps the try/catch structure intact (the throw is caught and pushed to // `loadErrors`, then surfaced by the existing aggregation), so the user still // gets a helpful error. +// +// On top of that, we inject a musl fallback into the linux gnu (glibc) arch +// branches. The generated loader picks gnu vs musl purely by libc *family* +// (`isMusl()`), with no glibc-version check. On a glibc host whose system glibc +// is older than what our gnu prebuild was linked against, requiring +// `./index.linux--gnu.node` throws (e.g. `GLIBC_2.33 not found`) and the +// loader gives up — even though we also bundle a statically-linked musl binary +// that runs fine on glibc hosts. So after the gnu `.node` require fails we also +// try the bundled `./index.linux--musl.node` before giving up. This is a +// defense-in-depth safety net: it can only ever turn a hard install failure +// into a working load, and never changes behaviour when the gnu binary loads. import fs from 'node:fs'; import path from 'node:path'; @@ -19,27 +30,79 @@ const dirname = path.dirname(fileURLToPath(import.meta.url)); const file = path.join(dirname, '..', 'binding.js'); let src = fs.readFileSync(file, 'utf8'); +// --- Step 1: inject the gnu -> musl fallback for the 64-bit linux arches ----- +// +// We target the gnu `.node` require together with the `catch` that pushes the +// failure to `loadErrors`, and rewrite the catch so that — after recording the +// gnu failure — it also attempts the bundled musl binary. The musl require is +// wrapped in its own try/catch that pushes to `loadErrors` on failure, matching +// the existing generated style. +const MUSL_FALLBACK_ARCHES = ['x64', 'arm64']; +for (const arch of MUSL_FALLBACK_ARCHES) { + const sentinel = `return require('./index.linux-${arch}-musl.node')`; + // The gnu require + its catch, captured so we can preserve the generator's + // exact indentation when we rewrite it. + const re = new RegExp( + `(?[ \\t]*)return require\\('\\.\\/index\\.linux-${arch}-gnu\\.node'\\)\\r?\\n` + + `(?[ \\t]*)\\} catch \\(e\\) \\{\\r?\\n` + + `(?[ \\t]*)loadErrors\\.push\\(e\\)\\r?\\n` + + `(?[ \\t]*)\\}`, + ); + const before = src; + src = src.replace(re, (match, ind, cind, bind, eind) => { + // ind = indentation of the `return require(...)` line + // cind = indentation of the `} catch (e) {` line + // bind = indentation of the `loadErrors.push(e)` line + // eind = indentation of the closing `}` + return ( + `${ind}return require('./index.linux-${arch}-gnu.node')\n` + + `${cind}} catch (e) {\n` + + `${bind}loadErrors.push(e)\n` + + // The bundled musl binary is statically linked, so it runs on glibc + // hosts too. Fall back to it when the gnu prebuild fails to load. + `${bind}try {\n` + + `${bind} return require('./index.linux-${arch}-musl.node')\n` + + `${bind}} catch (e) {\n` + + `${bind} loadErrors.push(e)\n` + + `${bind}}\n` + + `${eind}}` + ); + }); + if (src === before) { + console.error( + `strip-binding-fallbacks: could not inject musl fallback for ${arch} — generator output changed?`, + ); + process.exit(1); + } + if (!src.includes(sentinel)) { + console.error( + `strip-binding-fallbacks: musl fallback for ${arch} missing after rewrite — refusing to write`, + ); + process.exit(1); + } +} + +// --- Step 2: strip the @electron-internal/* package fallbacks ---------------- const sentinel = "require('@electron-internal/"; if (!src.includes(sentinel)) { console.log('strip-binding-fallbacks: no package fallbacks found (already stripped)'); - process.exit(0); -} - -const before = src; -src = src.replace( - /require\('(@electron-internal\/[^']+)'\)/g, - (_, pkg) => - `(() => { throw new Error('prebuild for this platform is not bundled (and ${pkg} is intentionally not published)') })()`, -); +} else { + const before = src; + src = src.replace( + /require\('(@electron-internal\/[^']+)'\)/g, + (_, pkg) => + `(() => { throw new Error('prebuild for this platform is not bundled (and ${pkg} is intentionally not published)') })()`, + ); -if (src === before) { - console.error('strip-binding-fallbacks: pattern matched nothing — generator output changed?'); - process.exit(1); -} -if (src.includes(sentinel)) { - console.error('strip-binding-fallbacks: leftover @electron-internal require — refusing to write'); - process.exit(1); + if (src === before) { + console.error('strip-binding-fallbacks: pattern matched nothing — generator output changed?'); + process.exit(1); + } + if (src.includes(sentinel)) { + console.error('strip-binding-fallbacks: leftover @electron-internal require — refusing to write'); + process.exit(1); + } } fs.writeFileSync(file, src); -console.log('strip-binding-fallbacks: removed package-name fallbacks from binding.js'); +console.log('strip-binding-fallbacks: removed package-name fallbacks and added musl fallback to binding.js'); From 83f159e86a999e4902e04df713850c05688389a2 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 21:52:04 +0000 Subject: [PATCH 2/5] fix(scripts): skip musl fallback injection when gnu block absent The gnu->musl fallback injection in strip-binding-fallbacks.js required the linux--gnu require block to be present and process.exit(1)'d otherwise. Per-target musl builds (napi build --target *-musl) emit a loader with no gnu require block, so the guard tripped and failed the linux-x64-musl / linux-arm64-musl CI jobs. Make the injection tolerant: if an arch's gnu block is found, inject the musl fallback as before; if it's absent, skip that arch and continue. The fallback is only meaningful when a gnu block exists, so skipping is correct. The Step 2 package-name fallback strip is unchanged. --- scripts/strip-binding-fallbacks.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/strip-binding-fallbacks.js b/scripts/strip-binding-fallbacks.js index 6d97541..750cd12 100644 --- a/scripts/strip-binding-fallbacks.js +++ b/scripts/strip-binding-fallbacks.js @@ -69,10 +69,14 @@ for (const arch of MUSL_FALLBACK_ARCHES) { ); }); if (src === before) { - console.error( - `strip-binding-fallbacks: could not inject musl fallback for ${arch} — generator output changed?`, + // Per-target musl builds (`napi build --target *-musl`) emit a loader with + // no `linux-${arch}-gnu` require block, so there is nothing to graft the + // musl fallback onto. That's fine — the fallback is only meaningful when a + // gnu block exists — so skip this arch instead of failing the build. + console.log( + `strip-binding-fallbacks: no linux-${arch}-gnu block found — skipping musl fallback for ${arch}`, ); - process.exit(1); + continue; } if (!src.includes(sentinel)) { console.error( From 0140d1a52514ab19619b4138d0af9b6eca8bc6fb Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:04:57 +0000 Subject: [PATCH 3/5] Revert regex musl injection from strip-binding-fallbacks.js This restores the script to its pre-#9 behavior: it only strips the @electron-internal/* package-name fallbacks from the generated binding.js. The gnu->musl fallback is being moved to a @napi-rs/cli patch (mirroring how #7 added the ia32 branch), so the runtime string-rewrite injection is no longer needed here. --- scripts/strip-binding-fallbacks.js | 101 +++++------------------------ 1 file changed, 17 insertions(+), 84 deletions(-) diff --git a/scripts/strip-binding-fallbacks.js b/scripts/strip-binding-fallbacks.js index 750cd12..0174022 100644 --- a/scripts/strip-binding-fallbacks.js +++ b/scripts/strip-binding-fallbacks.js @@ -10,17 +10,6 @@ // This keeps the try/catch structure intact (the throw is caught and pushed to // `loadErrors`, then surfaced by the existing aggregation), so the user still // gets a helpful error. -// -// On top of that, we inject a musl fallback into the linux gnu (glibc) arch -// branches. The generated loader picks gnu vs musl purely by libc *family* -// (`isMusl()`), with no glibc-version check. On a glibc host whose system glibc -// is older than what our gnu prebuild was linked against, requiring -// `./index.linux--gnu.node` throws (e.g. `GLIBC_2.33 not found`) and the -// loader gives up — even though we also bundle a statically-linked musl binary -// that runs fine on glibc hosts. So after the gnu `.node` require fails we also -// try the bundled `./index.linux--musl.node` before giving up. This is a -// defense-in-depth safety net: it can only ever turn a hard install failure -// into a working load, and never changes behaviour when the gnu binary loads. import fs from 'node:fs'; import path from 'node:path'; @@ -30,83 +19,27 @@ const dirname = path.dirname(fileURLToPath(import.meta.url)); const file = path.join(dirname, '..', 'binding.js'); let src = fs.readFileSync(file, 'utf8'); -// --- Step 1: inject the gnu -> musl fallback for the 64-bit linux arches ----- -// -// We target the gnu `.node` require together with the `catch` that pushes the -// failure to `loadErrors`, and rewrite the catch so that — after recording the -// gnu failure — it also attempts the bundled musl binary. The musl require is -// wrapped in its own try/catch that pushes to `loadErrors` on failure, matching -// the existing generated style. -const MUSL_FALLBACK_ARCHES = ['x64', 'arm64']; -for (const arch of MUSL_FALLBACK_ARCHES) { - const sentinel = `return require('./index.linux-${arch}-musl.node')`; - // The gnu require + its catch, captured so we can preserve the generator's - // exact indentation when we rewrite it. - const re = new RegExp( - `(?[ \\t]*)return require\\('\\.\\/index\\.linux-${arch}-gnu\\.node'\\)\\r?\\n` + - `(?[ \\t]*)\\} catch \\(e\\) \\{\\r?\\n` + - `(?[ \\t]*)loadErrors\\.push\\(e\\)\\r?\\n` + - `(?[ \\t]*)\\}`, - ); - const before = src; - src = src.replace(re, (match, ind, cind, bind, eind) => { - // ind = indentation of the `return require(...)` line - // cind = indentation of the `} catch (e) {` line - // bind = indentation of the `loadErrors.push(e)` line - // eind = indentation of the closing `}` - return ( - `${ind}return require('./index.linux-${arch}-gnu.node')\n` + - `${cind}} catch (e) {\n` + - `${bind}loadErrors.push(e)\n` + - // The bundled musl binary is statically linked, so it runs on glibc - // hosts too. Fall back to it when the gnu prebuild fails to load. - `${bind}try {\n` + - `${bind} return require('./index.linux-${arch}-musl.node')\n` + - `${bind}} catch (e) {\n` + - `${bind} loadErrors.push(e)\n` + - `${bind}}\n` + - `${eind}}` - ); - }); - if (src === before) { - // Per-target musl builds (`napi build --target *-musl`) emit a loader with - // no `linux-${arch}-gnu` require block, so there is nothing to graft the - // musl fallback onto. That's fine — the fallback is only meaningful when a - // gnu block exists — so skip this arch instead of failing the build. - console.log( - `strip-binding-fallbacks: no linux-${arch}-gnu block found — skipping musl fallback for ${arch}`, - ); - continue; - } - if (!src.includes(sentinel)) { - console.error( - `strip-binding-fallbacks: musl fallback for ${arch} missing after rewrite — refusing to write`, - ); - process.exit(1); - } -} - -// --- Step 2: strip the @electron-internal/* package fallbacks ---------------- const sentinel = "require('@electron-internal/"; if (!src.includes(sentinel)) { console.log('strip-binding-fallbacks: no package fallbacks found (already stripped)'); -} else { - const before = src; - src = src.replace( - /require\('(@electron-internal\/[^']+)'\)/g, - (_, pkg) => - `(() => { throw new Error('prebuild for this platform is not bundled (and ${pkg} is intentionally not published)') })()`, - ); + process.exit(0); +} + +const before = src; +src = src.replace( + /require\('(@electron-internal\/[^']+)'\)/g, + (_, pkg) => + `(() => { throw new Error('prebuild for this platform is not bundled (and ${pkg} is intentionally not published)') })()`, +); - if (src === before) { - console.error('strip-binding-fallbacks: pattern matched nothing — generator output changed?'); - process.exit(1); - } - if (src.includes(sentinel)) { - console.error('strip-binding-fallbacks: leftover @electron-internal require — refusing to write'); - process.exit(1); - } +if (src === before) { + console.error('strip-binding-fallbacks: pattern matched nothing — generator output changed?'); + process.exit(1); +} +if (src.includes(sentinel)) { + console.error('strip-binding-fallbacks: leftover @electron-internal require — refusing to write'); + process.exit(1); } fs.writeFileSync(file, src); -console.log('strip-binding-fallbacks: removed package-name fallbacks and added musl fallback to binding.js'); +console.log('strip-binding-fallbacks: removed package-name fallbacks from binding.js'); From ddaea6f482a86511f12e909316f76d03ba99bc83 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:05:22 +0000 Subject: [PATCH 4/5] Add gnu->musl loader fallback via @napi-rs/cli patch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror PR #7's approach (which added the linux-ia32-gnu branch by patching the napi-rs loader template) and add a gnu->musl fallback the same way, instead of the runtime regex string-rewrite that was in strip-binding-fallbacks.js. The generated loader picks gnu vs musl purely by libc *family* (isMusl()), with no glibc-version check. On a glibc host whose system glibc is older than what our gnu prebuild was linked against, requiring ./index.linux--gnu.node throws (e.g. GLIBC_2.33 not found) and the loader gives up — even though we also bundle a statically-linked musl binary that runs fine on glibc hosts. This patches @napi-rs/cli's requireNative() template so the linux x64 and arm64 gnu (else) branches also try ./index.linux--musl.node after the gnu require fails, using the generator's own requireTuple() helper so the emitted code matches the generated style exactly. Because the fallback is baked into the template at install time, the generated binding.js comes out correct on every build regardless of how many times the build runs — avoiding the docker+host double-pass idempotency problem that affected the regex post-processor. --- .../@napi-rs-cli-npm-3.6.2-b710c59d43.patch | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/.yarn/patches/@napi-rs-cli-npm-3.6.2-b710c59d43.patch b/.yarn/patches/@napi-rs-cli-npm-3.6.2-b710c59d43.patch index 88f58e8..ca9f788 100644 --- a/.yarn/patches/@napi-rs-cli-npm-3.6.2-b710c59d43.patch +++ b/.yarn/patches/@napi-rs-cli-npm-3.6.2-b710c59d43.patch @@ -1,8 +1,23 @@ diff --git a/dist/cli.js b/dist/cli.js -index 5787d8d0398b48987f9e59a628770259cf9c830c..260f2a7f742c3a14fbabb7b67c6121311a99a703 100755 +index 5787d8d..2cb0557 100755 --- a/dist/cli.js +++ b/dist/cli.js -@@ -802,6 +802,8 @@ function requireNative() { +@@ -773,12 +773,14 @@ function requireNative() { + ${requireTuple("linux-x64-musl", 10)} + } else { + ${requireTuple("linux-x64-gnu", 10)} ++ ${requireTuple("linux-x64-musl", 10)} + } + } else if (process.arch === 'arm64') { + if (isMusl()) { + ${requireTuple("linux-arm64-musl", 10)} + } else { + ${requireTuple("linux-arm64-gnu", 10)} ++ ${requireTuple("linux-arm64-musl", 10)} + } + } else if (process.arch === 'arm') { + if (isMusl()) { +@@ -802,6 +804,8 @@ function requireNative() { ${requireTuple("linux-ppc64-gnu")} } else if (process.arch === 's390x') { ${requireTuple("linux-s390x-gnu")} @@ -12,10 +27,25 @@ index 5787d8d0398b48987f9e59a628770259cf9c830c..260f2a7f742c3a14fbabb7b67c612131 loadErrors.push(new Error(\`Unsupported architecture on Linux: \${process.arch}\`)) } diff --git a/dist/index.cjs b/dist/index.cjs -index 8f467564ab9b95d009c562cb9a78aa513b6d2bb2..291d75aca1095ce67e80ced2de16b5cd59c161c0 100644 +index 8f46756..ac31f59 100644 --- a/dist/index.cjs +++ b/dist/index.cjs -@@ -827,6 +827,8 @@ function requireNative() { +@@ -798,12 +798,14 @@ function requireNative() { + ${requireTuple("linux-x64-musl", 10)} + } else { + ${requireTuple("linux-x64-gnu", 10)} ++ ${requireTuple("linux-x64-musl", 10)} + } + } else if (process.arch === 'arm64') { + if (isMusl()) { + ${requireTuple("linux-arm64-musl", 10)} + } else { + ${requireTuple("linux-arm64-gnu", 10)} ++ ${requireTuple("linux-arm64-musl", 10)} + } + } else if (process.arch === 'arm') { + if (isMusl()) { +@@ -827,6 +829,8 @@ function requireNative() { ${requireTuple("linux-ppc64-gnu")} } else if (process.arch === 's390x') { ${requireTuple("linux-s390x-gnu")} @@ -25,10 +55,25 @@ index 8f467564ab9b95d009c562cb9a78aa513b6d2bb2..291d75aca1095ce67e80ced2de16b5cd loadErrors.push(new Error(\`Unsupported architecture on Linux: \${process.arch}\`)) } diff --git a/dist/index.js b/dist/index.js -index e2bfc6368247a58ea3a712bb156e15b440bf139a..f74f13e7189be927cedbd4fd5ba9dd4ca335e6f9 100644 +index e2bfc63..263ceae 100644 --- a/dist/index.js +++ b/dist/index.js -@@ -801,6 +801,8 @@ function requireNative() { +@@ -772,12 +772,14 @@ function requireNative() { + ${requireTuple("linux-x64-musl", 10)} + } else { + ${requireTuple("linux-x64-gnu", 10)} ++ ${requireTuple("linux-x64-musl", 10)} + } + } else if (process.arch === 'arm64') { + if (isMusl()) { + ${requireTuple("linux-arm64-musl", 10)} + } else { + ${requireTuple("linux-arm64-gnu", 10)} ++ ${requireTuple("linux-arm64-musl", 10)} + } + } else if (process.arch === 'arm') { + if (isMusl()) { +@@ -801,6 +803,8 @@ function requireNative() { ${requireTuple("linux-ppc64-gnu")} } else if (process.arch === 's390x') { ${requireTuple("linux-s390x-gnu")} From 4ec2a1afbddbf45f7146019990dab98f4361c6a6 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:11:05 +0000 Subject: [PATCH 5/5] Update yarn.lock for @napi-rs/cli patch content change Regenerated by `yarn install --mode update-lockfile` after editing the @napi-rs/cli patch. Only the patched-package resolution hash and checksum change (the patch now also adds the gnu->musl fallback). Keeps `yarn install --immutable` green in CI. --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index f27a28d..971256a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -286,7 +286,7 @@ __metadata: "@napi-rs/cli@patch:@napi-rs/cli@npm%3A3.6.2#~/.yarn/patches/@napi-rs-cli-npm-3.6.2-b710c59d43.patch": version: 3.6.2 - resolution: "@napi-rs/cli@patch:@napi-rs/cli@npm%3A3.6.2#~/.yarn/patches/@napi-rs-cli-npm-3.6.2-b710c59d43.patch::version=3.6.2&hash=058488" + resolution: "@napi-rs/cli@patch:@napi-rs/cli@npm%3A3.6.2#~/.yarn/patches/@napi-rs-cli-npm-3.6.2-b710c59d43.patch::version=3.6.2&hash=f47b21" dependencies: "@inquirer/prompts": "npm:^8.0.0" "@napi-rs/cross-toolchain": "npm:^1.0.3" @@ -308,7 +308,7 @@ __metadata: bin: napi: dist/cli.js napi-raw: cli.mjs - checksum: 10c0/73681d26ce5100769c231f12494ef4af673455e073b72507d0404a8e927342dcfd49df8e612dc517e263add37acab4832203fd6965825e7e871ea93796f9e003 + checksum: 10c0/6085b03f54cf2fd80936852ca2e2aca94a9485d25175851a34f6e9fd7ceec6fb7f340ebd00ee76900c1c1e6cc43afd3edf8f24a1051462af694aca612050913f languageName: node linkType: hard