Dartで書くタスクランナー Grinderのススメ

# 概要
Grinder とはDart で実装されたタスクランナーです。
あまり聞かないかもしれませんが、Flutterの公式サンプルでも導入されていたりします。

今回は導入方法からビルドタスクの書き方を紹介したいと思います。

Flutterの公式サンプルを確認したい方は下のリンクからどうぞ
https://github.com/flutter/samples/blob/master/web/samples_index/tool/grind.dart

# Version

- Flutter 
    - version: 1.22.4
    - channel: stable
- Grinder  
    - version: 0.8.6

# Grinder のインストール
Grider の[install](https://pub.dev/packages/grinder/install)を参考に進めていきます。
プロジェクトに対してビルドタスクを追加するので、１つFlutterプロジェクトを開いてください。

Grinder を有効にする

```bash
$ flutter pub global activate grinder
```

Grinderが有効になっているかを確認します。
grind と打って実際に動くか確認しましょう。
`Grinder`という名前ですがコマンドは`grind`です。

```bash
$ grind
```

まだビルドタスクの設定ファイルを作成していないので、下記のようなエラーが出ると思います。
`Error: expected to find 'tool/grind.dart' relative to the current directory.` 
あとで設定ファイルを作成するので、今は無視してください。

# 基本的な使い方
プロジェクトルートに `tool/grind.dart`というファイルを作成します

### ビルドタスクを追加する

以下は`dart analyze`を実行するビルドタスクです。

```dart:grind.dart
import 'package:grinder/grinder.dart';

main(List<String> args) => grind(args);

/// 静的解析
@Task('analyze')
void analyze() {
  Analyzer.analyze(['./lib']);
}
```

### 追加した静的解析のタスクを実行する

`grind analyze` で実行できます。
今回はわざと警告が出るようにしているので、以下のメッセージが表示されています。

```bash
$ grind analyze
grinder running analyze

analyze
  /Users/0maru/development/flutter/bin/cache/dart-sdk/bin/dartanalyzer ./lib/main.dart
  Analyzing lib/main.dart...
    lint • Prefer using lowerCamelCase for constant names. • lib/main.dart:9:16 • constant_identifier_names
  1 lint found.

finished in 7.9 seconds
```
### 追加したビルドタスク一覧を確認する

`grind -h` で登録しているタスク一覧を確認できます。

```bash
$ grind -h
Dart workflows, automated.

Usage: grinder [options] [<tasks>...]

Global options:
  --no-color           Whether to use terminal colors.
  --version            Reports the version of this tool.
  -h, --help           Print this usage information.

Available tasks:
  test                 unit test
                       (depends on: analyze)
  build                build
                       (depends on: test)
  analyze              analyze
  pub                  pub get
  runner               run build_runner
```


# 色々なビルドタスクを追加してみる

以下は、アプリのビルドやユニットテストを実行できるビルドタスク

```dart:grind.dat
import 'dart:io';
import 'dart:async';
import 'dart:convert';

import 'package:async/async.dart' show StreamGroup;
import 'package:grinder/grinder.dart';

main(List<String> args) => grind(args);

/// flutter pub get
@Task('pub get')
void pub() {
  _processLog(Process.start('flutter', ['pub', 'get']));
}


/// build_runner でファイルを生成
@Task('run build_runner')
void runner() {
  _runProcess(
      'flutter',
      ['pub', 'run', 'build_runner', 'build', '--delete-conflicting-outputs'],
  );
}

/// 静的解析
@Task('analyze')
void analyze() {
  Analyzer.analyze(['./lib']);
}

/// ユニットテスト
@Task('unit test')
@Depends(analyze) // 先にanalyzeを実行する
void test() {
  TestRunner().test(files: 'test/unit');
}

/// 静的解析・ユニットテストを行ったのちビルドする
@Task('build')
@Depends(test) // 先にtestを実行する
void build() {
  TaskArgs args = context.invocation.arguments;

  final mode = args.getOption('mode');
  log(mode);
  if (mode?.isEmpty ?? true) {
    throw Exception('mode option is required.');
  }

  // APサーバ、データベースの接続先などを指定する環境変数
  final isRelease = args.getFlag('release');
  final env = isRelease ? 'release' : 'staging';

  _processLog(Process.start(
    'flutter',
    ['build', mode, '--dart-define=ENV=${env}']
  ));
}


// ビルド中にログを出力する方法１
Future<void> _processLog(Future<Process> process) async {
  final _process = await process;
  final output = StreamGroup.merge([_process.stdout, _process.stderr]);
  await for (final message in output) {
    log(utf8.decode(message));
  }
}

// ビルド中にログを出力する方法２
Future<void> _runProcess(String executable, List<String> arguments) async {
  final result = await Process.run(executable, arguments);
  stdout.write(result.stdout);
  stderr.write(result.stderr);
}
```

# Github Actions でGrinderを利用する

Grinder で作成したビルドタスクはGithub Actions やTravis CI で使用することができます
以下Github Actions のサンプル

```yaml:build.yaml
name: Dart

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 2
    steps:
      - uses: actions/checkout@v2
      - uses: subosito/flutter-action@v1
        with:
          flutter-version: '1.22.x'
          channel: 'stable'
      - run: flutter pub get
      - name: test
        run: dart tool/grind.dart analyze
```

# まとめ

いかがでしたか？
Dart で書かれているので型を書くことができたり、CIサービスと同じ流れでローカルでもビルドができたりなどGrinderを使用するメリットはあるかと思います。
Makefileが早いだのシェルスクリプトで良いだのと言われるとそこまでなのですが、一度使ってみて快適なDartライフをお送りください。

今回作成したプロジェクトです
コード全体を見たい方は以下からどうぞ
https://github.com/0maru/grinder_sample

Twitterもやってます。
良ければフォローしてください。
https://twitter.com/0maru_dev