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_EXISTS:target> 当target存在为1,否则为0
    • $<CONFIG:cfg> 当config为cfg时为1,否则为0。这是非常高频使用的一个表达式,可以通过它来区分Debug/Release等不同的config。如下例所示,通过嵌套使用上述两个表达式,可以达到区分CONFIG来设置依赖库路径的目的。
target_link_directories(${PROJECT_NAME} PUBLIC                                                                                                                                                                      
  $<$<CONFIG:Debug>:${CONAN_LIB_DIRS_DEBUG}>                                                                                                                                                                        
  $<$<CONFIG:Release>:${CONAN_LIB_DIRS_RELEASE}>) 
    • ... 太多了,不一一列举。

以上是Modern CMake中常用的内容,还有些如IMPORTED,ALIAS暂时还没用到,等用到再更新吧。

参考

  1. onqtam/awesome-cmake
posted @ 2024-08-23 00:12  LiuYanYGZ  阅读(1)  评论(0编辑  收藏  举报