Conditionals
Summary: in this tutorial, you will learn learn to use ifdef, ifeq, and other conditionals to make your makefiles adapt to different platforms, build modes, and configurations.
Conditionals
Sometimes your Makefile needs to behave differently depending on the situation — debug vs release mode, Linux vs macOS, whether a tool is installed or not. Conditionals let you include or exclude parts of your Makefile based on these conditions.
What you'll learn in this tutorial:
- How to use
ifdef/ifndefto check if a variable exists - How to use
ifeq/ifneqto compare variable values - How to detect the operating system (Linux, macOS, Windows)
- How to create debug/release build configurations
- The difference between Make conditionals and shell conditionals
- Common gotchas and how to avoid them
ifdef and ifndef — Check if a Variable Exists
The simplest conditional checks whether a variable has been defined:
ifdef DEBUG
CFLAGS += -g -DDEBUG
$(info Debug mode enabled)
endifAnd the opposite — ifndef checks if a variable is not defined:
ifndef CC
CC := gcc
endifThe Syntax Pattern
All Make conditionals follow this structure:
ifdef VARIABLE_NAME
# Lines included when condition is TRUE
else
# Lines included when condition is FALSE (optional)
endifCritical gotcha with ifdef: It checks if the variable has any value at all — even a space counts! It does NOT check if the value is "true" or "1" or "yes."
# This might surprise you:
FOO := # FOO is defined but EMPTY
ifdef FOO
$(info FOO is defined) # This does NOT print!
endif
FOO := 0 # FOO is defined and has value "0"
ifdef FOO
$(info FOO is defined) # This DOES print! (0 is not empty)
endifIf you want to check for specific values like 1 or true, use ifeq instead.
ifeq and ifneq — Compare Values
ifeq checks if two values are equal:
ifeq ($(DEBUG),1)
CFLAGS += -g -O0 -DDEBUG
else
CFLAGS += -O2
endififneq checks if two values are not equal:
ifneq ($(CC),gcc)
$(info Using a non-GCC compiler: $(CC))
endifSyntax Options
There are two ways to write the comparison:
# Parentheses style (most common):
ifeq ($(VAR),value)
# Quote style (useful when values contain parentheses):
ifeq "$(VAR)" "value"Both work the same way. Use whichever you find more readable.
Watch out for trailing spaces! Make does not strip spaces from values when comparing. If $(DEBUG) is "1 " (with a trailing space), then ifeq ($(DEBUG),1) is false because "1 " ≠ "1".
If this bites you, use $(strip ...):
ifeq ($(strip $(DEBUG)),1)
CFLAGS += -g
endifDetecting the Operating System
A very common use case — running different commands on Linux, macOS, and Windows:
# Detect the OS
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
OS := linux
OPEN_CMD := xdg-open
INSTALL_CMD := sudo apt install
endif
ifeq ($(UNAME_S),Darwin)
OS := macos
OPEN_CMD := open
INSTALL_CMD := brew install
endif
# Windows (MSYS/Git Bash/WSL detection)
ifeq ($(OS),Windows_NT)
OS := windows
OPEN_CMD := start
INSTALL_CMD := choco install
endifWhat is uname -s? It's a command that prints the operating system name. On Linux it returns Linux, on macOS it returns Darwin (macOS's Unix name). On Windows, uname doesn't exist natively, but the OS environment variable is set to Windows_NT — so we check that separately.
Setting Platform-Specific Flags
UNAME_S := $(shell uname -s)
# Common flags for all platforms
CFLAGS := -Wall -Wextra
# Platform-specific additions
ifeq ($(UNAME_S),Linux)
LDLIBS += -lrt -lpthread
endif
ifeq ($(UNAME_S),Darwin)
CFLAGS += -mmacosx-version-min=11.0
LDFLAGS += -framework CoreFoundation
endifDebug vs Release Builds
Here's a common pattern for switching between debug and release modes:
# Default to release mode
BUILD ?= release
ifeq ($(BUILD),debug)
CFLAGS := -Wall -Wextra -g -O0 -DDEBUG
LDFLAGS :=
BUILD_DIR := build/debug
else ifeq ($(BUILD),release)
CFLAGS := -Wall -Wextra -O2 -DNDEBUG
LDFLAGS := -s
BUILD_DIR := build/release
else
$(error Unknown BUILD mode: $(BUILD). Use 'debug' or 'release')
endif
TARGET := $(BUILD_DIR)/myapp
SRCS := $(wildcard src/*.c)
OBJS := $(patsubst src/%.c,$(BUILD_DIR)/%.o,$(SRCS))
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) | $(BUILD_DIR)
$(CC) $(LDFLAGS) $^ -o $@
$(BUILD_DIR)/%.o: src/%.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
clean:
rm -rf buildUsage:
make # Release build (default)
make BUILD=debug # Debug build
make BUILD=release # Explicit release build
make BUILD=potato # Error: Unknown BUILD modeWhat do these flags mean?
-g— Include debug information (so debuggers like GDB can show source code)-O0— No optimization (makes debugging easier because code runs in the order you wrote it)-O2— Optimize for speed (production builds)-DDEBUG— Define theDEBUGsymbol in your C code (so#ifdef DEBUGblocks run)-DNDEBUG— DefineNDEBUGwhich disablesassert()statements-s— Strip debug symbols from the final binary (smaller file size)
Feature Toggles
You can use conditionals to enable or disable optional features:
# Optional features (users set these on the command line)
WITH_SSL ?= 0
WITH_LOGGING ?= 1
# Start with base flags
CFLAGS := -Wall -Wextra
LDLIBS :=
# SSL support
ifeq ($(WITH_SSL),1)
CFLAGS += -DWITH_SSL
LDLIBS += -lssl -lcrypto
SRCS += src/ssl_handler.c
$(info SSL support: enabled)
else
$(info SSL support: disabled)
endif
# Logging support
ifeq ($(WITH_LOGGING),1)
CFLAGS += -DWITH_LOGGING
SRCS += src/logger.c
$(info Logging: enabled)
else
$(info Logging: disabled)
endifUsage:
make # Default: no SSL, with logging
make WITH_SSL=1 # Enable SSL
make WITH_SSL=1 WITH_LOGGING=0 # SSL on, logging offChecking if a Command Exists
Before using a tool, you can check if it's installed:
# Check if a program exists
HAS_CLANG := $(shell command -v clang 2>/dev/null)
HAS_VALGRIND := $(shell command -v valgrind 2>/dev/null)
# Use clang if available, otherwise gcc
ifdef HAS_CLANG
CC := clang
else
CC := gcc
endif
# Only define the memcheck target if valgrind is installed
ifdef HAS_VALGRIND
memcheck: $(TARGET)
valgrind --leak-check=full ./$(TARGET)
else
memcheck:
@echo "valgrind is not installed. Install it with: sudo apt install valgrind"
endifWhat is command -v? It's a shell command that checks if a program exists and prints its path. If the program isn't found, it produces no output. The 2>/dev/null redirects any error messages to nowhere (so they don't show up in your Makefile). The result is: if the program exists, the variable has a value; if not, it's empty.
Make Conditionals vs Shell Conditionals
This is a common source of confusion. Make has two kinds of conditionals, and they work differently:
Make Conditionals (Run When Makefile is Read)
ifeq ($(DEBUG),1)
CFLAGS += -g
endifThese are processed when Make reads the Makefile, before any rules run. They control which lines of the Makefile are included.
Shell Conditionals (Run When a Rule Executes)
install:
@if [ -d /usr/local/bin ]; then \
cp myapp /usr/local/bin/; \
echo "Installed to /usr/local/bin"; \
else \
echo "Error: /usr/local/bin does not exist"; \
exit 1; \
fiThese are shell if statements that run inside a recipe when the rule executes. They're regular bash/shell commands.
Key differences:
| Make Conditional | Shell Conditional | |
|---|---|---|
| When it runs | When Makefile is parsed | When the rule's recipe executes |
| Where it goes | At the top level of the Makefile | Inside a rule's recipe (indented with tab) |
| Syntax | ifeq, ifdef, endif | if [ ... ]; then ... fi |
| What it controls | Which Makefile lines are active | What commands run during the rule |
| Line continuation | Not needed | Need backslash \ at end of each line |
Shell Conditional Gotcha: Each Line is a Separate Shell
Remember from the first tutorial — each line in a recipe runs in a separate shell. So you can't split a shell if across multiple lines without backslash continuation:
# WRONG — each line runs in a separate shell
install:
if [ -d /usr/local/bin ]; then
cp myapp /usr/local/bin/ # This line doesn't know about the "if"!
fi
# RIGHT — use backslashes to continue on one logical line
install:
@if [ -d /usr/local/bin ]; then \
cp myapp /usr/local/bin/; \
echo "Installed"; \
fiNesting Conditionals
You can nest conditionals, but don't go too deep — it gets hard to read:
ifdef WITH_SSL
ifeq ($(SSL_LIB),openssl)
LDLIBS += -lssl -lcrypto
else ifeq ($(SSL_LIB),mbedtls)
LDLIBS += -lmbedtls -lmbedcrypto -lmbedx509
else
$(error Unknown SSL_LIB: $(SSL_LIB). Use 'openssl' or 'mbedtls')
endif
endifelse ifeq on one line works just like else if in other languages. It's cleaner than nesting a new ifeq inside an else block.
Practical Example: Cross-Platform Makefile
Here's a complete Makefile that adapts to the platform and build configuration:
# === Platform Detection ===
UNAME_S := $(shell uname -s 2>/dev/null || echo Windows)
ifeq ($(UNAME_S),Linux)
PLATFORM := linux
EXE :=
RM := rm -f
MKDIR := mkdir -p
endif
ifeq ($(UNAME_S),Darwin)
PLATFORM := macos
EXE :=
RM := rm -f
MKDIR := mkdir -p
endif
ifeq ($(UNAME_S),Windows)
PLATFORM := windows
EXE := .exe
RM := del /Q
MKDIR := mkdir
endif
# === Build Configuration ===
BUILD ?= release
CC := gcc
TARGET := build/$(PLATFORM)/myapp$(EXE)
ifeq ($(BUILD),debug)
CFLAGS := -Wall -Wextra -g -O0 -DDEBUG
else
CFLAGS := -Wall -Wextra -O2
endif
# === Source Files ===
SRCS := $(wildcard src/*.c)
OBJS := $(patsubst src/%.c,build/$(PLATFORM)/%.o,$(SRCS))
# === Rules ===
.PHONY: all clean info
all: $(TARGET)
@echo "Built $(TARGET) for $(PLATFORM) ($(BUILD) mode)"
$(TARGET): $(OBJS) | build/$(PLATFORM)
$(CC) $(CFLAGS) $^ -o $@
build/$(PLATFORM)/%.o: src/%.c | build/$(PLATFORM)
$(CC) $(CFLAGS) -c $< -o $@
build/$(PLATFORM):
$(MKDIR) build/$(PLATFORM)
clean:
$(RM) build/$(PLATFORM)/*
info:
@echo "Platform: $(PLATFORM)"
@echo "Build: $(BUILD)"
@echo "Compiler: $(CC)"
@echo "CFLAGS: $(CFLAGS)"
@echo "Target: $(TARGET)"
@echo "Sources: $(SRCS)"Summary
Here's what you've learned:
ifdef/ifndefcheck if a variable has any value (even0counts as defined!)ifeq/ifneqcompare two values for equality- OS detection uses
$(shell uname -s)for Linux/macOS and$(OS)for Windows - Debug/release builds switch flags with
ifeq ($(BUILD),debug)and aBUILDvariable - Feature toggles use
?= 0defaults withifeq ($(WITH_FEATURE),1) - Make conditionals run at parse time; shell conditionals run in recipes
- Use
$(strip ...)when whitespace might cause comparison failures - Shell
ifblocks in recipes need backslash\continuation
In the next tutorial, you'll learn how to organize Makefiles for projects that span multiple directories.
Create a Configurable Cross-Platform Makefile
Write a Makefile that:
- Detects the OS (Linux, macOS, or Windows)
- Supports
BUILD=debugandBUILD=release(default: release) - Has an optional
WITH_TESTSflag that adds test source files when set to1 - Uses
$(error ...)if an unknownBUILDvalue is given - Has an
infotarget that displays all configuration
Show Solution
# === Platform Detection ===
UNAME_S := $(shell uname -s 2>/dev/null || echo Windows)
ifeq ($(UNAME_S),Linux)
PLATFORM := linux
EXE_EXT :=
endif
ifeq ($(UNAME_S),Darwin)
PLATFORM := macos
EXE_EXT :=
endif
ifneq (,$(findstring Windows,$(UNAME_S)))
PLATFORM := windows
EXE_EXT := .exe
endif
ifndef PLATFORM
$(warning Unknown platform: $(UNAME_S), defaulting to linux)
PLATFORM := linux
EXE_EXT :=
endif
# === Build Mode ===
BUILD ?= release
ifeq ($(BUILD),debug)
CFLAGS := -Wall -Wextra -g -O0 -DDEBUG
else ifeq ($(BUILD),release)
CFLAGS := -Wall -Wextra -O2 -DNDEBUG
else
$(error Unknown BUILD mode '$(BUILD)'. Use 'debug' or 'release')
endif
# === Configuration ===
CC := gcc
WITH_TESTS ?= 0
BUILD_DIR := build/$(PLATFORM)-$(BUILD)
TARGET := $(BUILD_DIR)/myapp$(EXE_EXT)
# === Source Files ===
SRCS := $(wildcard src/*.c)
ifeq ($(WITH_TESTS),1)
SRCS += $(wildcard tests/*.c)
CFLAGS += -DWITH_TESTS
$(info Tests: enabled)
else
$(info Tests: disabled)
endif
OBJS := $(patsubst %.c,$(BUILD_DIR)/%.o,$(SRCS))
# === Rules ===
.PHONY: all clean info
all: $(TARGET)
@echo "Built $(TARGET)"
$(TARGET): $(OBJS)
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) $^ -o $@
$(BUILD_DIR)/%.o: %.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -rf build
info:
@echo "=== Configuration ==="
@echo "Platform: $(PLATFORM)"
@echo "Build: $(BUILD)"
@echo "Compiler: $(CC)"
@echo "CFLAGS: $(CFLAGS)"
@echo "Tests: $(WITH_TESTS)"
@echo "Target: $(TARGET)"
@echo "Sources: $(SRCS)"
@echo "Objects: $(OBJS)"Test it:
make info # Show configuration
make # Release build
make BUILD=debug # Debug build
make BUILD=debug WITH_TESTS=1 # Debug build with tests
make BUILD=potato # Error messageKey points:
$(findstring Windows,$(UNAME_S))handles Windows detection more robustlyifndef PLATFORMwith$(warning)provides a safe fallbackelse ifeqchains multiple conditions cleanly$(error ...)stops Make with a clear message for invalid inputBUILD_DIRincludes both platform and mode for separate output directories
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 →