Qt 5.14.2 aarch64 静态交叉编译实战:从工具链到部署全流程
2026/9/20 1:52:25 网站建设 项目流程

1. 为什么值得折腾 Qt 5.14.2 的 aarch64 静态交叉编译

如果你手上有 Orange Pi CM5、树莓派这类 aarch64 开发板,又打算把 Qt 程序直接烧进系统镜像里跑,那你迟早会撞上“静态交叉编译”这堵墙。动态编译出来的 Qt 程序,部署到板子上要么缺库、要么版本对不上,cannot mix incompatible qt library这类报错能让人抓狂。静态编译把 Qt 库直接塞进可执行文件,拷过去就能跑,省掉一整套运行时依赖,这在嵌入式量产场景里几乎是刚需。

Qt 5.14.2 是个很微妙的版本。它比 5.12 系列新,又不像 5.15 那样对编译器和依赖要求那么苛刻,在 CentOS 7.9 aarch64 或者 Ubuntu 交叉环境里都相对好伺候。但“相对好伺候”不等于“好伺候”,静态编译 Qt 本身就是个连环坑:工具链要选对、configure 参数一个不能错、serialport 这类模块动不动就unknown module、静态链接还牵扯到-static-static-libstdc++的取舍。我前后在几块板子上折腾过好几轮,踩的坑足够写一本小册子。

这篇手册就是把这些经验摊开讲。从工具链选型、源码准备、configure 参数逐条拆解,到编译、安装、验证、排错,全流程走一遍。目标读者是已经会基本 Linux 操作、懂一点交叉编译概念、但没完整做过 Qt 静态交叉编译的嵌入式开发者。看完你应该能自己从零搭出一套可用的 aarch64 静态 Qt 工具链,并且知道每个参数为什么这么写,出了问题往哪儿查。

2. 整体方案设计与关键选型思路

2.1 静态编译 vs 动态编译:到底该选哪个

先把这件事说清楚,不然后面全是白费功夫。动态编译的 Qt 程序,运行时依赖一堆.so,部署时你得把libQt5Core.so.5libQt5Gui.so.5、平台插件platforms/libqlinuxfb.so等等全部拷到板子上,还得保证LD_LIBRARY_PATH对、版本一致。板子上的系统 Qt 版本和你编译用的版本一旦对不上,就是那句经典的cannot mix incompatible qt library (5.15.3) with this library (5.15.2)

静态编译则是把用到的 Qt 代码全部编进最终可执行文件。好处很直接:单文件部署,拷过去chmod +x就能跑,不依赖目标机的 Qt 环境。代价是体积大(一个带 GUI 的程序轻松几十 MB)、编译时间长(我这边 16 核机器全量编译接近 40 分钟)、而且一旦某个模块没编进去,运行时才发现就晚了。

我的建议是:面向量产的嵌入式设备、系统镜像定制、或者目标机 Qt 环境不可控的场景,优先静态编译。如果是开发调试阶段、板子上已经有完整 Qt 运行时,动态编译更省事。这篇手册聚焦静态方案。

2.2 工具链选型:为什么是 gcc-arm 而不是别的

aarch64 的交叉工具链选择其实不少,常见的有 Linaro 的gcc-arm-*系列、Bootlin 的 toolchain、以及各家芯片厂自带的 SDK 工具链。热词里提到的env工具链unity工具链autosar工具链etas工具链大多是特定行业或特定芯片的配套工具链,通用性不强。

我最终选的是Linaro 的gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu,理由有三条。第一,它基于 glibc,和主流 aarch64 发行版(Ubuntu、Debian、CentOS)的运行时兼容性最好;第二,GCC 10.3 对 C++17 支持完整,Qt 5.14.2 的源码用它能干净编过;第三,这个版本足够稳定,社区资料多,出问题好查。

注意:不要用aarch64-none-elf这类裸机工具链,那是给没有操作系统的场景用的,编 Qt 会缺一堆系统头文件。一定要选带linux-gnu后缀的。

工具链的sysroot也很关键。Linaro 工具链自带一个精简 sysroot,但 Qt 编译需要目标系统的完整头文件和库。稳妥做法是从目标板子上把/usr/include/usr/lib/lib打包下来,或者用和目标系统同版本的发行版 rootfs 作为 sysroot。我这边用的是和目标板同版本的 Ubuntu 20.04 aarch64 rootfs。

2.3 源码版本与目录规划

Qt 5.14.2 的源码包是qt-everywhere-src-5.14.2.tar.xz,注意是everywhere版本,包含了所有模块。热词里有人问qt-everywhere-src-5.15.10 交叉编译,思路完全一样,只是版本号不同。下载建议走国内镜像,速度差好几倍。

目录规划我习惯这样:

~/qt-build/ ├── toolchain/ # 交叉工具链解压目录 ├── sysroot/ # 目标系统 rootfs ├── src/ # Qt 源码 │ └── qt-everywhere-src-5.14.2/ ├── build/ # 编译输出目录(out-of-source) └── install/ # 安装目录

一定要用 out-of-source 编译,也就是在build/目录里执行 configure,源码目录保持干净。Qt 的 configure 会生成大量中间文件,污染源码目录后想重新配置会很痛苦,删都删不干净。

3. 环境准备与工具链配置实操

3.1 工具链安装与验证

先把工具链解压到~/qt-build/toolchain/,然后配置环境变量。我习惯写一个env.sh,每次开新终端 source 一下:

export TOOLCHAIN_ROOT=~/qt-build/toolchain/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu export SYSROOT=~/qt-build/sysroot export PATH=$TOOLCHAIN_ROOT/bin:$PATH export CROSS_COMPILE=aarch64-none-linux-gnu- export CC=${CROSS_COMPILE}gcc export CXX=${CROSS_COMPILE}g++

验证工具链能不能用:

aarch64-none-linux-gnu-gcc -v

能打印出 GCC 版本信息就对了。再编个 hello world 验证一下:

echo 'int main(){return 0;}' > test.c aarch64-none-linux-gnu-gcc test.c -o test file test

file输出里应该看到ELF 64-bit LSB executable, ARM aarch64。如果显示的是 x86-64,说明 PATH 没配对,编出来的是本机程序。

3.2 sysroot 的准备与常见坑

sysroot 是交叉编译里最容易出问题的地方。Qt 编译过程中会去 sysroot 里找libGL.solibX11.solibfontconfig.so这些依赖。如果 sysroot 不完整,configure 阶段就会报各种cannot find -lXXX

我的做法是从目标板子上直接 rsync 一份完整 rootfs:

rsync -avz --numeric-ids root@target:/usr/include/ $SYSROOT/usr/include/ rsync -avz --numeric-ids root@target:/usr/lib/ $SYSROOT/usr/lib/ rsync -avz --numeric-ids root@target:/lib/ $SYSROOT/lib/

注意:rsync 的时候一定要加--numeric-ids,否则 uid/gid 映射会出问题。另外/usr/lib里可能有指向绝对路径的符号链接,跨机器拷贝后可能失效,需要检查一下。

如果板子不方便联网,也可以用debootstrap在本地搭一个同版本的 aarch64 rootfs,或者直接下载发行版的 aarch64 minimal rootfs 压缩包。关键是目标系统的 glibc 版本不能低于工具链的 glibc 版本,否则编出来的程序在板子上跑不起来,报GLIBC_2.XX not found

3.3 依赖库的交叉编译

Qt 的 GUI 模块依赖一堆第三方库。如果 sysroot 里已经有这些库的 aarch64 版本,直接指过去就行。如果没有,就得自己交叉编译。常见的几个:

  • zlib:QtCore 的压缩支持
  • libpng / libjpeg:图片格式支持
  • freetype:字体渲染,GUI 必需
  • fontconfig:字体配置
  • libinput / tslib:触摸屏输入

以 freetype 为例,交叉编译的套路是:

./configure --host=aarch64-none-linux-gnu \ --prefix=$SYSROOT/usr \ --enable-static --disable-shared make -j$(nproc) && make install

--host指定交叉编译器前缀,--prefix指到 sysroot 里,这样 Qt configure 时能自动找到。静态编译 Qt 时,依赖库也尽量编成静态的,否则最终链接还是会引入.so依赖,静态编译的意义就打折了。

热词里提到的boost库交叉编译也是同样的思路,不过 Qt 本身不依赖 boost,除非你的业务代码用了。交叉编译chrony那是系统工具,和 Qt 无关,这里不展开。

4. configure 参数逐条拆解与编译实战

4.1 configure 参数为什么这么写

这是整个流程的核心。Qt 5.14.2 的 configure 参数有几百个,但真正关键的就那么十几个。我先把完整命令贴出来,再逐条解释:

../src/qt-everywhere-src-5.14.2/configure \ -prefix /opt/qt-5.14.2-aarch64-static \ -opensource -confirm-license \ -release \ -static \ -nomake examples -nomake tests \ -no-opengl \ -no-xcb \ -no-eglfs \ -linuxfb \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype \ -qt-pcre \ -no-iconv \ -skip qtwebengine \ -skip qtdeclarative \ -sysroot $SYSROOT \ -platform linux-g++ \ -xplatform linux-aarch64-gnu-g++ \ -device-option CROSS_COMPILE=aarch64-none-linux-gnu- \ -v

逐条说:

-prefix是安装路径,注意这是目标机上的路径,不是编译机的。程序运行时如果动态加载插件,会去这个路径找。静态编译下影响较小,但保持一致没坏处。

-static是核心,生成静态库。-release关掉调试符号,减小体积。-opensource -confirm-license是自动接受开源协议,不加会卡在交互式确认。

-nomake examples -nomake tests强烈建议加上。examples 和 tests 编译量巨大,而且静态编译下很多 example 会链接失败,白白浪费时间。

-no-opengl -no-xcb -no-eglfs -linuxfb这组是平台后端选择。嵌入式板子通常没有 X11,用linuxfb直接写 framebuffer 最省事。如果你的板子有 GPU 且要用 EGLFS,那就把-no-eglfs换成-eglfs,但需要额外的 GPU 驱动库支持。-no-opengl是因为很多嵌入式场景不需要 OpenGL,关掉能省不少编译时间和体积。

-qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-pcre这组是让 Qt 用自带的第三方库源码编译,而不是去 sysroot 找系统库。静态编译强烈建议这么干,因为系统库的静态版本往往没装,而且版本可能不匹配。-qt-pcre尤其重要,Qt 的正则表达式依赖它。

-no-iconv关掉字符集转换,嵌入式场景一般用不上,关掉省依赖。

-skip qtwebengine -skip qtdeclarative跳过这两个大块。qtwebengine 基于 Chromium,交叉编译极其痛苦,静态编译基本不可能;qtdeclarative 是 QML 运行时,如果你只用 Widgets 就不需要。如果你要用 QML,那 qtdeclarative 不能跳,但编译量和依赖会大幅增加,要有心理准备。

-sysroot指向前面准备的 sysroot。-platform linux-g++是编译机(host)的平台,-xplatform linux-aarch64-gnu-g++是目标机(target)的平台。这两个不能搞反。

-device-option CROSS_COMPILE=...把交叉编译器前缀传给 qmake 的 mkspec。

4.2 mkspec 的定制

linux-aarch64-gnu-g++这个 mkspec 在 Qt 5.14.2 里不一定存在,需要自己建。在qtbase/mkspecs/下复制一份linux-aarch64-gnu-g++目录(如果没有就从linux-arm-gnueabi-g++改),编辑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-none-linux-gnu-gcc QMAKE_CXX = aarch64-none-linux-gnu-g++ QMAKE_LINK = aarch64-none-linux-gnu-g++ QMAKE_LINK_SHLIB = aarch64-none-linux-gnu-g++ QMAKE_AR = aarch64-none-linux-gnu-ar cqs QMAKE_OBJCOPY = aarch64-none-linux-gnu-objcopy QMAKE_NM = aarch64-none-linux-gnu-nm -P QMAKE_STRIP = aarch64-none-linux-gnu-strip load(qt_config)

QT_QPA_DEFAULT_PLATFORM = linuxfb这行决定了默认的平台插件,和 configure 里的-linuxfb呼应。

4.3 编译过程与时间预估

configure 跑完会打印一份配置摘要,一定要仔细看。重点确认这几项:

  • Build type: release
  • Build options: -static
  • QPA backends里 linuxfb 是 yes
  • Qt modules里你需要的模块都是 yes

确认无误后开始编译:

make -j$(nproc) 2>&1 | tee build.log

tee是为了留日志,出问题好回溯。16 核机器全量编译大概 30-40 分钟,8 核大概 1 小时以上。编译过程中如果报错,先看build.log里第一个 error,后面的错误往往是连锁反应。

编译完成后安装:

make install

安装到-prefix指定的目录。静态编译下这个目录主要是给后续开发用的,里面有lib/libQt5Core.a这些静态库和头文件。

5. 常见问题排查与避坑经验

5.1 unknown module in qt:serialport 怎么解决

这是热词里出现频率最高的报错,:-1: error: unknown module(s) in qt: serialport。原因很简单:QtSerialPort 模块没被编译进去。Qt 5.14.2 的 serialport 在qtserialport子模块里,默认是编的,但如果你 configure 时加了-skip qtserialport,或者编译过程中 serialport 因为依赖问题失败了,就会缺。

排查步骤:

  1. 检查 configure 摘要里Qt SerialPort是不是 yes
  2. 检查install/lib/下有没有libQt5SerialPort.a
  3. 如果缺,重新 configure,确保没有 skip,并且 sysroot 里有libudev的开发文件(serialport 依赖 udev)

如果 sysroot 里没有 udev,可以-no-libudev关掉 udev 支持,serialport 仍能编,只是设备热插拔检测功能受限。

5.2 静态链接时的 -static 与 -static-libstdc++ 取舍

静态编译 Qt 后,你的业务程序链接时也要加-static。但这里有个坑:全静态链接 glibc 会有问题,尤其是涉及getaddrinfodlopen这些函数时,静态 glibc 的行为和动态不一样,可能出诡异 bug。

我的做法是:Qt 库静态链接,但 glibc 和 libstdc++ 动态链接。也就是链接时用:

-static-libgcc -static-libstdc++

而不是全-static。这样 Qt 的代码进了可执行文件,但 C 运行时还是用系统的,兼容性最好。代价是目标机得有对应版本的 glibc,不过这个要求通常都能满足。

5.3 编译过程中的典型报错速查

报错信息原因解决
cannot find -lGLsysroot 缺 libGL-no-opengl或补 sysroot
GLIBC_2.XX not found工具链 glibc 高于目标机换低版本工具链或升级目标机
unknown module(s) in qt: serialportserialport 未编译检查 configure 摘要,补 udev
cannot mix incompatible qt library动态库版本冲突静态编译或统一版本
undefined reference to dlopen全静态链接 glibc改用-static-libstdc++
qmake: command not foundPATH 未配置source env.sh
Project ERROR: Unknown module(s) in QT: quickqtdeclarative 被 skip去掉-skip qtdeclarative

5.4 几个只有踩过才知道的细节

第一,configure 的-v一定要加。它会打印详细的检测过程,哪个库没找到、哪个特性被禁用,一目了然。不加-v的话 configure 静默跳过很多东西,你以为编进去了其实没有。

第二,编译前先make confclean。如果你改过 configure 参数重新编,一定要先清理。Qt 的增量编译对 configure 变更支持不好,残留的中间文件会导致莫名其妙的链接错误。

第三,注意-qt-zlib和系统 zlib 的冲突。如果 sysroot 里有 zlib 且你的业务代码也链接了系统 zlib,可能出现符号重复定义。统一用-qt-zlib最省心。

第四,静态编译的插件加载。Qt 的静态插件(比如 platform 插件、imageformat 插件)需要在代码里用Q_IMPORT_PLUGIN宏显式导入,否则运行时找不到。这是静态编译和动态编译最大的行为差异,很多人栽在这里。比如要用 linuxfb:

#include <QtPlugin> Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin)

图片格式插件同理,Q_IMPORT_PLUGIN(QJpegPlugin)之类。

第五,qmake 的路径。安装完成后,install/bin/qmake是交叉编译版的 qmake,用它来生成 Makefile 才能编出 aarch64 程序。别用系统自带的 qmake,那是 x86 的。

6. 验证与部署:编出来到底能不能跑

6.1 用 file 和 readelf 验证架构

编出一个测试程序后,先验证架构:

file myapp # 应输出: ELF 64-bit LSB executable, ARM aarch64, ... readelf -d myapp | grep NEEDED

readelf -d看动态依赖。如果静态编译成功,NEEDED里应该只有libc.so.6libstdc++.so.6libm.so.6这几个基础库,没有libQt5*.so。如果看到libQt5Core.so.5,说明静态链接没生效,检查链接参数。

6.2 部署到板子上的注意事项

拷到板子上跑之前,确认几件事:

  • 板子的 glibc 版本不低于工具链的
  • 如果用 linuxfb,确认/dev/fb0存在且有权限
  • 触摸屏的话确认/dev/input/eventX权限
  • 字体文件要放到板子上,或者用 Qt 内置字体

跑的时候如果报This application failed to start because no Qt platform plugin could be initialized,说明 platform 插件没静态导入,回去加Q_IMPORT_PLUGIN

6.3 体积优化

静态编译的程序动辄几十 MB,量产时可能需要裁剪。几个手段:

  • -no-opengl -no-xcb已经砍掉一大块
  • -skip qtwebengine -skip qtdeclarative再砍一大块
  • 编译后aarch64-none-linux-gnu-strip myapp去掉符号表,能减 30% 以上
  • configure 时加-no-feature-XXX关掉不需要的特性

我实测一个纯 Widgets 的串口工具,静态编译 strip 后大概 12MB,可以接受。

7. 我个人的几点实操体会

折腾 Qt 静态交叉编译这件事,最大的感受是configure 参数决定了 80% 的成败。前面花半小时把参数理清楚,比后面花三小时排查链接错误划算得多。我现在的习惯是每次 configure 都把完整命令和摘要存一份,下次换版本直接改版本号复用。

另一个体会是sysroot 一定要干净且完整。我早期图省事,sysroot 里混了 x86 的头文件,结果编出来的东西架构混乱,报错信息还特别误导。后来严格用目标板 rsync 下来的 rootfs,问题少了一大半。

还有一点,别怕重编。Qt 编译虽然慢,但配置错了硬扛着改,最后往往要推倒重来。发现 configure 摘要不对,果断make confclean重来,比在错误的基础上打补丁快。

最后分享一个小技巧:如果你只是想在板子上快速验证 Qt 程序,又不想折腾静态编译,可以先用动态编译 + 把 Qt 库打包进一个目录,用LD_LIBRARY_PATH指过去跑通逻辑,确认业务代码没问题后,再切静态编译做最终交付。这样能把“业务调试”和“编译环境折腾”两件事解耦,效率高很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询