Skip to content

A Yocto Recipe for Qt Applications Built with CMake

How hard can it be to write a Yocto recipe for building a Qt application with CMake? Actually, it turns out to be pretty hard. I have seen my fair share of slow-and-dirty workarounds (nothing is ever quick with Yocto, not even the diry workarounds) how to force the Qt application into the Linux image and onto the device. Over the years, I turned my own slow-and-dirty workarounds into a hopefully quick-and-clean solution. Here it comes.


After a painful struggle and probably with some external help, you managed to build the Linux image for your chosen SoM, SBC or panel PC (a.k.a. operator terminal). You can log in to your device with a serial console or SSH terminal and execute commands at the Linux prompt. You can build your Qt application against an SDK and copy it with scp or via USB drive to the device. The Weston “window manager” displays the Qt application full-screen and you can interact with it via touch.

This approach of installing an application on the device is good enough for developers but not for normal users. When your hundreds or even thousands of devices reach the end of the production line, you don’t want the factory workers to install the Linux image first and then the application and its auxiliary files. This would be slow and error-prone. Instead, the workers should install the Linux image with the application in one step. Later in the field, users perform an OTA update to install the full Linux image with the application.

Eventually, you’ll sit down and write a Yocto recipe for your CMake-based Qt application. This is a bit tricky, as the ways how Yocto and CMake work together are mysterious. You need to know how Yocto’s cmake class calls CMake for configuration, compilation and installation, that all you need from the qt6-cmake class is the QT_HOST_PATH, and how the files installed by CMake end up in different Yocto packages or not.

In short, you need to understand the interface between Yocto/BitBake and CMake. And that’s what my post focuses on. Additionally, you’ll get templates for a Yocto recipe and a CMakeLists.txt file

Controlling a CMake Build from a Yocto Recipe

The Yocto Class cmake.bbclass

The Yocto class poky/meta/classes/cmake.bbclass defines three tasks to build an application or library with CMake.

  • cmake_do_configure generates the make files for the chosen generator (e.g., Ninja, Unix Makefiles).
  • cmake_do_compile compiles and links the application and library targets specified by the CMake commands add_executable, add_library and add_custom_target.
  • cmake_do_install installs binary, configuration and other files into the image sub-directory of the package working directory – using the CMake command install.

The class also generates a CMake toolchain file toolchain.cmake, which facilitates the cross-compilation from the host to the target architecture (e.g., from x86_64 to aarch64). You can find the toolchain file in the package working directory (WORKDIR in the builld directory diagram).

An abridged version of the CMake configure command looks like this:

cmake \
    -DCMAKE_INSTALL_BINDIR:PATH=${@os.path.relpath(d.getVar('bindir'), ...} \
    -DCMAKE_INSTALL_LIBDIR:PATH=${@os.path.relpath(d.getVar('libdir'), ...} \
    -DCMAKE_TOOLCHAIN_FILE=${WORKDIR}/toolchain.cmake \

The variable ${OECMAKE_GENERATOR_ARGS} is set to either



-G 'Unix Makefiles' -DCMAKE_MAKE_PROGRAM=make

earlier in the class file. CMake builds only support two generators: Ninja and Unix Makefiles. Ninja is the default generator. You can change this with the following setting in the recipe for your software:

OECMAKE_GENERATOR = "Unix Makefiles"

The variable ${OECMAKE_SOURCEPATH} points to the source directory (${S} by default) containing the top-level CMakeLists.txt file. If your CMakeLists.txt is in the top-level directory of your source tree, you don’t have to do anything. Otherwise, you must change the variable similar to this:


The CMake configure command continues by setting the CMAKE_INSTALL_<dir> variables, which get their values from the corresponding BitBake variables like ${prefix}, ${bindir} and ${libdir}. These BitBake variables are defined in poky/meta/conf/bitbake.conf. The script ${WORKDIR}/temp/run.do_configure for running the CMake configure task would typically have the following definitions, where executables end up in /usr/bin and libraries in /usr/lib:


CMake uses the toolchain file ${WORKDIR}/toolchain.cmake generated by cmake.bbclass. Among other things, the toolchain file defines the CMake variables for the C/C++ compilers, linkers and their flags.

set( CMAKE_C_COMPILER aarch64-poky-linux-gcc )
set( CMAKE_CXX_COMPILER aarch64-poky-linux-g++ )
set( CMAKE_C_FLAGS " -march=armv8-a+crc+crypto -fstack-protector-strong  -O2 ...")
set( CMAKE_CXX_FLAGS " -march=armv8-a+crc+crypto -fstack-protector-strong  -O2 ...")
set( CMAKE_CXX_LINK_FLAGS "...   -Wl,-z,relro,-z,now" CACHE STRING "LDFLAGS" )

The variable ${EXTRA_OECMAKE} at the end of the CMake configure call enables recipes and other classes to add their own CMake variable definitions. The class qt6-cmake.bbclass and the standard Yocto recipe below will use this extension point.

The Yocto Class qt6-cmake.bbclass

The class meta-qt6/classes/qt6-cmake.bbclass, which inherits cmake.bbclass, adds several CMake definitions to the Yocto variable ${EXTRA_OECMAKE}. The following definitions are relevant for all CMake configurations:

    -DINSTALL_BINDIR:PATH=${@os.path.relpath(d.getVar('QT6_INSTALL_BINDIR'), d.getVar('prefix') + '/')} \
    -DINSTALL_DOCDIR:PATH=${... d.getVar('QT6_INSTALL_DOCDIR'), ...)} \
    -DINSTALL_LIBDIR:PATH=${... d.getVar('QT6_INSTALL_LIBDIR'), ...)} \

When cross-compiling a Qt application, the Yocto build must run tools like moc, rcc, lupdate and lrelease on the host (similar to the compiler). QT_HOST_PATH points to the base directory of a host Qt installation, where the build can find the host tools. Yocto builds the host tools before it builds the target Qt libraries.

The definition QT_FORCE_BUILD_TOOLS=ON makes sure that the host tools are also built for the target. This option doesn’t apply to building an application but to building Qt itself. The option implies that you build Qt on the target. 1, 2 or even 4 GB RAM are not enough to build Qt on the device. Besides, on-target builds are excruciatingly slow. Just don’t do it! Cross-building on the host is the way to go.

The first two QT_* variables are undocumented. Similar to QT_FORCE_BUILD_TOOLS, they seem to be relevant for building Qt itself.

Then, the class qt6-cmake defines INSTALL_<dir> variables to install executables (BINDIR), documentation (DOCDIR), examples (EXAMPLESDIR), headers (INCLUDEDIR), libraries (LIBDIR), etc. They are duplicating the CMAKE_INSTALL_<dir> variables from cmake.bbclass. They also add some new installation directories like INSTALL_EXAMPLESDIR, INSTALL_SYSCONFDIR and INSTALL_TRANSLATIONSDIR, which are Qt-specific. The INSTALL_<dir> variables are needed for building the Qt libraries and the SDK but not for Qt applications.

The following lines append three more CMake definitions, if a target build – specified by the class override class-target – is performed.

EXTRA_OECMAKE:append:class-target = "\
    -D__harfbuzz_broken_config_file=TRUE \

The first two definitions are known. The third definition fixes a problem with HarfBuzz, which the Qt libraries use for converting Unicode text into glyphs. Hence, it is only relevant for building the Qt libraries but not for Qt applications.

In short, only one variable definition from qt6-cmake.bbclass is relevant for cross-building Qt applications: QT_HOST_PATH. All other definitions are only relevant for building the Qt libraries. So, your application recipe could inherit qt6-cmake and pull in a lot of irrelevant definitions, which hopefully don’t do any harm. Or, it could inherit the base class cmake and add QT_HOST_PATH manually. I’ll show you the second option in the next section, as it is a lot clearer what the recipe is doing.

A Typical Yocto Recipe

Let me walk you through a typical Yocto recipe that builds a Qt application with CMake. You can adapt the recipe to your needs. The Yocto documentation with its detailed description how to write a new recipe may be helpful.

SUMMARY = "The super-duper Qt application"
AUTHOR = "Paul Smith ("

SUMMARY describes what the software provided by the recipde does. If DESCRIPTION is not given, SUMMARY is used instead. AUTHOR gives the name and email address of the person who wrote the recipe. HOMEPAGE is the web page, where you can find more information about the software built by the recipe.

LICENSE = "Proprietary"
LICENSE_FLAGS = "commercial"

For commercial applications, you set LICENSE to Proprietary and LICENSE_FLAGS to commercial (see also Enabling Commercially Licensed Recipes). You must add the line

    LICENSE_FLAGS_ACCEPTED:append = " commercial"

to your local.conf file. If you forget it, the package my-app is not included in the image.

You assign your license file and its checksum to LIC_FILES_CHKSUM. If you run BitBake with an empty checksum md5=, it will tell you the correct checksum in an error message. Just copy this checksum into the recipe.


For packages licensed under FOSS licenses, you can skip LICENSE_FLAGS.

PV = "1.3.2"

PV is the “version of the recipe”. No, it is not! PV is “the version of the software being packaged”, that is, it is the version of the software being built and installed by the recipe (see Incrementing a Package Version for more details). That’s quite different to the recipe version. For example, PV for all Qt 6.5.1 packages like qtbase, qtserialbus and qtdeclarative is 6.5.1.

When you change the version in the source code of the package, you must not forget to change PV in the recipe – and vice versa. You must keep the two versions in sync.

inherit cmake
    -DCMAKE_MODULE_PATH=${STAGING_DIR_TARGET}/${libdir}/cmake/B4OtaUpdate \

The recipe inherits the base cmake class – and not qt6-cmake. It adds two CMake definitions to EXTRA_OECMAKE. The do_configure task passes these two definitions to the CMake command for configuring the Qt application – as you would do when calling CMake on the command line.

The first definition sets QT_HOST_PATH to something like /path/to/sdk/sysroots/x86_64-pokysdk-linux/usr so that the cross-build knows where to find moc, rcc and other Qt host tools. CMAKE_MODULE_PATH defines the search paths for CMake modules exported by other recipes so that my-app‘s CMakeLists.txt file can import the modules. For example, my-app could link against the library from the module B4OtaUpdate with the following line in its CMakeLists.txt file:

target_link_libraries(${ProjectId} PRIVATE B4OtaUpdate::UpdateAdapter ...)

Of course, you’ll add other CMake definitions to EXTRA_OECMAKE in the Yocto recipe of your Qt application – in addition to the definition for QT_HOST_PATH. I try to minimise the definitions added to EXTRA_OECMAKE by setting the variables to proper default values for a production build in my-app‘s CMakeLists.txt file. You might want to do the same.

SRC_URI = "git://;name=myapp;branch=main;protocol=ssh"
SRCREV_myapp = "ac078eb65da118354c6c21911b35ec49444e18af"
S = "${WORKDIR}/git"

DEPENDS += "b4-ota-update qtbase qtdeclarative qttools-native"

SRC_URI tells the fetch task to clone the my-app.git repository using the ssh protocol and switch to the branch main. SRCREV_myapp makes fetch check out the given commit SHA. Note that SRC_URI differs from the URL that you would use in git clone:

git clone

DEPENDS lists the recipes that must be built before the recipe By including qtbase, for example, the build installs all libraries, headers, tools and other files from qtbase into the sub-directory recipe-sysroot of my-app‘s working directory – before the configure task of my-app runs. This lets CMake find the library, if you add Qt6::Core to the target_link_libraries command of the application’s CMakeLists.txt.

The dependency qttools-native looks a bit odd, as its artifacts are for the host computer. It provides host tools like linguist, lrelease and lupdate, which are needed during development and during the build but not on the device.

A Typcial CMakeLists.txt File

Configuring and Building an Application

After all this preparation, it is high time to show you a typical top-level CMakeLists.txt file that the recipe uses to build your Qt application. Here is a step-by-step walk-through.

cmake_minimum_required(VERSION 3.22)

At the time of this writing, the current patch version of Yocto 4.0 (kirkstone) uses CMake 3.22. Older patch versions may use older CMake versions, newer patch versions newer CMake versions. Using newer CMake versions for building Qt applications has never been a problem for me. For Qt 6, you are on the safe side with CMake 3.19 or newer.

The project command sets the variable PROJECT_NAME to MyApp. It also sets a couple of less used PROJECT_* variables.


The CMake module GNUInstallDirs assigns the standard Linux installation paths to the variables CMAKE_INSTALL_<dir>. For example:


All CMAKE_INSTALL_<dir> directories are relative to CMAKE_INSTALL_PREFIX, which has the default value /usr. The values of the CMAKE_INSTALL_<dir> directories are used for the DESTINATION argument of the install commands (see below).

# Global settings for this project and its sub-projects

# Libraries and tools built by this project

The two set commands specify that the UpdateAdapter library, which is added to the project by the add_subdirectory command, shall use SwUpdate as the update client on the device and Memfault as the update server for managing the device fleet. The add_subdirectory command adds the UpdateAdapter library to this project. The Yocto recipe builds both the Qt application MyApp and the library UpdateAdapter in one go.

In general, the CMake variables from this section configure which parts of the project are built and how they are build. They apply to this project and all its sub-projects included through add_subdirectory commands.


This project mandates the use of the C++17 standard. You cannot use C++20 features, but you can use C++11 or C++98 features. Qt 6 supports C++17 by default.

find_package(Qt6 6.4 COMPONENTS Gui Quick REQUIRED)

The find_package command sets up the search paths where to find the Qt modules QtGui and QtQuick from Qt 6.4. The two Qt modules are mandatory for building the project. After the find_package command, other commands like target_link_libraries can refer to the Qt modules as Qt6::Gui and Qt6::Quick. You don’t have to provide link paths for the g++ compiler option -L or include paths for the option -I. CMake takes care of this.

The last two settings make CMake automatically apply moc to C++ files containing the Q_OBJECT macro and rcc to .qrc files.

add_executable(${PROJECT_NAME} main.cpp sub1/a.cpp sub1/a.h ...)

qt_add_qml_module(${PROJECT_NAME} URI Main VERSION 1.0 RESOURCE_PREFIX /
    QML_FILES main.qml A.qml ...)

target_include_directories(${PROJECT_NAME} PRIVATE sub1)

    PRIVATE B4OtaUpdate::UpdateAdapter Qt6::Gui Qt6::Quick)

You pass a list of C++ source and header files to add_executable. The file paths are relative to the directory containing the CMakeLists.txt file. The qt_add_qml_module command creates a QML module Main from the QML source files given after QML_FILES. You can load main.qml from the main() function with the following commands:

    QQmlApplicationEngine engine;

As usual, you can instantiate other QML components (e.g., A) from the same module without explicitly importing Main. As your application is growing, you’ll introduce more QML modules in sub-projects – included with add_subdirectory commands.

The command target_include_directories adds include directories (here: sub1) to the target object ${PROJECT_NAME} (here: MyApp) defined by add_executable (add_library for libraries). During the build, CMake passes the include directories to the C++ compiler with the option -I so that the compiler knows where to search for the header files.

The second argument of target_include_directories controls the access to the target object. PRIVATE include directories are only visible to the current target but not to other targets using the current target. PUBLIC include directories are also visible to other targets. The difference is irrelevant for top-level targets like MyApp. However, it is relevant for a library or executable target A linking against a library target B. Target B specifies which headers should be visible to A through the PUBLIC include directories and which headers should not be visible to A through the PRIVATE include directories.

The command target_link_libraries adds library target objects to the current target. A library target object like Qt6::Gui provides the include search paths, library search paths and library names through its public properties to the current project. CMake translates these properties into the arguments for the compiler options -I, -L and -l, respectively.

Again, the second option of target_link_libraries controls the access for other targets using the current target. If MyApp were a library target aptly called MyLib with the above target_link_libraries definition, another target would have to pass Qt6::Gui in addition to MyLib to its own target_link_libraries command. The other library doesn’t know that MyLib uses Qt6::Gui, as Qt6::Gui is PRIVATE. If Qt6::Gui were PUBLIC in MyLib, the other target could skip Qt6::Gui and would get away with listing only MyLib in its target_link_libraries.

Installing Executables

The commands in the CMakeLists.txt file so far contribute to the Yocto tasks do_configure and do_compile. The CMake install commands tell the Yocto task do_install which files to install in the rootfs image. The do_install task fails, if your project’s CMakeLists.txt files don’t contain any install commands.


This install command installs the executable (the RUNTIME) to the DESTINATION directory ${CMAKE_INSTALL_BINDIR}, which is set to bin by the CMake module GNUInstallDirs at the beginning of this CMakeLists.txt file. The destination directory is prefixed by ${CMAKE_INSTALL_PREFIX}, which defaults to /usr. You can find the executable in the file /usr/bin/MyApp on the device.

build/tmp/work/armv8a-mx8mp-poky-linux/my-app/0.1.0-r0/      # package working dir
    image/usr/bin/                                           # staging dir
        MyApp                                                # executable

The Yocto build stores the executable in the staging directory image/usr/bin, which is a sub-directory of the package working directory. The Yocto task do_rootfs copies the contents of the image sub-directory of all the packages to the rootfs sub-directory of the working directory of the image package. The executable could, for example, end up in the directory


The Yocto task do_image_ext4 creates an image for an ext4 file system, which you could burn on an SD card or into the internal eMMC storage of your device.

There are special install commands for installing dynamic and static libraries, files, directory trees and other output artifacts. Let me walk you through the most common variants (see the documentation for install for other variants).

Installing Shared Libraries

add_library(${PROJECT_NAME} SHARED <cpp-files> <h-files>)

The Yocto task do_install dutifully installs the shared library in the staging directory image/usr/lib with the CMake install(TARGETS) command. However, the task do_package_qa fails with this cryptic error message:

ERROR: b4-ota-updater-0.3.1-r2 do_package_qa: QA Issue: -dev package b4-ota-updater-dev
    contains non-symlink .so '/usr/lib/' [dev-elf]
ERROR: b4-ota-updater-0.3.1-r2 do_package_qa: Fatal QA errors were found, failing task.

The b4-ota-updater-dev package contains the file itself but should contain a link to it or its versioned cousins (e.g., You find the RPM packages in the sub-directory deploy-rpms/armv8a of the package working directory. You can check its contents with this command:

$ rpm -qlp b4-ota-updater-dev-0.3.1-r2.armv8a.rpm

The real problem is that there is a dbg, dev, lic and src package but no main package. The main package should contain the library file, to which the library in the dev package can link. As the main package doesn’t even exist, an application depending on the main package b4-ota-update would fail to build.

The configuration file bitbake.conf defines, which build artifacts (libraries, headers, sources, licenses, etc.) go into which package. Here are the definitions relevant for libraries with the fully evaluated path patterns in the comments:

SOLIBS = ".so.*"
FILES:${PN} = "... ${libdir}/lib*${SOLIBS} ..."             # /usr/lib/lib*.so.*
FILES_SOLIBSDEV ?= "... ${libdir}/lib*${SOLIBSDEV} ..."     # /usr/lib/*.so
FILES:${PN}-dev = "... ${FILES_SOLIBSDEV} ..."              # /usr/lib/*.so

FILES:${PN} holds the path patterns for the main package and FILES:${PN}-dev for the dev package. So, library files like /usr/lib/ and /usr/lib/ end up in the main package and library files like /usr/lib/ in the dev package. The CMake build only produces unversioned *.so files but not versioned *.so.1 or *.so.1.0.0 files. You change this by setting the properties VERSION and the SOVERSION for the library target.

add_library(${PROJECT_NAME} SHARED <cpp-files> <h-files>)
set_target_properties(${PROJECT_NAME} PROPERTIES
    VERSION 1.0.0

Adding the set_target_properties command eliminates the build error, creates a main package and puts the library file and its links into the right packages. By the way, the base name of the package changes from b4-ota-updater to libupdateadapter. The following checks corroborate this.

$ cd deploy-rpms/armv8a
$ rpm -qlp libupdateadapter1-0.3.1-r2.armv8a.rpm
$ rpm -qlp libupdateadapter-dev-0.3.1-r2.armv8a.rpm

The contents of the RPM packages look OK.

$ cd image/usr/lib
$ ls -l -> ->

The dev package now contains a link from to from the main package. So, the links are OK now. The last two entries are installed on the device. They also end up in the SDK together with the first entry.

Installing Files and Directories


The install(FILES) command puts the two header files into the staging directory image/usr/include. Yocto’s packaging task then puts them into the dev package. The populate_sdk task installs the headers in the SDK but not in the rootfs. The install command should only list public header files and not private ones, as other software should not depend on the private headers of a library. The install(FILES) command can deploy any files – e.g., image, calibration, configuration and database files – into the rootfs.


The install(DIRECTORY) command recursively copies the contents of the directory data/profiles/ to the staging directory image/usr/share/profiles. Whether the source directory ends with a slash or not makes a difference. With a trailing slash, the source file data/profiles/x/y.c is installed as shares/profiles/x/y.c. Without a trailing slash, it it is installed as shares/profiles/profiles/x/y.c. The latter is rarely what you want.

Leave a Reply

Your email address will not be published. Required fields are marked *