Make Basics
{:.gc-basic}
Basic
make reads a Makefile and rebuilds only the files that are out of date, saving time on large projects.
Anatomy of a Rule
target: dependencies
recipe # MUST be indented with a TAB (not spaces!)
A Minimal Makefile
# Build the 'app' binary from main.c and utils.c
app: main.o utils.o
gcc -o app main.o utils.o
main.o: main.c utils.h
gcc -c main.c
utils.o: utils.c utils.h
gcc -c utils.c
clean:
rm -f app *.o
make # builds 'app' (first target)
make clean # removes build artifacts
make utils.o # build a specific target
Variables
{:.gc-basic}
CC = gcc
CFLAGS = -Wall -Wextra -O2 -g
LDFLAGS = -lm
TARGET = app
SRCS = main.c utils.c sensor.c
OBJS = $(SRCS:.c=.o) # text substitution: replace .c with .o
$(TARGET): $(OBJS)
$(CC) $(LDFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f $(TARGET) $(OBJS)
Automatic Variables
| Variable | Meaning |
|---|---|
$@ |
The target name |
$< |
The first dependency |
$^ |
All dependencies (deduplicated) |
$? |
All dependencies newer than target |
$* |
The stem matched by % in pattern rules |
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
# ^^ target (e.g. main.o)
# ^^ first dep (main.c)
Pattern Rules and Phony Targets
{:.gc-mid}
Intermediate
Pattern Rules
Instead of a rule per .c file, one pattern rule handles all of them:
# Compile any .c file into a .o file
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
Phony Targets
Targets that don’t produce a file — always run when requested:
.PHONY: all clean install test
all: $(TARGET)
clean:
rm -f $(TARGET) $(OBJS) $(DEPS)
install: $(TARGET)
install -m 755 $(TARGET) /usr/local/bin/
test: $(TARGET)
./run_tests.sh
Without .PHONY, if a file named clean exists, make clean would do nothing.
Automatic Dependency Tracking
{:.gc-mid}
When a header changes, only the .c files that include it should recompile. Generate .d dependency files automatically with GCC:
CC = gcc
CFLAGS = -Wall -O2 -g
DEPFLAGS = -MMD -MP # generate .d files alongside .o files
TARGET = app
SRCS = $(wildcard src/*.c)
OBJS = $(SRCS:src/%.c=build/%.o)
DEPS = $(OBJS:.o=.d)
$(TARGET): $(OBJS)
$(CC) -o $@ $^
build/%.o: src/%.c | build
$(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $<
build:
mkdir -p build
-include $(DEPS) # include .d files; '-' suppresses error if missing
.PHONY: clean
clean:
rm -rf build $(TARGET)
The -MMD -MP flags make GCC output e.g. build/utils.d:
build/utils.o: src/utils.c src/utils.h src/config.h
src/utils.h:
src/config.h:
Advanced: Multi-Directory Projects
{:.gc-adv}
Advanced
Recursive Make (Traditional)
# Top-level Makefile
SUBDIRS = lib src tests
.PHONY: all clean $(SUBDIRS)
all: $(SUBDIRS)
src: lib # src depends on lib being built first
$(SUBDIRS):
$(MAKE) -C $@
clean:
for d in $(SUBDIRS); do $(MAKE) -C $$d clean; done
Non-Recursive Make (Better for Large Projects)
One Makefile at the root that includes module fragments:
# Makefile
CC = gcc
CFLAGS = -Wall -O2 -Iinclude
include lib/module.mk
include src/module.mk
TARGET = app
$(TARGET): $(ALL_OBJS)
$(CC) -o $@ $^
# lib/module.mk
LIB_SRCS := $(wildcard lib/*.c)
LIB_OBJS := $(LIB_SRCS:.c=.o)
ALL_OBJS += $(LIB_OBJS)
Cross-Compilation Makefile
ARCH ?= arm
CROSS ?= arm-linux-gnueabihf-
CC = $(CROSS)gcc
STRIP = $(CROSS)strip
SYSROOT ?= /opt/sysroot
CFLAGS = -Wall -O2 -march=armv7-a -mfpu=neon --sysroot=$(SYSROOT)
LDFLAGS = --sysroot=$(SYSROOT)
TARGET = sensor_daemon
$(TARGET): main.o sensor.o
$(CC) $(LDFLAGS) -o $@ $^
$(STRIP) $@
deploy: $(TARGET)
scp $(TARGET) root@192.168.1.100:/usr/local/bin/
make ARCH=arm CROSS=arm-linux-gnueabihf-
make deploy
Interview Q&A
{:.gc-iq}
Interview Q&A
Q1 — Basic: Why must Makefile recipe lines be indented with a tab, not spaces?
This is a historical quirk from the original 1976 Make implementation. The parser uses the tab character as a signal that a line is a recipe (command) rather than a Makefile directive. Using spaces causes the cryptic error:
Makefile:5: *** missing separator. Stop.Modern build systems (Meson, CMake, Ninja) don’t have this restriction.
Q2 — Basic: What does .PHONY do and why is it important?
.PHONYtells make that a target is not a real file. Without it, if a file namedcleanhappens to exist,make cleanwould see the file as up-to-date and do nothing..PHONYforces the recipe to run every time, regardless of any files with the same name.
Q3 — Intermediate: Explain the -MMD -MP GCC flags.
-MMDgenerates a.ddependency file alongside the.ofile containing themakerule for all headers the source includes.-MPadds an empty rule for each header to prevent errors if a header is deleted (without it, make would error because it can’t find the header listed in the.dfile). Together they enable automatic header dependency tracking without manually listing headers.
Q4 — Advanced: What is the difference between recursive and non-recursive make, and what are the trade-offs?
Recursive make runs
makein subdirectories. It’s simple to understand but has serious problems: Make can’t see the full dependency graph across directories, so it may rebuild in the wrong order or fail to detect that a library changed. Miller’s paper “Recursive Make Considered Harmful” (1998) explains this. Non-recursive make uses a single top-level Makefile that includes fragment.mkfiles from subdirectories. It sees the full graph, enables correct parallel builds (-j), and is faster — but harder to organise in large projects.
References
{:.gc-ref}
References
| Resource | Link |
|---|---|
| GNU Make Manual | gnu.org/software/make/manual |
man 1 make |
make manual page |
| “Recursive Make Considered Harmful” | P. Miller, 1998 |
| Makefile tutorial | makefiletutorial.com |