1. 为什么要在aarch64上折腾Qt静态编译
如果你手上有Orange Pi CM5、树莓派这类aarch64开发板,又打算用Qt做界面,迟早会撞上这个问题:板子上跑Qt程序,要么依赖一大堆动态库,要么版本对不上,部署一次能折腾半天。动态链接的Qt程序拷到板子上,ldd一看几十个.so找不到,这种场景我遇到过太多次了。
静态编译Qt的核心价值就一个:把Qt运行时全部塞进可执行文件里,拷过去就能跑,不用在目标板上装Qt环境。这在嵌入式量产、现场部署、给客户演示的场景下,省掉的是成倍的沟通成本和环境排查时间。
但静态编译Qt5.14.2到aarch64,坑比想象中多。Qt官方对静态编译的支持是"能用但不推荐",尤其是涉及serialport、svg、webengine这些模块时,配置项写错一个字母就是几小时的编译白费。这篇手册就是把我从零搭建的完整过程拆开讲,包括交叉编译工具链的选择、configure参数的取舍逻辑、编译过程中真实踩到的报错,以及最后怎么验证产物真的能在板子上跑起来。
适合谁看:有Linux基础、用过Qt但没做过交叉编译的开发者;正在用aarch64开发板做项目的嵌入式工程师;以及被"unknown module in qt: serialport"这类报错折磨过的人。全文基于Qt5.14.2 + aarch64 + 静态链接这条主线,但配置思路对Qt5.15.x同样适用。
提示:静态编译Qt的构建时间在普通四核机器上大约2到4小时,建议放在性能较好的x86_64主机上做,不要在开发板上直接编译。
2. 交叉编译工具链的选型与验证
2.1 为什么不用板子自带的gcc
很多人第一反应是在开发板上直接装Qt编译,觉得"原生编译最省事"。我试过在Orange Pi CM5上直接编译Qt5.14.2,结果是编译到一半内存爆了,swap拉满,进程被OOM Killer干掉。aarch64开发板通常内存2G到8G,Qt的编译峰值内存需求远超这个数,尤其是qtdeclarative和qtwebengine模块。
所以正确做法是:在x86_64主机上用交叉编译工具链,产出aarch64的二进制。主机负责编译,板子只负责运行。
工具链的选择上,主流有三类:
| 工具链来源 | 典型前缀 | 适用场景 | 注意事项 |
|---|---|---|---|
| Linaro官方 | aarch64-linux-gnu- | 通用aarch64 Linux | 版本较老,glibc版本需匹配 |
| 芯片厂商SDK | 如aarch64-none-linux-gnu- | 特定SoC优化 | 与板子BSP绑定,兼容性最好 |
| 发行版仓库 | gcc-aarch64-linux-gnu | Ubuntu/Debian主机 | 安装最方便,推荐入门 |
我个人的选择是Ubuntu主机上直接apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu,原因是安装简单、版本可控、和主机glibc兼容性好。厂商SDK的工具链虽然针对性强,但经常缺一些头文件,配置起来反而麻烦。
2.2 工具链安装与交叉编译能力验证
安装命令很直接:
sudo apt update sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu装完之后必须验证,不能假设它能用。验证分两步:
第一步,确认工具链存在且版本正常:
aarch64-linux-gnu-gcc --version aarch64-linux-gnu-g++ --version第二步,写一个最小测试程序,确认能编译出aarch64架构的可执行文件:
echo 'int main(){return 0;}' > test.c aarch64-linux-gnu-gcc test.c -o test_aarch64 file test_aarch64file命令的输出应该是ELF 64-bit LSB executable, ARM aarch64。如果显示的是x86-64,说明你调用的还是主机gcc,工具链没生效。
注意:交叉编译工具链的glibc版本必须小于等于目标板上的glibc版本。主机工具链用的glibc太新,编出来的程序在板子上会报
GLIBC_2.xx not found。用aarch64-linux-gnu-gcc -print-file-name=libc.so.6可以查看工具链链接的glibc版本,和板子上ldd --version对比一下。
2.3 sysroot的准备:最容易被忽略的一步
交叉编译Qt时,configure需要知道目标系统的头文件和库在哪里,这就是sysroot。很多人编译失败就是因为sysroot没配对。
sysroot的来源有两种:一是从开发板上直接拷贝/usr/include和/usr/lib,二是用工具链自带的sysroot。我推荐从板子上拷贝,因为这样能保证编译时链接的库版本和板子完全一致。
操作方式:
# 在开发板上执行,打包系统库 tar czf sysroot.tar.gz /usr/include /usr/lib /lib # 拷回主机后解压到指定目录 mkdir -p /opt/aarch64-sysroot tar xzf sysroot.tar.gz -C /opt/aarch64-sysroot如果板子空间紧张,至少要把/usr/include下的基础头文件和/usr/lib下的libc、libpthread、libdl、libm拷过来。Qt的configure会检查这些库是否存在。
3. Qt5.14.2源码获取与configure参数拆解
3.1 源码包的选择:为什么是5.14.2
Qt5.14.2是Qt5系列里最后一个长期支持版本(LTS),官方维护到2025年。相比5.12.x,它对aarch64的支持更完善;相比5.15.x,它的静态编译配置更稳定,社区踩坑记录也更多。
源码包从Qt官方归档下载qt-everywhere-src-5.14.2.tar.xz。注意不要下成qt-everywhere-opensource-src的旧版本,5.14之后官方改了命名。下载后校验一下md5,避免传输损坏导致解压后文件缺失。
wget https://download.qt.io/archive/qt/5.14/5.14.2/single/qt-everywhere-src-5.14.2.tar.xz md5sum qt-everywhere-src-5.14.2.tar.xz tar xf qt-everywhere-src-5.14.2.tar.xz cd qt-everywhere-src-5.14.23.2 configure参数逐项解释
configure是整个编译过程的核心,参数写错就是几小时白费。我把关键参数分成四组来讲。
第一组:平台与工具链
-xplatform linux-aarch64-gnu-g++ -device-option CROSS_COMPILE=aarch64-linux-gnu- -sysroot /opt/aarch64-sysroot-xplatform指定的是qtbase/mkspecs/devices/或qtbase/mkspecs/下的mkspec目录名。linux-aarch64-gnu-g++这个mkspec在5.14.2里是存在的,如果找不到,需要自己从linux-arm-gnueabi-g++复制一份改。
-device-option CROSS_COMPILE=是给mkspec里的变量赋值,告诉qmake用哪个前缀的工具链。
第二组:静态编译与安装路径
-static -prefix /opt/qt5.14.2-aarch64-static -hostprefix /opt/qt5.14.2-host-static开启静态编译,这是核心。-prefix是目标板上的安装路径,-hostprefix是主机上qmake等工具的安装路径。这两个必须分开,否则主机上的qmake会指向aarch64的库,导致主机工具无法运行。
第三组:模块裁剪
-skip qtwebengine -skip qtwebview -skip qtquickcontrols -skip qtquickcontrols2 -skip qtgamepad -skip qtserialbus-skip是跳过不需要的模块。qtwebengine基于Chromium,静态编译几乎不可能成功,直接跳过。qtquickcontrols和qtquickcontrols2如果不用QML可以跳过,能省大量编译时间。
但注意:-skip qtserialport要慎重。如果你要用串口,这个模块必须保留,而且静态编译serialport有个已知问题,后面会讲。
第四组:功能开关
-no-opengl -no-openssl -no-cups -no-glib -no-iconv -no-feature-dbus -no-feature-icu这些是关闭不需要的功能。-no-opengl在无GPU的板子上必须加,否则Qt会尝试链接OpenGL库导致失败。-no-glib关闭glib依赖,减少运行时依赖。
完整的configure命令:
./configure \ -prefix /opt/qt5.14.2-aarch64-static \ -hostprefix /opt/qt5.14.2-host \ -xplatform linux-aarch64-gnu-g++ \ -device-option CROSS_COMPILE=aarch64-linux-gnu- \ -sysroot /opt/aarch64-sysroot \ -static \ -release \ -opensource \ -confirm-license \ -no-opengl \ -no-openssl \ -no-cups \ -no-glib \ -no-iconv \ -no-feature-dbus \ -no-feature-icu \ -skip qtwebengine \ -skip qtwebview \ -skip qtquickcontrols \ -skip qtquickcontrols2 \ -skip qtgamepad \ -skip qtserialbus \ -nomake examples \ -nomake tests-nomake examples -nomake tests能省掉大量编译时间,examples和tests对最终产物没用。
3.3 configure报错的常见原因
configure阶段最常见的报错是Cannot find suitable mkspec。原因是-xplatform指定的目录不存在。解决方法是去qtbase/mkspecs/下确认目录名,5.14.2里aarch64相关的有linux-aarch64-gnu-g++和linux-arm-gnueabi-g++,前者是64位,后者是32位。
第二个常见报错是sysroot not found。检查-sysroot路径是否存在,以及路径下是否有usr/include和usr/lib。
第三个是CROSS_COMPILE not set。这个报错说明mkspec里的QMAKE_CXX等变量没被正确赋值,检查-device-option的写法,等号两边不能有空格。
4. 编译过程中的真实踩坑记录
4.1 serialport模块的静态编译陷阱
如果你保留了qtserialport模块,编译到qtserialport时大概率会遇到这个报错:
:-1: error: Unknown module(s) in QT: serialport这个报错的根因是:静态编译时,serialport模块的依赖没有正确传递。Qt的serialport模块依赖core和network,但静态链接时qmake的模块依赖解析会出问题。
解决方案是在configure时显式加上-qt-libudev,让serialport使用libudev而不是内置的枚举方式:
-qt-libudev同时确保sysroot里有libudev的头文件和库。如果板子上没有udev,可以改用-no-libudev,但这样serialport的自动端口枚举功能会受限。
另一个坑是:编译完serialport后,在项目里QT += serialport仍然报unknown module。这是因为静态Qt的.prl文件没有正确生成。检查/opt/qt5.14.2-aarch64-static/lib/libQt5SerialPort.prl是否存在,如果不存在,说明serialport模块编译失败但被跳过了。需要单独进qtserialport目录重新qmake && make。
4.2 编译到qtdeclarative时的内存爆炸
qtdeclarative是QML引擎,编译时内存占用极高。在4G内存的主机上,编译到qquicktextnode.cpp时经常被OOM Killer干掉。
解决方法有三个:一是加swap,二是限制并行编译数,三是单独编译这个模块。
加swap最直接:
sudo fallocate -l 8G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile限制并行数用make -j2而不是make -j$(nproc)。虽然慢,但不会爆内存。
如果还是不行,单独编译:
cd qtdeclarative /opt/qt5.14.2-host/bin/qmake make -j1 make install4.3 静态链接时的符号冲突
编译到最后链接阶段,可能会遇到multiple definition of的报错。这通常是Qt的静态库和系统库有同名符号。
典型的是qtbase/src/corelib/global/qglobal.cpp里的qVersion()和某些系统库冲突。解决方法是加链接选项-Wl,--allow-multiple-definition,但这是治标不治本。
更稳妥的做法是在configure时加-no-feature-...关掉冲突的功能。比如-no-feature-getentropy可以避免和glibc的getentropy冲突。
提示:静态链接的符号冲突很难一次性定位,建议在configure阶段就尽量精简功能,减少和系统库的交集。
4.4 编译时间与并行策略
完整编译Qt5.14.2静态版,在8核16G的主机上,用make -j8大约需要1.5到2小时。如果内存只有8G,建议make -j4。4G内存的话make -j2,并且必须加swap。
编译过程中可以用make -j8 2>&1 | tee build.log把日志存下来,方便出错时回溯。日志文件会很大,几个G是正常的。
5. 产物验证与目标板部署
5.1 验证静态Qt的完整性
编译安装完成后,先检查/opt/qt5.14.2-aarch64-static/目录结构:
ls /opt/qt5.14.2-aarch64-static/lib/应该能看到libQt5Core.a、libQt5Gui.a、libQt5Widgets.a等静态库文件。如果只有.so没有.a,说明-static没生效。
再检查主机工具:
/opt/qt5.14.2-host/bin/qmake -v输出应该是QMake version 3.1,并且Using Qt version 5.14.2。注意这个qmake是主机上运行的,不是aarch64的。
5.2 写一个最小测试程序
验证静态Qt能不能用,写一个最简单的Widgets程序:
#include <QApplication> #include <QLabel> int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label("Hello aarch64 static Qt"); label.show(); return app.exec(); }用静态Qt的qmake编译:
/opt/qt5.14.2-host/bin/qmake test.pro make file testfile输出应该是ELF 64-bit LSB executable, ARM aarch64, statically linked。注意statically linked这个关键词,如果显示dynamically linked,说明链接的还是动态库。
再用ldd test检查,静态链接的程序应该输出not a dynamic executable。
5.3 部署到开发板并运行
把编译好的test拷到开发板上:
scp test root@192.168.1.100:/root/在板子上直接运行:
chmod +x test ./test如果板子有显示环境(X11或Wayland),应该能看到窗口。如果没有显示环境,可以用-platform offscreen测试:
./test -platform offscreen程序不报错就说明静态Qt的运行时是完整的。
注意:静态Qt程序体积会很大,一个简单的Widgets程序可能就有20M到30M。如果板子存储紧张,可以用
strip去掉符号表,能减小30%左右。但strip后调试信息会丢失,量产版本可以strip,调试版本保留。
5.4 常见运行时问题
静态Qt程序在板子上跑不起来,最常见的原因是缺少字体。Qt的Widgets需要字体来渲染文字,如果板子上没有/usr/share/fonts,程序会报QFontDatabase: Cannot find font directory。
解决方法是在板子上装一个最小字体包,或者把字体文件打包进程序资源里。我通常会在板子上放一个DejaVuSans.ttf,然后在程序里用QFontDatabase::addApplicationFont()加载。
第二个常见问题是平台插件缺失。静态Qt默认只编译了libqlinuxfb.a和libqoffscreen.a,如果板子用X11,需要确保configure时没有-no-xcb,并且sysroot里有X11的库。
第三个是时区数据缺失。Qt的QDateTime依赖时区数据,静态编译时如果没打包/usr/share/zoneinfo,时间转换会出错。可以在程序里用QTimeZone::setDefaultTimeZone()手动设置。
6. 静态编译Qt的取舍与适用边界
6.1 静态编译不是万能药
静态编译Qt解决了部署依赖问题,但代价也很明显。首先是体积,一个动态链接的Qt程序可能只有几百K,静态链接后变成几十M。其次是更新困难,Qt出了安全补丁,静态程序必须重新编译整个Qt再重新编译程序,不能只替换.so。
所以静态编译适合的场景是:程序功能固定、部署环境不可控、更新频率低。比如工业控制面板、医疗设备界面、车载终端。如果你的程序需要频繁更新,或者板子存储空间紧张,动态编译加打包依赖可能是更好的选择。
6.2 和动态编译的对比
| 维度 | 静态编译 | 动态编译 |
|---|---|---|
| 部署复杂度 | 低,单文件 | 高,需打包依赖 |
| 程序体积 | 大,20M起 | 小,几百K起 |
| 更新成本 | 高,需重编Qt | 低,替换so即可 |
| 内存占用 | 略高,无共享 | 低,多进程共享 |
| 编译时间 | 长,2小时起 | 短,30分钟 |
| 适用场景 | 嵌入式量产 | 开发调试、频繁更新 |
6.3 混合方案:静态Qt + 动态业务库
实际项目中,我经常用混合方案:Qt静态编译,业务逻辑做成动态库。这样Qt的部署问题解决了,业务逻辑还能单独更新。
具体做法是:Qt用-static编译,业务库用-shared编译。主程序静态链接Qt,动态加载业务库。这样主程序体积大但稳定,业务库体积小可更新。
# 业务库编译 aarch64-linux-gnu-g++ -shared -fPIC business.cpp -o libbusiness.so # 主程序链接静态Qt,动态加载业务库 /opt/qt5.14.2-host/bin/qmake main.pro make主程序的.pro里用LIBS += -L. -lbusiness,运行时用LD_LIBRARY_PATH指定业务库路径。
6.4 后续扩展方向
这套静态Qt编译环境搭好后,可以复用到其他aarch64项目。比如把boost库也交叉编译成静态库,和Qt一起链接。boost的交叉编译比Qt简单,用b2工具指定toolset=gcc和address-model=64就行。
另一个方向是裁剪Qt模块。如果项目只用Widgets不用QML,可以在configure时-skip qtdeclarative,能省掉一半编译时间。如果不用网络,-skip qtnetwork也能省不少。
最后分享一个我踩过的坑:静态Qt的qmake生成的Makefile里,链接顺序很重要。Qt的静态库之间有依赖关系,libQt5Widgets.a依赖libQt5Gui.a,libQt5Gui.a依赖libQt5Core.a。如果链接顺序错了,会报undefined reference。qmake通常能处理对,但如果手动改Makefile,一定要保证依赖库在后面。
提示:静态Qt编译完成后,建议把整个
/opt/qt5.14.2-aarch64-static目录打包备份。下次换机器或者重装系统,直接解压就能用,不用重新编译。这个目录大概2G到3G,压缩后500M左右。