1. 为什么我要折腾这件事
先交代一下背景。我在一家做移动机器人底盘的公司干了快六年,日常跟ROS2打交道,从Foxy一路用到Humble。团队里积累了不少内部工具包,比如一个专门做轮式里程计标定的节点、一个把雷达点云转成代价地图的辅助库,还有一个封装了常见底盘CAN协议驱动的接口层。这些东西平时就在公司内部的GitLab上放着,谁要用就clone下来自己编译,时间一长问题就来了:新人入职光配环境就得折腾两天,版本对不上、依赖缺失、编译报错,各种鸡毛蒜皮的事反复消耗精力。
后来我就想,能不能把这些内部包整理一下,挑几个通用性强的发布到ROS2官方索引里去?这样不管是内部同事还是外部用户,直接一句rosdep install加colcon build就能跑起来,省掉大量沟通成本。而且说实话,把自己写的包挂到官方索引上,对个人履历也是一种背书。
但真动手之后才发现,从"本地能编译"到"官方能索引"之间,隔着一整套流程规范、元数据要求、CI校验和社区约定。我前后踩了差不多三周的坑,提交了四次PR才最终合并。这篇文章就把整个流程从头到尾拆一遍,包括每一步为什么要这么做、哪些地方容易翻车、以及我实际踩过的那些坑。如果你手里也有一个自认为还不错的ROS2包,想把它推到官方索引里,这篇应该能帮你省掉不少来回折腾的时间。
提示:本文基于ROS2 Humble Hawksbill版本和rosdistro的当前流程撰写,不同发行版在细节上可能有差异,但核心逻辑一致。
2. 先搞清楚"发布成官方包"到底意味着什么
2.1 官方索引的运作机制
很多人以为"发布ROS2官方包"就是把代码传到某个官方仓库,然后别人就能apt install了。这个理解只对了一半。ROS2的包分发体系其实分两层:
第一层是源码索引,也就是rosdistro仓库里的.distribution.yaml文件。这个文件记录了每个包的名字、仓库地址、版本分支、依赖关系等元数据。当你在rosdep里执行安装命令时,系统就是查这个文件来找到对应的源码仓库。
第二层是二进制分发,也就是通过ROS build farm(构建农场)自动编译出deb包,推送到APT源里。这一层是自动化的,你不需要手动编译deb,但前提是你的包已经进入了源码索引,并且通过了build farm的编译验证。
所以整个"发布"流程的核心,其实是把你的包信息注册到rosdistro的索引文件里,然后让build farm能成功编译通过。听起来简单,但实际操作中涉及的东西相当多。
2.2 什么样的包适合发布
不是所有包都适合往官方索引里塞。我总结了几条判断标准:
- 通用性:包的功能是否只对特定公司或特定硬件有意义?如果强依赖某个私有CAN协议或内部SDK,那发上去别人也用不了,反而增加维护负担。
- 依赖可控:所有依赖是否都能通过rosdep解析?如果依赖某个不在ROS生态里的第三方库,你得先确保它有对应的rosdep key,或者你自己去贡献一个。
- 许可证合规:代码的license必须是OSI认可的开源许可证,Apache 2.0、MIT、BSD这些都没问题。公司内部代码要发布的话,得先过法务。
- 维护意愿:发布之后你得持续维护,至少保证在新发行版出来时能及时适配。如果只是发完就不管了,build farm挂了也没人修,那还不如不发。
我最终选了三个包出来:一个是纯头文件的坐标变换工具库,一个是基于话题的里程计标定节点,还有一个是底盘驱动的抽象接口层。这三个包的共同特点是依赖干净、功能通用、不涉及公司核心业务逻辑。
2.3 整体流程概览
整个流程可以拆成这么几个阶段:
- 代码整理:确保包结构规范、package.xml完整、CMakeLists.txt正确
- 仓库准备:代码托管在公开仓库,分支策略清晰
- rosdep key处理:确保所有依赖都有对应的key
- 提交rosdistro PR:修改distribution.yaml,添加包信息
- CI校验与build farm编译:等待自动检查通过
- 后续维护:版本更新、新发行版适配
下面我逐个阶段展开讲。
3. 代码整理阶段:把包收拾干净再出门
3.1 package.xml的规范化
package.xml是ROS2包的身份证,官方索引和build farm都靠它来识别包的元信息。很多人本地写的时候比较随意,但要发布到官方,这个文件必须严格规范。
一个合格的package.xml至少包含这些字段:
<?xml version="1.0"?> <?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?> <package format="3"> <name>odom_calibrator</name> <version>0.1.0</version> <description>A ROS2 node for wheel odometry calibration based on topic messages.</description> <maintainer email="your.email@example.com">Your Name</maintainer> <license>Apache-2.0</license> <buildtool_depend>ament_cmake</buildtool_depend> <depend>rclcpp</depend> <depend>nav_msgs</depend> <depend>geometry_msgs</depend> <depend>tf2_ros</depend> <test_depend>ament_lint_auto</test_depend> <test_depend>ament_lint_common</test_depend> <export> <build_type>ament_cmake</build_type> </export> </package>几个容易出问题的地方我单独说一下:
description字段不能太短,也不能包含特殊字符。我第一次提交时写的是"calibration tool",结果CI直接报错说描述信息不够充分。后来改成了完整的一句话描述才通过。这个字段会显示在官方索引页面上,所以要写得清楚明白。
maintainer邮箱必须是真实可用的。build farm在编译失败时会往这个邮箱发通知,如果邮箱无效,出了问题你根本不知道。
license必须和仓库里的LICENSE文件一致。我见过有人package.xml写Apache-2.0,但仓库里放的是GPL的LICENSE文件,这种不一致会被CI直接拦下来。
version字段建议从0.1.0开始,遵循语义化版本规范。不要一上来就写1.0.0,除非你的API已经稳定到可以承诺兼容性。
3.2 CMakeLists.txt的常见坑
CMakeLists.txt是另一个高频出错点。ROS2的ament构建系统对CMakeLists.txt有一些约定俗成的要求,不满足的话build farm编译会失败。
首先,find_package的顺序和依赖声明要匹配。比如你package.xml里声明了tf2_ros依赖,那CMakeLists.txt里就必须有对应的find_package(tf2_ros REQUIRED)。我遇到过一次,本地编译能过是因为工作空间里恰好有其他包提供了tf2_ros,但build farm是干净环境,直接就找不到。
其次,安装规则要写全。头文件、可执行文件、launch文件、配置文件、甚至README,该install的都要install。build farm编译完之后会检查安装产物,如果某个文件没被install,运行时就会找不到。
install(TARGETS odom_calibrator_node DESTINATION lib/${PROJECT_NAME} ) install(DIRECTORY launch config DESTINATION share/${PROJECT_NAME} ) install(DIRECTORY include/ DESTINATION include )还有一个细节是ament_export_dependencies。如果你的包是给别人用的库,需要导出依赖,否则下游包链接时会找不到头文件路径。
3.3 代码风格与lint检查
ROS2官方对代码风格有明确要求,主要是基于ament_lint系列工具。你需要在CMakeLists.txt里加上lint测试:
if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) ament_lint_auto_find_test_dependencies() endif()然后在package.xml里加上对应的test_depend。这样在build farm编译时,会自动跑cpplint、cppcheck、uncrustify、lint_cmake等检查。任何一个不过,整个编译就失败。
我在这上面栽过跟头。我的代码里有一些行超过了120个字符,本地编译时没注意,因为lint测试默认不跑。但build farm会跑,结果直接报了一堆格式错误。后来我本地先跑一遍colcon test,把所有lint问题修完才提交。
实操心得:提交之前务必在本地干净工作空间里跑一次完整的
colcon build加colcon test,确保所有lint测试通过。这一步能帮你省掉至少一轮PR来回。
4. 仓库准备与rosdep key处理
4.1 仓库结构的最佳实践
官方索引对仓库结构没有强制要求,但有一些约定俗成的做法能让你的包更容易被接受。
最常见的是单仓库单包结构,也就是一个GitHub仓库里只放一个ROS2包。这种结构最简单,rosdistro配置也最直接。如果你的仓库里有多个包,需要在distribution.yaml里用packages字段逐个列出,稍微麻烦一点但也能做。
仓库的根目录下建议放这些文件:
README.md:说明包的功能、安装方法、使用示例LICENSE:开源许可证全文.gitignore:排除build、install、log等目录CHANGELOG.rst:版本变更记录(可选但推荐)
分支策略上,我建议至少维护一个main分支作为开发分支,然后为每个ROS2发行版开一个对应的分支,比如humble、iron、jazzy。rosdistro的distribution.yaml里会指定每个发行版对应哪个分支。这样做的好处是不同发行版的API差异可以隔离,不会因为适配新版本而破坏老版本的编译。
4.2 rosdep key的坑
这是整个流程里最容易被低估的环节。rosdep是ROS的依赖管理工具,它维护了一个从依赖名到系统包名的映射表。你的package.xml里声明的每个<depend>,都必须能在rosdep里找到对应的key,否则build farm编译时会报"cannot resolve dependency"。
ROS2核心包和常见第三方库的key都已经有了,比如rclcpp、nav_msgs、eigen、boost这些。但如果你依赖了某个比较冷门的库,就可能没有对应的key。
我遇到的情况是,我的标定节点依赖了一个做非线性优化的库,这个库不在ROS生态里。解决办法有两个:
一是自己贡献一个rosdep key。这需要往rosdistro仓库的rosdep/base.yaml或python.yaml里提交PR,添加一条映射规则。这个PR的审核周期可能比较长,而且需要你提供该库在各个平台上的包名信息。
二是把依赖打包进你的仓库。如果那个库不大,可以直接把源码放到你的仓库里作为子目录,然后在CMakeLists.txt里用add_subdirectory引入。这样就不需要rosdep key了,但会增加仓库体积和维护成本。
我最终选了第一种方案,因为那个优化库在Ubuntu的apt源里有现成的包,只是rosdep还没收录。提交key的PR大概等了一周多才合并。
注意:在提交rosdistro PR之前,一定要先确认所有依赖的rosdep key都已经存在。否则你的PR会被打回,让你先解决依赖问题。
4.3 版本号与tag管理
rosdistro的distribution.yaml里需要指定一个版本号,这个版本号会跟你的仓库tag关联。建议每次发布新版本时,在仓库里打一个tag,格式如0.1.0或v0.1.0,然后在distribution.yaml里引用这个版本。
版本号的更新不需要每次都改distribution.yaml。build farm会定期检查你的仓库是否有新tag,如果有就会自动触发编译。但distribution.yaml里的版本号需要手动更新,否则索引页面上显示的版本会过时。
5. 提交rosdistro PR的完整实操
5.1 Fork与克隆rosdistro仓库
rosdistro仓库在GitHub上,地址是ros/rosdistro。你需要先fork到自己账号下,然后clone到本地:
git clone https://github.com/<your-username>/rosdistro.git cd rosdistro git remote add upstream https://github.com/ros/rosdistro.git仓库里跟ROS2相关的主要是humble/distribution.yaml、iron/distribution.yaml这些文件。你要修改的是你目标发行版对应的那个文件。
5.2 编辑distribution.yaml
在distribution.yaml里,每个包对应一个条目。你需要添加的内容大概长这样:
odom_calibrator: source: type: git url: https://github.com/<your-username>/odom_calibrator.git version: humble status: maintained几个字段的含义:
source.type:源码类型,一般是gitsource.url:仓库地址,必须是公开可访问的source.version:分支名或tag名,建议用分支名方便后续更新status:维护状态,maintained表示活跃维护,developed表示开发中,end-of-life表示停止维护
添加的位置有讲究。distribution.yaml里的包是按字母序排列的,你得插到正确的位置,否则CI会报格式错误。我第一次提交时随手加在文件末尾,结果CI直接说排序不对。
5.3 提交PR与CI校验
提交PR之后,会自动触发一系列CI检查。这些检查包括:
- YAML格式校验:确保文件语法正确
- 排序校验:确保包名按字母序排列
- 仓库可访问性校验:确保url能正常clone
- rosdep key校验:确保所有依赖都能解析
如果任何一项不过,PR页面上会显示红色的叉,你需要修复后重新push。我前三次PR分别因为排序错误、rosdep key缺失、仓库权限问题被打回,第四次才全部通过。
CI通过之后,会有维护者来review你的PR。Review的内容主要是看包的功能是否通用、命名是否规范、描述是否清晰。如果没问题就会合并,然后build farm会在下一次构建周期里尝试编译你的包。
5.4 build farm编译与结果查看
build farm的编译状态可以在build.ros.org上查看。搜索你的包名,能看到各个平台的编译结果。绿色表示成功,黄色表示警告,红色表示失败。
如果编译失败,点击进去能看到详细的日志。常见的失败原因包括:
| 失败原因 | 典型日志关键词 | 解决方法 |
|---|---|---|
| 依赖缺失 | cannot find package | 检查package.xml和rosdep key |
| 编译错误 | error: | 本地复现并修复代码 |
| lint不通过 | cpplint/cppcheck | 本地跑colcon test修复 |
| 安装规则缺失 | file not found | 补全install规则 |
| 许可证问题 | license mismatch | 统一package.xml和LICENSE |
我遇到过一次编译失败,日志显示某个头文件找不到。排查后发现是CMakeLists.txt里漏了ament_export_include_directories,导致下游包链接时找不到头文件路径。补上之后重新触发编译就过了。
6. 常见问题与排查技巧实录
6.1 依赖解析失败怎么办
这是最高频的问题。表现是build farm编译时报"cannot resolve dependency"或者"rosdep key not found"。
排查步骤:
- 在本地干净环境里跑
rosdep check --from-paths src --ignore-src,看哪些依赖解析不了 - 对于解析不了的依赖,去
rosdistro/rosdep/base.yaml里搜索是否有对应的key - 如果没有,考虑自己贡献key,或者把依赖打包进仓库
- 如果有但版本不匹配,检查package.xml里的依赖名是否拼写正确
我踩过的一个坑是,package.xml里写的是<depend>opencv</depend>,但rosdep里的key其实是opencv2。这种命名不一致的问题很隐蔽,本地编译时因为系统里装了opencv所以能过,但build farm解析依赖时就挂了。
6.2 编译通过但运行时报错
有时候build farm编译能过,但用户安装后运行时报错。这种情况通常是运行时依赖没声明清楚。
比如你的代码在运行时需要读取某个配置文件,但CMakeLists.txt里没有把这个文件install到share目录。编译时不会报错,但运行时找不到文件。
解决办法是在CMakeLists.txt里补全所有运行时需要的资源文件:
install(DIRECTORY config launch rviz DESTINATION share/${PROJECT_NAME} )另外,如果代码里用了pluginlib或者class_loader,需要在package.xml里用<export>标签导出插件描述文件,否则运行时加载插件会失败。
6.3 PR被打回的常见原因
根据我的经验和观察其他PR的情况,被打回的原因主要有这几类:
- 命名不规范:包名包含大写字母、下划线开头、或者跟已有包重名。ROS2包名要求全小写,单词间用下划线分隔。
- 描述太简略:description字段只有一两个词,维护者会要求补充。
- 仓库没有LICENSE:或者LICENSE跟package.xml里的license字段不一致。
- 分支策略混乱:distribution.yaml里指定的分支不存在,或者分支里没有对应的package.xml。
- 依赖了私有库:依赖了某个不公开的仓库或SDK,这种直接会被拒。
6.4 版本更新的正确姿势
包发布之后,后续更新版本时不需要重新提交rosdistro PR(除非你要改仓库地址或分支名)。你只需要在仓库里打新tag,build farm会自动检测并触发编译。
但distribution.yaml里的版本号需要手动更新。这个更新可以通过提交PR来完成,也可以等维护者定期批量更新。我一般是在打tag之后顺手提一个PR更新版本号,保持索引信息准确。
实操心得:建议在仓库的README里加一个build farm的状态徽章,这样用户一眼就能看到当前编译状态。徽章可以从build.ros.org获取。
7. 我踩过的那些坑与最终建议
回过头看,整个流程最难的不是技术本身,而是对规范的理解和细节的把控。我前后花了三周多,其中大部分时间不是在写代码,而是在修各种格式问题、依赖问题和CI报错。
如果让我给后来者几条建议,我会说:
第一,先在本地模拟build farm的环境。用一个干净的Docker容器,只装ROS2基础环境,然后从零开始rosdep install加colcon build加colcon test。这一步能提前暴露90%的问题。
第二,package.xml和CMakeLists.txt要反复检查。这两个文件是build farm的主要检查对象,任何不一致都会导致失败。建议对照官方文档的模板逐字段核对。
第三,不要怕PR被打回。维护者打回你的PR是在帮你发现问题,每次打回都是一次学习机会。我第四次提交才通过,但通过之后对整个体系的理解深刻了很多。
第四,发布只是开始,维护才是长期工作。新发行版出来时要及时适配,依赖库升级时要跟进,用户提issue时要响应。如果没做好长期维护的准备,不如先在公司内部用着。
最后分享一个实用技巧:rosdistro仓库里有一个rosdistro/rosdep目录,里面维护了所有rosdep key的映射。在写package.xml之前,先去这个目录里搜一下你要用的依赖有没有对应的key,能省掉很多来回。另外,ros/rosdistro的PR页面里有很多历史PR可以参考,看看别人是怎么写的,照着改就行。