Qt 5.14.2 aarch64静态交叉编译实战:从configure到板端部署

Qt 5.14.2 aarch64静态交叉编译实战:从configure到板端部署 先说我自己的结论Qt 5.14.2 的 aarch64 静态交叉编译卡人的从来不是“编译动作本身”而是依赖关系。你一旦搞清楚 configure 里每个开关在干什么后面遇到编译错误、链接错误、板端运行时问题时就会知道该去哪里查。这篇文章是我完整跑通这条链路后的记录从宿主机准备、交叉工具链、Qt 源码配置、make到最小程序验证和板端部署细节每一步都给了可复制的参数也把踩过的坑写在了对应位置。这次选型的背景是目标平台是 ARM64aarch64开发板需要跑一个带界面的 Qt Widgets 程序现场部署环境不适合再放一堆 .so 和依赖库所以决定做静态编译。最终产物在宿主机上交叉编译好拷贝到板子上直接运行不依赖目标板上的 Qt 环境。整篇文章按操作顺序展开你可以把它当一份 checklist 来用也可以按章节跳到自己卡住的地方。1. 为什么要折腾静态交叉编译先想清楚再动手1.1 在板子上直接编译真的不现实吗很多第一次接触嵌入式 Qt 的开发者第一反应是“板子不是也有 Linux 吗直接在板子上编译不就行了”理论上是这样但实际体验非常痛苦。我试过在四核 Cortex-A53 的开发板上直接编译 Qt源码解压后光 configure 加上 make跑了将近四个小时期间 swap 一直在报警风扇转速直接拉满。这还只是 Qt 基础模块如果再带上 WebEngine 这类重量级模块基本可以把开发板变成一个“暖手宝”。开发板存在的意义是运行产物不是当编译服务器。另外板上编译还有一个隐藏问题目标板的文件系统和包管理器往往是精简过的。可能连 g、make 都没装就算装了交叉编译工具链和板上的系统库版本怎么对齐也是麻烦事。生产环境里板子刷完镜像后应该保持“干净”不为了开发去污染它。所以交叉编译是嵌入式 Linux 里绕不开的基本功。1.2 静态编译省的到底是一些什么事动态编译的 Qt 程序部署时要往板子上拷十几个 .solibQt5Core.so、libQt5Gui.so、libQt5Widgets.so还有平台插件、字体插件、图片格式插件的动态库。这些库之间还有依赖关系版本号、路径、符号表任何一个对不上都可能导致程序起不来。而静态编译就是把 Qt 框架和三方依赖库全部 .a 进去最后产出一个独立的可执行文件。部署时只需要拷一个文件不依赖目标板上的 Qt 环境也不会出现“换一张板卡镜像后 Qt 库找不到了”的尴尬。对量产项目和长期维护来说这个优势非常明显。代价也很直接可执行文件体积变大。一个简单的 QWidget 程序静态编译后可能 8 到 15MB体积换部署简单在存储不比价的嵌入式设备上我认为是划算的。1.3 版本选型为什么偏偏是 5.14.2Qt 版本这么多为什么选 5.14.2 而不是 5.12、5.15 或者 6.x我的理由是5.12 虽然是 LTS但在我需要用到的一些模块上对较新的交叉工具链适配不如 5.14 平滑5.15 的离线开源安装包获取路径比较麻烦而 5.14.2 有标准的qt-everywhere-src-5.14.2.tar.xz下载即所得6.x 对源码结构和构建系统有过一轮调整老项目的 Widgets 代码迁移过去要改不少编译配置对存量工程不友好5.14.2 在生产环境里的验证量足够大遇到问题几乎都能搜到资料。如果你没有特殊理由跟着 5.14.2 走不会踩到“版本太老编译不过”或“版本太新没有案例”两个极端。2. 正式开始之前宿主环境、工具链和源码下载2.1 宿主机基础环境准备我用的宿主机是 Ubuntu 20.04 x86_64。其他发行版问题不大只是包管理器命令不同。提前装好以下几类基础工具sudo apt update sudo apt install -y build-essential perl python3 libxkbcommon-devbuild-essential提供宿主机本地的 gcc、g 和 makeQt 源码编译过程中有一部分宿主工具比如处理配置文件的小程序是要先在 x86 上编译出来跑一遍的所以宿主机的编译环境必须可用。perl是 Qt configure 脚本的运行时依赖python3有些辅助脚本会用到。libxkbcommon-dev是我在某个模块编译报错后补装的如果你不装某些 Qt 图形相关的库检查可能过不去。2.2 aarch64交叉编译工具链的安装和验证交叉编译工具链是全局的核心依赖。Ubuntu 官方源里就有标准的 aarch64 交叉编译器sudo apt install -y gcc-aarch64-linux-gnu g-aarch64-linux-gnu安装完成后先验证一下aarch64-linux-gnu-gcc -v aarch64-linux-gnu-g -v正常会输出版本号。这个工具链自带的 sysroot 在/usr/aarch64-linux-gnu下里面有 aarch64 版本的 glibc、libstdc 等基础库。如果目标板用的是非标准版本的 glibc比如特别老的板卡 SDK那么最好用板卡厂商提供的工具链而不是 Ubuntu 源里这套。用官方源工具链做标准 aarch64 Linux 环境是最省心的。有个小提醒安装后一定要确认aarch64-linux-gnu-gcc在 PATH 里。如果不在可以手动把/usr/bin加入 PATH或者装完重新登录一次 shell。2.3 Qt 5.14.2 源码获取与完整性校验Qt 5.14.2 的完整源码包是qt-everywhere-src-5.14.2.tar.xz体积约 500MB解压后接近 6GB编译时还要占用更多空间建议工作磁盘留出至少 15GB。下载地址直接走 Qt 官方 archive 路径下载后用官方提供的 SHA256 值校验一下避免源文件损坏导致编译阶段出现奇奇怪怪的错误。wget https://download.qt.io/archive/qt/5.14/5.14.2/qt-everywhere-src-5.14.2.tar.xz sha256sum qt-everywhere-src-5.14.2.tar.xz tar -xJf qt-everywhere-src-5.14.2.tar.xz我把源码放在/srv/qt-everywhere-src-5.14.2下下文都按这个路径写。2.4 shadow build 构建目录为什么不用源码目录Qt 支持 shadow build也就是在源码目录之外单独建一个目录来跑 configure 和 make。强烈建议这么做原因有两点一是源码目录保持干净出问题想重新配置时直接删掉构建目录重来不需要重新解压源码 二是一份源码可以同时维护多套构建配置。比如一套静态 aarch64一套动态 x86_64互不干扰。我习惯这样建目录mkdir -p /srv/qt-build-5.14.2-aarch64 cd /srv/qt-build-5.14.2-aarch64后续所有 configure 和 make 操作都在这个目录里执行配置好之后源码目录里的文件不会被改动。3. configure 参数逐个拆解这一步决定了后面所有坑交叉编译 Qt最核心的一步就是 configure。这一步的每一个参数都对应后面编译期、链接期、运行期的行为。参数少了后面报错参数错了编译出来也是废的。我把它拆成几类来说。3.1 用 -device 指定交叉目标平台而不是直接改 qmake.conf交叉编译最关键的是告诉 Qt“你正在为哪个平台构建”。Qt 提供了两种方式-xplatform linux-aarch64-gnu-g直接指定平台 mkspec-device linux-generic-g指定设备 mkspec配合-device-option CROSS_COMPILE设置编译器前缀。我推荐用-device路线。原因很简单linux-generic-g这个设备 mkspec 里已经写好了对$$CROSS_COMPILE的使用方式-device-option CROSS_COMPILEaarch64-linux-gnu-一传进去qmake 就会自动用aarch64-linux-gnu-gcc、aarch64-linux-gnu-g、aarch64-linux-gnu-ar等工具。如果 Qt 源码qtbase/mkspecs/device-config/或qtbase/mkspecs/devices/里有和你目标板完全匹配的专用 mkspec优先用板子对应的设备名。没有的话linux-generic-g是最通用的兜底方案。树莓派、RK3399、飞腾等常见板子用这个都能编过。3.2 静态编译三件套-static、-release、-prefix这三个参数决定整个产物的形态。-static让 Qt 自身的库和插件以静态库形式构建。没有这个参数Qt 就算交叉编译出来了也是动态库部署时还是得拷一堆 .so。它是最关键的一个开关。-release只编译 release 版本。如果不加Qt 默认会尝试同时构建 debug 和 release编译时间翻倍产物体积也大。嵌入式场景下我们用不到板端调试符号release 就够了。-prefix指定安装路径。我设为/opt/Qt-5.14.2-aarch64-static。这个前缀会被写入 Qt 的配置信息里主要是给 qmake 找自身目录用的。实际部署到板子上的可执行文件是静态的并不依赖这个目录存在但交叉编译时用它来统一管理库、插件的安装位置非常清晰。3.3 三方依赖库策略-qt-* 与 -system-* 的取舍这是静态交叉编译里最容易被忽视、也最容易出问题的一环。Qt 自带了大量三方库源码libpng、libjpeg、zlib、freetype、harfbuzz、pcre 等。configure 时用-qt-*系列参数选择使用自带源码用-system-*系列选择链接系统里的库。交叉编译时我的建议非常明确凡是能-qt-的优先-qt-。比如-qt-libpng -qt-libjpeg -qt-zlib -qt-freetype -qt-harfbuzz -qt-pcre原因是如果你选择-system-*configure 和 make 会去宿主机系统目录里找对应的头文件和库。但你宿主机上的库几乎都是 x86_64 的交叉链接时要么找不到要么报架构不匹配。就算你找到了一批 aarch64 版的库还得一个一个去交叉编译它们并手动指定-I和-L路径工作量大而且版本之间很容易互相打架。Qt 自带的三方库是经过 Qt 测试过的版本组合并且会自动交给你的交叉编译器来编译。对一个追求稳定的嵌入式产物来说这种“固定依赖快照”的方式是最省心的。3.4 目标跑不了的功能直接关掉-no-*、-skip、-nomake嵌入式系统资源和功能都有限configure 里把用不到的功能关掉不只是为了编译得快更是为了减少将来链接期和运行期的麻烦。比如目标板上有屏但没 GPU那 OpenGL 相关的库链进去只会带来cannot find -lGL这类错误没有 X Serverxcb 插件链进去也没有意义。我实际关掉/跳过的主要模块-no-opengl -no-xcb -no-icu -no-iconv -no-dbus -no-tslib -no-pkg-config -no-rpath -skip qtwebengine -skip qtdeclarative -skip qtscript -skip qtmultimedia -skip qttools逐个说明一下-no-opengl无 GPU 或不需要 OpenGL 时必关否则链接到 libGL 就崩-no-xcb不用 X Window 时必关否则静态编译会去找一长串 xcb 相关库-no-icu不需要复杂 Unicode 排版时关掉能砍掉很大体积-no-iconv避免和工具链 sysroot 里的 iconv 纠缠不清-no-dbus嵌入式系统一般没有 D-Bus daemon关掉省掉一大块依赖-no-tslib不接电阻触摸屏时关掉-no-pkg-config防止 configure 阶段去宿主机上找 pkg-config 的 .pc 文件避免把 x86 库路径带进来-no-rpath避免把宿主机上的 /opt 路径写进最终二进制的 runpath-skip跳过整个模块最典型的就是 qtwebengine它的编译时间和体积都是重量级的qtdeclarative、qtmultimedia 等如果业务纯 Widgets也可以不要。这些开关看着像是“减负”实际上都是“避坑”。交叉编译最怕的就是一个模块隐晦地依赖了你根本不知道的宿主机库。3.5 完整的 configure 命令参考把上述参数串起来我实际使用的完整 configure 命令如下cd /srv/qt-build-5.14.2-aarch64 /srv/qt-everywhere-src-5.14.2/configure \ -static \ -release \ -prefix /opt/Qt-5.14.2-aarch64-static \ -device linux-generic-g \ -device-option CROSS_COMPILEaarch64-linux-gnu- \ -nomake examples \ -nomake tests \ -skip qtwebengine \ -skip qtdeclarative \ -skip qtscript \ -skip qtmultimedia \ -skip qttools \ -no-opengl \ -no-xcb \ -no-icu \ -no-iconv \ -no-dbus \ -no-tslib \ -no-pkg-config \ -no-rpath \ -qt-libpng \ -qt-libjpeg \ -qt-zlib \ -qt-freetype \ -qt-harfbuzz \ -qt-pcre \ -linuxfb-linuxfb的作用是启用 Linux Framebuffer 平台插件这是无 X 环境下让 Qt 程序直接在屏幕上显示的关键。到此configure 命令跑完后先别急着 make仔细看输出的配置摘要Platform 行是否显示 linux-generic-gBuild type 是否为 staticLinuxFB 是否为 yesOpenGL 是否为 no。这三项确认无误说明前面一大半路走对了。如果摘要里的参数和你设想的不一致就删掉构建目录重新 configure不要带着脏配置继续编。4. 编译过程中避不开的坑几个典型报错的排查链路4.1 编译过程中的资源控制和等待心态执行编译make -j$(nproc)-j后面跟的并行任务数不是越大越好。Qt 静态编译非常吃内存16 核机器上如果-j16编译进程的内存峰值可能超过 20GB。如果你宿主机内存只有 8GB建议老老实实-j4。我在第一次编译时因为并行度拉太高直接把宿主机卡到没法操作swap 写满。编译 Qt 是个长任务心态放平中间卡几分钟属于正常现象。整体编译时间在 8 核 16 线程、16GB 内存的机器上大约是 40 分钟到 1 小时看你裁剪了多少模块。4.2 链接期找不到库的通用排查方法静态交叉编译报错的高峰集中在链接阶段。学会一套通用的排查链路比记单个错误更管用。当链接器报cannot find -lXXX时它其实是在说在当前的库搜索路径里找不到libXXX.a或libXXX.so。第一步先确认这个“XXX”是什么库。以-lGL为例这个 GL 是 OpenGL 库。目标板没有 GPU 或没有完整 OpenGL 实现时工具链 sysroot 里自然没有libGL.a。这时你去重新交叉编译 OpenGL 反而跑偏了正确做法是回到 configure 阶段把 OpenGL 相关功能关掉也就是-no-opengl。如果你确认某个库其实有必要可以用下面的命令检查工具链 sysroot 里到底有没有aarch64-linux-gnu-gcc -print-file-namelibts.a aarch64-linux-gnu-gcc -print-file-namelibGL.so如果输出还是库名本身说明 sysroot 里确实没有就要回到依赖准备阶段去补齐或者考虑从业务上关掉对应功能。4.3 我实际踩过的三个具体报错第一个是cannot find -lGL。原因就是前面说的configure 默认可能探测到什么就启用什么导致编译时去找宿主环境不存在的 OpenGL 库。解决方式就是-no-opengl。如果你的板子有 GPU 且支持 EGL/GLES那是另一套方案需要板卡厂商的 EGL/GLES 库再配合-opengl es2 -eglfs属于进阶配置。第二个是链接时出现undefined reference to dlopen或dlsym相关符号。老版本工具链或精简 sysroot 里链接器没有自动加-ldl。解决办法很直接在项目的 .pro 文件里加上LIBS -ldl第三个是一堆cannot find -lxcb-*。这是我早期做交叉编译时最头疼的因为宿主机上明明有 xcb 的 .so但交叉链接时找不到 aarch64 版本的。问题根源就是我在 configure 里没有加-no-xcb。对纯 framebuffer 方案来说xcb 完全没有存在的必要关掉即可。4.4 不是所有报错都是参数问题编译过程中如果看到gmake: *** Error之类的报错通常真正的错误信息在它上面几行。不要只截图最后两行往上看找到具体是哪个文件、哪一行编译失败。常见原因包括源码目录权限、磁盘空间不足、工具链版本过旧、sysroot 头文件残缺等。遇到疑似工具链的问题可以用一个最简单的 C 程序先试一下交叉编译器本身是否正常echo int main(){return 0;} test.c aarch64-linux-gnu-gcc test.c -o test能编出可执行文件说明工具链没问题编不过就先去解决工具链。5. 从“编过了”到“能跑了”最小程序验证和板端部署make 完成之后执行make installQt 会在/opt/Qt-5.14.2-aarch64-static目录下生成完整的交叉编译产物。这个目录不是给你部署到板子上的而是给你在宿主机上交叉编译应用时用的。它里面有 aarch64 的 qmake、头文件和 .a 静态库。5.1 用交叉 qmake 构建最小 Qt 程序新建一个测试目录mkdir -p qt-test cd qt-test创建main.cpp#include QApplication #include QLabel int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label(Hello aarch64 Qt); label.resize(300, 100); label.show(); return app.exec(); }创建qt-test.proQT widgets SOURCES main.cpp编译时最关键的一个坑一定要用交叉编译版本的 qmake。如果你 PATH 里恰好装了宿主机版本的 Qt用错 qmake 后 Makefile 里全是宿主机编译器路径交叉编译会直接崩掉。保险起见直接用完整路径/opt/Qt-5.14.2-aarch64-static/bin/qmake qt-test.pro makemake 成功后用file看一眼产物file qt-test正常会显示ELF 64-bit LSB executable, ARM aarch64说明架构对了。5.2 检查可执行文件的依赖关系为了确认它到底是不是静态的用 readelf 看动态依赖aarch64-linux-gnu-readelf -d qt-test | grep NEEDED如果输出里还有libc.so.6、libstdc.so.6、libm.so.6这类系统基础库说明 Qt 静态了但 C/C 运行库还是动态链接。这时目标板的系统里得有对应的基础动态库。绝大多数嵌入式 Linux 板子都是有 glibc 和 libstdc 的所以这不是问题。如果你追求连基础库也静态化可以在 .pro 里追加QMAKE_LFLAGS -static-libgcc -static-libstdc再重新 make。是否完全静态还是以 readelf 的输出为准。5.3 拷贝到板子后还需要准备什么静态 Qt 程序到了板子上不是丢过去就能跑的。有两个东西要特别注意QPA 平台插件和字体。QPA 插件在 Qt 里负责抽象底层显示系统。我们 configure 时启用了 linuxfb所以板子上要用 linuxfb 作为 QPA 平台。运行前设置环境变量export QT_QPA_PLATFORMlinuxfb如果是 root 用户直接跑程序会去打开/dev/fb0显示。这个设备节点只要板子有 Framebuffer 驱动就会存在。另一个容易忽略的是字体。静态程序里不会自动带字体文件。如果没有安装任何中文字体界面上所有中文都会变成方块而且程序没有任何报错。最简单的解决办法把字体文件放到板子上指定字体目录export QT_QPA_FONTDIR/usr/share/fonts或者在代码里注册字体QFontDatabase::addApplicationFont(/path/to/wqy-microhei.ttc);如果你只跑英文界面不配字体也能显示但一旦界面有中文马上就会踩这个坑。5.4 真机运行时的快速定位手段程序在板子上起不来时先看几类信息。第一类程序根本没启动。用dmesg看有没有段错误、非法指令之类的内核日志。很多老工具链编译出来的程序在较新的 CPU 上反而会触发非法指令这种情况要考虑换工具链或者调整-march参数。第二类QPA 插件相关错误。运行前加一个调试环境变量Qt 会打印插件加载的详细日志export QT_DEBUG_PLUGINS1 ./qt-test它会告诉你插件是从哪个路径加载的、有没有加载成功。静态编译下插件是链进可执行文件里的如果这一步报找不到插件大概率是 configure 阶段没启用对应插件。第三类显示黑屏或者花屏。检查/dev/fb0是否存在、当前控制台是否被 fbcon 占用可能需要先退出桌面系统或者用con2fbmap把 framebuffer 映射到正确终端。这类问题和 Qt 本身关系不大更多是板卡环境问题。6. 静态交叉编译的扩展经验从跑通到稳定用于生产6.1 换到其他 Qt 版本时的迁移思路这套配置不是只能用在 5.14.2 上。Qt 5.12、5.15 甚至 6.x 的 configure 参数基本沿用了这套体系差别主要在于一些模块名和 feature 开关有调整。比如 Qt 6 里对 OpenGL 的选项换成了-no-opengl或-opengl es2但对linux-generic-g和CROSS_COMPILE的用法是兼容的。换版本时最省事的思路是先不裁剪任何模块用最小的-static -device配置把基础 Qt 编出来确认工具链没问题然后再逐步打开业务真正需要的模块。这样能避免“一下关掉太多东西导致 configure 失败还不知道该怪谁”。6.2 把构建流程沉淀成脚本交叉编译环境不是配一次就一劳永逸。过几个月同事新拉一台机器或者要换一块目标板重新手动敲一遍 configure 参数很可能漏掉某个开关然后产出完全不同的结果。我从第二次建环境开始就把 configure 命令完整写进了项目仓库里的scripts/qt-build.sh。脚本顶部固定记录宿主机版本、工具链版本、目标板型号、Qt 版本每次执行完 configure 后把输出摘要存档。这样无论是回退版本还是复现环境都知道当初到底是怎么编出来的。6.3 一个值得长期保留的习惯先验证加法再做减法静态交叉编译里的依赖问题经常是“你以为关掉了某个模块但它又通过别的路径被拉进来”。所以我的习惯是先不加任何-skip用最小 configure 配置跑通一条全链路确认交叉编译环境整体没问题然后才开始做减法一次只减一个模块编译一次验证一次。这样看起来花的时间多但排查问题的成本反而最低。如果一次性跳过十几个模块编译失败后根本不知道是哪个依赖引起的。交叉编译本身是一个信息量很大的过程稳定接近成功的方式就是让每次变量少一点。最后再说一个实战中很常见的小问题在板子上运行静态 Qt 程序时如果发现鼠标或触控输入不生效先确认 linuxfb 这个插件对应的输入协议是否被系统启用。很多时候程序起来了、画面正常了就是点不动这往往不是 Qt 代码的问题而是板子的输入设备没被正确识别。可以先用cat /proc/bus/input/devices看看有没有对应输入节点再决定是加 tslib 还是走 evdev 的输入协议。这类问题属于板卡 BSP 层面和 Qt 交叉编译本身关系不大但排查时最容易让人怀疑“是不是我编译错了”。