Skip to content

Building Qt 6.2 For Old Yocto Versions

The HMI of the ROPA sugar-beet harvesters runs on a Topcon Opus A8 terminal.

The Qt version of an embedded Linux system is tied to the Yocto version. Yocto 2.7 (Warrior) comes with Qt 5.12, Yocto 3.1 (Dunfell) with Qt 5.14, Yocto 3.2, 3.3 and 3.4 with Qt 5.15 . There is no Yocto version with Qt 6 yet. How can we build Qt 6.2 against the SDK of old Yocto versions?

Introduction

Yocto releases rarely come with the latest Qt release. Terminal manufacturers are very slow in updating their BSPs to newer Yocto versions. The Qt applications on these terminals depend on older and older Qt versions.

Currently, I am helping a client build the driver terminal for a construction machine. The terminal hardware is a Topcon Opus A8. Topcon’s BSP is based on Yocto 2.7 with Qt 5.12. Naturally, the client wants to work with Qt 6.2 or newer. And that’s exactly what I’ll show you in this post: how to build Qt 6.2.4 against an embedded Linux SDK built with Yocto 2.7.

The approach should work for other combinations of Yocto and Qt versions. The most important precondition is that the C++ compiler from the SDK can build Qt. G++ 8.3 could compile the C++17 code of Qt 6. It spit out a couple of warnings but no errors. I had to replace CMake 3.14 from the SDK with CMake 3.18 or newer. Then, the target build worked fine.

Before we can build Qt 6.2.4, we must build the SDK. We can follow the instructions in Setting Up Yocto Projects with kas or Building a Qt SDK with Yocto.

Building Qt 6 for embedded Linux has changed over Qt 5. It is now a two-step process.

  • We build Qt 6 from Git for the host. This gives us Qt tools like moc and rcc that are run during the build on the host computer.
  • We cross-build exactly the same Qt 6 version from Git for the target. If we want to build Qt 6.2.4 for the target, we must have built Qt 6.2.4 for the host. The build failed when I used Qt 6.2.0 on the host.

I use the following directory structure for the project DanishDynamite. The working directory <WORKDIR> is a placeholder for a specific directory, which is /private/Projects/DanishDynamite in my case.

<WORKDIR>/
    build-host-qt6.2.4/       # Host build of Qt 6.2.4
    build-target-qt6.2.4/     # Target build of Qt 6.2.4
    qt5/                      # Qt sources
    sdk/                      # Yocto SDK installation
    terminal-app/             # Sources of terminal application

Fetching the Qt 6 Sources

In our working directory <WORKDIR>, we clone the Qt repository, switch to version 6.2.4 and initialise the Git submodules.

$ cd <WORKDIR>
$ git clone git://code.qt.io/qt/qt5.git
$ cd qt5
$ git checkout 6.2.4
$ perl init-repository

Building Qt 6 for the Host

We follow the documentation page Building Qt 6 from Git with slight adjustments to build Qt 6.2.4 for the host. We create a directory for the shadow host build in <WORKDIR>.

$ cd <WORKDIR>
$ mkdir build-host-qt6.2.4
$ cd build-host-qt6.2.4

We configure Qt for the host with the same submodules as Qt for the target later on. So, we can develop Qt applications on the host as well as on the target. We build a -release version of Qt and install it in the -prefix directory on the host computer.

$ ../qt5/configure -release -prefix /public/Qt/host-qt6.2.4 \
    -submodules qtbase,qtdeclarative,qtimageformats,qtmultimedia,qtremoteobjects,qtscxml,qtserialbus,qtserialport,qtsystems,qtwayland
$ cmake --build . --parallel
$ cmake --install .

Note: Qt 6.2.0 does not support the option -submodules yet. Instead of listing the included Qt modules, we must list the excluded Qt modules with a -skip option for each module.

Note: In an earlier version of this post, I split up the -submodules option into two -submodules options. Some modules were not built (e.g., qtmultimedia).

Building Qt 6 for the Target

For the target build, we use the same sources as for the host build. Again, we create a directory for the shadow target build in <WORKDIR>.

$ cd $WORKDIR
$ mkdir build-target-qt6.2.4
$ cd build-target-qt6.2.4

We set up the build environment for the Yocto SDK by sourcing the environment-setup script. The script sets environment variables like CC, CXX, OECORE_TARGET_SYSROOT and OECORE_NATIVE_SYSROOT. For example:

$ . <WORKDIR>/sdk/environment-setup-armv7at2hf-neon-poky-linux-gnueabi
$ env
...
OECORE_NATIVE_SYSROOT=<WORKDIR>/sysroots/x86_64-pokysdk-linux
OECORE_TARGET_SYSROOT=<WORKDIR>/sdk/sysroots/armv7at2hf-neon-poky-linux-gnueabi
OECORE_TARGET_ARCH=arm
OECORE_DISTRO_VERSION=2.7.4
OE_CMAKE_TOOLCHAIN_FILE=<WORKDIR>/sdk/sysroots/x86_64-pokysdk-linux/usr/share/cmake/OEToolchainConfig.cmake
...

The Qt documentation page Configuring an Embedded Linux Device describes how to configure Qt 6.2. The description didn’t work right out of the box for me. I’ll walk you through the problems I encountered and how I fixed them.

The configure command for the target extends the command for the host by a couple of options (in bold face).

$ cd <WORKDIR>/build-target-qt6.2.4
$ ../qt5/configure -release -sysroot $OECORE_TARGET_SYSROOT \
    -prefix /opt/qt-6.2 -extprefix $OECORE_TARGET_SYSROOT/opt/qt-6.2 \
    -qt-host-path /public/Qt/host-qt6.2.4 \
    -submodules qtbase,qtdeclarative,qtimageformats,qtmultimedia,qtremoteobjects,qtscxml,qtserialbus,qtserialport,qtsystems,qtwayland \
    -- -DCMAKE_TOOLCHAIN_FILE=$OE_CMAKE_TOOLCHAIN_FILE

The target build must look for the libraries and headers in the target root filesystem specified by -sysroot $OECORE_TARGET_SYSROOT. The installation directory on the target device is given by -prefix /opt/qt-6.2 and the staging directory by -extprefix $OECORE_TARGET_SYSROOT/opt/qt-6.2. The Qt libraries and headers from the staging area are used when we cross-build Qt applications. The option -qt-host-path /public/Qt/host-qt6.2.4 tells the target build where to find host tools like moc and rcc.

The last line, which is separated with -- from the rest of the options, is passed to CMake. In Qt 6, the configure command is translated into a cmake command. The variable CMAKE_TOOLCHAIN_FILE is set to the standard CMake toolchain file created for a Yocto SDK.

Running the above configure command stops with this error:

CMake Error at <WORKDIR>/qt5/qtbase/cmake/QtProcessConfigureArgs.cmake:80 (list):
  list does not recognize sub-command POP_FRONT

The CMake 3.15 Release Notes reveal that POP_FRONT was newly introduced to CMake 3.15. The Yocto 2.7 SDK contains CMake 3.14. Qt 6 requires at least CMake 3.18. We install CMake 3.18 or newer on our build computer, say, in /usr/local/bin, and link cmake from the SDK to this new version.

$ cd $OECORE_NATIVE_SYSROOT/usr/bin
$ mv cmake cmake-3.14
$ ln -s /usr/local/bin/cmake cmake

This fixes the error about POP_FRONT. Rerunning configure now works.

We build Qt for the target and install it into the Yocto SDK at $OECORE_TARGET_SYSROOT/opt/qt-6.2 with the following commands.

$ cmake --build . --parallel
$ cmake --install . 

Building Qt 6 Applications for the Target

Section Building Applications for the Target Device of the documentation page Configuring an Embedded Linux Device tells us how to cross-build Qt applications. This only works, if we have sourced the environment-setup script of the Yocto SDK before.

$ . <WORKDIR>/sdk/environment-setup-armv7at2hf-neon-poky-linux-gnueabi

$ mkdir -p <WORKDIR>/build-terminal-app
$ cd <WORKDIR>/build-terminal-app

$ <WORKDIR>/sdk/sysroots/armv7at2hf-neon-poky-linux-gnueabi/opt/qt-6.2/bin/qt-cmake ../terminal-app/
$ cmake --build . --parallel

We could start QtCreator from the Linux prompt above with all the environment variables preset. However, this would make it impractical to switch between different kits, say, one for the target build and one for the host build of the application. That’s probably the reason why the documentation page Configuring an Embedded Linux provides An Example Toolchain File.

Unfortunately, the example toolchain file is incomplete. It lacks settings for QT_HOST_PATH and CMAKE_PREFIX_PATH. Setting the PKG_CONFIG_* environment variables is not required. The corrected toolchain file looks as follows. Don’t forget to adapt the parts in bold face to your development setup.

cmake_minimum_required(VERSION 3.18)
include_guard(GLOBAL)

set(TARGET_SYSROOT <WORKDIR>/sdk/sysroots/armv7at2hf-neon-poky-linux-gnueabi)
set(HOST_SYSROOT <WORKDIR>/sdk/sysroots/x86_64-pokysdk-linux)
set(QT_HOST_PATH /public/Qt/host-qt6.2.4)

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_SYSROOT ${TARGET_SYSROOT})
set(CMAKE_PREFIX_PATH ${CMAKE_SYSROOT}/opt/qt-6.2/lib/cmake)
set(CMAKE_C_COMPILER ${HOST_SYSROOT}/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-gcc)
set(CMAKE_CXX_COMPILER ${HOST_SYSROOT}/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-g++)

set(QT_COMPILER_FLAGS "-march=armv7-a -mthumb -mfpu=neon -mfloat-abi=hard")
set(QT_COMPILER_FLAGS_RELEASE "-O2 -pipe")
set(QT_LINKER_FLAGS "-Wl,-O1 -Wl,--hash-style=gnu -Wl,--as-needed")

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

include(CMakeInitializeConfigs)

function(cmake_initialize_per_config_variable _PREFIX _DOCSTRING)
  if (_PREFIX MATCHES "CMAKE_(C|CXX|ASM)_FLAGS")
    set(CMAKE_${CMAKE_MATCH_1}_FLAGS_INIT "${QT_COMPILER_FLAGS}")

    foreach (config DEBUG RELEASE MINSIZEREL RELWITHDEBINFO)
      if (DEFINED QT_COMPILER_FLAGS_${config})
        set(CMAKE_${CMAKE_MATCH_1}_FLAGS_${config}_INIT "${QT_COMPILER_FLAGS_${config}}")
      endif()
    endforeach()
  endif()

  if (_PREFIX MATCHES "CMAKE_(SHARED|MODULE|EXE)_LINKER_FLAGS")
    foreach (config SHARED MODULE EXE)
      set(CMAKE_${config}_LINKER_FLAGS_INIT "${QT_LINKER_FLAGS}")
    endforeach()
  endif()

  _cmake_initialize_per_config_variable(${ARGV})
endfunction()

My toolchain file is located at <WORKDIR>/terminal-app/cmake/Qt6CMakeToolchainConfig.cmake. We open a new Linux terminal and build the terminal application with the following commands.

$ cd <WORKDIR>/build-terminal-app
$ rm -r *

$ cmake -DCMAKE_TOOLCHAIN_FILE=../terminal-app/cmake/Qt6CMakeToolchainConfig.cmake \
    ../terminal-app/
$ cmake --build . --parallel

The values of TARGET_SYSROOT, HOST_SYSROOT and QT_HOST_PATH may differ between development setups. We allow developers to set these variables in their environment. The corresponding lines in the toolchain file change as follows.

set(TARGET_SYSROOT $ENV{TARGET_SYSROOT})
set(HOST_SYSROOT $ENV{HOST_SYSROOT})
set(QT_HOST_PATH $ENV{QT_HOST_PATH})

The QtCreator kit DanishDynamite Qt 6.2.4 is defined as follows:

QtCreator kit for Qt 6.2.4 built against Yocto 2.7 SDK

Environment holds the settings of the environment variables:

HOST_SYSROOT=<WORKDIR>/sdk/sysroots/x86_64-pokysdk-linux
QT_HOST_PATH=/public/Qt/host-qt6.2.4
TARGET_SYSROOT=<WORKDIR>/sdk/sysroots/armv7at2hf-neon-poky-linux-gnueabi

The CMake Configuration points to the new CMake toolchain file:

CMAKE_TOOLCHAIN_FILE:FILEPATH=<WORKDIR>/terminal-app/cmake/Qt6CMakeToolchainConfig.cmake

Running Qt 6 Applications on the Target

In this post, I show how to migrate the terminal application of the ROPA sugar-beet harvesters from Qt 5.12 over Qt 5.15 to Qt 6.2. After I rebuilt the terminal application for the Yocto 2.7 SDK with Qt 6.2, I could run the application on the Topcon Opus A8.

The HMI of the ROPA sugar-beet harvesters runs on a Topcon Opus A8 terminal.
ROPA terminal application running on Opus A8 (dual-core iMX6)

The Run Settings in QtCreator look as follows (see the post Building a Qt SDK with Yocto for a detailed description how to set up QtCreator for running Qt applications on a target device).

Run Settings for terminal application running on Topcon Opus A8