Contents
  1. The Rule Structure
  2. Targets and Dependency Chains
  3. User-Defined Targets and the all Convention
  4. .PHONY
  5. .SUFFIXES
  6. .PRECIOUS
  7. Automatic Variables
  8. A Complete Example
  9. What You Can Do Now
← All posts

Makefiles for C++ Projects: Targets, Dependencies, and Special Rules

A Makefile automates the build process for C++ projects. Understanding targets, prerequisites, dependency chains, and special rules like .PHONY and .PRECIOUS gives you full control over how your project compiles, tests, and cleans up.

A Makefile is a set of instructions that tells make how to build a project. For C++ projects, it replaces the manual step of typing compiler commands by defining which files to compile, in what order, and under what conditions. Beyond compilation, a Makefile can run tests, check code style, and clean up build artifacts, all from a single command. Understanding the structure and special rules makes the difference between a Makefile that works and one that is fragile.

The Rule Structure

Every Makefile is built from rules. A rule has three parts: a target, prerequisites, and a recipe.

target: prerequisite_1 prerequisite_2
	recipe_command

The target is typically a file that the rule produces. The prerequisites are files or other targets that must exist or be up to date before the recipe runs. The recipe is the shell command that produces the target. Crucially, the recipe must be indented with a tab character, not spaces. Make will reject a Makefile with space-indented recipes.

When you run make target, Make checks whether the target file exists and whether any prerequisite is newer than the target. If the target is missing or outdated, Make runs the recipe. If the target is already up to date, Make does nothing. This timestamp-based check is what makes incremental builds possible. Only what changed gets recompiled.

Targets and Dependency Chains

Targets can depend on other targets, forming a dependency chain. Make resolves the chain from the bottom up before running any recipe.

main.o: main.cpp
	g++ -std=c++11 -g -c main.cpp -o main.o

utils.o: utils.cpp utils.h
	g++ -std=c++11 -g -c utils.cpp -o utils.o

myapp: main.o utils.o
	g++ -std=c++11 -g main.o utils.o -o myapp

Running make myapp first checks that main.o and utils.o exist and are current. If they are not, Make compiles them first. Then it links them into myapp. If only utils.cpp changed since the last build, Make recompiles only utils.o and relinks, leaving main.o untouched.

User-Defined Targets and the all Convention

A Makefile target does not have to produce a file. Targets can represent actions, and they can depend on other targets in sequence.

all: clean compile test

When make all runs, Make first runs clean, then compile, then test, in that order. This is the standard way to define a full build pipeline. Because all is listed first in the file, running make with no arguments runs all by default.

User-defined targets can be chained arbitrarily:

compile: main.o utils.o
	g++ -std=c++11 main.o utils.o -o myapp

test: compile
	./run_tests.sh

checkstyle:
	cppcheck --enable=all *.cpp

clean:
	rm -f *.o myapp

test depends on compile, so running make test will compile first if needed, then run tests. checkstyle is independent and can be run on its own.

.PHONY

.PHONY declares that a target is not a file. This matters because Make uses the filesystem to determine whether a target is up to date. If a file named clean exists in your project directory, running make clean will find that file, see it as already current, and skip the recipe entirely.

Declaring a target as phony tells Make to always run its recipe regardless of whether a file with that name exists.

.PHONY: all clean test checkstyle

Any target that does not produce a file should be listed under .PHONY. This includes all, clean, test, and any other action-oriented target. Without it, creating a file that shares a name with a target silently breaks that target.

.SUFFIXES

.SUFFIXES defines the file extensions that Make recognises for its built-in implicit rules. Make has built-in knowledge of common build patterns, for example, it knows how to compile a .c file into a .o file. .SUFFIXES controls which extensions participate in those implicit rules.

.SUFFIXES: .cpp .o

In modern Makefiles with explicit rules for every target, .SUFFIXES is often cleared first to disable all built-in implicit rules and prevent unexpected behaviour:

.SUFFIXES:
.SUFFIXES: .cpp .o

.PRECIOUS

.PRECIOUS marks files that should not be deleted even if Make is interrupted mid-build or if a rule fails. By default, Make deletes intermediate files it created during a failed build. If you mark a file as precious, Make preserves it.

.PRECIOUS: %.o

This is useful for object files in long compilation pipelines. If the link step fails, you do not want to lose the compiled object files and be forced to recompile everything from source on the next run.

Automatic Variables

Inside a recipe, Make provides automatic variables that refer to parts of the current rule without hardcoding names.

VariableMeaning
$@The target name
$<The first prerequisite
$^All prerequisites
$?Prerequisites newer than the target
%.o: %.cpp
	g++ -std=c++11 -g -c $< -o $@

This pattern rule compiles any .cpp file into a .o file. $< is the .cpp source file, $@ is the .o output. Pattern rules combined with automatic variables eliminate the need to write a separate rule for every source file.

A Complete Example

CXX      = g++
CXXFLAGS = -std=c++11 -g -Wall
TARGET   = myapp
SRCS     = main.cpp utils.cpp
OBJS     = $(SRCS:.cpp=.o)

.PHONY: all clean test checkstyle
.PRECIOUS: $(OBJS)
.SUFFIXES:
.SUFFIXES: .cpp .o

all: clean $(TARGET) test

$(TARGET): $(OBJS)
	$(CXX) $(CXXFLAGS) $^ -o $@

%.o: %.cpp
	$(CXX) $(CXXFLAGS) -c $< -o $@

test: $(TARGET)
	./$(TARGET) --test

checkstyle:
	cppcheck --enable=all $(SRCS)

clean:
	rm -f $(OBJS) $(TARGET)

SRCS:.cpp=.o substitutes .cpp with .o in the source list to produce the object file list automatically. $(CXX) and $(CXXFLAGS) are variables that make the compiler and flags easy to change in one place.

What You Can Do Now

Create the Makefile above in a directory with at least two .cpp files and run each target:

make clean       # removes object files and binary
make myapp       # compiles only what changed
make test        # compiles if needed, then runs tests
make checkstyle  # runs static analysis
make all         # runs clean, then full build, then test

Create an empty file named clean in the same directory, then try make clean without .PHONY declared. Make will report that clean is already up to date and do nothing. Add .PHONY: clean back and run again. The recipe runs regardless. That demonstration is the clearest argument for always declaring action targets as phony.

← All posts