做嵌入式Linux开发的人,迟早都要碰一个坎:给目标板子的Qt环境凑出一套能用的开发链路。尤其当目标是aarch64架构,还要做Qt 5.14.2静态交叉编译的时候,网上能找到的资料大多是零散的,不是缺了sysroot环节,就是qmake配置不对,最后卡在某一个奇怪的链接错误上。我前后折腾了三天,从交叉工具链到静态库再到目标板部署,把所有坑都踩了一遍,才把整条链路完全跑通。这篇手册就是把过程完整记录下来,给准备上同一条船的朋友做参考。
这套方案解决的核心问题是:如何在x86的宿主机上,用aarch64交叉编译器把Qt 5.14.2编成静态库,并最终产出一个拷到目标板上就能直接运行的单一可执行文件。它不需要目标板安装庞大的Qt运行时库,也不需要在线安装依赖,特别适合离线部署、量产镜像制作,以及那些动不动就缺库、缺路径的现场环境。适合的读者包括嵌入式Linux应用开发者、板卡移植工程师、以及所有被aarch64交叉编译折磨过的人。
1. 项目概述与前置条件
1.1 为什么非要静态交叉编译
先说清楚动态编译和静态编译在嵌入式场景下的差别。动态编译出来的Qt应用,运行时要靠目标板上的Qt动态库,比如libQt5Core.so、libQt5Gui.so这些。你需要在目标板上完整部署一套和编译环境完全对应的Qt库,路径、版本、依赖缺一不可,稍有偏差程序起来就是一片报错。
静态交叉编译就省心得多。Qt本身和它依赖的libpng、libjpeg、freetype、pcre这些第三方库,全部静态链接进最终的可执行文件。拷到目标板上只依赖内核接口和基本的libc体系,基本上"一个文件就是一个程序"。这对于生产环境的设备部署极其友好,尤其是目标板不能连网、不方便用包管理器、或者需要批量烧写相同镜像的场景。
代价也很明显:静态编译出来的文件体积动辄十几MB甚至几十MB;Qt插件的加载机制要特殊处理;如果你的代码里有坑,程序崩了没法通过换库来修,只能重新编译整包。但这些在"能稳定部署"面前,其实都是可以接受的。
1.2 整体方案与关键选型
这套方案里最关键的决定就是把"工具链"和"图形平台"先定死,后面所有configure参数都围绕这两个选择展开。
工具链我选用Ubuntu官方源里的gcc-aarch64-linux-gnu和g++-aarch64-linux-gnu。理由很简单:够用、统一、apt一键安装。它基于glibc,配套的交叉编译头文件和库都在/usr/aarch64-linux-gnu目录下,不需要额外配置复杂的环境变量,对新手最友好。
图形平台选择linuxfb,也就是Linux Framebuffer。目标板上只要内核开启了framebuffer设备(/dev/fb0),Qt就可以直接在屏幕上画UI。这在没有GPU、不带桌面环境的工业板子上非常常用。如果你目标板带GPU,需要走eglfs或者OpenGL ES系列,那配置会不一样,本文不展开。
Qt源码用官方qt-everywhere-opensource-src-5.14.2.tar.xz完整包。之所以不单独拉qtbase,是为了保留Qt Quick等常用模块的扩展空间,一次编全,省的以后要用还要再单独补模块。
1.3 测试环境清单
| 项目 | 说明 |
|---|---|
| 宿主机系统 | Ubuntu 22.04 LTS x86_64 |
| 交叉工具链 | gcc-aarch64-linux-gnu / g++-aarch64-linux-gnu 11.x |
| Qt源码包 | qt-everywhere-opensource-src-5.14.2.tar.xz |
| 目标架构 | aarch64 (ARM64) |
| 编译模式 | Release + Static |
| 图形平台 | linuxfb |
| 目标板条件 | Linux内核支持framebuffer即可 |
还有一套之前容易踩坑的目录规划。建议把源码、宿主工具、目标产物三分开,我习惯在/opt/buildroot下工作:
/opt/buildroot/ ├── qt-src/ # Qt源码解压目录 ├── qt-host/ # 宿主机工具安装目录(moc/rcc/uic等) └── qt-aarch64/ # aarch64目标库安装目录2. 交叉编译工具链与sysroot准备
2.1 安装aarch64交叉编译工具链
Ubuntu上安装交叉工具链非常直接:
sudo apt update sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu安装完顺手验证一下编译器和二进制格式:
aarch64-linux-gnu-gcc -v echo 'int main(){return 0;}' | aarch64-linux-gnu-gcc -x c - -o /tmp/test-aarch64 file /tmp/test-aarch64如果输出显示ELF 64-bit LSB executable, ARM aarch64,说明工具链正常。
从零搭环境的人往往在这一步就忽略了一个问题:不同版本的工具链对应的C++库ABI不同,万一你用的是Ubuntu 20.04,编译器版本是9.x,而Qt源码里依赖了较新的语言特性,编译期就会出现莫名其妙的模板报错。所以我直接锁定Ubuntu 22.04配gcc 11,这个组合针对Qt 5.14.2测试下来非常稳。
2.2 理解sysroot以及什么时候需要它
sysroot说白了就是交叉编译器的"目标板根文件系统"。当你编译aarch64程序时,编译器自己跑在x86上,但程序要链接的libc、libstdc++、各种头文件,必须来自目标板的文件系统,而不能用宿主机的x86版本。交叉编译器会通过sysroot指定这个根目录。
使用Ubuntu官方交叉工具链有个省事的地方:它已经把目标板的C/C++运行库和头文件安装到了/usr/aarch64-linux-gnu目录,编译器会自动去那里找,你不需要额外操心sysroot。如果你用的是板卡厂商提供的BSP/SDK工具链,通常也会自带一个现成的sysroot,编译时通过--sysroot=/path/to/sysroot指向即可。
那什么时候需要自己构建sysroot呢?两种情况:一是拿私有工具链时,厂商没给完整sysroot;二是你的应用依赖了额外的第三方库(比如openssl、sqlite),需要交叉编译后放进sysroot里。构建的常用方法是用debootstrap拉一个arm64的最小rootfs:
sudo debootstrap --arch=arm64 jammy /opt/sysroot/aarch64然后用qemu-user-static配合chroot进入这个rootfs,安装必要的开发包:
sudo cp /usr/bin/qemu-aarch64-static /opt/sysroot/aarch64/usr/bin/ sudo mount -o bind /proc /opt/sysroot/aarch64/proc sudo chroot /opt/sysroot/aarch64 /bin/bash apt update apt install -y libc6-dev libstdc++-11-dev linux-libc-dev不过从我的实际经验看,如果你只是想让演示性的Qt程序跑起来,用Ubuntu工具链自带的/usr/aarch64-linux-gnu路径就够了,不需要一开始就折腾独立sysroot。真到了做产品的阶段,再考虑完整BSP工具链加私有sysroot的规范方案。
2.3 交叉编译第三方依赖库的通病
Qt configure里用了一堆-qt-zlib -qt-libpng之类的参数,就是让Q带来编译这些第三方库,避免你去手动交叉编译它们。这是静态交叉编译里最省力的做法。如果某个依赖Qt不自带,比如openssl,那你就得用aarch64-linux-gnu-gcc去交叉编译那个库,然后把产物放到工具链默认的搜索路径/usr/aarch64-linux-gnu里。
这块有个容易踩的坑:手动交叉编译库的时候,很多configure脚本会检测当前是x86环境而自动编译成x86库。我在早期就吃过这个亏,openssl编译出来是x86机器码,链接时整个二进制乱套。交叉编译任何第三方库,第一件事就是确认生成的.a或.so文件架构,用file查看架构标识,这个习惯必须养成。
3. Qt 5.14.2源码准备与解压
3.1 获取Qt源码包
Qt 5.14.2的源码包在官方archive目录下,直接下载:
wget https://download.qt.io/archive/qt/5.14/5.14.2/qt-everywhere-opensource-src-5.14.2.tar.xz tar -xf qt-everywhere-opensource-src-5.14.2.tar.xz mv qt-everywhere-opensource-src-5.14.2 /opt/buildroot/qt-src这里啰嗦一句,下载源码包可以选择国内镜像站,速度会快很多。如果你是在内网环境,直接把tar包拷贝进内网机器也行,Qt的编译不需要联网,这也算一种"离线安装包"式的使用方式。
3.2 理解src包内部结构
解压之后看目录结构,Qt的模块划分非常清晰。qtbase是核心,编译顺序最先、包含QtCore/QtGui/QtWidgets这些最基础的东西。其他qtdeclarative、qtquickcontrols2、qtmultimedia这些模块都在外层。configure脚本会在源码根目录统一配置所有模块,然后make按依赖关系依次构建。
目录规划上我坚持把宿主工具目录(qt-host)和最终aarch64库目录(qt-aarch64)分开,这是很多教程忽略的关键点。交叉编译时,Qt需要生成两个东西:一类是只能在x86宿主机上运行的构建工具,比如moc(元对象编译器)、rcc(资源编译器)、uic(界面编译器);另一类是最终要在aarch64目标板上运行的库和插件。前者装到qt-host,后者装到qt-aarch64,互不污染。
3.3 检查mkspec是否存在
在configure之前,先确认一个最容易被忽略的细节:qtbase/mkspecs/linux-aarch64-gnu-g++目录是否存在,以及qmake.conf内容是否匹配你的工具链:
ls /opt/buildroot/qt-src/qtbase/mkspecs/ | grep aarch64 cat /opt/buildroot/qt-src/qtbase/mkspecs/linux-aarch64-gnu-g++/qmake.confQt 5.14.2自带了linux-aarch64-gnu-g++这个mkspec,内容默认就是使用aarch64-linux-gnu-gcc。如果用的是厂商工具链,很可能需要复制一份这个目录,改名为自己工具链的名字,并修改qmake.conf里的QMAKE_CC、QMAKE_CXX等变量。这是交叉编译的第一步,也是很多人漏掉的自定义步骤。
4. configure参数详解与静态编译配置
4.1 核心参数表与含义
configure是整套流程里最重要也最容易出错的环节。我把自己最终跑通的完整命令放出来,逐项解释:
cd /opt/buildroot/qt-src ./configure \ -prefix /opt/buildroot/qt-aarch64 \ -hostprefix /opt/buildroot/qt-host \ -xplatform linux-aarch64-gnu-g++ \ -opensource -confirm-license \ -release -static \ -nomake examples -nomake tests \ -skip qtwebengine -skip qtwebview -skip qt3d \ -qt-zlib -qt-libpng -qt-libjpeg -qt-pcre -qt-freetype \ -no-xcb -no-opengl -no-eglfs -no-libinput -no-tslib \ -no-fontconfig -no-dbus -no-cups -no-openssl \ -linuxfb| 参数 | 作用与理由 |
|---|---|
-prefix | aarch64目标库的最终安装位置,即放到板子上的库路径 |
-hostprefix | 宿主机构建工具(moc/rcc/uic)安装位置 |
-xplatform | 指定交叉编译平台,必须对应mkspec目录名 |
-static | 核心参数,生成静态库并静态链接插件 |
-release | 编译Release版本,体积更小、性能更好 |
-skip | 跳过不用的模块,webengine几乎无法交叉静态编译,直接跳过 |
-qt-* | 使用Qt内置的第三方库,避免依赖目标板环境 |
-no-* | 禁用不需要依赖项,把配置简化到极致 |
-linuxfb | 启用linuxfb平台插件,嵌入式无桌面的基础图形栈 |
4.2 为什么同时用这么多-no参数
很多新手问,为什么要把xcb、opengl、dbus这些东西都关掉。核心原因是静态交叉编译最怕依赖关系复杂。每保留一个动态依赖,目标板上就多一分运行环境要求。比如-no-xcb,因为目标板压根没有X11服务器,保留xcb只会让Qt去链接一堆xcb相关的库,而这些库也需要交叉编译,纯属给自己添麻烦。
-no-opengl和-no-eglfs同理:如果目标板没有GPU,或者不打算在这篇基础教程里碰GPU驱动,干脆关掉,避免编译过程中出现cannot find -lGL之类的问题。如果后面实际板子带Mali、PowerVR这类GPU,再回过头来打开eglfs,配合GPU厂商提供的驱动交叉编译,那是另一个深度话题。
模块裁剪我是这么做的:qtwebengine体积大、依赖Chromium,交叉编译几乎无解,直接跳过;qt3d依赖OpenGL ES,关掉OpenGL就得跳过。如果以后要跑QML界面,qtdeclarative和qtquickcontrols2这两个建议保留,-skip列表里不要动它们。
4.3 静态编译下的插件处理策略
这是静态编译和动态编译差异最大的一块。动态编译时,Qt平台插件(比如linuxfb)是编译成plugins/platforms/libqlinuxfb.so,程序运行时去对应目录动态加载。静态编译时,插件会变成.a静态库,程序不会自动加载,必须在工程里显式导入。
所以后面配置应用工程时,要么在.pro文件里加QTPLUGIN += qlinuxfb,要么在main.cpp里手动写Q_IMPORT_PLUGIN(qlinuxfb)。这个坑几乎每个第一次做Qt静态编译的人都会撞上,症状是程序在目标板上一启动就报Could not find the Qt platform plugin "linuxfb",后面专门写一节排查。
5. 完整编译流程
5.1 configure执行与日志记录
执行configure之前,建议把编译配置的日志存下来,方便出错时排查:
cd /opt/buildroot/qt-src ./configure ... 2>&1 | tee /opt/buildroot/configure.logconfigure过程会做大量编译测试,包括检查编译器能力、检测系统库头文件是否存在。如果某个关键检测失败,会直接停止。看到+号前缀列出被启用的模块,并且末尾出现Qt is now configured for building字样,说明配置阶段通过。
如果configure中途失败,不要盲目重跑,先看configure.log的最后几十行。常见的是The specified sysroot ... is invalid或者Compiling the generated test ... failed。后者多是因为编译器路径不对或者头文件搜索路径不对,可以直接把你的工具链路径用完整路径写到mkspec的qmake.conf里再试。
5.2 make编译可能出现的问题
configure通过后的标准编译命令:
make -j$(nproc)如果你机器内存小于16G,建议把并发数压低,比如make -j4,防止内存爆掉。Qt整体编译非常吃内存,多模块并行时g++编译器本身就可能吃掉1G以上的内存,并行任务一多就OOM。
编译过程建议也记录日志:
make -j4 2>&1 | tee /opt/buildroot/build.log第一次编译大概需要1到2小时。出错的话直接看build.log最后输出。交叉编译时最经典的报错是某个模块的编译命令里仍然用了x86的gcc,而不是aarch64-linux-gnu-gcc。出现这种情况,十有八九是configure指定xplatform时没生效,检查Makefile里QMAKE_CC的值。
5.3 安装目标库与宿主工具
编译完成后分别安装:
make install这一步会同时往-prefix指定的/opt/buildroot/qt-aarch64安装aarch64目标库,往-hostprefix指定的/opt/buildroot/qt-host安装宿主机工具。执行完检查目录结构:
ls /opt/buildroot/qt-aarch64/lib/ ls /opt/buildroot/qt-aarch64/plugins/platforms/ ls /opt/buildroot/qt-host/bin/静态库就是libQt5Core.a、libQt5Gui.a这些。平台插件目录里应该能看到libqlinuxfb.a。
验证产物架构:
file /opt/buildroot/qt-aarch64/lib/libQt5Core.a /opt/buildroot/qt-host/bin/qmake -v5.4 一个真实的编译错误样本
我在编译时遇到过configure成功、但是编译qtbase时反复报如下错误:
In file included from .../qconfig.cpp:1: .../qconfig.h: No such file or directory这个问题的原因后来定位到是-hostprefix路径设置有问题,导致构建过程中生成的头文件没有安装到预期的临时目录。清理重来,把qt-host的父目录权限放开,再重新configure,问题就消失了。这类问题没有捷径,就是看日志、定位路径、清理重来。
6. 静态编译应用测试与目标板部署
6.1 编写一个最小测试程序
拿一个最简单的QWidgets程序做冒烟测试。HelloQt.pro内容如下:
QT += core gui widgets TARGET = HelloQt TEMPLATE = app CONFIG += static SOURCES += main.cpp # 静态编译时必须显式导入linuxfb插件 QTPLUGIN += qlinuxfbmain.cpp:
#include <QApplication> #include <QLabel> int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label("Hello aarch64 Qt"); label.show(); return app.exec(); }6.2 用宿主机qmake交叉编译应用
注意这里必须使用/opt/buildroot/qt-host/bin/qmake,不要用系统自带的qmake:
mkdir /opt/buildroot/app && cd /opt/buildroot/app /opt/buildroot/qt-host/bin/qmake -spec linux-aarch64-gnu-g++ HelloQt.pro make file HelloQt编译输出的HelloQt应该是aarch64架构的静态链接可执行文件,体积通常在6MB到20MB之间,取决于静态库裁剪程度。file命令输出应该包含statically linked这类标识。
下面是这个过程中的实用检查点:编译时如果提示cannot find -lqllinuxfb之类,说明QTPLUGIN没生效;如果提示undefined reference to qt_plugin_query_metadata,说明插件没链接上;如果提示cannot find -lGL,说明configure里OpenGL模块没裁干净。前两个都是插件机制问题,第三个属于configure阶段没做干净。
6.3 部署与运行
把可执行文件拷贝到目标板:
aarch64-linux-gnu-strip HelloQt scp HelloQt root@目标板IP:/usr/bin/在目标板执行:
QT_QPA_PLATFORM=linuxfb ./HelloQtQT_QPA_PLATFORM环境变量显式指定平台插件,这样即使程序内部没配置好插件加载,也能强制走linuxfb路径。看到界面上显示"Hello aarch64 Qt"就说明整个链路完全打通。
6.4 字体的坑
linuxfb模式下Qt不读系统字体管理服务,不配置字体的话界面上文字可能全是方块。最简单的方式是把一个中文字体文件拷贝到目标板的Qt字体目录:
mkdir -p /usr/share/fonts/truetype/ scp /usr/share/fonts/truetype/wqy/wqy-microhei.ttc root@目标板:/usr/share/fonts/truetype/ # 在目标板设置环境变量 export QT_QPA_FONTDIR=/usr/share/fonts/truetype/Qt应用启动时会扫这个目录,找到能用的字体后中文才能正常显示。这个小细节在生产部署里特别容易翻车,我在现场就见过上屏全是方块的尴尬情形。
7. 常见问题与排查实录
7.1 configure阶段的排查
| 错误现象 | 可能原因 | 解决办法 |
|---|---|---|
Illegal instruction或Cannot run aarch64 binary | configure试图在x86宿主机上运行aarch64测试程序 | 安装qemu-user-static,启用binfmt支持 |
The specified sysroot ... is invalid | sysroot路径下没有标准rootfs结构 | 确认sysroot下有usr/include、usr/lib目录 |
Compiler cannot create executables | 编译器为了crtbegin等功能要链接目标板libc失败 | 检查工具链是否正常,尝试手动编译一个hello.c |
| configure卡在某一步很久 | 在检测某个依赖模块,比如xcb | 回头看配置参数里是否遗漏了-no-xcb |
7.2 链接阶段的排查
| 错误现象 | 可能原因 | 解决办法 |
|---|---|---|
undefined reference to 'qt_plugin_query_metadata_v2' | 静态插件未链接进来 | .pro里加QTPLUGIN += qlinuxfb,或main.cpp加Q_IMPORT_PLUGIN |
cannot find -lGL | OpenGL依赖未完全禁用 | configure里确认有-no-opengl,必要时清理重配 |
cannot find -lxxx | 某个外部依赖库缺失 | 要么交叉编译该库并放到工具链搜索路径,要么用对应-no-xxx参数关闭模块 |
multiple definition of ... | 静态库链接时符号冲突 | 常见于自己同时链接了Qt内置库和系统同名库,去掉重复的-l参数 |
7.3 目标板运行时的排查
| 错误现象 | 可能原因 | 解决办法 |
|---|---|---|
Could not find the Qt platform plugin "linuxfb" | 插件未静态导入 | 设置QT_QPA_PLATFORM=linuxfb,并编译时导入插件 |
This application failed to start because no Qt platform plugin could be initialized | 插件加载完全失败 | 检查QTPLUGIN配置,确认linuxfb插件已静态编译 |
| 界面文字全是方块 | 缺少可用字体 | 部署字体文件并设置QT_QPA_FONTDIR |
| 程序启动卡住不动 | framebuffer没初始化 | 确认目标板/dev/fb0存在,用cat /dev/urandom > /dev/fb0做简单测试 |
| XDG_RUNTIME_DIR未设置 | Qt运行时提示性警告 | 不影响运行,可忽略 |
7.4 我最想强调的几点体会
整个流程走下来,最影响成败的不是某个神秘参数,而是"确认结果"。每执行完一个阶段,用file检查二进制架构;每裁掉一个模块,想清楚它会不会被其他模块依赖;每次配置变更,都先清理旧的编译产物再重新configure。
还有一点是关于"静态编译会踩glibc的坑"。静态链接glibc之后,程序里的DNS解析、用户信息查询这些功能在某些精简系统上可能行为异常。这不是Qt的问题,是glibc静态链接的固有问题。如果目标板功能只涉及网络socket直连、UI展示、基础文件操作,基本不受影响;但如果你的应用重度依赖getaddrinfo这类系统服务,建议仔细验证。真遇到了就老实用动态编译,只把Qt库打包放到目标板上,也不是什么丢人的事。
最后分享一个我个人体会最深的小技巧:整套交叉工具链、源码包、configure参数、mkspec文件这些,最好全部固定版本,放进版本管理里。我见过太多编译问题,最后定位出来就是工具链某个小版本不一致导致的。把这套东西固化下来,后续不管谁来编译,产出一致,才不会在项目交付前一天突然爆雷。这套流程虽然折腾,但一次跑通之后,收益确实立竿见影。