Cross-Compile Qt 6 for Raspberry Pi

From Qt Wiki
Revision as of 12:37, 23 June 2022 by Uraihan (talk | contribs)
Jump to navigation Jump to search

Introduction

This is a step-by-step guide to cross-compile Qt 6 for Raspberry Pi OS. The end result of this guide allows you to compile Qt 6 applications on a host machine and deploy it on the Raspberry Pi running a Raspberry Pi OS image. The instructions in this guide are targeted for beginners, but it should be easy to follow for everyone.

The steps on this guide was tested on Ubuntu 20.04 targeting Raspberry Pi 4 Model B 2GB running Raspberry Pi OS, but it should work with any Debian-based OS.

For an older guide, check this Qt Wiki guide or this one.

Getting Started: Planning Phase

Before we get our hands dirty and mess around with the configurations, let's familiarise ourselves with essential components needed to cross-compile Qt and decide which directory names we want to use to store these components. This way, we can easily track the location of these important components when we are typing commands in the terminal.

At its most basic setup, we need at least the following components in a host machine to cross-compile Qt:

  • Sysroot, which basically is the scaled down version of our target's filesystem inside our host machine. For this example, we will name this
    ~/rpi-sysroot
    
  • A cross-compiler on host machine. To make it as straightforward as possible, for this guide we will use the cross-compiler for Aarch64 from the official Ubuntu package repository.
  • Qt source code with its submodules, at least the qtbase submodule. For this guide, we will download Qt source code from Qt official GitHub repository. By default, it will be stored in
    ~/qt5
    
    .
  • CMake toolchain. CMake is the recommended build generator for Qt 6. Inside this toolchain file, we will specify in detail about the exact specifications of our cross-compiled Qt build. We will store this
    toolchain.cmake
    
    file on our
    $HOME
    
    directory.

We also have to decide the locations of our Qt builds, one for host machine and one for our Raspberry Pi in our PC before we transfer it to the Raspberry Pi.

  • We will store a build of Qt 6 for host machine on
    ~/qt-host
    
    . This is necessary because when building a Qt for our Raspberry Pi target, we will refer to this build of Qt.
  • Finally, our Qt 6 build for Raspberry Pi will be stored on
    ~/qt-raspi
    
    in the host machine temporarily before we move it to a location in our Raspberry Pi (for example,
    /usr/local/qt6
    
    ).

Following the best practices of building C/C++ applications, we also want to make sure that our final build directories are clean from all build configuration files that CMake will create during the configuration phase. For that, we will store build configuration files on

~/qthost-build

for configuration files of Qt for our host machine and

~/qtpi-build

for configuration files of Qt for Raspberry Pi.

After we decided these locations, let's create them all!

On host machine,

$ cd ~
$ mkdir rpi-sysroot rpi-sysroot/usr rpi-sysroot/opt
$ mkdir qt-host qt-raspi qthost-build qtpi-build

The second line will create the sysroot directory with its necessary subfolder, while the third line will create the directory for the rest of locations defined previously.

Installing Raspberry Pi OS

Let's now download and install/flash an image of Raspberry Pi OS to a microSD card. The easiest way to do this is to use the official Raspberry Pi Imager. You can download the latest Imager from https://www.raspberrypi.com/software/, and then flash an image of Raspberry Pi OS to your microSD card with only a few clicks on the Imager's GUI.

Setup Raspberry Pi

After successfully flashing an image of Raspberry Pi OS to a microSD card, insert it into the microSD card reader of our Raspberry Pi, connect a keyboard and monitor to the device and boot it up. The system will ask for a basic first-time setup. Please follow the setup according to the instruction.

After successfully setting up the device, the system will reboot to the home desktop. From here, open Terminal. We want to set our Raspberry Pi to be able to run Qt apps.

Install Dependencies

Update and upgrade to the newest version.

$ sudo apt update
$ sudo apt full-upgrade
$ sudo reboot

Next, install the package dependencies.

$ sudo apt-get install libboost-all-dev libudev-dev libinput-dev libts-dev libmtdev-dev libjpeg-dev libfontconfig1-dev libssl-dev libdbus-1-dev libglib2.0-dev libxkbcommon-dev libegl1-mesa-dev libgbm-dev libgles2-mesa-dev mesa-common-dev libasound2-dev libpulse-dev gstreamer1.0-omx libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev  gstreamer1.0-alsa libvpx-dev libsrtp2-dev libsnappy-dev libnss3-dev "^libxcb.*" flex bison libxslt-dev ruby gperf libbz2-dev libcups2-dev libatkmm-1.6-dev libxi6 libxcomposite1 libfreetype6-dev libicu-dev libsqlite3-dev libxslt1-dev

$ sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev libx11-dev freetds-dev libsqlite3-dev libpq-dev libiodbc2-dev firebird-dev libgst-dev libxext-dev libxcb1 libxcb1-dev libx11-xcb1 libx11-xcb-dev libxcb-keysyms1 libxcb-keysyms1-dev libxcb-image0 libxcb-image0-dev libxcb-shm0 libxcb-shm0-dev libxcb-icccm4 libxcb-icccm4-dev libxcb-sync1 libxcb-sync-dev libxcb-render-util0 libxcb-render-util0-dev libxcb-xfixes0-dev libxrender-dev libxcb-shape0-dev libxcb-randr0-dev libxcb-glx0-dev libxi-dev libdrm-dev libxcb-xinerama0 libxcb-xinerama0-dev libatspi2.0-dev libxcursor-dev libxcomposite-dev libxdamage-dev libxss-dev libxtst-dev libpci-dev libcap-dev libxrandr-dev libdirectfb-dev libaudio-dev libxkbcommon-x11-dev

This will install a bunch of libraries from the Debian repository that Qt needed to deploy various kinds of applications on Raspberry Pi.

We also will make a directory that will hold the Qt installation targeting Raspberry Pi, as specified in the previous section. Actually, this can be done at every time before sending Qt 6 build from host machine to the Raspberry Pi, but in this guide we will create it now:

$ sudo mkdir /usr/local/qt6

Setup Host Machine

Let's leave the Raspberry Pi for a moment and move to our host machine. As mentioned in the Introduction section, the steps below was tested for Ubuntu 20.04 running on a host machine with x86_64 architecture, but it should works with any Debian-based distributions running on x86_64 architecture.

Update System

Let's configure our Ubuntu/Debian host machine. First, let's update our packages to the latest version.

$ sudo apt update
$ sudo apt upgrade

Install Qt Dependencies

Next, we will install some more package dependencies. Some of these packages are build tools needed to compile Qt 6 that you might or might not have.

$ sudo apt-get install make build-essential libclang-dev ninja-build gcc git bison python3 gperf pkg-config libfontconfig1-dev libfreetype6-dev libx11-dev libx11-xcb-dev libxext-dev libxfixes-dev libxi-dev libxrender-dev libxcb1-dev libxcb-glx0-dev libxcb-keysyms1-dev libxcb-image0-dev libxcb-shm0-dev libxcb-icccm4-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-shape0-dev libxcb-randr0-dev libxcb-render-util0-dev libxcb-util-dev libxcb-xinerama0-dev libxcb-xkb-dev libxkbcommon-dev libxkbcommon-x11-dev libatspi2.0-dev libgl1-mesa-dev libglu1-mesa-dev freeglut3-dev

Install Cross Compiler

Next, let's get our cross-compiler. For this guide, we will install it from the Ubuntu/Debian package repository as this is the easiest way to get an ARM64 cross-compiler.

$ sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu

You can also download the cross-compiler from somewhere on the internet. If you want to do so, my recommendation is to check the ARM official download page and choose which one fits your purpose.

Setup SSH Connection

We also want to setup an SSH connection between Raspberry Pi and a host machine. This is needed to transfer data between both devices. The official Raspberry Pi documentation page provided a very detailed and thorough instruction on how to setup SSH between Raspberry Pi and other device, so you can refer this step into that page.

Build Sysroot

We need to build a Sysroot from our own Raspberry Pi device in the host machine. To do this, we will copy and paste a few directories from Raspberry Pi using rsync through SSH. If rsync is not installed yet in your host machine, simply install it by executing

sudo apt install rsync

. After the host machine and Raspberry Pi is connected,

$ rsync -avz --rsync-path="sudo rsync" --delete <pi_username>@<pi_ip_address>:/lib rpi-sysroot
$ rsync -avz --rsync-path="sudo rsync" --delete <pi_username>@<pi_ip_address>:/usr/include rpi-sysroot/usr
$ rsync -avz --rsync-path="sudo rsync" --delete <pi_username>@<pi_ip_address>:/usr/lib rpi-sysroot/usr
$ rsync -avz --rsync-path="sudo rsync" --delete <pi_username>@<pi_ip_address>:/opt/vc rpi-sysroot/opt

Change

<pi_username>

and

<pi_ip_address>

with your Raspberry Pi username and IP Address, respectively. Note: Your Raspberry Pi might not have a directory named

/opt/vc

, and it is fine. Usually this directory contains proprietary Broadcom libraries, but during the testing the author did not find any issue with the lack of this directory. After building our own sysroot, we need to fix absolute symbolic links that were broken during the syncing process by converting them to relative symlinks. The easiest, fastest way to do this is to use a command-line tool called

symlinks

, which is available from the Ubuntu official repository.

$ sudo apt install symlinks
$ cd ~
$ symlinks -rc rpi-sysroot

Check the symlinks documentation to learn more.

Building Qt 6

After all the setup process, finally we can start building Qt 6! This will be divided into two parts: Building Qt 6 for host machine, and building Qt 6 for our target device (i.e. the Raspberry Pi).

Building Qt 6 for Host Machine

For the detailed step-by-steps of this process, I will refer to this great wiki article that explained the whole process in detail. One thing to remember is that we will install Qt on

~/qt-host

, and we want to store all build configuration files on

~/qt-hostbuild

. Notice that you can also choose to install only Qt submodules that you need. This is good if you have a limited space in your host machine. At minimum, you only need the

qtbase

submodule to be able to develop and run a simple Qt Widget application. But to access other Qt features you need to install other or all Qt submodules. For this guide, I only installed the following submodules:

qtbase,qtsvg,qtdeclarative,qtquick3d,qtshadertools,qtimageformats,qtquicktimeline

. You can also prevent CMake to build application examples and tests by adding

-nomake examples

and

-nomake tests

flags when configuring Qt.

The rest of the guide is quite straightforward, so I will leave you to read it.

Building Qt 6 for Target Device

This is the interesting part. We will compile Qt 6 on a host machine and install it to the Raspberry Pi. The instruction for configuring and building are similar to building Qt 6 for host machine, but there are a few things we need to do to get the intended results.

Create a Toolchain File

First, we want to create our own CMake toolchain file. This file is important to link the right compilers and libraries so that CMake can produces the suitable Qt build for Raspberry Pi. For this guide, we will use the following CMake code and save it as

toolchain.cmake

.

cmake_minimum_required(VERSION 3.18)
include_guard(GLOBAL)

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

set(TARGET_SYSROOT /path/to/your/sysroot)
set(CMAKE_SYSROOT ${TARGET_SYSROOT})

set(ENV{PKG_CONFIG_PATH} $PKG_CONFIG_PATH:/usr/lib/aarch64-linux-gnu/pkgconfig)
set(ENV{PKG_CONFIG_LIBDIR} /usr/lib/pkgconfig:/usr/share/pkgconfig/:${TARGET_SYSROOT}/usr/lib/aarch64-linux-gnu/pkgconfig:${TARGET_SYSROOT}/usr/lib/pkgconfig)
set(ENV{PKG_CONFIG_SYSROOT_DIR} ${CMAKE_SYSROOT})

# if you use other version of gcc and g++ than gcc/g++ 9, you must change the following variables
set(CMAKE_C_COMPILER /usr/bin/aarch64-linux-gnu-gcc-9)
set(CMAKE_CXX_COMPILER /usr/bin/aarch64-linux-gnu-g++-9)

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${TARGET_SYSROOT}/usr/include")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}")

set(QT_COMPILER_FLAGS "-march=armv8-a")
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)
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
set(CMAKE_BUILD_RPATH ${TARGET_SYSROOT})


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()

set(XCB_PATH_VARIABLE ${TARGET_SYSROOT})

set(GL_INC_DIR ${TARGET_SYSROOT}/usr/include)
set(GL_LIB_DIR ${TARGET_SYSROOT}:${TARGET_SYSROOT}/usr/lib/aarch64-linux-gnu/:${TARGET_SYSROOT}/usr:${TARGET_SYSROOT}/usr/lib)

set(EGL_INCLUDE_DIR ${GL_INC_DIR})
set(EGL_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/aarch64-linux-gnu/libEGL.so)

set(OPENGL_INCLUDE_DIR ${GL_INC_DIR})
set(OPENGL_opengl_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/aarch64-linux-gnu/libOpenGL.so)

set(GLESv2_INCLUDE_DIR ${GL_INC_DIR})
set(GLIB_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/aarch64-linux-gnu/libGLESv2.so)

set(GLESv2_INCLUDE_DIR ${GL_INC_DIR})
set(GLESv2_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/aarch64-linux-gnu/libGLESv2.so)

set(gbm_INCLUDE_DIR ${GL_INC_DIR})
set(gbm_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/aarch64-linux-gnu/libgbm.so)

set(Libdrm_INCLUDE_DIR ${GL_INC_DIR})
set(Libdrm_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/aarch64-linux-gnu/libdrm.so)

set(XCB_XCB_INCLUDE_DIR ${GL_INC_DIR})
set(XCB_XCB_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/aarch64-linux-gnu/libxcb.so)

Now that we have our toolchain, as well as the cross compiler and sysroot, we can continue with configuring and building Qt for our Raspberry Pi! As decided in the planning process, we will put all build configuration files on

qtpi-build

, so let's

cd

into that directory:

$ cd ~/qtpi-build

Now, we can run the configuration command inside this directory.

$ ../qt5/configure -release -opengl es2 -nomake examples -nomake tests -qt-host-path $HOME/qt-host -extprefix $HOME/qt-raspi -prefix /usr/local/qt6 -- -DCMAKE_TOOLCHAIN_FILE=$HOME/toolchain.cmake -DQT_QMAKE_TARGET_MKSPEC=devices/linux-rasp-pi4-v3d-g++ -DQT_FEATURE_xcb=ON -DFEATURE_xcb_xlib=ON -DQT_FEATURE_xlib=ON

The command above will run the configuration file that came with the Qt source code and complementing it with the toolchain that we just created. It is equivalent to this CMake command:

$ cmake ../qt5/ -GNinja -DCMAKE_BUILD_TYPE=Release -DINPUT_opengl=es2 -DQT_BUILD_EXAMPLES=OFF -DQT_BUILD_TESTS=OFF -DQT_HOST_PATH=$HOME/qt-host -DCMAKE_STAGING_PREFIX=$HOME/qt-raspi -DCMAKE_INSTALL_PREFIX=/usr/local/qt6 -DCMAKE_TOOLCHAIN_FILE=$HOME/toolchain.cmake -DQT_QMAKE_TARGET_MKSPEC=devices/linux-rasp-pi4-v3d-g++ -DQT_FEATURE_xcb=ON -DFEATURE_xcb_xlib=ON -DQT_FEATURE_xlib=ON

Note: Both commands will configure Qt to be installed on

qt-raspi

folder on the host machine, with expectation that it will populate

usr/local/qt6

on the Raspberry Pi. The Qt installation from this build will be configured to run on the ARM64 architecture using X11 as the windowing system. If you want to use other windowing system such as Wayland or EGLFS, check this Qt Documentation. Then proceeds with building and installing.

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

After Qt is successfully installed on

~/qt-raspi

, we will then send this Qt installation back to the Raspberry Pi. To do this, we can use

rsync

as we did when building sysroot, or

scp

. Both will accomplish the same thing, and there should be no significant difference between both of them. To use

rsync

:

rsync -avz --rsync-path="sudo rsync" /path/to/qt-raspi/* <pi_username>@<pi_ip_address>:/usr/local/qt6

To use

scp

:

$ cd ~/qt-raspi
$ scp -r * <pi_username>@<pi_ip_address>:/usr/local/qt6

Final Configuration on Raspberry Pi

To ensure our Qt 6 installation can be run on Raspberry Pi, we need to setup a few environment variables. All of this can be done on the host device via SSH.

$ ssh pi@169.254.187.77

## enter your raspberry pi user password, if prompted

$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/qt6/lib/

This is the most important environment variable that we need to set, as it will enable Raspberry Pi to find our Qt 6 library path. If you intend to run the application through SSH from your host machine and see the application running on a monitor that is connected to the Raspberry Pi, you need to also set

DISPLAY

variable with

export DISPLAY=:0

in addition to the commands above.

Compiling an Application

Now we can compile a Qt application and get a binary that is suitable to deploy on Raspberry Pi.

We can try with one of the examples from the Qt source code.

$ cd ~/qt5/qtbase/examples/gui/analogclock/
$ ls ## make sure there is a CMakeLists.txt file
$ ~/qt-raspi/bin/qt-cmake CMakeLists.txt

### CMake is configuring here. After it is done without any error, we can proceed to build and install it

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

We now have the binary of our application ready to be run on the Raspberry Pi. We can then send the binary to our Raspberry Pi and run the application (either through SSH or directly on the Raspberry Pi with keyboard and mouse connected).

$ scp -r gui_analogclock <pi_username>@<pi_ip_address>:/home/pi
$ ssh <pi_username>@<pi_ip_address>
$ cd ~ ## or cd to the directory where you send the binary
$ ./gui_analogclock

Known Issues

Issue with libdbus

During testing, we found a compilation error related to

libdbus

.

...
/usr/lib/gcc-cross/aarch64-linux-gnu/9/../../../../aarch64-linux-gnu/bin/ld: /home/uraihan/rpi-sysroot/usr/lib/aarch64-linux-gnu/libdbus-1.a(libdbus_1_la-dbus-message.o): relocation R_AARCH64_ADR_PREL_PG_HI21 against symbol `dbus_message_unref' which may bind externally can not be used when making a shared object; recompile with -fPIC
...

This could happen when the libdbus inside the initial sysroot is not a dynamically shared objects. This is normally fixable by doing a clean reinstall of Raspberry Pi OS (reformat microSD card, then install Raspberry Pi OS again through the Imager). Another recommendation is to try to reinstall

libdbus-1-dev

package. But if the problem persists and you don't need dbus, you can disable the dbus feature by adding

-DFEATURE_dbus=OFF

flag in the CMake configuration command.

Further Readings