Docker Builds from QtCreator

On your development PC, you simply hit Ctrl+R (Run) in QtCreator to build and run your Qt application. When you want to run the application on an embedded system, you must perform four tasks:

  • You cross-build the Qt application for the target embedded system in a Docker container.
  • You stop the application on the target system.
  • You copy the application from the development PC to the target system with scp.
  • You start the application on the target system.

Wouldn’t you love to hit Ctrl+R in QtCreator and to have QtCreator perform the above four steps for you? Of course, you would! I’ll show you how in this post. Running an application on an embedded system will be the same as running the application on a PC.

Setup

Industrial terminals (a.k.a. display computers) run applications to monitor and control machines. They are often powered by an Intel or AMD processor with x86_64 architecture. Ubuntu is a fairly natural choice for the operating system. The Ubuntu desktop UI may be disabled, as the terminal only runs the main application and may be one or two auxiliary applications.

Setting up a development environment for industrial terminals is pretty straightforward. Your development PC runs some version of Ubuntu (for me: Ubuntu 16.04). The target system runs on another Ubuntu version (for me: Ubuntu 18.04). The cross-build environment in the Docker container is nothing more than a Ubuntu 18.04 environment with all the packages installed to build the Qt libraries and the application.

You don’t even need a custom-made terminal. You only need two Linux PCs that are connected over (W)LAN and that can communicate over OpenSSH with each other.

If the target system runs on an ARM SoC, setting up the cross-build environment in the Docker container will be a bit more complicated. You must build a Qt SDK (e.g., by building the Yocto target meta-toolchain-qt5) and install the Qt SDK in the Docker container. The container hides the cross-build environment from QtCreator. QtCreator doesn’t know whether the container calls a cross-compiler for ARM targets or a native compiler for Intel targets.

If you want to follow along on the sample project or on your own project, the following prerequisites must be in place.

You install Docker on your development PC as described here.

You create a working directory (for me: /public/Work) on your development PC. You clone the repository with the sample project or your project into the working directory.

$ cd /public/Work
$ git clone https
://github.com/bstubert/qtcreator-with-docker.git
$ cd qtcreator
-with-docker

The project directory contains a Dockerfile. Set WORKDIR in the last line to your working directory. This is essential as you’ll see in the next sections. For me, the last line looks like this:

WORKDIR /public/Work

Then you follow this description to build a Docker image qt-ubuntu-18.04-ryzen and use this image to build relocatable Qt libraries (Qt 5.14 or newer). The Qt build gives you a tarball qt-5.14.1-ubuntu-18.04-ryzen.tgz, which you unpack in the working directory.

$ cd /public/Work
$ tar xf
/path/to/qt-5.14.1-ubuntu-18.04-ryzen.tgz

Unpacking installs the Qt libraries into the directory /public/Work/qt-5.14.1.

You have a working OpenSSH connection between your PC and the target system. Authentication by password works fine.

Docker Wrapper for CMake

When you build and install a Qt application, QtCreator calls CMake

  • to generate the Makefiles. It calls
    cmake /public/Work/qtcreator-with-docker '-GCodeBlocks - Unix Makefiles <more options>'
    in a temporary directory like /tmp/QtCreator-jZQYdh/qtc-cmake-caIYzSxO.
  • to compile and link the Qt application. It calls
    cmake --build . --target all
    in a build directory like /public/Work/build-RelocatableQt-Qt_5_14_1-Debug.
  • to install the Qt application and its auxiliary files. It calls
    cmake --build . --target install
    in the build directory.

The idea is to call CMake inside the Docker container. Before QtCreator calls CMake, it changes to a specific directory on the development or host PC. This directory must be passed to the container, such that CMake can be called in the right location inside the container. The CMake wrapper script dr-cmake does this.

#!/bin/bash
docker run
--rm -v /public/Work:/public/Work -v/tmp:/tmp -w $(pwd) qt-ubuntu-18.04-ryzen cmake $@

The options -v /public/Work:/public/Work and -v/tmp:/tmp mirror the working and temporary directory tree from the host to the container. The option -w $(pwd) passes the host directory, where QtCreator calls CMake, as the current working directory to the Docker container. Docker runs cmake with the arguments $@ passed to the script dr-cmake.

The script dr-cmake translates QtCreator’s actions on the host PC into the same actions in the Docker container. This translation only works, because the host PC and the Docker container have the same directory structure.

Copy the wrapper script dr-cmake to a directory contained in $PATH (e.g. ~/bin) and make sure that the script is executable.

SSH Access to the Target System

QtCreator uses OpenSSH to copy files from the development PC to the target system. So, OpenSSH must be installed both on your PC and on the target. QtCreator doesn’t work with Dropbear, a lightweight OpenSSH alternative for embedded systems.

For ssh login, QtCreator offers password authentication and public-key authentication. Password authentication requires you to enter the password whenever you deploy the application to the target system. This quickly becomes tedious. So, you want to use public-key authentication.

You create private and public keys on your PC with ssh-keygen.

$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/burkhard/.ssh/id_rsa): /home/burkhard/.ssh/touch21-id_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/burkhard/.ssh/touch21-id_rsa.
Your public key has been saved in /home/burkhard/.ssh/touch21-id_rsa.pub.
...

You leave the passphrase empty by pressing Return. You make the SSH agent aware of the new key.

$ ssh-add ~/.ssh/touch21-id_rsa

You copy the public key to the target system.

$ scp ~/.ssh/touch21-id_rsa.pub benutzer@192.168.1.82:/home/benutzer/.ssh

You replace benutzer with your user name on the target system and 192.168.1.82 with the IP address of your target system.

You log in the target system and add the public key to the file ~/.ssh/authorized_keys.

On host:
$ ssh benutzer@192
.168.1.82
=> Enter your password

On target:
# cat ~/.ssh/touch21-id_rsa.pub > ~/.ssh/authorized_keys

The next time you log in the target system you don’t need to enter your password any more. SSH checks whether the user logging in has the private key of one of the public keys stored in ~/.ssh/authorized_keys. QtCreator will use the same mechanism to deploy the application files.

Creating the Docker Qt Kit

You need to define a kit that uses the dr-cmake script instead of cmake, that knows how to log in the target system with SSH and that deploys the application files and the Qt libraries to the target system.

Setup: CMake

Open the dialog Tools > Options > Kits > CMake and press the Add button. Fill out the fields as shown in the screenshot and press the Apply button.

Setup: Qt Version

Go to the sibling dialog Tools > Options > Kits > Qt Versions and press the Add button. Navigate to the QMake binary of the Qt version that you installed in your working directory. My QMake is located at /public/Work/qt-5.14.1/bin/qmake. Prefix the Version name with Docker such that this Qt version is easy to recognise. Press the Apply button to save the configuration.

Setup: Device

Go to the dialog Tools > Options > Devices to add the SSH login information. On the Devices tab, press the Add button, select Generic Linux Device in the separate pop-up dialog and press the Start Wizard button. For me, the first wizard page looks like this:

You will have different values for the name, IP address and user name. Press the Next button to move to the second wizard page. You browse to the private SSH key you created earlier.

As you have deployed the public key already, you move on to the third and last wizard page by pressing the Next button. The last page looks like this.

Press the Finish button. The dialog for a successful connectivity tests looks like this.

The fully filled-out form for the new device Touch21 looks similar to this.

Setup: Kit

You have defined all the pieces – CMake, Qt Version and Device – to configure a Kit. Head back to the dialog Tools > Options > Kits > Kits and press the Add button. Fill out the form as shown in the next screenshot.

Press the Change button for CMake Configuration at the bottom and replace the contents of the dialog by the following three lines.

CMAKE_CXX_COMPILER:STRING=%{Compiler:Executable:Cxx}
CMAKE_C_COMPILER
:STRING=%{Compiler:Executable:C}
CMAKE_PREFIX_PATH
:STRING=/public/Work/qt-5.14.1

Don’t forget to replace my working directory /public/Work with yours. Here is how the dialog should look before you press the OK button.

Configuring the Project

If you open the project file /public/Work/qtcreator-with-docker/CMakeLists.txt for the first time, QtCreator asks you to configure the project (see next screenshot). Select the kit Docker Qt 5.14.1 you just created and press the Configure Project button.

If you opened the project with another configuration before, you click on the entry Docker Qt 5.14.1 in the list below Build & Run and on the sub-entry Build.

In both cases, you’ll see the the typical CMake messages in the output pane General Messages when generating the Makefiles for the very first time. Have a look at the first line: QtCreator calls the script dr-cmake instead of cmake. The Docker container blocks the socket connection that the CMake option -E server tries to establish. This causes the error messages QLocalSocket::connectToServer: Invalid name. The CMake call works nevertheless.

Running "/home/burkhard/bin/dr-cmake -E server --pipe=/tmp/cmake-.JRorEM/socket --experimental" 
in /tmp/QtCreator-jZQYdh/qtc-cmake-cbJJnSrf.
QLocalSocket::connectToServer: Invalid name
...
Starting to parse CMake project, using:
"-DCMAKE_BUILD_TYPE:STRING=Debug",
"-DCMAKE_CXX_COMPILER:STRING=/usr/bin/g++",
"-DCMAKE_C_COMPILER:STRING=/usr/bin/gcc",
"-DCMAKE_PREFIX_PATH:STRING=/public/Work/qt-5.14.1".
The C compiler identification is GNU 7.4.0
The CXX compiler identification is GNU 7.4.0
...
Check for working CXX compiler: /usr/bin/g++
Check for working CXX compiler: /usr/bin/g++ -- works
Detecting CXX compiler ABI info
Detecting CXX compiler ABI info - done
Detecting CXX compile features
Detecting CXX compile features - done
Configuring done
Generating done
CMake Project was parsed successfully.

Open the build settings Projects > Build & Run > Docker Qt 5.14.1 > Build and press the button Add Build Step > Build. Check install in the Targets box and uncheck all other targets. The next screenshot shows the final build settings. Note that dr-cmake is used in the build and clean steps instead of cmake.

Switch to the run settings Projects > Build & Run > Docker Qt 5.14.1 > Run. Remove the section Install into temporary host directory by hovering over the Details button and by clicking on the cross just left of the Details button. The rest of the deployment section is OK.

When you have built the project at least once, you will see all the Files to deploy in the box with the same name. The file list tells QtCreator to which remote directories it must copy the local files on deployment. Here is an example entry:

/public/Work/qt-5.14.1/lib/libQt5Multimedia.so.5.14.1
-> /home/benutzer/MyComp/qt/lib/

QtCreator reads the mapping from local files to remote directories from the file QtCreatorDeployment.txt. The macros add_deployment_file and add_deployment_directory write the mapping entries into QtCreatorDeployment.txt. I have described this workaround in the post Deploying Qt Projects to Embedded Devices with CMake in detail.

The mapping contains two entries for the application executable.

/public/Work/build-qtcreator-with-docker-Docker_Qt_5_14_1-Debug/final/bin/SimpleApp
-> /home/benutzer/MyComp/bin
/public/Work/build-qtcreator-with-docker-Docker_Qt_5_14_1-Debug/SimpleApp
-> /home/benutzer/MyComp/.

The second entry comes from the install(TARGETS) call. The executable is the result of CMake’s build step. It contains a sequence of colons instead of the rpath. It wouldn’t run on the target system, because it wouldn’t find the Qt libraries. CMake’s install step replaces the sequence of colons by relative rpaths (see here for more details). The result of the install step is the executable of the first entry, which stems from the single add_deployment_file call. The executable of the first entry is the one that QtCreator will run on the target system.

If you use QtCreator 4.11.0 or newer and CMake 3.14 or newer, you don’t need the workaround with the file QtCreatorDeployment.txt any more. QtCreator and CMake work together to create the mapping from the install commands. Ubuntu 18.04, however, comes with CMake 3.10. So, you still need the workaround.

In the Run section, the Run configuration should say SimpleApp (on Touch21). In the box below the Run configuration, you enter the following values.

  • In the line Alternate executable on device, check the box Use this command instead and enter the full path to the executable on the target device (for me: /home/benutzer/MyComp/bin/SimpleApp).
  • In the line Command line arguments, enter the arguments required by the application (for me: -platform xcb -plugin evdevtouch).

In the Run Environment section, you add the variable DISPLAY with the value :0.

Running the Application on the Target System

Now comes the big magic moment. You press Ctrl+R (Run) in QtCreator. QtCreator builds the application, deploys the application and the Qt libraries to the target device and runs it on the target device – all in one step.

You see QtCreator’s Docker-CMake calls and the deployment calls in the Compile Output pane. Here is a shortened version (excluding compiler and progress messages).

11:19:37: Running steps for project SimpleApp...
11:19:37: Persisting CMake state...
11:19:37: Starting: "/home/burkhard/bin/dr-cmake" --build . --target all
...
11:19:39: The process "/home/burkhard/bin/dr-cmake" exited normally.
11:19:39: Starting: "/home/burkhard/bin/dr-cmake" --build . --target install
...
11:19:43: The process "/home/burkhard/bin/dr-cmake" exited normally.
11:19:43: Connecting to device "Touch21" (192.168.1.82).
11:19:44: The remote file system has 985 megabytes of free space, going ahead.
11:19:44: Deploy step finished.
11:19:44: Trying to kill "/home/benutzer/MyComp/bin/SimpleApp" on remote device...
11:19:45: Remote application killed.
11:19:45: Deploy step finished.
11:19:45: sending incremental file list

11:19:45: SimpleApp
...
total size
is 751,048 speedup is 1.00

11:22:24: Deploy step finished.
11:22:24: Elapsed time: 02:47.

The first deployment takes a couple of minutes (for me: 2:47 minutes), because QtCreator copies the runtime parts of Qt from the development PC to the target system. As long as Qt doesn’t change, you can skip the Qt deployment. Go to the build settings Projects > Build & Run > Docker Qt 5.14.1 > Build, set the variable DEPLOY_QT to OFF in the CMake section and press the Apply Configuration Changes button.

Your workflow is now the same as if you were running the application on your development PC. You change your code. You build, deploy and run the application by pressing Ctrl+R in Qt Creator. And then – you can try out your change on the target system. You get immediate feedback how your change behaves on the target system.

Also Interesting

My post Using Docker Containers for Yocto Builds describes how to install Docker and how to write a Dockerfile to build an embedded Linux image for the Raspberry Pi 3.

My posts Benefits of a Relocatable Qt and Creating Simple Installers with CPack provide the basic CMakeLists.txt file and the relocatable Qt libraries used in this post.

My post Deploying Qt Projects to Embedded Devices with CMake explains how to create the list of files to deploy on the target system with older CMake versions.


Blog Topics:

Comments