makeはMakefileを読み込み、どのターゲットが古くなっているかを判断して、必要なコマンドだけを実行する。1976年生まれなのに、いまだにUnix系のどの環境にも入っているもっともポータブルなタスクランナーだ。インストール不要、設定不要、ただ make と打てばいい。
要するに、Makefileはターゲット(成果物)・依存関係・レシピ(コマンド)を定義するファイル。makeはファイルのタイムスタンプを比較して、変更がなければスキップする。C言語のコンパイルだけでなく、Dockerビルド・テスト・デプロイなどあらゆるタスクを言語を問わず自動化できる。
makeが今も使われる理由
各言語にはそれぞれビルドツールがある。npm scripts、pip/tox、go build、cargo。なぜいまさらmakeなのか。
最初から入っている。 Linux でも macOS でもmakeは標準でインストール済み。npm install も pip install も要らない。CIサーバーにもある。
言語を選ばない。 1つのMakefileでGoのコンパイル、Pythonのリント、Dockerのビルド、Terraformのデプロイを一括管理できる。複数言語が混在するプロジェクト(実際のプロジェクトのほとんどがそうだ)で、make build、make test、make 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つの要素からなる:
ターゲット: 依存関係
レシピ(コマンド)
- ターゲット: 作りたいファイル(またはコマンド名)
- 依存関係(前提条件): レシピを実行する前に存在し、最新でなければならないファイル
- レシピ: ターゲットを作るシェルコマンド。インデントはタブ必須(スペース不可)
具体例:
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は以下をチェックする:
build/appは存在するか?- 3つのソースファイルすべてより新しいか?
- Yes → 何もしない。No → gccを実行。
複数ターゲットとデフォルト
makeは最初のターゲットをデフォルトで実行する。慣例的に all が使われる:
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/
引数なしの make は all を実行し、build → test の順に処理する。make clean は clean ターゲットだけを実行する。
覚えておくべき構造
ターゲット: 前提条件1 前提条件2
コマンド1
コマンド2
レシピの各行は 別々のシェル で実行される。つまり1行目の cd は2行目に影響しない:
# ダメ: cdは最初のコマンドにしか効かない
wrong:
cd src
gcc -o app main.c
# OK: &&でつなぐか1行にまとめる
right:
cd src && gcc -o app main.c
変数と自動変数
変数を使うと繰り返しが減り、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でコマンドラインから上書きできる
# := と = の違い — 他の変数を参照するとき効いてくる
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 |
$? | ターゲットより新しい前提条件 | 変更されたファイルのみ |
$* | % にマッチした部分 | パターンルール参照 |
build/app: src/main.c src/utils.c
$(CC) $(CFLAGS) -o $@ $^
# 展開結果: gcc -Wall -O2 -o build/app src/main.c src/utils.c
使用頻度の90%は $@ と $^ だ。レシピ内のパスをハードコードしなくて済むから、ファイル名を変えてもターゲット行だけ修正すればいい。
パターンルールとワイルドカード
同じビルドパターンのファイルが大量にある場合、個別にルールを書くのは面倒すぎる。パターンルールがこれを解決する。
% ワイルドカード
# 任意の .c ファイルを .o に変換
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
% は任意の文字列にマッチする。%.o: %.c は「foo.o を作るには foo.c を探せ」という意味。マッチした部分は $* で参照できる:
%.o: %.c
@echo "コンパイル中: $* → $@"
$(CC) $(CFLAGS) -c $< -o $@
# foo.o の場合: "コンパイル中: foo → foo.o"
wildcard と patsubst の組み合わせ
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.c を build/main.o に変換する。ソースファイルを追加してもMakefileの変更はゼロ — makeが自動で検出してくれる。
よく使う組み込み関数
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 というファイルがあったらどうなるか?
$ touch clean
$ make clean
make: 'clean' is up to date.
makeはファイル clean がターゲットだと解釈し、「もうビルド済み」と判断する。「このターゲットはファイルじゃない」と伝えるには .PHONY を宣言する:
.PHONY: all build test clean help
all: build test
clean:
rm -rf build/
原則: 同名のファイルを生成しないターゲットはすべて .PHONY にする。 よくあるphonyターゲット:all、build、test、clean、lint、deploy、help、install。
コマンドの出力を抑制する
デフォルトでは、makeは各コマンドを実行前に表示する。@ を先頭に付けると非表示にできる:
help:
@echo "利用可能なターゲット:"
@echo " make build — プロジェクトをビルド"
@echo " make test — テストスイートを実行"
@echo " make clean — ビルド成果物を削除"
エラーハンドリング
デフォルトでは、makeは最初のエラー(ゼロ以外の終了コード)で停止する。エラーを無視したい場合は - を先頭に付ける:
clean:
-rm -rf build/ # build/ がなくても失敗しない
-rm -f *.tmp
ただし rm の場合は -f フラグ自体がエラーを抑制するので、- プレフィックスは冗長。docker rm のようにコンテナが存在しない可能性があるコマンドでより役立つ。
デバッグ
Makefileが期待通りに動かないとき:
# ドライラン — コマンドを実行せずに表示
make -n build
# ルールと変数のデータベースを表示
make -p | less
# デバッグモード — なぜリビルドするかを表示
make -d build 2>&1 | head -50
make -n は本当に頼りになる。複雑な make deploy を実行する前に、何が起きるかを確認するのに毎回使っている。
C以外の実践Makefile
2026年にmakeを使う最も実用的な場面は、C言語のコンパイルではなくユニバーサルなタスクランナーとしての活用だ。実戦投入済みのパターンを紹介する。
Goプロジェクト
.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プロジェクト
.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プロジェクト
.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のワークフロー
.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
マルチ言語のモノレポ
.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でパースする:
.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は位置引数をネイティブにサポートしていないが、変数を使えばいい:
# コマンドラインから変数を上書き
make deploy ENV=production
# Makefile内
deploy:
./deploy.sh --env $(ENV)
makeとnpm scriptsはどちらを使うべき?
両方使う。npm scriptsはJavaScript固有のタスク(next dev、vitest)に最適。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があれば、大抵のプロジェクトで「自動化フレームワーク」はそれで十分だ。