diff --git a/.github/workflows/napi.yml b/.github/workflows/napi.yml index 472b027..ba2b5a6 100644 --- a/.github/workflows/napi.yml +++ b/.github/workflows/napi.yml @@ -25,35 +25,41 @@ 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 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 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/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; } } 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)); +});