BUILD: CMake Flow Control — Conditionals, Loops, and Generator Expressions

CMake's flow control constructs — if/elseif/else, foreach, while, and generator expressions — transform CMakeLists.txt from a static build description into a dynamic, programmable configuration system. Mastering these constructs is essential for cross-platform builds and advanced project configurations.

CMake is not just a build description format — it is a full scripting language. Flow control constructs allow your CMake scripts to make decisions based on the target platform, build type, available libraries, and any other condition. This makes it possible to write a single CMakeLists.txt that correctly configures a project across Windows, Linux, macOS, and embedded targets.


`if` / `elseif` / `else` / `endif`

The if construct evaluates a condition and executes commands conditionally. Every if must be closed with endif():

cmake
if (WIN32)
    # Commands executed only on Windows
    message("Configuring for Windows")
elseif(UNIX AND NOT APPLE)
    # Commands executed only on Linux
    message("Configuring for Linux")
elseif(APPLE)
    # Commands executed only on macOS
    message("Configuring for macOS")
else()
    # Fallback for any other platform
    message("Unknown platform")
endif()


Logical Operators

CMake's if supports AND, OR, and NOT for compound conditions:

cmake
# AND: both conditions must be true
if(WIN32 AND MSVC)
    message("Building with MSVC on Windows")
endif()

# OR: either condition may be true
if(WIN32 OR UNIX)
    message("Building on Windows or UNIX")
endif()

# NOT: negate a condition
if(NOT WIN32)
    message("Not building on Windows")
endif()

# Compound
if((WIN32 OR UNIX) AND NOT APPLE)
    message("Windows or Linux, but not macOS")
endif()


Comparison Operators

CMake provides numeric, string, and version comparison operators:

Numeric Comparisons

cmake
set(VERSION_MAJOR 3)

if(${VERSION_MAJOR} EQUAL 3)
    message("Version major is 3")
endif()

if(${VERSION_MAJOR} LESS 5)
    message("Version major is less than 5")
endif()

if(${VERSION_MAJOR} GREATER 2)
    message("Version major is greater than 2")
endif()

if(${VERSION_MAJOR} LESS_EQUAL 3)     # CMake 3.7+
    message("Version major is <= 3")
endif()

if(${VERSION_MAJOR} GREATER_EQUAL 3)  # CMake 3.7+
    message("Version major is >= 3")
endif()

OperatorMeaning
EQUALNumeric equality
LESSNumeric less-than
GREATERNumeric greater-than
LESS_EQUALNumeric less-than-or-equal (CMake 3.7+)
GREATER_EQUALNumeric greater-than-or-equal (CMake 3.7+)

String Comparisons

cmake
set(MY_OPTION "Release")

if(${MY_OPTION} STREQUAL "Release")
    message("Building in Release mode")
endif()

if(NOT ${MY_OPTION} STREQUAL "Debug")
    message("Not a Debug build")
endif()

Checking if a Variable Is Defined

cmake
if(DEFINED MY_VAR)
    message("MY_VAR is defined: ${MY_VAR}")
else()
    message("MY_VAR is not defined")
endif()

Checking if a File or Directory Exists

cmake
if(EXISTS "${CMAKE_SOURCE_DIR}/optional_module")
    message("Optional module found, including it")
    add_subdirectory(optional_module)
endif()

if(IS_DIRECTORY "${CMAKE_SOURCE_DIR}/src")
    message("src/ directory exists")
endif()


`foreach` Loop

The foreach command iterates over a list of items. Every foreach must be closed with endforeach():

cmake
# Iterate over an explicit list
foreach(item alpha beta gamma)
    message("Item: ${item}")
endforeach()
# Item: alpha
# Item: beta
# Item: gamma

foreach Over a Variable List

cmake
set(SOURCES main.cpp utils.cpp parser.cpp)

foreach(src ${SOURCES})
    message("Source file: ${src}")
endforeach()

foreach with a Numeric Range

cmake
# Range: 0 to 4
foreach(i RANGE 4)
    message("i = ${i}")
endforeach()
# i = 0, i = 1, i = 2, i = 3, i = 4

# Range: start, stop, step
foreach(i RANGE 2 10 2)
    message("i = ${i}")
endforeach()
# i = 2, i = 4, i = 6, i = 8, i = 10

foreach with ZIP_LISTS (CMake 3.17+)

cmake
set(targets lib_a lib_b lib_c)
set(sources a.cpp b.cpp c.cpp)

foreach(target source IN ZIP_LISTS targets sources)
    add_library(${target} STATIC ${source})
endforeach()


`while` Loop

The while command repeats a block as long as a condition is true:

cmake
set(counter 0)

while(${counter} LESS 5)
    message("counter = ${counter}")
    math(EXPR counter "${counter} + 1")
endwhile()
# counter = 0
# counter = 1
# counter = 2
# counter = 3
# counter = 4

The math(EXPR ...) command performs arithmetic on CMake variables:

cmake
math(EXPR result "10 + 5 * 2")   # result = 20
math(EXPR result "10 % 3")        # result = 1
math(EXPR result "1 << 4")        # result = 16


`break` and `continue`

Both foreach and while support early loop control:

cmake
foreach(i RANGE 10)
    if(${i} EQUAL 3)
        continue()  # skip i = 3
    endif()
    if(${i} EQUAL 7)
        break()     # stop at i = 7
    endif()
    message("i = ${i}")
endforeach()
# i = 0, 1, 2, 4, 5, 6


Generator Expressions

Generator expressions are a unique and powerful feature of CMake. Unlike regular if statements which are evaluated during the configure step (when cmake runs), generator expressions are evaluated during the generate step — just before the native build files are written.

Generator expressions cannot be printed with `message()` because they are not evaluated during configure time.

Basic Syntax

cmake
$<condition:true_value>
$<IF:condition,true_value,false_value>

Common Generator Expressions

cmake
# Use DEBUG_FLAG only in Debug builds
target_compile_definitions(myapp PRIVATE
    $<$<CONFIG:Debug>:DEBUG_MODE>)
# Adds -DDEBUG_MODE only for Debug builds

# Use NDEBUG in Release builds
target_compile_definitions(myapp PRIVATE
    $<$<CONFIG:Release>:NDEBUG>)

# Target-type based
$<$<STREQUAL:$<TARGET_PROPERTY:mylib,TYPE>,SHARED_LIBRARY>:BUILDING_DLL>

# Platform based
target_compile_options(myapp PRIVATE
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
    $<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra>)

text
Configure-time vs Generate-time:

cmake runs   ->  CMakeLists.txt processed  ->  Variables and if/foreach evaluated
                                            ->  Native build files generated
                                                (generator expressions evaluated HERE)

When to Use Generator Expressions vs if/else

SituationUse
Different source files per platformif(WIN32) ... endif()
Different compile flags per build typeGenerator expression
Setting properties per targetGenerator expression
Checking if a library was foundif(TARGET ...)

Generator Expression Reference

A few commonly used expressions:

cmake
$<CONFIG:cfg>             # True if build type matches cfg
$<PLATFORM_ID:platform>   # True if platform matches
$<CXX_COMPILER_ID:id>     # True if compiler ID matches
$<TARGET_EXISTS:target>   # True if target was defined
$<TARGET_FILE:target>     # Full path to the target's output file
$<BUILD_INTERFACE:...>    # Include value only for build tree
$<INSTALL_INTERFACE:...>  # Include value only for installed tree


Practical Example: Multi-Platform Configuration

cmake
cmake_minimum_required(VERSION 3.20)
project(CrossPlatform CXX)

add_executable(myapp src/main.cpp)

# Platform-specific source files
if(WIN32)
    target_sources(myapp PRIVATE src/platform/windows.cpp)
    target_compile_definitions(myapp PRIVATE WIN32_LEAN_AND_MEAN)
elseif(UNIX AND NOT APPLE)
    target_sources(myapp PRIVATE src/platform/linux.cpp)
    target_link_libraries(myapp PRIVATE pthread)
elseif(APPLE)
    target_sources(myapp PRIVATE src/platform/macos.cpp)
endif()

# Compiler-specific warning flags (generator expression)
target_compile_options(myapp PRIVATE
    $<$<CXX_COMPILER_ID:MSVC>:/W4 /WX>
    $<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra -Werror>)

# Debug-only definitions (generator expression)
target_compile_definitions(myapp PRIVATE
    $<$<CONFIG:Debug>:ENABLE_LOGGING>
    $<$<CONFIG:Release>:NDEBUG>)

# Iterate over a list of sub-modules
set(MODULES core network ui)
foreach(mod ${MODULES})
    if(EXISTS "${CMAKE_SOURCE_DIR}/modules/${mod}")
        add_subdirectory("modules/${mod}")
        target_link_libraries(myapp PRIVATE ${mod}_lib)
    else()
        message(WARNING "Module ${mod} not found, skipping")
    endif()
endforeach()


Conclusion

CMake's flow control constructs transform build scripts from static declarations into dynamic programs. The if/elseif/else construct enables platform-specific configurations. The foreach loop simplifies repetitive operations across lists of targets or files. Generator expressions provide deferred evaluation for settings that must vary by build type or configuration without re-running CMake.

Together, these constructs enable a single CMakeLists.txt to correctly configure complex, multi-platform, multi-configuration C and C++ projects.