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 实战:生成你的第一张依赖图
咱们拿一个简单的项目来演示。假设你有三个库:core、utils、app。其中 app 依赖 core 和 utils,utils 又依赖 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 会把每个目标(库、可执行文件、自定义目标)都列出来。箭头表示「依赖关系」——从被依赖者指向依赖者。
举个例子:
- 库目标:比如
core、utils,会显示为矩形节点。 - 可执行文件:比如
app,会显示为椭圆节点。 - 接口依赖:PUBLIC、PRIVATE、INTERFACE 的链接关系都会体现。
我个人习惯把生成的 .dot 文件用 dot -Tsvg 转成 SVG,这样可以直接在浏览器里缩放查看,比 PNG 方便多了。
40.4 避坑指南:我曾经踩过的坑
我曾经在一个大项目里用 --graphviz 生成依赖图,结果发现图里多了一堆莫名其妙的节点。仔细一看,原来是 add_custom_target 和 add_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 居然直接依赖了 core 和 utils,而 utils 又依赖 core。其实 app 只需要 utils 就够了,core 的依赖应该通过 utils 传递。这就是典型的「依赖冗余」。依赖图一画,问题一目了然。
40.7 总结
--graphviz 这个选项,说白了就是 CMake 给你的一张「项目地图」。它不复杂,但非常实用。尤其是当你面对一个几十上百个目标的大型项目时,一张图比一千行 CMakeLists.txt 更直观。
我个人建议,每个项目在 CI 里都加一步生成依赖图的流程。哪怕你不看,留着以后排查问题也是好的。毕竟,你永远不知道哪天会需要搞清楚「这个库到底被谁依赖了」。
嗯,今天就聊到这儿。下次你改 target_link_libraries 的时候,不妨试试 --graphviz,说不定会有意外发现。