1

The Pain of Manual Compilation

Welcome to the Makefiles Tutorial!

Before you care how a Makefile works, you need to feel why it exists. Every build tool exists to solve a real pain. Let’s feel that pain first.

Task 1: Compile the project manually

We have a small C project with three files: main.c, math.c, and io.c. Let’s compile them the hard way:

cd make_project
gcc main.c math.c io.c -o app

Oh no! The compilation failed. There is a syntax error in math.c.

Task 2: Fix the error and recompile

  1. Open math.c in the editor.
  2. Fix the missing semicolon at the end of the return statement.
  3. Save the file.
  4. Go back to the terminal and re-type (or press Up arrow) that entire gcc command to try compiling again.

Notice what just happened: to fix one file, you had to recompile all three. gcc has no memory — it blindly reprocesses everything you hand it. In a 500-file project, fixing a single typo means a minutes-long recompile of every untouched file. We need a smarter tool.

Starter files
make_project/main.c
#include 
int add(int a, int b);
void init_io();

int main() {
    init_io();
    printf("Math test: 2 + 3 = %d\n", add(2, 3));
    return 0;
}
make_project/math.c
int add(int a, int b) {
    return a + b // BUG: missing semicolon
}
make_project/io.c
#include 
void init_io() {
    printf("IO Initialized.\n");
}
2

Your First Makefile & The Tab Trap

The Anatomy of a Rule

Makefiles are made of rules that describe a dependency graph. A rule looks like this:

target: prerequisites
    recipe

Make reads these rules, builds a graph of what depends on what, and only runs the recipes that are needed.

Task 1: Run your first Make command

A basic Makefile has been added to your project. Try running it:

make

Error! You should see: Makefile:2: *** missing separator. Stop.

Task 2: Fix the Tab Trap

Makefiles have one notoriously strict, invisible rule: Recipes MUST be indented with a true Tab character, not spaces!

In many text editors (like the one on the right) it is often hard to replace spaces with tabs. So for this task you will need to be a bit more creative. Hint: terminal-based editors like nano or vi might help or the sed command!

Starter files
make_project/Makefile
app: main.c math.c io.c
    gcc main.c math.c io.c -o app
3

Don't Repeat Yourself (DRY) with Variables

Enabling Incremental Builds

Our single-rule Makefile still recompiles everything together. To let Make skip unchanged files, we must compile each .c file into an object file (.o) separately, then link the .o files into the final executable.

Look at the new Makefile. It does this — but notice the problem: gcc -Wall -std=c11 is hardcoded four times. If we ever switch to clang, we’d have to edit four lines. This violates the DRY principle (Don’t Repeat Yourself).

Task: Refactor using Variables

In Makefiles, you define variables at the top and reference them with $(VAR_NAME).

  1. Open Makefile.
  2. At the very top, define two variables (these are Make’s standard names for C builds):
    CC = gcc
    CFLAGS = -Wall -std=c11
    
  3. Replace all 4 instances of gcc with $(CC).
  4. Replace all 4 instances of -Wall -std=c11 with $(CFLAGS).
  5. Save the file and run make to confirm it still compiles successfully.

Now a compiler change is a one-line edit at the top of the file.

Starter files
make_project/Makefile
app: main.o math.o io.o
	gcc -Wall -std=c11 main.o math.o io.o -o app

main.o: main.c
	gcc -Wall -std=c11 -c main.c

math.o: math.c
	gcc -Wall -std=c11 -c math.c

io.o: io.c
	gcc -Wall -std=c11 -c io.c
4

Smarter Rules: Automatic Variables & Patterns

The Repetition Problem

Look at your current Makefile. The three .o rules are almost identical:

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

math.o: math.c
	$(CC) $(CFLAGS) -c math.c

io.o: io.c
	$(CC) $(CFLAGS) -c io.c

Each filename appears twice per rule. With 50 source files you’d have 50 nearly identical rules. There must be a better way.

Automatic Variables

Make provides special variables that expand to parts of the current rule being evaluated:

Variable Expands to
$@ The target name (left of the :)
$< The first prerequisite (first item after the :)
$^ All prerequisites

Pattern Rules

A pattern rule uses % as a wildcard to match any filename stem:

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

This single rule tells Make: “to build any .o file, compile the matching .c file.” It replaces all three of your explicit .o rules.

Task: Refactor with OBJS, automatic variables, and a pattern rule

  1. At the very top (after CFLAGS), add an OBJS variable:
    OBJS = main.o math.o io.o
    
  2. Update the app rule to use $(OBJS) and the automatic variable $^ (all prereqs):
    app: $(OBJS)
    	$(CC) $(CFLAGS) $^ -o $@
    
  3. Delete the three explicit .o rules (main.o, math.o, io.o).
  4. Replace them with one pattern rule:
    %.o: %.c
    	$(CC) $(CFLAGS) -c $< -o $@
    
  5. Save and run make to confirm it still builds correctly.

Your Makefile shrinks from 14 lines to 8 — and it handles any number of source files with zero changes to the rules.

Starter files
make_project/Makefile
CC = gcc
CFLAGS = -Wall -std=c11

app: main.o math.o io.o
	$(CC) $(CFLAGS) main.o math.o io.o -o app

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

math.o: math.c
	$(CC) $(CFLAGS) -c math.c

io.o: io.c
	$(CC) $(CFLAGS) -c io.c
5

The Magic of Incremental Builds

The Core Idea: Timestamp Comparison

Make’s central trick is brutally simple: it compares the last-modified timestamp of each target against its prerequisites. If a prerequisite is newer than the target, the target is out of date and Make runs its recipe. Otherwise, it skips it.

This timestamp heuristic is what turns a 2-hour full rebuild into a 2-second incremental one.

Task 1: Check if up to date

Run make right now:

make

Make should tell you: make: 'app' is up to date. It skipped all work because the .o files and app are all newer than the .c files.

Task 2: Simulate a file change

The touch command updates a file’s timestamp without changing its content — it tricks Make into thinking you just edited it.

Run this to “update” only math.c:

touch math.c

Task 3: Observe the magic

Run make one more time:

make

Look closely at the output! Make compiled math.cmath.o and then re-linked app. It completely skipped main.c and io.c. They were still up to date — so Make left them alone. In a massive codebase this is the difference between waiting seconds and waiting hours.

6

The .PHONY Sabotage

Non-File Targets

Make is fundamentally about building files. But sometimes we want a target that just runs a command — like cleaning up build artifacts. There’s no output file; you just want the action.

Task 1: Add a clean target

Add this to the very bottom of your Makefile:

clean:
	rm -f *.o app

Run make clean in the terminal. Your build artifacts are gone!

Task 2: The Sabotage

Because Make assumes targets are files, what happens when a file actually named clean exists?

  1. Create a dummy file named clean:
    touch clean
    
  2. Run make app to generate the build files again.
  3. Try running make clean.

It fails! Make says make: 'clean' is up to date. It finds the file named clean, sees it has no prerequisites, decides it’s already “built,” and does nothing.

Task 3: The Fix — .PHONY

We must tell Make that clean is a phony target — a command name, not a filename.

Right above the clean: target, add:

.PHONY: clean

Save and run make clean again. Even though a file named clean exists, Make ignores it and correctly removes your build files.