Workspace とプロジェクト管理

19 分で読了
このページの目次

Workspace は、関連する複数のパッケージが依存関係の解決記録とビルド管理のエントリポイントを共有できるようにします。コアライブラリ、コマンドラインツール、サーバーサイドが共に進化していくプロジェクトに適していますが、依存バージョンの重複を自動的に解消するわけでも、メンバを一緒に公開することを強制するわけでもありません。

分割設計では、インターフェースと責任を優先してください。ある crate の変更が別の crate に影響を与えるべきか、独立してテストできるか、外部に公開する必要があるか。小さすぎる crate が増えすぎると、保守コストやコンパイルの境界コストが増加します。

ルート Cargo.toml と Cargo.lockメンバ一覧、継承設定、統一された解決記録calc-core:ライブラリパッケージ再利用可能なビジネスインターフェースを維持demo-cli:バイナリパッケージpath 依存で calc-core に依存Workspace は共有管理エントリポイントを提供し、メンバは独立したパッケージのままです共有される target ディレクトリは、各依存が一度だけコンパイルされることや、依存グラフにバージョンが一つしかないことを保証しません。

完全な双メンバ構成のプロジェクト

ルートの Cargo.toml:

[workspace]
members = ["crates/calc-core", "crates/demo-cli"]
default-members = ["crates/demo-cli"]
resolver = "3"

[workspace.package]
version = "0.1.0"
edition = "2024"

[workspace.dependencies]
calc-core = { path = "crates/calc-core", version = "0.1.0" }

ルートには [workspace] しかなく、これは仮想マニフェストです。resolver=3 は Rust 2024 と同時に導入され、Rust/Cargo 1.85 以降が必要です。仮想 workspace には、推論に使用できるルートのパッケージの edition がないため、明示的に選択する必要があります。resolver=3 は rust-version に非互換な依存に対して適切なフォールバック戦略を採用しますが、最低ツールのチェーンでの実測に代わるものではありません。依存関係パーサー

crates/calc-core/Cargo.toml:

[package]
name = "calc-core"
version.workspace = true
edition.workspace = true

crates/calc-core/src/lib.rs:

pub fn doubled(value: u32) -> Option<u32> { value.checked_mul(2) }

crates/demo-cli/Cargo.toml:

[package]
name = "demo-cli"
version.workspace = true
edition.workspace = true

[dependencies]
calc-core.workspace = true

crates/demo-cli/src/main.rs:

fn main() {
    assert_eq!(calc_core::doubled(21), Some(42));
    assert_eq!(calc_core::doubled(u32::MAX), None);
    println!("42");
}
cargo run -p demo-cli
cargo check --workspace
cargo test --workspace
cargo metadata --no-deps --format-version 1

ここで checked_mul はオーバーフローを明示的に処理しており、リリース時とデバッグ時のデフォルトの算術チェックの違いをビジネスインターフェースに持ち込まないようにしています。

実際のライブラリテストでメンバの選択を観察する

calc-core/src/lib.rs の後に、以下のテストを追加します。境界値は u32::MAX / 2 を明確に使用し、デフォルトのオーバーフローチェックスイッチに依存せずに、最大で 2 倍にできる値と、それに隣接するオーバーフローする値を検証します。

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

    #[test]
    fn boundary() {
        assert_eq!(doubled(0), Some(0));
        assert_eq!(doubled(u32::MAX / 2), Some(u32::MAX - 1));
        assert_eq!(doubled(u32::MAX / 2 + 1), None);
    }
}

workspace ルートで cargo test を実行すると、default-members によって demo-cli のみが選択されます。calc-core は依存関係としてコンパイルされますが、そのライブラリのユニットテストはそれによって実行されません。cargo test -p calc-core または cargo test --workspace を使用して初めて、boundary テストの実行を確認できます。これは、「アプリケーションビルドがコアライブラリに依存している」ことと「コアライブラリのテストがカバーされている」ことが互いに代替できない理由を説明しています。

依存をビルドすると単体テストも動くか

依存関係は同じで、コマンドによって各パッケージのテスト target の選択が変わります。既定では CLI 経由で calc-core を使っても、calc-core の #[test] は実行しません。-p calc-core や --workspace に変えて対象の変化を確認します。

図を準備しています
依存をビルドすると単体テストも動くか

default-members=[demo-cli] なら既定のテストは CLI を選び、依存の calc-core をビルドしてもその単体テストは動きません。

継承は明示的な選択であり、ルート設定が自動的にすべてを上書きするわけではありません

メンバは workspace=true を通じて、対応する継承項目を選択する必要があります。workspace.dependencies の path は workspace ルート相対で解決されます。メンバの dependencies に直接記載された path はメンバのマニフェスト相対です。バージョン要件と path が同時に存在する場合、ローカルパッケージも要件を満たす必要があり、公開のセマンティクスにはレジストリバージョンも関与します。

メンバは継承された依存関係に feature を追加できますが、ルートで有効にされた feature はメンバによって任意に削除されることはありません。共通宣言によって設定の重複を減らせますが、他の推移的な依存関係が別の非互換バージョンを持ち込むのを阻止することはできません。ワークスペース

ルートの profiles や patch などの設定はワークスペースビルドに影響します。profile をメンバファイルに配置して独立して有効になることを期待しても、通常は無視される旨のヒントが返されます。lints や package のメタ情報なども、対応する継承構文に従って明示的に選択する必要があります。

lint 継承にもメンバの接続が必要

ルートマニフェストに以下を追加:

[workspace.lints.rust]
unsafe_code = "forbid"

この制約に従う必要がある各メンバに以下を追加:

[lints]
workspace = true

これは、純粋なビジネス crate 間で unsafe を共同で禁止するのに適しています。これは依存関係のソースコードにグローバルポリシーを適用するものではなく、外部依存関係には独自のコンパイルコンテキストがあり、ネイティブライブラリの安全性はこの lint によって証明されるものではありません。もし専用の FFI メンバが実際に unsafe の境界を担当している場合、ルートテーブルがすべてのメンバを無条件で上書きすると誤解するのではなく、明確なチェック方法と設定をそれに与えるべきです。ワークスペース継承設定

共有ロックファイルは、すべての依存関係が 1 つしかないことを意味しません

Cargo.lock は、foo 1.x と foo 2.x を同時に記録できます。異なるパッケージバージョンに由来する型は異なるアイデンティティを持ち、たとえ同名の構造体のフィールドが似ていたとしても、互いに渡せるわけではありません。cargo tree -d はこれらの状況の発見に役立ち、cargo tree -i はさらに参照元を探すことができます。

target ディレクトリも単に共有されたビルド成果物の場所です。ターゲットアーキテクチャ、プロファイル、feature の集合、ホストビルド依存関係、およびコンパイルオプションが異なる場合、異なるコンパイルユニットが発生する可能性があります。「共有キャッシュ」からすべての依存関係が一度だけコンパイルされることを推論することはできません。

コマンドの適用範囲は明確に記述する

仮想 workspace ルートで -p を指定しない場合、default-members がデフォルトのメンバ選択を制御します。--workspace は明示的にすべてのメンバを選択します。指定されたパッケージには -p を使用でき、パッケージ内に複数のバイナリがある場合は --bin をさらに使用します。

cargo test --workspace のテストターゲット、ドキュメントテスト、および通常のビルドは、完全に同じ設定ではありません。公開チェックは、公開準備が整った特定のパッケージに対して実行すべきであり、ワークスペースビルドが成功したからといって、各パッケージを独立してパッケージ化した場合に正しいファイルと解決可能な依存関係が含まれるとは限りません。

ルートディレクトリ cargo testdefault-members: demo-clicargo test --workspaceメンバ: demo-cli + calc-corecalc-core はライブラリ依存としてビルド其中的な boundary ユニットテストは実行されないcalc-core のテストターゲットもビルドboundary ユニットテストを実行まずパッケージを選択し、次にターゲットを選択;依存関係のコンパイルは依存関係のテストを意味しません図はこのページの双メンバ構成を使用しています。-p、--lib、--bin、--test はそれぞれ異なる選択レベルを制御します。
コマンド (workspace ルートで)明示的に選択するものこれによって推論できないもの
cargo checkデフォルトメンバ demo-cli他のメンバのすべてのターゲットがチェック済みであること
cargo test -p calc-core --libコアライブラリのユニットテストターゲットCLI、統合テスト、ドキュメントテストがすべて実行済みであること
cargo test --workspace各メンバのデフォルトテストセット各 feature と各ターゲットプラットフォームで通過していること
cargo check --workspace --all-targetsメンバのライブラリ、バイナリ、テスト、例、ベンチマークなどのターゲットドキュメントテストが実行済みであること、またはバイナリがリンク済みであること
cargo test --workspace --exclude demo-cliCLI を除外してメンバを選択他のパッケージに依存するコードが決してビルドされないこと

ここで言う Cargo ターゲット(lib/bin/test などのビルドターゲット)は、--target で指定されるプラットフォームのトライプレットの 2 つの次元です。「なぜまだこのパッケージをコンパイルしているのか」を調査する際は、まずそれが依存関係として出現しているかどうかを確認します。「なぜこのテストが漏れているのか」を調査する際は、パッケージとテストターゲットが選択されているかどうかを確認します。テストターゲットの選択

patch は、あるソースに対して候補パッケージを補足するもの

レジストリ内の codec パッケージにローカル修正が必要だと仮定します。ルートマニフェストに以下のように記述できます:

[patch.crates-io]
codec = { path = "vendor/codec" }

これは、vendor/codec が実際に codec という名前で、依存関係の制約を満たすバージョンのパッケージを含んでいることを要求します。patch はすべての同名バージョンを無条件に上書きするわけではありません。ロックファイルと候補バージョンの選択により、採用されない場合があるため、Cargo のヒント、cargo tree、およびロックファイルを確認し、必要に応じてそのパッケージに対して解析を更新してください。

また、「すべての依存関係の要件が完全に一致する必要がある」場合にのみ使用できるものでもありません。複数の互換範囲が同じバージョンに解決される場合もあれば、非互換バージョンが引き続き共存する場合もあります。別のメジャーバージョンを置き換えるには、適切な候補を個別に提供する必要があり、マニフェストのバージョン番号だけを変更して API を保守しないままにすることはできません。

ライブラリ自身が宣言した patch は、ダウンストリームアプリケーションのルート設定に自動的に適用されるわけではありません。ローカルでの修正検証のみを行う場合は、使用元と削除条件を記録してください。公開する場合は、ダウンストリームが正式な依存関係のソースから必要なバージョンを取得できることを確認してください。依存関係のオーバーライド

境界を用いてビルドの結合を減らす

コアライブラリは安定した型と明確なエラーをできるだけ公開し、CLI は引数と出力を担当してください。1 つの定数を共有するために、ネットワークランタイムやビルドジェネレータをすべてのメンバに持ち込まないでください。パブリック依存関係の再エクスポート、feature の和集合、および build.rs の出力は結合を拡大するため、インターフェースを変更する際に一緒に評価してください。

生成コードとネイティブビルドの責任については Cargo.toml を、条件分岐の検証については cfg を参照してください。

独立したパッケージ化は、ワークスペース内では見えない依存関係を露呈する

外部依存関係のない calc-core でローカルパッケージ化を練習してください:

cargo package -p calc-core --list
cargo package -p calc-core

1 行目はパッケージに含まれるファイルをチェックし、2 行目は .crate を生成し、デフォルトで解凍してビルド検証を行います。リストを確認する際は、src が存在するかどうかだけでなく、実行またはビルドに必要なテンプレート、ヘッダーファイル、設定サンプルに注目してください。build.rs が workspace 外の隣接ディレクトリに依存している場合、リポジトリ内でのビルドは成功するかもしれませんが、独立したパッケージ化後にはそれらの入力がなくなります。

パッケージ化時に Cargo はマニフェストを正規化し、継承フィールドを処理し、workspace、patch などのルート設定を削除します。demo-cli にとって、calc-core の path はローカル開発の位置を解決するだけであり、version がレジストリ依存関係を記述します。正式な配布では、必要なバージョンが対応するソースで取得可能であることを確保する必要があります。2 つのパッケージを同じリポジトリに配置しても、公開後の CLI がその相対パスに自動的にアクセスできるわけではありません。cargo package の処理手順

これらのメンバが内部でのみ一緒にビルドされる場合は、誤った公開を防ぐために、対応するパッケージで publish=false を設定できます。これは通常のローカル path 依存関係には影響しません。個別に公開する場合は、コアライブラリのパブリック API を中心に互換バージョンを決定し、使用側の要件を更新してください。統一された継承 version は単に値を統一するだけであり、セマンティックな互換性を自動的に分析したり、公開の順序を手配したりするものではありません。

新しいメンバの接続時には、実際の使用チェーンに沿って確認する

新しいサービスパッケージを追加した後、まず cargo metadata 内の workspace_members と workspace_default_members が期待どおり是否符合するかを確認します。次に、コアライブラリへの通常の依存関係、テスト専用依存関係、およびビルド依存関係が正しいテーブルに配置されているかどうかを確認します。最後に、ユーザーの実際のエントリポイントからエンドツーエンドのユースケースを実行します。例えば、CLI 引数をビジネス数値に変換し、doubled を呼び出し、None を理解可能なエラーに変換します。

ディレクトリの分割は、対応するインターフェースの境界がある場合にのみ価値を生みます。コアライブラリが Option を返すことは、この小さな例の完全な契約です。将来的に不正な入力、オーバーフロー、およびリソースエラーを区別する必要がある場合は、明確なエラー型にアップグレードし、各エントリが表示を担当するようにしてください。コアレイヤーが直接印刷したりプロセスを終了したりすることを避け、Web サービスやテストが関数を再利用しても、エラーレスポンスの方法を自由に決定できるようにしてください。