1. Home
  2. Blog
  3. Part 2: Testing with QEMU and Creating Your First Custom Layer

Part 2: Testing with QEMU and Creating Your First Custom Layer

In Part 1, we built a minimal Linux image for the Raspberry Pi 4 using Yocto and KAS. The system boots, but we can't do much with it yet. Before adding features to our embedded system, we need two things: a way to test quickly without hardware, and a place to put our custom code.
This part covers testing with QEMU and creating your first custom Yocto layer.

Why QEMU ?

Not everyone has a Raspberry Pi on their desk, and even if you do, the flash-boot-test cycle is slow. QEMU lets you build and test images without hardware, with boot times measured in seconds rather than minutes.
After publishing Part 1, a reader, Juha Viitanen, built on the tutorial and created their own custom layer with QEMU support. This demonstrates both that the approach works and that QEMU testing is a natural next step.

Adding QEMU Support

Let's add a QEMU configuration to our project. The project_qemu.yml would share most content with project.yml from Part 1, so first let's refactor to avoid duplication.

Extracting Common Configuration

Create ${PROJECT}/kas/include/common.yml with the shared parts:

header:
    version: 18

distro: poky

target:
    - core-image-minimal

repos:
    poky:
        url: "https://git.yoctoproject.org/git/poky"
        branch: scarthgap
        commit: 9c63e0c9646c61663e8cfc6b4c75865cd0cd3b34
        layers:
            meta:
            meta-poky:
            meta-yocto-bsp:

    meta-openembedded:
        url: "https://git.openembedded.org/meta-openembedded"
        branch: scarthgap
        commit: e92d0173a80ea7592c866618ef5293203c50544c
        layers:
            meta-oe:

local_conf_header:
    local_dirs: |
        DL_DIR = "/home/yocto/Projects/dl"
        SSTATE_DIR = "/home/yocto/Projects/sstate-cache"
    history: |
        INHERIT += "buildhistory"

Now simplify ${PROJECT}/kas/project.yml from Part 1:

header:
    version: 18
    includes:
        - include/common.yml
        - include/raspberrypi.yml

machine: raspberrypi4-64

QEMU-Specific Configuration

Create ${PROJECT}/kas/include/qemuarm64.yml for the ARM toolchain:

header:
    version: 18

repos:
    meta-arm:
        url: "https://git.yoctoproject.org/meta-arm"
        branch: scarthgap
        commit: 8e0f8af90fefb03f08cd2228cde7a89902a6b37c
        layers:
            meta-arm:
            meta-arm-toolchain:

Now create ${PROJECT}/kas/project_qemu.yml:

header:
    version: 18
    includes:
        - include/common.yml
        - include/qemuarm64.yml
        - include/yoseli-apps.yml

machine: qemuarm64

local_conf_header:
    debug: |
        EXTRA_IMAGE_FEATURES += "debug-tweaks"

A few things to notice:

  • include/common.yml - Shared repos and configuration from Part 1

  • machine: qemuarm64 - ARM64 QEMU machine instead of raspberrypi4-64

  • include/qemuarm64.yml - Pulls in meta-arm for ARM toolchain support

  • include/yoseli-apps.yml - Our custom layer (we'll create this next)

  • debug-tweaks - Allows root login without password, useful for testing

The qemuarm64 machine uses the same ARM64 architecture as the Raspberry Pi 4, so recipes that work in QEMU will generally work on the real hardware.

Building the QEMU Image

Using the same project directory from Part 1 (`${PROJECT}` which we set to ${HOME}/Projects/yoseli):

cd ${PROJECT}
kas build kas/project_qemu.yml

The first build takes time as it compiles the kernel and toolchain. Subsequent builds leverage the sstate cache and complete in minutes.

Running QEMU

After the build completes, launch QEMU:

kas shell kas/project_qemu.yml -c "runqemu nographic slirp"

The options:

  • nographic - No GUI window, output goes to terminal (works over SSH)

  • slirp - User-mode networking, no root privileges required

You should see the kernel boot messages followed by a login prompt. Login as root (no password needed thanks to debug-tweaks).
To exit QEMU, press Ctrl+a then x.

Creating Your First Custom Layer

Now we have a fast test environment. Let's create a custom layer to hold our code.

A Yocto layer is a directory containing recipes, configuration, and other metadata. Layers provide separation of concerns: upstream code stays upstream, your customizations stay in your layer.

Using bitbake-layers to Create the Layer

Yocto provides the bitbake-layers tool to create a properly structured layer:

$> kas shell kas/project_qemu.yml -c "bitbake-layers create-layer ../meta-example"
NOTE: Starting bitbake server...
Add your new layer with 'bitbake-layers add-layer /tmp/meta-example'
$> tree /tmp/meta-example/
/tmp/meta-example/
├── conf
│   └── layer.conf
├── COPYING.MIT
├── README
└── recipes-example
    └── example
        └── example_0.1.bb

The generated layer.conf contains the boilerplate we need:

BBPATH .= ":${LAYERDIR}"                                                  
                                                                              
BBFILES += "${LAYERDIR}/recipes-*/*/*.bb \                                
            ${LAYERDIR}/recipes-*/*/*.bbappend"                           
                                                                            
BBFILE_COLLECTIONS += "meta-example"                                      
BBFILE_PATTERN_meta-example = "^${LAYERDIR}/"                             
BBFILE_PRIORITY_meta-example = "6"                                        
                                                                              
LAYERDEPENDS_meta-example = "core"                                        
LAYERSERIES_COMPAT_meta-example = "scarthgap"            

The example recipe is just a placeholder that prints a banner during build. For our layer, we'll use the same structure but with our own name and recipes.

Creating meta-yoseli-apps

Create the layer:

$> kas shell kas/project_qemu.yml -c "bitbake-layers create-layer ../meta-yoseli-apps"

Then add a directory for our hello recipe:

$> mkdir -p meta-yoseli-apps/recipes-example/hello/files

The layer.conf generated by the tool is almost ready. Just verify that BBFILE_COLLECTIONS and related variables use meta-yoseli-apps as the layer name:

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

LAYERDEPENDS_meta-yoseli-apps = "core"
LAYERSERIES_COMPAT_meta-yoseli-apps = "scarthgap"

Key elements:

  • BBFILE_COLLECTIONS - Unique name for this layer

  • BBFILE_PRIORITY - Higher numbers take precedence in conflicts

  • LAYERSERIES_COMPAT - Declares compatibility with Scarthgap release

Your First Recipe: Hello World

A recipe tells BitBake how to build a package. Create recipes-example/hello/files/hello.c:

#include <stdio.h>

int main(void)
{
    printf("Hello from Yoseli!\n");
    printf("This is your first custom Yocto recipe.\n");
    return 0;
}

Now create the recipe recipes-example/hello/hello_1.0.bb:

SUMMARY = "Simple Hello World application"
DESCRIPTION = "A minimal C application to demonstrate Yocto recipe creation"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

SRC_URI = "file://hello.c"

S = "${WORKDIR}"

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

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

Let's break this down:

  • SUMMARY/DESCRIPTION - Human-readable package info

  • LICENSE - Must specify a license; we use MIT

  • LIC_FILES_CHKSUM - Checksum of the license file (prevents silent license changes)

  • SRC_URI - Where to get source files; file:// means local files in files/ subdirectory

  • S - Source directory, set to WORKDIR for simple recipes

  • do_compile() - How to build; uses cross-compiler variables ${CC}, ${CFLAGS}, ${LDFLAGS}

  • do_install() - Where to put built files; ${D} is the destination root, ${bindir} is /usr/bin

Alternative: Hello World with devtool

The manual approach above teaches recipe fundamentals, but Yocto provides devtool for a more streamlined workflow. Let's create the same hello recipe using devtool.

What is devtool?

devtool is a command-line tool that simplifies common recipe development tasks:

  • devtool add - Create a new recipe from source code

  • devtool modify - Set up a workspace to modify an existing recipe

  • devtool build - Build a recipe in the workspace

  • devtool finish - Move a recipe from workspace to a layer

The workspace is a temporary area where devtool manages recipe development before you integrate it into your layer.

Creating Hello with devtool

First, prepare a source directory.

Important: Initialize git before running devtool add - this is required for devtool finish to work later:

$> mkdir -p /tmp/hello-devtool
$> cd /tmp/hello-devtool
$> git init

Create the source file:

cat > hello.c << 'EOF'
#include <stdio.h>

int main(void)
{
    printf("Hello from devtool!\n");
    printf("This recipe was created with devtool add.\n");
    return 0;
}

We need a Makefile since devtool expects standard build systems. Include a clean target for devtool reset to work:

cat > Makefile << 'EOF'
CC ?= gcc
CFLAGS ?= -Wall
PREFIX ?= /usr

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

install: hello
        install -d $(DESTDIR)$(PREFIX)/bin
        install -m 0755 hello $(DESTDIR)$(PREFIX)/bin/

clean:
        rm -f hello

.PHONY: install clean
EOF

Commit the source files:

$> git add .
$> git commit -m "Initial hello-devtool source"

Return to the project directory and use devtool to create the recipe:

$> cd ${PROJECT}
$> kas shell kas/project_qemu.yml -c "devtool add hello-devtool /tmp/hello-devtool"
NOTE: Starting bitbake server...
INFO: Using source tree as build directory since that would be the default for this recipe
INFO: Recipe .../build/workspace/recipes/hello-devtool/hello-devtool.bb has been automatically created; further editing may be required to make it fully functional

devtool creates two files in build/workspace/:

  1. The recipe in recipes/hello-devtool/hello-devtool.bb

  2. A bbappend in appends/hello-devtool_%.bbappend that overrides the source location

Examine the generated recipe:

cat build/workspace/recipes/hello-devtool/hello-devtool.bb
# Recipe created by recipetool
LICENSE = "CLOSED"
LIC_FILES_CHKSUM = ""

SRC_URI = ""

do_configure () {
        :
}

do_compile () {
        oe_runmake
}

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

Notice SRC_URI = "" - the recipe has no source location. Now look at the bbappend that makes the magic happen:

$> cat build/workspace/appends/hello-devtool_%.bbappend
inherit externalsrc
EXTERNALSRC = "/tmp/hello-devtool"
EXTERNALSRC_BUILD = "/tmp/hello-devtool"

The externalsrc class tells BitBake to:

  • Use EXTERNALSRC as the source directory (instead of fetching from SRC_URI)

  • Use EXTERNALSRC_BUILD as the build directory

This means any changes you make to /tmp/hello-devtool are immediately used in the next build.

No need to commit or push to a remote repository during development.

The LICENSE = "CLOSED" is a placeholder ! You'll need to fix this for production.

Building with devtool

Build the recipe in the workspace:

$> kas shell kas/project_qemu.yml -c "devtool build hello-devtool"

NOTE: Executing Tasks
NOTE: hello-devtool: compiling from external source tree /tmp/hello-devtool
NOTE: Tasks Summary: Attempted 762 tasks of which 757 didn't need to be rerun and all succeeded.

Finishing: Moving to Your Layer

Clean build artifacts and move the recipe to your layer:

$> make -C /tmp/hello-devtool clean
$> kas shell kas/project_qemu.yml -c "devtool finish hello-devtool meta-yoseli-apps"
NOTE: Starting bitbake server...
INFO: No patches or files need updating
INFO: Moving recipe file to .../meta-yoseli-apps/recipes-hello-devtool/hello-devtool
INFO: Leaving source tree /tmp/hello-devtool as-is; if you no longer need it then please delete it manually

The recipe is now in meta-yoseli-apps/recipes-hello-devtool/hello-devtool/.
Important: The finished recipe still has SRC_URI = "".

For production, you need to:

  1. Host your source in a git repository (GitHub, GitLab, etc.)

  2. Update SRC_URI to point to that repository

  3. Set proper LICENSE and LIC_FILES_CHKSUM

Cleaning Up

Note: devtool finish automatically removes the recipe from the workspace, so no reset is needed after a successful finish. Use devtool reset only if you want to abandon changes without finishing:

# Only needed if you want to discard workspace changes instead of finishing
$> kas shell kas/project_qemu.yml -c "devtool reset -n hello-devtool"

The -n flag skips running make clean (useful if already cleaned or no clean target).

When to Use devtool vs Manual

  • manual: Learning recipe structure, simple local recipes, full control

  • devtool add: Packaging upstream git projects, quick prototyping

  • devtool modify: Patching existing recipes, debugging build issues

For simple local applications like our hello example, the manual approach is often simpler.

devtool shines when:

  • Working with upstream git repositories (SRC_URI is auto-generated)

  • Debugging and patching existing recipes with devtool modify

  • Rapid prototyping before finalizing a recipe

If you want to learn more about devtool, check out Devtool Hands-On Class from Michael Opdenacker.

Customizing Existing Recipes

Understanding DISTRO Overrides in File Search

Before we modify existing recipes, we need to understand how BitBake searches for files. This is crucial for avoiding a common pitfall.
When BitBake looks for a file specified in SRC_URI (like file://motd), it doesn't just search directories in order. It searches for override-qualified versions first.
Running bitbake -e base-files | grep ^FILESPATH= shows the actual search order (simplified):

FILESPATH=".../meta-yoseli-apps/.../files/poky:
           .../meta-poky/.../files/poky:
           .../meta/.../files/poky:
           .../meta-yoseli-apps/.../files/qemuarm64:
           .../meta-poky/.../files/qemuarm64:
           .../meta/.../files/qemuarm64:
           .../meta-yoseli-apps/.../files/aarch64:
           ...
           .../meta-yoseli-apps/.../files/:
           .../meta-poky/.../files/:
           .../meta/.../files/"

The pattern is:

  1. DISTRO override (`files/poky/`) - searched first across all layers

  2. MACHINE override (`files/qemuarm64/`) - searched second

  3. ARCH override (`files/aarch64/`) - searched third

  4. Plain files (`files/`) - searched last

This means if meta-poky provides files/poky/motd and you create files/motd, the distro-qualified version wins even though your layer has higher priority in FILESEXTRAPATHS.

Modifying Existing Recipes with bbappend


Sometimes you need to customize an existing recipe without forking it. That's what .bbappend files are for.
Let's customize the login message (motd). The base-files recipe in meta-poky provides a files/poky/motd with a Poky warning message.

To override it, we must match the path structure.
Create the directory structure:

$> mkdir -p meta-yoseli-apps/recipes-core/base-files/files/poky

Create recipes-core/base-files/files/poky/motd:

  __   __              _ _
  \ \ / /__  ___  ___| (_)
   \ V / _ \/ __|/ _ \ | |
    | | (_) \__ \  __/ | |
    |_|\___/|___/\___|_|_|

  Custom Embedded Linux - Built with Yocto & KAS
  https://www.yoseli.org

Tip: The ASCII art banner was generated with figlet:

$> figlet Yoseli

Now create recipes-core/base-files/base-files_%.bbappend:

# Customize base system files for Yoseli
# Our files/poky/motd overrides meta-poky's version via FILESEXTRAPATHS
FILESEXTRAPATHS:prepend := "${THISDIR}/files:"

Key concepts:

  • base-files_%.bbappend: The % wildcard matches any version of base-files

  • FILESEXTRAPATHS:prepend: Adds our files/ directory to the front of the search path

  • files/poky/motd: Matches the DISTRO override, ensuring our file is found before meta-poky's

Integrating the Layer with KAS

Create kas/include/yoseli-apps.yml:

header:
    version: 18

repos:
    meta-yoseli-apps:
        path: meta-yoseli-apps

local_conf_header:
    yoseli-apps: |
        IMAGE_INSTALL:append = " hello"

This tells KAS where to find our layer and adds the hello package to the image.

Building and Testing

Build the updated image:

$> kas build kas/project_qemu.yml

Launch QEMU and test:

$> kas shell kas/project_qemu.yml -c "runqemu nographic slirp"

Here's a demo of the QEMU boot with our custom layer:

After logging in as root, you should see the custom Yoseli banner:

  __   __              _ _
  \ \ / /__  ___  ___| (_)
   \ V / _ \/ __|/ _ \ | |
    | | (_) \__ \  __/ | |
    |_|\___/|___/\___|_|_|

  Custom Embedded Linux - Built with Yocto & KAS
  https://www.yoseli.org

qemuarm64 login: root

root@qemuarm64:~# hello
Hello from Yoseli!
This is your first custom Yocto recipe.

Essential Debugging Commands

When things don't work, these commands help:

# List all layers recognized by BitBake
$> bitbake-layers show-layers

# Show which recipes have bbappend files modifying them
$> bitbake-layers show-appends

# Display the computed value of a variable for a recipe
$> bitbake -e base-files | grep ^FILESPATH=

# Show the file search paths for a recipe
$> bitbake -e base-files | grep ^FILESEXTRAPATHS=

# Clear all build artifacts and force a complete rebuild
$> bitbake -c cleansstate base-files

# Run only the unpack task to inspect source files
$> bitbake -c unpack base-files

# Examine the unpacked files in the work directory
$> cat tmp/work/*/base-files/*/motd

What We Built

In this part, we:

  • Added QEMU support for fast iteration without hardware

  • Created a custom layer (`meta-yoseli-apps`)

  • Wrote a recipe from scratch (`hello`) - both manually and with devtool

  • Learned the devtool workflow: add → build → finish

  • Modified an existing recipe with bbappend (custom motd)

  • Learned about DISTRO overrides in file search paths

  • Integrated everything with KAS

The full code is available in the yocto-blog-posts repository on the part2-qemu-custom-layer branch.

Next Steps

The system now boots with custom software, but it's still missing practical features:

  • SSH access for remote management

  • User accounts with proper authentication

  • Secrets management

This article is part of a series on building embedded Linux systems with Yocto and KAS. Questions or feedback? Open an issue on GitHub.