Build a SITL binary
A fresh ArduPilot checkout needs ./waf configure --board sitl and ./waf plane before
there is anything to fly. build_sitl runs both as a background job, so an agent can go
from a clone to a flying SITL without a shell.
Start a build
Section titled “Start a build”build_sitl() # the server's ardupilot tree, plane, board sitlbuild_sitl(vehicle="copter") # another waf targetbuild_sitl(tree="~/ardupilot-dev", configure_args=["--debug"])Configure runs only when the tree is not already configured for the board (waf records
it in build/c4che/_cache.py), or when you pass configure=true or any
configure_args. The call waits up to about 85 s and returns the job’s state; a first
build takes minutes, so it usually comes back still running:
{ "build_id": "a41f0c", "status": "building", "step": 522, "steps": 1426, "progress_pct": 36.6, "configured": false, "python": "/home/you/venv-ardupilot/bin/python3" }Follow it
Section titled “Follow it”build_status(build_id="a41f0c") # waits up to about 85 s, with progress notifications{ "build_id": "a41f0c", "status": "done", "step": 1426, "steps": 1426, "binary": "/home/you/ardupilot/build/sitl/bin/arduplane", "provenance": { "binary_mtime": "2026-10-03T21:19:05+00:00", "ardupilot_sha": "4c98c92", "ardupilot_patch_sha256": null, "stale_binary": [] } }A failed build returns status: "failed" with the error and the last lines of waf’s log.
cancel_build stops one, process group and all; list_builds shows this server’s builds.
An incremental build after a small change took under a minute on the stock tree.
Which trees it builds
Section titled “Which trees it builds”A rebuild replaces a binary other runs may be flying, and a binary rebuilt mid-run makes results lie about what flew. So the server builds:
- any tree named in
MCARDUPILOT_BUILD_TREES(separated by:or,), and - the default
MCARDUPILOT_ARDUPILOTtree, only while its git checkout is clean.
A tree with uncommitted changes, a patched tree, is refused unless you name it. Set
MCARDUPILOT_BUILD_DEFAULT_TREE=0 to build named trees only. One build runs per tree at a
time.
The Python waf runs under
Section titled “The Python waf runs under”ArduPilot’s build needs Python packages (empy 3.3.4 and the rest) that ArduPilot’s
install-prereqs script puts in ~/venv-ardupilot. A login shell activates that venv; an
MCP server does not, and under uvx the server’s own venv comes first on PATH. So the
build uses, in order: MCARDUPILOT_BUILD_PYTHON, then ~/venv-ardupilot/bin/python3,
then python3 on PATH outside the server’s venv. A one-second check before configure
turns a missing package into a clear error rather than a failure deep in waf’s output.
If your tree is a git worktree, or a copy of one, its submodule pointers may not resolve:
pass configure_args=["--no-submodule-update"].
Build from pinned inputs in a container
Section titled “Build from pinned inputs in a container”A tree build compiles whatever the working tree holds. For a binary you can name, rebuild and check later, give build_sitl its inputs explicitly and let it build in a pinned image:
build_sitl( tree="~/ardupilot", # the source repository: only read commit="4c98c92", patches=["~/my-project/sim/patches/sitl.patch"], # applied in order image="ardupilot/ardupilot-dev-chibios@sha256:8bb0f850fb3f...", reproduce=True, # build twice and compare)What happens, in order:
- A fresh local clone of the source is checked out at the commit. Its objects are hardlinked, so it costs little disk on the same filesystem, and it does not depend on the source afterwards.
- Submodules are cloned from the source’s own module repositories, recursively, so nothing needs the network. A source that never ran
git submodule update --init --recursiveis refused with that instruction. - Each patch is checked with
git apply --check, then applied. One that does not apply fails the build with git’s reason. ./waf configure --board sitl --no-submodule-updateand./waf planerun in the image as your user, with no network, the checkout mounted at one fixed path,SOURCE_DATE_EPOCHset from the commit time and ccache off.- The binary and the frame tables are copied to
<data_dir>/builds/<build_id>/, in tree layout, withmanifest.jsonbeside them, and the checkout is deleted.
{ "build_id": "449c85e46491", "status": "done", "cached": false, "manifest": { "inputs": { "commit": "4c98c922...", "patch_sha256": "8762cf5c2eca", "image": "...@sha256:8bb0f850..." }, "toolchain": { "gcc": "gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0", "python": "Python 3.12.3", "waf": "waf 2.0.27 (c3e645e3...)" }, "binary": { "path": "build/sitl/bin/arduplane", "sha256": "cc3b984b...", "size": 6124568 }, "build_s": 133.3, "reproduced": true, "rebuild_sha256": "cc3b984b..." } }build_id is a hash of the inputs (commit, patch hashes in order, image digest, vehicle, board, configure flags), so the same inputs always name the same directory. Asking again answers at once with cached: true; asking again with reproduce=true runs only the verification build.
When a rebuild does not match, differences says where: the two sizes, the first differing byte, and which ELF sections differ.
Fly the result by pointing open_sitl at the directory:
open_sitl(tree="~/.local/share/mcardupilot/builds/449c85e46491")Frame defaults come from the copied frame tables, and the session’s provenance carries build_id, image, the commit, the patch hashes and the binary’s sha256, read from the manifest rather than a git tree.