Cargo 工作空間

在第十二章中,我們建立的套件包含一個執行檔 crate 與一個函式庫 crate。隨著專案開發,你可能會發現函式庫 crate 變得越來越大,而你可能會想要將套件拆成數個函式庫 crate。Cargo 提供了一個功能叫做工作空間(workspaces)能來幫助管理並開發數個相關的套件。

建立工作空間

工作空間是一系列的共享相同 Cargo.lock 與輸出目錄的套件。讓我們建立個使用工作空間的專案,我們會使用簡單的程式碼,好讓我們能專注在工作空間的架構上。組織工作空間的架構有很多種方式,我們會介紹其中一種常見的方式。我們的工作空間將會包含一個執行檔與兩個函式庫。執行檔會提供主要功能,並依賴其他兩個函式庫。其中一個函式庫會提供函式 add_one,而另一個函式庫會提供函式 add_two。這三個 crate 會包含在相同的工作空間中,我們先從建立工作空間的目錄開始:

$ mkdir add
$ cd add

接著在 add 目錄中,我們建立會設置整個工作空間的 Cargo.toml 檔案。此檔案不會有 [package] 段落。反之,他會使用一個 [workspace] 段落作為起始,讓我們可以透過指定執行檔 crate 的套件路徑來將它加到工作空間的成員中。在此例中,我們的路徑是 adder

檔案名稱:Cargo.toml

[workspace]

members = [
    "adder",
]

接下來我們會在 add 目錄下執行 cargo new 來建立 adder 執行檔 crate:

$ cargo new adder
     Created binary (application) `adder` package

在這個階段,我們已經可以執行 cargo build 來建構工作空間。目錄 add 底下的檔案應該會看起來像這樣:

├── Cargo.lock
├── Cargo.toml
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

工作空間在頂層有一個 target 目錄用來儲存編譯結果。adder 套件不會有自己的 target 目錄。就算我們在 adder 目錄底下執行 cargo build,編譯結果仍然會在 add/target 底下而非 add/adder/target。Cargo 之所以這樣組織工作空間的 target 目錄是因為工作空間的 crate 是會彼此互相依賴的。如果每個 crate 都有自己的 target 目錄,每個 crate 就得重新編譯工作空間中的其他每個 crate 才能將編譯結果放入它們自己的 target 目錄。共享 target 目錄的話,crate 可以避免不必要的重新建構。

在工作空間中建立第二個套件

接下來讓我們在工作空間中建立另一個套件成員 add_one。請修改頂層 Cargo.toml 來指定 add_one 的路徑到 members 列表中:

檔案名稱:Cargo.toml

[workspace]

members = [
    "adder",
    "add_one",
]

然後產生新的函式庫 crate add_one

$ cargo new add_one --lib
     Created library `add_one` package

add 目錄現在應該要擁有這些目錄與檔案:

├── Cargo.lock
├── Cargo.toml
├── add_one
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

add_one/src/lib.rs 檔案中,讓我們加上一個函式 add_one

檔案名稱:add_one/src/lib.rs

pub fn add_one(x: i32) -> i32 {
    x + 1
}

現在我們可以讓我們 adder 套件的執行檔依賴擁有函式庫的 add_one 套件。首先,我們需要將 add_one 的路徑依賴加到 adder/Cargo.toml

檔案名稱:adder/Cargo.toml

[dependencies]
add_one = { path = "../add_one" }

Cargo 不會假設工作空間下的 crate 會彼此依賴,我們要指定彼此之間依賴的關係。

接著讓我們在 adder 內使用 add_one crate 的 add_one 函式。開啟 adder/src/main.rs 檔案並在最上方加上 use 來將 add_one 函式庫引入作用域。然後變更 main 函式來呼叫 add_one 函式,如範例 14-7 所示。

檔案名稱:adder/src/main.rs

use add_one;

fn main() {
    let num = 10;
    println!(
        "你好,世界!{num} 加一會是 {}!", add_one::add_one(num);
    );
}

範例 14-7:在 adder crate 中使 add_one 函式庫 crate

讓我們在頂層的 add 目錄執行 cargo build 來建構工作空間吧!

$ cargo build
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished dev [unoptimized + debuginfo] target(s) in 0.68s

要執行 add 目錄的執行檔 crate,我們可以透過 -p 加上套件名稱使用 cargo run 來執行我們想要在工作空間中指定的套件:

$ cargo run -p adder
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/adder`
你好,世界!10 加一會是 11!

這就會執行 adder/src/main.rs 的程式碼,其依賴於 add_one crate。

在工作空間中依賴外部套件

注意到工作空間只有在頂層有一個 Cargo.lock 檔案,而不是在每個 crate 目錄都有一個 Cargo.lock。這確保所有的 crate 都對所有的依賴使用相同的版本。如果我們加了 rand 套件到 adder/Cargo.tomladd_one/Cargo.toml 檔案中,Cargo 會將兩者的版本解析為同一個 rand 版本並記錄到同個 Cargo.lock 中。確保工作空間所有 crate 都會使用相同依賴代表工作空間中的 crate 永遠都彼此相容。讓我們將 rand crate 加到 add_one/Cargo.toml 檔案的 [dependencies] 段落中,使 add_one crate 可以使用 rand crate:

檔案名稱:add_one/Cargo.toml

rand = "0.8.5"

我們現在就可以將 use rand; 加到 add_one/src/lib.rs 檔案中,接著在 add 目錄下執行 cargo build 來建構整個工作空間就會引入並編譯 rand crate。我們會得到一個警告,因爲我們還沒有開始使用引入作用域的 rand

$ cargo build
    Updating crates.io index
  Downloaded rand v0.8.5
   --省略--
   Compiling rand v0.8.5
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
 --> add_one/src/lib.rs:1:5
  |
1 | use rand;
  |     ^^^^
  |
  = note: `#[warn(unused_imports)]` on by default
warning: `add_one` (lib) generated 1 warning
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished dev [unoptimized + debuginfo] target(s) in 10.18s

頂層的 Cargo.lock 現在就包含 add_onerand 作為依賴的資訊。不過就算我們能在工作空間的某處使用 rand,並不代表我們可以在工作空間的其他 crate 中使用它,除非它們的 Cargo.toml 也加上了 rand。舉例來說,如果我們將 use rand; 加到 adder/src/main.rs 檔案中想讓 adder 套件也使用的話,我們就會得到錯誤:

$ cargo build
  --省略--
   Compiling adder v0.1.0 (file:///projects/add/adder)
error[E0432]: unresolved import `rand`
 --> adder/src/main.rs:2:5
  |
2 | use rand;
  |     ^^^^ no external crate `rand`

要修正此問題,只要修改 adder 套件的 Cargo.toml 檔案,指示它也加入 rand 作為依賴就好了。這樣建構 adder 套件就會將在 Cargo.lock 中將 rand 加入 adder 的依賴,但是沒有額外的 rand 會被下載。Cargo 會確保工作空間中每個套件的每個 crate 都會使用相同的 rand 套件版本。這可以節省空間,並能確保工作空間中的 crate 彼此可以互相兼容。

在工作空間中新增測試

讓我們再進一步加入一個測試函式 add_one::add_oneadd_one crate 之中:

檔案名稱:add_one/src/lib.rs

pub fn add_one(x: i32) -> i32 {
    x + 1
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        assert_eq!(3, add_one(2));
    }
}

現在在頂層的 add 目錄執行 cargo test。像這樣在工作空間的架構下執行 cargo test 會執行工作空間下所有 crate 的測試:

$ cargo test
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.27s
     Running unittests src/lib.rs (target/debug/deps/add_one-f0253159197f7841

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/adder-49979ff40686fa8e

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests add_one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

輸出的第一個段落顯示了 add_one crate 中的 it_works 測試通過。下一個段落顯示 adder crate 沒有任何測試,然後最後一個段落顯示 add_one 中沒有任何技術文件測試。

我們也可以在頂層目錄使用 -p 並指定我們想測試的 crate 名稱來測試工作空間中特定的 crate:

$ cargo test -p add_one
    Finished test [unoptimized + debuginfo] target(s) in 0.00s
     Running unittests src/lib.rs (target/debug/deps/add_one-b3235fea9a156f74

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests add_one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

此輸出顯示 cargo test 只執行了 add_one crate 的測試並沒有執行 adder crate 的測試。

如果你想要發佈工作空間的 crate 到 crates.io,工作空間中的每個 crate 必須分別獨自發佈。和 cargo test 一樣,我們可以用 -p 的選項來指定想要的 crate 名稱,來發布工作空間內的特定 crate。

之後想嘗試練習的話,你可以在工作空間中在加上 add_two crate,方式和 add_one crate 類似!

隨著你的專案成長,你可以考慮使用工作空間:拆成各個小部分比一整塊大程式還更容易閱讀。再者,如果需要經常同時修改的話,將 crate 放在同個工作空間中更易於彼此的協作。