ARTICLE DETAIL

资讯详情

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

ROS CMake构建报错:catkin与colcon排查笔记

ROS CMake构建报错:catkin与colcon排查笔记 跑ROS的人大概都有过这种经历节点代码自己觉得写得没毛病catkin_make或者colcon build一回车终端开始疯狂刷红字最后停在一行CMake Error上光标一闪一闪等着你。ROS、cmake、报错这三样东西几乎贯穿了从装环境第一天到日常维护的每一天——有人统计过新手在ROS上花掉的时间一半以上不是写算法而是跟构建系统较劲。这篇是我做ROS开发这几年攒下来的一份报错处理笔记边踩边记持续更新。覆盖范围主要是Ubuntu下的ROS 1Melodic、Noetic和ROS 2Humble、Jazzy会顺手带上Windows下CMake命令识别不了、嵌入式交叉编译、以及一些CMake不只服务ROS的跨界场景。它不是一个CMake语法教程也不是官方文档的搬运我写的是看到这行红字下一步该敲什么。目标读者有两类一类是已经能写节点、但一编译就懵的新手另一类是团队里那个专门负责修环境的人别人报错截图发过来你得在十分钟内判断是依赖没装还是缓存脏了。环境类、依赖声明类、链接类、缓存污染类的问题排查路径完全不一样用同一套手法去撞只会白费时间所以下面我会把它们拆开讲。1. 先把CMake报错分层别一上来就改CMakeLists1.1 ROS构建链路上CMake到底站在哪一层很多人对CMake的误解在于以为它是编译器。它其实是个构建脚本生成器你写一份CMakeLists.txtCMake读取它结合系统里已有的环境变量、依赖包配置生成Makefile或者Ninja文件然后才由make/ninja去调用gcc/g真正编译。所以CMake报错绝大多数时候不是代码写错了而是它没能拼出正确的编译命令。在ROS这套体系里这条链路还要多绕两圈。ROS 1用catkin本质是一层CMake宏catkin_package()、add_message_files()这些你在顶层调用catkin_make或catkin build工具会先遍历src下所有包的package.xml按依赖关系排序再逐个进包目录跑CMake。ROS 2换成ament_cmake colconfind_package(catkin ...)变成find_package(ament_cmake ...)消息生成从catkin的宏换成rosidl的接口生成器除此之外的核心逻辑没变。理解这一点很关键ROS构建报错的源头可能在四个完全不同的地方——package.xml声明依赖、CMakeLists.txt怎么用依赖、shell环境变量依赖在哪、以及系统层编译器、库文件是否存在。把它们混成一锅就会陷入改了十遍CMakeLists还是同样报错的死循环。1.2 catkin和colcon两代体系报错长得不一样新手最常见的一个坑是把ROS 1的教程命令照搬到ROS 2上。这两代的报错文案有很明显的特征认出特征就能立刻判断方向我给你整理成一张对照表报错特征属于哪一代通常是什么问题Could not find a package configuration file provided by xxx两代都有依赖包没装或CMake找不到配置文件catkin_package() ... must be called beforeROS 1宏调用顺序错了find_package(ament_cmake REQUIRED)失败ROS 2没source/opt/ros/distro/setup.bashrosidl_generate_interfaces() the first argument must be a valid target nameROS 2写消息包时参数写错或包名不合法Project xxx specifies /include/xxx as an include dir, which is not foundROS 1catkin_package里声明的INCLUDE_DIRS路径不存在Error(s) in package xxx: The manifest must not contain the following tags: run_dependROS 2package.xml用了ROS 1的format 1标签这张表只是入口后面每一类我都会展开。我的习惯是报错先看关键词判断它属于找不到东西还是顺序不对还是链接失败这三类的处理动作差别极大。找不到东西是环境问题去翻apt list和rosdep顺序不对是文件内容问题去翻CMakeLists.txt链接失败是二进制问题去翻ldd和nm。1.3 动手之前先确认的三条基线我踩过最冤的一类坑是花了两小时研究CMake语法最后发现是环境没source。所以在动手改任何文件之前先花三十秒确认三条基线ROS环境是否已sourceecho $ROS_DISTRO应该输出你的发行版名noetic/humble输出为空说明没sourceROS 2更要命colcon build找不到rclcpp十有八九就是这个原因。当前工作空间是否在覆盖链上echo $CMAKE_PREFIX_PATH看看里面有哪些工作空间的install或devel目录。如果你之前装过一个老版本的包它的路径排在前面CMake就会优先找到那个老版本。CMake缓存是否干净build/目录里有个CMakeCache.txt里面记着上次配置时的绝对路径、编译器版本。你换了路径、换了Python、换了gcc缓存里的旧值就成了定时炸弹。这也是为什么网上那么多人说删掉build重编就好了——他们不是玄学是踩过。这三条确认完再去读报错效率完全不一样。我一般会建议在动手前把这条命令当习惯cd ~/your_ws rm -rf build devel install # ROS 1 # 或者 rm -rf build install log # ROS 2注意删之前确认src里有你的源码并且所有改动已经保存。删build/devel/install是安全的删src是不可逆的。另外构建中的工作空间不要一边catkin build一边删会留下半截的缓存。2. 环境层的报错cmake命令找不到、版本不对、多版本打架2.1 cmake不是内部或外部命令与Linux下的command not found先说Windows上的那个经典报错我见过太多人卡在这cmake : 无法将cmake项识别为 cmdlet、函数、脚本文件或可运行程序的名称。这不是CMake坏了是Windows的PATH里没有CMake的bin目录。用官方安装包安装时安装向导里有一个Add CMake to the system PATH for all users / current user的勾选项很多人一路Next根本没注意勾没打上装完就在PowerShell里报错。补救方法有两种一是重新运行安装程序选择Add to PATH二是手动把C:\Program Files\CMake\bin加进系统环境变量加完之后必须重开一个新的终端窗口老窗口的环境变量不会刷新。我见过有人改完PATH在原窗口里反复敲cmake --version然后怀疑人生就是这个原因。Linux下的command not found更简单装就完了sudo apt update sudo apt install -y cmake build-essential cmake --version但如果是在一个刚装好的Ubuntu上apt update本身就失败或者装完CMake还是用不了那就得回头看软件源。这里顺便提一句社区里流传的那些一键安装脚本比如鱼香ROS那套确实能省掉配源、装rosdep、写环境变量这些琐事对纯新手很友好。但它省事的同时也替你改了不少系统配置比如替换APT源、批量装依赖一旦脚本中途失败或者源不匹配留下的半成品环境排查起来反而更麻烦。我的建议是用可以但用完之后记得自己确认cmake --version、echo $ROS_DISTRO、rosdep --version这三个命令的返回值别装完就以为万事大吉。2.2 CMake版本和ROS发行版是有对应关系的CMake版本不是越新越好。ROS的很多CMake宏是针对特定年代的CMake写的版本跨太大就会出现兼容性问题。我来把常见组合列一下这张表建议存下来Ubuntu版本ROS发行版系统自带CMake版本备注18.04Melodic3.10.2老工程多注意cmake_minimum_required写的2.820.04Noetic / Foxy3.16.3ROS 1的最后一个长期版本22.04Humble3.22.1目前装机量最大的ROS 2版本24.04Jazzy3.28.x新项目建议直接上这个组合两种典型报错的区分很重要。第一种是要求过高CMake Error at CMakeLists.txt:1 (cmake_minimum_required): CMake 3.20 or higher is required. You are running version 3.10.2这说明包作者写了一个比你系统更新的最低版本要求解决方式要么升级CMake要么把这一行改回你能满足的版本但要确认作者没有真的用到3.20的新特性否则改了也编不过。第二种是要求过低带来的策略告警——在CMake 3.30之后对cmake_minimum_required(VERSION 2.8)这种老写法会打出兼容性告警继续升级后可能直接变成错误。临时绕过的办法是配置时传一个策略下限colcon build --cmake-args -DCMAKE_POLICY_VERSION_MINIMUM3.5 # ROS 1 下 catkin_make -DCMAKE_POLICY_VERSION_MINIMUM3.5这只是权宜之计正规做法还是把老工程的cmake_minimum_required抬到3.5以上并顺手清掉不再需要的旧策略代码。2.3 想升级或卸载CMake别用apt硬怼用apt install cmake装的是发行版仓库里的版本想升到更新的版本用apt往往会把一堆系统包连带升级风险不小。我比较推荐的是官方预编译包解压到/opt然后用软链接或者PATH管理wget https://github.com/Kitware/CMake/releases/download/v3.28.1/cmake-3.28.1-linux-x86_64.tar.gz sudo tar -zxvf cmake-3.28.1-linux-x86_64.tar.gz -C /opt sudo ln -sf /opt/cmake-3.28.1-linux-x86_64/bin/cmake /usr/local/bin/cmake cmake --version关键点在于/usr/local/bin在PATH里的优先级高于/usr/bin所以这样不会破坏系统自带的CMake出问题把软链接删掉就能回退。这比apt remove cmake再装新版的粗暴做法干净得多——后者会连带卸掉依赖cmake的一批包某些情况下连构建工具链都跟着受损。如果你用多个版本做交叉验证可以用update-alternatives管理或者干脆用conda虚拟环境隔离。但要提醒一句用conda装的cmake在ROS里很容易出问题因为它会连带把Python解释器路径也改掉CMake配置时找到的是conda的Python链接出来的库和系统Python不匹配。我在这个坑里待过一下午最后靠conda deactivate解决的。3. 依赖声明类报错package.xml和CMakeLists.txt的两处一致3.1 依赖必须写两遍这是设计而不是冗余新手最想不通的一点我在package.xml里写了dependroscpp/depend为什么还要在CMakeLists.txt里写find_package(catkin REQUIRED COMPONENTS roscpp)这不是重复劳动吗不是。这两个文件服务的是两个不同的工具链。package.xml是给包管理器看的——rosdep、colcon、catkin build靠它计算依赖顺序、决定先编谁后编谁CMakeLists.txt是给构建系统看的——CMake靠find_package去定位头文件路径和库文件路径生成-I和-l参数。只写前者构建顺序对了但编译器不知道头文件在哪只写后者单包编译能过但整体构建时顺序可能错乱。ROS 2里逻辑一样只是名字变了package.xml里写dependrclcpp/dependCMakeLists.txt里写find_package(rclcpp REQUIRED)加ament_target_dependencies(my_node rclcpp)。ROS 2的package.xml还多了一层格式版本要求。如果你从ROS 1的包里直接复制了一份package.xml过来很可能带着run_depend这种format 1的标签构建时会直接报Error(s) in package xxx: The manifest (with format version 2) must not contain the following tags: run_depend解决就是把run_depend换成exec_depend把build_depend和run_depend该合并的合并成depend。这一步看着无聊但它是跨版本迁移时最容易忽略的一环。3.2 CMakeLists.txt的语句顺序顺序错了一种都编不过CMakeLists.txt是有隐含顺序要求的把它当成一份有执行次序的脚本而不是配置文件理解起来会顺很多。一份标准的ROS 1包大致是这个顺序cmake_minimum_required(VERSION 3.0.2) project(my_pkg) find_package(catkin REQUIRED COMPONENTS roscpp std_msgs message_generation ) add_message_files(FILES MyMsg.msg) generate_messages(DEPENDENCIES std_msgs) catkin_package( INCLUDE_DIRS include LIBRARIES my_pkg CATKIN_DEPENDS roscpp std_msgs ) include_directories(include ${catkin_INCLUDE_DIRS}) add_executable(my_node src/my_node.cpp) target_link_libraries(my_node ${catkin_LIBRARIES}) add_dependencies(my_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})我逐个说下关键点的意图。find_package必须在最前面因为它要把依赖包的变量catkin_INCLUDE_DIRS、catkin_LIBRARIES导入进来后面的语句才有东西可用。catkin_package()必须放在add_executable之前它的作用是向外部导出你这个包的能力如果顺序反了其他包依赖你的时候拿不到配置信息。generate_messages()要在catkin_package()之前因为它决定了导出的消息类型。ROS 2的顺序略有不同消息包和普通包几乎是两套写法。普通节点包是find_package(ament_cmake REQUIRED)→find_package(rclcpp REQUIRED)→add_executable→ament_target_dependencies→install(TARGETS ...)消息包则要用rosidl_generate_interfaces(${PROJECT_NAME} msg/MyMsg.msg)而且接口生成必须在任何依赖它的target之前完成。这里有个高频错误是ROS 2新手把rosidl_generate_interfaces写在add_executable后面结果编译时报找不到生成的.hpp——因为生成任务还没注册target就已经开始找头文件了。3.3 自定义消息引发的头文件找不到报错长这样fatal error: my_pkg/MyMsg.h: No such file or directory这个报错有几种可能排查顺序我一般是这样第一看消息文件是不是真的被注册了。ROS 1要确认add_message_files里列出了这个.msgROS 2要确认rosidl_generate_interfaces的参数里包含它。文件名写错一个字母生成的头文件名字就完全不一样。第二看依赖包有没有在package.xml里声明。消息编译需要message_generationROS 1的构建期和message_runtime运行期ROS 2需要rosidl_default_generators和rosidl_default_runtime。少写一个生成的头文件不会出现在include路径里。第三看是否加了target依赖。ROS 1里那个看起来又长又反直觉的写法是必需的add_dependencies(my_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})它的含义是我的可执行文件必须在所有消息生成完成之后再编译。${catkin_EXPORTED_TARGETS}管的是别人定义的消息${${PROJECT_NAME}_EXPORTED_TARGETS}管的是自己定义的消息。少写这一行并行编译时就会出现有时能过有时不过的诡异现象——因为编译顺序在并行环境下是不确定的这也是为什么很多人的包在自己机器上好好的换台机器就崩。4. 工作空间、链接与交叉编译的深水区4.1 overlay冲突你引用的可能不是你写的那个包ROS支持多工作空间叠加底层是/opt/ros/distro上面可以叠好几个自己建的overlay。这个机制很灵活但也是改了代码不生效的头号元凶。判断方法很简单rospack find my_pkg # ROS 1 ros2 pkg prefix my_pkg # ROS 2输出的路径如果不是你当前工作空间的devel或install目录说明你引用的确实不是刚改的那个。原因通常是.bashrc里source了好几个工作空间顺序靠后的会覆盖靠前的。解决方法有两种一是把当前工作空间的source挪到最后二是每次构建前手动source一次source /opt/ros/humble/setup.bash source ~/my_ws/install/setup.bash提示如果两个工作空间里有同名包CMAKE_PREFIX_PATH的顺序决定用哪个。用echo $CMAKE_PREFIX_PATH | tr : \n拆开看一眼就能看出优先级。还有一个反直觉的现象你明明删掉了一个包编译却还在报它的错。这是残留的install目录在作祟。ROS 2的install目录里带着编译期生成的CMake配置文件光删build是不够的三个目录要一起删。4.2 链接阶段的报错undefined reference和cannot find -l前面几类都是配置问题这一类是真正的链接问题通常发生在最后一步。两个典型文案/usr/bin/ld: cannot find -lmy_lib这个意思是链接器在标准库路径里找不到名为libmy_lib.so或libmy_lib.a的文件。常见原因是依赖库只装了开发包的运行时版本或者你自己编的库没有install。查询和修复ldconfig -p | grep my_lib # 系统里到底有没有这个库 find / -name libmy_lib* 2/dev/null # 全盘找一下undefined reference to SomeClass::method()这个更微妙说明库找到了但里面没有这个符号。原因通常是ABI不兼容或者版本不匹配——比如你链接的是一个头文件版本3.0、库文件版本2.0的组合。这种情况我会用nm直接查符号nm -D -C /path/to/libmy_lib.so | grep SomeClass如果符号确实不在里面那就不是链接参数的问题而是你链接的库本身就不是想要的那个。第三方库的这类问题八成能通过rosdep解决rosdep install --from-paths src --ignore-src -r -y这条命令的作用是扫描src下所有package.xml里声明的系统依赖自动用apt装上对应的开发包。它能治好的病比想象中多。唯一的坑是rosdep update本身依赖网络访问网络条件不好时会失败这种情况下社区有预置数据源的替代方案或者手动把缺的包用apt装上效果一样。4.3 交叉编译场景micro-ROS与ESP32的环境隔离这一类报错是ROS 2时代新增的因为越来越多人在做micro-ROS、ESP32这类嵌入式节点主机和目标的构建环境混在一起特别容易出问题。一个典型报错是IDF的环境变量没设置导致CMake里这行失败include($ENV{IDF_PATH}/tools/cmake/project.cmake)原因是IDF_PATH是空的。解决方案看起来很直白——export.sh一下就行了但这里有个顺序陷阱ESP-IDF的导出脚本和ROS的setup脚本会互相干扰尤其是涉及到Python环境和编译器前缀的时候。所以正确做法是用两个独立的终端一个终端只做主机侧的ROS构建另一个终端source $IDF_PATH/export.sh后做固件构建别在同一个shell里来回切。如果确实需要在同一个流程里跑用独立的构建目录和显式的工具链文件隔离cmake -B build-esp32 -DCMAKE_TOOLCHAIN_FILE$IDF_PATH/tools/cmake/toolchain-esp32.cmake ..另一个高频报错是主机的colcon build把目标平台的包也一起编了报出各种架构不匹配。这时候用--packages-select精确指定或者在工作空间里建一个COLCON_IGNORE空文件把嵌入式包排除掉。5. 从别的领域看CMake报错套路其实是通用的5.1 那些不是ROS但同样是CMake的案例我这两年有个体会CMake报错的排查方法论是跨领域的。你只要理解了CMake负责生成构建脚本报错一般是它找不到某个输入这个前提很多看起来完全无关的场景就能用同一套思路处理。比如深度学习框架的编译。有人装Detectron2时报错翻到最后发现是CMake找不到合适的编译器版本或者CUDA架构参数没配对。这和ROS里CMAKE_C_COMPILER is not a full path的报错是同一类问题——工具链的某个输入没有被正确指定。再比如Flutter的Android项目报unable to find suitable Visual Studio toolchain本质上也是构建系统找不到它需要的那套工具链和CMake找不到依赖包是同构的。桌面打包的链条就更典型了。用Electron打Linux的deb包时需要fpmfpm又依赖rpmbuild任何一环缺失报错都是从最外层抛出来的你得顺着链条往下挖。这个链式依赖的排查思路和ROS里rosdep找不到某个包、于是CMake找不到配置文件、于是find_package失败是一模一样的。5.2 嵌入式和桌面端CMake正在变成通用选择还有一个经常被问的问题CMake能不能替代Keil做嵌入式的构建从能力上说CMake从来不负责编译它负责生成构建脚本所以替代Keil的准确说法是用CMake arm-none-eabi-gcc 工具链文件替代Keil的IDE构建。链接脚本、启动文件、烧录步骤都由你自己用命令行串起来比如烧录用JLink的命令行工具构建用CMake生成的makefile。这条路的好处是构建过程可脚本化、可进版本管理、能接CI坏处是初始配置成本高一个toolchain.cmake要调半天。对ROS用户来说这其实是熟悉的领域——我们本来就在用命令行构建只不过把工具链从x86换成了ARM。5.3 为什么CMake的报错总是很绕CMake报错看起来绕根本原因在于它是个两层翻译系统你写的是声明式的脚本它翻译成MakefileMakefile再翻译成编译器命令。报错可能是任何一层抛出来的而CMake的输出又倾向于把调用栈也打出来——CMake Error at /opt/ros/noetic/share/catkin/cmake/catkinConfig.cmake:83 (find_package)这种路径和行号一大堆新手一看就晕。我的处理习惯是只看报错块的第一行和最后一行。第一行告诉你是什么错Error还是Warning最后一行通常告诉你缺什么中间那一大堆是调用栈。抓住缺什么这个信息再顺着前面说的层级往下查效率会高很多。6. 报错速查与三条实操定位手法6.1 常见报错速查表下面这张表是我自己常年备着的遇到报错先扫一眼匹配报错原文节选大概率根因第一步动作cmake: command not found/无法将cmake项识别为...CMake未安装或PATH未包含which cmake检查PATH并重开终端Could not find a package configuration file provided by xxx依赖包未安装或未source先source目标环境再rosdep installProject xxx specifies /include/xxx as an include dir, which is not foundcatkin_package里的路径不存在建目录或修正路径fatal error: xxx/msg/Yyy.h: No such file or directory消息生成顺序问题检查add_dependencies是否补齐c: fatal error: Killed signal terminated program cc1plus并行编译吃满内存被系统杀掉降并行度-j2或--parallel-workers 2/usr/bin/ld: cannot find -lxxx库文件缺失或路径不对ldconfig -p、find定位undefined reference to ...链接到的库版本不对nm -D查符号确认库来源The manifest must not contain the following tags: run_dependpackage.xml格式版本混用改成exec_dependCompatibility with CMake 3.5 will be removed老工程配新CMake抬高cmake_minimum_required或加策略下限那个cc1plus被杀掉的报错值得单独说一句因为它的表象非常吓人——编译到一半突然中断没有任何语法错误提示新手会以为是代码有问题。真实原因是g编译某个大文件时内存占用超过可用内存被系统的OOM机制干掉了。解决办法就是别贪并行度尤其是虚拟机只给了4G内存的时候catkin_make -j2或者colcon build --parallel-workers 2会稳得多。6.2 三步定位法清缓存、读日志、最小复现我把日常的排查流程压缩成三步按顺序走八成的问题能在十分钟内收敛。第一步清缓存重试。别觉得这是重启大法它是有依据的——CMakeCache.txt里存着上次配置的所有绝对路径路径变了、依赖变了缓存就成了错误来源。清理命令是rm -rf build devel installROS 1或rm -rf build install logROS 2。这一步能解决大概三成的莫名其妙问题。第二步读日志文件的最后二十行。终端输出会被截断完整日志才是真相。catkin build 的日志在log/latest_build/pkg/build.make.时间戳.logcolcon 的在log/latest_build/pkg/stderr.log。看的时候用tail -n 50并且专门找第一个error——后面跟着的一大串往往是第一个错误的连锁反应盯着第十个错误分析纯属浪费时间。第三步最小复现。如果整包构建失败把范围缩到单包colcon build --packages-select my_pkg --cmake-clean-cache --event-handlers console_direct catkin build my_pkg --no-deps--cmake-clean-cache会强制重新跑CMake配置console_direct让输出不经缓冲直接打到终端方便边看边改。ROS 1的--no-deps能跳过无关包把问题限制在一个包里。需要看完整的编译命令时加这两个参数colcon build --cmake-args -DCMAKE_VERBOSE_MAKEFILEON -DCMAKE_BUILD_TYPEDebugCMAKE_VERBOSE_MAKEFILEON会把每条g命令行都打出来链接参数里少了哪个-l、头文件路径里少了哪个-I一眼可见。这个参数是我排查链接类问题的第一选择比猜快得多。6.3 几条踩出来的心得比文档管用别在中文路径下编译。这不是危言耸听某些工具链处理非ASCII路径时确实会出问题报错还非常隐晦比如某个字体相关的处理会提示非Unicode TrueType字体的错误让你往完全错误的方向查。工作空间统一放~/ws这种纯英文路径下能省掉一类玄学问题。改完CMakeLists.txt一定要重新配置不能只重新make。有人改了文件直接make发现改动不生效因为CMake没有重新跑配置阶段。touch一下CMakeLists.txt或者加--cmake-clean-cache都行。注意conda和ROS的冲突。如果你装了conda并且默认激活了base环境find_package(Python)找到的会是conda的Python链接出来的.so和系统Python不匹配。症状通常是明明装了的Python包在ROS节点里import不到。最省事的办法是在跑ROS构建前conda deactivate或者在.bashrc里把conda的自动激活关掉。虚拟机内存不够的时候别用高并行度。这个坑我踩过好几次catkin_make -j8在4G内存的虚拟机上必崩而且崩溃方式是编译进程被杀报错信息看起来跟代码毫无关系。给虚拟机的内存和并行度做个匹配2核就-j2别贪。保留一份能跑的环境快照。环境类问题最烦的是改着改着原来能跑的也跑不了了。我现在的习惯是一个工作空间调通之后立刻打一个容器镜像或者至少把apt list --installed导出一份出了问题能快速对比出被改动了什么。这个习惯救过我两次尤其是在同一个系统上折腾多个ROS版本的时候。最后分享一个我最近才养成的小习惯每次遇到新的报错并且解决之后在项目根目录开一个BUILD_NOTES.md把报错原文、根因、解决命令三行记下来。一开始觉得麻烦坚持半年之后发现同一个坑基本不会再踩第二次——因为人对自己写过的字的记忆远比对自己看过的报错靠谱。
返回列表