TL;DR
- 依存関係を管理するには、SemVer の考え方と限界を知ることが大事である。
- 依存クレートのバージョンは
"1.4.3"や"0.7"のように、互換性のある後続のバージョンを含む形で指定する。"*"や"0.*"のような完全なワイルドカードは使わない。 - 長い目で見ればメジャーバージョンを上げないのも問題なので、
cargo updateや Dependabot で更新を知り、上げる時期を決める。
内容
Cargo と SemVer
Cargo は SemVer に従って依存クレートのバージョンを自動で選ぶ(Specifying Dependencies)。よく使う指定方法と、それぞれが受け入れるバージョンは次のとおりである。Cargo は、指定された範囲の中で最も新しいバージョンを選ぶ。
| 指定 | 1.2.2 | 1.2.3 | 1.2.4 | 1.3.0 | 2.0.0 |
|---|---|---|---|---|---|
"1.2.3" | × | ○ | ○ | ○ | × |
"^1.2.3" | × | ○ | ○ | ○ | × |
"=1.2.3" | × | ○ | × | × | × |
"~1.2.3" | × | ○ | ○ | × | × |
"1.2.*" | ○ | ○ | ○ | × | × |
"1.*" | ○ | ○ | ○ | ○ | × |
"*" | ○ | ○ | ○ | ○ | ○ |
SemVer の要点
SemVer の仕様では、一度リリースしたバージョンの中身は変更してはならず、どんな修正も新しいバージョンとしてリリースしなければならない。これを踏まえると、次のように言い換えられる。
- 何かを変更したら、パッチバージョンを上げる
- 既存の利用者のコードがそのままコンパイルでき動く何かを API に追加したら、マイナーバージョンを上げる
- API から何かを削除または変更したら、メジャーバージョンを上げる
仕様ではメジャーバージョンが 0 の間はどんな変更も起こりうるとされるが、Cargo は少し違い、最も左にある 0 以外の数字が互換性のない変更を表す。0.2.3 から 0.3.0、0.0.4 から 0.0.5 への変更には、後方互換性のない変更が含まれうる。
クレートの作者にとっての SemVer
パッチバージョンの規則は、何か変えたら上げればよいので守りやすい。ただし Hyrum の法則には気を配る必要がある。Git のタグをリリースに合わせるとさらに楽で、crates.io も同じバージョンでの再公開を拒否する。
難しいのはマイナーとメジャーの区別で、変更が後方互換かを判断しなければならない。The Cargo Book が詳しく分類しており、次のような変更は破壊的変更になりうる。
- 新しい項目の追加。利用者のコードの名前と衝突するとコンパイルが通らなくなる。ワイルドカードインポートでは特に起きやすい(項目23)が、デフォルト実装を持つトレイトメソッドの追加などでも起こりうる
- 利用者が使える enum のバリアントや構造体の public なフィールドの変更。コンパイラがすべての可能性を網羅することを求めるので、コンパイルが通らなくなる。
#[non_exhaustive]を付けていれば別だが、この属性を後から付けること自体が破壊的変更になる - トレイトがオブジェクト安全でなくなる変更や、新しいブランケット実装の追加
- ライセンスの変更。利用できなくなる人がいるので、ライセンスも API の一部と考える
- デフォルトのフィーチャの変更。フィーチャの削除は確実に破壊的変更になる
- Rust の新機能を使うこと。利用者はコンパイラを上げる必要がある。ただし多くのクレートは MSRV の変更を非破壊的な変更として扱っているので、MSRV を API に含めるかを検討する
public な項目が少ないほど、破壊的変更の危険は小さくなる(項目22)。目で確かめるのは大変なので、cargo-semver-checks、cargo-deny、cargo-msrv を CI で動かすとよい。
破壊的変更が必要なときは、まず新しい API を含むマイナーリリースを出し、古い API を deprecated にして移行方法を示す。その後、古い API を削除したメジャーリリースを出す。シグネチャは変わらないのに新しい動作が古い API と互換性を持たないときは、型や構造を変えて、破壊的変更であることを明示する。
API を固定したくないために 0.x のままにしているクレートも多いが、SemVer の表現力が減るので、1.0.0 にするのを恐れないほうがよい。
クレートの利用者にとっての SemVer
理想的には、新しいパッチバージョンとマイナーバージョンはそのまま動き、新しいメジャーバージョンは動く保証がない。新しいマイナーバージョンの API を使い始めると、前のバージョンには戻せなくなる。新しいメジャーバージョンでは、コンパイルが通っても制約や前提条件が変わっている可能性があるので、使い方が妥当かを確かめる。実際には、Hyrum の法則によってマイナーやパッチの更新でも思わぬ挙動の変化が起こりうる。
そのため、依存クレートは互換性のある後続のバージョンを含む形で指定し、完全なワイルドカードは使わない。crates.io に公開するクレートでは、完全なワイルドカードの指定はそもそも拒否される。一方、古いメジャーバージョンにはバグ修正やセキュリティ更新が届かないことがあるので、古いままでいる危険を受け入れるか、いずれ新しいメジャーバージョンに追従するかを選ぶ必要がある。
結論
SemVer は鈍器のようなものである。あるリリースがどの種類の変更かは作者の推測にすぎず、誰もが正しく判断できるわけではないし、「正しい」の意味もはっきりしないことが多い。それでも、SemVer は依存関係を管理するための唯一の方法なので、その考え方と限界を理解することが欠かせない。