Skip to content

feat(prebuild): build N-API artifacts, collapsing 36 CI jobs to 4 - #434

Merged
TimelordUK merged 1 commit into
masterfrom
feat/napi-prebuilds
Aug 25, 2026
Merged

feat(prebuild): build N-API artifacts, collapsing 36 CI jobs to 4#434
TimelordUK merged 1 commit into
masterfrom
feat/napi-prebuilds

Conversation

@TimelordUK

Copy link
Copy Markdown
Owner

cpp/ has been pure node-addon-api since the NAN migration — 0 of 133 files reference v8:: — which means the per-ABI build matrix has been redundant for a while. One N-API binary per platform/arch/libc serves every Node line and every Electron version.

Matrix

job before after
node (3 OS × 22/24/26) 9
electron (3 OS × 38–43) 18
alpine node (22/24/26) 3
alpine electron (38–43) 6
napi (3 OS + alpine) 4
total artifacts 36 4

prebuild-electron and prebuild-electron-alpine are removed outright; Electron consumers are served by the same napi artifact. prebuild.yml goes 529 → 352 lines.

The 4 artifacts are exactly the platforms we ship: linux-x64, win32-x64, darwin-arm64, linuxmusl-x64.

Verified end to end

Dispatched against this branch (run 32883710731) — all 5 jobs green, 4 assets uploaded to v5.2.3.

Windows, full test suite green against localdb, and the binary is byte-identical to the CI artifact:

CI artifact (release v5.2.3):  392FBB6FE2C6DD92FE71FE84A0E368E9D0D926A0A9931AFC98719498ABB39851
Installed on Windows:          392FBB6FE2C6DD92FE71FE84A0E368E9D0D926A0A9931AFC98719498ABB39851

That binary was built on Node 22 and run on Node 26.3.0 — the ABI-stability claim demonstrated rather than asserted. Previously this would have required separate node-v127/node-v137/node-v152 builds.

Also verified: prebuild-install fetches and unpacks the linux-x64 artifact (HTTP 200), the addon loads, and lib/sql.js exposes its API against it.

No regression for existing consumers. Published 5.2.3 has neither config nor binary (npm view msnodesqlv8@5.2.3 config binary → empty), so installed copies still resolve msnodesqlv8-v5.2.3-node-v127-… — confirmed by simulating that package.json against the live release. The 36 pre-existing assets are untouched; the upload was purely additive (40 total = 36 + 4).

package.json

binary.napi_versions declares the ABI floor — the single source of truth read by napi-build-utils on both builder and installer side.

config.runtime=napi flips prebuild-install over. pkg.config takes precedence over the environment (prebuild-install/rc.js:17), so tooling that sets npm_config_runtime=electron still resolves the napi artifact. This is what makes the Electron jobs unnecessary.

config.target=8 is load-bearing, not redundant. The napi target swap at prebuild-install/rc.js:47 is guarded on rc.target === process.versions.node, so anything passing an explicit target skips it and drops the raw target into the abi slot. Measured before pinning:

plain npm install       -> napi-v8-linux-x64       ok
electron-rebuild style  -> napi-v43.0.0-linux-x64  404 -> source build
explicit node target    -> napi-v24.0.0-linux-x64  404 -> source build

With it pinned, all scenarios (incl. arm64 and musl) resolve to napi-v8 with only platform/arch/libc varying.

binding.gyp

NAPI_VERSION=<(napi_build_version) pins the compiled level to the advertised floor, so it is no longer whatever the build headers happen to expose.

Added to the second defines block deliberately: the target dict has two "defines" keys, and GYP parses the file as a Python dict literal, so the later wins — BOUNDDATUM_USE_NODE_API in the first block has never reached the compiler. Confirmed against build/sqlserver.target.mk (NODE_GYP_V4 present, BOUNDDATUM_USE_NODE_API absent). Left as-is rather than merged, since merging would newly enable that define — worth a separate look.

napi_build_version% is a backstop only. Resolution order measured: prebuild's -Dnapi_build_version=8 wins over build/config.gypi (which carries the host Node's level, 10 on Node 22), which wins over the default. Note this means --build-from-source does not produce an equivalent binary — only the prebuild path is pinned to 8.

Workflow

-r napi is passed explicitly because prebuild's own rc.js:8-10 hardcodes runtime: 'node' and never reads package.json — unlike prebuild-install. --all drives the build from binary.napi_versions rather than from whichever Node is on the runner.

Not covered here

darwin-arm64 and linuxmusl-x64 are built and correctly named but have not been run against a database.

npm-publish.yml is manual and independent of prebuild.yml, so publishing 5.2.4 still means separately dispatching Prebuild Binaries for tag v5.2.4. That coupling predates this PR, but the blast radius grows: forgetting it now means every platform compiles from source rather than just some Electron users. A guard asserting napi assets exist for the tag is worth a follow-up.

🤖 Generated with Claude Code

cpp/ is pure node-addon-api -- 0 of 133 files reference v8:: -- so the
per-ABI build matrix was never necessary. One N-API binary per
platform/arch/libc serves every Node line and every Electron version.

Matrix before (36 artifacts):
  node     3 OS x node 22/24/26      =  9
  electron 3 OS x electron 38..43    = 18
  alpine   node 22/24/26             =  3
  alpine   electron 38..43           =  6

Matrix after (4 artifacts), exactly the platforms we ship:
  linux-x64, win32-x64, darwin-arm64, linuxmusl-x64

The prebuild-electron and prebuild-electron-alpine jobs are removed
outright; Electron consumers are served by the same napi artifact.

package.json
  binary.napi_versions declares the ABI floor -- the single source of
  truth read by napi-build-utils on both the builder and installer side.

  config.runtime=napi flips prebuild-install over. pkg.config takes
  precedence over the environment (prebuild-install/rc.js:17), so tooling
  that sets npm_config_runtime=electron still resolves the napi artifact.

  config.target=8 is load-bearing, not redundant. The napi target swap at
  prebuild-install/rc.js:47 is guarded on
  `rc.target === process.versions.node`, so anything passing an explicit
  target skips it and drops the raw target into the abi slot. Verified
  before pinning:

    plain npm install       -> napi-v8-linux-x64      (ok)
    electron-rebuild style  -> napi-v43.0.0-linux-x64 (404 -> source build)
    explicit node target    -> napi-v24.0.0-linux-x64 (404 -> source build)

  With config.target pinned, all four scenarios (incl. arm64 and musl)
  resolve to napi-v8 with only platform/arch/libc varying.

binding.gyp
  NAPI_VERSION=<(napi_build_version)> pins the compiled level to the
  advertised floor. Added to the *second* defines block: the target dict
  has two "defines" keys, and GYP parses as a Python dict so the later
  wins -- BOUNDDATUM_USE_NODE_API in the first block has never reached the
  compiler. Confirmed in build/sqlserver.target.mk. Left as-is rather than
  merged, since merging would newly enable that define.

  napi_build_version% is a backstop only; build/config.gypi already
  carries the host Node's level (10 on Node 22) and wins over it. prebuild
  passes -Dnapi_build_version=8, which wins over both.

workflow
  -r napi is passed explicitly because prebuild's own rc.js hardcodes
  runtime:'node' and never reads package.json -- unlike prebuild-install.
  --all drives the build from binary.napi_versions rather than from
  whichever Node is on the runner.

Verified locally end to end: `npx prebuild -r napi --all --strip` builds
with -DNAPI_VERSION=8 and writes
prebuilds/msnodesqlv8-v5.2.3-napi-v8-linux-x64.tar.gz -- byte-identical in
name to the URL prebuild-install resolves. Addon loads and lib/sql.js
exposes its API against it.

Not yet verified: the Windows, macOS and musl legs, and a real
release-asset round trip. Both need a workflow_dispatch run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@TimelordUK
TimelordUK merged commit f24d2b4 into master Aug 25, 2026
16 checks passed
@TimelordUK
TimelordUK deleted the feat/napi-prebuilds branch August 25, 2026 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant