
1. 为什么值得折腾一次 Qt 5.14.2 的 aarch64 静态交叉编译如果你手上有一块 Orange Pi CM5、树莓派 CM4或者任何一块跑着 aarch64 架构 Linux 的板子同时又想用 Qt 做界面那你迟早会撞上同一个问题板子上跑 Qt 程序动态库依赖一大堆部署的时候要拷.so、要配LD_LIBRARY_PATH、要担心目标机上的 Qt 版本和编译机不一致。静态编译就是来解决这个痛点的——把 Qt 库直接编进可执行文件里最后产出一个几乎不依赖外部 Qt 运行时的二进制拷过去就能跑。我这次做的项目就是在 x86_64 的 Ubuntu 开发机上用 aarch64 的交叉编译工具链把 Qt 5.14.2 完整地静态编译出来最终目标是给一块 aarch64 开发板提供一套可用的静态 Qt 开发环境。选 5.14.2 这个版本不是随便挑的它是 Qt 5.14 系列里比较稳定的一个 LTS 性质的版本很多工业项目和嵌入式项目至今还锁在这个版本上而且它对 C11 的支持、对 QML 的支持都比较成熟不像 5.15 后期那样改动频繁。热词里出现的qt-everywhere-src-5.15.10 交叉编译、qt5.12.10交叉编译其实都是同一类需求的不同版本分支思路完全一致。这篇文章适合谁看如果你已经会用 Qt Creator 在 PC 上写程序但对交叉编译只有模糊概念或者你做过动态交叉编译但静态编译一上手就各种报错再或者你只是想知道unknown module(s) in qt: serialport这种报错到底怎么来的——那这篇就是写给你的。我会把整个流程从工具链准备、源码配置、编译踩坑到最终验证一步步拆开讲参数为什么这么设、坑为什么会出现都尽量说透。需要提前说明的是静态编译 Qt 是个体力活全程编译时间在普通四核机器上大概要 1 到 3 小时配置阶段一旦出错重来一次的成本很高。所以下面每一步的配置我都会解释清楚它的作用让你在动手前就知道自己在干什么而不是复制粘贴一堆命令然后祈祷能过。2. 整体方案设计与关键选型思路2.1 静态编译和动态编译到底差在哪先把这个概念说清楚不然后面的配置你看着会一头雾水。动态编译的 Qt你的程序在运行时需要去加载libQt5Core.so、libQt5Gui.so这些共享库这些库要么装在目标机的系统目录里要么跟着程序一起打包。静态编译则是把这些库的代码在链接阶段直接塞进你的可执行文件最终产物是一个体积大但自包含的二进制。静态编译的好处很直接部署简单一个文件拷过去就能跑不用担心目标机缺库、版本不匹配。坏处也很明显可执行文件体积会从几 MB 膨胀到几十 MB如果程序里用了 LGPL 授权的 Qt 模块静态链接在合规上需要额外注意这个属于法务范畴本文不展开还有就是编译过程本身更复杂对工具链和配置的要求更高。我这次选静态编译核心原因就是目标板子的系统镜像里没有预装 Qt而且我不想每次更新程序都去同步一堆.so。对于嵌入式场景这种一次编译、到处运行的诉求非常普遍。2.2 为什么用 aarch64 而不是 armhfaarch64 指的是 ARM 的 64 位架构armhf 是 32 位硬浮点架构。现在主流的开发板比如 Orange Pi CM5、树莓派 4/5、瑞芯微 RK3588 系列基本都是 aarch64。64 位架构在内存寻址、寄存器数量、浮点性能上都有优势尤其是跑 Qt 这种图形框架64 位下内存管理更从容。选 aarch64 的另一个现实原因是工具链成熟度。现在gcc-arm-*系列的 aarch64 工具链比如gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu非常稳定交叉编译 Qt 的兼容性问题比早年少了很多。热词里提到的为什么还要用gcc-arm工具链交叉编译答案就在这里因为开发机是 x86_64目标机是 aarch64指令集不同必须用能生成 aarch64 代码的编译器这就是交叉编译工具链存在的意义。2.3 工具链的选择与取舍工具链这块我实际用的是 ARM 官方发布的gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu。选它的理由有几个一是 GCC 10.3 对 C17 支持完善Qt 5.14.2 的源码用它能顺利编过二是这个工具链自带的 sysroot 比较干净不会混入宿主机的库导致链接错误三是它是官方维护的稳定性有保障。热词里还出现了env工具链、unity工具链、autosar工具链、etas工具链这些大多是特定行业或特定厂商的专用工具链跟通用 Qt 交叉编译不是一回事。如果你手头有厂商提供的 SDK 工具链优先用厂商的因为它的 sysroot 和目标板系统最匹配。没有的话ARM 官方工具链是通用性最好的选择。这里有个关键点工具链的 sysroot 必须和目标板的根文件系统版本尽量一致。如果你的板子跑的是 Ubuntu 20.04 的 aarch64 镜像而工具链 sysroot 里的 glibc 版本比板子上的还新那编出来的程序在板子上就会报GLIBC_2.xx not found。这个坑我在后面排查章节会详细讲。2.4 源码版本与目录规划Qt 5.14.2 的源码包是qt-everywhere-src-5.14.2.tar.xz从 Qt 官方归档站可以下载到。注意不要用在线安装器装的那套那套是给桌面开发用的交叉编译必须用完整源码包。目录规划我建议这样~/qt-build/ ├── toolchain/ # 交叉编译工具链解压目录 ├── src/ # Qt 源码解压目录 │ └── qt-everywhere-src-5.14.2/ ├── build/ # 编译输出目录out-of-source 编译 └── install/ # 最终安装目录把源码和编译目录分开out-of-source 编译是个好习惯编译失败想重来时直接删掉 build 目录重新配置就行不用重新解压源码。这个习惯在反复调试配置参数的时候能省大量时间。3. 环境准备与工具链配置实操3.1 开发机基础依赖安装开发机我用的是 Ubuntu 20.04 x86_64。在动手编译 Qt 之前先把宿主机需要的基础工具装齐否则配置阶段会缺各种东西sudo apt update sudo apt install -y build-essential perl python3 git \ bison flex gperf libxcb-xinerama0-dev \ libfontconfig1-dev libfreetype6-dev \ libx11-dev libxext-dev libxfixes-dev \ libxi-dev libxrender-dev libxcb1-dev \ libxcb-glx0-dev libxcb-keysyms1-dev \ libxcb-image0-dev libxcb-shm0-dev \ libxcb-icccm4-dev libxcb-sync-dev \ libxcb-xfixes0-dev libxcb-shape0-dev \ libxcb-randr0-dev libxcb-render-util0-dev \ libxkbcommon-dev libxkbcommon-x11-dev这些包看着多其实分两类一类是编译 Qt 本身需要的构建工具perl、python3、bison、flex、gperf另一类是 Qt 的 xcb 平台插件依赖的 X11 相关开发库。哪怕你是给嵌入式板子编译Qt 的某些模块在配置阶段仍会检测这些库缺了会直接报配置失败。提示如果你只想编 QtCore、QtGui 这些核心模块不做 xcb 平台可以少装一些 X11 库。但 Qt 的 configure 脚本检测逻辑比较贪心装全一点能避免很多莫名其妙的配置中断。3.2 交叉编译工具链的部署把下载好的工具链解压到~/qt-build/toolchain/cd ~/qt-build/toolchain tar -xf gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu.tar.xz解压后目录结构是gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/里面有一堆aarch64-none-linux-gnu-*开头的工具。验证工具链能不能用export PATH~/qt-build/toolchain/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin:$PATH aarch64-none-linux-gnu-gcc --version能打印出版本信息就说明工具链本身没问题。接下来要确认它的 sysroot 位置一般在工具链目录下的aarch64-none-linux-gnu/libc/里。这个路径后面配置 Qt 的时候要用到。3.3 编写工具链描述文件Qt 的 configure 需要一个qt.toolchain.cmake或者对应的 qmake 配置来描述交叉编译环境。我这次用的是 qmake 配置方式在 Qt 源码的qtbase/mkspecs/devices/下新建一个自己的 mkspec 目录比如linux-aarch64-gnu-g里面放两个文件。第一个是qmake.confMAKEFILE_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 QMAKE_CFLAGS -pipe -O2 QMAKE_CXXFLAGS -pipe -O2 load(qt_config)第二个是qplatformdefs.h直接引用通用的 Linux 版本#include ../linux-g/qplatformdefs.h这里QT_QPA_DEFAULT_PLATFORM linuxfb是关键它指定了默认的平台插件是 Linux Framebuffer。如果你的板子跑的是 Wayland 或者 X11这里要相应改成wayland或xcb。选 linuxfb 是因为大多数嵌入式板子直接往 framebuffer 上画最省事。3.4 配置参数逐条拆解配置命令是整个流程的核心我把它拆成几段来讲。完整命令大致是这样cd ~/qt-build/build ../src/qt-everywhere-src-5.14.2/configure \ -prefix /opt/qt-5.14.2-aarch64-static \ -opensource -confirm-license \ -release -static \ -xplatform linux-aarch64-gnu-g \ -sysroot ~/qt-build/toolchain/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/aarch64-none-linux-gnu/libc \ -nomake examples -nomake tests \ -no-opengl \ -skip qtwebengine \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype \ -no-glib -no-icu -no-cups -no-dbus \ -no-feature-accessibility \ -qt-pcre \ -v逐条解释-prefix指定安装路径这个路径是目标机上的路径不是开发机上的。程序运行时如果去找 Qt 的插件会从这个路径找。静态编译下这个路径影响不大但插件比如平台插件还是需要单独拷到目标机。-static是静态编译的核心开关它会让 Qt 的库以静态库形式产出。-xplatform linux-aarch64-gnu-g指定用我们刚才建的 mkspec。-sysroot指向工具链的 sysroot这是交叉编译能找到目标系统头文件和库的关键。-nomake examples -nomake tests跳过示例和测试能省下大量编译时间。除非你要研究示例代码否则没必要编。-no-opengl是个重要取舍。如果你的板子没有 GPU 或者不需要 OpenGL 加速关掉它能避免一堆 EGL、GLES 的依赖问题。如果板子有 GPU 且需要 Qt Quick 的硬件加速那就要打开并配置对应的 EGL 库复杂度会上升不少。-skip qtwebengine跳过 WebEngine 模块。这个模块巨大编译要几个小时而且依赖 Chromium交叉编译极其麻烦。除非你明确需要内嵌浏览器否则一定跳过。-qt-zlib -qt-libpng -qt-libjpeg -qt-freetype这几个是让 Qt 用自带的第三方库而不是去链接目标系统的库。静态编译下强烈建议这么设否则会引入一堆外部依赖违背静态编译的初衷。-no-glib -no-icu -no-cups -no-dbus关掉这些可选依赖。glib 和 icu 在嵌入式上通常不需要dbus 如果板子上没有 dbus 服务也用不上。关掉能减少依赖和编译量。-v打开详细输出配置阶段能看到每一步检测的结果出问题好定位。4. 编译过程与核心环节实现4.1 配置阶段的输出解读配置跑完后重点看最后打印的总结信息。它会列出哪些模块会被编译、哪些被跳过、哪些依赖没找到。有几个地方要特别留意如果看到WARNING: Feature xcb is disabled说明 xcb 平台没启用这在纯嵌入式场景是正常的。如果看到某个你需要的模块被 skip 了比如Qt SerialPort那就要回头检查是不是-skip参数写多了或者依赖没满足。热词里反复出现的unknown module(s) in qt: serialport本质原因就是编译 Qt 的时候 SerialPort 模块没被编进去或者编了但没安装到 qmake 能找到的路径。静态编译下这个模块必须显式编进去否则你的.pro里写QT serialport就会报这个错。解决办法是在配置时确保没有-skip qtserialport并且编译安装后检查install/lib/cmake/Qt5SerialPort/目录是否存在。4.2 编译命令与并行度设置配置成功后用 make 开始编译。并行度设置很关键make -j$(nproc)nproc会返回 CPU 核心数。但这里有个经验并行度不要设得太高。如果你的机器是 8 核-j8可能因为内存不够导致某些编译单元被 OOM killer 干掉。我一般设成核心数 - 1比如 8 核用-j7留一点余量给系统。内存小于 16G 的话建议-j4甚至更低。编译过程中如果某个模块报错中断不要慌先看错误信息。常见的错误有几类头文件找不到sysroot 配置问题、某个特性检测失败依赖库缺失、编译器内部错误工具链和源码不兼容。定位到具体模块后可以单独进那个模块目录重新 make不用从头再来。4.3 安装与产物检查编译完成后make install产物会装到-prefix指定的目录。装完后重点检查几个东西ls install/lib/ # 应该有 libQt5Core.a 等静态库 ls install/plugins/platforms/ # 应该有 libqlinuxfb.a 或对应插件 ls install/bin/ # 应该有 qmake静态编译下库文件是.a后缀而不是.so。平台插件在静态模式下也是以静态库形式存在最终链接进你的程序。这一点和动态编译不同动态编译时插件是.so运行时加载静态编译时插件在链接期就被合并进去了。4.4 用 qmake 验证一个最小程序装完后用新编的 qmake 编一个最小 Qt 程序验证export PATH~/qt-build/install/bin:$PATH qmake -v确认 qmake 报告的 Qt 版本是 5.14.2并且是交叉编译版本。然后写个最简单的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 core gui widgets TARGET helloqt SOURCES main.cpp编译qmake make file helloqtfile命令应该显示它是ELF 64-bit LSB executable, ARM aarch64。如果显示的是 x86_64说明 qmake 用错了检查 PATH 和 mkspec 配置。把helloqt拷到板子上运行如果能看到窗口整个静态交叉编译流程就算打通了。5. 常见问题与排查技巧实录5.1 配置阶段典型报错配置阶段最常见的报错是找不到某个库或头文件。比如报Cannot find -lX11说明宿主机缺 X11 开发库回到 3.1 节把依赖装齐。如果报的是目标架构的库找不到比如Cannot find -lEGL那是 sysroot 里没有这个库要么关掉对应特性要么把库补进 sysroot。还有一个隐蔽的坑configure 有时会误用宿主机的库。比如它检测到宿主机有 zlib就直接链接了宿主机的 zlib编出来的程序在目标机上跑不了。这就是为什么我强烈建议加-qt-zlib这类参数强制用 Qt 自带的库避免这种交叉污染。5.2 编译阶段的内存与时间问题编译 Qt 是个吃内存的活。make -j并行度太高时链接阶段可能瞬间吃掉十几 G 内存。如果机器内存不够会看到virtual memory exhausted或者进程被 kill。解决办法就是降低并行度或者给机器加 swap。时间上完整编译 QtBase 大概要 30 到 60 分钟加上其他模块总共 1 到 3 小时是正常的。如果某个模块卡了很久可能是它在做大量模板实例化耐心等。实在等不了可以-skip掉不用的模块。5.3 运行阶段的依赖问题静态编译理论上不该有 Qt 库依赖但 glibc 依赖还是存在的。如果板子上的 glibc 版本比工具链 sysroot 里的旧运行时会报GLIBC_2.xx not found。这个问题的根源是工具链 sysroot 的 glibc 比目标机新。解决办法有两个一是换一个 sysroot 更旧、和目标机匹配的工具链二是把目标机的根文件系统拷过来当 sysroot。后者更彻底但配置起来麻烦。我一般优先选前者找和目标机系统版本接近的工具链。用ldd helloqt可以看程序依赖哪些动态库。静态编译的 Qt 程序ldd输出里应该只有 libc、libm、libpthread 这些系统库没有 libQt5 开头的。5.4 常见问题速查表报错信息可能原因解决方向unknown module(s) in qt: serialportSerialPort 模块未编译或未安装检查 configure 是否 skip确认 install 目录有对应 cmake 文件Cannot find -lX11宿主机缺 X11 开发库安装 libx11-dev 等依赖GLIBC_2.xx not found工具链 sysroot 的 glibc 比目标机新换匹配的工具链或使用目标机根文件系统作 sysrootvirtual memory exhausted并行编译内存不足降低 make -j 并行度或加 swapcannot mix incompatible Qt library混用了不同版本的 Qt 库清理环境变量确保只用一套 Qt程序在板子上无窗口平台插件未正确链接或配置检查 QT_QPA_DEFAULT_PLATFORM 和插件是否编入5.5 几个独家避坑经验第一个经验配置阶段一定要加-v并且把输出重定向到文件保存。配置一次要几分钟出错后如果没存日志只能重跑。存下来后可以慢慢翻定位是哪一步检测失败。第二个经验编译前先确认磁盘空间。Qt 源码加上编译中间产物轻松占用 20G 以上。df -h看一眼空间不够会编到一半失败前功尽弃。第三个经验静态编译的 Qt 程序体积大如果目标板存储紧张可以在链接时加-Wl,--gc-sections并配合-ffunction-sections -fdata-sections编译选项把没用到的代码段裁掉。这个优化能让体积减少 20% 到 40%但配置起来要改 mkspec属于进阶操作。第四个经验如果你同时维护多个目标板建议给每个板子单独建一套 install 目录不要共用。不同板子的 sysroot 和 glibc 版本可能不同共用一套 Qt 静态库迟早出问题。6. 静态 Qt 在嵌入式项目中的实际应用建议6.1 模块裁剪策略静态编译最大的成本是体积所以模块裁剪很重要。默认配置会编很多你用不到的模块比如 QtSql、QtMultimedia、QtWebSockets。如果你的项目只做界面和串口通信那QT core gui widgets serialport就够了其他模块在 configure 阶段用-skip掉。裁剪的原则是先按最小需求配编出来跑通再按需加模块。反过来先全编再裁浪费时间。我这次的项目只保留了 core、gui、widgets、serialport、network 这几个编译时间和产物体积都控制得不错。6.2 部署时的插件处理静态编译下平台插件是链接进程序的但有些插件比如图片格式插件、字体插件可能还是需要单独处理。Qt 提供了一套静态插件导入机制需要在代码里用Q_IMPORT_PLUGIN宏显式导入。比如要用 JPEG 图片得在 main.cpp 里加#include QtPlugin Q_IMPORT_PLUGIN(QJpegPlugin)不加这个程序运行时可能加载不了 JPEG。这个坑很多人踩过明明编了 JPEG 支持运行时却说格式不支持原因就是插件没被显式导入。6.3 调试与日志静态编译的程序调试起来和动态版差不多但因为没有独立的 Qt 库gdb 里看不到 Qt 库的符号。如果需要调试 Qt 内部得用带调试信息的 Qt 静态库编译时加-debug而不是-release。不过调试版体积更大一般只在排查疑难问题时用。日志方面Qt 的qDebug()输出默认走 stderr。嵌入式板子上如果没有终端可以把日志重定向到文件或者用qInstallMessageHandler自定义日志处理。这个在排查板子上的运行时问题时很有用。6.4 版本升级的考量Qt 5.14.2 是个相对稳定的版本但如果你的项目要长期维护得考虑后续升级。Qt 5.15 是 5 系列的最后一个 LTS6 系列则是大版本变更。静态交叉编译的配置在不同版本间会有差异比如 5.15 的 configure 参数有些变化6 系列则改用 CMake 构建系统。我的建议是如果 5.14.2 能满足需求就别轻易升级因为每次升级都意味着重新验证整套交叉编译流程。如果确实需要新特性先在开发机上把新版本的静态编译跑通再迁移项目代码。热词里提到的qt-everywhere-src-5.15.10 交叉编译和qt5.12.10交叉编译流程和本文基本一致主要差异在 configure 参数和依赖库版本上。掌握了 5.14.2 这套方法迁移到相邻版本问题不大。6.5 关于工具链的再补充最后再聊聊工具链。热词里stm开发需要安装 arm-gcc交叉编译链吗这个问题答案是STM32 这类 Cortex-M 单片机用的是arm-none-eabi工具链和本文的aarch64-none-linux-gnu不是一回事。前者面向裸机或 RTOS没有 Linux 系统调用后者面向跑 Linux 的 aarch64 应用处理器。选工具链前先搞清楚目标平台是裸机 MCU还是跑 Linux 的 AP这决定了你用哪套工具链。boost库交叉编译也是类似道理Boost 作为 C 库交叉编译时同样需要指定工具链和 sysroot思路和 Qt 一致配置时告诉构建系统用哪个编译器、去哪个 sysroot 找依赖。掌握了 Qt 这套流程编译其他 C 库的交叉版本会容易很多。我在实际项目里踩过的最大一个坑是早期没注意 sysroot 的 glibc 版本匹配编出来的程序在板子上死活跑不起来报一堆 GLIBC 版本错误。后来换成和目标机系统版本一致的工具链问题立刻消失。所以如果你只记一件事就记住工具链的 sysroot 版本必须和目标板系统匹配这是静态交叉编译能否成功的地基。