🍣

Taskfileを有効活用して、Makefileのシェル芸から逃げる

2024/06/15に公開
2

はじめに

皆さん、Makefileは使っていらっしゃるでしょうか?
Makefileは、ソフトウェアのビルドプロセスを自動化するための設定ファイルです。主にUNIX系OSで使用され、プログラムのコンパイル、リンク、インストールなどの手順を記述することで、簡単に実行できます。
今回はBetter MakefileとしてTaskfileを紹介したいと思います。

サンプルなどを以下に置いてあります。ただ読むもよしですが、devcontainer.jsonを用意してあるので手元のdevcontainerで開くか、codespacesで開くと即座に実行可能な環境を作成することができます。よしなに使ってください。
https://github.com/Mkamono/MakeToTask

この記事では、Makefileの記法とTaskfileの記法を比較して紹介します。

Taskfile前提知識

https://taskfile.dev/
Taskfileは、Go言語で書かれたタスクランナー兼ビルドツールです。YAML形式で記述することができ、シンプルに書けることが特徴です。インストールはドキュメントに従ってください。

なお、GitHub Actionsでも導入可能なActionsが公式から公開されています。やさしいですね。

基本記法

Makefileの基本記法は以下のようになります。

Makefile
.PHONY: greet
greet:
	echo 'Hello World from Make!'

make greetで実行できます。非常に簡単ですね。

しかし、.PHONYとは何でしょうか?
これはダミーターゲットと呼ばれるものです。Makefileはビルドツールであるという性質上、コマンドを実行した結果の生成物という意味でターゲットを指定します。このとき、ターゲットが既に存在している場合はコマンドの実行をスキップするようになっています。

タスクランナーとして使用する場合、生成物が存在しないのでgreetというファイルがディレクトリに存在するだけでコマンドの実行がスキップされては困ります。そのためダミーターゲットを設定し、ディレクトリからgreetファイルを検索しないようにしているのです。直感的ではないですね。

Taskfile.yaml
version: "3"

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

このファイルが存在するディレクトリでtask greetを実行できます。簡単そうですが、現時点ではMakefileのほうがシンプルですね。

記法の比較

変数を使う

Makefile
GREETING = Hello!

.PHONY: greet-var
greet-var:
	echo $(GREETING)
Taskfile.yaml
  greet-var:
    cmd: echo {{ .GREETING }}
    vars:
      GREETING: "Hello!"

変数に関してはあまり記法は変わりませんね。しかしTaskfileの方は、greet_var変数のスコープが閉じており、より安全です。

ループする

Makefile
LIST = "Good_Morning!", "Good_Afternoon!", "Good_Evening!"
.PHONY: greet-loop
greet-loop:
	for i in $(LIST); do \
		make -s greet-var GREETING=$$i; \
	done
Taskfile.yaml
  greet-loop:
    cmds:
      - for: ["Good Morning!", "Good Afternoon!", "Good Evening!"]
        task: greet-var
        vars:
          GREETING: "{{ .ITEM }}"

だんだんと複雑になってきましたね。Taskfileのほうが、実動作がどの部分なのかがわかりやすく、認知コストがかなり低いように思います。
ループのリストを定義する際、Makefileではコマンドの一部として渡す関係上、スペースを入れることができません。(_で置き換えています)
シェルコマンドであるという部分がネックになっていて、かなり工夫が必要なイメージです。

無駄な実行を避ける

自分自身をコピーするスクリプトです。

Makefile
.PHONY: source-and-generate
source-and-generate:
	make makefile.copy

makefile.copy: Makefile
	cp Makefile makefile.copy
Taskfile.yaml
  source-and-generate:
    cmd: cp Taskfile.yaml Taskfile.yaml.copy
    sources:
      - Taskfile.yaml
    generates:
      - Taskfile.yaml.copy

Makefile本来の使い方といったところですね。Makefileではターゲット名のあとにソースを指定することで、そのソースが変更されたときのみ実行するようになっています。しかし問題点として、生成されるファイル名を知っている必要があり、生成物として単一のファイルしか指定できません。

生成されるファイル名に関しては、上記のように別のスクリプトを挟むことで任意の名前で実行できるようにしています。

Taskfileの方は、それぞれに説明がわかりやすくついているように見えます。生成されるファイル名を知らなくても実行できますし、生成物が複数の場合にも対応しています。

状態に応じて動作を切り替える

.vscodeディレクトリ、またはMakefile(Taskfile)が存在していない場合にのみスクリプトを実行するようになっています。

Makefile
.PHONY: check-status
check-status:
	@if [ -d .vscode ] && [ -f Makefile ]; then \
		exit 0; \
	else \
		echo 'something went wrong'; \
	fi
Taskfile.yaml
  check-status:
    cmd: echo 'something went wrong'
    status:
      - test -d .vscode
      - test -f Taskfile.yaml

Makefileではこのような条件分岐は、&&||を使った表現や、if文を使った表現があります。
Makefile、もといシェルの条件分岐はかなり煩雑な書き方になりやすい印象があるのに対し、Taskfileではstatusのいずれかがfalseの場合、cmdを実行するというわかりやすい記載になっています。その分柔軟な条件には対応できませんが、cmdの中でMakefileと同じ表現が使えるので大した問題にはならないかと思います。

Yes/Noプロンプトを表示する

Makefile
.PHONY: yes-or-no
yes-or-no:
	@read -p "Are you sure? [y/N] " answer; \
	if [ "$$answer" = "y" ]; then \
		echo 'You said yes'; \
	else \
		echo 'You said no'; \
	fi
Taskfile.yaml
  yes-or-no:
    prompt: Are you sure?
    cmd: echo 'You said yes'

こちらはTaskfileのほうが圧倒的にわかりやすいですね。Makefileのほうはシェルコマンドの知識がかなり必要になってきます。

ここまでのまとめ

ここまで比較を見てもらった人にはおわかりだと思いますが、Makefileはシェルの機能を最大限活用してタスクを実行してね、という方針なのに対し、Taskfileは用意してあるシンプルな文法に沿ってわかりやすくタスクを実行してね、という方針です。

タスクランナーという性格上、あまり頻繁に編集をする部分ではないため、個人的にはMakefileのような認知負荷の高いものよりTaskfileで管理をしたほうが誰にとっても読みやすいと思います。

よりTaskfileを活用するために

シェル補完を導入しよう

https://taskfile.dev/installation/#setup-completions

各種シェルに対して補完の導入ガイドが用意されているので、導入しましょう。入力が快適になります。

スキーマを導入しよう

https://taskfile.dev/integrations/#schema

スキーマを導入すると、書くときにフィールドがサジェストされ、調べる手間が限りなくゼロに近づきます。これも便利。

拡張機能を入れよう(VS Code)

https://taskfile.dev/integrations/#visual-studio-code-extension

拡張機能をいれると、サイドバーからタスクの一覧を見れたり、実行したりできます。task -aでもタスク一覧は見れますが、サイドバーからはスキップされるタスクかどうかの状態まで見ることができます。

Taskfileのここすき

ドキュメントが見やすい

https://taskfile.dev/usage/

https://www.gnu.org/software/make/manual/html_node/index.html

Makefileの書き方、というものはかなり複雑で、一応公式からドキュメントは出されていますがかなり読む気が失せるような内容です。複雑で、一見して意味のわからない記法などもたくさん出てきます。
さらにMakefileは基本シェルの知識でゴリ推していくのが作法のような面はあるので、シェルの知識を記法として頭に入れなければいけません。これはかなり大変ですよね。

一方Taskfileは、様々な機能をシンプルな記法で、Taskfileの知識で組み立てられるようになっているため、Taskfileのドキュメントを読めば大抵のことはできるようになっています。そしてドキュメントも非常に綺麗で見やすく、簡潔に記載してあるので読んでいて楽しいです。

全部を流し読みしてもそこまで苦痛ではないと思います。

watchが便利

Makefileでは、ファイルの変更を検知して自動でタスクを実行しようとすると、外部のスクリプトファイルを使用したり、inotifyなどをインストールして使ったりする必要があり、なかなか面倒くさいですが、Taskfileではデフォルトでwatchフィールドが用意されています。

Taskfile.yaml
  # Makefileの変更を監視してタスクを実行
  watch-makefile:
    cmd: echo 'Makefile changed!'
    watch: true
    sources:
      - Makefile

このように書くとwatch-makefileを実行した状態でMakefileを変更するとコマンドが実行されますし、watchを設定していないタスクでも-wフラグを付けるとwatch状態になります。これだけでもとても助かります。

参考

https://taskfile.dev/usage/#running-a-global-taskfile
https://zenn.dev/keitean/articles/aaef913b433677#変数

おまけ: より複雑なタスクをやってみる

長くなるので、余裕のある方だけお読みください。
以下ではGitHub CLIを使ってリポジトリの作成、シークレットの作成、最終的なクリアまで行います。

Makefile
TEST_REPO = MakeToTask-test2
TEST_REMOTE_REPO = upstream
TEST_GITHUB_ENV = TEST_ENV_VAR
GITHUB_USER = $(shell gh config get -h github.com user || echo unknown)

.PHONY: show-installation
show-installation:
	@if [ -x "$(command -v gh)" ]; then \
		echo "github cli seems to be not installed. Please see https://github.com/cli/cli";\
	fi

.PHONY: clean
clean:
	@read -p "Are you sure? [y,N]:" ans; \
	if [ "$$ans" = y ]; then  \
		make delete-repo-env; \
		make delete-test-repo; \
		gh auth logout; \
	fi

STATUS = $(shell gh auth status)
.PHONY: login
login: show-installation
	@if [ -z "$(STATUS)" ]; then \
		gh auth login -h github.com -s delete_repo || exit 0; \
	fi

.PHONY: create-test-repo
create-test-repo: login
	@gh repo create $(TEST_REPO) --private --source=. --remote=$(TEST_REMOTE_REPO) || exit 0

.PHONY: delete-test-repo
delete-test-repo: login
	@gh repo delete $(TEST_REPO) --yes; \
	git remote remove $(TEST_REMOTE_REPO) || exit 0

.PHONY: set-repo-env
set-repo-env: create-test-repo
	@gh secret --repo $(GITHUB_USER)/$(TEST_REPO) set $(TEST_GITHUB_ENV) -b "test-value"

.PHONY: delete-repo-env
delete-repo-env:
	@gh secret --repo $(GITHUB_USER)/$(TEST_REPO) delete $(TEST_GITHUB_ENV) || exit 0
Taskfile.yaml
version: "3"

env:
  TEST_REPO: MakeToTask-test2
  TEST_REMOTE_REPO: upstream
  TEST_GITHUB_ENV: TEST_ENV_VAR
  GITHUB_USER:
    sh: gh config get -h github.com user || echo unknown

tasks:
  show-installation:
    cmd: echo "github cli seems to be not installed. Please see https://github.com/cli/cli"
    silent: true
    status:
      - test -x "$(command -v gh)"

  clean:
    prompt: "Are you sure?"
    deps:
      - login
    cmds:
      - task: delete-repo-env
      - task: delete-test-repo
      - gh auth logout
    silent: true

  login:
    deps:
      - show-installation
    cmd: gh auth login -h github.com -s delete_repo
    status:
      - gh auth status
    silent: true

  create-test-repo:
    deps:
      - login
    cmds:
      - cmd: gh repo create {{ .TEST_REPO }} --private --source=. --remote={{ .TEST_REMOTE_REPO }}
        ignore_error: true

  delete-test-repo:
    deps:
      - login
    cmds:
      - cmd: gh repo delete {{ .TEST_REPO }} --yes
        ignore_error: true
      - cmd: git remote remove {{ .TEST_REMOTE_REPO }}
        ignore_error: true

  set-repo-env:
    deps:
      - create-test-repo
    cmd: gh secret --repo {{ .GITHUB_USER }}/{{ .TEST_REPO }} set {{ .TEST_GITHUB_ENV }} -b "test-value"

  delete-repo-env:
    cmd: gh secret --repo {{ .GITHUB_USER }}/{{ .TEST_REPO }} delete {{ .TEST_GITHUB_ENV }}
    ignore_error: true

この2つのファイルを見比べると、全体的な分量としてはTaskfileのほうが多くなっているものの、Makefileを初見で理解できる人は多くはないと思います。初学者の身からすると、「@を書くと出力を出さない」や、「$(shell ~~)で動的変数を埋め込める」といったルールは、書く側にとってハードルを高くしている文法だと思います。これらをyamlのフィールドとして用意しているのは非常に書きやすいです。わざわざ記法を調べる必要がなくなります。

Taskfileの特徴として、依存関係が非常にわかりやすいです。depsに記載してあるものに依存していることが明確にわかります。

新しくJoinしてきた人にはMakefileよりもTaskfileのほうが見やすいのではないでしょうか。

ということで長々と話しましたが、最後に言いたいことは

Taskfile最高!みんなもっと使え!

Discussion

rakiraki

最初の例のタスク名のとこ間違ってるようです(make も taskfile も)

.PHONY: hello
greet:
	echo 'Hello World from Make!'

これでは make hello できません。