Command Invocations
CMake has its own scripting language, documented in the cmake-language(7) manual. Everything in a CMakeLists.txt file is a command invocation. There are no operators, no assignment syntax, and no standalone expressions — only commands with arguments.
The basic syntax of a command invocation is:
# Syntax: command_name(arg1 arg2 arg3 ...)
message("Hello, World!")
set(MY_VAR "some value")
add_executable(my_app main.cpp utils.cpp)
Key rules:
- A command name is followed immediately by an opening parenthesis
(— no space between them - Arguments are separated by whitespace (spaces, tabs, or newlines)
- The closing parenthesis
)ends the command - Commands can span multiple lines freely
# Multi-line command invocation — perfectly valid
add_executable(my_app
main.cpp
utils.cpp
logger.cpp
config.cpp
)
Argument Types
CMake has three types of arguments (see Command Arguments):
Quoted Arguments
Enclosed in double quotes. Whitespace and semicolons are preserved literally:
# Quoted arguments preserve spaces and special characters
set(GREETING "Hello, World!")
message("The path is: C:/Program Files/CMake")
message("Line one\nLine two") # \n is a newline escape
Unquoted Arguments
Not enclosed in quotes. They cannot contain whitespace, (, ), #, ", or \ unless escaped. Semicolons in unquoted arguments create list separators:
# Unquoted arguments
set(MY_VAR value_without_spaces)
set(MY_LIST a b c) # Creates a list: "a;b;c"
set(ALSO_LIST a;b;c) # Same result: "a;b;c"
message(STATUS ${MY_VAR}) # STATUS is an unquoted keyword
Bracket Arguments
Enclosed in [=[ and ]=] (with matching equals signs). No escaping or variable expansion occurs inside bracket arguments — content is taken literally:
# Bracket arguments — no escaping, no variable expansion
set(RAW_TEXT [=[
This is literal text.
${THIS_IS_NOT_EXPANDED}
Backslashes \ are literal.
Semicolons ; are literal.
Everything is preserved as-is.
]=])
message(${RAW_TEXT})
= signs can vary ([[, [=[, [==[) to avoid conflicts with content.
Case Insensitivity
Command names in CMake are case-insensitive. All of these are equivalent:
# All equivalent — command names are case-insensitive
message("hello")
MESSAGE("hello")
Message("hello")
MeSsAgE("hello")
However, the universal convention is lowercase for commands. Variable names and keywords are case-sensitive:
# Convention: lowercase commands, UPPERCASE variables and keywords
cmake_minimum_required(VERSION 3.21)
set(MY_VARIABLE "value") # MY_VARIABLE ≠ my_variable
if(CMAKE_BUILD_TYPE STREQUAL "Release") # Keywords like STREQUAL are uppercase
message(STATUS "Release build")
endif()
${MyVar} and ${MYVAR} are completely different variables. Stick to the convention: UPPER_CASE for variables, lower_case for commands.
Comments
Line Comments
A # character outside of a quoted or bracket argument starts a line comment. Everything after # on that line is ignored:
# This is a full-line comment
cmake_minimum_required(VERSION 3.21) # Inline comment
# Comments can explain complex logic
# Multiple lines of comments are common
project(MyProject LANGUAGES CXX)
set(SOURCES
main.cpp # Entry point
utils.cpp # Utility functions
config.cpp # Configuration parser
)
Bracket Comments
For multi-line comments or temporarily disabling blocks of code, use bracket comments #[[ ... ]]:
# Bracket comment — everything inside is ignored
#[=[
This entire block is a comment.
It can span multiple lines.
set(THIS_IS_IGNORED "not executed")
add_executable(also_ignored main.cpp)
]=]
# Useful for temporarily disabling code:
#[[
find_package(Boost REQUIRED)
target_link_libraries(my_app PRIVATE Boost::filesystem)
]]
# The project continues normally here
add_executable(my_app main.cpp)
#[[ ... ]] to disable it without deleting code. The number of = signs must match between opening and closing.
Variables
Variables are the primary data storage mechanism in CMake. All variables hold string values (or lists, which are semicolon-delimited strings). See cmake-language(7) Variables.
set() and unset()
The set() command creates or modifies variables. unset() removes them:
# Set a simple string variable
set(PROJECT_AUTHOR "Wasil Zafar")
set(BUILD_VERSION "2.1.0")
# Set a variable to an empty string
set(EMPTY_VAR "")
# Set a list variable (multiple values become semicolon-separated)
set(SOURCE_FILES main.cpp utils.cpp config.cpp)
# SOURCE_FILES = "main.cpp;utils.cpp;config.cpp"
# Unset (remove) a variable
unset(EMPTY_VAR)
# After unset, the variable is undefined (empty when dereferenced)
message("EMPTY_VAR = '${EMPTY_VAR}'")
Dereferencing with ${}
Variables are dereferenced (expanded) using ${variable_name} syntax. Expansion happens recursively:
# Basic dereferencing
set(NAME "CMake")
message("Hello, ${NAME}!") # Output: Hello, CMake!
# Nested/recursive dereferencing
set(VAR_NAME "GREETING")
set(GREETING "Welcome to CMake")
message("${${VAR_NAME}}") # Expands to ${GREETING} → "Welcome to CMake"
# Concatenation via dereferencing
set(PREFIX "/usr/local")
set(SUFFIX "bin")
set(FULL_PATH "${PREFIX}/${SUFFIX}")
message("Path: ${FULL_PATH}") # Output: Path: /usr/local/bin
# Undefined variables expand to empty string
message("Undefined: '${DOES_NOT_EXIST}'")
flowchart LR
A["Variable: ${MY_VAR}"] --> B{Defined?}
B -->|Yes| C[Replace with value]
B -->|No| D[Replace with empty string]
C --> E["Result string"]
D --> E
E --> F{"Contains ${...}?"}
F -->|Yes| A
F -->|No| G[Final result]
Cache Variables
Cache variables persist between CMake runs in CMakeCache.txt. They are the primary mechanism for user-configurable options (see set(CACHE)):
# Define a cache variable with type and description
set(ENABLE_TESTS ON CACHE BOOL "Enable building of tests")
set(INSTALL_PREFIX "/usr/local" CACHE PATH "Installation directory")
set(MAX_THREADS "4" CACHE STRING "Maximum thread count")
# FORCE overwrites existing cache values (use sparingly)
set(INTERNAL_VERSION "2.0" CACHE INTERNAL "Version string" FORCE)
Cache variable types:
| Type | Purpose | GUI Display |
|---|---|---|
BOOL | ON/OFF toggle | Checkbox |
STRING | Arbitrary text | Text field |
PATH | Directory path | Directory chooser |
FILEPATH | File path | File chooser |
INTERNAL | Hidden from GUI | Not shown |
Users set cache variables from the command line with -D:
# Setting cache variables from command line
cmake -S . -B build -DENABLE_TESTS=ON -DMAX_THREADS=8
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/opt/myapp
# The option() command is shorthand for BOOL cache variables
# In CMakeLists.txt:
# option(ENABLE_TESTS "Enable tests" ON)
# is equivalent to:
# set(ENABLE_TESTS ON CACHE BOOL "Enable tests")
Environment Variables
CMake can read and write environment variables using $ENV{NAME} syntax:
# Read an environment variable
message("HOME directory: $ENV{HOME}")
message("PATH: $ENV{PATH}")
# Set an environment variable (only affects CMake's subprocess environment)
set(ENV{MY_BUILD_FLAG} "enabled")
# Use environment variable in a condition
if(DEFINED ENV{CI})
message(STATUS "Running in CI environment")
set(ENABLE_SLOW_TESTS OFF)
endif()
# Common pattern: use env var as default, allow cache override
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
if(DEFINED ENV{INSTALL_DIR})
set(CMAKE_INSTALL_PREFIX "$ENV{INSTALL_DIR}" CACHE PATH "Install prefix")
endif()
endif()
set(ENV{...}) only affect processes launched by CMake during that configure run. They do not persist to the build step or the user's shell. Use cache variables for persistent configuration.
Variable Scope
CMake variables follow a directory and function scope model. Understanding scope is critical for larger projects:
flowchart TD
A[Global / Cache Scope] --> B[Top-Level Directory Scope]
B --> C[Subdirectory Scope
add_subdirectory]
B --> D[Function Scope
function call]
C --> E[Nested Subdirectory]
D --> F[Nested Function Call]
style A fill:#3B9797,color:#fff
style B fill:#16476A,color:#fff
style C fill:#132440,color:#fff
style D fill:#132440,color:#fff
# Scope demonstration
set(PARENT_VAR "I'm from the parent")
function(my_function)
# Function creates a new scope — copies parent variables
message("Inside function: ${PARENT_VAR}") # Accessible (copied)
set(PARENT_VAR "Modified in function") # Only modifies local copy
set(LOCAL_VAR "I'm local") # Only exists in this scope
endfunction()
my_function()
message("After function: ${PARENT_VAR}") # Still "I'm from the parent"
message("LOCAL_VAR: ${LOCAL_VAR}") # Empty — not visible here
# Propagating values to parent scope
function(get_version)
set(VERSION_STRING "3.2.1")
# PARENT_SCOPE makes the variable visible in the calling scope
set(VERSION_STRING "${VERSION_STRING}" PARENT_SCOPE)
endfunction()
get_version()
message("Version: ${VERSION_STRING}") # Output: Version: 3.2.1
Lists
List Basics
In CMake, a list is simply a string with semicolons as separators. There is no distinct list type — strings and lists are the same data type:
# These all create the same list: "apple;banana;cherry"
set(FRUITS apple banana cherry)
set(FRUITS "apple;banana;cherry")
set(FRUITS apple;banana;cherry)
# Verify they're identical
message("FRUITS = ${FRUITS}") # Output: FRUITS = apple;banana;cherry
# A quoted string with semicolons IS a list
set(NUMBERS "1;2;3;4;5")
# Whitespace in set() creates list elements
set(FILES main.cpp utils.cpp config.cpp)
# FILES = "main.cpp;utils.cpp;config.cpp"
# CAUTION: Quoting prevents list creation
set(NOT_A_LIST "main.cpp utils.cpp config.cpp")
# NOT_A_LIST = "main.cpp utils.cpp config.cpp" (single string with spaces)
list() Operations
The list() command provides comprehensive list manipulation:
# LENGTH — get number of elements
set(COLORS red green blue yellow)
list(LENGTH COLORS NUM_COLORS)
message("Number of colors: ${NUM_COLORS}") # Output: 4
# GET — retrieve elements by index (0-based)
set(LANGUAGES C CXX Fortran Python)
list(GET LANGUAGES 0 FIRST_LANG)
list(GET LANGUAGES -1 LAST_LANG) # Negative indices count from end
list(GET LANGUAGES 1 2 MIDDLE) # Get multiple elements
message("First: ${FIRST_LANG}") # Output: C
message("Last: ${LAST_LANG}") # Output: Python
message("Middle: ${MIDDLE}") # Output: CXX;Fortran
# APPEND — add elements to the end
set(SOURCES main.cpp)
list(APPEND SOURCES utils.cpp config.cpp)
message("SOURCES: ${SOURCES}") # Output: main.cpp;utils.cpp;config.cpp
# REMOVE_ITEM — remove specific values
set(MODULES audio video network graphics)
list(REMOVE_ITEM MODULES network)
message("MODULES: ${MODULES}") # Output: audio;video;graphics
# FILTER — keep or exclude elements matching a pattern
set(ALL_FILES main.cpp test_main.cpp utils.cpp test_utils.cpp)
list(FILTER ALL_FILES INCLUDE REGEX "^test_")
message("Test files: ${ALL_FILES}") # Output: test_main.cpp;test_utils.cpp
# Other useful operations
set(NUMBERS 5 3 1 4 2)
# SORT
list(SORT NUMBERS)
message("Sorted: ${NUMBERS}") # Output: 1;2;3;4;5
# REVERSE
list(REVERSE NUMBERS)
message("Reversed: ${NUMBERS}") # Output: 5;4;3;2;1
# FIND — returns index or -1
set(COLORS red green blue)
list(FIND COLORS "green" GREEN_IDX)
list(FIND COLORS "purple" PURPLE_IDX)
message("green at: ${GREEN_IDX}") # Output: 1
message("purple at: ${PURPLE_IDX}") # Output: -1
# REMOVE_DUPLICATES
set(ITEMS a b a c b d)
list(REMOVE_DUPLICATES ITEMS)
message("Unique: ${ITEMS}") # Output: a;b;c;d
# JOIN — combine with separator (CMake 3.12+)
set(PARTS "usr" "local" "bin")
list(JOIN PARTS "/" JOINED_PATH)
message("Path: ${JOINED_PATH}") # Output: usr/local/bin
Control Flow
CMake provides standard control flow constructs. See the if(), foreach(), and while() documentation.
if / elseif / else / endif
# Basic if/else
set(BUILD_TYPE "Release")
if(BUILD_TYPE STREQUAL "Debug")
message(STATUS "Debug build — enabling sanitizers")
elseif(BUILD_TYPE STREQUAL "Release")
message(STATUS "Release build — enabling optimizations")
elseif(BUILD_TYPE STREQUAL "RelWithDebInfo")
message(STATUS "Release with debug info")
else()
message(STATUS "Unknown build type: ${BUILD_TYPE}")
endif()
# Boolean conditions
set(ENABLE_TESTS ON)
set(HAS_BOOST FALSE)
# These are all "true": ON, YES, TRUE, Y, non-zero number
# These are all "false": OFF, NO, FALSE, N, 0, empty string, NOTFOUND, *-NOTFOUND
if(ENABLE_TESTS)
message(STATUS "Tests are enabled")
endif()
if(NOT HAS_BOOST)
message(STATUS "Boost not found — skipping Boost tests")
endif()
# Logical operators: AND, OR, NOT
if(ENABLE_TESTS AND NOT HAS_BOOST)
message(STATUS "Tests enabled but Boost unavailable")
endif()
# Check if variable is defined (not just truthy)
set(MY_VAR "")
if(DEFINED MY_VAR)
message("MY_VAR is defined (even though empty)")
endif()
if(NOT DEFINED UNDEFINED_VAR)
message("UNDEFINED_VAR does not exist")
endif()
# Check environment variables
if(DEFINED ENV{HOME})
message("HOME is set to: $ENV{HOME}")
endif()
Comparison Operators
| Operator | Type | Example |
|---|---|---|
STREQUAL | String equality | if("abc" STREQUAL "abc") |
STRLESS | String less than | if("abc" STRLESS "def") |
STRGREATER | String greater than | if("xyz" STRGREATER "abc") |
EQUAL | Numeric equality | if(42 EQUAL 42) |
LESS | Numeric less than | if(1 LESS 10) |
GREATER | Numeric greater than | if(10 GREATER 5) |
LESS_EQUAL | Numeric ≤ | if(5 LESS_EQUAL 5) |
GREATER_EQUAL | Numeric ≥ | if(10 GREATER_EQUAL 5) |
VERSION_EQUAL | Version comparison | if("3.21.0" VERSION_EQUAL "3.21") |
VERSION_LESS | Version less than | if("3.20" VERSION_LESS "3.21") |
MATCHES | Regex match | if("hello.cpp" MATCHES "\\.cpp$") |
IN_LIST | List membership | if("x" IN_LIST MY_LIST) |
# Version comparison example
set(MINIMUM_VERSION "3.21")
set(FOUND_VERSION "3.25.1")
if(FOUND_VERSION VERSION_GREATER_EQUAL MINIMUM_VERSION)
message(STATUS "Version ${FOUND_VERSION} meets minimum requirement")
endif()
# Regex matching
set(FILENAME "my_module_test.cpp")
if(FILENAME MATCHES "^.*_test\\.cpp$")
message(STATUS "${FILENAME} is a test file")
endif()
# IN_LIST check
set(SUPPORTED_PLATFORMS Linux Darwin Windows)
set(CURRENT_PLATFORM "Linux")
if(CURRENT_PLATFORM IN_LIST SUPPORTED_PLATFORMS)
message(STATUS "${CURRENT_PLATFORM} is supported")
endif()
foreach Loops
# Iterate over a list
set(SOURCES main.cpp utils.cpp config.cpp logger.cpp)
foreach(SRC IN LISTS SOURCES)
message(STATUS "Source file: ${SRC}")
endforeach()
# Iterate over inline items
foreach(LANG IN ITEMS C CXX Fortran CUDA)
message(STATUS "Language: ${LANG}")
endforeach()
# Numeric range: foreach(var RANGE stop)
foreach(i RANGE 5)
message("i = ${i}") # 0, 1, 2, 3, 4, 5 (inclusive!)
endforeach()
# Numeric range with start, stop, step
# foreach(var RANGE start stop [step])
foreach(i RANGE 0 20 5)
message("i = ${i}") # 0, 5, 10, 15, 20
endforeach()
# ZIP_LISTS — iterate multiple lists in parallel (CMake 3.17+)
set(NAMES "Alice" "Bob" "Charlie")
set(AGES "30" "25" "35")
foreach(NAME AGE IN ZIP_LISTS NAMES AGES)
message("${NAME} is ${AGE} years old")
endforeach()
# Output:
# Alice is 30 years old
# Bob is 25 years old
# Charlie is 35 years old
while Loops
# Basic while loop
set(COUNTER 0)
while(COUNTER LESS 5)
message("Counter: ${COUNTER}")
math(EXPR COUNTER "${COUNTER} + 1")
endwhile()
# break() and continue()
set(ITEMS a b c STOP d e f)
foreach(ITEM IN LISTS ITEMS)
if(ITEM STREQUAL "STOP")
break() # Exit the loop entirely
endif()
message("Processing: ${ITEM}")
endforeach()
# Output: Processing: a, Processing: b, Processing: c
# continue() skips to next iteration
set(NUMBERS 1 2 3 4 5 6 7 8 9 10)
foreach(N IN LISTS NUMBERS)
math(EXPR REMAINDER "${N} % 2")
if(REMAINDER EQUAL 0)
continue() # Skip even numbers
endif()
message("Odd: ${N}")
endforeach()
# Output: Odd: 1, Odd: 3, Odd: 5, Odd: 7, Odd: 9
flowchart TD
A[Start] --> B{if condition}
B -->|True| C[Execute block]
B -->|False| D{elseif condition}
D -->|True| E[Execute elseif block]
D -->|False| F[Execute else block]
C --> G[endif]
E --> G
F --> G
G --> H{foreach / while}
H --> I[Loop body]
I --> J{break?}
J -->|Yes| K[Exit loop]
J -->|No| L{continue?}
L -->|Yes| H
L -->|No| M[Rest of body]
M --> H
K --> N[After loop]
Functions
Functions create reusable blocks of code with their own variable scope. See function().
Defining Functions
# Basic function definition
function(print_greeting NAME)
message(STATUS "Hello, ${NAME}! Welcome to the build system.")
endfunction()
# Call the function
print_greeting("Developer")
print_greeting("CI Server")
# Function with multiple parameters
function(add_my_library LIB_NAME LIB_TYPE)
message(STATUS "Creating ${LIB_TYPE} library: ${LIB_NAME}")
# In real code you'd call add_library here
endfunction()
add_my_library(utils STATIC)
add_my_library(core SHARED)
Arguments: ARGC, ARGV, ARGN
CMake provides special variables inside functions for argument handling:
| Variable | Meaning |
|---|---|
ARGC | Total number of arguments passed |
ARGV | List of all arguments |
ARGN | List of arguments beyond the named parameters |
ARGV0, ARGV1, ... | Individual arguments by position |
# Using ARGN for variadic functions
function(print_all_args FIRST_ARG)
message(STATUS "First argument: ${FIRST_ARG}")
message(STATUS "Total arguments: ${ARGC}")
message(STATUS "All arguments: ${ARGV}")
message(STATUS "Extra arguments: ${ARGN}")
# Iterate over extra arguments
foreach(ARG IN LISTS ARGN)
message(STATUS " Extra: ${ARG}")
endforeach()
endfunction()
print_all_args(one two three four)
# Output:
# First argument: one
# Total arguments: 4
# All arguments: one;two;three;four
# Extra arguments: two;three;four
# Extra: two
# Extra: three
# Extra: four
# Practical example: function that adds sources to a target
function(target_add_sources TARGET_NAME)
# ARGN contains all the source files passed after TARGET_NAME
foreach(SOURCE IN LISTS ARGN)
message(STATUS "Adding ${SOURCE} to ${TARGET_NAME}")
endforeach()
# In real code: target_sources(${TARGET_NAME} PRIVATE ${ARGN})
endfunction()
target_add_sources(my_app main.cpp utils.cpp config.cpp)
Return Values via PARENT_SCOPE
Functions create a new scope, so to "return" a value, use PARENT_SCOPE:
# Returning a value from a function
function(calculate_sum A B RESULT_VAR)
math(EXPR SUM "${A} + ${B}")
# Set the variable in the caller's scope
set(${RESULT_VAR} ${SUM} PARENT_SCOPE)
endfunction()
calculate_sum(10 25 MY_RESULT)
message("10 + 25 = ${MY_RESULT}") # Output: 10 + 25 = 35
# Pattern: return multiple values
function(get_platform_info OS_VAR ARCH_VAR)
set(${OS_VAR} "${CMAKE_SYSTEM_NAME}" PARENT_SCOPE)
set(${ARCH_VAR} "${CMAKE_SYSTEM_PROCESSOR}" PARENT_SCOPE)
endfunction()
get_platform_info(DETECTED_OS DETECTED_ARCH)
message(STATUS "OS: ${DETECTED_OS}, Arch: ${DETECTED_ARCH}")
Understanding PARENT_SCOPE
A common pitfall: PARENT_SCOPE sets the variable in the calling scope but does not modify the local copy. After set(VAR value PARENT_SCOPE), the variable ${VAR} inside the function still holds its previous (local) value. If you need both, set it twice:
function(my_func RESULT_VAR)
set(LOCAL_VAL "computed")
set(${RESULT_VAR} "${LOCAL_VAL}" PARENT_SCOPE)
# ${RESULT_VAR} is still empty locally!
# Use LOCAL_VAL for further local work
endfunction()
Macros
Defining Macros
Macros look similar to functions but behave differently — they do not create a new scope. See macro():
# Macro definition
macro(set_output_directory DIR)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${DIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${DIR})
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${DIR})
endmacro()
# Call the macro — variables are set in the CURRENT scope (no new scope)
set_output_directory("${CMAKE_BINARY_DIR}/output")
message("Runtime dir: ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
# Macros with arguments
macro(define_option OPTION_NAME DESCRIPTION DEFAULT_VALUE)
option(${OPTION_NAME} "${DESCRIPTION}" ${DEFAULT_VALUE})
message(STATUS "Option ${OPTION_NAME} = ${${OPTION_NAME}}")
endmacro()
define_option(ENABLE_TESTS "Build tests" ON)
define_option(ENABLE_DOCS "Build documentation" OFF)
define_option(USE_ASAN "Enable AddressSanitizer" OFF)
Macros vs Functions
| Feature | function() | macro() |
|---|---|---|
| Variable scope | New scope (isolated) | Caller's scope (no isolation) |
| PARENT_SCOPE needed? | Yes, to return values | No — changes are automatic |
| ARGV/ARGC/ARGN | Real variables | String replacements (not real variables) |
| Recommended use | General purpose, complex logic | Simple wrappers, setting caller variables |
# Demonstration: macro vs function scope difference
set(X "original")
function(modify_with_function)
set(X "modified by function")
message(" Inside function: X = ${X}")
endfunction()
macro(modify_with_macro)
set(X "modified by macro")
message(" Inside macro: X = ${X}")
endmacro()
message("Before function: X = ${X}") # original
modify_with_function()
message("After function: X = ${X}") # still "original" (function has own scope)
message("Before macro: X = ${X}") # original
modify_with_macro()
message("After macro: X = ${X}") # "modified by macro" (no scope!)
${ARGN}, ${ARGC}, and ${ARGV} are string substitutions, not real variables. This means if(DEFINED ARGN) won't work as expected in a macro. Prefer functions for anything complex.
Strings and Math
The string() Command
The string() command provides extensive string manipulation:
# FIND — locate substring
set(TEXT "Hello, CMake World!")
string(FIND "${TEXT}" "CMake" POS)
message("'CMake' found at position: ${POS}") # Output: 7
# REPLACE — substitute substrings
set(PATH "/usr/local/lib/libfoo.so")
string(REPLACE "/usr/local" "/opt" NEW_PATH "${PATH}")
message("New path: ${NEW_PATH}") # Output: /opt/lib/libfoo.so
# TOUPPER / TOLOWER
set(NAME "cmake_mastery")
string(TOUPPER "${NAME}" UPPER_NAME)
string(TOLOWER "HELLO" LOWER_HELLO)
message("Upper: ${UPPER_NAME}") # Output: CMAKE_MASTERY
message("Lower: ${LOWER_HELLO}") # Output: hello
# LENGTH — get string length
set(GREETING "Hello, World!")
string(LENGTH "${GREETING}" LEN)
message("Length: ${LEN}") # Output: 13
# SUBSTRING — extract portion of string
set(VERSION "v3.21.4-rc1")
string(SUBSTRING "${VERSION}" 1 6 VER_NUMS)
message("Version numbers: ${VER_NUMS}") # Output: 3.21.4
# STRIP — remove leading/trailing whitespace
set(MESSY " hello world ")
string(STRIP "${MESSY}" CLEAN)
message("'${CLEAN}'") # Output: 'hello world'
# CONCAT and JOIN
set(PART1 "Hello")
set(PART2 "World")
string(CONCAT FULL "${PART1}" ", " "${PART2}" "!")
message("${FULL}") # Output: Hello, World!
# JOIN with separator (CMake 3.12+)
set(ITEMS "one" "two" "three")
string(JOIN " | " JOINED ${ITEMS})
message("${JOINED}") # Output: one | two | three
# CONFIGURE — variable expansion in a string template
set(PROJECT "MyApp")
set(VERSION "2.0")
string(CONFIGURE "Building @PROJECT@ version @VERSION@" OUTPUT @ONLY)
message("${OUTPUT}") # Output: Building MyApp version 2.0
math(EXPR)
The math() command evaluates arithmetic expressions:
# Basic arithmetic
math(EXPR RESULT "10 + 5")
message("10 + 5 = ${RESULT}") # Output: 15
math(EXPR RESULT "100 - 37")
message("100 - 37 = ${RESULT}") # Output: 63
math(EXPR RESULT "6 * 7")
message("6 * 7 = ${RESULT}") # Output: 42
math(EXPR RESULT "100 / 3")
message("100 / 3 = ${RESULT}") # Output: 33 (integer division)
math(EXPR RESULT "17 % 5")
message("17 % 5 = ${RESULT}") # Output: 2 (modulo)
# Using variables in expressions
set(WIDTH 1920)
set(HEIGHT 1080)
math(EXPR PIXELS "${WIDTH} * ${HEIGHT}")
message("Total pixels: ${PIXELS}") # Output: 2073600
# Bitwise operations
math(EXPR RESULT "0xFF & 0x0F") # AND
message("0xFF & 0x0F = ${RESULT}") # Output: 15
math(EXPR RESULT "0x0F | 0xF0") # OR
message("0x0F | 0xF0 = ${RESULT}") # Output: 255
math(EXPR RESULT "1 << 8") # Left shift
message("1 << 8 = ${RESULT}") # Output: 256
# Output in hex format
math(EXPR RESULT "255" OUTPUT_FORMAT HEXADECIMAL)
message("255 in hex: ${RESULT}") # Output: 0xff
Regular Expressions
CMake supports regular expressions in string(REGEX) and if(MATCHES):
# REGEX MATCH — find first match
set(TEXT "version 3.21.4 released 2023-01-15")
string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" VERSION_FOUND "${TEXT}")
message("Found version: ${VERSION_FOUND}") # Output: 3.21.4
# REGEX MATCHALL — find all matches
set(TEXT "file1.cpp file2.h file3.cpp file4.txt file5.cpp")
string(REGEX MATCHALL "[a-z0-9]+\\.cpp" CPP_FILES "${TEXT}")
message("C++ files: ${CPP_FILES}") # Output: file1.cpp;file3.cpp;file5.cpp
# REGEX REPLACE — substitute with capture groups
set(HEADER "my_module.h")
string(REGEX REPLACE "^(.+)\\.h$" "\\1.cpp" SOURCE "${HEADER}")
message("Source for ${HEADER}: ${SOURCE}") # Output: my_module.cpp
# Replace all occurrences
set(TEXT "Hello World Hello CMake Hello")
string(REGEX REPLACE "Hello" "Hi" RESULT "${TEXT}")
message("${RESULT}") # Output: Hi World Hi CMake Hi
# CMAKE_MATCH_N — capture groups from if(MATCHES)
set(VERSION_STRING "cmake-3.21.4-linux-x86_64")
if(VERSION_STRING MATCHES "cmake-([0-9]+)\\.([0-9]+)\\.([0-9]+)")
message("Major: ${CMAKE_MATCH_1}") # Output: 3
message("Minor: ${CMAKE_MATCH_2}") # Output: 21
message("Patch: ${CMAKE_MATCH_3}") # Output: 4
message("Full match: ${CMAKE_MATCH_0}") # Output: cmake-3.21.4
endif()
Properties
Properties are named values attached to specific CMake entities (targets, directories, source files, tests, etc.). They provide fine-grained control beyond what variables offer. See cmake-properties(7).
set_property and get_property
# Set a property on a target
cmake_minimum_required(VERSION 3.21)
project(PropDemo LANGUAGES CXX)
add_executable(my_app main.cpp)
# Set target properties
set_property(TARGET my_app PROPERTY CXX_STANDARD 17)
set_property(TARGET my_app PROPERTY CXX_STANDARD_REQUIRED ON)
# Shorthand for target properties
set_target_properties(my_app PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)
# Get a property value
get_property(STD_VALUE TARGET my_app PROPERTY CXX_STANDARD)
message(STATUS "C++ standard: ${STD_VALUE}") # Output: 17
# Get a directory property
get_property(DIR_DEFS DIRECTORY PROPERTY COMPILE_DEFINITIONS)
message(STATUS "Directory definitions: ${DIR_DEFS}")
# Check if a property is set
get_property(HAS_STD TARGET my_app PROPERTY CXX_STANDARD SET)
if(HAS_STD)
message(STATUS "CXX_STANDARD is explicitly set")
endif()
Property Scopes
| Scope | Keyword | Example Properties |
|---|---|---|
| Global | GLOBAL | RULE_LAUNCH_COMPILE, ALLOW_DUPLICATE_CUSTOM_TARGETS |
| Directory | DIRECTORY | COMPILE_DEFINITIONS, INCLUDE_DIRECTORIES |
| Target | TARGET | CXX_STANDARD, OUTPUT_NAME, POSITION_INDEPENDENT_CODE |
| Source File | SOURCE | COMPILE_FLAGS, LANGUAGE |
| Test | TEST | TIMEOUT, LABELS, ENVIRONMENT |
| Cache Entry | CACHE | TYPE, HELPSTRING, ADVANCED |
# Set a global property
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
# Set a directory property
set_property(DIRECTORY PROPERTY VS_STARTUP_PROJECT my_app)
# Set a source file property
set_property(SOURCE main.cpp PROPERTY COMPILE_FLAGS "-Wall -Wextra")
# Define and use a custom property
define_property(TARGET PROPERTY MY_CUSTOM_PROP
BRIEF_DOCS "A custom property for demonstration"
FULL_DOCS "This property stores a custom value on targets"
)
set_property(TARGET my_app PROPERTY MY_CUSTOM_PROP "custom_value")
Message and Debugging
Message Modes
The message() command outputs text during configuration. Different modes control severity and formatting:
# STATUS — informational, prefixed with "--"
message(STATUS "Configuring project version 2.0")
# Output: -- Configuring project version 2.0
# No mode — important messages (no prefix)
message("This is an important message")
# Output: This is an important message
# WARNING — non-fatal warning, continues processing
message(WARNING "Deprecated feature used — will be removed in v3.0")
# Output: CMake Warning at CMakeLists.txt:5 (message):
# Deprecated feature used — will be removed in v3.0
# AUTHOR_WARNING — warning for developers (suppressible with -Wno-dev)
message(AUTHOR_WARNING "TODO: Replace this hardcoded path")
# SEND_ERROR — error, continues processing but generation fails
message(SEND_ERROR "Required library not found")
# Processing continues, but no build files are generated
# FATAL_ERROR — stops processing immediately
message(FATAL_ERROR "Unsupported platform: ${CMAKE_SYSTEM_NAME}")
# CMake halts — nothing after this executes
| Mode | Behavior | Use Case |
|---|---|---|
STATUS | Prints with -- prefix | Progress, informational output |
VERBOSE | Shown only with --log-level=VERBOSE | Detailed debugging info |
DEBUG | Shown only with --log-level=DEBUG | Developer internals |
TRACE | Shown only with --log-level=TRACE | Low-level tracing |
WARNING | Warning message, continues | Deprecated usage, potential issues |
AUTHOR_WARNING | Suppressible with -Wno-dev | Notes for CMake authors |
SEND_ERROR | Error, continues but won't generate | Missing requirements, bad config |
FATAL_ERROR | Stops CMake immediately | Unrecoverable errors |
Debugging Tips
# Print all variables in the current scope (useful for debugging)
get_cmake_property(ALL_VARS VARIABLES)
foreach(VAR IN LISTS ALL_VARS)
message(STATUS "${VAR} = ${${VAR}}")
endforeach()
# Trace a specific variable — print its value with context
function(debug_var VAR_NAME)
if(DEFINED ${VAR_NAME})
message(STATUS "[DEBUG] ${VAR_NAME} = '${${VAR_NAME}}'")
else()
message(STATUS "[DEBUG] ${VAR_NAME} is UNDEFINED")
endif()
endfunction()
set(MY_PATH "/usr/local/lib")
debug_var(MY_PATH)
debug_var(NONEXISTENT)
# Use cmake --trace for full command tracing
# Run from command line:
# cmake -S . -B build --trace # Trace ALL commands
# cmake -S . -B build --trace-expand # Trace with variable expansion shown
# cmake -S . -B build --trace-source=CMakeLists.txt # Trace specific file only
# CMakePrintHelpers module — convenient debugging functions
include(CMakePrintHelpers)
set(MY_LIST a b c d)
set(MY_VAR "Hello")
cmake_print_variables(MY_LIST MY_VAR CMAKE_VERSION)
# Output:
# -- MY_LIST="a;b;c;d"
# -- MY_VAR="Hello"
# -- CMAKE_VERSION="3.28.1"
cmake_print_properties(TARGETS my_app PROPERTIES CXX_STANDARD SOURCES)
# Output:
# -- Properties for TARGET my_app:
# my_app.CXX_STANDARD = "17"
# my_app.SOURCES = "main.cpp"
cmake --trace-expand --trace-redirect=trace.log -S . -B build to dump a complete trace to a file. You can then search the log to understand exactly how variables are being expanded and which code paths are taken.
Exercises
Variable and List Practice
Create a CMakeLists.txt that:
- Defines a list of 5 source file names
- Uses
list(FILTER)to extract only.cppfiles - Uses
list(LENGTH)to print the count - Defines a cache variable for the project version
- Prints all values using
message(STATUS)
Run with cmake -P script.cmake (script mode — no project required).
Platform Detection Script
Write a CMake script that:
- Uses
if()to detect the operating system viaCMAKE_SYSTEM_NAME - Sets a
PLATFORM_LIBSlist based on the platform (e.g.,pthreadon Linux,ws2_32on Windows) - Uses
foreachto iterate and print each library - Uses
string(TOUPPER)to create a define name from the platform
Reusable Utility Function
Create a function called add_versioned_library that:
- Takes a library name and version string as parameters
- Parses the version string using
string(REGEX)into major, minor, patch - Prints the parsed components
- Returns the major version to the caller using
PARENT_SCOPE
Test it with: add_versioned_library(mylib "4.12.3")
Scope Difference Experiment
Create a CMake script that demonstrates the scope difference between macros and functions:
- Set a variable
COUNTERto 0 - Write a function that increments it (observe it doesn't persist)
- Write a macro that increments it (observe it persists)
- Call each three times and print
COUNTERafter each call
This clearly demonstrates why macros "leak" their variable changes.
Conclusion & Next Steps
You now have a comprehensive understanding of CMake's scripting language. The key concepts covered:
- Commands — Everything is a command invocation; case-insensitive names, three argument types
- Variables — String values with
${}dereferencing; normal, cache, and environment scopes - Lists — Semicolon-delimited strings with rich manipulation via
list() - Control flow —
if/elseif/else,foreach,while, withbreakandcontinue - Functions — Scoped reusable blocks with
PARENT_SCOPEfor return values - Macros — Scope-less text substitution (use sparingly)
- Strings & math — Comprehensive manipulation with regex support
- Properties — Fine-grained metadata on targets, directories, and sources
- Messages — Graded output from STATUS to FATAL_ERROR