上周接到一个需求,要给一块 ARM64 Linux 开发板搭一套 Qt 5 的运行环境。一开始我以为无非就是 apt install 的事,结果板子预装系统的源里根本没有合适的 qtbase5-dev:arm64 包,源里那个 Qt 版本又老得一批。没办法,只能自己交叉编译。这事前后折腾了三天,其中至少两天半都在处理各种"运行时才爆炸"的问题:configure 的时候一切正常,编译也顺利通过,拿到板子上跑起来却直接闪退;程序在主机上 link 得好好的,放到板子上就报一堆 shared library 找不到。回头复盘,真正的难点根本不在编译器,而在 Qt 的构建系统天然假设"编译机器和目标机器是同一台",一旦拆开,整条链路全是需要手动对齐的细节。
这篇文章就围绕这个主题展开:在 Ubuntu 22.04 上如何完整地交叉编译 Qt 5.15.10 到 ARM64,重点讲 OpenGL/EGL 相关的库处理、sysroot 的搭建方式,以及 WebEngine 这个模块值不值得硬啃。如果你也在给 RK3588、树莓派或者类似 ARM64 板子交叉编译 Qt,文里的命令和参数基本可以直接照抄,同时我会把每一步背后的原因说清楚,省得你踩同样的坑。
1. 为什么说 Qt5 交叉编译的困难不在编译器,而在构建系统
1.1 交叉编译最容易让人迷惑的一点:qmake 是 Host 程序,库是 Target 库
交叉编译 Qt 最核心的矛盾是:configure 脚本会在你的 x86_64 主机上执行,但它要检查的是目标板上的库和头文件;编译过程中生成的 moc、rcc、uic 这些工具是给当前编译机用的,编译出来的 libQt5Core.so 之类的库却是要给 ARM64 板子用的。这套机制本身没问题,但它要求你在配置阶段就把"哪些东西留在 host,哪些东西进 sysroot"分得非常清楚。
我第一次做的时候犯过一个很蠢的错:把 configure 的输出目录放在源码目录里,结果 Qt 的构建系统在同一个目录下既生成了 host 工具,又尝试用这些 host 工具去处理 target 库的源码,最后整棵源码树一团糟。正确做法是使用 shadow build,在源码目录之外单独建一个 build 目录来跑 configure。这个问题虽然不复杂,但在网上很多教程里反而被一笔带过了。
1.2 为什么不用 apt 直接装 arm64 版本 Qt
有人可能会问,Ubuntu 22.04 不是支持 multiarch 吗,直接把 arm64 的 Qt 包装进去不就行了?理论上可以,但实操中坑非常多。开启 arm64 架构后,整个 apt 系统会引入大量跨架构依赖,很多软件包之间的版本冲突会让你痛不欲生,而且你依然没法定制 OpenGL 后端。更重要的是,板子系统不一定和 Ubuntu 22.04 完全同源,有些 ARM64 板子用的是定制内核和老版本 glibc,直接拉装机器的 Qt 包很可能出现GLIBC_2.34 not found这种问题。
自编译最大的优势在于可控:你可以针对板子具体的内核和 GPU 驱动,选择正确的 EGL 库、编译出匹配的 QPA 插件,还能裁剪掉一堆用不到的模块。缺点就是麻烦,需要把整条交叉编译链打通。但一旦通了,后续扩展会非常顺手。
1.3 我最终确定的版本组合
这次选择的组合是:
- 主机系统:Ubuntu 22.04 LTS(x86_64)
- 工具链:gcc-aarch64-linux-gnu / g++-aarch64-linux-gnu(Ubuntu 22.04 自带 GCC 11 版本)
- Qt 源码:qt-everywhere-src-5.15.10.tar.xz
- 目标板:ARM64 架构,SoC 内集成 Mali GPU,系统是标准 systemd + glibc Linux
选择 Qt 5.15.10 的原因很简单,这是 Qt 5 分支后期维护得比较完整的版本,安全修复基本到位,而且 qmake 体系对嵌入式开发来说足够轻量。Qt 6 的构建系统变化太大,很多老项目的 qmake 工程迁移成本很高,没必要为了追新给自己加戏。
2. 搭建交叉工具链与 sysroot:前期工作决定后面 90% 的报错
2.1 交叉工具链的选择与验证
Ubuntu 22.04 自带 aarch64 交叉编译工具链,直接安装即可:
sudo apt update sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu build-essential如果还想省点事,可以顺带安装crossbuild-essential-arm64这个包,它会拉入一批 arm64 交叉编译常见依赖,包括 dpkg-cross 的一套基础工具。验证工具链是否正常:
aarch64-linux-gnu-gcc --version aarch64-linux-gnu-g++ -v 2>&1 | grep Target输出应该是Target: aarch64-linux-gnu,如果不是这个目标,说明你装错包了或者环境变量被某些第三方工具链污染了。
这里有个很容易被忽略的点:主机 GCC 版本和目标板 glibc 的关系。Ubuntu 22.04 的 GCC 11 编译出的动态库,默认可能要求 glibc 2.34 以上。如果板子系统还是老的 Ubuntu 20.04(glibc 2.31),你编译出来的程序放在板子上会直接报version GLIBC_2.34 not found。我的建议是,要么板子系统也用 Ubuntu 22.04 同源版本,要么在编译时通过-D_GLIBCXX_USE_CXX11_ABI=0之类的参数尽量降低 ABI 要求,但这不是万能的,最靠谱的还是让板子系统和主机的交叉编译目标 glibc 版本对齐。
2.2 用板子现成的根文件系统搭 sysroot,rsync 是首选
所谓 sysroot,就是交叉编译时让编译器去查找头文件和库文件的"虚拟根目录"。你当然可以自己用 apt 的dpkg-cross或者qemu-user去造一个,但我实测下来,最省事、最准确的方法是直接从目标板子上同步一份现成的根文件系统。
注意,千万不要用cp -r直接整个拷贝,因为板子的文件系统里包含大量软链接和特殊设备文件,cp -r往往会把软链接变成普通文件,后续编译时链接器找不到.so.1版本号,各种诡异报错就来了。用 rsync 可以完整保留符号链接:
mkdir -p ~/arm64-sysroot rsync -avP --delete root@板子IP:/lib ~/arm64-sysroot/ rsync -avP --delete root@板子IP:/usr/lib ~/arm64-sysroot/usr/ rsync -avP --delete root@板子IP:/usr/include ~/arm64-sysroot/usr/ rsync -avP --delete root@板子IP:/usr/local/lib ~/arm64-sysroot/usr/local/ rsync -avP --delete root@板子IP:/usr/local/include ~/arm64-sysroot/usr/local/这样同步出来的 sysroot 结构基本和板子一致。编译器配合--sysroot=/opt/arm64-sysroot参数使用时,会从这个目录下找头文件和库。
如果你的开发板和主机之间有网络隔离,没法直接 rsync,那就把板子上的这几个目录打成一个 tar 包传回来,但注意打包和解包时都要保留软链接:
tar -czhf sysroot.tar.gz /lib /usr/lib /usr/include /usr/local/lib /usr/local/include-h参数的作用是把软链接作为软链接打包,不是解引用。这一步很多人会漏。
2.3 软链接丢失是 sysroot 翻车的第一大原因
交叉编译时最常见的一类报错是cannot find -lEGL或者cannot find -lGLESv2,但你去 sysroot 里看,相关文件确实存在。问题往往出在软链接上。
我这次实际遇到的情况是,板子的 GPU 库是厂商闭源 blob,文件名是libmali-valhall-g610-g2d.so,然后通过软链接提供libEGL.so、libEGL.so.1、libGLESv2.so、libGLESv2.so.2这些标准名字。rsync 同步的时候如果因为权限问题跳过了一部分链接文件,Qt 的 configure 脚本就会在检测时认为系统里没有 EGL。
所以在搭建 sysroot 之后,务必检查关键库的软链接是否完整:
ls -l /opt/arm64-sysroot/usr/lib/aarch64-linux-gnu/libEGL* ls -l /opt/arm64-sysroot/usr/lib/aarch64-linux-gnu/libGLESv2* ls -l /opt/arm64-sysroot/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1再用这个命令找出所有断链的符号链接:
find /opt/arm64-sysroot -type l -xtype l如果发现断链,回到板子上确认原始路径,然后在 sysroot 里手动重建。这一步很机械,但绝对不能省,后面 configure 阶段的大量"假阴性"都来自这里。
2.4 自建 mkspecs 文件,把编译参数固化下来
Qt 的交叉编译通过 mkspecs 来告诉构建系统你用的是哪个平台和工具链。很多教程会让你直接使用-xplatform linux-aarch64-gnu-g++,但实际上 Qt 5.15.10 的源码里未必自带这个 spec,你需要自己建一个。
我习惯的做法是复制一个相近的配置再改:
cd qt-everywhere-src-5.15.10/qtbase/mkspecs cp -r linux-arm-gnueabi-g++ linux-aarch64-gnu-g++然后编辑linux-aarch64-gnu-g++/qmake.conf,内容如下:
MAKEFILE_GENERATOR = UNIX CONFIG += incremental QMAKE_INCREMENTAL_STYLE = sublib include(../common/linux.conf) include(../common/gcc-base-unix.conf) include(../common/g++-unix.conf) load(device_config) QMAKE_CFLAGS += -march=armv8-a QMAKE_CXXFLAGS += -march=armv8-a QMAKE_LINK = $${CROSS_COMPILE}g++ QMAKE_LINK_SHLIB = $${CROSS_COMPILE}g++ load(qt_config)这里CROSS_COMPILE变量会在 configure 时通过环境变量注入,这样整个构建过程的所有编译命令都会统一使用aarch64-linux-gnu-gcc系列工具链。
3. OpenGL 的真相:ARM64 板子上没有桌面 OpenGL,只有 EGL/GLES
3.1 先搞清楚目标板子的 GPU 到底以什么方式呈现
ARM64 开发板上基本没有独立显卡,绝大多数是 SoC 内集成的 GPU,比如 Mali、Adreno、PowerVR。它们对外提供的标准接口不是完整的桌面 OpenGL,而是 EGL + OpenGL ES。EGL 负责上下文管理和窗口系统绑定,GLES 负责实际的渲染。
这意味着交叉编译 Qt 时,你需要的不是桌面版 OpenGL 的头文件,而是 EGL、GLES2 和 GBM 这一套嵌入式图形栈。如果你在 configure 里用了-opengl desktop或者让 Qt 自动检测,它去找的是GL/gl.h这类桌面头文件,而 ARM 板子上通常没有,于是 Qt 会干脆禁用 OpenGL 支持,后续你连eglfs平台插件都编译不出来,程序在板子上连窗口都起不了。
3.2 sysroot 中的 libEGL 来源与软链接匹配问题
嵌入式 Linux 上的 OpenGL ES 图形栈一般有三种来源:
| 来源 | 典型文件名 | 优缺点 |
|---|---|---|
| Mesa 开源驱动 | libEGL_mesa.so、libGLESv2_mesa.so | 通用性好,调试方便,但某些厂商 GPU 硬件加速支持一般 |
| 厂商闭源 blob | libmali-valhall-g610-g2d.so 等 | 性能好,特性完整,但文件命名不规范,需要软链接补标准名 |
| 发行版软件包 | libgles2-mesa-dev:arm64 | 安装方便,但版本可能和板子驱动不匹配 |
这次项目中板子用的是 Mali GPU 的闭源 blob,库文件是libmali-valhall-g610-g2d.so。我需要在 sysroot 里手动建立如下软链接:
ln -sf libmali-valhall-g610-g2d.so /opt/arm64-sysroot/usr/lib/aarch64-linux-gnu/libEGL.so ln -sf libmali-valhall-g610-g2d.so /opt/arm64-sysroot/usr/lib/aarch64-linux-gnu/libEGL.so.1 ln -sf libmali-valhall-g610-g2d.so /opt/arm64-sysroot/usr/lib/aarch64-linux-gnu/libGLESv2.so ln -sf libmali-valhall-g610-g2d.so /opt/arm64-sysroot/usr/lib/aarch64-linux-gnu/libGLESv2.so.2头文件方面,如果板子的/usr/include里有 EGL/GLES 头文件,rsync 同步时已经带过来了。如果没有,要额外从板子上补:
rsync -avP root@板子IP:/usr/include/EGL /opt/arm64-sysroot/usr/include/ rsync -avP root@板子IP:/usr/include/GLES2 /opt/arm64-sysroot/usr/include/ rsync -avP root@板子IP:/usr/include/GLES3 /opt/arm64-sysroot/usr/include/ rsync -avP root@板子IP:/usr/include/KHR /opt/arm64-sysroot/usr/include/还需要确保gbm.h存在,因为 eglfs 的 KMS 后端依赖 GBM。如果板子上没有,可以在 sysroot 里从 Ubuntu 的libgbm-dev包提取头文件放到对应目录。
3.3 configure 里-opengl es2到底做了什么
Qt 5.15 中控制 OpenGL 类型的主要参数是-opengl es2。它会启用 OpenGL ES 2.0 的支持路径,并让 Qt 去检查 sysroot 下是否有GLES2/gl2.h和libGLESv2.so。
如果检测成功,Qt 会启用EGLFS平台插件;如果检测失败,Qt 会输出类似OpenGL ES 2.0 support cannot be enabled due to missing headers的信息,然后默默退化为无 OpenGL 状态。这种退化很多时候不报错,而是把QT_CONFIG里的opengl去掉,编译出的 Qt 缺少大量图形相关模块。
所以 configure 完之后,一定查看config.summary或config.log,确认OpenGL ES 2.0确实被启用。如果没启用,去config.log里搜GLES2,看具体是头文件缺失还是库文件缺失。
3.4 缺少 OpenGL 头文件的典型表现
如果 OpenGL 检测失败但仍然硬着头皮编完了 Qt,运行时会出现这些现象:
- 启动程序时提示
qt.qpa.plugin: Could not find the Qt platform plugin "eglfs"。 - 或者
Failed to create EGL display。 - 或者 GUI 程序能起来,但所有
QOpenGLWidget内容都是黑屏 / 空白。
这些情况十有八九不是系统配置问题,而是编译时 Qt 根本没把图形栈编译进去。排查第一步不是调板子环境,而是回来检查 sysroot 和 configure 参数。
4. configure 参数的每一步都对应一个坑:从 QtBase 到 WebEngine 的取舍
4.1 我用的 configure 完整命令逐段拆解
先给出我在没有 WebEngine 需求时使用的 configure 参数(这也是我最终实际使用的方案):
export CROSS_COMPILE=/usr/bin/aarch64-linux-gnu- export SYSROOT=/opt/arm64-sysroot export PKG_CONFIG_PATH=/opt/arm64-sysroot/usr/lib/aarch64-linux-gnu/pkgconfig export PKG_CONFIG_LIBDIR=/opt/arm64-sysroot/usr/lib/aarch64-linux-gnu/pkgconfig export PKG_CONFIG_SYSROOT_DIR=/opt/arm64-sysroot cd qt-everywhere-src-5.15.10 mkdir -p ../build-qt5-arm64 && cd ../build-qt5-arm64 ../qt-everywhere-src-5.15.10/configure \ -prefix /opt/qt5.15.10-arm64 \ -opensource -confirm-license \ -xplatform linux-aarch64-gnu-g++ \ -sysroot /opt/arm64-sysroot \ -nomake examples -nomake tests \ -skip qtwebengine -skip qt3d -skip qtcanvas3d \ -skip qtdoc -skip qtlocation -skip qtmqtt -skip qtopcua \ -skip qtserialbus -skip qtserialport -skip qtspeech \ -skip qtvirtualkeyboard -skip qtwebview \ -opengl es2 \ -qpa eglfs逐个解释关键参数的含义:
-prefix:最终安装到板子哪个路径。这个路径会写进 Qt 的配置里,运行时能够被 qmake 查询到。-xplatform linux-aarch64-gnu-g++:告诉 Qt 使用我们刚创建的交叉编译 mkspecs,而不是默认的 host 平台。-sysroot:指定编译时的根文件系统目录,编译器会在该目录下查找头文件和库。-skip:跳过不需要的模块,避免它们占用编译时间或引入额外依赖。-opengl es2:指定使用 OpenGL ES 2.0 作为图形后端。-qpa eglfs:指定默认的平台插件为 eglfs,这样部署到板子上之后即使不设置环境变量也能默认从 framebuffer 启动图形界面。
注意PKG_CONFIG_*这三个环境变量。Qt configure 检查系统库时依赖 pkg-config,如果不把它指向 sysroot,它会在主机上找库,然后找到一堆 x86_64 的.so,导致 configure 出现各种"看起来找到了,其实根本不能用"的情况。
4.2 那些可跳过的模块:为什么不要舍不得
很多人看到-skip一大堆就心疼,总觉得每个模块将来可能用上。以嵌入式板子的实际情况来看,大部分模块用了只会增加体积和构建时间:
| 模块 | 为什么跳过 |
|---|---|
| qt3d | 依赖深度 OpenGL 特性,嵌入式板子上基本跑不动 |
| qtcanvas3d | 废弃模块 |
| qtdoc | 文档系统,运行时用不到 |
| qtlocation | 依赖额外的地理服务库 |
| qtmqtt | 需要 MQTT 服务端配合,看项目需求决定 |
| qtopcua | OPC UA 工业协议,非自动化项目用不上 |
| qtserialbus | 工业总线协议栈 |
| qtspeech | 语音合成依赖外部库 |
| qtvirtualkeyboard | 如果不需要触摸屏软键盘就跳过 |
| qtwebview | 依赖 WebEngine 后端 |
保留核心的 qtbase、qtdeclarative、qtquickcontrols2、qtquicktimeline、qtgraphicaleffects 就能满足绝大部分嵌入式界面需求。QML 和 Widgets 都是自包含的,不需要额外扯进来一堆重型依赖。
4.3 ICU 与 xcb:两个容易让 configure 产生迷惑的依赖项
ICU(International Components for Unicode)是 Qt 处理文本和正则表达式的依赖库。如果 sysroot 里没有 libicu,Qt5 会尝试使用自己内置的 ICU 版本(Qt 源码里携带了部分 ICU 代码),但内置版本通常只够基础功能,某些高级文本处理和 WebEngine 会受影响。
我这里的选择是-no-icu,因为板子上根本没有 ICU 运行时的优化需求,而且交叉编译时额外拉一套 ICU 库非常折腾。如果你的程序强依赖复杂文本排版和语言处理,比如需要非常完整的 Unicode 特性,还是老老实实在 sysroot 里装好 ICU 再编。
xcb 是 X11 协议的 C 绑定库。如果你准备在板子上跑 X11/Wayland 桌面环境,那么 Qt 需要 xcb 平台插件;如果像我一样直接用 eglfs 直驱 framebuffer/DRM,就不需要 xcb。但有个坑是:configure 如果检测不到 xcb 库,它只会默默不启用 xcb,而不会报错。所以如果将来你打算在板子上接显示器跑完整的桌面环境,最好在 configure 参数里显式加上-xcb并确保 sysroot 里有对应的 libxcb 库。
这次我们的场景是纯嵌入式 Linux,-qpa eglfs已经足够,不需要 xcb,所以没有为 xcb 做额外准备。
4.4 与 WebEngine 纠缠:configure 时到底要不要带上
这是整个交叉编译里最让人纠结的选择。WebEngine 不是 Qt 自己的纯 C++ 模块,它内部嵌了整个 Chromium 浏览器内核。构建 WebEngine 相当于在交叉编译环境中再构建一份 Chromium,这对构建系统的复杂度、宿主机的资源要求都提出了极高的要求。
我在实际过程中 try 过保留 qtwebengine 的 configure,结果卡在 Chromium 的 GN/Ninja 构建系统上。后面单独用一节详细讲。这里先说一个重要的判断逻辑:如果你的应用只是显示网页、展示文档、跑 H5 页面,不要默认"我需要 WebEngine",先想想能不能用外部浏览器进程替代。如果确实需要内嵌无边框浏览器,再评估自己有多少时间和内存愿意花进去。
我在最终交付时没有编 WebEngine。这个决定帮我省了至少一天时间,而且从项目结果看完全正确。
5. WebEngine 的交叉编译:如果可以,我建议你在动手前先想清楚是否真的需要
5.1 WebEngine 为什么能把交叉编译的难度放大十倍
Qt WebEngine 的构建本质上是把 Chromium 塞进 Qt 的构建体系里。Chromium 使用的是独立的 GN/Ninja 构建系统,Qt 的 qmake 只是在外面套了一层壳。交叉编译时,你面对的不只是 qmake 一个工具链,而是 Chromium 一大套编译基础设施。
Chromium 的构建流程中,会先为宿主机编译一批代码生成工具(比如各种 code generator),然后再为 ARM64 目标编译真正需要的代码。这两个阶段如果混在一起,就会出现"用 ARM64 库链接 host 工具"或者反过来"host 工具链接了 target 库"的奇怪错误。Qt WebEngine 的交叉编译支持并不完善,官方的嵌入式参考大多假定你在目标板本地编译,或者使用非常严格的 Yocto 环境。
还有一个非常现实的问题是资源损耗。Chromium 每个大版本编译都要消耗大量内存,WebEngine 内部用的 Chromium 87 在链接 V8 引擎时,单个链接任务占用内存可以超过 8GB。普通开发机如果只有 16GB 内存,并行编译很容易 OOM。
5.2 如果你必须编 WebEngine,先把这些条件备齐
尽管不推荐,如果项目需求绕不开,下面这些准备条件是必须的:
- 宿主机内存至少 16GB,建议 32GB,最好准备 16GB 以上的 swap 分区。
- 磁盘剩余空间至少 30GB,WebEngine 单独构建过程会产生大量中间文件。
- sysroot 里要有 WebEngine 的运行时依赖库,包括
libnss3、libasound2、libpci、libxtst、libxss、libfontconfig等,这些是 Chromium 的依赖,不是 Qt 的。 - 在 configure 时不要
-skip qtwebengine,并加上-webengine-proprietary-codecs来启用 H.264/AAC 等编解码器(如果需要的话)。
configure 命令大致在之前的保底版基础上,去掉-skip qtwebengine,加上:
-webengine-proprietary-codecs然后编译时建议:
make -j2千万不要用-j$(nproc),WebEngine 的并行编译会把系统内存直接榨干。
5.3 编译期的经典崩溃与对策
我在试编 WebEngine 时遇到的第一个问题是ninja: error: ...,具体来说是为宿主机构建gn工具时发生了失败。QtWebEngine 在构建过程中会从某个外部位置下载 GN 工具,或者使用源码内置的脚本生成,而下载阶段如果因为网络原因中断或者拿到不完整的包,整个过程就会立即停止。
对策是提前查看 QtWebEngine 源码里src/3rdparty的说明,找到它需要的确切 GN 版本,单独手动下载并放到PATH中,让构建系统直接找到而不重新下载。
第二个常见问题是 V8 链接阶段的 OOM。这在低内存环境几乎是必然发生的。除了make -j1之外,还可以设置环境变量限制内存:
export NINJA_STATUS="[%f/%t] " export GYP_DEFINES="use_allocator=none"但这些手段属于从边缘上修修补补,不能完全解决内存不足的问题。最实际的方法是换一台内存更大的机器,或者临时加 swap。
第三个问题是 configure 阶段就报错,比如ERROR: WebEngine requires icu。这通常是因为 sysroot 里的 ICU 版本和 QtWebEngine 期望的版本不一致。这时候要么补齐 libicu-dev 到 sysroot 里,要么用-no-icu让整个 Qt 都不用 ICU。注意-no-icu会影响 WebEngine 中一些网页渲染功能,但在很多纯显示场景下问题不大。
5.4 不编 WebEngine 的替代方案:外部浏览器进程可能是更好的解
说实话,我在评估了项目需求后,最后发现"内嵌网页"这个需求完全可以用外部浏览器进程解决。Qt 程序通过QProcess调用板子上现成的浏览器,以全屏或者无边框模式打开指定 URL,交互效果其实一点不差。
这个方法有几个明显优势:
- 交叉编译工作量小了很多,不用碰 Chromium 这头大象。
- 浏览器版本可以独立升级,和 Qt 程序解耦。
- 内存和 CPU 占用由独立的浏览器进程管理,Qt 程序本身崩了不影响浏览器,反过来也一样。
- 调试简单,浏览器可以直接开远程调试端口。
代价是窗口不是真正"嵌入"在 Qt 应用里,对于需要长时间显示在 Qt 界面内嵌区域的场景不太友好。如果是多标签页互通、JS 和原生代码大量双向调用这种复杂需求,外部浏览器进程方案就不太合适了。
一个比较新的嵌入式场景会让 WebEngine 更不可替代:Qt Quick 应用中需要播放 H5 视频,并且希望视频画面渲染在 OpenGL 纹理上。这种需求只有 WebEngine 能直接支持。如果你的项目属于这种,那就只能硬着头皮编,或者考虑用平台自带的硬件解码方案替换。
6. 部署到 ARM64 板子:运行时才是真正的验收现场
6.1 把 Qt 部署到板子上,别遗漏符号链接
编译安装完成之后,产物通常在/opt/qt5.15.10-arm64/目录下。部署到板子上依然推荐 rsync:
rsync -avP /opt/qt5.15.10-arm64/ root@板子IP:/opt/qt5.15.10-arm64/这一步最怕中途用 Windows 中转,或者用压缩包方式拷贝到 Windows 再传回板子,那样所有软链接会全部丢失。之前检查过 libQt5Core.so.5 这种情况,如果是软链接或者是被改名的实际文件,拷贝后没有对应软链接名,动态链接器就会直接找不到库。
6.2 环境变量设置的顺序与内容
部署完成后,在板子上设置如下环境变量:
export QT_ROOT=/opt/qt5.15.10-arm64 export PATH=$QT_ROOT/bin:$PATH export LD_LIBRARY_PATH=$QT_ROOT/lib:$QT_ROOT/plugins/platforms:$LD_LIBRARY_PATH export QT_QPA_PLATFORM=eglfs export QT_QPA_EGLFS_INTEGRATION=eglfs_kms export QT_QPA_EGLFS_ALWAYS_SET_MODE=1QT_QPA_EGLFS_INTEGRATION这个变量很容易被忽略,但它在很多 Mali/Adreno GPU 板子上是必须的。如果设置为eglfs_kms告诉 eglfs 走 DRM/KMS 后端。如果板子用的闭源 blob 带有自己的一套 EGL 集成,可能要换成eglfs_mali之类的实现。判断方式很简单,看 sysroot 或板子上/usr/lib/aarch64-linux-gnu/下的 libqtqpa-eglfs-integration 相关库是否存在。在 Qt5.15 里,这些集成插件的编译产物通常在$QT_ROOT/plugins/eglfs/下。
6.3 运行时三个高频报错的排查链路
部署完第一次运行程序,大概率会碰到下面几个报错。我把排查链路列出来,遇到时可以按顺序走一遍。
报错一:Could not find the Qt platform plugin "eglfs"
第一步检查插件是否存在:
ls -l /opt/qt5.15.10-arm64/plugins/platforms/如果没有libqeglfs.so,说明编译时 eglfs 插件没被构建。回到主机上检查 configure 时 OpenGL ES 检测是否成功,重点看config.log中GLES2关键字。如果插件存在但仍然报这个错误,用调试模式看具体加载失败原因:
QT_DEBUG_PLUGINS=1 ./你的程序这个输出会告诉你它在找哪些库、有没有找到你的libqeglfs.so。
报错二:Failed to create EGL display
这行日志通常意味着 Qt 已经成功加载了 eglfs 插件,但调用 EGL 初始化时失败了。逐项排查:
- 确认板子的 DRM 设备存在:
ls /dev/dri/,至少要有card0。 - 确认
LD_LIBRARY_PATH里能找到 libEGL.so.1,执行ldconfig -p | grep EGL或者ls /usr/lib/aarch64-linux-gnu/libEGL*。 - 用
glmark2-es2或eglinfo这类工具验证板子的 EGL 环境本身是否健康。如果系统里的eglinfo都失败,说明不是 Qt 的问题,是板子的 GPU 环境没初始化好,先解决驱动问题。
报错三:error while loading shared libraries: libQt5Core.so.5
这是动态库路径问题。先用file确认二进制确实是 ARM64 架构,再用ldd查看实际依赖哪些动态库和路径:
file ./你的程序 ldd ./你的程序如果发现某个库的路径仍然指向编译机上的/usr/lib/x86_64-linux-gnu/,那是构建阶段的 sysroot 没有正确作用。如果只是路径不对,在板子上重新设置LD_LIBRARY_PATH即可。
6.4 快速验证交叉编译结果是否可用的三步走
每次部署完一个新增模块,我都会用这三步验证,屡试不爽。
第一步,在板子上运行:
/opt/qt5.15.10-arm64/bin/qmake -query如果 qmake 输出的路径是你设置的QT_INSTALL_PREFIX=/opt/qt5.15.10-arm64,说明 qmake 配置正确。如果输出的是/usr/local/Qt-5.15.10之类的其它路径,说明编译时的-prefix参数没有覆盖到 qmake 的配置里。
第二步,写一个最简单的 Widgets 程序,项目文件hello.pro:
QT += widgets SOURCES += main.cpp#include <QApplication> #include <QLabel> int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label("Hello ARM64 Qt"); label.resize(320, 200); label.show(); return app.exec(); }第三步,在主机上用我们刚刚编出来的 qmake 和 make 构建:
/opt/qt5.15.10-arm64/bin/qmake hello.pro make -j$(nproc)把生成的hello文件传到板子上运行。如果界面正常显示,说明 Qt 核心、OpenGL 插件、部署路径全部打通。如果失败,按上面 6.3 的链路去排查。
再多说一句关于这套环境后续扩展的事情。交叉编译只解决"能不能编出来"的问题,真正上线后你还会遇到板子性能、GPU 驱动升级、Qt 模块增删等一堆事。我的建议是,在第一次交叉编译成功之后,把整个 sysroot 打包存一份,把你有用的 configure 参数写进脚本提交到版本库里。这样不管是换人维护还是换板子同步环境,都能在十分钟内重新拉起来,而不是靠脑子里的记忆重来一遍。