Natives Build, Release, and Debugging Runbook
This runbook describes how the @oh-my-pi/pi-natives build pipeline produces .node addons, how compiled distributions load them, and how to debug loader/build failures.
It follows the architecture terms from docs/natives-architecture.md:
- build-time artifact production (
scripts/build-native.ts) - embedded addon manifest generation (
scripts/embed-native.ts) - runtime addon loading + validation gate (
src/native.ts)
Implementation files
packages/natives/scripts/build-native.tspackages/natives/scripts/embed-native.tspackages/natives/package.jsonpackages/natives/src/native.tscrates/pi-natives/Cargo.toml
Build pipeline overview
1) Build entrypoints
packages/natives/package.json scripts:
bun scripts/build-native.ts(build:native) → release buildbun scripts/build-native.ts --dev(dev:native) → debug/dev buildbun scripts/embed-native.ts(embed:native) → generatesrc/embedded-addon.tsfrom built files
2) Rust artifact build
build-native.ts runs Cargo in crates/pi-natives:
- base command:
cargo build - release mode adds
--releaseunless--devis passed - cross target adds
--target <CROSS_TARGET>
crates/pi-natives/Cargo.toml declares crate-type = ["cdylib"], so Cargo emits a shared library (.so/.dylib/.dll) that is then copied/renamed to a .node addon filename.
3) Artifact discovery and install
After Cargo completes, build-native.ts scans candidate output directories in order:
${CARGO_TARGET_DIR}(if set)<repo>/targetcrates/pi-natives/target
For each root it checks profile directories:
- cross build:
<root>/<crossTarget>/<profile>then<root>/<profile> - native build:
<root>/<profile>
Then it looks for one of:
libpi_natives.solibpi_natives.dylibpi_natives.dlllibpi_natives.dll
When found, it atomically installs into packages/natives/native/ with temp-file + rename semantics (Windows fallback handles locked DLL replacement failures explicitly).
Target/variant model and naming conventions
Platform tag
Both build and runtime use platform tag:
<platform>-<arch> (example: darwin-arm64, linux-x64)
Variant model (x64 only)
x64 supports CPU variants:
modern(AVX2-capable path)baseline(fallback)
Non-x64 uses a single default artifact (no variant suffix).
Output filenames
Release builds:
- x64:
pi_natives.<platform>-<arch>-modern.nodeor...-baseline.node - non-x64:
pi_natives.<platform>-<arch>.node
Dev build (--dev):
pi_natives.dev.node
Runtime loader candidate order in native.ts:
- if
PI_DEVis set: trypi_natives.dev.nodefirst - then release candidates
- compiled mode prepends extracted/cache candidates before package-local files
Environment flags and build options
Runtime flags
PI_DEV(loader behavior): prefer dev addon candidates firstPI_NATIVE_VARIANT(loader behavior, x64 only): forcemodernorbaselineselection at runtimePI_COMPILED(loader behavior): enable compiled-binary candidate/extraction behavior
Build-time flags/options
--dev(script arg): build debug profile and emitpi_natives.dev.nodeCROSS_TARGET: passed to Cargo--targetTARGET_PLATFORM: override output platform tag namingTARGET_ARCH: override output arch namingTARGET_VARIANT(x64 only): forcemodernorbaselinefor output filename and RUSTFLAGS policyCARGO_TARGET_DIR: additional root when searching Cargo outputsRUSTFLAGS:- if unset and not cross-compiling, script sets:
- modern:
-C target-cpu=x86-64-v3 - baseline:
-C target-cpu=x86-64-v2 - non-x64 / no variant:
-C target-cpu=native
- modern:
- if already set, script does not override
- if unset and not cross-compiling, script sets:
Build state/lifecycle transitions
Build lifecycle (build-native.ts)
- Init: parse args/env (
--dev, target overrides, cross flags) - Variant resolve:
- non-x64 → no variant
- x64 +
TARGET_VARIANT→ explicit variant - x64 cross-build without
TARGET_VARIANT→ hard error - x64 local build without override → detect host AVX2
- Compile: run Cargo with resolved profile/target
- Locate artifact: scan target roots/profile dirs/library names
- Install: copy + atomic rename into
packages/natives/native - Complete: output addon ready for loader candidates
Failure exits happen at any stage with explicit error text (invalid variant, failed cargo build, missing output library, install/rename failure).
Embed lifecycle (embed-native.ts)
- Init: compute platform tag from
TARGET_PLATFORM/TARGET_ARCHor host values - Candidate set:
- x64 expects both
modernandbaseline - non-x64 expects one default file
- x64 expects both
- Validate availability in
packages/natives/native - Generate manifest (
src/embedded-addon.ts) with Bunfileimports and package version - Runtime extraction ready for compiled mode
--reset bypasses validation and writes a null manifest stub (embeddedAddon = null).
Dev workflow vs shipped/compiled behavior
Local development workflow
Typical local loop:
- Build addon:
- release:
bun --cwd=packages/natives run build:native - debug:
bun --cwd=packages/natives run dev:native
- release:
- Set
PI_DEV=1when testing debug addon loading - Loader in
native.tsresolves package-localnative/(and executable-dir fallback) candidates validateNativeenforces export compatibility before wrappers use the binding
Shipped/compiled binary workflow
In compiled mode (PI_COMPILED or Bun embedded markers):
- Loader computes versioned cache dir:
<getNativesDir()>/<packageVersion>(operationally~/.pisces/natives/<version>) - If embedded manifest matches current platform+version, loader may extract selected embedded file into that versioned dir
- Runtime candidate order includes:
- versioned cache dir
- legacy compiled-binary dir (
%LOCALAPPDATA%/ompon Windows,~/.local/binelsewhere) - package/executable directories
- First successfully loaded addon still must pass
validateNative
This is why packaging + runtime loader expectations must align: filenames, platform tags, and exported symbols must match what native.ts probes and validates.
JS API ↔ Rust export mapping (validation gate subset)
native.ts requires these JS-visible exports to exist on the loaded addon. They map to Rust N-API exports in crates/pi-natives/src:
JS name required by validateNative | Rust export declaration | Rust source file |
|---|---|---|
glob | #[napi(js_name = "glob")] pub fn glob(...) | crates/pi-natives/src/glob.rs |
grep | #[napi(js_name = "grep")] pub fn grep(...) | crates/pi-natives/src/grep.rs |
search | #[napi(js_name = "search")] pub fn search(...) | crates/pi-natives/src/grep.rs |
highlightCode | #[napi(js_name = "highlightCode")] pub fn highlight_code(...) | crates/pi-natives/src/highlight.rs |
getSystemInfo | #[napi(js_name = "getSystemInfo")] pub fn get_system_info(...) | crates/pi-natives/src/system_info.rs |
getWorkProfile | #[napi] pub fn get_work_profile(...) (camel-cased export) | crates/pi-natives/src/prof.rs |
invalidateFsScanCache | #[napi(js_name = "invalidateFsScanCache")] pub fn invalidate_fs_scan_cache(...) | crates/pi-natives/src/fs_cache.rs |
If any required symbol is missing, loader fails fast with a rebuild hint.
Failure behavior and diagnostics
Build-time failures
- Invalid variant configuration:
TARGET_VARIANTset on non-x64 → immediate error- x64 cross-build without explicit
TARGET_VARIANT→ immediate error
- Cargo build failure:
- script surfaces non-zero exit and stderr
- Artifact not found:
- script prints every checked profile directory
- Install failure:
- explicit message; Windows includes locked-file hint
Runtime loader failures (native.ts)
- Unsupported platform tag:
- throws with supported platform list
- No candidate could load:
- throws with full candidate error list and mode-specific remediation hints
- Missing exports:
- throws with exact missing symbol names and rebuild command
- Embedded extraction problems:
- extraction mkdir/write errors recorded and included in final diagnostics
Troubleshooting matrix
| Symptom | Likely cause | Verify | Fix |
|---|---|---|---|
Native addon missing exports ... Missing: <name> | Stale .node binary, Rust export name mismatch, or wrong binary loaded | Run with PI_DEV=1 to see loaded path; inspect export list for that file | Rebuild build:native; ensure Rust #[napi(js_name=...)] matches JS name; remove stale cached/versioned files |
| x64 machine loads baseline when modern expected | PI_NATIVE_VARIANT=baseline, no AVX2 detected, or only baseline file present | Check PI_NATIVE_VARIANT; inspect native/ for -modern file | Build modern variant (TARGET_VARIANT=modern ... build:native) and ensure file is shipped |
| Cross-build produces unusable/wrong-labeled binary | Mismatch between CROSS_TARGET and TARGET_PLATFORM/TARGET_ARCH, or missing TARGET_VARIANT for x64 | Confirm env tuple and output filename | Re-run with consistent env values and explicit x64 TARGET_VARIANT |
| Compiled binary fails after upgrade | Stale extracted cache (~/.pisces/natives/<old-or-mismatched-version>) or embedded manifest mismatch | Inspect versioned natives dir and loader error list | Delete versioned natives cache for the package version and rerun; regenerate embedded manifest during packaging |
| Loader probes many paths and none work | Platform mismatch or missing release artifact in package native/ | Check platformTag vs actual filename(s) | Ensure built filename exactly matches pi_natives.<platform>-<arch>(-variant).node convention and package includes native/ |
embed:native fails with "Incomplete native addons" | Required variant files not built before embedding | Check expected vs found list in error text | Build required files first (x64: both modern+baseline; non-x64: default), then rerun embed:native |
Operational commands
# Release artifact for current host
bun --cwd=packages/natives run build:native
# Debug artifact (load first when PI_DEV=1)
bun --cwd=packages/natives run dev:native
# Build explicit x64 variants
TARGET_VARIANT=modern bun --cwd=packages/natives run build:native
TARGET_VARIANT=baseline bun --cwd=packages/natives run build:native
# Generate embedded addon manifest from built native files
bun --cwd=packages/natives run embed:native
# Reset embedded manifest to null stub
bun --cwd=packages/natives run embed:native -- --reset