🐙

Makefile卒業 & Taskfile入門

2023/07/03に公開

はじめに

ある日、はてブを眺めていると以下の記事が流れてきました。

https://qiita.com/schrosis/items/12b4361c528819d13901

Makefileと違ってコマンドを書きやすそうでいいなと思いました。
というわけでMakefileでの記述方法とTaskfile.yamlでの代替方法についてまとめます。

記述方法

これまでMakefileの書き方とTaskfile.yamlの書き方を比較していきます。

基本

元記事にもあったHello, world!を両方でも出力するには以下のように記述します。
Makefileの方は、ファイルを生成しないため.PHONYという文言を付与する必要があります。
ここまではシンプルですね。

Makefile
.PHONY: hello
hello:
	echo "Hello, World!"

実行するときは$ make helloです。

Taskfile.yaml
version: '3'

tasks:
  hello:
    desc: This task says Hello, World!
    cmds:
      - echo "Hello, World!"

実行するときは$ task helloです。

実行可能なタスクのリスト

地味にあると便利なのが実行できるタスクの一覧です。

makeにはデフォルトでこの機能はないですが次のコマンドを組み合わせることで表示可能です。
makeでは$ makeと入力した時には一番上に定義されているタスクが実行されます。
そのため、そこに一覧を表示するようなタスクを定義することで一覧表示ができます。

Makefile
.PHONY: help
help: ## show commands ## make
	@printf "\033[36m%-30s\033[0m %-50s %s\n" "[Sub command]" "[Description]" "[Example]"
	@grep -E '^[/a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | perl -pe 's%^([/a-zA-Z_-]+):.*?(##)%$$1 $$2%' | awk -F " *?## *?" '{printf "\033[36m%-30s\033[0m %-50s %s\n", $$1, $$2, $$3}'

.PHONY: hello
hello: ## say hello ## make hello
	echo "Hello, World!"

この状態で$ makeを実行すると次のようになります。

一方で、taskにはデフォルトでタスクを一覧する機能があります。
そして、defaultというキーでタスクを定義すると$ taskを実行した時に呼ばれます。
それらを組み合わせることでタスクの一覧を$ task -lとせずとも一覧できます。
注意点として、Taskfile.yamldescキーを埋める必要があります。

Taskfile.yaml
version: '3'

tasks:
  default:
    desc: show commands
    cmds:
      - task -l --sort none
  hello:
    desc: This task says Hello, World!
    cmds:
      - echo "Hello, World!"

この状態で$ taskを実行すると次のようになります。

実行ディレクトリの移動

Makefileだと1つのコマンドの中に以下のように記述する必要がありました。

Makefile
.PHONY: cd-ls
cd-ls:
	(cd example-dir && ls)

一方でtaskだと、ディレクトリを移動する機能が存在します。
便利ですね!

Taskfile.yaml
version: '3'

tasks:
  cd-ls:
    dir: example-dir
    cmds:
      - ls

定数

定数の定義は以下のように書けます。

Makefile
MESSAGE := Hello

.PHONY: hello
hello:
	echo "$(MESSAGE), World!"

taskでは定数の定義をタスクごとか、全体で共有するかも分けることができます。

タスク全体の場合。

Taskfile.yaml
version: '3'

vars: { MESSAGE: 'Hello' }

tasks:
  hello:
    desc: This task says Hello, World!
    cmds:
      - echo "{{.MESSAGE}}, World!"

タスクごとの場合。

Taskfile.yaml
version: '3'

tasks:
  hello:
    desc: This task says Hello, World!
    cmds:
      - echo "{{.MESSAGE}}, World!"
    vars: { MESSAGE: 'Hello' }

引数によって利用する定数を変更する

例えば環境ごとに実行するタスクの定数などを変えたい場合を想定しています。

Makefileだと強引に以下のように記述することで、引数に応じて値を変えることもできます。
(本来の使い方から大きく外れている気がするので良くない気もしますが…)。
これは$ make env=dev$ make env=prodのように実行できます。

Makefile
ifeq ($(env),prod)
  MESSAGE := "this is prod. Hello"
else ifeq ($(env),staging)
  MESSAGE := "this is staging. Hello"
else
  MESSAGE := "this is dev. Hello"
endif

.PHONY: hello
hello:
	echo "$(MESSAGE), World!"

taskでは環境変数ファイル*.envを利用することで同じことができます。

dev.dist.env
MESSAGE="this is dev. Hello"
prod.dist.env
MESSAGE="this is prod. Hello"
Taskfile.yaml
version: '3'

# デフォルトの引数を与えている
# see: https://taskfile.dev/usage/#namespace-aliases
env:
  ENV: '{{.ENV | default "dev"}}'

dotenv: ['{{.ENV}}.dist.env']

tasks:
  hello:
    desc: This task says Hello, World!
    cmds:
      - echo "$MESSAGE, $env World!"
    requires:
      vars: [env]

以下のコマンドで実行できます。
注意点としては、変数をtaskコマンドより前に記述することです。
(shellでよく使うコマンドの前に変数を定義しているだけです)

# dev環境
$ ENV=dev task hello
# デフォルトの引数を利用する場合
$ task hello

# prod環境
$ ENV=prod task hello

変数の定義

値を動的に取得する場合(shellを実行して値を取得する場合)は少し煩雑で、
以下のようにevalを利用する必要があります。

Makefile
$(eval AWS_ACCOUNT := $(shell aws sts get-caller-identity | jq -r .Account))

.PHONY: hello
hello:
	echo "$(AWS_ACCOUNT)"

taskの場合はシンプルに通常のコマンドと同じように記述できます。
いいですね!

Taskfile.yaml
version: '3'

vars:
  AWS_ACCOUNT:
    sh: aws sts get-caller-identity | jq -r .Account

tasks:
  hello:
    cmds:
      - echo "{{.AWS_ACCOUNT}}"

引数によって変数の定義コマンドを変更する

かなり気持ち悪い状況ですが、
例えばAWS_ACCOUNTprofileに応じた値を取得したい場合などに使えます。

Makefileではifdefというものを利用してprofileが定義されているか判定し、
されている場合とそうでない場合とで実行するコマンドを切り替えています。
これは$ make account profile={profile-name}で実行できます。

Makefile
ifdef profile
  $(eval AWS_ACCOUNT := $(shell aws sts get-caller-identity --profile $(profile) | jq -r .Account))
else
  $(eval AWS_ACCOUNT := $(shell aws sts get-caller-identity | jq -r .Account))
endif

.PHONY: account
account:
	echo "$(AWS_ACCOUNT)"

Taskfile.yamlでは、if[]を使って実行するコマンドを切り替えています。
これは$ profile={profile-name} task accountで実行できます。
一応標準的なシェルスクリプトの範囲で収まっているので見やすいですね。

Taskfile.yaml
version: '3'

vars:
  AWS_ACCOUNT:
    sh: |
      if [ "${profile:+foo}" ]; 
      then aws sts get-caller-identity --profile $profile | jq -r .Account; 
      else aws sts get-caller-identity | jq -r .Account; 
      fi

tasks:
  account:
    cmds:
      - echo "{{.AWS_ACCOUNT}}"

参考:bashで[ -v "$VAR" ]を使わずに変数が未定義か判定する

他のTaskfile.yamlを含める

あるTaskfile.yamlに他のTaskfile.other.yamlなどを含めることができます。
makeにはない機能です(あったらすいません)。

例えば、dockerに関する操作をまとめたTaskfile.docker.yamlを作成します。

Taskfile.dokcer.yaml
version: '3'

tasks:
  build:
    desc: build image
    cmds:
      - echo "build"

  push:
    desc: push image
    cmds:
      - echo "push"

  run:
    desc: run image
    cmds:
      - echo "run"

  exec:
    desc: exec container
    cmds:
      - echo "exec"

  stop:
    desc: stop container
    cmds:
      - echo "stop"

そして、今まで通りのTaskfile.yaml以下のように作成します。

Taskfile.yaml
version: '3'

includes:
  docker: ./Taskfile.docker.yml

tasks:

  default:
    cmds:
      - task -l --sort none
    silent: true

  hello:
    desc: This task says Hello, World!
    cmds:
      - echo "$MESSAGE, $env World!"

include:の時に指定したdockerというキーがprefixとして機能します。
$ taskを実行すると以下のようになり、$ task docker:buildでタスクを実行できます。

CI/CDについて

GitHub Actionsや各種クラウドでのパイプライン時に使う場合を考えてみます。
makeはデフォルトで入っていますが、taskはインストールする必要があります。
そこで、GitHub ActionsとAWSのCodeBuildへの導入方法もまとめます。

GitHub Actions

GitHub Actionsに関しては公式が導入方法を提供してくれています。
以下のコードを追加することで実行できます。

.github/workflows/example.yaml
- name: Install Task
  uses: arduino/setup-task@v1
  with:
    version: 3.x

公式サイトではrepo-tokenを付与していますが、arduino/setup-taskを見た限りでは、
任意でよくて頻繁に利用する場合のみ付与した方が良いとのことです。

CodeBuild

CodeBuildに関しても直接の記述はないですが、Install Scriptを参考に導入できます。
ただ、PATHは通っていないので通す必要があります。

buildspec.yaml
version: 0.2
phases:
  install:
    runtime-versions:
      nodejs: 16
  pre_build:
    commands:
      # install task
      - sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin
      - export PATH=$PATH:~/.local/bin
  build:
    commands:
      - task deploy

上記のpre_buildの内容をそのまま使うことで他のサービスへも流用できます。

おわりに

今回は今まで利用していたMakefileを卒業して、Taskfileに入門してみました。
CI/CDでも難なく使えるので徐々に乗り換えて行く予定です!

GitHubで編集を提案

Discussion