Pattern Rules, Wildcards, and Implicit Rules

Lesson 5: Pattern Rules, Wildcards, and Implicit Rules

Writing one rule per file is madness. Pattern rules let you write one rule that matches every file of a given shape โ€” and Make even ships a library of implicit rules for common languages so you often don't need to write recipes at all.

Pattern Rules: One Rule, Infinite Files

A pattern rule uses % as a wildcard that matches any (possibly empty) string โ€” the stem:

%.o: %.c
	$(CC) $(CFLAGS) -c {{LESSON_CONTENT}}lt; -o $@

This says: "any .o file can be built from the corresponding .c file." When Make needs util.o, it matches the pattern with stem util, finds util.c, and runs the recipe with $@ = util.o, {{LESSON_CONTENT}}lt; = util.c, $* = util.

The stem is available as $* โ€” handy when a recipe needs the name without the extension:

%.html: %.md
	pandoc {{LESSON_CONTENT}}lt; -o $@ --metadata title="$*"

Wildcards and the $(wildcard) Function

Shell wildcards (*, ?, [...]) work in targets and prerequisites โ€” but not the way you'd expect. They're expanded by the shell inside recipes, while in the rest of the Makefile they need the $(wildcard ...) function:

# Works: wildcard function expands at parse time
SRCS := $(wildcard src/*.c)
OBJS := $(SRCS:.c=.o)

# The classic bug: this often expands to nothing in prerequisites
# app: src/*.o    โ† DON'T write this

$(SRCS:.c=.o) is substitution reference โ€” a shorthand that replaces .c with .o in every word of SRCS. The result: a list of object files that always matches your sources, automatically.

A Realistic "Auto-Growing" Makefile

Add a new .c file and this Makefile just works โ€” no edits needed:

SRCS := $(wildcard src/*.c)
OBJS := $(SRCS:.c=.o)

app: $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c {{LESSON_CONTENT}}lt; -o $@

clean:
	rm -f app $(OBJS)

Implicit Rules: Make's Built-in Library

Make knows how to build common file types out of the box. The most famous: a .o from a .c. In fact, the pattern rule above is nearly identical to Make's built-in:

# built-in: uses CC, CFLAGS, CPPFLAGS, TARGET_ARCH
%.o: %.c
	$(CC) $(CPPFLAGS) $(CFLAGS) -c {{LESSON_CONTENT}}lt; -o $@

Because of that, you can often omit recipes entirely:

app: main.o util.o
	$(CC) $(CFLAGS) -o $@ $^

# no %.o: %.c rule needed โ€” Make's implicit rule handles it

Built-in implicit rules cover .c โ†’ .o, .cpp โ†’ .o, .c โ†’ executable, .tex โ†’ .pdf, .y โ†’ .c (yacc), .l โ†’ .c (lex), and more. See them all with make -p.

Key idea: implicit rules are just pattern rules that ship with Make, driven by convention-named variables (CC, CFLAGSโ€ฆ). That's why naming your variables CC and CFLAGS matters โ€” you plug into free behavior.

Automatic Dependency Generation (the Header Problem Solved)

Lesson 4's "missing dependency" bug has a clean fix. Compilers can emit a dependency file (.d) listing every header a source includes:

DEPS := $(OBJS:.o=.d)

%.o: %.c
	$(CC) $(CFLAGS) -MMD -MP -c {{LESSON_CONTENT}}lt; -o $@

# pull generated .d files in as extra prerequisites
-include $(DEPS)
  • -MMD โ€” write a .d file next to each .o, listing included headers.
  • -MP โ€” add phony targets for missing headers, avoiding errors during cleanup.
  • -include โ€” include the .d files if they exist, without erroring on first build.

Now edit any header and Make recompiles every object that includes it โ€” precisely, automatically, no hand-maintained lists. This single pattern is the difference between a hobby Makefile and a professional one.

Other Graph Tools: VPATH, .DEFAULT_GOAL, .DEFAULT

  • VPATH = src include โ€” directories Make searches for prerequisites it can't find in the current directory. Great for keeping sources separate from build output.
  • .DEFAULT_GOAL := app โ€” explicitly choose the default target instead of relying on "first rule wins."
  • .DEFAULT: โ€” a rule run when Make needs a target with no other matching rule. Usually a debugging aid.
VPATH = src
app: main.o
	$(CC) -o $@ $^

# Make finds src/main.c via VPATH and builds main.o in the current dir
Watch out: wildcards in prerequisites only expand if the files exist when Make parses the file. New files added later (e.g. by a codegen step) won't be picked up mid-build โ€” regenerate the list or restart Make. This is why the "two-pass Makefile" pattern exists in advanced projects.

๐Ÿง  Knowledge Check

1. In the pattern rule %.o: %.c building util.o, what does $* expand to?

2. What does SRCS := $(wildcard src/*.c) do?

3. Why does -include $(DEPS) use -include instead of plain include?

Further Reading