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():
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:
# 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
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()
| Operator | Meaning |
|---|---|
EQUAL | Numeric equality |
LESS | Numeric less-than |
GREATER | Numeric greater-than |
LESS_EQUAL | Numeric less-than-or-equal (CMake 3.7+) |
GREATER_EQUAL | Numeric greater-than-or-equal (CMake 3.7+) |
String Comparisons
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
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
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():
# 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
set(SOURCES main.cpp utils.cpp parser.cpp)
foreach(src ${SOURCES})
message("Source file: ${src}")
endforeach()
foreach with a Numeric Range
# 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+)
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:
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:
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:
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
$<condition:true_value>
$<IF:condition,true_value,false_value>
Common Generator Expressions
# 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>)
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
| Situation | Use |
|---|---|
| Different source files per platform | if(WIN32) ... endif() |
| Different compile flags per build type | Generator expression |
| Setting properties per target | Generator expression |
| Checking if a library was found | if(TARGET ...) |
Generator Expression Reference
A few commonly used expressions:
$<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_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.