Build
Build the NAPI-RS project
Usage
# CLI
napi build [--options]
// Programmatically
import { NapiCli } from '@napi-rs/cli'
new NapiCli().build({
// options
})
Options
| Options | CLI Options | type | required | default | description |
|---|---|---|---|---|---|
| --help,-h | get help | ||||
| target | --target,-t | string | false | Build for the target triple, bypassed to cargo build --target | |
| cwd | --cwd | string | false | The working directory of where napi command will be executed in, all other paths options are relative to this path | |
| manifestPath | --manifest-path | string | false | Path to Cargo.toml | |
| configPath | --config-path,-c | string | false | Path to napi config json file | |
| packageJsonPath | --package-json-path | string | false | Path to package.json | |
| targetDir | --target-dir | string | false | Directory for all crate generated artifacts, see cargo build --target-dir | |
| outputDir | --output-dir,-o | string | false | Path to where all the built files would be put. Default to the crate folder | |
| platform | --platform | boolean | false | Add platform triple to the generated nodejs binding file, eg: [name].linux-x64-gnu.node | |
| jsPackageName | --js-package-name | string | false | Package name in generated js binding file. Only works with --platform flag | |
| constEnum | --const-enum | boolean | false | Whether generate const enum for typescript bindings | |
| jsBinding | --js | string | false | Path and filename of generated JS binding file. Only works with --platform flag. Relative to --output-dir. | |
| noJsBinding | --no-js | boolean | false | Whether to disable the generation JS binding file. Only works with --platform flag. | |
| dts | --dts | string | false | Path and filename of generated type def file. Relative to --output-dir | |
| dtsHeader | --dts-header | string | false | Custom file header for generated type def file. Only works when typedef feature enabled. | |
| noDtsHeader | --no-dts-header | boolean | false | Whether to disable the default file header for generated type def file. Only works when typedef feature enabled. | |
| dtsCache | --dts-cache | boolean | false | true | Whether to enable the dts cache, default to true |
| esm | --esm | boolean | false | Whether to emit an ESM JS binding file instead of CJS format. Only works with --platform flag. | |
| strip | --strip,-s | boolean | false | Whether strip the library to achieve the minimum file size | |
| release | --release,-r | boolean | false | Build in release mode | |
| verbose | --verbose,-v | boolean | false | Verbosely log build command trace | |
| bin | --bin | string | false | Build only the specified binary | |
| package | --package,-p | string | false | Build the specified library or the one at cwd | |
| profile | --profile | string | false | Build artifacts with the specified profile | |
| crossCompile | --cross-compile,-x | boolean | false | [experimental] cross-compile by swapping the cargo subcommand: any Windows-platform target from a non-Windows host uses cargo-xwin (which supports MSVC triples only; windows-gnu fails — see below); every other target uses cargo-zigbuild (requires zig on PATH). The selected subcommand is auto-installed on first use. Conflicts with --use-cross | |
| useCross | --use-cross | boolean | false | [experimental] legacy, not recommended: build inside a Docker/Podman container via cross (cross-rs); prefer --use-napi-cross or --cross-compile. Requires cross to be installed manually and a running container engine. Conflicts with --cross-compile | |
| useNapiCross | --use-napi-cross | boolean | false | [experimental] download a gcc cross toolchain from npm (@napi-rs/cross-toolchain) and set linker/CC env vars. Linux glibc targets only: x64, arm64, armv7, ppc64le, s390x (glibc 2.17). Host must be Linux x64/arm64; on failure it warns and falls back to plain cargo | |
| watch | --watch,-w | boolean | false | watch the crate changes and build continuously with cargo-watch crates | |
| features | --features,-F | string[] | false | Space-separated list of features to activate | |
| allFeatures | --all-features | boolean | false | Activate all available features | |
| noDefaultFeatures | --no-default-features | boolean | false | Do not activate the default feature |
Cross-compilation flags
napi build has three cross-compilation flags: --use-napi-cross, --cross-compile (-x) and --use-cross. All three are experimental: behavior may change between minor releases.
The recommended flags are --use-napi-cross for Linux glibc targets on a Linux x64/arm64 host, and --cross-compile (-x) for Windows MSVC targets from a non-Windows host and for musl targets. -x is also the fallback for glibc, macOS and FreeBSD targets when the preferred setup is not available on your host. Android, WASI and OpenHarmony targets need no cross flag at all: the CLI configures their toolchains from platform environment variables. --use-cross is legacy and not recommended, and the Docker-image based builds are deprecated. This page is a reference for what each flag does. To pick the right flag for your host and target, see Cross build. For Alpine/musl specifics, see the FAQ.
Each flag changes exactly one thing about the build:
| Flag | What it changes | Resulting command |
|---|---|---|
| (none) | nothing | cargo build --target <triple> |
--use-cross |
the binary only | cross build --target <triple> |
--cross-compile / -x |
the subcommand only (plus two env side effects) | cargo zigbuild --target <triple> or cargo xwin build --target <triple> |
--use-napi-cross |
env vars only (linker, CC, sysroot) | still cargo build --target <triple> |
Pick exactly one
WARNING
These flags do not combine. Pick exactly one. Two of the combinations print a warning saying one flag will be ignored, but both mechanisms actually stay active.
| Combination | What actually happens |
|---|---|
--use-cross + --cross-compile |
Hard error: the CLI refuses to build. (It may still auto-install cargo-xwin or cargo-zigbuild before erroring.) |
--use-napi-cross + --use-cross |
Prints a warning saying --use-cross will be ignored — but cross still runs, with host paths injected into its environment. Do not combine. |
--use-napi-cross + --cross-compile |
Prints a warning saying --cross-compile will be ignored — but cargo zigbuild still runs, with the napi-cross env also set (the napi-cross linker wins over zig). Do not combine. |
--watch + --cross-compile |
Produces an invalid command (cargo watch ... -- cargo build zigbuild). Do not combine. |
Prerequisites
| Flag | Installed for you | You must provide |
|---|---|---|
-x, non-Windows target (cargo-zigbuild) |
cargo-zigbuild via cargo install on first use (which can be slow). |
zig on PATH. The CLI never installs or checks zig; cargo-zigbuild errors if it is missing. |
-x, Windows target (cargo-xwin) |
cargo-xwin via cargo install, on first use. It downloads the Microsoft CRT and Windows SDK itself (the Microsoft license applies). |
clang (e.g. apt install clang / brew install llvm) — zig is not used on this path. For dependencies that compile assembly, also the LLVM tools (rustup component add llvm-tools). The CLI checks none of these. |
--use-cross |
Nothing. | The cross binary (a missing binary fails with spawn cross ENOENT), plus a running Docker >= 20.10 or Podman >= 3.4. |
--use-napi-cross |
The gcc toolchain, downloaded automatically from npm (@napi-rs/cross-toolchain) and cached under ~/.napi-rs/cross-toolchain. |
npm on PATH, and a Linux x64 or arm64 host. The CLI does not check the host: on macOS or Windows the toolchain still downloads without error, and the Linux gcc then fails at link time. Any failure here only prints a warning and the build continues as plain cargo build. |
Examples
One copy-paste command per flag:
# Linux glibc targets, from a Linux x64/arm64 host
napi build --release --target aarch64-unknown-linux-gnu --use-napi-cross
# Windows MSVC from a macOS/Linux host, musl, or the zigbuild fallback cases
napi build --release --target x86_64-unknown-linux-musl --cross-compile
# Legacy container build (not recommended)
napi build --release --target x86_64-unknown-linux-gnu --use-cross
What napi build runs
napi build is a wrapper around one spawned command plus a set of environment variables. This section spells both out.
The command
| Mode | Spawned command |
|---|---|
| No cross flag | cargo build --target <triple> |
--use-napi-cross |
cargo build --target <triple> (only the env changes) |
--use-cross |
cross build --target <triple> (same args, same host-computed env) |
--cross-compile, target platform is Windows, host is not Windows |
cargo xwin build --target <triple> (XWIN_ARCH=x86 is set for i686) |
--cross-compile, any other target |
cargo zigbuild --target <triple> |
--cross-compile, target is Windows, host is Windows |
warns, then plain cargo build --target <triple> |
--cross-compile picks cargo-xwin by the target's platform: every *-windows-* triple built from a non-Windows host routes through cargo-xwin — including x86_64-pc-windows-gnu, not just MSVC. Being routed is not the same as being supported, though: cargo-xwin handles MSVC only, so for windows-gnu it configures nothing and the build fails at link time with error: linker `x86_64-w64-mingw32-gcc` not found. Build that target without -x, with a mingw-w64 toolchain — see the windows-gnu note in Recipes per target. Every non-Windows target goes through cargo-zigbuild — the CLI keeps no list of supported targets, and it uses zigbuild even when the target matches the host.
If the CARGO environment variable is set, the CLI spawns that binary instead — in every mode, including --use-cross, where it silently replaces cross.
RUSTFLAGS
- Any
*musl*target: the CLI appends-C target-feature=-crt-statictoRUSTFLAGS. --strip: the CLI appends-C link-arg=-s.
Both are applied through the exported RUSTFLAGS environment variable. Cargo gives the env var precedence over rustflags in .cargo/config.toml, so once the CLI exports it, the rustflags from your .cargo/config.toml are ignored. If you need extra flags, add them to the RUSTFLAGS env var, not to .cargo/config.toml.
C compiler
When both TARGET_CC and CC are set, TARGET_CC wins (since @napi-rs/cli 3.0.0-alpha.92).
Default linkers for less common targets
Without --cross-compile, these targets get CARGO_TARGET_<T>_LINKER pointed at a cross gcc that you must install yourself. The CLI sets the env var without checking it: if the binary is missing, the build fails at link time. Your own CARGO_TARGET_<T>_LINKER env var always wins. With --cross-compile this table is skipped — linking is delegated to zig or xwin.
| Target | Linker set by the CLI |
|---|---|
aarch64-unknown-linux-musl |
aarch64-linux-musl-gcc |
loongarch64-unknown-linux-gnu |
loongarch64-linux-gnu-gcc-13 |
riscv64gc-unknown-linux-gnu |
riscv64-linux-gnu-gcc |
powerpc64le-unknown-linux-gnu |
powerpc64le-linux-gnu-gcc |
s390x-unknown-linux-gnu |
s390x-linux-gnu-gcc |
Android, WASI and OpenHarmony
These targets get their toolchain env from the CLI whenever the target platform matches — with or without any cross flag — but each platform has its own conditions:
- Android: linker/CC/AR env is built from
ANDROID_NDK_LATEST_HOME. A missing variable only prints a warning — the env vars are still set, with broken paths, and the build fails at link time. The whole setup is skipped when the host itself is Android. - WASI:
EMNAPI_LINK_DIRis always set to the bundled emnapi (the CLI errors if theemnapi,@emnapi/coreand@emnapi/runtimeversions mismatch). The wasi-sdk linker/CC env is set only whenWASI_SDK_PATHis set and the directory exists — otherwise linking falls back to cargo's default, rustup's bundledrust-lld. - OpenHarmony: env is built from
$OHOS_SDK_PATH/native, or fromOHOS_SDK_NATIVEwhenOHOS_SDK_PATHis unset. If neither is set, the CLI warns and sets nothing.
Passing flags to Cargo
Flags after -- will be passed through to the cargo build command. For example:
napi build -- --locked
This will pass the --locked flag to cargo build, resulting in cargo build --locked.
Note for --js-package-name
In the Deep dive section, we recommended you publish your package under npm scope. But if you are migrating an existed package which is not under the npm scope or you just don't want your package under an npm scope , you may trigger the npm spam detection while publishing the native platform packages. Like snappy-darwin-x64 snappy-darwin-arm64 etc...
In this case, you can publish your platform packages under npm scope to avoid the npm spam detection. And your users don't need to care about the platform native packages in optionalDependencies. Like snappy, users only need to install it via yarn add snappy. But platform native packages are under @napi-rs scope:
{
"name": "snappy",
"version": "7.0.0",
"optionalDependencies": {
"@napi-rs/snappy-win32-x64-msvc": "7.0.0",
"@napi-rs/snappy-darwin-x64": "7.0.0",
"@napi-rs/snappy-linux-x64-gnu": "7.0.0",
"@napi-rs/snappy-linux-x64-musl": "7.0.0",
"@napi-rs/snappy-linux-arm64-gnu": "7.0.0",
"@napi-rs/snappy-win32-ia32-msvc": "7.0.0",
"@napi-rs/snappy-linux-arm-gnueabihf": "7.0.0",
"@napi-rs/snappy-darwin-arm64": "7.0.0",
"@napi-rs/snappy-android-arm64": "7.0.0",
"@napi-rs/snappy-android-arm-eabi": "7.0.0",
"@napi-rs/snappy-freebsd-x64": "7.0.0",
"@napi-rs/snappy-linux-arm64-musl": "7.0.0",
"@napi-rs/snappy-win32-arm64-msvc": "7.0.0"
}
}
For this case, @napi-rs/cli provides the --js-package-name to override generated package loading logic. For example in snappy we have package.json like this:
{
"name": "snappy",
"version": "7.0.0",
"napi": {
"name": "snappy"
}
}
Without the --js-package-name flag, @napi-rs/cli will generate JavaScript binding to load platform native packages for you:
switch (platform) {
case 'darwin':
switch (arch) {
case 'x64':
localFileExisted = existsSync(join(__dirname, 'snappy.darwin-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./snappy.darwin-x64.node')
} else {
nativeBinding = require('snappy-darwin-x64')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(join(__dirname, 'snappy.darwin-arm64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./snappy.darwin-arm64.node')
} else {
nativeBinding = require('snappy-darwin-arm64')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on macOS: ${arch}`)
}
break
...
}
This isn't what we want. So build it with --js-package-name to override the package name in generated JavaScript binding file: napi build --release --platform --js-package-name @napi-rs/snappy. Then the generated JavaScript file will become:
switch (platform) {
case 'darwin':
switch (arch) {
case 'x64':
localFileExisted = existsSync(join(__dirname, 'snappy.darwin-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./snappy.darwin-x64.node')
} else {
nativeBinding = require('@napi-rs/snappy-darwin-x64')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(join(__dirname, 'snappy.darwin-arm64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./snappy.darwin-arm64.node')
} else {
nativeBinding = require('@napi-rs/snappy-darwin-arm64')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on macOS: ${arch}`)
}
break
...
}