The stock make world image is a reasonable default, but if you’re building for a specific board, a specific deployment, or a specific test scenario, you’ll want to customise it. This guide covers the pieces you’d typically change and how the image-assembly system keeps them glued together.
The image layout
A darwinOS disk image is four things laid out on a block device:
- EFI system partition — holds
boot.efiand the kernel image. - Boot volume —
/System/, read-only on release builds. Kernel extensions, frameworks, and the core daemon set. - Root volume —
/Users/,/var/,/etc/. Read-write, mounted over the boot volume via APFS firmlinks. - Recovery volume (optional) — a minimal image for re-installing.
The image target builds all four and writes them into a single file:
make image
# writes build/<target>/darwinos.imgCustomising what goes in
Image assembly is driven by a manifest — image/manifest.yaml:
kernel:
config: debug
extra_boot_args: "-v serial=3"
boot_volume:
include:
- path: /System/Library/CoreServices
- path: /System/Library/LaunchDaemons
- path: /usr/lib
kexts:
- com.apple.iokit.virtio
- io.darwinos.drivers.pl011
root_volume:
include:
- path: /etc/default
- path: /usr/local/bin
first_boot_script: scripts/first-boot.sh
default_user:
name: dev
shell: /bin/bash
size_mb: 2048Copy the file to image/custom.yaml, edit what you want, and build:
make image MANIFEST=custom.yamlKernel configuration
kernel.config picks which kernel build to embed. Valid values: debug, release, release-with-debug. kernel.extra_boot_args appends to the default boot args — -v for verbose, serial=3 to force the console onto UART3, and so on.
Kexts
boot_volume.kexts pins a list of kernel extensions by bundle ID. Only the listed ones land in the image; everything else in build/<target>/kexts/ is ignored.
First-boot script
root_volume.first_boot_script runs once, the first time the image boots. Use it to seed SSH keys, configure hostname, pre-download packages, or anything else you don’t want hardcoded into the rootfs.
Size
size_mb is the total image size. For an SD card destined for a 32 GB card, size the image to something below the card capacity (e.g. 30720 MB); the last partition auto-grows on first boot.
Writing the image
For QEMU, point -drive at the file:
-drive if=virtio,file=build/arm64-debug/darwinos.img,format=rawFor an SD card:
sudo dd if=build/arm64-debug/darwinos.img of=/dev/sdX bs=4M status=progressTriple-check /dev/sdX before running dd — there is no undo. lsblk (Linux) or diskutil list (macOS) helps identify the right device.
Signing
Release images can be code-signed end to end. Create a keypair once:
darwinos-certtool genkey --out release.key --pub release.pubPoint the manifest at it:
signing:
key: /path/to/release.key
identity: "darwinOS Release 2026"The resulting image has signed kexts, a signed bootloader, and a manifest that AMFI will accept on a SIP-enforced install. For development images, just omit the signing: block.
Multiple images from one build
make image accepts multiple manifests:
for m in manifests/*.yaml; do
make image MANIFEST=$m
doneUseful when you maintain a set of images for different boards or different testing scenarios — all built from the same kernel + userland + kext set.
Next steps
- Run darwinOS in QEMU — test your custom image.
- Port to a new board — once you have a custom image, get it onto real hardware.