BitBake Tasks and Classes

Understanding task execution, recipe flow, and how classes provide reusable build behaviour

Once you have written a recipe, the next question is simple: what actually happens when BitBake builds it?

The answer is that BitBake does not treat a recipe as one large build script. Instead, it works through a sequence of named tasks, with each task responsible for one stage of the build. A lot of the behaviour in those tasks also comes from classes that the recipe inherits.

If you understand tasks and classes, recipes become much easier to read, debug, and customise.

The Big Picture

At a high level, BitBake usually works through a flow like this:

  • fetch the source
  • unpack the source
  • apply patches
  • configure the software
  • compile the software
  • install files into a staging area
  • split those files into packages

That sequence is expressed through tasks.

For example, when you run:

bitbake mytool

BitBake does not only run one command. It decides which tasks are needed, checks their dependencies, reuses cached results where possible, and then executes the required task graph in the right order.

Common Tasks

Some of the most important tasks you will see are:

do_fetch

Download or retrieve the source defined by SRC_URI.

do_unpack

Unpack the fetched source into the working area.

do_patch

Apply patches listed in SRC_URI.

do_configure

Prepare the source tree for compilation.

do_compile

Build the software.

do_install

Copy the built files into the package staging area.

do_package

Split installed files into packages.

do_rootfs

Install selected packages into the final root filesystem for an image.

You do not define every one of these tasks yourself. In many recipes, several come from inherited classes.

A Simple Recipe Flow

Consider our demo recipe:

SUMMARY = "Simple demo tool"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

SRC_URI = "file://hello-demo.sh"
S = "${WORKDIR}"

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

Even though the recipe only defines do_install(), BitBake still knows how to run the earlier and later stages around it.

That means:

  • do_fetch gets the local file from SRC_URI
  • do_unpack stages it in the working area
  • do_install copies it into ${D}
  • do_package turns the installed result into one or more packages

This is one of the most important Yocto ideas: you usually describe behaviour by adding metadata to an existing build framework, not by writing every step from scratch.

Tasks Are Connected by Dependencies

Tasks do not run in isolation.

For example, do_compile normally depends on earlier work such as fetching, unpacking, patching, and configuration. do_install depends on compilation finishing successfully. Packaging depends on installation.

That dependency structure is what allows BitBake to:

  • execute tasks in the correct order
  • skip work that is already up to date
  • rebuild only the parts affected by a change
  • share cached results when possible

This is why Yocto builds can be both powerful and sometimes confusing. A small metadata change may affect one task, several tasks, or an entire chain of tasks depending on what changed.

Running a Specific Task

You do not always need to run the full recipe build.

If you just want to run a specific task (with dependencies) you can use the -c flag with BitBake,

bitbake -c <TASK> <RECIPE>

Where <TASK> is the name of the task without the do_, e.g.

bitbake -c fetch mytool
bitbake -c unpack mytool
bitbake -c compile mytool
bitbake -c install mytool

This is extremely useful when debugging.

For example:

  • use -c fetch if you suspect the source URL is wrong
  • use -c patch if a patch is failing
  • use -c compile if the build system is misconfigured
  • use -c install if files are not being staged where you expect

What do_install() Really Does

One common mistake is thinking do_install() places files directly on the target image.

It does not.

do_install() copies files into a staging area, usually under ${D}, which is inside the recipe’s work area. Later tasks package those files, and image tasks decide which packages should actually be installed into the final root filesystem.

That separation is why Yocto can:

  • generate several packages from one recipe
  • decide which packages belong in which image
  • support development packages, debug packages, and runtime packages separately

What Classes Are

A class is a reusable piece of BitBake metadata. These often contain python functions that are useful in many different places.

Classes usually live in .bbclass files and provide shared behaviour that many recipes can inherit.

For example:

inherit cmake

or:

inherit autotools pkgconfig

When a recipe inherits a class, it gains variables, task implementations, helper functions, and defaults defined by that class.

This keeps recipes shorter and more consistent.

Why Classes Matter

Without classes, every recipe would need to manually define how to configure and compile its software.

That would create a lot of repetition.

Instead, classes capture common patterns such as:

  • building an Autotools project
  • building a CMake project
  • integrating pkg-config
  • installing a systemd service
  • handling Python packaging

This means the recipe can focus on the parts that are specific to that piece of software rather than reimplementing the whole build process.

Common Classes

Some very common classes include:

autotools

Provides standard configure, compile, and install behaviour for Autotools projects.

cmake

Provides build behaviour for projects that use CMake.

pkgconfig

Helps ensure pkg-config integration works correctly during the build.

systemd

Adds support for packaging and enabling systemd service units.

kernel

Provides shared kernel recipe behaviour.

core-image

Provides standard behaviour for image recipes.

You do not need to memorise every class immediately. The important thing is to recognise that much of the recipe behaviour is often inherited rather than defined directly in the .bb file.

Example: An Autotools Recipe

A recipe for a normal Autotools project might look like this:

SUMMARY = "Example application"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

SRC_URI = "git://github.com/example/myapp.git;branch=main;protocol=https"
SRCREV = "0123456789abcdef0123456789abcdef01234567"

inherit autotools pkgconfig

That recipe may not define do_configure(), do_compile(), or do_install() explicitly at all.

That is because the autotools class already provides the standard logic for those stages.

If the project mostly follows the expected Autotools pattern, the recipe can stay very small.

Customising Inherited Behaviour

Inheriting a class does not mean you lose control.

You can still customise behaviour in a few common ways:

  • set variables that the class uses
  • append to or prepend task functions
  • override a task completely if necessary

For example, you might add extra configure options:

EXTRA_OECONF += "--enable-tools"

Or extend a task:

do_install:append() {
    install -d ${D}${sysconfdir}/myapp
    install -m 0644 ${WORKDIR}/myapp.conf ${D}${sysconfdir}/myapp/myapp.conf
}

This is usually better than replacing the whole task, because it keeps the class behaviour intact and only adds the project-specific part you need.

Prefer Extending Over Replacing

If a class already provides working behaviour, try to extend it before replacing it.

For example, this is often a good approach:

do_install:append() {
    install -m 0755 extrascript ${D}${bindir}/extrascript
}

Whereas replacing the full do_install() task should normally be a deliberate decision:

do_install() {
    ...
}

Inspecting the Environment with bitbake -e

When a recipe behaves differently from what you expected, a useful tools is:

bitbake -e mytool

This shows all the expanded BitBake environment variables for that recipe.

This is different from the bitbake-getvar in the variables lesson in that it returns all of the variables not just the specific one you may be looking for.

It helps you answer questions such as:

  • what is the final value of S?
  • which class set this variable?
  • what does EXTRA_OECONF expand to?
  • which overrides are active?

The output is large, but it is one of the best ways to understand how BitBake arrived at the final metadata for a recipe.

Looking at Task Work Directories

When you need to inspect what happened during a task, the recipe work area is often the next place to look.

You will typically find useful files under paths like:

tmp/work/<machine>/<recipe>/<version>/

Important subdirectories often include:

  • temp/ for task logs and run scripts
  • the unpacked source tree
  • staged install output before packaging

This is where you can inspect:

  • the exact command script BitBake generated for a task
  • the log from a failed compile or install
  • the patched source tree as BitBake saw it

Task Logs

When a task fails, the logs in temp/ are usually far more useful than the short error message printed at the end of the build.

For example, you may find files such as:

log.do_compile
run.do_compile
log.do_install
run.do_install

The log.* files show what happened.

The run.* files show the generated shell script or command flow BitBake used for that task.

If you are ever unsure whether the problem is in your recipe or inside the upstream build system, these files often answer that very quickly.

A Helpful Mental Model

When reading a recipe, it helps to think in layers:

  1. What metadata is written directly in this recipe?
  2. Which classes does it inherit?
  3. Which tasks come from those classes?
  4. Which variables modify that behaviour?
  5. Which task is actually failing or producing the wrong result?

This mental model makes Yocto much less mysterious.

Instead of seeing a recipe as a block of magic, you begin to see it as:

  • metadata
  • inherited behaviour
  • task execution
  • packaged output

A Practical Debug Example

Suppose a recipe inherits cmake, but the build fails during configuration.

A sensible debugging path might be:

  1. Run bitbake -c configure myapp
  2. Inspect tmp/work/.../temp/log.do_configure
  3. Check bitbake -e myapp to see the final values of S, B, and related variables
  4. Confirm that the source tree really uses CMake and not some other build system
  5. Add or adjust the variables that the cmake class expects

That process is much more effective than immediately rewriting the recipe from scratch.

Summary

BitBake tasks describe the stages of the build.

Classes provide reusable implementations and defaults for common build patterns.

Together, they are what make Yocto recipes compact, flexible, and composable.

If you understand:

  • the common task sequence
  • where inherited behaviour comes from
  • how to inspect task logs and the expanded environment

then you have a much stronger foundation for writing and debugging recipes.

Quick quiz: tasks and classes

Check your understanding of task flow and inherited behaviour.

Question 1 A recipe inherits a class and suddenly gains configure and compile behaviour you did not write by hand. What is the best explanation?
Question 2 When trying to understand a recipe’s actual behaviour, which question is more useful than just reading the recipe body?
Question 3 A recipe inheriting `cmake` fails during configuration. Which next step best matches the debugging approach from this lesson?