Source | Network
For C/C++ developers, managing projects with complex third-party dependencies can become very tricky, especially when cross-platform support is needed.
CMake, as a cross-platform build management tool, provides a mature solution for finding and including third-party dependencies, creating build systems, testing programs, and installation. By writing a single CMakeLists.txt file and executing the same command, you can create executables or link libraries on different systems. After getting familiar with CMake, I believe this compilation experience can barely match half of modern languages like Rust and Go, with the other half lacking in package management, which I won’t discuss here. Of course, if you’re just solving algorithm problems, you don’t need such a complex tool as CMake; simple use of gcc or clang will suffice.
Like C++, CMake has evolved over the years, with many improvements made to its design, resulting in significant differences compared to older versions, hence the term modern CMake. The traditional way of using CMake is still valid, but just like modern C++, the modern way of using CMake is clearer in some concepts, more user-friendly, and less error-prone.
# A simple example of a modern CMake project
cmake_minimum_required(VERSION 3.12)
project(myproj)
find_package(Poco REQUIRED COMPONENTS Net Util)
add_executable(MyEXE)
target_source(MyEXE PRIVATE "main.cpp")
target_link_library(MyEXE PRIVATE Poco::Net Poco::Util)
target_compile_definition(MyEXE PRIVATE std_cxx_14)
1. Target and Configuration Around Target
<span>target</span>
in modern CMake. The creation commands are<span>add_library()</span>
and <span>add_executable()</span>
. The types of link libraries are further divided into many kinds, with the most common being <span>SHARED</span>
and <span>STATIC</span>
. You declare them in the command by adding keywords:<span>add_library(MyLib SHARED)</span>
, where the first argument is the name of the <span>target</span>
, and subsequent configurations will need to use this name.<span>CMakeLists.txt</span>
, there can be multiple <span>target</span>
s, and most related configurations revolve around these targets. For example, specifying the source files for a <span>target</span>
:target_source(MyLib PRIVATE "main.cpp" "func.cpp")
<span>PRIVATE</span>
keyword is used to describe the “scope of application” of parameters. There are also two other possible values: <span>INTERFACE</span>
and <span>PUBLIC</span>
. These will be introduced in detail in the next section, and can be ignored for now.<span>file</span>
command to traverse and get all the source files:file(GLOB_RECURSE SRCS ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
The first argument of the command <span>GLOB_RECURSE</span>
indicates a recursive search in subfolders, the second argument <span>SRCS</span>
is the name of the variable that stores the results, and the third argument is the matching pattern for target files. After finding the matching cpp files, their paths will be saved in the SRCS variable as a string array, and can be used as follows:
target_source(MyLib PRIVATE ${SRCS})
<span>target</span>
, you usually also need to specify the header file directories:target_include_directories(MyLib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/)
Language features required at compile time:
target_compile_features(MyLib PRIVATE std_cxx_14)
And macro definitions at compile time:
target_compile_definitions(MyLib PRIVATE LogLevel=3)
If you have some parameters you want to pass directly to the underlying compiler (like gcc, clang, cl), you can use:
target_compile_options(MyLib PRIVATE -Werror -Wall -Wextra)
<span>target_source</span>
and <span>target_*</span>
are only effective for the specified target. In traditional CMake, these configurations are usually defined as global variables, such as using <span>include_directories()</span>
and <span>set_cxx_flags()</span>
. The issue with the traditional method is low flexibility; when multiple targets exist, they cannot be configured separately, leading to the accidental pollution of a target’s properties. Therefore, the modern CMake configuration method based on targets is much more manageable, similar to introducing namespaces.2. Build Specification and Usage Requirement
target_compile_features(MyLib PRIVATE cxx_std_14)
target_link_libraries(MyLib PRIVATE Boost::Format)
Here, we use PRIVATE to indicate that C++14 support is only needed at compile time, and linking to the Boost library is also only needed at compile time. However, if the header files we provide externally also use C++14, we need to use PUBLIC, changing it to:
target_compile_features(MyLib PUBLIC cxx_std_14)
target_link_libraries(MyLib PRIVATE Boost::Format)
When the library is header-only, our project does not need to be compiled separately, so there is no build specification; we can configure it with the INTERFACE modifier:
target_compile_features(MyLib INTERFACE cxx_std_14)
<span>PRIVATE</span>
, <span>INTERFACE</span>
, and <span>PUBLIC</span>
attributes?3. Finding and Using Link Libraries
<span>find_package</span>
, which works in two ways: one is based on the Config File search, and the other is based on the Find File search. When executing <span>find_package</span>
, CMake is actually looking for these two types of files and retrieves library information from them.<span>find_package(ABC)</span>
, CMake will only look for <span>ABCConfig.cmake</span>
or <span>abc-config.cmake</span>
. The default search paths for CMake depend on the platform; on Linux, the search paths include <span>/usr/lib/cmake</span>
and <span>/usr/lib/local/cmake</span>
, where you can find a lot of Config Files. Generally, when installing a library, its accompanying Config File will be placed here.<span>find_package(Abc)</span>
, if CMake cannot find the Config File, users can provide the <span>Abc_DIR</span>
variable, and CMake will search for the Config File in the path pointed to by <span>Abc_DIR</span>
.<span>find_package(ABC)</span>
, if CMake does not find the Config File, it will try to search for <span>FindABC.cmake</span>
. Find Files serve the same purpose as Config Files, but they are written by others rather than the library developers. If the library you are using does not provide a Config File, you can search online for a Find File or write one yourself and then include it in your CMake project.<span>find_package</span>
command. However, since the installation locations of libraries are not fixed, these Find scripts may not always find the libraries. In such cases, you can set the corresponding variables according to CMake’s error messages, usually needing to provide the installation path, so that the Find File can retrieve the library’s usage requirements. Whether it’s a Config File or a Find File, the goal is not just to find the library but to inform CMake how to use it.list(INSERT CMAKE_MODULE_PATH 0 ${CMAKE_SOURCE_DIR}/cmake)
<span>find_library</span>
and <span>find_path</span>
commands:find_library(MPI_LIBRARY
NAMES mpi
HINTS "${CMAKE_PREFIX_PATH}/lib" ${MPI_LIB_PATH}
# If libmpi.so is not found in the default paths, it will look in MPI_LIB_PATH; downstream users can set this variable value
)
find_path(MPI_INCLUDE_DIR
NAMES mpi.h
PATHS "${CMAKE_PREFIX_PATH}/include" ${MPI_INCLUDE_PATH}
# If mpi.h is not found in the default paths, it will look in MPI_INCLUDE_PATH; downstream users can set this variable value
)
<span>add_library(Abc STATIC IMPORTED)</span>
, which indicates an external dependency that already exists and does not need to be compiled. The second parameter indicates the type, such as whether it is a static or dynamic library. Developers tend to use a namespace-style naming convention for Imported Targets, such as Boost::Format, Boost::Asio, etc. Similarly, a CMake script can have multiple Imported Targets.<span>target_link_libraries</span>
to specify their usage requirements. However, there is another configuration method; as mentioned earlier, we can use <span>PRIVATE</span>
, <span>INTERFACE</span>
, and <span>PUBLIC</span>
to modify target properties, which can be viewed as syntactic sugar. In CMake, most target properties have corresponding private and interface variable versions. For example, when configuring header file directories with <span>target_include_directories</span>
, if we use <span>PRIVATE</span>
modifier, the value is written to the target’s INCLUDE_DIRECTORIES variable; if we use <span>INTERFACE</span>
, it is written to INTERFACE_INCLUDE_DIRECTORIES variable; and if we use <span>PUBLIC</span>
, it will be written to both variables. In CMake, we can modify these variable values directly using <span>set_target_properties</span>
instead of using target commands.<span>set_target_properties</span>
. In actual production environments, due to the differences between Release and Debug environments, IMPORTED_LOCATION actually has multiple versions, such as IMPORTED_LOCATION_RELEASE and IMPORTED_LOCATION_DEBUG. After setting these, CMake will choose the correct link library for downstream users based on these variables in the corresponding environment.# Imported Target for spdlog library
set_target_properties(spdlog::spdlog PROPERTIES
IMPORTED_LINK_INTERFACE_LANGUAGES_RELEASE "CXX"
IMPORTED_LOCATION_RELEASE "${_IMPORT_PREFIX}/lib/spdlog/spdlog.lib"
)
find_package(spdlog REQUIRED)
add_executable(MyEXE)
target_source(MyExe "main.cpp")
target_link_libraries(MyExe SPDLog::spdlog)
<span>target_include_directories</span>
, the header file directories of spdlog will be added automatically.<span>find_package</span>
command, this command can specify many parameters, such as version and specific modules. For example, with the SFML multimedia library, which includes network, audio, graphic modules, etc., if I only need the graphic module, other modules’ corresponding link libraries do not need to be linked. Thus, the CMake script can be written as follows:# Require the graphic module of SFML library with major version 2
find_package(SFML 2 COMPONENTS graphics REQUIRED)
# The target name provided by SFML is sfml-graphics
target_link_libraries(MyEXE PRIVATE sfml-graphics)
<span>find_package</span>
command, these parameters for versions, modules, etc., need to be handled in the Config File or Find File. In cases of version mismatches or non-existent modules, downstream users should be prompted. On this front, CMake has also considered the needs of dependency developers by providing the <span>FindPackageHandleStandardArgs</span>
module. By including this module in CMake scripts, you can use <span>find_package_handle_standard_args</span>
command to inform CMake how to obtain the current package’s version variable and how to determine whether the library has been found, as shown in the following CMake script for RapidJSON:include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(RapidJSON
REQUIRED_VARS RapidJSON_INCLUDE_DIR
VERSION_VAR RapidJSON_VERSION
)
<span>RapidJSON_VERSION</span>
variable, while the <span>RapidJSON_INCLUDE_DIR</span>
variable can be used to indicate whether the library has been found. When this script is executed, CMake first checks whether the <span>RapidJSON_INCLUDE_DIR</span>
variable is empty. If it is empty, it indicates that the library was not found, and CMake will directly report an error to downstream users. If this variable is not empty, and if downstream users provided a version number when calling <span>find_package</span>
, CMake will compare it with the value from the <span>RapidJSON_VERSION</span>
variable. If the version does not meet the requirements, it will also report an error.4. Using CMake to Compile
After CMake generates the build environment, the underlying make, ninja, MSBuild compilation commands are different, but CMake provides a unified method for compilation:
cmake --build .
Using the –build flag, CMake will call the underlying compilation commands, which is very convenient for cross-platform development.
For Visual Studio, its Debug and Release environments are based on configuration, so the CMAKE_BUILD_TYPE variable is invalid and needs to be specified when building:
cmake --build . --config Release
5. Drawbacks of CMake
<span>find_package</span>
do not return results but instead produce side effects on global variables or targets, making their behavior difficult to predict without consulting the documentation. Additionally, in CMake, the distinctions between variables, targets, and strings are unclear, which can be confusing, leaving users unsure of when to use <span>${}</span>
to read values.References:
cmake-buildsystem cmake-packages It’s Time To Do CMake Right
Copyright Notice: Author | Night Breeze
https://ukabuer.me/blog/more-modern-cmake/
Copyright belongs to the original author. This is for academic discussion and research. If there are any copyright issues, please contact us promptly, thank you!
Finally,
The author has collected some embedded learning materials,
Reply with 【1024】 in the public account to get the download link~
Recommended Good Articles Click the blue text to jump
☞ Collection | Comprehensive Programming in Linux
☞ Collection | Learn Some Networking Knowledge
☞ Collection | Manual C Language
☞ Collection | Manual C++ Language
☞ Collection | Experience Sharing
☞ Collection | From Microcontrollers to Linux
☞ Collection | Power Control Technology
☞ Collection | Essential Mathematical Knowledge for Embedded Systems
☞ Collection | MCU Advanced Collection
☞ Collection | Advanced Embedded C Language Collection
☞ Collection | Experience Sharing