項目21:セマンティックバージョン(SemVer)を理解しよう
https://effective-rust.com/semver.html
LT;DR
依存関係を管理するには、SemVer の概念と限界を知ることが大事
依存クレートのバージョン指定には、"1.4.3" や "0.7" のように、互換性のある後続バージョンを含むように指定する
完全なワイルドカード(e.g. "*", "0.*")を使わない
ただし、長期的に見ればメジャーバージョンを上げないのも問題なので、cargo update や Dependabot で更新通知を受け取り、それを見て更新するタイミングを考えるのが良い
hr.icon
Cargo と SemVer
Cargo は セマンティックバージョニング(SemVer)に従って、依存クレートを自動的に選択する
https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
よく使われるバージョンの指定方法
"1.2.3": 1.2.3 以上の互換性のあるバージョン
メジャーバージョン は固定、マイナーバージョン および パッチバージョン は変更可能
"^1.2.3": 上と同じ
"=1.2.3": 1.2.3 固定
"~1.2.3": 1.2.3 以上のマイナーバージョン内(1.2.x)の互換性があるバージョン
"1.2.*": ワイルドカード にマッチするものすべて
table:対応表
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.*" ⚪︎ ⚪︎ ⚪︎ ⚪︎ ×
"*" ⚪︎ ⚪︎ ⚪︎ ⚪︎ ⚪︎
Cargo は、指定された範囲内の最大のバージョンを選択する
SemVer の要点
概要(https://semver.org/lang/ja/#概要)
バージョンナンバーは、メジャー.マイナー.パッチ とし、バージョンを上げるには、
1. APIの変更に互換性のない場合はメジャーバージョンを、
2. 後方互換性があり機能性を追加した場合はマイナーバージョンを、
3. 後方互換性を伴うバグ修正をした場合はパッチバージョンを上げます。
特に重要な仕様
https://semver.org/lang/ja/#spec-item-3
一度パッケージをリリースしたのなら、そのバージョンのパッケージのコンテンツは修正してはなりません(MUST NOT)。
いかなる修正も新しいバージョンとしてリリースしなければなりません(MUST)。
これを踏まえると、概要は以下のように言い換えることができる
何かしら変更した場合は、バッチバージョンを上げる
既存ユーザがこれまでどおりコンパイル・動作する何かを API に追加した場合は、マイナーバージョンを上げる
API から何かを「削除」、または「変更」した場合は、メジャーバージョンを上げる
https://semver.org/lang/ja/#spec-item-4
メジャーバージョンのゼロ(0.y.z)は初期段階の開発用です。
いつでも、いかなる変更も起こりえます(MAY)。
この時のパブリックAPIは安定していると考えるべきではありません(SHOULD NOT)。
Cargo では少し異なり、最も左にある 0 以外の数字が非互換な変更を表す
e.g. 0.2.3 → 0.3.0 や 0.0.4 → 0.0.5 の変更は、後方互換性 がない API 変更が含まれる可能性がある
クレート作者のための SemVer
何かしら変更した場合は、バッチバージョンを上げる
何か変更したらバージョンを上げれば良いので、遵守するのは簡単
warning.icon ただし、Hyrum の法則 に気を配る必要はある
Git のタグをリリースと合わせればより簡単に
デフォルトでは、タグは特定の コミット に固定されており、--force を付与しない限りは修正できない
また、crates.io はバージョンを監視しており、同じバージョンで再度公開しようとしても拒否する
既存ユーザがこれまでどおりコンパイル・動作する何かを API に追加した場合は、マイナーバージョンを上げる
API から何かを「削除」、または「変更」した場合は、メジャーバージョンを上げる
変更が後方互換かどうかを判断する必要があるため、遵守するのが難しい
後方互換かどうかをどう判断するか
公式では The Cargo Book で後方互換になる場合とならない場合を詳細に書いている
破壊的変更 となる可能性のある変更例
新しい項目の追加
通常安全だが、クレートを利用しているコード内で追加した項目と同じ名前を使っていると衝突し、コンパイル が通らなくなる
特にユーザが ワイルドカードインポート をしている場合
項目23:ワイルドカードインポートを避けよう
warning.icon ただし、ワイルドカードインポートをしていない場合でも既存の名前と衝突する可能性がある
e.g. デフォルト実装を持つトレイトメソッドを追加する、構造体にメソッドを追加する
ユーザが利用できる enum のバリアントや構造体の public なフィールドの変更
∵ Rust の コンパイラ はすべての可能性を網羅することを求めるため、コンパイルが通らなくなる
ただし、事前に #[non_exhausive] を利用していた場合はこの限りではないが、この属性を追加すること自体は破壊的変更となる
トレイト が オブジェクト安全 でなくなる変更
∵ トレイトオブジェクト として利用しているユーザのコンパイルが通らなくなる
トレイトに対する新しい ブランケット実装 の追加
∵ ユーザがそのトレイトを実装していた場合、2 つの実装が衝突する
OSS ライセンス の変更
新しいライセンスによっては、利用できなくなるユーザがいるため
「ライセンスは API の一部だと考えよう」
デフォルトフィーチャの変更
特に(フィーチャが no-op の場合を除いて)フィーチャの削除は、確実に破壊的変更となる
フィーチャの追加は、追加内容によっては破壊的変更となる
「デフォルトフィーチャの集合も API の一部だと考えよう」
Rust の新機能の利用
∵ ユーザがその機能を使えるバージョンまでコンパイラをバージョンアップする必要がある
ただし、多くのクレートでは MSRV の変更を非破壊的変更として扱っている
したがって、「MSRV を API の一部として含めるかを検討しよう」
以上を踏まえると、public な項目が少ないほど破壊的な変更を引き起こす危険性が少なくなる
項目22:可視範囲を最小化しよう
ただし、目視でチェックするのは大変なので cargo-semver-check や cargo-deny、cargo-msrv を用いて CI 上で行うのが良い
破壊的変更のユーザへの周知
破壊的な変更が必要な場合は、以下のような手順で行うとユーザの負担を軽減できる
1. 新しい API を含むマイナーバージョンアップデートをリリースする
このとき、古い API は deprecated とし、新しい API への移行方法もリリースに含める
2. 古い API を削除して、メジャーバージョンアップデートをリリースする
場合によっては、破壊的変更が API に直接関係のないこともある
e.g. 新しい動作が古い API と互換性がないにも関わらず、API のシグネチャが変わらないケース
この場合は、「破壊的変更を明示する」のが良い
e.g. 型や構造を変更し、新旧の API が混在しないようにする
1.0.0 にすることを恐れない
1.0.0 になると API を固定しなければならないため、多くクレートが 0.x のままであるケースがある
しかし、これだと SemVer の表現力が減る(事実上のメジャーとマイナーのみ)ため避けた方が良い
クレートユーザにとっての SemVer
ユーザにとって、依存するクレートの新しいバージョンに対する理想は以下のとおり
新しいバッチバージョンはそのまま動くはず
新しいマイナーバージョンもそのまま動くはずだが、新しい API を試すとより効率的に利用できるかもしれない
ただし、新しい API を使うと以前のバージョンに戻すことはできなくなる
新しいメジャーバージョンは、動く保証がない
コンパイルできなくなる可能性が高く、新しい API に対応するためにコードを書き直す必要がある
仮にコンパイルが通ったとしても、API の使用方法がバージョン変更後も妥当かチェックが必要
∵ 制約や前提条件が変わっている可能性が高いため
実際には、 マイナー、バッチバージョンの変更であっても、Hyrum の法則 により予期しない挙動の変化が起こる可能性がある
以上を踏まえると、依存クレートのバージョン指定は "1.4.3" や "0.7" のように、互換性のある後続バージョンを含む形式になる
「依存ライブラリの指定に、完全なワイルドカード("*" や "0.*")を用いるのはやめよう」
crates.io に公開する場合、完全なワイルドカードの指定は拒否されるため強制される
しかし、長期的にはメジャーバージョンを無視するのも安全でない
古いメジャーバージョンに対しては、バグの修正やセキュリティ更新がされない場合があるため
したがって、古いバージョンのままでいるリスクを受け入れるか、最終的にはメジャーバージョンへの更新に追従するか」のどちらかを選択する必要がある
cargo update や Dependabot などを用いれば、更新を通知してくれるので、それを見て更新を行うタイミングを決定すれば良い
結論
クレートに変更を加えた際には、上記の基準に照らしてどのバージョンをインクリメントするか決定する必要がある
しかし、「セマンティックバージョニングは鈍器のようなものである」
つまり、あるリリースが 3 つのどのバージョンに当てはまるかを、クレートの作者が推量した結果を反映しているのに過ぎない
そのため、誰もが正しく判断できるわけでもなく、「正しい」とは何を意味するのかも明確でないことが多い
仮に正しく判断したとしても、Hyrum の法則 に引っかかる可能性もある
それでもセマンティックバージョニングは依存関係を管理するための唯一の方法であるので概念と限界を理解することが不可欠である
#Rust #Effective_Rust_―_Rustコードを改善し、エコシステムを最大限に活用するための35項目