Once your C project grows beyond one file, compiling by hand becomes painful. You either recompile everything every time (slow) or manually track which files changed (error-prone). A Makefile automates this — it rebuilds only what needs rebuilding, and it does it with a single make command.
The Problem Make Solves
/* Multi-file project */
gcc -c -Wall main.c -o main.o
gcc -c -Wall utils.c -o utils.o
gcc -c -Wall math.c -o math.o
gcc main.o utils.o math.o -o programIf you only change utils.c, you still recompile main.c and math.c. Make tracks file modification timestamps and only rebuilds targets whose dependencies are newer than the target itself.
A Minimal Makefile
# Makefile
program: main.o utils.o
gcc main.o utils.o -o program
main.o: main.c utils.h
gcc -c -Wall main.c -o main.o
utils.o: utils.c utils.h
gcc -c -Wall utils.c -o utils.o
clean:
rm -f *.o programImportant: the indented lines must use a tab character, not spaces. This is the most common Makefile error. Every recipe line starts with a literal tab.
Structure of a rule:
target: dependency1 dependency2
recipe commandMake reads: “to build target, first ensure dependency1 and dependency2 are up to date, then run the recipe.”
Variables — DRY Up Your Makefile
CC = gcc
CFLAGS = -Wall -Wextra -std=c11
LDFLAGS =
TARGET = program
SRCS = main.c utils.c math.c
OBJS = $(SRCS:.c=.o) /* replace .c with .o */
$(TARGET): $(OBJS)
$(CC) $(OBJS) $(LDFLAGS) -o $(TARGET)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(TARGET)The %.o: %.c pattern rule matches any .o file against the corresponding .c file. Now adding a new source file is a one-line change to SRCS.
Automatic Variables
| Variable | Meaning |
|---|---|
$@ | The target name |
$< | The first dependency |
$^ | All dependencies (space-separated) |
$* | The stem matched by % |
/* Using automatic variables */
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
/* $< = the .c file, $@ = the .o file */Generating Dependencies Automatically
CC = gcc
CFLAGS = -Wall -Wextra -std=c11 -MMD -MP
TARGET = program
SRCS = main.c utils.c math.c
OBJS = $(SRCS:.c=.o)
DEPS = $(SRCS:.c=.d)
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
-include $(DEPS) /* include generated .d dependency files */
clean:
rm -f $(OBJS) $(DEPS) $(TARGET)The -MMD -MP flags tell GCC to generate a .d file alongside each .o file. Each .d file lists the headers that the source file includes. When a header changes, Make knows which .o files depend on it and rebuilds exactly those.
Build Modes — Debug and Release
CC = gcc
TARGET = program
SRCS = main.c utils.c
OBJS = $(SRCS:.c=.o)
DEBUG_FLAGS = -g -O0 -fsanitize=address,undefined
RELEASE_FLAGS = -O2 -DNDEBUG
ifeq ($(BUILD), release)
CFLAGS = -Wall -Wextra -std=c11 $(RELEASE_FLAGS)
else
CFLAGS = -Wall -Wextra -std=c11 $(DEBUG_FLAGS)
endif
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $(OBJS) -o $(TARGET)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(TARGET)make /* debug build (default) */
make BUILD=release /* release build */Phony Targets
.PHONY: clean all run
all: $(TARGET)
run: $(TARGET)
./$(TARGET)
clean:
rm -f $(OBJS) $(TARGET).PHONY tells Make these targets are not files. Without it, if you create a file named clean, make clean would see the file as up-to-date and skip the recipe. Always declare non-file targets as .PHONY.
Putting It Together
CC = gcc
CFLAGS = -Wall -Wextra -Werror -std=c11 -g -O0 -MMD -MP
TARGET = program
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
DEPS = $(SRCS:.c=.d)
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
-include $(DEPS)
clean:
rm -f $(OBJS) $(DEPS) $(TARGET)This Makefile compiles every .c file in the current directory, generates automatic dependency files, and uses the same flags as recommended in our guide on GCC warning flags. Drop it in any small C project and it works. For testing individual snippets before building a project, the gcc compiler online gives you immediate feedback without a local setup.
TL;DR
- Make rebuilds only targets whose dependencies are newer — saves time on multi-file projects
- Recipe lines must start with a tab, not spaces — this is the most common error
- Use
$(SRCS:.c=.o)to generate object lists from source lists - Pattern rules
%.o: %.celiminate repetition - Add
-MMD -MPto CFLAGS to auto-generate header dependencies - Always declare non-file targets in
.PHONY - Separate debug and release flags with an
ifeqon aBUILDvariable