Skip to content

Makefiles

Makefiles are build automation tools that automatically determine which pieces of a program need to be recompiled and issue commands to recompile them. They are essential for managing C++ projects of any significant size.

Make is a build automation tool that reads files called Makefiles to determine which files need to be updated and runs the necessary commands to rebuild them. It uses file timestamps to efficiently rebuild only what has changed.

# Comments start with #
# Note: Tabs (not spaces) are required before commands!
CXX = g++ # C++ compiler
CXXFLAGS = -Wall -Wextra -std=c++17 -O2 # Compiler flags
TARGET = myprogram # Final executable name
SRCS = main.cpp utils.cpp helper.cpp # Source files
OBJS = $(SRCS:.cpp=.o) # Object files (auto-converted)
# Default target - what runs when you just type 'make'
all: $(TARGET)
# Link object files to create executable
$(TARGET): $(OBJS)
$(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS)
# Pattern rule: compile .cpp to .o
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
# Clean build artifacts
clean:
rm -f $(TARGET) $(OBJS)
# Phony targets (not actual files)
.PHONY: all clean
  • Targets: Files to create (e.g., myprogram)
  • Prerequisites: Files needed to create the target (e.g., .o files)
  • Recipes: Commands to create the target from prerequisites
  • Pattern Rules: Templates using % wildcards

Makefiles support different types of variables:

# Recursive - evaluated when used, can change later
CC = gcc
CC = gcc # Can be reassigned
# Usage
show:
@echo $(CC) # Shows final value
# Evaluated when defined - more efficient
CC := gcc
CXX := g++
# Using variables
CFLAGS := -Wall -Wextra
CXXFLAGS := $(CFLAGS) -std=c++17 # Combines variables
# Append to variable
CXXFLAGS += -O3

Make provides automatic variables that have special meanings:

# $@ - Target name
# $< - First prerequisite
# $^ - All prerequisites (space-separated)
# $? - Prerequisites newer than target
# $* - Stem of pattern match (part before .o in %.o)
# $+ - All prerequisites (保留顺序,可能重复)
# Example: compile each .cpp to .o
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
# Example: link all objects
program: main.o utils.o lib.o
$(CXX) $^ -o $@ # $^ = main.o utils.o lib.o
# Access environment variables
HOME_DIR := $(HOME)
PATH := $(PATH)
# Override with command line: make CC=clang
# Or: export CC=clang && make

Pattern rules allow generic rules that apply to multiple files:

# Compile any .cpp to .o
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
# Support both .cpp and .cc extensions
SRCS := $(wildcard *.cpp) $(wildcard *.cc)
OBJS := $(SRCS:.cpp=.o) $(SRCS:.cc=.o)
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
%.o: %.cc
$(CXX) $(CXXFLAGS) -c $< -o $@
# Build from src/ to build/
BUILD_DIR = build
SRC_DIR = src
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(BUILD_DIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
# Create directory if needed
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)

Phony targets don’t represent actual files - they’re just commands:

.PHONY: all clean test run install rebuild
# Clean removes build artifacts
clean:
rm -f $(TARGET) *.o
# Rebuild = clean + build
rebuild: clean all
# Run the program
run: $(TARGET)
./$(TARGET)
# Run tests
test: $(TARGET)
./$(TARGET) --test
# Install program
install: $(TARGET)
cp $(TARGET) /usr/local/bin/

Makefiles support conditionals:

# Check if equal
ifeq ($(DEBUG),1)
CXXFLAGS += -g -O0 -DDEBUG
else
CXXFLAGS += -O2
endif
# Check if not equal
ifneq ($(MODE),release)
CXXFLAGS += -DTESTING
endif
# If defined
ifdef RELEASE
CXXFLAGS += -DNDEBUG
endif
# If not defined
ifndef OUTPUT_DIR
OUTPUT_DIR = ./build
endif
ifeq ($(OS),Windows_NT)
RM = del /Q
EXE = .exe
PATH_SEP = \\
else
RM = rm -f
EXE =
PATH_SEP = /
endif

Make provides built-in functions:

# Find all .cpp files in current directory
SOURCES = $(wildcard *.cpp)
# Find in subdirectories
ALL_SRCS = $(wildcard src/*.cpp utils/*.cpp)
# Replace .cpp with .o
SOURCES = main.cpp utils.cpp helper.cpp
OBJECTS = $(patsubst %.cpp,%.o,$(SOURCES))
# Result: main.o utils.o helper.o
# Get only C++ files
ALL_FILES = $(wildcard *.*)
CXX_FILES = $(filter %.cpp %.cc %.cxx,$(ALL_FILES))
# Get just filenames without path
SRCS = src/main.cpp src/utils.cpp
FILES = $(notdir $(SRCS))
# Result: main.cpp utils.cpp
# Get current date
DATE = $(shell date)
# List files
FILES = $(shell ls *.cpp)
# Count processors
JOBS = $(shell nproc)
# Project structure
SRC_DIR := src
INC_DIR := include
BUILD_DIR := build
BIN_DIR := bin
# Find all source files
SOURCES := $(wildcard $(SRC_DIR)/*.cpp)
HEADERS := $(wildcard $(INC_DIR)/*.h)
# Convert to objects
OBJECTS := $(patsubst $(SRC_DIR)/%.cpp,$(BUILD_DIR)/%.o,$(SOURCES))
# Get dependencies
DEPS := $(OBJECTS:.o=.d)
# Final target
TARGET := $(BIN_DIR)/myprogram

Generate dependency files during compilation:

CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++17 -MMD
# Include dependency files
-include $(DEPS)
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
# This generates .d files with -MMD flag

The -MMD flag generates dependencies excluding system headers.

Here’s a production-ready Makefile:

# ===========================================
# Project Configuration
# ===========================================
PROJECT_NAME = MyApplication
CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++17 -MMD
LDFLAGS =
LIBS = -lm -lpthread
# Directories
SRC_DIR = src
INC_DIR = include
BUILD_DIR = build
BIN_DIR = bin
# Find source and header files
SOURCES = $(wildcard $(SRC_DIR)/*.cpp)
HEADERS = $(wildcard $(INC_DIR)/*.h)
OBJECTS = $(patsubst $(SRC_DIR)/%.cpp,$(BUILD_DIR)/%.o,$(SOURCES))
DEPS = $(OBJECTS:.o=.d)
TARGET = $(BIN_DIR)/$(PROJECT_NAME)
# ===========================================
# Build Modes
# ===========================================
DEBUG_MODE = 0
ifeq ($(DEBUG_MODE),1)
CXXFLAGS += -g -O0 -DDEBUG
TARGET := $(BIN_DIR)/$(PROJECT_NAME)_debug
else
CXXFLAGS += -O3 -DNDEBUG
TARGET := $(BIN_DIR)/$(PROJECT_NAME)_release
endif
# ===========================================
# Main Targets
# ===========================================
.PHONY: all clean test run rebuild info
all: info $(TARGET)
@echo "Build complete: $(TARGET)"
info:
@echo "Project: $(PROJECT_NAME)"
@echo "Sources: $(SOURCES)"
@echo "Objects: $(OBJECTS)"
@echo "Target: $(TARGET)"
$(TARGET): $(OBJECTS) | $(BIN_DIR)
$(CXX) $(CXXFLAGS) $^ -o $@ $(LDFLAGS) $(LIBS)
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp $(HEADERS) | $(BUILD_DIR)
$(CXX) $(CXXFLAGS) -c $< -o $@ -I$(INC_DIR)
# ===========================================
# Directory Creation
# ===========================================
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
$(BIN_DIR):
mkdir -p $(BIN_DIR)
# ===========================================
# Utility Targets
# ===========================================
clean:
rm -rf $(BUILD_DIR) $(BIN_DIR)
test: $(TARGET)
@echo "Running tests..."
./$(TARGET) --test
run: $(TARGET)
./$(TARGET)
rebuild: clean all
# ===========================================
# Include Dependencies
# ===========================================
-include $(DEPS)
Terminal window
# Show what would be executed without running
make -n
# Show with environment
make -n DEBUG=1
Terminal window
# Show commands being executed
make V=1
# Or set in Makefile
MAKEFLAGS += --print-directory
debug:
@echo "SOURCES = $(SOURCES)"
@echo "OBJECTS = $(OBJECTS)"
@echo "TARGET = $(TARGET)"
@echo "CXX = $(CXX)"
lib: libmylib.a
libmylib.a: $(OBJS)
ar rcs $@ $^
ranlib $@ # Some systems need this
lib.so: $(OBJS)
$(CXX) -shared -fPIC -o $@ $^
# With version
lib.so.1: lib.so
ln -sf lib.so lib.so.1
Terminal window
# Use all CPU cores
make -j$(nproc)
# Or specific number
make -j4
.PHONY: debug release
debug: CXXFLAGS += -g -O0 -DDEBUG
debug: TARGET := $(BIN_DIR)/program_debug
debug: all
release: CXXFLAGS += -O3 -DNDEBUG
release: TARGET := $(BIN_DIR)/program_release
release: all
  1. Use Variables: Define compiler, flags, and paths as variables
  2. Enable Warnings: Always use -Wall -Wextra
  3. Pattern Rules: Use %.o: %.cpp to reduce duplication
  4. Dependencies: Include automatic dependency files
  5. Phony Targets: Mark non-file targets with .PHONY
  6. Separate Builds: Keep debug and release configurations
  7. Wildcards: Use $(wildcard) for flexible source discovery
  8. Directories: Create output directories before building
  9. Tab Characters: Use actual tabs (not spaces) before commands
  10. Clean Target: Always provide a clean target
  • Makefiles automate builds based on file timestamps
  • Variables simplify maintenance and configuration
  • Pattern rules (%.o: %.cpp) enable generic build rules
  • Automatic dependencies prevent rebuild issues
  • Conditionals enable flexible build configurations
  • Phony targets prevent conflicts with actual files

This comprehensive Makefile guide provides everything needed to build C++ projects efficiently. For larger projects, consider using CMake (covered in the previous chapter) which generates Makefiles automatically.