はじめに
こんにちは、バックエンドエンジニアのよしかわです。
今回の記事は Rust のプログラムで利用する外部パッケージのバージョン管理についてです。Rust で書かれたあるプログラムが外部パッケージ A とB に依存しており、さらに A と B はどちらも別の外部パッケージ C に依存している、といったことはよくあると思います。このとき A が依存する C のバージョンと B が依存する C のバージョンが異なり、さらにはそのバージョンに互換性がない場合もあります。本稿ではそのような状況で起きるエラー例や検出方法を紹介します。
互換性のないバージョンの混在によるエラー
Cargo.lock を眺めると同名のパッケージのバージョン違いが並んでいることがあります。Cargo の依存性の解決の仕組み上、互換性のある並びはできません。つまりこの並びは互換性のないバージョンの混在を示しています。
(例) syn の 1.0.109 と 2.0.39 が混在している
[[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ]
このような混在が起きるのはそれぞれのバージョンに依存する別々のパッケージが存在するからです。
(例) prost-derive が syn 1.0.109 に依存し serde_derive が syn 2.0.39 に依存する
[[package]] name = "prost-derive" version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", "itertools", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "serde_derive" version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", "syn 2.0.39", ]
Cargo は混在するバージョンをそれぞれビルドします。そのため混在するバージョンのどれか一つしか利用できず、他のバージョンに依存するパッケージがビルドできないといったことはありません。
If multiple packages have a common dependency with semver-incompatible versions, then Cargo will allow this, but will build two separate copies of the dependency.
https://doc.rust-lang.org/cargo/reference/resolver.html#semver-compatibility
つまり Cargo.lock に記載されている通りの複数バージョンが動くようなコードがビルドされるわけですが、これがビルドエラーや実行時エラーに繋がる場合があります。
ビルドエラー
ビルドエラーは例えば複数バージョンが混在するパッケージの構造体の受け渡しで起きます。
(例) yup-oauth2 8.3.0 の構造体を、yup-oauth2 7.0.1 に依存する gcp-bigquery-client へ渡す
[dependencies] gcp-bigquery-client = "0.14.0" # yup-oauth2 のバージョン 7.0.1 を利用している yup-oauth2 = "8.3.0"
let sa_key = yup_oauth2::parse_service_account_key(key)?; gcp_bigquery_client::Client::from_service_account_key(sa_key, false).await
上記のようなコードをビルドしようとすると下記のようなエラーが出ます。
error[E0308]: mismatched types --> foo/bq.rs:42:55 | 42 | gcp_bigquery_client::Client::from_service_account_key(sa_key, false).await | ----------------------------------------------------- ^^^^^^ expected `yup_oauth2::service_account::ServiceAccountKey`, found `ServiceAccountKey` | | | arguments to this function are incorrect | = note: `ServiceAccountKey` and `yup_oauth2::service_account::ServiceAccountKey` have similar names, but are actually distinct types note: `ServiceAccountKey` is defined in crate `yup_oauth2` --> /Users/et-yoshikawa/.cargo/registry/src/github.com-1ecc6299db9ec823/yup-oauth2-8.3.0/src/service_account.rs:70:1 | 70 | pub struct ServiceAccountKey { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ note: `yup_oauth2::service_account::ServiceAccountKey` is defined in crate `yup_oauth2` --> /Users/et-yoshikawa/.cargo/registry/src/github.com-1ecc6299db9ec823/yup-oauth2-7.0.1/src/service_account.rs:70:1 | 70 | pub struct ServiceAccountKey { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ = note: perhaps two different versions of crate `yup_oauth2` are being used? note: associated function defined here --> /Users/et-yoshikawa/.cargo/registry/src/github.com-1ecc6299db9ec823/gcp-bigquery-client-0.14.0/src/lib.rs:88:18 | 88 | pub async fn from_service_account_key(sa_key: ServiceAccountKey, readonly: bool) -> Result<Self, BQError> { | ^^^^^^^^^^^^^^^^^^^^^^^^ For more information about this error, try `rustc --explain E0308`.
このエラーメッセージは親切に perhaps two different versions of crate `yup_oauth2` are being used?
とまで出してくれているので原因も見つけやすいです。
実行時エラー
やっかいなのはビルド時ではなく実行時です。エラーが起きればまだ良いのですが、エラーは起きないが動作が正しくないといったことがあり得ます。
(例) opentelemetry::global
を通した TexMapPropagator
の受け渡し
reqwest-tracing は opentelemetry::global::get_text_map_propagator
で取得した TexMapPropagator
を使ってトレース情報を HTTP リクエストに付加する機能を持ちます。
pub fn inject_opentelemetry_context_into_request(mut request: Request) -> Request { let context = Span::current().context(); global::get_text_map_propagator(|injector| { injector.inject_context(&context, &mut RequestCarrier::new(&mut request)) }); request }
その機能を使うため opentelemetry::global::set_text_map_propagator
で事前に TexMapPropagator
を設定しておく必要があるのですが、このとき opentelemetry のバージョンに注意する必要があります。opentelemetry::global::get_text_map_propagator
の呼び出しは reqwest-tracing 経由で、 opentelemetry::global::set_text_map_propagator
の呼び出しは opentelemetry のものを直接、という呼び出し方になるので両者で opentelemetry のバージョンが食い違う可能性があります。もし opentelemetry のバージョンに食い違いがあると実行時にエラーは起きないがトレース情報を伝達できないという問題が起きてしまいます。
互換性のないバージョンの混在の検出
上述のような Version-incompatibility hazards とも言われる状況への対策としては、CI で検出してリポジトリへの混入を防ぐのが手軽だと思います。検出方法は The Cargo Book に記載されています。
The
cargo tree -d
command can be used to identify duplicate versions and where they come from.
https://doc.rust-lang.org/stable/cargo/reference/resolver.html#version-incompatibility-hazards
cargo tree -d
の出力はコマンド名の通り樹状なので人間には見やすいですがプログラムからは少し扱いにくいです。CI に組み込む際は --depth
オプションで出力内容を減らした上で grep
コマンドでパッケージを絞り込むくらいがちょうどよいかもしれません。
(例) opentelemetry 関連パッケージのバージョン混在を検出する
$ cargo tree -d --depth 0 \ | grep 'opentelemetry' \ | awk '{ print } END { if (NR>1) exit 1 }' opentelemetry v0.20.0 opentelemetry v0.21.0 opentelemetry_sdk v0.20.0 opentelemetry_sdk v0.21.1 tracing-opentelemetry v0.20.0 tracing-opentelemetry v0.22.0
おわりに
複数バージョンの混在が実行時に引き起こす問題は意外と分かりにくいのではないかと思います。本記事の内容が少しでも対策の参考になれば幸いです。
エモーションテックでは顧客体験、従業員体験の改善をサポートし、世の中の体験を変えるプロダクトを開発しており、その中で Rust も使っています。この記事や他の記事を見て少しでも弊社に興味をもっていただけましたら、ぜひ採用ページからご応募をお願いいたします。