在嵌入式开发中,将现代化的 UI 框架 Qt 6 移植到 ARM 板子上是一件极具成就感但也充满挑战的事情。尤其是面对基于 Debian/Ubuntu 系统的树莓派时,由于其独特的多架构(Multi-arch)库路径设计,往往会让编译器找不到头文件或链接库。
本文将基于最新的 Qt 6.9.2 源码,记录一次从零构建、解决各类诡异报错、最终成功运行 QML 程序的完整交叉编译全过程。
参考官方教程(QtAssistant)
Qt 6.9/Product information/Supported Platforms/Qt for Embedded Linux
Qt 6.9/Product information/Supported Platforms/Qt for Embedded Linux/Configure an Embedded Linux Device
Qt 6.9/Product information/ Supported Platforms/ Cross-compiling Qt
内容为自己写了一份, 真实的编译过程, 完整测试并且成功运行, 让AI整理了一下, 基本都是实际遇到的问题与步骤
核心术语与架构预览
在正式动手之前,我们需要理清几个核心概念:
- 宿主机(Host): 负责编写代码并执行编译的机器,通常是一台高性能的 x86_64 Linux 电脑(此处使用的是 Ubuntu/WSL2)
- 目标机(Target): 最终运行程序的嵌入式设备,例如树莓派 4(AArch64 架构)
- 交叉编译(Cross-compiling): 在宿主机上编译出能在目标机上运行的二进制程序
- Sysroot(目标系统根目录): 目标板上真实的
/lib、/usr/include等目录的一个本地镜像备份。编译器需要通过它来了解目标板上有哪些库和头文件
主机工具(Host Tools)的核心作用
在编译 Qt 源码时,需要运行很多 Qt 自身的工具(如 moc 元对象编译器、rcc 资源编译器、qsb 着色器编译器等)来生成代码。
关键点: 这些工具必须能在宿主机(x86_64)上直接运行。因此,在为目标机编译 Qt 之前,我们必须先在宿主机上编译出一套同版本的本地 Qt,专门用来提供这些"主机工具"。确保版本绝对一致的最佳方法,就是用同一套源码先后编译两次
QPA 平台插件该如何选择?
Qt 通过 QPA(Qt Platform Abstraction,Qt平台抽象) 插件来适配不同的显示环境。嵌入式平台常见的插件对比如下:
| QPA 插件 | 适用场景 | 核心特性与性能 |
|---|---|---|
| EGLFS | 无桌面环境的纯嵌入式系统 | 推荐首选默认 基于 EGL 和 OpenGL ES 2.0,直接全屏单窗口输出,硬件加速性能最高。 |
| Wayland | 新版现代桌面系统 | 新一代轻量化窗口管理器,高刷新率优化极佳。 |
| LinuxFB | 无 GPU 加速的老旧设备 | 直接读写 framebuffer(/dev/fb0),不支持 OpenGL/Qt Quick 2,纯 CPU 渲染,性能较低。 |
| XCB | 传统 X11 桌面(如老 Ubuntu) | 支持多窗口和弹窗,但对嵌入式硬件加速支持极差,不推荐。 |
| VNC | 远程无头(Headless)部署 | 将显示画面通过网络 VNC 协议输出,便于远程调试。 |
第一步:准备工作(Host 构建与目录规划)
我设工作目录中包含以下内容:
- 工具链(Toolchain):
cross-pi-gcc-14.2.0-64(64位树莓派交叉编译器)abhiTronix/raspberry-pi-cross-compilers: Latest GCC Cross Compiler & Native (ARM & ARM64) CI generated precompiled standalone toolchains for all Raspberry Pis. 🍇 - Sysroot: 从树莓派上同步过来的根文件系统(只需要
/lib /usr/lib /usr/include, 其中/lib是/usr/lib的软链接),存放在/home/nichijou_wsl/LinuxTools/raspegl_sysroot中 - Qt 源码:
qt-everywhere-src-6.9.2Index of /archive/qt/6.9/6.9.2/single
sysroot(系统根目录)是交叉编译工具链中的一个核心概念,它指定了目标系统(Target System)的根文件系统路径. 注意复制sysroot时,树莓派使用的是多架构模式, 其内容不在/usr/lib下, 而是在/usr/lib/aarch64-linu-gnu下, 可以考虑直接把这个文件复制到主机sysroot/usr/lib下, include也是同理. 也可以在cmake中手动指定搜索路径, 后文采用的是这个方式, 这样不用修改sysroot中的内容.
1. 编译宿主机 Host Qt
首先,进入一个独立的构建目录,仅编译出供后续交叉编译使用的主机工具(host_tools),这样可以大幅节省时间,且不需要真正执行 make install。
bash
mkdir ~/QtHostBuild && cd ~/QtHostBuild
/path/to/qt-everywhere-src-6.9.2/configure -developer-build -nomake tests
cmake --build . --target host_tools
这个命令需要调整, 命令来自官方教程, 但是编译主机的Qt基本不会遇到什么问题, 这样编译出来的主机不一定可用, 但是用来交叉编译目标机器的Qt需要的工具已经具备了. 也可编译好后安装到指定目录, 具体过程略, 总之最后我是编译并安装到了
/home/nichijou_wsl/LinuxTools/qt6-host-x86_64中.
第二步:编写 CMake Toolchain 配置文件
Qt 6 全面拥抱 CMake。为了能够优雅地管理交叉编译参数,强烈建议准备一个专属的 raspberrypi.cmake 工具链文件
针对树莓派的多架构(aarch64-linux-gnu)路径,我们需要显式通过 -B 指定运行时启动文件的位置,以及 -I 和 -Wl,-rpath-link 来告诉链接器去哪里找隐式依赖。
在本地创建 /home/nichijou_wsl/LinuxTools/raspberrypi.cmake,内容如下:
cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
# 1. 定义 sysroot 与平台架构
set(CMAKE_SYSROOT /home/nichijou_wsl/LinuxTools/raspegl_sysroot)
set(CMAKE_LIBRARY_ARCHITECTURE aarch64-linux-gnu)
set(TARGET_SYSROOT ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
# 2. 关联之前编译好的 Host Qt 路径与本地安装路径
set(QT_HOST_PATH /home/nichijou_wsl/LinuxTools/qt6-host-x86_64)
set(CMAKE_STAGING_PREFIX /home/nichijou_wsl/LinuxTools/qt_raspegl) # 宿主机上的临时安装点
set(CMAKE_INSTALL_PREFIX /usr/local/qt6) # 最终在树莓派上的部署路径
# 3. 指定编译器路径
set(CMAKE_C_COMPILER /home/nichijou_wsl/LinuxTools/cross-pi-gcc-14.2.0-64/bin/aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER /home/nichijou_wsl/LinuxTools/cross-pi-gcc-14.2.0-64/bin/aarch64-linux-gnu-g++)
# 4. 核心:强制注入多架构路径(解决 crt1.o 与头文件丢失问题)
# -B 告诉编译器去哪里找内部启动组件和库
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -B${CMAKE_SYSROOT}/usr/lib/${CMAKE_LIBRARY_ARCHITECTURE}")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${CMAKE_SYSROOT}/usr/include/${CMAKE_LIBRARY_ARCHITECTURE}")
# -Wl,-rpath-link 引导链接器解决间接依赖的库寻找
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wl,-rpath-link,${CMAKE_SYSROOT}/lib/${CMAKE_LIBRARY_ARCHITECTURE}")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wl,-rpath-link,${CMAKE_SYSROOT}/usr/lib/${CMAKE_LIBRARY_ARCHITECTURE}")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}")
# 5. 修正 Pkg-Config 在交叉编译下的行为
set(ENV{PKG_CONFIG_SYSROOT_DIR} ${CMAKE_SYSROOT})
set(ENV{PKG_CONFIG_LIBDIR} ${CMAKE_SYSROOT}/usr/lib/${CMAKE_LIBRARY_ARCHITECTURE}/pkgconfig)
set(ENV{PKG_CONFIG_PATH} "")
# 6. 查找模式防御:只在 sysroot 中查找库和头文件,绝不污染宿主机环境
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
第三步:交叉编译经典踩坑录(血泪排查)
在执行 configure 配置和 cmake --build 编译期间,我们极大概率会遇到以下几个经典报错。这里给出原因和一剂特效药:
坑 1:Mesa 图形库导致的 SBOM 识别错误
- 报错信息: >
CMake Error at qtbase/cmake/QtPublicCMakeHelpers.cmake:548 (message): Unknown arguments: (-;kisak-mesa;PPA) - 翻译与原因: 意思是"未知的参数:(-;kisak-mesa;PPA)"。这是因为树莓派的 Sysroot 中安装了第三方的
kisak-mesa图形驱动加速源,其特殊的版本字符串干扰了 Qt 6.9 的 SBOM(软件物料清单组件) 检查机制。 - 解决办法: 在
configure参数中加上-no-sbom跳过物料清单生成。
坑 2:找不到 crt1.o 或 libc-header-start.h
- 报错信息: >
cannot find crt1.o: No such file or directory
fatal error: bits/libc-header-start.h: No such file or directory - 翻译与原因: 提示找不到 C 语言运行时的核心启动文件(
crt1.o)或标准 C 库头文件。由于 Debian 系的多架构策略,这些文件被藏在了/usr/lib/aarch64-linux-gnu下。 - 解决办法: 确保你的
toolchain.cmake中正确添加了上文提到的-B、-I和-Wl,-rpath-link标志。
坑 3:缺失三方依赖库(Brotli / GTK3)
- 报错信息: >
fatal error: brotli/decode.h: No such file or directory
cannot find -lgtk-3: No such file or directory - 翻译与原因: 分别代表"找不到网页压缩算法 Brotli 的解码头文件"以及"找不到 GTK3 图形主题库"。
- 解决办法: 如果我们是做纯嵌入式全屏 UI,网络压缩和桌面主题不是必须的,可以直接裁剪掉。在
configure阶段添加-no-feature-brotli -no-feature-gtk3。
坑 4:符号冲突------被 Sysroot 原有的旧版本 Qt6 污染
- 报错信息: >
sysroot/lib/aarch64-linux-gnu/libQt6OpenGL.so.6: undefined reference to 'lcOpenGLProgramDiskCache()@Qt_6' - 翻译与原因: "未定义的引用 lcOpenGLProgramDiskCache"。这意味着链接器在编译时,错误地链接到了树莓派 Sysroot 本身自带的旧版本 Qt6 动态库,而不是我们当前正在编译的全新 Qt 6.9.2 源码,从而导致符号对不上。
- 解决办法: 釜底抽薪,断了链接器的后路。直接在 Sysroot 中把旧的 Qt 库隔离备份起来:
bash
cd /home/nichijou_wsl/LinuxTools/raspegl_sysroot/lib/aarch64-linux-gnu/
mkdir -p qt6_old_backup
mv libQt6* qt6_old_backup/ # 强行移走,迫使链接器使用本次编译输出的新库
第四步:最终配置与编译安装
扫清所有障碍后,在源码同级创建 build-rasp-new 目录,并进入该目录, 执行最终的配置指令:
bash
../configure -release -opengl es2 -egl -no-sbom \
-skip qtwebengine -skip qtwebview \
-no-feature-brotli -no-feature-gtk3 \
-- -DCMAKE_TOOLCHAIN_FILE=/home/nichijou_wsl/LinuxTools/raspberrypi.cmake
注意: 配置完成后,请务必仔细检查终端输出目录的的
config.summary(配置摘要) !确保以下两项为yes,这才意味着你的 GPU 硬件加速成功开启了:
OpenGL ES 2.0 ........................ yes
EGLFS .................................. yes
EGLFS GBM ............................ yes
确认无误后,全核起飞开始编译并安装:
bash
cmake --build .
编译好后安装到指定目录, 方便将安装目录的内容部署到树莓派上
bash
cmake --install . --prefix ../../qt_egl_rasp
第五步:部署至树莓派并配置环境变量
编译完成后,本地的 qt_egl_rasp 目录中会生成 bin、lib、plugins、qml 等完备的构建产物。我们只需将运行时必需的动态库和插件同步到树莓派即可。
1. 使用 rsync 进行文件同步
bash
# 同步到临时目录(避免权限问题)
rsync -avz lib nichijou@192.168.137.18:/tmp/qt6/
rsync -avz plugins nichijou@192.168.137.18:/tmp/qt6/
rsync -avz qml nichijou@192.168.137.18:/tmp/qt6/
# 登录树莓派,将其移动到规范的安装目录
sudo cp -r /tmp/qt6 /usr/local/qt6
这样目标机器就拥有运行使用这一套工具编译的软件的能力了. 测试发现这样编译下来支持这些平台插件
wayland, eglfs, linuxfb, vnc, minimal, minimalegl, offscreen, wayland-egl
2. 关键:在树莓派上配置环境变量
为了让树莓派能正确识别到我们的新 Qt6 库、插件以及字体,必须在系统的 ~/.bashrc 或启动脚本中配置以下环境变量:
bash
# 优先引导我们的新 Qt6 动态库路径
export LD_LIBRARY_PATH=/usr/local/qt6/lib:$LD_LIBRARY_PATH
# 明确指示 Qt 插件的寻址位置
export QT_QPA_PLATFORM_PLUGIN_PATH=/usr/local/qt6/plugins
# 如果你需要直接输出到显示器而没有桌面,指定 EGLFS 平台
export QT_QPA_PLATFORM=eglfs
# Qt 6 移除了默认内置字体,必须指定系统有效字体路径,否则界面文字会变成方块
export QT_QPA_FONTDIR=/usr/share/fonts/truetype/dejavu
第六步:应用开发实战:使用 qt-cmake 飞速编译
这一套流程下来,最爽利的部分就在这里了。Qt 编译完成后,会在其安装目录的 bin/ 下生成一个名叫 qt-cmake 的脚本。
这个脚本是一个完美的"高阶封装"。它已经将我们的交叉编译器、Sysroot 路径、以及编译好的目标板 Qt 库路径 全部打包内置成了通用的 qt.toolchain.cmake。
后续我们在 CLion、VS Code 或终端编译自己的项目(比如 NQtQmlExample)时,不再需要往项目的 CMakeLists.txt 里塞满各种交叉编译参数-例如sysroot位置, Qt位置, 交叉编译工具链位置 。项目保持最纯净的桌面级编写逻辑,只需在配置项目时用 qt-cmake 替代标准 cmake 即可:
bash
# 使用生成的快捷 qt-cmake 脚本进行工程配置
/usr/local/qt6/bin/qt-cmake \
-DCMAKE_BUILD_TYPE=Release \
-G Ninja \
-S /path/to/your/NQtQmlExample \
-B /path/to/your/NQtQmlExample/build-target
# 进入构建目录一键编译
cd /path/to/your/NQtQmlExample/build-target
cmake --build .
把编译出来的可执行文件直接拷贝至树莓派。运行,收工!