Make is a build automation tool that automatically builds executable programs and libraries from source code by reading files called Makefiles which specify how to derive the target program.
Quick Start
- Make uses Makefiles to define build rules and dependencies
- Rules specify targets, prerequisites (dependencies), and recipes (commands)
- Make only rebuilds targets when dependencies have changed (timestamp-based)
- Tab character (not spaces) required before recipe commands
- Variables and functions provide powerful text manipulation
Basic Makefile Structure
A Makefile consists of rules with this syntax:
target: prerequisites
recipe
recipe
Simple Example
# Build a C program
program: main.o utils.o
gcc -o program main.o utils.o
main.o: main.c utils.h
gcc -c main.c
utils.o: utils.c utils.h
gcc -c utils.c
clean:
rm -f program *.o
Run with:
make # Builds default target (first target)
make program # Builds specific target
make clean # Removes built files
Make Rules
Rule Syntax
targets: prerequisites
recipe
...
- Targets: Files to be generated (or phony targets like 'clean')
- Prerequisites: Files that must exist/be up-to-date before building target
- Recipe: Shell commands to build the target (must be indented with TAB)
Multiple Targets
# Multiple targets with same recipe
file1.o file2.o: common.h
gcc -c $*.c
# Equivalent to:
file1.o: common.h
gcc -c file1.c
file2.o: common.h
gcc -c file2.c
Phony Targets
Targets that don't represent files:
.PHONY: clean all install test
all: program1 program2
clean:
rm -f *.o program1 program2
install: all
cp program1 /usr/local/bin/
cp program2 /usr/local/bin/
test: all
./run-tests.sh
Variables
Defining Variables
# Simple assignment (evaluated when used)
CC = gcc
CFLAGS = -Wall -O2
# Immediate assignment (evaluated when defined)
NOW := $(shell date)
# Append to variable
CFLAGS += -g
# Conditional assignment (only if not already set)
CC ?= gcc
# Using variables
program: main.o
$(CC) $(CFLAGS) -o program main.o
Built-in Variables
Common automatic variables:
# $@ - Target name
# $< - First prerequisite
# $^ - All prerequisites
# $? - Prerequisites newer than target
# $* - Stem of pattern rule match
example: file1.c file2.c file3.c
gcc -o $@ $^
# Expands to: gcc -o example file1.c file2.c file3.c
%.o: %.c
gcc -c $< -o $@
# For main.o: gcc -c main.c -o main.o
Standard Variables
# Compiler and tools
CC = gcc # C compiler
CXX = g++ # C++ compiler
LD = ld # Linker
AR = ar # Archiver
RM = rm -f # Remove command
# Flags
CFLAGS = -Wall -O2
CXXFLAGS = -Wall -O2 -std=c++17
LDFLAGS = -L/usr/local/lib
LDLIBS = -lm -lpthread
# Directories
SRCDIR = src
OBJDIR = obj
BINDIR = bin
INCDIR = include
# Common pattern
SOURCES = $(wildcard $(SRCDIR)/*.c)
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
Pattern Rules
Basic Patterns
# Pattern rule: %.o depends on %.c
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Pattern with subdirectories
obj/%.o: src/%.c
@mkdir -p obj
$(CC) $(CFLAGS) -c $< -o $@
# Multiple patterns
%.pdf: %.tex
pdflatex $<
%.html: %.md
pandoc $< -o $@
Static Pattern Rules
OBJECTS = main.o utils.o helper.o
# Static pattern: $(targets): target-pattern: prereq-patterns
$(OBJECTS): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
# Equivalent to writing separate rules for each object file
Functions
Text Functions
# $(subst from,to,text) - Substitute text
SOURCES = main.c utils.c
OBJECTS = $(subst .c,.o,$(SOURCES)) # main.o utils.o
# $(patsubst pattern,replacement,text) - Pattern substitution
OBJECTS = $(patsubst %.c,%.o,$(SOURCES))
# $(strip text) - Remove leading/trailing whitespace
TRIMMED = $(strip hello world )
# $(findstring find,text) - Find substring
HAS_MAIN = $(findstring main,$(SOURCES))
# $(filter pattern,text) - Keep matching words
C_FILES = $(filter %.c,$(FILES))
# $(filter-out pattern,text) - Remove matching words
NOT_MAIN = $(filter-out main.c,$(SOURCES))
# $(sort list) - Sort and remove duplicates
SORTED = $(sort c b a c b) # a b c
# $(word n,text) - Get nth word (1-indexed)
FIRST = $(word 1,$(SOURCES))
# $(words text) - Count words
COUNT = $(words $(SOURCES))
# $(firstword text) - Get first word
FIRST = $(firstword $(SOURCES))
# $(lastword text) - Get last word
LAST = $(lastword $(SOURCES))
File Functions
# $(wildcard pattern) - List files matching pattern
SOURCES = $(wildcard src/*.c)
HEADERS = $(wildcard include/*.h)
# $(dir names) - Extract directory part
DIRS = $(dir src/main.c include/utils.h) # src/ include/
# $(notdir names) - Extract filename part
FILES = $(notdir src/main.c src/utils.c) # main.c utils.c
# $(suffix names) - Extract file extension
EXTS = $(suffix main.c utils.o script.py) # .c .o .py
# $(basename names) - Remove extension
BASES = $(basename main.c utils.o) # main utils
# $(addsuffix suffix,names) - Add suffix
OBJECTS = $(addsuffix .o,main utils) # main.o utils.o
# $(addprefix prefix,names) - Add prefix
PATHS = $(addprefix src/,main.c utils.c) # src/main.c src/utils.c
# $(join list1,list2) - Join lists element by element
JOINED = $(join src/ obj/,main.c utils.c) # src/main.c obj/utils.c
# $(realpath names) - Get absolute path
ABS = $(realpath ../src)
# $(abspath names) - Get absolute path (doesn't require file to exist)
ABS = $(abspath ../src)
Conditional Functions
# $(if condition,then-part,else-part)
DEBUG = 1
CFLAGS = $(if $(DEBUG),-g -O0,-O2)
# $(or condition1,condition2,...)
COMPILER = $(or $(CC),gcc)
# $(and condition1,condition2,...)
BUILD = $(and $(SOURCES),$(COMPILER))
Shell Function
# $(shell command) - Execute shell command
DATE = $(shell date +%Y-%m-%d)
GIT_HASH = $(shell git rev-parse --short HEAD)
FILES = $(shell find src -name '*.c')
# Warning: shell is executed every time variable is referenced
# Use := for immediate assignment to execute once
NOW := $(shell date)
Custom Functions
# $(call variable,param1,param2,...)
# Define function with $(1), $(2), etc. for parameters
# Function to compile a source file
define COMPILE_C
@echo "Compiling $(1)..."
$(CC) $(CFLAGS) -c $(1) -o $(2)
endef
# Function to create directory
define MKDIR
@mkdir -p $(1)
endef
# Usage
obj/main.o: src/main.c
$(call MKDIR,obj)
$(call COMPILE_C,$<,$@)
Conditionals
Conditional Syntax
# ifdef / ifndef
ifdef DEBUG
CFLAGS += -g
else
CFLAGS += -O2
endif
ifndef VERBOSE
.SILENT:
endif
# ifeq / ifneq
ifeq ($(CC),gcc)
CFLAGS += -Wall
endif
ifneq ($(OS),Windows_NT)
RM = rm -f
else
RM = del
endif
# Comparing variables
ifeq ($(strip $(SOURCES)),)
$(error No source files found)
endif
Runtime Conditionals
# Using shell test
check-file:
@test -f config.ini || echo "Warning: config.ini not found"
# Conditional execution
all:
@if [ "$(DEBUG)" = "1" ]; then \
echo "Debug build"; \
else \
echo "Release build"; \
fi
Advanced Patterns
Automatic Dependency Generation
# Generate .d files with header dependencies
DEPDIR = .deps
DEPFLAGS = -MT $@ -MMD -MP -MF $(DEPDIR)/$*.d
%.o: %.c $(DEPDIR)/%.d | $(DEPDIR)
$(CC) $(DEPFLAGS) $(CFLAGS) -c $< -o $@
$(DEPDIR):
@mkdir -p $@
# Include generated dependency files
SOURCES = main.c utils.c
DEPS = $(SOURCES:%.c=$(DEPDIR)/%.d)
-include $(DEPS)
Order-Only Prerequisites
Prerequisites that don't affect target rebuild if only their timestamp changes:
# Normal prerequisite: rebuilds if directory timestamp changes
# Order-only (after |): only ensures directory exists
obj/main.o: src/main.c | obj
$(CC) -c $< -o $@
obj:
mkdir -p obj
Multi-line Variables
define HELP_TEXT
Usage: make [target]
Targets:
all - Build everything
clean - Remove built files
install - Install to system
test - Run tests
endef
help:
@echo "$(HELP_TEXT)"
# Alternative: export for use in shell
export HELP_TEXT
help2:
@echo "$$HELP_TEXT"
Complete Project Example
C Project Makefile
# Project configuration
PROJECT = myapp
VERSION = 1.0.0
# Directories
SRCDIR = src
INCDIR = include
OBJDIR = obj
BINDIR = bin
DEPDIR = .deps
# Compiler settings
CC = gcc
CFLAGS = -Wall -Wextra -std=c11 -I$(INCDIR)
LDFLAGS =
LDLIBS = -lm -lpthread
# Debug/Release
DEBUG ?= 0
ifeq ($(DEBUG),1)
CFLAGS += -g -O0 -DDEBUG
else
CFLAGS += -O2 -DNDEBUG
endif
# Find sources
SOURCES = $(wildcard $(SRCDIR)/*.c)
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
DEPENDS = $(SOURCES:$(SRCDIR)/%.c=$(DEPDIR)/%.d)
TARGET = $(BINDIR)/$(PROJECT)
# Default target
.PHONY: all
all: $(TARGET)
# Link
$(TARGET): $(OBJECTS) | $(BINDIR)
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
@echo "Build complete: $@"
# Compile with automatic dependencies
$(OBJDIR)/%.o: $(SRCDIR)/%.c $(DEPDIR)/%.d | $(OBJDIR) $(DEPDIR)
$(CC) -MT $@ -MMD -MP -MF $(DEPDIR)/$*.d $(CFLAGS) -c $< -o $@
# Create directories
$(BINDIR) $(OBJDIR) $(DEPDIR):
@mkdir -p $@
# Include dependencies
-include $(DEPENDS)
# Phony targets
.PHONY: clean install test run help
clean:
rm -rf $(OBJDIR) $(BINDIR) $(DEPDIR)
install: $(TARGET)
install -m 755 $(TARGET) /usr/local/bin/
test: $(TARGET)
@echo "Running tests..."
@./tests/run-tests.sh
run: $(TARGET)
@$(TARGET)
help:
@echo "Makefile for $(PROJECT) $(VERSION)"
@echo ""
@echo "Targets:"
@echo " all - Build the project (default)"
@echo " clean - Remove all build artifacts"
@echo " install - Install to /usr/local/bin"
@echo " test - Run test suite"
@echo " run - Build and run the program"
@echo " help - Show this help message"
@echo ""
@echo "Options:"
@echo " DEBUG=1 - Build with debug symbols"
Python Project Makefile
PYTHON = python3
PIP = $(PYTHON) -m pip
VENV = venv
ACTIVATE = . $(VENV)/bin/activate
.PHONY: all init test lint format clean help
all: init test
init: $(VENV)
$(ACTIVATE) && $(PIP) install -r requirements.txt
$(ACTIVATE) && $(PIP) install -e .
$(VENV):
$(PYTHON) -m venv $(VENV)
test:
$(ACTIVATE) && pytest tests/ -v --cov=src
lint:
$(ACTIVATE) && pylint src/
$(ACTIVATE) && flake8 src/
format:
$(ACTIVATE) && black src/ tests/
$(ACTIVATE) && isort src/ tests/
clean:
rm -rf $(VENV)
rm -rf .pytest_cache
rm -rf .coverage
find . -type d -name __pycache__ -exec rm -rf {} +
find . -type f -name '*.pyc' -delete
help:
@echo "Python Project Makefile"
@echo ""
@echo "Targets:"
@echo " init - Create venv and install dependencies"
@echo " test - Run test suite"
@echo " lint - Run linters"
@echo " format - Format code with black and isort"
@echo " clean - Remove virtual environment and cache"
Docker Project Makefile
IMAGE_NAME = myapp
IMAGE_TAG = latest
CONTAINER_NAME = myapp-container
REGISTRY = docker.io/username
.PHONY: build run stop clean push pull logs shell test
build:
docker build -t $(IMAGE_NAME):$(IMAGE_TAG) .
run: build
docker run -d \
--name $(CONTAINER_NAME) \
-p 8080:8080 \
$(IMAGE_NAME):$(IMAGE_TAG)
stop:
docker stop $(CONTAINER_NAME) || true
docker rm $(CONTAINER_NAME) || true
clean: stop
docker rmi $(IMAGE_NAME):$(IMAGE_TAG) || true
push: build
docker tag $(IMAGE_NAME):$(IMAGE_TAG) $(REGISTRY)/$(IMAGE_NAME):$(IMAGE_TAG)
docker push $(REGISTRY)/$(IMAGE_NAME):$(IMAGE_TAG)
pull:
docker pull $(REGISTRY)/$(IMAGE_NAME):$(IMAGE_TAG)
logs:
docker logs -f $(CONTAINER_NAME)
shell:
docker exec -it $(CONTAINER_NAME) /bin/bash
test: build
docker run --rm $(IMAGE_NAME):$(IMAGE_TAG) pytest
Best Practices
Organization
# 1. Configuration at top
PROJECT = myapp
VERSION = 1.0
# 2. Tool variables
CC = gcc
CFLAGS = -Wall
# 3. Directory variables
SRCDIR = src
OBJDIR = obj
# 4. File lists
SOURCES = $(wildcard $(SRCDIR)/*.c)
OBJECTS = $(SOURCES:%.c=%.o)
# 5. Default target first
all: $(PROJECT)
# 6. Real targets
$(PROJECT): $(OBJECTS)
$(CC) -o $@ $^
# 7. Pattern rules
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 8. Phony targets at end
.PHONY: all clean install
Silent Output
# Suppress command echoing with @
all:
@echo "Building..."
@$(CC) -o program main.c
# Or use .SILENT target
.SILENT:
all:
echo "Building..."
$(CC) -o program main.c
# Verbose flag
V ?= 0
ifeq ($(V),0)
Q = @
else
Q =
endif
all:
$(Q)echo "Building..."
$(Q)$(CC) -o program main.c
Error Handling
# Stop on error (default behavior)
.DELETE_ON_ERROR: # Delete target if recipe fails
# Ignore errors
clean:
-rm -f *.o # - prefix ignores error
# Check prerequisites exist
check-tools:
@which gcc > /dev/null || (echo "gcc not found" && exit 1)
@which make > /dev/null || (echo "make not found" && exit 1)
# Error messages
ifndef CC
$(error CC is not defined)
endif
ifeq ($(strip $(SOURCES)),)
$(warning No source files found)
endif
Performance
# Use := instead of = for variables that don't change
SOURCES := $(wildcard src/*.c) # Execute once
OBJECTS := $(SOURCES:.c=.o) # Expand once
# Avoid excessive shell calls
# Bad:
FILES = $(shell find . -name '*.c') # Executed every reference
# Good:
FILES := $(shell find . -name '*.c') # Executed once
# Parallel builds
# make -j4 # Use 4 cores
# make -j # Use all cores
# Optimize for parallel builds
.PHONY: parallel-safe
parallel-safe: obj1 obj2 obj3 # Can build in parallel
Platform Independence
# Detect OS
UNAME := $(shell uname -s)
ifeq ($(UNAME),Linux)
OS = linux
EXE =
RM = rm -f
endif
ifeq ($(UNAME),Darwin)
OS = macos
EXE =
RM = rm -f
endif
ifneq (,$(findstring MINGW,$(UNAME)))
OS = windows
EXE = .exe
RM = del
endif
TARGET = program$(EXE)
Common Recipes
Colorized Output
# Colors
RED = \033[0;31m
GREEN = \033[0;32m
YELLOW = \033[0;33m
NC = \033[0m # No Color
all:
@echo "$(GREEN)Building...$(NC)"
@$(CC) -o program main.c
@echo "$(GREEN)Build complete!$(NC)"
error:
@echo "$(RED)Error: Something went wrong$(NC)"
Progress Indicators
SOURCES = file1.c file2.c file3.c file4.c file5.c
TOTAL := $(words $(SOURCES))
CURRENT = 0
%.o: %.c
$(eval CURRENT := $(shell echo $$(($(CURRENT)+1))))
@echo "[$(CURRENT)/$(TOTAL)] Compiling $<..."
@$(CC) -c $< -o $@
Self-Documenting Makefile
.PHONY: help
help: ## Show this help message
@echo "Usage: make [target]"
@echo ""
@echo "Targets:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " %-15s %s\n", $$1, $$2}'
.DEFAULT_GOAL := help
all: ## Build everything
@echo "Building..."
clean: ## Remove build artifacts
@echo "Cleaning..."
test: ## Run tests
@echo "Testing..."
Troubleshooting
Common Issues
Missing TAB character:
# Wrong (spaces instead of TAB)
target:
echo "This will fail"
# Correct (TAB character)
target:
echo "This works"
Check for TAB:
cat -A Makefile # Shows TAB as ^I
Debugging:
# Print variable values
make --print-data-base
# Dry run (show commands without executing)
make -n
# Debug mode
make -d
# Print variables
make print-SOURCES
# With this rule:
print-%:
@echo $* = $($*)
Circular Dependencies:
# Wrong
a: b
b: a
# Make will error: "Circular a <- b dependency dropped"
Common Errors
# Error: target pattern contains no '%'
# Fix: Use correct pattern syntax
%.o: %.c
# Error: missing separator
# Fix: Use TAB not spaces before recipe
# Warning: overriding recipe for target
# Fix: Don't define same target twice (unless intentional)
# Error: No rule to make target
# Fix: Check prerequisite spelling and existence
Quick Reference
Command Line Options
| Option | Description |
|---|---|
make |
Build default target |
make target |
Build specific target |
make -f file |
Use specific makefile |
make -n |
Dry run (show commands) |
make -s |
Silent mode |
make -j4 |
Parallel build (4 jobs) |
make -k |
Keep going on errors |
make -B |
Force rebuild all |
make -d |
Debug mode |
make VAR=value |
Set variable |
Special Targets
| Target | Purpose |
|---|---|
.PHONY |
Declare phony targets |
.SILENT |
Suppress command echoing |
.DELETE_ON_ERROR |
Delete target on error |
.PRECIOUS |
Don't delete intermediate files |
.DEFAULT_GOAL |
Set default target |
.SUFFIXES |
Define suffix rules |
Automatic Variables
| Variable | Meaning |
|---|---|
$@ |
Target name |
$< |
First prerequisite |
$^ |
All prerequisites |
$? |
Prerequisites newer than target |
$* |
Stem of pattern match |
$(@D) |
Directory part of target |
$(@F) |
File part of target |
$(<D) |
Directory of first prerequisite |
$(<F) |
File of first prerequisite |
Further Reading
- GNU Make Manual - Official comprehensive documentation
- make man page - Command line reference
- Managing Projects with GNU Make - O'Reilly book
- Makefile Tutorial - Interactive tutorial
- Recursive Make Considered Harmful - Classic paper on make best practices