Simple Recipes

How to write practical BitBake recipes for common application types

This section expands the first recipe idea into a practical set of patterns you can reuse for small applications. The examples are deliberately tiny, but each one is complete enough to copy into a layer and build.

The matching files are in the repository at:

https://github.com/ming4real/meta-hello-worlds

Copy the recipes-apps directory into your own layer, or copy one recipe directory at a time while working through the lesson.

Anatomy of a Simple Recipe

Most application recipes answer the same questions:

What is it?

SUMMARY, DESCRIPTION, LICENSE, and LIC_FILES_CHKSUM describe the software and its licence.

Where does the source come from?

SRC_URI lists local files, patches, archives, or repositories. Local files use file://.

Where is the source tree?

S points to the source directory used by configure and compile tasks.

Where is the build tree?

B points to the build directory. Some classes use a separate build directory, especially cmake.

How is it built?

do_compile() may be handwritten, or supplied by a class such as cmake, cargo, or a Python build class.

How is it installed?

do_install() stages files into ${D} using target paths such as ${bindir} and ${sysconfdir}.

The important mental model is:

  • ${UNPACKDIR} is the recipe’s temporary work area after fetch and unpack
  • ${S} is the source directory used by build tasks
  • ${B} is the build directory used by build tasks
  • ${D} is the staged install root used by packaging
  • ${bindir} is normally /usr/bin
  • ${sysconfdir} is normally /etc
  • ${systemd_system_unitdir} is normally the target systemd unit directory

Compiled languages usually need do_compile(). Interpreted languages and script-only packages often do not compile anything, but they still need SRC_URI, licence metadata, do_install(), and image package selection.

Keep your own recipes in your own layer. A normal application layout looks like this:

The example layer in this repository follows that pattern:

For local files, BitBake searches the recipe’s files/ directory by default. That is why this works:

SRC_URI = "file://hello-c.c"

The local file is found at:

recipes-apps/hello-c/files/hello-c.c

C Example

This example demonstrates a minimal compiled C application. The recipe compiles directly with ${CC} and uses ${CFLAGS} and ${LDFLAGS} supplied by Yocto.

Directory structure:

recipes-apps/hello-c/
  hello-c_1.0.bb
  files/
    hello-c.c

Recipe:

SUMMARY = "Small C hello application"
DESCRIPTION = "Builds a single C source file with the Yocto C compiler."
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

SRC_URI = "file://hello-c.c"

S = "${UNPACKDIR}"

do_compile() {
    ${CC} ${CFLAGS} ${LDFLAGS} ${S}/hello-c.c -o hello-c
}

do_install() {
    install -d ${D}${bindir}
    install -m 0755 hello-c ${D}${bindir}/hello-c
}

Source file:

#include <stdio.h>

int main(void)
{
    puts("hello-c: built by BitBake with the Yocto C compiler");
    return 0;
}

Build and verify:

bitbake hello-c

Add hello-c to the image, boot the target, then run:

hello-c

Common mistakes:

  • using gcc instead of ${CC}
  • forgetting ${LDFLAGS}, which can trigger QA warnings
  • installing to ${bindir} without the ${D} prefix

C++ Example

This example is the C++ version of the direct compile pattern. It uses ${CXX} and ${CXXFLAGS} rather than assuming the host compiler.

Directory structure:

recipes-apps/hello-cpp/
  hello-cpp_1.0.bb
  files/
    hello-cpp.cpp

Recipe:

SUMMARY = "Small C++ hello application"
DESCRIPTION = "Builds a single C++ source file with the Yocto C++ compiler."
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

SRC_URI = "file://hello-cpp.cpp"

S = "${UNPACKDIR}"

do_compile() {
    ${CXX} ${CXXFLAGS} ${LDFLAGS} ${S}/hello-cpp.cpp -o hello-cpp
}

do_install() {
    install -d ${D}${bindir}
    install -m 0755 hello-cpp ${D}${bindir}/hello-cpp
}

Source file:

#include <iostream>
#include <string>

int main()
{
    const std::string message = "hello-cpp: built by BitBake with the Yocto C++ compiler";
    std::cout << message << '\n';
    return 0;
}

Build and verify:

bitbake hello-cpp

On the target:

hello-cpp

If the program uses extra libraries, add build-time dependencies to DEPENDS and runtime dependencies to RDEPENDS:${PN} when automatic shared-library dependency detection is not enough.

Make or CMake Example

Real projects often have a build system. This example uses a Makefile and oe_runmake, which passes the Yocto build environment through to make.

Directory structure:

recipes-apps/hello-make/
  hello-make_1.0.bb
  files/
    Makefile
    hello-make.c

Recipe:

SUMMARY = "Small Makefile-based C application"
DESCRIPTION = "Builds a C application using a Makefile that respects Yocto tool variables."
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

SRC_URI = " \
    file://Makefile \
    file://hello-make.c \
"

S = "${UNPACKDIR}"

do_compile() {
    oe_runmake
}

do_install() {
    oe_runmake install DESTDIR=${D} bindir=${bindir}
}

Makefile:

CC ?= cc
CFLAGS ?= -O2
LDFLAGS ?=
bindir ?= /usr/bin

all: hello-make

hello-make: hello-make.c
	$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)

install: hello-make
	install -d $(DESTDIR)$(bindir)
	install -m 0755 hello-make $(DESTDIR)$(bindir)/hello-make

clean:
	rm -f hello-make

Source file:

#include <stdio.h>

int main(void)
{
    puts("hello-make: built through oe_runmake");
    return 0;
}

oe_runmake is preferred over plain make because it runs Make with the environment BitBake prepared. The Makefile still has to respect variables such as CC, CFLAGS, LDFLAGS, DESTDIR, and bindir.

For CMake projects, prefer:

inherit cmake

and let the cmake class provide do_configure() and do_compile(). Use EXTRA_OECMAKE only for project-specific CMake options.

Rust Example

This example demonstrates a dependency-free Rust application using Yocto’s Cargo support. The important part is that Cargo runs inside the BitBake task environment. Dependencies should be vendored or declared through the normal Yocto crate mechanisms, not downloaded during do_compile().

Directory structure:

recipes-apps/hello-rust/
  hello-rust_1.0.bb
  files/
    Cargo.lock
    Cargo.toml
    src/
      main.rs

Recipe:

SUMMARY = "Small Rust hello application"
DESCRIPTION = "Builds a dependency-free Rust binary using Yocto Cargo support."
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

inherit cargo

SRC_URI = " \
    file://Cargo.toml \
    file://Cargo.lock \
    file://src/main.rs \
"

S = "${UNPACKDIR}"

do_install() {
    install -d ${D}${bindir}
    install -m 0755 ${B}/target/${CARGO_TARGET_SUBDIR}/hello-rust ${D}${bindir}/hello-rust
}

Cargo.toml:

[package]
name = "hello-rust"
version = "1.0.0"
edition = "2021"

[dependencies]

Cargo.lock:

# This file is automatically @generated by Cargo.
version = 3

[[package]]
name = "hello-rust"
version = "1.0.0"

Source file:

fn main() {
    println!("hello-rust: built by BitBake with Cargo support");
}

Build and verify:

bitbake hello-rust

On the target:

hello-rust

Common mistakes:

  • allowing Cargo to access the network during compile tasks
  • omitting Cargo.lock from application recipes
  • installing from the wrong target output directory

Go Example

This example demonstrates a dependency-free Go module. It inherits Yocto’s Go support and builds with ${GO}, not a host go binary found from your shell.

Directory structure:

recipes-apps/hello-go/
  hello-go_1.0.bb
  files/
    go.mod
    main.go

Recipe:

SUMMARY = "Small Go hello application"
DESCRIPTION = "Builds a dependency-free Go binary using Yocto Go support."
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

inherit go

GO_IMPORT = "example.com/hello-go"

SRC_URI = " \
    file://go.mod;subdir=${GO_IMPORT} \
    file://main.go;subdir=${GO_IMPORT} \
"

S = "${UNPACKDIR}/${GO_IMPORT}"

do_compile() {
    cd ${S}
    ${GO} build ${GOBUILDFLAGS} -o ${B}/hello-go .
}

do_install() {
    install -d ${D}${bindir}
    install -m 0755 ${B}/hello-go ${D}${bindir}/hello-go
}

go.mod:

module example.com/hello-go

go 1.22

Source file:

package main

import "fmt"

func main() {
	fmt.Println("hello-go: built by BitBake with Yocto Go support")
}

The subdir=${GO_IMPORT} parameters make BitBake unpack the local files into a module-like source directory. That keeps ${S} aligned with the Go import path.

Build and verify:

bitbake hello-go

On the target:

hello-go

For third-party Go modules, use Yocto’s normal vendoring and module handling rather than letting go build download from the network during do_compile().

Local Python Package Example

Python packages are interpreted, so there is no C-style compile step here. Instead, the Python class builds and installs the package from Python metadata. This example uses local source files and pyproject.toml.

Directory structure:

recipes-apps/hello-python/
  hello-python_1.0.bb
  files/
    pyproject.toml
    setup.cfg
    hello_python/
      __init__.py
      __main__.py

Recipe:

SUMMARY = "Small local Python package"
DESCRIPTION = "Installs a Python package from local source using pyproject metadata."
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

inherit python_setuptools_build_meta

SRC_URI = " \
    file://pyproject.toml \
    file://setup.cfg \
    file://hello_python/__init__.py \
    file://hello_python/__main__.py \
"

S = "${UNPACKDIR}"

RDEPENDS:${PN} += "python3-core"

pyproject.toml:

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

setup.cfg:

[metadata]
name = hello-python
version = 1.0.0

[options]
packages = find:

[options.entry_points]
console_scripts =
    hello-python = hello_python.__main__:main

Source files:

__version__ = "1.0.0"
def main():
    print("hello-python: installed from local source by a Yocto recipe")


if __name__ == "__main__":
    main()

Build and verify:

bitbake hello-python

On the target:

hello-python

Common mistakes:

  • forgetting the target runtime dependency on python3-core
  • mixing hyphenated distribution names with underscored Python import names
  • trying to run pip install on the target instead of packaging the module into the image

When people say “install it with pip” in a Yocto project, they usually mean one of two different things:

  • use pip on a developer machine to inspect or prototype a Python package
  • write a BitBake recipe that fetches, builds, packages, and installs that Python package into the image

For production images, prefer the second option. The image should contain a package produced by BitBake, not depend on the target running pip at first boot.

This local hello-pip example uses the same PEP 517 build path that Yocto uses for many pip-style Python projects, but keeps the source local and dependency-free for training.

Directory structure:

recipes-apps/hello-pip/
  hello-pip_1.0.bb
  files/
    pyproject.toml
    setup.cfg
    hello_pip/
      __init__.py
      __main__.py

Recipe:

SUMMARY = "Small Python wheel-style package example"
DESCRIPTION = "Builds and installs a Python package with the same PEP 517 path used for pip-style packages."
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

inherit python_setuptools_build_meta

SRC_URI = " \
    file://pyproject.toml \
    file://setup.cfg \
    file://hello_pip/__init__.py \
    file://hello_pip/__main__.py \
"

S = "${UNPACKDIR}"

RDEPENDS:${PN} += "python3-core"

The source files are the same shape as the local Python package example, but the lesson point is different: use Yocto’s Python packaging classes to create an image package. Do not call pip install in do_install() and do not expect the target to fetch packages from the internet.

For a real PyPI package, SRC_URI would normally point at a pinned sdist or wheel, with checksums, and any Python dependencies would be represented as Yocto recipes and RDEPENDS:${PN} entries.

Build and verify:

bitbake hello-pip

On the target:

hello-pip

Shell Script Example

Script-only packages still need a recipe. They usually skip compilation and install the script into ${bindir}.

Directory structure:

recipes-apps/hello-shell/
  hello-shell_1.0.bb
  files/
    hello-shell

Recipe:

SUMMARY = "Small shell script package"
DESCRIPTION = "Installs a script-only package with no compile step."
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

SRC_URI = "file://hello-shell"

RDEPENDS:${PN} += "bash"

do_compile[noexec] = "1"

do_install() {
    install -d ${D}${bindir}
    install -m 0755 ${WORKDIR}/hello-shell ${D}${bindir}/hello-shell
}

Script:

#!/usr/bin/env bash
set -eu

echo "hello-shell: installed by a script-only BitBake recipe"

The script uses Bash, so the recipe declares RDEPENDS:${PN} += "bash". If you write POSIX shell with #!/bin/sh, you may not need that dependency.

Build and verify:

bitbake hello-shell

On the target:

hello-shell

Configuration-File Example

Configuration packages are useful when you want to install managed defaults under /etc. This example marks the file as a conffile so package upgrades know it is configuration.

Directory structure:

recipes-apps/hello-config/
  hello-config_1.0.bb
  files/
    hello-app.conf

Recipe:

SUMMARY = "Small configuration file package"
DESCRIPTION = "Installs a configuration file under /etc."
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

SRC_URI = "file://hello-app.conf"

do_compile[noexec] = "1"

do_install() {
    install -d ${D}${sysconfdir}/hello-app
    install -m 0644 ${WORKDIR}/hello-app.conf ${D}${sysconfdir}/hello-app/hello-app.conf
}

CONFFILES:${PN} += "${sysconfdir}/hello-app/hello-app.conf"

Config file:

message=hello-config: this file was installed into /etc by BitBake
enabled=true

Build and verify:

bitbake hello-config

On the target:

cat /etc/hello-app/hello-app.conf

Files installed under common paths such as ${sysconfdir} are normally packaged automatically. If you install into a non-standard path, extend FILES:${PN}.

Systemd Service Example

This example installs a small script and a systemd unit. The systemd class handles package integration when the image uses systemd.

Directory structure:

recipes-apps/hello-service/
  hello-service_1.0.bb
  files/
    hello-service
    hello-service.service

Recipe:

SUMMARY = "Small systemd service package"
DESCRIPTION = "Installs a script and a systemd service unit."
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

inherit systemd

SRC_URI = " \
    file://hello-service \
    file://hello-service.service \
"

do_compile[noexec] = "1"

do_install() {
    install -d ${D}${bindir}
    install -m 0755 ${WORKDIR}/hello-service ${D}${bindir}/hello-service

    install -d ${D}${systemd_system_unitdir}
    install -m 0644 ${WORKDIR}/hello-service.service ${D}${systemd_system_unitdir}/hello-service.service
}

SYSTEMD_SERVICE:${PN} = "hello-service.service"
RDEPENDS:${PN} += "bash"

Service script:

#!/usr/bin/env bash
set -eu

echo "hello-service: started by systemd"

Unit file:

[Unit]
Description=Hello service recipe example

[Service]
Type=oneshot
ExecStart=/usr/bin/hello-service

[Install]
WantedBy=multi-user.target

Build and verify:

bitbake hello-service

On the target:

systemctl status hello-service
journalctl -u hello-service

If your distribution does not use systemd, this recipe is not the right service pattern. Use the init system selected by your distro.

Adding Packages to an Image

Building a recipe creates one or more packages. It does not automatically add those packages to an image.

For a quick local test, add packages in conf/local.conf:

CORE_IMAGE_EXTRA_INSTALL += "hello-c hello-cpp hello-make hello-shell"

For a product image, put the package list in an image recipe or package group:

IMAGE_INSTALL:append = " hello-c hello-cpp hello-rust hello-go"
IMAGE_INSTALL:append = " hello-python hello-pip hello-shell"
IMAGE_INSTALL:append = " hello-config hello-service"

After rebuilding and booting the image, verify each installed command or file:

hello-c
hello-cpp
hello-make
hello-rust
hello-go
hello-python
hello-pip
hello-shell
cat /etc/hello-app/hello-app.conf
systemctl status hello-service

Remember that image variables take package names. For these examples, each main package has the same name as the recipe.

Inspecting Recipe Variables and Tasks

Use these commands when a simple recipe does not behave the way you expect:

bitbake hello-c
bitbake -c clean hello-c
bitbake -c devshell hello-c
bitbake-getvar -r hello-c S
bitbake-getvar -r hello-c WORKDIR
bitbake-getvar -r hello-c D

To inspect installed files after a recipe build:

find tmp/work -path '*hello-c/1.0*/image/*' -type f

To ask pkgdata which package installed a path:

oe-pkgdata-util find-path /usr/bin/hello-c
oe-pkgdata-util find-path /etc/hello-app/hello-app.conf

Useful task logs are under the recipe work directory:

tmp/work/<machine-or-arch>/<recipe>/<version>/temp/log.do_compile
tmp/work/<machine-or-arch>/<recipe>/<version>/temp/log.do_install
tmp/work/<machine-or-arch>/<recipe>/<version>/temp/log.do_package

For recipes using classes, inspect the inherited behaviour as well as the recipe file itself:

bitbake -e hello-rust | less
bitbake -c listtasks hello-rust

Troubleshooting and Common QA Errors

Nothing PROVIDES 'hello-c'

: Check that your layer is listed in conf/bblayers.conf, and that your recipe path matches the layer’s BBFILES pattern.

Unable to get checksum for file://...

: Check that the file named in SRC_URI exists under the recipe’s files/ directory, and that the filename matches exactly.

installed-vs-shipped

: do_install() staged a file under ${D}, but no output package claimed it. Install into standard paths or extend FILES:${PN} for custom paths.

ldflags or related compile QA warnings

: Make sure handwritten compile commands and Makefiles include ${LDFLAGS}. For C and C++, use ${CC} or ${CXX} plus Yocto’s flags.

Rust or Go tries to download during compile

: Vendor or declare dependencies through Yocto metadata. do_compile() should not depend on network access.

Python imports fail on target

: Check that the package was added to the image, the console script was installed, and RDEPENDS:${PN} includes the Python runtime modules the program needs.

Systemd unit is installed but not enabled

: Check that the image uses systemd, the recipe inherits systemd, and SYSTEMD_SERVICE:${PN} names the installed unit file.

Suggested Exercises

  1. Change the output string in hello-c.c, then rebuild with bitbake -c clean hello-c && bitbake hello-c.
  2. Add a second installed file to hello-config and inspect it with oe-pkgdata-util find-path.
  3. Convert hello-make to a C++ Makefile and use CXX and CXXFLAGS.
  4. Add a command-line argument to hello-go or hello-rust.
  5. Create a package group that installs all of the examples together.

Summary

Simple recipes are small, but they teach the core Yocto workflow:

  • declare source files with SRC_URI
  • point S and B at the right directories
  • use Yocto-provided compilers, tools, flags, and classes
  • stage installed files under ${D}
  • declare runtime dependencies with RDEPENDS:${PN}
  • add the resulting packages to an image before expecting them on the target

Once those habits are solid, larger recipes are mostly a matter of using the right class and understanding the upstream build system.

Quick quiz: first recipe

A short review of the basic recipe-building workflow.

Question 1A recipe should install a script shipped in your layer, but nothing needs compiling. Which part of the recipe still matters immediately?
Question 2Why is installing directly to `/usr/bin` on the host wrong inside `do_install()`?
Question 3You run `bitbake hello-demo`, but BitBake says nothing provides it. What is the most likely first check?
Question 4A vendor recipe already exists and you only need to add one patch plus one config file from your own layer. What is the best pattern?
Question 5Your `.bbappend` parses, but BitBake cannot find the extra patch file you referenced. Which variable is the most likely missing piece?
Question 6Which change is the clearest sign that you should write a new recipe instead of a `.bbappend`?