Wednesday, October 23, 2024

How to Cross-compile C++ and Qt 6 Apps on Raspberry Pi

The Raspberry Pi has become a favorite tool for hobbyists and developers alike, offering a versatile platform for tinkering and experimentation. For my recent project, I needed to develop a custom application for my Raspberry Pi 4. As a newcomer to the world of Raspberry Pi development, I opted for Qt 6, a powerful cross-platform framework, using C++ as my language of choice.

While Qt 6 boasts comprehensive documentation, my journey wasn't without its hurdles. Setting up the development environment for cross-compilation proved to be a challenge, requiring careful navigation through a series of dependencies and configurations. In this article, I'll share my experiences and provide a detailed roadmap to help you successfully build and deploy your own Qt 6 C++ apps on the Raspberry Pi.

Laying the Foundation: Host and Target System Setup

The key to successful cross-compilation lies in configuring both your host machine (the machine you'll use for development) and your target device (the Raspberry Pi). Think of this as building a bridge between two separate worlds.

Essential Ingredients:

  • Raspberry Pi 4: We'll be working with a Raspberry Pi 4, running the 64-bit version of Debian 11 (Bullseye).

  • Host Machine: I used Ubuntu 20.04 (Focal Fossa), a reliable and widely used Linux distribution.

  • Compatible Versions: The success of cross-compilation hinges on aligning the versions of essential libraries between the host and target systems. Here's the breakdown of the versions I used:

    • CMake: Ubuntu: 3.23.0

    • GCC: Ubuntu: 9.4.0, Raspberry: 10.2.1

    • GLIBC: Both Ubuntu and Raspberry: 2.31

    • GNU Binutils: Ubuntu: 2.34, Raspberry: 2.35.2

    • OpenSSL: Both Ubuntu and Raspberry: 1.1.1w (11 Sep 2023)

Setting Up the Raspberry Pi:

First, follow the official Raspberry Pi instructions to get your Pi up and running. Once you have a functional Raspberry Pi, you'll need to install the necessary packages for Qt development. This includes essential libraries and tools, ensuring that your Pi is ready to receive the cross-compiled application.

Building the Bridge: Configuring Your Host Machine

This is where the real work begins. The host machine is where we'll perform the compilation process, generating the code that will ultimately run on the Raspberry Pi.

1. A Firm Foundation: Installing Dependencies

Start by making sure your Ubuntu machine has the necessary dependencies to support Qt and cross-compilation. This includes compilers, build tools, and crucial libraries:

sudo apt update
sudo apt upgrade
sudo apt-get install build-essential libssl-dev make cmake build-essential libclang-dev clang 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
    

2. The Foundation of Compilation: Building CMake 3.23.0

Ubuntu 20.04 comes with CMake 3.16.3, but for successful Qt compilation, we need to upgrade to version 3.23.0. Download, compile, and install it manually using these steps:

cd /tmp
wget https://github.com/Kitware/CMake/releases/download/v3.23.0/cmake-3.23.0.tar.gz
tar -zxvf cmake-3.23.0.tar.gz
cd cmake-3.23.0
./bootstrap
make
sudo make install
    

Verify your CMake version using the command cmake --version. It should now display 3.23.0.

3. Cross-Compiler Power: Building Qt for the Raspberry Pi

To compile for the Raspberry Pi, we need a toolchain that understands its architecture. This is where cross-compilers come in.

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

4. Building Qt for Host and Target:



With the necessary components in place, we can begin compiling Qt itself. First, we'll build Qt for the host machine to provide a consistent development environment:

cd ~
mkdir qt-host qt-raspi qt-hostbuild qtpi-build
git clone https://code.qt.io/qt/qt5.git qt6
cd qt6
git switch 6.5.3
    

Since Qt 6.5.3 is the minimum version I used for my project, this step ensures compatibility.

You can initialize the entire Qt repository using perl init-repository -f, but to save disk space, select only the necessary submodules. This typically includes qtbase, qtsharertools, and qtdeclarative.

      perl init-repository --module-subset=qtbase, qtsharertools, qtdeclarative
    

Now, build Qt for the host machine:

cd $HOME/qt-hostbuild
cmake ../qt6/ -GNinja -DCMAKE_BUILD_TYPE=Release -DQT_BUILD_EXAMPLES=OFF -DQT_BUILD_TESTS=OFF -DCMAKE_INSTALL_PREFIX=$HOME/qt-host
cmake --build . --parallel 8
cmake --install .
    

5. Bringing the Target System In: Building Qt for the Raspberry Pi

We need to create a representation of the Raspberry Pi's file system on our host machine. This is called a sysroot.

cd $HOME
mkdir rpi-sysroot rpi-sysroot/usr rpi-sysroot/opt
rsync -avzS --rsync-path="rsync" --delete <pi_username>@<pi_ip_address>:/lib/* rpi-sysroot/lib
mkdir $HOME/rpi-sysroot/usr
rsync -avzS --rsync-path="rsync" --delete <pi_username>@<pi_ip_address>:/usr/include/* rpi-sysroot/usr/include
rsync -avzS --rsync-path="rsync" --delete <pi_username>@<pi_ip_address>:/usr/lib/* rpi-sysroot/usr/lib
mkdir $HOME/rpi-sysroot/opt
rsync -avzS --rsync-path="rsync" --delete <pi_username>@<pi_ip_address>:/opt/vc rpi-sysroot/opt/vc
    

  • pi_username: Your Raspberry Pi's username.

  • pi_ip_address: The IP address of your Raspberry Pi.

6. Symbolic Links and a Custom Toolchain

During the syncing process, symbolic links might break. We need to fix these links:

wget https://raw.githubusercontent.com/riscv/riscv-poky/master/scripts/sysroot-relativelinks.py
chmod +x sysroot-relativelinks.py 
python3 sysroot-relativelinks.py $HOME/rpi-sysroot
    
Next, create a toolchain file (toolchain.cmake) that tells CMake how to build for the Raspberry Pi.
cd ~
nano toolchain.cmake
    

Paste the following script into the toolchain.cmake file:

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

7. Building Qt for the Raspberry Pi: The Final Step

Now, we can build Qt for the Raspberry Pi using this toolchain file:

cd ~/qtpi-build
cmake ../qt6/ -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-aarch64 \
-DQT_FEATURE_xcb=ON -DFEATURE_xcb_xlib=ON -DQT_FEATURE_xlib=ON

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

8. Sending the Bridge Across: Deploying to the Raspberry Pi

We've built the Qt for the Raspberry Pi, now let's transfer it:

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

Connecting the Dots: Configuring the Raspberry Pi for Qt 6

Now, we need to tell the Raspberry Pi where to find the newly installed Qt libraries:

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

If you're using a monitor to view the application on the Raspberry Pi, the DISPLAY variable is essential. Sometimes, the export command might not work; in those cases, you can edit the .bashrc file in your user's home directory on the Raspberry Pi to include the variables.

Building and Running Your Qt Project: Bringing It All Together

Finally, we're ready to compile and run your Qt project on the Raspberry Pi:

  1. Clone your project onto your host machine. Make sure it has a CMakeLists.txt file.

  2. Configure your project with the cross-compiled Qt libraries.

cd ~/your_project 
../qt-raspi/bin/qt-cmake CMakeLists.txt
cmake --build . --parallel 4
cmake --install .
    

  1. Transfer the compiled application to the Raspberry Pi:

      scp -r app_name <pi_username>@<pi_ip_address>:/home/pi
    
  1. Connect to the Raspberry Pi via SSH:

      ssh <pi_username>@<pi_ip_address>
    
  1. Navigate to the directory where you copied the application and run it:

cd home/pi
./app_name
    

And there you have it! You've successfully built and deployed your first Qt 6 C++ application on the Raspberry Pi.

This process might seem complex, but it's a journey of discovery and empowerment. By understanding the intricacies of cross-compilation, you gain the ability to bring your ideas to life on this versatile platform.

0 comments:

Post a Comment