先说一句,这个问题我真的见了不知道多少次:板子上用的 buildroot,Qt 环境是交叉编译的,写了个 .pro 文件,里面加了QT += serialport或者QT += charts,然后在宿主机上执行qmake && make,结果 qmake 阶段直接给你来一句:
Project ERROR: Unknown module(s) in QT: serialport然后你就开始怀疑人生——明明宿主机 Qt 环境里用得好好的,怎么一交叉编译就翻车?更诡异的是,有些模块在宿主机上装了,buildroot 的 sysroot 里也有对应的 .so,但 qmake 还是说不认识。
这篇文章就把这个问题彻底拆开,从 qmake 判断模块的机制讲起,到五步定位法,再给 buildroot 里的完整操作流程,最后把上板运行时的配套坑也一并端上来。用的板子是全志 T113 这类常见 ARM 平台,但思路对所有 buildroot + Qt 的嵌入式环境都通用。
1. 先搞清楚 qmake 判断“这个模块存在”的依据
1.1 qmake 不是看文件,而是查“词典”
很多人的第一反应是去 sysroot 里搜,比如:
find output/staging/usr/lib -name "*Qt5SerialPort*"搜出来确实有libQt5SerialPort.so,然后就很困惑:库都在,你 qmake 凭啥说 Unknown module?
这里要理解一个关键点:qmake 在解析QT += serialport时,并不是去文件系统里找有没有libQt5SerialPort.so,而是去查自己的一组“模块记录文件”。你可以把这堆记录文件理解成一本词典,qmake 查词典里有没有serialport这个词条。词条里有库名、头文件路径、依赖关系这些信息。查到了,才认为模块可用,才会在后续生成 Makefile 时加上-lQt5SerialPort和头文件路径;查不到,直接报 Unknown module(s)。
这套记录文件在 Qt5 里的表现形态主要是:
- 模块安装时生成的
.prl文件,一般在<prefix>/lib/下,比如libQt5SerialPort.prl; - 对应的 pkg-config 描述文件,一般在
<prefix>/lib/pkgconfig/下,比如Qt5SerialPort.pc; - 一部分模块信息在
<prefix>/mkspecs/modules/下的.pri文件里。
qmake 编译时会把QT_INSTALL_PREFIX这些路径硬编码进去。PC 上你装 Qt,安装器会把这些“词条”全部写全;buildroot 环境里则完全不一样。
1.2 buildroot 的 Qt 是被拆成一堆独立包的
在 buildroot 里,qt5base只是基础包,包括 Core、Gui、Widgets、Network 这些最常用的模块。serialport、charts、mqtt、multimedia、websockets、webengine这些统统是独立包,每个包都有自己的 Kconfig 开关。
比如:
QT += serialport,对应BR2_PACKAGE_QT5SERIALPORTQT += charts,对应BR2_PACKAGE_QT5CHARTSQT += mqtt,对应BR2_PACKAGE_QT5MQTT
如果对应的 Kconfig 没有勾上,buildroot 根本不会去编译这个模块,那么 sysroot 里连“词典”都没有——虽然可能残留了一些 .so 文件(比如你手动拷进去的),但 qmake 的记录文件不存在,照样报 Unknown module(s)。
换句话说,这个报错的直接原因基本都是同一句话:当前这个 qmake 的模块记录文件里,没有你要用的那个模块。要么是没使能,要么是使能了但没重新编译进去,要么是你用错了 qmake。
1.3 宿主机和 buildroot 的 qmake 混用是最常见的翻车现场
我自己遇到过的案例里,至少有一半是这条:用户用 buildroot 编译了工具链,if 他们的 PATH 里没有 buildroot 的 qmake 路径,Qt Creator 或者终端里which qmake指到了宿主机的/usr/bin/qmake。宿主机 qmake 的词典里也许有 serialport,但那是宿主机的 Qt,不是你目标板的 Qt。于是出现两种情况:
一是宿主机 Qt 版本和目标板 Qt 版本不一致,宿主机上这个词条恰好没有,报错;二是宿主机 qmake 能找到模块,编译也过了,但生成的是 x86 二进制,链接的是宿主机库,拷到 ARM 板子上直接段错误或者“cannot open shared object file”。
所以遇到这个报错,先别急着去 buildroot 里加包,第一件事是确认你正在用的 qmake 到底是谁。
2. 动手定位:五步确认是“没使能模块”还是“用错了 qmake”
2.1 第一步:用 buildroot 自带的方式拿到 qmake
假设你的 buildroot 在/home/xxx/buildroot,目标板配置已经编好。打开终端,先确认 buildroot 有没有生成 qmake 工具:
make qmake这个 target 会触发 buildroot 内部的 Qt qmake 工具准备,执行完以后,qmake 的路径一般在:
output/host/bin/qmake建议直接查看这个路径是否存在,并且看一下版本:
ls -l output/host/bin/qmake output/host/bin/qmake -v如果显示的是类似:
QMake version 3.1 Using Qt version 5.15.2 in /home/xxx/buildroot/output/staging/usr/lib那说明这个 qmake 是 buildroot 内部的,且它认可的 Qt 前缀是 staging 目录,逻辑正确。如果提示“command not found”,说明你压根没编 Qt,回 buildroot 里先确认BR2_PACKAGE_QT5BASE有没有勾选。
2.2 第二步:用 qmake -query 解剖路径
这一步特别有用,它能直接暴露 qmake 认的安装路径和系统 qmake 认的路径差异:
output/host/bin/qmake -query重点看这几个值:
QT_INSTALL_PREFIX:/home/xxx/buildroot/output/staging/usr QT_INSTALL_LIBS:/home/xxx/buildroot/output/staging/usr/lib QT_INSTALL_HEADERS:/home/xxx/buildroot/output/staging/usr/include/qt5 QT_INSTALL_ARCHDATA:/home/xxx/buildroot/output/staging/usr/lib/qt5如果QT_INSTALL_PREFIX是/usr这种,说明你执行的其实是宿主机的 qmake,不是 buildroot 的。如果前缀是 buildroot 的 staging 路径,名词条就在这个路径下面找。
2.3 第三步:在 sysroot 里搜模块的“词典”
确认 qmake 前缀没问题后,去 staging 里搜你想要的那个模块,是否带上了 qmake 能识别的记录文件。
比如查 serialport:
find output/staging/usr/lib -name "*Qt5SerialPort*" find output/staging/usr/mkspecs/modules -name "*serialport*"正常情况下应该看到类似:
output/staging/usr/lib/libQt5SerialPort.so output/staging/usr/lib/libQt5SerialPort.so.5 output/staging/usr/lib/libQt5SerialPort.so.5.15.2 output/staging/usr/lib/libQt5SerialPort.prl output/staging/usr/lib/pkgconfig/Qt5SerialPort.pc output/staging/usr/mkspecs/modules/qt_lib_serialport.pri其中.prl、.pc、.pri就是 qmake 查的“词典”;如果只有 .so,没有 .prl 和 .pri,qmake 照样不认识。
2.4 第四步:查 buildroot 的 .config 确认模块开关
直接看配置:
grep -i serialport .config如果输出有:
BR2_PACKAGE_QT5SERIALPORT=y说明 buildroot 配置里已经开了这个模块。如果 grep 不到,或者显示# BR2_PACKAGE_QT5SERIALPORT is not set,那就是模块压根没使能。到这里,报错的直接原因基本就锁定了。
2.5 第五步:用一个小 .pro 让 qmake 自己告诉你答案
为了快速确认,你可以建一个临时目录,写一个只有几行的 .pro 文件:
QT += core greaterThan(QT_MAJOR_VERSION, 4): QT += widgets qtHaveModule(serialport) { message("serialport module is available") } else { error("serialport module is NOT available") }然后:
output/host/bin/qmake如果 qmake 还报 Unknown module(s) in QT: serialport,说明词条完全没有;如果报 “module is NOT available”,说明词条不完整或模块编译有问题。如果直接打出 “serialport module is available”,那 .pro 里加QT += serialport之后编译就不该报这个错——除非你的工程文件里写错了模块名。
3. buildroot 里真正让模块“可用”的完整操作流程
3.1 menuconfig 里使能 Qt 模块
前面的定位如果确认是“没使能模块”,那就回到 buildroot 根目录:
make menuconfig进入路径:
Target packages → Graphic libraries and applications (graphic/text) → Qt5在 Qt5 的菜单树里找到你需要的模块。命名一般很清楚,比如qt5serialport、qt5charts、qt5mqtt、qt5multimedia、qt5websockets等。用空格键把它勾上。
这里给一张我常用的映射表,方便你对照:
| .pro 里写的模块名 | buildroot 菜单/Kconfig 选项 | 说明 |
|---|---|---|
QT += serialport | BR2_PACKAGE_QT5SERIALPORT | 串口通信模块,嵌入式必备 |
QT += charts | BR2_PACKAGE_QT5CHARTS | 图表模块,工业 HMI 常用 |
QT += mqtt | BR2_PACKAGE_QT5MQTT | MQTT 通信模块 |
QT += multimedia | BR2_PACKAGE_QT5MULTIMEDIA | 多媒体模块,依赖较多 |
QT += websockets | BR2_PACKAGE_QT5WEBSOCKETS | WebSocket 客户端/服务端 |
QT += script | BR2_PACKAGE_QT5SCRIPT | Qt Script 模块 |
QT += xmlpatterns | BR2_PACKAGE_QT5XMLPATTERNS | XML 模式匹配模块 |
QT += location | BR2_PACKAGE_QT5LOCATION | 定位相关模块 |
QT += connectivity | BR2_PACKAGE_QT5CONNECTIVITY | 蓝牙、NFC 等模块 |
注意几个点:
- 模块名大小写敏感,
.pro里写QT += SerialPort是不行的,必须是serialport; - 有些模块依赖其他模块,比如
charts依赖widgets,multimedia依赖一堆底层库,buildroot 会自动处理依赖并勾选,不必手动逐个开; webengine这个模块不建议在嵌入式板子上轻易开,依赖极多,编译极慢,最终镜像也大得吓人。
3.2 增量重建 Qt 模块,而不是全量刷机
配置勾选完以后,很多人会直接make,然后等半小时甚至几个小时。其实 buildroot 支持按包增量编译,效率高得多。
先单独编译目标模块:
make qt5serialport-rebuild如果之前从来没编译过这个包,直接:
make qt5serialportbuildroot 会自动把这个包的依赖、源码下载、编译、安装到 staging 和 target 全流程走完。看到类似:
>>> qt5serialport 5.15.2 Installing to target directory就说明模块已经安装进 target 目录了。
如果你不确定当前 buildroot 里这个包处于什么状态,也可以先 clean 再重建:
make qt5serialport-dirclean make qt5serialportdirclean会把这个包的源码目录整个删掉,重新解压编译,适合包状态异常的情况。
3.3 检查 staging 和 target 的区别,这一步常常被人忽略
buildroot 里有两个目录的作用完全不同:
output/staging/:开发时编程序用的 sysroot,里面有头文件、静态库、.prl、.pc 等开发素材;output/target/:最终根文件系统的内容,里面只有运行时要用的动态库和可执行文件。
交叉编译 Qt 程序时,链接器用的是 staging 里的库;程序拷到板子上运行时,加载的是 target 目录打包进镜像里的库。很多场景下,qmake 编译已经通过,但上板后一运行就报:
error while loading shared libraries: libQt5SerialPort.so.5: cannot open shared object file原因就是 target 目录里根本没有这个库——也就是模块虽然编译进了 staging,但没进最终镜像。
所以 rebuild 完以后,务必确认:
ls -l output/target/usr/lib/libQt5SerialPort*有输出才算真正装进了 rootfs。如果 staging 有、target 没有,常见原因是目标包被标记为“仅用于构建”或依赖关系没刷新,这时回到 buildroot 根目录做一次:
make把依赖和 rootfs 打包流程整体走一遍。这一步会把 target 目录重新整理并生成最终的镜像文件,比如output/images/rootfs.ext4或者rootfs.tar。
3.4 重跑 qmake 和 make 时的两个坑
模块编译好了,回到你的工程目录,一定要把之前 qmake 生成的缓存清掉:
make clean rm -f Makefile output/host/bin/qmake make为什么不直接make?因为 qmake 生成的 Makefile 里已经把“模块缺失”的结论固化进去了,即使 sysroot 里现在有词条了,不重新跑 qmake 它也不会重新解析。直接make可能还会继续报错,甚至报一些奇奇怪怪的链接错误。删掉 Makefile、重新 qmake 是最干净的。
第二个坑是:如果你的工程里原本就集成了宿主机 Qt 的环境,比如.pro里写了硬编码的/usr/lib/x86_64-linux-gnu/这种路径,那就算 buildroot 的 qmake 能识别模块,链接时也可能因为路径优先级问题选错库。所以交叉编译的工程,.pro里尽量只写相对路径和QT +=、CONFIG +=这种语义化配置,不要写死绝对路径。
4. 在全志 T113 这类板子上从编译到上板的完整闭环
4.1 解决“qmake 找不到”的最省事姿势
全志 T113 是 ARM Cortex-A7 双核,buildroot 工具链常见的前缀是arm-buildroot-linux-gnueabihf-。很多人拿到板子后直接敲qmake,系统说 command not found,就开始怀疑工具链没装好。其实不是工具链问题,是 qmake 不在 PATH 里。
最省事的做法是写一个环境脚本,把 buildroot 的 host 工具链加进 PATH。假设 buildroot 路径是/home/xxx/buildroot,脚本内容可以这样:
export BUILDROOT_DIR=/home/xxx/buildroot export PATH=$BUILDROOT_DIR/output/host/bin:$PATH export CROSS_COMPILE=arm-buildroot-linux-gnueabihf- export ARCH=arm以后每次开终端先source env.sh,然后直接:
which qmake就能看到/home/xxx/buildroot/output/host/bin/qmake。如果你的 buildroot 版本生成了output/host/environment-setup脚本,也可以直接 source 它,会把交叉编译器、qmake、pkg-config 这些路径一次性配置好。
4.2 完整交叉编译环境变量,照着抄就行
除了 PATH,还有几个环境变量在编译 Qt 程序时也经常绊人:
export PATH=$BUILDROOT_DIR/output/host/bin:$PATH export CROSS_COMPILE=arm-buildroot-linux-gnueabihf- export SYSROOT=$BUILDROOT_DIR/output/staging export PKG_CONFIG_PATH=$SYSROOT/usr/lib/pkgconfig:$SYSROOT/usr/share/pkgconfig export PKG_CONFIG_SYSROOT_DIR=$SYSROOTPKG_CONFIG_PATH和PKG_CONFIG_SYSROOT_DIR这两条尤其重要。Qt 模块在 buildroot 里都带.pc文件,如果 pkg-config 找不到它们,某些第三方库在探测 Qt 模块时也会失败。比如你后面要编一个依赖 QtSerialPort 的 CMake 工程,CMake 的find_package(Qt5SerialPort)本质上也是走这些.pc或者.cmake文件。
4.3 编译过了,上板运行又炸了?多半是库路径和插件路径问题
程序编好,拷贝到 T113 板子上,运行提示cannot open shared object file,这属于部署问题。先确认动态库依赖:
arm-buildroot-linux-gnueabihf-readelf -d ./your_app | grep NEEDED找到libQt5SerialPort.so.5这类依赖后,在板子上确认/usr/lib下有没有对应文件。没有就把它从output/target/usr/lib/拷贝到板子的/usr/lib/,或者直接把新镜像烧进去。临时验证时可以设置:
export LD_LIBRARY_PATH=/usr/lib:$LD_LIBRARY_PATH但正式产品不要依赖这个变量,直接丢到/usr/lib或者用/etc/ld.so.conf配置才是正路。
更隐蔽的是 Qt 的插件路径问题。常见的现象是程序启动时报:
qt.qpa.plugin: Could not find the Qt platform plugin "linuxfb" in ""这不是模块缺失,而是 Qt 的 QPA platform 插件(比如libqlinuxfb.so)没在预期路径。buildroot 编出来的插件在:
output/target/usr/lib/qt/plugins/对应板子上的路径一般是/usr/lib/qt/plugins。如果启动时找不到,设置:
export QT_QPA_PLATFORM_PLUGIN_PATH=/usr/lib/qt/plugins/platforms export QT_QPA_PLATFORM=linuxfb再运行就能看到日志明显干净很多。嵌入式板子上,linuxfb是最常用的轻量 QPA 后端,适合没有 GPU 或不需要复杂合成场景的界面;如果你的板子有 GPU,比如 Mali 或者 PowerVR,buildroot 里配了 EGLFS 的话,可以用eglfs后端,显示效果更好。
4.4 从零到板端跑起来的最小链路
最后给一条我觉得最稳妥的验证链路。假设你的 buildroot 和工程都在手边:
在 buildroot 里
make menuconfig勾上需要的 Qt 模块,比如qt5serialport;退出后执行
make qt5serialport-rebuild && make,确保模块装进 staging 和 target;确认
output/target/usr/lib/libQt5SerialPort*存在;写一个最小工程,目录下放
main.cpp和test.pro:#include <QCoreApplication> #include <QSerialPort> #include <QDebug> int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QSerialPort port; qDebug() << "Qt SerialPort works"; return 0; }QT += core serialport CONFIG += console CONFIG -= app_bundle TARGET = test_serial TEMPLATE = app SOURCES += main.cppsource环境脚本后执行qmake && make,得到 ARM 版可执行文件test_serial;拷贝到板子,
export QT_QPA_PLATFORM_PLUGIN_PATH=/usr/lib/qt/plugins/platforms,然后跑一下。
全程如果顺利,板子上会打印Qt SerialPort works。这里任意一步卡住,都能明确知道是 buildroot 打包问题、qmake 识别问题还是运行时路径问题。
我在实际项目中还遇到过一个比较隐蔽的情况:模块在 buildroot 里勾了,qt5serialport-rebuild也完成了,但.pc文件没进 staging,导致后来用 CMake 的工程死活 find 不到模块。这种情况直接把output/build/qt5serialport-5.15.2目录删掉重新make qt5serialport就能解决,比-rebuild更彻底。
还有一点个人体会:遇到这种 Unknown module(s) 报错,先别急着改 buildroot 配置。把qmake -query的输出截图存下来,对比一下模块词条是否存在,往往五分钟就能定位。真正花时间的不是编译,而是搞明白 qmake 这种“查词典”的工作方式。把这套机制想通了,以后换任何板子、任何 Qt 模块,排查思路都是一样的。