大型 Rust 項目經驗分享

大型 Rust 項目工作空間

原文:https://matklad.github.io/2021/08/22/large-rust-workspaces.html

譯者: 韓玄亮(一個熱愛開源,喜歡 rust 的 go 開發者)

在本篇文章中,我將分享我組織大型 Rust 項目的經驗。但這絕不是權威的,只是我通過嘗試和錯誤中發現的一些小技巧。

Cargo,作爲 Rust 的構建系統,遵循約定大於配置的原則。它不僅爲小型項目提供了一套良好的默認配置集,尤其爲公共 crates.io 庫量身定做。雖然這些默認值並不完美,但它們已經足夠用了。這對整個生態系統的一致性也是值得歡迎的。

然而當涉及到大型的、多 crate 的項目時,Cargo 就不那麼統一了,它被組織成一個 Cargo 工作空間。而工作空間是靈活的 —— Cargo 對工作空間的佈局並沒有一個偏好統一。因此,人們會嘗試不同的東西,取得不同程度的效果。

回到標題,我認爲對於代碼行數在一萬到一百萬之間的項目,扁平化結構是最爲合理的。此處 rust-analyzer[1] (多達 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/

在這種情況下,有幾個原因可以說明樹形結構是低級的:

  1. crates 的 cargo 分級命名空間是扁平的。在 Cargo.toml 中不可能寫出 hir::def ,所以一般 crate 的名字中都有前綴。樹狀排列創造了另一種層次結構,這就增加了不一致的可能性。

  2. 即使是比較大的列表也比小的樹更容易讓人一目瞭然。ls ./crates 給出了項目的層級概覽,而且這看起來足夠小。

```shell
> 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 數量可能會充滿一個屏幕。

  1. 層級佈局的最後一個問題是:沒有完美的分層結構。但是對於扁平結構,增加或拆分 crates 的代價微不足道。在樹狀結構下,你需要弄清楚把新的 crate 放在哪裏,而且,如果已經沒有一個完美的匹配,你將不得不選擇以下幾種情況:

• 在頂部附近添加一個愚蠢的空的文件夾

• 添加到一個巨無霸的 utils 文件夾

• 將代碼放在一個已存在但是不是很理想的目錄中 (所以結構會隨着維護而慢慢惡化)

對於長期維護的多人項目來說,這是一個重要的問題 —— 樹狀結構往往會隨着時間的推移而惡化,而扁平結構則不怎麼需要維護。

小技巧

讓工作空間的根部成爲虛擬清單。

這可以驅使我們把 main crate 放在根目錄下,但這樣做會使根目錄被 src/ 污染,需要在每個 Cargo 命令中傳遞 -- workspace,並向其他一致的結構添加異常。

反對從文件夾名稱中去除普通前綴。

如果每個板塊的名字都和它所在的文件夾一模一樣,這讓導航和重命名就會變得更容易。反向依賴的 Cargo.toml 同時提到了文件夾和 crate 的名稱,當它們完全相同的時候就很有用。

對於大型項目來說,很多版本庫的臃腫往往來自於自動化。

Makefiles 和各種 prepare.sh 腳本。爲了避免臃腫和臨時工作流程的泛濫,可以將所有的 Rust 自動化寫在一個專門的 crate 裏。這裏安利一個有用的庫:cargo-xtask[2]。

對於你不打算髮布的內部 crate,可以使用 version = "0.0.0"。

如果你確實想發佈具有符合語義版本 API 的 crate 的子集,那麼要非常慎重對待它們。將所有這樣的 crate 提取到一個單獨的頂層文件夾,即 libs/,這樣做對未來可能是有意義的。這使得檢查 libs/ 中的東西是否使用了 crates/ 中的東西更加容易。

由一個文件組成一個 crate。

對於這些文件,我們很容易陷入:把 src 目錄展開,把 lib.rs 和 Cargo.toml 放在同一個目錄下。但是我建議不要這樣做 —— 即使 crate 現在是單文件,以後也可能會被擴展。

References

[1]rust-analyzer: h__ttps://github.com/rustanalyzer/rust-analyzer

[2]cargo-xtask: https://github.com/matklad/cargo-xtask

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/UodI-15rhYLS30FB8HGbiw