ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

buildroot下Qt5报Unknown module serialport?一文彻底搞懂qmake模块机制

buildroot下Qt5报Unknown module serialport?一文彻底搞懂qmake模块机制 用 buildroot 折腾嵌入式 Linux跑完内核、文件系统终于轮到自己的 Qt 界面上板了结果 qmake 一执行终端里甩出来一行Project ERROR: Unknown module(s) in QT: serialport这个场景我实在太熟了。全志 T113、瑞芯微、IMX6不管哪个平台只要是在 buildroot 环境里编 Qt5几乎每个人都会踩一次这个坑。如果你看到的报错里 serialport 换成了 charts、multimedia、svg、websockets那恭喜你踩的是同一个坑的不同变体。这篇文章从编译原理的层面把这个报错拆开讲适合用过 buildroot 但还没完全搞懂 Qt/qmake 内部机制的人。看完你不仅能解决当前报错以后遇到任何 Unknown module(s) 都能自己定位不用再满世界搜帖子。1. 先把报错现场还原一遍它发生在哪一步qmake在做什么1.1 这个报错到底长什么样我在调试一块 ARM 开发板时工程里用了 Qt 的串口模块.pro 文件里写了QT core gui serialport然后在 buildroot 环境里执行$ /home/user/buildroot/output/host/usr/bin/qmake myapp.pro终端直接给你一个红色高亮的Project ERROR: Unknown module(s) in QT: serialport有些时候 qmake 还会顺带把整个 QT 变量打出来比如Project ERROR: Unknown module(s) in QT: serialport注意这个报错发生在 qmake 解析 .pro 文件的阶段还没有进入 C 编译阶段。也就是说跟 main.cpp 里的#include QSerialPort是否成功没有关系qmake 在还没开始编译源码之前就已经拒绝干活了。搞清楚这一点排查范围就可以缩得很小问题一定出在qmake 环境和Qt 模块配置这两块里。1.2 qmake解析QT变量时的查找机制qmake 在处理QT serialport这行代码时不是去系统里随便找找有没有 libQt5SerialPort.so 就完事了。它有一套自己的模块检索逻辑大致是这样的读取 .pro 文件里的QT变量把 serialport 当作一个模块名。去 Qt 安装目录下的mkspecs/modules/文件夹里找qt_lib_serialport.pri这个文件。找到就读取这个 .pri 文件里面记录的 include 路径、库路径、宏定义等信息然后生成 Makefile。找不到对应的 .pri 文件立即报Unknown module(s) in QT整个 qmake 流程终止。你可以把 qmake 想象成一个采购员拿着你的购物清单QT serialport去仓库找货架mkspecs/modules。货架上没这个货采购员就直接把清单打回根本不会管你楼下的门店里是不是其实有货。这就解释了为什么有时候你在 target 目录里明明看到了libQt5SerialPort.soqmake 依然报错——因为它认的是货架上的 .pri 文件不是仓库里的 .so 库文件。这个机制很关键后面的所有排查其实都是围绕它展开的。2. 排查第一步你用的qmake到底是不是buildroot的那一个2.1 用which和qmake -query验证身份很多人在这一步会犯一个低级错误明明 buildroot 里把 Qt 编好了但执行 qmake 时调用的却是宿主机的 qmake。这种张冠李戴会让整个排查方向彻底跑偏。先执行下面两条命令$ which qmake $ qmake -v如果输出的是/usr/bin/qmake或者/usr/lib/qt5/bin/qmake并且qmake -v显示 Qt 5.15.2 之类的版本那就要警惕了——你很可能在用 Linux 发行版自带的 Qt而不是 buildroot 编译出来的 Qt。再执行$ qmake -query重点看这几项QT_SYSROOT是否指向 buildroot 的 staging 目录QT_INSTALL_PREFIX是否指向宿主机路径QT_INSTALL_LIBS是否指向 /usr/lib 下的宿主机库QT_INSTALL_HEADERS同上是否为宿主机头文件如果这些路径全是宿主机那套qmake 自然去宿主机的mkspecs/modules/里找模块。宿主机上如果没装 libqt5serialport5-dev那它确实不认识 serialport。就算宿主机认识它找到的也是 x86 架构的 Qt 库你交叉编译根本没法用。所以第一步一定是确认你用的就是 buildroot 的 qmake最保险的办法是直接使用绝对路径。2.2 buildroot里qmake的标准位置buildroot 编译完 Qt5 相关包后qmake 一般位于output/host/usr/bin/qmake我用过的 buildroot 2021.x、2023.x 版本基本都在这个路径。有些版本里它可能是一个符号链接实际指向../lib/qt5/bin/qmake这不影响使用。交叉编译工具链则位于output/host/bin/arm-buildroot-linux-gnueabihf-gcc不同平台前缀不同比如全志 T113 用 arm-buildroot-linux-gnueabihf 或 arm-buildroot-linux-uclibcgnueabihf具体看你的 buildroot 配置。不确定时执行$ ls output/host/bin/*gcc看一眼真实的前缀就行。正确用法是直接使用绝对路径调用 qmake不要靠 PATH 环境变量去找这样才能排除掉宿主机 qmake 的干扰。2.3 PATH和QMAKESPEC两个环境变量的坑如果说 qmake 路径是明面上的坑那环境变量就是暗处的坑。先说 PATH。有些人会把output/host/bin加进 PATH这个本身没问题但如果宿主机的 qmake 路径排在前头系统还是会优先用宿主机版本。所以即使你添加了 PATH调用时也建议明确使用output/host/usr/bin/qmake。再说 QMAKESPEC。如果你之前折腾过别的交叉编译工程可能在环境里设置过export QMAKESPEC/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g一旦设置了 QMAKESPECqmake 就会优先使用它指定的 mkspecs而不是 buildroot 里编译 Qt 时对应的那套。这样即使 qmake 路径正确它读取的配置文件仍然可能是宿主机架构的最终编译出来的 Makefile 会一团糟。排查 QMAKESPEC 是否被设置$ echo $QMAKESPEC如果输出非空建议先unset QMAKESPEC再重新执行 qmake。3. 最核心的原因buildroot根本没编译你要的QT模块3.1 到menuconfig里把模块勾上排除了 qmake 用错的问题之后接下来要面对的就是最常见的情况buildroot 确实编译了 Qt 基础库但没编译你需要的那个模块。比如默认情况下 buildroot 的 qt5 配置core、gui、network 这些基础模块大概率都在但 serialport、charts、multimedia 这种独立模块通常默认不开启。这跟你在 Ubuntu 上装 Qt 时还需要单独装 libqt5serialport5-dev 是一个道理。解决办法很直接进入 buildroot 配置界面把模块勾上。$ cd buildroot $ make menuconfig菜单路径是Target packages --- Graphic libraries and applications --- Qt --- [*] qt5base [*] qt5serialport [*] qt5charts ...不同 buildroot 版本的菜单位置略有差异但基本都在Graphic libraries and applications下的 Qt 子菜单里。勾选完成后保存退出重新执行$ makebuildroot 会自动增量编译新勾选的模块然后把库打包进 target 目录。整个过程不需要从零开始但 Qt 相关的包如果配置变化较大有时会触发 qt5base 重建这个时间会比较长做好心理准备。3.2 模块、.pro写法、buildroot配置项对照表我整理了一份常用对照表遇到 Unknown module(s) 时直接查表定位Qt 模块.pro 文件里的写法buildroot 配置项对应的库核心QT coreBR2_PACKAGE_QT5BASElibQt5Core图形QT guiBR2_PACKAGE_QT5BASElibQt5Gui窗口部件QT widgetsBR2_PACKAGE_QT5BASElibQt5Widgets网络QT networkBR2_PACKAGE_QT5BASElibQt5Network串口QT serialportBR2_PACKAGE_QT5SERIALPORTlibQt5SerialPort图表QT chartsBR2_PACKAGE_QT5CHARTSlibQt5Charts多媒体QT multimediaBR2_PACKAGE_QT5MULTIMEDIAlibQt5MultimediaSVGQT svgBR2_PACKAGE_QT5SVGlibQt5SvgWebSocketQT websocketsBR2_PACKAGE_QT5WEBSOCKETSlibQt5WebSockets定位QT positioningBR2_PACKAGE_QT5LOCATIONlibQt5Positioning快速控件2QT quickcontrols2BR2_PACKAGE_QT5QUICKCONTROLS2libQt5QuickControls2注意模块名必须全小写。有人习惯写QT SerialPort或QT Networkqmake 的模块查找是大小写敏感的qt_lib_serialport.pri对应的是小写serialport写成SerialPort一样会报 Unknown module(s)。3.3 重新编译时的增量策略和耗时提示如果只是新增了 qt5serialport 这种独立模块重新 make 通常只会编译这个包和它的依赖不会全量重编。但如果改动的是 qt5base 的配置选项比如启用了 widgets、network 等基础功能buildroot 可能触发 qt5base 的重编译那时候你就得等一会儿了。这里有一个小技巧只想单独编译某个 Qt 模块时可以指定包名$ make qt5serialport这比直接 make 整个工程快得多。但不建议跳过依赖检查除非你非常确定依赖关系没问题。另外如果你是团队协作或者经常调整 buildroot 配置强烈建议把改动固化到 defconfig 里。比如在自定义的 defconfig 文件中加入BR2_PACKAGE_QT5SERIALPORTy这样下次别人拉取代码重新构建时就不会再一次次地进入 menuconfig 手动勾选了。4. 再往底层挖一层库文件在qmake照样不认识你4.1 qmake认的是mkspecs/modules里的.pri文件前面说了qmake 判断模块是否可用靠的是mkspecs/modules/下的 .pri 文件。这个目录的位置在 buildroot 环境里通常是output/host/usr/lib/qt5/mkspecs/modules/或者output/staging/usr/lib/qt5/mkspecs/modules/如果你勾选了 qt5serialport 并重新 make 成功这个目录下应该能看到qt_lib_serialport.pri如果这个文件不存在那不管 .so 库文件在不在qmake 都会报 Unknown module(s)。这就是我之前说的货架理论——qmake 只认货架上的 .pri。为什么 .pri 这么重要因为它里面不仅记录了模块名还记录了头文件路径、库文件路径、编译宏等一整套信息。比如 qt_lib_serialport.pri 里大致会有这样的内容QT.serialport.VERSION 5.15.2 QT.serialport.MODULE_DEPENDS core QT.serialport.INCLUDES $$QT_MODULE_INCLUDE_BASE/QtSerialPort QT.serialport.LIBS -lQt5SerialPortqmake 拿到这些信息后才能正确地为你的 .pro 文件生成编译和链接规则。没有 .pri 文件qmake 等于没有模块的档案自然不认识它。4.2 host、staging、target三个目录各管什么buildroot 编译完 Qt 之后产物会分散在三个目录里很多人搞不清它们的区别这里捋一下目录用途在我们的问题中的角色output/host存放交叉编译工具链、qmake、mkspecs、host端工具qmake 在这里运行时读取的 .pri 也在这里找output/staging对外部编译应用呈现的系统根相当于 sysroot包含了头文件和库的符号链接交叉编译时依赖它output/target最终打包到板子 rootfs 的内容实际运行板子时用的库文件在这里外部编译 Qt 应用时qmake 依赖的是 host 里的 mkspecs 和 staging 里的头文件、库文件。target 目录是给板子运行用的外部编译阶段基本不参与。所以排查的时候不能只盯着 target 目录看。target 里有libQt5SerialPort.so只能说明板子上的 rootfs 已经有这个库了但 host 的 mkspecs/modules 里如果没有对应的 .pristaging 里如果没有头文件你的编译环境依然是残缺的。4.3 一行命令确认模块是否完整我常用的确认方法是直接看 staging 目录里模块的头文件和库是否齐全。比如检查 serialport# 检查头文件 $ ls output/staging/usr/include/qt5/QtSerialPort/ qserialport.h qserialportinfo.h QtSerialPort ... # 检查库文件 $ ls output/staging/usr/lib/libQt5SerialPort* libQt5SerialPort.so libQt5SerialPort.so.5 libQt5SerialPort.so.5.15.2如果头文件目录不存在或者库文件是空的说明 buildroot 编译这个模块时出了问题或者根本没编译。另一种可能是你勾选了配置但 make 过程出过错这时可以强制重编$ make qt5serialport-dirclean $ make qt5serialport注意-dirclean会把这个包的编译目录删掉属于比较暴力的操作但对付改了配置却好像没生效的奇怪问题很有效。5. 外部编译Qt应用的标准姿势照着抄就行5.1 qmake交叉编译命令模板确认 buildroot 配置正确、模块完整之后外部编译 Qt 应用的命令也有讲究。我一般是这样用的$ export BR2_DIR/path/to/your/buildroot $ $BR2_DIR/output/host/usr/bin/qmake \ -spec $BR2_DIR/output/host/usr/lib/qt5/mkspecs/linux-arm-gnueabi-g \ QT_SYSROOT$BR2_DIR/output/staging \ CROSS_COMPILE$BR2_DIR/output/host/bin/arm-buildroot-linux-gnueabihf-参数解释一下-spec指定 mkspecs 路径。具体用哪个名字取决于你的 ARM 平台。buildroot 的 host 目录下一般有多个候选可以用ls $BR2_DIR/output/host/usr/lib/qt5/mkspecs/devices/查看。全志 T113 这类 Cortex-A7 平台通常用linux-arm-gnueabi-g或者linux-arm-gnueabihf-g有些 buildroot 版本还会有专门的linux-imx6-g之类的设备规范。QT_SYSROOT指定交叉编译的 sysroot指向 staging 目录。这一步很关键让 qmake 知道去哪里找头文件和库文件而不是去宿主机 /usr/include 瞎翻。CROSS_COMPILE指定交叉编译工具链前缀。执行完 qmake 之后正常的 make 就行$ make生成的二进制用 file 命令看一眼架构$ file myapp myapp: ELF 32-bit LSB executable, ARM, EABI5 version 1 ...看到 ARM 字样说明交叉编译成功。5.2 .pro文件里的正确模块写法.pro 文件里写模块有几点容易踩坑第一模块名全小写不要写SerialPort、Network这种。qmake 是大小写敏感的。第二QT serialport要放在.pro文件的靠前位置但不要和QT -冲突。有时候从网上抄的代码里会有QT - gui QT serialport如果你不需要去 gui那无所谓但如果你其实需要 gui却被前面的QT - gui减掉了编译时会出现一堆QWidget找不到的错误看起来跟模块缺失很像。第三如果给嵌入式板子开发串口模块通常还会依赖一些系统库比如LIBS -ludev在 buildroot 里如果没选BR2_PACKAGE_LIBUDEV或者 udev链接时会报找不到-ludev。这个不是 qmake 的 Unknown module 错误但经常连着出现。一个比较完整的嵌入式 Qt 串口程序 .pro 文件长这样QT core gui serialport greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET serial_demo TEMPLATE app SOURCES \ main.cpp \ serialwindow.cpp HEADERS \ serialwindow.h LIBS -ludev5.3 实在不行再用的应急方案有时候 buildroot 配置确实没法改比如团队里负责 buildroot 的人不在或者改配置成本太高而模块的库文件明明确实存在于系统里。这种时候可以绕过 qmake 的模块检查在 .pro 文件里手动指定 include 路径和库。举个例子serialport 模块在 staging 里已经存在但 qmake 就是不认你可以在 .pro 里加INCLUDEPATH $$[QT_INSTALL_HEADERS]/QtSerialPort LIBS -L$$[QT_INSTALL_LIBS] -lQt5SerialPort$$[QT_INSTALL_HEADERS]和$$[QT_INSTALL_LIBS]是 qmake 内置的属性变量指向当前 Qt 环境的头文件目录和库目录。加上之后编译器可以直接找到#include QSerialPort并链接到libQt5SerialPort。但这个方法只推荐应急。它跳过了模块的依赖检查万一这个模块还依赖其他模块比如 serialport 依赖 core你还是得手动把依赖链补全。而且如果 staging 里本身就没有这个库文件强行指定路径只会把编译错误从 qmake 阶段拖延到链接阶段报的错更难看。6. 常见问题与排查技巧实录6.1 典型场景速查表这些年下来我遇到过的 Unknown module(s) 报错基本都能归到下面几类场景判断方法解决方案调用了宿主机 qmakewhich qmake 显示 /usr/bin/qmake改用 buildroot 的绝对路径qmake 版本不对qmake -v 显示 Qt6 或宿主机版本确认 buildroot 里配置的是 Qt5使用对应 qmakebuildroot 没启用该模块.config 里没有对应 BR2_PACKAGE_QT5XXXmenuconfig 勾选后重新 make模块已启用但没编译成功staging 里找不到头文件或 .somake 包名-dirclean 后重编.pro 模块名写错检查大小写、拼写改成小写正确模块名qmake 缓存了旧配置改配置后仍报错删除 .qmake.stash、build 目录后重跑QMAKESPEC 环境变量干扰echo $QMAKESPEC 有输出unset QMAKESPECsysroot 没指向 stagingqmake -query 显示宿主机路径加 QT_SYSROOT 参数6.2 两个特别隐蔽的坑第一个坑是 qmake 的缓存文件.qmake.stash。我在 T113 平台上遇到过一种情况buildroot 里明明已经把 qt5serialport 勾上并且重新 make 成功了staging 里也能看到对应的头文件和 .pri 文件但工程目录下执行 qmake 还是报 Unknown module(s)。后来发现是工程目录里残留了一个.qmake.stash文件是之前用错误环境跑 qmake 时生成的里面记录了旧的 Qt 路径。qmake 优先读取这个缓存文件导致新的配置一直没生效。解决办法是删掉缓存文件再重新执行 qmake$ rm -f .qmake.stash .qmake.cache $ rm -rf build # 如果是 shadow build连构建目录一起删 $ $BR2_DIR/output/host/usr/bin/qmake ...第二个坑是 buildroot 的 external tree。有些项目的 buildroot 配置不在默认的.config里而是使用 external tree 方式管理。这种情况下如果 defconfig 文件里没有BR2_PACKAGE_QT5SERIALPORTy就算你这次在 menuconfig 里勾选了下次重新构建时配置会被 defconfig 覆盖回去。所以一定要检查你的配置是保存在哪个 defconfig 文件里并且把新增的模块项同步进去。6.3 我个人的排查顺序习惯踩过的坑多了我现在遇到 Unknown module(s) 基本按下面这个顺序走一遍十分钟内能定位绝大多数问题which qmake和qmake -v确认是 buildroot 的 qmake。qmake -query看 QT_SYSROOT、QT_INSTALL_LIBS 等路径是否指向 buildroot。grep BR2_PACKAGE_QT5 .config看目标模块是否被启用。ls output/staging/usr/include/qt5/看头文件是否真实存在。ls output/host/usr/lib/qt5/mkspecs/modules/ | grep 模块名看 .pri 是否注册。如果上面都正常删掉工程里的 .qmake.stash 和 build 目录重新跑 qmake。这套流程下来大部分问题都被掐死在前面几步了真正需要用应急方案手动指定 INCLUDEPATH 和 LIBS的场合少之又少。最后再分享一个小技巧在 buildroot 环境下调试 Qt 应用时如果板子上运行报error while loading shared libraries: libQt5SerialPort.so.5: cannot open shared object file那就是 target 目录或者实际 rootfs 里缺少这个库。把 staging 里的库同步到板子上或者重新生成镜像就行别又回到编译阶段找问题。分清编译问题、链接问题、运行问题排查速度能快上一大截。
返回列表