1. 项目概述
1.1 为什么要折腾Qt静态交叉编译
先说说这个项目的背景。嵌入式设备上跑Qt,传统的做法是动态交叉编译,也就是在x86主机上用交叉工具链把Qt编译成arm/aarch64平台能运行的共享库,再把Qt运行时、插件、依赖的库一起打包部署到目标板上。这套模式用起来简单,开发阶段方便,但到产品化阶段就会遇到几个很现实的问题:目标板上的rootfs必须和编译时完全一致,系统里的glibc、libstdc++、zlib这些公共库版本不能乱动,否则就可能出现“本地跑得好好的,板子上启动就崩”的尴尬局面。而且动态库拖家带口,发布之前还得用ldd挨个检查依赖,漏一个库就白忙活半天。
静态交叉编译的思路就完全不同了。它生成的最终可执行文件把Qt库代码直接链接进了二进制,跑的时候不再需要目标板上有任何Qt的so文件。好处显而易见:部署流程简化为拷贝一个文件,不需要关注系统Qt环境的差异;而且对rootfs的改动降到了最低,非常契合嵌入式设备量产时那种“镜像刷进去就要能跑”的需求。代价则是可执行文件体积偏大,并且交叉编译过程中第三方依赖的处理复杂度直线上升。本手册就以Qt 5.14.2为例,基于aarch64(ARM 64位)平台,把从零开始搭建静态交叉编译环境的每一个环节拆开讲清楚。
1.2 这份手册覆盖的范围和适用人群
这次做的完整环境,目标平台是aarch64架构的Linux系统,主机是标准x86_64的Ubuntu 20.04,Qt版本固定在5.14.2,工具链选用Linaro GCC 9.4(aarch64-linux-gnu-g++)。整体覆盖到的内容包含:交叉工具链的安装、目标sysroot的准备、第三方基础库(xcb、xkbcommon等)的静态交叉编译、Qt源码的configure参数定制、静态链接的编排,以及最后编译结果在arm64 Linux系统(树莓派4B这种带桌面的板子,或者纯命令行环境)上的实际验证。
这套进阶内容适合这么几类人:一是已经在用Qt做应用开发、但一直没接触过嵌入式交叉编译的工程师;二是被动态库部署问题反复折腾过的嵌入式Linux开发者;三是想在开发机上搭一整套快速验证环境、不想频繁往板子上刷镜像的朋友。文章后面讲到很多参数和报错,都是我实际踩坑后的经验总结,不是纸上谈兵的空泛理论。
2. 环境准备与整体思路拆解
2.1 从一个大方向开始:静态交叉编译到底改变了什么
很多朋友第一次接触交叉编译时,总是习惯性地把工作流程理解为“在x86上编译出来的程序,拷贝到arm板上就能跑”。这其实只说对了一半,真正决定能不能跑的原因,是二进制指令集(aarch64)和系统ABI必须匹配。动态交叉编译和静态交叉编译两者的共同点都是要用aarch64交叉工具链,区别在于链接器如何处理依赖关系。
动态交叉编译的标准姿势是这样的:先交叉编译出libQt5Core.so、libQt5Widgets.so等一堆动态库,然后在编译用户程序时通过-L指定这些库的路径,编译出来的程序启动时,系统加载器会去目标板固定路径搜索这些so文件。这里就埋了一个隐患:一旦你动了板子上Qt的库版本,或者板子里压根没有某个辅助库(比如libxkbcommon.so.0),程序当场就会拒绝启动。
静态交叉编译的做法是把所有Qt相关的目标文件(.a静态库)直接打入最终可执行文件。这样得到的程序运行时不再依赖任何Qt动态库了。但要特别小心一个概念——-static加上Qt的静态库,并不代表程序是“完全静态链接”的。在真实嵌入式环境里,glibc这样和系统强耦合的库通常还是会动态链接,因为静态链接glibc会导致DNS解析(NSS)、用户账号体系等功能失效,这在桌面环境里是致命的。所以本手册的目标是“尽量静态、关键的公共库保持动态”,这样既最大化部署便利性,又避免系统功能缺失。这个折中方案才是工业级产品真正在用的方式。
2.2 工具链与依赖库的全貌
静态交叉编译Qt的完整依赖链条比预想的要长。除了Qt自身,你还需要提前编译好一小批第三方库,它们都是Qt运行时或xcb插件间接依赖的。在我这套方案里,按依赖关系列出来就这几项:
| 库 | 作用 | 是否必须 | 说明 |
|---|---|---|---|
| glibc + libstdc++ | C/C++标准库 | 必须 | 动态链接,来自目标平台sysroot |
| zlib | 压缩库 | QtCore依赖 | 静态编译 |
| libxcb及其扩展库 | X11协议客户端库 | 带GUI必须 | 静态编译 |
| xkbcommon | 键盘输入与布局 | Qt GUI插件依赖 | 静态编译 |
| libpng、libjpeg | 图片解码 | QImage常用格式 | 静态编译 |
| freetype、fontconfig、harfbuzz | 字体渲染 | Qt GUI必需 | 静态编译 |
| openssl | 加密通信 | QtNetwork可选 | 静态编译时容易出问题,可先禁用 |
| sqlite | 数据库 | QtSql可选 | 可用Qt自带源码 |
顺着这个表往下看,你就会明白为什么静态交叉编译Qt比较费劲——第三方依赖库并不总是以静态库的形式躺在sysroot里,很多发行版默认只安装xxx-dev的deb包也不会给你.a文件。所以在真正configure Qt之前,得先自己把这些基础库逐个用aarch64交叉工具链编译出来,再放回sysroot目录。整个链路就是一条“工具链 → sysroot → 基础库 → Qt → 用户程序”的金字塔结构。每一层踩的坑不一样,下面从环境准备开始一步步展开。
3. 从零搭建:工具链与sysroot准备
3.1 安装交叉编译工具链
工具链我选择了Linaro的aarch64-linux-gnu的GCC 9.4版本。选GCC 9而不是更新的版本,理由是在实际编译Qt 5.14.2时,GCC 10以上偶尔会有C++标准库头文件兼容性的告警,个别模块还会编译失败;GCC 7/8也能编,但GLIBCXX的版本太低,后续链接用C++17新特性的代码会受限。GCC 9是Qt 5.14时代的主流版本,兼容性最稳,编出来的东西也足够新。
安装方式不用刻意下载单独的交叉工具链压缩包,Ubuntu 20.04的软件源里就有现成的:
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu装完之后检查一下版本号:
aarch64-linux-gnu-gcc --version正常输出会显示“gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.2)”,这就对了。
但这里有个容易忽略的坑:Ubuntu源里的这套工具链,其默认的libc头文件来自Ubuntu自身构建的glibc,版本偏新(arm64宿主上是2.31)。如果目标板上的glibc版本是2.25或2.28,那么用这套工具链编译出的程序在目标板上可能会因为“requires glibc 2.28”而无法运行。所以在做真正的产品时,我建议工具链和sysroot做成一套,直接从目标板的rootfs里拿sysroot,配合Linaro的原版工具链使用,这样glibc版本天然匹配。开发阶段先在ubuntu源的工具链上把整个编译流程打通,之后再整体切换也是很顺的。
3.2 搭建目标系统的sysroot
sysroot就是目标板根文件系统的镜像目录,交叉工具链编译时会从这个目录里找头文件和系统的动态/静态库。Ubuntu源里的交叉工具链默认已经包含了一个aarch64版的基础sysroot,目录一般在/usr/aarch64-linux-gnu。但那里的内容只有最基本的glibc和libstdc++,像libxcb、libxkbcommon、fontconfig这些扩展库是没有的。
我实际开发时用的方案是直接对目标板做一个“文件系统抽取”,把板子用的rootfs(用debootstrap从Ubuntu ports构建,或者板厂给的rootfs tar包)解压到主机目录,例如放到/opt/aarch64-sysroot。这个sysroot会同时承担两个作用:一是作为编译时的头文件/库文件查找路径,二是后续Qt产物部署的目标参考。
sudo mkdir -p /opt/aarch64-sysroot sudo tar -xzf rootfs.tar.gz -C /opt/aarch64-sysroot这里要额外留意,sysroot里的动态库如果带符号链接,tar解压时最好加上-p参数保留权限,否则后续ldconfig或者编译时可能会报各种奇怪的问题。
3.3 工具链中的sysroot引用机制
交叉工具链有一个很方便的机制:它在搜索头文件和库时,默认会往自身sysroot目录去找。使用Ubuntu源里的aarch64-linux-gnu工具链时,这个自动sysroot就是/usr/aarch64-linux-gnu。所以当你把第三方库安装到目标版rootfs之后,还需要把它们也拷贝一份到工具的sysroot里,或者通过编译参数显式指定。
比较稳妥的做法是给Qt的configure传下面这几个环境变量,让它明确知道该用哪个sysroot:
export CROSS_COMPILE=/usr/bin/aarch64-linux-gnu- export SYSROOT=/opt/aarch64-sysroot export CPPFLAGS="--sysroot=$SYSROOT -I$SYSROOT/usr/include -I$SYSROOT/usr/include/aarch64-linux-gnu" export LDFLAGS="--sysroot=$SYSROOT -L$SYSROOT/usr/lib/aarch64-linux-gnu -Wl,-rpath-link=$SYSROOT/usr/lib/aarch64-linux-gnu"其中-rpath-link这个参数值得解释一下。在静态链接Qt库时,链接器需要读取so文件的动态依赖信息,但此时它不会立刻加载这些so,只需要定位到它们所在目录。-rpath-link就是给链接器指一条“只查找依赖、不写入最终程序”的路,这在交叉编译中非常常用。很多Qt静态编译失败的案例,最后就缺了这个参数,现象就是“undefined reference toxcb_xxx”一类。
4. 第三方依赖库的静态交叉编译
4.1 依赖分析:到底哪些库必须动手编
前面表格里已经列了依赖库,但实际动手前还需要理清优先级。因为Qt的configure脚本在检测到某些库存在时会自动开启对应的功能模块,如果检测不到就直接禁用。比如检测不到OpenSSL,QtNetwork就编译不出SSL功能;但如果你的程序用不到SSL,它并不会报错。所以这里有一个非常实用的原则:按需编译,不要贪多。
对我这次的目标(自绘界面、GUI渲染、事件输入),最小依赖集是:libxcb、xkbcommon、freetype、fontconfig、harfbuzz、libpng、libjpeg。其中harfbuzz、libpng、libjpeg在Qt源码自带的src/3rdparty目录里就有,如果configure时不指定外部版本,Qt会直接使用自带的第三方源码,不需要单独准备。真正需要手动交叉编译的是xcb、xkbcommon、freetype、fontconfig这几个。
xcb再展开的话,它其实是一组库的集合:libxcb主库、libxcb-render-util、libxcb-image、libxcb-keysyms、libxcb-randr、libxcb-xinerama、libxcb-xkb、libxcb-xinput、libxcb-shm、libxcb-xfixes。Qt xcb插件会对它们逐一检测。如果刻意不装全,也可以,Qt configure会禁用掉对应的功能,但最常见的问题是缺少xcb-render-util这种“不大不小却到处被依赖”的库。
4.2 手动交叉编译 xcb:一个标准流程示例
以libxcb最核心的依赖xcb-proto和libxcb为例,流程大致是“配置→编译→安装到sysroot”。先准备xcb-proto:
wget https://xcb.freedesktop.org/dist/xcb-proto-1.14.1.tar.gz tar xzf xcb-proto-1.14.1.tar.gz && cd xcb-proto-1.14.1 ./configure --prefix=$SYSROOT/usr make -j$(nproc) sudo make installxcb-proto提供的是XML协议描述文件,本身没有代码,所以不存在交叉编译的问题,直接装进sysroot就行。
接着编libxcb。这里有个烦人的地方:libxcb用autotools,configure时会检测python能否正确处理xcb-proto的文件,而这个脚本会去引用系统目录,所以需要把xcb-proto的安装路径加进PYTHONPATH,同时用PKG_CONFIG_PATH让pkg-config能找到xml文件:
wget https://xcb.freedesktop.org/dist/libxcb-1.14.tar.gz tar xzf libxcb-1.14.tar.gz && cd libxcb-1.14 export PKG_CONFIG_PATH=$SYSROOT/usr/lib/pkgconfig export PYTHONPATH=$SYSROOT/usr/lib/python3/dist-packages:$PYTHONPATH ./configure --host=aarch64-linux-gnu --prefix=$SYSROOT/usr \ --disable-shared --enable-static \ --without-doxygen --without-python make -j$(nproc) sudo make install--disable-shared --enable-static这两个参数是“完全静态化”的开关。如果只写--enable-static不写--disable-shared,configure会同时生成so和a文件,链接时默认优先动态链接,最终的可执行文件依然会依赖so。后面所有依赖库的编译,都要保证“只出.a”的策略,这样Qt在链接时才不会选错。
4.3 编译freetype和fontconfig的小门道
freetype和fontconfig在Qt GUI中的角色非常重要。freetype负责字体光栅化,fontconfig负责字体查找与匹配,两者缺一,QPainter画文本时会大几率出现空白。这里只需要注意fontconfig在静态链接时必须显式依赖freetype和expat,configure它之前先把依赖编好,否则会报找不到头文件。
# freetype 2.10.4 ./configure --host=aarch64-linux-gnu --prefix=$SYSROOT/usr \ --disable-shared --enable-static --without-harfbuzz make -j$(nproc) sudo make install # fontconfig 2.13.1 ./configure --host=aarch64-linux-gnu --prefix=$SYSROOT/usr \ --disable-shared --enable-static \ CPPFLAGS="-I$SYSROOT/usr/include -I$SYSROOT/usr/include/freetype2" make -j$(nproc) sudo make install关于harfbuzz,我在freetype里特意用--without-harfbuzz禁用了它。原因是Qt自己会编译一个harfbuzz,如果freetype又静态链接了一份harfbuzz,后面和Qt链接时会出现重复符号冲突,属于“能跑但很脏”的状态。真正干净的做法是:外部只给Qt提供freetype的头文件和字体数据,harfbuzz字形整形交给Qt自带版本统一管理,这样避免扯皮。
fontconfig编译时还有个细节:它的头文件里经常会依赖fcfreetype.h,这个头文件声明了一堆跨库函数,编译Qt项目时必须让编译器能在include路径里同时找到fontconfig和freetype2两个目录,不然在链接时会出现“undefined reference toFcFreeTypeCharIndex”这类问题。
4.4 把第三方库同步进工具链自带sysroot
编译安装到/opt/aarch64-sysroot/usr之后,我还会把这些新生成的.a文件同步一份到工具链自带的sysroot里。这一步不是必须的,但能大幅简化后续链接参数的书写。原因很简单,工具链自带的sysroot是交叉编译器的“默认地盘”,Qt configure在检测这些库时,如果走默认头文件路径就能找到,就不需要我反复传CPPFLAGS和LDFLAGS。
sudo cp -a /opt/aarch64-sysroot/usr/include/xcb* /usr/aarch64-linux-gnu/include/ sudo cp -a /opt/aarch64-sysroot/usr/lib/aarch64-linux-gnu/libxcb* /usr/aarch64-linux-gnu/lib/这个操作要注意命名习惯:不同发行版在64位库目录路径上的差异很大,有的叫lib/aarch64-linux-gnu,有的直接叫lib。同步前务必用find确认一下目标目录结构,杜绝路径写错导致Qt死活找不到库。
5. Qt 5.14.2 源码的下载与configure配置
5.1 下载源码并准备mkspec
Qt源码建议从官方仓库或者镜像站下载源码包,因为后续可能要根据自己环境修改mkspec,用git仓库会比较方便。这里用5.14.2分支:
git clone -b v5.14.2 https://code.qt.io/qt/qt5.git cd qt5 perl init-repository --module-subset=qtbase,qtdeclarative,qtsvg,qttoolsinit-repository这步比较关键,它能帮我们把Qt5拆成各个子模块,如果只做GUI嵌入式开发,拿到qtbase、qtdeclarative、qtsvg、qttools就足够日常使用了。若需要网络、串口、Modbus等能力,需要额外加qtnetworkauth、qtserialport模块。
先来看mkspec。Qt官方仓库里已经带了linux-aarch64-gnu-g++这个平台配置,路径在qtbase/mkspecs/linux-aarch64-gnu-g++。但我们在静态交叉编译时,需要告诉Qt这个平台的编译器路径、sysroot位置以及静态编译的指定方式。最简单的做法是修改这个mkspec目录下的qmake.conf,它会让Qt的构建系统识别到正确的编译环境。
cat qtbase/mkspecs/linux-aarch64-gnu-g++/qmake.conf # # qmake configuration for building with aarch64-linux-gnu-g++ # MAKEFILE_GENERATOR = UNIX CONFIG += incremental QMAKE_INCREMENTAL_STYLE = sublib include(../common/linux.conf) include(../common/gcc-base-unix.conf) include(../common/g++-unix.conf) load(device_config) load(qt_config) QMAKE_CC = aarch64-linux-gnu-gcc QMAKE_CXX = aarch64-linux-gnu-g++ QMAKE_LINK = aarch64-linux-gnu-g++ QMAKE_LINK_SHLIB = aarch64-linux-gnu-g++如果你用的是Linaro工具链且只改了编译器名,就可以直接复用这个文件。如果自己另写了工具链路径,务必在文件里显式写死完整路径。很多初学者在这儿踩的坑是:只改了configure的-xplatform参数,却忘了Opensource版Qt默认使用的主机编译器仍然保留为gcc/g++,结果编译出一堆x86对象文件,后面链接时一片混乱。
5.2 configure参数梳理与逐项解释
进入源码目录后,执行configure是全局最关键的一步。参数写错、路径写错,后面排查问题的成本是几何级上升。我这次实际使用的配置如下(以纯字符界面Linux环境为例):
./configure -static -release -opensource -confirm-license \ -xplatform linux-aarch64-gnu-g++ \ -prefix /opt/Qt-5.14.2-aarch64-static \ -sysroot /opt/aarch64-sysroot \ -no-opengl \ -no-gui \ -no-dbus \ -no-icu \ -nomake examples \ -nomake tests \ -skip qtdeclarative \ -qt-zlib \ -qt-libpng \ -qt-libjpeg \ -qt-freetype \ -qt-harfbuzz \ -no-feature-cups \ -no-feature-printdialog \ -no-feature-printer逐一解释每个核心参数:
-static:Qt代码将生成为.a静态库。如果这里不写,Qt只会编出so,后面你做静态链接时会发现根本找不到libQt5Core.a。
-release:编译release版本,不生成debug信息,体积更小。开发中如果想后期调栈信息,可以改成-debug,但静态编译的调试版会极大膨胀,而且对交叉编译的现场分析帮助很有限,不推荐。
-xplatform linux-aarch64-gnu-g++:指定目标平台配置,指向之前确认过的mkspec。
-sysroot /opt/aarch64-sysroot:这个是关键中的关键。Qt configure脚本会以这个路径为根去查找目标系统里的所有头文件和库,不写它的话,使用的还是本机x86目录,编译出的Qt代码就会混杂x86库依赖。
-no-opengl -no-gui:嵌入式纯命令行或仅使用QCoreApplication的场景可以这样关掉GUI模块。但如果你的目标是带Qt Quick或QWidget界面的应用,就必须保留-gui,并配好libxcb关键依赖。
-no-icu:ICU对QtWebKit、QtQuick的多语言支持有帮助,但其交叉编译麻烦且体积巨大。静态编译时我通常关闭它,界面上的多语言问题完全可以通过Qt自带的QTextCodec或依赖系统字符编码转换解决。如果确认目标板需要ICU,建议提前准备ICU的aarch64静态库,再打开-icu。
-qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-harfbuzz:强制Qt使用源码自带的这些库版本,而不是链接系统库。这样做的好处是少折腾外部依赖,坏处是可执行文件体积进一步增大。在本方案里,外部只保留了xcb、xkbcommon、fontconfig这几个无法绕开的库,其他全靠Qt自带。
5.3 configure过程中常见的检测失败与处理
执行configure后,经常会出现几个典型失败:
第一是“The specified sysroot is not valid”,多半是因为sysroot路径下缺少Qt头文件要求的基础目录。Qt会检查$SYSROOT/usr/include/stdlib.h这类存在性,如果目标rootfs做的是精简版,可能根本没有这些开发头文件。这时需要回到第3步的rootfs准备,确认它带build-essential或者development相关的包;另一个常用手段是把本机交叉工具链自带的/usr/aarch64-linux-gnu/include下的头文件全部拷贝到sysroot的/usr/include,诸葛填坑。
第二是“xcb not found”,这时需要回看第4步的第三库编译是否真的把.a和头文件放到了sysroot的正确位置,同时检查pkg-config能否搜得到它们:
export PKG_CONFIG_PATH=/opt/aarch64-sysroot/usr/lib/pkgconfig PKG_CONFIG_SYSROOT_DIR=/opt/aarch64-sysroot pkg-config --exists xcb && echo yes第三是“Cannot find -lGL”。对于不需要OpenGL的纯显示项目,我们直接在configure里加-no-opengl即可关闭。但Qt的图形平台插件在linuxfb和xcb模式下有时仍会引用OpenGL的头文件,所以保险做法是在环境变量里把OPENGL相关的库禁掉,或者在configure参数里补充-no-feature-opengl。
我实际执行完configure后,日志大概长这样:
Qt is now configured for building ... Running configuration tests... The configuration is done.如果前面参数有问题,这一阶段的日志就会提前中断并提示错误点,所以需要养成每次执行configure之后立刻检查输出日志的习惯,别等make跑一半再回来找问题。
5.4 开始编译并安装Qt静态库
configure通过之后,就可以正式编译了。Qt的源码编译比较吃CPU,推荐把-j参数调到主机核心数附近,但要注意别一次性给满,否则主机内存不够时很容易OOM,报错会极其难排查:
make -j8 sudo make install整个编译耗时取决于主机性能。我用的是一台8核16线程的机器,全量编译qtbase+qtdeclarative+qtsvg+qttools,大概四十分钟左右。编完后检查安装目录:
ls /opt/Qt-5.14.2-aarch64-static/lib/ # 应当看到 libQt5Core.a、libQt5Gui.a、libQt5Widgets.a 等一系列静态库如果只看到so文件,说明configure的-static参数没有生效或者被某些模块覆盖了,需要倒回去查mkspec。
6. 写一个测试程序:交叉编译与静态链接验证
6.1 编写最小测试工程
验证环境是否真正可用的标准姿势,是写一个最小化的Qt程序并把它编译成目标板可执行单元。我一般先写一个不依赖GUI的QtCore控制台程序,再写一个带QWidget窗口的程序,层层递进。
先建一个目录hello_qt,里面放三个文件:main.cpp、hello_qt.pro、一个简单的构建脚本。
main.cpp的内容:
#include <QCoreApplication> #include <QDebug> int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); qDebug() << "Hello from Qt static, version:" << QT_VERSION_STR; return 0; }hello_qt.pro的内容:
QT -= gui CONFIG += console c++11 TARGET = hello_qt SOURCES += main.cpp这里QT -= gui是为了让工程只链接QtCore,验证最基础的部分。
然后手动调用交叉编译工具链下的qmake。
export PATH=/opt/Qt-5.14.2-aarch64-static/bin:$PATH /opt/Qt-5.14.2-aarch64-static/bin/qmake hello_qt.pro make编译完成后用file命令查看类型:
file hello_qt正常的输出应该是:
hello_qt: ELF 64-bit LSB executable, ARM aarch64, dynamically linked (uses shared libs), for GNU/Linux 3.7.0, stripped注意到这里虽然加了Qt的-static,文件依然会标称“dynamically linked”,原因就是它仍然动态链接了glibc的so。但这正是我们期望的“大多数依赖静态、系统基础库动态”的混合方案。
6.2 验证静态链接是否真的彻底
如何确认这个程序启动时不需要任何Qt的.so?最简单的方法是看它的动态依赖:
aarch64-linux-gnu-readelf -d hello_qt | grep NEEDED输出里应当只有libstdc++.so.6、libgcc_s.so.1、libc.so.6这类系统库,绝不应出现libQt5Core.so.5的字样。如果看到了,说明工程的makefile里没有真正使用Qt的静态库,多半是qmake的路径没有指到我们自己编出来的那套,系统的PATH环境变量里还有另一套动态Qt的bin目录在抢先。
另外可以用ldd工具交叉观察一下(实际上直接对aarch64二进制跑x86的ldd会报“not a dynamic executable”,所以要用带--root参数的交叉环境,或者直接在目标板上跑):
aarch64-linux-gnu-readelf -d hello_qt | grep -i needed这样能看到真实依赖。
6.3 测试GUI程序的实战准备
Qgui涉及的平台插件编译和运行时加载策略,远比纯QtCore复杂。在配置Qt时只要没加-no-gui,就会编出GUI模块,但要把它跑起来,还得先搞定平台插件。
当你在目标板上运行带Qt Widgets界面的程序时,Qt会通过platform plugin去创建一个窗口。可选插件有linuxfb(直接操作framebuffer)、eglfs(OpenGL渲染)、xcb(在X11桌面环境里运行)等。静态编译下,默认不会像动态库那样自动从插件目录加载so,而是需要把插件代码直接链接进主程序,并在代码里显式加载。
最顺滑的方案是用xcb插件在带X11桌面(比如树莓派的Raspberry Pi OS Desktop)的板子上运行。编译时只要sysroot里有libxcb的静态库,Qt configure阶段就会自动开启xcb plugin,最终生成的库文件libqlinuxfb.a和libqxcb.a会存放在Qt安装目录的plugins/platforms/下。静态链接时你需要让qmake知道要把哪些插件打包进程序。
最省事的做法是写一个自定义的qt.conf文件,或是在main.cpp里手动注册插件:
#include <QApplication> #include <QLabel> #include <QtPlugin> Q_IMPORT_PLUGIN(QXcbIntegrationPlugin) int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label("Hello, aarch64 static Qt"); label.resize(320, 200); label.show(); return app.exec(); }同时.pro文件里加入:
QTPLUGIN += qxcb这样链接进程序后,在X11环境执行时无需额外部署插件目录。
7. 部署到目标板与运行中的常见坑
7.1 拷贝与运行
把编译好的hello_qt程序和那套带GUI界面的hello_gui程序直接拷到目标板上,放到任意用户目录下执行:
./hello_qt ./hello_gui如果一切正常,QtCore程序会打印版本,GUI程序会弹出一个小窗口。理论上不需要设置QT_QPA_PLATFORM_PLUGIN_PATH,因为静态链接的插件已经进二进制了。这里最常见的错误就是仍然沿用动态Qt的部署习惯去手动导出各种环境变量,结果反而把静态运行环境搞乱。
7.2 运行时的字体与渲染问题
在纯命令行环境下用linuxfb插件跑GUI,常常会遇到中文字体显示成方块的毛病。原因不是Qt编译错了,而是板子上没装中文字体。静态编译不会把字体文件也打包进去。解决办法是在目标板上安装一份字体文件,比如从Windows或Linux桌面拷贝一个思源黑体的ttc/otf放到/usr/share/fonts/truetype/,然后执行fc-cache -f重建字体缓存。
如果界面出现输入法、X11键盘布局不工作,多半是xkbcommon库没有正确加载。静态链接xkbcommon时,它依赖的XKB_CONFIG_ROOT环境变量有时候不会被Qt默认传递,导致keymap索引失败。此时在板子上运行:
export QT_XKB_CONFIG_ROOT=/usr/share/X11/xkb这个设置通常能解决大部分按键无响应、布局错误的问题。原理是Qt xcb插件底层调用xkbcommon,而xkbcommon需要从文件系统读取键盘布局数据,这个数据目录经常不在默认搜索路径里。
7.3 程序体积为什么这么大
静态编译的Qt程序动不动就几十甚至上百MB。遇到体积焦虑是正常的,但得知道哪些因素影响最大。完全静态的Qt Widgets程序往往会占30~80MB,其中最大的几块是:QtGui和QtWidgets库本身、freetype和xcb插件、harfbuzz字形整形引擎。如果目标是纯命令行工具、只需要QtCore和QtNetwork,体积会大幅缩小到15MB左右。
有几个立竿见影的体积优化手段:
- 编译时用
-no-icu、-no-opengl,会大幅减少QtGui带的依赖; - 在.pro里只
QT += core gui widgets,别把network、sql等无关模块带上; - 编译选项里加
-s(strip)去掉符号表,一般能再瘦身20%~30%; - 用upx压缩可执行文件,不过upx压缩后的启动时间和内存映射行为会变化,嵌入式上谨慎使用。
这些都是在“程序功能完整”和“体积可控”之间做权衡,具体取舍看项目需求。
8. 常见问题与排查技巧实录
8.1 常见错误与对策速查表
| 错误现象 | 直接原因 | 解决办法 |
|---|---|---|
| configure提示“The specified sysroot is not valid” | sysroot路径缺失必需开发包/头文件 | 补全sysroot的头文件与软链接,或检查路径拼写 |
| make时报“cannot find -lxcb” | 第三方库没有安装到工具链默认搜索路径 | 确保libxcb.a在sysroot/usr/lib下,且路径对得上 |
| 链接时报“undefined reference to xcb_*” | libxcb的依赖库不全或顺序不对 | 确认libxcb-render-util等扩展库都已编译,链接时使用-lxcb-render-util -lxcb-image等补充 |
| 链接时报“recompile with -fPIC” | 编译静态库时未启用位置无关代码 | 第三方库configure时加上CFLAGS="-fPIC" |
| 运行时提示“Could not load the Qt platform plugin xcb” | 静态插件未导入或依赖的xkbcommon库缺失 | 检查Q_IMPORT_PLUGIN、QTPLUGIN配置,确保xkbcommon静态库已编入 |
| 程序运行直接段错误 | glibc版本或内核太旧,与编译工具链不匹配 | 换用与目标板glibc匹配的工具链,或更新内核/rootfs |
| 中文/文字全部显示为方块 | 目标板缺少对应字体 | 安装中文字体并执行fc-cache,用fc-list确认字体可见 |
| 程序退出时有内存泄漏或崩溃 | fontconfig或freetype的静态数据未初始化 | 检查有没有重复初始化,必要时在入口调用QApplication::setFont前确保字体配置加载 |
8.2 链接阶段的Fpic问题
在静态编译xcb、xkbcommon这类库时,我们踩过最反复的一个坑就是-fPIC。因为Qt自身编译成静态库时为了能被最终可执行文件链接,默认会使用fPIC(不加入-fPIC的代码无法undergo shared object link)。但第三库通常在configure时并不会默认开启fPIC,尤其是在32位平台上更明显,aarch64上有时也会莫名触发。
解决方式非常朴素:给每个第三方库的configure加一个通用的CFLAGS:
export CFLAGS="-fPIC --sysroot=$SYSROOT" export CXXFLAGS="-fPIC --sysroot=$SYSROOT"如果是在某个库的Makefile里手动修改,也要把这行加到全局变量里。编译完检查一下生成的.a里目标文件是否开了PIC:
aarch64-linux-gnu-ar t libxcb.a | head看到的目标文件都应当能用aarch64-linux-gnu-readelf -r查看是否有GOT表的重定位记录,如果没有说明没编出PIC代码。
8.3 工具链与glibc版本不一致的坑
Linaro官方历史版本里GCC 7/9/10并存,它们各自配套的libc版本并不一样。用GCC 9.4编译的程序,链接的glibc主要需要2.27+。如果目标板上的glibc回合是2.24,则会直接跑不起来,连readelf都能看出问题:
aarch64-linux-gnu-readelf --version-info hello_qt | grep GLIBC_这里会列出程序所有用到的GLIBC版本号。假如有2.28而板子是旧的,那么就只有两条路:换新的rootfs,或者下调工具链版本。静态编译解决不了glibc的系统级问题,因为为了保底我们不会静态链接glibc,所以这一点务必在一开始就确认好。
8.4 静态链接xcb与xkbcommon时“又臭又长”的库顺序
在链接静态库时,库的顺序非常关键。gcc在处理-l参数时,被链接的库只能解析它前面未解析的符号,如果库A引用了库B的符号,B就必须出现在A之后。Qt静态库、xcb静态库、xkbcommon静态库之间互有依赖,顺序写错就会出现“undefined reference”却怎么找也找不到实际缺少符号的库。
一个通用原则是“从用户程序开始,越底层的库越往后放”。Qt的链接在qmake生成的Makefile里通常已经处理好了,但如果你手动用gcc命令链接遇到这类问题,直接按下面的顺序组织:
-lQt5Widgets -lQt5Gui -lQt5Core -lxcb -lxkbcommon -lfreetype -lfontconfig -lz -lpthread -ldl实践中把-lpthread -ldl放在最末尾基本可以覆盖线程和动态加载的需求。如果需要网络,还要在Qt5Network后面补上-lssl -lcrypto。
8.5 一次真实排查:运行时找不到xkbcommon
有一个案例很有代表性。程序在开发板上启动立即崩溃,用dmesg能看到段错误,但代码里的初始化日志一行都没打出来。后来通过gdb远程调试,发现段错误发生在XkbKeymap相关的初始化函数里。检查之后发现xkbcommon库确实编译进静态库了,但运行环境里缺少/usr/share/X11/xkb/rules/evdev.xml这个文件。原因是xkbcommon在解析键盘布局时会在几个路径里找规则文件,如果找不到就返回空指针,Qt没有做空指针保护。
解决方式很简单:把开发板上/usr/share/X11/xkb整个目录拷到目标板的对应位置,或者设置QT_XKB_CONFIG_ROOT指向正确的目录。这类问题在官方文档里写得非常隐晦,基本只有踩过坑才明白。所以如果你的Qt程序交叉编译一切正常、但一跑就在键盘/事件初始化阶段崩掉,优先检查目标板的xkb数据和权限。
9. 扩展与进阶:更多模块与自动化构建
9.1 如何添加QtNetwork、QtSerialPort等模块
本手册第一部分只编译了qtbase等几个基础模块。如果项目需要网络、串口、Modbus等能力,可以在init-repository时把模块清单扩大:
perl init-repository --module-subset=qtbase,qtdeclarative,qtsvg,qttools,qtnetworkauth,qtserialport,qtmqtt然后重新configure时不需要删掉已经编好的Qt库,Qt的构建系统会检测已有的模块并增量编译。注意QtNetwork如果有SSL需求,必须提前准备OpenSSL的aarch64静态库,并在 configure 参数中加上-openssl-linked,同时把它加进外部库搜索路径。如果只是普通TCP/UDP通信,不涉及HTTPS或TLS,那-no-openssl即可,配置起来非常省心。
这里的经验是:能用Qt自带模块解决的,就不要额外引入需要单独编译的第三方库。qtserialport虽然底层操作的是Linux tty,但不必额外写任何C库依赖;而qtmqtt比直接装mosquitto客户端库省事得多。
9.2 一个自动化构建脚本的思路
环境搭好以后,手动执行这一长串命令就不太经济了。我通常会把整个过程固化成Shell脚本,写成一个“一键构建”工具。脚本的核心思路是模块化分层:第一阶段准备sysroot和第三方库,第二阶段配置并编译Qt,第三阶段编译用户程序。脚本里要特别注意幂等性,比如重复执行configure前先删除build目录,避免旧缓存干扰。
脚本也很适合接入CI,比如每次代码提交后自动跑一遍静态编译,把生成的可执行文件归档成发布包。对于团队协作,把工具链、sysroot、Qt安装目录做成一个可打包的tar归档,新人拉下来直接就能用,比各自重新编译省下半天时间。
9.3 静态编译与动态编译的切换小技巧
在项目早期做界面调试时,静态编译每次改动一点都要重新链接、拷贝到板子,效率很低。我的习惯是同时搭建两套环境:一套动态编译环境用于开发时的快速迭代,一套静态编译环境用于最终的发布验证。动态环境里配置Qt时把-static去掉、-prefix改到另一个目录,其余参数完全一致。调试时把动态依赖库拷到板子的/usr/lib或者程序同目录下,用LD_LIBRARY_PATH指过去就行。等到功能稳定,切到静态环境编译,跑一遍全量回归测试,最终发布用静态产物。这个习惯帮我节省了大量等链接和等拷贝的时间。
10. 收尾:一些心得体会
做完整套环境之后,最大的感受是Qt的静态交叉编译并不是一个“跑个脚本就完成”的简单任务,而是对工具链、系统库依赖、链接器行为三方面知识有相当要求的综合工程。真正花费时间的往往不是Qt本身的编译,而是那些隐藏在xcb、xkbcommon、fontconfig里的第三方库细节,以及嵌在sysroot和工具链版本之间的天坑。
我个人在实际操作中的体会是:做这类环境搭建,一定不要追求一次成功,而是要有“日志驱动”的思维。每执行一个大步骤,就把中间的产物保留下来,然后用file、readelf、ldd这样的工具去检查中间产物是否符合预期。与其到最后链接阶段面对几百行报错,不如在configure阶段就把每一个检测警告看明白。
如果这篇手册能帮你少排查几个小时的古怪问题,那这功夫就没白费。后面如果再编译带OpenGL的Qt Quick环境,或者想把这套流程移植到RISC-V架构上,思路与这里的步骤是完全一致的。祝各位一次编译通过,顺利把程序跑起来。