Cross-compiling

Contents

napikit build --release

That single command builds your addon for every platform listed in .npm.platforms and lays out the npm package structure ready to publish. Zig's cross-compilation is built in: there are no toolchains to install, no Docker images, no QEMU.

The same command is also safe to run repeatedly. The build owns and reconciles the files it generates; anything you've added or edited (your seam over the addon, user fields on the main package.json, the version, files unrelated to the build) is preserved.

Output layout

The first napikit build --release writes a complete npm/ tree:

npm/<name>/
├── package.json              # main package
├── index.js                  # your seam over the addon
├── binding.js                # platform detection + dynamic require
├── index.d.ts                # only with .dts = .auto or .{ .file = ... }
└── <scope>/
    ├── binding-darwin-arm64/
    │   ├── package.json
    │   └── <name>.node
    ├── binding-linux-x64-gnu/
    │   └── ...
    └── ...                   # one binding per platform

<name> is your addon's name. <scope> is the npm scope from the .scope field of the .npm block in build.zig. Every per-platform binding lives under that scope.

Calling addLib more than once in build.zig is supported; each addon gets its own npm/<name>/ subtree and is reconciled independently. See Multiple addons in one repo.

What every release build does

napikit build --release is the only command you need during release. It cross-compiles, then reconciles npm/ against the policy in build.zig. Re-run it as often as you like; you cannot drift npm/ out of sync with build.zig.

The reconciler is conservative about your work:

File or directoryBehavior on every release build
<name>.node (each platform)Refreshed.
binding.jsRefreshed.
index.d.tsRefreshed for .auto or .{ .file = … }; removed when the package switches to .none.
package.json (main, policy fields)Refreshed: name, type, main, optionalDependencies, plus types only when declarations are enabled. The dependency keys track .platforms.
package.json (main, files)Merged: index.js and binding.js are canonical; index.d.ts is canonical only when enabled. Other entries you add (for example assets/) are preserved.
package.json (main, user fields)Preserved: description, repository, homepage, keywords, author, bugs, funding, engines, scripts, anything else you've added.
package.json (main, version)Preserved. Only napikit bump changes it. The bindings' optionalDependencies values are kept in lockstep with this version.
package.json (per-binding)Refreshed: name, os, cpu, libc, main, files. version is pinned to the main package's version.
<scope>/binding-*/Recreated to match .platforms. Bindings for removed platforms are deleted; bindings for newly added platforms are created.
<scope>/Renames cleanly. If you change .scope in build.zig, the old scope dir is removed on the next build and the new one takes its place.
index.jsSeeded once on the first release build, then preserved. Your seam: edit it freely.
Anything else under npm/<name>/Preserved. Add CHANGELOG.md, .npmignore, etc.; the build will not touch them.

The practical guarantee: edit build.zig, then re-run napikit build --release. That works for every change, including renaming the scope, adding or removing platforms, changing .dts, and renaming .host_exe. You do not need to delete npm/ first; the reconciler removes a disabled build-owned declaration and otherwise preserves unrelated user files.

The one exception is renaming the addon's .name itself. The new tree is created fresh under npm/<new-name>/, the old npm/<old-name>/ becomes an orphan, and the build prints a warning that asks you to copy any user fields you want to keep on the new main package.json and delete the old folder. (Renaming a published npm package is a rare and disruptive event; this is intentional.)

Building a subset

A full release build cross-compiles every platform for every addon. During local development you often want something faster.

--current builds only the host platform's binding:

napikit build --release --current

This compiles a single .node for the machine you are on and skips the others. The main package.json still lists every platform in optionalDependencies, and the build is additive: bindings already in npm/ from a previous full build are left in place, not deleted. Run a full napikit build --release before publishing so every platform is present.

--only narrows the build to specific addons by .name, useful when build.zig calls addLib more than once:

napikit build --release --only math,crypto

Unlisted addons are skipped entirely and their npm/ subtrees are left untouched. Combine the two flags to rebuild just one addon for just your platform:

napikit build --release --only math --current

What index.js is for

binding.js is fully owned by the build. It implements platform detection and loads the matching <scope>/binding-… package. Do not edit it; your changes will be overwritten on the next build.

index.js is your seam. The default napikit build --release writes is a plain re-export:

import binding from "./binding.js";
export default binding;

That is already a working entry point. Keep it as-is, or use it for JS-side wrapping, normalization, or higher-level helpers. Edits to index.js survive every rebuild.

Default platforms

If .platforms is omitted from .npm, you get this set:

OSArchitectureslibc
Linuxx64, arm64, armglibc and musl
macOSx64, arm64n/a
Windowsx64, arm64n/a
FreeBSDx64n/a

That is 11 binaries from one napikit build --release call.

Custom platforms

Override the default list to ship only what you need:

napi.addLib(b, napi_dep, .{
    // ...
    .npm = .{
        .scope = "@myscope",
        .platforms = &.{
            .linux_x64_gnu,
            .macos_arm64,
        },
    },
});

Adding or removing entries from .platforms is a regular edit. The next napikit build --release adds bindings for the new entries and deletes bindings for ones you took out. The full list of Platform values is in build.zig (addLib).

The Zig enum uses names such as .macos_arm64 and .windows_x64; generated npm binding suffixes still use Node/npm platform names such as darwin-arm64 and win32-x64.

Electron and other hosts

By default, addons are compiled to load into node.exe on Windows. To target Electron, set .host_exe:

napi.addLib(b, napi_dep, .{
    // ...
    .host_exe = "electron.exe",
});

This only affects Windows import-library generation. On macOS and Linux the addon loads into whatever process imports it without any host-specific configuration.

Optimization

napikit build --release uses ReleaseFast for every cross-compiled binary. For development, napikit build uses whatever -Doptimize you specify (default Debug). Override the release mode with:

napikit build --release --optimize=safe    # ReleaseSafe
napikit build --release --optimize=small   # ReleaseSmall