Dartでバイナリ配布可能なCLIツールを作る

エンジニアのスキルセットは基本が重要!、とか考えていると永遠にHelloWorldしてしまう。
そこそこ長い間同じことを考えることで深い洞察ができるようになる効果はあると思うが、
それとは別に趣味とか知的好奇心とか、興味ドリブンでやってみたいという何かは永遠に満たされない。

Dart-langに慣れるために今欲しいツールをDart-langで書いてみる。
なんでDart-langなのか、とか細かいことは気にしない。

インストール

Flutter 1.21からFlutter-SDKに完全なDart-SDKが含まれる. FlutterをやるならFlutterを入れた方が良い.
Dart-langは別でインストールできる. 軽いのでFlutterをやらないならこちらが良い.


$ brew tap dart-lang/dart
$ brew install dart
$ dart --version
Dart SDK version: 2.16.0 (stable) (Mon Jan 31 15:28:59 2022 +0100) on "macos_x64"

空プロジェクトを作る

Dart-langのパッケージマネージャはpub. dart-langをインストールするとパスが通り使えるようになる.
空プロジェクトの足場を作るパッケージを使う. ちなみに足場はScaffold. 舞台裏はStagehand.


$ pub global activate stagehand
$ mkdir dart-cli-sample
$ stagehand console-full

出来上がった雛形の構成は以下.
よくある構成なので説明は省略.


.
├── CHANGELOG.md
├── README.md
├── analysis_options.yaml
├── bin
│   └── dart_cli_sample.dart
├── lib
│   └── dart_cli_sample.dart
├── pubspec.lock
├── pubspec.yaml
└── test
    └── dart_cli_sample_test.dart

Hello World

雛形のエントリポイントは bin/dart_cli_sample.dart にある main().
別途、lib/dart_cli_sample.dart にあるコードをimportしている.
CLIの雛形だからargumentsを引数にとる. Listで渡される.
まぁ普通.


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

void main(List arguments) {
  print('Hello world: ${dart_cli_sample.calculate()}!');
}

で、lib/dart_cli_sample.dart はどうなっているかというと以下みたいな感じ.
int型を返すcalculate()という関数が定義されていて6*7の計算結果を返す.


int calculate() {
  return 6 * 7;
}

インタラクティブに実行するには、dartコマンドにエントリポイントを渡す.
6*7=42がちゃんと出力された.


$ dart bin/dart_cli_sample.dart
Hello world: 42!

バイナリ生成と実行

これやりたいためにDart-langを選んでみた.
Macでバイナリ生成する場合Mac用のバイナリしか作れないといったように、
残念ながらクロスプラットフォーム非対応. CIを構築して各プラットフォーム用に実行しないといけない.


$dart compile --help                                                                                                           544ms  日  2/ 6 02:19:51 2022
Compile Dart to various formats.

Usage: dart compile  [arguments]
-h, --help    Print this usage information.

Available subcommands:
  aot-snapshot   Compile Dart to an AOT snapshot.
  exe            Compile Dart to a self-contained executable.
  jit-snapshot   Compile Dart to a JIT snapshot.
  js             Compile Dart to JavaScript.
  kernel         Compile Dart to a kernel snapshot.

Run "dart help" to see global options.

$ dart compile exe bin/dart_cli_sample.dart -o bin/out1                                                                                    12.9s  日  2/ 6 02:18:30 2022
Info: Compiling with sound null safety
Generated: /Users/ikuty/ikuty/dart-cli-sample/bin/out1

$ ./bin/out1
Hello world: 42!

exeオプションでself-contained、つまりDartランタイムが無い環境で実行可能ファイルを作成できる.
コンパイル方式としてAOT(Ahead Of Time)、JIT(Just In Time)を選べるという充実ぶり.
バイナリのサイズは, self-containedの場合, 5,033,856 bytes(約5MB) だった.


$ dart compile aot-snapshot bin/dart_cli_sample.dart -o bin/out2                                                                        日  2/ 6 02:47:32 2022
Info: Compiling with sound null safety
Generated: /Users/ikuty/ikuty/dart-cli-sample/bin/out2

$ dartaotruntime bin/out2                                                                                                               日  2/ 6 02:47:52 2022
Hello world: 42!

aot-snapshotオプションにより, プラットフォーム用の共有ライブラリとアプリケーションコードを分けられる.
self-containedと同様にAOTはMacOS,Windows,Linuxそれぞれのプラットフォームが提供される.
dartaotruntimeというコマンドにより実行する.
バイナリのサイズは 905,072 (約900KB)だった.


$ dart compile jit-snapshot bin/dart_cli_sample.dart -o bin/out3                                                                        日  2/ 6 02:53:02 2022
Compiling bin/dart_cli_sample.dart to jit-snapshot file bin/out3.
Info: Compiling with sound null safety
Hello world: 42!

$ dart run bin/out3                                                                                                            1005ms  日  2/ 6 02:53:23 2022
Hello world: 42!

jit-snapshotオプションにより,JIT実行可能なバイナリを出力できる.
プラットフォーム固有のDart中間コードを生成する.
dart compile時に1度実行されて処理結果が表示される.
ソースコードをparseした結果を事前に準備し,JIT実行時に再利用することで処理速度を上げる.
ちょっと詳しくは不明だがAOTよりも高速に処理できる可能性がある.
バイナリサイズは 4,824,016bytes. (約4.8MB)だった.


$ dart compile kernel bin/dart_cli_sample.dart -o bin/out4                                                                              日  2/ 6 03:05:54 2022
Compiling bin/dart_cli_sample.dart to kernel file bin/out4.
Info: Compiling with sound null safety

$ dart run bin/out4                                                                                                             699ms  日  2/ 6 03:06:02 2022
Hello world: 42!

kernelオプションにより,プラットフォーム非依存のKernelASTを生成する.
出力されたバイナリのサイズは1056 bytes (約1KB)だった.ソースコードのパスが含まれており,
おそらくソースコードを同時に配布する必要がある. AOTより遅い.

まとめ

Dart-langのHelloWorldコードを作成し各種コンパイルオプションを試した.
Go-langのそれとは異なりクロスプラットフォームのバイナリを生成できないが,
複数のコンパイルオプションが用意されていて,様々なパターンの運用に対応できそう.