こんにちは、教育系エンジニアのひらまつ(@hiramatsuu)です。
書籍「ゼロからわかる Linuxコマンド200本ノック(技術評論社)」の著者。Udemy受講者8万人。
プログラミング教育をメインに活動するエンジニアとして、動画教材の作成・技術書の執筆・学習アプリの開発などを行なっています(詳しくはこちら)。
本記事では、Flutterアプリなどで使う、pubspec.yamlとpubspec.lockについて解説していきます。ただし、この話はFlutterアプリに限った話ではなく、あらゆるDart・Flutterのパッケージにおいても同様です。
pubspec.yamlとpubspec.lockは、パッケージに依存する上で欠かせないファイルですが、
- これらのファイルがそれぞれどのような役割なのか?
- なぜ2つとも必要なのか?
が理解できていない方も多いと思います。そんな方にとって、参考になる記事になっているのではないかと思います。
pubspec.yamlとpubspec.lockの概要
Flutterアプリを開発していると、pubspec.yamlとpubspec.lockという2つのファイルを扱うことになる。これらのファイルは、ざっくり言うとそれぞれ次のような役割を持つ。
- pubspec.yaml:依存するパッケージのバージョン制約(version constraint)を範囲で記入する。新たなパッケージに依存する場面などで、開発者が編集する。「pubspec」と呼ばれることもある。
- pubspec.lock:pubspec.yamlを元にして生成される、依存パッケージの具体的なバージョンなどが記載されたファイル。手動での変更はしない。「lockfile」と呼ばれることもある。
それぞれのサンプルを以下に掲載。
pubspec.yaml
name: my_app
description: A new Flutter project.
publish_to: none
version: 1.0.0+1
environment:
sdk: ^3.3.1
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
assets:
- assets/locations.json
pubspec.lock
packages:
async:
dependency: transitive
description:
name: async
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
url: "https://pub.dev"
source: hosted
version: "2.10.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
パッケージを使うことは、高速にアプリ開発を行う上で必須であり、ほとんどのアプリは、自分以外の誰かが開発したパッケージに依存している。依存するパッケージを指定するのが、pubspec.yamlの最も重要な役割と言える。
そして、第三者のパッケージに依存する上で重要なのが、「パッケージにバージョンがある」ということ。これはどういうことか?
なぜバージョンが必要なのか?
pub.devで公開されているあらゆるパッケージには、2.0.5のようなバージョンが記載されているが、そもそもなぜ、バージョンが必要なのだろうか?
結論から言えば、パッケージに依存するユーザーへの影響を最小化するために、バージョンは必要。
バージョンの役割を一言で言えば、パッケージのスナップショットの保存を可能にすること、と言える。例えば、widgetsというパッケージにおいて、バージョン2.0.5を一度リリースしたら、開発者は今後、widgets 2.0.5には一切の変更を加えない。もし新たな機能を追加したり、バグを修正した際には、2.0.6や2.1.0という別のバージョンをリリースするようにする。
このようにすることで、widgets 2.0.5に依存しているユーザーは、widgetsのより新しいバージョンがリリースされても、バージョン2.0.5を使い続けることができるようになる。もちろん、ユーザーが望めば、より新しいバージョンにアップグレードすることもできる。
要するに、各時点でのパッケージのスナップショットが、バージョンと紐づいて管理されていることによって、アップグレードの対応を、ユーザーの好きなタイミングで行うことができるようになる、ということ。その結果、多くのパッケージに依存しても、依存先のアップグレードに対応するための変更作業に追われるということが、起こりづらくなる。
固定されたバージョンへの依存が引き起こす問題
だが一方で、バージョン2.0.5のような、固定されたバージョンに依存すると、発生してしまう問題もある。
例えば、複数のパッケージの依存関係のグラフが、次の画像のようになってしまっている場合に、問題が発生する。
上画像のように、あなたのアプリでは、widgetsとtemplatesという2つのパッケージに依存しており、これら2つのパッケージがどちらも、collectionパッケージに依存しているとする。そして厄介なことに、widgetsではcollection2.3.5を使う必要があり、templatesではcollection2.3.7を使う必要があるとする。
このような場合、collectionのバージョンはどのように設定すれば良いだろうか?
npmなど、同一パッケージの別バージョンを両立することが可能なパッケージマネージャーもあるが、Dartでは、別バージョンの同一パッケージは、まったく別のパッケージとして認識される。そのため、同じcollectionのクラスを使っているつもりでも、実際には別の型を扱っていることになり、widgetsとtemplatesの間でデータのやり取りが必要なら、うまくいかないかもしれない。
それならば、collectionのバージョンを揃えれば良いと思うかもしれない。確かに、widgetsをアップグレードすることで、collection2.3.7に依存するwidgetsを使えるようになれば、問題は解決するはず。だが実際には、多くの場合このアイデアはうまくいかない。
なぜなら、バージョンロックが起こってしまう可能性が高いからである。
バージョンロックとは
バージョンロックについて理解するためには、パッケージの開発者の気持ちになることが重要。なので、widgetsパッケージの開発者の立場から、上述の「widgetsをアップグレードする」という解決策を検討してみよう。
widgetsの開発者は、widgetsに新しい機能を追加したので、新しいバージョンのものを公開しようとしているとする。このとき、開発者には2つの選択肢がある。widgetsが依存するパッケージのバージョンを、更新するか・そのままにするか、である。
例えば、widgetsのアップグレードに際して、widgetsが依存するcollectionのバージョンも、2.3.5から2.3.7にアップグレードするとしよう。このとき、新しいバージョンのwidgetsのユーザーも、望む・望まないに関わらず、もれなくcollectionのアップグレードが必要になる。
そして、これは多くのユーザーにとって、非常に悩ましいことである。なぜなら、collectionにwidgetsとtemplatesが依存する、という先ほど見たパターン以外にも、widgetsと別のパッケージが、共にcollection2.3.5に依存している可能性があるためである。そのような依存関係を持つアプリの開発者は、アップグレードの手間が大きくなりすぎると感じて、widgetsのアップグレードを渋るだろう。
その結果、widgetsの開発者としては、バグや脆弱性が更新された最新のバージョンを使って欲しいので、既存のユーザーにとっての使いやすさを優先して、前と変わらずにcollection2.3.5に依存する、という決断をしてしまう可能性が高い。
そしてこれは、widgets以外のcollectionに依存するパッケージにおいても同様であるし、複数のパッケージに依存されるパッケージが、collection以外のパッケージになるケースもある。つまり、ユーザーのこと(そして、自分の開発するパッケージのこと)を考えると、どのパッケージの開発者も、依存パッケージをアップグレードしたがらなくなるのだ。
これが、バージョンロックと呼ばれる現象である。すなわち、「全員が依存パッケージを更新したいと思っているが、その決断は他の全員にも同じことを強いることになるため、誰も最初の一方を踏み出すことができない」現象である。
Dartがどのようにバージョンロックを解決するか
結局のところ、具体的なバージョンを指定したために、このような問題が起きているのだから、バージョンが持つ制約を緩めれば良いのでは?という考えは自然と出てくるはず。実際、Dartのpubでは、このような方法をとっている。
具体的には、pubspec.yamlにおいて、依存パッケージの具体的なバージョンではなく、許容するバージョンの範囲を書くようになっている。例えば、widgetsのpubspec.yamlに、次のような記載があったとする。
dependencies:
collection: '>=2.3.5 <2.4.0'
「’>=2.3.5 <2.4.0’」は「バージョン2.3.5以上、2.4.0未満」の意味。1
もしこのような範囲で、依存するcollectionのバージョンが指定されていたら、templatesに合わせてバージョン2.3.7を採用することが可能になる。
バージョン制約の解決方法のイメージ
上記では、widgetsが「2.3.5以上、2.4.0未満」という範囲のバージョンの、collectionに依存するのに対して、templatesがcollection2.3.7という、具体的なバージョンに依存していたが、実際の場面では、基本的にほとんどのパッケージでは、依存するバージョンの範囲が指定されている。
このような場面で、実際に依存する具体的なバージョンを決める(バージョン制約を解決する)ために、pubは裏側で何を行なっているのだろうか?これから、そのイメージを抱けるように解説していく。
仮に、collectionに依存するパッケージが3つあり、それぞれ次のようなバージョンの範囲を指定していたとする。
>=1.7.0
^1.4.0
<1.9.0
これらをまとめると、「バージョン1.7.0以上、1.9.0未満」のcollectionに依存する、ということなる。
そして、collectionではこれまでに、次のようなバージョンがリリースされているとする。
1.7.0
1.7.1
1.8.0
1.8.1
1.8.2
1.9.0
この場合、上記の3つの条件を満たすバージョンのうち、最新のものが選択されるので、バージョン1.8.2が選択されることになる。つまり、collectionパッケージに依存する、あなたのアプリ内のすべてのパッケージは、collection1.8.2を使用するようになる。
lockfileとは
このようにして、バージョンの制約が解決されたら、1つに定まった具体的なバージョンを記載するファイルが、pubによって自動で生成される。これが、pubspec.lock、通称「lockfile」である。
lockfileの中身のサンプルは以下(Dart SDKのバージョンによって多少異なる)。
packages:
async:
dependency: transitive
description:
name: async
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
url: "https://pub.dev"
source: hosted
version: "2.10.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
この中でも、次の部分に注目してみよう。
async:
dependency: transitive
description:
name: async
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
url: "https://pub.dev"
source: hosted
version: "2.10.0"
ここでは、asyncパッケージの情報として、次のようなものが記載されている。
- dependency:直接の依存(immediate dependency)か、推移的な依存(transitive dependency)か。
- name:パッケージの名前。
- sha256:このバージョンのasyncパッケージから計算されたハッシュ値。パッケージを一意に特定する用途で使う。
- url:ダウンロードしたURL。
- source:どこかからダウンロードしたのか、ローカルにあるのか、といったソースに関する情報。
- version:使用しているasyncの具体的なバージョン。
このlockfileを元にして、pubは「.dart_tool/package_config.json」ファイルを作成する。より詳しくは「【Flutter・Dart】パッケージの使用について深く理解する」を参照。
ちなみに、Flutterアプリなどの、アプリケーションパッケージにおいては、lockfileはバージョン管理システムにコミットした方が良い。その理由は「lockfileはコミットすべきか?【Flutter・Dart】」を参照。
まとめ
要するに、
- pubspec.yamlで、具体的な一つのバージョンではなく、範囲でバージョンを指定することで、バージョンロックが起こらないようにすることができる。
- pubspec.yamlで指定されたバージョン制約を解決した結果、使用することになった具体的なバージョンを記載するのが、pubspec.lock。
ということになる。そのため、pubspec.yamlとpubspec.lockの2つのファイルが用意されている。
補足:バージョン制約の解決がうまくいかないケース
注意点として、次のような場面では、バージョン制約の解決がうまくいかない。
- 別のパッケージで指定した範囲が重なっていない場合
- 指定した範囲のバージョンが存在しない場合
- 不安定な依存グラフになる場合
3のケースは、例えば、次のようなpubspecになっていた場合に起こる。
name: my_app
version: 0.0.0
dependencies:
yin: '>=1.0.0'
name: yin
version: 1.0.0
dependencies:
name: yin
version: 2.0.0
dependencies:
yang: '1.0.0'
name: yang
version: 1.0.0
dependencies:
yin: '1.0.0'
上記のような場合、うまく動作する具体的なバージョンを決めることはできない。
バージョンの制約の解決ができない場合は、pubはそのことを開発者に教えてくれるので、理由はわからないがうまく動かない、ということは起こらないので、安心して良い。
その他の情報
Flutter開発についての、他の記事はこちらを参照。
参考文献
脚注
- バージョン制約の表記方法については「pubspec.yamlの「^2.4.0」はどういう意味か?【caret syntaxについて解説】」を参照 ↩︎