packages/wasmbuilder/src/build.nu — the wasm build driver (library API).
The pipeline, end to end and fully local:
.nu ──nurlc──▶ LLVM IR ──wb_prepare_ir_for_wasi──▶ wasm32 IR ──[zig] cc --target=wasm32-wasi + runtime.wasm.o──▶ .wasm
Link flags mirror nurlapi's production /build_wasm handler: -Wl,--no-gc-sections NURL closures store function-table indices; gc-sections renumbers the table → call_indirect traps at scale (proven on nurlc.wasm >150 fns) (nurlapi's -Wl,--allow-undefined is replaced by per-declare "wasm-import-module"="env" IR attributes — zig cc's driver rejects the linker flag; see wasi_ir.nu. Same semantics: undefined symbols become wasm imports the host runtime resolves.)
Library entry points (embed these — no subprocess, no HTTP): ( wb_build_file nu_path out_wasm opts ) → !v String ( wb_build_source source filename opts ) → !( Vec u ) String
Other packages import src/build.nu (which pulls wasi_ir.nu + toolchain.nu); the CLI in main.nu is a thin wrapper over the same calls.
: WbOpts: WbOpts {
s opt // clang opt level: `-O0`..`-O3`, `-Oz` (nurlapi uses -O1)
b host_imports // pass --ffi-host-imports (auto-dropped on old nurlc)
b asyncify // wasm-opt asyncify wrap for canvas programs (needs binaryen)
b keep_ll // leave the rewritten .ll next to the .wasm
b quiet // suppress progress lines on stderr
}
Build options. opt is a BORROWED view (literal or string_data of a caller-owned String) — WbOpts is passed by value, so it must not own heap fields.
@ wb_opts_default → WbOpts@ wb_build_file s nu_path s out_wasm WbOpts opts → !v StringCompile one .nu file to out_wasm. The heavy lifting shared by the CLI and wb_build_source.
@ wb_build_source s source s filename WbOpts opts → !( Vec u ) StringCompile NURL source (a string) to wasm bytes — the embedding API swarm-mcp's kernel path uses. Single-file sources only (imports resolve against the installed stdlib; a multi-file package should go through wb_build_file on its entry point instead).
packages/wasmbuilder/src/toolchain.nu — locate (or provision) everything a local wasm32-wasi build needs, using only what the installed NURL toolchain already ships.
The wasm pipeline needs three things beyond nurlc itself:
The toolchain's bundled zig cc (at $NURL_HOME/zig/zig, preferred by nurl.sh for native builds too) does this out of the box — zig carries its own wasi-libc and wasm-ld, so NO wasi-sdk is needed.
stdlib ships runtime.c, so we compile it on first use and cache the object keyed by the SOURCE HASH — a toolchain upgrade that changes runtime.c automatically invalidates the cache. No version skew.
Resolution order for the wasm compiler: $NURL_ZIG → $NURL_HOME/zig/zig → zig on PATH → $WASI_CLANG (a wasi-sdk clang, for setups that already have one) → download a pinned, sha256-verified zig release into $NURL_HOME/zig (opt out with NURL_WASM_NO_DOWNLOAD=1).
Everything is namespaced wb_/__wb_ — NURL has one flat function namespace and this module is meant to be imported by other packages (swarm-mcp, nurl-mcp).
@ wb_is_windows → b@ wb_nurl_home → String$NURL_HOME, defaulting to <home>/.nurl — the same layout tools/install-toolchain.sh and get-nurl.{sh,ps1} produce.
@ wb_stdlib_dir → StringThe directory holding the stdlib C sources (runtime.c & friends). $NURL_STDLIB is the toolchain PREFIX (import strings like stdlib/core/string.nu resolve against it), so the C sources live at $NURL_STDLIB/stdlib/. Fall back to $NURL_HOME/stdlib for direct runs.
@ wb_find_nurlc → !String StringLocate nurlc: $NURLC → $NURL_HOME/bin/nurlc → nurlc on PATH. The $NURL_HOME/bin shim is preferred over PATH because it exports NURL_STDLIB, keeping stdlib import resolution correct even when wasmbuilder itself was started without the shims.
: WbCompiler: WbCompiler {
String cmd
b is_zig
}
WbCompiler.cmd is the executable; is_zig=T means invoke as cmd cc … (zig's clang driver), F means a wasi-sdk clang invoked as cmd ….
@ wb_compiler_free WbCompiler c → v@ wb_find_compiler b allow_download → !WbCompiler String@ wb_cache_dir → StringCache root: $NURL_HOME/build/wasmbuilder (created on demand).
@ wb_ensure_wasm_obj WbCompiler cc s src_c → !String StringCompile <stdlib>/<src_c> for wasm32-wasi and cache the object keyed by the source's content hash: runtime.c changes with the toolchain, so an upgrade automatically produces a fresh object that matches it. Returns the cached object path.
wasmbuilder — compile NURL to wasm32-wasi locally, with zero setup beyond the installed toolchain.
wasmbuilder program.nu # → program.wasm wasmbuilder program.nu -o out/app.wasm # explicit output wasmbuilder program.nu -O z # size-optimised link wasmbuilder program.nu --emit-ll # keep the rewritten .ll too wasmbuilder --doctor # explain what is resolved
Run the result with any wasm runtime — the reference wasmtime, or the pure-NURL one: nurlpkg install wasmtime && wt run program.wasm.
The first build on a machine without a bundled zig downloads a pinned, sha256-verified zig 0.13.0 into $NURL_HOME/zig (set NURL_WASM_NO_DOWNLOAD=1 to forbid that and fail instead).
@ main → ipackages/wasmbuilder/src/wasi_ir.nu — LLVM-IR → wasm32-wasi rewriter.
nurlc emits IR targeting the host (x86_64/arm64, 64-bit pointers, glibc FFI declares). This module retargets that IR for wasm32-wasi:
@main → @__main_argc_argv (wasi-libc crt entry shape)wasm32-unknown-wasi triplewasm32 ABI uses i32 (malloc/strlen/memcpy/… — trunc/zext wrappers)
so the stdlib's unconditional declares link; the stubs return error sentinels and are unreachable behind posix_const runtime gates
@ inside c"…" string constants first so a program thatitself EMITS IR (nurlc.wasm!) doesn't get its data corrupted
Extracted verbatim from nurlapi/main.nu (the /build_wasm handler), which is the production-proven pipeline behind play.nurl-lang.org — only the function names are prefixed (wb_/__wb_) for library use.
@ wb_prepare_ir_for_wasi String ir → String