Build pipeline (end-to-end)
The canonical entrypoint is:
python3 forge_cli.py build --version <mjver>
That command orchestrates the full pipeline for the requested version/ref:
Prepare upstream sources
Clone or update upstream MuJoCo under
external/mujoco.Check out the requested ref (e.g.
3.5.0or a commit hash).Apply build-system patches needed for Emscripten (e.g. qhull).
Introspect headers
Parse
mujoco.hand produce canonical JSON underdist/<ver>/abi/:mujoco_ast.jsonfunctions_introspect_like.jsonstructs_introspect_like.jsonenums_introspect_like.json
Collect implemented symbols
Create or refresh
nm_symbols.jsonso the pipeline knows what MuJoCo actually implements for the build.
Generate ABI and wrappers
Generate wrapper sources and metadata (
mjwf_abi_funcs.*,mjwf_abi_structs.*, wrapper export manifests).Produce the final export list
dist/<ver>/abi/exports.lst.
Build WASM bundle
Configure with
emcmake cmakeand build withcmake --build.app/CMakeLists.txtconsumes the generated wrapper sources and the export list, then emits:dist/<ver>/mujoco.jsdist/<ver>/mujoco.wasm
Post-build validation
Validate exports/ABI manifests and (optionally) coverage against native symbols.
Optional runtime checks (
--with-checks)Run Node-based smoke + quality gates (
check/tests/*.mjs) against the activedist/<ver>.
Fast paths for iteration (avoid full rebuilds)
python3 forge_cli.py build --version <mjver> optimizes for correctness and produces a fully validated dist/<ver> tree.
For day-to-day editing you can often skip most stages and rebuild only what changed.
1) Docs / benchmarks / Node checks only
If you only changed documentation, benchmarks, or check/tests/*.mjs, you do not need to rebuild WASM. Re-run the
relevant Node scripts against an existing dist/<ver> tree.
2) Wrapper/app code only (app/*)
If you only changed wrapper sources under app/ (or other non-upstream code consumed by the CMake build), rebuild the
existing build directory directly instead of rerunning the full pipeline.
Build directory layout (default):
<repo>/build/forge/<short>/{single|pthreads}If you set
MJWF_BUILD_ROOT, it becomes:<MJWF_BUILD_ROOT>/forge/<short>/{single|pthreads}
Example (run in the same shell environment where EMSDK is available):
cmake --build "<build_dir>" -- -j "$(nproc)"
Running the full forge command will re-run upstream refresh + introspection + generators, and may force a rebuild even when you only edited a couple of local files.
3) ABI generator scripts only (abi_exports/*)
If you only changed generator logic (for example abi_exports/gen_funcs.py), regenerate the ABI outputs and then build:
python3 -m abi_exports.gen_funcs --version <mjver>
cmake --build "<build_dir>" -- -j "$(nproc)"
If you changed other generators, rerun them as well (e.g. abi_exports/gen_structs.py, abi_exports/gen_enums.py,
abi_exports/gen_scene_geom_soa.py).
4) Upstream ref / introspect/* / MuJoCo patches
If you changed anything that affects upstream headers or ABI inputs, run the full pipeline:
python3 forge_cli.py build --version <mjver>
Debug-friendly builds (when you need symbols/assertions)
forge_cli.py build configures with -DCMAKE_BUILD_TYPE=Release and -DMJWF_PROFILE=fast by default (see
app/CMakeLists.txt). If you need more debug visibility (assertions, source maps, etc.), reconfigure the existing build
directory with a different MJWF_PROFILE and rebuild via cmake --build. Note that debug profiles may increase build
time and output size.
If any stage fails, it usually means one of:
environment/toolchain mismatch (Node/emcc/clang not available),
upstream ref change requiring wrapper/introspection updates,
export list drift (a previously exported function is missing or renamed),
runtime regression caught by the smoke tests.