Search by Tags

Torizon Qt Creator

 

Article updated at 31 Mar 2021
Compare with Revision




Subscribe for this article updates


Remember that you can always refer to the Torizon Documentation, there you can find a lot of relevant articles that might help you in the application development.

Introduction

This article shows a way how you can use Qt Creator with Torizon. However, please note that this is more a proof of concept than an official howto. It may happen that because of version changes some parts may work slightly differently than written in this article.

Attention: Please note that this article is a currently unsupported proof of concept and not a 1:1 howto. We are collecting customer feedback, if you feel this is a good idea, or you need to use Qt Creator with Torizon, please contact us.

This article complies to the Typographic Conventions for Torizon Documentation.

Prerequisites

To have a better understanding of this article, the following prerequisites are recommended:

Idea

We use a toolchain installed in a container image to develop applications with the Qt Creator. To speed up compilation we use the feature of Debian multiarch. We don't do the native compilation for arm with qemu but instead have two separate containers one for development (with multiarch) and one which runs on the target. This looks as follows:


  • Qt Creator Torizon Containers

The Qt Creator doesn't invoke qmake, gcc, and make directly but uses some wrapper scripts to run a container instead. The container needs access to the qt project on the host system which can be done with a bind mount. However, it will link against the libraries in the development container. On the target, it will then use the same libraries but this time the whole container image is made for ARM. The target container however will not include the header files and compilers which would unnecessarily increase the size of the image.

Wrapper scripts

The idea is to wrap docker calls for Qt Creator so that they look like normal application calls. All wrapper and Dockerfiles can be downloaded from Qt Creator Docker. Here a step by step guide on how you can create them manually with some explanation.

Create a directory containing all these wrapper scripts:

$ mkdir ~/bin-torizon

Create an environment file:

environment
# Sysroot where the rootfs of the development container is exported
QT_SYSROOT="/home/stefan/projects/torizon/qt-dev"
# The name of the docker image for developement
export DOCKER_IMAGE=qt5-dev
# The project directory which is mounted into the Docker container. Can also be more than just the project directory (for example whole home).
export PROJECT_DIR=$HOME
# Which qmake should be used inside the container (arch specific!)
QMAKE="/usr/bin/arm-linux-gnueabihf-qmake"

Create a helper file:

helper
bin_dir=$(dirname $0)
. $bin_dir/environment
 
docker_run() {
    environment=$(tempfile)
    env | grep -v PATH >> $environment
    # Hack for iMX8 devices
    echo ACCEPT_FSL_EULA=1 >> $environment
 
    # Run the command in a container
    docker run --rm --env-file $environment -v $PROJECT_DIR:$PROJECT_DIR $DOCKER_IMAGE /bin/bash -c "$@"
    rm $environment
}

Create a wrapper file for make:

make
#!/bin/bash
 
bin_dir=$(dirname $0)
. $bin_dir/helper
 
ARGS="$@"
 
# Run make inside the container
docker_run "cd $PWD && make $ARGS"

Create a wrapper file for gcc:

gcc
#!/bin/bash
 
bin_dir=$(dirname $0)
. $bin_dir/helper
 
ARGS="$@"
 
# Prepend sysroot to all directories under /usr... Qt uses this for syntax check with libclang
docker_run "cd $PWD && \${CROSS_COMPILE}gcc $ARGS" 2> >(sed -e "s+/usr/+$QT_SYSROOT/usr/+g" >&2)

Create a wrapper file for g++:

g++
#!/bin/bash
 
bin_dir=$(dirname $0)
. $bin_dir/helper
 
ARGS="$@"
 
# Prepend sysroot to all directories under /usr... Qt uses this for syntax check with libclang
docker_run "cd $PWD && \${CROSS_COMPILE}g++ $ARGS" 2> >(sed -e "s+/usr/+$QT_SYSROOT/usr/+g" >&2)

Create a wrapper file for qmake:

qmake
#!/bin/bash
 
bin_dir=$(dirname $0)
. $bin_dir/helper
 
ARGS="$@"
 
# Fix the paths if you run qmake -query. This is done by qtcreator for finding the include and lib directories which are then used by libclang for syntax check.
# Also fix the QMAKE_OBJCOPY which is used by qmake when run from Qt Creator
if [ "$1" == "-query" ]; then
  for line in $(docker_run "cd $PWD && echo QMAKE_OBJCOPY=arm-linux-gnueabihf-objcopy >> /usr/lib/arm-linux-gnueabihf/qt5/mkspecs/linux-g++/qmake.conf && $QMAKE $ARGS" 2>/dev/null); do
    line=$(echo $line | sed -e "s+QT_HOST_BINS:.*+QT_HOST_BINS:$bin_dir+g")
    line=$(echo $line | sed -e "s+QT_HOST_DATA:+QT_HOST_DATA:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_HOST_LIBS:+QT_HOST_LIBS:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_HOST_PREFIX:+QT_HOST_PREFIX:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_INSTALL_ARCHDATA:+QT_INSTALL_ARCHDATA:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_INSTALL_BINS:+QT_INSTALL_BINS:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_INSTALL_CONFIGURATION:+QT_INSTALL_CONFIGURATION:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_INSTALL_DATA:+QT_INSTALL_DATA:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_INSTALL_DEMOS:+QT_INSTALL_DEMOS:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_INSTALL_DOCS:+QT_INSTALL_DOCS:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_INSTALL_EXAMPLES:+QT_INSTALL_EXAMPLES:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_INSTALL_HEADERS:+QT_INSTALL_HEADERS:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_INSTALL_IMPORTS:+QT_INSTALL_IMPORTS:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_INSTALL_LIBEXECS:+QT_INSTALL_LIBEXECS:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_INSTALL_LIBS:+QT_INSTALL_LIBS:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_INSTALL_PLUGINS:+QT_INSTALL_PLUGINS:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_INSTALL_PREFIX:+QT_INSTALL_PREFIX:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_INSTALL_QML:+QT_INSTALL_QML:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_INSTALL_TESTS:+QT_INSTALL_TESTS:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_INSTALL_TRANSLATIONS:+QT_INSTALL_TRANSLATIONS:$QT_SYSROOT+g")
    line=$(echo $line | sed -e "s+QT_SYSROOT:+QT_SYSROOT:$QT_SYSROOT+g")
    echo $line
  done
else
  docker_run "cd $PWD && echo QMAKE_OBJCOPY=arm-linux-gnueabihf-objcopy >> /usr/lib/arm-linux-gnueabihf/qt5/mkspecs/linux-g++/qmake.conf && $QMAKE $ARGS"
fi

Create a wrapper for qmlcachegen:

qmlcachegen
#!/bin/bash
 
bin_dir=$(dirname $0)
. $bin_dir/helper
 
ARGS="$@"
 
# Run qmlcachgen inside the container
docker_run "/usr/lib/qt5/bin/qmlcachegen $ARGS"

Create a wrapper for qmlscene:

qmlscene
#!/bin/bash
 
bin_dir=$(dirname $0)
. $bin_dir/helper
 
ARGS="$@"
 
# Run qmlcachgen inside the container
docker_run "/usr/lib/qt5/bin/qmlscene $ARGS"

Create a wrapper for rcc:

rcc
#!/bin/bash
 
bin_dir=$(dirname $0)
. $bin_dir/helper
 
ARGS="$@"
 
# Run rcc inside the container
docker_run "/usr/lib/qt5/bin/rcc $ARGS" > >(tee -a /tmp/rcc-output) 2> >(tee -a /tmp/rcc-stderr >&2)

Create a script which allows to export the rootfs of a container image:

docker-export-image
#!/bin/bash
 
usage() {
  echo "docker-export-image <path to export dir>"
  echo
  echo "Will export the rootfs of the development container to the export directory"
}
 
bin_dir=$(dirname $0)
. $bin_dir/environment
 
test "$1" == "" && { usage; exit 1; }
test -d $1 || { usage; exit 1; }
 
container_name=dev$(echo $RANDOM)
 
# Run a container just to allow exporting
docker run --name=$container_name --rm -d -ti -e ACCEPT_FSL_EULA=1 -v $PROJECT_DIR:$PROJECT_DIR $DOCKER_IMAGE /bin/bash
# Export container
docker export $container_name | tar -x -C $1
# Stop exported container again
docker stop $container_name

Dockerfiles

The idea is to have two different containers. One container runs on the host machine and allows to cross compile qt applications while the other runs on the target machine and will allow debugging. This means normally that the cross compile container is a native x86/amd64 container while the other is a armhf or arm64 container.

Development Container

Dockerfile.dev
ARG ARCH_ARG=armhf
FROM torizon/debian-cross-toolchain-$ARCH_ARG
# We also need the ARCH_ARG in the first build stage
ARG ARCH_ARG=armhf
ARG USER_ID_ARG=1000
 
# Remove Toradex feeds for now because of broken qt5 dependencies on iMX8
RUN sed -i 's/\(.*\)feeds.toradex.com\(.*\)/# \1feeds.toradex.com\2/g' /etc/apt/sources.list
 
# Install required packages
RUN apt-get -q -y update \
    && apt-get -q -y install \
    qt5-qmake:${ARCH_ARG} \
    && apt-get clean \
    && apt-get autoremove \
    && rm -rf /var/lib/apt/lists/*
 
# Install almost all available qt packages. This makes the development container a more general purpose.
RUN apt-get -q -y update \
    && apt-get -q -y install \
    libaccounts-qt5-dev:${ARCH_ARG} \
    libdbusextended-qt5-dev:${ARCH_ARG} \
    libdbusmenu-qt5-dev:${ARCH_ARG} \
    libfcitx-qt5-dev:${ARCH_ARG} \
    libgrantlee5-dev:${ARCH_ARG} \
    libgwengui-qt5-dev:${ARCH_ARG} \
    libjreen-qt5-dev:${ARCH_ARG} \
    liblastfm5-dev:${ARCH_ARG} \
    libmpris-qt5-dev:${ARCH_ARG} \
    libodsstream-qt5-dev:${ARCH_ARG} \
    libpackagekitqt5-dev:${ARCH_ARG} \
    libphonon4qt5-dev:${ARCH_ARG} \
    libpoppler-qt5-dev:${ARCH_ARG} \
    libqaccessibilityclient-qt5-dev:${ARCH_ARG} \
    libqjdns-qt5-dev:${ARCH_ARG} \
    libqscintilla2-qt5-dev:${ARCH_ARG} \
    libqt5charts5-dev:${ARCH_ARG} \
    libqt5datavisualization5-dev:${ARCH_ARG} \
    libqt5gamepad5-dev:${ARCH_ARG} \
    libqt5networkauth5-dev:${ARCH_ARG} \
    libqt5opengl5-dev:${ARCH_ARG} \
    libqt5sensors5-dev:${ARCH_ARG} \
    libqt5serialbus5-dev:${ARCH_ARG} \
    libqt5serialport5-dev:${ARCH_ARG} \
    libqt5svg5-dev:${ARCH_ARG} \
    libqt5texttospeech5-dev:${ARCH_ARG} \
    libqt5waylandclient5-dev:${ARCH_ARG} \
    libqt5waylandcompositor5-dev:${ARCH_ARG} \
    libqt5webchannel5-dev:${ARCH_ARG} \
    libqt5webkit5-dev:${ARCH_ARG} \
    libqt5websockets5-dev:${ARCH_ARG} \
    libqt5webview5-dev:${ARCH_ARG} \
    libqt5x11extras5-dev:${ARCH_ARG} \
    libqt5xmlpatterns5-dev:${ARCH_ARG} \
    libqtspell-qt5-dev:${ARCH_ARG} \
    libquazip5-dev:${ARCH_ARG} \
    libqwt-qt5-dev:${ARCH_ARG} \
    libqwtmathml-qt5-dev:${ARCH_ARG} \
    libsignon-qt5-dev:${ARCH_ARG} \
    libtelepathy-qt5-dev:${ARCH_ARG} \
    qt3d5-dev:${ARCH_ARG} \
    qt3d5-dev-tools:${ARCH_ARG} \
    qtbase5-dev:${ARCH_ARG} \
    qtbase5-dev-tools:${ARCH_ARG} \
    qtconnectivity5-dev:${ARCH_ARG} \
    qtdeclarative5-dev:${ARCH_ARG} \
    qtdeclarative5-dev-tools:${ARCH_ARG} \
    qtlocation5-dev:${ARCH_ARG} \
    qtmultimedia5-dev:${ARCH_ARG} \
    qtpositioning5-dev:${ARCH_ARG} \
    qtquickcontrols2-5-dev:${ARCH_ARG} \
    qtscript5-dev:${ARCH_ARG} \
    qttools5-dev:${ARCH_ARG} \
    qttools5-dev-tools:${ARCH_ARG} \
    qtwayland5-dev-tools:${ARCH_ARG} \
    qtwebengine5-dev:${ARCH_ARG} \
    qtwebengine5-dev-tools:${ARCH_ARG} \
    qtxmlpatterns5-dev-tools:${ARCH_ARG} \
    \
    qtbase5-dev:${ARCH_ARG} \
    qtwayland5:${ARCH_ARG} \
    libqt5websockets5-dev:${ARCH_ARG} \
    qtdeclarative5-dev:${ARCH_ARG} \
    qtdeclarative5-dev:${ARCH_ARG} \
    qt3d5-dev:${ARCH_ARG} \
    libqt5serialport5-dev:${ARCH_ARG} \
    qtquickcontrols2-5-dev:${ARCH_ARG} \
    qml-module-qtquick2:${ARCH_ARG} \
    qml-module-qtquick-controls2:${ARCH_ARG} \
    qml-module-qt-websockets:${ARCH_ARG} \
    qml-module-qt3d:${ARCH_ARG} \
    qml-module-qtaudioengine:${ARCH_ARG} \
    qml-module-qtav:${ARCH_ARG} \
    qml-module-qtbluetooth:${ARCH_ARG} \
    qml-module-qtcharts:${ARCH_ARG} \
    qml-module-qtdatavisualization:${ARCH_ARG} \
    qml-module-qtgraphicaleffects:${ARCH_ARG} \
    qml-module-qtgstreamer:${ARCH_ARG} \
    qml-module-qtlocation:${ARCH_ARG} \
    qml-module-qtmultimedia:${ARCH_ARG} \
    qml-module-qtnfc:${ARCH_ARG} \
    qml-module-qtpositioning:${ARCH_ARG} \
    qml-module-qtqml-models2:${ARCH_ARG} \
    qml-module-qtqml-statemachine:${ARCH_ARG} \
    qml-module-qtquick-controls:${ARCH_ARG} \
    qml-module-qtquick-controls2:${ARCH_ARG} \
    qml-module-qtquick-dialogs:${ARCH_ARG} \
    qml-module-qtquick-extras:${ARCH_ARG} \
    qml-module-qtquick-layouts:${ARCH_ARG} \
    qml-module-qtquick-localstorage:${ARCH_ARG} \
    qml-module-qtquick-particles2:${ARCH_ARG} \
    qml-module-qtquick-privatewidgets:${ARCH_ARG} \
    qml-module-qtquick-scene2d:${ARCH_ARG} \
    qml-module-qtquick-scene3d:${ARCH_ARG} \
    qml-module-qtquick-shapes:${ARCH_ARG} \
    qml-module-qtquick-templates2:${ARCH_ARG} \
    qml-module-qtquick-virtualkeyboard:${ARCH_ARG} \
    qml-module-qtquick-window2:${ARCH_ARG} \
    qml-module-qtquick-xmllistmodel:${ARCH_ARG} \
    qml-module-qtquick2:${ARCH_ARG} \
    qml-module-qtsensors:${ARCH_ARG} \
    qml-module-qttest:${ARCH_ARG} \
    qml-module-qtwayland-compositor:${ARCH_ARG} \
    qml-module-qtwebchannel:${ARCH_ARG} \
    qml-module-qtwebengine:${ARCH_ARG} \
    qml-module-qtwebkit:${ARCH_ARG} \
    qml-module-qtwebsockets:${ARCH_ARG} \
    qml-module-qtwebview:${ARCH_ARG} \
    \
    qmlscene:${ARCH_ARG} \
    && apt-get clean && apt-get autoremove && rm -rf /var/lib/apt/lists/*
 
# Overwrite qt_lib_gui by setting opengles2 instead of opengl flags
COPY qt_lib_gui.pri /usr/lib/aarch64-linux-gnu/qt5/mkspecs/modules/qt_lib_gui.pri
 
# Make sure that the right objcopy will be called
RUN cross_arg=$(echo ${CROSS_COMPILE} | sed 's/-$//g') \
    && echo QMAKE_OBJCOPY=${CROSS_COMPILE}objcopy >> /usr/lib/$cross_arg/qt5/mkspecs/linux-g++/qmake.conf
 
# Remove torizon and replace it with a build user with the same ID as the host user
RUN userdel torizon
# Add a group which has the same gid as the customers uid
RUN groupadd -g ${USER_ID_ARG} build
# Add a user with the uid from the args
RUN useradd -u ${USER_ID_ARG} -p "$1$Yx4IlJWd$8YS1kb37eyHMbIBoO12Br." -g build build
 
USER build

This container contains all tools to cross compile as well as the development files for the target architecture.

Target Container

Dockerfile
ARG BASE_NAME=arm32v7-debian-qt5-wayland
FROM torizon/$BASE_NAME:bullseye
 
SHELL ["/bin/bash", "-c"]
 
RUN apt-get -y update && apt-get install -y --no-install-recommends \
    qml-module-qtquick-controls2 \
    qml-module-qt-websockets \
    qml-module-qt3d \
    qml-module-qtaudioengine \
    qml-module-qtav \
    qml-module-qtbluetooth \
    qml-module-qtcharts \
    qml-module-qtdatavisualization \
    qml-module-qtgraphicaleffects \
    qml-module-qtgstreamer \
    qml-module-qtlocation \
    qml-module-qtmultimedia \
    qml-module-qtnfc \
    qml-module-qtpositioning \
    qml-module-qtqml-statemachine \
    qml-module-qtquick-controls \
    qml-module-qtquick-controls2 \
    qml-module-qtquick-dialogs \
    qml-module-qtquick-extras \
    qml-module-qtquick-scene2d \
    qml-module-qtquick-scene3d \
    qml-module-qtquick-shapes \
    qml-module-qtquick-templates2 \
    qml-module-qtquick-virtualkeyboard \
    qml-module-qtsensors \
    qml-module-qtwayland-compositor \
    qml-module-qtwebchannel \
    qml-module-qtwebengine \
    qml-module-qtwebkit \
    qml-module-qtwebsockets \
    qml-module-qtwebview \
    qt5-qmltooling-plugins \
    openssh-server \
    gdbserver \
    rsync \
    && apt-get clean && apt-get autoremove && rm -rf /var/lib/apt/lists/*
 
# Allow empty password hack (only for developement)
# sed -i /etc/ssh/sshd_config -e 's/UsePAM yes/UsePAM no/g'
RUN sed -i 's/nullok_secure/nullok/g' /etc/pam.d/common-auth && \
    echo "PermitEmptyPasswords yes" >> /etc/ssh/sshd_config && \
    echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \
    sed -i /etc/shadow -e 's/root:.*/root::18254:0:99999:7:::/g'
 
RUN test $(arch) == "aarch64" && { cd /usr/lib/aarch64-linux-gnu/ && ln -s libGL.so.1.7.0 libGL.so.1.2; } || true
 
COPY entrypoint.sh /entrypoint.sh
 
RUN echo "set -o allexport; . /etc/environment; set +o allexport;" >> /etc/profile
 
ENTRYPOINT ["/entrypoint.sh"]

Note: Please note we recommend having an additional container for production which includes only the bare minimum. Remove all unnecessary modules and also gdbserver, sshd and rscync. The Dockerfile above should only be used during development.

The entrypoint is very simple. It just spawns sshd and then waits for other commands on bash. For debugging GUI applications it is expected to run Weston. See the section "Start the Qt Container on the Target" for how you start the container for debugging.

entrypoint.sh
#!/bin/bash
 
mkdir /run/sshd && /usr/sbin/sshd&
/bin/bash $@

Building the container

Before you proceed, please make sure to have followed the steps presented at Configure Build Environment for Torizon Containers.

Based on the Dockerfile we now need to create the container. This is done by calling docker build:

$ docker build -t qt5-dev -f Dockerfile.dev .
$ docker build -t qt5-target -f Dockerfile .
$ docker build -t qt5-dev --build-arg USER_ID_ARG=$(id -u) --build-arg ARCH_ARG=arm64 -f Dockerfile.dev .
$ docker build -t qt5-target -build-arg BASE_NAME=arm64v8-debian-qt5-wayland-vivant -f Dockerfile .

You can then transfer the target container to the module with the following command:

$ docker save qt5-target | ssh torizon@<IP> docker load

Filesystem Export

Qt Creator needs to see the actual filesystem of the container so that it can index the header files. Therefore, it is necessary to export the content of the development container to a directory. The docker-export-image script allows to do that.

$ docker-export-image qt5-dev /home/<user>/torizon/qt-dev

Start the Qt Container on the Target

We transferred the qt container to the target and now need to start it. This can be done with docker run:

# docker run -ti --rm --name=qt-debug --cap-add CAP_SYS_TTY_CONFIG \
             -v /dev:/dev -v /tmp:/tmp -v /run/udev/:/run/udev/ \
             -p 2222:22 -p 10000-10010:10000-10010 \
             --device-cgroup-rule='c 4:* rmw' --device-cgroup-rule='c 13:* rmw'  --device-cgroup-rule='c 226:* rmw'\
              qt-target
# docker run -e ACCEPT_FSL_EULA=1 -p 2222:22 -p 10000-10010:10000-10010 --rm  \
              -it --name=qt5 -v /tmp:/tmp -v /dev/dri:/dev/dri -v /dev/galcore:/dev/galcore \
               --device-cgroup-rule='c 199:* rmw' --device-cgroup-rule='c 226:* rmw' qt5-target

The entrypoint script will automatically start ssh inside the container and then spawns a bash console. Inside the container, you need to start weston if you want to write a graphical application. You can launch weston in a separate container as shown in the Debian Containers for Torizon article. Now you are ready to start debugging with the Qt Creator.

Setup Qt Creator

In Qt Creator we need to create a new kit which allows us to use the wrapped qmake.

First you need to install gdb-multiarch as debugger. On Debian based distributions you can do:

$ sudo apt get install gdb-multiarch

Then you need to start Qt Creator. Open Tools/Options and select Kits. Under Qt Version click on "Add" and select the qmake wrapper created in the section above:


  • Qt Versions

Under Debuggers you can add the previously installed gdb-multiarch from the host system:


  • Debuggers

Under Compilers you can add the wrapped gcc and g++:


  • Compilers

Under Devices press Add and choose Generic Linux Device. Make sure you change the ssh port to 2222 in the end and to only define 10 ports for gdb:


  • Devices

After that, you can create a Kit. Select your defined Qt Version, gdb-multiarch, gcc and g++ as well as the Device:


  • Kits

Start Development

Create a new project as usual. Select "Qt Torizon" when you are asked about the kit to use. You should be able to compile and debug your Qt Application as it would be a normal embedded Qt Linux project. See the Qt Creator manual for reference.

References