Building a Yocto distribution only from ready-made meta-layers is like baking brownies from a box — quick and decent, but rarely the perfect dessert. To really match your taste, you’ll need to add your own ingredients with custom layers and recipes.

This tutorial is a continuation of the Yocto series. Did you setup your raspberry pi distribution already? If not, it’s highly recommended to go through the first article of the series, describing the setup in detail.

Yocto Theory#

Before getting into the practical details, let’s first talk the theory. To understand the operations done in the following subsections, two crucial Yocto concepts have to be introduced, recipes and meta-layers.

recipes#

A Yocto recipe is like a kitchen recipe in that it lays out, step by step, how to gather and prepare the ingredients (what to fetch, patch, configure, build, package). It’s a script interpreted by Yocto’s chef and the build engine - bitbake. Recipes usually are named after the package they describe and have a *.bb (bb for bitbake) extension. A typical recipe instructs where to fetch the resources from, be it a git repository or a remotely hosted tar archive, then how to unpack them, apply patches, configure the build system, compile the sources, and finally package the results. The recipe itself does not perform these actions, it only describes them. It is BitBake that interprets the recipe and executes the corresponding tasks.

You’re likely asking what kind of resources can be fetched and build, am I right? Well, almost anything. Anything that is downloadable of copyable (as resources stored on a local disk are valid, too) and available under a url or a file path will do. A library, a whole application, a gallery of images (rather unusual for an embedded system), even the full Linux kernel. The build step is optional, as all those things can be pre-build binaries, just make sure that they are in fact compatible with the target system CPU architecture.

This was supposed to be a picture of the recipe

A very unique characteristic of Yocto is that recipes can be stacked together, or in the language of the framework - appended. Imagine a basic brownie recipe: the flour, milk, and chocolate that make up the dough, along with the oven temperature and baking time, are all listed there. That’s a complete recipe for a Basic Brown Brownie. Now, imagine a brown brownie with cherries on top. It’s almost the same, with only a single thing added. How should that change be applied to the original recipe? In the world of Yocto, if the original recipe were on a paper page, a single line of "add cherries on top of the dough" would be written on a sticky note, attached at the bottom of the page. Each further tweak gets its own note, stacked in order, until the updated recipe is complete.

Those sticky notes share the same name as the original recipe, but with the extension *.bbappend.

meta-layers#

Recipes are usually grouped into larger chapters based on the type of treat: brownies and donuts fit well into Desserts, while Wiener Schnitzel alongside the potato salad belongs in a chapter on German Dinners. In Yocto, these chapters are called meta-layers, each devoted to a single domain such as networking features, multimedia, or providing support for a programming language like Python. However, meta-layers are more than just chapters in a cookbook. They not only group recipes into logical domains, but also define how changes stack: the order of the layers and their priority decides the order of application of the *.bbappend files - our sticky notes. In case of two meta-layers defining a recipe with the same name, only the one with the higher priority is parsed. The configuration of each meta-layer is stored in a conf/layer.conf file and must contain variables:

  • BBPATH: the root directory of the meta-layer.

  • BBFILES: Defines the location for all recipes in the layer.

  • BBFILE_COLLECTIONS: name of the layer, to which meta- is prepended. This name is used to refer to the layer in the other Yocto components. For example raspberrypi in case of meta-raspberrypi layer.

  • BBFILE_PATTERN: Expands immediately during parsing to provide the directory of the layer.

  • BBFILE_PRIORITY: Already covered priority, the higher the value, the more important the layer is and will overwrite layer of less importance.

  • LAYERVERSION: The version number for the layer.

  • LAYERDEPENDS: Lists all layers on which this layer depends (if any).

  • LAYERSERIES_COMPAT: Lists the Yocto Project releases for which the current version is compatible.

More about the meta-layers and their configuration is available at the official Yocto documentation

Meta-layers contain other files than just the recipes and the *.bbappend files. They also hold *.bbclass files, as well as machine and distribution configuration files. These are important topics, but they’re out of scope for now - I’ll cover them another time. For this article, let’s focus only on recipes. One particular category deserves special mention: the image recipe. This type of recipe defines which packages are included in the final flashable image.

Image recipe#

An image recipe is like a wedding banquet menu - a full multi-course plan that lists which dishes (packages) will be served, but leaves the cooking steps to the individual recipes (*.bb recipes). The recipe determines the contents of the final image, which is adjusted by appending package names to the IMAGE_INSTALL variable. More on that in the Practice section.

Practice#

Now, that the basic Yocto terms are covered, let’s move into the actual work. Below subsections assumes the knowledge from the previous tutorial on setting up the RPI environment. Start by sourcing the bitbake environment from the poky meta-layer:

source sources/poky/oe-init-build-env

Create a meta-layer#

A meta layer can be created in at least two ways, either with the use of a bitbake-layers script or manually, by creating a new directory with an adequate structure of the <layer-name>/conf/. In the case of the latter, the best is to start by copying an existing meta-layer.

bitbake-layers#

bitbake-layers create-layer --help
NOTE: Starting bitbake server...
usage: bitbake-layers create-layer [-h] [--add-layer] [--layerid LAYERID] [--priority PRIORITY] [--example-recipe-name EXAMPLERECIPE] [--example-recipe-version VERSION] layerdir

Create a basic layer

positional arguments:
  layerdir              Layer directory to create

optional arguments:
  -h, --help            show this help message and exit
  --add-layer, -a       Add the layer to bblayers.conf after creation
  --layerid LAYERID, -i LAYERID
                        Layer id to use if different from layername
  --priority PRIORITY, -p PRIORITY
                        Priority of recipes in layer
  --example-recipe-name EXAMPLERECIPE, -e EXAMPLERECIPE
                        Filename of the example recipe
  --example-recipe-version VERSION, -v VERSION
                        Version number for the example recipe

bitbake-layers is rather simple to get around, use --help if confused. Before setting up a meta-layer, figure out its name, location and the priority. The the tutorial purpose the layer will be called meta-custom and located in sources. Below command assumes that build is working directory, as it’s the default behavior after sourcing the bitbake environment.

bitbake-layers create-layer ../sources/meta-custom/ --add-layer --example-recipe-name custom-image

The command creates below file structure and the new layer is already appended to the build/conf/bblayers.conf.

meta-custom/
├── conf
│   └── layer.conf
├── COPYING.MIT
├── README
└── recipes-custom-image
    └── custom-image
        └── custom-image_0.1.bb

manual setup#

The same file structure can be achieved manually, however my advice is to name the directories slightly differently, moreover, in our case the license file is not a necessity as the LICENSE variable in the image recipe can simply be set to "CLOSED". The easier to read file structure is as below:

meta-custom/
├── conf
│   └── layer.conf
├── README
└── recipes-custom
    └── images
        └── custom-image_0.1.bb

layer.conf#

layer.conf stores the configuration of the meta layer; by default bitbake-layers generates it in below form, it’s a good example to follow:

# We have a conf and classes directory, add to BBPATH
BBPATH .= ":${LAYERDIR}"

# We have recipes-* directories, add to BBFILES
BBFILES += "${LAYERDIR}/recipes-*/*/*.bb \
            ${LAYERDIR}/recipes-*/*/*.bbappend"

BBFILE_COLLECTIONS += "meta-custom"
BBFILE_PATTERN_meta-custom = "^${LAYERDIR}/"
BBFILE_PRIORITY_meta-custom = "6"

LAYERDEPENDS_meta-custom = "core"
LAYERSERIES_COMPAT_meta-custom = "scarthgap"

In case of the --priority parameter being passed to the bitbake-layers create-layer command, the BBFILE_PRIORITY_meta-custom variable is adjusted, however by default it’s set to 6.

The Image recipe#

Finally, let’s practice the image recipe. Here, the final list of the packages present on the operating system is put together with either of two variables IMAGE_FEATURES and IMAGE_INSTALL. They are quite similar to each other, the difference being that IMAGE_INSTALL lists single packages, whilst IMAGE_FEATURES manages groups of packages as a single feature may involve multiple packages - those are managed by the Yocto itself. To create an actual image from the image recipe, one of the directives inherit image or inherit core-image has to be used, here, the choice is inherit core-image as it provided the openssh feature. What should a basic image for a raspberry pi device have? It all depends on the usecase of the device, but there are must-haves like an ssh server, enabling remote communication with the device - this is a feature. A package that likely will turn out useful is python3 - containing the Python 3 language. A bit less useful may be a C library such as mosquitto or an audio server - pipewire. All three of them are packages, that can be appended to the IMAGE_INSTALL variable. The customization of images is tremendously well described in the official Yocto Project documentation, so go there to learn more! An image file with all of those changes applied would look like below:

SUMMARY = "simple image recipe"
DESCRIPTION = "simple image recipe"
LICENSE = "CLOSED"

python do_display_banner() {
    bb.plain("*******************************************************************");
    bb.plain("*                                                                 *");
    bb.plain("*   A custom recipe: ssh server, python, pipewire and mosquitto   *");
    bb.plain("*                                                                 *");
    bb.plain("*******************************************************************");
}

addtask display_banner before do_build

inherit core-image

IMAGE_FEATURES += "ssh-server-openssh"

IMAGE_INSTALL:append = " python3"
IMAGE_INSTALL:append = " mosquitto"
IMAGE_INSTALL:append = " pipewire"

The append instructions is new here. Just like its name says, it appends to the variable it follows, meaning that after all the appends are parsed the IMAGE_INSTALL variable has a value of ` python3 mosquitto pipewire`.

Build the image#

Assuming that the image recipe name is indeed custom-image.bb and that the layer is already added to the bblayer.conf file, to build the image, running below command should be enough

bitbake custom-image

A successfully run build command should send below output to the stdout:

$ bitbake custom-image
Loading cache: 100% |#########################################################################################################################################################################################################| Time: 0:00:04
Loaded 4746 entries from dependency cache.
NOTE: Resolving any missing task queue dependencies

Build Configuration:
BB_VERSION           = "2.8.0"
BUILD_SYS            = "x86_64-linux"
NATIVELSBSTRING      = "universal"
TARGET_SYS           = "aarch64-poky-linux"
MACHINE              = "raspberrypi4-64"
DISTRO               = "poky"
DISTRO_VERSION       = "5.0.11"
TUNE_FEATURES        = "aarch64 crc cortexa72"
TARGET_FPU           = ""
meta
meta-poky
meta-yocto-bsp       = "HEAD:792d18b4cb2451b00280641403e6eaf37bd6e53f"
meta-raspberrypi     = "HEAD:8e9ec2685a902038d1d6ad20f0821ee5655432a9"
meta-oe
meta-multimedia
meta-networking
meta-python          = "HEAD:e8fd97d86af86cdcc5a6eb3f301cbaf6a2084943"
meta-custom          = "<unknown>:<unknown>"

Sstate summary: Wanted 1180 Local 0 Mirrors 0 Missed 1180 Current 2044 (0% match, 63% complete)##########################################################################################################                     | ETA:  0:00:01
Initialising tasks: 100% |####################################################################################################################################################################################################| Time: 0:00:11
NOTE: Executing Tasks
*******************************************************************
*                                                                 *
*   A custom recipe: ssh server, python, pipewire and mosquitto   *
*                                                                 *
*******************************************************************
NOTE: Tasks Summary: Attempted 6628 tasks of which 4471 didn't need to be rerun and all succeeded.

Now the image is available at tmp/deploy/images/raspberrypi4-64/custom-image-raspberrypi4-64.rootfs.wic.bz2. Flash it, upload it, do whatever you need to make it run on your device!