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
endif

Make 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
endif

ifdef / 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/local

Important 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
endif

else 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.h

Conditionals 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)
endif

Conditionals 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))
endif

Common 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

  1. What problem does Conditionals in Make solve, and what assumptions does it rely on?
  2. Which boundary or failure case is easiest to miss, and how would you expose it?
  3. What alternative design would you consider, and what trade-off would change the decision?
  4. 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