2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Makefileの書き方

Last updated at Posted at 2025-02-16

最近、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=barecho "$$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

自動変数

Makefile の特殊変数・自動変数の一覧

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}'
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?