Module 2: Conditional and Portable Configuration
Conditionals in Make
Learning objectives
- Explain the core mental model behind Conditionals in Make
- Apply Conditionals in Make within Conditional and Portable Configuration
- Identify important boundaries, trade-offs, and failure modes
- Produce concrete evidence from the practice exercise
Related: Variables | Functions | Make Basics | Make Index
Why Conditionals Exist in Make
A single Makefile often needs to handle multiple scenarios: debug vs release builds, different operating systems, presence or absence of optional tools. Conditionals let you adapt behavior without separate Makefiles or complex shell scripting:
ifeq ($(DEBUG),1)
CFLAGS := -g -O0 -DDEBUG
else
CFLAGS := -O2 -DNDEBUG
endifMake conditionals are evaluated at parse time (when the Makefile is read), not when rules execute. This means they control which variables are set and which rules are defined — not which shell commands run inside a recipe.
Analogy: Make conditionals are like #ifdef in C — they control what code the compiler sees. They're a preprocessing step, not a runtime branch.
ifeq / ifneq
Test whether two values are equal (or not equal):
# Syntax variants:
ifeq (arg1,arg2)
ifeq "arg1" "arg2"
ifeq 'arg1' 'arg2'
# Simple debug flag
ifeq ($(DEBUG),1)
CFLAGS := -g -O0
else
CFLAGS := -O2
endif
# Test against empty string
ifeq ($(VERBOSE),)
Q := @ # quiet mode — prefix commands with @
else
Q := # verbose mode — no suppression
endif
# Use Q in rules:
%.o: %.c
$(Q)$(CC) $(CFLAGS) -c -o $@ $<# ifneq — not equal
ifneq ($(OS),Windows_NT)
SHELL_EXT :=
else
SHELL_EXT := .exe
endififdef / ifndef
Test whether a variable is defined (not just whether it's empty):
# ifdef — true if variable has any value (even empty after stripping)
ifdef CROSS_COMPILE
CC := $(CROSS_COMPILE)gcc
AR := $(CROSS_COMPILE)ar
endif
# ifndef — true if variable is NOT defined
ifndef PREFIX
PREFIX := /usr/local
endif
# Same effect as: PREFIX ?= /usr/localImportant distinction: ifdef X is true if X is defined (was assigned, even to empty string). ifeq ($(X),) is true if X is empty (could be undefined OR defined as "").
X := # defined but empty
ifdef X
$(info X is defined) # THIS PRINTS — X is defined (even though empty)
endif
ifeq ($(X),)
$(info X is empty) # ALSO PRINTS
endifelse and else ifeq Chaining
OS := $(shell uname -s)
ifeq ($(OS),Linux)
PLATFORM := linux
LIBS += -lpthread
else ifeq ($(OS),Darwin)
PLATFORM := macos
LIBS += -framework CoreFoundation
else ifeq ($(OS),FreeBSD)
PLATFORM := freebsd
LIBS += -lpthread
else
$(error Unsupported OS: $(OS))
endif
$(info Building for platform: $(PLATFORM))Conditionals Inside Rules — Shell-Level Branching
Make conditionals are parse-time; they can't react to things determined at recipe execution time (like whether a command succeeded). For runtime branching inside recipes, use shell conditionals:
install:
# Shell if — this runs when the recipe executes
if [ -d $(PREFIX)/bin ]; then \
echo "$(PREFIX)/bin exists"; \
else \
mkdir -p $(PREFIX)/bin; \
fi
install -m 755 $(TARGET) $(PREFIX)/bin/# Portable check: only copy if source changed
update-config:
cmp -s src/config.h include/config.h || cp src/config.h include/config.hConditionals for Cross-Platform Builds
UNAME := $(shell uname)
# Compiler selection
ifeq ($(UNAME),Darwin)
CC := clang
CFLAGS += -arch x86_64
LDFLAGS += -dead_strip
else
CC := gcc
CFLAGS += -fPIC
LDFLAGS += -Wl,--gc-sections
endif
# Windows detection (when running under MSYS2/Cygwin)
ifeq ($(OS),Windows_NT)
TARGET_EXT := .exe
RM := del /Q
else
TARGET_EXT :=
RM := rm -f
endif
TARGET := myprogram$(TARGET_EXT)Conditionals for Optional Features
# Check if a tool is available
HAVE_CLANG_FORMAT := $(shell command -v clang-format 2>/dev/null)
ifdef HAVE_CLANG_FORMAT
format:
clang-format -i $(SRCS)
else
format:
@echo "clang-format not found; skipping"
endif
# Feature flags from environment
ifdef WITH_OPENSSL
CFLAGS += -DWITH_OPENSSL $(shell pkg-config --cflags openssl)
LIBS += $(shell pkg-config --libs openssl)
endifConditionals for Verbosity Control
A very common pattern — hide commands by default, show them when V=1 or VERBOSE=1:
ifeq ($(V),1)
VERB :=
HIDE :=
else
VERB := @
HIDE := --quiet
endif
%.o: %.c
$(VERB)echo " CC $@"
$(VERB)$(CC) $(CFLAGS) -c -o $@ $<
# Without V=1: just shows " CC main.o"
# With V=1: shows the full gcc command$(if ...) — Functional Conditional
Unlike ifeq/ifdef (which are directives that control parsing), $(if ...) is a function that can be used inside variable assignments and rule context:
# $(if condition,then,else)
MODE := $(if $(DEBUG),debug,release)
FLAGS := $(if $(DEBUG),-g -O0,-O2)
# In a recipe
test:
$(if $(VERBOSE),echo "Running tests",) ./run_tests.sh$(if condition,then,else) — condition is true if non-empty; else clause is optional.
Guarding Against Missing Required Variables
# Hard stop if required variable not set
ifndef API_ENDPOINT
$(error API_ENDPOINT must be set. Usage: make API_ENDPOINT=https://...)
endif
# Soft warning
ifndef OPTIONAL_TOOL
$(warning OPTIONAL_TOOL not set; some features disabled)
endif
# Validate a variable's value
ifeq ($(filter $(BUILD_TYPE),debug release profile),)
$(error BUILD_TYPE must be 'debug', 'release', or 'profile'. Got: $(BUILD_TYPE))
endifCommon Pitfalls
# PITFALL 1: Comparing with leading/trailing spaces
ifeq ($(DEBUG), 1) # WRONG — "1" ≠ " 1"
... # use $(strip): ifeq ($(strip $(DEBUG)),1)
endif
# PITFALL 2: ifdef on a simply-expanded empty variable
TOOL :=
ifdef TOOL
$(info TOOL is defined) # PRINTS — it IS defined, even if empty
endif
# Use ifeq ($(TOOL),) to check for empty
# PITFALL 3: Conditional inside a recipe — doesn't work as expected
target:
ifeq ($(DEBUG),1) # WRONG — this is a Make directive, not a shell command
echo "debug" # Make sees this outside the recipe!
endif
# Fix: use shell if, or move the conditional outside the rule
# PITFALL 4: Case sensitivity
ifeq ($(OS),linux) # Linux uname returns "Linux" (capital L)
... # This will never match
endif
# Fix: use lowercase conversion: ifeq ($(shell echo $(OS) | tr A-Z a-z),linux)Practice lab
Create a small repository that exercises Conditionals in Make. Capture a clean build, an incremental no-op build, and a deliberately broken dependency case; explain every command Make chooses to run. Add an operational constraint such as concurrency, recovery, security, latency, or cost, and defend the resulting design trade-off.
Review questions
- What problem does Conditionals in Make solve, and what assumptions does it rely on?
- Which boundary or failure case is easiest to miss, and how would you expose it?
- What alternative design would you consider, and what trade-off would change the decision?
- What artifact, trace, test, or metric proves that your implementation is correct?
Completion evidence
- A working artifact, annotated trace, or reproducible experiment
- At least one normal case and one deliberately failing or boundary case
- A concise explanation of the design choice and its trade-offs
- Saved output showing how correctness was evaluated