なぜpubspec.yamlとpubspec.lockの2つが必要なのか?【Dart・Flutter】

Flutter

こんにちは、教育系エンジニアのひらまつ(@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のような、固定されたバージョンに依存すると、発生してしまう問題もある。

例えば、複数のパッケージの依存関係のグラフが、次の画像のようになってしまっている場合に、問題が発生する。

https://dart.dev/tools/pub/versioningより引用

上画像のように、あなたのアプリでは、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つのファイルが用意されている。

補足:バージョン制約の解決がうまくいかないケース

注意点として、次のような場面では、バージョン制約の解決がうまくいかない。

  1. 別のパッケージで指定した範囲が重なっていない場合
  2. 指定した範囲のバージョンが存在しない場合
  3. 不安定な依存グラフになる場合

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開発についての、他の記事はこちらを参照。

Flutter
「Flutter」の記事一覧です。

参考文献

Package versioning
How Dart's package management tool, pub, handles versioning of packages.

脚注

  1. バージョン制約の表記方法については「pubspec.yamlの「^2.4.0」はどういう意味か?【caret syntaxについて解説】」を参照 ↩︎
タイトルとURLをコピーしました