項目25:依存グラフを管理しよう
LT;DR
また、クレート内ではクレート名と同じ名前の フィーチャ を定義することはできない クレート名には - を含めることができるが、コード内では _ となる
Cargo は同じクレートが複数の異なるバージョンを持つことを許容する ただし、これらのバージョンは SemVer 非互換でなくてはならない 依存クレートを指定する際には、SemVer 互換なバージョンを許容するように指定すべき(e.g. "1" や "1.4.3")
Cargo.lock を用いるとビルドが決定的になる
ただし、ライブラリの Cargo.lock ファイルはライブラリユーザには無視される
依存関係に関する問題の発見と対応には、cargo tree や cargo deny、cargo udeps を用いる
依存クレートを使うことには、コードを書く時間の節約にはなるが、コストがないわけではない
ビルド時間やバイナリサイズの増加
依存クレートに問題が起きた際の修正
hr.icon
そのため、たとえば serde という名前のクレートは crates.io 上に 1 つしか存在できない 早いもの勝ち
また、クレート内ではクレート名と同じ名前のフィーチャを定義することはできない
知っておくべき 3 つの細かい注意点
1. クレート名にはハイフンが使える(some-crate)が、コード内ではアンダースコア(some_crate)になる
逆に言えば、SemVer 互換のある複数バージョンを選択することは無い
e.g. クレート A が some-crate="^1.0.0" とクレート B に依存し、B も some-crate="^1.0.0" に依存している場合、それぞれのクレートで 1.2 と 1.3 が選択されることは無い
また、Cargo ではゼロでない最初のサブバージョンをメジャーバージョンとして扱う ため、以下も制限される e.g. クレート A が some-crate="^0.1.0" とクレート B に依存し、B も some-crate="^0.1.0" に依存している場合、それぞれのクレートで 0.1.2 と 0.1.3 が選択されることは無い
warning.icon
FFI を用いて C / C++ のコードにアクセスしていると、複数バージョンは問題を引き起こす可能性がある ODR: 関数や定数、グローバル変数について単一の定義しか存在できない
ある依存クレートに対して有効化されるフィーチャは、依存グラフ 全体のフィーチャの 和集合 となる Cargo のバージョン選択アルゴリズム
具体的には、
受け入れ可能なバージョンが重なり合っていて SemVer 互換であれば、重なり合っている部分内の最新バージョンを選択する
e.g. クレート A が some-crate="^1.2.0" とクレート B に依存し、B が some-crate="^1.3.0" に依存しているケース
Cargo は 1.2.0 <= x < 2.0.0 と 1.3.0 <= x < 2.0.0 の重なりから最も新しいバージョンを選択
逆に重なりがなければ、各々のバージョンを ビルド する e.g. クレート A が some-crate="^1.2.0" とクレート B に依存し、B が some-crate="^2.0.0" に依存しているケース
Cargo は 1.x と 2.x の両方をビルドする
アプリケーションやバイナリを生成する際には、Cargo.lock をコミットして決定的にビルドできるようにする
逆に、ライブラリクレートでは Cargo.lock を含めるべきではない
∵ ライブラリクレートのユーザは独自の Cargo.lock ファイルを使う
「ライブラリの Cargo.lock ファイルはライブラリユーザには無視される」
ただし、Cargo.toml をコミットすることで、ライブラリでも定期的なビルドや CI で依存関係のバージョニングが変動するのを防ぐこともできる
SemVer の仕様によって理論上は失敗するのは防げるはずだが、固定させることでより安定させることができる
ただし、「Cargo.lock をバージョン管理するなら、アップグレードを管理するプロセス(e.g. Dependabot)を設定しよう」 しない場合、依存関係は固定されたバージョンのままになり、安全でなくなる可能性もある
一方、Cargo.lock ファイルをコミットしてバージョンを固定することで、アップグレードのタイミングを自分でコントロールできる という利点がある
これにより、依存しているクレートの変更に即座に対応する必要がなくなり、計画的にアップグレードすることが可能となる
また、問題のあるバージョンがリリースされた場合でも、修正版がリリースされた後でアップグレードを行うことで、問題を回避できる
どのようにバージョンを指定すれば良いか?
不必要に特定のバージョンを指定しない
e.g. "=1.2.3"
∵ (セキュリティ修正を含むかもしれない)新しいバージョンを使えないし、同じ依存クレートを利用する他のクレートとの範囲が大幅に狭くなる
広すぎる範囲のバージョンを指定しない
e.g. "*"
∵ 新しいメジャーバージョンがリリースされた場合、API が完全に変更される可能性があり、コードが動作しなくなる可能性がある
e.g. "1"(1.x を許容)
必要な機能とバグ修正を含む最小のバージョンを設定するのでも良い
e.g. "1.4.23" または "^1.4.23"(1.4.23 よりも大きい 1.x を許容)
ツールを用いて問題を解決する
コード中で参照している依存クレートが Cargo.toml に書いていなければ、コンパイラが指摘してくれる
しかし、逆に Cargo.toml に書いているがコード中で参照していない依存クレートについては指摘してくれない
そのため、これを発見したい場合はサードパーティ製の cargo ツールを使うことになる cargo-deny は依存グラフを解析し、以下のような問題がある依存クレートを検出してくれる
使用されているバージョンに既知のセキュリティ問題がある
依存グラフ中で複数の異なるバージョンが使われている
単に許容できない
上記の機能は個々に設定可能で、例外を指定することも可能
上記のようなツールは CI に組み込んで、定期的かつ確実に実行されるようにした方が良い これにより、開発しているクレートの外で起きた問題(e.g. 脆弱性)を発見することができる しかし、上記のようなツールで問題を発見した場合でも、依存グラフのどの部分で問題が起きたのか正確に知ることは難しい
cargo tree を実行すると、依存グラフを 木構造 として表示できる よく用いられるオプション
--invert <クレート名>: 指定したクレートに依存するクレートを表示
--edges features: 依存関係リンク によって有効化されたフィーチャを表示する --duplicates: 依存グラフ内に複数のバージョンを持つクレートを表示
どのような依存クレートを導入すべきか?
あるクレートの機能が必要ならそれを利用する以外の選択肢はほとんどなく、唯一の代替案はそれをゼロから実装すること
しかし、依存クレートを導入することにはコストが伴う
ビルド時間やバイナリサイズの増加
依存クレートに問題が起きた際の修正
依存クレートは build.rs や 手続き的マクロ を用いることで任意のコードをビルド時に実行できるため注意が必要 依存グラフが大きくなるほどコストは増加する
そのため、装飾的(動作そのものには直接影響を与えない)なクレートは慎重に導入した方が良い
しかし、最終的には「導入するべき」という結論になる
∵ 同等の機能をゼロから実装する時間 >>> 依存クレートに関する問題に費やす時間