Code of the Day
AdvancedC compilation model

Makefiles and build systems

Automate C project builds with make — writing Makefile rules, using pattern rules and variables, and only recompiling changed files.

CAdvanced11 min read
Recommended first
By the end of this lesson you will be able to:
  • Write a Makefile with explicit rules for a multi-file C project
  • Use automatic variables like $@, $<, and $^ in rules
  • Define phony targets like clean and all
  • Explain how make determines which files need recompilation

When a project has ten source files and you change one, you do not want to recompile all ten. make reads a Makefile that describes which files depend on which other files and how to build them. It then recompiles only the files whose source has changed since the last build.

The anatomy of a Makefile rule

target: prerequisites
	recipe
  • target: the file to build (or a phony name like all or clean).
  • prerequisites: files that must be up to date before the target can be built.
  • recipe: the shell command(s) to build the target. Must be indented with a tab, not spaces.

A simple Makefile

For a project with main.c, vector.c, and vector.h:

CC = gcc
CFLAGS = -Wall -Wextra -g
TARGET = myprogram

$(TARGET): main.o vector.o
	$(CC) $(CFLAGS) main.o vector.o -o $(TARGET)

main.o: main.c vector.h
	$(CC) $(CFLAGS) -c main.c -o main.o

vector.o: vector.c vector.h
	$(CC) $(CFLAGS) -c vector.c -o vector.o

.PHONY: clean
clean:
	rm -f *.o $(TARGET)

Running make builds myprogram. Running make clean removes all build artefacts.

make checks timestamps: if vector.c was modified after vector.o, it recompiles vector.o. If only main.c changed, only main.o and the final link are redone.

Automatic variables

Typing filenames explicitly everywhere is tedious and error-prone. Make provides automatic variables:

VariableMeaning
$@The target name
$<The first prerequisite
$^All prerequisites
$*The stem of a pattern rule

Rewrite:

$(TARGET): main.o vector.o
	$(CC) $(CFLAGS) $^ -o $@

main.o: main.c vector.h
	$(CC) $(CFLAGS) -c $< -o $@

vector.o: vector.c vector.h
	$(CC) $(CFLAGS) -c $< -o $@

Pattern rules

Instead of writing a rule for every .o file, use a pattern rule:

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

% matches any stem. This one rule handles all .c to .o compilations. Combine with a variable listing all objects:

CC = gcc
CFLAGS = -Wall -Wextra -g
TARGET = myprogram
SRCS = main.c vector.c
OBJS = $(SRCS:.c=.o)

$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) $^ -o $@

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

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

$(SRCS:.c=.o) replaces .c with .o in SRCS — a Make substitution reference.

Handling header dependencies

The pattern rule above recompiles main.o if main.c changes, but not if vector.h changes. Fix with explicit per-file dependencies or automatic dependency generation:

# Explicit (manageable for small projects):
main.o: main.c vector.h
vector.o: vector.c vector.h

# Automatic (better for larger projects):
DEPFLAGS = -MMD -MP
DEPS = $(OBJS:.o=.d)

%.o: %.c
	$(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@

-include $(DEPS)

-MMD -MP makes gcc generate a .d dependency file listing which headers each .o depends on. -include reads those files, automatically adding the right prerequisites.

Useful targets to add

.PHONY: all clean test valgrind

all: $(TARGET)

test: $(TARGET)
	./run_tests.sh

valgrind: $(TARGET)
	valgrind --leak-check=full ./$(TARGET)

cmake and meson are the modern alternatives. For large projects with cross-platform requirements, CMake or Meson generates platform-specific build files from a higher-level description. Learning make first is worthwhile — it is used in millions of projects and CMake generates Makefiles on Linux anyway.

Where to go next

Next: the multi-file project lab — you will split a previous project across multiple files, write the header and Makefile, and confirm that incremental builds work correctly.

Finished reading? Mark it complete to track your progress.

On this page