手上有一块跑了两年多的板子,根文件系统是早年用 Buildroot 搭起来的,里面自带的 Qt 停在 5.6 那个年代。新接的需求要用到 Qt 5.12 里才稳定的几个模块和特性,客户那边也明确要求锁死在 5.12 LTS 这个长期支持分支上,理由是它的维护周期够长、生态里的第三方库兼容性好。于是问题就摆在面前:怎么在 Buildroot 里把 Qt 5.12 塞进去,而不是重装一套更新的构建系统把整个 BSP 推倒重来。这件事看起来只是改几个版本号,实际动手你会发现坑集中在源码包命名规则变了、configure 参数和 device mkspec 需要跟着调整、host 工具依赖多了、以及一堆编译期和链接期的报错。下面这套流程是我自己完整走过一遍之后整理出来的,从判断“到底要不要升级”开始,一直到板子上真的弹出第一个窗口,适合正在维护老 Buildroot 树、又不得不引入新 Qt 版本的嵌入式同行参考,刚接触 Buildroot 包机制的朋友也能顺着看下去。
1. 先搞清楚 Buildroot 为什么总比 Qt 慢半拍
1.1 版本冻结策略与 Qt 发布节奏之间的时间差
Buildroot 的包管理逻辑和发行版很像:每次发版前会做一轮 package bump,然后把版本号冻结,后面只接受修复类补丁,不接受大版本跳跃。这么做的好处是稳定,坏处就是你手上这棵树里 Qt 的版本,基本等于“你当初拉代码那一天 Qt 的最新稳定版”。Buildroot 每年发四次版本(2 月、5 月、8 月、11 月),而 Qt 5 系列的小版本迭代周期大概是半年一次,两者一错位,就会出现“我用的 Buildroot 里 Qt 是 5.6,而外面已经 5.12 了”这种局面。
我第一次遇到这个问题时的第一反应是“那我把整个 Buildroot 换成最新版不就行了”。真去试了才发现,BSP 厂商给的板级支持往往绑死在某个 Buildroot 版本上,内核 patch、u-boot 配置、toolchain 版本都是配套验证过的,换构建系统等于把整个底层重新验证一遍,工期根本不允许。所以在实际项目里,在旧 Buildroot 树上单独升级某一个 package,是非常常见的做法,也是本文要解决的核心场景。
判断值不值得动手,我一般看三个信号:目标版本里有没有你非用不可的 API;当前版本有没有卡住你的已知缺陷;以及这个升级会不会大量牵扯其他包的依赖。三条里有两条中了,就值得干。
1.2 Qt 5.12 到底带来了什么,值不值得为它折腾
Qt 5.12 是 Qt 5 系列里的一个 LTS 分支,官方对它的维护周期给得很长,这对产品化项目来说价值很大——意味着后面几年你不用再为安全修复和小 bug 反复升级整个框架。从工程角度看,5.12 相对 5.9、5.11 有几个实实在在的变化:
- Qt Quick 的渲染路径更成熟,软件渲染后端的可用性比早期版本好很多,对没有 GPU 的板子很关键。
- Qt SerialBus、Qt SerialPort 这些工业场景常用的模块,接口和稳定性都有改进。
- 对 C++11 的使用更加彻底,编译器和标准库的要求相应提高,这一点后面会单独讲。
- Qt 5.12 起,官方发布的源码包统一改名成
*-everywhere-src-*,不再区分 opensource 和 commercial 的包名。这个改动看着不起眼,却是升级过程中第一个会绊倒人的地方。
反过来说,如果你的板子没有 GPU、Qt 只用 Widgets 画几个界面、也不涉及网络和高频刷新,那 5.6 其实也能凑合,升级的收益就没那么大。我的建议是:如果这次不升级后面还要再升,那就一次做到位,别拖。
1.3 三条升级路线的取舍
动手之前先把可选方案摆清楚,别一上来就改文件。Buildroot 里升级一个包,实际有三条路可走,代价和维护性差别很大。
| 方案 | 做法 | 优点 | 代价 |
|---|---|---|---|
| 直接改树内文件 | 修改package/qt5/qt5base/qt5base.mk及相关文件 | 最快,改动直观 | 一旦重新拉取或升级 Buildroot,全部丢失,无法评审 |
| BR2_EXTERNAL 覆盖 | 在外部树里放同名 package 覆盖内置版本 | 与主树解耦,可提交到自己的仓库 | 需要理解 package 覆盖规则和 include 顺序 |
| 全局补丁目录 | 用BR2_GLOBAL_PATCH_DIR打补丁 | 改动最小 | 只能打补丁,改不了版本号和源码包地址 |
我在小项目上会先用第一条路快速验证可行性,把问题摸清楚;确认能跑通之后,再改造成第二条路的形态提交进项目仓库。第三条路基本只适合修复个别编译错误,不适合做大版本迁移。下面主要按第二条路的思路讲,但每一步都会说明在第一条路上怎么对应。
2. 动手前的硬性门槛检查
2.1 交叉工具链的 C++ 版本能不能扛住
Qt 5.12 对编译器的最低要求比 5.6 高了不止一档。我手上的工具链是 GCC 6.4,实测可以编过;如果你还在用 GCC 4.9 甚至更老的版本,编译中途大概率会撞上'numeric_limits' is not a member of 'std'这类错误——这不是 Qt 写错了,而是老版本 libstdc++ 的头文件包含关系不够完整,某些本该间接引入的头文件没引入。
遇到这种情况有两条路:一是给 Qt 打补丁,在相关源文件里显式加上#include <limits>;二是把工具链换到 GCC 6 以上。我更倾向后者,因为这类头文件问题通常不止一处,逐个打补丁是无底洞。检查方式很简单,直接在 host 上跑一下交叉编译器:
# 假设交叉编译器前缀是 aarch64-linux-gnu- aarch64-linux-gnu-g++ --version # 确认 C++11 可用 echo 'int main(){auto x=1;return x;}' | aarch64-linux-gnu-g++ -std=c++11 -x c++ - -o /tmp/t && echo OK第二条要检查的是 host 侧工具。Qt 5.12 的 configure 脚本和构建过程中的一部分生成脚本依赖 host 上的 perl 和 python(2.7 系列)。Buildroot 里对应BR2_PACKAGE_HOST_PERL和BR2_PACKAGE_HOST_PYTHON,需要确认它们是开启状态。我在第一次编译时就是卡在 host 缺 python 上,报错信息很隐晦,只说某个脚本执行失败,翻到config.log里才看到python: command not found。
2.2 图形后端先定下来,别等编译完再改
这一步非常关键,因为图形后端决定了 configure 参数,而 configure 参数决定了你要编进去多少东西、编多久。常见选择:
- linuxfb:直接操作
/dev/fb0,无 GPU 依赖,最省事,适合工控屏和简单界面。 - eglfs:走 EGL 和 OpenGL ES,需要板子上有可用的 GPU 驱动,性能和特效最好。
- xcb:需要完整 X11 环境,嵌入式里很少用,除非你要跑桌面应用。
- wayland:Buildroot 老版本里对应支持还不算完善,不建议作为首次迁移的选择。
没有 GPU 的板子用 linuxfb 是常规做法。但这里有个容易被忽略的点:Qt Quick 的默认渲染后端依赖 OpenGL,如果 configure 时加了-no-opengl,Qt Quick 程序跑起来会报渲染相关错误。解决办法是运行时设置QT_QUICK_BACKEND=software走软件渲染,或者在 configure 阶段保留 OpenGL ES 支持。我在一块无 GPU 的板子上就踩过这个,界面能起来但白屏,日志里提示 OpenGL 上下文创建失败,加上那个环境变量才正常。
2.3 源码包怎么喂进去,别让构建卡在下载环节
Qt 的源码包不小,qtbase 单独一个就有几十兆,加上其他子模块动辄几百兆。构建机如果不能直连官方下载地址,make会在下载阶段卡很久然后失败。
Buildroot 给了一个很实用的机制:BR2_PRIMARY_SITE。你可以自己准备一个本地目录或内网文件服务,把 tarball 按 Buildroot 期望的路径放好,然后把这个变量指过去。Buildroot 会先从这里找,找不到再回落到原始地址。tarball 的命名必须和.mk里$(XXX_SOURCE)完全一致,路径也要按BR2_PRIMARY_SITE/<包名>/<文件名>的结构放。
另一个更省事的做法是直接把包丢进dl/目录。Buildroot 下载前会先检查dl/下有没有同名文件,有就直接用。我一般是在能上网的机器上手工下好,校验完 sha256 再拷进构建机,这样构建过程完全离线,也避免了网络抖动导致的中断。
提示:手工放置 tarball 之后,务必同步更新对应的
.hash文件,否则 Buildroot 在make source-check或正式构建时会报 hash mismatch 直接停下。这个报错信息还算清楚,会告诉你期望的哈希和实际算出来的哈希。
3. 用 BR2_EXTERNAL 写一个 Qt 5.12 的覆盖包
3.1 外部树的目录骨架与加载顺序
BR2_EXTERNAL是 Buildroot 提供的外部扩展机制,指向一个目录,里面可以放Config.in、external.mk、external.desc以及自己的package/子目录。它的加载顺序在内部树之前,所以当外部树里出现和内部树同名的 package 时,外部的会覆盖内部的。这条规则是整套方案能成立的基础。
目录结构大致这样:
my-external/ ├── external.desc ├── external.mk ├── Config.in └── package/ └── qt5/ ├── Config.in ├── qt5base/ │ ├── Config.in │ ├── qt5base.mk │ └── qt5base.hash ├── qt5declarative/ │ └── ... └── ...external.desc里写两行就行:
name: MYEXT desc: Project external tree for Qt 5.12external.mk里把 package 目录包含进来:
include $(sort $(wildcard $(BR2_EXTERNAL_MYEXT_PATH)/package/*/*.mk))Config.in里引入各 package 的菜单:
source "$BR2_EXTERNAL_MYEXT_PATH/package/qt5/Config.in"然后在 Buildroot 的.config或者环境变量里指定BR2_EXTERNAL=/path/to/my-external。这一步做完,make menuconfig里看到的 Qt 选项就来自你的外部树了。
这里有个细节值得提醒:不要以为把 qt5base 一个目录搬过去就完事了。Qt5 在 Buildroot 里是一组包,qt5base、qt5declarative、qt5graphicaleffects、qt5serialport、qt5svg等等,它们共享同一个版本号变量QT5BASE_VERSION。如果你只覆盖qt5base,其他包仍然引用主树里的同名变量——好在 Buildroot 会把所有.mk都 include 进来,变量是全局的,而qt5base.mk按字母序排在前面,所以其他包拿到的就是你覆盖后的版本号。这个依赖顺序是隐式的,升级 Buildroot 时要重新确认一次。
3.2 qt5base.mk 里必须改的那几行
打开原版的qt5base.mk,需要改的地方集中在文件头部和 configure 段。我按改动重要性列一下:
QT5BASE_VERSION = 5.12.12 QT5BASE_VERSION_MAJOR = 5.12 QT5BASE_VERSION_MINOR = 12 QT5BASE_SOURCE = qtbase-everywhere-src-$(QT5BASE_VERSION).tar.xz QT5BASE_SITE = https://download.qt.io/official_releases/qt/$(QT5BASE_VERSION_MAJOR)/$(QT5BASE_VERSION) QT5BASE_LICENSE = LGPL-2.1 or GPL-3.0 or Commercial QT5BASE_LICENSE_FILES = LICENSE.LGPLv3 LICENSE.GPLv3最容易翻车的是QT5BASE_SOURCE。5.11 及以前是qtbase-opensource-src-5.11.3.tar.xz,5.12 起变成qtbase-everywhere-src-5.12.12.tar.xz。文件名不改,下载阶段直接 404,而报错信息只会告诉你下载失败,不会提示你名字变了。我第一次就是这么浪费了半个多小时。
QT5BASE_VERSION_MAJOR用于拼下载路径,5.12 的路径是.../qt/5.12/5.12.12/,注意这里是5.12而不是5.12.12,改动时别顺手写错。
hash 文件同步更新:
sha256 <实际的sha256值> qtbase-everywhere-src-5.12.12.tar.xz sha256 <实际的sha256值> LICENSE.LGPLv3 sha256 <实际的sha256值> LICENSE.GPLv3许可证文件的哈希从 tarball 里解出来算一下就行,别偷懒用旧值。
3.3 configure 阶段的参数微调
这一块是最费时间的,因为不同板子、不同后端组合下需要的参数不一样。原版 Buildroot 的 qt5base configure 参数里有几个在 5.12 上需要特别关注:
QT5BASE_CONF_OPTS = \ -prefix /usr \ -hostprefix $(HOST_DIR)/qt5 \ -plugindir /usr/lib/qt/plugins \ -importdir /usr/lib/qt/imports \ -archdatadir /usr/lib/qt \ -sysroot $(STAGING_DIR) \ -no-gcc-sysroot \ -device buildroot \ -device-option CROSS_COMPILE=$(TARGET_CROSS) \ -no-pch \ ...-no-gcc-sysroot这个参数在 5.12 上尤其重要。不加的话,qmake 生成的 Makefile 里会带上--sysroot,在部分工具链上会导致头文件搜索路径混乱,最终报bits/c++config.h: No such file or directory。这个错误看着像工具链坏了,其实只是 sysroot 被重复指定了。
-device buildroot用的是 Buildroot 自己维护的一份 mkspec,落在package/qt5/qt5base/qmake.conf。5.12 里 mkspec 的 include 路径层级和 5.11 相比有细微差别,如果编译时报某个common/*.conf找不到,基本就是这里需要调整。我的做法是对照 5.12 源码包里mkspecs/devices/下任一示例 device 的 qmake.conf 结构,把 Buildroot 这份对齐。
另外 5.12 的 configure 对特性开关的校验更严格了。比如你开了-fontconfig却没有把fontconfig加进依赖,configure 会直接失败并提示前置条件不满足。这类报错其实很好排查,它会把期望的库和实际探测结果都打出来。
3.4 子模块的连带修改
qt5base改完之后,同目录下其他模块通常只需要确认版本变量引用是对的。但有两类要注意:
一类是独立版本号的模块,比如qt5webengine在部分 Buildroot 版本里用自己的版本变量,需要单独看。另一类是依赖关系变化的模块,5.12 里某些模块对qt5base的 configure 特性有额外要求,比如需要 OpenGL 支持。
我的建议是先只编qt5base,把它编过、装进 staging 之后再逐个加模块。一次性全开会让你在面对一堆报错时不知道从哪下手。
# 只编 qt5base,顺便看清楚它拉起了哪些依赖 make qt5base 2>&1 | tee build-qt5base.log提示:
make <pkg>-show-info可以看到这个包的所有元信息,包括版本、依赖、源码地址。改完.mk之后先跑一遍这个,能快速确认你的改动是否被正确解析,比直接开编高效得多。
4. 编译期的典型报错与排查链路
4.1 host qmake 阶段就挂了
Qt 的构建分两段:先在 host 上编出 qmake、moc、rcc、uic 这些工具,再用这些工具去生成目标平台的 Makefile。第一段挂掉的表现通常是 configure 跑到一半停下来,日志里出现Project ERROR: Cannot run compiler 'xxx-g++'。
这个报错的原因百分之九十九是交叉编译器不在 PATH 里,或者-device-option CROSS_COMPILE=指定的前缀和工具链实际前缀对不上。检查方法是手工在 shell 里执行一遍$(TARGET_CROSS)g++ --version,确认能跑通。如果 host 上跑不通,Buildroot 构建环境里自然也跑不通。
还有一种更隐蔽的情况:工具链能跑,但 qmake 探测编译器能力时用的测试程序编译失败。这时候要去翻build/qtbase-5.12.12/config.log,里面记录了每次探测的完整命令行和输出。我遇到过因为工具链的--sysroot默认值和-sysroot参数冲突导致探测全挂的情况,最后就是靠 config.log 里那行编译命令定位的。
4.2 依赖顺序和特性前置条件不满足
Project ERROR: Unknown module(s) in QT: xxx-private这类错误,通常出现在你启用了某个模块但它的依赖没有一起打开的时候。比如开了qt5declarative但qt5base编的时候没带 QML 相关特性。
排查思路是倒着看:先确认模块在 menuconfig 里勾上了,再确认它在.mk里的DEPENDENCIES是不是完整,最后确认被依赖的那个包确实先编完并安装到了 staging。
# 看某个包声明的依赖 grep -n "DEPENDENCIES" package/qt5/qt5declarative/qt5declarative.mk # 确认 staging 里有没有对应的 .pc 和头文件 ls $(STAGING_DIR)/usr/include/qt5/ ls $(STAGING_DIR)/usr/lib/pkgconfig/ | grep Qt54.3 链接阶段的 undefined reference
编译都过了,链接libQt5Gui.so或者你的应用程序时报undefined reference to '__atomic_fetch_add_4',这是老平台上的经典问题。原因是目标架构(尤其是某些 32 位 ARM)没有原生的原子操作指令,需要链接libatomic。
解决办法是在链接参数里加-latomic。在 Buildroot 里可以通过给 qt5base 加一个QMAKE_LFLAGS的覆盖实现,也可以在应用层解决。我更倾向在 qt5base 的 configure 参数里通过QMAKE_LFLAGS+=传进去,这样所有基于这套 Qt 的应用都能受益。
另一类 undefined reference 来自libstdc++和libgcc_s。如果目标系统是纯静态链接的根文件系统,而 Qt 编成了动态库,运行时就会找不到这些库。这种情况下要么把 Qt 编成静态库(-static),要么把libstdc++一起装进 rootfs。Buildroot 里对应的是BR2_TOOLCHAIN_BUILDROOT_CXX和BR2_STATIC_LIBS的组合,需要提前想清楚。
| 报错关键字 | 常见原因 | 处理方向 |
|---|---|---|
bits/c++config.h: No such file | sysroot 被重复指定 | 加-no-gcc-sysroot |
'numeric_limits' is not a member of 'std' | 老 GCC 头文件包含不全 | 升级工具链或打补丁 |
undefined reference to __atomic_* | 目标架构缺原子指令 | 链接-latomic |
Unknown module(s) in QT | 模块依赖未开启 | 检查 DEPENDENCIES 和 menuconfig |
hash mismatch | tarball 换了但 hash 没更新 | 重算并更新.hash |
4.4 编译时间和内存,别在小机器上硬扛
qtbase是这一路最耗资源的包。我在一台 8 核的机器上用-j8编,大概四十分钟到一个小时;如果只给 4G 内存,链接libQt5Gui.so那一步有概率被 OOM killer 干掉,表现是 ld 突然收到 SIGKILL,日志里没有任何有用的错误信息。
应对办法有几个:把并行度降下来(BR2_JLEVEL=4),给构建机加 swap,或者在链接参数里加-Wl,--no-keep-memory降低 ld 的内存占用。我自己是直接加了一块 swap,效果最直接。
还有一个能显著省时间的做法:先把BR2_PACKAGE_QT5BASE_EXAMPLES关掉。示例代码编译占了相当一部分时间,调试阶段完全没必要编。
5. 上板验证:从交叉编译一个 Demo 到窗口真的弹出来
5.1 用 Buildroot 的 qmake 基础设施编自己的程序
Buildroot 内置了qmake-package这套基础设施,写一个应用包非常省事。在自己的外部树里加一个package/myapp/myapp.mk:
MYAPP_VERSION = 1.0 MYAPP_SITE = /path/to/myapp MYAPP_SITE_METHOD = local MYAPP_DEPENDENCIES = qt5base MYAPP_LICENSE = Proprietary MYAPP_LICENSE_FILES = LICENSE define MYAPP_CONFIGURE_CMDS (cd $(@D) && $(HOST_DIR)/qt5/bin/qmake myapp.pro) endef define MYAPP_BUILD_CMDS $(TARGET_MAKE_ENV) $(MAKE) -C $(@D) endef define MYAPP_INSTALL_TARGET_CMDS $(INSTALL) -D -m 0755 $(@D)/myapp $(TARGET_DIR)/usr/bin/myapp endef $(eval $(generic-package))注意这里没有用$(eval $(qmake-package)),因为我更想让你看清底层发生了什么:核心就是用 host 侧编出来的 qmake 去处理.pro文件。如果你想省事,直接用qmake-package基础设施也可以,它会自动帮你处理QMAKE变量和依赖。
myapp.pro里加一行QT += widgets,主函数里创建一个QApplication和一个窗口,就够验证了。
5.2 运行时环境变量,少一个都不行
程序编出来拷到板子上,直接跑大概率起不来。Qt 运行时依赖几个环境变量,缺一个就是一个不同的报错:
export QT_QPA_PLATFORM=linuxfb export QT_QPA_FONTDIR=/usr/share/fonts export QT_PLUGIN_PATH=/usr/lib/qt/plugins export QT_QPA_FB_DRM=0QT_QPA_PLATFORM不设的话,Qt 会尝试自动探测,探测失败就报This application failed to start because no Qt platform plugin could be initialized。这个报错信息其实挺友好,后面通常还会跟着一行 “available platform plugins are: ...”,照着那行选就行。
字体目录不设的话,Widgets 程序能起来,但中文全是方块。解决办法是把字体文件放进/usr/share/fonts,Buildroot 里可以开BR2_PACKAGE_DEJAVU,或者自己往 rootfs 里塞一个思源黑体的子集。
5.3 qt.conf 与部署目录精简
默认情况下 Qt 会去/usr/lib/qt下面找插件和 QML 模块。如果你想把整个 Qt 运行时放到一个独立目录(方便做 OTA 或者多版本共存),在可执行文件同目录放一个qt.conf:
[Paths] Prefix = /opt/qt5 Plugins = plugins Imports = qml Qml2Imports = qml Libraries = lib这样 Qt 就只会在/opt/qt5下面找东西,不会去翻系统目录。这个做法在产品化阶段很有用,尤其是同一块板子上要跑两套不同 Qt 版本的时候。
另外值得一提的是,部署时记得把libQt5Core.so.5、libQt5Gui.so.5、libQt5Widgets.so.5以及platforms/libqlinuxfb.so这几样带上。少带 plugins 目录是最常见的疏漏,因为编译机上一切正常,拷到板子上就报平台插件初始化失败。
6. 几个容易被忽略的取舍与后续演进
6.1 静态链接还是动态链接
这个选择在项目初期就要定,后期改起来很痛苦。动态链接的好处是多个应用共享一份 Qt 库,rootfs 占用可控,缺点是部署时容易漏文件、版本冲突难排查。静态链接的好处是部署简单,一个可执行文件扔上去就能跑,缺点是体积大(一个带 Widgets 的程序轻松几十兆)、多个应用重复占用空间、而且 Qt 的部分模块(尤其是涉及插件的)在静态链接下需要显式导入。
我的经验是:单一应用、rootfs 空间紧张,走动态;应用数量少、但部署和升级频繁,走静态。Qt 5.12 对静态链接的支持比早期版本好,但插件加载那套机制还是需要你在Q_IMPORT_PLUGIN上花点功夫。
6.2 OpenSSL 版本和 Qt Network 的匹配
如果你的应用要用 HTTPS,qt5base需要开启-openssl-linked并链接目标系统上的 OpenSSL。这里的坑在于版本匹配:Qt 5.12 早期版本对 OpenSSL 1.1 的支持是逐步完善的,如果系统里的 OpenSSL 版本过新(比如 3.x),编译能过但运行时会报 SSL 握手失败。
稳妥的搭配是把系统 OpenSSL 控制在 1.1.1 系列,并在 Buildroot 里同时打开BR2_PACKAGE_OPENSSL和 Qt 对应的特性开关。真要上 OpenSSL 3.x,建议先跑一个最小的 HTTPS 请求验证,别等到业务代码写完才发现握手不过。
6.3 给下一次升级留好退路
这次升到 5.12 之后,下一次大概率是 5.15。既然已经用BR2_EXTERNAL把 Qt 的 package 抽出来了,就把版本号相关的改动都收敛到.mk头部和 hash 文件里,configure 参数里凡是和版本强相关的,加注释说明为什么这么写。这样下次升级时,改动面能控制在很小的范围。
我在项目里还额外做了一件事:把每次升级踩到的报错和解决办法记在一个PORTING.md里,放在外部树根目录。看起来是个小事,但半年后再回来升级,或者同事接手的时候,这份记录能省掉大量重复排查。嵌入式的这类版本迁移工作,最贵的成本从来不是改代码的那几个小时,而是重新把上下文捡起来的时间。
再补一个实操上的小技巧:升级前先make qt5base-dirclean清干净,不要指望增量编译能过关。Qt 的构建系统对配置变更的感知并不可靠,残留的旧 Makefile 会导致各种诡异的链接错误,清干净重编反而更快。