可视化项目成员包的调用关系
作者: 吴翱翔
通常一个大型 Rust 项目都会用 cargo workspace 来管理 workspace 下面有多个 member 也叫 package 或者叫 crate
伞结构?
在《张汉东的Rust实战课》鉴赏 Rust 各个知名项目的视频分集中
经常会提到某某项目用的是 伞结构,但是 伞结构 又是什么意思呢?
借助 rust-analyzer 可视化 API 如上图就是 clippy 源码内各个库的依赖关系
可见 clippy_lints package 往下依赖很多子 package 但各个子 package 之间没有任何互相依赖
所以 clippy 源码这种项目就叫 伞结构 通过可视化工具发现确实很像 一把倒立的雨伞
本文介绍基于 rust-analyzer 公有 API 对项目中各个 package 依赖关系进行可视化
导入 rust-analyzer 库
rust-analyzer 的 lsp-server 的工作原理是 vscode 创建 rust-analyzer 子进程
然后 vscode 跟 rust-analyzer 之间通过两个管道借助 STDIN/STDOUT 进行通信
其实 rust-analyzer 还可以作为一个库调用它的 API
由于公开的接口尚未稳定频繁改动,所以 rust-analyzer 并没上传到 crates.io
我们可以将 rust-analyzer 源码下载到本地,在 Cargo.toml 下加上以下内容就可以引入
[dependencies]
# rust-analyzer commit hash dd21ad6a5e8ffa166c97447212d3da0f86555aee
rust-analyzer = { path = "../rust-analyzer/crates/rust-analyzer" }
project_model = { path = "../rust-analyzer/crates/project_model" }
paths = { path = "../rust-analyzer/crates/paths" }
syntax = { path = "../rust-analyzer/crates/syntax" } # AST
base_db = { path = "../rust-analyzer/crates/base_db" }
hir = { path = "../rust-analyzer/crates/hir" }
hir_expand = { path = "../rust-analyzer/crates/hir_expand" }
ide = { path = "../rust-analyzer/crates/ide" }
ide_db = { path = "../rust-analyzer/crates/ide_db" }
vfs = { path = "../rust-analyzer/crates/vfs" }
load_cargo API 加载需要分析的项目
假设我们想要分析 rust-analyzer 源码中各个模块库的调用关系
#![allow(unused)] fn main() { let manifest_path = "/home/w/repos/clone_repos/rust-analyzer/Cargo.toml"; let manifest_path: paths::AbsPathBuf = manifest_path.try_into().unwrap(); let manifest = project_model::ProjectManifest::from_manifest_file(manifest_path).unwrap(); let workspace = project_model::ProjectWorkspace::load( manifest, &project_model::CargoConfig::default(), &|_| {}, ) .unwrap(); }
通过 project_model::ProjectWorkspace::load
加载出 workspace 信息后
此时我们可以遍历打印出该项目的 cargo workspace 下面总共有多少个 crate
#![allow(unused)] fn main() { // traverse all cargo_package(members) in cargo_workspace for package in workspace.to_roots() { if !package.is_local { continue; } let package_path: &std::path::Path = package.include[0].as_ref(); println!("found package {}", package_path.to_str().unwrap()); } }
接着开始分析项目并生成 graphviz 格式的依赖关系图
#![allow(unused)] fn main() { let (analysis_host, _vfs, _proc_macro_srv_opt) = rust_analyzer::cli::load_cargo::load_workspace( workspace, &rust_analyzer::cli::load_cargo::LoadCargoConfig { load_out_dirs_from_check: false, with_proc_macro: false, prefill_caches: false, }, ) .unwrap(); let analysis = host.analysis(); // graphviz 文件格式的 dot 图 let is_include_std_and_dependencies_crate = false; let dot: String = analysis .view_crate_graph(is_include_std_and_dependencies_crate) .unwrap() .unwrap(); // 再把 dot 图字符串写入到文件中 let file_path = concat!(env!("CARGO_MANIFEST_DIR"), "/target/graph.gv"); let mut f = std::fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .open(file_path) .unwrap(); std::io::Write::write_all(&mut f, dot.as_bytes()).unwrap(); // 最后调用 xdot 可视化 graphviz let is_success = std::process::Command::new("xdot") .arg(file_path) .spawn() .unwrap() .wait() .unwrap() .success(); assert!(is_success); }
更简单的可视化方法
我在阅读 rust-analyzer 源码后发现其实 rust-analyzer 本身就提供 "crate graph" 的 vscode 如下图