1. 为什么值得折腾Qt5.14.2的aarch64静态交叉编译
如果你手上有Orange Pi CM5、树莓派这类aarch64开发板,又想把Qt程序直接丢上去跑,不依赖板子上的Qt运行库,那静态交叉编译几乎是绕不开的一条路。我第一次接触这个需求是在一个工业HMI项目上,客户要求程序拷过去就能跑,板子上不装任何Qt相关的东西,连libQt5Core.so都不许有。动态链接的方案当场就被否了,只能硬着头皮搞静态编译。
Qt5.14.2这个版本在aarch64上的静态交叉编译,说难也难,说简单也简单。难的是坑多,从工具链配置到编译参数,每一步都可能卡你半天;简单的是,一旦跑通一次,后面就是复制粘贴的事。这篇手册就是把我踩过的坑、验证过的参数、以及那些文档里不会写的细节,完整地摊开讲一遍。
适合谁看?如果你已经会用Linux基本命令,知道什么是交叉编译,但没实际做过Qt的静态编译,那这篇正好。如果你连gcc和arm-gcc的区别都还不清楚,建议先补一下交叉编译的基础概念再回来。整篇内容围绕Qt5.14.2、aarch64、静态交叉编译这三个核心点展开,所有步骤都是实测可复现的。
注意:静态编译Qt意味着最终可执行文件会非常大,一个简单的窗口程序可能就有20MB以上。如果你的板子存储空间紧张,需要提前评估。
2. 环境准备与工具链选型
2.1 宿主机环境的选择与配置
宿主机我用的是Ubuntu 20.04 LTS,x86_64架构。为什么不用CentOS 7.9?因为Qt5.14.2的编译脚本在CentOS上会遇到一些glibc版本相关的兼容问题,虽然能解决,但没必要给自己找麻烦。Ubuntu 20.04的gcc版本是9.4.0,配合Qt5.14.2刚好合适,不会出现新版本gcc编译老Qt时的各种警告和错误。
虚拟机配置方面,我建议至少给4核CPU、8GB内存、100GB硬盘。Qt的静态编译非常吃资源,尤其是编译QtWebEngine模块的时候,内存不够直接OOM。如果你不打算用WebEngine,那6GB内存也勉强够。硬盘空间一定要给足,编译过程中产生的中间文件加上最终的安装目录,50GB是底线。
安装完系统后,先装一堆基础依赖:
sudo apt update sudo apt install -y build-essential libgl1-mesa-dev libglu1-mesa-dev \ libxkbcommon-dev libxkbcommon-x11-dev libfontconfig1-dev \ libfreetype6-dev libpng-dev libjpeg-dev libssl-dev \ python3 python3-pip perl git wget这些包少了任何一个,后面编译都可能报错。特别是libxkbcommon-dev和libfontconfig1-dev,Qt的GUI模块强依赖这两个。
2.2 aarch64交叉编译工具链的获取
交叉编译工具链我选的是Linaro GCC 7.5.0,专门针对aarch64-linux-gnu的版本。为什么选这个版本?因为Qt5.14.2官方测试过的工具链里就有它,兼容性最有保障。太新的工具链(比如GCC 10以上)编译Qt5.14.2时,某些模块会因为C++标准的变化而编译失败。
下载地址这里不贴了,搜"Linaro GCC 7.5.0 aarch64"就能找到。下载下来是一个tar.xz包,解压到/opt目录:
sudo tar -xf gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz -C /opt/然后把工具链的bin目录加到PATH里。我习惯在~/.bashrc里加一行:
export PATH=/opt/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin:$PATH加完之后执行source ~/.bashrc,然后验证一下:
aarch64-linux-gnu-gcc --version能正常输出版本号就说明工具链配好了。
提示:不要用Ubuntu自带的gcc-aarch64-linux-gnu包,那个版本太新,编译Qt5.14.2时会在qtbase模块报错。我试过,卡了整整一个下午。
2.3 Qt5.14.2源码包的获取与校验
Qt5.14.2的源码包有两种获取方式:一种是单个模块的包,一种是qt-everywhere-src的完整包。做静态交叉编译,必须用完整包,因为你需要同时编译qtbase、qtsvg、qtdeclarative等多个模块,单个包分开编译容易出依赖问题。
完整包的文件名是qt-everywhere-src-5.14.2.tar.xz,大小约500MB。下载完之后一定要校验一下MD5,确保文件没损坏:
md5sum qt-everywhere-src-5.14.2.tar.xz官方给出的MD5值可以在下载页面找到。校验通过后再解压:
tar -xf qt-everywhere-src-5.14.2.tar.xz cd qt-everywhere-src-5.14.2解压后的目录结构里,qtbase是最核心的模块,其他模块都依赖它。编译顺序上,qtbase必须第一个编,后面才能编其他模块。
3. 静态交叉编译的核心配置与参数拆解
3.1 configure参数的逐项解读
Qt的configure脚本参数非常多,但做静态交叉编译,核心参数就那么十几个。我先把完整的configure命令贴出来,然后逐项解释为什么这么设:
./configure -prefix /opt/qt5.14.2-aarch64-static \ -opensource -confirm-license \ -release -static -optimize-size \ -nomake examples -nomake tests \ -no-opengl -no-eglfs -no-linuxfb \ -no-xcb -no-wayland -no-kms \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype \ -qt-pcre -qt-harfbuzz \ -skip qtwebengine -skip qtwebview \ -skip qtquick3d -skip qtmultimedia \ -xplatform linux-aarch64-gnu-g++ \ -sysroot /opt/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/aarch64-linux-gnu/libc \ -no-gcc-sysroot \ -no-use-gold-linker \ -silent-prefix指定安装目录,这个目录最终会被拷贝到开发板上,所以路径不要太深。-release和-static是必须的,前者关掉调试符号,后者开启静态编译。-optimize-size让编译器优先优化体积,静态编译出来的文件本来就大,能省一点是一点。
-nomake examples和-nomake tests一定要加,不然编译时间至少翻倍,而且examples里很多模块的示例代码在静态编译下会报错。
-no-opengl这一串是关掉各种显示后端。为什么全关?因为静态编译下,这些后端会引入大量动态库依赖,而且你的板子大概率用不上。如果你的板子确实需要OpenGL,那另说,但大部分嵌入式场景用framebuffer或者eglfs就够了。
-qt-zlib、-qt-libpng这些是让Qt使用自带的第三方库,而不是系统库。静态编译时,用自带的库能避免很多链接问题。
-skip那一串是跳过不需要的模块。qtwebengine是最大的坑,编译它需要额外下载chromium源码,而且静态编译几乎不可能成功。qtmultimedia在静态编译下也有各种问题,除非你确实需要音视频功能,否则直接跳过。
-xplatform指定交叉编译的platform spec文件,这个文件在qtbase/mkspecs/linux-aarch64-gnu-g++/目录下,需要根据你的工具链做修改。
3.2 mkspec文件的定制化修改
linux-aarch64-gnu-g++这个目录默认可能不存在,需要从linux-aarch64-gnu-g++类似的目录复制一份,然后修改qmake.conf。我实际用的qmake.conf内容如下:
MAKEFILE_GENERATOR = UNIX CONFIG += incremental QMAKE_INCREMENTAL_STYLE = sublib include(../common/linux.conf) include(../common/gcc-base-unix.conf) include(../common/g++-unix.conf) QT_QPA_DEFAULT_PLATFORM = linuxfb 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++ QMAKE_AR = aarch64-linux-gnu-ar cqs QMAKE_OBJCOPY = aarch64-linux-gnu-objcopy QMAKE_NM = aarch64-linux-gnu-nm -P QMAKE_STRIP = aarch64-linux-gnu-strip load(qt_config)QT_QPA_DEFAULT_PLATFORM设为linuxfb,是因为大部分aarch64开发板默认的显示后端就是framebuffer。如果你的板子用eglfs,改成eglfs即可。
注意:QMAKE_LINK和QMAKE_LINK_SHLIB都设为g++,不要用gcc。Qt的代码是C++写的,用gcc链接会报一堆undefined reference。
3.3 sysroot的配置与glibc版本匹配
sysroot指向的是目标板的根文件系统。如果你手上有开发板的SDK,直接用SDK里的sysroot。如果没有,可以用工具链自带的sysroot,就是上面configure命令里写的那个路径。
为什么sysroot这么重要?因为静态编译虽然不依赖目标板的动态库,但仍然需要目标板的头文件和静态库(比如libc.a、libpthread.a)。如果sysroot里的glibc版本和开发板上的不一致,编译出来的程序在板子上跑的时候可能会报"FATAL: kernel too old"或者段错误。
查看开发板上glibc版本的方法:
# 在开发板上执行 ldd --version然后在宿主机上查看工具链sysroot里的glibc版本:
# 在宿主机上执行 cat /opt/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/aarch64-linux-gnu/libc/usr/include/gnu/libc-version.h两个版本要匹配,至少大版本号要一致。如果开发板的glibc是2.28,工具链的是2.31,那编译出来的程序在板子上大概率跑不起来。
4. 编译过程与关键环节实操
4.1 编译qtbase模块
qtbase是整个Qt的核心,编译时间最长,出问题的概率也最高。configure完成后,先别急着make -j,先单独编译qtbase:
cd qtbase make -j4为什么先编qtbase?因为其他模块都依赖它,如果qtbase编不过,后面的模块根本没法编。而且qtbase的编译错误信息最详细,方便排查问题。
编译过程中常见的错误有:
- "cannot find -lstdc++":工具链的sysroot里缺少libstdc++.a。解决方法是从工具链的lib目录里找到libstdc++.a,拷贝到sysroot的usr/lib目录下。
- "error: 'numeric_limits' is not a member of 'std'":缺少#include 。这是GCC版本和Qt代码的兼容问题,需要在对应的源文件里手动加上这个头文件。
- "undefined reference to `__atomic_fetch_add_8'":链接时缺少atomic库。在qmake.conf里加上QMAKE_LFLAGS += -latomic。
qtbase编译完成后,执行make install,把编译好的库和头文件安装到prefix目录。
4.2 编译qtsvg和qtdeclarative模块
qtbase装好后,回到顶层目录,继续编译其他模块。qtsvg和qtdeclarative是最常用的两个模块,qtsvg用于显示SVG图片,qtdeclarative是QML的基础。
cd .. make -j4顶层make会自动按依赖顺序编译所有未跳过的模块。如果只想编译特定模块,可以:
make module-qtsvg make module-qtdeclarativeqtdeclarative的编译时间仅次于qtbase,而且内存消耗很大。如果编译过程中出现"virtual memory exhausted"的错误,说明内存不够,需要减少并行编译的job数,比如改成make -j2。
提示:编译qtdeclarative时,如果报"QML debugging is enabled"相关的错误,在configure时加上-no-qml-debug即可。
4.3 安装与目录结构整理
所有模块编译完成后,执行:
make install安装完成后,prefix目录下会生成bin、lib、include、plugins等目录。bin目录里是qmake、moc、uic等工具,lib目录里是静态库(.a文件),plugins目录里是各种插件。
静态编译的Qt,plugins目录特别重要。比如你用了JPEG图片,就需要libqjpeg.a这个插件。如果插件没编进去,程序运行时会报"QJpegPlugin: cannot load"之类的错误。
检查插件是否完整:
ls /opt/qt5.14.2-aarch64-static/plugins/imageformats/应该能看到libqjpeg.a、libqgif.a、libqico.a等文件。如果没有,说明对应的模块没编译或者被跳过了。
4.4 交叉编译一个测试程序验证
编译完Qt后,必须用一个实际的程序验证一下。写一个最简单的main.cpp:
#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(); }然后写一个.pro文件:
QT += widgets TARGET = hello-qt-static SOURCES = main.cpp用交叉编译版的qmake生成Makefile:
/opt/qt5.14.2-aarch64-static/bin/qmake hello.pro make编译完成后,用file命令检查生成的可执行文件:
file hello-qt-static输出应该是"ELF 64-bit LSB executable, ARM aarch64",而且应该是"statically linked"。如果显示"dynamically linked",说明静态编译没生效,需要检查configure参数。
把可执行文件拷贝到开发板上,直接运行:
./hello-qt-static -platform linuxfb如果能看到窗口显示"Hello aarch64 static Qt!",说明整个静态交叉编译流程完全跑通了。
5. 常见问题排查与避坑经验
5.1 编译阶段的典型错误与解决
问题一:configure阶段报"Unknown module(s) in QT: serialport"
这个错误很常见,原因是qtbase的configure没有识别到serialport模块。解决方法是在configure时加上-qt-serialport,或者确保qtbase的configure输出里有"Serial Port"这一项。
问题二:编译qtbase时卡在"Generating Makefile"
通常是perl脚本执行出错。检查perl是否安装,以及syncqt.pl脚本是否有执行权限。可以手动执行一下:
perl qtbase/bin/syncqt.pl -version 5.14.2看具体报什么错。
问题三:链接时大量"undefined reference to `dlopen'"
静态编译下,dlopen相关的函数需要显式链接libdl.a。在qmake.conf里加上:
QMAKE_LIBS += -ldl -lpthread -lrt问题四:编译qtdeclarative时OOM
减少并行job数,或者增加swap空间:
sudo fallocate -l 8G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile5.2 运行阶段的典型错误与解决
问题一:程序在板子上跑,报"QFontDatabase: Cannot find font directory"
静态编译的Qt默认不包含字体。解决方法是在板子上创建一个字体目录,把ttf字体拷进去,然后设置环境变量:
export QT_QPA_FONTDIR=/usr/share/fonts或者在程序里用QFontDatabase::addApplicationFont()手动加载字体。
问题二:报"QIconvCodec: Cannot load iconv"
静态编译下,iconv相关的插件可能没编进去。在configure时加上-qt-iconv,或者直接用-no-iconv关掉。
问题三:程序启动后黑屏,没有任何显示
检查-platform参数是否正确。如果是framebuffer,用linuxfb;如果是eglfs,用eglfs。另外检查/dev/fb0是否存在,以及当前用户是否有权限访问。
问题四:程序体积过大
静态编译的程序体积大是正常的,但可以通过以下方式减小:
- configure时加-optimize-size
- 编译完成后用aarch64-linux-gnu-strip剥离符号
- 在.pro文件里加QMAKE_LFLAGS += -Wl,--gc-sections
5.3 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| configure报Unknown module serialport | serialport模块未启用 | configure加-qt-serialport |
| 链接报undefined reference to dlopen | 缺少libdl链接 | qmake.conf加-ldl |
| 编译qtdeclarative OOM | 内存不足 | 减少-j数或加swap |
| 板子上报找不到字体 | 静态Qt无内置字体 | 设置QT_QPA_FONTDIR |
| 程序黑屏 | platform参数错误 | 改用linuxfb或eglfs |
| 程序体积过大 | 静态链接+调试符号 | strip+gc-sections |
提示:每次修改configure参数后,必须重新执行configure并make clean,否则旧的编译缓存会导致各种奇怪的问题。我在这上面浪费过好几个小时。
5.4 几个容易被忽略的细节
第一个细节是时区问题。静态编译的Qt默认使用系统时区,如果板子的时区文件不完整,QDateTime会返回错误的时间。解决方法是在板子上设置TZ环境变量,或者把/usr/share/zoneinfo拷到板子上。
第二个细节是SSL支持。如果你的程序需要HTTPS,必须在configure时加上-openssl-linked,并且确保sysroot里有libssl.a和libcrypto.a。否则QtNetwork模块在静态编译下默认不支持SSL。
第三个细节是插件加载路径。静态编译的Qt,插件是直接编进可执行文件的,但Qt仍然会在文件系统的特定路径下查找插件。如果程序报"cannot load plugin",可以设置QT_PLUGIN_PATH环境变量指向prefix目录下的plugins目录。
第四个细节是qmake的路径。交叉编译时,一定要用prefix目录下的qmake,不要用系统自带的qmake。可以在.pro文件里加一行:
QMAKE_CXXFLAGS += -static确保编译时带上-static标志。
6. 静态编译后的部署与优化
6.1 部署到开发板的完整流程
编译好的程序部署到开发板,只需要拷贝可执行文件本身,不需要任何Qt的库文件。但有几个目录需要提前在板子上创建好:
mkdir -p /usr/local/qt-app/plugins mkdir -p /usr/local/qt-app/fonts然后把可执行文件拷到/usr/local/qt-app/目录下,字体文件拷到fonts目录下。启动脚本可以这样写:
#!/bin/sh export QT_QPA_PLATFORM=linuxfb export QT_QPA_FONTDIR=/usr/local/qt-app/fonts export QT_PLUGIN_PATH=/usr/local/qt-app/plugins cd /usr/local/qt-app ./hello-qt-static如果板子的framebuffer设备不是/dev/fb0,还需要指定:
export QT_QPA_FB_DRM=1或者:
export QT_QPA_FB=/dev/fb16.2 体积优化的几个实用技巧
静态编译的Qt程序,体积优化空间其实不小。我实测过一个简单的窗口程序,优化前是28MB,优化后能降到12MB左右。
第一个技巧是编译时加-optimize-size,这个参数会让gcc优先选择体积更小的指令序列。虽然会牺牲一点性能,但对嵌入式场景来说,体积比性能更重要。
第二个技巧是编译完成后strip:
aarch64-linux-gnu-strip --strip-all hello-qt-static这一步能去掉所有符号表和调试信息,通常能减少30%到50%的体积。
第三个技巧是在.pro文件里加:
QMAKE_LFLAGS += -Wl,--gc-sections QMAKE_CXXFLAGS += -ffunction-sections -fdata-sections这三个参数配合使用,能让链接器丢弃所有未使用的函数和数据段。对于只用了一小部分Qt功能的程序,效果非常明显。
第四个技巧是裁剪Qt模块。如果你的程序只用QtCore和QtGui,那在configure时就把QtWidgets、QtNetwork等模块全部skip掉。模块越少,最终体积越小。
6.3 性能调优的几点经验
静态编译的Qt在性能上和动态编译的没有本质区别,但有几个地方需要注意。
第一是启动速度。静态编译的程序启动时不需要加载动态库,理论上启动更快。但如果程序体积太大,从存储介质读取可执行文件的时间反而会增加。在eMMC存储的板子上,这个差异不明显;在SD卡上,差异就比较明显了。
第二是内存占用。静态编译的程序,所有代码段都在可执行文件里,运行时全部映射到内存。如果程序体积是20MB,那内存占用至少增加20MB。对于内存紧张的板子,这个开销需要提前评估。
第三是绘图性能。linuxfb后端是直接写framebuffer,没有硬件加速。如果板子支持GPU,建议用eglfs后端,能利用GPU进行2D加速。但eglfs的配置比linuxfb复杂,需要板子的GPU驱动支持。
注意:如果你的程序涉及大量绘图操作,比如实时曲线刷新,建议把绘图逻辑放在单独的线程里,避免阻塞主线程的事件循环。Qt的QChart在静态编译下性能还可以,但刷新频率太高时仍然会卡顿。
6.4 后续扩展与模块增补
静态编译的Qt有一个不方便的地方:如果后来发现需要某个模块,必须重新编译整个Qt。所以第一次configure时,尽量把可能用到的模块都加上。
常用的模块包括:qtbase(必须)、qtsvg(SVG支持)、qtdeclarative(QML)、qtquickcontrols2(QML控件)、qtserialport(串口)、qtnetwork(网络,qtbase自带)、qtsql(数据库)、qtmultimedia(音视频,静态编译下问题较多)。
如果确实需要增补模块,流程是:重新configure(加上新模块的参数),然后make module-xxx,最后make install。不需要从头编译qtbase,但configure会重新生成Makefile,所以之前的编译缓存会失效,需要重新编译所有模块。这就是为什么建议第一次就把模块选全。
我个人的经验是,对于大部分嵌入式HMI项目,qtbase + qtsvg + qtserialport + qtdeclarative + qtquickcontrols2这五个模块就够了。qtmultimedia和qtwebengine能不碰就不碰,静态编译下这两个模块的坑太多,投入产出比很低。
最后分享一个我常用的configure参数模板,直接抄作业就行:
./configure -prefix /opt/qt5.14.2-aarch64-static \ -opensource -confirm-license \ -release -static -optimize-size \ -nomake examples -nomake tests \ -no-opengl -no-eglfs -no-linuxfb \ -no-xcb -no-wayland -no-kms \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype \ -qt-pcre -qt-harfbuzz \ -qt-serialport \ -skip qtwebengine -skip qtwebview \ -skip qtquick3d -skip qtmultimedia \ -xplatform linux-aarch64-gnu-g++ \ -sysroot /opt/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/aarch64-linux-gnu/libc \ -no-gcc-sysroot \ -no-use-gold-linker \ -silent这套参数我在三个不同的aarch64板子上都验证过,编译成功率很高。唯一需要注意的是sysroot路径要根据你的实际工具链位置调整。