BitBake Overview
{:.gc-basic}
Basic
BitBake is a task execution engine — closer to a make replacement than a package manager. It reads recipes (.bb files), resolves their dependency graph, and executes tasks in the correct order, potentially in parallel. BitBake itself has no knowledge of Linux distributions, packages, or embedded systems; all that knowledge comes from the OpenEmbedded metadata layers built on top of it.
The Yocto Stack
┌─────────────────────────────────────────────┐
│ Your product layer │ meta-myproduct/
├─────────────────────────────────────────────┤
│ BSP layer (board support) │ meta-raspberrypi/
├─────────────────────────────────────────────┤
│ Poky distribution layer │ meta-poky/
├─────────────────────────────────────────────┤
│ OpenEmbedded-Core (OE-core, recipes) │ meta/
├─────────────────────────────────────────────┤
│ BitBake (task engine) │ bitbake/
└─────────────────────────────────────────────┘
Poky = BitBake + OE-core + meta-poky + meta-yocto-bsp. It is the Yocto Project’s reference distribution — a working starting point, not something you ship.
File Types
| Extension | Role | Example |
|---|---|---|
.bb |
Recipe — describes how to build one package | curl_8.7.1.bb |
.bbclass |
Class — shared logic inherited by recipes | autotools.bbclass |
.bbappend |
Append — extends/overrides an existing recipe | curl_%.bbappend |
.conf |
Configuration — global variables | local.conf, bblayers.conf |
.inc |
Include file — shared variable blocks | curl.inc |
Essential BitBake Commands
# Source the environment (must do this in every new shell)
source oe-init-build-env build/
# Build a recipe
bitbake curl
# Build a specific task of a recipe
bitbake -c compile curl
bitbake -c clean curl
bitbake -c cleanall curl # removes downloads + sstate cache entry
# Build an image
bitbake core-image-minimal
# Show all recipes matching a name
bitbake-layers show-recipes curl
# Dump the fully expanded variable environment for a recipe
bitbake -e curl | grep ^WORKDIR
# List all tasks for a recipe
bitbake -c listtasks curl
# Build and show which tasks actually ran (vs cache hits)
bitbake -v curl 2>&1 | grep "^NOTE: recipe"
Recipe File Anatomy
{:.gc-basic}
Basic
Header Variables
# meta-myproduct/recipes-myapp/myapp/myapp_1.4.2.bb
SUMMARY = "My sensor data collection daemon"
DESCRIPTION = "myapp collects sensor readings from I2C/SPI peripherals \
and forwards data to a cloud backend over MQTT or HTTPS."
HOMEPAGE = "https://github.com/myorg/myapp"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://LICENSE;md5=abc123def456abc123def456abc123de"
# Package version components
PV = "1.4.2" # Package Version (usually in filename)
PR = "r0" # Package Revision (bump when recipe changes, not upstream)
# Source
SRC_URI = "https://github.com/myorg/myapp/archive/refs/tags/v${PV}.tar.gz"
SRC_URI[sha256sum] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
# Source directory inside the tarball
S = "${WORKDIR}/myapp-${PV}"
Minimal C Application Recipe
# meta-myproduct/recipes-myapp/myapp/myapp_1.4.2.bb
SUMMARY = "Hello World daemon for embedded target"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://LICENSE;md5=d41d8cd98f00b204e9800998ecf8427e"
SRC_URI = "https://releases.myorg.io/myapp/myapp-${PV}.tar.gz \
file://0001-fix-cross-compile-strncpy.patch \
file://myapp.service \
"
SRC_URI[sha256sum] = "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"
S = "${WORKDIR}/myapp-${PV}"
DEPENDS = "libcurl openssl"
inherit autotools systemd
SYSTEMD_SERVICE:${PN} = "myapp.service"
SYSTEMD_AUTO_ENABLE = "enable"
# Passed to ./configure
EXTRA_OECONF = "--enable-daemon --disable-tests"
Version and Name Variables
PN = "myapp" # Package Name (derived from filename)
PV = "1.4.2" # Package Version (derived from filename)
PR = "r0" # Package Revision
# Full name: ${PN}-${PV}-${PR} → myapp-1.4.2-r0
# WORKDIR: ${TMPDIR}/work/${MULTIMACH_TARGET_SYS}/${PN}/${PV}-${PR}/
# Example: tmp/work/aarch64-poky-linux/myapp/1.4.2-r0/
Tasks
{:.gc-mid}
Intermediate
Standard Task Execution Order
do_fetch
└─► do_unpack
└─► do_patch
└─► do_prepare_recipe_sysroot
└─► do_configure
└─► do_compile
└─► do_install
└─► do_package
└─► do_package_qa
└─► do_packagedata
└─► do_rootfs (image recipes)
# Run a recipe up to and including a specific task:
bitbake -c configure myapp # stops after do_configure
bitbake -c compile myapp # stops after do_compile
bitbake -c install myapp # stops after do_install
Key Directory Variables
| Variable | Typical Value | Meaning |
|---|---|---|
WORKDIR |
tmp/work/aarch64-poky-linux/myapp/1.4.2-r0/ |
Per-recipe working area |
S |
${WORKDIR}/myapp-1.4.2 |
Extracted source directory |
B |
${WORKDIR}/build |
Build directory (may equal S) |
D |
${WORKDIR}/image |
Fake installation root (destdir) |
STAGING_DIR_TARGET |
tmp/sysroots/myboard/ |
Target sysroot (headers + libs) |
STAGING_BINDIR_CROSS |
tmp/sysroots-components/.../bin |
Cross-compiler binaries |
Custom Tasks
# Add a task that runs after do_compile
do_sign_binary() {
codesign --key ${SIGNING_KEY} ${B}/myapp
}
addtask sign_binary after do_compile before do_install
# Python task
python do_check_version() {
import re
pv = d.getVar('PV')
if not re.match(r'^\d+\.\d+\.\d+$', pv):
bb.fatal("PV must be in X.Y.Z format, got: %s" % pv)
}
addtask check_version before do_fetch
Overriding Standard Tasks
# Replace do_compile entirely
do_compile() {
oe_runmake -C ${S} \
CC="${CC}" \
CFLAGS="${CFLAGS}" \
LDFLAGS="${LDFLAGS}" \
PREFIX=/usr \
all
}
# Append to do_install without replacing it
do_install:append() {
install -d ${D}${sysconfdir}/myapp
install -m 0644 ${S}/myapp.conf.example ${D}${sysconfdir}/myapp/myapp.conf
# Remove static libraries we don't want in the package
rm -f ${D}${libdir}/*.a
}
SRC_URI and Patching
{:.gc-mid}
Intermediate
SRC_URI Fetcher Types
# HTTP tarball (most common)
SRC_URI = "https://example.com/releases/myapp-${PV}.tar.gz"
# Local file in the recipe's files/ directory
SRC_URI = "file://myapp.conf \
file://0001-fix-cross.patch \
"
# Git repository at a specific commit
SRC_URI = "git://github.com/myorg/myapp.git;protocol=https;branch=main"
SRCREV = "a3f92c1b8d45e7f01234567890abcdef01234567"
PV = "1.0+git${SRCPV}"
# Multiple sources
SRC_URI = "https://example.com/myapp-${PV}.tar.gz \
https://example.com/myapp-data-${PV}.tar.gz;subdir=data \
file://0001-fix-makefile.patch \
file://myapp.service \
"
SRC_URI[myapp-${PV}.tar.gz.sha256sum] = "abc123..."
SRC_URI[myapp-data-${PV}.tar.gz.sha256sum] = "def456..."
Checksums
Every downloaded file must have a checksum to ensure reproducibility:
SRC_URI[sha256sum] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
# Generate the correct checksum:
sha256sum ~/Downloads/myapp-1.4.2.tar.gz
# Or let BitBake tell you (it will error and print the correct value):
bitbake myapp
# ERROR: myapp-1.4.2-r0 do_fetch: SRC_URI checksum error...
# Computed checksums:
# sha256sum: e3b0c44298fc1c149afbf4c8996fb924...
Patches
Patches in files/ are applied automatically by do_patch in lexical order:
# Patch naming convention: <NNN>-<description>.patch
recipes-myapp/myapp/files/
├── 0001-fix-cross-compile-strncpy.patch
├── 0002-disable-werror-on-gcc14.patch
└── 0003-add-systemd-notify-support.patch
Creating a patch:
# Inside output/build/myapp-1.4.2/ (or use devtool):
cd tmp/work/aarch64-poky-linux/myapp/1.4.2-r0/myapp-1.4.2/
git init && git add -A && git commit -m "original"
# Make your changes, then:
git diff > /path/to/meta-myproduct/recipes-myapp/myapp/files/0001-my-fix.patch
AUTOREV — Always Latest Git
# Track HEAD of a branch (not reproducible — use only during development)
SRCREV = "${AUTOREV}"
PV = "1.0+git${SRCPV}"
# Better: pin to a tag
SRCREV = "v1.4.2"
devtool Patch Workflow
# Check out a recipe's source for editing
devtool modify myapp
# Edit source files in workspace/sources/myapp/
vim workspace/sources/myapp/src/main.c
# Create a patch from your changes
devtool finish myapp meta-myproduct/
# → Creates patch in recipes-myapp/myapp/files/ and adds to SRC_URI
DEPENDS and RDEPENDS
{:.gc-mid}
Intermediate
Build-time vs Runtime Dependencies
# DEPENDS: needed to compile (headers, static libs, build tools)
# These packages are installed into the sysroot before do_configure runs
DEPENDS = "libcurl openssl zlib host-pkgconf"
# RDEPENDS: needed at runtime on the target device
# These are added to the target image when myapp is included
RDEPENDS:${PN} = "libcurl openssl"
# RDEPENDS can be package-specific (useful for -dev, -doc splits)
RDEPENDS:${PN}-dev = "openssl-dev"
Virtual Recipes and PROVIDES
# A virtual recipe doesn't build anything itself — it's a placeholder
# that other recipes fulfill.
# In virtual/kernel (provided by linux-yocto, linux-raspberrypi, etc.):
PROVIDES = "virtual/kernel"
# Your BSP layer picks which kernel:
PREFERRED_PROVIDER_virtual/kernel = "linux-raspberrypi"
PREFERRED_PROVIDER_virtual/libc = "glibc"
PREFERRED_PROVIDER_virtual/xserver = "xserver-xorg"
# Consumer recipe depends on the virtual:
DEPENDS = "virtual/kernel"
# BitBake resolves this to whichever recipe PROVIDES virtual/kernel
Common inherit Directives
inherit autotools # do_configure = ./configure, do_compile = make
inherit cmake # do_configure = cmake, do_compile = cmake --build
inherit pkgconfig # enables pkg-config in do_configure
inherit systemd # installs and enables systemd units
inherit useradd # creates users/groups on target
inherit update-rc.d # manages SysVinit scripts
inherit python3native # uses host Python 3 for build steps
inherit native # builds recipe for host machine (not target)
Package Splitting
# By default BitBake splits packages into:
# ${PN}, ${PN}-dev, ${PN}-doc, ${PN}-dbg, ${PN}-staticdev
# Add files to a specific sub-package:
FILES:${PN} = "${bindir}/myapp ${sysconfdir}/myapp"
FILES:${PN}-dev = "${includedir} ${libdir}/*.so"
FILES:${PN}-doc = "${docdir}"
# Create a custom sub-package:
PACKAGES =+ "${PN}-tools"
FILES:${PN}-tools = "${bindir}/myapp-cli ${bindir}/myapp-debug"
RDEPENDS:${PN}-tools = "${PN}"
.bbappend Files
{:.gc-adv}
Advanced
A .bbappend file extends or overrides an existing recipe without forking it. This is the idiomatic way to customise upstream recipes in your own layer.
bbappend Discovery
BitBake finds .bbappend files via the BBFILES variable in layer.conf. A % wildcard matches any version:
# meta-myproduct/conf/layer.conf
BBFILES += "${LAYERDIR}/recipes-*/*/*.bb \
${LAYERDIR}/recipes-*/*/*.bbappend"
A file named curl_%.bbappend appends to any version of the curl recipe. A file named curl_8.7.1.bbappend appends to exactly that version.
Adding a Patch via bbappend
# meta-myproduct/recipes-support/curl/curl_%.bbappend
# FILESEXTRAPATHS must be prepended (not appended) so your files
# take precedence over the recipe's own files/ directory
FILESEXTRAPATHS:prepend := "${THISDIR}/files:"
SRC_URI += "file://0001-disable-ipv6-on-myboard.patch"
meta-myproduct/recipes-support/curl/
├── curl_%.bbappend
└── files/
└── 0001-disable-ipv6-on-myboard.patch
Overriding Variables
# meta-myproduct/recipes-core/busybox/busybox_%.bbappend
FILESEXTRAPATHS:prepend := "${THISDIR}/files:"
# Replace the upstream busybox configuration with our custom one
SRC_URI += "file://myboard-busybox.config"
# Override the fragment merging
do_configure:append() {
cat ${WORKDIR}/myboard-busybox.config >> ${B}/.config
merge_config.sh -m ${B}/.config ${WORKDIR}/myboard-busybox.config
}
Kernel bbappend — Adding a Config Fragment
# meta-myproduct/recipes-kernel/linux/linux-yocto_%.bbappend
FILESEXTRAPATHS:prepend := "${THISDIR}/files:"
# Add a kernel config fragment (enables CAN bus and disables NFS)
SRC_URI += "file://myboard.cfg"
# Add an out-of-tree kernel module
SRC_URI += "file://0001-add-mymcu-spi-driver.patch"
KERNEL_FEATURES:append = " features/netfilter/netfilter.scc"
# meta-myproduct/recipes-kernel/linux/files/myboard.cfg
# (Kernel config fragment — only changed values)
CONFIG_CAN=y
CONFIG_CAN_RAW=y
CONFIG_CAN_MCP251X=y
CONFIG_NFS_FS=n
CONFIG_NFSD=n
CONFIG_EXPERT=y
CONFIG_SLAB=y
Modifying do_install via bbappend
# meta-myproduct/recipes-support/openssh/openssh_%.bbappend
FILESEXTRAPATHS:prepend := "${THISDIR}/files:"
SRC_URI += "file://sshd_config_hardened"
do_install:append() {
# Replace upstream sshd_config with our hardened version
install -m 0600 ${WORKDIR}/sshd_config_hardened \
${D}${sysconfdir}/ssh/sshd_config
# Remove server keys — they'll be generated on first boot
rm -f ${D}${sysconfdir}/ssh/ssh_host_*_key
}
Advanced: Python in Recipes
{:.gc-adv}
Advanced
BitBake supports Python functions for complex logic that would be awkward in shell.
Python Tasks
python do_validate_config() {
import os
cfg = d.getVar('MY_CONFIG_FILE')
if not os.path.exists(cfg):
bb.fatal("Config file not found: %s" % cfg)
bb.note("Config file validated: %s" % cfg)
}
addtask validate_config before do_configure after do_unpack
Accessing and Modifying Variables
python () {
# Anonymous Python — runs during parsing, not during task execution
pv = d.getVar('PV')
major = int(pv.split('.')[0])
if major < 2:
bb.warn("myapp %s is end-of-life, upgrade to 2.x" % pv)
# Conditionally set variables based on MACHINE
machine = d.getVar('MACHINE')
if machine.startswith('raspberrypi'):
d.appendVar('EXTRA_OECONF', ' --enable-raspi-gpio')
}
OVERRIDES — Conditional Variables
# OVERRIDES is a colon-separated list of active overrides
# e.g.: aarch64:poky:class-target:myboard
# Set a variable only for a specific machine:
EXTRA_OECONF:myboard = "--enable-board-specific-feature"
# Set for a specific class:
CFLAGS:class-native = "-O2"
# Append for a specific arch:
CFLAGS:append:aarch64 = " -march=armv8-a+crc"
# Override for a specific distro feature:
MY_VAR:raspberrypi4 = "gpu_enabled"
MY_VAR = "gpu_disabled"
Interview Q&A
{:.gc-iq}
Interview Q&A
Q1 — Basic: What is the difference between DEPENDS and RDEPENDS?
DEPENDSlists packages that must be built and available in the sysroot before this recipe’sdo_configuretask can run. These are build-time dependencies — the cross-compiler needs their headers and libraries.RDEPENDSlists packages that must be present on the running target for this package to function — they are runtime dependencies installed alongside the package in the image. A package likeopensslis both aDEPENDS(you link against libssl.so during the build) and anRDEPENDS(the target needs libssl.so.3 at runtime). A package likehost-pkgconfis only aDEPENDS— it runs on the build machine, not the target.
Q2 — Basic: Why does every recipe need LIC_FILES_CHKSUM?
The Yocto Project enforces license tracking for legal compliance.
LIC_FILES_CHKSUMprovides a checksum of the actual license text inside the source tarball. If an upstream maintainer modifies the license (even a single word) between releases, the checksum will fail and BitBake will error out, forcing you to manually review the change. This prevents silently shipping code under a different license than you intended. TheLICENSEvariable uses SPDX identifiers for machine-readable license metadata in SBOM generation.
Q3 — Intermediate: Explain the relationship between WORKDIR, S, and B.
WORKDIRis the recipe’s working area:tmp/work/<arch>/<recipe>/<ver>/. Everything related to building this recipe lives here.S(Source Directory) is where BitBake unpacks the tarball — typically${WORKDIR}/<name>-<version>/.B(Build Directory) is where compilation happens — by default it equalsS, but CMake and some autotools projects setB = ${WORKDIR}/buildto support out-of-source builds (which keeps the source tree clean and allows multiple concurrent build configurations). Yourdo_configureanddo_compilefunctions run withBas the working directory.
Q4 — Intermediate: When would you use a .bbappend instead of forking a recipe?
Always prefer
.bbappendover forking. When you fork a recipe (copy it into your layer), you become responsible for maintaining it — every upstream security fix, version bump, or patch must be manually merged. A.bbappendlets the upstream recipe update while your customisations persist. Fork only when the upstream recipe is so fundamentally different from what you need that a bbappend would be longer than the recipe itself, or when you need to change the package name or version in ways that bbappend cannot express.
Q5 — Advanced: How do you add a kernel patch using a bbappend?
Create
meta-myproduct/recipes-kernel/linux/linux-yocto_%.bbappend, setFILESEXTRAPATHS:prepend := "${THISDIR}/files:"so BitBake finds your local files directory, and addSRC_URI += "file://0001-my-driver.patch". Place the patch inmeta-myproduct/recipes-kernel/linux/files/. Thedo_patchtask is inherited from thekernelbbclass and applies allSRC_URIpatches automatically. Usedevtool modify linux-yoctoto make and test the patch interactively before committing it to your layer.
Q6 — Advanced: What is the difference between SRCREV and pinning to a version tag?
Using a version tag in
SRC_URI(e.g., a tarball from a GitHub release) ties your recipe to a specific tarball that is hashed viaSRC_URI[sha256sum]. This is the most reproducible approach — the same bits every time. UsingSRCREVwith a git commit hash (SRCREV = "a3f92c1...") is equally reproducible but lets you use the git fetcher, which is useful when there are no official tarballs or when you want to use commits between releases.SRCREV = "${AUTOREV}"is the least reproducible — it fetches HEAD at build time, meaning two builds a day apart may produce different output. UseAUTOREVonly in development; always pinSRCREVbefore tagging a release.
References
{:.gc-ref}
References
| Resource | Link |
|---|---|
| BitBake User Manual | docs.yoctoproject.org/bitbake/ |
| Yocto Project Reference Manual | docs.yoctoproject.org/ref-manual/ |
| OpenEmbedded Layer Index | layers.openembedded.org |
| Yocto Project — devtool | docs.yoctoproject.org/sdk-manual/extensible.html |
| Bootlin Yocto Training | bootlin.com/doc/training/yocto |
| SPDX License List | spdx.org/licenses |