Makefileとタスクランナー

目次

概要

タスクランナーは、ビルド、テスト、整形、検証、デプロイなどの操作を名前付きコマンドとしてまとめる道具です。CLIの操作をチームで共有し、CI/CDと同じ手順で実行できるようにします。

要点

よいタスクは「何をするか」が名前で分かり、ローカルでもCIでも同じように実行できます。READMEに長いコマンドを書くより、make buildnpm run build のように入口を固定すると運用が安定します。

タスクランナーが必要になる理由

プロジェクトが大きくなると、手順が増えます。

npm install
npm run build
npm run test
aws s3 sync dist/ s3://bucket/

これを毎回手で打つと、順序やオプションを間違えやすくなります。タスクランナーを使うと、手順をコードとして管理できます。

flowchart LR D["developer"] --> T["task runner"] CI["CI"] --> T T --> B["build"] T --> Test["test"] T --> L["lint"] T --> Deploy["deploy"]

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 を実行すると、buildtest が順に実行されます。

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は本来、ファイルの更新時刻を見て「再実行が必要か」を判断します。

flowchart LR SRC["src/*.md"] --> HTML["dist/index.html"] CSS["build/assets/styles.css"] --> HTML HTML --> SITE["site"]

ファイル生成では、この更新判定が役立ちます。一方、builddeploy のような「名前付き操作」は実ファイルではないため、.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 を実行すると、自動的に prebuildbuildpostbuild が実行されます。

スクリプト間の依存

{
  "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設定に近い感覚で書ける
flowchart TD A["プロジェクト"] --> B{"Node.js中心か"} B -->|yes| N["npm scripts"] B -->|no| C{"生成物の依存関係が重要か"} C -->|yes| M["Makefile"] C -->|no| D{"追加ツールを許容できるか"} D -->|yes| J["just / Taskfile"] D -->|no| S["shell script + README"]

どれを選んでも、重要なのは「正式な入口を1つに寄せる」ことです。make buildnpm run build が両方あるなら、片方が片方を呼ぶようにして、実装が二重化しないようにします。

CIとの接続

ローカルとCIで同じコマンドを使うと、差分が減ります。

steps:
  - run: npm ci
  - run: npm run build

理想は、README、ローカル、CIの入口が一致していることです。

flowchart TD README["README"] --> CMD["npm run build"] Local["local"] --> CMD CI["GitHub Actions"] --> CMD CMD --> Dist["dist/"]

CI専用の長いコマンドをworkflowに直接書くと、ローカルで再現しにくくなります。可能ならscriptsやMakefileに寄せます。

GitHub Actionsでは、workflow、job、stepの階層で処理を書きます。

flowchart TD W["workflow"] --> J1["job: build"] J1 --> S1["step: checkout"] J1 --> S2["step: install"] J1 --> S3["step: npm run build"]

ローカルとCIの差を小さくするには、workflow内に長い処理を書かず、プロジェクトのタスクを呼びます。

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - run: npm ci
      - run: npm run build

CIでしか動かないコマンドは、障害時に手元で再現しにくくなります。deployなどCI固有の権限が必要な処理でも、buildcheck はローカルで同じ入口にしておくと調査が楽です。

環境変数と作業ディレクトリ

タスクは、どのディレクトリで実行され、どの環境変数を読むかで挙動が変わります。

観点 確認
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 名前と確認を強くする
flowchart TD A["task"] --> B{"destructive?"} B -->|yes| C["名前で明示<br>confirm / dry-run"] B -->|no| D{"external state?"} D -->|yes| E["環境を明示<br>dev/staging/prod"] D -->|no| F["ローカルで何度でも実行可能"]

タスクランナーは便利ですが、危険な操作を短い名前にしすぎると事故が起きます。deploy-proddeploy-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 buildmake 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で同じ入口を使うと、作業の再現性が上がります。

参考文献

公式・標準

タスクランナー

実装ガイド