Developer guide

Install the toolchain

The shared setup for everything else in this guide. Both building packages and building the kernel compile through the same brintOS toolchain — do this once, then follow either guide.

The coroutine pass is mandatory, not optional. Both your packages and the kernel are compiled through our LLVM pass, HwjsCoroutinize — it's what makes a wasm program suspendable, resumable, and forkable. The build arms and then verifies the pass ran on every compile; a build that drops the plugin fails loudly rather than silently producing untransformed code. There is no Emscripten, Binaryen/Asyncify, or JSPI in the picture. See The brintOS toolchain.

What you'll build

  • Pinned clang + wasm-ld (from brintos/llvm-project) — clang has no stable plugin ABI across releases, so the pass must load into the exact LLVM it was built against.
  • The HwjsCoroutinize pass + wasmcc/wasmld drivers (from brintos/hwjs-cc).
  • Your libc — musl or glibc (from brintos/musl or brintos/glibc), cross-built into a sysroot — only needed for packages; the kernel is freestanding and doesn't link libc. Pick one (see step 3) — you don't need both.

Which pieces each workflow needs:

WorkflowLLVM + HwjsCoroutinizelibc sysroot (musl or glibc)
Build packagesrequiredrequired
Build the kernelrequirednot needed

1. Check out the repos

Clone over HTTPS (or use git over SSH):

git clone https://brintos.io/brintos/llvm-project.git
git clone https://brintos.io/brintos/hwjs-cc.git
git clone https://brintos.io/brintos/musl.git    # packages, if you choose musl
git clone https://brintos.io/brintos/glibc.git   # packages, if you choose glibc

2. Build clang, wasm-ld, and the pass

This is the part both workflows need. First build the pinned LLVM — it must be built with the LLVM dynamic-library and plugin flags so the coroutine pass can load into it, so use the repo's build script rather than a plain cmake invocation. Then build the HwjsCoroutinize pass and the wasmcc/wasmld drivers against it. Each repo ships a one-command build that carries the authoritative flags:

# Build the pinned clang + wasm-ld + opt + libLLVM (clang 22, commit 60513b8d)
#   with the dylib + plugin flags the pass loader needs. See llvm-project/BRINTOS.md.
./llvm-project/build-brintos.sh              # -> llvm-project/build/bin

# Build the HwjsCoroutinize pass plugin AND wire up the wasmcc/wasmld drivers.
#   toolchain/build.sh auto-discovers the LLVM you just built.
cd hwjs-cc
toolchain/build.sh
export PATH="$PWD/bin:$PATH"
wasmcc --version

This produces the wasmcc/wasmld drivers (now on your PATH, from hwjs-cc/bin) and the pass plugin at hwjs-cc/pass/coroutinize/build/HwjsCoroutinize.so. Packages compile through wasmcc, which emits LLVM bitcode per translation unit; wasmld then runs HwjsCoroutinize once over the whole merged program. (The kernel build, by contrast, loads the same .so per translation unit via -fpass-plugin — see Build the Linux kernel.) For the full driver environment interface — sysroots, the musl bitcode pool, the pinned plugin — see docs/BUILDING.md in brintos/hwjs-cc.

3. Build your libc — musl or glibc (packages only)

Skip this entirely if you only want to build the kernel (it's freestanding and links no libc). For packages, cross-build one libc for wasm32 into a sysroot, using the toolchain you just built. Pick the one that matches the userland you're targeting:

Choose musl or glibc — you won't need both. A package is compiled and linked against a single libc, and a machine's userland is built on one of them, so most developers pick one and stay there. Reach for musl when you want small, simple, statically-linked binaries; reach for glibc when you need GNU/Linux source compatibility — real dynamic loading (dlopen), NSS, and fuller locale/printf coverage. The two sysroots are independent, and each has its own driver: musl links through wasmcc, glibc through wasmcc-glibc.

Option A — musl

Cross-build our musl into a sysroot (see brintos/musl for the exact configure flags):

cd ../musl
./configure --target=wasm32 CC=wasmcc --prefix="$HOME/brintos-sysroot"
make && make install

# Point the drivers at the musl artifacts and select the coroutinizing arm.
# The env tables in docs/BUILDING.md (brintos/hwjs-cc) are authoritative:
export WASMLD_HWJS=1                       # required: the coroutinizing arm
export HWJS_TOOLS_DIR=/path/to/artifacts   # base for startfiles + the musl
                                           # bitcode pool + musl headers
# (individual overrides exist too: WASMLD_HWJS_LIBC_BC, WACKY_LIBC_INCLUDE, ...)

Option B — glibc

Cross-build our wasm32 glibc port. It's heavier than musl: glibc is built as a coroutinized bitcode object pool plus native startfiles and installed headers, so each package link pulls exactly the translation units it needs. Packages then compile through the glibc driver, wasmcc-glibc, in place of wasmcc. Follow brintos/glibc for the authoritative recipe, pins, and build script — the shape is:

cd ../glibc
# Configure for wasm32, then build the coro bitcode pool + startfiles + headers
# into a sysroot. glibc's build is multi-stage (~1900 TUs), so brintos/glibc
# ships the build script that drives configure -> pool build -> install.
./configure --host=wasm32-hwjs-linux-gnu --build=x86_64-pc-linux-gnu \
  CC=wasmcc-glibc --prefix="$HOME/brintos-glibc-sysroot"
make && make install

# Point the driver at the glibc build tree (sysroot/, glibc-bc/, startfiles/)
# — the env tables in docs/BUILDING.md (brintos/hwjs-cc) are authoritative:
export WASMLD_HWJS=1
export GLIBC_BUILD=/path/to/glibc/build
wasmcc-glibc --version
Order matters: your libc is cross-compiled with the clang + pass you just built, so build LLVM and the wrappers (step 2) before it. Everything here is a normal native build — nothing runs in the browser at this stage.

Next