目次

パッケージのバージョン管理

pubパッケージマネージャーは、バージョン管理を扱うのに役立ちます。このガイドでは、バージョン管理の歴史とpubのアプローチについて少し説明します。

これは高度な情報と考えてください。pubがなぜこのように設計されたのかを学ぶには、読み続けてください。pubを*使用*したい場合は、他のドキュメントを参照してください。

現代のソフトウェア開発、特にWeb開発は、既存のコードを大量に再利用することに大きく依存しています。これには、*あなたが*過去に書いたコードだけでなく、サードパーティ製のコード、大きなフレームワークから小さなユーティリティライブラリまで、あらゆるものが含まれます。アプリケーションが数十個の異なるパッケージやライブラリに依存することは珍しくありません。

これがどれほど強力かは言い尽くせません。小さなWebスタートアップが数週間で数百万人のユーザーを獲得するサイトを構築したという話を聞くと、彼らがこれを達成できる唯一の理由は、オープンソースコミュニティがソフトウェアの饗宴を彼らの足元に用意してくれたからです。

しかし、これは無料ではありません。コードの再利用、特に自分が管理していないコードの再利用には課題があります。アプリが他の人が開発しているコードを使用している場合、彼らがそれを変更するとどうなりますか?彼らはあなたのアプリを壊したくありませんし、あなたも当然壊したくありません。私たちは*バージョン管理*によってこの問題を解決します。

名前と番号

#

外部コードに依存する場合、「私のアプリは `widgets` を使用しています」と言うだけではありません。「私のアプリは `widgets 2.0.5` を使用しています」と言います。名前とバージョン番号の組み合わせは、*不変*のコードチャンクを一意に識別します。 `widgets` を更新している人たちは、好きなだけ変更を加えることができますが、すでにリリースされたバージョンには手を触れないことを約束しています。彼らは `2.0.6` や `3.0.0` を出すことができますが、あなたが使用しているバージョンは変更されないため、あなたには少しも影響しません。

それらの変更を取得*したい*場合は、いつでもアプリを新しいバージョンの `widgets` に向けることができ、それを行うために開発者と調整する必要はありません。しかし、それで問題が完全に解決するわけではありません。

このガイドで説明されているバージョン番号は、パッケージファイル名に設定されているバージョン番号と異なる場合があります。 `-0` や `-beta` が含まれている場合があります。これらの表記は依存関係の解決には影響しません。

共有依存関係の解決

#

特定のバージョンへの依存は、依存関係*グラフ*が実際には依存関係*ツリー*である場合にうまく機能します。アプリが多数のパッケージに依存し、それらのパッケージがさらに独自の依存関係を持っている場合など、それらの依存関係が*重複*しない限り、すべてうまく機能します。

次の例を考えてみましょう

dependency graph

つまり、アプリは `widgets` と `templates` を使用しており、*両方*が `collection` を使用しています。これは**共有依存関係**と呼ばれます。では、 `widgets` が `collection 2.3.5` を使用したいのに対し、 `templates` が `collection 2.3.7` を使用したい場合はどうなりますか?バージョンが一致しない場合はどうなりますか?

共有されていないライブラリ (npmアプローチ)

#

1つの選択肢は、アプリに両方のバージョンの `collection` を使用させることです。ライブラリの2つのコピーが異なるバージョンで存在し、 `widgets` と `templates` はそれぞれ必要なバージョンを取得します。

これは npm が node.js で行っていることです。Dart でもうまくいくでしょうか?次のシナリオを考えてみましょう

  1. `collection` は `Dictionary` クラスを定義しています。
  2. `widgets` は、`collection` のコピー ( `2.3.5` ) からそのインスタンスを取得します。そしてそれを `my_app` に渡します。
  3. `my_app` は辞書を `templates` に送信します。
  4. それは今度はそれを*独自の*バージョンの `collection` ( `2.3.7` ) に送ります。
  5. それを受け取るメソッドには、そのオブジェクトの `Dictionary` 型注釈があります。

Dart にとって、 `collection 2.3.5` と `collection 2.3.7` は完全に無関係なライブラリです。一方からクラス `Dictionary` のインスタンスを取得して、もう一方のメソッドに渡すと、それは完全に異なる `Dictionary` 型になります。つまり、受信側のライブラリにある `Dictionary` 型注釈と一致しません。おっと。

このため (そして、同じ名前の複数のバージョンのものを持つアプリをデバッグしようとする頭痛の種のため)、npm のモデルは適切ではないと判断しました。

バージョンロック (行き詰まりアプローチ)

#

代わりに、パッケージに依存する場合、アプリはそのパッケージのコピーを1つだけ使用します。共有依存関係がある場合、それに依存するすべてのものが、使用するバージョンに同意する必要があります。そうでない場合、エラーが発生します。

しかし、それでも問題は解決しません。そのエラーが発生した場合、それを解決できる必要があります。前の例で、そのような状況に陥ったとしましょう。 `widgets` と `templates` を使用したいのですが、それらは異なるバージョンの `collection` を使用しています。どうすればよいでしょうか?

答えは、それらのいずれかをアップグレードしてみることです。 `templates` は `collection 2.3.7` を必要としています。そのバージョンで動作する、より新しいバージョンの `widgets` にアップグレードできますか?

多くの場合、答えは「いいえ」です。 `widgets` を開発している人たちの視点から見てみましょう。彼らは*自分たち*のコードに新しい変更を加えた新しいバージョンをリリースしたいと考えており、できるだけ多くの人々にそれをアップグレードしてもらいたいと考えています。彼らが `collection` の*現在の*バージョンに固執すれば、現在のバージョンの `widgets` を使用している人は誰でも、この新しいバージョンもドロップインできます。

`collection` に対する*彼ら*の依存関係をアップグレードする場合、 `widgets` をアップグレードするすべての人が、*望むと望まざるとにかかわらず*、アップグレードする必要があります。これは苦痛なので、依存関係をアップグレードしたくないという気持ちになります。これは**バージョンロック**と呼ばれます。誰もが依存関係を आगे बढ़ाना したいのですが、そうすると他のすべての人が強制的にアップグレードされるため、誰も最初の一歩を踏み出すことができません。

バージョン制約 (Dartアプローチ)

#

バージョンロックを解決するために、パッケージが依存関係に課す制約を緩めます。 `widgets` と `templates` の両方が、動作する `collection` のバージョンの*範囲*を示すことができれば、依存関係を新しいバージョンに移行するための十分な余裕が生まれます。それらの範囲に重複がある限り、両方を満足させる単一のバージョンを見つけることができます。

これは bundler が従うモデルであり、pubのモデルでもあります。 pubspec に依存関係を追加する場合、受け入れることができるバージョンの*範囲*を指定できます。 `widgets` の pubspec が次のようになっている場合

yaml
dependencies:
  collection: '>=2.3.5 <2.4.0'

`collection` のバージョン `2.3.7` を選択できます。単一の具体的なバージョンは、 `widgets` パッケージと `templates` パッケージの両方の制約を満たします。

セマンティックバージョン

#

パッケージに依存関係を追加する場合、許可するバージョンの範囲を指定したい場合があります。どの範囲を選択すればよいでしょうか?前方互換性が必要なので、理想的には、まだリリースされていない将来のバージョンを含む範囲です。しかし、まだ存在していない新しいバージョンでパッケージが動作することをどのように知ることができるでしょうか?

この問題を解決するには、バージョン番号の*意味*について合意する必要があります。依存しているパッケージの開発者が「後方互換性のない変更を加える場合は、メジャーバージョン番号をインクリメントすることを約束します」と述べたとします。彼らを信頼する場合、パッケージが彼らの2.3.5で動作することがわかっていれば、3.0.0まで動作すると信頼できます。範囲は次のように設定できます。

yaml
dependencies:
  collection: ^2.3.5

これを機能させるには、一連の約束を考え出す必要があります。幸いなことに、他の賢い人々がこれらすべてを考え出し、*セマンティックバージョニング*と名付けました。

これは、バージョン番号の形式と、後のバージョン番号にインクリメントしたときのAPIの動作の違いを正確に記述しています。 Pubはバージョンがこの形式に従うことを要求しており、pubコミュニティとうまく連携するには、パッケージは指定されたセマンティクスに従う必要があります。依存するパッケージもそれに従うと想定する必要があります。(そうでないことがわかった場合は、作者に知らせてください!)

セマンティックバージョニングは1.0.0より前のバージョン間の互換性を約束していませんが、Dartコミュニティの慣習では、これらのバージョンもセマンティックに扱うことです。各番号の解釈は1つ下のスロットにシフトされます。0.1.2から0.2.0への移行は破壊的な変更を示し、0.1.3への移行は新機能を示し、0.1.2+1への移行はパブリックAPIに影響しない変更を示します。簡潔にするために、バージョンが1.0.0に達した後、+を使用しないでください。

これで、バージョニングとAPIの進化に対処するために必要な要素がほぼすべて揃いました。それらがどのように連携し、pubが何をするのかを見てみましょう。

制約解決

#

パッケージを定義するときは、その直接依存関係をリストします。これらは、パッケージが使用するパッケージです。これらの各パッケージについて、パッケージが許可するバージョンの範囲を指定します。これらの依存パッケージには、それぞれ独自の依存関係がある場合があります。これらは推移的な依存関係と呼ばれます。 Pubはこれらをトラバースし、アプリの依存関係グラフ全体を構築します。

グラフ内の各パッケージについて、pubはそれに依存するすべてのパッケージを調べます。それらのバージョン制約をすべてまとめて、同時に解決しようとします。基本的に、それらの範囲の共通部分を計算します。次に、pubはそのパッケージに対してリリースされた実際のバージョンを調べ、これらの制約をすべて満たす最新のバージョンを選択します。

たとえば、依存関係グラフにcollectionが含まれており、3つのパッケージがそれに依存しているとします。それらのバージョン制約は次のとおりです。

>=1.7.0
^1.4.0
<1.9.0

collectionの開発者は、以下のバージョンをリリースしました。

1.7.0
1.7.1
1.8.0
1.8.1
1.8.2
1.9.0

これらのすべての範囲に収まる最も高いバージョン番号は1.8.2であるため、pubはそれを選択します。つまり、アプリ*とアプリが使用するすべてのパッケージ*はすべてcollection 1.8.2を使用します。

制約コンテキスト

#

パッケージバージョンの選択は、それに依存する*すべて*のパッケージを考慮に入れるという事実には、重要な結果があります。*選択されるパッケージの特定のバージョンは、そのパッケージを使用するアプリのグローバルなプロパティです。*

次の例は、これが何を意味するのかを示しています。2つのアプリがあるとします。それらのpubspecは次のとおりです。

yaml
name: my_app
dependencies:
  widgets:
yaml
name: other_app
dependencies:
  widgets:
  collection: '<1.5.0'

どちらもwidgetsに依存しており、そのpubspecは次のとおりです。

yaml
name: widgets
dependencies:
  collection: '>=1.0.0 <2.0.0'

other_appパッケージは、collection自体に直接依存しています。興味深い部分は、widgetsとは異なるバージョン制約を持っていることです。

これは、widgetsパッケージだけを見て、どのバージョンのcollectionを使用するかを判断できないことを意味します。それはコンテキストに依存します。 my_appでは、widgetscollection 1.9.9を使用します。ただし、other_appでは、otherappが配置した*他の*制約のために、widgetscollection 1.4.9を使用することになります。

これが、各アプリに独自のpackage_config.jsonファイルがある理由です。各パッケージに対して選択される具体的なバージョンは、含まれているアプリの依存関係グラフ全体に依存します。

エクスポートされた依存関係の制約解決

#

パッケージの作成者は、パッケージの制約を注意深く定義する必要があります。次のシナリオを考えてみましょう。

dependency graph

bookshelfパッケージはwidgetsに依存しています。 widgetsパッケージは現在1.2.0で、export 'package:collection/collection.dart'を介してcollectionをエクスポートし、2.4.0です。 pubspecファイルは次のとおりです。

yaml
name: bookshelf
dependencies:
  widgets: ^1.2.0
yaml
name: widgets
dependencies:
  collection: ^2.4.0

その後、collectionパッケージは2.5.0に更新されます。 collectionの2.5.0バージョンには、sortBackwards()と呼ばれる新しいメソッドが含まれています。 bookshelfは、collectionへの推移的な依存関係しかないにもかかわらず、widgetsによって公開されているAPIの一部であるため、sortBackwards()を呼び出す場合があります。

widgetsにはバージョン番号に反映されていないAPIがあるため、bookshelfパッケージを使用し、sortBackwards()を呼び出すアプリがクラッシュする可能性があります。

APIをエクスポートすると、そのAPIはパッケージ自体で定義されているかのように扱われますが、APIが機能を追加するときにバージョン番号を増やすことはできません。これは、bookshelfsortBackwards()をサポートするwidgetsのバージョンが必要であることを宣言する方法がないことを意味します。

このため、エクスポートされたパッケージを扱う場合は、パッケージの作成者が依存関係の上限と下限を厳しく制限することをお勧めします。この場合、widgetsパッケージの範囲を狭める必要があります。

yaml
name: bookshelf
dependencies:
  widgets: '>=1.2.0 <1.3.0'
yaml
name: widgets
dependencies:
  collection: '>=2.4.0 <2.5.0'

これは、widgetsの下限が1.2.0、collectionの下限が2.4.0になることを意味します。誰かがcollectionの2.5.0バージョンをリリースすると、pubはwidgetsを1.3.0に更新し、対応する制約も更新します.

この規則を使用すると、一方のパッケージが直接の依存関係ではない場合でも、ユーザーは両方のパッケージの正しいバージョンを持っていることが保証されます.

ロックファイル

#

pubがアプリのバージョン制約を解決したら、どうなるでしょうか?最終的な結果は、アプリが直接または間接的に依存するすべてのパッケージと、アプリの制約で機能するそのパッケージの最適なバージョンの完全なリストです。

pubは各パッケージについて、その情報からコンテンツハッシュを計算し、アプリのディレクトリにあるpubspec.lockと呼ばれる* ロックファイル*に両方を書き込みます。 pubがアプリの.dart_tool/package_config.jsonファイルをビルドするとき、ロックファイルを使用して、どのバージョンの各パッケージを参照するかを認識します。 (そして、選択したバージョンを確認したい場合は、ロックファイルを読んで確認できます。)

pubが行う次の重要なことは、*ロックファイルに触れない*ことです。アプリのロックファイルを取得したら、指示するまでpubはそれに触れません。これは重要です。つまり、意図せずにアプリでランダムなパッケージの新しいバージョンを使用し始めることはありません。アプリがロックされると、手動でロックファイルを更新するように指示するまでロックされたままになります。

パッケージがアプリ用である場合は、*ロックファイルをソース管理システムにチェックインしてください!*そうすれば、チームの全員がアプリをビルドするときに、すべての依存関係のまったく同じバージョンを使用することになります。また、アプリをデプロイするときにもこれを使用するため、本番サーバーが開発に使用しているのと同じパッケージを使用していることを確認できます。

問題が発生した場合

#

もちろん、これはすべて、依存関係グラフが完璧で欠陥がないことを前提としています。バージョン範囲とpubの制約解決とセマンティックバージョニングを使用しても、バージョン問題の危険から完全に逃れることはできません。

次のいずれかの問題が発生する可能性があります。

互いに素な制約を持つことができます

#

アプリがwidgetstemplatesを使用し、どちらもcollectionを使用するとします。ただし、widgets1.0.0から2.0.0までのバージョンを要求し、templates3.0.0から4.0.0までのバージョンを必要としています。これらの範囲は重複していません。機能する可能性のあるバージョンはありません。

リリースされたバージョンを含まない範囲を持つことができます

#

共有依存関係のすべての制約をまとめた後、>=1.2.4 <1.2.6の狭い範囲があるとします。空の範囲ではありません。依存関係のバージョン1.2.4があれば、問題ありません。しかし、おそらく彼らはそのバージョンをリリースしたことがありません。代わりに、彼らは1.2.3から1.3.0に直接移行しました。中には何もない範囲があります。

不安定なグラフを持つことができます

#

これは、pubのバージョン解決プロセスの中で最も難しい部分です。プロセスは、*依存関係グラフを構築し、すべての制約を解決してバージョンを選択する*と説明されました。しかし、実際にはそのようには機能しません。 *任意の*バージョンを選択する前に、*全体*の依存関係グラフをどのように構築できますか? * pubspec自体はバージョン固有です。*同じパッケージの異なるバージョンには、異なる依存関係セットがある場合があります。

パッケージのバージョンを選択すると、依存関係グラフ自体の形状が変化します。グラフが変更されると、制約が変更される可能性があり、それによって異なるバージョンが選択され、円を描いて戻ることになります。

場合によっては、このプロセスが安定したソリューションに落ち着くことはありません。深淵を覗いてみてください。

yaml
name: my_app
version: 0.0.0
dependencies:
  yin: '>=1.0.0'
yaml
name: yin
version: 1.0.0
dependencies:
yaml
name: yin
version: 2.0.0
dependencies:
  yang: '1.0.0'
yaml
name: yang
version: 1.0.0
dependencies:
  yin: '1.0.0'

これらすべての場合において、アプリで機能する具体的なバージョンのセットはなく、これが発生すると、pubはエラーを報告し、何が起こっているかを知らせます。物事がうまくいくと思うが、うまくいかない奇妙な状態に陥ることはありません。

まとめ

#

要約すると

  • コードの再利用には利点がありますが、パッケージは独立して進化できる必要があります。
  • バージョニングにより、その独立性が実現します。単一の具体的なバージョンに依存すると、柔軟性がなくなります。共有依存関係と組み合わせると、バージョンロックが発生します。
  • バージョンロックに対処するには、パッケージはバージョンの*範囲*に依存する必要があります。 pubは依存関係グラフをウォークし、最適なバージョンを選択します。適切なバージョンを選択できない場合、pubは警告します。
  • アプリの依存関係に安定したバージョンセットが揃ったら、そのセットはロックファイルに固定されます。これにより、アプリを実行するすべてのマシンで、すべての依存関係の同じバージョンが使用されるようになります。

pub のバージョン解決アルゴリズムの詳細については、Medium のPubGrubの記事をご覧ください。