ダイヤモンド依存関係とは?ライブラリのバージョン競合が起きる仕組み

ダイヤモンド依存関係とは?ライブラリのバージョン競合が起きる仕組み

#Web開発#開発手法#パッケージ管理

パッケージを一つ追加しただけなのに、直接指定していないライブラリのエラーが出る。 依存関係を調べると、二つのパッケージが同じライブラリの別バージョンを要求している。

ダイヤモンド依存関係とは、このような場面で現れる構造です。 名前は難しそうですが、構造は複雑ではありません。

ダイヤモンド依存関係とは何か

アプリケーションがパッケージBとパッケージCを利用しているとします。 BとCは、どちらも内部でライブラリDを利用しています。

        Application
        /         \
  Package B     Package C
        \         /
         Library D

依存関係を線で結ぶと、途中で二つに分かれ、Dで再び合流します。 この形がひし形に見えるため、ダイヤモンド依存関係と呼ばれます。

ここで問題になるのは、ひし形そのものではありません。 BとCが同じバージョンのDを使えるなら、そのまま動きます。 BとCが異なるバージョンのDを必要とするときに、競合が起こります

問題はDのどのバージョンを使うか

BはDの1系を必要とし、CはDの2系を必要としているとします。 アプリケーションは、Dの1系と2系のどちらを使えばよいのでしょうか。

Dの1系だけを使うと、Cが必要とする機能が見つからない可能性があります。 Dの2系だけを使うと、今度はBが新しいAPIに対応していない可能性があります。

新しいバージョンを選べば解決するとは限りません。 メジャーバージョンが変わると、関数の名前や引数、戻り値などが変わることがあるからです。

パッケージマネージャーは、BとCのコードが本当に動くかまでは判断しません。 宣言されたバージョン条件をもとに、インストールするDを決めます。

npmとBundlerでは結果が異なる

npmは、Dの1系と2系を別々の場所へインストールできます。 一方のパッケージにはDの1系を、もう一方にはDの2系を使わせる仕組みです。 公式ドキュメントでも、条件が両立しないパッケージを別のnode_modulesへ配置する例が示されています。

この方法なら、バージョンが違ってもインストールは成功します。 ただし、同じライブラリが複数入るため、容量が増え、更新対象もわかりにくくなります。

Bundlerは、同じgemについて一つのバージョンを選びます。 開発環境に複数バージョンがインストールされていても、一つのbundleが同時に使うバージョンは一つです。 Bundlerの公式FAQにも、すべてのグループとプラットフォームを通じて、一つのバージョンへ解決するとあります。

BとCが要求する範囲に重なりがあれば、Bundlerはその中からDを選べます。 重なりがなければ、bundle installは依存関係を解決できずに止まります。

npmは複数バージョンを共存させられますが、Bundlerは一つのバージョンへそろえます

競合している依存関係を見つける

調べたいのは、Dを使っている二つの経路です。 アプリケーションが直接追加したパッケージだけを見ても、BからD、CからDというつながりはわかりません。

npmでは、次のコマンドでDが入った経路を確認できます。

npm explain <package-name>

インストールされているDのバージョンをまとめて見る場合は、npm ls <package-name> --allを使います。

Bundlerでは、まずbundle installのエラーを確認します。 どのgemが、どのバージョン範囲を要求しているかが表示されます。 インストール済みの依存関係は、Gemfile.lockGEMセクションからたどれます。

package-lock.jsonGemfile.lockには、最終的に選ばれたバージョンが記録されています。 ロックファイルは解決結果を固定するものであり、競合自体を直すものではありません

依存元を更新して解消する

競合を見つけたら、Dを無理に固定する前にBとCを確認します。 対処の順序は次の三つです。

  1. Dを要求しているBとCを特定する
  2. BまたはCを更新し、両方が使えるDの範囲を作る
  3. 更新できなければ、BかCの置き換えを検討する

Bundlerなら、対象を絞ってbundle update <gem-name>を実行できます。 すべてのgemを更新するbundle update --allは変更範囲が広いため、最初から使う必要はありません。 Bundlerも、Gemfileを変更した後はまずbundle installで保守的に更新する手順を案内しています。

最終手段として、GemfileでDのバージョンを直接指定したり、npmのoverridesで上書きしたりする方法もあります。 ただし、指定によってインストールに成功しても、BとCがそのDで動くとは限りません。 テストを実行し、ロックファイルの差分まで確認します。

ダイヤモンド依存関係を直すとき、最初に見るべきなのは合流先のDではありません。 Dへ異なる条件を持ち込んだBとCを見つけ、更新によって条件をそろえられるかを確認するのが出発点です。

まとめ

  • ダイヤモンド依存関係:二つのパッケージが同じライブラリへ依存し、経路がひし形になる構造です
  • npmとBundlerの違い:npmは複数バージョンを共存させられますが、Bundlerは一つのバージョンへそろえます
  • 対処の順序:競合したライブラリを直接固定する前に、異なる条件を持ち込んだ二つの依存元を更新できるか確認します
この記事をシェア