Porting

Build a custom image

Assemble a darwinOS image with a custom kernel configuration, a pinned userland set, and your own first-boot scripts.

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:

  1. EFI system partition — holds boot.efi and the kernel image.
  2. Boot volume/System/, read-only on release builds. Kernel extensions, frameworks, and the core daemon set.
  3. Root volume/Users/, /var/, /etc/. Read-write, mounted over the boot volume via APFS firmlinks.
  4. 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.img

Customising 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: 2048

Copy the file to image/custom.yaml, edit what you want, and build:

make image MANIFEST=custom.yaml

Kernel 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=raw

For an SD card:

sudo dd if=build/arm64-debug/darwinos.img of=/dev/sdX bs=4M status=progress

Triple-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.pub

Point 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
done

Useful 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