资讯详情

资讯详情

ROS2 Jazzy工作空间与colcon编译实战:从零创建并运行功能包

1. 为什么一上来就要讲工作空间和编译你要是搜过 ROS2 的教程大概率会看到一堆人上来就让你“跑一下小乌龟”或者直接扔给你一段 publisher/subscriber 的代码。然后你复制、粘贴、编译好像也没出什么大问题但你心里其实没底——这些东西到底放在哪里为什么有的包能编译过有的不行为什么我改了代码 rostopic echo 一点反应都没有我当年踩过的坑几乎全是在这一步埋下的。所以这个系列的第一步我特意选了“工作空间”和“编译”这两个看似最基础、实际上最劝退新手的话题。尤其是到了 ROS2 Jazzy 这个版本工具链已经从早期 ROS1 的 catkin 全面切换到了 colcon目录结构、环境变量机制、依赖查找逻辑都和 ROS1 有本质区别。你如果还用 ROS1 那套思路去理解 ROS2后面写包、跑 launch、做多机通信每一步都会莫名奇妙地卡住。这篇文章的目标很简单用一个可复现的最小工程带你完整走一遍工作空间创建、功能包编写、colcon 编译、环境加载、节点运行的流程。每一步我都会讲清楚“为什么这么做”而不是只甩给你几条命令让你抄。顺便说一句我用的环境是 Ubuntu 24.04 ROS2 Jazzy这也是目前 Jazzy 唯一官方完整支持的组合。你要是还在 Ubuntu 22.04 上折腾 Jazzy 的第三方源我建议直接换系统省下的时间绝对比重装系统的时间多。2. 工作空间到底是什么2.1 从 ROS1 的 catkin 到 ROS2 的 colcon如果你接触过 ROS1可能对catkin_make或者catkin build不陌生。那套体系里工作空间分 src、build、devel 三个目录编译产物和源码纠缠在一起依赖顺序按拓扑自动排序但跨包引用经常因为环境变量没刷新生出各种玄学问题。ROS2 把整个工具链换成了 colcon本质上是把 ROS1 时代的 catkin_tools 思想发扬光大所有功能包统一放在src目录下编译产物分到build和install两个目录install目录就是最终的运行环境通过一个setup.bash就能把路径全部打进当前终端。这里我建议你从一开始就养成一个习惯把build目录和install目录当成“不可靠的临时产物”删了不心疼随时可以重新生成。你在网上看别人报错绝大多数情况下让“删掉 build install 重新编译”就能好这并不是玄学而是 ROS2 的增量编译在依赖没刷新的情况下确实会残留旧配置。2.2 Jazzy 工作空间的目录结构先看一份最标准的 ROS2 工作空间长什么样ros2_ws/ ├── src/ │ ├── my_package/ │ │ ├── package.xml │ │ ├── CMakeLists.txt │ │ ├── src/ │ │ │ └── my_node.cpp │ │ └── launch/ │ │ └── demo.launch.py ├── build/ ├── install/ └── log/src你的所有源码都在这里一个功能包一个子目录。build编译过程中的中间文件、CMake 缓存都在这里。一般不需要手动进去翻。install编译完成后的安装目录包含可执行文件、库文件、Python 模块、launch 文件等相当于一个打包好的“运行目录”。log编译日志报错时先来这里看。这里面最关键的一点是你写的功能包能不能被系统找到取决于install目录里的结构对不对以及当前终端的AMENT_PREFIX_PATH环境变量有没有包含这个路径。这也是为什么每次打开新终端都要source install/setup.bash——不 source系统根本不知道你的包在哪。提示ROS2 不会像 ROS1 那样自动把当前工作空间的包加进来。你写了新包必须重新编译、重新 source否则ros2 pkg list里永远看不到它。2.3 为什么要分“底层覆盖”和“上层覆盖”ROS2 里还有一个概念叫 overlay覆盖层。默认安装的系统 ROS2 包在/opt/ros/jazzy这叫 underlay底层。你自己的工作空间里的包编译完以后通过 source setup.bash 叠加在底层之上叫 overlay。底层优先提供标准库和标准工具overlay 里的同名包会优先于底层的包被加载。这意味着你可以只改自己工作空间里的某个包而不动系统安装的原版包调试起来非常灵活。实际开发中我一般开两个终端一个专门跑底层的原生环境一个专门跑自己的 overlay用来对比行为差异。这个习惯帮我排查过至少十几次“为什么我用 ros2 run 跑出来的节点行为跟预期不一样”的问题——十有八九是环境变量串了跑到了底层的旧版本包。3. colcon 编译先从核心命令说起3.1 colcon 命令全家桶在开始实操之前我先把 colcon 最常用的几个命令给你列一遍后面操作用到哪个讲哪个命令作用常用参数colcon build编译工作空间内所有包--packages-select只编译指定包colcon list列出 src 下的所有包可加--packages-select过滤colcon test运行包内注册的测试一般配--event-handlers用colcon clean清理 build/install/log需要单独安装colcon-clean扩展colcon graph显示包间依赖图需要colcon-graph扩展最大概率踩坑的是colcon build的参数组合。你如果在 src 里既有 C 包又有 Python 包或者某个包的依赖还没装全直接colcon build可能报错。我推荐一开始就固定用这一套colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPERelWithDebInfo--symlink-install对 Python 包非常重要它会让 install 目录里的 Python 文件以软链接方式指向 src 里的源文件改代码不用每次重新编译就能生效。-DCMAKE_BUILD_TYPERelWithDebInfoC 编译的优化级别配合调试信息跑 gdb 时体验好很多。如果你只用 Release 模式gdb 断点经常看不到变量值只用 Debug 模式节点运行性能会明显下降。RelWithDebInfo 是折中最稳的选择。3.2 依赖安装rosdep 和 apt很多人一上来就colcon build然后编译报错看半天 CMake 信息才发现是缺了某个系统的开发包。ROS2 的依赖有两层一层是系统级依赖需要用sudo apt install安装另一层是 ROS2 的包依赖可以用 rosdep 自动识别并拉取。官方推荐的流程是先装 rosdep然后在工作空间根目录跑sudo rosdep init rosdep update rosdep install --from-paths src --ignore-src -r -y它会读取每个包里的package.xml自动把缺失的依赖项用 apt 装好。但说实话在国内网络环境下rosdep update经常失败尤其是访问 raw.githubusercontent.com 的时候。我个人的替代方案是先看报错信息缺什么包就sudo apt install ros-jazzy-xxx直接装简单直接。package.xml里声明的依赖名和 apt 里的包名有时候对不上比如dependrclcpp/depend对应的 apt 包名是ros-jazzy-rclcpp。这也是新手最容易懵的地方。实在不行就模糊搜索apt search ros-jazzy | grep 你要找的关键字4. 从零创建一个可编译的功能包4.1 初始化工作空间现在开始实操。先建一个干净的工作空间mkdir -p ~/ros2_ws/src cd ~/ros2_ws注意src目录必须存在否则 colcon 会直接告诉你找不到源码目录。然后确认你的 ROS2 环境已经加载过source /opt/ros/jazzy/setup.bash echo $ROS_DISTRO如果输出jazzy说明环境正常。如果你没装好 ROS2先回头把安装步骤补上别在这步省事。4.2 用 ros2 pkg create 生成包模板早期的教程喜欢让你手动写CMakeLists.txt和package.xml我完全不推荐太容易出错。ROS2 内置的ros2 pkg create会自动生成一个合法的最小工程模板我们只需要在上面做增量修改。创建一个 C 包cd ~/ros2_ws/src ros2 pkg create my_first_pkg \ --build-type ament_cmake \ --dependencies rclcpp创建一个 Python 包ros2 pkg create my_py_pkg \ --build-type ament_python \ --dependencies rclpy--build-type指定构建系统类型C 用ament_cmakePython 用ament_python。--dependencies声明当前包直接依赖的 ROS2 包生成时会自动写入package.xml和CMakeLists.txt的相应位置。对于纯 Python 包我建议再加一个--node-name参数省得自己手写入口ros2 pkg create my_py_pkg \ --build-type ament_python \ --dependencies rclpy \ --node-name my_node它会顺手生成一个my_node.py的可执行入口编译后ros2 run直接能跑。4.3 package.xml 里究竟写了什么打开生成的package.xml你会看到一堆 XML 标签。新手不需要全部记住但有几个字段必须理解标签作用说明name包名必须和目录名一致否则编译报错version版本号随意但要符合语义化版本格式maintainer维护者邮箱和姓名缺失会导致编译告警license许可证默认 Apache-2.0可改depend编译和运行都需要的依赖对应#include的包名实际操作中很多人改了包名或者从网上拷了一份 package.xml忘了改name一编译就报“package xxx not found”其实问题出在这。注意ROS2 里包名不允许有大写字母、连字符、点号只能用小写字母、数字和下划线。你在 GitHub 上看到my-pkg这种写法在 ROS2 里是过不了编译的。4.4 给 C 包写一个最小节点我直接用最简代码演示不搞花活。编辑src/my_first_pkg/src/my_first_pkg_node.cpp#include rclcpp/rclcpp.hpp int main(int argc, char * argv[]) { rclcpp::init(argc, argv); auto node std::make_sharedrclcpp::Node(minimal_node); RCLCPP_INFO(node-get_logger(), Hello ROS2 Jazzy!); rclcpp::spin(node); rclcpp::shutdown(); return 0; }这段代码做的事很简单初始化 ROS2、创建一个名为minimal_node的节点、打印一句日志、然后进入事件循环等待退出。如果你创建包的时候加了--dependencies rclcppCMakeLists.txt里已经自动处理好了find_package(rclcpp REQUIRED)。如果没加你需要手动检查以下三处find_package(rclcpp REQUIRED) add_executable(my_first_pkg_node src/my_first_pkg_node.cpp) ament_target_dependencies(my_first_pkg_node rclcpp) install(TARGETS my_first_pkg_node DESTINATION lib/${PROJECT_NAME} )第三行install(TARGETS ...)特别容易被忽略。如果不配置这一步即使colcon build成功了ros2 run也找不到可执行文件。4.5 给 Python 包写一个最小节点Python 包的结构和 C 不同核心是setup.py。默认生成的my_py_pkg目录结构类似my_py_pkg/ ├── package.xml ├── setup.py ├── setup.cfg ├── resource/ ├── test/ └── my_py_pkg/ └── my_node.py关键点在于setup.py里的entry_pointsentry_points{ console_scripts: [ my_node my_py_pkg.my_node:main, ], },这个配置的意思是生成一个名叫my_node的命令行命令运行时执行my_py_pkg/my_node.py里的main()函数。修改入口时文件名和函数名都要对上否则ros2 run会报找不到命令。默认的my_node.py内容就是一个简单的节点模板直接可以跑。你如果以后要加新的可执行文件记得同步在entry_points里注册习惯了这个流程就不会漏。5. 编译、加载与运行5.1 第一次 colcon build回到工作空间根目录cd ~/ros2_ws colcon build --symlink-install第一次编译会比较慢因为要生成 CMake 缓存和编译所有依赖。看到Summary: 2 packages finished的字样说明两个包都成功了。如果只想编译某一个包避免每次全量编译浪费时间用colcon build --packages-select my_first_pkg还有一点我个人习惯在编译后顺手看一眼log目录里最近一次编译的输出这样报错的时候能找到最原始的线索。5.2 source install/setup.bash编译完成后在同一个终端里执行source install/setup.bash这一步把工作空间的install路径加进AMENT_PREFIX_PATH。你如果换了一个终端必须重新执行否则ros2 pkg list里看不到你的自定义包。有个小技巧为了避免每次手动 source我把这两行写进了~/.bashrcsource /opt/ros/jazzy/setup.bash source ~/ros2_ws/install/setup.bash但这样有个隐患当你新开一个终端它会自动加载工作空间如果此时工作空间还没编译或install目录不存在终端就会报错。所以更稳妥的做法是只在~/.bashrc里加底层 ROS2 的 source工作空间的环境需要哪个终端就用哪个终端手动 source。这也是 ROS2 官方推荐的做法。等你真正理解了环境变量的工作机制再按需把 overlay 加进去也不迟。5.3 跑第一个节点用ros2 run分别运行两个包ros2 run my_first_pkg my_first_pkg_node ros2 run my_py_pkg my_node终端里出现Hello ROS2 Jazzy!说明节点正常运行。再开一个新终端用ros2 node list查看节点列表你会看到minimal_node和my_node。到这一步你已经成功走完了工作空间创建、功能包编写、colcon 编译、环境加载、节点运行的全链路。接下来所有 ROS2 开发都会在这个基础上展开。6. 工作空间里最常见的坑和排查思路6.1 找不到功能包执行ros2 pkg list | grep my没有输出基本可以断定是环境没 source 对。排查顺序如下确认当前终端是否执行过source install/setup.bash。确认install目录里确实有my_first_pkg这个目录。检查install目录的setup.bash是否来自当前工作空间而不是/opt/ros/jazzy的 setup。如果改过~/.bashrc确认里面加载的路径正确没有重复覆盖。这个问题的根源在于 AMENT_PREFIX_PATH 这个环境变量。ROS2 在启动时通过它查找所有已安装的包你 source 哪个目录它才会把哪个目录加进来。没有 source 或 source 错了路径系统就完全不知道你的包存在。6.2 编译报错说找不到 rclcppCMake Error: Could not find a package configuration file provided by rclcpp这种报错一般有几种原因没有安装对应 ROS2 开发包用sudo apt install ros-jazzy-rclcpp补上。当前 shell 没有 source 过 ROS2 环境AMENT_PREFIX_PATH为空CMake 自然找不到。包的package.xml和CMakeLists.txt里声明的依赖不一致漏了某个依赖。我自己的经验是九成情况是第一个原因。ROS2 二进制安装默认带了很多包但不代表开发依赖齐全。6.3 Python 包改了代码但行为没变化如果你用了--symlink-installPython 代码改动后会直接生效不需要重新编译。但有一个例外如果新增了 Python 文件尤其是新增了模块还是需要重新执行colcon build因为软链接只针对已经生成的文件。还有个常见坑两个终端一个 source 了旧环境的 overlay一个 source 了新环境的 overlay你运行的时候可能用错了终端跑的是旧代码。排查时先确认当前终端的AMENT_PREFIX_PATH到底指向哪里。6.4 C 包改了代码但 ros2 run 没变化C 必须重新编译colcon build --packages-select my_first_pkg source install/setup.bash有几个新手常犯的错编译了但忘了 source环境还是旧的。把源文件加在了src目录下某个不属于任何包的路径colcon 根本不识别。编译成功了但 install 目录里的可执行文件没有更新因为你改了源码忘了重新 install。从我的经验来说建议你每次编译完都顺手执行一下source install/setup.bash。虽然有时候不开新终端也能用但这是一个很好的防呆习惯。6.5 编译日志太长看不懂colcon build默认只打印摘要报错细节往往被折叠了。想看到完整编译输出用colcon build --event-handlers console_cohesion console_directconsole_cohesion把每个包的错误日志合并显示。console_direct直接把编译过程中的全部输出打到终端。如果日志刷屏太厉害先看log/latest_build目录下的完整日志文件然后用 grep 定位 error 关键字grep -r error: log/latest_build/6.6 工作空间里既有 C 又有 Python 包编译相互影响colcon 本身支持多语言混合编译常见问题出在依赖顺序上Python 包依赖某个 C 包生成的库但编译时 Python 包先编译了找不到库报错。解决办法有两个显式指定依赖顺序colcon build --packages-select cpp_pkg先编译底层包再编译上层包。利用colcon list检查 package.xml 里的依赖声明是否完整让 colcon 自动排序。我见过不少人在混合工作空间里遇到“明明写对了代码就是编译不过”的诡异问题最后发现是package.xml少写了一个depend导致 colcon 认为两个包没有依赖关系编译顺序错乱。所以还是那句话依赖声明要写全别嫌麻烦。6.7 LD_LIBRARY_PATH 导致的段错误这个坑比较隐蔽但也相当常见。如果你的 C 包动态链接了第三方的.so文件或者你手动设置了LD_LIBRARY_PATH编译和运行阶段可能会频繁出现段错误Segmentation fault。排查方式ldd install/my_first_pkg/lib/my_first_pkg/my_first_pkg_node这条命令会打印可执行文件依赖的共享库路径。如果发现某个库指向了系统的旧版本多半就是LD_LIBRARY_PATH污染了。我的习惯是不到万不得已不修改LD_LIBRARY_PATH优先用 rpath 或者直接把库文件放到系统库目录下。ROS2 本身自带的库非常多手动设置LD_LIBRARY_PATH极其容易让系统库版本冲突轻则编译不过重则所有节点一启动就崩。6.8 一个包编译失败导致后续所有包无法编译colcon 默认是遇到错误就停但这个“停”不是整个编译停掉而是标记失败后继续尝试编译其他包。但某些共享依赖的问题会导致连锁失败看起来就像“整个工作空间废了”。应对思路先单独编译失败的包把错误解决了再全量编译。colcon build --packages-select 失败的包名如果这个包单独能过说明问题是依赖顺序或者环境变量如果单独也过不了那就是源码问题专心看报错即可。还有个小技巧如果你经常改 CMakeLists.txt可以用colcon build --cmake-clean-cache强制清理 CMake 缓存。有时候 CMake 缓存里残留了旧的路径配置改了 CMakeLists 也不生效这个参数能解决不少“我明明改了却没用”的诡异问题。7. 聊聊我踩过的一个具体坑从工作空间到实际项目的经验说一个我最近在实际项目中遇到的案例特别能说明工作空间管理的重要性。我接手的一个机器人项目代码仓库里塞了二十多个包有底层驱动、有导航算法、有视觉处理还有一堆 launch 文件。仓库的 README 写的是“clone 下来直接colcon build就能用”。结果我colcon build直接报错好几个依赖包缺失。查了一圈发现这个仓库有人把两个不同 ROS2 版本Humble 和 Jazzy的包混在一起提交了导致package.xml里声明的依赖跟实际代码需要的老接口完全对不上。后来我花了大概一个下午把所有包按照功能分成三组底层驱动、中间算法、上层应用一组一组分别编译。colcon build --packages-select 底层驱动组 --cmake-args -DCMAKE_BUILD_TYPERelease source install/setup.bash colcon build --packages-select 中间算法组 --cmake-args -DCMAKE_BUILD_TYPERelease source install/setup.bash colcon build --packages-select 上层应用组 --cmake-args -DCMAKE_BUILD_TYPERelease source install/setup.bash每编完一组就实时source依赖顺序不对的时候能立刻定位到是哪个包导致的。如果是全量编译报错信息实在太多了根本分不清是谁先谁后。最后把稳定的依赖全部写进package.xml和 README后续同事再拉代码一条命令就能编译通过。这件事给了我一个很深的体会工作空间不是简单地把代码往 src 里一扔就完事。合理的功能分组、清晰的依赖声明、规范的编译流程才是真正省时间的点。很多新手在单个包上花了很多时间却对整套工作空间的组织方式没有概念等代码量上来了就会寸步难行。8. 后续内容还可以怎么扩展第一步到这里你已经能把一个最小功能包编译并运行起来。下一步建议你按顺序去补这几块自定义消息接口创建 msg/srv/action 接口包理解接口定义和编译生成的关系。launch 文件用 Python 的 launch 文件一键启动多个节点替换掉手动开终端的低效流程。tf2 坐标变换理解 ROS2 里坐标系的管理机制这是机器人开发绕不开的核心。参数和生命周期节点理解 ROS2 的动态参数系统以及 Lifecycle Node 在复杂系统中的作用。我在实际项目中的体会是工作空间和编译这块基础打扎实了后面学消息通信、学生命周期、学导航栈都会顺手很多。如果你在编译或运行过程中卡住了欢迎带着报错信息来交流我能帮上忙的一定帮。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →