皆さんMakefile使っていますか?
長いコマンドをプロジェクト毎に短いコマンドとして定義できるツールとして、多くの開発で使われていると思います。
近年、そのMakefileの上位互換と言われるTaskfileというツールをよく聞くので、今回はTaskfileについて調査してみることにしました。
Taskfileとは
Makefileよりも使いやすいことを目標としたGo製のタスクランナーツールです。
そもそもMakefileとは?
Makefileは、ソフトウェアのビルドプロセスを自動化するための設定ファイルです。
Makefileを使うことで、複雑なビルドプロセスを一つのコマンドとして、下のコマンドで簡単に実行することができるようにできます。
make <エイリアス>
ですが、近年タスクランナーとしての使い方をしているケースが多くみられます。
また、Makefileはコマンドを羅列する関係でかなり可読性が悪くなる傾向があります。
本来の使い方が違うこと、また可読性等のMakefileが持つ問題を解決し、より使い勝手良く開発されたタスクランナーツールがTaskfileです。
インストール
- homebrew
brew install go-task
- npm
npm install -g @go-task/cli
- go
go install github.com/go-task/task/v3/cmd/task@latest
他にもscoopyなど他のパッケージマネージャーにも対応していましたが、今回は割愛させていただきます。
詳しくは公式ドキュメントをご覧ください。
使い方
HelloWorldを出力させてみる
taskfile.ymlを作成して、下のようなフォーマットで記述します。
今回は公式ドキュメントにも書かれている例でechoコマンドで文字を出力するコマンドを打ってみようと思います。
version: '3'
tasks:
hello:
cmds:
- echo 'Hello World from Task!'
silent: true //コマンドを表示せず、出力だけ表示します。
ここでtask hello
と実行してみます
❯ task hello
Hello World from Task!
このようにechoコマンドでされて欲しい内容を実行してくれました。
デフォルトコマンドを設定する
taskfileではデフォルトコマンドを設定できます。
デフォルトコマンドというのは、task
と打っただけで実行できるコマンドのことです。
今回はtask一覧を表示させるコマンドを使ってみたいと思います。
taskfileではmakefileと違い、コマンド一覧を表示するコマンドがあります。
task --list-all
これを追加してみます。
version: '3'
tasks:
default:
cmds:
- task --list-all
hello:
cmds:
- echo 'Hello World from Task!'
silent: true
この状態でtask
を打ってみると
❯ task
task: [default] task --list-all
task: Available tasks for this project:
* default:
* hello:
このようにtask一覧が表示されるようになります。
ディレクトリを指定してみる
例えばフロントエンドとバックエンドのディレクトリが分かれてる時に依存関係のインストール用のコマンドを2つのMakefileを作成して行うのは不便ですよね
それを解決してくれる機能がtaskfileにはあります。
例えばbiomeのlintとstylelintのlintのコマンドを定義するとします。
Makefileの場合
lint:
yarn biome check --write ./src
yarn stylelint --fix "**/*.scss" --fix
Makefileではフロントエンドのディレクトリのルートにmakefileを作って、フロントエンドのディレクトリで実行する必要がありました。
make lint
Taskfileの場合
taskfileでは下のように記述して、プロジェクトのルートディレクトリに配置します。
cmds
を使用することで箇条書きに複数コマンドを指定できます。
version: '3'
tasks:
front:linter:
dir: ./client
cmds:
- yarn biome check --write ./src
- yarn stylelint --fix "**/*.scss" --fix
これを実行してみます。
❯ task front:linter
task: [front:linter] yarn biome check --write ./src
yarn run v1.22.22
$ /Users/maoz/Developers/HubMe/client/node_modules/.bin/biome check --write ./src
Checked 77 files in 43ms. No fixes applied.
✨ Done in 0.26s.
task: [front:linter] yarn stylelint --fix "**/*.scss" --fix
yarn run v1.22.22
$ /Users/maoz/Developers/HubMe/client/node_modules/.bin/stylelint --fix '**/*.scss' --fix
✨ Done in 1.11s.
これからさらにフロントエンドのディレクトリに移動して使ってみます。
❯ cd client
❯ task front:linter
task: [front:linter] yarn biome check --write ./src
yarn run v1.22.22
$ /Users/maoz/Developers/HubMe/client/node_modules/.bin/biome check --write ./src
Checked 77 files in 19ms. No fixes applied.
✨ Done in 0.21s.
task: [front:linter] yarn stylelint --fix "**/*.scss" --fix
yarn run v1.22.22
$ /Users/maoz/Developers/HubMe/client/node_modules/.bin/stylelint --fix '**/*.scss' --fix
✨ Done in 0.70s.
なんと動きました...
taskfileはgitと似ている感じで、プロジェクトのルートディレクトリにあれば、どこからでも、サブディレクトリ内でコマンドを呼び出せるようになるそうです。
今まで複数Makefileを作っていた問題が解決されそうです。
ファイルを分割する
Taskfileはファイルを分割して管理することができます。
先述したようにtaskfileひとつをプロジェクトのルートに置くことで、どこのディレクトリでもコマンドを呼び出せるようになりました。
しかし、大規模なプロジェクトでtaskfileひとつに全てのコマンドを書いていくととんでもない行数になって管理が難しくなることは想像しやすいと思います。
そういう場合にtaskfileはtaskfile.frontend.yml
、task.backend.yml
と二種類分けて使用することができます。
taskfileでは、ファイル名のtaskfileとymlの間に何かしら入れることができるようになるので、ファイルを分割して管理することができます。
公式では、プロジェクトで使うコマンドをtaskfile.dist.yml
として、個人で使うファイルをtaskfile.yml
として.gitignore
に追加して、区別して管理することができると紹介されていました。
他にもincludesを使って分けることもできます。
下のようにtaskfileを設定したとします。
version: '3'
includes:
docs: ./documentation # will look for ./documentation/Taskfile.yml
docker: ./DockerTasks.yml
この時実行は下のようなコマンドで行えます。
includesで指定していた名前空間の後ろに指定したファイルで記述したタスクを書くことで実行できます。
task docs:serve
task docker:build
こうすることでファイルを分割・共有して、ファイルの肥大化を抑えることができます。
グローバルで使えるコマンドを定義できる
npmで-gをつけてインストールするとグローバルにモジュールがインストールされて、そのモジュールをどこからでも呼び出せるようになると思います。
npm install -g <module>
それと似たようなことをtaskfileでも行うことができます。
$HOME
ディレクトリに当たる部分にtaskfileを設置することで、そこで設定したコマンドを実行してくれるようになります。
例えば、Macだと$HOME
ディレクトリは~
にあたるので、そこにTaskfile.yml
を設置します。
そして、-g(--global)をつけてtaskコマンドを実行すると~
ディレクトリ($HOME
ディレクトリ)にあるTaskfile.yml
を参照して、コマンドを実行してくれます。
task -g <cmd>
動的にTaskfileを生成して実行する
-t(--taskfile)を使用することで、標準入力からtaskコマンドの内容を渡して実行することができます。
下のコマンドだとcatコマンドで受け取ったtaskfileの中身を受け取って、そこからtaskコマンドが実行されていると思います。
task -t - <(cat ./Taskfile.yml)
# OR
cat ./Taskfile.yml | task -t -
catで取得できるのはtaskfileの中身なので、下のように|を通してtaskfileの中身をtaskに渡すことで実行することが可能になります。
echo 'version: "3"
tasks:
hello:
cmds:
- echo "Hello!"' | task -t - hello
環境変数が設定できる
taskfileは環境変数を設定ができます。
下のようにGREETING
という環境変数を設定して、$
でその環境変数を使用することができます。
version: '3'
env:
GREETING: Hey, there!
tasks:
greet:
cmds:
- echo $GREETING
またタスクごとに環境変数も設定でき、環境変数を除くことができるスコープも設定できます。
version: '3'
tasks:
greet:
cmds:
- echo $GREETING
env:
GREETING: Hey, there!
それ以外にも.env
を読み込むこともできます。
version: '3'
env:
ENV: testing
dotenv: ['.env', '{{.ENV}}/.env.', '{{.HOME}}/.env']
tasks:
greet:
cmds:
- echo "Using $KEYNAME and endpoint $ENDPOINT"
変数を設定できる
varsでコマンドに当たる変数名を指定して、requiredで変数名として設定する必要のある変数名を指定できます。
version: '3'
tasks:
deploy:
cmds:
- echo "deploying to {{.ENV}}"
requires:
vars:
- name: ENV
enum: [dev, beta, prod]
また、for文を使って変数を繰り返し使用できるようになります。
version: '3'
tasks:
default:
vars:
MY_VAR: foo.txt bar.txt
cmds:
- for: { var: MY_VAR }
cmd: cat {{.ITEM}}
内部で動作するタスク
internal
というプロパティをtrueにすることでtaskコマンドでは呼び出せないが、他のタスクでは呼び出せるコマンドを作成できます。
下の例ではbuild-image
というタスクはinternalがtrueになっているためtask build-image
と打っても呼び出すことができません。
しかし、build-image-1
のタスクでbuild-image
のタスクを呼び出すことはできるようになっています。
version: '3'
tasks:
build-image-1:
cmds:
- task: build-image
vars:
DOCKER_IMAGE: image-1
build-image:
internal: true
cmds:
- docker build -t {{.DOCKER_IMAGE}} .
こうすることで、タスク内で行われるコマンドをモジュール化して再利用することができます。
実行できるOSを指定できる
ファイル名をTaskfile_{{OS}}.yml
とすることで、OSで指定したOSの時のみ実行できるようになります。
例えば、windowsならTaskfile_windows.yml
、lunuxならTaskfile_linux.yml
というように名前を変更するとそれぞれOSごとに異なる動作をするタスクを定義できます。
必要なファイルのインストールに関して、使用するパッケージマネージャーが違うと思うのでこの仕組みは便利だと思いました。
また、taskfile内で記述して指定することもできます。
platformsにOSを指定することで、そのOSで実行されるコマンドを指定できます。
version: '3'
tasks:
build-windows:
platforms: [windows]
cmds:
- echo 'Running command on Windows'
他のタスクも一緒に呼ぶ
一緒に使用するタスクを定義
cmdsで箇条書きでコマンドを定義したと思うのですが、それをタスクに置き換えることもできます。
下の例のようにtask:
の後にタスク名を指定することでそのタスクを一緒に呼ぶことができます。
version: '3'
tasks:
main-task:
cmds:
- task: task-to-be-called
- task: another-task
- echo "Both done"
task-to-be-called:
cmds:
- echo "Task to be called"
another-task:
cmds:
- echo "Another task"
依存関係を定義
タスクを実行した後に実行するタスクをdepsプロパティで指定することができます。
下の例だとbuildタスクを実行した後、assetsタスクで定義された内容を実行されるようになっています。
version: '3'
tasks:
build:
deps: [assets]
cmds:
- go build -v -i main.go
assets:
cmds:
- esbuild --bundle --minify css/index.css > public/bundle.css
Makefileでもひとつのmakeコマンドでも前提条件として指定できました。
build: assets
go build -v -i main.go
assets:
esbuild --bundle --minify css/index.css > public/bundle.css
ただこれはbuildを指定した際に最初に実行されていたのが、taskfileでは依存関係として実行後に行われるので、実行タイミングの違いのようなものがあると感じました。
タスクの不要な実行を避けられる
taskfileでは、チェックサムで変更を検知することで、変更なファイルを無駄に実行しないようにできます。
例えば下のようなredoclyの結合タスクを設定した時に、sourceで中身が変更されたかどうか確認したらbundleを実行し、変更がなかったらスキップするようにできます。
version: '3'
tasks:
precommit:
deps: [bundle]
cmds:
- go build -v -i main.go
bundle:
cmds:
- docker run -v $(PWD):/spec --rm redocly/cli:latest bundle docs/swagger/root.swagger.yml --output=docs/swagger/generated.gen.swagger.yml
sources:
- docs/**/*.yml
generates:
- docs/swagger/generated.gen.swagger.yml
また、statusプロパティを使って行う方法もあります。
下の例だとstatusで書くファイルがあるかどうかをチェックし、もしなければcmdsを実行するようになっています。
version: '3'
tasks:
generate-files:
cmds:
- mkdir directory
- touch directory/file1.txt
- touch directory/file2.txt
# test existence of files
status:
- test -d directory
- test -f directory/file1.txt
- test -f directory/file2.txt
precommandsというプロパティでも同じことを行えます。
version: '3'
tasks:
build:
preconditions:
- sh: command -v go
msg: "Go must be installed"
- sh: test -f go.mod
msg: "go.mod file must exist"
cmds:
- go build
こうすることで実行時間の短縮を行えます。
加えて、runプロパティをつけることで実行できる回数を指定することができます。
runプロパティでは、always
、once
、when_changed
の三種類選べます。
version: '3'
run: once //タスクは一度まで選べる
tasks:
build:
cmds:
- go build
test:
run: always //いつでも実行できる
deps: [build]
cmds:
- go test ./...
generate-file:
run: when_changed //変数が変わったら実行
cmds:
- touch {{.CONTENT}}
並行実行する
--paralell
を指定することでタスクを並行実行させることができるようになります。
task --parallel js css
makefileでも-jで数を指定することで複数スレッドによるコマンドを実行することができました。
make -j2 js css
後置実行
defer
を書くことでcmdsのコマンドが実行された後に実行されるコマンドを指定できます。
クリーンアップとして使えるコマンドを指定するときに使えます。
version: '3'
tasks:
default:
cmds:
- mkdir -p tmpdir/
- defer: { task: cleanup }
- echo 'Do work on tmpdir/'
cleanup: rm -rf tmpdir/
Taskfileのよかったなと思った点
PHONYを書かなくてもいい
Makefileではコマンドと同じファイルがあった場合に実行できないということを防ぐため、.PHONY
でコマンドを指定する必要がありました。
.PHONY: greet
greet:
echo 'Hello World from Make!'
もしつけずに実行するとコマンドの実行をスキップされてしまいます。
しかし、taskfileではそのようなことが起きず、シンプルに書くことができます。
version: "3"
tasks:
greet:
cmd: echo 'Hello World from Task!'
複数ディレクトリに配置しなくていい
先述したように一つ定義してしまえば、ディレクトリを指定してコマンドを実行できてかつどこのディレクトリでもコマンドを呼び出せます。
とは言っても肥大化してしまうのは避けられないのでtaskfile.frontend.yml
、taskfile.backend.yml
と分けられるので、これをプロジェクトのルートディレクトリに置いておくと良さそうですね。
コマンドを使いまわしやすくするプロパティが多い
OSごとに動作内容を変えたり、precommit
等で不要なコマンド実行を避けたりなど、簡単にタスクを使い勝手良くすることができるのが良いと思います。
Makefileではどうしてもコマンドを駆使して書くようなケースも見かけることが多かったため、設定できる内容が多いのは良かったなと感じました。
まとめ
Taskfileはmakefileの欠点を補い、とても使いやすくしているツールだなと思いました。
今のところ欠点が見当たらないので、makefileではなくこれからtaskfileを使っていこうと思います。