最近、Terraformの実行など依存関係のある処理の実行でMakefileを使うので、よく使う基本的な文法を残しておきます。
基本的な書き方
ターゲット: 依存関係
レシピ
Makefileの決まり事
インデントはタブ
スペースでのインデントは認められていないので注意
example:
echo "Hello"
@echo "World"
コマンドの先頭の @
はコマンドを表示せずに実行
レシピ内のコマンドの戦闘に @
をつけると、コマンド自体の表示を行わなくなります。
example:
echo "Hello"
@echo "World"
make
# echo "Hello"
# Hello
# World
レシピ内では行を分けると別シェル扱いになる
Makefileのレシピでは行を改行すると別シェルでコマンドが実行されるため、変数情報などを引き継げません。
例えば、このレシピだと、echo "$$FOO"
で FOO
を参照できません。
all:
FOO=bar
echo "$$FOO" # FOO=bar を定義したシェルとは別のシェルで実行されるため、FOOを参照できない
FOO
を参照するには FOO=bar
と echo "$$FOO"
を同じシェルで実行しなければならないため、改行を \
でエスケープし、1行にします。
all:
FOO=bar; \
echo "$$FOO" # bar
for文を書く場合はfor文全体を1行にする必要があります。
ARR = a b
ARR += c d
all:
@for e in $(ARR); do \
echo "$$e"; \
done
レシピ内で $
を利用する場合は $$
とエスケープする
レシピ内で変数の参照などを行う場合は $
を $$
とエスケープします。
all:
FOO=bar; \
echo "$$FOO"
make
# bar
.PHONY
: ファイルではなく命令として解釈すべきターゲット
Makefileではターゲット名がディレクトリやファイルと同じ名前の場合、そのファイルやディレクトリが存在するとターゲットは「最新」とみなされて実行されないことがあります。
.PHONY
はこの挙動を回避して、常にターゲットを実行するための仕組みです。
このMakefileは clean
というファイルが存在していても常に clean
ターゲットを実行することができます
.PHONY: clean
clean:
@echo "clean!!"
touch clean
make clean
# clean!!
.DEFAULT_GOAL
: デフォルトターゲット
Makefileをターゲットを指定せずに実行したときのデフォルトターゲットを定義します。
.DEFAULT_GOAL := b
a:
@echo "a"
b:
@echo "b"
make
# b
変数定義
=
: 再帰的展開 (レシピ内で使うと展開されるたびに評価)
FOO = bar
- 変数が参照されるたびに評価されます。
-
FOO = $(BAR)
のように他の変数を入れておくと、BARの値が後で変更された場合でも常に最新のBARが展開されます。
:=
: 単純展開 (定義時に一度だけ評価)
FOO := bar
- 変数定義時に一度だけ評価されます。(不変)
-
FOO := $(BAR)
と記述した場合、定義時のBAR
の値がFOO
に代入され、後からBAR
を変更してもFOO
の値は変わりません。
例)
BAR = 1
FOO := $(BAR)
BAR = 2
.PHONY: test
test:
@echo "FOO = $(FOO)"
make test
# FOO = 1
?=
: 未定義の場合のみ代入
FOO ?= bar
- すでに
FOO
が他の場所で定義されている場合は何もしません。未定義の場合のみ値を代入します。 - 実行時に環境変数で上書きしたい変数にデフォルト値を設定しておきたい際に便利です。
例)
FOO ?= bar
all:
@echo $(FOO)
何も指定しないとデフォルト値、環境変数で指定すると上書きされる
make
# bar
make FOO=baz # 環境変数を指定して実行
# baz
例) 環境変数が入力されているかをバリデーションする
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)"
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
+=
: 追加代入
FOO += bar
- すでに定義されている
FOO
の末尾に文字列を追加する - リスト形式で複数要素を追加する際によく利用される
例)
ARR = a b
ARR += c d
all:
@for e in $(ARR); do \
echo "$$e"; \
done
変数展開
定義した変数を参照するときは $(変数名)
または ${変数名}
と書きます。
FOO = bar
all:
@echo $(FOO)
make
# bar
自動変数
a:
touch a
b:
touch b
c: a
touch c
d: b c
@echo "ターゲット: $@"
@echo "依存関係の一番最初: $<"
@echo "すべての依存関係: $^"
@echo "ターゲットよりもタイムスタンプが新しい依存関係: $?"
make d
# touch b
# touch a
# touch c
# ターゲット: d
# 依存関係の一番最初: b
# すべての依存関係: b c
# ターゲットよりもタイムスタンプが新しい依存関係: b c
-
$@
: ターゲット名 -
$<
: 依存関係の一番最初の名前 -
$^
: 全ての依存関係の名前 -
$?
: ターゲットよりタイムスタンプが新しい依存関係の名前
実際に使っている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}'