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 hello

When Make runs, it replaces $(CC) with gcc and $(CFLAGS) with -Wall -Wextra, so the actual command becomes:

gcc -Wall -Wextra hello.c -o hello

What 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?

  1. Change once, update everywhere. Want to switch from gcc to clang? Change one line (CC := clang) instead of editing every rule.
  2. Readability. $(CC) $(CFLAGS) $< -o $@ is easier to understand at a glance than a long hardcoded command, once you learn the conventions.
  3. Flexibility. Users can override variables from the command line: make CC=clang uses clang without editing the Makefile.

Simple vs Recursive Assignment

Make has two types of variable assignment, and the difference matters:

Simple Assignment (:=)

CC := gcc

The 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 = gcc

The 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 assigned

Rule 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

OperatorNameWhat It Does
=RecursiveValue is evaluated each time the variable is used
:=SimpleValue is evaluated immediately when defined
+=AppendAdds to an existing variable (with a space separator)
?=ConditionalSets 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 set

The ?= operator is especially useful for providing defaults that users can override:

PREFIX ?= /usr/local     # Default install location, user can override

Automatic 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:

VariableWhat It ContainsMemory 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.o

And 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:

VariableWhat It ContainsExample (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

VariableConventionExample
CCC compilergcc, clang
CXXC++ compilerg++, clang++
CFLAGSC compiler flags-Wall -Wextra -O2
CXXFLAGSC++ compiler flags-Wall -std=c++17
LDFLAGSLinker flags (library paths)-L/usr/local/lib
LDLIBSLibraries to link-lm -lpthread
CPPFLAGSC 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

VariableConventionTypical Default
PREFIXInstallation root/usr/local
BINDIRWhere executables go$(PREFIX)/bin
LIBDIRWhere libraries go$(PREFIX)/lib
INCLUDEDIRWhere headers go$(PREFIX)/include
DESTDIRStaging 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):

  1. Command linemake CFLAGS="-O3" always wins
  2. Makefile — Variables defined in the Makefile
  3. 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 CFLAGS

This 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 takes main.c utils.c and replaces .c with .o, giving main.o utils.o. We'll cover this more in the pattern rules tutorial.
  • %.o: %.c is a pattern rule — the % matches any name, so this one rule handles compiling every .c file. We'll cover this in detail in the next tutorial.
  • ifeq is a conditional — we'll cover this in the conditionals tutorial. For now, just know it checks if DEBUG equals 1.

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 directory

Multi-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 := value and 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 .c with .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:

  1. Uses variables for the compiler, flags, source files, object files, and target name
  2. Uses automatic variables ($@, $<, $^) in all rules
  3. Has a DEBUG variable that adds -g flag when set to 1
  4. Has a clean target
  5. 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 files

Key takeaways:

  • $(SRCS:.c=.o) automatically generates the list of object files from source files
  • $@, $<, and $^ eliminate hardcoded file names in recipes
  • ?= provides a default for DEBUG that users can override
  • $(info ...) prints a message during Makefile parsing (useful for debugging)
Was this page helpful?
SR

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 →