Back to Technology

GNU Make Mastery Part 4: Automatic Variables & Pattern Rules

February 20, 2026 Wasil Zafar 20 min read

Eliminate repetition in your Makefiles with automatic variables ($@, $<, $^, $?, $*) and pattern rules (%.o: %.c) that scale to any number of source files automatically.

Table of Contents

  1. Automatic Variables
  2. Pattern Rules
  3. Built-in Implicit Rules
  4. Hands-On Milestone
  5. Next Steps

Automatic Variables

Part 4 of 16 — GNU Make Mastery Series. With Part 3's variable fundamentals mastered, we now tackle automatic variables and pattern rules — the features that turn a Makefile for 5 files into a Makefile that handles 500.

Automatic variables are special variables that Make sets within each recipe based on the target and prerequisites of the rule being executed. They are only valid inside recipes — not in variable assignments or prerequisites lists.

Reference

Automatic Variable Quick Reference

VariableExpands To
$@The target name
$<The first prerequisite
$^All prerequisites (no duplicates)
$+All prerequisites (with duplicates)
$?Prerequisites newer than the target
$*The stem matched by % in a pattern rule
$(@D)Directory part of $@
$(@F)File part of $@
$(<D)Directory part of $<
$(<F)File part of $<

$@ — The Target

Expands to the name of the current target. This is the most-used automatic variable.

build/myapp: main.o utils.o network.o
	$(CC) $(LDFLAGS) -o $@ $^
#                       ↑ expands to: build/myapp

$< — First Prerequisite

Expands to the first prerequisite. In a compile rule, this is the source file.

main.o: main.c main.h config.h
	$(CC) $(CFLAGS) -c $< -o $@
#   here $< = main.c  and  $@ = main.o
#   $< is the SOURCE — config.h (2nd prereq) is NOT compiled

$^ — All Prerequisites

Expands to all prerequisites, de-duplicated. Used in link rules to pass all object files.

myapp: main.o utils.o network.o crypto.o
	$(CC) $(LDFLAGS) -o $@ $^
#                         ↑ expands to: main.o utils.o network.o crypto.o

$? — Prerequisites Newer Than Target

Expands to only those prerequisites whose modification time is newer than the target. Useful for incremental archiving and partial rebuilds.

# Only re-archive object files that changed since the last ar run
libutils.a: utils.o stringops.o fileio.o
	ar rcs $@ $?     # only adds the *changed* .o files — fast incremental update

$* — Stem

In a pattern rule, $* expands to the stem — the portion matched by the % wildcard. Essential for constructing output paths.

# In pattern rule:  %.o: %.c
# When building foo.o from foo.c:
#   $*  = foo      (the stem, without extension)
#   $@  = foo.o    (the target)
#   $<  = foo.c    (the source)

build/%.o: src/%.c
	@mkdir -p $(dir $@)
	$(CC) $(CFLAGS) -c $< -o $@
#   When building build/net/tcp.o from src/net/tcp.c:
#   $* = net/tcp   (directory + stem)

Directory and File Components

Every automatic variable has companion D (directory) and F (file) variants. These are critical when targets live in subdirectories.

build/obj/main.o: src/main.c
	@echo "Dir:  $(@D)"   # → build/obj
	@echo "File: $(@F)"   # → main.o
	@mkdir -p $(@D)
	$(CC) $(CFLAGS) -c $< -o $@

Sample Source Files

Self-contained examples: Automatic-variable and pattern-rule examples compile main.c, utils.c, network.c, and foo.c. Drop these files next to your Makefile and every snippet will build cleanly.

main.c

/* main.c — calls helpers from all modules */
#include <stdio.h>
#include "utils.h"

int main(void) {
    utils_greet("Pattern Rules");
    return 0;
}

main.h

/* main.h — top-level application declarations */
#ifndef MAIN_H
#define MAIN_H

#define APP_VERSION "1.0.0"

#endif /* MAIN_H */

utils.h

/* utils.h — utility declarations */
#ifndef UTILS_H
#define UTILS_H

void utils_greet(const char *name);
int  utils_add(int a, int b);

#endif /* UTILS_H */

utils.c

/* utils.c */
#include <stdio.h>
#include "utils.h"

void utils_greet(const char *name) {
    printf("Hello, %s!\n", name);
}

int utils_add(int a, int b) { return a + b; }

network.c

/* network.c — stub network layer */
#include <stdio.h>

void net_connect(const char *host, int port) {
    printf("Connecting to %s:%d\n", host, port);
}

void net_send(const char *data) {
    printf("Sending: %s\n", data);
}

foo.c

/* foo.c — placeholder module used in pattern-rule examples */
#include <stdio.h>

void foo(void) {
    printf("foo() called\n");
}

Pattern Rules

A pattern rule uses % as a wildcard in both the target and prerequisite. Make matches any target of the right shape and substitutes the stem into the prerequisite. Write one rule to compile all your C files.

Basic Pattern Rules

CC      := gcc
CFLAGS  := -Wall -Wextra -std=c11

# Pattern rule: compile every .c → .o
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@      # $< = the .c source, $@ = the .o target

# Pattern rule: link every single-file binary
%: %.o
	$(CC) $(LDFLAGS) -o $@ $<         # $@ = binary name (stem), $< = its .o file
# With separate build directory:
BUILDDIR := build
OBJ      := $(BUILDDIR)/main.o $(BUILDDIR)/utils.o

$(BUILDDIR)/%.o: src/%.c
	@mkdir -p $(BUILDDIR)              # create build/ if it doesn't exist (@ = silent)
	$(CC) $(CFLAGS) -c $< -o $@       # $< = src/foo.c, $@ = build/foo.o

Static Pattern Rules

A static pattern rule restricts which targets the pattern applies to. This prevents Make from applying the pattern to unintended file names.

CC    := gcc
SRCS  := main.c utils.c network.c
OBJS  := $(SRCS:.c=.o)         # → main.o utils.o network.o

# Static pattern: targets : target-pattern : prereq-pattern
# ONLY apply %.c→%.o for items listed in $(OBJS)
$(OBJS): %.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@   # $< = matching .c file, $@ = matching .o
Why static over plain pattern? A plain %.o: %.c can match any .o target in the entire Makefile. A static pattern rule only applies to the named target list ($(OBJS)), making the build more predictable and catching typos in file lists.

Chained Pattern Rules

Pattern rules can chain: if Make can't find a direct prerequisite but can build it by applying rules in sequence, it will.

# Chain: .proto → .c → .o
%.c: %.proto
	protoc --c_out=. $<

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

# Make will go: message.proto → message.c → message.o automatically

Built-in Implicit Rules

GNU Make ships with a library of implicit rules for common compilation patterns (C, C++, Fortran, Yacc, Lex, etc.). These use CC, CFLAGS, CXX, CXXFLAGS from your Makefile or environment.

# You can rely on implicit rules — Make knows %.o: %.c already
CC     = gcc
CFLAGS = -Wall -O2

main.o: main.h utils.h   # only list extra headers the .c needs
# No recipe needed — Make's built-in %.o: %.c rule takes care of it
Best Practice: While relying on implicit rules is fine for toy projects, always write explicit pattern rules in production Makefiles. Implicit rules can be hard to debug, may vary between Make versions, and become invisible contributors to build failures.
# See all built-in implicit rules:
make -p | grep -A2 "^%.o"

# Disable ALL implicit rules to speed up large builds:
MAKEFLAGS += -r      # in Makefile
# or
make -r              # from CLI

Hands-On Milestone

Milestone: Write a zero-repetition Makefile using automatic variables and a static pattern rule — one that can accommodate any number of source files by only changing the SRCS variable.
# Zero-repetition Makefile — pattern rules + automatic variables

# ── Variables ────────────────────────────────────────
CC       := gcc
CFLAGS   := -Wall -Wextra -std=c11 -O2
LDFLAGS  :=

TARGET   := myapp
SRCDIR   := src
BUILDDIR := build

SRCS     := $(wildcard $(SRCDIR)/*.c)                  # auto-discover all .c files
OBJS     := $(SRCS:$(SRCDIR)/%.c=$(BUILDDIR)/%.o)       # map src/foo.c → build/foo.o

.PHONY: all clean

# ── Default target ──
all: $(TARGET)

# ── Link: combine all .o files into the binary ──
$(TARGET): $(OBJS)
	$(CC) $(LDFLAGS) -o $@ $^          # $@ = myapp, $^ = all .o files
	@echo "Linked: $@"

# ── Compile: static pattern rule with automatic variables ──
$(OBJS): $(BUILDDIR)/%.o: $(SRCDIR)/%.c
	@mkdir -p $(BUILDDIR)               # ensure build/ exists
	$(CC) $(CFLAGS) -c $< -o $@        # $< = src/foo.c, $@ = build/foo.o
	@echo "  CC  $(@F)"                  # $(@F) = filename part of $@ (e.g. foo.o)

clean:
	rm -rf $(BUILDDIR) $(TARGET)

info:
	@echo "CC       = $(CC)"
	@echo "CFLAGS   = $(CFLAGS)"
	@echo "LDFLAGS  = $(LDFLAGS)"
	@echo "TARGET   = $(TARGET)"
	@echo "SRCDIR   = $(SRCDIR)"
	@echo "BUILDDIR = $(BUILDDIR)"
	@echo "SRCS     = $(SRCS)"
	@echo "OBJS     = $(OBJS)"
# --- Setup: create source files and Makefile ---
mkdir -p src

cat > src/main.c << 'EOF'
#include <stdio.h>
#include "utils.h"

int main(void) {
    utils_greet("Pattern Rules");
    return 0;
}
EOF

cat > src/utils.h << 'EOF'
#ifndef UTILS_H
#define UTILS_H
void utils_greet(const char *name);
int  utils_add(int a, int b);
#endif
EOF

cat > src/utils.c << 'EOF'
#include <stdio.h>
#include "utils.h"
void utils_greet(const char *name) { printf("Hello, %s!\n", name); }
int  utils_add(int a, int b)       { return a + b; }
EOF

cat > src/network.c << 'EOF'
#include <stdio.h>
void net_connect(const char *host, int port) {
    printf("Connecting to %s:%d\n", host, port);
}
EOF

cat > src/foo.c << 'EOF'
#include <stdio.h>
void foo(void) { printf("foo() called\n"); }
EOF
# --- Try it ---
make info     # display all variable values
make          # compiles src/*.c → build/*.o → links myapp
make          # nothing rebuilt (up-to-date)
touch src/utils.c
make          # only recompiles utils.c and re-links
make clean    # remove build artifacts

Next in the Series

In Part 5: Built-in Functions & Make Language, we unlock the full expressive power of Make — using wildcard, patsubst, filter, foreach, call, and $(shell) to build self-configuring Makefiles that adapt to the project automatically.

Technology