Makefileの書き方

最近、Terraformの実行など依存関係のある処理の実行でMakefileを使うので、よく使う基本的な文法を残しておきます。



# 基本的な書き方

```Makefile
ターゲット: 依存関係
	レシピ
```

# Makefileの決まり事

## インデントはタブ
スペースでのインデントは認められていないので注意

```Makefile
example:
	echo "Hello"
	@echo "World"
```

## コマンドの先頭の `@` はコマンドを表示せずに実行
レシピ内のコマンドの戦闘に `@` をつけると、コマンド自体の表示を行わなくなります。

```Makefile
example:
	echo "Hello"
	@echo "World"
```

```bash
make
# echo "Hello"
# Hello
# World
```

## レシピ内では行を分けると別シェル扱いになる
Makefileのレシピでは行を改行すると別シェルでコマンドが実行されるため、変数情報などを引き継げません。

例えば、このレシピだと、`echo "$$FOO"` で `FOO` を参照できません。
```Makefile
all:
	FOO=bar
	echo "$$FOO"  # FOO=bar を定義したシェルとは別のシェルで実行されるため、FOOを参照できない
```

`FOO` を参照するには `FOO=bar` と `echo "$$FOO"` を同じシェルで実行しなければならないため、改行を `\` でエスケープし、1行にします。

```Makefile
all:
	FOO=bar; \
	echo "$$FOO"  # bar
```

for文を書く場合はfor文全体を1行にする必要があります。

```makefile
ARR = a b
ARR += c d

all:
	@for e in $(ARR); do \
	  echo "$$e"; \
	done
```

## レシピ内で `$` を利用する場合は `$$` とエスケープする

レシピ内で変数の参照などを行う場合は `$` を `$$` とエスケープします。

```Makefile
all:
	FOO=bar; \
	echo "$$FOO"
```

```bash
make
# bar
```

## `.PHONY` : ファイルではなく命令として解釈すべきターゲット
Makefileではターゲット名がディレクトリやファイルと同じ名前の場合、そのファイルやディレクトリが存在するとターゲットは「最新」とみなされて実行されないことがあります。
`.PHONY` はこの挙動を回避して、常にターゲットを実行するための仕組みです。

このMakefileは `clean` というファイルが存在していても常に `clean` ターゲットを実行することができます

```makefile
.PHONY: clean
clean:
	@echo "clean!!"
```

```bash
touch clean
make clean
# clean!!
```

## `.DEFAULT_GOAL` : デフォルトターゲット
Makefileをターゲットを指定せずに実行したときのデフォルトターゲットを定義します。

```Makefile
.DEFAULT_GOAL := b

a:
	@echo "a"

b:
	@echo "b"
```

```bash
make
# b
```



# 変数定義
### `=` : 再帰的展開 (レシピ内で使うと展開されるたびに評価)

```Makefile
FOO = bar
```

- 変数が参照されるたびに評価されます。
- `FOO = $(BAR)` のように他の変数を入れておくと、BARの値が後で変更された場合でも常に最新のBARが展開されます。

### `:=` : 単純展開 (定義時に一度だけ評価)
```Makefile
FOO := bar
```
- 変数定義時に一度だけ評価されます。(不変)
- `FOO := $(BAR)` と記述した場合、定義時の`BAR` の値が `FOO` に代入され、後から `BAR` を変更しても `FOO` の値は変わりません。


#### 例)

```Makefile
BAR = 1
FOO := $(BAR)
BAR = 2

.PHONY: test
test:
	@echo "FOO = $(FOO)"
```

```bash
make test
# FOO = 1
```


### `?=` : 未定義の場合のみ代入
```Makefile
FOO ?= bar
```

- すでに `FOO` が他の場所で定義されている場合は何もしません。未定義の場合のみ値を代入します。
- 実行時に環境変数で上書きしたい変数にデフォルト値を設定しておきたい際に便利です。

#### 例)
```makefile
FOO ?= bar
all:
	@echo $(FOO)  
```
何も指定しないとデフォルト値、環境変数で指定すると上書きされる
```bash
make
# bar 

make FOO=baz  # 環境変数を指定して実行
# baz
```
#### 例) 環境変数が入力されているかをバリデーションする

```Makefile
APP_NAME ?=  # デフォルト値なし
STAGE ?= dev  # デフォルト値あり

# バリデーション: APP_NAMEが指定されていなければエラー
.PHONY: validate
validate:
	@if [ -z "$(APP_NAME)" ]; then \
		echo "[Err] APP_NAME is required"; \
		exit 1; \
	fi

.PHONY: deploy
deploy: validate
	@echo "APP_NAME=$(APP_NAME), STAGE=$(STAGE)"
```

```bash
make deploy APP_NAME=mido
# APP_NAME=mido, STAGE=dev

make deploy APP_NAME=mido STAGE=prd
# APP_NAME=mido, STAGE=prd

# デフォルト値が存在しない変数を指定しない場合はエラー
make deploy 
# [Err] APP_NAME is required
# make: *** [Makefile:7: validate] Error 1
```


### `+=` : 追加代入
```Makefile
FOO += bar
```

- すでに定義されている `FOO` の末尾に文字列を追加する
- リスト形式で複数要素を追加する際によく利用される

#### 例)

```makefile
ARR = a b
ARR += c d

all:
	@for e in $(ARR); do \
	  echo "$$e"; \
	done
```


# 変数展開
定義した変数を参照するときは `$(変数名)` または `${変数名}` と書きます。

```makefile
FOO = bar

all:
	@echo $(FOO)
```

```bash
make
# bar
```

# 自動変数
[Makefile の特殊変数・自動変数の一覧](https://tex2e.github.io/blog/makefile/automatic-variables)

```makefile
a:
	touch a
b:
	touch b
c: a
	touch c
d: b c
	@echo "ターゲット: $@"
	@echo "依存関係の一番最初: $<"
	@echo "すべての依存関係: $^"
	@echo "ターゲットよりもタイムスタンプが新しい依存関係: $?"
```

```bash
make d
# touch b
# touch a
# touch c
# ターゲット: d
# 依存関係の一番最初: b
# すべての依存関係: b c
# ターゲットよりもタイムスタンプが新しい依存関係: b c
```

- `$@` : ターゲット名
- `$<` : 依存関係の一番最初の名前
- `$^` : 全ての依存関係の名前
- `$?` : ターゲットよりタイムスタンプが新しい依存関係の名前

# 実際に使っているMakefile

```Makefile
SCRIPT_DIR := $(shell cd $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) && pwd)
PROJECT_NAME ?= hybrid-nodes-sample
STAGE ?=
COMPONENT ?=
COMMON_BACKEND_CONFIG := $(SCRIPT_DIR)/terraform/components/tfvars/backend.tfvars
COMMON_TFVARS := $(SCRIPT_DIR)/terraform/components/tfvars/common.tfvars
COMPONENT_DIR := $(SCRIPT_DIR)/terraform/components/$(COMPONENT)
COMPONENT_TFVARS=$(COMPONENT_DIR)/tfvars/$(STAGE).tfvars
TFPLAN_DIR := $(SCRIPT_DIR)/.tfplan/$(STAGE)/$(COMPONENT)

.PHONY: option-parser
option-parser:
	@if [ -z "$(PROJECT_NAME)" ]; then \
	  echo "[Err] PROJECT_NAME is required"; \
	  exit 1; \
	fi
	@if [ -z "$(STAGE)" ]; then \
	  echo "[Err] STAGE is required"; \
	  exit 1; \
	fi
	@if [ -z "$(COMPONENT)" ]; then \
	  echo "[Err] COMPONENT is required"; \
	  exit 1; \
	fi

.PHONY: tf-init
tf-init: option-parser ## terraform init
	terraform -chdir=$(COMPONENT_DIR) init \
	  -upgrade \
	  -reconfigure \
	  -backend-config $(COMMON_BACKEND_CONFIG) \
	  -backend-config "key=$(PROJECT_NAME)/$(STAGE)/$(COMPONENT)/terraform.tfstate"

.PHONY: tf-validate 
tf-validate: tf-init  ## terraform validate
	terraform -chdir=$(COMPONENT_DIR) validate

.PHONY: tf-plan
tf-plan: tf-validate ## terraform plan
	mkdir -p $(TFPLAN_DIR)
	terraform -chdir=$(COMPONENT_DIR) plan \
	  -var "project_name=$(PROJECT_NAME)" \
	  -var "stage=$(STAGE)" \
	  -var-file=$(COMMON_TFVARS) \
	  -var-file=$(COMPONENT_TFVARS) \
	  -out $(TFPLAN_DIR)/.plan
	terraform -chdir=$(COMPONENT_DIR) show -json $(TFPLAN_DIR)/.plan > $(TFPLAN_DIR)/plan.tfgraph

.PHONY: tf-apply
tf-apply: tf-validate ## terraform apply
	terraform -chdir=$(COMPONENT_DIR) apply \
	  -var "project_name=$(PROJECT_NAME)" \
	  -var "stage=$(STAGE)" \
	  -var-file=$(COMMON_TFVARS) \
	  -var-file $(COMPONENT_TFVARS) \
	  --auto-approve

.PHONY: tf-output
tf-output: tf-validate ## terraform apply
	terraform -chdir=$(COMPONENT_DIR) output

.PHONY: tf-destroy
tf-destroy: tf-validate ## terraform destroy
	terraform -chdir=$(COMPONENT_DIR) destroy \
	  -var "project_name=$(PROJECT_NAME)" \
	  -var "stage=$(STAGE)" \
	  -var-file=$(COMMON_TFVARS) \
	  -var-file $(COMPONENT_TFVARS) \
	  --auto-approve

.PHONY: help
.DEFAULT_GOAL := help
help: ## HELP表示
	@grep --no-filename -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
```