Makefile Reference — targets, .PHONY, automatic variables, pattern rules, and functions
GNU Make is ubiquitous in backend and DevOps repos as a task runner. The mental model: a target depends on prerequisites; if any prerequisite is newer than the target, Make rebuilds. For software projects that aren’t C/C++, the dependency tracking is mostly irrelevant — you use Make as a documented command runner with tab-completion and self-describing help targets.
Makefile:N: *** missing separator. Stop. — that’s spaces where a tab should be.1. Anatomy of a Makefile
Targets, prerequisites, recipes, variables, and .PHONY
# Basic structure:
# target: prerequisites
# [TAB] recipe
# Variables (= lazy, := immediate, ?= default-if-unset):
APP_NAME := myapp
BUILD_DIR := ./build
GO_FLAGS ?= -v
VERSION := $(shell git describe --tags --always --dirty)
# .PHONY: tell Make these aren't files (prevents conflicts with same-named files):
.PHONY: build test clean lint docker help
# Default target (runs when you just type `make`):
.DEFAULT_GOAL := help
build: ## Build the application binary
go build $(GO_FLAGS) -ldflags "-X main.Version=$(VERSION)" -o $(BUILD_DIR)/$(APP_NAME) ./cmd/main.go
test: ## Run all tests
go test ./... -race -cover -coverprofile=coverage.out
lint: ## Run linter
golangci-lint run ./...
clean: ## Remove build artifacts
rm -rf $(BUILD_DIR)
go clean -testcache
# Self-documenting help target (parses ## comments):
help: ## Show this help message
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
2. Variables, Functions & Conditionals
Automatic variables ($@, $<, $^), built-in functions, and conditional blocks
# Automatic variables (only available inside recipes): # $@ = current target name # $< = first prerequisite # $^ = all prerequisites (space-separated, no duplicates) # $* = stem of pattern rule (% match) # $? = prerequisites newer than target # Pattern rule example: %.o: %.c gcc -c $< -o $@ # $< = the .c file, $@ = the .o file # Built-in functions: SOURCES := $(wildcard src/*.go) # glob OBJECTS := $(patsubst src/%.go,obj/%.o,$(SOURCES)) # replace pattern UPPER := $(shell echo "hello" | tr a-z A-Z) # shell command FILES := $(filter %.go, $(SOURCES)) # filter by pattern COUNT := $(words $(SOURCES)) # count words FIRST := $(firstword $(SOURCES)) # first item # String functions: MSG := hello world TRIMMED := $(strip $(MSG)) # trim whitespace UPPER := $(subst world,Deno,$(MSG)) # "hello Deno" # Conditional blocks: OS := $(shell uname -s) ifeq ($(OS),Darwin) OPEN_CMD := open else OPEN_CMD := xdg-open endif ifdef VERBOSE Q := else Q := @ # @ silences the command echo endif build: $(Q)go build ./... # silent unless VERBOSE=1
3. Real-World Patterns for Software Projects
Docker builds, version tagging, multi-target chains, and parallel execution
# VERSION from git (semver-compatible): VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "0.0.0-dev") GIT_SHA := $(shell git rev-parse --short HEAD) REGISTRY := ghcr.io/myorg IMAGE := $(REGISTRY)/myapp:$(VERSION) docker-build: ## Build Docker image docker build \ --build-arg VERSION=$(VERSION) \ --build-arg GIT_SHA=$(GIT_SHA) \ -t $(IMAGE) \ -t $(REGISTRY)/myapp:latest \ . docker-push: docker-build ## Push to registry (builds first) docker push $(IMAGE) docker push $(REGISTRY)/myapp:latest # Multi-environment deploy: NAMESPACE ?= staging deploy: ## Deploy to NAMESPACE (default: staging). Use NAMESPACE=production for prod. kubectl set image deployment/myapp myapp=$(IMAGE) -n $(NAMESPACE) kubectl rollout status deployment/myapp -n $(NAMESPACE) # Chain targets with dependencies: release: lint test docker-push deploy ## Full release pipeline # Parallel execution: test-parallel: $(MAKE) -j4 test-unit test-integration test-e2e test-lint # Include other Makefiles (split large Makefiles): include ./scripts/docker.mk include ./scripts/k8s.mk # Pass variables to sub-makes: all: $(MAKE) -C subdir VAR=value
4. DevOps & CI Patterns
Generate files, check prerequisites, env file loading, and GitHub Actions integration
# Check required tools before running:
REQUIRED_BINS := docker kubectl helm jq
$(foreach bin,$(REQUIRED_BINS),\
$(if $(shell command -v $(bin) 2>/dev/null),,\
$(error "Required binary not found: $(bin)")))
# Load .env file if it exists:
-include .env # leading - = ignore if file missing
export # export all variables to child processes
# Generate files (e.g. from templates):
config/app.yaml: config/app.yaml.tpl .env
envsubst < $< > $@ # $< = template, $@ = output
# Force rebuild of generated file:
.PHONY: config/app.yaml # mark as phony to always regenerate
# Timestamp-based guard (run only if source changed):
.build/deps-installed: go.mod go.sum
go mod download
mkdir -p .build && touch $@ # update sentinel file timestamp
install: .build/deps-installed # deps only re-run if go.mod changed
# GitHub Actions integration:
ci: lint test build ## Run full CI suite locally (matches GitHub Actions)
# Run in Docker for reproducible builds:
docker-run: ## Run dev server in Docker
docker run --rm -it \
-v $(PWD):/app \
-p 8080:8080 \
--env-file .env \
golang:1.22 \
sh -c "cd /app && make dev"
5. Python, Node.js & Polyglot Project Makefiles
Makefiles for non-C projects: Python venv management, Node.js scripts, and monorepos
# Python project (manage venv, pip-compile, formatting): PYTHON := python3 VENV := .venv ACTIVATE := . $(VENV)/bin/activate $(VENV)/bin/activate: requirements.in $(PYTHON) -m venv $(VENV) $(ACTIVATE) && pip install pip-tools $(ACTIVATE) && pip-compile requirements.in $(ACTIVATE) && pip install -r requirements.txt touch $(VENV)/bin/activate # mark as fresh install: $(VENV)/bin/activate ## Install Python deps (only if requirements.in changed) run: install ## Run the app $(ACTIVATE) && python main.py test: install ## Run tests $(ACTIVATE) && pytest tests/ -v --cov=src fmt: ## Format code $(ACTIVATE) && black . && isort . # Node.js project: node_modules: package.json package-lock.json npm ci touch node_modules # sentinel install: node_modules ## Install Node deps build: node_modules ## Build npm run build dev: ## Start dev server npm run dev # Monorepo: run make in subdirectories: SERVICES := api worker scheduler $(SERVICES): $(MAKE) -C services/$@ $(TARGET) build-all: $(SERVICES) ## Build all services. TARGET=test to run tests. # Usage: make build-all # Usage: make build-all TARGET=test
Track Go, Python, and toolchain releases at ReleaseRun. Related: Go Modules Reference | GitHub Actions Reference | Docker Compose Reference
🔍 Free tool: Go Module Health Checker — if your Makefile automates Go builds, check your module dependencies for EOL status and known CVEs.
Founded
2023 in London, UK
Contact
hello@releaserun.com