Chapter 123: Yocto vs Buildroot, an honest comparison¶
What: picking a build system shapes your product’s CI, release, and maintenance flow for years. Buildroot (the make-driven, “tightly-curated tree of packages” approach) vs Yocto/OpenEmbedded (the metadata-driven, “recipes + layers” approach). We walk through the mental model of each, build the same image with both side-by-side, compare reproducibility, build times, SDK production, multi-machine support, layer composition, and BSP integration. Then a verdict on when each wins, when each is a poor fit, and when neither is right. BSP: Board Support Package: vendor patches, configs, bootloader files, and scripts needed to boot one board. Yocto: a metadata-driven build system for producing custom Linux distributions. Buildroot: a configuration-driven build system that produces a complete root filesystem and related images.
Why: most production embedded Linux teams use Buildroot or Yocto. Some use both. The choice affects:
hiring (Yocto skills are scarcer and more expensive than Buildroot),
CI infrastructure (Yocto builds are slower and need more storage),
onboarding a new engineer (Buildroot is friendlier),
maintaining a fleet of variants (Yocto wins),
debugging a build problem (Buildroot wins).
Choosing badly costs months. Choosing well saves them.
Focus: the mental model is different, Buildroot is “menuconfig builds a complete image”. Yocto is “metadata recipes are combined to produce many possible images.” Buildroot scales by adding packages. Yocto scales by adding layers + machines + distros. For a single product with 1–3 variants, Buildroot wins. For a vendor BSP that serves dozens of customer products from one codebase, Yocto wins. Most teams overestimate their multi-variant complexity and end up with Yocto sledgehammers cracking Buildroot walnuts.
123.1 Mental model side-by-side¶
Buildroot |
Yocto / OpenEmbedded |
|
|---|---|---|
Configuration unit |
a single |
a stack of layers; a machine + distro + image recipe |
Build engine |
GNU Make |
BitBake (Python-driven, task-based) |
Package definitions |
|
|
Per-package config |
menuconfig globals |
|
Parallel builds |
yes (make -j) |
yes (BitBake tasks across CPUs + machines) |
Build cache |
none (rebuild from scratch is the norm) |
sstate-cache (artifacts keyed by input hash) |
Time-to-first-image |
30–60 min |
1–4 hours (initial); 5–30 min (incremental) |
Output |
a single |
per-image |
SDK production |
|
|
Reproducibility |
quasi (depends on host) |
strong (with |
Number of packages |
~3000 |
~10,000+ |
Documentation |
clear, single PDF |
sprawling, spread across many sources |
Learning curve |
gentle (few weeks) |
steep (months) |
123.2 Buildroot in 5 minutes¶
git clone https://git.buildroot.net/buildroot
cd buildroot
make qemu_arm_versatile_defconfig # or your custom defconfig
make menuconfig
# Target options → Target Architecture → ARM
# Toolchain → external Linaro toolchain
# Target packages → Hardware handling → i2c-tools (check)
# Filesystem images → ext2/3/4 root filesystem (check)
# Bootloaders → U-Boot
# Kernel → use the Linux 6.6
make -j$(nproc)
# 30 min later:
ls output/images/
# bzImage rootfs.ext4 rootfs.tar u-boot.bin
That’s everything in one tree: cross-toolchain (built or downloaded), U-Boot, kernel, rootfs with the packages you picked. One command, predictable output. To save your config:
MCU bridge: Think of the rootfs as the firmware image’s file-backed runtime environment. On an MCU you link everything into flash. On Linux, programs and config live in this mounted tree. MCU bridge: Think of U-Boot like a much larger boot stub plus debug monitor: it initializes hardware, loads the next image, and gives you commands before Linux starts. rootfs: root filesystem, the directory tree mounted at / that contains /bin, /etc, /dev, and libraries. U-Boot: the bootloader that initializes enough hardware to load and start the Linux kernel.
make savedefconfig # writes to configs/myboard_defconfig
Commit configs/myboard_defconfig to git. Teammates make myboard_defconfig && make to get identical output.
Adding a custom package:
# package/myapp/Config.in
config BR2_PACKAGE_MYAPP
bool "myapp"
help
My internal app.
# package/myapp/myapp.mk
MYAPP_VERSION = 1.0
MYAPP_SITE = $(BR2_EXTERNAL)/src/myapp
MYAPP_SITE_METHOD = local
MYAPP_DEPENDENCIES = libcurl
define MYAPP_BUILD_CMDS
$(MAKE) CC="$(TARGET_CC)" -C $(@D)
endef
define MYAPP_INSTALL_TARGET_CMDS
$(INSTALL) -m 0755 $(@D)/myapp $(TARGET_DIR)/usr/bin/
endef
$(eval $(generic-package))
50 lines. The generic-package framework provides defaults for everything you don’t override. Add source "package/myapp/Config.in" somewhere in the Config.in tree. Rebuild. myapp is now in the rootfs.
123.3 Yocto in 30 minutes¶
git clone -b kirkstone https://git.yoctoproject.org/poky
cd poky
. oe-init-build-env
# Working in build/
# Pick a machine
# bblayers.conf: BBLAYERS += "/path/to/meta-imx /path/to/meta-mybsp"
# local.conf: MACHINE = "imx6ull-myboard"
# DISTRO = "poky"
bitbake core-image-minimal
# Takes 1–4 hours first build (downloads + compiles ~1000 packages)
ls tmp/deploy/images/imx6ull-myboard/
# core-image-minimal-imx6ull-myboard.tar.bz2
# zImage imx6ull-myboard.dtb u-boot.imx ...
The pieces:
poky= the reference distro + tooling.meta-imx= NXP’s BSP layer (ormeta-freescalefor older).meta-mybsp= your layer with board DTS + machine config.local.conf= build-specific knobs.
A layer’s structure:
meta-mybsp/
├── conf/
│ ├── layer.conf
│ └── machine/imx6ull-myboard.conf
├── recipes-bsp/
│ └── u-boot/u-boot-mybsp_2026.04.bb
├── recipes-kernel/
│ └── linux/linux-mybsp_6.6.bb
├── recipes-core/
│ └── images/myapp-image.bb
└── recipes-myapp/
└── myapp/myapp_1.0.bb
Each .bb recipe is a Python+shell file describing how to fetch, configure, build, install, and package one component.
myapp_1.0.bb:
DESCRIPTION = "My internal app"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://LICENSE;md5=abc..."
SRC_URI = "git://github.com/myorg/myapp.git;protocol=https;branch=main"
SRCREV = "abc123def456"
S = "${WORKDIR}/git"
DEPENDS = "curl"
do_compile() {
oe_runmake CC="${CC}" CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}"
}
do_install() {
install -d ${D}${bindir}
install -m 0755 ${S}/myapp ${D}${bindir}/
}
The ${...} are BitBake variables filled in by the layer config + base classes (inherit autotools, inherit cmake, etc., provide build-system-specific defaults).
123.4 When Buildroot wins¶
One product, few variants. A single defconfig per variant. Everything in one tree.
Build-from-source is the goal. No interest in vendor SDKs or sharing recipes across products.
Small team. 1–5 engineers can master Buildroot in a week.
Predictable build time matters. Buildroot builds are fast (30 min) and deterministic.
Debugging build failures. Stepping through a
makeis easier than a BitBake task graph.Small rootfs. Buildroot’s curated packages are lean. The default core-image is ~30 MB. Yocto’s
core-image-minimalis closer to ~10 MB (depending on init/packagegroups). Onlycore-image-baseorcore-image-full-cmdlineclimbs into the 60 MB+ range.
Example: a hobbyist product, a one-off consumer gadget, a small-fleet IoT device. Buildroot for these is right.
123.5 When Yocto wins¶
Multi-customer BSP. One vendor’s layer (
meta-imx) shared across 50+ downstream products.Many machines, one codebase.
MACHINE = imx6ull-foovsMACHINE = imx8mq-barfrom the same layer.Vendor support contracts. NXP, TI, ST ship Yocto BSPs as their officially-supported integration.
Reproducibility (binary-identical builds). sstate-cache +
BB_HASHSERVEgive the strongest reproducibility in the industry.Compliance / license tracking. Yocto generates SBOMs (Software Bills of Materials), license manifests, source archives for GPL compliance.
Distro-style packaging. Build .deb or .rpm for OTA delivery.
Example: a vendor-grade industrial gateway, a fleet of medical devices, anything needing rigorous compliance + 10-year maintenance.
123.6 When neither is right¶
Bare-metal RTOS: both these tools assume a full Linux userspace.
Single-binary container: use Alpine + Docker. No rootfs builder needed.
Pre-built distribution:
Ubuntu CoreorDebian arm64may give you everything in 1 day. Skip the build tooling.Microcontroller: neither applies. Use Zephyr or vendor SDKs.
123.7 Side-by-side example, same recipe in both¶
A package foo that builds with autotools:
Buildroot¶
package/foo/Config.in:
config BR2_PACKAGE_FOO
bool "foo"
select BR2_PACKAGE_LIBBAR
package/foo/foo.mk:
FOO_VERSION = 1.0
FOO_SITE = https://example.com
FOO_SOURCE = foo-$(FOO_VERSION).tar.gz
FOO_DEPENDENCIES = libbar
FOO_LICENSE = MIT
$(eval $(autotools-package))
~10 lines. Done.
Yocto¶
meta-mybsp/recipes-myapp/foo/foo_1.0.bb:
DESCRIPTION = "foo utility"
HOMEPAGE = "https://example.com"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://COPYING;md5=abc..."
SRC_URI = "https://example.com/foo-${PV}.tar.gz"
SRC_URI[md5sum] = "abc..."
SRC_URI[sha256sum] = "abc..."
DEPENDS = "libbar"
inherit autotools
~10 lines too! Yocto’s class system (inherit autotools) handles configure/build/install identically to Buildroot’s autotools-package. For simple recipes, similar effort.
For complex builds, Yocto’s task graph (do_fetch, do_unpack, do_patch, do_configure, do_compile, do_install, do_package, …) gives finer control but more places to get lost.
123.8 Reproducible builds¶
Buildroot is mostly reproducible: pin Buildroot version + pin defconfig + pin host toolchain → likely identical output, but not guaranteed.
Yocto with BB_HASHSERVE is strongly reproducible, same inputs always produce bit-for-bit identical outputs. Important for security-audited products and regulatory compliance.
Both can be made fully reproducible with care. Yocto requires less effort.
123.9 SDK production¶
For application developers (who don’t want to build the whole kernel + rootfs), produce an SDK:
Buildroot¶
make sdk
# output/images/sdk.tar.gz — extract, source environment-setup, cross-compile your app
Yocto¶
bitbake -c populate_sdk core-image-minimal
# tmp/deploy/sdk/poky-glibc-x86_64-core-image-minimal-...-toolchain-....sh
# Run the installer; source environment-setup-...
Both produce a directory with arm-none-linux-gnueabihf-gcc and a sysroot. Yocto’s SDK is slightly larger (more headers, more docs). Buildroot’s is leaner.
123.10 CI integration¶
Both work in CI. Yocto needs more storage (sstate-cache can be 10+ GB) and longer initial build. Buildroot starts faster but has no cache. Every clean build is 30 min.
For CI:
Buildroot:
make defconfig && makein a Docker container per build.Yocto: keep an sstate-cache mounted across builds (NFS, S3). First build is slow, subsequent are fast.
NFS: Network File System, which lets the target mount a host directory over Ethernet during development.
123.11 Lab¶
Buildroot bring-up. Clone Buildroot.
make qemu_arm_versatile_defconfig.make. Run the resulting image in QEMU. Time the build.Buildroot custom package. Add a “hello world” package. Verify it appears in the rootfs.
Buildroot defconfig for your board. Customize for i.MX6ULL Point Atom. Save defconfig. Commit. Teammate reproduces.
Yocto bring-up. Clone poky kirkstone branch. Source
oe-init-build-env. SetMACHINE = "qemuarm".bitbake core-image-minimal. Time the build (initial vs second).Yocto custom recipe. Write a
.bbfor the same “hello world.” Verify it’s in the rootfs.Side-by-side comparison. Build the same rootfs from both. Compare: size, package list, build time, disk usage during build.
Yocto layer. Create your own layer (
bitbake-layers create-layer ../meta-mine). Add a board machine config. Verify it works.SDK from each. Generate SDK. Cross-compile a 50-line C program with each. Compare results.
Update one package. Increment a package version in both. Buildroot: edit
foo.mk, rebuild. Yocto: write a newfoo_1.1.bb, rebuild.Verdict. Write a 200-word “we should choose X” for your team’s situation. Defend.
123.12 Pitfalls¶
Yocto initial build crushing your laptop. Needs 50+ GB disk, 4+ GB RAM, 4+ cores. A Raspberry Pi can’t do it. Use a beefy build server.
Buildroot reproducibility myths. Same defconfig + different host = different output (host gcc version, host libc influences some packages). Pin everything you can.
Yocto layer recipes too generic. Easy to write recipes that work on x86 but break on ARM.
bitbake -c devshellto debug per-package builds.sstate-cache poisoning. A bad sstate entry causes cryptic failures.
bitbake -c cleansstate <recipe>to evict.Layer-priority confusion. Two layers ship the same recipe. The higher-priority one wins. Set explicit priorities in
layer.conf.bbappendfor wrong version.foo_%.bbappendmatches any version.foo_1.2.%.bbappendmatches 1.2.x. Use the right pattern.Buildroot
make cleanblasts everything. Then full 30-min rebuild. Usemake foo-rebuildfor incremental.License compliance forgotten. GPL requires source distribution. Both tools can produce source tarballs. Configure and store them.
DISTRO != MACHINE. Yocto’s
DISTRO(e.g.,poky,oe-core,mistral) andMACHINE(e.g.,imx6ull-myboard) are orthogonal. Mixing them up = surprising configurations.Building on macOS / Windows. Both Buildroot and Yocto only support Linux build hosts well. Use a Linux VM or WSL2.
Choosing Yocto for a 1-person project. You’ll spend more time on Yocto than on your product. Use Buildroot.
123.13 Going deeper¶
Buildroot manual: https://buildroot.org/downloads/manual/manual.html. Concise, covers everything.
Yocto Project Documentation: https://docs.yoctoproject.org/. Sprawling but authoritative.
OpenEmbedded Layer Index: https://layers.openembedded.org/. Search for existing recipes/layers.
bblayers.conf+local.conf: your Yocto build’s primary controls.Bootlin’s Buildroot training material: free PDFs.
Konsulko Group’s Yocto training: paid but excellent.
bitbake-cookerdaemon-log: for debugging build orchestration.oe-pkgdata-util+bitbake-getvar: for introspecting recipes.Ch 123A: when you’ve decided Yocto + need to write production-quality layers.
Ch 35: original Buildroot chapter.
Next chapter: Chapter 123A: Yocto layer development in depth.