您当前的位置: 首页 > 

Modern CMake 简介

蔚1 发布时间:2019-08-09 23:30:54 ,浏览量:2

作为拥有 20 年发展历史的 CMake,肩负 Build-system Generate 的重任,被越来越多的开源库接纳,其自身理念也在不断推陈出现。到底 Modern CMake 和 Traditional 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),会发现两者的组织方式非常相似:

  • 构造函数:addexecutableaddlibrary
  • 成员函数:gettargetproperty()settargetproperties()getproperty(TARGET)setproperty(TARGET)targetcompiledefinitions()targetcompilefeatures()targetcompileoptions()targetincludedirectories()targetlinklibraries()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.txtcmake_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 从而将 DLLAPI 定义为declspec(dllexport)是 build-requirement,而对于该 Target 的调用者,需要的是不定义 DLLEXPORT。因而需要在定义 compile_definitions 时将 DllEXPORT 放在 PRIVATE 关键词下。

当其他 Target 使用 hello-lib 的时候,还需要知道 hello.hpp 的路径。传统的 CMake 写法是通过在调用者的 CMakeLists.txt 中添加 includedirectory 来实现。但这种写法会依赖库之间的相对路径,一旦调整路径,所有的 CMakeLists 都将需要更新。在 Modern CMake 中不必如此,你只需要通过 targetincludedirectories 指定 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 定义为$的形式。该表达式的值有多种形式,而且支持嵌套使用:

  • 条件表达式
  • $\ 当条件为 1 时,表达式为 true_string,否则为空
  • $\ 当条件为 1 时,表达式为 true_string,否则为 false_string
  • 变量表达式
  • $ 当 target 存在为 1,否则为 0
  • $\ 当 config 为 cfg 时为 1,否则为 0。这是非常高频使用的一个表达式,可以通过它来区分 Debug/Release 等不同的 config。如下例所示,通过嵌套使用上述两个表达式,可以达到区分 CONFIG 来设置依赖库路径的目的。
target_link_directories(${PROJECT_NAME} PUBLIC                                                                                                                                                                        $                                                                                                                                                                          $) 
  • ... 太多了,不一一列举。以上是 Modern CMake 中常用的内容,还有些如 IMPORTED,ALIAS 暂时还没用到,等用到再更新吧。
参考
  • https://crascit.com/2016/01/31/enhanced-source-file-handling-with-target_sources/#comment-414
  • https://github.com/onqtam/awesome-cmake/blob/master/README.md
  • https://www.youtube.com/watch?v=y7ndUhdQuU8

本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。

阅读全文: http://gitbook.cn/gitchat/activity/5d4cbaf5f84543415feac3ee

您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。

FtooAtPSkEJwnW-9xkCLqSTRpBKX

关注
打赏
1688896170
查看更多评论

蔚1

暂无认证

  • 2浏览

    0关注

    4645博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文
立即登录/注册

微信扫码登录

0.0640s