Cargo.toml 詳解

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

Cargo.toml はパッケージのビルド契約であり、どのターゲットを生成するか、依存関係をどこから取得するか、どのオプション機能(feature)を有効にするか、そしてどのようなビルド設定を使用するかを宣言します。Cargo.lock は依存関係解決の結果を記録するもので、両者は互いに代替できません。

1つのパッケージには、1つのライブラリターゲットと、複数のバイナリ、テスト、またはサンプルターゲットを含めることができます。crate は Rust のコンパイル単位であり、package、crate、workspace を同義語として扱うべきではありません。

Cargo.toml依存元の宣言、バージョン範囲、feature、ターゲットの指定1Cargo による依存関係グラフの解決適用される feature をマージし、制約を満たすパッケージバージョンを選択2Cargo.lock今回の解決で使用されたバージョン、ソース、検証情報を記録3コンパイルとリンクツールチェーン・対象・ネイティブ依存・構築入力にも依存4ビルドの再現性:制約、解決結果、環境はどれも欠かせませんロックファイルは依存関係の選択を固定しますが、OS全体やツールチェーンを自動的にロックするものではありません。

バージョン文字列は通常、範囲指定です

表記許可される一般的な正式バージョンの範囲
"1.2.3" または "^1.2.3">=1.2.3 かつ <2.0.0
"=1.2.3"正確に 1.2.3
"~1.2.3">=1.2.3 かつ <1.3.0
"0.2.3">=0.2.3 かつ <0.3.0
"0.0.3">=0.0.3 かつ <0.0.4

したがって、"1.2.3" は固定バージョンを意味するものではありません。プレリリースバージョンには追加のマッチングルールがあり、この正式バージョンの表をすべての -alpha/-beta 版にそのまま適用しないでください。依存関係のバージョン構文

バージョン制約が SemVer の範囲に準拠していても、Cargo がアップストリームの各 API が実際に互換性があるかどうかをチェックするわけではありません。ロックファイル内の既存バージョンが依然として制約を満たしている場合、通常のビルドではそれを継続して使用するのが一般的です。依存関係を更新する際は、ロックファイルを削除して「運試し」するのではなく、解決変更を明示的に確認すべきです。

Cargo.lock は、アプリケーションおよびライブラリリポジトリが CI 結果を再現するのに役立ちます。現在の Cargo ガイドラインでは、不明確な場合はロックファイルをコミットすることを推奨しています。「すべてのライブラリでロックファイルをコミットしない」というのはもはや適切な一般則ではありません。ただし、ダウンストリームがライブラリを使用する場合、その依存関係グラフはダウンストリーム側で解決され、そのライブラリリポジトリ自身の lock ファイルによる強制制限は受けません。マニフェストとロック

バージョン、ソース、インポート名は3つの次元です

[dependencies]
json_codec = { package = "serde_json", version = "1" }

ここで依存関係にあるパッケージは依然として serde_json ですが、Rust コードでは json_codec パスを使用します。package フィールドはパッケージ名を特定し、表のキーが依存関係を現在のコードでの名前として決定します。これが、マクロやプロシージャルマクロがユーザー側の依存関係名を安易にハードコーディングできない理由でもあります。

依存関係のソース解決時に固定されるもの維持すべきもの
registry + バージョン範囲要件を満たす公開バージョンlock 内の実際の選択と検証情報
git + rev指定されたリポジトリのコミットそのコミットへのアクセス元とビルド環境
git + branchブランチ要件、lock には解決されたコミットを記録ブランチのその後の移動は、既存の lock の自動更新にはなりません
pathローカルパッケージの場所とマニフェストローカルソースファイルの内容、lock はその内容のスナップショットを保存しません

path と version は組み合わせて使用できます。ローカル開発ではパスを使用しつつ、ローカルパッケージのバージョンが制約を満たすことを検証し、registry への公開後の依存関係に対してバージョン要件を提供します。これは「ローカルビルド時にいつでも registry に自動的にフォールバックする」汎用メカニズムではありません。パス指定パッケージのソースファイルを変更すると、Cargo.lock が変更されていなくても、ビルド成果物が変化する可能性があります。複数ソースの依存関係

依存関係の種類によって利用可能な側が決まります

設定主な使用箇所
dependencies通常のライブラリ/プログラムコード
dev-dependenciesテスト、サンプル、ベンチマークなどの開発用ターゲット
build-dependenciesホスト上で実行される build.rs
target.'cfg(...)'.dependenciesターゲットの条件を満たす場合の通常の依存関係

同じ依存関係が build 依存関係と通常の依存関係の両方に現れる場合、ホスト用とターゲット用にそれぞれコンパイルされる可能性があります。名前が同じだからといって、すべての feature やビルド成果物を共有すると仮定しないでください。ターゲット条件を、Cargo のオプション依存関係のメカニズムの代わりとして feature で代替すべきではありません。

feature は組み合わせ可能な能力です

以下は、完全な小規模プロジェクトの Cargo.toml です。

[package]
name = "feature-demo"
version = "0.1.0"
edition = "2024"

[features]
default = []
json = ["dep:serde_json"]

[dependencies]
serde_json = { version = "1", optional = true }

src/main.rs:

#[cfg(feature = "json")]
fn output() -> String {
    serde_json::json!({"ready": true}).to_string()
}

#[cfg(not(feature = "json"))]
fn output() -> String {
    "ready=true".into()
}

fn main() { println!("{}", output()); }
cargo run --no-default-features
cargo run --features json
cargo tree -e features --features json

dep:serde_json はオプション依存関係を明示的に有効にし、単に依存関係の名前だけで同名の feature が自動的に公開されるのを防ぎます。feature の加法性は、まず依存関係解決と API 設計の原則です。同一パッケージに対する適用リクエストは通常、和集合としてマージされます。これは、cfg(not(feature=...)) が構文上、特定のコードブロックの削除を禁止するわけではないことを意味します。Cargo Features

依存関係 A がデフォルト feature を無効にしても、依存関係 B が同じパッケージのデフォルト feature を再度有効にすることを妨げるものではありません。公開ライブラリの機能はできるだけ組み合わせ可能であるべきです。相互に排他的な feature は compile_error! で検証できますが、それにより通常の依存関係マージでそのライブラリを使用できなくなる可能性があります。resolver のバージョンとビルドコンテキストが、どの feature セットを分離して解決するかを決定し、「ワークスペース全体が無条件にすべてマージされる」と一般化してはなりません。

依存元 Adefault-features = false依存元 Bdefault を有効化、default は std を含む同じ適用解決コンテキスト内の shared自分の依存関係エッジのデフォルト機能を無効にしても、他の依存関係エッジを無効化しない最終的な feature の和集合には依然として default と std が含まれる図は同一パッケージインスタンスを対象としています。resolver はバージョン、依存関係の種類、ターゲットなどのルールに基づいてコンテキストを区別します。

依存関係エッジに沿って「誰がそれを有効にしたか」を追跡する

ビルド中に予期しない std や特定のバックエンドが現れた場合、現在のパッケージの default-features のみをチェックすべきではありません。まず cargo tree -e features -i パッケージ名 を使用して逆方向の依存関係を確認し、通常の依存関係、開発用ターゲット、またはビルド側のコンテキストのいずれかが要件を提示していないかを確認してください。

--no-default-features もまた、「依存関係グラフ全体でデフォルト機能を無効にする」わけではありません。各依存関係のデフォルト機能は、それぞれの宣言とマージルールによって依然として決定されます。真の最小ビルドをサポートするには、最小限の feature 組み合わせで個別にコンパイルする必要があり、--all-features を1回実行しただけで全構成を網羅したと主張してはいけません。

既定 feature を再び有効にしたのは誰か

同じ依存への 2 つの要求を和集合にすると、default を無効にしても std が残る理由が分かります。B の既定を残して A を切り替え、B も解除すると std が消えます。実際は版と resolver の文脈を確認してから cargo tree で feature の要求元を辿ります。

図を準備しています
既定 feature を再び有効にしたのは誰か

A が既定を無効にしても B の要求を拒否せず、適用される要求は和集合になります。

profile はトレードオフを制御し、最適解を保証するものではありません

dev はデフォルトでデバッグとインクリメンタルビルドに偏り、release はデフォで opt-level=3 です。現在のデフォルトの release では lto=false であり、明示的なクロス crate の lto="thin" ではありません。false は特定の条件下では依然として同一 crate 内のローカルな ThinLTO を許可し、off とは異なります。Profiles

別の最適化セットを比較したい場合は、独立した profile を作成した方が再現性が高くなります。

[profile.release-lto]
inherits = "release"
lto = "thin"
codegen-units = 1
debug = 1

cargo build --profile release-lto を実行します。codegen-units を減らすか LTO を有効にすると、最適化の機会が増える可能性がありますが、時間とメモリが増加する可能性もあり、必ずしも最小かつ最速の成果物が得られるわけではありません。s/z によるサイズ最適化も測定する必要があり、opt-level=3 を最小サイズオプションとして見なすべきではありません。

panic="abort" は例外処理のセマンティクスを変更し、catch_unwind はこのような panic をキャッチできなくなります。これはサイズのみに影響する無害なスイッチではありません。debug-assertions や overflow-checks もチェックの動作に影響を与えるため、ユーザーはテスト用とリリース用の設定の違いを把握しておく必要があります。

build.rs はビルド入力から出力を生成するプログラムです

スクリプトはホスト上で実行されますが、コンパイルターゲットは別のプラットフォームである可能性があります。ターゲットを照会する場合は TARGET または CARGO_CFG_* を読み取り、build.rs 内で cfg!(target_os=...) を使用してクロスコンパイルターゲットを判断しないでください。そうすると、スクリプト自身のホスト設定を見てしまうことになります。

出力ファイルは OUT_DIR に配置し、依存するファイルや環境を rerun-if-changed、rerun-if-env-changed を通じて宣言します。OUT_DIR は再ビルドの間、保持される可能性があるため、スクリプトは毎回空のディレクトリであると仮定すべきではありません。スクリプト自体やビルド依存関係の変更も再ビルドをトリガーするため、rerun 指示を1行出力するだけでは、トリガー条件は1つだけではありません。Build Scripts

C のコンパイルやバインディング生成の完全な例は bindgen を参照してください。build.rs は、依存関係グラフがすでに解決された後、rustc-cfg を通じて自分自身に新たな Cargo 依存関係を後から追加することはできません。カスタム cfg と Cargo feature は別のメカニズムです。

再生成結果を確認できる build.rs の例

前述の feature-demo プロジェクトのルートに label.txt を追加し、内容は service-a とします。次に、以下の build.rs を追加します。Cargo はこのデフォルトファイル名を自動的に認識します。スクリプトは設定を読み取り、それを Rust の文字列リテラルとしてエンコードして OUT_DIR に書き込みます。

use std::{env, fs, path::PathBuf};
fn main() {
    println!("cargo::rerun-if-changed=label.txt");
    let label = fs::read_to_string("label.txt").expect("read label.txt");
    let output = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR"));
    fs::write(output.join("build_info.rs"),
              format!("pub const LABEL: &str = {:?};\n", label.trim()))
        .expect("write build_info.rs");
}

このステップで src/main.rs を以下に置き換えます。

include!(concat!(env!("OUT_DIR"), "/build_info.rs"));
fn main() {
    assert!(!LABEL.is_empty());
    println!("{LABEL}");
}

最初の cargo run で service-a が出力されます。label.txt を service-b に変更して再度実行すると、再生成されて service-b が出力されるはずです。ここで {:?} は、文字列を引用符と必要なエスケープを含む Rust リテラルにエンコードするために使用されます。エスケープされていない外部テキストを直接ソースコードに連結してはいけません。trim はこのサンプルの設定戦略であり、先頭と末尾の空白を削除します。ラベルにこれらの文字が含まれる可能性がある場合は、別の解析方法を採用すべきです。

rerun-if-changed は、そのファイルの変更時にスクリプトを再実行する必要があることを宣言し、OUT_DIR は現在のビルドインスタンスの生成ディレクトリを提供します。他のワークスペースメンバー内で target/debug/build 下のハッシュ化されたディレクトリ名を手動で連結しないでください。ターゲット、profile、または feature コンテキストの変化により、このようなパスの仮定は脆くなります。

この例では、label を Cargo feature として扱わず、依存関係グラフも変更しません。入力が欠落している場合、expect によりビルドは失敗し、デフォルトの内容を黙って生成することはありません。実際のツールでは、必要に応じてより有用なビルド診断を出力できます。

設定が意図した通りに機能しているか確認する

cargo metadata はパッケージとターゲットを表示し、cargo tree -e features は feature の由来を説明し、cargo tree -d は重複するバージョンの特定を支援します。cargo build --locked はロックファイルの変更を必要とする解決変更を拒否しますが、ネットワークへのアクセスを禁止するものではありません。--offline はネットワークアクセスを制限しますが、ローカルインデックスやキャッシュが不足している場合に失敗する可能性があります。

再現可能な環境が必要な場合は、rust-toolchain、ターゲット、ネイティブ依存関係、およびビルドスクリプトの外部入力を記録する必要があります。crate のバージョンをロックすることはその一部に過ぎません。

「ロック、オフライン、再現性」を分離して検証する

オプション制約一般的な失敗の説明
--lockedロックファイルの変更を許可しないマニフェストと既存の解決結果が不一致、または必要な lock がまだ存在しない
--offlineネットワークへのアクセスなしローカルインデックスまたはソースキャッシュが不足している、またはオプションのバージョンセットが変更される可能性あり
--frozenlocked と offline の両方を要求lock の再解決と書き込みができず、依存関係の取得もできない

同じ lock と同じソースコードがあっても、バイトレベルで同一の成果物が得られるとは限りません。ツールチェーン、ネイティブコンパイラ、ターゲット設定、環境パス、およびビルドスクリプトの入力はすべて出力に影響を与える可能性があります。再現性のあるビルドを追求する場合は、依存関係バージョン、テスト結果、プログラムの動作、または最終成果物のハッシュのいずれを比較対象とするかを明確にする必要があります。

最小ビルドマトリックスを使用して隠れた問題を発見する

前述の feature-demo に対しては、少なくともデフォルト/デフォルトなし、および json feature を個別に確認する必要があります。公開ライブラリの場合、宣言された最小 Rust バージョン、サポートされるターゲット、および必要な開発用ターゲットも確認すべきです。最新ツールチェーンでのみ1回ビルドしただけでは、rust-version 宣言が成立することを証明できません。

cargo check は通常、タイプエラーや設定エラーを迅速に見つけるのに十分ですが、最終的なリンクや実行を代替するものではありません。ネイティブライブラリ、エクスポートされたシンボル、またはランタイム初期化の問題は、より遅い段階で現れる可能性があります。変更内容に応じて検証フェーズを選択し、「マニフェストが解決できる」ことを「すべての構成で配信可能」と見なさないでください。