From d445738d74237b0717a6d1eee49e80cade1ba44f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 22:30:10 +0000 Subject: [PATCH 1/2] ci: add 32-bit Linux (ia32 + armv7l) prebuilds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Old release lines (notably Electron's armv7l builds) still install @electron-internal/extract-zip, but no 32-bit prebuild was being shipped, so it failed to load on 32-bit Linux. Add cross-compiled prebuilds for both 32-bit Linux targets: - i686-unknown-linux-gnu (linux/ia32) via gcc-multilib - armv7-unknown-linux-gnueabihf (linux/arm) via napi's --use-napi-cross Both run in the shared CI build matrix (reused by the Release workflow via workflow_call) and are flagged `cross: true` so the smoke-test step is skipped — there are no native 32-bit runners and the host's 64-bit node can't load a 32-bit .node. napi's generated loader has no `process.arch === 'ia32'` branch under Linux, so strip-binding-fallbacks.js now also splices that branch in; otherwise the ia32 prebuild would be unreachable. package.json targets and the check-prebuilds publish gate are updated to match. --- .github/workflows/ci.yml | 30 ++++++++++- package.json | 4 +- scripts/check-prebuilds.js | 2 + scripts/strip-binding-fallbacks.js | 87 ++++++++++++++++++++---------- 4 files changed, 93 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3954f1e..b325892 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,6 +122,34 @@ jobs: yarn napi build --platform --release --esm --output-dir . --target aarch64-unknown-linux-musl --js binding.js --dts binding.d.ts artifact: index.linux-arm64-musl.node + # ── Linux 32-bit (cross-compiled) ───────────────────────────────── + # Old release lines (e.g. Electron's armv7l / linux-ia32 builds) still + # install this package, so keep shipping 32-bit prebuilds. There are no + # native 32-bit runners, so these are cross-compiled and flagged + # `cross: true` to skip the smoke test below (the host's 64-bit node + # can't load a 32-bit .node). + - name: linux-ia32-gnu + os: ubuntu-latest + cross: true + # i686 isn't in @napi-rs/cross-toolchain, but an x86_64 host can target + # it directly with gcc-multilib (gcc -m32), which also builds zlib-ng. + build: | + sudo apt-get update + sudo apt-get install -y gcc-multilib g++-multilib + rustup target add i686-unknown-linux-gnu + yarn napi build --platform --release --esm --output-dir . --target i686-unknown-linux-gnu --js binding.js --dts binding.d.ts + artifact: index.linux-ia32-gnu.node + + - name: linux-arm-gnueabihf + os: ubuntu-latest + cross: true + # --use-napi-cross pulls @napi-rs/cross-toolchain and wires up the + # armv7 linker / CC / sysroot (used to cross-compile zlib-ng too). + build: | + rustup target add armv7-unknown-linux-gnueabihf + yarn napi build --platform --release --esm --output-dir . --target armv7-unknown-linux-gnueabihf --use-napi-cross --js binding.js --dts binding.d.ts + artifact: index.linux-arm-gnueabihf.node + steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -170,7 +198,7 @@ jobs: run: node scripts/strip-binding-fallbacks.js - name: Smoke test (native targets only) - if: ${{ !matrix.docker && (!contains(matrix.name, 'arm64') || contains(matrix.os, 'arm') || matrix.os == 'macos-latest') }} + if: ${{ !matrix.docker && !matrix.cross && (!contains(matrix.name, 'arm64') || contains(matrix.os, 'arm') || matrix.os == 'macos-latest') }} run: yarn test - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/package.json b/package.json index 03e79c2..3ab0ce4 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,9 @@ "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-musl", - "aarch64-unknown-linux-musl" + "aarch64-unknown-linux-musl", + "i686-unknown-linux-gnu", + "armv7-unknown-linux-gnueabihf" ] }, "scripts": { diff --git a/scripts/check-prebuilds.js b/scripts/check-prebuilds.js index 7af9a95..c052c25 100644 --- a/scripts/check-prebuilds.js +++ b/scripts/check-prebuilds.js @@ -15,6 +15,8 @@ const EXPECTED = { 'aarch64-unknown-linux-gnu': 'index.linux-arm64-gnu.node', 'x86_64-unknown-linux-musl': 'index.linux-x64-musl.node', 'aarch64-unknown-linux-musl': 'index.linux-arm64-musl.node', + 'i686-unknown-linux-gnu': 'index.linux-ia32-gnu.node', + 'armv7-unknown-linux-gnueabihf': 'index.linux-arm-gnueabihf.node', }; const dir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); diff --git a/scripts/strip-binding-fallbacks.js b/scripts/strip-binding-fallbacks.js index 0174022..64d4595 100644 --- a/scripts/strip-binding-fallbacks.js +++ b/scripts/strip-binding-fallbacks.js @@ -1,15 +1,23 @@ -// `napi build --js binding.js` emits a loader that falls back to -// `require('@electron-internal/extract-zip-')` when the bundled -// `./index..node` file is missing. We ship every supported triple in -// one fat package, so the only time the fallback fires is on an *unsupported* -// triple — at which point requiring an unscoped-by-us package is a -// dependency-confusion foothold. Strip those branches; an unsupported triple -// should hard-fail instead. +// Post-process the napi-generated `binding.js` loader before it ships. Two +// independent fix-ups, both guarded so they fail loudly if napi's generated +// output ever changes shape: // -// Strategy: rewrite each `require('@electron-internal/…')` line into a `throw`. -// 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. +// 1) Strip @electron-internal/* package fallbacks. +// `napi build --js binding.js` emits a loader that falls back to +// `require('@electron-internal/extract-zip-')` when the bundled +// `./index..node` file is missing. We ship every supported triple in +// one fat package, so the only time the fallback fires is on an *unsupported* +// triple — at which point requiring an unscoped-by-us package is a +// dependency-confusion foothold. Rewrite each such `require(...)` into a +// `throw` (still caught and surfaced by the existing aggregation), so an +// unsupported triple hard-fails with a helpful error instead. +// +// 2) Add the missing 32-bit x86 Linux branch. +// napi's loader template has no `process.arch === 'ia32'` case under Linux +// (it covers x64/arm64/arm/loong64/riscv64/ppc64/s390x only), so the +// index.linux-ia32-gnu.node prebuild we ship would otherwise be unreachable +// and 32-bit Linux would hit "Unsupported architecture on Linux". Splice in +// the branch (gnu only — we don't build a 32-bit musl prebuild). import fs from 'node:fs'; import path from 'node:path'; @@ -19,27 +27,50 @@ const dirname = path.dirname(fileURLToPath(import.meta.url)); const file = path.join(dirname, '..', 'binding.js'); let src = fs.readFileSync(file, 'utf8'); +// ── 2) Inject the linux/ia32 branch ─────────────────────────────────────── +const ia32Marker = "require('./index.linux-ia32-gnu.node')"; +if (src.includes(ia32Marker)) { + console.log('strip-binding-fallbacks: linux/ia32 branch already present'); +} else { + // Anchor on the Linux-specific "unsupported arch" else; insert before it. + const anchor = + ' } else {\n loadErrors.push(new Error(`Unsupported architecture on Linux:'; + if (!src.includes(anchor)) { + console.error('strip-binding-fallbacks: linux unsupported-arch anchor not found — generator output changed?'); + process.exit(1); + } + const ia32Branch = + " } else if (process.arch === 'ia32') {\n" + + ' try {\n' + + ` return ${ia32Marker}\n` + + ' } catch (e) {\n' + + ' loadErrors.push(e)\n' + + ' }\n'; + src = src.replace(anchor, ia32Branch + anchor); + console.log('strip-binding-fallbacks: added linux/ia32 branch to binding.js'); +} + +// ── 1) Strip @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); + } + console.log('strip-binding-fallbacks: removed package-name fallbacks from binding.js'); } fs.writeFileSync(file, src); -console.log('strip-binding-fallbacks: removed package-name fallbacks from binding.js'); From 923fc8a599faec26030e3ed2928c4eac19a01f87 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 23:02:29 +0000 Subject: [PATCH 2/2] ci: generate linux/ia32 loader branch via @napi-rs/cli patch Address review feedback: instead of splicing the missing 32-bit x86 Linux branch into the generated binding.js from strip-binding-fallbacks.js, patch @napi-rs/cli's loader template directly (via Yarn's patch protocol) so napi emits the `process.arch === 'ia32'` branch natively, consistent with every other arch (including its @electron-internal fallback, which the existing strip step then removes). strip-binding-fallbacks.js is reverted to its original single responsibility. --- .../@napi-rs-cli-npm-3.6.2-b710c59d43.patch | 39 +++++++++ package.json | 2 +- scripts/strip-binding-fallbacks.js | 87 ++++++------------- yarn.lock | 32 ++++++- 4 files changed, 98 insertions(+), 62 deletions(-) create mode 100644 .yarn/patches/@napi-rs-cli-npm-3.6.2-b710c59d43.patch 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 new file mode 100644 index 0000000..88f58e8 --- /dev/null +++ b/.yarn/patches/@napi-rs-cli-npm-3.6.2-b710c59d43.patch @@ -0,0 +1,39 @@ +diff --git a/dist/cli.js b/dist/cli.js +index 5787d8d0398b48987f9e59a628770259cf9c830c..260f2a7f742c3a14fbabb7b67c6121311a99a703 100755 +--- a/dist/cli.js ++++ b/dist/cli.js +@@ -802,6 +802,8 @@ function requireNative() { + ${requireTuple("linux-ppc64-gnu")} + } else if (process.arch === 's390x') { + ${requireTuple("linux-s390x-gnu")} ++ } else if (process.arch === 'ia32') { ++ ${requireTuple("linux-ia32-gnu")} + } else { + loadErrors.push(new Error(\`Unsupported architecture on Linux: \${process.arch}\`)) + } +diff --git a/dist/index.cjs b/dist/index.cjs +index 8f467564ab9b95d009c562cb9a78aa513b6d2bb2..291d75aca1095ce67e80ced2de16b5cd59c161c0 100644 +--- a/dist/index.cjs ++++ b/dist/index.cjs +@@ -827,6 +827,8 @@ function requireNative() { + ${requireTuple("linux-ppc64-gnu")} + } else if (process.arch === 's390x') { + ${requireTuple("linux-s390x-gnu")} ++ } else if (process.arch === 'ia32') { ++ ${requireTuple("linux-ia32-gnu")} + } else { + loadErrors.push(new Error(\`Unsupported architecture on Linux: \${process.arch}\`)) + } +diff --git a/dist/index.js b/dist/index.js +index e2bfc6368247a58ea3a712bb156e15b440bf139a..f74f13e7189be927cedbd4fd5ba9dd4ca335e6f9 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -801,6 +801,8 @@ function requireNative() { + ${requireTuple("linux-ppc64-gnu")} + } else if (process.arch === 's390x') { + ${requireTuple("linux-s390x-gnu")} ++ } else if (process.arch === 'ia32') { ++ ${requireTuple("linux-ia32-gnu")} + } else { + loadErrors.push(new Error(\`Unsupported architecture on Linux: \${process.arch}\`)) + } diff --git a/package.json b/package.json index 3ab0ce4..ad0bdab 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "url": "https://github.com/electron/extract-zip" }, "devDependencies": { - "@napi-rs/cli": "^3.6.2", + "@napi-rs/cli": "patch:@napi-rs/cli@npm%3A3.6.2#~/.yarn/patches/@napi-rs-cli-npm-3.6.2-b710c59d43.patch", "extract-zip": "2.0.1", "yazl": "3.3.1" } diff --git a/scripts/strip-binding-fallbacks.js b/scripts/strip-binding-fallbacks.js index 64d4595..0174022 100644 --- a/scripts/strip-binding-fallbacks.js +++ b/scripts/strip-binding-fallbacks.js @@ -1,23 +1,15 @@ -// Post-process the napi-generated `binding.js` loader before it ships. Two -// independent fix-ups, both guarded so they fail loudly if napi's generated -// output ever changes shape: +// `napi build --js binding.js` emits a loader that falls back to +// `require('@electron-internal/extract-zip-')` when the bundled +// `./index..node` file is missing. We ship every supported triple in +// one fat package, so the only time the fallback fires is on an *unsupported* +// triple — at which point requiring an unscoped-by-us package is a +// dependency-confusion foothold. Strip those branches; an unsupported triple +// should hard-fail instead. // -// 1) Strip @electron-internal/* package fallbacks. -// `napi build --js binding.js` emits a loader that falls back to -// `require('@electron-internal/extract-zip-')` when the bundled -// `./index..node` file is missing. We ship every supported triple in -// one fat package, so the only time the fallback fires is on an *unsupported* -// triple — at which point requiring an unscoped-by-us package is a -// dependency-confusion foothold. Rewrite each such `require(...)` into a -// `throw` (still caught and surfaced by the existing aggregation), so an -// unsupported triple hard-fails with a helpful error instead. -// -// 2) Add the missing 32-bit x86 Linux branch. -// napi's loader template has no `process.arch === 'ia32'` case under Linux -// (it covers x64/arm64/arm/loong64/riscv64/ppc64/s390x only), so the -// index.linux-ia32-gnu.node prebuild we ship would otherwise be unreachable -// and 32-bit Linux would hit "Unsupported architecture on Linux". Splice in -// the branch (gnu only — we don't build a 32-bit musl prebuild). +// Strategy: rewrite each `require('@electron-internal/…')` line into a `throw`. +// 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. import fs from 'node:fs'; import path from 'node:path'; @@ -27,50 +19,27 @@ const dirname = path.dirname(fileURLToPath(import.meta.url)); const file = path.join(dirname, '..', 'binding.js'); let src = fs.readFileSync(file, 'utf8'); -// ── 2) Inject the linux/ia32 branch ─────────────────────────────────────── -const ia32Marker = "require('./index.linux-ia32-gnu.node')"; -if (src.includes(ia32Marker)) { - console.log('strip-binding-fallbacks: linux/ia32 branch already present'); -} else { - // Anchor on the Linux-specific "unsupported arch" else; insert before it. - const anchor = - ' } else {\n loadErrors.push(new Error(`Unsupported architecture on Linux:'; - if (!src.includes(anchor)) { - console.error('strip-binding-fallbacks: linux unsupported-arch anchor not found — generator output changed?'); - process.exit(1); - } - const ia32Branch = - " } else if (process.arch === 'ia32') {\n" + - ' try {\n' + - ` return ${ia32Marker}\n` + - ' } catch (e) {\n' + - ' loadErrors.push(e)\n' + - ' }\n'; - src = src.replace(anchor, ia32Branch + anchor); - console.log('strip-binding-fallbacks: added linux/ia32 branch to binding.js'); -} - -// ── 1) Strip @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); - } - console.log('strip-binding-fallbacks: removed package-name fallbacks from binding.js'); +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'); diff --git a/yarn.lock b/yarn.lock index 0660750..f27a28d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,7 +9,7 @@ __metadata: version: 0.0.0-use.local resolution: "@electron-internal/extract-zip@workspace:." dependencies: - "@napi-rs/cli": "npm:^3.6.2" + "@napi-rs/cli": "patch:@napi-rs/cli@npm%3A3.6.2#~/.yarn/patches/@napi-rs-cli-npm-3.6.2-b710c59d43.patch" extract-zip: "npm:2.0.1" yazl: "npm:3.3.1" languageName: unknown @@ -256,7 +256,7 @@ __metadata: languageName: node linkType: hard -"@napi-rs/cli@npm:^3.6.2": +"@napi-rs/cli@npm:3.6.2": version: 3.6.2 resolution: "@napi-rs/cli@npm:3.6.2" dependencies: @@ -284,6 +284,34 @@ __metadata: languageName: node linkType: hard +"@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" + dependencies: + "@inquirer/prompts": "npm:^8.0.0" + "@napi-rs/cross-toolchain": "npm:^1.0.3" + "@napi-rs/wasm-tools": "npm:^1.0.1" + "@octokit/rest": "npm:^22.0.1" + clipanion: "npm:^4.0.0-rc.4" + colorette: "npm:^2.0.20" + emnapi: "npm:^1.9.1" + es-toolkit: "npm:^1.41.0" + js-yaml: "npm:^4.1.0" + obug: "npm:^2.0.0" + semver: "npm:^7.7.3" + typanion: "npm:^3.14.0" + peerDependencies: + "@emnapi/runtime": ^1.7.1 + peerDependenciesMeta: + "@emnapi/runtime": + optional: true + bin: + napi: dist/cli.js + napi-raw: cli.mjs + checksum: 10c0/73681d26ce5100769c231f12494ef4af673455e073b72507d0404a8e927342dcfd49df8e612dc517e263add37acab4832203fd6965825e7e871ea93796f9e003 + languageName: node + linkType: hard + "@napi-rs/cross-toolchain@npm:^1.0.3": version: 1.0.3 resolution: "@napi-rs/cross-toolchain@npm:1.0.3"