TL;DR

  • Cargo は同じクレートの複数のバージョンを許すが、それらは SemVer で非互換でなければならない。依存クレートは SemVer 互換のバージョンを許す形("1" や "1.4.3")で指定する。
  • Cargo.lock を使うとビルドが決定的になるが、ライブラリの Cargo.lock は利用者には無視される。
  • 依存の問題は cargo tree、cargo-deny、cargo-udeps で見つける。依存クレートはコードを書く時間を節約するが、コストもある。

内容

crates.io のクレート名

crates.io のクレート名は、すべて 1 つの大きな名前空間で管理されている。たとえば serde という名前のクレートは 1 つしか存在できず、早い者勝ちである。また、クレートの中では、依存クレートと同じ名前のフィーチャを定義できない(項目26)。

知っておくべき 3 つの注意点

1 つ目に、クレート名にはハイフンを使える(some-crate)が、コードの中ではアンダースコア(some_crate)になる。

2 つ目に、Cargo は 1 つのビルドの中で、SemVer 非互換な複数のバージョンを選ぶことがある。逆に、SemVer 互換な複数のバージョンを選ぶことはない。クレート A とその依存先 B がどちらも some-crate = "^1.0.0" に依存していれば、A に 1.2、B に 1.3 が選ばれることはない。Cargo は 0 でない最初の数字をメジャーバージョンとして扱うので、"^0.1.0" どうしでも同じである。一方、A が 3.x に、B が 1.x に依存していれば、両方がビルドに入る(項目24)。ただし、C や C++ には、関数や定数、グローバル変数の定義は 1 つしか存在できないという ODR(One Definition Rule)があるので、FFI で C や C++ のコードを使っていると、複数のバージョンが問題を起こしうる。

3 つ目はフィーチャ統合である。ある依存クレートで有効になるフィーチャは、依存グラフ全体で指定されたフィーチャの和集合になる(項目26)。

バージョンの選び方

実際に使うバージョンは、Cargo のリゾルバのアルゴリズムで決まる。受け入れられるバージョンの範囲が重なっていて SemVer 互換なら、重なりの中で最も新しいバージョンを選ぶ。たとえば A が "^1.2.0"、B が "^1.3.0" を求めていれば、両方を満たす最新の 1.x を選ぶ。範囲が重ならなければ("^1.2.0" と "^2.0.0" など)、それぞれのバージョンをビルドする。

Cargo.lock をバージョン管理するか

Cargo の FAQ の助言は次のとおりである。アプリケーションやバイナリでは、Cargo.lock をコミットして決定的にビルドできるようにする。ライブラリでは、利用者が自分の Cargo.lock を使うので、ライブラリの Cargo.lock は無視される。それでも、ライブラリで Cargo.lock をコミットしておけば、定期的なビルドや CI で依存のバージョンが変わるのを防げる。

Cargo.lock をバージョン管理するなら、Dependabot などでアップグレードを管理する仕組みも用意する。そうしないと、依存が古いバージョンのまま固定され、安全でなくなるおそれがある(項目21)。一方で、バージョンを固定すれば、アップグレードの時期を自分で決められる。依存クレートの変更にすぐ対応しなくてよくなり、問題のあるバージョンが出ても修正版を待ってから上げられる。

バージョンの指定方法

The Cargo Book は、次の 2 つを避けるよう勧めている。

  • "=1.2.3" のように、特定のバージョンに不必要に固定すること。セキュリティ修正を含むかもしれない新しいバージョンを使えず、同じクレートに依存する他のクレートと範囲が合いにくくなる
  • "*" のように、範囲を広く取りすぎること。新しいメジャーバージョンで API が変わると、コードが動かなくなるおそれがある

その中庸として、"1"(1.x を許す)のように指定するのがよい。必要な機能とバグ修正を含む最小のバージョンを "1.4.23" のように指定してもよい。

ツールで問題を見つける

コードで使っている依存クレートが Cargo.toml になければ、コンパイラが指摘する。しかし逆に、Cargo.toml に書いてあるのにコードで使っていないクレートは指摘されない。これを見つけるには、cargo-udeps のようなサードパーティのツールを使う。

より多機能なツールに cargo-deny がある。依存グラフを解析して、既知のセキュリティ問題があるバージョン、受け入れられないライセンス、複数のバージョンが混在するクレートなどを検出する。チェックは個別に設定でき、例外も指定できる。こうしたツールを CI に組み込めば、脆弱性のような自分のクレートの外で起きた問題に気づける(項目32)。

問題が依存グラフのどこにあるかを調べるには、標準の cargo tree を使う。依存グラフを木の形で表示し、--invert <クレート名> で指定したクレートに依存するクレートを、--edges features で有効になったフィーチャを、--duplicates で複数のバージョンがあるクレートを表示できる。

どの依存クレートを入れるか

あるクレートの機能が必要なら、それを使う以外の選択肢はほとんどない。唯一の代わりは、ゼロから実装することである。しかし、依存を増やすことにはコストがある。

  • ビルド時間とバイナリサイズが増える
  • 依存クレートに問題が起きたときに対応が要る。過去には、メンテナがパッケージを削除したり、ライセンスを変えたりして、大きな障害が起きたこともある
  • サプライチェーン攻撃の危険がある。依存クレートは build.rs や手続き的マクロで、ビルド時に任意のコードを実行できる

依存グラフが大きいほどコストは増えるので、動作に直接関わらない装飾的なクレートは慎重に入れる。それでも、同じ機能をゼロから実装する時間は、依存クレートの問題に費やす時間よりはるかに大きいので、最終的には入れるべきという結論になる。

キーワード