2026年5月26日
2026年5月26日
Makefileでビルドを自動化する方法:WordPressテーマ・プラグイン開発の定型作業をまとめる
Makefile を使うと、composer install、npm run build、コードチェック、配布用 ZIP の作成といった毎回同じ手順のコマンドを make build や make zip の一言にまとめられます。この記事では、WordPress のテーマやプラグインを開発している人向けに、Makefile の基本ルールから実用的なサンプル、つまずきやすい点と確認方法までを順に説明します。
Makefileで何が楽になるのか
テーマやプラグインの開発では、「PHP の依存関係を入れる」「JavaScript と CSS をビルドする」「コーディング規約をチェックする」「本番用に開発用ファイルを除いて ZIP にする」といった作業が繰り返し発生します。手順を README に書いておいても、人によってオプションが違ったり、--no-dev を付け忘れて開発用パッケージ入りの ZIP を配ってしまったりと、ばらつきが出がちです。
Makefile に手順を書いておけば、手元でも CI でも同じコマンドで同じ処理を実行できます。make は「どのファイルが更新されたか」を見て必要な処理だけをやり直す仕組みも持っているため、composer.lock が変わっていなければ composer install を省く、といった無駄の削減もできます。
npm scripts や Composer scripts でも似たことはできますが、PHP と JavaScript の両方をまたぐ処理や、シェルコマンドを組み合わせた処理をひとつの入口にまとめたいときに Makefile は向いています。
Makefileの基本ルール
Makefile は「ターゲット」「前提条件」「レシピ」の組み合わせで書きます。
ターゲット: 前提条件1 前提条件2
レシピ(実行するコマンド)
- ターゲット: 作りたいファイル名、または
buildのような作業名です。 - 前提条件: ターゲットを作る前に必要なファイルや別のターゲットです。前提条件のほうがターゲットより新しければ、レシピが再実行されます。
- レシピ: 実行するシェルコマンドです。行頭はスペースではなく タブ文字 でなければなりません。
build や clean のように実在するファイルを指さない作業名は、.PHONY に登録しておきます。登録しないと、たまたま同名のファイルやディレクトリ(たとえば build/)があったときに「最新なので何もしない」と判断され、レシピが動かなくなります。
変数は NAME := 値 で定義し、$(NAME) で参照します。レシピの中でシェルの変数や awk の $1 を使うときは、make に解釈されないよう $$1 のようにドルを重ねます。
WordPressテーマ開発向けのサンプルMakefile
以下は、Composer と npm を使うテーマを想定したサンプルです。テーマのルートディレクトリに Makefile という名前で置きます。package.json に build と lint のスクリプトがあり、composer.json の require-dev に PHP_CodeSniffer と PHPUnit が入っている前提です。
SHELL := /bin/bash
.DEFAULT_GOAL := help
THEME_SLUG := my-theme
VERSION := $(shell node -p "require('./package.json').version")
STAGE_DIR := .stage
DIST_DIR := dist
.PHONY: help install build lint test zip clean
help: ## ターゲットの一覧を表示する
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "%-10s %s\n", $$1, $$2}'
install: vendor/autoload.php node_modules/.installed ## 依存関係をインストールする
vendor/autoload.php: composer.json composer.lock
composer install --no-interaction --prefer-dist
@touch $@
node_modules/.installed: package.json package-lock.json
npm ci
@touch $@
build: install ## CSS と JavaScript をビルドする
npm run build
lint: install ## PHP と JavaScript のコードをチェックする
vendor/bin/phpcs
npm run lint
test: install ## PHPUnit を実行する
vendor/bin/phpunit
zip: build ## 配布用 ZIP を作る
rm -rf $(STAGE_DIR)
mkdir -p $(STAGE_DIR)/$(THEME_SLUG) $(DIST_DIR)
rsync -a --exclude-from=.distignore ./ $(STAGE_DIR)/$(THEME_SLUG)/
cd $(STAGE_DIR)/$(THEME_SLUG) && composer install --no-dev --optimize-autoloader --no-interaction
cd $(STAGE_DIR) && zip -rq ../$(DIST_DIR)/$(THEME_SLUG)-$(VERSION).zip $(THEME_SLUG)
@echo "作成: $(DIST_DIR)/$(THEME_SLUG)-$(VERSION).zip"
clean: ## ビルド成果物と作業用ディレクトリを削除する
rm -rf $(STAGE_DIR) $(DIST_DIR)
ZIP に含めたくないファイルは .distignore に書いておきます。rsync の除外パターンとして読み込まれるので、先頭の / はテーマのルートからの位置を表します。
/.git
/.github
/.stage
/dist
/node_modules
/vendor
/src
/tests
/Makefile
/package.json
/package-lock.json
/phpcs.xml.dist
/phpunit.xml.dist
/.distignore
vendor を除外しているのは、作業用ディレクトリで composer install --no-dev をやり直し、本番に不要な開発用パッケージを含まない vendor を作るためです。composer.json と composer.lock は除外しないでください。除外するとこの手順の composer install が失敗します。/src はビルド前のソースを置いている場合の例なので、構成に合わせて調整します。
各ターゲットの考え方
installは「ファイルの更新日時」で無駄を省く
vendor/autoload.php と node_modules/.installed をターゲットにしているのがポイントです。make は composer.lock が vendor/autoload.php より新しいときだけ composer install を実行します。最後の touch $@ は、Composer が何も書き換えなかった場合でもターゲットの更新日時を新しくして、次回以降の再実行を防ぐためのものです($@ はターゲット名を表す自動変数です)。npm 側は node_modules 内に目印のファイルを作る方式にしています。
npm ci は package-lock.json どおりに入れ直すコマンドで、CI や他の人の環境で同じバージョンを再現したい場面に向いています。
build・lint・testは前提条件にinstallを置く
build: install と書くことで、依存関係が入っていない状態で make build を実行しても、先に install が走ります。新しく参加した人がリポジトリを取得して最初に打つコマンドが make build だけで済むようになります。
zipは作業用ディレクトリで組み立てる
開発中のディレクトリでそのまま composer install --no-dev を実行すると、開発用ツールが消えて make lint や make test が動かなくなります。そのため、作業用ディレクトリ .stage にコピーしてから本番用の依存関係を入れ、そこで ZIP を作っています。
WP-CLI を使っている場合は、.distignore を読んで ZIP を作る wp dist-archive コマンドを使う方法もあります。WP-CLI のバージョンや導入方法によっては別途パッケージの追加が必要なことがあるため、手元で wp help dist-archive を実行して使えるか確かめてから組み込んでください。
つまずきやすいポイントと確認方法
「missing separator」エラー
Makefile:12: *** missing separator. Stop. のようなエラーは、ほとんどの場合レシピの行頭がタブではなくスペースになっていることが原因です。エディタの「タブをスペースに変換する」設定が Makefile にも効いていないか確認してください。多くのエディタは Makefile だけタブを使う設定にできます。cat -A Makefile を実行すると、タブは ^I と表示されるので目で確かめられます。
レシピの各行は別々のシェルで動く
レシピは 1 行ごとに新しいシェルで実行されます。cd .stage を 1 行目に書き、2 行目で zip を実行しても、2 行目は元のディレクトリで動きます。サンプルのように cd ディレクトリ && コマンド と同じ行にまとめるか、GNU Make の .ONESHELL: を使います。
実行前に中身を確認する
make -n zip のように -n を付けると、実際には実行せずに実行予定のコマンドだけを表示します。rm -rf を含むターゲットを初めて動かす前や、変数の展開結果を確かめたいときに便利です。STAGE_DIR や DIST_DIR が空のまま rm -rf に渡ると意図しない場所を消す恐れがあるので、変数名の打ち間違いにも注意し、重要なファイルはバックアップかバージョン管理で守っておきます。
環境ごとのmakeの違い
この記事のサンプルは GNU Make を前提にしています。Linux では一般的に GNU Make が入っていますが、macOS に標準で入っている make は古い版であることが多く、Windows には標準で make がありません。Windows では WSL や Git Bash などの環境で実行するのが現実的です。チームで使う場合は README に「GNU Make と rsync、zip が必要」と書いておくと、環境差による失敗を減らせます(2026年10月現在の一般的な状況です。OS の更新で変わる可能性があります)。
並列実行
make -j4 install のように -j を付けると、依存関係のないターゲットを並列で実行します。サンプルでは Composer と npm のインストールが同時に進みます。出力が混ざって読みにくくなるので、エラーの原因を探すときは -j を外して実行し直すのがおすすめです。
CIでも同じMakefileを使う
GitHub Actions などの CI でも、ワークフローのステップに make lint や make test を書くだけで、手元と同じ手順を実行できます。CI 側の設定に長いコマンドを直接書くと、手元の手順とずれていきがちですが、Makefile を唯一の手順書にしておけば変更は 1 か所で済みます。
CI で使う際は、PHP と Node.js のバージョンを手元と揃えること、composer.lock と package-lock.json をリポジトリにコミットしておくことが前提になります。ロックファイルがないと、実行のたびに入るバージョンが変わり、「手元では通るのに CI では落ちる」原因になります。
まとめ
- Makefile は「ターゲット・前提条件・レシピ」で書き、レシピの行頭はタブにする。
- ファイルを指さない作業名は
.PHONYに登録する。 composer.lockなどを前提条件にすると、変更があったときだけインストールをやり直せる。- 配布用 ZIP は作業用ディレクトリで
composer install --no-devを実行して組み立てると、開発環境を壊さずに済む。 - 初めて動かすターゲットは
make -nで実行内容を確認し、CI でも同じ Makefile を使って手順を一本化する。