aarch64 平台 Qt 5.14.2 静态编译与交叉编译实战手册
1. 为什么要在 aarch64 上折腾 Qt 5.14.2 静态编译如果你手上有块 ARM 开发板、一台国产化平台的工控机或者正在给某个嵌入式设备做 HMI大概率绕不开一个需求把 Qt 程序编译成一个不依赖系统 Qt 库的独立可执行文件。动态链接的方案在开发机上跑得好好的一拷到目标板就报cannot mix incompatible Qt library或者干脆找不到libQt5Core.so.5这种场景我遇到过太多次了。静态编译的核心价值就一句话把 Qt 运行时全部塞进可执行文件里目标机器上只要有 glibc 和基本的系统库就能跑。对于 aarch64 这种交叉编译场景这个特性尤其重要——你不可能要求每台目标设备都预装一套版本完全匹配的 Qt 动态库。Qt 5.14.2 是个比较特殊的选择。它是 Qt 5.14 系列的最后一个补丁版本LTS 支持周期长很多工业项目至今还锁死在这个版本上。而 aarch64也就是 ARM64现在是嵌入式 Linux 的主流架构从瑞芯微到全志再到各种国产 SoC基本都是这个架构。把这两个组合起来做静态交叉编译是很多做嵌入式 Qt 开发的人必须跨过的一道坎。这篇手册面向的是已经会用 Qt 写程序、但对交叉编译工具链和静态构建流程不太熟悉的开发者。我会从工具链准备开始一步步走到最终产出一个能在目标板上直接运行的静态可执行文件。整个过程我在多个项目里反复走过踩过的坑都会标出来。注意静态编译 Qt 会涉及开源许可问题。Qt 的 LGPL 协议对静态链接有额外要求需要提供目标文件或使用商业许可商用项目请务必确认许可合规性。本文只讨论技术实现。2. 工具链与 sysroot 的准备工作2.1 交叉编译工具链的选型逻辑aarch64 的交叉编译工具链有好几个来源常见的有 Linaro 的 GCC 工具链、ARM 官方维护的 GNU Toolchain、以及各种 SoC 厂商随 SDK 附带的工具链。选哪个不是随便挑的核心判断标准是工具链的 glibc 版本必须小于等于目标板上的 glibc 版本。举个例子如果你的工具链用的是 glibc 2.31而目标板上只有 glibc 2.28那编译出来的程序在目标板上会直接报GLIBC_2.31 not found。这个错误跟 Qt 没关系是系统 C 库的版本问题。所以第一步永远是确认目标板的 glibc 版本# 在目标板上执行 ldd --version假设目标板输出的是ldd (GNU libc) 2.28那你就需要找一个 glibc 版本不超过 2.28 的工具链。Linaro 的gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu用的是 glibc 2.25 左右兼容性比较好是我常用的一个版本。工具链下载后解压到某个目录比如/opt/toolchain/然后把它加到 PATH 里export PATH/opt/toolchain/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin:$PATH export CROSS_COMPILEaarch64-linux-gnu-验证一下aarch64-linux-gnu-gcc --version能正常输出版本信息就说明工具链可用了。2.2 sysroot 的构建从目标板提取还是自己搭sysroot 是交叉编译里最容易出问题的地方。它本质上是一个目录里面放着目标系统的头文件和库文件编译时链接器会从这里找依赖。sysroot 有两个来源方案一从目标板直接提取。如果目标板已经跑起来了可以把/usr/include、/usr/lib、/lib这些目录打包拷到开发机上。这个方案最准确因为用的就是目标板真实的库。但缺点是目标板上的库可能不全缺开发用的头文件。方案二用 Buildroot 或 Yocto 生成。如果你能控制目标系统的构建流程直接用 Buildroot 生成一个带 sysroot 的 SDK 是最干净的方案。Buildroot 执行make sdk后会产出一个 SDK 压缩包解压后里面就有完整的工具链和 sysroot。我个人的经验是如果目标板是现成的商用设备用方案一如果是自己做的板子用方案二。方案一提取 sysroot 的时候要注意不要直接把整个/usr/lib拷过来里面有很多开发机上不需要的东西。至少要保证这几个目录存在sysroot/ ├── usr/ │ ├── include/ # 头文件 │ └── lib/ # 库文件 ├── lib/ # 系统库 └── usr/lib/aarch64-linux-gnu/ # 架构相关的库2.3 用 qmake 的 mkspec 描述工具链Qt 的交叉编译配置依赖 mkspec 文件来告诉 qmake 用哪个编译器和 sysroot。Qt 源码里已经自带了一些 aarch64 的 mkspec比如linux-aarch64-gnu-g。但自带的 mkspec 通常不会指定你的 sysroot 路径需要自己改。最省事的做法是复制一份现有的 mkspec 然后修改cd qt-everywhere-src-5.14.2/qtbase/mkspecs cp -r linux-aarch64-gnu-g linux-aarch64-custom然后编辑linux-aarch64-custom/qmake.confQMAKE_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 QMAKE_CFLAGS -marcharmv8-a -mtunecortex-a53 QMAKE_CXXFLAGS $$QMAKE_CFLAGS QMAKE_LFLAGS --sysroot/opt/sysroot QMAKE_CFLAGS --sysroot/opt/sysroot QMAKE_CXXFLAGS --sysroot/opt/sysroot这里-marcharmv8-a是 aarch64 的基础指令集-mtunecortex-a53是针对具体 CPU 核心做优化。如果你的目标板是 Cortex-A72 或 A55把-mtune改掉就行。--sysroot指向你准备好的 sysroot 目录。提示-march和-mtune不要乱写。-march决定了能用的指令集写高了在旧 CPU 上会跑出非法指令。不确定的话就用-marcharmv8-a这是所有 aarch64 CPU 都支持的基线。3. Qt 5.14.2 源码配置与静态构建参数拆解3.1 源码获取与目录规划Qt 5.14.2 的源码包可以从 Qt 官方下载站获取文件名是qt-everywhere-src-5.14.2.tar.xz大概 500 多 MB。解压后是一个包含所有 Qt 模块的大目录。我建议的目录结构是这样的/home/user/qt-build/ ├── qt-everywhere-src-5.14.2/ # 源码 ├── build-aarch64-static/ # 构建目录out-of-source └── install-aarch64-static/ # 安装目录一定要用 out-of-source 构建也就是在源码目录外面单独建一个 build 目录。Qt 的构建系统支持这样做好处是源码目录保持干净构建失败重来的时候不用重新解压源码。我见过太多人直接在源码目录里./configure结果配置错了想重来只能删掉整个源码重新解压。3.2 configure 参数逐项分析Qt 5.14.2 用的是./configure脚本不是 CMake配置命令比较长我把它拆开逐项解释cd /home/user/qt-build/build-aarch64-static ../qt-everywhere-src-5.14.2/configure \ -prefix /home/user/qt-build/install-aarch64-static \ -opensource -confirm-license \ -release \ -static \ -nomake examples -nomake tests \ -no-opengl \ -no-xcb \ -no-eglfs \ -no-linuxfb \ -qt-zlib -qt-libpng -qt-libjpeg \ -qt-freetype -qt-harfbuzz \ -skip qtwebengine \ -skip qtwebkit \ -skip qtdeclarative \ -xplatform linux-aarch64-custom \ -sysroot /opt/sysroot \ -no-glib \ -no-icu \ -no-cups \ -no-dbus \ -no-gui \ -no-widgets先别急着抄这里面有几个参数需要根据你的实际需求调整。我逐项说明-prefix指定安装路径。编译完成后make install会把所有文件装到这里。这个路径后面配置 Qt Creator 或者写 qmake 的-query时会用到。-static是核心参数生成静态库.a文件而不是动态库.so文件。所有 Qt 模块都会被编译成静态库最终链接进你的可执行文件。-nomake examples -nomake tests跳过示例和测试代码的编译。Qt 的示例和测试非常多编译它们会浪费大量时间而且交叉编译环境下很多示例根本编译不过。除非你需要某个特定示例做参考否则一律跳过。-no-opengl这个要看你的目标板有没有 GPU。如果目标板有 OpenGL ES 支持并且你需要 Qt Quick 的硬件加速渲染那就要保留 OpenGL 相关配置。但如果是纯 CPU 渲染的场景关掉 OpenGL 能省掉一大堆依赖问题。我遇到的大部分工控 HMI 场景都是纯 CPU 渲染直接关掉最省事。-no-xcb关掉 X11 平台插件。嵌入式设备通常不用 X11用 EGLFS 或 LinuxFB 直接渲染到 framebuffer。如果你确实需要 X11那要确保 sysroot 里有 X11 的开发库。-qt-zlib -qt-libpng -qt-libjpeg这几个参数让 Qt 使用自带的第三方库版本而不是系统里的。静态编译时强烈建议这样做因为系统库的静态版本不一定存在而且版本兼容性不好控制。Qt 自带的这些库版本是经过测试的用起来最稳。-skip qtwebengine -skip qtwebkit这两个模块体量巨大而且依赖非常多尤其是 qtwebengine 依赖 Chromium交叉编译极其困难。除非你的项目确实需要浏览器内核否则直接跳过。-skip qtdeclarative这个要特别注意。qtdeclarative 就是 Qt Quick / QML 模块。如果你只用 Qt Widgets 做传统桌面风格的界面可以跳过它能省很多编译时间。但如果你要用 QML就绝对不能跳过。我见过有人跳过了 qtdeclarative 然后发现 QML 程序编译不了回头重新编译白白浪费几个小时。-no-gui -no-widgets这两个参数只在做纯后台服务、不需要任何图形界面时才用。如果你要做 HMI千万不要加这两个参数否则编译出来的 Qt 根本没有 GUI 模块。我在第一次做静态编译时就犯过这个错configure 的时候顺手加了-no-gui结果编译完发现连QApplication都没有只能从头再来。3.3 一个更实用的配置示例上面那个配置是偏保守的适合纯 CPU 渲染的 Widgets 应用。如果你的场景需要 QML 和 OpenGL用下面这个../qt-everywhere-src-5.14.2/configure \ -prefix /home/user/qt-build/install-aarch64-static \ -opensource -confirm-license \ -release \ -static \ -nomake examples -nomake tests \ -opengl es2 \ -eglfs \ -qt-zlib -qt-libpng -qt-libjpeg \ -qt-freetype -qt-harfbuzz \ -skip qtwebengine \ -skip qtwebkit \ -xplatform linux-aarch64-custom \ -sysroot /opt/sysroot \ -no-glib \ -no-icu \ -no-cups \ -no-dbus \ -no-feature-accessibility这里-opengl es2启用 OpenGL ES 2.0-eglfs启用 EGLFS 平台插件直接渲染到 framebuffer不经过窗口系统。-no-feature-accessibility关掉无障碍支持嵌入式场景基本用不到能减小体积。配置完成后configure 会输出一个摘要列出哪些模块会被编译、哪些被跳过。一定要仔细看这个摘要确认 GUI 模块是 enabled 状态确认你需要的模块都在列表里。如果发现某个模块被意外跳过了现在改还来得及等编译到一半再发现就晚了。4. 编译过程中的资源控制与常见报错处理4.1 并行编译的度怎么把握Qt 的编译时间很长全量编译在普通开发机上可能要一两个小时。用make -jN并行编译能大幅缩短时间但 N 的值不能随便设。make -j$(nproc)nproc会返回 CPU 核心数。但 Qt 编译时单个编译单元的内存占用可能达到 1-2 GB如果你的机器内存不够开太多并行任务会导致 OOM内存耗尽编译进程被系统杀掉。我的经验值是每 2 GB 可用内存对应 1 个并行任务。比如 16 GB 内存的机器make -j8比较稳妥。32 GB 可以上-j16。如果编译过程中看到virtual memory exhausted或者Killed这样的错误就是并行度太高了降低 N 值重新来。另外Qt 的编译不是完全并行的某些模块之间有依赖关系。make -jN在依赖处理上没问题但如果你中途中断了编译重新make的时候可能会因为部分文件已经编译而产生奇怪的错误。遇到这种情况最稳妥的做法是make clean后重新编译虽然费时间但能避免很多玄学问题。4.2 几个高频报错的定位与修复报错一fatal error: bits/libc-header-start.h: No such file or directory这个错误说明编译器找不到 sysroot 里的头文件。检查两个地方一是--sysroot路径是否正确二是 sysroot 里是否真的有usr/include/bits/libc-header-start.h这个文件。如果 sysroot 是从目标板提取的很可能缺少开发用的头文件需要从工具链自带的 sysroot 里补充。报错二cannot find -lstdc链接阶段找不到 C 标准库的静态版本。交叉编译工具链通常同时提供动态和静态的 libstdc但有些精简版的工具链只带了动态版本。检查工具链目录下是否有libstdc.afind /opt/toolchain -name libstdc.a如果没有需要换一个完整的工具链或者从其他地方获取静态版 libstdc。报错三error: unknown type name clockid_t之类的类型未定义这通常是 sysroot 里的头文件版本和工具链不匹配导致的。比如工具链的 GCC 版本较新需要 glibc 2.30 的头文件但 sysroot 里是 glibc 2.28 的头文件。解决办法是让工具链和 sysroot 来自同一个来源比如都用 Buildroot 生成的 SDK。报错四编译到某个模块时突然报undefined reference to ...静态编译时链接顺序很重要。Qt 的构建系统一般会处理好但如果你手动修改过.pro文件或Makefile可能会打乱链接顺序。静态库的链接顺序原则是依赖别人的库放在前面被依赖的库放在后面。比如libQt5Widgets.a依赖libQt5Gui.a那链接时-lQt5Widgets要写在-lQt5Gui前面。4.3 编译完成后的验证编译和安装完成后先别急着写程序做几个基本验证# 检查安装目录结构 ls /home/user/qt-build/install-aarch64-static/ # 应该看到 bin/ include/ lib/ 等目录 # 检查 qmake 是否可用 /home/user/qt-build/install-aarch64-static/bin/qmake -query # 检查静态库是否存在 ls /home/user/qt-build/install-aarch64-static/lib/libQt5Core.aqmake -query会输出一堆配置信息重点看QT_INSTALL_PREFIX、QT_VERSION、QT_HOST_PREFIX这几项是否正确。如果QT_VERSION显示的不是 5.14.2说明 qmake 用错了可能 PATH 里还有系统自带的 qmake。5. 用静态 Qt 编译你的第一个 aarch64 程序5.1 项目 .pro 文件的静态适配用静态 Qt 编译程序和动态 Qt 在.pro文件写法上基本一样但有几个细节要注意QT core gui widgets TARGET myapp TEMPLATE app SOURCES main.cpp \ mainwindow.cpp HEADERS mainwindow.h # 静态编译时建议加上这些 CONFIG static CONFIG release # 如果用到网络模块 QT network # 如果用到串口模块 QT serialport这里特别说一下QT serialport。Qt 5.14.2 的 serialport 模块在静态编译时有个坑如果你在 configure 时没有显式启用 serialport它可能不会被编译进静态库。编译时会报unknown module(s) in qt: serialport。解决办法是在 configure 时加上-qt-serialport参数或者确认 configure 摘要里 serialport 是 enabled 状态。5.2 交叉编译时的 qmake 调用用静态 Qt 的 qmake 来生成 Makefilecd /path/to/your/project /home/user/qt-build/install-aarch64-static/bin/qmake \ -spec linux-aarch64-custom \ yourproject.pro make -j4生成的 Makefile 里会自动带上--sysroot和交叉编译器的路径。编译完成后用file命令检查产物file myapp # 应该输出ELF 64-bit LSB executable, ARM aarch64, ...如果输出的是x86-64说明 qmake 用错了检查一下是不是 PATH 里的 qmake 覆盖了静态 Qt 的 qmake。5.3 静态可执行文件的体积优化静态链接 Qt 后可执行文件的体积会非常大。一个简单的 Widgets 程序动态链接时可能只有几百 KB静态链接后可能达到 20-30 MB。这是因为 Qt 的静态库把所有用到的代码都链接进来了。优化体积有几个手段第一编译时加-Os优化。在 mkspec 的QMAKE_CFLAGS里加上-Os让编译器优先优化体积而不是速度。对于大多数 HMI 场景体积比那一点点性能差异重要得多。第二链接时加-Wl,--gc-sections。这个链接器选项会移除没有被引用的代码段。配合编译时的-ffunction-sections -fdata-sections使用效果最好。第三strip 掉符号表。编译完成后执行aarch64-linux-gnu-strip myappstrip 能去掉调试符号通常能减小 30%-50% 的体积。但 strip 之后就没办法用 gdb 调试了所以建议保留一份未 strip 的版本用于调试。第四按需裁剪 Qt 模块。如果项目只用到了 core、gui、widgets那 configure 时就只编译这几个模块其他全部 skip。每少一个模块最终体积就能小一些。我做过一个对比一个中等复杂度的 Widgets 程序全功能静态编译后是 28 MB经过上述优化后能压到 12 MB 左右。如果目标板的存储空间紧张这些优化还是很有必要的。6. 部署到目标板与运行时问题排查6.1 部署前的依赖检查静态编译的可执行文件虽然不依赖 Qt 的动态库但仍然依赖系统 C 库和其他基础库。部署前用readelf检查一下动态依赖aarch64-linux-gnu-readelf -d myapp | grep NEEDED输出可能类似0x0000000000000001 (NEEDED) Shared library: [libpthread.so.0] 0x0000000000000001 (NEEDED) Shared library: [libdl.so.2] 0x0000000000000001 (NEEDED) Shared library: [librt.so.1] 0x0000000000000001 (NEEDED) Shared library: [libstdc.so.6] 0x0000000000000001 (NEEDED) Shared library: [libm.so.6] 0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1] 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]这些都是系统基础库目标板上一般都有。但要确认版本匹配尤其是libstdc.so.6和libgcc_s.so.1这两个是 GCC 运行时库版本不对会直接报错。6.2 目标板上的运行环境配置把可执行文件拷到目标板后直接运行可能会遇到几个问题问题一error while loading shared libraries: libstdc.so.6: cannot open shared object file目标板上没有 libstdc。解决办法是从工具链里把libstdc.so.6和libgcc_s.so.1拷到目标板的/usr/lib下。注意要用工具链里的版本不要用开发机上的。问题二程序启动后黑屏或花屏这通常是平台插件的问题。静态编译的 Qt 需要显式指定平台插件。在main()函数开头加上#include QApplication #include qpa/qplatformintegrationfactory_p.h int main(int argc, char *argv[]) { // 如果使用 EGLFS qputenv(QT_QPA_PLATFORM, eglfs); // 或者使用 LinuxFB // qputenv(QT_QPA_PLATFORM, linuxfb); QApplication app(argc, argv); // ... }或者在启动脚本里设置环境变量export QT_QPA_PLATFORMeglfs ./myapp问题三字体显示为方块或乱码静态编译时如果没把字体编译进去程序运行时找不到字体就会显示方块。解决办法是在 configure 时加上-qt-freetype和-qt-harfbuzz并且在程序里用QFontDatabase::addApplicationFont()加载字体文件int fontId QFontDatabase::addApplicationFont(/usr/share/fonts/myfont.ttf); QStringList fontFamilies QFontDatabase::applicationFontFamilies(fontId); if (!fontFamilies.isEmpty()) { QFont font(fontFamilies.at(0)); app.setFont(font); }6.3 一个完整的部署脚本示例把部署过程写成一个脚本每次更新程序时直接跑脚本省得手动操作#!/bin/bash # deploy.sh - 部署静态编译的 Qt 程序到目标板 TARGET_IP192.168.1.100 TARGET_USERroot APP_NAMEmyapp LOCAL_APP./myapp REMOTE_DIR/opt/myapp # 检查可执行文件是否存在 if [ ! -f $LOCAL_APP ]; then echo 错误找不到 $LOCAL_APP exit 1 fi # 检查是否为 aarch64 架构 ARCH$(file $LOCAL_APP | grep -o ARM aarch64) if [ -z $ARCH ]; then echo 错误$LOCAL_APP 不是 aarch64 架构 exit 1 fi # 创建远程目录 ssh ${TARGET_USER}${TARGET_IP} mkdir -p ${REMOTE_DIR} # 拷贝可执行文件 scp $LOCAL_APP ${TARGET_USER}${TARGET_IP}:${REMOTE_DIR}/ # 拷贝可能需要的运行时库如果目标板缺少 # scp /opt/toolchain/aarch64-linux-gnu/lib/libstdc.so.6 \ # ${TARGET_USER}${TARGET_IP}:/usr/lib/ # 设置执行权限 ssh ${TARGET_USER}${TARGET_IP} chmod x ${REMOTE_DIR}/${APP_NAME} echo 部署完成这个脚本里我加了架构检查防止误把 x86 的程序拷到 ARM 板上。这个检查很有必要我就干过这种事——在开发机上编译完直接 scp到板子上运行报cannot execute binary file排查了半天才发现是架构不对。7. 静态编译 Qt 的几个经验性判断7.1 什么时候不该用静态编译静态编译不是万能的有些场景下动态链接反而更合适场景一多个 Qt 程序跑在同一台设备上。如果设备上要跑三四个 Qt 程序每个都静态链接一份 Qt存储空间浪费严重。这种情况下动态链接一份共享的 Qt 库更合理。场景二需要热更新 Qt 版本。静态编译后 Qt 的代码已经嵌在可执行文件里了想升级 Qt 版本必须重新编译整个程序。动态链接的话只需要替换.so文件。场景三LGPL 许可合规有困难。前面提过LGPL 对静态链接有额外要求。如果法务那边过不了就只能用动态链接。7.2 编译时间的现实预期在一台 8 核 16 GB 的机器上用make -j8编译 Qt 5.14.2 的 core gui widgets network serialport 这几个模块大概需要 40 分钟到 1 小时。如果加上 qtdeclarativeQML时间会翻倍。全模块编译不 skip 任何东西可能要 3 小时以上。所以我的建议是第一次编译时只编译你确定需要的模块后面发现缺什么再补。不要一上来就全模块编译浪费时间不说还容易因为某个不相关的模块编译失败而卡住。7.3 关于 Qt 5.14.2 的版本锁定Qt 5.14.2 是 2020 年发布的到现在已经有好几年了。新项目如果没特殊原因其实可以考虑 Qt 5.15 系列。但很多工业项目因为历史原因锁死在 5.14.2 上迁移成本太高。如果你正在维护这样的项目那这篇手册里的流程可以直接用。需要留意的是Qt 5.14.2 的源码里有些第三方库的版本比较老在较新的编译环境下可能会有警告甚至错误。比如某些模块里的configure脚本对 GCC 10 的支持不太好。如果遇到这类问题最简单的办法是用 GCC 7 或 GCC 8 的工具链兼容性最好。7.4 一个容易忽略的细节qmake 的缓存Qt 的 qmake 会在项目目录下生成.qmake.stash文件缓存一些配置信息。如果你切换了不同的 Qt 版本比如从动态 Qt 切到静态 Qt这个缓存文件可能导致 qmake 行为异常。遇到莫名其妙的编译错误时先删掉.qmake.stash和Makefile重新跑 qmakemake distclean rm -f .qmake.stash /path/to/static/qmake yourproject.pro make -j4这个操作我至少做过几十次每次切换 Qt 版本或者修改 mkspec 后都应该做一遍。看起来是小事但能避免很多让人抓狂的玄学问题。8. 从编译产物到可维护的构建流程8.1 把配置参数固化到脚本里每次手动敲那一长串 configure 参数很容易出错而且过几个月回头看根本记不住当时为什么这么配。我的做法是写一个build.sh脚本把所有参数和注释都放进去#!/bin/bash # build.sh - Qt 5.14.2 aarch64 静态编译脚本 # 最后更新2024-01-15 # 目标平台aarch64 Linux, glibc 2.28 # 工具链gcc-linaro-7.5.0-2019.12 set -e # 遇到错误立即退出 QT_SRC/home/user/qt-build/qt-everywhere-src-5.14.2 BUILD_DIR/home/user/qt-build/build-aarch64-static INSTALL_DIR/home/user/qt-build/install-aarch64-static SYSROOT/opt/sysroot TOOLCHAIN/opt/toolchain/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu export PATH${TOOLCHAIN}/bin:${PATH} # 清理旧的构建目录 if [ -d ${BUILD_DIR} ]; then echo 清理旧构建目录... rm -rf ${BUILD_DIR} fi mkdir -p ${BUILD_DIR} cd ${BUILD_DIR} # 配置 ${QT_SRC}/configure \ -prefix ${INSTALL_DIR} \ -opensource -confirm-license \ -release \ -static \ -nomake examples -nomake tests \ -no-opengl \ -no-xcb \ -no-eglfs \ -no-linuxfb \ -qt-zlib -qt-libpng -qt-libjpeg \ -qt-freetype -qt-harfbuzz \ -skip qtwebengine \ -skip qtwebkit \ -skip qtdeclarative \ -xplatform linux-aarch64-custom \ -sysroot ${SYSROOT} \ -no-glib \ -no-icu \ -no-cups \ -no-dbus \ -no-feature-accessibility \ 21 | tee configure.log # 编译 make -j8 21 | tee build.log # 安装 make install 21 | tee install.log echo 编译完成安装目录${INSTALL_DIR}这个脚本的好处是所有参数一目了然想改什么直接改脚本日志被保存下来出问题可以回溯set -e保证任何一步失败都会立即停止不会带着错误继续往下跑。8.2 版本管理与可复现性Qt 静态编译的产物安装目录通常有几个 GB不适合直接放进 Git 仓库。但构建脚本、mkspec 文件、sysroot 的获取方式这些应该纳入版本管理。我的做法是建一个qt-build-config仓库里面放qt-build-config/ ├── build.sh # 构建脚本 ├── mkspecs/ │ └── linux-aarch64-custom/ │ └── qmake.conf # 自定义 mkspec ├── sysroot-info.md # sysroot 来源和版本说明 └── README.md # 整体说明这样换一台机器或者换一个同事来做直接 clone 这个仓库按照 README 操作就能复现出一样的编译环境。比口头交接或者写一堆零散的笔记靠谱得多。8.3 增量编译的可行性Qt 的构建系统支持增量编译。如果你只改了 mkspec 里的某个编译选项理论上make会重新编译受影响的文件。但实际经验是改了 configure 参数后最好还是重新完整编译。因为 configure 参数会影响很多模块的编译条件增量编译可能只重新编译了一部分导致最终产物里混着新旧两种配置的代码运行起来行为诡异。如果只是改了项目代码不是 Qt 本身那增量编译完全没问题make会只重新编译改动的文件几秒钟就能完成。8.4 交叉编译环境的容器化如果团队里有多个人需要做这个编译或者需要在 CI 上自动构建把整个环境容器化是最省事的方案。写一个 Dockerfile把工具链、sysroot、依赖包都装进去FROM ubuntu:20.04 RUN apt-get update apt-get install -y \ build-essential \ wget \ xz-utils \ python3 \ perl \ bison \ flex \ gperf \ libssl-dev \ rm -rf /var/lib/apt/lists/* # 安装交叉编译工具链 COPY gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz /opt/ RUN cd /opt tar xf gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz # 拷贝 sysroot COPY sysroot /opt/sysroot ENV PATH/opt/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin:${PATH} WORKDIR /build用这个镜像构建不管在谁的机器上跑结果都是一致的。CI 上也能直接用不需要每次手动配环境。9. 一些零散但重要的补充9.1 关于 Qt 的插件系统静态编译 Qt 时插件比如平台插件、图片格式插件的处理方式和动态编译不同。动态编译时插件是独立的.so文件运行时从插件目录加载。静态编译时插件需要被显式链接进可执行文件。Qt 提供了一个宏Q_IMPORT_PLUGIN来导入静态插件。比如要使用 LinuxFB 平台插件#include QtPlugin Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin)如果忘了导入插件程序启动时会报This application failed to start because no Qt platform plugin could be initialized。这个错误在静态编译场景下非常常见解决办法就是确认你需要的平台插件被正确导入了。9.2 静态编译下的资源文件Qt 的资源系统.qrc文件在静态编译时工作正常资源会被编译进可执行文件。但要注意如果资源文件很大比如包含大量图片可执行文件的体积会进一步增大。对于资源较多的项目可以考虑把资源放在外部文件里运行时从文件系统加载而不是编译进二进制。9.3 调试静态编译的程序静态编译的程序调试起来比动态编译麻烦一些因为所有 Qt 代码都在可执行文件里gdb 加载符号表会比较慢。建议保留一份带调试符号的版本用于开发阶段调试发布时再用 strip 处理。如果需要在目标板上用 gdb 调试需要交叉编译版本的 gdb# 在开发机上 aarch64-linux-gnu-gdb myapp # 连接到目标板的 gdbserver (gdb) target remote 192.168.1.100:2345目标板上需要运行gdbservergdbserver :2345 ./myapp这套流程在排查段错误、死锁这类问题时非常有用。静态编译的程序崩溃时如果没有调试符号堆栈信息基本没法看。9.4 最后一点个人体会Qt 静态交叉编译这件事第一次做的时候会觉得步骤繁多、处处是坑。但把整个流程走通一遍之后把它脚本化、容器化后面就是一条命令的事。真正花时间的不是编译本身而是排查各种环境不匹配的问题。我的建议是第一次做的时候每一步都验证不要跳步。工具链验证完再配 sysrootsysroot 验证完再跑 configureconfigure 摘要确认无误再开始 make。每一步都确认正确比一口气跑到底然后面对一堆报错要快得多。另外把编译日志保存好。Qt 的编译输出非常多出错时关键信息往往淹没在几千行日志里。用tee把日志存下来出错时用grep -i error快速定位比在终端里往上翻页效率高得多。