CWriting Makefiles

Writing Makefiles

Building on the core concepts from Build Basics with Make, this page walks through a complete, practical Makefile for a small multi-file C project — introducing variables, pattern rules, a clean target, and the automatic variables that make Makefiles concise.
The Project

Assume a small project with this layout:

Bash
project/
  main.c
  stack.c
  stack.h
  queue.c
  queue.h
  Makefile
A Complete Minimal Makefile

Text
CC = gcc
CFLAGS = -Wall -Wextra -std=c11 -g
OBJS = main.o stack.o queue.o
TARGET = program

# Default target: build the final executable
$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) $(OBJS) -o $(TARGET)

# Pattern rule: how to build any .o from its matching .c
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

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

.PHONY: clean
Variables: CC, CFLAGS, OBJS

Makefile variables avoid repeating the same values everywhere, and make the whole file easy to adjust in one place.

Variable

Convention

Purpose

CC

The compiler to use

e.g. gcc, clang — change once to switch compilers everywhere

CFLAGS

Compiler flags

Warnings, standard version, optimization/debug info, all in one place

OBJS

List of object files

Lets rules refer to "all the object files" without repeating the list

Pattern Rules: %.o: %.c

Rather than writing a separate rule for main.o, stack.o, and queue.o individually, a pattern rule describes how to build any .o file from its correspondingly named .c file in one line:

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

Make matches this pattern against whichever .o file it needs (say, stack.o), infers the matching source file (stack.c), and runs the recipe.

Automatic Variables: $@ and $<

Variable

Meaning

In the pattern rule above

$@

The target of the current rule

The .o file being built, e.g. stack.o

$<

The first dependency of the current rule

The matching .c file, e.g. stack.c

$^

All dependencies of the current rule

Used less often; e.g. all .o files in a link rule

These let a single generic rule work correctly no matter which specific .o file is currently being built — Make substitutes in the real filenames each time.

The clean Target

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

Running make clean removes every generated file, letting you force a completely fresh rebuild — useful after changing compiler flags or when something seems stuck in a stale state.

.PHONY: Why It's Needed

Make normally treats a target name as the name of a file it should produce. But clean doesn't produce a file called clean — it just runs cleanup commands. If a file literally named clean ever existed in the project directory, Make would see it's already "up to date" and refuse to run the recipe at all. Declaring clean as .PHONY tells Make: "this target isn't a real file — always run its recipe when asked, regardless of what's on disk."

Text
.PHONY: clean
Note
Any target whose job is to *do something* rather than *produce a file with that exact name* — `clean`, `all`, `test`, `install` — should be declared `.PHONY`.
Using the Makefile

Bash
make          # builds "program" (the first target, so it's the default)
make clean    # removes all object files and the executable
make program  # explicitly builds the "program" target
Tip
Real-world Makefiles often add a `-MMD` flag (or similar) to automatically track header file dependencies too, so that editing a `.h` file correctly triggers recompilation of every `.c` file that includes it. That's a natural next step once you're comfortable with the basics shown here.
Warning
Remember from Build Basics with Make: every recipe line must start with an actual tab character. Copy-pasting a Makefile from a web page or chat window can silently convert tabs to spaces — if `make` reports `missing separator`, check your indentation first.