Makefiles — Sample Solutions
Makefiles: From Pain to Power (C Edition) — Sample Solutions
These are reference solutions for each exercise in the interactive tutorial. Each solution explains why it is correct, connecting the code back to the concepts taught in that step.
Step 1: The Pain of Manual Compilation — math.c
Fix the missing semicolon in math.c:
int add(int a, int b) {
return a + b; // Bug fixed: added the missing semicolon
}
Then recompile:
cd make_project
gcc main.c math.c io.c -o app
Why this is correct:
- Test 1:
grep -q 'a + b;' math.c— the semicolon must be present at the end of thereturnstatement. - Test 2:
[ -f app ]— the compiled executableappmust exist. - The pain of manual compilation: After fixing the one-character bug, you had to re-type (or recall) the entire
gcccommand to recompile all three files — evenmain.candio.cwere untouched. This is the core problem Make solves: in a 500-file project, fixing one typo means recompiling everything.
Step 2: Your First Makefile & The Tab Trap — Makefile
Fix the spaces-to-Tab issue with sed, then verify:
# Replace the 4 leading spaces with a real Tab character
sed -i 's/^ /\t/' Makefile
# Verify the Tab is there (recipe lines show as ^I in cat -A)
cat -A Makefile
# Run make — should now compile successfully
make
The corrected Makefile (with a real Tab before gcc):
app: main.c math.c io.c
gcc main.c math.c io.c -o app
Why this is correct:
- Test 1:
grep -qP '^\tgcc' Makefile— the recipe line must start with a real Tab character (\t), not spaces.grep -Puses Perl-compatible regex where\tmatches a literal Tab. - Test 2:
[ -f app ]— Make must have run successfully and produced theappexecutable. - The Tab Trap: Make’s parser uses the Tab character specifically to identify recipe lines. Spaces look identical on screen but cause the infamous
missing separator. Stop.error. Most editors silently convert Tab keypresses to spaces, which is why this trap catches beginners. sed -i 's/^ /\t/':s/pattern/replacement/substitutes the pattern.^matches four spaces only at the start of a line (^anchors to line start).\tis a Tab character.-iedits the file in-place.
Step 3: Don’t Repeat Yourself (DRY) with Variables — 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
Why this is correct:
- Test 1:
grep -q 'CC *=' Makefile— theCCvariable must be defined. - Test 2:
grep -q 'CFLAGS *=' Makefile— theCFLAGSvariable must be defined. - Test 3:
grep -q '\$(CC)' Makefile—$(CC)must appear in the file (replacing the hardcodedgcc). - Test 4:
make && [ -f app ]— the build must still succeed. - DRY principle: Before this refactor,
gcc -Wall -std=c11appeared 4 times. WithCC = gccandCFLAGS = -Wall -std=c11, a switch fromgcctoclangrequires editing exactly one line. This is the same principle as C++#defineor Python constants. $(CC)syntax: Make expands variables with$(VAR_NAME)or${VAR_NAME}. The parentheses (or braces) are required for multi-character variable names —$CCalone would be interpreted as$Cfollowed by the literal characterC.
Step 4: Smarter Rules: Automatic Variables & Patterns — Makefile
CC = gcc
CFLAGS = -Wall -std=c11
OBJS = main.o math.o io.o
app: $(OBJS)
$(CC) $(CFLAGS) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
Why this is correct:
- Test 1:
grep -q 'OBJS *=' Makefile— theOBJSvariable must be defined. - Test 2:
grep -q '\$(OBJS)' Makefile—$(OBJS)must appear in theapprule. - Test 3:
grep -qP '%\.o.*:.*%\.c' Makefile— a pattern rule%.o: %.cmust exist. - Test 4:
grep -qP '\$[<^@]' Makefile— at least one automatic variable ($<,$^, or$@) must be used. - Test 5:
make && [ -f app ]— build must succeed. $^(all prerequisites): In theapprule,$^expands tomain.o math.o io.o— all the files listed in$(OBJS). This replaces the repetitivemain.o math.o io.oin the recipe.$@(target name): In theapprule,$@expands toapp. In the pattern rule when buildingmath.o,$@expands tomath.o.$<(first prerequisite): In the pattern rule,$<expands to the.cfile (e.g.,math.c). Using$<instead of$^compiles only the single matching source file.- Pattern rule
%.o: %.c: The%wildcard matches any filename stem. This single rule replaces all three explicit.orules. Addingnewfile.ctoOBJSis all that’s needed — no new explicit rule required.
Step 5: The Magic of Incremental Builds
# Run make — should say "make: 'app' is up to date"
make
# Touch math.c to simulate a change (updates its timestamp)
touch math.c
# Run make again — only math.c is recompiled
make
Why this is correct:
- Test:
[ math.o -nt main.o ]—math.omust be newer thanmain.o. Aftertouch math.c+make, onlymath.c→math.owas recompiled, somath.ohas a newer timestamp thanmain.o(which was not recompiled). - Make’s timestamp heuristic: Make compares the last-modified time of each target against its prerequisites. If a prerequisite is newer than the target, the target is out-of-date and its recipe runs.
touch math.c: Updatesmath.c’s modification timestamp without changing its content. Make seesmath.cis now newer thanmath.oand recompiles just that one file, then re-linksapp.main.candio.care untouched.- Why this matters: In a large project, this turns a potential hours-long full rebuild into a seconds-long incremental one.
Step 6: The .PHONY Sabotage — Makefile
Add the clean target and .PHONY declaration:
CC = gcc
CFLAGS = -Wall -std=c11
OBJS = main.o math.o io.o
app: $(OBJS)
$(CC) $(CFLAGS) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -f *.o app
Why this is correct:
- Test 1:
grep -q '\.PHONY:.*clean' Makefile—.PHONY: cleanmust appear in the file (before or after theclean:rule). - Test 2:
grep -q 'clean:' Makefile && make clean >/dev/null 2>&1 && [ ! -f app ] && ! ls *.o 2>/dev/null— thecleantarget must exist,make cleanmust succeed, and afterwards noappor.ofiles should remain. - The sabotage scenario: If a file named
cleanexists in your project directory and.PHONYis absent, Make thinkscleanis a real file target. Sincecleanhas no prerequisites, Make sees it as always up-to-date and refuses to run the recipe (make: 'clean' is up to date.). .PHONY: clean: Tells Make thatcleanis a command name, not a filename. Make ignores any file on disk namedcleanand always runs the recipe. Always declare non-file targets (likeclean,test,all) as.PHONY.rm -f *.o app:-fsuppresses errors when files don’t exist. Without it,make cleanwould fail if called when already clean.
Step 7: Mastering Make
No file edits are required for this step — it is a review and summary.
# Review the complete, professional Makefile you've built:
cat Makefile
# Final build to confirm everything still works
make clean
make
The complete professional Makefile:
CC = gcc
CFLAGS = -Wall -std=c11
OBJS = main.o math.o io.o
app: $(OBJS)
$(CC) $(CFLAGS) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -f *.o app
Why this is correct:
- This Makefile demonstrates every concept from the tutorial in 10 lines:
- Variables (
CC,CFLAGS,OBJS): DRY principle — change the compiler or flags in one place. $(OBJS)prerequisite: Declarative dependency graph — Make knows which.ofilesappneeds.$^and$@: Automatic variables — no repetition of filenames in the link command.- Pattern rule
%.o: %.c: One rule handles all source files; addingnewfile.cjust requires addingnewfile.otoOBJS. .PHONY: clean: Guaranteesmake cleanalways runs regardless of filesystem state.- Tab characters on recipe lines: The invisible but critical requirement that separates Make from all other config formats.
- Variables (
Key concept connections:
| Makefile feature | Why it matters |
|---|---|
| Tab trap | Parser requirement — spaces cause missing separator error |
Variables (CC, CFLAGS) |
DRY — one-line change to switch compilers |
Pattern rule %.o: %.c |
Scalable — one rule for any number of source files |
Automatic variables $@, $<, $^ |
No filename repetition in recipes |
| Timestamp-based DAG | Incremental builds — only recompiles what changed |
.PHONY |
Non-file targets always run, even if a same-named file exists |