大型 Rust 我的项目工作空间
原文:https://matklad.github.io/2021/08/22/large-rust-workspaces.html
在本篇文章中,我将分享我组织大型 Rust 我的项目的教训。但这绝不是权威的,只是我通过尝试和谬误中发现的一些小技巧。
Cargo,作为 Rust 的构建零碎,遵循约定大于配置的准则。它不仅为小型我的项目提供了一套良好的默认配置集,尤其为公共 crates.io 库量身定做。尽管这些默认值并不完满,但它们曾经足够用了。这对整个生态系统的一致性也是值得欢送的。
然而当波及到大型的、多 crate 的我的项目时,Cargo 就不那么对立了,它被组织成一个 Cargo 工作空间。而工作空间是灵便的 —— Cargo 对工作空间的布局并没有一个偏好对立。因而,人们会尝试不同的货色,获得不同水平的成果。
回到题目,我认为对于代码行数在一万到一百万之间的我的项目,扁平化构造是最为正当的。此处 rust-analyzer (多达 200k 行)是一个比拟好的例子,它的我的项目组织如下:
rust-analyzer/
Cargo.toml
Cargo.lock
crates/
rust-analyzer/
hir/
hir_def/
hir_ty/
...
在 repo 的根部,Cargo.toml 定义了一个虚构清单:
Cargo.toml:
[workspace]
members = ["crates/*"]
其余的货色 (包含 rust-analyzer “main” crate) 都嵌套在 crates/
下的某一个层级中。每个目录的名称都等于 crate
的名称。
crates/hir_def/Cargo.toml:
[package]
name = "hir_def"
version = "0.0.0"
edition = "2018"
在撰写本文时,crates/
中有 32 个不同子文件夹。
扁平式比嵌套式更好
乏味的是,这个倡议和按层级组织的习惯偏向 (注:依照咱们平时开发习惯来说) 刚好对抗:
rust-analyzer/
Cargo.toml
src/
hir/
Cargo.toml
src/
def/
ty/
在这种状况下,有几个起因能够阐明树形构造是低级的:
- crates 的 cargo 分级命名空间是扁平的。在
Cargo.toml
中不可能写出hir::def
,所以个别 crate 的名字中都有前缀。树状排列发明了另一种层次结构,这就减少了不统一的可能性。 - 即便是比拟大的列表也比小的树更容易让人高深莫测。
ls ./crates
给出了我的项目的层级概览,而且这看起来足够小。
> ls ./crates
base_db
cfg
flycheck
hir
hir_def
hir_expand
hir_ty
ide
ide_assists
ide_completion
ide_db
ide_diagnostics
ide_ssr
limit
mbe
parser
paths
proc_macro_api
proc_macro_srv
proc_macro_test
profile
project_model
rust-analyzer
sourcegen
stdx
syntax
test_utils
text_edit
toolchain
tt
vfs
在基于树状构造的布局做同样的事件比拟艰难的:
- 只从单层级上看并不能通知你哪些文件夹蕴含嵌套的 crate
- 而在所有层级上看又会列出太多的文件夹(无关文件烦扰视觉)
正确形式 : 只看蕴含 Cargo.toml 的文件夹能够失去正确的后果,但并没有 ls
那样简略。
嵌套构造的确比扁平构造更容易扩大。但常数很重要 —— 在你达到一百万行代码之前,我的项目中的 crates 数量可能会充斥一个屏幕。
- 层级布局的最初一个问题是:没有完满的分层构造。然而对于扁平构造,减少或拆分 crates 的代价微不足道。在树状构造下,你须要弄清楚把新的 crate 放在哪里,而且,如果曾经没有一个完满的匹配,你将不得不抉择以下几种状况:
- 在顶部左近增加一个愚昧的空的文件夹
- 增加到一个巨无霸的 utils 文件夹
- 将代码放在一个已存在然而不是很现实的目录中 ( 所以构造会随着保护而缓缓好转)
对于长期保护的多人我的项目来说,这是一个重要的问题 —— 树状构造往往会随着工夫的推移而好转,而扁平构造则不怎么须要保护。
小技巧
让工作空间的根部成为虚构清单。
这能够驱使咱们把 main crate 放在根目录下,但这样做会使根目录被 src/
净化,须要在每个 Cargo 命令中传递 --workspace
,并向其余统一的构造增加异样。
拥护从文件夹名称中去除一般前缀。
如果每个板块的名字都和它所在的文件夹截然不同,这让导航和重命名就会变得更容易。反向依赖的 Cargo.toml 同时提到了文件夹和 crate 的名称,当它们完全相同的时候就很有用。
对于大型项目来说,很多版本库的臃肿往往来自于自动化。
Makefiles 和各种 prepare.sh 脚本。为了防止臃肿和长期工作流程的泛滥,能够将所有的 Rust 自动化写在一个专门的 crate 里。这里安利一个有用的库:cargo-xtask。
对于你不打算公布的外部 crate,能够应用
version = "0.0.0"
。
如果你的确想公布具备合乎语义版本 API 的 crate 的子集,那么要十分慎重对待它们。将所有这样的 crate 提取到一个独自的顶层文件夹,即 libs/
,这样做对将来可能是有意义的。这使得查看 libs/
中的货色是否应用了 crates/
中的货色更加容易。
由一个文件组成一个 crate。
对于这些文件,咱们很容易陷入:把 src
目录开展,把 lib.rs 和 Cargo.toml 放在同一个目录下。然而我倡议不要这样做 —— 即便 crate 当初是单文件,当前也可能会被扩大。