在本篇文章中,我将分享我组织大型 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/
在这种情况下,有几个原因可以说明树形结构是低级的:
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 数量可能会充满一个屏幕。
对于长期维护的多人项目来说,这是一个重要的问题 —— 树状结构往往会随着时间的推移而恶化,而扁平结构则不怎么需要维护。
让工作空间的根部成为虚拟清单。
这可以驱使我们把 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 现在是单文件,以后也可能会被扩展。