在QtCreator中构建Docker输出

本文翻译自Docker Builds from QtCreator
原文作者:Burkhard Stubert,个体企业家,为团队用Qt开发嵌入式系统软件提供独立咨询服务
校审:Amos Yang

在PC上开发时,只需在QtCreator中按Ctrl+R (Run)就可以构建并运行Qt应用程序。而当你想在嵌入式系统上运行应用程序时,你必须执行四个步骤:

  • 在Docker容器中交叉构建目标嵌入式系统的Qt应用程序。
  • 在目标系统上停止应用程序。
  • 使用scp命令将应用程序从PC复制到目标系统。
  • 在目标系统上启动应用程序。

你想不想在QtCreator中按Ctrl+R并让QtCreator为你执行以上四个步骤?当然!我将在这篇文章中为您演示如何实现。在嵌入式系统上运行应用程序与在PC上运行应用程序是一样的。

配置

工控屏(又称显控计算机)运行设备的监控程序。通常采用x86_64架构的Intel或AMD处理器。Ubuntu是首选操作系统。Ubuntu的桌面UI可能被禁用,因为工控屏只运行主应用程序,可能有一两个辅助应用程序。

为工控屏设置开发环境非常简单。您的开发PC运行某个版本的Ubuntu(对我来说:是Ubuntu 16.04)。目标系统运行在另一个Ubuntu版本上 (对我来说:是Ubuntu 18.04)。Docker容器中的交叉构建环境只不过是利用一个Ubuntu 18.04环境,安装了所有构建Qt库和应用程序的包。

你甚至不需要一个定制的工控屏。您只需要两台通过局域网连接并且能够通过OpenSSH互相通信的Linux PC。

如果目标系统在ARM SoC上运行,那么在Docker容器中设置交叉构建环境会稍微复杂一些。您必须构建Qt SDK(比如构建Yocto目标meta-toolchain-qt5),并在Docker容器中安装Qt SDK。容器对QtCreator隐藏交叉构建环境。QtCreator不知道容器是调用ARM目标的交叉编译器,还是调用Intel目标的本地编译器。

如果您希望继续执行本示例或您自己的项目,则必须具备以下先决条件。
按照这里所说的在您的开发PC上安装Docker。

在您的开发PC上创建一个工作目录(对我来说是:/public/Work)。您需要把示例项目或您的项目克隆到工作目录中。

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

项目目录包含一个Dockerfile。将其中最后一行中的WORKDIR设置为您的工作目录。这一点非常重要,您将在下一节中看到。对我来说,最后一行是这样的:

WORKDIR /public/Work

然后按照这个描述构建一个Docker映像Qt -ubuntu-18.04-ryzen,并使用这个映像构建可重定位的Qt库(Qt 5.14或更新版本)。Qt构建会生成一个目标 Qt -5.14.1-ubuntu-18.04-ryzen.tgz。您将在工作目录中对其解压缩。

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

解压安装Qt库到目录/public/Work/Qt -5.14.1中。
您的开发PC和目标系统之间需要一个可用的OpenSSH连接。也可以使用密码进行身份验证。

CMake的Docker封装

当您构建并安装Qt应用程序时,QtCreator会调用CMake

  • 生成Makefiles文件。使用以下命令在一个临时文件夹(比如/tmp/QtCreator-jZQYdh/qtc-cmake-caIYzSxO)中生成
    cmake /public/Work/qtcreator-with-docker '-GCodeBlocks - Unix Makefiles <more options>'
    
  • 编译并链接Qt应用。使用以下命令在构建文件夹(比如/public/Work/build-RelocatableQt-Qt_5_14_1-Debug)中编译
    cmake --build . --target all
    
  • 安装Qt应用及其辅助文件。使用以下命令安装到一个临时文件夹(比如/tmp/QtCreator-jZQYdh/qtc-cmake-caIYzSxO)
    cmake --build . --target install
    

本文的思路是在Docker容器内调用CMake。在QtCreator调用CMake前,(把以上目录)重定向到开发机(宿主PC)上的指定目录。此目录必须挂载到容器中,这样便可以在容器内正确位置调用CMake。封装脚本dr-cmake会执行这个操作。

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

选项-v /public/Work:/public/Work和-v /tmp:/tmp将工作目录和临时目录从宿主机挂载到容器。选项 -w $(pwd) 将宿主机目录(QtCreator在其中调用CMake)作为当前工作目录传递给Docker容器。Docker使用传递给脚本dr-cmake的参数$@来运行cmake。

脚本dr-cmake将QtCreator在宿主机上的操作转换为Docker容器中的相同操作。这种转换只有在宿主机和Docker容器具有相同的目录结构时才有用。

将封装脚本dr-cmake复制到$PATH包含的目录中(例如~/bin),并确保脚本是可执行的。

通过SSH访问目标系统

QtCreator使用OpenSSH将文件从开发PC复制到目标系统。因此,OpenSSH必须同时安装在您的PC和目标机上。QtCreator不支持Dropbear,一款用于嵌入式系统的轻量级OpenSSH替代品。

对于ssh登录,QtCreator提供密码身份验证和公钥身份验证。密码身份验证要求您在将应用程序部署到目标系统时输入密码。这很繁琐。因此,您希望使用公钥身份验证。

使用ssh-keygen在PC上创建私钥和公钥。

$ 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.
...

按下回车,可以将密码保留为空。让SSH代理知道新密钥。

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

将公钥复制到目标系统。

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

benutzer替换为目标系统上的用户名,将192.168.1.82替换为目标系统的IP地址。

登录目标系统并将公钥添加到~/.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

下一次登录目标系统时,您不再需要输入密码。SSH检查登录的用户是否拥有存储在~/. SSH /authorized_keys中的一个公钥的私钥。QtCreator将使用相同的机制来部署应用程序文件。

创建Docker Qt工具包

您需要定义一个使用dr-cmake脚本而不是cmake的工具包,它知道如何使用SSH登录目标系统,并将应用程序文件和Qt库部署到目标系统。

设置CMake

通过“Tools > Options > Kits > CMake”打开对话,并点击Add按钮。如图所示填写字段并点击Apply按钮。
在这里插入图片描述

设置Qt Version

转到同级对话” Tools > Options > Kits > Qt Versions”,并按下添加按钮。导航到您在工作目录中安装的Qt版本的QMake二进制文件。我的QMake位于/public/Work/qt-5.14.1/bin/ QMake。在版本名前面加上Docker可以使 Qt版本更容易识别。点击Apply按钮保存配置。
在这里插入图片描述

设置Device

进入对话框“Tools > Options > Devices”添加SSH登录信息。在Devices选项卡上,点击Add按钮,在单独弹出的对话框中选择Generic Linux设备并点击Start Wizard按钮。对我来说,第一个向导页面是这样的:
在这里插入图片描述
名称、IP地址和用户名需要填写不同的值。按下Next按钮移动到第二个向导页面。你会浏览到前面创建的私有SSH密钥。
在这里插入图片描述
由于您已经部署了公钥,您可以通过按下Next按钮进入第三个也是最后一个向导页面。最后一页是这样的。
在这里插入图片描述
点击Finish按钮。连接测试成功的对话框如下所示。
在这里插入图片描述
新设备Touch21的完整填写表单与此类似。
在这里插入图片描述

设置Kit

你已经定义了所有的组件,CMake、Qt Version和Qt Device,来配置一个Kit。回到对话框“Tools > Options > Kits > Kits”并按下Add按钮。填写表单,如下面的屏幕截图所示。
在这里插入图片描述
按下底部CMake配置的Change按钮,用以下三行替换对话框的内容。

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

不要忘记将我的工作目录/public/Work替换为您的工作目录。在按下OK按钮之前,对话框应该是这样的。
在这里插入图片描述

配置项目

如果您第一次打开项目文件/public/Work/qtcreator-with-docker/CMakeLists.txt, QtCreator会要求您配置项目(参见下一个屏幕截图)。选择刚刚创建的kit Docker Qt 5.14.1并按下Configure Project按钮。
在这里插入图片描述
如果您以前使用另一种配置打开项目,您可以在Build & Run下面的列表和子条目Build中单击Docker Qt 5.14.1条目。

在这两种情况下,在第一次生成Makefiles时,您将在输出面板常规消息中看到普通的CMake信息。看一下第一行: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。尽管如此,CMake调用仍然可以工作。

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.

通过“Projects > Build & Run > Docker Qt 5.14.1 > Build”打开构建设置,然后点击按钮“Add Build Step > Build”,选中“install in the Targets box”,并取消选中“all other targets”。下一个屏幕截图显示了最终的构建设置。注意,构建和清理步骤中使用的是dr-cmake,而不是cmake。
在这里插入图片描述
通过“Projects > Build & Run > Docker Qt 5.14.1 > Run”切换到运行设置,将“Install”部分移到临时主机目录中,方法是将鼠标悬停在“Details”按钮上,并单击“Details”按钮左边的“交叉”按钮。部署的其余部分没有问题。
在这里插入图片描述
当您至少构建了一次项目之后,您将看到所有要部署的文件都在具有相同名称的框中。文件列表告诉QtCreator在部署时必须将本地文件复制到哪个远程目录。下面是一个例子:

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

QtCreator通过QtCreatorDeployment.txt文件中读取本地文件到远程目录的映射。宏定义add_deployment_file和add_deployment_directory将映射条目写入到QtCreatorDeployment.txt。我在《Deploying Qt Projects to Embedded Devices》一文中详细描述了这个解决方案。

映射包含应用程序可执行文件的两个条目。

/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/.

第二个条目来自install(TARGETS)调用。可执行文件是CMake构建步骤的结果。它包含冒号序列,而不是rpath。它不会在目标系统上运行,因为它不会找到Qt库。CMake的安装步骤将冒号序列替换为相对的rpaths(请参阅此处了解更多详细信息)。安装步骤的结果是第一个条目的可执行文件,它源于单个add_deployment_file调用。第一个条目的可执行文件就是QtCreator将在目标系统上运行的那个。

如果您使用的是QtCreator 4.11.0或更新版本和CMake 3.14或更新版本,那么就不再需要使用QtCreatorDeployment.txt文件来解决问题。QtCreator和CMake一起从安装命令创建映射。然而,Ubuntu 18.04附带了CMake 3.10。所以,你仍然需要变通。

在Run部分,Run配置应该说很简单(在Touch21上)。在运行配置下的输入框中,输入以下值。

  • 在“Alternate executable on device”中,选中“Use this command instead and enter the full path to the executable on the target device”,并输入目标设备上可执行文件的完整路径(对我来说是:/home/benutzer/MyComp/bin/SimpleApp)。
  • 在命令行参数中,输入应用程序所需的参数(对我来说:-platform xcb -plugin evdevtouch)。
    在这里插入图片描述
    在Run Environment部分中,添加变量DISPLAY并设置值为0。
    在这里插入图片描述

在目标系统上运行应用程序

现在是见证奇迹的时刻。在QtCreator中按住Ctrl+R(运行)。QtCreator会构建应用程序,将应用程序和Qt库部署到目标设备上,并在目标设备上运行—所有这些都在一个步骤中完成。

您可以在编译输出面板中看到QtCreator的Docker-CMake调用和部署调用。这是一个简化的版本(不包括编译器和进度消息)。

1: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.

第一次部署需要几分钟(对我来说,花费了2分47秒),因为QtCreator将Qt的运行时部分从开发PC复制到目标系统。只要Qt不变,就可以跳过Qt部署。进入构建设置“Projects > Build & Run > Docker Qt 5.14.1 > Build”将变量DEPLOY_QT设置为OFF,并按下按钮Apply Configuration Changes。

您的工作流程现在与在开发PC上运行应用程序一样。更改代码。在Qt Creator中按Ctrl+R构建、部署和运行应用程序。然后——你可以在目标系统上尝试你的改变。您可以立即得到关于更改在目标系统上的行为的反馈。

相关的内容

我的博文《Using Docker Containers for Yocto Builds》描述了如何安装Docker以及如何编写Dockerfile来为Raspberry pi3构建嵌入式Linux映像。

《Benefits of a Relocatable Qt》《Creating Simple Installers with CPack》两篇博文提供了基本的CMakeLists.txt文件和本贴子中使用的可重定位的Qt库。

另一篇博文《Deploying Qt Projects to Embedded Devices with CMake》介绍了如何在目标系统上使用较老的CMake版本创建进行部署的文件列表。