🐙

Makefileで開発効率をあげよう!

2023/07/29に公開

始めまして。@nerusan です。

開発において、コマンドを使うのは当たり前ですよね。
例えば、Docker を開発に使っているのであれば、以下のようなコマンドでコンテナを起動します。

$ docker compsoe build # ビルド
$ docker compose up -d # コンテナ起動
$ docker compose logs app -f # ログ確認

コマンドを打つ量も多く大変ですよね。

実際の開発は、Docker だけではなくて、さまざまなツールを使います。
それに応じて、さまざまなコマンドがあります。

以下、例を出します。(Go 言語での開発を前提とします。)

# フォーマッター
$ gofmt -l -s -w .
$ goimports -w -l .

# リンター
$ golangci-lint run

# マイグレーション
$ mysqldef -u ${DB_USER} -p ${DB_PASSWORD} -h ${DB_HOST} -P ${DB_PORT} ${DB_NAME} < ./_tools/mysql/schema.sql

# データシード
$ mysql ${DB_NAME} -h ${DB_HOST} -u ${DB_USER} -p${DB_PASSWORD} < ./_tools/mysql/seed.sql

# テスト
$ go test -cover -race -shuffle=on ./...

# テストカバレッジの詳細
$ docker compose exec app go test -cover ./... -coverprofile=cover.out
$ docker compose exec app go tool cover -html=cover.out -o tmp/cover.html
$ docker compose exec app rm cover.out
$ open ./tmp/cover.html

たくさんありますよね。。
実際は、これだけではなく、もっとたくさんのコマンドがあります。

チーム開発において、これらのコマンドは、README.md などのドキュメントに残すことが多い思います。
しかし、開発者はこれらのドキュメントがどこに書かれているのか探す必要があったり、また、都度手動で打ち込んだりするのは大変です。

また、追加のコマンドや使われなくなったコマンドがあれば、都度ドキュメントを更新する必要があり、
記述漏れが起きる可能性があり、うまく動作しないってことも考えられます。

その問題を解決する方法として、Makefileを紹介します。

基本的な使い方

実際に使ってみる方が早いと思うで、見てみます。

プロジェクトディレクトリのルートに以下のMakefileを準備します。

Makefile
# makeを打った時のコマンド
.DEFAULT_GOAL := help

.PHONY: build
build: ## ビルド
  # ビルド中
  @docker compsoe build

.PHONY: up
up: ## コンテナ起動
  # コンテナ起動中
  @docker compose up -d

.PHONY: build-up
build-up: build up ## ビルドし、コンテナ起動

.PHONY: help
help: ## ヘルプ
  @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
		awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'

そして、同じ街道でmekeと打ってみてください。

$ make
build                ビルド
up                   コンテナ起動
build-up             ビルドし、コンテナ起動

各コマンドと説明が出ていますね!
それに従って、次のコマンドを打ってみましょう。

$ make build
# ビルド中
[+] Building 104.8s (6/7)
 => [app internal] load build definition from Dockerfile                   0.2s
 => => transferring dockerfile: 1.24kB

実際にビルドされています!
これはかなり便利ですよね:)
コマンドの記述量も減りますし、
どのコマンドが何をするのか?をわかるので、別途ドキュメントを見る必要も記述する必要もなさそうです。

次の章から具体的な記述方法を見てみましょう。

Mkaefile の詳細

基本的な記述

ターゲットとその依存関係、そしてターゲットを実行するためのコマンドを定義します。
以下は、Makefile の基本的な書き方のテンプレート例です。

Makefile
ターゲット1: 依存関係1 依存関係2 ...
    コマンド1
    コマンド2
    ...

ターゲット名は、実際にコマンドを実行するために利用されます。
ターゲット名は自由に決めることができます。

依存関係 1 では、他のターゲット名を指定することができ、
ターゲットに指定されたコマンドが、実行前に実行されます。
また、複数指定することができます。

実行すると以下のようになります。

$ make ターゲット名1
依存関係1のコマンド
依存関係2のコマンド
...
コマンド1
コマンド2
...

具体的なコマンドで見てみましょう。

Makefile
build-up: build
  docker compose up -d

build:
  docker compose build
$ make build-up
docker compose build
docker compose up -d

以上が、Makefile の基本でした。

help

基本がわかったところで、help ので実行されるコマンドを見てみましょう。

help: ## ヘルプ
  @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
		awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'

何やら難しいことをしていますね。

このコードは、Makefile 内のターゲットとその説明を抽出して表示するスニペットです。
ターゲットの行には、##で始まるコメントを表示してくれます。
このコードの目的は、Makefile の中でドキュメンテーションを提供し、使用可能なターゲットとそれらの説明を簡単に確認できるようにすることです。

具体的なコマンドの動作は次のようになります。

grep -E '^[a-zA-Z_-]+:._?## ._$$' $(MAKEFILE_LIST):

$(MAKEFILE_LIST)は現在の Makefile のファイル名を表す特殊な変数です。このコマンドは、Makefile 内の各ターゲット行を抽出します。ターゲット行はターゲット名で始まり、その後ろに##で始まる説明コメントが続きます。

ターゲット名: ## 説明コメント

awk 'BEGIN {FS = ":._?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}':

awk コマンドは、抽出されたターゲット行のフォーマットを整形して表示します。FS = ":.\_?## "によって、コロン(:)と##以降の部分を区切り文字として指定しています。
ターゲット名と説明部分がそれぞれ$$1$$2に割り当てられます。printf関数を使ってフォーマットされた出力を生成します。
\033[36m は ANSI エスケープコードを使って色を指定するためのコードで、%-20s は 20 文字分の左寄せ表示を意味します。
\033[0mは色をリセットするためのコードです。

これで$ make helpを実行すると、Makefile 内の各ターゲット名とその説明が表示されます。
これにより、ターゲットとその説明が見やすく表示されるため、Makefile のドキュメンテーションを管理しやすくなります。

$ make help
ターゲット名                説明コメント

.DEFAULT_GOAL

これはmakeのみを叩いた場合にデフォルトのコマンドラインを示します。
ここでは help としているので、$ make help が表示されます。
つまり、以下のコマンドは同義です。

$ make help
$ make

.PHONY

.PHONY は、Makefile 内で使用される特別なターゲットの 1 つです。
.PHONY ターゲットは、実際のファイル名とは関係なく、常に実行されるターゲットを定義するために使用されます。

通常、Makefile では、ファイルのターゲットとその依存関係を定義し、make コマンドを実行すると、ターゲットの依存関係に基づいて必要なタスクが実行されます。

しかし、実は現在のディレクトリ以下にターゲットと同じファイル名が存在する場合、このサブコマンドは機能しません。

たとえば ↓ のように現在のディレクトリ以下に build というファイルを作ります。

$ touch build
$ make build
make: 'build' is up to date.

ビルドが実行されません。
つまり Makefile が build というファイルをビルドするターゲットのファイルとして認識していて、さらに build というファイルが存在しているので、結果的にコマンドを実行しないということになります。

そこで、.PHONYbuildを指定してあげることによって、同名のファイル、ディレクトリの存在に関わらず、コマンドが実行されるということです。

詳しくは以下の記事が参考になりました。
https://yu-nix.com/archives/makefile-phony/

@

実際のコマンドを表示させたくない場合は、コマンドの初めに@をつけます。

# @をつけない場合
# build:
#   docker compose build
$ make build
docker compose build
...

# @をつけた場合
# build:
#   @docker compose build
$ make build
...

Docker を使う場合のテクニック

Docker を使っていると、コンテナ内で実行するか、ホスト側で実行するかでコマンドの種類が異なる可能性があり、それぞれに応じて、コマンドを作ることになり大変です。
そこで、同じコマンドですが、それぞれの環境に応じて自動でコマンドを切り替える方法を考えます。

まず、コンテナかどうか判断するための環境変数を用意し、docker-compose.yml にフラグ環境変数を記述します。
環境変数名はなんでもいいですが、ホスト側で定義していないものがいいでしょう。

docker-compose.yml
services:
  app:
    environment:
      CONTAINER_ENV: true

そして、Makefile を以下のように記述します。

Makefile
.PHONY: test
test: ## テスト
  # テスト実行中
  @if [ ${CONTAINER_ENV} ]; then \
    # コンテナ側
    go test -cover -race -shuffle=on ./...; \
  else \
    # ホスト側
    docker compose exec app go test -cover -race -shuffle=on ./...; \
  fi

そうすると、同じ$ make testでもホスト側とコンテナ側で自動で切り分けることができるで、2 つ用意する必要がなくなります。

まとめ

どうでしたか?
Makefile を使うことでかなり、コマンドを打つことが楽になり、
効率も爆上がりになるかなって思います。
ぜひ、皆さんのプロジェクトにも取り入れてみてはいかがでしょうか。

GitHubで編集を提案

Discussion