🤷🏻‍♂️

なぜdart pub global activateでコマンドが利用できるようになるのか

2024/04/13に公開

はじめに

dart pub global activateコマンドを叩いたことありますか?例えばFVMやFlutterFire CLIを利用する際に使ったことがある方もいるかと思います。
dart pub global activate fvmのように叩くことで、fvmコマンドがどこからでも利用できるようになります。(以下長いのでactivateコマンドと呼ばせてください。)
今回はなぜこのコマンドを叩くことで別のコマンドが叩けるようになるのかが気になったので、調べつつ記事にしようと思います。

Dart CLI

DartはFlutterで利用するほかにCLIツールを開発するために利用することもできます。
例えばdart create {name}コマンドを叩くことでCLIツールのサンプルプロジェクトが作成され、このプロジェクトでdart runコマンドを叩くことでCLIが実行できます。詳しくは「DartのPackage周りについて」で実際に試していますので気になる方はご参照ください。

おそらく、activateコマンドはこのdart runを名前解決してどこからでも呼び出せるようにパスを通しているのでは?と考えました。

実はShellScriptを呼んでるだけ?

さて、余談にも合ったようにactivateコマンドでインストールされたアプリはDartに依存しています。
最初はFVMがDartに依存しているのかと思いましたが、brewでFVMをインストールする場合はDartがインストールされてなくても動いていたので、activateコマンドでインストールされたアプリがDartに依存しているといって間違い無いでしょう。

ここで疑問が生じました。
疑問1:そもそもactivateされたコマンドはどこにあるのか?
疑問2:なぜdartに依存しているのか

今回はこれらの疑問についてflutterfire_cliを題材に確認してみようと思います。

flutterfire_cliは名前の通りFlutterとFirebaseを使ったアプリケーションを開発するのに便利なCLIツールです。

まず、すでにactivateされているパッケージを確認してみます。

$ dart pub global list
coverage 1.7.2
dtt 0.0.1

現状2つパッケージがactivateされているようです。
それでは、flutterfire_cliをactivateしてみます。

dart pub global activate flutterfire_cli
dart pub global activate flutterfire_cli
+ ansi_styles 0.3.2+1s...
+ args 2.5.0
+ async 2.11.0
+ boolean_selector 2.1.1
+ characters 1.3.0
+ ci 0.1.0
+ cli_util 0.4.1
+ clock 1.1.1
+ collection 1.18.0
+ dart_console 1.2.0 (4.0.2 available)
+ deep_pick 1.0.0
+ ffi 2.1.2
+ file 6.1.4 (7.0.0 available)
+ flutterfire_cli 1.0.0
+ http 0.13.6 (1.2.1 available)
+ http_parser 4.0.2
+ interact 2.2.0
+ intl 0.18.1 (0.19.0 available)
+ json_annotation 4.8.1
+ matcher 0.12.16+1
+ meta 1.14.0
+ path 1.9.0
+ petitparser 6.0.2
+ platform 3.1.4
+ process 4.2.4 (5.0.2 available)
+ pub_semver 2.1.4
+ pub_updater 0.2.4 (0.4.0 available)
+ pubspec 2.3.0
+ quiver 3.2.1
+ source_span 1.10.0
+ stack_trace 1.11.1
+ stream_channel 2.1.2
+ string_scanner 1.2.0
+ term_glyph 1.2.1
+ test_api 0.7.1
+ tint 2.0.1
+ typed_data 1.3.2
+ uri 1.0.0
+ win32 5.4.0
+ xml 6.5.0
+ yaml 3.1.2
Building package executables... (3.2s)
Built flutterfire_cli:flutterfire.
Installed executable flutterfire.
Activated flutterfire_cli 1.0.0.

上記のコマンドを実行し改めてlistを確認してみると

$ dart pub global list
coverage 1.7.2
dtt 0.0.1
flutterfire_cli 1.0.0

無事flutterfire_cliが追加されたようです。

さて一つ目の疑問このコマンドはどこにインストールされたのでしょうか?の
答えはwhichコマンドを使うことでわかります。

$ which flutterfire_cli
flutterfire_cli not found

あれれ、、、

公式ページを確認したところ、package名はflutterfire_cliであるのに対してコマンド名はflutterfireのようです。
気を取り直して、

$ which flutterfire
/Users/miyasic/.pub-cache/bin/flutterfire

お、コマンドのありかを見つけました。これで1つ目の疑問は解消されました!

さて、このコマンドの中身はどうなっているのかというと

flutterfire
#!/usr/bin/env sh
# This file was created by pub v3.3.1.
# Package: flutterfire_cli
# Version: 1.0.0
# Executable: flutterfire
# Script: flutterfire
if [ -f /Users/miyasic/.pub-cache/global_packages/flutterfire_cli/bin/flutterfire.dart-3.3.1.snapshot ]; then
  dart "/Users/miyasic/.pub-cache/global_packages/flutterfire_cli/bin/flutterfire.dart-3.3.1.snapshot" "$@"
  # The VM exits with code 253 if the snapshot version is out-of-date.
  # If it is, we need to delete it and run "pub global" manually.
  exit_code=$?
  if [ $exit_code != 253 ]; then
    exit $exit_code
  fi
  dart pub global run flutterfire_cli:flutterfire "$@"
else
  dart pub global run flutterfire_cli:flutterfire "$@"
fi

ShellScriptっぽい。
上の方はコメントのようなので、重要なのは7行目のifからですね。
ShellScriptにあまり明るくないので、雰囲気で読みますが、
まず/Users/miyasic/.pub-cache/global_packages/flutterfire_cli/bin/flutterfire.dart-3.3.1.snapshotというファイルがあるかどうかをみているようですね。ある場合はそのsnapshotファイルに対してdartコマンドで呼び出していそうです。
また、この呼び出しがexit_code 253で失敗した場合とsnapshotファイルがなかった場合についてはdart pub global runというコマンドでflutterfire_cliパッケージのflutterfireコマンドを呼び出しているようです。

これで2つ目の疑問も解消しました。
コマンドの正体はShellScriptであり、(何らかの分岐はしているものの)cliをshellscriptからdart pub global runコマンドで実行しているからDartに依存しているんですね、なるほど!

さて2つ疑問を解消しましたが、2つ疑問が生まれました。

疑問3: snapshotファイルって何?
疑問4: exit_code:253って何?

snapshotファイルについてですが、dartの公式ドキュメントによるとdartにはいくつかの種類のsnapshotファイルがあるようです。これらは基本的にはコンパイル済み(部分的もしくは全部)の生成物であり、0からコンパイルをするのではなく、途中からコンパイルをすることで実行速度を高めるためのもののようです。

ちょっと思ったより奥が深そうなのでこの程度の理解に留めておきます。

疑問4についてはGPTに聞いてみました。

GPTの回答

exit code 253は、Dart VMが特定の状況で返す終了コードです。このコードは、実行しようとしたDartのスナップショットが最新ではなく、更新が必要であることを示します。つまり、スナップショットファイルが古くなっていて、現在のDart VMや関連するライブラリと互換性がなくなっている場合にこの終了コードが返されます。

簡単にいうとそのsnapshotが期限切れになっているということですね。
先ほどのShellScript的にも253エラーが出たら0から実行し直すことになっていたので、おおかた間違ってなさそうです。

これで疑問3、4もふんわり解消されました!

ちなみに

じゃあ、brewでインストールしたfvmはなぜdartに依存していないんだというのも気になったので、確認してみました。

activateコマンドでインストールしてるのはdartのパッケージ(ソースコード)そのものだったのに対して、brewでは実行ファイルをそのままインストールしてるからDartに依存せずに実行できたんですね!

おわりに

今回は去年から気になっていたdart pub global activateのモヤモヤについて調べてみて解消されたのでよかったです。

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

https://github.com/miyasic

Discussion