Variables and Macros
Summary: in this tutorial, you will learn master makefile variables — simple vs recursive assignment, automatic variables, standard conventions, and environment variable interaction.
Variables and Macros
In the previous tutorial, you hardcoded everything — the compiler name (gcc), flags, and file names appeared repeatedly throughout the Makefile. Variables fix this by letting you define values once and reuse them everywhere.
What you'll learn in this tutorial:
- How to define and use variables in Makefiles
- The difference between
:=(simple) and=(recursive) assignment - What automatic variables are (
$@,$<,$^) and how they save you from repetition - Standard variable names that the Make community expects
- How environment variables interact with Makefile variables
- How to override variables from the command line
Your First Variable
Variables are defined with NAME = value or NAME := value and referenced with $(NAME):
CC := gcc
CFLAGS := -Wall -Wextra
hello: hello.c
$(CC) $(CFLAGS) hello.c -o helloWhen Make runs, it replaces $(CC) with gcc and $(CFLAGS) with -Wall -Wextra, so the actual command becomes:
gcc -Wall -Wextra hello.c -o helloWhat are -Wall and -Wextra? These are compiler flags that enable extra warnings. -Wall turns on "all common warnings" and -Wextra enables even more. They help you catch mistakes in your code at compile time. You don't need to memorize these — just know that CFLAGS is where you put compiler options.
Why Use Variables?
- Change once, update everywhere. Want to switch from
gcctoclang? Change one line (CC := clang) instead of editing every rule. - Readability.
$(CC) $(CFLAGS) $< -o $@is easier to understand at a glance than a long hardcoded command, once you learn the conventions. - Flexibility. Users can override variables from the command line:
make CC=clanguses clang without editing the Makefile.
Simple vs Recursive Assignment
Make has two types of variable assignment, and the difference matters:
Simple Assignment (:=)
CC := gccThe value is set immediately when Make reads this line. Like a snapshot — whatever the value is right now, that's what CC becomes.
Recursive Assignment (=)
CC = gccThe value is not evaluated until the variable is used. It's like a formula that gets recalculated every time.
Why Does This Matter?
Most of the time it doesn't! But here's a case where it makes a difference:
# With recursive (=) — this WORKS
A = $(B)
B = hello
# When $(A) is used later, it expands to "hello" because B is defined by then
# With simple (:=) — this gives an EMPTY value for A
A := $(B)
B := hello
# A is set to "" because B wasn't defined yet when A was assignedRule of thumb: Use := (simple) by default. It's more predictable and slightly faster. Only use = (recursive) when you intentionally need a variable to reference other variables that might be defined later.
Other Assignment Operators
| Operator | Name | What It Does |
|---|---|---|
= | Recursive | Value is evaluated each time the variable is used |
:= | Simple | Value is evaluated immediately when defined |
+= | Append | Adds to an existing variable (with a space separator) |
?= | Conditional | Sets the value only if the variable is not already defined |
CC := gcc # Set CC to "gcc"
CFLAGS := -Wall # Set CFLAGS to "-Wall"
CFLAGS += -Wextra # Now CFLAGS is "-Wall -Wextra"
CFLAGS += -O2 # Now CFLAGS is "-Wall -Wextra -O2"
DEBUG ?= 0 # Set DEBUG to "0" only if not already setThe ?= operator is especially useful for providing defaults that users can override:
PREFIX ?= /usr/local # Default install location, user can overrideAutomatic Variables
Automatic variables are special variables that Make sets automatically for each rule. They refer to parts of the current rule — the target, the prerequisites, etc.
Here are the ones you'll use most often:
| Variable | What It Contains | Memory Trick |
|---|---|---|
$@ | The target file name | "at the target" (the goal) |
$< | The first prerequisite | "the first thing that comes in" |
$^ | All prerequisites (no duplicates) | "all of them (caret up)" |
$? | Prerequisites that are newer than the target | "what changed?" |
$* | The stem of a pattern rule (the % part) | "the wildcard match" |
Let's see how these clean up a real Makefile. Here's the version without automatic variables:
myapp: main.o utils.o
gcc main.o utils.o -o myapp
main.o: main.c
gcc -c main.c -o main.o
utils.o: utils.c
gcc -c utils.c -o utils.oAnd here's the same Makefile with automatic variables:
CC := gcc
CFLAGS := -Wall
myapp: main.o utils.o
$(CC) $(CFLAGS) $^ -o $@
main.o: main.c
$(CC) $(CFLAGS) -c $< -o $@
utils.o: utils.c
$(CC) $(CFLAGS) -c $< -o $@Let's trace through how Make expands the first rule:
$^(all prerequisites) →main.o utils.o$@(the target) →myapp- Result:
gcc -Wall main.o utils.o -o myapp
And the second rule:
$<(first prerequisite) →main.c$@(the target) →main.o- Result:
gcc -Wall -c main.c -o main.o
Why use $< instead of $^ for compilation? When compiling a single .c file, you only want to pass that one file to the compiler — not the header files listed as prerequisites. $< gives you just the first prerequisite (the .c file), while $^ would give you everything (including headers, which would cause errors).
Directory and File Name Variables
These extract parts of a file path:
| Variable | What It Contains | Example (if $@ is build/main.o) |
|---|---|---|
$(@D) | The directory part of $@ | build |
$(@F) | The file name part of $@ | main.o |
$(<D) | The directory part of $< | (directory of first prerequisite) |
$(<F) | The file name part of $< | (file name of first prerequisite) |
Standard Variable Names
The GNU Make community has conventions for variable names. Using standard names makes your Makefiles familiar to other developers:
Compiler and Flags
| Variable | Convention | Example |
|---|---|---|
CC | C compiler | gcc, clang |
CXX | C++ compiler | g++, clang++ |
CFLAGS | C compiler flags | -Wall -Wextra -O2 |
CXXFLAGS | C++ compiler flags | -Wall -std=c++17 |
LDFLAGS | Linker flags (library paths) | -L/usr/local/lib |
LDLIBS | Libraries to link | -lm -lpthread |
CPPFLAGS | C preprocessor flags | -DDEBUG -I./include |
What are linker flags and libraries? When your program uses external code (like a math library), the linker needs to know where to find it. LDFLAGS tells the linker where to look (-L/path/to/libs), and LDLIBS tells it which libraries to include (-lm means "link the math library"). Don't worry about memorizing these — you'll pick them up naturally as you build more complex projects.
Installation and Paths
| Variable | Convention | Typical Default |
|---|---|---|
PREFIX | Installation root | /usr/local |
BINDIR | Where executables go | $(PREFIX)/bin |
LIBDIR | Where libraries go | $(PREFIX)/lib |
INCLUDEDIR | Where headers go | $(PREFIX)/include |
DESTDIR | Staging directory for packaging | (empty) |
Environment Variables
Make automatically imports your shell's environment variables. You can use them in your Makefile:
# Use the HOME environment variable
install:
cp myapp $(HOME)/bin/Variable Precedence
When the same variable name exists in multiple places, Make follows this priority order (highest to lowest):
- Command line —
make CFLAGS="-O3"always wins - Makefile — Variables defined in the Makefile
- Environment — Your shell's environment variables
# Command-line variables override everything:
make CC=clang CFLAGS="-O3"The override Directive
If you want a Makefile variable to override even command-line values, use override:
override CFLAGS += -Wall # This -Wall is always present, even if user sets CFLAGSThis is rarely used — normally you want users to be able to override things.
Practical Example: Configurable Build
Here's a Makefile that uses everything you've learned:
# --- Configuration (users can override these) ---
CC := gcc
CFLAGS := -Wall -Wextra
PREFIX ?= /usr/local
DEBUG ?= 0
# --- Conditional: add debug flags if DEBUG=1 ---
ifeq ($(DEBUG), 1)
CFLAGS += -g -DDEBUG
else
CFLAGS += -O2
endif
# --- Project files ---
SRCS := main.c utils.c
OBJS := $(SRCS:.c=.o)
TARGET := myapp
# --- Rules ---
.PHONY: all clean install
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
install: $(TARGET)
install -m 755 $(TARGET) $(PREFIX)/bin/
clean:
rm -f $(TARGET) $(OBJS)What's new here?
$(SRCS:.c=.o)is a substitution reference — it takesmain.c utils.cand replaces.cwith.o, givingmain.o utils.o. We'll cover this more in the pattern rules tutorial.%.o: %.cis a pattern rule — the%matches any name, so this one rule handles compiling every.cfile. We'll cover this in detail in the next tutorial.ifeqis a conditional — we'll cover this in the conditionals tutorial. For now, just know it checks ifDEBUGequals1.
Users can customize the build like this:
make # Normal build with defaults
make DEBUG=1 # Build with debug symbols
make CC=clang # Build with clang instead of gcc
make PREFIX=$HOME/.local # Install to home directoryMulti-Line Variables
For longer values (like help text), use define and endef:
define HELP_TEXT
Usage: make [target]
Targets:
all Build the project
clean Remove build files
install Install to $(PREFIX)
help Show this message
endef
help:
@echo "$(HELP_TEXT)"Summary
Here's what you've learned:
- Variables are defined with
NAME := valueand used with$(NAME) :=(simple) evaluates immediately;=(recursive) evaluates when used+=appends to a variable;?=sets only if not already defined- Automatic variables:
$@(target),$<(first prerequisite),$^(all prerequisites) - Standard names:
CC,CFLAGS,LDFLAGS,LDLIBS,PREFIX— use these conventions - Precedence: command line overrides Makefile, which overrides environment variables
$(SRCS:.c=.o)performs substitution (replace.cwith.o)
In the next tutorial, you'll learn about pattern rules and wildcards — which eliminate the need to write a separate rule for every single file.
Create a Flexible Build Configuration
Write a Makefile for a project with main.c, math.c, and string.c that:
- Uses variables for the compiler, flags, source files, object files, and target name
- Uses automatic variables (
$@,$<,$^) in all rules - Has a
DEBUGvariable that adds-gflag when set to1 - Has a
cleantarget - Allows the user to override the compiler with
make CC=clang
Show Solution
# Configuration
CC := gcc
CFLAGS := -Wall -Wextra
DEBUG ?= 0
TARGET := myapp
# Source and object files
SRCS := main.c math.c string.c
OBJS := $(SRCS:.c=.o)
# Debug mode
ifeq ($(DEBUG), 1)
CFLAGS += -g -DDEBUG
$(info Building in DEBUG mode)
else
CFLAGS += -O2
endif
.PHONY: all clean
all: $(TARGET)
# Link all object files into the final program
# $^ = all prerequisites (main.o math.o string.o)
# $@ = the target (myapp)
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $^ -o $@
# Compile each .c file into a .o file
# $< = the first prerequisite (the .c file)
# $@ = the target (the .o file)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(TARGET) $(OBJS)Test it:
make # Build with gcc, optimized
make DEBUG=1 # Build with debug symbols
make CC=clang # Build with clang
make clean # Remove build filesKey takeaways:
$(SRCS:.c=.o)automatically generates the list of object files from source files$@,$<, and$^eliminate hardcoded file names in recipes?=provides a default forDEBUGthat users can override$(info ...)prints a message during Makefile parsing (useful for debugging)
Written by the ShellRAG Team
The ShellRAG editorial team writes practical, beginner-friendly Makefile tutorials with tested code examples and real-world use cases. Every article is technically reviewed for accuracy and updated regularly.
Learn more about us →