🐙

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

2022/02/18に公開

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

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

導入方法

以下のコマンドを実行

dart pub global activate grinder

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

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

使い方

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

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

  1. プロジェクトのルートにtoolディレクトリを作り、そこにgrind.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.

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

ターミナル
$ grind doc

具体例

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

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

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

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

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

('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',
  );
}

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

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

  • context.invocation.argumentsで引数を受け取ってます
  • failメソッドを使って引数がないとき処理を止めてます
tool/grind.dart
('バージョン更新')
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';
}

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

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

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

補足

おすすめの使い方

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

ディレクトリ構成
tool
├── grind.dart
└── grinder_task
    ├── generate.dart
    ├── increment_version.dart
    └── update_pods.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);
tool/grinder_task/generate.dart
part of '../grind.dart';

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

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

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

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);
}

利用側

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

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

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

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

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

ものです。

Github Actionsで利用する

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

.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