ROS环境配置里最让人头大的报错之一,绝对是那句“The following packages have unmet dependencies”。我见过太多人在这一步卡住:新装系统、按教程装desktop-full,结果 apt 报一串依赖不满足,然后开始盲目尝试apt --fix-broken install、换源、甚至重装系统。老实说,大部分时候这些操作不但没用,还可能把环境搞得更乱。
这篇文章不打算再给你一份“复制粘贴即可”的命令列表,而是把这条报错的来龙去脉讲清楚。我会从报错文本本身开始拆解,再讲 apt、rosdep、软件源这三者到底怎么协作,然后给你一套真正可复用的排查链路,以及对应不同成因的处理方案。无论你是刚接触 ROS 的新手,还是被团队环境反复折磨的老开发者,这篇文章都值得收藏着,遇到问题照着走一遍。
1. 先把报错现场看清楚:unmet dependencies 到底长什么样
很多人在处理这个报错时太急躁,看到“unmet dependencies”就开始操作,其实第一件该做的事,是静下心把报错信息完整读一遍。这一节先把典型的报错形态拆开,你才知道哪些行能忽略,哪些行必须盯住。
1.1 一段典型报错的信息拆解
假设你在 Ubuntu 20.04 上执行:
sudo apt install ros-noetic-desktop-full常见的报错长这样:
The following packages have unmet dependencies: ros-noetic-desktop-full : Depends: ros-noetic-move-base but it is not going to be installed Depends: ros-noetic-rviz but it is not going to be installed E: Unable to correct problems, you have held broken packages.这段信息看起来像天书,其实就三部分。最上面的The following packages have unmet dependencies:是汇总提示,告诉你接下来列出的所有包都存在依赖缺口。中间的每一行格式固定:冒号前面是你想安装的包,Depends:后面跟的是它想要的依赖项,最后那段but it is not going to be installed是关键,翻译成人话就是“这个依赖包应该装,但 apt 经过计算后认为不该装”。
这种“不该装”通常不是一个独立包有问题,而是多个包的条件互相矛盾,apt 的依赖解析器放弃了。最底下的E: Unable to correct problems, you have held broken packages.是一个总括错误,里面的held broken packages不是指你手动 hold 了包,而是 apt 认为当前系统存在无法自动修复的破损状态。
注意:如果你看到
held broken packages,优先怀疑的不是某个包坏了,而是系统里多个包之间的版本关系不兼容。真正要做的不是强行装,而是找出“候选版本”之间的矛盾。
1.2 新手最容易踩的三个误区
看到这个报错的时候,新手最容易干三件“看似合理但实际有害”的事。
第一,直接上sudo apt --fix-broken install。这条命令本身没有错,但它会自动卸载一些包来满足依赖关系。如果系统里有你辛辛苦苦配好的 ROS 功能包,--fix-broken很可能会把某些依赖标记为“自动移除”,误伤面非常大。
第二,把软件源来回切换,今天官方源明天镜像源,每次都是apt update后接着硬装。换源是解决这类问题的常见手段,但前提是你先搞清当前源的版本状态。盲目换源会让 apt 的缓存索引变成“混合状态”,下次报错可能更离谱。
第三,直接卸载整个desktop-full再重装。这个操作非常耗时,而且如果根因是源或系统包版本问题,卸载重装后大概率还会遇到一模一样的报错,折腾半天回到原点。
这些都是我见过的真实操作,尤其是团队里多人共用一台机器时,有人手欠改过/etc/apt/sources.list,后面的人再来装 ROS 就会遇到这种“谁都没动过却装不上”的诡异情况。
1.3 报错文本里真正值得关注的行
判断一次依赖报错的严重程度,只看两个地方。
第一个地方是冒号前面的包名集合。如果报错集中在ros-noetic-xxx这一批包,说明问题多半出在 ROS 源或 ROS 包的版本关系上;如果报错里混入了系统包,比如libopencv-dev、python3-catkin-pkg这类非 ROS 包,那就要把排查范围扩大到整个 apt 依赖网络。
第二个地方是Candidate版本。也就是说你不仅要看报错文本,还要结合apt-cache policy输出来判断“当前 apt 认为这个依赖包应该装哪个版本”。版本候选往往比报错本身更早暴露问题。比如候选版本是 1.14 而依赖方要求 1.16,这就直接指向“源索引太旧”或者“多个源混用”。
2. 依赖解析的底层逻辑:apt、rosdep 和软件源各干了什么活
想真正消化后面那些解决方案,你得先理解这条报错是三个角色协作失败的结果:apt 负责解析二进制包依赖,rosdep 负责补齐源码编译时的系统依赖声明,软件源则提供了包索引和安装候选。任一层出问题,最后爆发出来的都是unmet dependencies。
2.1 apt 的依赖解析规则:Depends、Candidate 与 Held
apt 在安装一个包时,会先读这个包的Depends:字段,找出它需要哪些其他包以及符合的版本范围,然后从当前软件源里为每个依赖包选出候选版本。选完所有候选版本后,apt 会检查它们之间是否存在循环依赖、版本冲突、已被 hold 等状态。
如果一切正常,apt 会列出待装列表并让你确认。只要有一对依赖关系无法满足,它就不会继续装,而是输出你看到的那段报错。
这里的“版本候选”概念很多人没有意识。同一个包名,在不同软件源里的版本可能完全不同。例如 ROS 官方源里的ros-noetic-rviz是 1.14.x,而某镜像源里同步滞后,可能还停留在 1.12.x。如果主包要求不低于 1.13,而 apt 从当前源里选出的候选版本是 1.12,就会直接触发Depends: ros-noetic-rviz but 1.12 is to be installed。
依赖解析器有个特点:它很少为了满足一个依赖去主动“升级”你系统中已有的其他包,除非你明确允许。默认策略是保守安装,所以系统里已有包的版本往往成了“不动点”,新装的包必须迁就它们。这解释了为什么 apt 有时候宁愿报错,也不愿意主动帮你把某些包升级到新版本——它怕升级引发更大范围的破坏。
2.2 rosdep 的出现原因与工作边界
rosdep 的存在是因为 ROS 生态的源码依赖横跨了“ROS 包”和“系统包”两类。ROS 包之间的依赖通过<depend>标签写在package.xml里,而 ROS 包依赖的系统级库,比如OpenCV、Boost、PCL,不可能靠 ROS 自己的源全包下来,所以需要一种机制把它们映射到apt、pip等系统包管理器。
rosdep干的就是这个映射活。它读取一个叫rosdep.yaml的索引文件,把package.xml里声明的依赖 key,翻译成当前系统应该执行的安装规则。比如你声明依赖opencv,rosdep 查表后可能翻译成apt install libopencv-dev(在 Ubuntu 上),也可能是brew install opencv(在 macOS 上)。
但是很多人的问题是:用二进制的ros-noetic-desktop-full时也去跑rosdep install。二进制安装时,desktop-full内部的依赖已经被 ROS 官方打包成 apt 依赖关系了,rosdep 在这条路径上基本是多余的。只有当你开始catkin build或colcon build编译源码包时,rosdep 才真正派上用场。
所以如果你的报错出现在安装二进制包阶段,别急着怪 rosdep,先看 apt 层面。如果出现在编译源码阶段,rosdep 索引过期、缺少某条 key 是常见原因,但这时候报错一般不会是unmet dependencies,而是ERROR: cannot locate rosdep definition for [xxx],这是另一套问题。
2.3 软件源、系统版本和 ROS 发行版的映射关系
ROS 每个发行版都对应特定的 Ubuntu 版本。拿大家最常用的几个举例:ROS Noetic 对应 Ubuntu 20.04,ROS Melodic 对应 Ubuntu 18.04,ROS Kinetic 对应 Ubuntu 16.04。你在 22.04 上想装ros-noetic-desktop-full,就算强行往源里塞进 Noetic 的仓库,apt 也会因为系统库版本不匹配而报依赖错误——这就是系统版本错配造成的典型unmet dependencies。
软件源这里还有一层坑:ROS 官方源和系统软件源是两个独立体系。ROS 包放在packages.ros.org,系统基础库放在 Ubuntu 自己的源里。当 ROS 二进制包依赖某个系统库的新版本,而你的 Ubuntu 源没有同步到这个新版本时,也会出现依赖不满足。
所以处理这类报错时,你要同时确认三件事:Ubuntu 源是否正常、ROS 源是否正确配置、这两个源里关键包的版本是否处在兼容区间。这也是我后面排查链路里反复强调“先看版本,再动操作”的原因。
3. 我在实际项目中遇到的四类高频成因:按图索骥最省力
光看原理还不够,你得知道这些原理具体会在什么场景下爆雷。我梳理了这几年自己在项目里反复见的四类高频成因,每个都给出对应特征和排查思路,你可以直接对号入座。
3.1 镜像源与官方源混用,ROS 包版本候选错乱
这类问题最常见的表现是:同一个 ROS 包名,在apt-cache policy里能看到多个版本候选,而当前选中的候选版本无法满足主包要求。
成因通常是有人把官方源和某个镜像源同时写进了/etc/apt/sources.list或者/etc/apt/sources.list.d/下的文件里,然后又加了不同优先级。ROS 官方源版本新,镜像源版本旧,两个源合并后,apt 默认会选版本号最高的那个。理论上这没问题,但镜像源如果同步不完整,导致某个主包是新版而其依赖包还是旧版,那就“混装”了,依赖自然不满足。
排查入口:先跑
apt-cache policy ros-noetic-desktop-full看候选从哪里来,再看apt-cache policy ros-noetic-move-base这类具体依赖包的候选版本。如果发现它们分别来自不同源文件,基本可以断定是源列表混用。
3.2 Ubuntu 版本和 ROS 发行版错配
这类通常发生在“教程看了三家”的场景。比如某个教程让你装 Melodic,你实际系统是 Ubuntu 20.04,而教程里的源配置用的是bionic(18.04 的代号)。你把bionic源写进 20.04 系统后,apt 能拿到 Melodic 的包索引,但安装时会发现这些包依赖的系统库版本与你系统里的不一致,直接报unmet dependencies。
这种错配还有一种隐蔽变体:你安装的 ROS 发行版本身正确,但/etc/apt/sources.list里的 Ubuntu 组件写了focal,而系统实际是jammy,导致 apt 在更新基础源时把混合版本的索引拉下来了,之后装 ROS 就各种怪问题。
排查入口:运行
lsb_release -a确认系统版本,再检查你写的 ROS 源里用的发行版代号是否匹配。一个 ROS 发行版对应一个系统版本,这是铁律,没有特例。
3.3 rosdep 索引没更新或缺少关键源
如果你不是装二进制包而是从源码编译,rosdep索引过期是高频坑。rosdep 的索引文件需要通过rosdep update定期从远端拉取,而它依赖的sources.list.d/20-default.list里有多个官方源入口。一旦某个入口长期没有更新,或者你用了精简版配置只留了部分入口,rosdep 就会查不到某些 key。
当依赖的 key 无法解析时,rosdep 会提示 “cannot locate rosdep definition”,但如果你在安装某个系统依赖时接到错误,可能又会被包装成 apt 层的unmet dependencies。这里有个很容易误判的点:你在编译一个功能包时,先手动装了某个库,然后系统里出现“Depends: x but it is not going to be installed”,这往往是因为 rosdep 把同名 key 映射到了另一个系统包,而那个包版本不匹配。
排查入口:执行
rosdep update后重试,如果报错仍然存在,用rosdep resolve <key>查看具体映射目标,看它到底落在哪个 apt 包或 pip 包上。
3.4 编译产物与 apt 包互相覆盖,版本判定两头落空
这一类常见于开发机器人底层库的团队。某开发者从源码编译了move_base并装到了/usr/local,同时系统里还留着旧版的二进制ros-noetic-move-base。之后上一个新功能包,依赖ros-noetic-move-base的新版本,apt 检测到二进制包还在,但/usr/local里的自定义编译版本又覆盖了运行时路径,导致系统行为和新包预期不一致,最终报出依赖不满足。
这种“半源码半二进制”的环境是最难排查的,因为 dpkg 数据库认为某个包已经安装,而实际运行环境是另一套库。你需要同时检查 dpkg 状态和物理文件位置。
4. 从报错到根因:一次完整排查链路复盘
理论讲完了,我带你走一遍真实的排查链路。之后你再遇到类似问题,不用慌,照着这个链路一步步做就行。
4.1 案例场景说明
有一次我接手一个竞赛团队的机器人环境,现象很典型:某开发者在一台 20.04 工作站上装ros-noetic-desktop-full,报错说ros-noetic-move-base和ros-noetic-rviz无法安装。团队已经试过换源、--fix-broken install、甚至把ros-noetic-desktop-full换成单独装部分包,最后都以失败告终。我介入时,系统已经处于“半装半卸”的混乱状态了,这是最容易被搞坏的场景之一。
4.2 排查步骤的每一步在做什么
我没有急着执行安装命令,而是把排查拆成了几步,每步都明确目的。
第一步,读取完整报错信息并保存下来。执行安装命令时加上记录输出:
sudo apt install ros-noetic-desktop-full 2>&1 | tee /tmp/ros_install.logtee的作用是让输出内容同时显示在屏幕上并存到文件里,这样后续能够反复翻看,不会被终端的滚动冲掉。这一步很基础,但能帮你避免“看到最后几行就把前面关键信息忘了”的情况。
第二步,查看候选版本,判断是「源的问题」还是「依赖关系本身不兼容」:
apt-cache policy ros-noetic-desktop-full ros-noetic-move-base ros-noetic-rviz这条命令会列出三个包各自的已安装版本、候选版本和版本来源仓库。我当时一眼就看到关键信息:ros-noetic-move-base的候选版本来自一个旧镜像源,版本号比ros-noetic-desktop-full要求的低了一截。这说明问题出在“源混合”而不是包本身。
第三步,检查源列表,找出到底有哪些源文件在起作用:
grep -r "^deb" /etc/apt/sources.list /etc/apt/sources.list.d/输出里能看到/etc/apt/sources.list和sources.list.d/下所有被启用的源。我当时发现sources.list.d/下同时存在 ROS 官方源、一个旧镜像源,还有一个已经废弃的第三方自定义源。三个源里的同一批 ROS 包版本不一致,apt 最终选出了“能匹配一部分、匹配不了另一部分”的组合。
第四步,检查 dpkg 是否存在未完成状态,避免在问题根子上叠加新问题:
dpkg --audit dpkg --configure -adpkg --audit会列出处于半安装、半配置状态的包。如果这些包存在,即使换好源,安装过程也可能被它们带偏。这一步相当于先“止血”,再去找“病因”。
第五步,查看具体依赖缺口。如果前面几步还看不出结论,就用apt-cache depends指定依赖链:
apt-cache depends ros-noetic-move-base这条命令能看到move-base本身的完整依赖列表。把它和你系统里实际可获得的版本对照,就能定位到究竟是哪个底层库的版本缺了。
4.3 根因确认的瞬间与修复验证
确认过程其实很顺:把/etc/apt/sources.list.d/下的旧镜像源和第三方自定义源文件全部注释掉,只保留官方 ROS 源和一个同步正常的镜像源,然后执行:
sudo apt update sudo apt install ros-noetic-desktop-full这次 apt 在依赖计算阶段就通过了,装完后roscore能正常启动,rviz也没有报缺库。整个过程没动任何包,也没执行--fix-broken,纯粹是把“源候选版本”理顺后,问题自然消失。
提示:修完源之后,如果系统里已经有
dpkg残留状态,建议先跑一次sudo apt --fix-broken install让 apt 把残留状态收尾,然后再装新包。顺序很重要,先修源,再修包状态,最后装目标包。
5. 分情况处理方案:最小改动原则与强拆代价
排查出根因之后,处理手段也要分层次。我的原则永远是“最小改动”:能修源就不动包,能让 apt 自己处理就不手动卸载,手动卸载只作为最后手段。
5.1 第一优先:修软件源与固定版本
凡是源列表里存在多个冲突入口,第一步就是把它们清理干净。编辑/etc/apt/sources.list.d/下相关文件,把不需要的源注释掉或移除备份,然后重新apt update。
如果你发现某个包必须使用特定版本,可以使用apt install <包名>=<版本号>显式指定。但要注意,这只能解决“单包版本固定”的问题,如果底层依赖链很长,逐个指定版本会很痛苦,不如直接统一源。
查看包的所有可用版本可以用:
apt-cache madison ros-noetic-move-base这条命令会按版本号排列出所有源里该包的可用版本,方便你判断应该固定到哪个版本,或者哪个源太旧需要移除。
5.2 第二优先:让 apt 自己修(--fix-broken 与 configure)
如果清理源之后,系统里还残留半安装状态的包,可以执行:
sudo apt --fix-broken install这条命令会尝试修复所有状态不一致的包,必要时候会卸载冲突包。在执行前,建议先看一眼它会干什么:
sudo apt --fix-broken install --dry-run--dry-run只是模拟执行,不会真正改动系统。我强烈建议先跑一次模拟,看看它准备卸载哪些包。如果里面有你想保留的包,就要手动排除,或者放弃这条路线,改用手动处理。
如果系统里有大量“未配置”状态的包,先跑:
sudo dpkg --configure -a处理完 dpkg 状态,再用apt -f install收尾。
5.3 谨慎操作:卸载、清除与回退
如果--fix-broken也解决不了,通常是因为某个包已经损坏且没有新版本可选,需要局部卸载重装。这时候优先只动冲突链上的包,不要卸载整个desktop-full。
比如冲突集中在ros-noetic-rviz,就只卸载并重装这个包:
sudo apt remove ros-noetic-rviz sudo apt install ros-noetic-rviz如果依赖链上有一批包互相拉扯,可以试试先卸载所有相关包,然后一次性重新安装它们:
sudo apt remove ros-noetic-move-base ros-noetic-rviz ros-noetic-desktop-full sudo apt autoremove sudo apt install ros-noetic-desktop-full这一步会删除大量已安装组件,代价很大,但在“根因已确认是版本错配且源已修复”的前提下,效果通常立竿见影。
5.4 特殊路线:源码编译替代二进制包
有些情况下,二进制包和当前系统版本就是无法匹配,但你的代码必须跑起来。此时可以考虑从源码编译对应功能包。
源码编译的大致流程是:拉取功能包源码,确保package.xml里的依赖都能被 rosdep 解析,然后rosdep install补齐系统依赖,再用catkin或colcon编译。
但这里要提醒一个坑:源码编译出来的包会覆盖或混合二进制包的运行环境,如果你之后又想切回二进制包,很可能重新触发依赖问题。所以我建议先彻底卸载二进制包,再走源码编译,别两者同时留着。
6. 三个容易漏掉的隐藏依赖问题
很多时候,报错信息和排查结论都指向“系统包版本不对”,但真实原因藏在系统里一些不起眼的地方。下面三个问题是我在帮不同团队处理环境时反复撞上的,它们的隐蔽性都很高。
6.1 /usr/local 目录污染:dpkg 与 ldconfig 的错位
/usr/local是源码编译安装的默认前缀,理论上它不该和/usr混用。但在实际开发里,很多人为了方便,把自编译的OpenCV、PCL、Boost直接装进/usr/local,甚至sudo make install时用了默认路径。
问题来了:apt 判定依赖时只认 dpkg 数据库,它不知道/usr/local/lib下有新版库。所以当 ROS 包要求libopencv-dev >= 某版本,而 dpkg 里的版本不够时,apt 就会报依赖缺失。但你的程序运行时却能通过ldconfig找到/usr/local/lib里的新版库,于是出现“程序能编译、能运行,但 apt 一直报缺依赖”的诡异情况。
排查方法:检查
LD_LIBRARY_PATH和/etc/ld.so.conf.d/下是否有指向/usr/local/lib的配置,然后对比 dpkg 安装版本和库文件实际版本。如果有偏差,要决定到底是完全采用源码版还是完全退回 apt 版,不要两边都留。
6.2 pip 替 apt 打工:Python 依赖的“假满足”
ROS Noetic 及其许多工具链依赖 Python 3 的系统包,比如python3-catkin-pkg、python3-rospkg。很多人为了装某个 Python 库,执行了pip install --user或pip install rospkg,把同名包装到了用户目录或虚拟环境里。
这样一来,你的 Python 代码 import 时优先命中了 pip 装的版本,但 dpkg 数据库里python3-rospkg还是未安装或旧版本状态。之后你编译 ROS 包时,package.xml里的<depend>被 rosdep 映射成apt install python3-rospkg,apt 却告诉你“依赖不满足”或“版本冲突”。
这种“假满足”最坑的地方是,你直接运行某些脚本完全正常,一旦catkin_make或colcon build检测依赖时就露馅。
排查方法:对报错里提到的 Python 包,分别执行
dpkg -l | grep python3-xxx和pip3 show xxx,确认两个来源是否版本不一致。如果一致了就没事,不一致就要卸载 pip 版本,改装 apt 版本,或者反过来明确项目只用虚拟环境并彻底脱离系统 Python。
6.3 公钥过期与多发行版索引残留
ROS 源需要验证签名,如果你的系统时间不对或源里配置的公钥过期,apt 会在update阶段跳过某些索引。表面看apt update好像正常结束,实际上一部分包索引没有被刷新。这时候装包时,apt 拿到的候选版本可能是几天甚至几周前的状态,与当前包依赖要求不匹配,就会报unmet dependencies。
另外,如果/etc/apt/sources.list.d/里残留了旧发行版的源文件,比如从 Kinetic 时代保留下来的ros-latest.list,它依然指向 16.04 的仓库。系统是 20.04,apt 在 update 时会把旧索引拉下来,这就相当于把“多个发行版”的包混在了一起。
这类问题排查起来很隐蔽,因为报错并不会直接说“公钥过期”,而是转成各种奇怪的依赖关系失败。
排查方法:执行
sudo apt update后看输出里有没有NO_PUBKEY或Signature相关警告。如果有,就按 ROS 官方 wiki 的指引重新添加当前发行版的密钥,并清理旧发行版残留源文件。
7. 让团队环境不再反复触发这条报错:预防设计
每次遇到unmet dependencies都做一次“外科手术”,时间成本太高。我后来在带团队项目时,会提前做几层预防设计,把这类问题出现的概率降到最低。
7.1 锁定软件源基线与安装脚本
我强烈建议团队维护一份经过测试的软件源基线文件,明确指定使用哪个 ROS 官方源或镜像源、使用哪个 Ubuntu 主源,并把sources.list.d/下需要启用的文件整理成一份清单。不管是新成员装环境,还是老成员换机器,直接用这份基线,不要临时搜教程。
同时,把 ROS 的安装过程固化成一个安装脚本。脚本里固定了 ROS 发行版、桌面完整版或核心版的指定包名,并统一处理公钥更新。这样每次安装的起点都是一致的,变量越少,依赖错配的可能性就越小。
7.2 用容器和虚拟化隔离环境
如果你的项目可能有多个 ROS 版本共存,比如团队既有 Noetic 也有 Melodic 的代码,我建议直接上容器或虚拟机隔离。容器里用固定镜像,把 ROS 版本、Ubuntu 版本、系统依赖都锁死,物理机上的环境改动完全不影响容器内的开发流程。
这也是我后来最推荐的一层防护。很多所谓“依赖冲突”,本质是因为一台物理机上有多个项目的旧包残留,彼此抢版本。容器把这层干扰彻底隔开,你就不需要再为“环境A的包影响了环境B”这种问题头疼。
7.3 依赖出口清单与 CI 验证
第三个习惯是维护“依赖出口清单”。具体来说,就是让每个 ROS 功能包的package.xml保持严格整洁,依赖声明要完整而不多余。不要临时缺什么就漏掉什么,因为一旦声明不完整,rosdep 就无法正确反映项目需要什么,迟早会以某种形式爆出来。
有条件的话,在 CI 拉起的干净环境里定期跑一次编译。这样源码依赖的变化会第一时间被暴露在“干净环境”下,而不是等团队成员一个个在自己的脏环境里排查。这个方法说起来简单,但能帮你把unmet dependencies这类问题从“被动救火”变成“事前发现”。
最后再说说我个人的实际感受:处理这种依赖问题时,最大的敌人永远是急躁。我的习惯是,不管遇到什么报错,先把完整输出存一份,然后按“源列表 -> 候选版本 -> dpkg 状态 -> 残留手动安装”的顺序排查一遍。大部分情况下,真正的问题花不了十分钟就能定位出来,剩下的时间都浪费在“试命令”上。你只要愿意先把报错读透,再动手,至少能少踩一半的坑。