Modern CMake 简介
摘自:https://zhuanlan.zhihu.com/p/76975231
Modern CMake 简介
CMake是一个构建系统生成器(build-system generator)。常见的构建系统,有Visual Studio,XCode,Make等等。CMake可以支持不同平台下构建系统的生成。
CMake的出现已经有接近20年的历史,它的发展过程也初步经历了三个阶段。
- ~2000 (~v2.x) ,刚刚启动,过程式描述为主。
- 2000~2014 (v3.0~) ,引入Target概念。
- 2014~now (~v3.15),有了Target和Property的定义,更现代化。
概 述
现代化的CMake是围绕 Target 和 Property 来定义的,并且竭力避免出现变量variable的定义。Variable横行是典型CMake2.8时期的风格。现代版的CMake更像是在遵循OOP的规则,通过target来约束link、compile等相关属性的作用域。如果把一个Target想象成一个对象(Object),会发现两者的组织方式非常相似:
- 构造函数:
- add_executable
- add_library
- 成员函数:
- get_target_property()
- set_target_properties()
- get_property(TARGET)
- set_property(TARGET)
- target_compile_definitions()
- target_compile_features()
- target_compile_options()
- target_include_directories()
- target_link_libraries()
- target_sources()
- 成员变量
- Target properties(太多)
在Target中有两个概念非常重要:Build-Requirements 和 Usage-Requirements。这两个概念对于理解为什么现代CMake会如此设计提供了指导意义。
- Build-Requirements: 包含了所有构建Target必须的材料。如源代码,include路径,预编译命令,链接依赖,编译/链接选项,编译/链接特性等。
- Usage-Requirements:包含了所有使用Target必须的材料。如源代码,include路径,预编译命令,链接依赖,编译/链接选项,编译/链接特性等。这些往往是当另一个Target需要使用当前target时,必须包含的依赖。
传统的CMake和现代化的CMake的主要区别(非语法层面)如下图所示。Traditioncal CMake在设置build-requirements和usage-requirements上都依赖手动输入命令,并且人工维持其作用域(变量的作用域以目录为单位)。而Modern CMake在设置上述requirement均以target为单位,所以在传递target属性到其依赖的下游链条中更自动也更智能。
在Moden CMake中新增了不少关键字,其中最常见的是PUBLIC、PRIVATE、INTERFACE。
- PRIVATE/INTERFACE/PUBLIC:定义了Target属性的传递范围。
- PRIVATE: 表示Target的属性只定义在当前Target中,任何依赖当前Target的Target不共享PRIVATE关键字下定义的属性。
- INTERFACE:表示Target的属性不适用于其自身,而只适用于依赖其的Target。
- PUBLIC:表示Target的属性既是build-requirements也是usage-requirements。凡是依赖。凡是依赖于当前Target的Target都会共享本属性。
解剖麻雀
我们来尝试写一个实例,看看在CMake v3.13及以后版本中的写法如何。
HelloWorld
|___ CMakeLists.txt
|___ hello-exe
|______ CMakeLists.txt
|______ main.cpp
|___ hello-lib
|______ CMakeLists.txt
|______ hello.hpp
|______ hello.cpp
以这样一个简单的HelloWorld开启有助于我们快速进入主题。这个项目结构很简单,包含两个子文件夹,hello-exe生成executable,hello-lib生成链接库(动态)。
- 我们先看下顶层CMakeLists的内容:
# HelloWorld/CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(HelloWorld VERSION 1.0.0)
add_subdirectory(hello-lib)
add_subdirectory(hello-exe)
这里没有什么值得多讨论的,与传统CMake一样的写法,定义project名称,版本号,添加子文件夹。
- 我们接着看hello-lib。首先看源码。
源码比较简单,只是定义一个hello_printer类,并在其cpp中定义成员函数print。请注意头文件中的预编译命令。这在VS中是非常常用的预编译命令,用于导出动态库的符号。而当该库被其他Target调用时,需要使用dllimport导入符号。注意这条预编译命令刚好符合build-requirement和usage-requirement的定义。对于hello-lib而言,定义DLL_EXPORT从而将DLL_API定义为_declspec(dllexport)是build-requirement,而对于该Target的调用者,需要的是不定义DLL_EXPORT。因而需要在定义compile_definitions 时将Dll_EXPORT放在PRIVATE关键词下。
当其他Target使用hello-lib的时候,还需要知道hello.hpp的路径。传统的CMake写法是通过在调用者的CMakeLists.txt中添加includedirectory来实现。但这种写法会依赖库之间的相对路径,一旦调整路径,所有的CMakeLists都将需要更新。在Modern CMake中不必如此,你只需要通过target_include_directories指定hello.hpp的路径,将之纳入INTERFACE(当然PUBLIC)也行。则调用者就可以得到该include路径。
CMakeLists.txt 全文如下:
set(target_name "hello-lib")
add_library(${target_name} SHARED
hello.cpp
hello.hpp
)
target_include_directories(${target_name} INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(${target_name} PRIVATE DLL_EXPORT)
- 最后看下hello-exe。hello-exe中的CMakeLists.txt就可以比较简单了:
add_executable(hello-exe main.cpp)
target_link_libraries(hello-exe PUBLIC hello-lib)
补充
Modern CMake中还有些有意思的知识点,这里没法一一覆盖,只能稍稍展开。最有意思的点是generator-expression。在现代IDE中,Build-type一般都不是在CMake config期间能确定的。如VS,XCode都支持Multi-configuration,具体使用Debug还是Release是在编译时才确定,那如果Target的依赖路径或者依赖库需要区分Configuration来配置该怎么办呢?在传统CMake中是比较难办的,target_link_libraries提供了一种手段,可以用debug和optimized来区分具体的库名,而其他的编译或链接设置则比较困难。在Modern CMake中,我们可以通过generator-expression来实现。
generator-expression定义为$<...>的形式。该表达式的值有多种形式,而且支持嵌套使用:
- 条件表达式
- $<condition:true_string> 当条件为1时,表达式为true_string,否则为空
- $<IF:condition,true_string,false_string> 当条件为1时,表达式为true_string,否则为false_string
- 变量表达式
target_link_directories(${PROJECT_NAME} PUBLIC
$<$<CONFIG:Debug>:${CONAN_LIB_DIRS_DEBUG}>
$<$<CONFIG:Release>:${CONAN_LIB_DIRS_RELEASE}>)
- ... 太多了,不一一列举。
以上是Modern CMake中常用的内容,还有些如IMPORTED,ALIAS暂时还没用到,等用到再更新吧。