📦

DartのPackage周りについて

2024/03/10に公開

DartにはPubDevというパッケージの管理システムが存在し、dart pubコマンドを利用することで簡単に外部ライブラリを利用することができます。pubspec.yamlに利用するライブラリを記述することやversionの管理方法など簡単に使うくらいはできますが、公式ドキュメントを改めて読み直すことで、Packageについての知識を整理するための記事です。

https://dart.dev/guides/packages

対象読者

上記にあるようにDartのPackageを利用して開発したことがある人を想定していますが、全く使ったことがなくても読めるような記事にはしようと思っています。

公式ドキュメントの構成について

Dartの公式ドキュメントにおいてPackageに触れている章は下記の通りになっています。

How to use packages
Commonly used packages
Creating packages
Publishing packages
Writing package pages
Package reference

  • Dependencies
  • Glossary
  • Package layout conventions
  • Pub environment variables
  • Pubspec file
  • Troubleshooting pub
  • Verified publishers
  • Security advisories
  • Versioning

本記事ではPacakgeの公開周りを覗く1,2,3章について一部を引用しながら、パッケージとは何か、どのように作成するのかについて解説していこうと思います。

How to use packages

At a minimum, a Dart package is a directory containing a pubspec file.

Dartのパッケージにおける最小限のパッケージはpubspec fileを1つ持ったディレクトリとのことです。逆に言えばpubspec fileを持ったディレクトリは全てDart packageと言えるということですね。

To use a package, do the following:

Create a pubspec (a file named pubspec.yaml that lists package dependencies and includes other metadata, such as a version number).
Use dart pub get to retrieve your package's dependencies.
If your Dart code depends on a library in the package, import the library.

こちらはDartPacakgeの使い方についてです。DartPacakgeを使ったことがある人にはお馴染みの手順で、pubspec.yamlにpacakgeを追加→pub getコマンドを叩く→実際に使いたいファイルでimportをすると書いてあります。

ここで気になったので、本当にpubspec.yamlさえあればdart packageと言えるのか確かめてみました。

まずローカルで適当なディレクトリを作成します。今回はdart_minimum_packageとしました。
そこにpubspec.yamlを作成しました。

pubspec.yaml
name: dart_minimum_package

environment:
  sdk: '^3.2.0'

dart pub getをしてみます。

$ dart pub get
Resolving dependencies... 
Got dependencies!

どうやら成功したようです。
また、ディレクトリを確認してみると

pub getをする前
$ ls -a
.		..		pubspec.yaml
pub get後
$ ls -a
.		..		.dart_tool	pubspec.lock	pubspec.yaml

.dart_toolとpubspec.lockが生成されたようです。
.dart_toolの中を見てみるとpackage_config.jsonというファイルだけありました。

package_config.json
{
  "configVersion": 2,
  "packages": [
    {
      "name": "dart_minimum_package",
      "rootUri": "../",
      "packageUri": "lib/",
      "languageVersion": "3.2"
    }
  ],
  "generated": "2024-03-08T22:09:58.715057Z",
  "generator": "pub",
  "generatorVersion": "3.2.1"
}

今回のpackageは他のdependencyを一つも追加していないためpackagesには自分自身のdart_minimum_packageだけパスが記載されてました。
次にpath(というパッケージ)を追加してみることにします。まず、pubspec.yamlに追記します。

pubspec.yaml
name: dart_minimum_package

environment:
  sdk: '^3.2.0'
+
+ dependencies:
+   path: any

再度pub getをするとpackage_config.jsonが更新されました。

package_config.json
@@ -1,14 +1,20 @@
{
  "configVersion": 2,
  "packages": [
    {
+      "name": "path",
+      "rootUri": "file:///Users/miyaji/.pub-cache/hosted/pub.dev/path-1.9.0",
+      "packageUri": "lib/",
+      "languageVersion": "3.0"
    },
    {
      "name": "dart_minimum_package",
      "rootUri": "../",
      "packageUri": "lib/",
      "languageVersion": "3.2"
    }
  ],
-  "generated": "2024-03-08T22:17:42.515085Z",
+  "generated": "2024-03-08T22:22:01.361175Z",
  "generator": "pub",
  "generatorVersion": "3.2.1"
}

今回追加したpathパッケージの私のPC上の絶対パスが表示されています。また、generatedが更新されているので、最後にpubgetした時間を保持していることがわかります。

公式ドキュメントによると

Pub creates a package_config.json file (under the .dart_tool/ directory) that maps each package name that your app depends on to the corresponding package in the system cache.

アプリが依存する各パッケージ名をシステムキャッシュ内の対応するパッケージにマッピングするとのことです。
要するにローカル環境にキャッシュされているパッケージのパスをパッケージごとに記載していると言い換えれそうです。自分はpackage_config.jsonを意識したことがなかったですが、このファイルが全部のパッケージの場所を管理してくれてたんですね。感謝。

次にお馴染みpubspec.lockです。
先ほどのpathを追加した際のdiffを確認してみます。

pubspec.lock
@@ -1,5 +1,13 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
- packages: {}
+ packages:
+  path:
+    dependency: "direct main"
+    description:
+      name: path
+      sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.9.0"
sdks:
  dart: ">=3.2.0 <4.0.0"

pathパッケージについて追加されていることがわかります。version以外は何を記載しているかよくわからないですね。。特にこのsha256で何を管理しているんだろう、、

また、公式ドキュメントによると

The first time you get a new dependency for your package, pub downloads the latest version of it that's compatible with your other dependencies. It then locks your package to always use that version by creating a lockfile. This is a file named pubspec.lock that pub creates and stores next to your pubspec. It lists the specific versions of each dependency (immediate and transitive) that your package uses.

最初にdependenciesを取得したときには他の依存関係と互換性のある最新のバージョンをダウンロードする。
そして、lockfileを作成することで、今後そのバージョンを使用するように指定します。
これは、それぞれのdependencyのバージョンのリストである。(immedidateとtransitiveの訳し方がよくわからん。)

If your package is an application package you should check this file into source control. That way, everyone working on your app uses the same versions of all of its dependencies. Checking in the lockfile also ensures that your deployed app uses the same versions of code.

もしもあなたのパッケージがアプリケーションパッケージであれば、ソース管理に含めるべきであるとのことです。
そうすることで、誰の環境でも同じバージョンのdepencenciesを利用することができるからです。

つまり、pubspec.yamlには利用するdependencyのversionに対するルールを指定するが、実際に利用するversionを厳密に定義しているのはpubspec.lockということになります。ドキュメントに書いてあるわけではないですが、これは他のdependencyとの兼ね合いで依存関係を解決できなくなることを防ぐためにこのような方式になっていると考えられます。

The dart pub upgrade command tells pub to regenerate the lockfile, using the newest available versions of your package's dependencies. If you want to upgrade only one dependency, you can specify the package to upgrade:

また、dart pub upgradeを使うことで、pubspec.lockを再生成して各種versionを上げることができるようです。私はいつもpubspec.lockを削除してpub getを叩いてました。。

ということで、pubspec.lockに実際に使うパッケージのversionが記載されており、package_config.jsonには各自の環境におけるパッケージのパスが記載されていることがわかりました。これらのおかげで簡単に外部ライブラリを使えるんですね〜。

また、公式ドキュメントにはpubのサブコマンドが紹介されてました。

Pub subcommands
The dart pub tool provides the following subcommands:

  • add
  • cache
  • deps
  • downgrade
  • get
  • global
  • outdated
  • publish
  • remove
  • token
  • upgrade

自分はadd,get,global,upgradeくらいしか知りませんでした。今回は一つずつには触れませんが、別の記事でこれもまとめてみようかなと思います。

Commonly used packages

Commonly used packagesではよく使われているパッケージ一覧が紹介されています。

Commonly used packages fall into three groups:

  • General-purpose packages
  • Packages that expand on Dart core libraries
  • Specialized packages

ここではGeneral-purpose packages,Packages that expand on Dart core libraries,Specialized packagesの3つに分類しているようです。
ここで挙げられているパッケージ一つずつを紹介すると日が暮れてしまうので、ここではどう分類されているのかについてだけみてみます。気になった方はぜひCommonly used packagesから確認ください。

General-purpose packages

Use these packages for a wide range of projects.

汎用的なライブラリであり広くさまざまなプロジェクトで利用するパッケージのようです。

Packages that expand on Dart core libraries

Each of the following packages builds upon a core library, adding functionality and filling in missing features:

core libraryの上に構築されて、dartの言語機能を拡張するようなライブラリ群のようです。
ここで気になるのはcore libraryとは?ということですね。dart:core libraryのページによると

Built-in types, collections, and other core functionality for every Dart program.

This library is automatically imported.

Dartに標準で用意されている型、コレクション、その他のコア機能が含まれており、自動的にimportされるようです。
なるほど、importしなくても利用できるからあまり気にしたことがなかったのか。

main.dart
main() {
  print(hello);
}

final String hello = "hello";

確かに上記のコードはStringというクラスを利用するためにファイルをimportしていませんが、問題なく動きます。

Specialized packages

To find specialized packages such as packages for Flutter and web development, consult the following sections.

お、ついにFlutterが出てきました。Flutterのような何かに特化したパッケージを紹介しているようです。

Creating packages

ここまではパッケージとは何か、どのように利用するのか、どんなパッケージがあるのかの話でした。FlutterやDartで開発をしたことある方であれば、少なくともぼんやり知っている内容だったかと思いますが、ここからはそのパッケージをどのように作成して、共有するのかの話がされています。

$ dart create -t package <PACKAGE_NAME>

上記のコマンドでpackageプロジェクトを作成することができます。
ちなみにdartのシンプルなプロジェクトは下記コマンドで生成できるので何が変わるのか比較してみました。

$ dart create <PROJECT_NAME>

今回はパッケージとしてdart_test_toolsというパッケージを作ります。また比較対象のプロジェクトはsampleとしますので、それで読み替えてください。

まず共通で生成されるファイルは下記です。

共通に生成されるファイル
  .gitignore
  analysis_options.yaml
  CHANGELOG.md
  pubspec.yaml
  README.md
  lib/<PACKAGE_NAME>.dart
  test/<PACKAGE_NAME>_test.dart
  .dart_tool/package_config.json

次にパッケージの場合は共通ファイルに加えて下記のファイルが作成されます。

-t packageオプションをつけた場合のみ生成されるファイル
example/<PACKAGE_NAME>.dart
lib/src/<PACKAGE_NAME>_base.dart

またアプリケーションパッケージの場合は共通ファイルに加えて下記のファイルが生成されるようです。

オプションをつけない場合にのみ生成されたファイル
bin/<PROJECT_NAME>.dart

まず、共通で生成されたファイルから見ていきます。

pubspec.yamlはほとんど同じでdescriptionの行だけ説明が異なっていました。
そのため、pubspecを参考にするpubspec.lockとpackage_config.jsonは全く同じものでした。

次に.gitignoreですがパッケージの方ではpubspec.lockを無視していました。これはアプリケーションパッケージでは先ほど述べたとおり開発者がみんな同じVersionのdependencyを利用するべきなのに対して、パッケージでは利用する人の他のパッケージとの兼ね合いでVersionを決めるべきだからだろうと考えられます。と思ったらこちらにそう記載されていました。

analysis_options.yamlは全く同じ内容でした。静的解析についてはパッケージでもアプリケーションパッケージでも大きく変わらないということですね。

次にCHANGELOG.mdとREADME.mdですが、前者は全く同じ、後者はパッケージの方のみ色々と書かれていました。しかしどちらもドキュメントファイルなので、特にここでは気にしなくていいかなと思います。

次にlib/<PROJECT_NAME>.dartとtest/<PROJECT_NAME>_test.dartについてですが、これは大きく異なっていました。

アプリケーションパッケージとして作成作成した場合

sample.dart
int calculate() {
  return 6 * 7;
}

アプリケーションパッケージとして作成するとシンプルな計算のサンプルが用意されています。

下記がパッケージとして作成した場合

dart_test_tools.dart
/// Support for doing something awesome.
///
/// More dartdocs go here.
library;

export 'src/<PACKAGE_NAME>_base.dart';

// TODO: Export any libraries intended for clients of this package.

ちょっと見慣れない記述ですね。一旦公式ドキュメントに戻りましょう。

As you might expect, the library code lives under the lib directory and is public to other packages. You can create any hierarchy under lib, as needed. By convention, implementation code is placed under lib/src. Code under lib/src is considered private; other packages should never need to import src/.... To make APIs under lib/src public, you can export lib/src files from a file that's directly under lib.

・libの直下にあるファイルは他のパッケージに対して公開される。
・慣習的にはlib/src/に実装コードを記載し、lib直下のファイルから実装コードをエクスポートする。
・srcは以下のコードはprivateとなり直接他のパッケージからアクセスできなくなる。(これなぜそうなるのか読み取れないので後で調べたいです。)

ということのようです。
そのため、src/<PACKAGE_NAME>_base.dartをexportしているということみたいです。

そのためtest/<PROJECT_NAME>_test.dartはテストしている対象のコードが異なり、アプリケーションパッケージの場合はlib/<PROJECT_NAME>.dartのテストを、パッケージの場合はlib/src/<PACKAGE_NAME>_base.dartのテストを記載しているようです。

はい、ここまでが共通ファイルの差分になります。同じものもあれば異なるものもありました。

次にそれぞれでしか生成されないファイルについてです。
example/<PACKAGE_NAME>_example.dartはパッケージの場合にのみ生成されます。

example/dart_test_tools_example.dart
import 'package:dart_test_tool/dart_test_tool.dart';

void main() {
  var awesome = Awesome();
  print('awesome: ${awesome.isAwesome}');
}

これはパッケージを利用するサンプルですね。確かに自分が利用したことあるパッケージもサンプルコードが用意されてて使う際に助けられた覚えがあります。また、注目したいのはimport文で、これはlib/dart_test_tool.dartを参照しているだけなのにsrc/dart_test_tools_base.dartに定義されているAwesomeクラスを利用できています。これが先ほどの公式の説明の内容になるということですね。

bin/<PROJECT_NAME>.dartはアプリケーションパッケージの場合に生成されます。

sample.dart
import 'package:sample/sample.dart' as sample;

void main(List<String> arguments) {
  print('Hello world: ${sample.calculate()}!');
}

このファイルは実際にアプリケーションを実行する際に呼び出されるファイルのようです。
dart runコマンドでオプションなしで実行できます。

$ dart run
Building package executable... 
Built sample:sample.
Hello world: 42!

パッケージの話からだいぶ脱線してしまいましたが、dart create の-t pacakgeオプションについて詳しく理解することができました。

公式ドキュメントでは次にshelfを例にパッケージの基本構成について触れていますが、構成については上述した内容と大体同じようです。

次にimport文の書き方についての説明があります。

When importing a library file from your own package, use a relative path when both files are inside of lib, or when both files are outside of lib. Use package: when the imported file is in lib and the importer is outside.

簡単にいうと自分で開発したパッケージ同士で参照をする場合、lib配下にあるファイルであれば、相対パスを利用し、それ以外のファイルであれば、package:からパスを書き始めるということみたいです。あまり意識したことなかったので、勉強になりました。

次にConditionally importing and exporting library filesでは、複数プラットフォームをサポートする際にexportするファイルを条件分岐させる方法が書いてあります。特に難しくもなさそうなのでここでは触れずに紹介に留めておきます。

Providing additional files

ここでは追加ファイルについて触れられています。

A well-designed package is easy to test. We recommend that you write tests using the test package, placing the test code in the test directory at the top of the package.

テストコードはtestディレクトリに書こうねとあります。また、よく設計されたパッケージはテストを書くのが簡単とのことです。おっしゃる通りです。肝に命じます。

If you create any command-line tools intended for public consumption, place those in the bin directory, which is public. Enable running a tool from the command line, using dart pub global activate. Listing the tool in the executables section of the pubspec allows a user to run it directly without calling dart pub global run.

公開を目的としたコマンドラインを開発する場合はbinディレクトリを作成する必要があるとのこと。
あ、これさっき見たbin/samples.dartのことですね!
こうすることでdart pub global activateでどこからでもコマンドラインツールを叩けるようになるみたいです。dart pub global activateでどこからでも叩けるようになる理由はpubspecの実行可能セクションのリストに追加されるから見たいです。(この辺りもどこかで深掘りしたい。。)

It's helpful if you include an example of how to use your library. This goes into the example directory at the top of the package.

exampleディレクトリについても先ほどのコマンドで生成されました。

Any tools or executables that you create during development that aren't for public use go into the tool directory.

開発中に作成したツールや実行可能ファイルのうち、公開しないものについてはtool directoryに入れるとのこと。ちょっとここで言及しているツールはどういうものを指しているのか現状イメージが湧いてないです。

Other files that are required if you publish your library to the pub.dev site, such as README.md and CHANGELOG.md, are described in Publishing a package. For more information on how to organize a package directory, see the pub package layout conventions.

その他パッケージ公開に必要なファイルについてはパッケージ公開の章で紹介しているとのこと

If your library is open source, we recommend sharing it on the pub.dev site. To publish or update the library, use pub publish, which uploads your package and creates or updates its page. For example, see the page for the shelf package. See Publishing a package for details on how to prepare your package for publishing.

ライブラリがオープンソースであればpub.devに公開してみようとのことです。この辺りは実際にpub.dev公開をしながら別の記事で触れてみようかなと思ってます。

まとめ

今回はパッケージとは何か、パッケージ生成オプションで生成されるファイルについてどんなものがあるのか見てみました。
次は実際にパッケージを開発して、pub.devに公開する記事を書いてみたいと思います。

おわりに

最後まで読んでいただきありがとうございました。
2024年は技術発信も頑張ろうと思っているので、記事が参考になった方はいいねとフォローをしていただけると励みになります!GitHubのフォローも!
よろしくお願いします!

https://github.com/miyasic

Discussion