Build and run

Run darwinOS in QEMU

Boot darwinOS inside QEMU on Linux, macOS, or Windows. The fastest way to try the system without touching real hardware.

QEMU is the quickest way to run darwinOS. It works on Linux, macOS, and Windows (via WSL), and it’s what the CI fleet uses to smoke-test every pull request. This guide assumes you’ve already built darwinOS from source. If you want UTM (the macOS-native QEMU frontend), there’s a dedicated UTM guide.

Prerequisites

  • QEMU 8.0 or newer, with qemu-system-aarch64 (for arm64) or qemu-system-x86_64 (for x86_64)
  • The build artefacts under build/<arch>-<profile>/: kernel.img, initrd.img, rootfs.img, boot.efi
  • About 4 GB of free RAM for the VM

Check your QEMU version:

qemu-system-aarch64 --version

If it’s older than 8.0 you may hit boot regressions — upgrade via your package manager or build from source.

Boot an arm64 image

The canonical arm64 invocation runs the virt machine with a UEFI loader:

qemu-system-aarch64 \
  -machine virt,gic-version=3 \
  -cpu max \
  -smp 4 \
  -m 4096 \
  -bios /usr/share/qemu-efi-aarch64/QEMU_EFI.fd \
  -drive if=virtio,file=build/arm64-debug/rootfs.img,format=raw \
  -kernel build/arm64-debug/kernel.img \
  -initrd build/arm64-debug/initrd.img \
  -append "console=ttyAMA0 root=/dev/vda" \
  -nographic

-nographic pipes the serial console to your terminal. You’ll see the kernel banner, then launchd coming up, then a shell prompt. Press Ctrl-a x to kill QEMU.

What the flags mean

  • -machine virt,gic-version=3 — the virt machine with a GICv3 interrupt controller (what XNU expects)
  • -cpu max — expose every architectural feature QEMU supports; drops to the host’s set on native hosts
  • -smp 4 — four virtual CPUs
  • -m 4096 — 4 GiB of RAM; drop to 2048 on low-memory hosts
  • -bios …/QEMU_EFI.fd — the AAVMF UEFI firmware shipped by your distribution
  • -drive if=virtio,… — exposes the root filesystem as a virtio-blk device; XNU’s virtio driver picks it up
  • -kernel … -initrd … -append — direct kernel boot, bypassing the EFI boot stub. Faster for dev than going through boot.efi

Locating the EFI firmware

The -bios path varies by distribution:

  • Debian/Ubuntu: /usr/share/qemu-efi-aarch64/QEMU_EFI.fd
  • Fedora: /usr/share/edk2/aarch64/QEMU_EFI.fd
  • Arch: /usr/share/edk2-armvirt/aarch64/QEMU_EFI.fd
  • Homebrew on macOS: $(brew --prefix)/share/qemu/edk2-aarch64-code.fd

Boot an x86_64 image

qemu-system-x86_64 \
  -machine q35 \
  -cpu max \
  -smp 4 \
  -m 4096 \
  -drive if=virtio,file=build/x86_64-debug/rootfs.img,format=raw \
  -kernel build/x86_64-debug/kernel.img \
  -initrd build/x86_64-debug/initrd.img \
  -append "console=ttyS0 root=/dev/vda" \
  -nographic

On Linux hosts you can add -enable-kvm for near-native speed; on macOS, -accel hvf enables Apple’s Hypervisor.framework. Without acceleration, expect noticeably slower boots.

Networking

By default QEMU gives the guest a user-mode NAT — the guest can reach the outside world, but nothing can reach the guest. For SSH-style access, add a host forward:

-netdev user,id=n0,hostfwd=tcp::2222-:22 \
-device virtio-net-pci,netdev=n0

Then from the host:

ssh -p 2222 root@localhost

(Once sshd is part of the image — currently root autologs on the serial console.)

Snapshots

To take a snapshot of a running VM:

Ctrl-a c
(qemu) savevm my-snapshot
(qemu) quit

Restore with -loadvm my-snapshot on the next boot. Great for skipping boot time during iterative debugging.

Common problems

“No bootable device” in the UEFI menu

The -drive path is wrong or rootfs.img is empty. Double-check the path and ls -lh the image.

Kernel panics immediately after the banner

Almost always a mismatch between kernel.img and rootfs.img. Rebuild both from the same config:

make clean && make world

Terminal control characters are weird

Some shells mangle the serial console. If line editing looks broken, try -serial mon:stdio instead of -nographic — it gives you the QEMU monitor on the same terminal.

Next steps