From e5591e49c11a6b6dd03f709e70d99c466f525917 Mon Sep 17 00:00:00 2001 From: Colin McDonnell <3084745+colinhacks@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:44:51 -0700 Subject: [PATCH 1/3] napi: build the musl addons against a dynamic C runtime and smoke them on Alpine The musl targets link the C runtime statically by default, which cannot go into a cdylib: cargo refused both musl builds. The addon alone is built with -crt-static, as napi-rs does. The smoke moves to napi/scripts/smoke.cjs so the musl builds can run it inside node:22-alpine on the same runner. --- .github/workflows/napi.yml | 24 +++++++++++++----------- napi/scripts/smoke.cjs | 13 +++++++++++++ 2 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 napi/scripts/smoke.cjs diff --git a/.github/workflows/napi.yml b/.github/workflows/napi.yml index 472b027..18df3aa 100644 --- a/.github/workflows/napi.yml +++ b/.github/workflows/napi.yml @@ -32,6 +32,10 @@ jobs: node-version: 22 - name: Build the addon working-directory: napi + # musl links the C runtime statically by default, and a static runtime cannot go + # into a cdylib; the addon alone is built against a dynamic one. + env: + RUSTFLAGS: ${{ matrix.musl && '-C target-feature=-crt-static' || '' }} run: cargo build --locked --release --target ${{ matrix.target }} - name: Package it working-directory: napi @@ -39,21 +43,19 @@ jobs: run: node scripts/platform-package.mjs ${{ matrix.platform }} target/${{ matrix.target }}/release/${{ matrix.lib }} dist/${{ matrix.platform }} - name: Load it and install a real package # A cross-built addon cannot load on this runner; the native ones prove the whole path. - if: ${{ !contains(matrix.platform, 'musl') && matrix.platform != 'darwin-x64' && matrix.platform != 'win32-arm64' }} + if: ${{ !matrix.musl && matrix.platform != 'darwin-x64' && matrix.platform != 'win32-arm64' }} working-directory: napi shell: bash run: | cp dist/${{ matrix.platform }}/microbe.${{ matrix.platform }}.node . - node -e ' - const { install, installSync } = require("./index.js"); - const dir = require("node:path").join(process.env.RUNNER_TEMP, "napi-smoke"); - const done = installSync(["is-odd@^3"], dir); - if (done.roots[0].name !== "is-odd") throw new Error(JSON.stringify(done)); - install({ typescript: "5" }, dir).then((r) => { - if (!r.bins.tsc) throw new Error(JSON.stringify(r)); - console.log(r.roots.map((x) => x.name + "@" + x.version).join(" "), Object.keys(r.bins)); - }); - ' + node scripts/smoke.cjs + - name: Load it and install a real package, on Alpine + # The runner's Node is glibc; a musl build loads only in a musl Node. + if: matrix.musl + working-directory: napi + run: | + cp dist/${{ matrix.platform }}/microbe.${{ matrix.platform }}.node . + docker run --rm -v "$PWD:/napi" node:22-alpine node /napi/scripts/smoke.cjs - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: microbe-${{ matrix.platform }} diff --git a/napi/scripts/smoke.cjs b/napi/scripts/smoke.cjs new file mode 100644 index 0000000..32e7a2a --- /dev/null +++ b/napi/scripts/smoke.cjs @@ -0,0 +1,13 @@ +// Loads the addon beside index.js and installs real packages with it, sync and async. +// Run on a runner that can load the build, natively or inside a container. +"use strict"; +const path = require("node:path"); +const { install, installSync } = require("../index.js"); + +const dir = path.join(require("node:os").tmpdir(), "napi-smoke"); +const done = installSync(["is-odd@^3"], dir); +if (done.roots[0].name !== "is-odd") throw new Error(JSON.stringify(done)); +install({ typescript: "5" }, dir).then((r) => { + if (!r.bins.tsc) throw new Error(JSON.stringify(r)); + console.log(r.roots.map((x) => x.name + "@" + x.version).join(" "), Object.keys(r.bins)); +}); From b76bd370eccd298cee51628e9b888a4cd44278e2 Mon Sep 17 00:00:00 2001 From: Colin McDonnell <3084745+colinhacks@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:49:20 -0700 Subject: [PATCH 2/3] napi: report why a present build failed to load, not only that the platform package is missing --- napi/index.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/napi/index.js b/napi/index.js index 9bfa13a..efcbfd2 100644 --- a/napi/index.js +++ b/napi/index.js @@ -24,11 +24,12 @@ try { } catch (fromPackage) { try { native = require(`./microbe.${platform}.node`); - } catch { - const err = new Error( - `@nubjs/microbe has no build for ${platform}. Install failed for @nubjs/microbe-${platform}: ${fromPackage.message}`, - ); - err.cause = fromPackage; + } catch (fromFile) { + // A build that exists but cannot load is the failure worth reporting; a missing + // local file only means this is not a checkout. + const cause = fromFile.code === "MODULE_NOT_FOUND" ? fromPackage : fromFile; + const err = new Error(`@nubjs/microbe has no build for ${platform}: ${cause.message}`); + err.cause = cause; throw err; } } From a36b151d1d2432060c9d65cb2130922ad47ed2fa Mon Sep 17 00:00:00 2001 From: Colin McDonnell <3084745+colinhacks@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:56:22 -0700 Subject: [PATCH 3/3] napi: link the musl addons with Alpine's toolchain A musl cdylib linked on the Ubuntu runner with musl-tools did not load in a musl Node. The same crate built in rust:1-alpine with -crt-static loads in node:22-alpine and installs real packages (verified locally on arm64). --- .github/workflows/napi.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/napi.yml b/.github/workflows/napi.yml index 18df3aa..ba2b5a6 100644 --- a/.github/workflows/napi.yml +++ b/.github/workflows/napi.yml @@ -25,18 +25,22 @@ jobs: - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable with: targets: ${{ matrix.target }} - - if: matrix.musl - run: sudo apt-get update && sudo apt-get install -y musl-tools - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - name: Build the addon + if: ${{ !matrix.musl }} working-directory: napi - # musl links the C runtime statically by default, and a static runtime cannot go - # into a cdylib; the addon alone is built against a dynamic one. - env: - RUSTFLAGS: ${{ matrix.musl && '-C target-feature=-crt-static' || '' }} run: cargo build --locked --release --target ${{ matrix.target }} + - name: Build the addon on Alpine + # A musl cdylib linked on the Ubuntu runner does not load in a musl Node, so the musl + # builds link with Alpine's own toolchain. musl links the C runtime statically by + # default, and a static runtime cannot go into a cdylib, hence -crt-static. + if: matrix.musl + run: | + docker run --rm -v "$PWD:/src" -w /src/napi \ + -e RUSTFLAGS='-C target-feature=-crt-static' rust:1-alpine \ + sh -c 'apk add --no-cache musl-dev >/dev/null && cargo build --locked --release --target ${{ matrix.target }}' - name: Package it working-directory: napi shell: bash