Lesson
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, andLIC_FILES_CHKSUMdescribe the software and its licence.- Where does the source come from?
SRC_URIlists local files, patches, archives, or repositories. Local files usefile://.- Where is the source tree?
Spoints to the source directory used by configure and compile tasks.- Where is the build tree?
Bpoints to the build directory. Some classes use a separate build directory, especiallycmake.- How is it built?
do_compile()may be handwritten, or supplied by a class such ascmake,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.
Recommended Recipe and File Layout
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
gccinstead 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.lockfrom 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 installon the target instead of packaging the module into the image
Python Package Sourced Externally or Installed Using Pip-Related Tooling
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
- Change the output string in
hello-c.c, then rebuild withbitbake -c clean hello-c && bitbake hello-c. - Add a second installed file to
hello-configand inspect it withoe-pkgdata-util find-path. - Convert
hello-maketo a C++ Makefile and useCXXandCXXFLAGS. - Add a command-line argument to
hello-goorhello-rust. - 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
SandBat 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.
Check your understanding
Quick quiz: first recipe
A short review of the basic recipe-building workflow.