Makefileとタスクランナー
目次
- 概要
- タスクランナーが必要になる理由
- Makefile
- Makeの考え方
- npm scripts
- justとTaskfile
- どれを選ぶか
- CIとの接続
- 環境変数と作業ディレクトリ
- タスク設計のコツ
- 依存関係と冪等性
- プロジェクト用タスク例
- 並列実行とログ
- よくある失敗
- 高度な Makefile テクニック
- タスク実行の最適化パターン
- npm scripts の詳細な活用
- Taskfile による YAML ベースのタスク管理
- just - シェルスクリプトベースのランナー
- GitHub Actions との統合
- タスク設計のベストプラクティス
- よくある失敗と対策
- まとめ
- 参考文献
概要
タスクランナーは、ビルド、テスト、整形、検証、デプロイなどの操作を名前付きコマンドとしてまとめる道具です。CLIの操作をチームで共有し、CI/CDと同じ手順で実行できるようにします。
よいタスクは「何をするか」が名前で分かり、ローカルでもCIでも同じように実行できます。READMEに長いコマンドを書くより、make build や npm run build のように入口を固定すると運用が安定します。
タスクランナーが必要になる理由
プロジェクトが大きくなると、手順が増えます。
npm install
npm run build
npm run test
aws s3 sync dist/ s3://bucket/
これを毎回手で打つと、順序やオプションを間違えやすくなります。タスクランナーを使うと、手順をコードとして管理できます。
Makefile
Makeは古典的ですが、今でも広く使われます。
.PHONY: build test clean
build:
npm run build
test:
npm test
clean:
rm -rf dist
実行します。
make build
Makefileの注意点は、recipeの行頭がtabであることです。spaceでは動きません。
依存関係も書けます。
.PHONY: verify
verify: build test
make verify を実行すると、build と test が順に実行されます。
Makefile の詳細な文法
変数定義と展開
CC = gcc
CFLAGS = -Wall -O2
SRC = main.c utils.c
OBJ = $(SRC:.c=.o)
program: $(OBJ)
$(CC) $(CFLAGS) -o $@ $^
変数の定義形式には3種類あります:
=(遅延評価):定義時に展開しない:=(即座評価):定義時に展開する?=(条件付き):未定義のとき定義
自動変数
target: prereq1 prereq2
$(rule)
recipeで使える自動変数:
| 変数 | 意味 |
|---|---|
$@ |
target名 |
$^ |
すべてのprerequisite(重複あり) |
$+ |
すべてのprerequisite(重複なし) |
{{CONTENT}}lt; |
最初のprerequisite |
$? |
より新しいprerequisite |
$* |
.PHONYの基本名 |
例:
dist/%.o: src/%.c
gcc -c {{CONTENT}}lt; -o $@
パターンルール
# C++ファイルのコンパイル
%.o: %.cpp
g++ -c {{CONTENT}}lt; -o $@
# マークダウン→HTML
%.html: %.md
markdown {{CONTENT}}lt; -o $@
条件分岐
ifdef VERBOSE
QUIET =
else
QUIET = @
endif
build:
$(QUIET)echo "Building..."
$(QUIET)npm run build
Makeの考え方
GNU Makeの基本は、target、prerequisite、recipeです。
target: prerequisite
recipe
| 要素 | 意味 |
|---|---|
| target | 作りたいもの、または実行したい名前 |
| prerequisite | targetが依存するもの |
| recipe | targetを更新するためのコマンド |
Makeは本来、ファイルの更新時刻を見て「再実行が必要か」を判断します。
ファイル生成では、この更新判定が役立ちます。一方、build や deploy のような「名前付き操作」は実ファイルではないため、.PHONY を付けます。
.PHONY: build deploy
build:
npm run build
.PHONY がないと、同名ファイルが存在したときにrecipeが実行されないことがあります。
Makeの変数は、環境変数やコマンドラインから上書きできます。
S3_BUCKET ?= example-bucket
deploy:
aws s3 sync dist/ s3://$(S3_BUCKET)/
make deploy S3_BUCKET=my-bucket
ただし、deployのような危険な操作では、上書きしやすさが事故につながることもあります。productionでは確認や環境名の明示を入れます。
デバッグ用ターゲット
Makeファイルをデバッグする場合、以下が役立ちます。
# すべての変数を表示
.PHONY: info
info:
@echo "PROJECT_NAME=$(PROJECT_NAME)"
@echo "BUILD_DIR=$(BUILD_DIR)"
@echo "CFLAGS=$(CFLAGS)"
@echo "OBJ=$(OBJ)"
# ドライラン(実行しない)
dry-run:
make -n build
npm scripts
Node.jsプロジェクトでは、package.json のscriptsが自然です。
{
"scripts": {
"build": "node build/build.js",
"check": "npm run build",
"dev": "python3 -m http.server 8000"
}
}
実行します。
npm run build
npm scriptsは、プロジェクト内の node_modules/.bin を自動でPATHに含めるため、ローカル依存のCLIを使いやすいです。
npm scripts の詳細
プリ・ポストスクリプト(Lifecycle Scripts)
{
"scripts": {
"prebuild": "npm run lint",
"build": "webpack",
"postbuild": "npm run test"
}
}
npm run build を実行すると、自動的に prebuild → build → postbuild が実行されます。
スクリプト間の依存
{
"scripts": {
"check": "npm-run-all lint:* test:*",
"lint:js": "eslint .",
"lint:css": "stylelint src/**/*.css",
"test:unit": "jest",
"test:e2e": "cypress run"
}
}
npm-run-all パッケージで複数タスクを並列或いは順序実行できます。
環境変数の参照
npm scripts実行時、npm_package_* 形式の環境変数が自動設定されます。
{
"name": "my-app",
"version": "1.0.0",
"scripts": {
"info": "echo Version=$npm_package_version"
}
}
npm run info
# Version=1.0.0
justとTaskfile
just やTaskfileは、Makefileよりタスク実行に寄せた道具です。
# justfile
build:
npm run build
serve:
python3 -m http.server 8000
# Taskfile.yml
version: '3'
tasks:
build:
cmds:
- npm run build
desc: "Build the project"
serve:
cmds:
- python3 -m http.server 8000
desc: "Serve locally"
チームで導入する場合は、追加ツールをインストールする負担と、得られる読みやすさを比較します。
justfile の構文
# justfile
# デフォルトレシピ(引数なしで実行)
default: build serve
# 引数を受け取るレシピ
deploy env='dev':
echo "Deploying to {{env}}"
./scripts/deploy.sh {{env}}
# 複数行コマンド
build:
rm -rf dist
npm run build
echo "Build complete"
# レシピの依存関係
verify: lint test
echo "All checks passed"
# スクリプトの埋め込み
run-script:
#!/bin/bash
echo "Running bash script"
for i in {1..3}; do
echo "Count: $i"
done
どれを選ぶか
タスクランナーは、プロジェクトの主言語とチームの慣習で選ぶのが現実的です。
| 状況 | 候補 | 理由 |
|---|---|---|
| Node.js中心 | npm scripts | 追加ツールなし、依存CLIを呼びやすい |
| 言語混在 | Makefile / just / Taskfile | 入口を言語非依存にできる |
| ファイル生成の依存関係が重要 | Makefile | 更新時刻ベースの依存関係が強い |
| 人間向けコマンド集 | just | recipeが読みやすい |
| YAMLでタスク定義したい | Taskfile | CI設定に近い感覚で書ける |
どれを選んでも、重要なのは「正式な入口を1つに寄せる」ことです。make build と npm run build が両方あるなら、片方が片方を呼ぶようにして、実装が二重化しないようにします。
CIとの接続
ローカルとCIで同じコマンドを使うと、差分が減ります。
steps:
- run: npm ci
- run: npm run build
理想は、README、ローカル、CIの入口が一致していることです。
CI専用の長いコマンドをworkflowに直接書くと、ローカルで再現しにくくなります。可能ならscriptsやMakefileに寄せます。
GitHub Actionsでは、workflow、job、stepの階層で処理を書きます。
ローカルとCIの差を小さくするには、workflow内に長い処理を書かず、プロジェクトのタスクを呼びます。
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: npm ci
- run: npm run build
CIでしか動かないコマンドは、障害時に手元で再現しにくくなります。deployなどCI固有の権限が必要な処理でも、build や check はローカルで同じ入口にしておくと調査が楽です。
環境変数と作業ディレクトリ
タスクは、どのディレクトリで実行され、どの環境変数を読むかで挙動が変わります。
| 観点 | 確認 |
|---|---|
| working directory | repository rootか、subdirectoryか |
| PATH | project local CLIが優先されるか |
| env | 必須環境変数が明示されているか |
| shell | sh, bash, zsh のどれか |
| OS | Linux/macOS/Windows差があるか |
npm scriptsは、プロジェクト内の node_modules/.bin をPATHに含めます。そのため、ローカル依存のCLIを直接呼びやすいです。
{
"scripts": {
"lint": "eslint ."
}
}
Makefileではshellの違いを意識します。
SHELL := /usr/bin/env bash
.PHONY: check-env
check-env:
@test -n "$S3_BUCKET" || (echo "S3_BUCKET is required" >&2; exit 1)
Makefile内でshell変数を使う場合、$ はMakeの変数展開と衝突するため $ と書きます。
タスク設計のコツ
タスク名は、操作ではなく目的で付けると読みやすくなります。
| よい名前 | 弱い名前 |
|---|---|
build |
run-node-script |
test |
jest-command |
format |
prettier-all |
deploy |
sync-s3 |
verify |
all |
タスク設計のコツです。
- よく使う入口を少数にする
- destructiveなタスクは名前で分かるようにする
- deployは環境を明示する
- secretをタスク定義に直書きしない
- CIとローカルのコマンドを揃える
- dry-runを用意できるなら用意する
依存関係と冪等性
タスクは、何度実行しても安全なものと、そうでないものに分けて考えます。
| 種類 | 例 | 方針 |
|---|---|---|
| 冪等なタスク | build, test, format |
何度実行しても同じ結果に近づく |
| 状態を変えるタスク | deploy, migrate |
環境と対象を明示する |
| 破壊的なタスク | clean, reset-db |
名前と確認を強くする |
タスクランナーは便利ですが、危険な操作を短い名前にしすぎると事故が起きます。deploy-prod と deploy-dev は分け、productionだけ確認を入れるなどの工夫が必要です。
プロジェクト用タスク例
静的サイトなら、次のようなタスク構成が扱いやすいです。
{
"scripts": {
"build": "node build/build.js",
"serve": "python3 -m http.server 8000",
"check": "npm run build",
"clean": "rm -rf dist"
}
}
Makefileに入口をまとめるなら次のようにできます。
.PHONY: build serve check clean
build:
npm run build
serve:
python3 -m http.server 8000
check: build
clean:
rm -rf dist
チームでは「正式な入口はどれか」を決めます。npm run build と make build が両方ある場合、片方がもう片方を呼ぶようにして、実体が分裂しないようにします。
並列実行とログ
Makeは -j で並列実行できます。
make -j4
並列実行は速くなりますが、依存関係が正しく書かれていないと壊れます。
dist/index.html: md/index.md build/build.js
node build/build.js
「たまたま上から順に実行される」ことに依存したMakefileは、並列化で壊れます。依存関係をtarget/prerequisiteとして明示します。
CIではログの読みやすさも重要です。
| 工夫 | 効果 |
|---|---|
| stepを分ける | どこで落ちたか分かる |
| task名を明確にする | ログ検索しやすい |
| quietにしすぎない | 失敗時の情報が残る |
| verboseを切り替え可能にする | 通常時と調査時を分けられる |
VERBOSE=1 npm run build
よくある失敗
| 失敗 | 原因 | 対策 |
|---|---|---|
| Makefileが動かない | recipe行がspace | tabにする |
make build が実行されない |
build というファイルがある |
.PHONY を付ける |
| CIだけ失敗 | working directoryが違う | pwd, ls を確認 |
| ローカルだけ成功 | global CLIに依存 | project dependencyに入れる |
| secretが漏れる | コマンドに直接書いた | secret store/envを使う |
| deploy先を間違える | 環境名が曖昧 | deploy-dev, deploy-prod を分ける |
| shell差で壊れる | bash前提をshで実行 | shebang/SHELLを明示 |
タスクランナーは、手順を短くする道具であると同時に、事故を防ぐための道具でもあります。危険な操作ほど、短い便利コマンドにしすぎないことが大切です。
高度な Makefile テクニック
関数(Functions)
# 文字列を大文字に変換
UPPERCASE = $(shell echo $(1) | tr a-z A-Z)
TARGET = $(call UPPERCASE,mytarget)
# TARGET = MYTARGET
条件付き処理(Conditionals)
RELEASE ?= dev
ifeq ($(RELEASE),prod)
CFLAGS += -O3 -DNDEBUG
else
CFLAGS += -g
endif
build:
gcc $(CFLAGS) -o app main.c
ファイル名の変換(Text Manipulation)
SOURCES = main.c util.c
OBJECTS = $(SOURCES:.c=.o)
# OBJECTS = main.o util.o
インクルード(Include)
# Makefile.rules
.PHONY: clean
clean:
rm -rf build/
# Makefile
include Makefile.rules
タスク実行の最適化パターン
パターン1: キャッシング戦略
タスク実行時間を削減するため、依存関係や成果物をキャッシュします。
# Makefileでのキャッシング例
node_modules: package-lock.json
npm ci
dist: src/** node_modules
npm run build
# .PHONY を使わない(ファイルの更新時刻を基準に実行判定)
.PHONY: clean
clean:
rm -rf dist node_modules
# キャッシュの明示的な無視
.PHONY: fresh-build
fresh-build: clean build
GitHub Actions でのキャッシング:
- uses: actions/cache@v3
with:
path: |
~/.npm
./node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
パターン2: 平列化と順序制御
# 順序制御が必要な場合
.PHONY: ci
ci: lint test build
# 並列実行可能なタスク
.PHONY: check
check: lint format-check security
# GNU Make の並列実行
# make -j 4 check # 4個同時実行
パターン3: 環境別タスク
ENV ?= development
.PHONY: setup
setup: install-deps config-$(ENV)
config-development:
cp config.example.json config.json
echo "Development config ready"
config-production:
@echo "Loading production secrets..."
aws ssm get-parameters --names /myapp/config
.PHONY: deploy
deploy:
@if [ "$(ENV)" != "production" ]; then \
echo "Error: Only deploy to production"; \
exit 1; \
fi
npm run build
aws s3 sync dist/ s3://my-bucket/
npm scripts の詳細な活用
pre/post hook
npm scripts には自動的に実行される hook があります。
{
"scripts": {
"prebuild": "npm run clean",
"build": "webpack",
"postbuild": "npm run minify",
"pretest": "npm run lint",
"test": "jest",
"posttest": "echo 'Tests completed'"
}
}
実行順序:
npm run build
→ prebuild (自動)
→ build
→ postbuild (自動)
環境変数の活用
{
"scripts": {
"dev": "NODE_ENV=development node index.js",
"prod": "NODE_ENV=production node index.js",
"test": "NODE_ENV=test jest",
"build:dev": "webpack --mode development",
"build:prod": "webpack --mode production"
}
}
Taskfile による YAML ベースのタスク管理
Taskfile は Go 製のシンプルなタスクランナーです。YAML 記法で直感的に記述できます。
version: '3'
vars:
GREETING: Hello
tasks:
default:
cmds:
- echo "{{.GREETING}}"
build:
desc: Build the project
deps:
- clean
- install
cmds:
- npm run build
sources:
- src/**
generates:
- dist/**
install:
cmds:
- npm ci
sources:
- package-lock.json
clean:
cmds:
- rm -rf dist node_modules
test:
cmds:
- npm run test
env:
NODE_ENV: test
lint:format:
cmds:
- prettier --write .
ci:
deps:
- lint
- test
- build
実行:
task # default task
task build # build task
task ci # ci task (dependencies automatically run)
task -l # list all tasks
just - シェルスクリプトベースのランナー
just は Rust 製で、レシピ記法が Make に似ていますが、シェルスクリプトのように書けます。
# Justfile
set shell := ["bash", "-uc"]
default:
@just --list
build:
npm ci
npm run build
test:
npm test
deploy target:
#!/bin/bash
echo "Deploying to {{target}}"
if [ "{{target}}" = "production" ]; then
npm run build:prod
aws s3 sync dist/ s3://prod-bucket/
else
npm run build:dev
aws s3 sync dist/ s3://dev-bucket/
fi
@verify:
just lint
just test
just build
実行:
just # default recipe
just build # build recipe
just deploy staging # deploy with argument
just --list # list all recipes
GitHub Actions との統合
実務で最も使われるCI/CDはGitHub Actionsです。タスクランナーと連携する方法です。
name: CI
on: [push, pull_request]
env:
NODE_VERSION: '20'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run test
- run: npm run build
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm audit
- run: npx snyk test
deploy:
if: github.ref == 'refs/heads/main'
needs: [test, security]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npm run deploy:prod
タスク設計のベストプラクティス
1. タスク名は動詞で始める
# Good
build:
test:
deploy:
format:
# Bad
compilation:
testing:
deployment:
fmt:
2. .PHONY を明示的に宣言する
.PHONY: build test lint format clean
# これにより、dist/ というディレクトリが存在してても
# build タスクが実行される
3. 依存関係は明示的に
# Good
release: clean build test package
# Bad: 暗黙的
release:
make clean
make build
make test
make package
4. エラーハンドリング
deploy-prod:
@if [ "$(ENV)" != "production" ]; then \
echo "Error: Use ENV=production"; \
exit 1; \
fi
./scripts/deploy.sh
lint:
prettier --check . || (prettier --write . && exit 1)
test:
npm test || { echo "Tests failed"; exit 1; }
5. ヘルプドキュメント
# タスク定義の直前にコメントを記述
help:
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | sort | \
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $1, $2}'
build: ## Build the project
npm run build
test: ## Run unit tests
npm test
deploy: ## Deploy to production
./scripts/deploy.sh
実行:
make help
# Output:
# build Build the project
# deploy Deploy to production
# test Run unit tests
よくある失敗と対策
| 失敗パターン | 原因 | 対策 |
|---|---|---|
| スペース/タブの混在 | Makefileの文法エラー | エディタで tab を使用するよう設定 |
| 依存関係の循環 | タスク設計が不適切 | DAG (有向非環グラフ) を前提に設計 |
| 冪等性がない | 同じコマンドを実行すると結果が変わる | ファイル削除→再作成のパターンを使う |
| ローカルとCI で結果が異なる | 環境差 | ci/Dockerfile で同じ環境を再現 |
| タスクが肥大化 | 複数の機能が1タスクに | 単一責任の原則を適用し、細分化 |
まとめ
タスクランナーは、CLI操作をチームで共有するための入口です。ビルド、テスト、検証、デプロイのコマンドを名前付きにし、README、ローカル、CIで同じ入口を使うと、作業の再現性が上がります。
参考文献
公式・標準
- npm Docs: npm run-script
- npm Docs: scripts
- GitHub Actions Documentation
- GitHub Actions: Workflow syntax
- GNU Make Manual
タスクランナー
- just - Command runner
- just Programmer’s Manual
- Taskfile - Task runner / Build tool
- Task: A task runner / simpler Make