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.
1
Build Systems Foundations
Why Make, compilation pipeline, basics
2
Targets, Prerequisites & Execution
Rule anatomy, PHONY, build workflows
3
Variables, Expansion & Scope
= vs :=, ?=, +=, CLI overrides
4
Automatic Variables & Pattern Rules
$@, $<, $^, $?, %.o: %.c patterns
You Are Here
5
Built-in Functions & Make Language
subst, wildcard, foreach, $(shell)
6
Conditionals & Configurable Builds
ifeq, ifdef, platform detection, flags
7
Automatic Dependency Generation
-M, -MM, -MD, .d files, header deps
8
Compilation Workflow & Libraries
Static/shared libs, ar, -fPIC, SONAME
9
Project Architecture & Multi-Directory
Recursive make, include, out-of-source
10
Cross-Compilation & Toolchains
Toolchain prefixes, sysroots, embedded
11
Parallel Builds & Performance
make -j, jobserver, race conditions
12
Testing, Coverage & Debug Tooling
Test targets, gcov/lcov, sanitizers
13
Make as Automation & DevOps Tool
Task runner, Docker, install, packaging
14
CI/CD Integration
Deterministic builds, GitHub Actions, cache
15
Advanced Make & Debugging
--debug, dynamic rules, evaluation traps
16
Ecosystem, Alternatives & Mastery
CMake, Ninja, Meson, Bazel, production
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
| Variable | Expands 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.
Continue the Series
Part 5: Built-in Functions & Make Language
Use wildcard, patsubst, foreach, call, and $(shell) to auto-generate source lists, filter targets, and build self-configuring Makefiles.
Read Article
Part 6: Conditionals & Configurable Builds
Use ifeq, ifdef, and OS detection to write portable, multi-configuration Makefiles for debug, release, and cross-platform builds.
Read Article
Part 3: Variables, Expansion & Scope
Review recursive vs simply-expanded variables, ?=, +=, CLI overrides, and target-specific variable scoping.
Read Article