Cargoのワークスペース
第12章で、バイナリクレートとライブラリクレートを含むパッケージを構築しました。プロジェクトの開発が進むにつれて、 ライブラリクレートの肥大化が続き、その上で複数のライブラリクレートにパッケージを分割したくなることでしょう。 Cargoはワークスペースという協調して開発された関連のある複数のパッケージを管理するのに役立つ機能を提供しています。
ワークスペースを生成する
ワークスペースは、同じCargo.lockと出力ディレクトリを共有する一連のパッケージです。
ワークスペースを使用したプロジェクトを作成し、ワークスペースの構造に集中できるよう、瑣末なコードを使用しましょう。
ワークスペースを構築する方法は複数あるので、よく使われる方法だけを提示しましょう。バイナリ1つとライブラリ2つを含むワークスペースを作ります。
バイナリは、主要な機能を提供しますが、2つのライブラリに依存しています。
一方のライブラリは、add_one関数を提供し、2番目のライブラリは、add_two関数を提供します。
これら3つのクレートが同じワークスペースの一部になります。ワークスペース用の新しいディレクトリを作ることから始めましょう:
$ mkdir add
$ cd add
次にaddディレクトリにワークスペース全体を設定するCargo.tomlファイルを作成します。
このファイルには[package]セクションはありません。
代わりに、バイナリクレートを含むパッケージへのパスを指定することでワークスペースにメンバを追加させてくれる[workspace]セクションから開始します;
今回の場合、そのパスはadderです:
ファイル名: Cargo.toml
[workspace]
members = [
"adder",
]
次に、addディレクトリ内でcargo newを実行することでadderバイナリクレートを作成しましょう:
$ 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/adder/targetではなく、add/targetに落ち着くでしょう。ワークスペースのクレートは、
お互いに依存しあうことを意味するので、Cargoはワークスペースのtargetディレクトリをこのように構成します。
各クレートがtargetディレクトリを持っていたら、各クレートは自身のtargetディレクトリにワークスペースの他のクレートの生成物を置くために、
ワークスペースの他のクレートを再コンパイルしなくてはならなくなるでしょう。一つのtargetディレクトリを共有することで、
クレートは不必要な再ビルドを回避できるのです。
ワークスペース内に2番目のパッケージを作成する
次に、ワークスペースに別のメンバパッケージを作成し、add_oneと呼びましょう。
最上位のCargo.tomlを変更してmembersリストでadd_oneパスを指定するようにしてください:
ファイル名: Cargo.toml
[workspace]
members = [
"adder",
"add_one",
]
それから、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はワークスペースのクレートが、お互いに依存しているとは想定していないので、 依存関係について明示する必要があります。
次に、adderクレート内で(add_oneクレートの)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;
// "こんにちは世界!{num}+1は{}!"
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
リスト14-7: adderクレートからadd_oneライブラリクレートを使用する
最上位の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ディレクトリからバイナリクレートを実行するには、-p引数とパッケージ名をcargo runと共に使用して、
実行したいワークスペースのパッケージを指定することができます:
$ cargo run -p adder
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
これにより、adder/src/main.rsのコードが実行され、これはadd_oneクレートに依存しています。
ワークスペースの外部パッケージに依存する
ワークスペースには、各クレートのディレクトリそれぞれにCargo.lockが存在するのではなく、
最上位階層にただ一つのCargo.lockが存在するだけのことに注目してください。
これにより、全クレートが全依存の同じバージョンを使用していることが確認されます。
randパッケージをadder/Cargo.tomlとadd_one/Cargo.tomlファイルに追加すると、
Cargoは両者をあるバージョンのrandに解決し、それを一つのCargo.lockに記録します。
ワークスペースの全クレートに同じ依存を使用させるということは、
クレートが相互に互換性を常に維持するということになります。
add_oneクレートでrandクレートを使用できるように、
add_one/Cargo.tomlファイルの[dependencies]セクションにrandクレートを追加しましょう:
ファイル名: add_one/Cargo.toml
[dependencies]
rand = "0.8.5"
これで、add_one/src/lib.rsファイルにuse rand;を追加でき、
addディレクトリでcargo buildを実行することでワークスペース全体をビルドすると、
randクレートを持ってきてコンパイルするでしょう。
スコープ内に持ち込んだrandを参照していないので、警告が出るでしょう:
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
--snip--
Compiling rand v0.8.5
Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
(警告: 未使用のインポート: `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は、randに対するadd_oneの依存の情報を含むようになりました。
ですが、randはワークスペースのどこかで使用されているにも関わらず、それぞれのCargo.tomlファイルにも、
randを追加しない限り、ワークスペースの他のクレートでそれを使用することはできません。
例えば、adderパッケージのadder/src/main.rsファイルにuse rand;を追加すると、
エラーが出ます:
$ cargo build
--snip--
Compiling adder v0.1.0 (file:///projects/add/adder)
error[E0432]: unresolved import `rand`
(エラー: 未解決のインポート`rand`)
--> adder/src/main.rs:2:5
|
2 | use rand;
| ^^^^ no external crate `rand`
| (外部クレート`rand`は存在しません)
これを修正するには、adderパッケージのCargo.tomlファイルを編集し、これもrandに依存していることを示してください。
adderパッケージをビルドすると、randをCargo.lockのadderの依存一覧に追加しますが、
randのファイルが追加でダウンロードされることはありません。Cargoが、
ワークスペース内でrandパッケージを使用するすべてのパッケージ内のすべてのクレートが、
同じバージョンを使用することを保証してくれるのです。これによりスペースを節約し、
ワークスペースのクレートが相互に互換性があることを保証してくれます。
ワークスペースにテストを追加する
さらなる改善として、add_oneクレート内にadd_one::add_one関数のテストを追加しましょう:
ファイル名: 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を走らせると、ワークスペースの全クレートのテストを実行します:
$ 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クレートのit_worksテストが通ったことを示しています。
次の区域には、adderクレートにはテストが見つからなかったことが示され、
さらに最後の区域には、add_oneクレートにドキュメンテーションテストは見つからなかったと表示されています。
-pフラグを使用し、テストしたいクレートの名前を指定することで最上位ディレクトリから、
ワークスペースのある特定のクレート用のテストを実行することもできます:
$ 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クレートのテストのみを実行し、adderクレートのテストは実行しなかったことを示しています。
ワークスペースのクレートを crates.io に公開したら、ワークスペースのクレートは個別に公開される必要があります。
cargo testのように、-pフラグを使用して公開したいクレートの名前を指定することで、ワークスペース内の特定のクレートを公開することができます。
鍛錬を積むために、add_oneクレートと同様の方法でワークスペースにadd_twoクレートを追加してください!
プロジェクトが肥大化してきたら、ワークスペースの使用を考えてみてください: 大きな一つのコードの塊よりも、 微細で個別のコンポーネントの方が理解しやすいです。またワークスペースにクレートを保持することは、 同時に変更されることが多いのなら、クレート間の協調をしやすくなることにも繋がります。