Makefiles
Build automation tool that uses dependency tracking to execute commands and manage project workflows.
Makefiles
Build automation tool that uses dependency tracking to execute commands and manage project workflows.
Overview
Make is a build automation utility that automatically determines which pieces of a program need to be recompiled and issues commands to recompile them. Beyond compilation, Make is widely used for task running, deployment scripts, and workflow automation. A Makefile defines targets (tasks), their dependencies, and recipes (commands) to execute.
flowchart LR
A[Makefile] --> B[Parse Targets]
B --> C{Dependencies Changed?}
C -->|Yes| D[Execute Recipe]
C -->|No| E[Skip Target]
D --> F[Update Timestamps]
F --> G[Next Target]
E --> G
Basic Structure
A Makefile consists of rules that define targets, their prerequisites (dependencies), and recipes (commands to execute).
Key Concepts
- Target - The file to create or task to run (left of colon)
- Prerequisites/Dependencies - Files that must exist before the target can be built
- Recipe - Shell commands to execute (must be indented with a tab)
- Rule - A target with its prerequisites and recipe combined
flowchart TD
subgraph "Makefile Rule Structure"
A["target: prerequisites"] --> B[" recipe line 1"]
B --> C[" recipe line 2"]
end
Common Patterns
# Basic rule structure
target: prerequisites
recipe
# Simple compilation example
main.o: main.c main.h
gcc -c main.c -o main.o
# Multiple prerequisites
program: main.o utils.o config.o
gcc main.o utils.o config.o -o program
# Multiple targets with same recipe
clean distclean:
rm -rf build/
# First target is the default
all: build test
Examples
# Complete C project example
CC = gcc
CFLAGS = -Wall -g
# Default target
all: myapp
# Build the application
myapp: main.o utils.o
$(CC) $(CFLAGS) -o myapp main.o utils.o
# Compile source files
main.o: main.c utils.h
$(CC) $(CFLAGS) -c main.c
utils.o: utils.c utils.h
$(CC) $(CFLAGS) -c utils.c
# Clean build artifacts
clean:
rm -f *.o myapp
Variables and Functions
Variables store values for reuse; functions transform text and perform operations.
Key Concepts
- Recursive Assignment (
=) - Recursively expanded when used - Immediate Assignment (
:=) - Expanded once when defined - Conditional Assignment (
?=) - Set only if not already defined - Append (
+=) - Add to existing variable - Automatic Variables - Special variables set by Make for each rule
Common Patterns
# Variable assignment types
CC = gcc # Recursive expansion
CFLAGS := -Wall -g # Immediate expansion
PREFIX ?= /usr/local # Only if not set
LDFLAGS += -lm # Append to existing
# Using variables
program: main.o
$(CC) $(CFLAGS) -o $@ $^
# Automatic variables
# $@ - Target name
# $< - First prerequisite
# $^ - All prerequisites
# $* - Stem of pattern rule match
# $? - Prerequisites newer than target
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Multi-line variables
define HELP_TEXT
Usage: make [target]
Targets:
all Build everything
clean Remove build artifacts
endef
export HELP_TEXT
help:
@echo "$$HELP_TEXT"
# Environment variables
# Make imports environment variables automatically
export PATH := $(PATH):./bin
export DEBUG = 1
Built-in Functions
# Text functions
FILES = foo.c bar.c baz.c
OBJS = $(patsubst %.c,%.o,$(FILES)) # foo.o bar.o baz.o
BASE = $(basename $(FILES)) # foo bar baz
DIR = $(dir src/main.c) # src/
FILE = $(notdir src/main.c) # main.c
SUFFIX = $(suffix main.c) # .c
# String substitution (suffix-only)
SRCS = main.c utils.c
OBJS = $(SRCS:.c=.o) # main.o utils.o
OBJS2 = $(patsubst %.c,%.o,$(SRCS)) # Equivalent long form
# Note: $(subst .c,.o,...) is NOT equivalent — it replaces ".c"
# anywhere in the string, not just as a suffix
# Filtering
SOURCES = main.c utils.h config.c data.h
C_FILES = $(filter %.c,$(SOURCES)) # main.c config.c
NOT_H = $(filter-out %.h,$(SOURCES)) # main.c config.c
# Wildcard and shell
SRCS = $(wildcard src/*.c) # All .c files in src/
VERSION = $(shell git describe --tags) # Run shell command
# Conditionals in functions
DEBUG_FLAG = $(if $(DEBUG),-g,)
ITEMS = $(or $(VAR1),$(VAR2),default)
# Word functions
WORDS = one two three four
FIRST = $(firstword $(WORDS)) # one
LAST = $(lastword $(WORDS)) # four
COUNT = $(words $(WORDS)) # 4
SECOND = $(word 2,$(WORDS)) # two
# Foreach loop
DIRS = src lib test
CLEAN_TARGETS = $(foreach dir,$(DIRS),clean-$(dir))
# Call function
reverse = $(2) $(1)
RESULT = $(call reverse,a,b) # b a
Examples
# Real-world variable usage
PROJECT_NAME := myproject
VERSION := $(shell git describe --tags 2>/dev/null || echo "dev")
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
# Source file discovery
SRC_DIR := src
BUILD_DIR := build
SRCS := $(wildcard $(SRC_DIR)/*.c)
OBJS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS))
# Compiler settings
CC := gcc
CFLAGS := -Wall -Wextra -std=c11
LDFLAGS := -lm
# Debug/Release builds
ifdef DEBUG
CFLAGS += -g -DDEBUG
else
CFLAGS += -O2 -DNDEBUG
endif
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR):
mkdir -p $@
Pattern Rules
Pattern rules define implicit rules using wildcards to match multiple files.
Key Concepts
- Pattern - Uses
%as a wildcard to match any string - Stem - The part matched by
%, accessible via$* - Implicit Rules - Built-in rules for common operations
- Static Pattern Rules - Apply patterns to a specific list of targets
Common Patterns
# Generic pattern rule
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Pattern with directories
build/%.o: src/%.c
$(CC) $(CFLAGS) -c $< -o $@
# Multiple patterns
%.html: %.md
pandoc -o $@ $<
%.pdf: %.md
pandoc -o $@ $<
# Static pattern rules (apply to specific files only)
OBJECTS = foo.o bar.o baz.o
$(OBJECTS): %.o: %.c
$(CC) -c $< -o $@
# Cancel implicit rule
%.o: %.c
# Pattern with multiple prerequisites
%.o: %.c %.h
$(CC) $(CFLAGS) -c $< -o $@
Examples
# Build system with pattern rules
SRC_DIR := src
OBJ_DIR := obj
BIN_DIR := bin
SRCS := $(wildcard $(SRC_DIR)/*.c)
OBJS := $(patsubst $(SRC_DIR)/%.c,$(OBJ_DIR)/%.o,$(SRCS))
# Pattern rule for object files
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
# Include generated dependencies
-include $(OBJS:.o=.d)
# Directory creation
$(OBJ_DIR) $(BIN_DIR):
mkdir -p $@
# Documentation generation
docs/%.html: docs/%.md
mkdir -p $(dir $@)
pandoc --standalone -o $@ $<
# Multiple language support
%.js: %.ts
tsc $<
%.css: %.scss
sass $< $@
Phony Targets
Phony targets represent actions rather than files, ensuring they always execute.
Key Concepts
- Phony Target - A target that doesn't represent a file
- .PHONY - Special target declaring targets as phony
- Always Runs - Phony targets execute even if a file with that name exists
- Common Phonies -
all,clean,install,test,help
Common Patterns
# Declare phony targets
.PHONY: all clean install test help
# Standard targets
all: build
clean:
rm -rf build/ dist/
install: build
cp -r build/* /usr/local/
test:
pytest tests/
# Help target
help:
@echo "Available targets:"
@echo " all - Build the project"
@echo " clean - Remove build artifacts"
@echo " install - Install to system"
@echo " test - Run tests"
# Group phony targets by category
.PHONY: build test lint format
# Build targets
build: compile link
# Quality targets
lint:
flake8 src/
format:
black src/
Examples
# Comprehensive phony targets
.PHONY: all build clean test lint format docs install deploy help
# Default target
all: build test lint
# Build targets
build: deps
@echo "Building project..."
go build -o bin/app ./cmd/app
deps:
go mod download
# Clean targets
clean:
rm -rf bin/ dist/ coverage/
clean-all: clean
go clean -cache -testcache
# Test targets
test:
go test -v ./...
test-coverage:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# Quality targets
lint:
golangci-lint run
format:
gofmt -w .
# Documentation
docs:
godoc -http=:6060
# Deployment
deploy: build test
./scripts/deploy.sh
# Self-documenting help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " %-15s %s\n", $$1, $$2}'
build: ## Build the application
test: ## Run tests
clean: ## Remove build artifacts
Conditional Execution
Conditionals allow different behaviour based on variable values or system state.
Key Concepts
- ifdef/ifndef - Check if variable is defined
- ifeq/ifneq - Compare values for equality
- Nesting - Conditionals can be nested
- Scope - Conditionals can wrap rules or variable assignments
Common Patterns
# Check if variable is defined
ifdef DEBUG
CFLAGS += -g -DDEBUG
endif
ifndef PREFIX
PREFIX = /usr/local
endif
# Compare values
ifeq ($(OS),Windows_NT)
RM = del /Q
EXE = .exe
else
RM = rm -f
EXE =
endif
# Compare with empty
ifeq ($(VERBOSE),)
Q = @
else
Q =
endif
# Nested conditionals
ifdef RELEASE
ifeq ($(ARCH),arm64)
CFLAGS += -march=armv8-a
else
CFLAGS += -march=native
endif
CFLAGS += -O3
endif
# String comparison (built-in CC has origin "default", not "undefined")
ifeq "$(origin CC)" "default"
CC = gcc
endif
# Using $(filter)
ifneq ($(filter debug,$(MAKECMDGOALS)),)
DEBUG = 1
endif
Examples
# Cross-platform Makefile
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
PLATFORM := linux
LDFLAGS += -lpthread
endif
ifeq ($(UNAME_S),Darwin)
PLATFORM := macos
LDFLAGS += -framework CoreFoundation
endif
ifeq ($(OS),Windows_NT)
PLATFORM := windows
EXE_EXT := .exe
endif
# Build configuration
BUILD_TYPE ?= release
ifeq ($(BUILD_TYPE),debug)
CFLAGS += -g -O0 -DDEBUG
BUILD_DIR := build/debug
else ifeq ($(BUILD_TYPE),release)
CFLAGS += -O3 -DNDEBUG
BUILD_DIR := build/release
else
$(error Unknown BUILD_TYPE: $(BUILD_TYPE))
endif
# Feature flags
ifdef ENABLE_SSL
CFLAGS += -DUSE_SSL
LDFLAGS += -lssl -lcrypto
endif
ifdef SANITIZE
CFLAGS += -fsanitize=address,undefined
LDFLAGS += -fsanitize=address,undefined
endif
# Verbose mode
V ?= 0
ifeq ($(V),1)
Q =
ECHO = @true
else
Q = @
ECHO = @echo
endif
%.o: %.c
$(ECHO) "CC $<"
$(Q)$(CC) $(CFLAGS) -c $< -o $@
Common Use Cases
Build Automation
# C/C++ Project
CC := gcc
CXX := g++
CFLAGS := -Wall -Wextra -std=c11
CXXFLAGS := -Wall -Wextra -std=c++17
SRC_DIR := src
BUILD_DIR := build
TARGET := $(BUILD_DIR)/app
SRCS := $(wildcard $(SRC_DIR)/*.c $(SRC_DIR)/**/*.c)
OBJS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS))
DEPS := $(OBJS:.o=.d)
.PHONY: all clean rebuild
all: $(TARGET)
$(TARGET): $(OBJS) | $(BUILD_DIR)
$(CC) $(LDFLAGS) -o $@ $^
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
$(BUILD_DIR):
mkdir -p $@
clean:
rm -rf $(BUILD_DIR)
rebuild: clean all
-include $(DEPS)
Task Running
# Project task runner
.PHONY: dev prod test lint format db-migrate db-seed docker-build docker-run
# Development
dev:
npm run dev
prod:
npm run build
npm run start
# Testing
test:
npm test
test-watch:
npm test -- --watch
test-coverage:
npm test -- --coverage
# Code quality
lint:
npm run lint
format:
npm run format
lint-fix:
npm run lint -- --fix
# Database
db-migrate:
npx prisma migrate dev
db-seed:
npx prisma db seed
db-reset:
npx prisma migrate reset
# Docker
docker-build:
docker build -t myapp:latest .
docker-run:
docker run -p 3000:3000 myapp:latest
docker-push:
docker push registry.example.com/myapp:latest
# Combined workflows
ci: lint test build
deploy: ci docker-build docker-push
Python Project
.PHONY: install dev test lint format docs clean
PYTHON := python3
VENV := .venv
PIP := $(VENV)/bin/pip
PYTEST := $(VENV)/bin/pytest
BLACK := $(VENV)/bin/black
FLAKE8 := $(VENV)/bin/flake8
MYPY := $(VENV)/bin/mypy
# Setup
install: $(VENV)
$(PIP) install -r requirements.txt
dev: $(VENV)
$(PIP) install -r requirements-dev.txt
$(PIP) install -e .
$(VENV):
$(PYTHON) -m venv $(VENV)
$(PIP) install --upgrade pip
# Testing
test:
$(PYTEST) tests/ -v
test-coverage:
$(PYTEST) tests/ --cov=src --cov-report=html
# Code quality
lint:
$(FLAKE8) src/ tests/
$(MYPY) src/
format:
$(BLACK) src/ tests/
format-check:
$(BLACK) --check src/ tests/
# Documentation
docs:
cd docs && make html
# Cleanup
clean:
rm -rf $(VENV) dist/ build/ *.egg-info
find . -type d -name __pycache__ -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
Go Project
.PHONY: build run test lint clean docker
# Variables
BINARY := myapp
VERSION := $(shell git describe --tags --always --dirty)
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -ldflags "-X main.version=$(VERSION) -X main.buildTime=$(BUILD_TIME)"
# Build
build:
go build $(LDFLAGS) -o bin/$(BINARY) ./cmd/$(BINARY)
build-all:
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY)-linux-amd64 ./cmd/$(BINARY)
GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY)-darwin-amd64 ./cmd/$(BINARY)
GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY)-windows-amd64.exe ./cmd/$(BINARY)
# Run
run:
go run ./cmd/$(BINARY)
# Test
test:
go test -v ./...
test-coverage:
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
# Lint
lint:
golangci-lint run
# Clean
clean:
rm -rf bin/ coverage.out
# Docker
docker:
docker build -t $(BINARY):$(VERSION) .
Debugging Makefiles
Techniques for troubleshooting and understanding Makefile behaviour.
Key Concepts
- Dry Run - Show commands without executing
- Debug Mode - Print detailed Make internals
- Warning/Error - Print messages during parsing
- Origin - Determine where a variable was defined
Common Commands
# Dry run - show commands without executing
make -n target
make --dry-run target
# Print database of rules and variables
make -p
make --print-data-base
# Debug mode
make -d target # Full debug output
make --debug=basic target # Basic debugging
make --debug=verbose target # Verbose debugging
make --debug=implicit target # Show implicit rule search
make --debug=jobs target # Show job processing
make --debug=all target # All debugging
# Show why target is being rebuilt (GNU Make 4.4+)
make --debug=why target
# Print working directory
make -w target
make --print-directory target
# Trace execution
make --trace target
# Show Makefile being used
make -f Makefile.custom target
Debugging Patterns
# Print variable value
debug:
@echo "CC = $(CC)"
@echo "CFLAGS = $(CFLAGS)"
@echo "SRCS = $(SRCS)"
@echo "OBJS = $(OBJS)"
# Show variable origin
print-origin:
@echo "CC origin: $(origin CC)"
@echo "SHELL origin: $(origin SHELL)"
# Warning messages during parsing
$(warning Current directory: $(CURDIR))
$(warning Building: $(TARGET))
# Error with message (stops Make)
ifndef REQUIRED_VAR
$(error REQUIRED_VAR is not set)
endif
# Info messages (less intrusive than warning)
$(info Compiling for $(PLATFORM))
# Debugging function calls
DEBUG_FUNC = $(warning Calling $(0) with args: $(1), $(2))
my_func = $(DEBUG_FUNC)$(1)-$(2)
# Verbose target information
VERBOSE ?= 0
ifeq ($(VERBOSE),1)
define log
@echo "$(1)"
endef
else
define log
endef
endif
build:
$(call log,Building target: $@)
$(call log,Prerequisites: $^)
$(CC) -o $@ $^
# Trace rule execution
TRACE ?= 0
ifeq ($(TRACE),1)
OLD_SHELL := $(SHELL)
SHELL = $(warning Running: $@)$(OLD_SHELL) -x
endif
Examples
# Comprehensive debugging Makefile
.PHONY: debug-vars debug-rules debug-env
# Print all project variables
debug-vars:
@echo "=== Build Variables ==="
@echo "CC = $(CC)"
@echo "CFLAGS = $(CFLAGS)"
@echo "LDFLAGS = $(LDFLAGS)"
@echo ""
@echo "=== Paths ==="
@echo "SRC_DIR = $(SRC_DIR)"
@echo "BUILD_DIR = $(BUILD_DIR)"
@echo "SRCS = $(SRCS)"
@echo "OBJS = $(OBJS)"
@echo ""
@echo "=== Configuration ==="
@echo "DEBUG = $(DEBUG)"
@echo "PLATFORM = $(PLATFORM)"
@echo "VERSION = $(VERSION)"
# Show Make's automatic variables for a target
debug-auto: main.o
@echo "Target: $@"
@echo "First prereq: $<"
@echo "All prereqs: $^"
@echo "Newer prereqs: $?"
@echo "Stem: $*"
@echo "Dir of target: $(@D)"
@echo "File of target: $(@F)"
# Print environment
debug-env:
@echo "=== Environment ==="
@echo "SHELL = $(SHELL)"
@echo "MAKE = $(MAKE)"
@echo "MAKEFLAGS = $(MAKEFLAGS)"
@echo "CURDIR = $(CURDIR)"
@echo "PATH = $(PATH)"
# Check prerequisites
.PHONY: check-deps
check-deps:
@echo "Checking dependencies..."
@which $(CC) > /dev/null || (echo "ERROR: $(CC) not found" && exit 1)
@which pkg-config > /dev/null || (echo "WARNING: pkg-config not found")
@echo "All required dependencies found."
Task, Just, and Other Task Runners
Modern alternatives to Make for task running and build automation.
Just
Just is a command runner focused on simplicity and convenience for project-specific commands.
# justfile
# Set shell
set shell := ["bash", "-uc"]
# Variables
project := "myapp"
version := `git describe --tags`
# Default recipe
default: build
# Build the project
build:
cargo build --release
# Run tests
test:
cargo test
# Format code
fmt:
cargo fmt
# Lint code
lint:
cargo clippy
# Clean build artifacts
clean:
cargo clean
# Recipe with arguments
greet name:
echo "Hello, {{name}}!"
# Recipe with default argument
serve port="8080":
python -m http.server {{port}}
# Private recipe (not shown in --list)
_setup:
mkdir -p build
# Recipe dependencies
ci: fmt lint test build
# Conditional recipe
docker:
#!/usr/bin/env bash
if command -v docker &> /dev/null; then
docker build -t {{project}}:{{version}} .
else
echo "Docker not installed"
exit 1
fi
# Multi-line recipe with different shell
python-script:
#!/usr/bin/env python3
import sys
print(f"Python {sys.version}")
Task (go-task)
Task is a task runner written in Go, using YAML syntax.
# Taskfile.yml
version: '3'
vars:
PROJECT: myapp
VERSION:
sh: git describe --tags
tasks:
default:
deps: [build]
build:
desc: Build the application
cmds:
- go build -o bin/{{.PROJECT}} ./cmd/{{.PROJECT}}
sources:
- ./**/*.go
generates:
- bin/{{.PROJECT}}
test:
desc: Run tests
cmds:
- go test -v ./...
lint:
desc: Run linter
cmds:
- golangci-lint run
clean:
desc: Clean build artifacts
cmds:
- rm -rf bin/
docker:
desc: Build Docker image
cmds:
- docker build -t {{.PROJECT}}:{{.VERSION}} .
ci:
desc: Run CI pipeline
deps: [lint, test, build]
serve:
desc: Run development server
cmds:
- go run ./cmd/{{.PROJECT}}
env:
DEBUG: "true"
db:migrate:
desc: Run database migrations
cmds:
- migrate -path migrations -database $DATABASE_URL up
# Task with parameters
greet:
desc: Greet someone
cmds:
- echo "Hello, {{.CLI_ARGS}}!"
Comparison Table
| Feature | Make | Just | Task |
|---|---|---|---|
| Syntax | Custom | Custom | YAML |
| File-based deps | Yes | No | Yes |
| Cross-platform | Yes | Yes | Yes |
| Variables | Yes | Yes | Yes |
| Parameters | Limited | Yes | Yes |
| Private recipes | No | Yes | Yes |
| Shell selection | Limited | Yes | Yes |
| Tab requirement | Yes | No | No |
When to Use Each
flowchart TD
A[Need Task Runner?] --> B{File Dependencies?}
B -->|Yes| C{Complex Build?}
B -->|No| D{Team Familiar with YAML?}
C -->|Yes| E[Use Make]
C -->|No| F{Need Parameters?}
D -->|Yes| G[Use Task]
D -->|No| H[Use Just]
F -->|Yes| I[Use Just or Task]
F -->|No| E
Migration from Make to Just
# Makefile
.PHONY: build test clean
CC = gcc
CFLAGS = -Wall
build:
$(CC) $(CFLAGS) -o app main.c
test:
./test.sh
clean:
rm -f app
# Equivalent justfile
cc := "gcc"
cflags := "-Wall"
build:
{{cc}} {{cflags}} -o app main.c
test:
./test.sh
clean:
rm -f app
Migration from Make to Task
# Equivalent Taskfile.yml
version: '3'
vars:
CC: gcc
CFLAGS: -Wall
tasks:
build:
cmds:
- "{{.CC}} {{.CFLAGS}} -o app main.c"
test:
cmds:
- ./test.sh
clean:
cmds:
- rm -f app
Quick Reference
Common Targets
| Target | Purpose |
|---|---|
all |
Default target, typically builds everything |
build |
Compile/build the project |
test |
Run tests |
clean |
Remove build artifacts |
install |
Install to system |
uninstall |
Remove from system |
dist |
Create distribution package |
help |
Show available targets |
lint |
Run linters |
format |
Format code |
Automatic Variables
| Variable | Description |
|---|---|
$@ |
Target filename |
$< |
First prerequisite |
$^ |
All prerequisites |
$? |
Prerequisites newer than target |
$* |
Stem of pattern match |
$(@D) |
Directory of target |
$(@F) |
Filename of target |
Common Functions
| Function | Example | Result |
|---|---|---|
$(wildcard *.c) |
Find files | main.c utils.c |
$(patsubst %.c,%.o,$(SRCS)) |
Pattern replace | main.o utils.o |
$(filter %.c,$(FILES)) |
Keep matching | Only .c files |
$(filter-out %.h,$(FILES)) |
Remove matching | Without .h files |
$(shell cmd) |
Run shell | Command output |
$(dir path) |
Extract directory | src/ |
$(notdir path) |
Extract filename | file.c |
$(basename file.c) |
Remove suffix | file |
$(suffix file.c) |
Get suffix | .c |
$(if cond,then,else) |
Conditional | Result based on condition |
Command-Line Options
| Option | Description |
|---|---|
-n / --dry-run |
Print commands without executing |
-f FILE |
Use FILE as Makefile |
-j N |
Run N jobs in parallel |
-k |
Continue after errors |
-B |
Unconditionally make all targets |
-d |
Print debugging information |
-p |
Print database of rules |
-w |
Print working directory |
--trace |
Trace target execution |
Common Issues and Solutions
| Issue | Cause | Solution |
|---|---|---|
missing separator |
Recipe not indented with tab | Use tab (not spaces) for recipes |
No rule to make target |
Missing prerequisite file | Check file paths and names |
Nothing to be done |
Target is up to date | Use -B to force rebuild |
Circular dependency |
A depends on B which depends on A | Restructure dependencies |
| Recipe not executing | Target file already exists | Add to .PHONY if it's not a file |
| Variables not expanding | Using $ instead of $$ |
Use $$ for shell variables |
| Command not found | PATH not set correctly | Export PATH or use full path |
| Parallel build fails | Missing dependencies | Add missing prerequisites |
| Wrong shell used | Default shell is /bin/sh |
Set SHELL := /bin/bash |
| Pattern rule not matching | Pattern doesn't match file names | Check % placement and paths |
Debugging Tips
# Check why target is being rebuilt (GNU Make 4.4+)
make --debug=why target
# See what Make thinks it needs to do
make -n target
# Print all variables and rules
make -p | less
# Run with maximum verbosity
make -d target 2>&1 | less
# Check variable value
make -p | grep "^VAR_NAME"
# Trace with timestamps
time make target
Recipe Best Practices
# Suppress command echo
clean:
@rm -rf build/
# Continue on error
clean:
-rm -f *.o
# Use bash features
SHELL := /bin/bash
target:
[[ -d build ]] || mkdir build
# Multi-line commands
target:
for f in *.c; do \
echo "Processing $$f"; \
$(CC) -c "$$f"; \
done
Related Topics
The following topics would complement this Makefiles cheatsheet:
- Bash - Shell scripting fundamentals for writing Make recipes
- Docker - Container builds often use Makefiles for workflow automation
- CI/CD Patterns - Integrating Make targets into continuous integration pipelines
- CMake - More advanced build system generator for cross-platform C/C++ projects
- Go - Go projects commonly use Makefiles for build automation
- Python - Python project automation with Make for testing, linting, and packaging