🐙

Grinderを使って、ターミナルで実行するタスクをDartで書く

に公開

Dartで書いたタスクをターミナルやCIで実行できるGrinderパッケージの紹介です。
https://pub.dev/packages/grinder
Grinderを導入するとgrind {タスク名}でターミナルから色々なタスクを行えるようになります。自分は以下のようなタスクをgrinderで実行してます。

  • build_runnerの実行(コマンド毎回忘れるやつ)
  • リリース時のpubspec.yamlのバージョン更新
  • リリースPull Requestの作成

導入方法

以下のコマンドを実行

dart pub global activate grinder

```text

パスが通ってないとエラーが出たら.zshrcや.bashrcに以下を追加してください。

```text:.zshrc
export PATH="$PATH":"$HOME/.pub-cache/bin"

```text

# 使い方

1. pubspec.yamlのdev_dependenciesにgrinderを追加します。

```yaml:pubspec.yaml
dev_dependencies:
  grinder: ^0.9.0

```text

(バージョンは執筆時の最新です)

2. プロジェクトのルートにtoolディレクトリを作り、そこにgrind.dartというファイルを作ります。

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

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

  1. Taskアノテーションをつけてタスクを作る
tool/grind.dart
('Generate docs.')
void doc() {
 log("Generating docs...");
}

  1. 実行する
    grind -hコマンドで実行可能なタスクが確認できます
    docがタスク名です
ターミナル
$ grind -h
~~~
Available tasks:
  doc          Generate docs.

```text

grind docのようにgrindの後にタスク名を指定すると、Dartで書いたタスクを実行できます。

```text:ターミナル
grind doc

```text

# 具体例

最近書いたタスクを紹介します。気に入ったらコピペして使ってみてください。

## よく使うzshのコマンドのショートカット

よく使うけど覚えられないコマンドをタスク化してます。エイリアスでもいいですが、grinderのタスクにするとFlutterプロジェクトとセットで扱えるので、チームメンバーと気軽に共有できるのが良いです。

runはgrinderパッケージのメソッドでioパッケージのProcess.runメソッドをラップしたものです。

```dart:tool/grind.dart
@Task('build_runnerでファイル生成')
Future<void> generate() async {
  run(
    'flutter',
    arguments: ['pub', 'run', 'build_runner', 'build', '--delete-conflicting-outputs'],
  );
}

@Task('CocoaPodsのアップデート(パッケージアプデ時に使う)')
void updatePods() {
  run(
    'rm',
    arguments: ['-rf', 'Pods/'],
    workingDirectory: 'ios',
  );
  run(
    'rm',
    arguments: ['-rf', 'Podfile.lock'],
    workingDirectory: 'ios',
  );
  run(
    'flutter',
    arguments: ['clean'],
  );
  run(
    'flutter',
    arguments: ['pub', 'get'],
  );
  run(
    'pod',
    arguments: ['install', '--repo-update'],
    workingDirectory: 'ios',
  );
}

```text

## pubspec.yamlのバージョンアップ

ファイル内の特定の文字列の置換みたいな操作もDartなので気軽にできます。

- context.invocation.argumentsで引数を受け取ってます
- failメソッドを使って引数がないとき処理を止めてます

```dart:tool/grind.dart
@Task('バージョン更新')
String incrementVersion() {
  final args = context.invocation.arguments;
  final newVersionName = args.getOption('version-name');
  if (newVersionName == null) {
    fail('--version-name=X.X.Xで新しいバージョン名を指定してください');
  }

  final pubspecFile = File('./pubspec.yaml');
  final pubspecString = pubspecFile.readAsStringSync();

  final pubspec = loadYaml(pubspecString);
  final version = pubspec['version'] as String;
  final splits = version.split('+');
  final versionCode = int.parse(splits[1]);
  final newVersionCode = versionCode + 1;

  final updatedPubspecString = pubspecString.replaceFirst(
    'version: $version',
    'version: $newVersionName+$newVersionCode',
  );
  pubspecFile.writeAsStringSync(updatedPubspecString);

  return '$newVersionName+$newVersionCode';
}

```text

上記のタスクにはyamlパッケージが必要です。
<https://pub.dev/packages/yaml>

実行時は以下のようにします。

```text:ターミナル
grind increment-version --version-name=1.0.1

```text

# 補足

## おすすめの使い方

Grinderのタスクが増えてるとgrind.dartが読みづらくなるので、自分はpartでタスクごとにファイルを分けました。

```text:ディレクトリ構成
tool
├── grind.dart
└── grinder_task
    ├── generate.dart
    ├── increment_version.dart
    └── update_pods.dart

```text

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

part 'grinder_task/generate.dart';
part 'grinder_task/increment_version.dart';
part 'grinder_task/update_pods.dart';

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

```text

```dart:tool/grinder_task/generate.dart
part of '../grind.dart';

@Task('build_runnerでファイル生成')
Future<void> generate() async {
  run(
    'flutter',
    arguments: ['pub', 'run', 'build_runner', 'build', '--delete-conflicting-outputs'],
  );
}

```text

## プロセスの経過を表示する

grinderパッケージのrunを使ってスクリプトを実行すると完了までログが出ません。
以下のようなメソッドを使えば経過が表示できます。

```dart:tool/grind.dart
Future<void> runCommand({
  required String command,
}) async {
  final splittedCommand = command.split(' ');
  log(command);
  final process = await Process.start(
    splittedCommand.first,
    splittedCommand.sublist(1),
  );
  stdout.addStream(process.stdout);
  stderr.addStream(process.stderr);
}

```text

利用側

```dart:tool/grinder_task/generate.dart
part of '../grind.dart';

@Task('build_runnerでファイル生成')
Future<void> generate() async {
  runCommand(
    command: 'flutter pub run build_runner build --delete-conflicting-outputs',
  );
}

```bash

## その他のGrinderタスクのアノテーション

具体例で使いませんでしたが、@DefaultTaskと@Dependsというアノテーションがあります。詳しくは公式のReadmeを見ていただきたいのですが、それぞれ

- @DefaultTask: grindとだけ打ったとき実行するタスクにつける
- @Depends: タスク実行前に他のタスクを実行する
  
ものです。

## Github Actionsで利用する

GrinderのタスクをGithub Actionsのworkflowに組み込むこともできます。
flutter pub getに時間がかかるので、利用はFlutter関連の処理にとどめた方がいいかもです。
こんな感じです

```yaml:.github/workflows/flutter_analyze.yml
name: on release pr merged
on:
    pull_request:
        branches:
            - master
        types: [closed]

jobs:
  on-release-pr-merged:
    runs-on: ubuntu-latest
    if: github.event.pull_request.merged == true
    steps:
      - uses: actions/checkout@v2.3.4
      - name: "Install Flutter"
        run: ./.github/workflows/scripts/install-flutter.sh stable
      - run: flutter pub get
      - name: grinder task
        run: dart tool/grind.dart hoge

Derryパッケージとの比較

Flutterプロジェクトによく使うコマンドを設定する方法としてDerryパッケージを使う方法もあります。
https://zenn.dev/k9i/articles/c54446a72f1f46
手軽にコマンドを共有するだけならDerry、より複雑なタスクを共有したいならGrinderが良いと思います。個人的には両方使い分けるのはややこしいのでGrinderを使うことにしました。

まとめ

Flutterエンジニアにとって使い慣れたDartでスクリプトが書けるのはとても便利ですね。

追記

最近はこんな感じの設定を使っています。よかったら参考にしてみてください。
気に入ったらリポジトリにスターしてもらえるとモチベが上がりますw
https://github.com/K9i-0/flutter_k9i_portfolio/blob/main/tool/grind.dart

GitHubで編集を提案

Discussion