ROS 2 Lyrical迁移指南:CMake 4.x与rosdep依赖解析实战

发布时间:2026/9/20 5:31:35
ROS 2 Lyrical迁移指南:CMake 4.x与rosdep依赖解析实战
1. 动机为什么第一时间升级到 Lyrical以及升级前必须想清楚的一件事ROS 2 的版本节奏是每年一发2026 年发布的 Lyrical 属于长期支持LTS版本支持周期长达五年。对我这种还在 Humble 和 Iron 上维护一堆老工程的人来说Lyrical 最大的吸引力在于它终于把一套新的编译基础设施固化成了默认选项——包括对 CMake 4.x 的完整适配以及 rosdep 依赖解析流程的大幅调整。但是这里我得先说句实在话升级 LTS 版本并不是你重新装一遍系统、把代码 clone 下来就能顺利colcon build的事。我从 Humble 升级到 Iron 的时候已经踩过一次坑那时候最多是ament_cmake的一些宏定义行为变了而 Lyrical 这一代直接把 CMake 的最低版本要求推到了 3.16部分包甚至要求 4.0 才能编译连带把一堆老 CMakeLists.txt 里潜伏多年的写法问题全炸了出来。所以在动任何编译命令之前先想清楚一个问题你的项目里有没有依赖老版本 ROS 社区包而且这些包可能已经无人维护如果有它们在 Lyrical 环境下的编译指纹大概率和你原来的环境对不上。换句话说这一轮踩坑的主角往往不是你自己的代码而是那些“别人写的、还能跑但已经很久没更新的依赖包”。这篇文章记录的是我在 Lyrical 从零开始搭建编译环境、处理依赖、最终把一个混合了自研包和第三方包的 workspace 完整编译通过的全过程。核心聚焦三块rosdep 的使用逻辑变化、现代 CMake 的破坏性变更、以及 CMake 4.x 引入的兼容性政策。这些内容既适合准备迁移的人提前避雷也适合已经在编译报错里挣扎的人对照排查。2. 环境准备装完 Ubuntu 后第一轮依赖冲突比想象中来得更早2.1 基础依赖安装时的“默认陷阱”不管你是用 Debian 包安装 ROS 2 Lyrical还是从源码编译系统层面的依赖都得先凑齐。官方文档给的指令很标准sudo apt install ros-lyrical-desktop python3-colcon-common-extensions python3-rosdep看着没问题但实际执行完以后我建议你立刻跑下面这两条检查cmake --version python3 -c import ament_index_python; print(ament_index_python.__file__)为什么要做这个检查因为ros-lyrical-desktop会拉进来一整套编译链但它不保证你系统里的 cmake 是它想要的版本。Ubuntu 26.04 的 apt 源默认带的可能是 CMake 3.28而 Lyrical 的很多核心包在编译时会执行cmake_minimum_required(VERSION 3.16)如果你另外安装了更高版本甚至 CMake 4.x多数时候反而没问题——问题恰恰出在你用 apt 升级系统包时cmake 被替换成了某个中间版本导致 ament 的宏和 CMake 的 policy 行为对不上。我这次遇到的现象很典型colcon build时大量包报错错误信息全是Unknown CMake command ament_export_dependencies。排查到最后发现不是 ament_cmake 没装而是工作区里同时存在两个 Python 环境colcon 调用的 ament_cmake 和 ROS 2 真正使用的 ament_cmake 不是同一份。这种问题在 Humble 时代几乎不会出现因为那时候所有东西都通过 apt 统一管理而 Lyrical 的 Python 依赖开始走 pip 安装后污染路径的概率大幅上升。2.2 选择源码编译还是二进制包一个影响后续所有坑走向的分叉口如果你只想用 ROS 2 提供的现成功能比如跑跑 nav2、turtlesim、或者做点上层应用开发直接安装二进制包体验最好省时省力。但如果你跟我一样需要修改核心包源码、做底层传感器驱动适配、或者把老项目的源码迁移过来那源码编译几乎是唯一的选择。源码编译 Lyrical 意味着你要轻度“重编译”一整套 ROS 2 基础环境。官方推荐的 vcstool colcon 流程本身并不复杂麻烦的是依赖解析mkdir -p ~/ros2_lyrical/src cd ~/ros2_lyrical vcs import src ros2.repos rosdep update rosdep install --from-paths src --ignore-src -y这句话看着简单实际执行时 rosdep 很容易卡住。卡住的原因要么是网络问题要么是某个第三方包的package.xml里写了一个已经不存在于当前 Ubuntu 源里的系统依赖包名导致 rosdep 直接报错中断。对于后者先去看那个包的package.xml里依赖的具体版本段再用apt-cache search找替代包名手动在rosdep install命令后追加-r参数暂时跳过它之后单独处理。我个人的建议是如果你的目标只是让自己的业务代码跑起来优先用二进制包源码 overlay 的方式也就是系统装二进制版 Lyrical自己的代码用 colcon 单独编成一个 overlay workspace。这样基础环境由 apt 保证一致性你只需要关注自己代码的编译问题排查面会小很多。我这次最终采用的就是这个方案纯源码构建主要用于定位那些“用二进制包编译时看不到”的深层次问题。3. rosdep看似简单却最容易卡住人的第一道坎3.1 rosdep 的工作机制和它在 Lyrical 里的行为变化rosdep 的本质是一个“把 ROS 包依赖翻译成系统包依赖然后交给 apt 去安装”的工具。每个 ROS 包的package.xml里写的depend标签对应的是一个 rosdep keyrosdep 会根据你当前的操作系统版本把这个 key 映射成具体的 apt 包名。你可能会想这不是挺简单的吗为什么还要单独写一节因为 Lyrical 默认启用了新版 rosdep 的数据索引逻辑它对package.xml的格式要求更严格也更容易判定“找不到 key”。一个典型报错长这样ERROR: the following packages/stacks could not have their rosdep keys resolved to system dependencies: ros_imu_driver: No definition of [libserial] for OS [ubuntu]这里的意思是libserial这个 rosdep key 在 Ubuntu 系统的映射表里不存在。解决办法一般是两个方向一是这个 key 在旧版本系统里有新系统里改名了二是这个 key 来自第三方 rosdep 仓库你没有添加对应的 rosdep source list。第二种情况尤其误导人。很多人以为rosdep update已经把 rosdep 仓库的全部数据拉下来了实际上它默认只拉取rosdistro主仓库的数据很多设备厂商的自定义 key 是不在里面的。你需要在/etc/ros/rosdep/sources.list.d/下添加厂商提供的额外源或者在rosdep install时通过--rosdistro参数指定正确的发行版名称。3.2 一条能大幅减少挫败感的 rosdep 命令组合我不太建议直接在官方文档的命令上无脑加-y跑因为一旦遇到 key 解析失败它会中途直接停下来前面的包可能装了一半后面的全没装。我更常用的组合是这样rosdep install --from-paths src --ignore-src -r -y 21 | tee rosdep_install.log解释一下这里两个参数的意义-r表示遇到解析失败时继续往后走不中断整个流程。这样你可以拿到一份尽量长的失败清单一次性去处理而不是反复重跑命令。-y是 apt 安装时自动确认。跑完之后重点看 log 里的ERROR行逐条排查那些 key。如果是网络原因导致部分数据没拉下来重新rosdep update多试几次通常能解决。如果 key 本身在系统里不存在优先去查它的上游项目页面看看有没有对应的 apt 源或二进制安装说明。3.3 网络超时和镜像问题我怎么处理和它相关的一系列连锁问题在依赖解析和安装过程中网络问题出现的频率远超普通开发者的预期。rosdep update默认访问的是 GitHub 上的 rosdistro 仓库在国内网络环境下经常超时。这种情况我不会建议你反复硬试更有效的办法是配置代理或者改用镜像源。这里为了避免任何踩线我不展开具体工具只说一个原则只要能让rosdep update的数据源可访问且地址改动只影响你的开发环境就可以放心改。拿到更新数据后apt 源也要注意。Ubuntu 26.04 的默认源在部分区域访问速度很慢导致rosdep install在安装 system dependency 时长时间卡在Waiting for cache lock或者直接报 404。我的处理方式是先用apt-cache policy检查需要安装的包是否存在于当前源如果确认是源的问题再切换镜像源重试。这些网络层面的坑实际占了我整个依赖处理时间的三分之一。但说实话这类问题几乎没有技术含量纯粹是环境问题解决方案就是耐心多试几次。4. CMake 4.x 的破坏性变更一次被错误日志误导的定位经历4.1 错误日志与实际根因之间的巨大鸿沟从源码编译 Lyrical 过程中我遇到的最有代表性的一次报错是来自一个自研的激光驱动包。错误输出如下CMake Error at CMakeLists.txt:35 (target_link_libraries): The link interface of target imu_serial_parser contains: Qt5::Core but the target was not found. Possible reasons include: * There is a typo in the target name. * A find_package call is missing for an IMPORTED target or an platform-specific target. * An attempt to set the INTERFACE_LINK_LIBRARIES for a target to include a target that is not in the export set.这是我见过最典型的误导性报错。表面看问题指向 Qt5::Core 没找到好像应该去检查 Qt5 的安装。但实际上在 CMake 4.x 的环境下这个错误的真正根因是 CMakeLists.txt 里cmake_minimum_required(VERSION 3.0)和现代 CMake 的 policy 机制不兼容导致find_package(Qt5)的查找路径和 target 导出行为发生了改变。CMake 4.x 里默认启用了CMP0177等一系列新 policy它们的作用是“纠正”老版本 CMake 的一些不合理默认行为。听起来很合理但对老项目来说一旦cmake_minimum_required声明的版本低于某个阈值CMake 不仅会警告还会直接改变查找路径的语义。也就是说你在3.28里能正常编译的项目在4.x下可能连 target 都找不到。4.2 我梳理出的 CMake 4.x 核心破坏点为了搞清楚到底哪些行为变了我专门把 CMake 4.0 的 release notes 从头到尾过了一遍这里挑几个跟我实际踩坑相关的重点变化点旧行为CMake 3.x新行为CMake 4.x典型影响最低版本要求允许cmake_minimum_required(VERSION 2.8.12)等cmake_minimum_required必须 ≥ 3.5否则直接报 FATAL_ERROR一堆古早包的构建脚本直接不能跑find_package的搜索路径对非导入 target 比较宽容更严格地校验 package config 中的 target 是否真实存在伪造 target 或手写 config 的项目会炸CMAKE_CXX_STANDARD默认值未设置时可能继承编译器默认值强调显式声明标准否则编译选项行为不一致C14 老代码在默认 C17/20 下出现兼容性问题与ament_cmake的配合主要通过全局变量传递编译选项推荐使用target_*系列命令全局变量优先级降低大量旧 package 的编译选项失效这张表看起来抽象我举个例子你就明白了。以前很多 ROS 包的 CMakeLists.txt 里写的是cmake_minimum_required(VERSION 3.0) project(my_package) ... include_directories(include) add_definitions(-stdc14) ... ament_export_dependencies(geometry_msgs)这套写法在 CMake 3.28 上能顺利编译因为那些被include_directories指定的头文件路径会全局透传给所有 target编译选项也是全局生效。但在 CMake 4.x ament_cmake 的组合下全局可见的编译选项优先级被降低include_directories对依赖它的库的传递性变弱于是你的代码可能因为找不到某个头文件、或者编译标准变成了 C17 而报出一堆莫名其妙错误。我的建议是与其逐个排查这些隐性问题不如在老项目里直接用target_include_directories、target_compile_features重写一套符合现代 CMake 规范的 CMakeLists.txt一劳永逸地消除绝大多数 policy 层面带来的兼容性风险。后面我会给出一份具体的改造模板。4.3 CMake 4.x 与 ament_cmake 的兼容层级官方支持并不等于默认支持还有一个需要重点提醒的地方Lyrical 官方的核心包确实已经迁移到 CMake 4.x 兼容模式了但很多第三方社区包并没有。你在源码编译时如果某个包报出诡异的 CMake 错误先看一眼它的CMakeLists.txt里cmake_minimum_required的声明版本。凡是低于 3.5 的几乎必然在 CMake 4.x 下出问题。处理方式有两个短平快的办法在该包的 CMakeLists.txt 里把最低版本改成 3.16不要改成 3.5很多旧包用了只有 3.16 才支持的函数。更规范的办法创建一份package.xml补丁或维护一个自己的 overlay 分支修改后单独编译安装。实际项目中我倾向于用第二个办法因为第一个方法虽然快捷但每次 vcs import 更新源码后改动会被覆盖。维护 overlay 分支可以保证每次同步上游代码后只需要 rebase 一次即可保留修改。这里我还想多提一句CMake 4.x 对FetchContent的支持度变得非常高很多新项目开始用FetchContent来拉取依赖源码而不是依赖系统的 find_package。这在 ROS 2 生态里有利有弊——好处是依赖版本可以精确锁定坏处是如果你的网络环境不稳定构建过程会反复失败。Lyrical 时代做源码编译时尽量先确认所有第三方依赖的获取方式统一走 apt 或 vcs 导入避免混用。5. 现代 CMake 的语义变化为什么“能编译”和“编译正确”是两回事5.1 从全局变量到 target-based理解这一转变才能看懂新报错说到现代 CMake很多从 ROS 1 时代过来的开发者第一反应是“不就是把变量从全局改成 target 属性吗”这个理解方向是对的但漏掉了最核心的一点现代 CMake 把“依赖关系”变成了可以被编译器感知的东西。举个具体例子。在旧式写法的 CMakeLists.txt 中include_directories(/usr/include/eigen3) add_executable(my_node src/main.cpp) target_link_libraries(my_node ${catkin_LIBRARIES})include_directories对所有 target 都生效target_link_libraries只是把链接库名字传给了编译器。问题在于如果你的库 A 链接了 Eigen而另一个 target B 链接了 AB 并不会自动获得 Eigen 的头文件路径。这在旧版编译环境中经常被忽略因为很多头文件恰好就在系统默认路径里。但到了 Lyrical 时代系统环境变得更干净了这个隐患就会被成片地暴露出来。现代 CMake 的正确做法是find_package(Eigen3 REQUIRED) add_library(my_lib src/my_lib.cpp) target_include_directories(my_lib PUBLIC $BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include $INSTALL_INTERFACE:include) target_link_libraries(my_lib PUBLIC Eigen3::Eigen)这里的关键词是PUBLIC。它声明了“我的库会对外暴露 Eigen 的头文件路径任何链接我的 target 都会自动获得这些路径”。这种传递性依赖解析才是现代 CMake 真正的价值所在。5.2 ament_cmake 里的导出机制变化ament_export_dependencies不是万能的在 ROS 2 的 ament 环境里包之间的依赖通过ament_export_dependencies进行导出。但我在 Lyrical 上遇到的一个很有意思的问题是这个宏只在某些条件下才真正生效。ament_export_dependencies(geometry_msgs)如果你的包里有target_link_libraries(your_target ${geometry_msgs_TARGETS})并且你的 target 没有设置EXPORT_NAME那么ament_export_dependencies导出的依赖信息可能无法正确传递给你这个 target 的消费者。这就会导致你的包单独编译没问题但被其他包依赖时报出找不到geometry_msgs的 target。这类问题在二进制包安装方式下几乎不会暴露因为 apt 安装时会统一把头文件装到系统路径下编译器总能找到。只有源码编译、按包隔离安装的时候才能真正体现出依赖声明是否完整。所以我的编译原则是每个自定义包必须显式声明它需要的一切依赖不依赖任何“碰巧装上了”的隐式库。在package.xml里多写一行depend的成本远小于在别人环境里编译失败后做远程排查的成本。5.3 构建目录碎片化什么时候应该把 build 目录整个删掉重来这次迁移中我还发现一个很容易被忽略的坑colcon 的 build 目录非常容易残留旧配置。尤其是在小版本升级或者切换 CMake 版本后build/目录下的 CMakeCache.txt 里记录的还是旧路径导致新编译链路直接加载了错误的变量。如果你在改完 CMakeLists.txt 后编译行为没有任何变化大概率是缓存问题。这时候不要犹豫直接rm -rf build/ install/ log/ colcon build --symlink-install很多人对删除 build 目录有心理障碍觉得重编要花很长时间。实际上对于大多数中等规模的工作区删掉重编反而比排查那些藕断丝连的缓存问题更节省时间。而且--symlink-install参数可以让你对 Python 文件或 launch 文件的修改即时生效不需要反复重装。6. 针对编译失败的通用排查链路与止损预案6.1 自己总结的源码编译排错流程图文字版踩了这么多坑之后我整理出一套相对固定的排查顺序基本能覆盖大多数编译失败场景先看错误日志的第一行而不是最后一行。CMake 报错时候的错误信息经常是上下文倒装的根因在第一行或前半段的某个check中。确认 CMake 版本与编译标准。在 build 目录下执行grep CMAKE_CXX_STANDARD CMakeCache.txt看当前实际生效的 C 标准是不是你代码预期的。确认依赖包的导出是否完整。查看你的依赖包是否ament_export_dependencies了所有必要的库检查它是否设置了BUILD_SHARED_LIBS如果依赖包是纯 header-only 库必须用INTERFACE关键字链接。查看 package.xml 的依赖声明。很多编译问题的根源是逻辑依赖存在但在包里没有声明导致 colcon 的拓扑排序出错你的包编译时依赖包还没编完。检查 rosdistro 的版本坐标。如果你的ROS_DISTRO环境变量没有正确设为lyrical或者/opt/ros/lyrical/setup.bash没被 sourcerosdep 和 colcon 会按照错误的发行版信息去解析依赖。最后才考虑源码问题。前面的步骤全部排查完后再去看具体的代码错误这样能避免被表面报错带偏。6.2 colcon 编译参数的最佳实践我每次必带的参数组合针对 Lyrical 这种大型工作区我建议的 colcon 编译指令长这样colcon build \ --symlink-install \ --cmake-args \ -DCMAKE_BUILD_TYPERelease \ -DBUILD_TESTINGOFF \ -DCMAKE_EXPORT_COMPILE_COMMANDSON逐项说明一下我的考虑--symlink-installPython 脚本和 launch 文件以软链接方式安装到你当前的 src 目录改代码不用重新 build。-DCMAKE_BUILD_TYPERelease调试优化版本release 下很多 third-party 库的 vector 计算能有数量级的性能提升。-DBUILD_TESTINGOFF关闭编译时跑测试大幅节省时间。-DCMAKE_EXPORT_COMPILE_COMMANDSON生成compile_commands.json这是 clangd 等现代补全工具的基础没有它你写代码时会感到明显不便。而每次编译时我会在colcon build后追加--event-handlers console_direct让日志实时输出而不是缓冲到最后。否则遇到卡死的编译过程时你完全不知道它此刻执行到哪个文件。6.3 遇到死活编不过的包时我的止损策略源码编译最忌讳的是死磕某个第三方包。如果一个包重试了三次以上还是编不过我的建议步骤是先去看这个包在 CI 上的构建配置看看它官方支持的 Ubuntu / CMake 版本是什么。如果它的 CI 显示在更高版本编译器上有已知失败记录这个包可能已经实质上“失联”了。搜索这个包在官方 issue 区里关于新版本系统的讨论如果已经有人提了 PR 但尚未合入你可以直接裁掉这个包等合入后再升级。如果业务上必须要用就只能做本地 patch修改源码并维护一个自己的 fork在 vcs.repos 文件里把它指向自己的仓库地址。在整个 workspace 编译时用--packages-select参数跳过它优先保证其他包能编过避免一个包拖累全局进度。我这次就遇到了一个串口通信库在 Ubuntu 26.04 上因内核头文件 API 变化而编译失败的情况。花了一个小时深入调查后决定暂时 fork 掉包修改了那一行系统调用相关的代码打了补丁后问题解决。但后续为了安全性我还会定期去查上游是否修复一旦修复立即切回官方版本。7. 一些实实在在的体会给同样准备迁移的人整个过程走完之后我才真正理解 Lyrical 这次改动的分量。它并不是简单的“换个版本号”而是从构建系统层面彻底清理了历史包袱。这就意味着你手里的老代码短期内大概率需要一次集中的 CMake 现代化重构。对于短期目标我建议你把编译环境分为两层第一层用二进制包保证基础环境稳定可用第二层用源码 overlay 编译自己需要改的包。别一上来就想全部源码编译那是维护者才需要做的事。还有一个操作层面的小建议把每个第三方包的版本号固定下来。不要直接从main分支拉代码编译而是检查到具体的 release tag。Lyrical 的 API 还在不断变化中如果你每次都拉到最新最尖的代码第一天能编过第二天可能就编不过了。用 tag 固定版本至少能让你的环境在几周内保持可复现状态。最后如果你是在一个团队里做迁移我强烈建议第一时间把rosdep_install.log、compile_commands.json、以及你改过的 CMakeLists.txt 补丁提交到一个专门的“迁移仓库”里标记好日期和问题描述。这些记录在后续其他成员踩同样的坑时能省下好几天的排查时间。祝迁移顺利少踩编译坑。如果遇到相似问题但我的排查链路里有没覆盖到的情况欢迎在评论区补充你们的报错日志我们可以一起把这个 Lyrical 迁移踩坑系列做得更完整。