Make gets overlooked because people think it’s for compiling C code. In reality, it’s a universal task runner with one killer feature: it only runs what needs to run.
Basic Structure#
1
2
| target: dependencies
command
|
That’s it. The target is what you’re building, dependencies are what it needs, and commands run to create it.
Important: Commands must be indented with a tab, not spaces.
Simple Task Runner#
1
2
3
4
5
6
7
8
9
10
11
12
13
| .PHONY: build test deploy clean
build:
npm run build
test:
npm test
deploy: build test
rsync -avz dist/ server:/var/www/
clean:
rm -rf dist/ node_modules/
|
.PHONY tells Make these aren’t real files—just task names.
Run with:
1
2
3
4
| make build
make test
make deploy # Runs build and test first
make clean
|
Variables#
1
2
3
4
5
6
7
8
9
| PROJECT = myapp
VERSION = 1.0.0
DOCKER_IMAGE = $(PROJECT):$(VERSION)
build:
docker build -t $(DOCKER_IMAGE) .
push:
docker push $(DOCKER_IMAGE)
|
Override from command line:
1
| make build VERSION=2.0.0
|
Environment Variables#
1
2
3
4
5
| # Use env var with fallback
DATABASE_URL ?= postgres://localhost/dev
migrate:
DATABASE_URL=$(DATABASE_URL) ./migrate.sh
|
The ?= only sets if not already defined.
Conditional Logic#
1
2
3
4
5
6
7
8
| ifdef CI
FLAGS = --ci --coverage
else
FLAGS = --watch
endif
test:
npm test $(FLAGS)
|
Default Target#
First target is default. Or be explicit:
1
2
3
4
5
6
7
| .DEFAULT_GOAL := help
help:
@echo "Available targets:"
@echo " build - Build the project"
@echo " test - Run tests"
@echo " deploy - Deploy to production"
|
The @ suppresses command echo.
Real File Dependencies#
Make shines when tracking actual files:
1
2
3
4
5
| dist/bundle.js: src/*.js package.json
npm run build
deploy: dist/bundle.js
rsync -avz dist/ server:/var/www/
|
If src/*.js haven’t changed since last build, make deploy skips the build step.
Pattern Rules#
1
2
3
4
5
6
7
8
9
| # Convert all .md files to .html
%.html: %.md
pandoc $< -o $@
# Build all HTML files
SOURCES = $(wildcard *.md)
TARGETS = $(SOURCES:.md=.html)
all: $(TARGETS)
|
$< is the first dependency, $@ is the target.
Multi-line Commands#
Each line runs in a separate shell. For multi-line:
1
2
3
4
5
| deploy:
@echo "Deploying..." && \
cd dist && \
rsync -avz . server:/var/www/ && \
echo "Done"
|
Or use .ONESHELL:
1
2
3
4
5
6
| .ONESHELL:
deploy:
echo "Deploying..."
cd dist
rsync -avz . server:/var/www/
echo "Done"
|
Error Handling#
1
2
3
4
5
6
7
8
9
| # Continue even if command fails
clean:
-rm -rf dist/
-rm -rf node_modules/
# Stop on first error (default)
build:
npm install
npm run build
|
The - prefix ignores errors.
Parallel Execution#
1
2
3
4
5
6
7
8
9
| .PHONY: all frontend backend
all: frontend backend
frontend:
cd frontend && npm run build
backend:
cd backend && go build
|
1
| make -j2 all # Run frontend and backend in parallel
|
Self-Documenting Makefile#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| .PHONY: help
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " %-15s %s\n", $$1, $$2}'
build: ## Build the project
npm run build
test: ## Run tests
npm test
deploy: ## Deploy to production
./deploy.sh
clean: ## Remove build artifacts
rm -rf dist/
|
1
2
3
4
5
| $ make help
build Build the project
test Run tests
deploy Deploy to production
clean Remove build artifacts
|
Include Other Makefiles#
1
2
3
4
5
| include config.mk
include docker.mk
# Or optional include (no error if missing)
-include local.mk
|
Real-World Example#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| .PHONY: all build test lint deploy clean help
.DEFAULT_GOAL := help
# Config
APP_NAME := myapp
VERSION := $(shell git describe --tags --always)
DOCKER_IMAGE := $(APP_NAME):$(VERSION)
DOCKER_REGISTRY := registry.example.com
# Go settings
GOCMD := go
GOBUILD := $(GOCMD) build
GOTEST := $(GOCMD) test
help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
build: ## Build the binary
$(GOBUILD) -ldflags "-X main.Version=$(VERSION)" -o bin/$(APP_NAME) .
test: ## Run tests
$(GOTEST) -v ./...
lint: ## Run linter
golangci-lint run
docker-build: ## Build Docker image
docker build -t $(DOCKER_IMAGE) .
docker-push: docker-build ## Push Docker image
docker tag $(DOCKER_IMAGE) $(DOCKER_REGISTRY)/$(DOCKER_IMAGE)
docker push $(DOCKER_REGISTRY)/$(DOCKER_IMAGE)
deploy: docker-push ## Deploy to Kubernetes
kubectl set image deployment/$(APP_NAME) $(APP_NAME)=$(DOCKER_REGISTRY)/$(DOCKER_IMAGE)
clean: ## Clean build artifacts
rm -rf bin/
docker rmi $(DOCKER_IMAGE) 2>/dev/null || true
dev: ## Run in development mode
$(GOCMD) run . --dev
all: lint test build ## Run lint, test, and build
|
Why Make Over npm Scripts or Shell Scripts?#
- Dependency tracking: Only rebuilds what changed
- Parallel execution:
make -j4 runs independent tasks concurrently - Universal: Works on any Unix system, no runtime needed
- Self-documenting: With the help pattern above
- Composable: Include and layer Makefiles
For simple task running, Make does exactly what you need without the ceremony of more complex build tools.