メインコンテンツにスキップ

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

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

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

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

これがどれほど強力であるかを過小評価することは困難です。数週間で数百万人のユーザーを獲得するサイトを構築した小規模なWebスタートアップのストーリーを目にしたとき、彼らがこれを達成できる唯一の理由は、オープンソースコミュニティが彼らの足元にソフトウェアの饗宴を広げたからです。

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

名前と数字

#

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

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

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

共有依存関係の解決

#

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

次の例を考えてみてください

dependency graph

したがって、あなたのアプリはwidgetstemplatesを使用し、それらの*両方*がcollectionを使用します。これは*共有依存関係*と呼ばれます。では、widgetscollection 2.3.5を使用したいのに、templatescollection 2.3.7を使用したい場合はどうなるでしょうか?バージョンについて合意が得られない場合はどうなるでしょうか?

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

#

1つの選択肢は、アプリが両方のバージョンのcollectionを使用できるようにすることです。異なるバージョンのライブラリのコピーが2つあり、widgetstemplatesはそれぞれ望む方を取得します。

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

  1. collectionDictionaryクラスを定義します。
  2. widgetsは、collectionのコピー(2.3.5)からそのインスタンスを取得します。そして、それをmy_appに渡します。
  3. my_appは、その辞書をtemplatesに渡します。
  4. 次に、それは*その*バージョンのcollection2.3.7)にそれを渡します。
  5. それを受け取るメソッドには、そのオブジェクトのDictionary型アノテーションがあります。

Dartにとって、collection 2.3.5collection 2.3.7は完全に無関係なライブラリです。一方のクラスDictionaryのインスタンスを取得し、もう一方のメソッドに渡すと、それは完全に異なるDictionary型になります。これは、受信ライブラリのDictionary型アノテーションと一致しないことを意味します。あらら。

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

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

#

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

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

答えは、それらのうちの1つをアップグレードしようとすることです。templatescollection 2.3.7を求めています。それと互換性のあるwidgetsのより新しいバージョンにアップグレードできますか?

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

もし彼らがcollectionへの*自分たちの*依存関係をアップグレードした場合、widgetsをアップグレードするすべての人も、*望むかどうかにかかわらず*アップグレードしなければならなくなります。これは苦痛なので、依存関係をアップグレードすることへのインセンティブがなくなります。これは*バージョンロック*と呼ばれます。誰もが依存関係を前進させたいのですが、他のすべての人に強制されるため、誰も最初の一歩を踏み出せません。

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

#

バージョンロックを解決するために、パッケージが依存関係に設定する制約を緩めます。widgetstemplatesの両方が、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では、other_appがそれに設定する*別の*制約のために、widgetscollection 1.4.9を背負わされます。

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

エクスポートされた依存関係の制約ソルバー

#

パッケージの作成者は、注意してパッケージの制約を定義する必要があります。次のシナリオを検討してください。

dependency graph

bookshelfパッケージはwidgetsに依存しています。現在1.2.0のwidgetsパッケージは、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記事を参照してください。