CMake Practical Guide Section 4: CMake Targets and Target Properties

Code link for this section: https://github.com/HuPengsheet/use_cmake/tree/master/course_04

In previous chapters, we have mentioned the concept of targets in CMake many times. In this section, we will systematically introduce related concepts.

? There are three main ways to generate targets in CMake:

add_executable(run main.cpp) #Generate executable file target run
add_library(math SHARED add.cpp sub.cpp) #Generate shared library target math
add_library(math STATIC add.cpp sub.cpp) #Generate static library target math

? Use add_executable to generate executable targets. add_library can be used to generate shared library and static library targets (SHARED means shared libraries, STATIC means static libraries). So what exactly does the target in CMake mean? In fact, the concept of goals here is similar to the object-oriented concept of C++. run and math are equivalent to an object. The object has various attribute values. We can assign values to these attributes using some functions. In the code in Section 3, we wrote this line of code

target_include_directories(run PRIVATE ${INCLUDE})

? target_include_directories() is equivalent to assigning a value to INCLUDE_DIRECTORIES of the run target, indicating the include path of the run target, so that we can generate the run executable file in the process , it will know under which paths to find the header files.

? Seeing this, some people may think that since there is an attribute that specifies the path of the header file, there must also be an attribute that specifies the storage address of the target source file (cpp). Yes, the SOURCES attribute does this, so we have this way of writing CMake

add_executable(run)
target_sources(run PRIVATE ${SRC})
target_include_directories(run PRIVATE ${INCLUDE}

? Use target_sources and target_include_directories respectively to specify where the source files and header files of the target are. PRIVATE represents scope, which we will explain later in this section.

Set target attribute value

? To set the attribute value of the target, we must know what built-in attribute values the target has. We can find it in the official CMake manual.

? https://cmake.org/cmake/help/latest/manual/cmake-properties.7.html

? After knowing what attributes are there and their corresponding functions, there are generally two ways to set them.

? The first and more commonly used one is:

#Set include path
target_include_directories(target_name PUBLIC include_dir)
#Set predefined macros at compile time
target_compile_definitions(target_name PUBLIC definition)
#Set compilation options
target_compile_options(target_name PUBLIC option)
#Set the linked library
target_link_libraries(target_name library_name)
#Set source file
target_sources(target_name PUBLIC source_file)
#Set compile C++ features
target_compile_features(target_name PUBLIC feature)

? There is also a general command for setting the properties of a target

set_target_properties(target1 target2 ...
                      PROPERTIES prop1 value1
                      prop2 value2 ...)

? That is, use set_target_properties to specify which property of which target is to be assigned a value.

? So we can use one line of set_target_properties to obtain the equivalent code above (it will not be completely equivalent, mainly because of the (PUBLIC) scope, which will be discussed at the end of this section):

set_target_properties(target_name
                      PROPERTIES
                      INCLUDE_DIRECTORIES include_dir
                      COMPILE_DEFINITIONS definition
                      COMPILE_OPTIONS option
                      LINK_LIBRARIES library_name
                      SOURCES source_file
                      COMPILE_FEATURES feature)

Get the attribute value of the target

get_target_property(<VAR> target property)

Use get_target_property to get the value of a certain property of the target and store it in a variable for our use. Use the following code to take out the attributes we assigned above:

get_target_property(include_dir target_name INCLUDE_DIRECTORIES)
get_target_property(definition target_name COMPILE_DEFINITIONS)
get_target_property(option target_name COMPILE_OPTIONS)
get_target_property(library_name target_name LINK_LIBRARIES)
get_target_property(source_file target_name SOURCES)
get_target_property(feature target_name COMPILE_FEATURES)

INTERFACE, PUBLIC and PRIVATE

? I have mentioned many times before that when using commands such as target_include_directories, there is a scope parameter. This parameter can only be selected from INTERFACE, PUBLIC and PRIVATE. The following are the three corresponding situations. .

target_include_directories(target_name PUBLIC include_dir)
target_include_directories(target_name INTERFACE include_dir)
target_include_directories(target_name PRIVATE include_dir)

? Let’s explain their differences through a simple example. Suppose we want to generate an executable program run and a static library my_math. Run needs to be linked to the my_math library. How should this CMakelists.txt be written? The source code is basically the same as the code in the previous section, except for some changes in structure.

Code link: https://github.com/HuPengsheet/use_cmake/tree/master/course_04

The code structure is as follows:

.
├── CMakeLists.txt
├── include
│ └── my_math.hpp
├── src
│ ├── add.cpp
│ └── sub.cpp
└── test
└── main.cpp

/********************************************** *****/
//my_math.hpp

#ifndef MY_MATH_HPP
#define MY_MATH_HPP

int add(int a,int b);
int sub(int a,int b);

#endif
/****************************************************** **/



/****************************************************** **/
//add.cpp
#include"my_math.hpp"

int add(int a,int b){
    return a + b;
}


//sub.cpp
#include"my_math.hpp"

int sub(int a,int b){
    return a-b;
}
/****************************************************** **/


//main.cpp
#include<iostream>
#include"my_math.hpp"
using namespace std;
int main(){
    cout<<"3 + 2="<<add(2,3)<<endl;
    cout<<"3-2="<<sub(3,2)<<endl;

}

? The contents of the rewritten CMakeLists.txt are as follows:

#CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(course_04)

set(CMAKE_CXX_STANDARD 11) # Set C++ standard to C++11
set(CMAKE_CXX_STANDARD_REQUIRED ON) # C++11 is mandatory and will not fall back to lower versions
set(CMAKE_CXX_EXTENSIONS OFF) # Disable the use of compiler-specific extensions

if(NOT CMAKE_BUILD_TYPE)
message(WARNING "NOT SET CMAKE_BUILD_TYPE")
    set(CMAKE_BUILD_TYPE "Release")
endif()

aux_source_directory(src SRC) #Get all source files under the src target
set(INCLUDE "include") #Set the header file path

add_library(my_math)
target_sources(my_math PRIVATE ${SRC}) #Set source files

target_include_directories(my_math PUBLIC ${INCLUDE}) #Set header files
##target_include_directories(my_math INTERFACE ${INCLUDE})
##target_include_directories(my_math PRIVATE ${INCLUDE})


add_executable(run test/main.cpp)
target_link_libraries(run my_math)

? We try to set the scope of target_include_directories to INTERFACE, PUBLIC and PRIVATE respectively. Observe Code compilation status.

? In the first case, set to PUBLIC and the code runs normally.

? In the second case, if it is set to PRIVATE, an error will be reported, indicating that main.cpp cannot find the header file.

? In the third case, if it is set to INTERFACE, an error will be reported, indicating that add.cpp cannot find the header file.

? It is not difficult to find that setting it to PRIVATE actually means setting where the my_math library can find its own header files when compiling, that is, the header files of its source files add.cpp and sub.cpp. Where to look. And INTERFACE is how other targets find the header files provided by this library when linking to this library. In fact, one is for internal use and the other is for external use. Setting it to PUBLIC means setting the include paths of both internal and external header files to be the same.

? So when we set it to PUBLIC, during the compilation process, the static library will be compiled first. The add.cpp and sub.cpp source files will know that the header files are placed under include, and the static library will be generated successfully. When compiling main.cpp, although we did not set the header file path of the run target, when we linked the my_math target through target_link_libraries(run my_math), the path was set to PUBLIC code>, so the my_math library indicates that its external header files are also placed under include, and main.cpp will be found under include. Finally, the run target is successfully compiled and generated. The corresponding commands such as target_compile_definitions are also similar.

? One thing that needs to be explained here is that in most cases the internal header files and the external header files are different. This is because during the development process, we will have many internally used functions that will not expose their interfaces. We will only put part of the interfaces in external header files for others to use, so the opposite header There are generally fewer functions in the file. So sometimes we will see this way of writing, which is to add a macro definition in front of the variable name. If there is this macro, it means that the class or function should be exported for external use. If it is not exported, it means that it will be used internally in the library.

class EXPORT Mat{
    
}

class Vestor{
    
}

? Going back to the scope issue just now, in essence, target_include_directories is always assigning values to the properties of the target. If it is set to PRIVATE, then it actually assigns a value to the attribute INCLUDE_DIRECTORIES. If it is set to INTERFACE, then it actually assigns a value to the attribute INTERFACE_INCLUDE_DIRECTORIES. If set to PUBLIC, then assign values to both INCLUDE_DIRECTORIES and INTERFACE_INCLUDE_DIRECTORIES. Related properties are explained at https://cmake.org/cmake/help/latest/manual/cmake-properties.7.html

? Finally, to summarize, when target A itself is compiled and generated, it looks for the path INCLUDE_DIRECTORIES in itself. Target B needs to be linked to target A, so it not only looks for its own INCLUDE_DIRECTORIES path, but also looks for target A’s INTERFACE_INCLUDE_DIRECTORIES path.