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 / ifndef to check if a variable exists
  • How to use ifeq / ifneq to 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)
endif

And the opposite — ifndef checks if a variable is not defined:

ifndef CC
    CC := gcc
endif

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

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

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

ifneq checks if two values are not equal:

ifneq ($(CC),gcc)
    $(info Using a non-GCC compiler: $(CC))
endif

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

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

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

Debug 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 build

Usage:

make                  # Release build (default)
make BUILD=debug      # Debug build
make BUILD=release    # Explicit release build
make BUILD=potato     # Error: Unknown BUILD mode

What 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 the DEBUG symbol in your C code (so #ifdef DEBUG blocks run)
  • -DNDEBUG — Define NDEBUG which disables assert() 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)
endif

Usage:

make                         # Default: no SSL, with logging
make WITH_SSL=1              # Enable SSL
make WITH_SSL=1 WITH_LOGGING=0  # SSL on, logging off

Checking 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"
endif

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

These 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; \
	fi

These are shell if statements that run inside a recipe when the rule executes. They're regular bash/shell commands.

Key differences:

Make ConditionalShell Conditional
When it runsWhen Makefile is parsedWhen the rule's recipe executes
Where it goesAt the top level of the MakefileInside a rule's recipe (indented with tab)
Syntaxifeq, ifdef, endifif [ ... ]; then ... fi
What it controlsWhich Makefile lines are activeWhat commands run during the rule
Line continuationNot neededNeed 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"; \
	fi

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

else 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/ifndef check if a variable has any value (even 0 counts as defined!)
  • ifeq/ifneq compare 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 a BUILD variable
  • Feature toggles use ?= 0 defaults with ifeq ($(WITH_FEATURE),1)
  • Make conditionals run at parse time; shell conditionals run in recipes
  • Use $(strip ...) when whitespace might cause comparison failures
  • Shell if blocks 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:

  1. Detects the OS (Linux, macOS, or Windows)
  2. Supports BUILD=debug and BUILD=release (default: release)
  3. Has an optional WITH_TESTS flag that adds test source files when set to 1
  4. Uses $(error ...) if an unknown BUILD value is given
  5. Has an info target 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 message

Key points:

  • $(findstring Windows,$(UNAME_S)) handles Windows detection more robustly
  • ifndef PLATFORM with $(warning) provides a safe fallback
  • else ifeq chains multiple conditions cleanly
  • $(error ...) stops Make with a clear message for invalid input
  • BUILD_DIR includes both platform and mode for separate output directories
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 →