Click the blue textFollow the author
1. Introduction
So far, the CMake examples we have encountered follow a straightforward process: starting from a series of source files, ultimately building into a single executable program or generating static/dynamic libraries.
This linear process is simple and easy to understand, but often proves inadequate in more complex projects.
Building a project is not just about compiling and linking source files; it also requires consideration of various configuration options, dependencies, and platform differences. These complex requirements need to be communicated to the build system in some form.
To truly gain complete control over project building, configuration, compilation, and linking, CMake provides a powerful language. This article focuses on exploring the conditional structures in CMake language: <span>if-else-elseif-endif</span>
, <span>foreach endforeach</span>
, and <span>while-endwhile</span>
, injecting more flexibility into the build process.
For a deeper understanding of various aspects of CMake language, including control structures, CMake-specific commands, and the infrastructure for modular extension of the language, refer to the official documentation: https://cmake.org/cmake/help/latest/manual/cmake-language.7.html
2. CMake Conditional Statements
Conditional statements are key tools in CMake for controlling the build process, allowing different operations to be executed based on different conditions. The basic syntax structure is as follows:
if(condition)
# Code executed when condition is true
elseif(another_condition)
# Code executed when condition is false and another_condition is true
else()
# Code executed when all conditions are false
endif()
It is mandatory to have <span>endif()</span>
to terminate the <span>if</span>
statement.
2.1. Comparison Operators
CMake provides a rich set of comparison operators for evaluating different conditions.
String Comparison Operators:
Operator | Description | Case Sensitive |
---|---|---|
<span>EQUALS</span> or <span>==</span> |
Determines if two strings are equal | Yes |
<span>STRLESS</span> |
Determines if a string is less than another string | Yes |
<span>STRGREATER</span> |
Determines if a string is greater than another string | Yes |
<span>STREQUAL</span> |
Determines if two strings are equal | No |
<span>STRLESS</span> |
Determines if a string is less than another string | No |
<span>STRGREATER</span> |
Determines if a string is greater than another string | No |
Numeric Comparison Operators:
Operator | Description |
---|---|
<span>LESS</span> |
Determines if a number is less than another number |
<span>GREATER</span> |
Determines if a number is greater than another number |
<span>EQUAL</span> |
Determines if a number is equal to another number |
<span>LESS_EQUAL</span> |
Determines if a number is less than or equal to another number |
<span>GREATER_EQUAL</span> |
Determines if a number is greater than or equal to another number |
Version Comparison Operators:
Operator | Description |
---|---|
<span>VERSION_LESS</span> |
Determines if a version number is less than another version number |
<span>VERSION_GREATER</span> |
Determines if a version number is greater than another version number |
<span>VERSION_EQUAL</span> |
Determines if a version number is equal to another version number |
<span>VERSION_LESS_EQUAL</span> |
Determines if a version number is less than or equal to another version number |
<span>VERSION_GREATER_EQUAL</span> |
Determines if a version number is greater than or equal to another version number |
Example:
set(MY_STRING "hello")
set(MY_NUMBER 10)
set(MY_VERSION "1.2.3")
if("${MY_STRING}" EQUALS "hello")
message("MY_STRING is equal to hello")
endif()
if(MY_NUMBER LESS 20)
message("MY_NUMBER is less than 20")
endif()
if("${MY_VERSION}" VERSION_GREATER "1.2.0")
message("MY_VERSION is greater than 1.2.0")
endif()
2.2. Logical Operators
CMake provides standard logical operators for combining multiple conditions:
<span>AND</span>
: Logical AND, the result is true only when all conditions are true.<span>OR</span>
: Logical OR, the result is true if at least one condition is true.<span>NOT</span>
: Logical NOT, negates the condition.
Example:
set(FLAG1 ON)
set(FLAG2 OFF)
if(FLAG1 AND NOT FLAG2)
message("FLAG1 is ON and FLAG2 is OFF")
endif()
if(FLAG1 OR FLAG2)
message("Either FLAG1 or FLAG2 is ON")
endif()
You can also use <span>DEFINED</span>
and <span>NOT DEFINED</span>
to check if a variable has been defined:
if(DEFINED MY_VARIABLE)
message("MY_VARIABLE is defined")
else()
message("MY_VARIABLE is not defined")
endif()
if(NOT DEFINED ANOTHER_VARIABLE)
message("ANOTHER_VARIABLE is not defined")
endif()
2.3. File and Directory Existence Checks
You can use <span>EXISTS</span>
, <span>IS_DIRECTORY</span>
, and <span>IS_FILE</span>
to check if files and directories exist:
if(EXISTS "${CMAKE_SOURCE_DIR}/CMakeLists.txt")
message("CMakeLists.txt exists")
endif()
if(IS_DIRECTORY "${CMAKE_SOURCE_DIR}/src")
message("src directory exists")
endif()
if(IS_FILE "${CMAKE_SOURCE_DIR}/README.md")
message("README.md is a file")
endif()
2.4. Usage Example
This example continues from the previous “Build Library” code, demonstrating how to flexibly switch between two different build modes:
- Library Mode: Compile
<span>message.hpp</span>
and<span>message.cpp</span>
into a library (static or dynamic), then link this library to the<span>hello-world</span>
executable. In this mode, the<span>message</span>
related code is encapsulated into an independent module, making it easier to reuse and maintain. - Direct Compilation Mode: Compile
<span>message.hpp</span>
,<span>message.cpp</span>
, and<span>helloWorld.cpp</span>
directly into a single<span>hello-world</span>
executable without generating any libraries. This mode is suitable for small projects or scenarios where dependencies need to be minimized.
<span>helloWorld.cpp</span>
code:
#include "include/message.hpp"
#include <iostream>
int main(int argc, char *argv[])
{
Message hello("Hello, CMake World!");
std::cout << hello << std::endl;
Message goodbay("Goodbye, CMake World!");
std::cout << goodbay << std::endl;
return 0;
}
<span>message.hpp</span>
code:
#ifndef MESSAGE_HPP
#define MESSAGE_HPP
#include <iostream>
#include <string>
class Message {
public:
Message(const std::string &s) : contents(s) { }
// Friend function, using constant reference and internal print method
friend std::ostream &operator<<(std::ostream &os, Message &msg) {
return msg.print(os);
}
private:
// Actual output function, const qualified to ensure no modification of object state
std::ostream &print(std::ostream &os) const;
private:
std::string contents;
};
#endif
<span>message.cpp</span>
code:
#include "../include/message.hpp"
std::ostream&
Message::print(std::ostream &os) const{
os << "This is a message: " << std::endl;
os << contents;
return os;
}
Project Directory Structure:
MyProject/
├── CMakeLists.txt (Top-level CMakeLists.txt)
├── helloWorld.cpp (Main program source file)
├── include/ (Header files)
│ └── message.hpp
└── src/ (Source files)
└── message.cpp
Switch between these two build modes through conditional checks in the <span>CMakeLists.txt</span>
file. <span>CMakeLists.txt</span>
Implementation Steps:
(1) Define the minimum required version of CMake, project name, and supported languages:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(project6 LANGUAGES CXX)
(2) Introduce the control variable <span>USE_LIBRARY</span>
, a logical variable used to control whether to compile the <span>Message</span>
related source files into a library. By default, it is set to <span>OFF</span>
, indicating that the library mode is not used.
set(USE_LIBRARY OFF)
message(STATUS "Compile sources into a library? ${USE_LIBRARY}")
(3) Set the global variable <span>BUILD_SHARED_LIBS</span>
, which controls whether the created library is static or dynamic by default. If set to <span>OFF</span>
, the <span>add_library</span>
command will build a static library unless explicitly specified. If set to <span>ON</span>
, it will build a dynamic library. Although set to OFF here, the <span>add_library</span>
command can explicitly specify the type of library using the <span>STATIC</span>
or <span>SHARED</span>
keywords, unaffected by <span>BUILD_SHARED_LIBS</span>.
set(BUILD_SHARED_LIBS OFF)
(4) Define the source file list <span>_sources</span>
, used to store the <span>Message</span>
related source files. This list will be used later when building the library or directly compiling the executable.
list(APPEND _sources Message.hpp Message.cpp)
(5) Use the <span>if-else</span>
statement to choose the build mode based on the value of <span>USE_LIBRARY</span>
:
if(USE_LIBRARY)
# add_library will create a static library
# since BUILD_SHARED_LIBS is OFF (by default)
add_library(message ${_sources})
add_executable(hello-world hello-world.cpp)
target_link_libraries(hello-world message)
else()
add_executable(hello-world hello-world.cpp ${_sources})
endif()
(6) Verify the build result (for GNU/Linux): Since <span>USE_LIBRARY</span>
defaults to <span>OFF</span>
, the direct compilation mode will be used by default. You can use the <span>objdump -x hello-world</span>
command to check the generated executable file to confirm whether the <span>Message</span>
related code has been included. If you change <span>USE_LIBRARY</span>
to <span>ON</span>
and rebuild, you can use the <span>ldd hello-world</span>
command to check whether <span>hello-world</span>
is linked to the <span>message</span>
library (dynamic library), or use the <span>objdump -x hello-world</span>
command to see if it depends on the <span>message</span>
library (static library).
Below is the complete content of <span>CMakeLists.txt</span>
:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(hello-world LANGUAGES CXX)
# Set C++ standard (optional but recommended)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set( USE_LIBRARY OFF)
message(STATUS "Compile sources into a library? ${USE_LIBRARY}")
set(BUILD_SHARED_LIBS OFF)
list(APPEND _sources include/message.hpp src/message.cpp)
# Add include directory so the compiler can find message.hpp
# include_directories(${CMAKE_SOURCE_DIR}/include)
if (USE_LIBRARY)
# Create a static library (or dynamic library)
add_library(message ${_sources})
add_executable(hello-world helloWorld.cpp)
target_link_libraries(hello-world message)
else()
# Create an executable file
add_executable(hello-world helloWorld.cpp ${_sources})
endif()
Build and run:
$ mkdir -p build
$ cd build/
$ cmake ..
-- The CXX compiler identification is GNU 11.4.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Compile sources into a library? OFF
-- Configuring done
-- Generating done
-- Build files have been written to: /home/fly/workspace/cmakeProj/recipe_06/build
$ cmake --build .
[ 33%] Building CXX object CMakeFiles/hello-world.dir/helloWorld.cpp.o
[ 66%] Building CXX object CMakeFiles/hello-world.dir/src/message.cpp.o
[100%] Linking CXX executable hello-world
[100%] Built target hello-world
$ ./hello-world
This is a message:
Hello, CMake World!
This is a message:
Goodbye, CMake World!
By modifying the value of <span>USE_LIBRARY</span>
, you can easily switch between these two build modes, flexibly adapting to different project requirements. This conditional building method is very common in CMake and can be used to handle various complex build scenarios.
2.5. Working Principle
The above example introduces two important variables: <span>USE_LIBRARY</span>
and <span>BUILD_SHARED_LIBS</span>
, both initially set to <span>OFF</span>
. Understanding these two variables and the logic behind them is crucial for mastering conditional building in CMake.
The CMake language specification defines various representations of logical true and false:
-
Logical True. A logical variable is considered true when set to any of the following values:
<span>1</span>
,<span>ON</span>
,<span>YES</span>
,<span>TRUE</span>
,<span>Y</span>
, any non-zero number. -
Logical False. A logical variable is considered false when set to any of the following values:
<span>0</span>
,<span>OFF</span>
,<span>NO</span>
,<span>FALSE</span>
,<span>N</span>
,<span>IGNORE</span>
,<span>NOTFOUND</span>
, empty string (<span>""</span>
), any string ending with<span>-NOTFOUND</span>
.
<span>USE_LIBRARY</span>
Custom Variable Purpose: To switch between two different build modes.
<span>USE_LIBRARY=ON</span>
: Compiles<span>message.hpp</span>
and<span>message.cpp</span>
into a library and links that library to the<span>hello-world</span>
executable.<span>USE_LIBRARY=OFF</span>
: Compiles<span>message.hpp</span>
,<span>message.cpp</span>
, and<span>helloWorld.cpp</span>
directly into a single<span>hello-world</span>
executable without generating any libraries.
By modifying the value of <span>USE_LIBRARY</span>
, you can control whether the build process generates and uses an independent library, thus meeting different project requirements.
<span>BUILD_SHARED_LIBS</span><span> is a global flag in CMake that directly affects the default behavior of the </span><code><span>add_library</span>
command.
- If
<span>BUILD_SHARED_LIBS</span>
is set to<span>ON</span>
, the<span>add_library</span>
command will build a dynamic library (shared library) unless explicitly specified otherwise. - If
<span>BUILD_SHARED_LIBS</span>
is set to<span>OFF</span>
or undefined, the<span>add_library</span>
command will build a static library unless explicitly specified otherwise.
<span>add_library</span>
command itself can explicitly specify the type of library using <span>STATIC</span>
, <span>SHARED</span>
, or <span>MODULE</span>
keywords, in which case the value of <span>BUILD_SHARED_LIBS</span>
will be ignored.
Conditional Control: The example demonstrates how to use the <span>if</span>
statement in CMake for conditional control, deciding which build steps to execute based on the value of variables. This allows for building different targets based on different conditions, achieving a flexible build process.
The current setup requires manual modification of the <span>CMakeLists.txt</span>
file to switch build modes, which is not convenient. We hope to configure these variables externally without modifying the build code. For example, you can override the default value set in <span>CMakeLists.txt</span>
by passing <span>-DUSE_LIBRARY=ON</span>
in the CMake command line, achieving flexible configuration. A future article will introduce how to implement this, allowing adjustments to configurations without modifying the build code.
About the parentheses in <span>else()</span>
and <span>endif()</span>
:
<span>else()</span>
and <span>endif()</span><span> parentheses exist for historical reasons, to clearly specify the scope of the </span><code><span>if</span>
statement. For example, you can use <span>if(USE_LIBRARY)...else(USE_LIBRARY)...endif(USE_LIBRARY)</span><span> to clearly indicate which </span><code><span>else</span>
and <span>endif</span>
belong to which <span>if</span>
statement.
This format is not mandatory; you can choose whether to use it. Modern CMake encourages a more concise syntax, and omitting parentheses is perfectly acceptable as long as the logic is clear. For example, <span>if(USE_LIBRARY)...else...endif()</span><span> is equally valid.</span>
Local Variable Naming: The <span>_sources</span>
variable is a local variable and should not be used outside the current scope. To remind yourself and other developers, you can prefix the variable name with an underscore <span>_</span>
, indicating that it is a local variable and should not be used elsewhere. This can improve code readability and maintainability. Similar naming conventions also apply to local variables within functions.
3. CMake Loop Statements:<span>foreach</span>
In the previous example, we learned about the <span>if-else-endif</span>
structure used to control the execution flow of CMake based on conditions. In addition to conditional statements, CMake also provides language tools for creating loop structures, including <span>foreach-endforeach</span>
and <span>while-endwhile</span>
, which allow for repeated execution of a block of code, processing lists, or satisfying specific conditions.
Both <span>foreach</span>
and <span>while</span>
loops can be combined with the <span>break</span>
command to exit the loop early when certain conditions are met. The <span>break</span>
command immediately terminates the execution of the current loop and transfers control to the next statement after the loop.
3.1. Syntax and Basic Usage
CMake provides the <span>foreach-endforeach</span>
statement to implement loop structures, allowing iteration over lists, ranges, and files.
<span>foreach</span>
statement syntax:
foreach(loop_var arg1 arg2 ...)
# Loop body
endforeach()
<span>loop_var</span>
: The loop variable, which will be assigned different values in each iteration.<span>arg1 arg2 ...</span>
: Determines the iteration of the loop, with different meanings based on different keywords (<span>IN LISTS</span>
,<span>RANGE</span>
,<span>IN ITEMS</span>
, etc.).<span>endforeach()</span>
: Marks the end of the loop. The<span>endforeach(<loop_var>)</span>
syntax was introduced in CMake 3.0 to improve readability, especially in nested loops.
Example:
foreach(WORD hello world cmake)
message("The word is: ${WORD}")
endforeach()
Output:
The word is: hello
The word is: world
The word is: cmake
Iterating Over Lists:<span>foreach(item IN LISTS my_list)</span><span>,</span><span> allows iterating over a CMake list. You can also iterate over multiple lists by merging them and iterating over them. For example:</span>
foreach(loop_var IN LISTS list1 list2 ...)
# Loop body
endforeach()
Example:
set(MY_LIST "apple;banana;cherry") # Define a CMake list
foreach(FRUIT IN LISTS MY_LIST)
message("Fruit: ${FRUIT}")
endforeach()
# Multiple lists
set(list1 "A;B;C")
set(list2 "1;2;3")
foreach(item IN LISTS list1 list2)
message("Item: ${item}")
endforeach()
Iterating Over Ranges:<span>foreach(i RANGE start stop [step])</span>
allows iterating over a numeric range.
<span>i</span>
: The loop variable, which will be assigned a number in each iteration.<span>RANGE</span>
: A keyword indicating that the iteration object is a numeric range.<span>start</span>
: The starting value of the range (inclusive).<span>stop</span>
: The ending value of the range (inclusive).<span>[step]</span>
: An optional parameter, the step size, defaulting to 1.
Iterating Over Files:<span>foreach(file IN ITEMS file1 file2 ...)</span><span> can be used in older CMake versions. However, the </span><code><span>ITEMS</span>
keyword is deprecated; it is recommended to use <span>foreach(file IN LISTS files)</span><span> or </span><code><span>glob</span>
command + <span>IN LISTS</span>
instead.
It is strongly recommended to use <span>glob</span>
command combined with <span>IN LISTS</span>
:
file(GLOB CPP_FILES "*.cpp") # Find all .cpp files in the current directory
foreach(FILE IN LISTS CPP_FILES)
message("CPP File: ${FILE}")
endforeach()
3.2. Usage Example
This example demonstrates how to use the <span>foreach</span>
loop in CMake to set different compilation options for specific source files. It will create a geometry library (calculating the area of different geometric shapes) and apply a lower optimization level to some of the source files to demonstrate the use of foreach.
The logic for calculating the area of different geometric shapes is spread across multiple independent files. The project includes multiple functions for calculating the area of geometric shapes, each with its corresponding header file (<span>.hpp</span>
) and source file (<span>.cpp</span>
). The project’s directory structure is as follows:
recipe_06_foreach/
├── CMakeLists.txt
├── helloWorld.cpp
├── include
│ ├── geometryCircle.hpp
│ ├── geometryPolygon.hpp
│ ├── geometryRhombus.hpp
│ ├── geometrySquare.hpp
│ └── message.hpp
└── src
├── geometryCircle.cpp
├── geometryPolygon.cpp
├── geometryRhombus.cpp
├── geometrySquare.cpp
└── message.cpp
The following files need to be compiled:
- 4 header files (
<span>*.hpp</span>
): These files define the interfaces for the geometric shape classes. - 5 source files (
<span>*.cpp</span>
): These files implement the specific logic for the geometric shape classes, as well as the main program logic in<span>helloWorld.cpp</span>
.
To keep the article concise, the complete code will not be listed here. The full code example can be found in the following Git repository: https://gitee.com/long-xu/cmake-learning-code/recipe_06_foreach
<span>helloWorld.cpp</span>
content example:
#include <iostream>
#include <vector>
#include "include/geometryCircle.hpp"
#include "include/geometryPolygon.hpp"
#include "include/geometryRhombus.hpp"
#include "include/geometrySquare.hpp"
int main()
{
try {
geometry::Circle circle(5.0);
std::cout << "Circle area: " << circle.getArea() << std::endl;
std::vector<std::pair<double, double>> polygon_vertices = {
{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}
};
geometry::Polygon polygon(polygon_vertices);
std::cout << "Polygon area: " << polygon.getArea() << std::endl;
geometry::Rhombus rhombus(6.0, 8.0);
std::cout << "Rhombus area: " << rhombus.getArea() << std::endl;
geometry::Square square(4.0);
std::cout << "Square area: " << square.getArea() << std::endl;
} catch (const std::exception&e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}
<span>CMakeLists.txt</span>
Detailed Steps
(1) Specify the minimum version of CMake, project name, and language:
cmake_minimum_required(VERSION 3.15 FATAL_ERROR) # It is recommended to use a newer version of CMake
project(recipe_06_foreach LANGUAGES CXX)
(2) Create the geometry library target: Note that both source files and header files need to be added to the library to be correctly processed in subsequent compilation and linking processes. Here, all source files and header files are specified directly, and you can also use the <span>file(GLOB ...)</span><span> command to automatically find them. Using explicit specification allows for better control over which files are included in the project.</span>
add_library(geometry STATIC
include/geometryCircle.hpp
include/geometryPolygon.hpp
include/geometryRhombus.hpp
include/geometrySquare.hpp
src/geometryCircle.cpp
src/geometryPolygon.cpp
src/geometryRhombus.cpp
src/geometrySquare.cpp
)
(3) Set global compilation options: The <span>target_compile_options</span>
command allows setting compilation options for a specific target (here, the <span>geometry</span>
library). The <span>PRIVATE</span>
keyword indicates that these options apply only to the compilation of the <span>geometry</span>
library and will not be passed to other targets linked to that library. Here, the <span>geometry</span>
library is set to the <span>-O3</span>
optimization level.
target_compile_options(geometry
PRIVATE -O3 # Default optimization level is -O3
)
(4) Create a list of source files that require a lower optimization level:
list(
APPEND sources_with_lower_optimization
src/geometryCircle.cpp
src/geometryRhombus.cpp
)
(5) Use the <span>foreach</span>
loop to set source file properties: This is the core part of this example, using the <span>foreach</span>
loop to iterate over each source file in the list. Inside the loop, use the <span>set_source_files_properties</span>
command to set the <span>COMPILE_FLAGS</span>
property for each source file, setting it to <span>-O2</span>
. The <span>IN LISTS</span>
syntax tells the <span>foreach</span>
command to iterate from a list.
message(STATUS "Setting source properties using IN LISTS syntax:")
foreach(_source IN LISTS sources_with_lower_optimization)
set_source_files_properties(${_source} PROPERTIES COMPILE_FLAGS "-O2")
message(STATUS "Appending -O2 flag for ${_source}")
endforeach()
(6) To ensure properties are set, verify property settings (optional): Verify whether the <span>COMPILE_FLAGS</span>
property has been successfully set for the source files. Again, use the <span>foreach</span>
loop to iterate over the <span>sources_with_lower_optimization</span>
list and use the <span>get_source_file_property</span>
command to retrieve the value of the <span>COMPILE_FLAGS</span>
property for each source file.
message(STATUS "Querying sources properties using plain syntax:")
foreach(_source ${sources_with_lower_optimization})
get_source_file_property(_flags ${_source} COMPILE_FLAGS)
message(STATUS "Source ${_source} has the following extra COMPILE_FLAGS: ${_flags}")
endforeach()
(7) Create an executable target and link the library:
add_executable(compute-areas helloWorld.cpp src/message.cpp) # Add message.cpp
target_link_libraries(compute-areas geometry)
Complete <span>CMakeLists.txt content</span>
:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) # CMake version check
project(recipe_06_foreach VERSION 0.1 LANGUAGES CXX) # Create project "recipe_06_foreach" with version 0.1 and two languages C and C++ enabled
add_library(geometry STATIC
include/geometryCircle.hpp
include/geometrySquare.hpp
include/geometryPolygon.hpp
include/geometryRhombus.hpp
src/geometryCircle.cpp
src/geometrySquare.cpp
src/geometryPolygon.cpp
src/geometryRhombus.cpp)
target_compile_options(geometry PRIVATE -O3)
list(APPEND sources_with_lower_optimization
src/geometryCircle.cpp
src/geometryRhombus.cpp)
message(STATUS "Setting source properties using IN LISTS syntax:")
foreach(_source IN LISTS sources_with_lower_optimization)
set_source_files_properties(${_source} PROPERTIES COMPILE_FLAGS "-O2")
message(STATUS "Appending -O2 flag for ${_source}")
endforeach()
message(STATUS "Querying sources properties using plain syntax:")
foreach(_source ${sources_with_lower_optimization})
get_source_file_property(_flags ${_source} COMPILE_FLAGS)
message(STATUS "Source ${_source} has the following extra COMPILE_FLAGS: ${_flags}")
endforeach()
add_executable(compute-areas helloWorld.cpp src/message.cpp) # Add message.cpp
target_link_libraries(compute-areas geometry)
Create a build directory and configure the project using CMake:
mkdir -p build
cd build
cmake ..
Output:
-- Setting source properties using IN LISTS syntax:
-- Appending -O2 flag for src/geometryCircle.cpp
-- Appending -O2 flag for src/geometryRhombus.cpp
-- Querying sources properties using plain syntax:
-- Source src/geometryCircle.cpp has the following extra COMPILE_FLAGS: -O2
-- Source src/geometryRhombus.cpp has the following extra COMPILE_FLAGS: -O2
-- Configuring done
-- Generating done
-- Build files have been written to: /home/fly/workspace/cmake-learning-code/recipe_06_foreach/build
As you can see, the <span>foreach</span>
loop successfully set the <span>-O2</span>
compilation flag.
Using CMake to build the project:
cmake --build . -- VERBOSE=1
Output:
......
[ 12%] Building CXX object CMakeFiles/geometry.dir/src/geometryCircle.cpp.o
/usr/bin/c++ -O3 -O2 -MD -MT CMakeFiles/geometry.dir/src/geometryCircle.cpp.o -MF CMakeFiles/geometry.dir/src/geometryCircle.cpp.o.d -o CMakeFiles/geometry.dir/src/geometryCircle.cpp.o -c /home/fly/workspace/cmake-learning-code/recipe_06_foreach/src/geometryCircle.cpp
[ 25%] Building CXX object CMakeFiles/geometry.dir/src/geometrySquare.cpp.o
/usr/bin/c++ -O3 -MD -MT CMakeFiles/geometry.dir/src/geometrySquare.cpp.o -MF CMakeFiles/geometry.dir/src/geometrySquare.cpp.o.d -o CMakeFiles/geometry.dir/src/geometrySquare.cpp.o -c /home/fly/workspace/cmake-learning-code/recipe_06_foreach/src/geometrySquare.cpp
[ 37%] Building CXX object CMakeFiles/geometry.dir/src/geometryPolygon.cpp.o
/usr/bin/c++ -O3 -MD -MT CMakeFiles/geometry.dir/src/geometryPolygon.cpp.o -MF CMakeFiles/geometry.dir/src/geometryPolygon.cpp.o.d -o CMakeFiles/geometry.dir/src/geometryPolygon.cpp.o -c /home/fly/workspace/cmake-learning-code/recipe_06_foreach/src/geometryPolygon.cpp
[ 50%] Building CXX object CMakeFiles/geometry.dir/src/geometryRhombus.cpp.o
/usr/bin/c++ -O3 -O2 -MD -MT CMakeFiles/geometry.dir/src/geometryRhombus.cpp.o -MF CMakeFiles/geometry.dir/src/geometryRhombus.cpp.o.d -o CMakeFiles/geometry.dir/src/geometryRhombus.cpp.o -c /home/fly/workspace/cmake-learning-code/recipe_06_foreach/src/geometryRhombus.cpp
[ 62%] Linking CXX static library libgeometry.a
......
<span>VERBOSE=1</span>
option will display detailed build information, including compiler commands. By examining the compiler commands, you can verify whether the <span>-O2</span>
flag has been added. You will find that the <span>-O2</span>
flag appears after the <span>-O3</span>
flag, indicating that the final effective optimization level is <span>-O2</span>
, as the compiler processes compilation options from left to right, with later options overriding earlier ones.
3.3. Working Principle
<span>foreach-endforeach</span><span> syntax is a powerful loop structure in CMake that allows for iterating over a list of variables, repeating specific operations for each element in the list. In the software build process, this looping capability is very useful; for example, it can be used to uniformly set compiler flags or properties for specific sets of files in a project. This example focuses on utilizing the </span><code><span>foreach-endforeach</span><span> loop to set, operate on, and retrieve specific compiler flags for a series of files, demonstrating its application in fine-grained control of the build system.</span>
In this CMake code snippet, two key commands are introduced that together achieve fine control over individual file properties:
-
<span>set_source_files_properties(<file1> <file2> ... PROPERTIES <property1> <value1> <property2> <value2> ...)</span>
: This command is used to set properties for one or more source files. It accepts one or more files as input, followed by a list of property-value pairs. Files in CMake, similar to targets, have their own properties, and through the<span>set_source_files_properties</span>
command, properties such as compiler flags and include paths can be configured for specific files, achieving fine-grained control over the build process. For example, specific compilation options can be set for a source file to lower its optimization level or enable specific warnings. A list of available properties for source files can be found in the CMake official documentation: https://cmake.org/cmake/help/v3.5/manual/cmake-properties.7.html#source-file-properties. -
<span>get_source_file_property(<VAR> <file> <property>)</span>
: This command is used to retrieve the value of a specific property for a specified source file and store that value in the specified CMake variable<span><VAR></span>
. This allows for dynamically obtaining file property information during the build process and performing subsequent operations based on that information. For example, you can first use<span>get_source_file_property</span>
to retrieve existing compiler flags for a file and then add new flags based on that.
About CMake Lists:
In CMake, a list is a collection of strings separated by semicolons (<span>;</span>
). CMake provides various ways to create and manipulate lists:
- Using the
<span>set</span>
command:<span>set(MY_LIST item1 item2 item3)</span>
creates a list named<span>MY_LIST</span>
with the value<span>item1;item2;item3</span>
. - Using the
<span>list</span>
command:<span>list(APPEND MY_LIST item1 item2)</span>
appends<span>item1</span>
and<span>item2</span>
to the end of the<span>MY_LIST</span>
list. <span>list(CREATE)</span>
command is used to create an empty list.- There are other commands for manipulating lists such as
<span>list(INSERT)</span><span>, </span><code><span>list(REMOVE_ITEM)</span><span>, </span><code><span>list(LENGTH)</span>, etc.
About CMake <span>target_compile_options</span>
command:
<span>target_compile_options</span>
is used to add compilation options for a specified build target (e.g., executable, static library, shared library, or module). These compilation options are passed to the compiler to control the code compilation process. Through <span>target_compile_options</span>
, you can flexibly configure the compilation behavior of each target, such as:
- Setting Optimization Levels:
<span>-O0</span>
(no optimization),<span>-O1</span>
,<span>-O2</span>
,<span>-O3</span>
(different levels of optimization),<span>-Os</span>
(optimize for code size) - Enabling or Disabling Warnings:
<span>-Wall</span>
(enable all common warnings),<span>-Werror</span>
(treat all warnings as errors),<span>-Wno-unused-variable</span>
(disable unused variable warnings) - Defining Preprocessor Macros:
<span>-DDEBUG</span>
(define DEBUG macro),<span>-DVERSION="${PROJECT_VERSION}"</span>
(define version macro) - Selecting C++ Standards:
<span>-std=c++11</span>
,<span>-std=c++14</span>
,<span>-std=c++17</span>
,<span>-std=c++20</span>
, etc. - Controlling Code Generation:
<span>-fPIC</span>
(generate position-independent code, required for shared libraries)
Here, a brief introduction to <span>target_compile_options</span>
is provided; a later article will detail its usage and various options.
Recommendations:
While using the <span>foreach-endforeach</span>
loop allows for flexible setting of compiler flags for individual files, there are more concise methods for the need to lower optimization levels in bulk. One recommended approach is to collect files that require special handling into a separate library (static or shared library) target, and then set the optimization level directly for that target. The benefit of this is that configuration information can be managed centrally, improving code readability and maintainability. For example:
add_library(low_optimization_lib STATIC file1.cpp file2.cpp file3.cpp)
target_compile_options(low_optimization_lib PRIVATE -O0) # Set optimization level to -O0 (no optimization)
The main goal of this example is to demonstrate the usage of the <span>foreach-endforeach</span><span> loop, so the focus is on how to use that loop and related file property commands to achieve fine control over files. This method is very useful when special configurations are needed for individual files.</span>
4. CMake <span>while</span>
Loop
<span>while</span>
loop is a basic control flow statement in CMake that allows for repeated execution of a block of code until a specified condition is no longer met. The <span>while</span>
loop is useful for performing repetitive tasks, processing data collections, or implementing complex logic control.
Syntax:
while(<condition>)
<commands>
endwhile()
<span><condition></span>
: A CMake boolean expression that, when true, will execute the loop body<span><commands></span>
. CMake boolean expressions can include variables, string comparisons, numeric comparisons, logical operators (<span>AND</span>
,<span>OR</span>
,<span>NOT</span>
), and results of other CMake commands.<span><commands></span>
: A sequence of CMake commands to be repeated.
Execution Flow:
- First, CMake checks the
<span><condition></span>
expression. - If
<span><condition></span>
is true, the<span><commands></span>
block will be executed. - After executing
<span><commands></span>
, CMake checks the<span><condition></span>
expression again. - Repeat steps 2 and 3 until the
<span><condition></span>
expression is false. - When
<span><condition></span>
is false, the loop ends, and CMake continues executing the code after<span>endwhile()</span>.
Example 1, Simple Counter Loop:
set(counter 0)
while(counter LESS 5) # counter < 5
message("Counter value: ${counter}")
math(EXPR counter "${counter} + 1")
endwhile()
message("Loop finished. Counter value: ${counter}")
<span>math(EXPR ...)</span>
command is used to perform mathematical operations, here used to increment the counter.
Example 2, Processing Elements in a List:
set(my_list "item1;item2;item3")
list(LENGTH my_list list_length)
set(index 0)
while(index LESS list_length)
list(GET my_list ${index} current_item)
message("Item at index ${index}: ${current_item}")
math(EXPR index "${index} + 1")
endwhile()
<span>list(LENGTH ...)</span>
retrieves the length of the list, and <span>list(GET ...)</span>
retrieves the element at the specified index in the list.
Example 3, Reading Data from a File Until a Specific Line is Encountered (Requires CMake 3.15+):
Although CMake is not adept at file operations, you can use <span>file(READ ...)</span>
and string operations to simulate the process of reading a file. A more recommended approach is to use external scripts (e.g., Python) for complex file operations and then call that script from CMake. The following example is only to demonstrate the <span>while</span>
loop:
set(file_content "line1\nline2\nEND_OF_DATA\nline4") # Simulate file content
string(REPLACE "\n" ";" file_lines "${file_content}") # Replace newline characters with semicolons to create a list
list(LENGTH file_lines num_lines)
set(line_index 0)
while(line_index LESS num_lines)
list(GET file_lines ${line_index} current_line)
if(current_line STREQUAL "END_OF_DATA")
message("Found END_OF_DATA. Exiting loop.")
break() # Use break command to exit the loop
endif()
message("Line ${line_index}: ${current_line}")
math(EXPR line_index "${line_index} + 1")
endwhile()
<span>break()</span>
command is used to terminate the loop early. The <span>string(REPLACE ...)</span>
command is used to replace newline characters in the string with semicolons, converting multi-line text into a CMake list. The <span>STREQUAL</span>
is a string equality comparison operator.
Notes:
- Avoid Infinite Loops: Ensure that the
<span><condition></span>
will eventually become false; otherwise, it will lead to an infinite loop, causing the CMake process to hang. - You can also use
<span>break()</span>
command to exit the loop early, even if the<span><condition></span>
is true. - Use
<span>continue()</span>
to skip the current iteration. - CMake String Comparisons: CMake string comparisons are case-sensitive.
- Variables defined within a
<span>while</span>
loop have a scope limited to that loop. - Avoid using complex loop logic in performance-critical code. Consider using more efficient methods, such as list operations or calling external scripts.
- Refer to the CMake official documentation for specific command minimum version requirements.
Comparison with Other Loop Statements:
<span>foreach</span>
Loop: The<span>foreach</span>
loop is used to iterate over elements in a list. If you need to iterate over a known set of elements, the<span>foreach</span>
loop is more convenient than the<span>while</span>
loop.<span>if</span>
Statement: The<span>if</span>
statement is used to execute conditional code blocks. Unlike the<span>while</span>
loop, the<span>if</span>
statement only performs a single condition check, not repeated execution.
5. Conclusion
The conditional statements in CMake (<span>if</span>
, <span>elseif</span>
, <span>else</span>
) and loop statements (<span>while</span>
, <span>foreach</span>
) are core elements of build scripts, determining the flexibility and controllability of the build process. Dynamically adjusting the build process based on different environments, platforms, and configurations enables cross-platform compatibility, customized builds, and automated tasks.
Mastering these techniques can significantly enhance build efficiency and simplify the management of complex projects. In the future, as project scale and complexity increase, the ability to flexibly utilize conditions and loops will become even more important, serving as a key skill in building modern software projects.
Lion Welcome to follow my public account Learn technology or contribute articles