Search by Tags

Custom meta layers, recipes and images in Yocto Project (hello-world examples)


Article updated at 18 Mar 2021
Compare with Revision

Subscribe for this article updates


This article describes how you can create a new meta layer, how you can add your own hello-world application in a recipe, and how you can create your own image in Yocto Project / OpenEmbedded (from now on only called Yocto Project in this article). We don't go into details, it should only give you an easy introduction on how to start. For further details please read the Yocto Project Mega-Manual.

This article complies to the Typographic Conventions for Torizon Documentation.


If you are building a Reference Image for Yocto Project:

If you are building TorizonCore:

Create a Meta Layer

Once you have an image compiled as proposed in the section Prerequisites above, create a new meta-customer layer:

Note: bitbake-layers create-layer PATH creates a new layer with a basic directory structure.

$ cd build
$ bitbake-layers create-layer ../layers/meta-customer

The new meta layer will be generated with an example recipe:

├── conf
│   └── layer.conf
└── recipes-example
    └── example

3 directories, 4 files

After that you can add the newly created layer to the Yocto Project environment in conf/bblayers.conf:

# LAYER_CONF_VERSION is increased each time build/conf/bblayers.conf
# changes incompatibly
  ${TOPDIR}/../layers/meta-toradex-nxp \
  ${TOPDIR}/../layers/meta-freescale \
  ${TOPDIR}/../layers/meta-freescale-3rdparty \
  ${TOPDIR}/../layers/meta-toradex-tegra \
  ${TOPDIR}/../layers/meta-toradex-bsp-common \
  ${TOPDIR}/../layers/meta-openembedded/meta-oe \
  ${TOPDIR}/../layers/meta-openembedded/meta-filesystems \
  ${TOPDIR}/../layers/meta-openembedded/meta-gnome \
  ${TOPDIR}/../layers/meta-openembedded/meta-xfce \
  ${TOPDIR}/../layers/meta-openembedded/meta-initramfs \
  ${TOPDIR}/../layers/meta-openembedded/meta-networking \
  ${TOPDIR}/../layers/meta-openembedded/meta-multimedia \
  ${TOPDIR}/../layers/meta-openembedded/meta-python \
  ${TOPDIR}/../layers/meta-lxde \
  ${TOPDIR}/../layers/meta-browser \
  ${TOPDIR}/../layers/meta-qt5 \
  ${TOPDIR}/../layers/meta-qt5-extra \
  ${TOPDIR}/../layers/meta-rust \
  ${TOPDIR}/../layers/meta-freescale-distro \
  ${TOPDIR}/../layers/meta-toradex-demos \
  ${TOPDIR}/../layers/meta-toradex-distro \
  ${TOPDIR}/../layers/meta-yocto/meta-poky \
  ${TOPDIR}/../layers/openembedded-core/meta \
  ${TOPDIR}/../layers/meta-customer \

Warning: There would also be a command to add a new layer to bblayer.conf: bitbake-layers add-layer. But this includes the meta layer with absolute paths, which can be avoided by adding it manually.

Initialize a Git Project (Mandatory for TorizonCore)

Revision control is something that one must use during modern software development. We recommend Git for meta layers. Inside your new layer directory, run:

$ git init
$ git commit -m "Initial Commit" -m "Add <meta-mylayer> from template"

On TorizonCore all your custom layers must be version controlled by Git, due to how we include layer revision information with OSTree. If you don't do it, a similar error will pop up during your build:

WARNING: Failed to get layers information. Exception: <class 'bb.process.ExecutionError'>
WARNING: torizon-core-docker-1.0-r0 do_image_ostreecommit: Failed to get layers information. Exception: <class 'bb.process.ExecutionError'>
ERROR: torizon-core-docker-1.0-r0 do_image_ostreecommit: Execution of '/workdir/build-torizon/tmp-torizon/work/apalis_imx8-tdx-linux/torizon-core-docker/1.0-r0/temp/run.do_image_ostreecommit.290621' failed with exit code 1:
error: Parsing oe.layers=None : 0:expected value
WARNING: exit code 1 from a shell command.

Working With Non-Git Revision Systems

While Git is one of the most popular and widely used revision control systems for software, there are of course other solutions out there. For the time being we at Toradex choose to only support Git as a compatible revision control for OSTree, and therefore Torizon by extension. Of course it is possible that Git may not be your chosen system for revision control. In such a case it becomes necessary to work around this limitation in order to avoid the above build error.

Some easy work-arounds:

  • Initialize your custom layer as a "fake" git project.
    • This can be done like so: git init && git commit -m 'fake GIT project'
    • As a side-note you don't need to also link this to a remote Git project, a local Git project is all that is required
  • If possible for your infrastructure one can also just mirror your project as a Git project. Then the mirrored Git project can be used for building.
    • While more work the benefits of this include having real layer revision information included in OSTree. Which is important if you are interested in using OTA updates.
  • Add compatibiltiy for whichever revision control system you use.
    • This file here, defines how we extract layer revision information for OSTree.
    • In theory this file can be edited/added upon to add support for your specific revision control system. In such a case as with all our open-source code we would be willing to review and accept such patches.

These are just a few of the more practical work-arounds for non-Git users. Though in the end there are many ways to get around this. The only thing to keep in mind is whether you are interested in or plan to use the OTA update features of Torizon. In this case it is heavily recommended to use Git to have the best possible compatibility with all our Torizon tools and systems.

Create a Recipe

Now that we have a meta layer we can create a custom recipe e.g. a hello-world: Note: Having hello-world/hello-world/ is on purpose. The top folder is used for the recipe, while the subfolder is used for the sources.

$ cd layers/meta-customer/
$ mkdir -p recipes-customer/hello-world/hello-world

Create a recipe file recipes-customer/hello-world/

# Package summary
SUMMARY = "Hello World"
# License, for example MIT
# License checksum file is always required
LIC_FILES_CHKSUM = "file://${COREBASE}/meta/files/common-licenses/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
# hello-world.c from local file
SRC_URI = "file://hello-world.cpp"
# Set LDFLAGS options provided by the build system
# Change source directory to workdirectory where hello-world.cpp is
S = "${WORKDIR}"
# Compile hello-world from sources, no Makefile
do_compile() {
    ${CXX} -Wall hello-world.cpp -o hello-world
# Install binary to final directory /usr/bin
do_install() {
    install -d ${D}${bindir}
    install -m 0755 ${S}/hello-world ${D}${bindir}

Note: Yocto Project also provides the tools devtool and recipetool to create or change recipes. See the dev-manual for more details.

Add the hello world sources to recipes-customer/hello-world/hello-world/hello-world.cpp:

#include <stdio.h>
int main(int argc, char *argv[]){
    printf("Hello world!\n");
    return 0;

After creating your recipe and providing the sources, you would be able to build the package:

$ cd build/
$ bitbake hello-world

Add the Package to an Existing Image

To add a recipe to an existing image, you can add the additional packages to be installed to build/conf/local.conf. In the following example we add the hello-world from the chapter Create a recipe

IMAGE_INSTALL_append = " hello-world"
After that, for the moment you can rebuild an existing image as the Reference Minimal Image or Reference Multimedia Image. Now it will contain the hello-world binary under /usr/bin/hello-world.

Build the image using bitbake as explained on Build a Reference Image with Yocto Project. Later on in this article, we will focus on creating your own image.

Customize the Kernel

Let's assume you want to add a custom devicetree to the kernel and enable an additional kernel module. How can you do that?

We recommend changing the kernel outside of Yocto to reduce the integration and test cycle time. Please follow the instructions in this article: Build U-Boot and Linux Kernel from Source Code

After several integration and test cycles you should come up with a devicetree file and kernel configuration that fits your custom application. Now you need to integrate this changes into your custom meta layer.

$ cd layers/meta-customer/
$ mkdir -p recipes-kernel/linux

You want to append some changes to our linux-toradex kernel recipe. Therefore, you need to create a bbappend file recipes-kernel/linux/linux-toradex_%.bbappend. Please note that instead of % you may want to append the change to a specific version (e.g. recipes-kernel/linux-toradex_4.14%.bbappend). Check Build U-Boot and Linux Kernel from Source Code for the kernel version which fits your module.

The bbappend file looks as follows:

FILESEXTRAPATHS_prepend := "${THISDIR}/linux-toradex:"
CUSTOM_DEVICETREE = "my-custom-devicetree-file.dts"
SRC_URI += "\ 
	file://custom-display.patch \
do_configure_append() {
	# For arm32 bit devices
	cp ${WORKDIR}/${CUSTOM_DEVICETREE} ${S}/arch/arm/boot/dts
	# For arm64 bit freescale/NXP devices
	# cp ${WORKDIR}/${CUSTOM_DEVICETREE} ${S}/arch/arm64/boot/dts/freescale

Create the directory recipes-kernel/linux/linux-toradex where you store the additional files which you include in SRC_URI.

First we add a patch which adds a custom-display. Patches are automatically applied by Yocto. You don't have to do anything if the file has the ending .patch.

diff --git a/drivers/gpu/drm/panel/panel-simple.c b/drivers/gpu/drm/panel/panel-simple.c
index e817a71062dbb..2d9f1ca8a04bb 100644
--- a/drivers/gpu/drm/panel/panel-simple.c
+++ b/drivers/gpu/drm/panel/panel-simple.c
@@ -2053,6 +2053,31 @@ static const struct panel_desc winstar_wf35ltiacd = {
 	.bus_format = MEDIA_BUS_FMT_RGB888_1X24,
+static const struct drm_display_mode custom_display_mode = {
+	.clock = 6410,
+	.hdisplay = 320,
+	.hsync_start = 320 + 20,
+	.hsync_end = 320 + 20 + 30,
+	.htotal = 320 + 20 + 30 + 38,
+	.vdisplay = 240,
+	.vsync_start = 240 + 4,
+	.vsync_end = 240 + 4 + 3,
+	.vtotal = 240 + 4 + 3 + 15,
+	.vrefresh = 60,
+static const struct panel_desc custom_display = {
+	.modes = &custom_display_mode,
+	.num_modes = 1,
+	.bpc = 8,
+	.size = {
+		.width = 80,
+		.height = 63,
+	},
+	.bus_format = MEDIA_BUS_FMT_RGB888_1X24,
 static const struct of_device_id platform_of_match[] = {
 		.compatible = "ampire,am-480272h3tmqw-t01h",
@@ -2271,6 +2296,9 @@ static const struct of_device_id platform_of_match[] = {
 		.compatible = "winstar,wf35ltiacd",
 		.data = &winstar_wf35ltiacd,
 	}, {
+		.compatible = "custom,display",
+		.data = &custom_display,
+	, {
 		/* sentinel */

You can use any kernel configuration as defconfig. The linux-toradex recipe will automatically use the defconfig that is provided. Because we prepend our file path to FILESEXTRAPAHTS the one from meta customer will be used.

# Automatically generated file; DO NOT EDIT.
# Linux/arm 4.14.170 Kernel Configuration
# General setup

For the deviceree file we added a copy operation in the recipe. In theory we could also add the devicetree file with a patch. Both methods work equualy well.

#include <dt-bindings/input/input.h>
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/pwm/pwm.h>
#include "imx6dl-colibri-eval-v3.dts"
/ {
	model = "Customer Carrier Board with Toradex Colibri iMX6DL/S";
	compatible = "customer,colibri_imx6dl", "toradex,colibri_imx6dl",
&pwm2 {
	// We need to disable pwm2 so that we can use GPIO01 IO01
	status = "disabled";
&iomuxc {
	// The additonal gpio is not used by any node in the devicetree
	// therefore we define it here so that the pins are configured when
	// the iomux driver is loaded
	pinctrl-0 = <&pinctrl_csi_gpio_1 &pinctrl_csi_gpio_2 &additonal_gpio>;
	additonal_gpio: additonal-gpio {
		// GPIO2 IO17 we just define as input so that it doesn't influence GPIO1
		// IO1. This SODIMM pin is connected to two SoC pins so one must be 
		// configured as input and should be in High-Z state all the time.
		fsl,pins = <
			MX6QDL_PAD_GPIO_1__GPIO1_IO01	0x1b0b0
			MX6QDL_PAD_EIM_A21__GPIO2_IO17	0x00040
&panel {
	compatible = "custom,display";

We also need to add the device tree file to the variable KERNEL_DEVICETREE which defines which devicetrees are built and included into the final image.

KERNEL_DEVICETREE_append = " my-custom-devicetree-file.dtb"

By default this file is not included anywhere. Therefore, we must make sure that it is included by another conf file (e.g. layer.conf). We can do that by adding the following line to conf/layer.conf:

include conf/machine/colibri-imx6-extra.conf

In a last step we need to tell U-Boot to load my-custom-devicetree-file.dtb instead of the standard one. For this we need to replace the original name with the new name by adding a bbappend file again. Create a directory recipes-bsp/u-boot:

$ cd layers/meta-customer/
$ mkdir -p recipes-bsp/u-boot
do_configure_append() {
	sed -i 's/#define FDT_FILE.*/#define FDT_FILE "my-custom-devicetree-file.dtb"/' ${S}/include/configs/colibri_imx6.h

Note: There are many ways to customize a kernel. If a lot of changes are added to the kernel then it is recommended to maintain your own git repository. In that case you need to change the SRC_URI and the SRCREV within the bbappend file. If only a tiny change is necessary you can also patch the default devicetree file and just leave everything else untouched.

Compile a Custom Kernel Module

While it is always preferable to work with sources integrated into the Linux kernel, there may be some reasons you might want to build a custom kernel module outside the kernel source code tree, for example, if the code is not GPLv2 compatible.

Let's say you have a "Hello World" kernel module:

#include <linux/module.h>
int init_module(void)
    printk("Hello World!\n");
    return 0;
void cleanup_module(void)
    printk("Goodbye Cruel World!\n");

And a simple Makefile to build this module:

obj-m := hello.o
SRC := $(shell pwd)
    $(MAKE) -C $(KERNEL_SRC) M=$(SRC)
    $(MAKE) -C $(KERNEL_SRC) M=$(SRC) modules_install
    rm -f *.o *~ core .depend .*.cmd *.ko *.mod.c
    rm -f Module.markers Module.symvers modules.order
    rm -rf .tmp_versions Modules.symvers

You can go to your custom layer and create a recipe to build this module:

$ cd layers/meta-customer/
$ mkdir -p recipes-kernel/hello-mod/
$ vim recipes-kernel/hello-mod/

In the recipe, you just need to define the location of the kernel module source code, and inherit module.bbclass to build the kernel module:

SUMMARY = "Example of how to build an external Linux kernel module"
LIC_FILES_CHKSUM = "file://COPYING;md5=b234ee4d69f5fce4486a80fdaf4a4263"
inherit module
SRCREV = "4f082b755fdf2ef8da30adf2dfbca6fc0745cd2f"
SRC_URI = " \
    git://;branch=main;protocol=https \
PV = "1.0+git${SRCPV}"
S = "${WORKDIR}/git"
RPROVIDES_${PN} += "kernel-module-hello" 

To include the kernel module in your images, you can add the following line to your machine configuration file:

MACHINE_EXTRA_RDEPENDS += "kernel-module-hello"

Note: Depending on the build system used by the module sources, you might need to make some adjustments in the recipe, like overriding the do_compile task or patching/adjusting the Makefile. Check the Yocto Project documentation for more information.

Create an Image

Toradex provides some reference images which can be built on their own or then directly be installed with the Toradex Easy Installer. But these images are considered reference images and should not be used in products directly, instead one should derive a custom image from them.

Using the meta layer created above, we create a recipe folder for our custom images:

$ cd layers/meta-customer/
$ mkdir -p recipes-images/images/

In that folder, we can create recipes and include-files to create one or multiple images. E.g. we can create a simple console image similar to the console-tdx-image containing the hello-world from the chapter Create a recipe. For that we create the recipes-images/images/
SUMMARY = "My Custom Image"
DESCRIPTION = "This is my customized image containing a simple hello-world"
inherit core-image
#start of the resulting deployable tarball name
export IMAGE_BASENAME = "Custom-Console-Image"
ROOTFS_PKGMANAGE_PKGS ?= '${@oe.utils.conditional("ONLINE_PACKAGE_MANAGEMENT", "none", "", "${ROOTFS_PKGMANAGE}", d)}'
IMAGE_INSTALL_append = " \
    packagegroup-boot \
    packagegroup-basic \
    udev-extra-rules \
    timestamp-service \
    weston weston-init wayland-terminal-launch \
    hello-world \
IMAGE_LOGIN_MANAGER = "busybox shadow"

Finally, we can build the image with:

$ cd build/
$ bitbake custom-console-image