32blogby StudioMitsu

make実践ガイド:Makefileでなんでも自動化

makeとMakefileをゼロから解説。ターゲット・依存関係・変数・パターンルール・.PHONYの使い方から、Go・Python・Docker・Node.jsプロジェクトの実践例まで。

17 min read
目次

makeはMakefileを読み込み、どのターゲットが古くなっているかを判断して、必要なコマンドだけを実行する。1976年生まれなのに、いまだにUnix系のどの環境にも入っているもっともポータブルなタスクランナーだ。インストール不要、設定不要、ただ make と打てばいい。

要するに、Makefileはターゲット(成果物)・依存関係・レシピ(コマンド)を定義するファイル。makeはファイルのタイムスタンプを比較して、変更がなければスキップする。C言語のコンパイルだけでなく、Dockerビルド・テスト・デプロイなどあらゆるタスクを言語を問わず自動化できる。

makeが今も使われる理由

各言語にはそれぞれビルドツールがある。npm scripts、pip/tox、go build、cargo。なぜいまさらmakeなのか。

最初から入っている。 Linux でも macOS でもmakeは標準でインストール済み。npm installpip install も要らない。CIサーバーにもある。

言語を選ばない。 1つのMakefileでGoのコンパイル、Pythonのリント、Dockerのビルド、Terraformのデプロイを一括管理できる。複数言語が混在するプロジェクト(実際のプロジェクトのほとんどがそうだ)で、make buildmake testmake deploy という統一されたインターフェースになる。

無駄な作業をスキップする。 makeはファイルのタイムスタンプを比較して、変更がなければ何もしない。C言語のコンパイルだけでなく、入力ファイルと出力ファイルがあるワークフローならなんでも恩恵がある。

32blogの開発でもmakeをタスクランナーとして使っている。リントスクリプト、OGPデータ生成、ビルドを node scripts/lint-articles.mjs && node scripts/generate-ogp-data.mjs && npm run build と毎回打つ代わりに、make build 一発。リポをクローンした人も make help を打てば使えるコマンドが全部わかる。

Makefileの基本構造:ターゲット・依存関係・レシピ

Makefileはルールの集合で構成される。各ルールは3つの要素からなる:

makefile
ターゲット: 依存関係
	レシピ(コマンド)
  • ターゲット: 作りたいファイル(またはコマンド名)
  • 依存関係(前提条件): レシピを実行する前に存在し、最新でなければならないファイル
  • レシピ: ターゲットを作るシェルコマンド。インデントはタブ必須(スペース不可)

具体例:

makefile
build/app: src/main.c src/utils.c src/utils.h
	gcc -o build/app src/main.c src/utils.c

make build/app を実行すると、makeは以下をチェックする:

  1. build/app は存在するか?
  2. 3つのソースファイルすべてより新しいか?
  3. Yes → 何もしない。No → gccを実行。

複数ターゲットとデフォルト

makeは最初のターゲットをデフォルトで実行する。慣例的に all が使われる:

makefile
all: build test

build: build/app

build/app: src/main.c
	gcc -o build/app src/main.c

test:
	./run-tests.sh

clean:
	rm -rf build/

引数なしの makeall を実行し、buildtest の順に処理する。make clean は clean ターゲットだけを実行する。

覚えておくべき構造

ターゲット: 前提条件1 前提条件2
	コマンド1
	コマンド2

レシピの各行は 別々のシェル で実行される。つまり1行目の cd は2行目に影響しない:

makefile
# ダメ: cdは最初のコマンドにしか効かない
wrong:
	cd src
	gcc -o app main.c

# OK: &&でつなぐか1行にまとめる
right:
	cd src && gcc -o app main.c

変数と自動変数

変数を使うと繰り返しが減り、Makefileが保守しやすくなる。

ユーザー定義変数

makefile
CC = gcc
CFLAGS = -Wall -O2
SRC = src/main.c src/utils.c
TARGET = build/app

$(TARGET): $(SRC)
	$(CC) $(CFLAGS) -o $@ $^
  • = は再帰展開(使うたびに再評価)
  • := は即時展開(代入時に1回だけ評価)
  • ?= はデフォルト値の設定。make CC=clang でコマンドラインから上書きできる
makefile
# := と = の違い — 他の変数を参照するとき効いてくる
A = $(B)
B = hello
# $(A) → "hello"(再帰展開:Bを参照時に評価)

C := $(B)
B = world
# $(C) → "hello"(即時展開:代入時にBを評価済み)

自動変数

makeが各ルールの実行時に自動的にセットする変数:

変数意味
$@ターゲット名build/app
$<最初の前提条件src/main.c
$^すべての前提条件(重複除去)src/main.c src/utils.c
$?ターゲットより新しい前提条件変更されたファイルのみ
$*% にマッチした部分パターンルール参照
makefile
build/app: src/main.c src/utils.c
	$(CC) $(CFLAGS) -o $@ $^
# 展開結果: gcc -Wall -O2 -o build/app src/main.c src/utils.c

使用頻度の90%は $@$^ だ。レシピ内のパスをハードコードしなくて済むから、ファイル名を変えてもターゲット行だけ修正すればいい。

パターンルールとワイルドカード

同じビルドパターンのファイルが大量にある場合、個別にルールを書くのは面倒すぎる。パターンルールがこれを解決する。

% ワイルドカード

makefile
# 任意の .c ファイルを .o に変換
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

% は任意の文字列にマッチする。%.o: %.c は「foo.o を作るには foo.c を探せ」という意味。マッチした部分は $* で参照できる:

makefile
%.o: %.c
	@echo "コンパイル中: $* → $@"
	$(CC) $(CFLAGS) -c $< -o $@
# foo.o の場合: "コンパイル中: foo → foo.o"

wildcard と patsubst の組み合わせ

makefile
SRC = $(wildcard src/*.c)
OBJ = $(patsubst src/%.c, build/%.o, $(SRC))

build/app: $(OBJ)
	$(CC) -o $@ $^

build/%.o: src/%.c
	@mkdir -p build
	$(CC) $(CFLAGS) -c $< -o $@

$(wildcard src/*.c)src/ 内のすべての .c ファイルに展開される。$(patsubst src/%.c, build/%.o, $(SRC))src/main.cbuild/main.o に変換する。ソースファイルを追加してもMakefileの変更はゼロ — makeが自動で検出してくれる。

よく使う組み込み関数

makefile
FILES = one.c two.c three.c

# 文字列置換
OBJS = $(FILES:.c=.o)         # one.o two.o three.o

# フィルタ
HEADERS = $(filter %.h, $(ALL_FILES))

# シェルコマンド実行
GIT_HASH = $(shell git rev-parse --short HEAD)

# 条件分岐
DEBUG ?= 0
ifeq ($(DEBUG), 1)
  CFLAGS += -g -DDEBUG
else
  CFLAGS += -O2
endif

.PHONYとよくあるハマりどころ

.PHONYの問題

clean というターゲットがあるのに、ディレクトリに clean というファイルがあったらどうなるか?

bash
$ touch clean
$ make clean
make: 'clean' is up to date.

makeはファイル clean がターゲットだと解釈し、「もうビルド済み」と判断する。「このターゲットはファイルじゃない」と伝えるには .PHONY を宣言する:

makefile
.PHONY: all build test clean help

all: build test

clean:
	rm -rf build/

原則: 同名のファイルを生成しないターゲットはすべて .PHONY にする。 よくあるphonyターゲット:allbuildtestcleanlintdeployhelpinstall

コマンドの出力を抑制する

デフォルトでは、makeは各コマンドを実行前に表示する。@ を先頭に付けると非表示にできる:

makefile
help:
	@echo "利用可能なターゲット:"
	@echo "  make build  — プロジェクトをビルド"
	@echo "  make test   — テストスイートを実行"
	@echo "  make clean  — ビルド成果物を削除"

エラーハンドリング

デフォルトでは、makeは最初のエラー(ゼロ以外の終了コード)で停止する。エラーを無視したい場合は - を先頭に付ける:

makefile
clean:
	-rm -rf build/    # build/ がなくても失敗しない
	-rm -f *.tmp

ただし rm の場合は -f フラグ自体がエラーを抑制するので、- プレフィックスは冗長。docker rm のようにコンテナが存在しない可能性があるコマンドでより役立つ。

デバッグ

Makefileが期待通りに動かないとき:

bash
# ドライラン — コマンドを実行せずに表示
make -n build

# ルールと変数のデータベースを表示
make -p | less

# デバッグモード — なぜリビルドするかを表示
make -d build 2>&1 | head -50

make -n は本当に頼りになる。複雑な make deploy を実行する前に、何が起きるかを確認するのに毎回使っている。

C以外の実践Makefile

2026年にmakeを使う最も実用的な場面は、C言語のコンパイルではなくユニバーサルなタスクランナーとしての活用だ。実戦投入済みのパターンを紹介する。

Goプロジェクト

makefile
.PHONY: all build test lint clean docker-build

APP_NAME = myapi
VERSION = $(shell git describe --tags --always)
LDFLAGS = -ldflags "-X main.version=$(VERSION)"

all: lint test build

build:
	go build $(LDFLAGS) -o bin/$(APP_NAME) ./cmd/$(APP_NAME)

test:
	go test ./... -race -cover

lint:
	golangci-lint run ./...

clean:
	rm -rf bin/

docker-build:
	docker build -t $(APP_NAME):$(VERSION) .

Pythonプロジェクト

makefile
.PHONY: venv install test lint format clean

PYTHON = python3
VENV = .venv
BIN = $(VENV)/bin

venv:
	$(PYTHON) -m venv $(VENV)

install: venv
	$(BIN)/pip install -r requirements.txt

test: install
	$(BIN)/pytest tests/ -v --cov=src

lint: install
	$(BIN)/ruff check src/ tests/

format: install
	$(BIN)/ruff format src/ tests/

clean:
	rm -rf $(VENV) __pycache__ .pytest_cache .coverage

Node.js / Next.jsプロジェクト

makefile
.PHONY: dev build lint test clean deploy

dev:
	npm run dev

build: lint
	npm run build

lint:
	node scripts/lint-articles.mjs
	npx next lint

test:
	npx vitest run

clean:
	rm -rf .next node_modules/.cache

deploy: build
	npx vercel --prod

help:
	@echo "使い方:"
	@echo "  make dev     — 開発サーバー起動"
	@echo "  make build   — リント + プロダクションビルド"
	@echo "  make lint    — リンター実行"
	@echo "  make deploy  — ビルドしてVercelにデプロイ"

Docker Composeのワークフロー

makefile
.PHONY: up down logs build restart db-migrate

COMPOSE = docker compose -f docker-compose.yml

up:
	$(COMPOSE) up -d

down:
	$(COMPOSE) down

logs:
	$(COMPOSE) logs -f

build:
	$(COMPOSE) build --no-cache

restart: down up

db-migrate:
	$(COMPOSE) exec api python manage.py migrate

マルチ言語のモノレポ

makefile
.PHONY: all frontend backend test

all: frontend backend

frontend:
	$(MAKE) -C frontend build

backend:
	$(MAKE) -C backend build

test:
	$(MAKE) -C frontend test
	$(MAKE) -C backend test

$(MAKE) -C dir はサブディレクトリでmakeを実行する。各サブディレクトリが独自のMakefileを持ち、ルートのMakefileがオーケストレーションする構成で、きれいにスケールする。

FAQ

makeのインストール方法は?

Linuxならほぼ確実にインストール済み。なければ sudo apt install make(Debian/Ubuntu)か sudo dnf install make(Fedora)。macOSなら xcode-select --install でXcode Command Line Toolsを入れる。WindowsならMSYS2かWSLを使う。

makeとcmakeの違いは?

makeはビルド実行ツール — Makefileを読んでコマンドを実行する。CMakeはビルドシステムジェネレータ — 上位レベルの CMakeLists.txt からMakefileやNinjaファイルやVisual Studioプロジェクトを生成する。C/C++プロジェクトではCMakeがMakefileを生成し、タスクランナーとして使う場合はMakefileを直接書く。

スペースじゃダメなの?

ダメ。GNU Makeはレシピのインデントにタブを要求する。1976年の設計上の選択で、後方互換性のために変更できない。Makefile というファイル名ならほとんどのエディタが自動でタブを使ってくれる。

自己文書化するhelpターゲットの作り方は?

各ターゲットに ## 説明 コメントを付けて、awkでパースする:

makefile
.PHONY: help
help: ## このヘルプメッセージを表示
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
		awk 'BEGIN {FS = ":.*?## "}; {printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2}'

build: ## プロジェクトをビルド
	go build -o bin/app ./cmd/app

test: ## テストをカバレッジ付きで実行
	go test ./... -cover

clean: ## ビルド成果物を削除
	rm -rf bin/

make help を実行すると、すべてのターゲットの一覧がフォーマットされて表示される。

makeターゲットに引数を渡す方法は?

makeは位置引数をネイティブにサポートしていないが、変数を使えばいい:

bash
# コマンドラインから変数を上書き
make deploy ENV=production

# Makefile内
deploy:
	./deploy.sh --env $(ENV)

makeとnpm scriptsはどちらを使うべき?

両方使う。npm scriptsはJavaScript固有のタスク(next devvitest)に最適。makeはツール横断のワークフロー — リンター実行、Dockerイメージビルド、デプロイの統合 — に向いている。よくあるパターンは、makeターゲットが内部でnpm scriptsを呼ぶ構成だ。

= と := の違いは?

= は再帰展開 — 変数を参照するたびに値が再評価される。:= は即時展開 — 代入時に1回だけ評価される。意図的に遅延評価したい場合以外は := をデフォルトで使うほうが安全。

レシピのエラーを処理する方法は?

コマンドの先頭に - を付けると終了コードを無視できる。|| true でも同じことができて、意図がより明確になる。重要なコマンドについては、makeのデフォルト動作(エラーで停止)のままが正解。

まとめ

makeはUnixエコシステムで最も古いツールの一つだが、いまだに最も実用的なツールの一つでもある。押さえるべきポイント:

  • ターゲット・依存関係・レシピ — すべてのルールの3部構成
  • タブであってスペースではない — エディタを一度設定すれば終わり
  • .PHONY — 実ファイルを生成しないターゲットには必ず宣言
  • $@$^ — どこでも使う自動変数2つ
  • % のパターンルール — 50個のルールを1個にまとめる

C/C++プロジェクトでは、makeのタイムスタンプベースの依存関係追跡が真価を発揮する。それ以外のGo・Python・Node.js・Dockerでも、どのプラットフォームでも追加インストールなしで使えるユニバーサルタスクランナーとして機能する。

CLIワークフローを構築するなら、このシリーズの他のツールとmakeを組み合わせてみてほしい。xargsで並列処理、findでファイル検索、grepで内容検索、curlでAPI呼び出し。それらをまとめるMakefileがあれば、大抵のプロジェクトで「自動化フレームワーク」はそれで十分だ。