Taskfileについて調べてみる

皆さんMakefile使っていますか?

長いコマンドをプロジェクト毎に短いコマンドとして定義できるツールとして、多くの開発で使われていると思います。

近年、そのMakefileの上位互換と言われるTaskfileというツールをよく聞くので、今回はTaskfileについて調査してみることにしました。

# Taskfileとは
Makefileよりも使いやすいことを目標としたGo製のタスクランナーツールです。

https://taskfile.dev/

## そもそもMakefileとは?
Makefileは、ソフトウェアのビルドプロセスを自動化するための設定ファイルです。

Makefileを使うことで、複雑なビルドプロセスを一つのコマンドとして、下のコマンドで簡単に実行することができるようにできます。

```zsh:zsh
make <エイリアス>
```

ですが、近年タスクランナーとしての使い方をしているケースが多くみられます。

また、Makefileはコマンドを羅列する関係でかなり可読性が悪くなる傾向があります。

本来の使い方が違うこと、また可読性等のMakefileが持つ問題を解決し、より使い勝手良く開発されたタスクランナーツールがTaskfileです。

# インストール

- homebrew
```zsh:zsh
brew install go-task
```

- npm
```zsh:zsh
npm install -g @go-task/cli
```

- go
```zsh:zsh
go install github.com/go-task/task/v3/cmd/task@latest
```

他にもscoopyなど他のパッケージマネージャーにも対応していましたが、今回は割愛させていただきます。

詳しくは公式ドキュメントをご覧ください。

https://taskfile.dev/installation/

# 使い方

## HelloWorldを出力させてみる
taskfile.ymlを作成して、下のようなフォーマットで記述します。
今回は公式ドキュメントにも書かれている例でechoコマンドで文字を出力するコマンドを打ってみようと思います。

```taskfile.yml
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と違い、コマンド一覧を表示するコマンドがあります。
```zsh:zsh
task --list-all
```

これを追加してみます。
```taskfile.yml
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の場合
```Makefile
lint:
	yarn biome check --write ./src
	yarn stylelint --fix "**/*.scss" --fix
```

Makefileではフロントエンドのディレクトリのルートにmakefileを作って、フロントエンドのディレクトリで実行する必要がありました。
```zsh:zsh
make lint
```

### Taskfileの場合

taskfileでは下のように記述して、プロジェクトのルートディレクトリに配置します。
`cmds`を使用することで箇条書きに複数コマンドを指定できます。
```taskfile.yml
version: '3'

tasks:
  front:linter:
    dir: ./client
    cmds:
      - yarn biome check --write ./src
      - yarn stylelint --fix "**/*.scss" --fix

```

これを実行してみます。
```zsh:zsh
❯ 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.
```

これからさらにフロントエンドのディレクトリに移動して使ってみます。

```zsh:zsh
❯ 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を作っていた問題が解決されそうです。

:::note
### 呼び出しタスク名を短くする

aliasを指定すると呼び出す際の別名を決めることができます。

例えば下の例だと`task generate`と呼び出すところを`task gen`と呼び出せるようになります。

```taskfile.yml
version: '3'

includes:
  generate:
    taskfile: ./taskfiles/Generate.yml
    aliases: [gen]
```
:::

## ファイルを分割する
Taskfileはファイルを分割して管理することができます。

先述したようにtaskfileひとつをプロジェクトのルートに置くことで、どこのディレクトリでもコマンドを呼び出せるようになりました。

しかし、大規模なプロジェクトでtaskfileひとつに全てのコマンドを書いていくととんでもない行数になって管理が難しくなることは想像しやすいと思います。

そういう場合にtaskfileは`taskfile.frontend.yml`、`task.backend.yml`と二種類分けて使用することができます。
taskfileでは、ファイル名のtaskfileとymlの間に何かしら入れることができるようになるので、ファイルを分割して管理することができます。

公式では、プロジェクトで使うコマンドを`taskfile.dist.yml`として、個人で使うファイルを`taskfile.yml`として`.gitignore`に追加して、区別して管理することができると紹介されていました。

他にもincludesを使って分けることもできます。

下のようにtaskfileを設定したとします。
```taskfile.yml
version: '3'

includes:
  docs: ./documentation # will look for ./documentation/Taskfile.yml
  docker: ./DockerTasks.yml
```

この時実行は下のようなコマンドで行えます。
includesで指定していた名前空間の後ろに指定したファイルで記述したタスクを書くことで実行できます。

```zsh:zsh
task docs:serve
```

```zsh:zsh
task docker:build
```

こうすることでファイルを分割・共有して、ファイルの肥大化を抑えることができます。

:::note
### flattenオプション

flattenをtrueにすることで名前空間を使用せずに実行できるようになります。

例えば下ようにすると
```taskfile.yml
includes:
  docker:
    taskfile:  ./DockerTasks.yml
    flatten: true
```

先ほどまで名前空間でdockerを指定していたのが必要なくなります。
```zsh:zsh
task build
```

:::

# グローバルで使えるコマンドを定義できる
npmで-gをつけてインストールするとグローバルにモジュールがインストールされて、そのモジュールをどこからでも呼び出せるようになると思います。
```zsh:zsh
npm install -g <module>
```
それと似たようなことをtaskfileでも行うことができます。

`$HOME`ディレクトリに当たる部分にtaskfileを設置することで、そこで設定したコマンドを実行してくれるようになります。

例えば、Macだと`$HOME`ディレクトリは`~`にあたるので、そこに`Taskfile.yml`を設置します。

そして、-g(--global)をつけてtaskコマンドを実行すると`~`ディレクトリ(`$HOME`ディレクトリ)にある`Taskfile.yml`を参照して、コマンドを実行してくれます。

```zsh:zsh
task -g <cmd>
```

### 動的にTaskfileを生成して実行する
-t(--taskfile)を使用することで、標準入力からtaskコマンドの内容を渡して実行することができます。

下のコマンドだとcatコマンドで受け取ったtaskfileの中身を受け取って、そこからtaskコマンドが実行されていると思います。

```zsh:zsh
task -t - <(cat ./Taskfile.yml)
# OR
cat ./Taskfile.yml | task -t -
```

catで取得できるのはtaskfileの中身なので、下のように|を通してtaskfileの中身をtaskに渡すことで実行することが可能になります。

```zsh:zsh
echo 'version: "3"
tasks:
  hello:
    cmds:
      - echo "Hello!"' | task -t - hello
```

## 環境変数が設定できる

taskfileは環境変数を設定ができます。

下のように`GREETING`という環境変数を設定して、`$`でその環境変数を使用することができます。

```taskfile.yml
version: '3'

env:
  GREETING: Hey, there!

tasks:
  greet:
    cmds:
      - echo $GREETING
```

またタスクごとに環境変数も設定でき、環境変数を除くことができるスコープも設定できます。

```taskfile.yml
version: '3'

tasks:
  greet:
    cmds:
      - echo $GREETING
    env:
      GREETING: Hey, there!
```

それ以外にも`.env`を読み込むこともできます。

```taskfile.yml
version: '3'

env:
  ENV: testing

dotenv: ['.env', '{{.ENV}}/.env.', '{{.HOME}}/.env']

tasks:
  greet:
    cmds:
      - echo "Using $KEYNAME and endpoint $ENDPOINT"
```

## 変数を設定できる

varsでコマンドに当たる変数名を指定して、requiredで変数名として設定する必要のある変数名を指定できます。

```taskfile.yml
version: '3'

tasks:
  deploy:
    cmds:
      - echo "deploying to {{.ENV}}"

    requires:
      vars:
        - name: ENV
          enum: [dev, beta, prod]
```

また、for文を使って変数を繰り返し使用できるようになります。

```taskfile.yml
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`のタスクを呼び出すことはできるようになっています。

```taskfile.yml
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で実行されるコマンドを指定できます。
```taskfile.yml
version: '3'

tasks:
  build-windows:
    platforms: [windows]
    cmds:
      - echo 'Running command on Windows'
```

## 他のタスクも一緒に呼ぶ

### 一緒に使用するタスクを定義

cmdsで箇条書きでコマンドを定義したと思うのですが、それをタスクに置き換えることもできます。

下の例のように`task:`の後にタスク名を指定することでそのタスクを一緒に呼ぶことができます。
```taskfile.yml
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タスクで定義された内容を実行されるようになっています。
```taskfile.yml
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コマンドでも前提条件として指定できました。
```makefile
build: assets           
    go build -v -i main.go

assets:
    esbuild --bundle --minify css/index.css > public/bundle.css
```
ただこれはbuildを指定した際に最初に実行されていたのが、taskfileでは依存関係として実行後に行われるので、実行タイミングの違いのようなものがあると感じました。

## タスクの不要な実行を避けられる

taskfileでは、チェックサムで変更を検知することで、変更なファイルを無駄に実行しないようにできます。

例えば下のようなredoclyの結合タスクを設定した時に、sourceで中身が変更されたかどうか確認したらbundleを実行し、変更がなかったらスキップするようにできます。

```taskfile.yml
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を実行するようになっています。
```taskfile.yml
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というプロパティでも同じことを行えます。

```taskfile.yml
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`の三種類選べます。
```taskfile.yml
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`を指定することでタスクを並行実行させることができるようになります。
```zsh:zsh

task --parallel js css
```

makefileでも-jで数を指定することで複数スレッドによるコマンドを実行することができました。

```zsh:zsh
make -j2 js css
```

## 後置実行

`defer`を書くことでcmdsのコマンドが実行された後に実行されるコマンドを指定できます。

クリーンアップとして使えるコマンドを指定するときに使えます。

```taskfile.yml
version: '3'

tasks:
  default:
    cmds:
      - mkdir -p tmpdir/
      - defer: { task: cleanup }
      - echo 'Do work on tmpdir/'

  cleanup: rm -rf tmpdir/
```

# Taskfileのよかったなと思った点

## PHONYを書かなくてもいい

Makefileではコマンドと同じファイルがあった場合に実行できないということを防ぐため、`.PHONY`でコマンドを指定する必要がありました。

```makefile
.PHONY: greet
greet:
	echo 'Hello World from Make!'
```
もしつけずに実行するとコマンドの実行をスキップされてしまいます。

しかし、taskfileではそのようなことが起きず、シンプルに書くことができます。
```taskfile.yml
version: "3"

tasks:
  greet:
    cmd: echo 'Hello World from Task!'
```

## 複数ディレクトリに配置しなくていい

先述したように一つ定義してしまえば、ディレクトリを指定してコマンドを実行できてかつどこのディレクトリでもコマンドを呼び出せます。

とは言っても肥大化してしまうのは避けられないので`taskfile.frontend.yml`、`taskfile.backend.yml`と分けられるので、これをプロジェクトのルートディレクトリに置いておくと良さそうですね。

## コマンドを使いまわしやすくするプロパティが多い
OSごとに動作内容を変えたり、`precommit`等で不要なコマンド実行を避けたりなど、簡単にタスクを使い勝手良くすることができるのが良いと思います。


Makefileではどうしてもコマンドを駆使して書くようなケースも見かけることが多かったため、設定できる内容が多いのは良かったなと感じました。


# まとめ

Taskfileはmakefileの欠点を補い、とても使いやすくしているツールだなと思いました。

今のところ欠点が見当たらないので、makefileではなくこれからtaskfileを使っていこうと思います。