Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cargo ワークスペース

第12章では、バイナリクレートとライブラリクレートを含むパッケージを構築しました。プロジェクトの開発が進むにつれて、ライブラリクレートがどんどん大きくなり、パッケージをさらに複数のライブラリクレートに分割したくなるかもしれません。Cargo には workspaces と呼ばれる機能があり、並行して開発される複数の関連パッケージの管理に役立ちます。

ワークスペースを作成する

workspace とは、同じ Cargo.lock と出力ディレクトリを共有するパッケージの集合です。ワークスペースを使ったプロジェクトを作ってみましょう。ワークスペースの構造に集中できるように、コードは簡単なものを使います。ワークスペースの構成方法はいくつかありますが、ここでは一般的な方法のひとつだけを示します。バイナリ 1 つとライブラリ 2 つを含むワークスペースを用意します。メインの機能を提供するバイナリは、2 つのライブラリに依存します。1 つのライブラリは add_one 関数を提供し、もう 1 つのライブラリは add_two 関数を提供します。これら 3 つのクレートは同じワークスペースの一部になります。まず、ワークスペース用の新しいディレクトリを作成するところから始めましょう。

$ mkdir add
$ cd add

次に、add ディレクトリ内に、ワークスペース全体を設定する Cargo.toml ファイルを作成します。このファイルには [package] セクションはありません。代わりに、ワークスペースにメンバーを追加できるようにする [workspace] セクションから始まります。また、resolver の値を "3" に設定することで、Cargo の依存関係解決アルゴリズムの最新かつ最良のバージョンをワークスペースで使うようにしています。

ファイル名: Cargo.toml

[workspace]
resolver = "3"

次に、add ディレクトリ内で cargo new を実行して、adder バイナリクレートを作成します。

$ cargo new adder
     Created binary (application) `adder` package
      Adding `adder` as member of workspace at `file:///projects/add`

ワークスペース内で cargo new を実行すると、新しく作成されたパッケージは、ワークスペースの Cargo.toml にある [workspace] 定義の members キーにも自動的に追加されます。次のようになります。

[workspace]
resolver = "3"
members = ["adder"]

この時点で、cargo build を実行すればワークスペースをビルドできます。add ディレクトリ内のファイルは次のようになっているはずです。

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

ワークスペースには最上位レベルに 1 つの target ディレクトリがあり、コンパイルされた生成物はそこに配置されます。adder パッケージは独自の target ディレクトリを持ちません。たとえ adder ディレクトリの中から cargo build を実行したとしても、コンパイルされた生成物は add/adder/target ではなく add/target に出力されます。Cargo がワークスペース内の target ディレクトリをこのように構成するのは、ワークスペース内のクレート同士が相互に依存することを想定しているからです。各クレートが独自の target ディレクトリを持っていた場合、各クレートは生成物を自分の target ディレクトリに配置するために、ワークスペース内のほかの各クレートを毎回再コンパイルしなければなりません。1 つの target ディレクトリを共有することで、クレートは不要な再ビルドを避けられます。

ワークスペース内に 2 つ目のパッケージを作成する

次に、ワークスペース内に別のメンバーパッケージを作成し、add_one と名付けましょう。add_one という名前の新しいライブラリクレートを生成します。

$ cargo new add_one --lib
     Created library `add_one` package
      Adding `add_one` as member of workspace at `file:///projects/add`

最上位の Cargo.toml には、members リスト内に add_one のパスが含まれるようになります。

ファイル名: Cargo.toml

[workspace]
resolver = "3"
members = ["adder", "add_one"]

これで 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 パッケージに依存できるようになります。まず、adder/Cargo.tomladd_one へのパス依存関係を追加する必要があります。

ファイル名: adder/Cargo.toml

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

Cargo は、ワークスペース内のクレート同士が相互に依存することを前提にはしていないため、依存関係を明示的に記述する必要があります。

次に、adder クレートで add_one 関数(add_one クレートから提供されるもの)を使いましょう。adder/src/main.rs ファイルを開き、リスト14-7のように main 関数を変更して add_one 関数を呼び出します。

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

最上位の 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` profile [unoptimized + debuginfo] target(s) in 0.22s

add ディレクトリからバイナリクレートを実行するには、cargo run-p 引数とパッケージ名を使って、ワークスペース内のどのパッケージを実行するかを指定できます。

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

これにより、add_one クレートに依存している adder/src/main.rs のコードが実行されます。

外部パッケージに依存する

ワークスペースの最上位レベルには Cargo.lock ファイルが 1 つだけあり、 各クレートのディレクトリごとに Cargo.lock があるわけではないことに注目してください。これにより、 すべてのクレートがすべての依存関係について同じバージョンを使うことが保証されます。adder/Cargo.toml ファイルと add_one/Cargo.toml ファイルに rand パッケージを追加すると、Cargo は その両方を 1 つの 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 を 参照していないため、警告が 1 つ表示されます:

$ 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`
 --> add_one/src/lib.rs:1:5
  |
1 | use rand;
  |     ^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: `add_one` (lib) generated 1 warning (run `cargo fix --lib -p add_one` to apply 1 suggestion)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s

最上位の Cargo.lock には、add_onerand への依存関係に関する情報が含まれるようになりました。しかし、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`
 --> adder/src/main.rs:2:5
  |
2 | use rand;
  |     ^^^^ no external crate `rand`

これを修正するには、adder パッケージの Cargo.toml ファイルを編集し、 rand がそれにとっても依存関係であることを示します。adder パッケージをビルドすると、 Cargo.lock 内の adder の依存関係一覧に rand が追加されますが、 rand の追加のコピーがダウンロードされることはありません。Cargo は、ワークスペース内の すべてのパッケージにある、rand パッケージを使うすべてのクレートが、互換性のある rand のバージョンを指定している限り、同じバージョンを使うことを保証してくれます。これにより、 容量を節約でき、ワークスペース内のクレート同士の互換性も確保されます。

ワークスペース内のクレートが同じ依存関係について互換性のないバージョンを 指定している場合、Cargo はそれぞれを解決しますが、それでも可能な限り 少ないバージョン数に収めようとします。

ワークスペースにテストを追加する

別の改善として、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` profile [unoptimized + debuginfo] target(s) in 0.20s
     Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)

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-3a47283c568d2b6a)

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 クレートではテストが 0 件見つかったことを示しており、最後のセクションは、 add_one クレートではドキュメントテストが 0 件見つかったことを示しています。

また、ワークスペース内の特定の 1 つのクレートに対するテストだけを、最上位の ディレクトリから -p フラグを使い、テストしたいクレート名を指定して 実行することもできます:

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

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 testadd_one クレートのテストだけを実行し、 adder クレートのテストは実行しなかったことを示しています。

ワークスペース内のクレートを crates.io に公開する場合、ワークスペース内の各クレートは それぞれ個別に公開する必要があります。cargo test と同様に、-p フラグを使って公開したいクレート名を指定することで、ワークスペース内の 特定のクレートを公開できます。

追加の練習として、add_one クレートと同様の方法で、このワークスペースに add_two クレートを追加してみてください!

プロジェクトが大きくなってきたら、ワークスペースの利用を検討してください。ワークスペースを使うと、 大きな 1 つのコードの塊ではなく、より小さくて理解しやすいコンポーネントを扱えるようになります。 さらに、クレートが同時に変更されることが多い場合、ワークスペース内にまとめておくことで、 クレート間の調整もしやすくなります。