Makefiles and Build Systems

Make is a build automation tool that tracks file dependencies and only recompiles what changed. A Makefile defines targets, their dependencies, and the commands to build them.

Why It Matters

Real C projects have dozens of source files. Recompiling everything after changing one file wastes time. Make’s dependency graph means editing utils.c only recompiles utils.o and relinks — not the entire project. Understanding Make also helps you read build systems in any C/C++ project.

Makefile Anatomy

CC = gcc
CFLAGS = -Wall -Wextra -O2 -g
LDFLAGS = -lm
 
SRCS = main.c utils.c parser.c
OBJS = $(SRCS:.c=.o)
 
# Default target (first rule)
program: $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
 
# Pattern rule: how to build any .o from its .c
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@
 
clean:
	rm -f $(OBJS) program
 
.PHONY: clean

Automatic Variables

VariableMeaningExample
$@Target nameprogram
$<First dependencymain.c
$^All dependenciesmain.o utils.o parser.o
$*Stem of pattern matchmain (from main.o)

Key gcc Flags

FlagPurpose
-Wall -WextraEnable warnings (always use)
-O2Optimize for speed
-gDebug symbols for GDB
-fsanitize=addressAddressSanitizer — catches memory bugs at runtime
-MMD -MPGenerate dependency files (see below)
-I./includeAdd header search path
-staticStatic linking (self-contained binary)
-lmLink math library

Automatic Dependency Tracking

Header changes should trigger recompilation. Use -MMD -MP to auto-generate .d files:

CFLAGS += -MMD -MP
DEPS = $(OBJS:.o=.d)
 
-include $(DEPS)   # include if they exist, ignore if not

This generates main.d containing something like:

main.o: main.c utils.h parser.h

Now changing utils.h correctly rebuilds main.o.

Static vs Dynamic Linking

AspectStatic (.a)Dynamic (.so)
Binary sizeLarger — code copied inSmaller — loaded at runtime
DeploymentSelf-containedNeeds library on target system
UpdatesMust recompile to update libJust replace .so file
Load timeFaster (no dynamic linking)Slightly slower
# Create and use static library
ar rcs libutils.a utils.o parser.o
gcc main.c -L. -lutils -o program
 
# Create and use shared library
gcc -shared -fPIC -o libutils.so utils.c parser.c
gcc main.c -L. -lutils -o program
# At runtime: LD_LIBRARY_PATH=. ./program

Complete Project Example

CC      = gcc
CFLAGS  = -Wall -Wextra -O2 -g -MMD -MP
LDFLAGS =
 
SRC_DIR = src
OBJ_DIR = build
SRCS    = $(wildcard $(SRC_DIR)/*.c)
OBJS    = $(SRCS:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
DEPS    = $(OBJS:.o=.d)
TARGET  = program
 
$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
 
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
	$(CC) $(CFLAGS) -c $< -o $@
 
$(OBJ_DIR):
	mkdir -p $@
 
clean:
	rm -rf $(OBJ_DIR) $(TARGET)
 
-include $(DEPS)
.PHONY: clean