cargo-makeがrust以外でも便利

まとめ

  • タスクランナーをどうするかは人類共通の課題
    • 自分以外のコンテキストの違う人を考えると大変
    • フロント・スマホの人がサーバ触るとか
  • 汎用的に使えるツールは表現力や変更が辛い
    • make, シェルスクリプト, 自作バイナリ
  • 表現力が高いツールは環境設定でハマりやすい
    • node/ruby/pythonは手元に入ってない場合がある
    • バージョン・依存関係問題で失敗したりする
    • Dockerで開発環境を提供してもDockerの外の事柄には対応できない
      • Dockerビルドしてデプロイとか
  • cargo-makeがいい感じに使えそう
    • バイナリ一個置くだけでインストールできる
      • rustのツールであるcargoは不要
    • Makefile.tomlの表現力がそこそこ高い
    • Pythonで拡張可能
    • npm-scriptsのより汎用的に使えるイメージ
  • 欠点も少ない
    • 表現力は豊か
    • Dockerと組み合わせばだいたいなんとかなる
      • docker run xxxxするタスクを作る
      • ユーザが触る世界はcargo-makeさえあればいい
      • cargo-makeとアプリ側のタスクとの両管理になるのはちょっと煩雑
    • ガッツリ開発する人→Docker内/直接インストールして自分でタスクを叩く
    • 利用者やそこまで深く触らない人→cargo-makeでDocker経由でタスクを叩く
    • cargo-makeから各言語のツールを叩いても良い

非メイン開発者を想定した場合のタスクランナー

プロジェクトにおいてmakeやrakeといったタスクランナーをどうするかは結構難しい問題です。多くのプログラミング言語では何らかのタスクランナー12が提供されているため、ほとんどの場合まずはそれを利用してプロジェクトのテストやlint、データ作成やデプロイなどを行っていると思います。

メイン開発者は多くの場合ランタイムや関連ライブラリをインストールし、それらのタスクランナーを実行できる環境にあるため問題は起きません。ですがDocker内でvscodeを利用して開発をしている場合はローカル環境(Dockerデーモンを実行している側の環境)にはランタイムを入れない場合があります。

また、サーバを手元で動かしたいマイクロサービスの利用者側の開発者やフロントエンド開発者など、必ずしもアプリケーションを実行できる環境をインストールしていない人たちも、テスト用にデータ作成といったタスクを実行したい場合があります。

macOSではRubyがデフォルトで入らなくなる3、WSL2によってWindows上のLinuxでの開発環境の勢力が強くなるといったことを考えると、特定のランタイムが入っていることを期待できません。 前述のようなゲスト開発者に対してランタイムのインストールから始める、またbundle installnpm install時に関連するライブラリのバージョン違い等でインストールが失敗するといった問題を解決してもらうのはかなりハードルが高く、特に違う技術スタックの開発者だった場合は解決しづらいです。 Dockerで環境を一式用意することである程度解決できますが、イメージのpushといったDocker外でやる作業、Dcokerへの習熟度といった問題もあるため、完全にDockerのみで解決はできません4

シェルスクリプトやmakeといったほとんどの環境で動くものを利用することでインストールの煩雑さを回避できますが、表現力はそこまで高くなく、また複数のタスクの依存関係といった点で書きづらさがあります。 goやrustでは簡単に複数の環境に対して実行可能なバイナリを作成できるため、専用ツールをプロジェクトごとに生成することで書きやすさを確保しつつ、汎用的に利用できることを両立できますが、代わりにちょっとしたタスクの変更ができないため更新や変更時に煩雑です。

ということで、様々な環境で汎用的に動き、インストール時に問題が起きにくく、気軽に更新でき、表現力もそこそこ高いものが欲しくなります。 今回はそれを実現するものとして cargo-make を紹介します。

cargo-mke

cargo-makeはRustで書かれたタスクランナー&ビルドツールです。 タスクを記述したTOMLファイルをおいておくと、それを読み込んで適切なタスクを実行してくれます。 基本はRustのcargoと組み合わせて使うものですが、実際にはcargoのインストールは不要で、cargo-make単体で動きます。 インストールもcargoを使わず、公式サイトからバイナリを落とすだけで入れられるため、インストールにハマるといったこともありません。

詳しいインストール方法や便利なtipsはcargo-makeをrust以外で利用するときの便利tipsに書きましたのでこちらをご覧ください。

cargo-makeのタスク定義

タスクはMakefile.tomlに書いていきます。 Makeよりだいぶ書きやすいです(ただし主観)。

[tasks.build]
category = "develop"
description = "ビルドを実行する"
script = [
'''
go build ./server
go build ./worker
'''
]

[tasks.upload]
private = true
category = "release"
description = "ファイルをS3にアップロード"
command = "aws"
args = ["s3", "cp", "xxxxxx"]

[tasks.deploy]
category = "release"
describe = "リリースを行う。ビルド&アップロード
run_task = [
    { name = ["build", "upload"] },
]

Dockerのラッパーにする

やっていることはただのコマンド実行なので、docker run xxx npx cypress runみたいに他のコマンドツールとの組み合わせも容易です。 また、docker-composeを使っている場合、開発用のdocker imageにcargo-makeを入れておき、以下のようなタスクを用意することでmakers docker_run makers build のように利用できます。 これにより、dockerに慣れていない利用者も気にせずに利用できますし、手元に環境を作っているメイン開発者はdockerなしで動かすことができ、非常に便利になります。

[tasks.docker_run]
category = "develop"
description = "docker-composeでコマンドを実行します (e.g. makers docker_run makers build)"
command = "docker-compose"
args = ["exec", "app", "${@}"]

cargo-makeの欠点

ただし、欠点も存在します。 たとえばRubyのRakeではアプリと同じ言語なため、ライブラリを使い回す、メソッドを呼び出すなどかなり結合度の高いタスクを書くことが出来ますが、cargo-makeではそういったものは難しいです。自分ひとりしか触らない場合や、ライブラリ開発など同じ技術スタックを期待できるような場合、より特化したタスクランナーのほうが効率が良いです。

そのため、基本的にはコマンド実行やタスクの依存関係の記述、ファイルの変更などに抑え、複雑な処理はアプリと親和性の高いタスクランナーを使うなど、ハイブリッドで利用していくのが良いと思います。 実際、例えばawscliを使うようなコマンドはそのままシェルスクリプトMakefile内に書いている一方で、DBにユーザを作るタスクはバリデーションなどがあるのでscript/test_user/main.goにコードを書いて、go run script/test_user/main.go USER PASSを実行するタスクをMakefile.toml書くといった使い分けをしています。

まとめ

cargo-makeは依存関係が無いために簡単に入れられ、makeよりも読み書きしやすい(主観)タスクランナーです。 特に依存関係が無いため、コンテキストを共有しない人が参加するプロジェクトや、手元に開発環境を整えないような場合には非常に便利です。 一方で特化したものに比べると弱い部分があるため、Dockerと組み合わせるなど、組み合わせて使うことでより便利に利用できます。

参考資料


  1. Pythonならinvokeとか https://www.pyinvoke.org/ ↩︎

  2. RubyならRakeがデフォルトで入っています https://docs.ruby-lang.org/ja/latest/library/rake.html ↩︎

  3. https://applech2.com/archives/20190606-apple-deprecations-python-ruby-perl.html ↩︎

  4. iOS/Android開発ならDocker使わなかったりするのですべての人が習熟していることを期待できない ↩︎