40、依赖图生成:使用--graphviz选项生成项目依赖图

说实话,我第一次接触 CMake 的 --graphviz 选项时,心里想的是:「这玩意儿能干啥?」

那时候我还在维护一个大型 C++ 项目,光 CMakeLists.txt 就有二十多个。每次改一个库的依赖关系,都得手动翻半天文件,生怕漏掉某个链接。后来有个同事甩给我一张图,说:「你看,这是咱们项目的依赖关系。」我一看,好家伙,原来我写的那些 target_link_libraries 居然绕成了个圈!

嗯,从那天起,我就再也没手动画过依赖图了。

40.1 什么是 --graphviz?

--graphviz 是 CMake 提供的一个命令行选项。它能把项目的依赖关系,自动生成一个 Graphviz 格式的文本文件。说白了,就是让 CMake 帮你画一张「谁依赖谁」的关系图。

你不需要装任何额外的 CMake 模块,也不需要写脚本。只要在配置项目时加一个参数,CMake 就会乖乖地把依赖关系吐出来。

核心命令:

cmake --graphviz=output.dot ..

这会在当前目录生成一个 output.dot 文件。然后用 Graphviz 工具把它转成图片就行。

40.2 实战:生成你的第一张依赖图

咱们拿一个简单的项目来演示。假设你有三个库:coreutilsapp。其中 app 依赖 coreutilsutils 又依赖 core

项目结构大概是这样:

project/
├── CMakeLists.txt
├── core/
│   ├── CMakeLists.txt
│   └── core.cpp
├── utils/
│   ├── CMakeLists.txt
│   └── utils.cpp
└── app/
    ├── CMakeLists.txt
    └── main.cpp

顶层的 CMakeLists.txt 大概长这样:

cmake_minimum_required(VERSION 3.10)
project(DemoProject)

add_subdirectory(core)
add_subdirectory(utils)
add_subdirectory(app)

然后每个子目录里定义自己的目标和依赖。比如 utils/CMakeLists.txt

add_library(utils utils.cpp)
target_link_libraries(utils PUBLIC core)

好,现在咱们生成依赖图:

mkdir build && cd build
cmake --graphviz=deps.dot ..

你会得到一个 deps.dot 文件。打开看看,里面是 Graphviz 的 DOT 语言描述:

digraph "DemoProject" {
  ...
  "utils" -> "core"
  "app" -> "core"
  "app" -> "utils"
  ...
}

然后用 Graphviz 的 dot 命令转成图片:

dot -Tpng deps.dot -o deps.png

搞定!你现在有了一张清晰的依赖关系图。

小提示:如果你没装 Graphviz,可以用 brew install graphviz(macOS)或 apt install graphviz(Ubuntu)。Windows 用户可以去官网下载安装包。

40.3 依赖图里能看到什么?

生成的 .dot 文件里,CMake 会把每个目标(库、可执行文件、自定义目标)都列出来。箭头表示「依赖关系」——从被依赖者指向依赖者。

举个例子:

  • 库目标:比如 coreutils,会显示为矩形节点。
  • 可执行文件:比如 app,会显示为椭圆节点。
  • 接口依赖:PUBLIC、PRIVATE、INTERFACE 的链接关系都会体现。

我个人习惯把生成的 .dot 文件用 dot -Tsvg 转成 SVG,这样可以直接在浏览器里缩放查看,比 PNG 方便多了。

40.4 避坑指南:我曾经踩过的坑

我曾经在一个大项目里用 --graphviz 生成依赖图,结果发现图里多了一堆莫名其妙的节点。仔细一看,原来是 add_custom_targetadd_dependencies 创建的那些「伪目标」也被列出来了。

嗯,这里要注意:

  • --graphviz 会列出所有目标,包括自定义目标。如果你的项目里有很多辅助目标(比如代码生成、测试打包),图会变得很乱。
  • 解决办法:可以用 --graphviz=output.dot --graphviz-filter=... 来过滤,但说实话,我一般直接手动删掉 .dot 文件里不需要的节点。反正就几行文本,删起来很快。

警告:如果你用了 INTERFACE 库(比如纯头文件库),依赖图里也会显示。但要注意,INTERFACE 库本身不生成任何编译产物,箭头只表示「接口依赖」。别被图里的箭头数量吓到——不是所有箭头都代表链接。

40.5 进阶:自定义依赖图的样式

默认生成的 .dot 文件样式比较朴素。你可以手动修改它,或者写个脚本批量调整。比如把不同种类的目标染成不同颜色:

// 在 .dot 文件里加样式
"core" [style=filled, fillcolor=lightblue]
"utils" [style=filled, fillcolor=lightgreen]
"app" [style=filled, fillcolor=lightyellow]

我个人习惯写一个简单的 Python 脚本,自动解析 .dot 文件,然后根据目标类型(库、可执行文件、自定义目标)设置颜色。这样每次生成依赖图后,跑一下脚本就能得到一张漂亮的彩色图。

你想想看,在代码评审或者项目汇报时,甩出一张彩色依赖图,是不是比干巴巴的 CMakeLists.txt 有说服力多了?

40.6 依赖图能帮你发现什么?

依赖图不只是「好看」。它真的能帮你发现项目里的问题:

  • 循环依赖:如果图里出现了环,说明你的库之间互相依赖。CMake 在链接时可能会报错,或者产生未定义行为。
  • 多余的依赖:有些库明明不需要,却被链接了。依赖图上一眼就能看出来——箭头指向了不该指向的节点。
  • 依赖层级过深:如果某个库被几十个目标依赖,那它可能是个「上帝库」。考虑拆分成更小的模块。

我记得有一次,我在一个项目里发现 app 居然直接依赖了 coreutils,而 utils 又依赖 core。其实 app 只需要 utils 就够了,core 的依赖应该通过 utils 传递。这就是典型的「依赖冗余」。依赖图一画,问题一目了然。

40.7 总结

--graphviz 这个选项,说白了就是 CMake 给你的一张「项目地图」。它不复杂,但非常实用。尤其是当你面对一个几十上百个目标的大型项目时,一张图比一千行 CMakeLists.txt 更直观。

我个人建议,每个项目在 CI 里都加一步生成依赖图的流程。哪怕你不看,留着以后排查问题也是好的。毕竟,你永远不知道哪天会需要搞清楚「这个库到底被谁依赖了」。

嗯,今天就聊到这儿。下次你改 target_link_libraries 的时候,不妨试试 --graphviz,说不定会有意外发现。


公众号:蓝海资料掘金营,微信 deep3321