我年初在做一个基于ARM64平台(aarch64)的工业网关项目,Qt程序在x86开发机上编译得顺顺当当,一部署到目标板就报各种找不到库里、找不到插件、platform plugin缺失之类的问题。目标板是裁剪过的系统,没有dpkg、没有apt,也不能随便装桌面环境,反复折腾后我决定换一条路:直接用Qt 5.14.2做静态交叉编译,把Qt核心库、平台插件、第三方依赖全部打进一个可执行文件,部署时就是拷一个二进制过去,跑就完了。
这篇文章是我完整跑通这套流程后的整理,覆盖从零搭建交叉编译环境、工具链选型、Qt源码configure参数调整、静态编译安装,最终在aarch64设备上运行Qt程序的全过程,并包含我在实际编译和部署中踩过的坑。如果你正准备给ARM64嵌入式设备做Qt应用开发,或者想搞明白“静态交叉编译”到底是怎么一回事,这篇文章应该能帮你省掉不少弯路。
1. 静态交叉编译解决什么问题,以及Qt 5.14.2为什么合适
1.1 交叉编译和静态链接的本质
先厘清两个概念。交叉编译,简单说就是在一种架构的主机上编译另一种架构的程序。我们平时用的开发机基本都是x86_64,目标板则是ARM64(aarch64),两个架构的CPU指令集不一样,编译器也必须不一样,所以要用aarch64-linux-gnu-g++这类的交叉工具链,而不是本机的g++。
静态链接则是在编译最终可执行文件时,把用到的所有.a静态库直接打包进二进制文件,生成的可执行程序不依赖目标系统上的.so动态库。这样带来的好处很直接:
- 拷贝单文件即可部署,不需要往目标板同步任何Qt动态库
- 不会出现“动态库版本不对”“加载不到libQt5Core.so.5”这类问题
- 目标板的系统镜像可以做得非常干净,磁盘空间和启动流程都更可控
代价也有:可执行文件体积变大,链接时间变长,有些模块在静态方式下需要手动注册插件。不过对于嵌入式设备来说,这些代价通常都能接受。
1.2 为什么选Qt 5.14.2
Qt版本我纠结过一阵。选5.14.2,主要有几个原因:
- 5.14是Qt公司标记的LTS分支,修了很多稳定性问题,社区资料多,遇到问题基本都能搜到现成答案
- 相比Qt 6.x,Qt 5的configure参数体系更为成熟稳定,交叉编译相关文档也更全,踩坑成本低
- 5.14.2在aarch64场景下,不需要额外打特殊的补丁,mkspecs目录里已经内置了aarch64平台配置,直接用就行
- 我的业务代码原本基于Qt 5.12/5.14,升级跨度小,兼容成本几乎为零
如果你是做一个全新项目,用Qt 5.15或Qt 6也是可以的,但本文所有命令和路径都基于Qt 5.14.2,如果你用别的版本,configure输出里的Feature列表会有些差异。
2. 交叉编译环境搭建与工具链验证
2.1 主机系统与依赖包
我的构建主机是Ubuntu 20.04 LTS(x86_64)。理论上其它Linux发行版也可以,但Ubuntu/Debian系apt源里现成的交叉工具链最省事,我不用花时间去手工配置sysroot。下面这套依赖包是跑通整个流程必需的:
sudo apt update sudo apt install -y build-essential ninja-build perl python3 flex bison gperf \ libglib2.0-dev libfontconfig1-dev libfreetype6-dev libxkbcommon-dev \ gcc-aarch64-linux-gnu g++-aarch64-linux-gnu其中flex、bison、gperf是Qt构建系统生成解析器时用到的工具,ninja-build可选,但多一种构建器选择总不是坏事。libglib2.0-dev这类包是给Qt宿主端工具编译用的,交叉编译目标库时不会链接它们,但configure阶段可能探测它们的存在。
2.2 工具链选型:发行版自带、Linaro还是Buildroot
交叉工具链我在不同项目里用过三种方案,简单对比一下:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 发行版自带(Ubuntu源) | 安装快,自带默认sysroot,路径固定 | 版本跟随发行版,无法自由改 | 快速验证、原型开发 |
| Linaro工具链 | 专门为ARM优化,版本全 | 需要手动配置sysroot环境变量 | 对工具链版本有特定要求 |
| Buildroot全套 | 能同步构建内核、根文件系统、Qt,一致性最好 | 学习曲线陡,首次构建慢 | 产品量产、需要完整系统镜像 |
这次我选的是发行版自带工具链,因为项目目标是先把应用跑通,不需要从零定制整块系统镜像。Ubuntu 20.04自带的aarch64工具链是GCC 9.3.0,对Qt 5.14.2来说完全够用,C++11/14/17都支持,不会出现语法兼容问题。
2.3 验证工具链可用的三条命令
装完以后不要急着配Qt,先做三个基本验证:
aarch64-linux-gnu-g++ --version aarch64-linux-gnu-gcc -v aarch64-linux-gnu-g++ -print-file-name=libc.so.6第三条命令很重要,它会打印出工具链默认sysroot里libc的完整路径,正常情况下应该是/usr/aarch64-linux-gnu/lib/libc.so.6。如果你的路径不对,后面链接阶段会出现一堆“找不到libc”“找不到libstdc++”的诡异错误,问题根源往往就是sysroot没配对。
3. configure配置:决定整个编译走向的核心参数
3.1 关键参数逐项拆解
Qt从源码构建的第一步就是执行configure,交叉编译的成败有一半取决于这里的参数。我按自己项目需求一个个拆开讲:
-release -static:编译发布版本并生成静态库。-static是这次的核心开关,Qt会据此调整内部构建逻辑,把所有目标模块编成.a文件。
-opensource -confirm-license:选择开源协议并跳过交互确认,否则configure会停在许可证问答界面,无人值守构建时很容易卡住。
-prefix /opt/qt-5.14.2-aarch64:指定make install的安装目录。注意,这个前缀在交叉编译场景下代表的是“目标设备视角”,但实际上安装完以后Qt工具链仍然运行在主机上,用来给目标平台生成Makefile。我习惯把交叉编译出的Qt单独放一个目录,避免和x86版本的Qt混在一起。
-xplatform linux-aarch64-gnu-g++:告诉Qt使用交叉编译平台配置。Qt源码的qtbase/mkspecs目录里已经内置了linux-aarch64-gnu-g++,它会告诉qmake“主机是x86_64,目标机是aarch64,目标编译器是aarch64-linux-gnu-g++”。
-no-opengl -no-xcb -no-icu -no-cups -no-dbus:这些是我针对嵌入式无桌面环境做的裁剪。目标板不跑X11,没有GPU OpenGL加速,也没有D-Bus系统服务,这些模块留着不但增加编译时间,静态链接时还会引入一堆不可能找到的依赖库。
-qt-libpng -qt-libjpeg -qt-zlib -qt-pcre -qt-freetype:强制使用Qt源码自带的第三方库实现,而不是依赖目标系统的共享库版本。这一步对静态编译用户态程序特别关键,否则你还要去交叉编译libpng、libjpeg、freetype,工作量大得多。
-nomake examples -nomake tests -nomake benchmarks:不编译自带示例和测试。全套示例编译起来非常耗时,我们只需要Qt库和工具链,省略掉可以让整个构建时间缩短一半以上。
3.2 我的configure命令行
综合以上考虑,最终我使用的完整configure命令是:
cd /path/to/qt-everywhere-src-5.14.2 make clean # 如果之前跑过别的配置,先清理 ./configure \ -release \ -static \ -opensource \ -confirm-license \ -prefix /opt/qt-5.14.2-aarch64 \ -xplatform linux-aarch64-gnu-g++ \ -no-opengl \ -no-xcb \ -no-icu \ -no-cups \ -no-dbus \ -qt-libpng \ -qt-libjpeg \ -qt-zlib \ -qt-pcre \ -qt-freetype \ -nomake examples \ -nomake tests \ -nomake benchmarks你可能会问,为什么我不用-sysroot参数?因为发行版工具链的默认sysroot已经是/usr/aarch64-linux-gnu,Qt的configure会自动探测到,不需要额外指定。如果你用了Linaro之类需要手动指定sysroot的工具链,就要加上:
./configure -sysroot /path/to/your/sysroot ...3.3 configure过程中的隐性问题
configure执行完后,终端会打印一张支持/不支持的特性列表。我建议你重点检查几项:
Static build .......... yesPlatform plugin ....... linuxfb(或你期望的平台插件)Xcb ................... noOpenGL ............... no
如果Static build显示的是no,说明你configure参数没生效,后面编译出来的还是动态库,这一版基本白跑了。
configure阶段最常见的失败是缺工具,报错信息里会直接提示flex not found或bison not found,按提示装对应包再重跑就行。因为configure带了明确的错误提示,反而是最不难解决的一步。
4. 编译、安装与第一个静态程序
4.1 make与make install
configure通过后,正式编译时间取决于机器配置。我用8核16线程的机器,编译整个Qt 5.14.2静态库大约花了40分钟左右。命令很简单:
make -j8 make install编译过程中不建议同时开很多大型应用,Qt静态编译时C++模板实例化非常吃内存,我有一次在4G内存的机器上跑-j16,直接OOM掉了。保险起见,内存小于8G的机器建议-j4,8G以上再考虑-j8。
编译完以后检查安装目录:
ls /opt/qt-5.14.2-aarch64/bin你会看到qmake、moc、rcc等工具。这里的qmake是在主机上运行的,用来生成目标平台的Makefile,它本身不是给aarch64设备用的运行程序。
4.2 QPA平台插件的静态注册
这是静态编译和动态编译最大的区别之一。动态编译时,Qt会在运行时根据QT_QPA_PLATFORM环境变量去加载platforms/libqlinuxfb.so这样的插件。静态编译时,插件代码已经被编进可执行文件里了,运行时就不知道要加载哪一个,于是出现经典的报错:
This application failed to start because no Qt platform plugin could be initialized.解决办法是告诉qmake“把哪个平台插件链接进来”。我的.pro文件里这样写:
QT += core gui widgets QTPLUGIN += qlinuxfb TARGET = hello TEMPLATE = app SOURCES += main.cppQTPLUGIN += qlinuxfb会把linuxfb平台插件静态链入可执行文件。如果你的目标板有X11环境,这里应该写qxcb;如果有GPU驱动,可能会用qeglfs。嵌入式无桌面Linux设备,linuxfb是最通用、最不依赖硬件厂商库的选择。
4.3 用qmake构建目标程序
为了让qmake找到交叉编译环境,我习惯在shell环境里导出两个变量,每次构建时不用重复写:
export PATH=/opt/qt-5.14.2-aarch64/bin:$PATH export QMAKESPEC=/opt/qt-5.14.2-aarch64/mkspecs/linux-aarch64-gnu-g++然后正常构建:
mkdir build && cd build qmake ../hello.pro make -j4构建完成后,用file命令检查产物架构:
file hello输出里会有一个关键特征statically linked,以及ARM aarch64:
hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, with debug_info, not stripped再用ldd验证一下:
ldd hello静态链接的程序,ldd会输出一句not a dynamic executable,这说明程序不依赖任何动态库。
5. 部署到aarch64设备:平台插件、字体与运行环境
5.1 部署清单
把静态编译的hello程序部署到目标板,理论上只需要拷一个文件,但实际运行还有两个隐性依赖:设备节点和运行参数。
先看设备节点,linuxfb平台依赖Linux帧缓冲设备,也就是/dev/fb0。如果内核没有启用framebuffer支持,或者设备被占用,程序会启动失败。检查方式:
ls -l /dev/fb*程序拷到目标板后,运行前设置平台:
export QT_QPA_PLATFORM=linuxfb ./hello5.2 字体和时区问题
静态编译不解决字体问题。我用linuxfb跑起来后,中文全部显示成方框,原因就是目标板根文件系统里根本没有中文字体文件。
解决方法是把字体文件跟着程序一起部署,然后在程序代码里指定字体路径:
#include <QApplication> #include <QFontDatabase> #include <QLabel> #include <QFont> int main(int argc, char *argv[]) { QApplication app(argc, argv); int id = QFontDatabase::addApplicationFont("/usr/share/fonts/DroidSansFallback.ttf"); QString family = QFontDatabase::applicationFontFamilies(id).first(); QFont font(family, 16); app.setFont(font); QLabel label("你好,aarch64静态Qt"); label.resize(320, 160); label.show(); return app.exec(); }把字体文件放到目标板的/usr/share/fonts/目录,中文显示就没问题了。如果你的设备不需要中文排版,这一步可以跳过。
时区问题比较隐蔽。我部署之后发现程序里打印的本地时间比真实时间晚了8小时,因为目标板根文件系统里没有/usr/share/zoneinfo。静态链接的glibc照样会读取系统时区文件,所以部署时记得把/etc/localtime和/usr/share/zoneinfo下对应的时区文件同步过去。
5.3 触摸屏场景的选项
如果你的设备带触摸屏,linuxfb默认不带触摸事件处理,需要在configure阶段额外支持tslib。思路是先交叉编译tslib到sysroot,然后configure时加上:
./configure ... -tslib -I/path/to/tslib/include -L/path/to/tslib/lib这是嵌入式设备常见的组合:linuxfb负责显示输出,tslib负责触摸输入。我在另一个带7寸电容屏的项目里就是这么配的,跑起来后用Qt的QTouchEvent接收触摸事件,效果正常。如果你的设备用的是鼠标或纯按键导航,这部分可以直接跳过。
6. 实测踩坑排查:从configure到链接器错误
写这篇手册的时候,我特意把整个过程重新跑了一遍,把遇到的典型问题按阶段整理出来。这些坑很大概率你也会踩到,直接按排查思路走能省大量时间。
6.1 configure阶段:xcb依赖连环报错
第一次configure时,我没有加-no-xcb,结果报了一长串错误,核心内容大概是:
ERROR: feature 'xcb' was enabled, but the pre-condition 'libs.xcb' failed.原因是Qt默认会开启xcb支持,而xcb依赖一堆X11开发库,交叉编译时configure会去sysroot里找这些库。发行版工具链的sysroot里默认只有基础C库和C++库,没有完整的X11头文件,自然找不到。
排查思路是先确认你目标板是不是真的需要X11。如果目标是带X服务的桌面版Linux,那确实需要补X11的交叉编译库;如果是嵌入式设备,直接用-no-xcb关闭是最干净的做法。我在最终配置里就关闭了它。
同类问题还包括-no-opengl:configure可能会探测到EGL库的缺失而报错。嵌入式设备如果不用GPU加速,直接关掉即可。
6.2 make阶段:mkspec路径把编译器搞混
有一次我手动执行make,发现编译出来的东西莫名其妙是x86_64架构的,马上停下来检查。
原因是我在shell里设置的QMAKESPEC指向了主机的x86 mkspec,导致qmake在生成Makefile时,CXX编译器用的是本机的g++,而不是aarch64-linux-gnu-g++。交叉编译项目最忌讳的就是路径环境变量残留。
排查方法是查看生成的Makefile:
grep -n "^CXX" Makefile正确输出应该是:
CXX = aarch64-linux-gnu-g++如果输出是g++,说明qmake没有使用交叉平台配置,这时需要重新核对-xplatform参数和QMAKESPEC环境变量。我建议直接在.pro里用一行的方式指定,不容易出问题:
QMAKE_CXX = aarch64-linux-gnu-g++ QMAKE_CC = aarch64-linux-gnu-gcc这样即使环境变量没配对,qmake也会强制走交叉编译器。
6.3 链接阶段:-lGL、-lgcc_s这类经典缺失
链接阶段报错通常长这样:
/usr/lib/gcc-cross/aarch64-linux-gnu/9/../../../../aarch64-linux-gnu/bin/ld: cannot find -lGL collect2: error: ld returned 1 exit status这个-lGL就是OpenGL库,说明-no-opengl参数没有生效,或者有模块强行依赖OpenGL。排查方法:重新看configure输出的特性列表,确认OpenGL为no。另外还有cannot find -lgcc_s、cannot find -lgcc这类问题,通常是sysroot配置不对,但我用发行版工具链时很少遇到。
还有一个隐藏较深的情况:某些模块,比如Qt WebEngine,本身无法用静态方式编译,configure会自动跳过,但你的.pro如果显式依赖了WebEngine库,链接时会报找不到相关符号。我的建议是嵌入式项目干脆不要碰WebEngine,体积大,静态化的坑也多。
常见链接错误可以按照下表排查:
| 报错关键词 | 可能原因 | 解决方向 |
|---|---|---|
| cannot find -lGL | 没关OpenGL相关模块 | configure加-no-opengl |
| cannot find -lxcb | xcb依赖未满足 | 加-no-xcb或补交叉X11库 |
| cannot find -lgcc_s | 工具链sysroot没配对 | 检查默认sysroot路径 |
undefined reference toqt_plugin_query_metadata | 平台插件未注册 | .pro里加QTPLUGIN |
| cannot find -lfontconfig | freetype/fontconfig缺失 | configure加-qt-freetype |
链接错误定位时,可以先用aarch64-linux-gnu-g++ -print-search-dirs看编译器默认搜索路径,再决定是补库还是关闭对应特性,不要盲目加-l参数硬试。
7. 体积、工具链和工作流的经验补充
7.1 静态Qt可执行文件的体积与裁剪
静态链接后,体积是很多人在意的一环。我编译的helloworld级别程序,静态链接Qt Widgets模块,体积大约18MB左右。经过aarch64-linux-gnu-strip去掉符号表之后,能降到6~7MB。
如果你觉得体积还是太大,有几个可操作方向:
- 在configure时裁剪不需要的Qt模块:比如纯命令行或轻量界面,就不要编Qt Widgets模块,只用QtCore/QtGui会小很多
- 去掉debug信息,用
-release编译 - 启用Qt的feature机制,按需裁剪库内功能,这是更细粒度的控制,前期不建议动
对于嵌入式设备的存储空间来说,6MB的单个程序通常在可接受范围内。
7.2 后续扩展:OpenSSL、数据库驱动、外部库
静态Qt和动态Qt的扩展方式不太一样。以OpenSSL为例,如果你要链接静态Qt并支持HTTPS,需要在configure时加-openssl-linked,同时确保工具链sysroot里有交叉编译好的libssl.a和libcrypto.a。否则Qt会采用运行时加载方式,静态可执行文件里压根不包含OpenSSL代码,在目标板上就会提示找不到libssl.so,让我卡了很久才反应过来。
数据库驱动方面,Qt内置SQLite时可以用-qt-sqlite参数,这样编译出来的程序自带SQLite能力,不需要目标板安装任何库。如果你还需要MySQL或PostgreSQL驱动,就得先去交叉编译对应客户端的静态库,然后configure时加-plugin-sql-mysql之类参数,操作量会明显增加。
7.3 个人建议
结合我这次完整跑通的经验,有几个建议想留给后面的人:
第一,不要一上来就把整个业务工程塞进交叉编译,先用一个最简单的hello窗口程序验证工具链和Qt配置是正确的,再逐步增加业务代码模块。交叉编译的变量比本机编译多得多,一次只引入一个变量是最快的排错方式。
第二,configure输出的特性列表值得完整检查一遍。Qt的configure不是“你让它关什么它一定关什么”,有些模块会因为检测到依赖存在而自动开启,你可能没注意到它被编译进了静态库。
第三,静态编译后,插件注册的事情不要忘。Qt的imageformats(图片格式插件)、platforms(平台插件)、sqldrivers(数据库驱动),在动态编译下是运行时加载的.so文件,静态编译下必须在.pro里通过QTPLUGIN显式声明,或者用Q_IMPORT_PLUGIN宏在代码层注册,否则功能正常也会报“Unknown error”或“Cannot load plugin”这类让人摸不着头脑的错。
这套流程走完以后,我的aarch64部署方式从“拷贝一堆.so+配置文件再到板子上调环境变量”,简化成了“拷一个文件、跑起来”。后续即使是换了硬件平台,只要还是aarch64的Linux环境,这套工程能力基本可以直接复用。最后再提醒一句:别把qmake的-xplatform写错,这是我整个接触过程中唯一一次想摔键盘的bug,核对三次环境,最后就败在一个参数上。