またまたRustについてです。

今回はworkspace機能についてよくわからなかったので勉強がてら書いていこうと思います。

Rustのワークスペースとは

大元であるCargoパッケージ内に複数パッケージがある場合に、相互にパッケージを扱うための機能です。

大きくなってきたプロジェクトで使用します。

Cargoのワークスペース - The Rust Programming Language 日本語版

公式のチュートリアルをなぞる形になりますが、やってみます。

まずはワークスペース機能のチュートリアルを行うCargoパッケージを作成します。

cargo new rust-workspace-playground

作成したパッケージに移動し、更にadderというパッケージを作成します。

cargo new adder

この時点でcargo buildしても、adderの依存性をrust-workspace-playgroundには記述していないため、targetに出力されるのはrust-workspace-playgroundにあるhello worldプログラムのみです。

# cargo build実行後のtargetディレクトリの中身
target
├── CACHEDIR.TAG
└── debug
    ├── build
    ├── deps
    │   ├── librust_workspace_playground-25fa51ac7f690b9a.rmeta
    │   ├── librust_workspace_playground-5cbc2718815bd853.rmeta
    │   ├── librust_workspace_playground-9f7880f2efd546b2.rmeta
    │   ├── rust_workspace_playground-01238b030f757f04
    │   ├── rust_workspace_playground-01238b030f757f04.d
    │   ├── rust_workspace_playground-25fa51ac7f690b9a.d
    │   ├── rust_workspace_playground-5cbc2718815bd853.d
    │   └── rust_workspace_playground-9f7880f2efd546b2.d
    ├── examples
    ├── incremental
    │   ├── rust_workspace_playground-1xmwk187vqtbq
    │   ├── rust_workspace_playground-3oehp425tljca
    │   ├── rust_workspace_playground-3tb4u1zdv4nwu
    ├── rust-workspace-playground
    └── rust-workspace-playground.d

ここで、rust-workspace-playgroundのCargo.tomlに以下のworkspaceセクションを追加します。

[workspace]
members = [
    "adder",
]

この状態で再度cargo buildしてみます。すると…?

target
├── CACHEDIR.TAG
└── debug
    ├── build
    ├── deps
    │   ├── adder-cbecbe60265a0fa6.d
    │   ├── adder-fe72b53491ff983c.d
    │   ├── libadder-cbecbe60265a0fa6.rmeta
    │   ├── libadder-fe72b53491ff983c.rmeta
    │   ├── librust_workspace_playground-25fa51ac7f690b9a.rmeta
    │   ├── librust_workspace_playground-5cbc2718815bd853.rmeta
    │   ├── librust_workspace_playground-9f7880f2efd546b2.rmeta
    │   ├── rust_workspace_playground-01238b030f757f04
    │   ├── rust_workspace_playground-01238b030f757f04.d
    │   ├── rust_workspace_playground-25fa51ac7f690b9a.d
    │   ├── rust_workspace_playground-5cbc2718815bd853.d
    │   └── rust_workspace_playground-9f7880f2efd546b2.d
    ├── examples
    ├── incremental
    │   ├── adder-3mb5u48tdpopp
    │   ├── adder-vwno9594lzgk
    │   ├── rust_workspace_playground-1xmwk187vqtbq
    │   ├── rust_workspace_playground-3oehp425tljca
    │   ├── rust_workspace_playground-3tb4u1zdv4nwu
    │   └── rust_workspace_playground-8wlz61uwis6j
    ├── rust-workspace-playground
    └── rust-workspace-playground.d

targetにadderの実装が増えています!

このように、ワークスペースに依存性のあるパッケージを記述すると、一緒にコンパイルしていくれるので、わざわざadderをビルドした後に親パッケージを再ビルドする必要性がなくなります。

ワークスペース内で実装が依存する場合

続けて、add-oneというライブラリを作成します。

cargo new --lib add-one

add-oneのlib.rsには以下の実装を追加します。

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

そしてこの実装をadderから使用してみます。

adderのCargo.tomlのdependenciesセクションに、add_oneへの依存を追加します。

[dependencies]
add-one = { path = "../add-one" }

依存追加したらadderからadd_oneを使用する実装を追加します。

extern crate add_one;

fn main() {
    let num = 10;
    println!("Hello, world! {} plus one is {}!", num, add_one::add_one(num));
}

adderディレクトリに移動してこれを実行してみます。

% cargo run            
   Compiling add-one v0.1.0 (/home/username/Project/study/rust-workspace-playground/add-one)
   Compiling adder v0.1.0 (/home/username/Project/study/rust-workspace-playground/adder)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s
     Running `/home/username/Project/study/rust-workspace-playground/target/debug/adder`
Hello, world! 10 plus one is 11!

依存関係の追加によって、それぞれのCargoパッケージを相互に実行することができました。

これをトップディレクトリのCargoパッケージから実行するためには、workspaceセクションにadd-oneを追加してあげます。

[workspace]
members = [
    "adder",
    "add-one", # これを追加
]

トップディレクトリに移動し、adderを指定して実行します。

% cargo run -p adder
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/adder`
Hello, world! 10 plus one is 11!

実行できました!

単にadderを実行するだけなら、workspaceセクションにadd-oneは書いていなくても実行は可能です。

[workspace]
members = [
    "adder",
    # "add-one", # コメントアウトする
]
% cargo run -p adder
    Finished dev [unoptimized + debuginfo] target(s) in 0.07s
     Running `target/debug/adder`
Hello, world! 10 plus one is 11!

このときにtargetディレクトリを見てみると、adderの依存関係をトップディレクトリのCargoパッケージが内包してくれていることが分かります。

target
├── CACHEDIR.TAG
└── debug
    ├── adder
    ├── adder.d
    ├── build
    ├── deps
    │   ├── add_one-5168b2c83d0536d9.d
    │   ├── adder-6d0954c78f9b2a9c
    │   ├── adder-6d0954c78f9b2a9c.d
    │   ├── libadd_one-5168b2c83d0536d9.rlib
    │   └── libadd_one-5168b2c83d0536d9.rmeta
    ├── examples
    └── incremental
        ├── add_one-q1oi50ut71e4
        └── adder-3qznkde7cai8g

頭いい〜

この動きは外部モジュールをインポートしている場合でも同じです。

add-oneの依存関係にrandを追加してtargetディレクトリを確認してみます。

# add-oneのCargo.toml
[dependencies]
rand = "0.8.5"

追加したら、再度トップディレクトリでcargo buildを実行してtargetディレクトリを確認してみます。

target
├── CACHEDIR.TAG
└── debug
    ├── adder
    ├── adder.d
    ├── build
    │   ├── libc-71bfd4a20ae19e3a
    │   └── libc-a761acc43f1934dd
    ├── deps # randの依存関係も全部入ってる
    │   ├── add_one-4b8dfa575ad7380d.d
    │   ├── add_one-5168b2c83d0536d9.d
    │   ├── add_one-581f1a9188327ca9.d
    │   ├── add_one-c81e23f3aee9a915.d
    │   ├── add_one-c8a2936fd4a8f7eb.d
    │   ├── adder-09bb83f0a4e3e4bf.d
    │   ├── adder-4e39b8f5b576d33e.d
    │   ├── adder-6d0954c78f9b2a9c
    │   ├── adder-6d0954c78f9b2a9c.d
    │   ├── adder-9e293660ddbcfd52.d
    │   ├── adder-c92e197a5751b803.d
    │   ├── cfg_if-14458ed2700f0b6c.d
    │   ├── getrandom-4afcf66a92bc0894.d
    │   ├── libadd_one-4b8dfa575ad7380d.rmeta
    │   ├── libadd_one-5168b2c83d0536d9.rlib
    │   ├── libadd_one-5168b2c83d0536d9.rmeta
    │   ├── libadd_one-581f1a9188327ca9.rmeta
    │   ├── libadd_one-c81e23f3aee9a915.rmeta
    │   ├── libadd_one-c8a2936fd4a8f7eb.rmeta
    │   ├── libadder-09bb83f0a4e3e4bf.rmeta
    │   ├── libadder-4e39b8f5b576d33e.rmeta
    │   ├── libadder-9e293660ddbcfd52.rmeta
    │   ├── libadder-c92e197a5751b803.rmeta
    │   ├── libc-be18f47547602408.d
    │   ├── libcfg_if-14458ed2700f0b6c.rmeta
    │   ├── libgetrandom-4afcf66a92bc0894.rmeta
    │   ├── liblibc-be18f47547602408.rmeta
    │   ├── libppv_lite86-04333437dd28a938.rmeta
    │   ├── librand-8a6d3b1609e3b558.rmeta
    │   ├── librand_chacha-f6d33a0b3d88ccc9.rmeta
    │   ├── librand_core-ba7e5bf0385b471a.rmeta
    │   ├── librust_workspace_playground-5cbc2718815bd853.rmeta
    │   ├── librust_workspace_playground-9f7880f2efd546b2.rmeta
    │   ├── ppv_lite86-04333437dd28a938.d
    │   ├── rand-8a6d3b1609e3b558.d
    │   ├── rand_chacha-f6d33a0b3d88ccc9.d
    │   ├── rand_core-ba7e5bf0385b471a.d
    │   ├── rust_workspace_playground-01238b030f757f04
    │   ├── rust_workspace_playground-01238b030f757f04.d
    │   ├── rust_workspace_playground-5cbc2718815bd853.d
    │   └── rust_workspace_playground-9f7880f2efd546b2.d
    ├── examples
    ├── incremental
    │   ├── add_one-21eecct0kd129
    │   ├── add_one-2ccc3ytqkzrjq
    │   ├── add_one-3jik0adxp4co5
    │   ├── add_one-q1oi50ut71e4
    │   ├── add_one-yp14fxs0zi82
    │   ├── adder-1cydw5mbduoj4
    │   ├── adder-3qznkde7cai8g
    │   ├── adder-5q6n4hi11en3
    │   ├── adder-afird3wdl4ox
    │   ├── adder-e598hbdk7zh7
    │   ├── rust_workspace_playground-1xmwk187vqtbq
    │   ├── rust_workspace_playground-3oehp425tljca
    │   └── rust_workspace_playground-3tb4u1zdv4nwu
    ├── rust-workspace-playground
    └── rust-workspace-playground.d

randの依存関係が入っていることが確認できました。

このケースではplayground -> adder -> add-one -> randという依存関係になっているため、workspaceにadderを指定しただけでキレイにすべての依存関係が入ります。

テストの一括実行

ワークスペースで管理するメリットは他にもあります。

トップディレクトリでcargo testをするだけで、ワークスペースに登録されているCargoパッケージのテストが一斉に実行できます。

試しにadd-oneにテストコードを実装します。

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));
    }
}

これをトップディレクトリから実行できるか試してみます。

# rust-workspace-playgroundから実行
% cargo test
   Compiling rust-workspace-playground v0.1.0 (/home/username/Project/study/rust-workspace-playground)
    Finished test [unoptimized + debuginfo] target(s) in 0.43s
     Running unittests src/main.rs (target/debug/deps/rust_workspace_playground-8d966a1f0e1420d6)

running 0 tests

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

あら、実行できませんでした。

先程workspaceからadd-oneの設定を抜いていたためでした。

このように、テストコードは依存関係とは関係がないので、workspaceで定義されていないと実行されません。

(まぁそりゃ依存先のテストコードが全部実行されたら面倒ですもんね…)

add-oneのコメントアウトを復活させます。

[workspace]
members = [
    "adder",
    "add-one", # コメントアウトを外す
]

再度実行します。

% cargo test
    Finished test [unoptimized + debuginfo] target(s) in 0.01s
     Running unittests src/main.rs (target/debug/deps/rust_workspace_playground-8d966a1f0e1420d6)

running 0 tests

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

あれ、思ったとおりの出力にならない…

どうやらworkspaceのすべてのテストを実行するには、cargo test--workspaceオプションをつける必要があるみたいです。

アップデートの影響かな?

% cargo test --workspace
   Compiling adder v0.1.0 (/home/username/Project/study/rust-workspace-playground/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.37s
     Running unittests src/lib.rs (target/debug/deps/add_one-9254bbc45c7edb6a)

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-6b8696df6a92a81f)

running 0 tests

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

     Running unittests src/main.rs (target/debug/deps/rust_workspace_playground-8d966a1f0e1420d6)

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をコメントアウトした状態でcargo test --workspaceを実行しても、テストが認識されませんでした。

% cargo test --workspace
    Finished test [unoptimized + debuginfo] target(s) in 0.01s
     Running unittests src/lib.rs (target/debug/deps/add_one-9254bbc45c7edb6a)

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-6b8696df6a92a81f)

running 0 tests

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

     Running unittests src/main.rs (target/debug/deps/rust_workspace_playground-8d966a1f0e1420d6)

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

2023-08-07 追記

workspaceセクションに書けるすべてのプロパティはここに書いてありました。

Workspaces - The Cargo Book

  • members
    • ワークスペースに含めるパッケージ。
  • resolver
    • 使用する依存性リゾルバを設定します。
  • exclude
    • ワークスペースから除外するパッケージ。
  • default-members
    • 特定のパッケージが選択されていない場合に操作するパッケージ。
    • cargo run --workspaceで指定しない場合に実行されるデフォルトのパッケージのこと
    • 複数選択できるのはなぜ…?
  • package
    • パッケージを継承するためのキー。後述するサンプルを参考。
  • dependencies
    • パッケージの依存関係を継承するためのキー。後述するサンプルを参考。
  • metadata
    • 外部ツール用の追加設定。通常は使用しない。

packageは面白い機能で、子パッケージに親パッケージのプロパティを継承するときに使用するプロパティのようです。

# 親パッケージ
# [PROJECT_DIR]/Cargo.toml
[workspace]
members = ["bar"]

[workspace.package] # 子のパッケージに継承する情報
version = "1.2.3"
authors = ["Nice Folks"]
description = "A short description of my package"
documentation = "https://example.com/bar"
# 子パッケージ
# [PROJECT_DIR]/bar/Cargo.toml
[package]
name = "bar"
version.workspace = true
authors.workspace = true
description.workspace = true
documentation.workspace = true

dependenciesも子パッケージに依存関係を継承するための機能になっています。

[依存パッケージ].workspace = trueとするだけで継承できます。

# 親パッケージ
# [PROJECT_DIR]/Cargo.toml
[workspace]
members = ["bar"]

[workspace.dependencies]
cc = "1.0.73"
rand = "0.8.5"
regex = { version = "1.6.0", default-features = false, features = ["std"] }
# 子パッケージ
# [PROJECT_DIR]/bar/Cargo.toml
[package]
name = "bar"
version = "0.2.0"

[dependencies]
regex = { workspace = true, features = ["unicode"] }

[build-dependencies]
cc.workspace = true

[dev-dependencies]
rand.workspace = true

ちなみに、このドキュメント読んでて気づいたんですが、[package]セクションを実装しているCargo.tomlを配置したパッケージをルートパッケージといい、その子として配置した[package]セクションのないCargo.tomlを配置したパッケージをバーチャルワークスペースと呼ぶようです。

まとめ

プロジェクトの規模によって使い分けると便利そうですね。

個人的には、モノリポ運用では役に立ちそうだなと感じています。

参考

Cargoのワークスペース