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 key,rosdep 会根据你当前的操作系统版本把这个 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 2>&1 | 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默认值 | 未设置时可能继承编译器默认值 | 强调显式声明标准,否则编译选项行为不一致 | C++14 老代码在默认 C++17/20 下出现兼容性问题 |
与ament_cmake的配合 | 主要通过全局变量传递编译选项 | 推荐使用target_*系列命令,全局变量优先级降低 | 大量旧 package 的编译选项失效 |
这张表看起来抽象,我举个例子你就明白了。以前很多 ROS 包的 CMakeLists.txt 里写的是:
cmake_minimum_required(VERSION 3.0) project(my_package) ... include_directories(include) add_definitions(-std=c++14) ... ament_export_dependencies(geometry_msgs)这套写法在 CMake 3.28 上能顺利编译,因为那些被include_directories指定的头文件路径会全局透传给所有 target,编译选项也是全局生效。但在 CMake 4.x + ament_cmake 的组合下,全局可见的编译选项优先级被降低,include_directories对依赖它的库的传递性变弱,于是你的代码可能因为找不到某个头文件、或者编译标准变成了 C++17 而报出一堆莫名其妙错误。
我的建议是:与其逐个排查这些隐性问题,不如在老项目里直接用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 链接了 A,B 并不会自动获得 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没被 source,rosdep 和 colcon 会按照错误的发行版信息去解析依赖。最后才考虑源码问题。前面的步骤全部排查完后,再去看具体的代码错误,这样能避免被表面报错带偏。
6.2 colcon 编译参数的最佳实践:我每次必带的参数组合
针对 Lyrical 这种大型工作区,我建议的 colcon 编译指令长这样:
colcon build \ --symlink-install \ --cmake-args \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_TESTING=OFF \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON逐项说明一下我的考虑:
--symlink-install:Python 脚本和 launch 文件以软链接方式安装到你当前的 src 目录,改代码不用重新 build。-DCMAKE_BUILD_TYPE=Release:调试优化版本,release 下很多 third-party 库的 vector 计算能有数量级的性能提升。-DBUILD_TESTING=OFF:关闭编译时跑测试,大幅节省时间。-DCMAKE_EXPORT_COMPILE_COMMANDS=ON:生成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 迁移踩坑系列做得更完整。