用 colcon build 构建 ROS 2 工作空间,大多数人都是从一条命令开始的:
cd ~/ros2_ws colcon build我当年也是这样,后来在一个移动底盘项目里,因为改了底盘驱动包里的一行代码,发现每次都要把工作空间里二十多个包重新编译一遍,几分钟才能等到可执行文件,才开始认真研究 colcon build 的参数。用熟了之后才发现,参数这东西不能靠背,而是分层的:选哪些包、怎么装、怎么编译、并发多少,每类参数解决一类问题。这篇文章就按这个思路,把 colcon build 的常用参数和背后的逻辑一次讲透。刚接触 ROS 2 的初学者可以把它当入门手册,已经在用 colcon 但总觉得“哪里不对劲”的开发者,建议重点看后面实战和排错部分。
1. 为什么 colcon build 的参数值得单独写一篇
1.1 从一次全量构建的等待说起
我记得很清楚,当时新拉下来一个机器人仿真工作空间,直接执行colcon build,屏幕上哗哗刷了快十分钟。等就等吧,最烦的是我明明只改了一个 Python 文件,重新colcon build时又把所有包从头编译了一遍。那会儿不懂,就在终端里一次次rm -rf build install log,然后全量重来,时间全耗在等待上了。
后来翻了 colcon 的文档和源码,才发现 colcon 其实是一个任务编排框架,它自身并不直接调用编译器,而是通过插件去调 ament_cmake、catkin、setuptools 这些后端工具。默认情况下,它扫描src目录下所有包,把每个包都当成一个独立任务,所以什么都不加时,行为就是“全量构建”:
- 扫描
src下所有含package.xml的包; - 把所有包按依赖关系排序;
- 并行执行所有包的构建任务;
- 把构建产物放到
install目录,日志放到log目录。
这个设计带来的好处是入口统一、扩展灵活,坏处是如果你不懂参数,就只能接受最笨的默认行为。换句话说,colcon build 的参数本质上是在控制两件事:告诉它要处理哪些包,以及告诉它怎么处理这些包。
1.2 colcon 的扩展架构与参数的本质
很多人以为 colcon 是一个新构建系统,实际上它更像一个“构建系统之上的构建系统”。它对外提供统一的命令行接口,对内则通过colcon-core的插件机制调用不同的构建后端。比如一个包如果package.xml里有<build_type>ament_cmake</build_type>,colcon 就调用 ament_cmake 的扩展去跑 CMake;如果是ament_python,就走 setuptools 那套流程;如果遇到 catkin 包,也有对应的扩展去兼容处理。
理解了这一点,再看参数就清晰多了。colcon build 的参数大致可以分成四类:
- 全局控制参数:控制路径、日志、执行器等通用行为;
- 包选择参数:决定处理哪些包,忽略哪些包;
- 安装布局参数:决定产物以什么方式落到
install目录; - 传递参数:把额外的参数透传给 CMake、make、setuptools 等底层工具。
后面几章我就按这个思路展开。遇到不熟悉的参数,我建议你先执行colcon build --help,把参数列表扫一遍,再结合这篇讲到的分类去对号入座,比死记硬背高效得多。
2. 选包参数:只构建你需要的那一部分
2.1 packages-select 与 packages-up-to
日常开发中最常用的需求是“我只改了某个包,只想构建它”。比如我只想构建my_controller这个包:
cd ~/ros2_ws colcon build --packages-select my_controller这样 colcon 只会在src里找my_controller,跳过其他包。如果你同时改了多个包,可以空格拼接:
colcon build --packages-select my_controller my_robot_bringup但这里马上会撞到一个新手高频问题:如果my_controller依赖工作空间里的另一个包,而那个包还没有安装,直接--packages-select my_controller会报“找不到依赖”的错误。原因很简单:被选中的包只有它自己,依赖链上游的包没有被构建,自然找不到对应的 CMake 配置文件或头文件。
这种场景要把参数换成--packages-up-to my_controller。它的含义不是“构建我选中的包”,而是“构建我选中的包,以及它依赖的、当前工作空间里存在的包”。换句话说,它会沿着依赖关系往上游走,把缺的依赖一起编出来。
我自己的习惯是:
- 只是改一个叶子功能包:
--packages-select 包名; - 改了业务包但不确定依赖:直接
--packages-up-to 包名; - 新克隆的工作空间第一次编某个包:用
--packages-up-to最稳妥。
2.2 packages-ignore 与 packages-above
和精确选择相反,有时候你只希望跳过某些包,其余全部正常构建。这时候用--packages-ignore:
colcon build --packages-ignore bad_driver我遇到过的情况是:工作空间里放了几个依赖特殊硬件 SDK 的驱动包,在没有对应设备的开发机上根本无法编译,但其他包都正常。用--packages-ignore跳过它们,整个工作空间就能顺利构建。注意--packages-ignore接收的是包名列表,可以空格拼接多个包名。
另一个方向性很强、但非常实用的参数是--packages-above。它的作用和--packages-up-to正好相反:--packages-up-to是选中某个包以及它依赖的上游包,--packages-above是选中某个包以及依赖它的下游包。
什么时候用?比如我改了自定义消息包my_msgs里的一个.msg文件,所有依赖my_msgs的业务包理论上都需要重新编译,否则运行时拿到的还是旧接口定义。这时候只按包名去一个个编太容易漏,直接用:
colcon build --packages-above my_msgs它会把my_msgs和所有直接或间接依赖它的包挑出来一起构建。这两个参数可以配套深度限制使用:--packages-up-to-depth限制向上最多走几层,--packages-above-depth限制向下最多走几层。实际工程里我很少限制深度,但如果你只是改了接口的第一层消费者,限制深度能再省一点时间。
2.3 选包参数的实践建议
选包参数用多了,我整理了几条很实用的建议,希望你少走弯路。
第一,不要一上来就colcon build不带任何参数。除非是新环境第一次全量构建,否则绝大多数场景都应该用选包参数缩小范围。全量构建看着省心,实际上把无关包的编译时间、日志噪音都一起拖进来了。
第二,改接口类包(msg、srv、action定义包)之后,优先用--packages-above。因为接口变更影响的是所有下游包,用--packages-select只编自己的业务包很可能导致运行时接口不匹配。
第三,如果遇到“明明选了包,但是报依赖缺失”,别急着质疑参数,先确认是不是漏了--packages-up-to。依赖缺失的报错信息通常很长,但开头一般会指出是哪个.cmake文件找不到,顺藤摸瓜就能定位到是哪个包没有被构建。
| 参数 | 作用 | 典型场景 |
|---|---|---|
--packages-select | 只构建选中的包 | 改单个功能包 |
--packages-up-to | 构建选中包及它依赖的包 | 依赖尚未安装或不确定 |
--packages-above | 构建选中包及依赖它的下游包 | 改了接口后重建所有消费者 |
--packages-ignore | 构建时跳过指定包 | 某些平台编不过的驱动包 |
3. 安装与链接参数:决定产物如何落盘
3.1 symlink-install 的利弊
做 ROS 2 开发,尤其是写 Python 节点和 launch 文件比较多的人,我强烈建议加上--symlink-install:
colcon build --packages-select my_py_pkg --symlink-install这个参数的作用是:构建时不在install目录里复制文件,而是创建符号链接,指回src里的源文件。Python 包本身不需要编译,加了符号链接之后,你改了.py文件,下次直接运行节点就是新代码,不需要重新 build。
对 C++ 包来说,--symlink-install的影响没有 Python 包那么大,因为 C++ 改了源码还是得走编译。但它对 launch 文件、yaml 配置这类资源文件是有益的,能避免“改了 launch 文件却忘了重新构建”这种尴尬。
使用时有几个注意点:
- 在 Windows 上,创建符号链接可能需要开发者模式或管理员权限,否则构建阶段会失败;
- 有些部署脚本、打包工具会遍历
install目录,那个目录里一堆符号链接,打包时一定要留意; - 如果遇到诡异问题,比如“明明改了 Python 代码,运行结果还是旧的”,先确认
install里对应文件是不是真链接到了源文件,再确认终端有没有重新source install/setup.bash。
这个参数是典型的本机开发神器,但我不建议在正式发布镜像或 CI 打包产物的场景里使用,因为符号链接在跨容器、跨目录移动时容易断。
3.2 merge-install 与独立安装路径的区别
colcon 默认的安装布局是每个包在install下各占一块地方,比如install/my_pkg/lib、install/my_pkg/include。这种布局隔离性很好,不同包的同名文件不会互相覆盖,排查问题也比较容易定位。
--merge-install则把所有包合并到同一棵安装树下,也就是install/lib、install/include、install/share。这样做的好处是目录结构干净,更接近传统 CMake 的安装习惯,适合最终把整个install目录打包成部署包或者在 CI 中作为产物上传。
但要注意,合并布局也有风险。如果两个包都安装了同名动态库或者同名头文件,后者很可能覆盖前者,而且这种覆盖在构建阶段不一定报错,等到运行时出现符号找不到、头文件内容不对等情况才暴露出来。所以我一般只在比较可控的 CI 流程里用--merge-install,本机开发还是默认布局更安全。
| 布局方式 | 产物位置 | 优点 | 风险 |
|---|---|---|---|
| 默认分离布局 | install/pkg 各自独立 | 隔离好、便于排查 | 目录较杂 |
--merge-install | install/lib、install/include 等合并 | 干净、便于打包 | 同名文件可能被覆盖 |
3.3 build-base、install-base、log-base 三件套
这三个参数分别指定构建目录、安装目录、日志目录,默认是当前目录下的build、install、log。日常开发不用动,但有两个场景我建议你主动改。
第一个场景是同一份代码要在同一台机器上同时维护 Debug 和 Release 两种构建。默认目录下两个配置会互相污染,每次切换都要清一次缓存。改用独立目录就能并存在一起:
colcon build --build-base build/debug --install-base install/debug --cmake-args -DCMAKE_BUILD_TYPE=Debug colcon build --build-base build/release --install-base install/release --cmake-args -DCMAKE_BUILD_TYPE=Release两个产物互不干扰,切配置时只要 source 对应的install目录即可。第二个场景是磁盘空间紧张,比如在 Docker 容器里,默认路径可能落在容量较小的挂载层上,你可以把构建产物指到别的位置:
colcon build --build-base /tmp/colcon_build --install-base /tmp/colcon_install --log-base /tmp/colcon_log这三个参数本身不复杂,但在“多配置并存”和“路径规划”这类问题上,能省掉很多全量重编的时间。
4. 控制构建过程:线程数、执行器与错误处理
4.1 colcon build 线程数怎么设
“colcon build 线程数”是很多人会专门搜索的关键词,因为默认全量构建实在太慢了。但调线程数之前,你要先搞清楚并发发生在哪一层,否则容易调了个寂寞。
colcon 的并发有两个层级:
--parallel-workers:控制 colcon 同时处理多少个包的任务,默认值通常和 CPU 核心数相关;- 每个包内部的编译并发:由底层构建系统决定,比如 CMake 生成 Makefile 后执行
make -j的并行度,或者 Ninja 默认使用的核心数。
只看--parallel-workers而不限制包内并发,效果可能很夸张:比如你机器有 8 核,--parallel-workers 8表示同时编 8 个包,而每个包内部默认再用 8 线程编译,瞬间可能有几十个编译进程在跑。内存稍微小一点,或者某个大包本来就吃内存,很容易把机器卡死。
比较稳妥的做法是两层都控制。比如机器 16GB 内存,我一般这样:
colcon build --parallel-workers 4 --cmake-args -DCMAKE_BUILD_PARALLEL_LEVEL=4-DCMAKE_BUILD_PARALLEL_LEVEL=4是 CMake 3.12 之后支持的编译并行参数,对 Makefile 和 Ninja 生成器都有效。如果你一直在用 Makefile 生成器,也可以写成:
colcon build --parallel-workers 4 --make-args -j4注意这两个写法的目标不太一样:前者是配置阶段传给 CMake 的通用并行选项,后者是构建阶段直接传给 make 的命令行参数。我在项目里优先用CMAKE_BUILD_PARALLEL_LEVEL,因为跨生成器可移植性更好。
另外,--executor sequential可以把所有包改成串行执行。它不会加速,但在排查并行构建时随机报错的问题上非常好用:如果串行就不报错、并行就随机失败,基本可以断定是并发资源争抢或者包之间存在隐藏的构建顺序依赖。
4.2 传递参数:cmake-args、make-args 与 python-args
colcon build 最容易被误解的参数就是--cmake-args。它表示:把这些参数原样透传给所有 CMake 类型包在配置阶段的 CMake 命令。最常见的用法是控制编译类型和是否编译测试:
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF这里有一个我见过无数次的坑:有人习惯把整段参数用引号包起来:
# 错误示范 colcon build --cmake-args "-DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF"这样 colcon 会把整串内容当成一个参数传给 CMake,CMake 收到一长串以-D开头的东西,很容易解析失败或者生成出意料之外的配置。正确做法是让 shell 把每个参数拆开,不要加引号合并。
--ament-cmake-args和--catkin-cmake-args是更精细的版本,分别只把参数传给 ament_cmake 类型的包或者 catkin 类型的包。如果你的工作空间里两种包混杂,而某个参数只想对其中一类生效,这两个就很有用。
--python-args用于给 Python 包的构建流程传递参数。说实话我日常用得不多,因为 Python 包一般也不需要太多配置项。但当你需要调整 setuptools 行为、指定编译目录时,可以先用colcon build --help查一下当前版本支持的参数项,再按需使用。
4.3 继续构建与清理策略
全量构建大工作空间时,最怕的是编到中间某个包挂了,后面所有包跟着停。如果只想快速看一轮“全军覆没”的错误列表,用:
colcon build --continue-on-error它会让 colcon 在某个包失败后继续尝试其他包。不过要注意,这个参数只是“不中断”,失败包本身不会自动重试。
构建缓存“脏”了也是高频问题。比如你改了 CMake 选项,但重新构建时发现配置没变化,十有八九是 CMake 缓存作怪。这时可以:
colcon build --packages-select xxx --cmake-clean-cache它只删除目标包的CMakeCache.txt,让 CMake 重新配置,比全量清build目录要温和得多。如果这样还不行,再用--clean-first,它在构建之前会把目标包在build和install目录里的旧产物清掉。
我自己的经验是:能定位到包就不要全清目录。远程开发或 CI 环境里,build目录动辄几个 GB,一次全量重编的成本很高,先用最小代价的清理手段,确实无效再考虑rm -rf build install log。
5. 实战:一套可复用的 colcon build 参数组合
5.1 从零构建工作空间的完整命令
如果是在新机器上第一次拉取一个大型工作空间,我通常会这样构建:
cd ~/ros2_ws colcon build \ --symlink-install \ --parallel-workers 4 \ --cmake-args -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF拆开看每一段:
--symlink-install:本机开发阶段用符号链接安装,改资源文件不用反复 build;--parallel-workers 4:控制包级并发,避免全量编译时内存爆掉;--cmake-args -DCMAKE_BUILD_TYPE=Release:统一用 Release 类型,节省运行时执行效率;-DBUILD_TESTING=OFF:关闭测试编译。除非你接下来要跑colcon test,否则测试代码只会拖慢构建速度。
构建完成后记得:
source install/setup.bash新开终端也要先 source 一遍,这是 ROS 2 开发里最基本的操作。很多“找不到包”“找不到可执行文件”的问题,根因就是忘了 source。
5.2 增量开发场景的参数选择
增量开发时,我不建议直接全量colcon build。假设我负责一个robot_bringup包,它依赖自定义消息包robot_msgs,我通常这样操作:
一开始要把依赖也编出来:
colcon build --packages-up-to robot_bringup --symlink-install --parallel-workers 2之后只改robot_bringup自身的代码:
colcon build --packages-select robot_bringup --symlink-install --parallel-workers 2如果改了robot_msgs里的消息定义,则反过来构建所有依赖它的包:
colcon build --packages-above robot_msgs --symlink-install --parallel-workers 2这套组合的思路很简单:用--packages-up-to保证依赖先就位,用--packages-select缩小日常构建范围,用--packages-above应对接口变更的连锁影响,最后用--symlink-install让 Python 和资源文件的改动即时生效。
5.3 配合 CI 的参数写法
CI 环境和本机开发的需求不太一样:CI 更注重可重复性和产物可迁移性,不在乎符号链接带来的便利。所以我一般在 CI 里这样写:
colcon build \ --merge-install \ --cmake-args -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF \ --log-base /tmp/colcon_log这里用--merge-install把产物统一收拢,后续打包install目录会比较方便。日志目录单独指出来,是因为 CI 平台通常只会保留工作空间里的部分目录,把日志放到固定路径再归档,出问题时有迹可循。
如果需要在 CI 日志里实时看到编译输出,可以加上:
colcon build --event-handlers console_direct+默认情况下,colcon 会把每个包的日志写到log目录下的文件里,终端只显示摘要信息。某个包卡住或者编译信息非常多的时候,你会觉得像在看哑剧。console_direct+会直接把这些日志实时打印到终端,排查问题直观很多。这个参数我在本机调试时也常用,尤其是在怀疑某个包编译命令写错的时候。
6. colcon build 报错排查速查
6.1 常见报错与对应参数
把几个高频问题整理成表格,方便对照:
| 现象 | 可能原因 | 建议做法 |
|---|---|---|
| 只编一个包却提示找不到依赖 | 依赖包未安装或未被构建 | 改用--packages-up-to 包名 |
| 改了 Python 代码运行没变化 | 没有启用符号链接或没 source | 加--symlink-install,重新 source |
| 改了 CMake 选项但配置没变 | CMake 缓存了旧配置 | --cmake-clean-cache |
| 多包并行构建时随机失败 | 并发过高,内存或 IO 争抢 | 调低--parallel-workers和包内 -j |
| 终端看不到编译中间日志 | 默认日志写文件,终端只显示摘要 | 加--event-handlers console_direct+ |
| source install/setup.bash 后找不到新包 | 新包构建完成后没重新 source | 重新source install/setup.bash |
| merge-install 后出现链接冲突 | 多个包安装了同名文件 | 改回默认分离布局,检查重复包名 |
6.2 参数优先级与覆盖关系
colcon 支持通过默认配置文件来预设参数,默认读取路径是~/.colcon/defaults.yaml。文件里可以按子命令分类写参数,比如:
build: cmake-args: -DBUILD_TESTING=OFF parallel-workers: 4这样每次执行colcon build,即使命令行什么都不带,也会自动带上-DBUILD_TESTING=OFF和--parallel-workers 4。命令行显式传入的参数会覆盖配置文件里的同名参数,所以不用担心被写死。
这个机制非常适合团队统一构建规范。但我提醒一句:不要把--symlink-install写进默认配置,尤其是团队里有人负责打包发布时,那个符号链接布局很容易在迁移和归档时出问题。默认配置适合放那些“所有环境下都希望保持一致”的参数,比如关闭测试编译、限制并行度。
6.3 我在实际项目中踩过的坑
最后分享几个我踩过之后印象特别深的坑。
第一个是 Docker 里的构建目录问题。默认的build、install、log都在/root/ros2_ws下,容器镜像层一多,每次docker commit或者复制容器内容时,这些动辄几个 GB 的目录都会让操作慢到怀疑人生。后来我把构建目录指到单独的挂载卷里,镜像和源码目录都干净了很多。
第二个是并发参数叠加导致的卡死。有一回我把--parallel-workers设成 8,又没限制包内的-j,结果每个包默认又用多线程编译,内存直接顶满,整个系统几乎无响应。后来我把包级并发和包内并发都降到 2,构建虽然慢了一点点,但整个过程中机器还能正常使用,人也轻松得多。
第三个是引号问题。我自己也曾经把--cmake-args后面的一串-D参数用引号包起来,结果 CMake 报了一屏看不懂的错。拆开之后马上就好。这里再强调一次:colcon 把--cmake-args后面的内容按空格拆分成多个参数传给 CMake,所以不要在--cmake-args后面自作聪明地加引号合并字符串。
第四个是 Python 包的一处“玄学”。我改了.py文件之后运行节点,发现还是旧代码。检查了半天,最后发现是因为开了多个终端,其中一个终端 source 的还是旧版本的install目录。重新source install/setup.bash之后一切正常。这种问题看着像构建问题,其实和环境刷新有关。
我个人现在的固定工作流是:本机增量开发用--packages-select加--symlink-install,改接口用--packages-above,新环境全量构建用--packages-up-to加限制并发,CI 里用--merge-install加日志归档。这些参数组合并不复杂,但每一条都是迭代项目过程中被真实问题逼出来的。理解了参数背后的分层逻辑,以后再遇到 colcon 的新参数,你也能很快判断它属于哪一类、该在哪一层生效。