Developer guide

Build the Linux kernel for HardwareJS

brintOS runs a real Linux kernel, compiled to WebAssembly. This walks through building it from source: our fork of Linux 6.12 with a new arch/wasm32 port (the wasm32-hwjs-linux target), compiled with the brintOS toolchain into a vmlinux.wasm that any machine can boot.

The kernel is the same Linux you know — fs/, mm/, net/, the scheduler, binfmt_wasm — with a wasm32 architecture under arch/wasm32/. It is not an emulator or a reimplementation. See Linux in the browser for the architecture.

Prerequisites

You need the brintOS toolchain installed — specifically the pinned LLVM (clang + wasm-ld) and the HwjsCoroutinize pass plugin. The kernel is freestanding (it doesn't link libc), so you can skip the musl step. If you haven't built the toolchain yet, work through Install the toolchain first (steps 1–2; the musl step is optional for a kernel-only build).

The pass is mandatory for the kernel. The kernel is compiled coro-only: every kernel translation unit must go through HwjsCoroutinize (that's what makes the kernel's own tasks suspendable, resumable, and forkable). The build arms the pass and then verifies it ran — a coro build whose compile flags drop the plugin produces untransformed code and fails the build loudly, so this isn't optional.

You also need the usual native kernel-build tools (make, flex, bison, bc, libelf, pkg-config, Python 3). The build itself cross-compiles to wasm32 — nothing runs in the browser at this stage.

1. Get the kernel source

Clone our fork. It's Linux 6.12 with the arch/wasm32 tree, the HardwareJS arch hooks, and a wasm-ld-based link-vmlinux already in tree:

git clone https://brintos.io/brintos/linux.git
cd linux

Read more about the port in the repo's Documentation/wasm/ (the boot protocol, memory model, syscall path, and scheduler model) — browse it at brintos/linux.

2. Configure for wasm32

Put the pinned clang + wasm-ld on your PATH, then configure with the wasm32 arch. The defaults live in arch/wasm32/configs/wasm32_defconfig:

# Use the pinned LLVM you built for the toolchain
export PATH="$HOME/llvm-project/build/bin:$PATH"

make ARCH=wasm32 LLVM=1 wasm32_defconfig
make ARCH=wasm32 LLVM=1 olddefconfig

The build uses the whole LLVM toolchain (LLVM=1 selects clang, ld.lld/wasm-ld, llvm-ar, llvm-nm, llvm-objcopy). The wasm arch builds with CONFIG_MODULES=n — there are no loadable modules; everything is built in.

3. Build vmlinux

Compile the kernel. The key addition over a normal LLVM kernel build is the HwjsCoroutinize pass, loaded into every compile via -fpass-plugin — that's what makes the kernel's own tasks suspendable, resumable, and forkable (the same pass your packages use):

make -j"$(nproc)" ARCH=wasm32 LLVM=1 \
  KCFLAGS=-fpass-plugin=$HOME/hwjs-cc/pass/coroutinize/build/HwjsCoroutinize.so \
  vmlinux

The arch's link-vmlinux replacement drives wasm-ld with --import-memory --shared-memory and exports the kernel's entry points, then splices in the second (user) memory. The on-disk artifact is called vmlinux by Linux convention, but it is a WebAssembly module — make ARCH=wasm32 vmlinux.wasm is just an alias for the same target.

Large native build, no browser involved. The fork ships its own scripts/link-vmlinux-wasm.sh and post-link helpers, and a one-knob coroutine build — see the repo for the exact wrapper/env it expects around clang and the pass plugin. Build deps and pins are documented in brintos/linux.

4. Verify the image

Confirm you produced a wasm module that exports the kernel entry points HardwareJS calls and imports the shared kernel memory:

file vmlinux     # WebAssembly (wasm) binary module
wasm-objdump -x vmlinux | grep -E 'wasm_start_kernel|wasm_syscall|kernel_memory'
# export  func    wasm_start_kernel   # HardwareJS calls this to boot
# export  func    wasm_syscall        # the syscall trap your packages call into
# import  memory  env.kernel_memory   # shared, SharedArrayBuffer-backed

5. Boot it on a machine

A machine boots whatever kernel image its machine spec names in bootKernel (default /boot/vmlinux.wasm), read from the machine's own filesystem. So shipping your kernel is just copying it into a machine's drive — exactly like copying a package. Mount a machine you own with brintos-fs and drop the image in:

cp vmlinux ~/mnt/brintos/boot/vmlinux.wasm
sync     # flush the write-back cache to the server

# then open the machine on brintos.io and press Power on —
# it boots YOUR kernel.

If your kernel lives at a non-default path, set the machine's bootKernel to match in its spec (see Machines). On boot, HardwareJS instantiates the module, calls wasm_start_kernel, and streams the live dmesg to the machine's serial console — you're watching your own kernel come up.

Want userland too? Build packages against this kernel's ABI in Build locally, run on brintOS, and set the machine's init to your program to have the kernel exec it as PID 1.