タスクランナーをmakeからcargo-makeへ移行

Jun 30, 2019 18:00 · 456 words · 3 minute read rust

cargo-makeはRustで書かれたタスクランナー。makeをよりリッチにしたようで使い勝手が良かった。 今までMakefileや各種スクリプト言語で書いていたようなタスクをこれに移行している。

cargo-make | Rust task runner and build tool.

Rustじゃないプロジェクトでも普通に使えるので、Pythonをメインでつかう機械学習エンジニアの人でも使うと便利になると思う。

何ができるかというと、

まずインストール。後述するがRustがない環境向けにバイナリも配布されてる。

cargo install --force cargo-make

以下のように、タスクをMakefile.tomlに書いて、

[tasks.BUILD]
description = "Build hoge"
script = ['''
#!/usr/bin/env bash
echo "build ${@}..."
''']

[tasks.TEST]
description = "Test hoge"
script = ['''
#!/usr/bin/env python3
print("test ...")
''']
dependencies = ["BUILD"]

cargo make TEST あるいは makers TEST で実行。後者のが短いので好き。 これは BUILD(bash)->TEST(python) と依存するスクリプトを流した例。 以下のように、タスクが実行されログが出る。

$ makers TEST -- --option-a --option-b
[cargo-make] INFO - makers 0.20.0
[cargo-make] INFO - Using Build File: Makefile.toml
[cargo-make] INFO - Task: TEST
[cargo-make] INFO - Profile: development
[cargo-make] INFO - Running Task: init
[cargo-make] INFO - Running Task: BUILD
[cargo-make] INFO - Execute Command: "/usr/bin/env" "bash" "/var/folders/wc/k31v1cd10pg3l46x7q7kc1wjjk8c7c/T/cargo-make/x9KmMmJyej.sh" "--option-a" "--option-b"
build --option-a --option-b...
[cargo-make] INFO - Running Task: TEST
[cargo-make] INFO - Execute Command: "/usr/bin/env" "python3" "/var/folders/wc/k31v1cd10pg3l46x7q7kc1wjjk8c7c/T/cargo-make/LwmKbxQaWP.sh" "--option-a" "--option-b"
test ...
[cargo-make] INFO - Running Task: end
[cargo-make] INFO - Build Done  in 0 seconds.

やりたかったことや、既存のツールでの課題

それなりのプロジェクトで作業していると、メインのソフトウェアを書く以外にも以下のようなタスクが発生する。

  • CIでのビルドやテスト、デプロイ
  • データのダウンロード
  • ドキュメントのビルド
  • 開発ツールの起動

こうしたタスクを簡単に実行できるようにするために、Makefileを書いて make build test deploy とかで実行できるようにしておくと便利だ。 Makefileをタスクランナーとして上記のようなことをする例としては、 cookiecutter-docker-scienceのMakefileを見ると雰囲気がわかる。 これは当時かなりお世話になったツールです。

しかし、Makefileは書きにくい。凝ったことは難しいのでシェルスクリプト別に書いてそれをMakefileから呼んで…と、 いつの間にかCI用のオレオレフレームワークができてしまってメンテナンス辛い、みたいな感じになると思う。 この課題感に共感できる方はご飯食べに行きましょう(愚痴りましょう)。

特に、リリースビルドやデバッグビルドなどのオプションを実行時に与えたいときはmakeで実現するのは面倒だ。 とはいえ、AirflowやDigdag、Luigiなどワークフローエンジンを覚えたり開発環境に入れたりして使うほどでもないし、何より複雑にしたくない。1

使ったことないチームメンバでも、ぱっとみて、あーなるほどとなり、公式のドキュメントを見れば1時間もあればそれなりに使いこなせて、makeとワークフローエンジンの中間にあるくらいのものが欲しかった。

cargo-make

前置きが長くなったが、上記を解決してくれるなと思ったのがcargo-cmakeだった。 cargo-makeは、Makeの上位互換のような印象。 依存関係をもつタスクを簡単に定義できるのはもちろん、Makefile.tomlの継承やPythonスクリプトやシェルスクリプトの直書きなど、 タスクランナーとしてやりたいことはたいていカバーされていて便利だった。なにより記法や概念がシンプルでわかりやすかった。

Rustじゃないプロジェクトでも簡単に使えるのがポイント。 各プラットフォーム向けにビルド済みのバイナリがGitHubでリリースされているので、Rustの環境がないところでもうごくはず。

以降は、特に自分がよく使う例を書いておく。調べながら書いていたら長くなってしまった…

より詳細は本家README参照。

シェルスクリプトを直接書く

" で囲ったりせず普通に書けるので書きやすい

[tasks.bash]
script = [
'''
#!/usr/bin/env bash
echo Hello, World!
'''
]

Pythonスクリプトを直接書く

https://github.com/sagiegurari/cargo-make#other-programming-languages

ディレクトリ走査やJsonなどのコンフィグからなにかする時、Pythonで書くと楽に書けるのでタスクをPythonで直接記述できるのは嬉しい。

テンポラリファイルとしてPythonスクリプトを作成してそれをpython3で呼び出してるみたい。

[tasks.python]
script_runner = "python"
script_extension = "py"
script = [
'''
print("Hello, World!")
'''
]

ただし、shebangでも動くので以下のほうがシンプルに書ける。

[tasks.shebang-py]
script = [
'''
#!/usr/bin/env python3
print("Hello, World!")
'''
]

同様にして任意のスクリプト言語を埋め込める。

コマンドライン引数の読み取り

実行時に引数を渡したいケースはよくある。

これも、スクリプト実行時に普通に渡されてくるので、シェルスクリプトなら$@や$1とかで取ればいい。

[tasks.BUILD]
description = "Build hoge"
script = ['''
#!/usr/bin/env bash
echo "build ${@}..."
''']

ただし実行時は注意。–option のようなdashではじまるオプションはcargo-make自体のオプションだと解釈されてしまうので、–を挟む。

- cargo make BUILD --option-a  # NG
+ cargo make BUILD -- --option-a  # OK

Extend

https://github.com/sagiegurari/cargo-make#default-tasks-and-extending

複数のリポジトリやディレクトリからなるプロジェクトの場合、 プロジェクト全体で共通のタスクをデフォルトのMakefile.tomlに切り出し、各リポジトリ独自の部分は差分として書くようにしておくとメンテナンスが楽。

こんなイメージ。

extend = "../../Makefile.default.toml"

[tasks.repository-specific]
...

同じ名前のタスクを定義すると、 定義した属性(script, command, args, …)のみ 上書き/新規追加する。 上書き対象を一旦初期化したい場合は clear = true する。

たとえば、tasks.build を継承する場合、clear = true せずに script を定義すると、script と command が両方定義された情報になり実行時エラーになるのでclearは必要。 こうしたときのデバッグ手段として cargo make –print-steps とすると、タスクの詳細をダンプできるので、期待するtomlがかけているかを確認できる。ただしエラーメッセージを見ればそこまでしなくてもわかる。

[tasks.sometask]
clear = true
command = "echo"
args = [
    "extended task"
]

特にRustのプロジェクトでworkspaceを使う場合、workspace内のすべてのcrateのtomlに前記のextend を書かなくてもworkspace共通のtomlにCARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = “true”を書いとけば自動でextendされる機能があるみたい。他にも、Rust開発向けの特有の機能は色々あった。

また、extendで指定できるのはローカルのtomlのみだが、リモートからのダウンロードもサポート。例えば、共通のコンフィグレーションをまとめたGitHubのリポジトリから、最新のtomlをダウンロードして使いたいケースは、extendより先にダウンロードするためにload_scriptが使える。

[config]
load_script = ["git clone git@mygitserver:user/project.git /home/myuser/common"]

ちなみに、cargo-cmakeのビルトインのデフォルトタスクはの定義はここ。 何も書いてなくても cargo make build-release などが動くのはこのため。 他にも、カバレッジやlintなど、CIで実行する想定のタスクがいくつかあるので参考になる。

その他の機能

その他、こんなこともできるよ集。書ききれないくらい機能がある。

  • コマンド一覧をみたい
    • makers –list-all-steps でカテゴリ毎に説明がでる
    • これそのままREADMEに貼れば、リポジトリの使い方がわかっていいかも
    • ただcargo-cmakeのデフォルトのタスクもすべてダンプされるのでなあ
  • toml間じゃなくて、タスク間でもextendできる
  • CI実行時はstdoutをカラーでなくする
    • makers –no-color xxx
  • 同じコマンドでもOS毎に実行内容を切り替える機能
  • 環境変数やOSなどで、タスクの実行可否を指定する
  • あるコマンドの後に他のコマンドを実行したい
    • run_task をつかう。逆に、あるコマンドの前に実行してほしいのはdependenciesに書く
  • Rustのスクリプトをtomlの中に書きたい
  • devとprodで環境変数のファイルをわけたい
    • makers –env-file xxx.env
    • tomlに書いた環境変数をprofileで分ける機能もある
      • [env.development] [env.production] をそれぞれ書いて、makers –profile production みたいな
  • 実行時に環境変数をわたしたい
    • makers –env A=xxx
  • rustのworkspaceの扱い
    • デフォルトだと、workspace以下のすべてのcrateに対して指定したタスクを実行してしまう
    • makers –no-workspace xxx にすれば解決
  • makers task-a task-b task-c のように複数指定はできないみたい
    • これはmakeと違うので注意。task-a以降は引数として処理される
    • dependenciesでtask-c -> task-b, task-a として、 makers task-c とすればいい話

感想

自分はPython, C/C++, Rustなどを書くプロジェクトをやることが多いのだけど、それらのタスクランナーとしてcargo-makeは使えると思った。

これでタスクを定義しておくと、自分自身書きやすいし、他の人が書いたのもわかりやすいなあと思う。

例えば、MLで言えばモデルの学習とかも、個人でやる分にはいいんじゃないだろうか。複数のpythonスクリプトの組み合わせをタスクとして定義すると楽になりそう。例えば、

# 学習→デプロイ
$ makers train-deploy -- --config 001.yaml
# 評価単体
$ makers evaluate -- --config 001.yaml --threshold 1 
# traing-loopのデバッグ
$ makers run-test -- --config 001.yaml --data-slice 10

すくなくとも開発時に使うコマンドはエイリアスとしてtomlに書いちゃって、各種CIの設定ファイルからもmakers ci-buildとかさせるように、cargo-makeを中心に設計しちゃうのがわかりやすくていいんじゃないかと思ってる。とはいえ、dockerのビルドはdocker-compose build hogeで十分なので、わざわざcargo-makeにタスク化しなくてもいいよな、みたいな気持ちもある。

ただ、dockerわからないユーザーに対して処理を隠蔽するために、cookiecutter-docker-scienceのようにするのはありかも。

$ makers init-docker

作者の方のブログを読むと、使い所がわかってくる。 GitHub - sagiegurari/cargo-make: Rust task runner and build tool.

Rust開発時に便利な機能は、また別途。

また、以下は対応指定なさそう。ちゃんと調べていないけど。間違ってたら追記します。

  • タスクの時間計測は?
  • コマンドラインのオートコンプリートは?
  • タスクの依存関係をグラフで可視化できる?

  1. ちなみに私は @toyama0919 さんに教えてもらって以来、ワークフローエンジンはdigdagを好んで使ってる。 ↩︎

tweet