先说我自己的结论:Qt 5.14.2 的 aarch64 静态交叉编译,卡人的从来不是“编译动作本身”,而是依赖关系。你一旦搞清楚 configure 里每个开关在干什么,后面遇到编译错误、链接错误、板端运行时问题时,就会知道该去哪里查。这篇文章是我完整跑通这条链路后的记录,从宿主机准备、交叉工具链、Qt 源码配置、make,到最小程序验证和板端部署细节,每一步都给了可复制的参数,也把踩过的坑写在了对应位置。
这次选型的背景是:目标平台是 ARM64(aarch64)开发板,需要跑一个带界面的 Qt Widgets 程序,现场部署环境不适合再放一堆 .so 和依赖库,所以决定做静态编译。最终产物在宿主机上交叉编译好,拷贝到板子上直接运行,不依赖目标板上的 Qt 环境。整篇文章按操作顺序展开,你可以把它当一份 checklist 来用,也可以按章节跳到自己卡住的地方。
1. 为什么要折腾静态交叉编译:先想清楚再动手
1.1 在板子上直接编译真的不现实吗
很多第一次接触嵌入式 Qt 的开发者,第一反应是“板子不是也有 Linux 吗?直接在板子上编译不就行了?”理论上是这样,但实际体验非常痛苦。
我试过在四核 Cortex-A53 的开发板上直接编译 Qt,源码解压后光 configure 加上 make,跑了将近四个小时,期间 swap 一直在报警,风扇转速直接拉满。这还只是 Qt 基础模块,如果再带上 WebEngine 这类重量级模块,基本可以把开发板变成一个“暖手宝”。开发板存在的意义是运行产物,不是当编译服务器。
另外,板上编译还有一个隐藏问题:目标板的文件系统和包管理器往往是精简过的。可能连 g++、make 都没装,就算装了,交叉编译工具链和板上的系统库版本怎么对齐也是麻烦事。生产环境里,板子刷完镜像后应该保持“干净”,不为了开发去污染它。所以交叉编译是嵌入式 Linux 里绕不开的基本功。
1.2 静态编译省的到底是一些什么事
动态编译的 Qt 程序,部署时要往板子上拷十几个 .so:libQt5Core.so、libQt5Gui.so、libQt5Widgets.so,还有平台插件、字体插件、图片格式插件的动态库。这些库之间还有依赖关系,版本号、路径、符号表,任何一个对不上都可能导致程序起不来。
而静态编译,就是把 Qt 框架和三方依赖库全部 .a 进去,最后产出一个独立的可执行文件。部署时只需要拷一个文件,不依赖目标板上的 Qt 环境,也不会出现“换一张板卡镜像后 Qt 库找不到了”的尴尬。对量产项目和长期维护来说,这个优势非常明显。
代价也很直接:可执行文件体积变大。一个简单的 QWidget 程序,静态编译后可能 8 到 15MB;体积换部署简单,在存储不比价的嵌入式设备上我认为是划算的。
1.3 版本选型:为什么偏偏是 5.14.2
Qt 版本这么多,为什么选 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++ 和 make,Qt 源码编译过程中有一部分宿主工具(比如处理配置文件的小程序)是要先在 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_COMPILE=aarch64-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 Server,xcb 插件链进去也没有意义。
我实际关掉/跳过的主要模块:
-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_COMPILE=aarch64-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-g++;
- Build type 是否为 static;
- LinuxFB 是否为 yes;
- OpenGL 是否为 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-name=libts.a aarch64-linux-gnu-gcc -print-file-name=libGL.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 install,Qt 会在/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.pro:
QT += 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_PLATFORM=linuxfb如果是 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_PLUGINS=1 ./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 交叉编译本身关系不大,但排查时最容易让人怀疑“是不是我编译错了”。