6.programming
Programming in CMake
Control Flow
- CMake provides a set of all-caps keywords for control flow, particularly in
if()statements. Variables can be referenced directly or with${}. - Historically,
if()statements predate variable expansion, but as of CMake 3.1+, quoting variable references avoids potential re-expansions (policy CMP0054).
Example of if statement:
if(variable)
# True if variable is `ON`, `YES`, `TRUE`, `Y`, or any non-zero number
else()
# True if variable is `0`, `OFF`, `NO`, `FALSE`, `N`, `IGNORE`, `NOTFOUND`, `""`, or ends in `-NOTFOUND`
endif()
Quoted Variables in CMake 3.1+:
With CMake 3.1+ and CMP0054 enabled, quoted variables will not be re-expanded:
if("${variable}")
# True if variable is not "false-like"
else()
# False if variable is undefined, which expands to `""`
endif()
Common Control Flow Keywords:
-
Unary Keywords:
NOT: Negates the result.TARGET: Checks if a given name is a target.EXISTS (file): Checks if a file exists.DEFINED: Checks if a variable is defined.
-
Binary Keywords:
STREQUAL: Compares two strings for equality.AND,OR: Logical operators.MATCHES: Evaluates a regular expression match.VERSION_LESS,VERSION_LESS_EQUAL: Compares version numbers (CMake 3.7+ forVERSION_LESS_EQUAL).
-
Parentheses: You can use parentheses to group expressions for clarity and precedence:
if((variable1 AND variable2) OR NOT variable3) # Logic with grouped conditions endif()
Generator Expressions
Most CMake logic (like if statements) is executed at configure time. However, when you need logic to be evaluated at build or install time, you use generator expressions. These expressions are evaluated within target properties.
Basic Forms of Generator Expressions:
-
Informational Expressions:
- Form:
$<KEYWORD> - These provide configuration-specific information.
- Form:
-
Conditional Expressions:
- Form:
$<KEYWORD:value> KEYWORDcontrols the evaluation, andvalueis what gets evaluated.- If
KEYWORDevaluates to1, thevalueis substituted; if0, it is not. - Example of nesting expressions:
$<$<CONFIG:Debug>:--my-flag>
- Form:
Example: Debug-Specific Compile Option
To apply a compile flag only for the Debug configuration:
target_compile_options(MyTarget PRIVATE "$<$<CONFIG:Debug>:--my-flag>")
- This approach is better than using specialized
*_DEBUGvariables and works across configurations.
Why Avoid Configure-Time Values for Configurations?
Never use configure-time values for the current configuration, as multi-configuration generators (like IDEs) don't have a "current" configuration at configure time. Generator expressions should handle this dynamically at build time.
Common Uses for Generator Expressions
-
Limiting to a Specific Language:
- Use generator expressions to limit an option or property to certain languages, such as
CXX, and avoid mixing with others likeCUDA.
- Use generator expressions to limit an option or property to certain languages, such as
-
Accessing Configuration-Dependent Properties:
- Generator expressions allow access to properties like target file locations that change based on the configuration.
-
Handling Build and Install Directories:
- A common pattern for including directories during both build and install:
target_include_directories( MyTarget PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include> ) - This sets different include paths depending on whether the project is being built or installed.
- A common pattern for including directories during both build and install:
Key Considerations
- You can nest generator expressions for more complex logic.
- Variables can help simplify nested generator expressions.
- Some generator expressions accept multiple values, separated by commas.
Generator expressions are a powerful tool for making CMake logic dynamic and configuration-specific, particularly for multi-configuration builds like those in IDEs.
Macros and Functions
The primary difference between macros and functions in CMake is scope:
- Functions: Have their own scope. Variables set inside a function are local to that function unless explicitly passed to the parent scope using
PARENT_SCOPE. - Macros: Do not have a scope. Variables set in a macro are visible globally, which can lead to "leakage" of variables into other parts of the code.
Functions and Scope
When you define and call functions in CMake, variables declared or modified inside the function are local to that function by default. This means the changes made to a variable inside a function won't be reflected outside of it, unless you explicitly use PARENT_SCOPE to tell CMake that you want the variable to be modified in the parent scope (the scope where the function was called from).
PARENT_SCOPE: When you use theset()command withPARENT_SCOPE, the variable is updated in the scope of the caller (the parent function or global scope), not within the local scope of the function.
Important Notes:
- CMake functions don't return values like traditional programming languages. Instead, you manipulate variables directly by modifying their scope.
- When nesting functions, if you want a variable to be passed up multiple levels of scope, you'll need to propagate it using
PARENT_SCOPEin each function.
# Define a function that sets a variable in the parent scope
function(set_variable var_name var_value)
message(STATUS "Inside function: setting ${var_name} to ${var_value}")
set(${var_name} "${var_value}" PARENT_SCOPE)
endfunction()
# Usage
set_variable(MyVar "Hello from function!")
message(STATUS "Outside function: MyVar is now '${MyVar}'")
-- Inside function: setting MyVar to Hello from function!
-- Outside function: MyVar is now 'Hello from function!'
- Inside the Function: The variable
MyVaris set to "Hello from function!" usingPARENT_SCOPE, meaning the value is updated in the calling scope, not just inside the function. - Outside the Function: When
message(STATUS ...)is called after the function, the value ofMyVaris printed as it was set by the function.
# Define an inner function that sets a variable in the parent scope
function(inner_function var_name)
set(${var_name} "Inner Value" PARENT_SCOPE)
endfunction()
# Define an outer function that calls the inner function
function(outer_function_not_propagate var_name)
inner_function(${var_name})
endfunction()
function(outer_function_propagate_again var_name)
inner_function(${var_name})
set(${var_name} ${var_name} PARENT_SCOPE)
endfunction()
# Usage
outer_function_not_propagate(OuterVar)
message(STATUS "OuterVar is now '${OuterVar}'")
# OuterVar is now ''
outer_function_propagate_again(OuterVar)
message(STATUS "OuterVar is now '${OuterVar}'")
# OuterVar is now 'OuterVar'
Handling Named Arguments in CMake
- CMake supports named arguments, similar to the named parameters in built-in CMake functions. You can implement this feature in your own functions using the
cmake_parse_argumentscommand. - For versions of CMake older than 3.5, you need to include the
CMakeParseArgumentsmodule.
function(COMPLEX)
cmake_parse_arguments(
COMPLEX_PREFIX # Prefix for parsed arguments
"FLAG1;FLAG2" # Boolean flags (no value expected)
"ONE_VALUE;ONE_VALUE2" # Single-value arguments
"MULTI_VALUES" # Multi-value arguments
${ARGN} # Arguments passed to the function
)
message("COMPLEX_PREFIX_FLAG1=${COMPLEX_PREFIX_FLAG1}")
message("COMPLEX_PREFIX_FLAG2=${COMPLEX_PREFIX_FLAG2}")
message("COMPLEX_PREFIX_ONE_VALUE=${COMPLEX_PREFIX_ONE_VALUE}")
message("COMPLEX_PREFIX_ONE_VALUE2=${COMPLEX_PREFIX_ONE_VALUE2}")
message("COMPLEX_PREFIX_MULTI_VALUES=${COMPLEX_PREFIX_MULTI_VALUES}")
message("COMPLEX_PREFIX_UNPARSED_ARGUMENTS=${COMPLEX_PREFIX_UNPARSED_ARGUMENTS}")
endfunction()
complex(FLAG1
MULTI_VALUES some other values
ONE_VALUE value
expected_not_parsed1 expected_not_parsed2)
# COMPLEX_PREFIX_FLAG1=TRUE
# COMPLEX_PREFIX_FLAG2=FALSE
# COMPLEX_PREFIX_ONE_VALUE=value
# COMPLEX_PREFIX_ONE_VALUE2=
# COMPLEX_PREFIX_MULTI_VALUES=some;other;values
# COMPLEX_PREFIX_UNPARSED_ARGUMENTS=expected_not_parsed1;expected_not_parsed2
Additional Notes
-
Positional arguments: You can combine named arguments with positional arguments. Any remaining unparsed arguments are captured in
COMPLEX_PREFIX_UNPARSED_ARGUMENTS. -
Alternative method: The official documentation shows a method using
set()to handle lists without explicitly writing semicolons. Choose the style that best suits your needs.
configure_file
- The
configure_filecommand in CMake is used to generate files by substituting CMake variables within an input template file (usually ending with.in). - This is frequently used for configuration headers or other generated files where project-specific information (such as version numbers) needs to be included.
Example: Version Header File
Suppose you have a file Version.h.in:
// Version.h.in
#pragma once
#define MY_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define MY_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define MY_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define MY_VERSION_TWEAK @PROJECT_VERSION_TWEAK@
#define MY_VERSION "@PROJECT_VERSION@"
This file contains placeholders (@PROJECT_VERSION_MAJOR@, etc.) that will be replaced with actual values.
To generate the file during the build process:
configure_file(
"${PROJECT_SOURCE_DIR}/include/My/Version.h.in"
"${PROJECT_BINARY_DIR}/include/My/Version.h"
)
- The
configure_filecommand copiesVersion.h.into the target location and substitutes all CMake variables. - After this, include the binary directory in your build system, as this file will be generated there.
Keywords for configure_file:
@ONLY: Prevents substitution of${}syntax (useful when both${}and@syntax are present in the input file).COPY_ONLY: Skips variable substitution and acts like a simple file copy.
Example of Use with Boolean Variables
If you need to handle boolean flags in a header, CMake provides:
#cmakedefine: For#defineif the variable is true.#cmakedefine01: For#define 1if true and#define 0if false.
Reading Values from Files
CMake can also read values from a file. For example, you might want to read a version number from a header file.
Example: Reading a Version from a Header
# Regular expression to extract version from the header
set(VERSION_REGEX "#define MY_VERSION[ \t]+\"(.+)\"")
# Read the line containing the version
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/include/My/Version.hpp"
VERSION_STRING REGEX ${VERSION_REGEX})
# Extract the version string
string(REGEX REPLACE ${VERSION_REGEX} "\\1" VERSION_STRING "${VERSION_STRING}")
# Use the extracted version in the project definition
project(My LANGUAGES CXX VERSION ${VERSION_STRING})
file(STRINGS): Reads lines from a file that match a given regular expression.string(REGEX REPLACE): Extracts the matched version string using a capture group (\\1).
This method is useful when you need to synchronize version numbers between CMake and a manually maintained source file (such as a header-only library).