Build nos
Build the kernel, its modules and the boot images for x86-64 and arm64: in Docker on macOS, or natively on Linux with the pinned Rust nightly.
By the end of this guide you will have built nos from source: the x86-64 ISO and kernel, the arm64 kernel image, the loadable modules, and, if you want them, a disk image for QEMU and the KVM clouds.
You need:
- a clone of the repository;
- either Docker, which is how nos is built on macOS (Apple Silicon included), or a Linux machine with the toolchain installed. The reference system is Ubuntu 22.04: the builder image and CI both run on it;
- disk space: the builder image takes about 4.7 GB, and the build outputs of both architectures need a couple of gigabytes more under
out/andsrc/rust/target/; - network access to install the toolchain. The build itself never touches the network.
git clone https://github.com/irqlevel/nos.git
cd nos
Build in Docker
./scripts/build-iso-docker.sh
The script builds the nos-builder image from the Dockerfile. The image is Ubuntu 22.04 for linux/amd64 with clang, lld, nasm, GRUB for both BIOS and UEFI, xorriso, mtools, cppcheck, e2fsprogs, squashfs-tools, QEMU and the pinned Rust nightly. The script then runs make clean && make all in a container, with your checkout mounted at /src. The image holds only the toolchain. Your sources are mounted when the container starts, so you never rebuild the image after editing them. When the build finishes, the script prints:
Built nos.iso and kernel64.elf (use kernel64.elf in GDB for symbols)
make all runs cppcheck first, then builds the x86-64 ISO and every module. The script starts with make clean, so every run is a full rebuild. On an Apple Silicon Mac the linux/amd64 image runs under emulation, so the build takes a while.
Rebuild only what changed
Once the image exists, you can run make in it directly. This is the same build the smoke test runs:
docker run --platform linux/amd64 --rm -v "$PWD:/src" -w /src nos-builder \
bash -c 'make rust && make nocheck'
For arm64:
docker run --platform linux/amd64 --rm -v "$PWD:/src" -w /src nos-builder \
bash -c 'make nocheck ARCH=aarch64'
The image has no git. The build scripts work out the release name and the commit on the host and pass them in as
VERSIONandGIT_DESC. If you rundocker runyourself without them, the kernel'sversioncommand printsnos dev (unknown). To record the commit, add-e GIT_DESC="$(git rev-parse --short HEAD)", as the release workflow does.
Build a disk image
./scripts/build-disk.sh
This builds the kernel in the container (make clean && make nocheck) and then writes nos.qcow2. It's an MBR disk with GRUB in the boot sector and one ext2 partition labelled nos. That partition holds /boot/kernel64.elf, and nos mounts it read-write as the root filesystem. The script doesn't need --privileged. Two variables change what it builds:
SIZE_MB=4096 ROOTFS=my-root ./scripts/build-disk.sh
SIZE_MB sets the size in MB (default 1024). ROOTFS names a directory to copy into the root filesystem next to /boot, for example modules under lib/modules, an /etc/rc or SSH keys. The disk boots under BIOS only, the way the KVM clouds boot it (see Run: Google Cloud). Because the script builds the kernel too, the same run also produces nos.iso and bin/kernel64.elf.
Build the system image
./scripts/build-image.sh
./scripts/build-image.sh --clean
Either command produces nos-x86_64.squashfs, which holds the kernel, every module and the GRUB entries that a server is set up from (see Install nos on a dedicated server). With no argument, the script rebuilds what changed since the last build. With --clean it builds everything from scratch, the way a release is built.
Build natively on Linux
On Ubuntu 22.04, install the same packages the builder image has:
sudo apt-get update
sudo apt-get install -y build-essential clang lld llvm nasm \
grub2-common grub-pc-bin grub-efi-amd64-bin xorriso mtools cppcheck \
parted e2fsprogs squashfs-tools qemu-utils qemu-system-x86 qemu-system-arm \
python3 curl
The Rust toolchain must be a dated nightly with the rust-src component. Run the following from the repository root. It's the command CI uses, and it installs the exact nightly named in src/rust/rust-toolchain.toml:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --component rust-src --default-toolchain \
"$(sed -n 's/^channel *= *"\(.*\)"/\1/p' src/rust/rust-toolchain.toml)"
export PATH="$HOME/.cargo/bin:$PATH"
Then build:
make
| Command | What it does |
|---|---|
make |
runs make check, then builds nos.iso and the modules |
make nocheck |
the same without cppcheck |
make check |
runs only cppcheck over src/cpp; any finding fails it |
make rust |
builds only the Rust staticlib, libkernel.a |
make modules |
builds only the modules |
make nocheck ARCH=aarch64 |
builds the arm64 kernel, its image and its modules |
make image |
builds nos-x86_64.squashfs (x86-64 only; needs mksquashfs) |
make nocheck UBSAN=1 |
builds the C++ under clang's UB sanitizer (see below) |
make nocheck RUSTUB=1 |
builds the Rust with core's unsafe-precondition and overflow checks (see below) |
make smoke |
runs scripts/smoke-test.sh, which builds in Docker and boots headless |
make clean |
removes out/, the kernels, nos-arm64.img, the ISO and the system image, and runs cargo clean |
ARCH selects the architecture: x86_64 (the default) or aarch64. Objects go to out/$(ARCH)/. For an arm64-only build you need only make, clang, lld, llvm and the nightly; that's all the release workflow installs for it. make smoke builds in Docker. With a native toolchain, build with make nocheck and then run ./scripts/smoke-test.sh --skip-build.
What you get
| File | What it is |
|---|---|
nos.iso |
the x86-64 boot ISO with GRUB and the kernel; it boots under both BIOS and UEFI |
kernel64.elf, bin/kernel64.elf |
the x86-64 kernel; the GDB scripts load the copy in bin/ |
nos-arm64.img |
the arm64 kernel in Linux Image format, for QEMU -kernel |
kernel-arm64.elf, bin/kernel-arm64.elf |
the arm64 kernel ELF, with symbols |
out/<arch>/modules/*.ko |
the modules: hello, modtest, slowexit, blkload, netblk, netload, sshd, hv, heapbench |
nos.qcow2 |
the disk image, from scripts/build-disk.sh |
nos-x86_64.squashfs |
the system image, from make image or scripts/build-image.sh |
A module loads only into a kernel built from the same tree. The loader checks a digest of the kernel interface and refuses a module built against a different one (see What a module may call). Tagged releases carry the kernels, nos.iso, nos.qcow2, nos-arm64.img, the system image and every module, prebuilt (release.yml).
How the toolchain is pinned and dependencies are vendored
src/rust/rust-toolchain.tomlpins a dated nightly (nightly-2026-08-30at the time of writing) withrust-src. The kernel's Rust needs nightly for two reasons:-Z build-std, which rebuildscoreandallocwith the large code model, and thealloc_error_handlerfeature.- The external crates are vendored in
src/rust/vendor, and the Makefile runs cargo with--offline. These include rustls and the RustCrypto crates behind TLS and SSH, and the crates that std itself depends on. No build ever reaches crates.io. If a build asks for the network, a dependency was never vendored, and cargo fails with an error instead of downloading it. - After you add, remove or bump a dependency in
src/rust, runscripts/vendor.sh, which is the only step that needs the network. Commit the updatedsrc/rust/vendortogether withCargo.lock. If you bump the toolchain pin, re-runscripts/vendor.shin the same commit: the vendored set includes std's own dependencies, and those change from one nightly to the next. - The Dockerfile and both CI workflows read the pin from that one file, so it's the only place the version lives.
Build with undefined-behaviour checks
make nocheck UBSAN=1
make nocheck RUSTUB=1
make nocheck ARCH=aarch64 UBSAN=1 RUSTUB=1
UBSAN=1 builds the C++ with clang's undefined-behaviour sanitizer. RUSTUB=1 turns on core's checks of its own unsafe preconditions and arithmetic overflow checks in the Rust. You can use the flags separately or together. The first failed check panics and names its site:
PANIC:Report():ubsan.cpp,267: UBSAN: shift exponent 64 is too large for 64-bit type 'unsigned long' at src/cpp/...
The output files keep their usual names. The instrumented C++ objects go to out/<arch>-ubsan/, and a stamp in out/ relinks the images whenever you switch flavour, so a plain build never ships an instrumented kernel. version adds +ubsan or +rustub to its output. To collect every C++ report during a boot instead of panicking on the first, boot with ubsan=warn. For details, see UBSan and Rust UB checks.
Keep both architectures green
For both x86_64 and aarch64, CI (ci.yml) runs make check, make nocheck ARCH=... and a smoke boot under TCG. Its last step builds with UBSAN=1 RUSTUB=1 and smoke-boots that build too. A change to common code that builds only on x86 breaks CI.
Two things to watch for:
- Source lists are explicit. The Makefile lists every C++ file in
CXX_SRC_x86_64andCXX_SRC_aarch64, and the assembly inASM_SRC_*andASM_S_SRC_*. A new.cppfile that isn't in a list is silently not compiled, so add portable code to both. If common code calls an x86-only entry point, add an unreachable link stub tox86_driver_stubs.cpprather than an#ifdef. - Judge a run by its exit code. Before you send a change, build both architectures and boot both:
make nocheck && ./scripts/smoke-test.sh --skip-build
make nocheck ARCH=aarch64 && ./scripts/smoke-arm64.sh --skip-build
On macOS, run ./scripts/smoke-test.sh and ./scripts/smoke-arm64.sh without --skip-build, and each script builds incrementally in the nos-builder container first. A smoke script exits with 0 when the boot printed all its markers, and with 1 on a panic, a timeout or a failed build. For what each check covers, see Run the gates.
Troubleshooting
-
No rule to make targetafter you move, rename or delete a header. A stale dependency (.d) file inout/still names the old path. Runmake clean, or delete the.dfiles that mention the old path. The message doesn't contain the word "error", so checkmake's exit code instead of searching its output. Don't pipe the build intotaileither: the pipeline then returnstail's exit status. -
cppcheck fails
makeorbuild-iso-docker.sh.make checkstops on any finding. Usemake nocheckto skip it while you iterate, but CI runsmake check. -
The ISO boots under BIOS but not under UEFI.
grub-mkrescuedidn't find thex86_64-efimodules (grub-efi-amd64-bin) ormtools, and silently built a BIOS-only ISO. The builder image and CI have both packages. -
A build tries to reach the network. A dependency was never vendored. Run
scripts/vendor.sh. -
The container downloads a Rust toolchain, or reports that it isn't installed. Your
nos-builderimage is older than the current toolchain pin.build-iso-docker.shandbuild-disk.shrebuild the image on every run.build-image.shbuilds it only when there is none, and the smoke scripts (andmkrootfs.sh, when it falls back to Docker) use whichever image exists. Rebuild it:docker build --platform linux/amd64 -t nos-builder . -
The link fails with
a static constructor or destructor: this kernel runs none. The kernel runs no.init_array, so the linker rejects any global whose type has a non-constexprconstructor or a non-trivial destructor. Give the type aconstexprconstructor, or make the object a function-local static. -
The link fails because the functions aren't where the symbol table says. The three-pass link checks that the symbol table used for backtraces matches the final image. See Build.
Next steps
- Run nos in QEMU
- Run the gates
- Debug nos with GDB
- Write a loadable kernel module in Rust
- The full reference: Build