all
Stage 10

BitBake & Recipes

Master BitBake — the build engine powering Yocto. Learn recipe file syntax, task execution (do_fetch, do_compile, do_install), SRC_URI and patching, bbappend files for customization, and DEPENDS/RDEPENDS.

11 min read
96097 chars

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 reproducibleuse 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 itselfit'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 keysthey'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 Pythonruns 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?

DEPENDS lists packages that must be built and available in the sysroot before this recipe’s do_configure task can run. These are build-time dependencies — the cross-compiler needs their headers and libraries. RDEPENDS lists 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 like openssl is both a DEPENDS (you link against libssl.so during the build) and an RDEPENDS (the target needs libssl.so.3 at runtime). A package like host-pkgconf is only a DEPENDS — 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_CHKSUM provides 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. The LICENSE variable uses SPDX identifiers for machine-readable license metadata in SBOM generation.

Q3 — Intermediate: Explain the relationship between WORKDIR, S, and B.

WORKDIR is 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 equals S, but CMake and some autotools projects set B = ${WORKDIR}/build to support out-of-source builds (which keeps the source tree clean and allows multiple concurrent build configurations). Your do_configure and do_compile functions run with B as the working directory.

Q4 — Intermediate: When would you use a .bbappend instead of forking a recipe?

Always prefer .bbappend over 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 .bbappend lets 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, set FILESEXTRAPATHS:prepend := "${THISDIR}/files:" so BitBake finds your local files directory, and add SRC_URI += "file://0001-my-driver.patch". Place the patch in meta-myproduct/recipes-kernel/linux/files/. The do_patch task is inherited from the kernel bbclass and applies all SRC_URI patches automatically. Use devtool modify linux-yocto to 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 via SRC_URI[sha256sum]. This is the most reproducible approach — the same bits every time. Using SRCREV with 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. Use AUTOREV only in development; always pin SRCREV before 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