Skip to content

Build

Build the NAPI-RS project

Usage

sh
# CLI
napi build [--options]
typescript
// 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:

sh
# 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-static to RUSTFLAGS.
  • --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_DIR is always set to the bundled emnapi (the CLI errors if the emnapi, @emnapi/core and @emnapi/runtime versions mismatch). The wasi-sdk linker/CC env is set only when WASI_SDK_PATH is set and the directory exists — otherwise linking falls back to cargo's default, rustup's bundled rust-lld.
  • OpenHarmony: env is built from $OHOS_SDK_PATH/native, or from OHOS_SDK_NATIVE when OHOS_SDK_PATH is 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:

sh
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:

json
{
  "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:

json
{
  "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:

index.js
js
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:

index.js
js
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
    ...
}