ARM64嵌入式Linux下Qt 5.14.2静态交叉编译实战:从零搭建到一键部署
2026/9/12 4:43:38 网站建设 项目流程

事情得从一块ARM64板子说起。手里这块板子带四核A55,跑的是Linux 4.19内核,打算做成工业HMI设备,界面用Qt来画。Qt版本我定在5.14.2,理由后面细讲。真正上手之后才发现,交叉编译只是第一步,部署才是真正折磨人的环节:目标板上没有编译器,根文件系统总共就2GB,缺一个共享库就得重新打包整个折腾一晚上。所以我把方案锁定在Qt5.14.2的aarch64静态交叉编译上——在x86主机上把Qt编成ARM64静态库,产出的应用单独一个可执行文件直接往板子上一拷就能跑,不依赖目标板上的Qt动态库、第三方运行库和七七八八的插件文件。这篇文章就是我从零开始搭环境、写configure、跑脚本到最终出包的完整记录,中间踩过的坑一个不落全写出来。适合正在做ARM64嵌入式Linux、工控HMI、边缘网关项目的同学参考,看完你也能在半天之内把整套工具链跑通。

1. 为什么非要用静态交叉编译不可

1.1 ARM64设备上的Qt部署痛点

很多人一开始的想法是:交叉编译一个动态链接的Qt,然后放到目标板上运行。这个思路没错,但在实际项目里往往会变成一场灾难。目标板上的系统是BusyBox裁剪过的,为了省空间连glibc都做了精简,更别说Qt运行库和一堆插件了。等你把Qt动态库、platforms插件、字体插件、图片格式插件一个个拷上去,再配置LD_LIBRARY_PATHQT_PLUGIN_PATH,最后一个不小心版本没对齐,整个应用直接起不来。

静态编译能从根本上解决这套依赖搬运问题。把Qt核心库、必要插件和第三方库全部编进你的可执行文件里,拷到任何同架构、同内核版本的Linux上都能直接运行。对于产量不高的非标工控设备来说,这种“一个文件搞定”的交付方式效率极高,省去现场调试依赖的时间。

1.2 静态编译到底“静”到什么程度

这里得先说明白一个特别容易混淆的概念。网上说“Qt静态编译”,通常指的是Qt库本身编成.a静态库,你的应用程序链接后不再需要Qt的动态库,运行时不依赖libQt5Widgets.so这些东西。但请注意,程序最终还是要依赖libc、libstdc++、libm这些底层系统库的。

如果你连这些底层库都想彻底摆脱,那就得做完全静态链接,把glibc或者musl也编进去。在aarch64上做完全静态链接,一个绕不开的麻烦是glibc的NSS模块、locale数据、DNS解析这类机制需要动态加载,完全静态化之后非常容易出问题。所以我的建议很明确:做交叉静态编译,先把Qt静态化,链接libstdc++和libgcc的时候用静态方式,但保留libc动态链接。这是嵌入式Linux下最稳的方案,也是我这篇手册采用的路线。

1.3 选择Qt 5.14.2的几个现实理由

Qt 5.14.2是LTS之前比较成熟的一个版本,发布之后过了这么久,各种交叉编译的问题在网上几乎都有答案。相比Qt 6,5.14.2对老式ARM GPU、Framebuffer、嵌入式显示设备的兼容性更好,很多工业HMI方案到今天依然锁死在Qt 5.14。另外,官方提供了qt-everywhere-opensource-src-5.14.2.tar.xz这种一站式源码包,解压后所有模块都在里面,离线环境下也能构建,这一点对我们这种保密网络环境下的项目尤其友好。

2. 交叉编译环境与工具链准备工作

2.1 宿主机环境建议

交叉编译对宿主机的配置要求不高,我在四核八线程的x86_64 Ubuntu 20.04上完整编译过一次Qt,耗时不到四十分钟。建议宿主机使用Debian系Linux发行版,因为ARM64交叉工具链在软件源里开箱即用,不需要自己从头编GCC。磁盘空间预留至少15GB,我实测Qt静态库安装完之后体积大概在2GB左右,但源码目录加构建目录再加中间文件,峰值空间占用会到10GB以上。

内存方面,如果make的并行任务数开得太高,GCC编C++的大文件时内存很容易吃紧。我建议并行数设为CPU核心数减1,避免编译到一半Killed。

2.2 安装aarch64交叉编译器

Ubuntu 20.04直接安装工具链即可:

sudo apt update sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu

装完之后确认版本:

aarch64-linux-gnu-gcc --version

GCC版本在9到10之间都没问题,Qt 5.14.2的代码在这个范围里不会出现编译兼容性毛病。如果你用的是Ubuntu 22.04,默认的GCC 11也能用,但编译过程中可能会有个别模块因为警告被当作错误处理,我后面会在问题排查章节讲怎么绕过。

2.3 没有sysroot也能编译的底层逻辑

交叉编译嵌入式项目一般要准备一个sysroot,里面放着目标板的完整头文件和系统库,否则编译器找不到目标板的glibc和内核头文件。不过在Qt的交叉编译里,我们可以用另一种方式降低sysroot的依赖:把Qt依赖的第三方库全部换成Qt源码自带的内部实现。

Qt 5.14.2在configure阶段提供了若干-qt-*参数,比如-qt-zlib-qt-libpng-qt-libjpeg-qt-freetype-qt-pcre。这些参数让Qt在编译时直接使用源码里捆绑的第三方库,而不是去sysroot里找主机版本。这样配置好之后,我们只需要保证基础编译器可用,不需要额外建一个庞大的sysroot。这个设计极大简化了Qt交叉编译的起步阶段,也是后面所有步骤能一次跑通的前提。

3. Qt 5.14.2源码下载与模块裁剪

3.1 下载源码包

官方归档目录下载qt-everywhere-opensource-src-5.14.2.tar.xz,大概700多MB。下载后用MD5或者SHA256校验一下,避免源站镜像损坏。

我习惯放到/opt/src下集中管理:

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

解压后的目录结构里,qtbase是核心,qtdeclarativeqtquickcontrols这些是Qt Quick相关。如果你只做QWidget传统桌面风格的应用,很多模块是用不到的。

3.2 模块裁剪:该扔的扔,该留的留

qt-everywhere把Qt所有模块打包在一起,默认全量编译耗时极长,而且交叉编译时部分重量级模块还会带来一堆额外的依赖检查。我建议在configure之前先整理一个“黑名单”。

首选要跳过的是qtwebengine,这个模块要做完整Chromium内核的交叉编译,链到自己怀疑人生,基本没人在裸板环境里需要它。然后是qt3dqtchartsqtquick3d这些和OpenGL有暧昧关系的模块,只要不搞3D界面,直接skip。工控场景如果用不到Qt SerialBus和Qt SerialPort以外的高级网络模块,也尽量裁剪。

我实际使用的跳过清单是这样的:

-skip qtwebengine -skip qtwebview -skip qtwebchannel -skip qtwebsockets \ -skip qt3d -skip qtcharts -skip qtdatavis3d -skip qtgamepad \ -skip qtlocation -skip qtmqtt -skip qtnetworkauth -skip qtopcua \ -skip qtremoteobjects -skip qtscript -skip qtscxml -skip qtsensors \ -skip qtwayland -skip qtwebengine

注意qtserialportqtserialbus我没有跳,因为工控HMI基本都要用串口。你根据自己项目的实际需求来增减,原则很简单:不用到的模块就别编。

4. configure参数逐条拆解

4.1 一段可以直接用的configure命令

cross-compile配置是整个流程的核心,参数非常多,我先把一段实测可用的完整命令贴出来,再一条一条解释里面的逻辑:

cd /opt/src mkdir build-qt-aarch64-static cd build-qt-aarch64-static ../qt-everywhere-opensource-src-5.14.2/configure \ -prefix /opt/Qt/5.14.2/aarch64-static \ -opensource -confirm-license \ -release -static \ -xplatform linux-aarch64-gnu-g++ \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-pcre \ -no-icu -no-dbus \ -no-opengl \ -no-xcb -no-xkbcommon \ -linuxfb \ -no-cups -no-gtk \ -nomake examples -nomake tests -nomake benchmarks \ -skip qtwebengine -skip qtwebview -skip qtwebchannel -skip qtwebsockets \ -skip qt3d -skip qtcharts -skip qtdatavis3d -skip qtgamepad \ -skip qtlocation -skip qtmqtt -skip qtnetworkauth -skip qtopcua \ -skip qtremoteobjects -skip qtscript -skip qtscxml -skip qtsensors \ -skip qtwayland

4.2 关键参数背后的原理与选择

-static这个参数决定了Qt库以静态库形式输出。不加它,后面一切免谈。-release是发布模式,去掉调试符号和附加调试信息,能显著缩小最终可执行文件体积。-xplatform linux-aarch64-gnu-g++告诉Qt使用哪个交叉编译平台配置文件。

这里展开讲一下-xplatform。Qt的mkspecs目录里放着很多平台配置,linux-aarch64-gnu-g++这个目录在Qt 5.14.2源码中是默认带了的。它会自动把编译器设置成aarch64-linux-gnu-g++,不需要手动修改。但如果你下载的是修改过的源码包或者自定义的工具链,就需要检查一下这个目录是否存在。不存在的话,最简单的办法是复制linux-g++改名,然后手动编辑qmake.conf里几个编译器的变量。

-qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-pcre这几个参数前面说过了,是为了避免依赖宿主机或sysroot里的第三方库,统一使用Qt捆绑版本,并且这些库会以静态方式编入Qt库。

-no-icu-no-dbus是我个人强烈推荐的。ICU是个巨大的国际化库,交叉编译时依赖特别多,QWidget传统界面完全不需要它;D-Bus在嵌入式系统上也是可选的,阉割掉能减少不少麻烦。

-no-opengl-no-xcb-no-xkbcommon这三个是为了把Graphics和Windowing相关的依赖全部关掉。工控HMI如果用Framebuffer驱动屏幕,根本不需要X11,也不需要OpenGL。这一套组合拳下来,configure阶段最常见的“找不到GL库”“找不到xcb库”问题就从根源上消失了。

4.3 显示后端到底选什么:linuxfb / minimal / eglfs

Qt 5.14在嵌入式Linux下的显示后端主要有三个:linuxfb直接写Framebuffer,minimal不碰任何显示设备,eglfs基于OpenGL ES驱动显示。

适合大多数工控场景的是linuxfb。它直接操作/dev/fb0,不依赖X11、不依赖Wayland,普通LCD屏、HDMI转RGB屏都能支持。configure参数里加上-linuxfb就能编出linuxfb平台插件。如果目标板带GPU加速器,而且你会写OpenGL ES代码,那可以考虑eglfs,但它要额外准备GPU驱动对应的libEGLlibGLESv2,交叉编译配置复杂度高一个档次。

minimal平台插件默认也会编译,它不做任何显示输出,主要用来做无头环境下的冒烟测试。比如在x86主机上用qemu运行aarch64程序时,没有实际Framebuffer可用,就可以用-platform minimal先验证程序能正常走完初始化流程。

4.4 裁剪Qt库模块,减掉一半编译时间

刚才那串-skip参数直接决定了configure阶段要处理哪些模块。还有一个细节要注意,-nomake examples -nomake tests -nomake benchmarks能阻止构建系统编译大量的示例程序。这些示例对正式项目没有用处,在交叉编译时只会白白消耗几十分钟。

另外,-no-cups -no-gtk是关闭打印支持和GTK主题插件。如果你确定设备上没有打印机,也不需要通过GTK方式拉取桌面外观,这两个功能就是纯负担。

4.5 configure阶段需要重点检查的输出

跑完configure命令后,屏幕末尾会输出一份摘要,展示Qt将要构建的模块列表,以及各个特性是enabled还是disabled。你一定要花两分钟扫一遍这页内容,重点看这几个位置:

  • Qt Print Support 是否为 no,如果意外变成yes,说明-no-cups没生效。
  • X11/XCB Support 是否为 no。
  • OpenGL 相关是否都为 no。
  • LinuxFB 是否为 yes。

如果发现某项和你预期不符,先别急着make,回头检查对应参数。否则等到编到一半才报错,排查代价就大多了。

5. 从configure到install的一键构建

5.1 完整的自动构建脚本

每次手工敲configure参数容易出错,我习惯把整个流程写成一个shell脚本,放在构建目录里,方便反复执行。脚本内容如下:

#!/bin/bash set -e QT_SRC=/opt/src/qt-everywhere-opensource-src-5.14.2 BUILD_DIR=/opt/src/build-qt-aarch64-static PREFIX=/opt/Qt/5.14.2/aarch64-static JOBS=$(nproc --ignore=1) mkdir -p "$BUILD_DIR" cd "$BUILD_DIR" "$QT_SRC/configure" \ -prefix "$PREFIX" \ -opensource -confirm-license \ -release -static \ -xplatform linux-aarch64-gnu-g++ \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-pcre \ -no-icu -no-dbus \ -no-opengl -no-xcb -no-xkbcommon \ -linuxfb \ -no-cups -no-gtk \ -nomake examples -nomake tests -nomake benchmarks \ -skip qtwebengine -skip qtwebview -skip qtwebchannel -skip qtwebsockets \ -skip qt3d -skip qtcharts -skip qtdatavis3d -skip qtgamepad \ -skip qtlocation -skip qtmqtt -skip qtnetworkauth -skip qtopcua \ -skip qtremoteobjects -skip qtscript -skip qtscxml -skip qtsensors \ -skip qtwayland make -j"$JOBS" make install

脚本里我用nproc --ignore=1把并行编译任务数设为物理CPU数减1,避免内存被吃满。如果你在虚拟机里编译,建议直接固定make -j4,稳很多。

5.2 构建耗时与资源消耗

我实测下来,上面这套裁剪配置在8核16GB的机器上,完整构建时间大约35到40分钟。如果你不裁剪模块直接全量编译,时间奔着3小时以上去了。所以说,模块裁剪不只是省硬盘空间,更是在省你的时间。

编译过程最怕的就是中途磁盘空间耗尽,我建议每编译十分钟看一眼磁盘:

df -h /opt/src

构建目录里会堆积大量中间对象文件,峰值空间很容易到10GB以上,别让开发分区成为瓶颈。

5.3 验证安装产物是否正常

make install完成之后,到安装目录里检查一下文件结构:

ls /opt/Qt/5.14.2/aarch64-static/bin/qmake find /opt/Qt/5.14.2/aarch64-static -name "*.a" | head -20

如果能看到大量的.a静态库文件,说明Qt确实是按照静态模式编译出来的。再重点确认一下平台插件文件的位置,正常情况下应该在这里:

ls /opt/Qt/5.14.2/aarch64-static/plugins/platforms/

里面应该有libqminimal.alibqlinuxfb.a这两个静态库文件,这是后面应用能否正常跑起来的关键。如果你在目录里看到了.so后缀的动态库,那说明configure时-static没生效,需要回头检查参数。

5.4 给qmake配置交叉链接环境

虽然Qt安装完带了独立的qmake,但有些交叉编译环节还是需要手动指定编译环境。最好在/opt/Qt/5.14.2/aarch64-static/bin/qmake同目录下放一个环境变量文件,例如env.sh

export PATH=/opt/Qt/5.14.2/aarch64-static/bin:/usr/bin:/bin export CC=aarch64-linux-gnu-gcc export CXX=aarch64-linux-gnu-g++ export SYSROOT=/usr/aarch64-linux-gnu

每次编译应用之前先source env.sh,确保qmake和编译器都在PATH里,避免把x86宿主机自带的qmake误用了。

6. 用静态Qt构建一个真正的GUI应用

6.1 创建第一个QWidget工程

先写一个最小可用的Qt Widgets程序。创建hello目录,里面放三个文件。

hello.pro

QT += core gui widgets TARGET = hello TEMPLATE = app SOURCES += main.cpp QTPLUGIN += qlinuxfb qminimal qjpeg qgif CONFIG += static QMAKE_LFLAGS += -static-libgcc -static-libstdc++

main.cpp

#include <QApplication> #include <QPushButton> int main(int argc, char *argv[]) { QApplication app(argc, argv); QPushButton button("Hello from aarch64"); button.resize(320, 200); button.show(); return app.exec(); }

这个程序逻辑很简单,但里面埋了一个静态交叉编译最容易踩的大坑:插件链接。Qt 5的QPA架构里,创建窗口必须依赖平台插件。动态编译时插件是独立so文件,运行时从目录加载;静态编译时插件变成了静态库,如果不在编译阶段链进主程序,运行时就会报“could not find or load the Qt platform plugin”。

QTPLUGIN += qlinuxfb qminimal qjpeg qgif这一行就是解决这个问题。qmake在静态模式下看到QTPLUGIN,会去安装目录的plugins/platformsplugins/imageformats下面找对应的静态库并链接进来。少写这个,程序大概率起不来。

6.2 用aarch64静态qmake编译

环境变量准备好之后,进入hello目录直接编译:

source /opt/Qt/5.14.2/aarch64-static/bin/env.sh qmake hello.pro make -j4

编译完成后用file命令检查产物:

file hello

如果输出里包含ELF 64-bit LSB executable, ARM aarch64,说明交叉编译成功。再用ldd查看依赖:

aarch64-linux-gnu-readelf -d hello | grep NEEDED

你会发现依赖列表里没有Qt的库,只剩下libc.so.6libm.so.6libstdc++.so.6libgcc_s.so.1这几个底层系统库。这正是静态Qt应用该有的样子。

6.3 静态插件与静态库的链接顺序问题

有时候make链接阶段会报一堆“undefined reference”错误,指向QPlatformIntegrationPluginQPlatformScreenPlugin这类符号。这通常不是Qt本身的问题,而是链接顺序不对。

qmake生成的Makefile默认生成的链接命令是合理的,但如果你手动加-l参数或者调整了LIBS配置,很容易把插件库放在Qt库之前,导致链接器先处理libQt5Widgets.a时找不到插件符号。遇到这种问题,检查你.pro文件里的LIBSQTPLUGIN顺序,尽量让QTPLUGIN放在最后,或者干脆别手动干预插件库的链接。

6.4 部署到开发板运行实测

把编译好的hello文件通过scp拷到ARM64板子上,直接执行:

chmod +x ./hello export QT_QPA_PLATFORM=linuxfb ./hello

如果你的板子有真实的/dev/fb0接口,屏幕上应该能看到一个“Hello from aarch64”按钮窗口。如果板子是SSH连接的没有接屏,可以用minimal平台做无显示冒烟测试:

./hello -platform minimal

能正常退出不报错,就说明Qt库本身在目标系统上工作正常。要是这一步能过,后面接真屏的时候基本不会有阴暗问题。

7. 高频问题与排查技巧实录

7.1 configure阶段的两个经典误报

第一个是“找不到xcb”相关错误。很多新手明明加了-no-xcb,configure还是会因为检查系统库失败而中止。原因多半是在-skip-no-xcb之外还有一个模块强制开启了xcb支持。我的经验是,与其一个一个参数排查,不如直接在configure命令里同时加上-no-xcb -no-xkbcommon -no-xcb-xlib,把X11相关的检查彻底封印起来。

第二个是“ICU not found”或者“xkbcommon not found”。Qt 5.14的configure脚本对某些库的检测有优先级,即使你后面加了-no-icu,如果前面的顺序写错了,检测程序还是会去跑一遍。解决办法是确认-no-icu -no-dbus -no-xkbcommon这几个参数都出现在configure命令行中,而不是只靠环境变量。

7.2 链接阶段报找不到GL库

如果configure阶段-no-opengl没有正确生效,编译Qt某个模块时就会尝试链接libGL.so,而交叉编译环境里没有ARM64版的OpenGL库。排查思路是先回看configure摘要里OpenGL是enabled还是disabled,如果是enabled,把-no-opengl放到configure命令靠前的位置重新执行。

另一种情况是虽然configure摘要里OpenGL是no,但某个第三方模块编译时还是会找GL。这种情况直接把对应模块加入-skip列表,或者干脆不编译那个模块,一劳永逸。

7.3 运行时提示找不到platform plugin

这是静态Qt最经典的问题,出现频率极高。错误信息一般是“This application failed to start because no Qt platform plugin could be initialized”。

先说结论:99%的原因是QTPLUGIN没有配置正确。请检查hello.pro里有没有QTPLUGIN += qlinuxfb,并且确认安装目录下的plugins/platforms/libqlinuxfb.a文件真实存在。

还有一个隐蔽的原因:如果你在configure阶段没有加-linuxfb,linuxfb插件压根不会被编译,那么就算你写了QTPLUGIN += qlinuxfb,链接的时候也会因为找不到库而失败。所以项目规划时就要想清楚目标板用哪种显示后端,configure阶段就定下来,后面改起来比较麻烦。

7.4 完全静态libc的坑与实测教训

我试过一次用-static把整个产物做成完全静态可执行文件,当时觉得“一个文件哪里都能跑”很爽。实测跑起来才发现,程序里用QNetworkAccessManager访问域名解析时直接卡死,后面一查才知道是因为glibc的NSS模块和/etc/nsswitch.conf那一套机制没法在纯静态环境下正常工作。另外,locale、时区这类依赖动态加载数据的功能也全部退化成C默认行为,中文路径、中文字符串在这种环境下极易出乱码。

所以要再强调一次:做静态Qt,链接的libstdc++和libgcc可以用静态,libc请保留动态方式。这个方案既保住了Qt的“一键拷贝部署”优势,又不牺牲glibc的基础能力。对部署体验要求高的项目,可以考虑用musl工具链做完全静态,但那套方案需要所有依赖库都用musl重新编译,工作量完全另一个量级,不适合作为入门首选。

7.5 静态可执行文件体积优化

Qt静态编译出来的GUI应用体积一般在10MB左右,文件系统紧张的话可以做两步优化。第一步是用strip去掉符号:

aarch64-linux-gnu-strip hello

实测可以减掉20%到30%体积。第二步是检查你是不是不小心链接了用不上的Qt模块,例如:

QT -= network

只用QWidget的界面程序,完全可以去掉Network,体积降幅非常明显。

7.6 中文字体与Framebuffer显示优化

linuxfb本身不做字体渲染,Qt会通过freetype引擎读取系统里的字体文件。如果你的目标板是个精简rootfs,里面没有中文字体,那么界面上所有中文都会变成方块。解决方法是把wqy-microhei.ttc或者NotoSansCJK-Regular.ttc放到板子/usr/share/fonts目录下,然后设置环境变量:

export QT_QPA_FONTDIR=/usr/share/fonts

再有就是linuxfb模式下如果界面有闪烁感,可以尝试启用双缓冲。在程序启动参数里加-plugin linuxfb:fb=/dev/fb0:transform=0,同时在内核启动参数里确认fbcon驱动正常工作,帧缓冲设备的刷新策略不理想时,还可以在main.cpp里用QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL)捧一把软件渲染。

7.7 GCC版本与编译告警的处理

在Ubuntu 22.04上使用GCC 11编译Qt 5.14.2,偶尔会遇到因为C++标准变化导致的编译错误,典型现象是某个头文件报deprecatedcannot convert。遇到这种问题,最快的修复方式是在configure之前设置环境变量:

export CXXFLAGS="-Wno-deprecated-copy -Wno-error=deprecated-copy"

如果某个模块始终编不过去,先看它是不是你项目里必需的,非必需模块直接skip,不要在一个不用的模块上浪费半天时间。


最后再分享一个我实际维护这套工具链的小技巧:建议把构建目录整体打包留档,不要编完就删。我自己的习惯是编译成功后,把整个/opt/src/build-qt-aarch64-static目录连同Qt安装目录一起备份到外部存储里,以后换电脑、开新项目、升级目标板内核时,不需要重新花四十分钟跑一遍编译,直接把备份解出来就能用。这套Qt 5.14.2的aarch64静态交叉编译方案我用了快两个项目周期,期间只因为目标板换了内核版本重新编过一次,其他时间基本是“拷贝即用”的状态。希望这份手册能帮你少走几步弯路,顺利把ARM64板子上的Qt界面跑起来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询