Go言語開発を便利にするMakefileの書き方

# Go言語開発での makeコマンド と Makefile

Go言語の開発では`make`コマンドをタスク自動化ツールとしてよく使います。
よく使うコマンド、自動化したいタスクを`Makefile`に記述しておくと、開発に使う複雑なコマンドをすぐに実行したり、チームで共有出来ます。
Makefileに対して、難しいイメージを持っているかもしれませんが、超基本のMakefileの書き方はとてもシンプルなものです。


# この記事の目的

* Makefileの超基本がわかる
* Go言語開発のタスク自動化ツールとしてのMakefileの書き方がわかる

# 前提知識

* シェルスクリプト についての知識

# 書き方

## 書き始める前の準備

EditorConfigを設定して、タブ / スペース によるインデントのトラブルに会わないようにしましょう。
[公式サイト](https://editorconfig.org/#download)にあなたのエディタが、EditorConfigをサポートしているか、プラグインの追加が必要かの一覧があります。
エディタのアイコンをクリックすればそれぞれのエディタ用のインストールページに飛びます。


`.editorconfig`ファイルを次のように設定して、プロジェクトのルートディレクトリに置きます。

```ini:.editorconfig
root = true

[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 2

[*.go]
indent_style = tab
indent_size = 4

[Makefile]
indent_style = tab
indent_size = 4
```

この設定で、Go言語とMakefileのどちらもハードタブ [^1] が適用されます。

[^1]: インデントにタブ文字を使うこと。タブの代わりにスペースを2個、または４個入れてインデントすることを「ソフトタブ」と言います。

## Makefileを作成する

プロジェクトのルートディレクトリに`Makefile`というファイルを作成します。
拡張子は不要です。

## コメントの書き方

コメントは行頭に `#` を書きます。

```Makefile:Makefile
# これはコメントです
```

## タスクの実行方法

`make`コマンドの引数にタスク名を渡します。
以下は `task` という名前のタスクを実行します。

```sh:make実行
$ make task
```

## タスクを定義する

タスク定義のインデントは必ずタブで行います。

### 一番シンプルなタスク定義

```Makefile:Makefile
task:
	command
```

### 実行したコマンドを表示したくない場合

```Makefile:Makefile
task:
	@command
```

### タスク定義が複数のコマンドの場合

```Makefile:Makefile
task:
	command1
	command2
```

### ワンライナーで書く

Makefileの記法というより、シェルスクリプトの記法です。

```Makefile:Makefile
task:
	command1 && command2
```

```Makefile:Makefile
task:
	command1 ; command2 ;\
	command3
```

### タスク定義が複数ある場合

```Makefile:Makefile
task1:
	command1
task2:
	command2
```

実行する場合は、タスク定義の名前を指定します。

```sh:make実行
$ make task2
```

### タスク定義名が、プロジェクトのファイル名やディレクトリ名と同じ場合

makeの仕様でタスク定義名と同じファイルが存在している場合はタスクが実行されません。
これを回避するためには、`.PHONY: task` をタスク定義に付けます。

```Makefile:Makefile
.PHONY: task
task:
	command
```

### あるタスク定義実行の前に別のタスク定義を依存タスクとして実行したい場合
`go build` の前に `go get` や `go test` を実行するなどが出来ます。

```Makefile:Makefile
task1:
	echo "task1実行"

task2: task1
	echo "task2実行"

task3: task1 task2
	echo "task3実行"
```

```sh:make実行結果
$ make task3
echo "task1実行"
task1実行
echo "task2実行"
task2実行
echo "task3実行"
task3実行
```

なお、task3の依存タスク定義を辿ると、 task1 が2回登場しますが、無駄な実行はされません。

### 自身のMakefileに定義してある別のタスク定義を実行したい場合

```Makefile:Makefile
task1:
	echo "task1実行"

task2:
	$(MAKE) task1
	echo "task2実行"
```

```sh:make実行結果
$ make task2
/Library/Developer/CommandLineTools/usr/bin/make task1
echo "task1実行"
task1実行
echo "task2実行"
task2実行
```

`$(MAKE)` ではなく、 `make` で書いてもこのケースでは動作しますが、
`make task2` に付けたオプションも引き継がせるためには`$(MAKE)`を使います。

## 変数を使う

通常のシェルスクリプト変数(環境変数も含む)に加え、Makefile変数があります。

### Makefile変数

#### 固定値を変数に設定する

`Makefile`に `変数名=値` の形式でMakefile変数を定義します。

```Makefile:Makefile
VAR=hello make
```

Makefile変数をタスク定義で使うには、変数名を`$()`で囲みます。[^2]

[^2]: `${}` で囲んでも`$()`と同じ動きになりますが、シェルスクリプトの変数と紛らわしくなるため、`$()`のみを使用します。


```Makefile:Makefile
VAR=hello make

hello:
	echo $(VAR)
```

```sh:make実行結果
$ make hello
echo hello make
hello make
```

#### シェル実行結果をMakefile変数に入れたい場合

`$(shell command)` を使用します。

```Makefile:Makefile
NOW=$(shell date)
task:
	echo $(NOW)
```

```sh:make実行結果
$ make task
echo Wed Oct 10 20:24:20 JST 2018
Wed Oct 10 20:24:20 JST 2018
```

### シェルスクリプト変数・環境変数の扱い

#### シェルスクリプト変数・環境変数の参照

通常、シェルスクリプト変数・環境変数は `$変数名` または `${変数名}` で参照出来ますが、MakefileではMakefile変数としての解釈が優先されるため、
`$${変数名}` などのように`$$`を重ねて、 `$`をエスケープする必要があります。

``` Makefile:Makefile
task:
	VAR="hello make" && echo $$VAR
```

```sh:make実行結果
$ make task
VAR="hello make" && echo $VAR
hello make
```

#### シェルスクリプト変数・環境変数の書き換え

タスク実行中に環境変数を書き換えたい場合は、コマンド実行が1行終わるごとに環境変数がmakeコマンド実行時に戻ることに注意します。

例えば、`export`で一時的に環境変数を書き換えて何かをするタスクを複数コマンドに分けて記述したい場合は、 `;\` で改行してコマンドを複数書きます。

```Makefile:Makefile
task1:
	export VAR="この値は消えてしまいます"
	echo VAR=$${VAR}

task2:
	export VAR="この値は残ります" ;\
	echo VAR=$${VAR}
```

```sh:make実行結果
$ make task1
export VAR="この値は消えてしまいます"
echo VAR=${VAR}
VAR=

$ make task2
export VAR="この値は残ります" ;\
        echo VAR=${VAR}
VAR=この値は残ります
```

## サブディレクトリのMakefileのタスク定義を実行する

```Makefile:サブディレクトリ./subdir/Makefile
sub-task:
	echo "hello"
```

```Makefile:ルートディレクトリMakefile
task:
	make -C subdir sub-task
```

## make実行時に変数の値を渡す

`make task 変数名=値` の形で変数を渡すことができます。

Makefile変数とシェルスクリプト変数の両方が上書きされます。

```Makefile:Makefile
VAR="これは上書きされる"
task:
	echo $(VAR) $$VAR
```

```sh:make実行結果
$ make task VAR=hello
echo hello $VAR
hello hello
```

# 応用パターンの紹介

## シェルスクリプトのifを使う

golintがインストールされていなければインストールする例です。

```Makefile:Makefile
install-golint:
	@if ! type golint; then go get -u golang.org/x/lint/golint ; fi
```

複数行で書くこともできます。

```Makefile:Makefile
install-golint:
	@if ! type golint; \
		then go get -v -u golang.org/x/lint/golint ; \
	fi
```

## Go言語情報の取得

go version, GOOS, GOARCHを取得してMakefile変数に入れる例です。

```Makefile:Makefile
GOVERSION=$(shell go version)
GOOS=$(shell go env GOOS)
GOARCH=$(shell go env GOARCH)
```

## go test -v の結果に色をつける

`go test -v`の結果は色気がないですが、`sed` とANSIカラーで無理やり色をつけてみる例です。

```Makefile
# ANSI color
RED=\033[31m
GREEN=\033[32m
RESET=\033[0m

COLORIZE_PASS=sed ''/PASS/s//$$(printf "$(GREEN)PASS$(RESET)")/''
COLORIZE_FAIL=sed ''/FAIL/s//$$(printf "$(RED)FAIL$(RESET)")/''

test:
  	go test -v ./... | $(COLORIZE_PASS) | $(COLORIZE_FAIL)
```

## コマンドをテンプレート化して使い回す

go build コマンドを開発環境用と本番環境用のパラメータ切り替えして使い回すことを想定した例です。
ビルドタグも切り替えしています。

```Makefile:Makefile
BUILD_TAGS_PRODUCTION='production'
BUILD_TAGS_DEVELOPMENT='development unittest'

build-base:
	go build -o $(BIN_NAME) -tags '$(BUILD_TAGS) netgo' -installsuffix netgo -ldflags '-s -w' main.go

build-development:
	$(MAKE) build-base BUILD_TAGS=$(BUILD_TAGS_DEVELOPMENT) BIN_NAME=dev

build-production:
	$(MAKE) build-base BUILD_TAGS=$(BUILD_TAGS_PRODUCTION) CGO_ENABLED=0 GOOS=linux GOARCH=amd64 BIN_NAME=prod 
```

## lintタスクを定義する

シェルスクリプトのコマンド置換`$()`を使っていますが、 `$` をエスケープして、`$$()`とする必要があります。

```Makefile:Makefile
lint:
	golint -set_exit_status $$(go list ./...)
	go vet ./...
```

# 終わりに

makeコマンド、 Makefileの仕様はここで紹介したもの以外にもたくさんあり、
私自身も使いこなせていない・よくわかっていない機能があります。
ですが、Go言語開発のお供のタスクランナーとして、よく使うコマンドを登録するくらいの用途であればMakefileは怖いものではありません。
どんどんタスクを登録して開発を便利にしていきましょう。