Patched qemu-system-arm Binary
nano-ros ships a project-local QEMU build that the test harness uses
in preference to whatever qemu-system-arm is on $PATH. This
chapter explains why it exists, how it gets built, how a test picks
it up, and how to add a new patch.
Why
Two production-blocking issues motivated the patched build:
-
LAN9118 RX FIFO drain bug (mainline QEMU through at least 11.0). Under sustained burst RX, the LAN9118 emulator’s
lan9118_can_receive()can stop returning true even after the guest drains the FIFO, so frames silently disappear. bisected this and ships the fix asthird-party/qemu/patches/0001-hw-net-lan9118-add-can_receive-flush-on-FIFO-drain.patch. Bare-metal MPS2-AN385 RTPS / Zenoh tests on the system QEMU sporadically fail without it. -
-netdev dgram,local.type=unix,…requires QEMU 7.2+. Ubuntu jammy ships QEMU 6.2, and the dgram-tunnel pattern is how NuttX / ThreadX DDS multi-instance tests cross-deliver frames between two QEMU processes after retired the broken-netdev socket,mcast=cross-process path. With a too-old system QEMU, those tests fall back to[SKIPPED].
Both problems disappear once tests use the patched build at
build/qemu/bin/qemu-system-arm.
Layout
third-party/qemu/qemu/ # submodule, pinned to stable-11.0
third-party/qemu/patches/ # patch series, applied on top
0001-hw-net-lan9118-…patch
build/qemu/bin/qemu-system-arm # final installed binary
build/qemu/share/qemu/… # firmware, etc.
The submodule URL and pin live in .gitmodules:
[submodule "third-party/qemu/qemu"]
path = third-party/qemu/qemu
url = https://gitlab.com/qemu-project/qemu.git
branch = stable-11.0
stable-11.0 is QEMU 11.0.x — already past the 7.2 cutoff for
-netdev dgram unix and recent enough that the patch series is
small.
Build
just qemu setup-qemu (pulled in by just setup qemu and
just setup all) does the full
build:
just qemu setup-qemu
The recipe:
- Inits the submodule shallowly if it is not already present.
- Short-circuits when
build/qemu/bin/qemu-system-armis newer than every file underthird-party/qemu/patches/(touch a patch to force a rebuild). - Resets the submodule to its pinned tip, applies every
.patchunderthird-party/qemu/patches/in alphabetical order viagit apply. - Configures with
--target-list=arm-softmmu(no other arches, no docs, no tools) and--enable-slirp. make -j$(nproc) && make installintobuild/qemu/.
End-to-end cost is roughly ten minutes the first time and ~150 MB of disk. Subsequent runs are no-ops.
just qemu doctor reports the build status and clearly distinguishes
the patched build from the system fallback.
How tests pick it up
The single resolver lives in
packages/testing/nros-tests/src/qemu.rs:
#![allow(unused)]
fn main() {
pub fn qemu_system_arm_path() -> std::ffi::OsString { … }
pub fn qemu_system_arm_cmd() -> std::process::Command { … }
}
Selection order:
QEMU_SYSTEM_ARMenv var — developer override / CI pin.- Project-local
<workspace>/build/qemu/bin/qemu-system-armwhen it exists (auto-detected viaCARGO_MANIFEST_DIRwalk to the workspaceCargo.toml). - System
qemu-system-armon$PATH— kept as fallback so a minimal install still produces a clean[SKIPPED]rather than an exec error.
Every Command::new("qemu-system-arm") in the test crates goes
through this helper. New tests must use it.
just/qemu-baremetal.just, just/nuttx.just and just/freertos.just
do the same in shell, gating their qemu-system-arm invocations
through:
QEMU_BIN := absolute_path("build/qemu") / "bin/qemu-system-arm"
# inside a recipe:
{{ if path_exists(QEMU_BIN) == "true" { QEMU_BIN } else { "qemu-system-arm" } }} -M virt …
Smoke test
packages/testing/nros-tests/tests/qemu_patched_binary.rs asserts:
qemu_system_arm_path()resolves to eitherQEMU_SYSTEM_ARMor<workspace>/build/qemu/bin/qemu-system-arm.- The patched binary reports version ≥ 7.2.
-netdev helpadvertisesdgram(the multi-instance backend).
Tests use nros_tests::skip! when the patched build is absent —
a fresh clone without just qemu setup-qemu surfaces a clear
[SKIPPED] with the suggested remedy instead of silently passing.
Adding a new patch
- Land the upstream fix or write a downstream-only patch against the pinned submodule tip.
- Save it as a numbered file under
third-party/qemu/patches/(e.g.0002-hw-net-…patch). Keep one logical change per patch. - Bump any inline comment that names specific patches.
- Touch the patch file (or just commit it) —
just qemu setup-qemudetects the patch is newer than the installed binary and rebuilds. - Bump the relevant CI cache key (see) so other machines also rebuild.
Submodule pin bump
When upstream rolls a new stable branch and an existing patch either lands upstream or no longer applies cleanly:
- Edit
.gitmodulesbranch = stable-…to the new branch name. cd third-party/qemu/qemu && git fetch && git checkout origin/stable-…- Re-run
just qemu setup-qemu. If a patch fails to apply, either drop it (landed upstream) or rebase it onto the new tip. - Commit the submodule SHA bump together with any patch-series updates.
Scope
Only qemu-system-arm is unified. qemu-system-riscv32 is
Espressif’s fork (different upstream, different patches);
qemu-system-riscv64 and qemu-system-aarch64 ship no patches
today. Other arches stay on the system binary until they
accumulate their own patches; the helper pattern is easy to copy
when that happens.