Modern CMake 最佳实践

CMake 是一个开源的跨平台自动化建构系统,是目前最主流的 C/C++语言构建工具。CMake3.0 之后引入很多新的特性,有效提升了编写构建脚本的效率,称为 Modern CMake。本文总结了在 Modern CMake 使用中的一些最佳实践,供大家参考。

Target 概念


旧版 CMake 2.0 主要是基于 directory 来构建,很多复用只能靠变量实现。Modern CMake 最大的改进是引入了 target,支持了对构建的闭包性和传播性的控制
,从而实现了构建可以模块化。

推荐 1: 在 Modern CMake 中强烈推荐抛弃旧的 directory 方式,使用 target 的方式构建整个工程。

1. tagert 分类

Target 中最核心的两个分类是:executable, library。

  • 其中 executable 是可执行程序,在不同的操作系统会有不同的格式,同样一个工程内也可能需要生成多个可执行程序。 具体指令如下所示:

    add_executable(<name> [WIN32] [MACOSX_BUNDLE]
                  [EXCLUDE_FROM_ALL]
                  [source1] [source2 ...])
    
  • library 代表链接库,可以分为 share, static, object, module, interface 五个种类。

    • share 表示共享库,在编译构建过程中,需要链接但不会添加到最后的可执行文件中。共享库在程序运行中可以被动态加载和替换,当被多个程序使用时还可以在内存中被共享。如果期望 library 可以被独立的部署和替换的话,需要选择这种方式。

    • static 表示静态库,会在编译过程中被一起添加生成到可执行文件中。当静态库的实现发生变更时,必须要重新编译整个系统才可以使用。使用静态库的一个好处是,生成的可执行程序可以独立的运行,不再需要依赖这个静态库。

    • module 也是共享库的一种,CMake 中限制了 moudle 类型的 libray 不能被编译时链接,只能通过 dlopen 在运行时动态加载使用。

    • object 类型的库表示一组编译后的文件,并不会打包和链接。使用 object 类型的库可以避免一些大的源文件被重复的编译,提升编译效率。

    • interface 类型的并不会编译输出文件,代表一组接口文件,可以在编译构建中被其他 target 使用。使用 interface 类型的库可以把多个模块公共的接口头文件作为一个单独 target 来被引用,构建更加高效。

定义库具体指令如下:

add_library(<name> [STATIC | SHARED | MODULE |OBJECT |INTERFACE] ...)

2. target 闭包性

为了实现 target 闭包性,Modern CMake 实现 target 与 构建和使用中所有依赖建立绑定关系,从而可以拿来即用。正常情况下编译一个 target(可执行程序或者库)需要依赖如下所示:

  • 源文件列表,通过 target_sources 配置。
  • 头文件列表,通过 target_include_directories 配置。
  • 预编译宏,通过 target_compile_definition 配置。
  • 编译选项和特性,通过 target_compile_options,target_compile_features 配置。
  • 链接选项,通过 target_link_options 配置。

在 C/C++软件系统中,一个 target 中大部分的头文件是仅在模块内使用,为内部接口,仅有小一部分接口头文件是外部使用,称为对外接口。在软件设计过程中,要从高内聚低耦合的角度出发,去严格设计每个 target 的外部接口和内部接口。同样构建过程中,在链接不同 target 时也需要明确指明依赖的外部接口文件,从而提高编译构建的效率。

为了更好支持这个特性,Modern CMake 针对 target 引入两个概念:user requriement(用户依赖) 和 build requirement(编译依赖)。用户依赖表示 target 使用方需要的依赖,而编译依赖表示当前 target 编译构建时需要依赖。

Modern CMake 增加了三个关键字 INTERFACE、PUBLIC、PRIVATE 分布表示不同作用域, 下面以添加头文件依赖命令为例说明:

target_include_directories(<target> [SYSTEM] [BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

给 target 添加头文件依赖路径时:

  • INTERFACE : 表示添加的头文件路径仅 target 的使用方需要,编译当前 target 并不需要。
  • PRIVATE : 表示添加的头文件路径仅当前 target 编译时使用,其他 target 不需要。
  • PUBLIC : 表示编译时和链接该 target 都需要使用。

推荐 2: 在 Modern CMake 中强烈建议为 target 添加依赖接口时,从使用者角度考虑写明 INTERFACE, PRIVATE, PUBLIC。

推荐 3: 在 Modern CMake 中推荐使用 target_sources 来添加源文件依赖,保持每个接口的职责单一。

3. target 传播性

当构建工程中 包含比较多的 libary 时,编译和管理这些 Libary 之间的依赖就变得尤为重要。在 Modern CMake 中,当给 Libary 定义用户依赖和编译依赖后,通过在 target_link_libraries 中定义与其他组件间的依赖关系, 就可以自动传递和推演 target 之间的所有编译依赖。

组件间的依赖关系定义命令如下:

target_link_libraries(<target>
                      <PRIVATE|PUBLIC|INTERFACE> <item>...
                     [<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
  • PRIVATE: 被依赖 libary 的 user requirement 的会变成当前 target 的 build requirement
  • PUBLIC:被依赖 libary 的 user requirement 的会变成当前 target 的 build requirement 和 user requirement.
  • INTERFACE:被依赖 libary 的 user requirement 的会变成当前 target 的 user requirement

推荐 4: 充分利用 Modern CMake 强大的依赖传递功能,合理设计每个 target 间的依赖关系。

package 能力


package 代表携带版本信息的 target,用于方便的导入第三方库。当系统最终发布的 target 比较大时,通过功能拆解为更多小粒度的 target,然后使用 package 机制组合实现所有功能。通过这种策略,从而可以实现系统更小粒度功能的单独构建与发布很有价值。在 CMake 中可以使用 find_package 来导入一个特定版本的第三方库。

例如: find_package(Qt5Gui 5.1.0 CONFIG) ,导入包 Qt5Gui,版本号 5.1.0,通用的导入包的命令如下:

find_package(<PackageName> [version][exact] [QUIET][module]
[REQUIRED][COMPONENTS][components...]]
[OPTIONAL_COMPONENTS components...][no_policy_scope])

不同语言的包管理器,在管理第三方包中实现存在很大差异。例如 JAVA 中 marven 会下载包到本地公共仓库地址中,构建时直接从本地仓库中选择合适的包进行构建。而很多动态语言例如 ruby, nodejs 等则直接把依赖的源码包下载到本地工程的一个单独目录中使用。

受制于 C/C++语言的特有复杂性,目前 Modern CMake 目前无法做到像其他语言包管理器灵活的使用方式,但 CMake 也在不断完善使用第三方库的能力。Modern CMake 目前提供两种方式使用第三方库,分别是 find_package, find_content。

find_package 用于查找本地安装的第三方包。Modern CMake 可以在用户级包仓库和系统级包仓库中寻找到已经安装的包。

  • 用户级包仓库地址

    windows 的注册表 HKEY_CURRENT_USER 下:
    HKEY_CURRENT_USER\Software\Kitware\CMake\Packages\<PackageName>

    linux:
    ~/.CMake/packages/<PackageName>

  • 系统级包仓库地址

    windows 的注册表 HKEY_LOCAL_MACHINE 下:
    HKEY_LOCAL_MACHINE\Software\Kitware\CMake\Packages\<PackageName>
    在非 windows 系统中没有系统级包仓库地址。

使用 find_package 包括两种方式:config-file package 和 module package。其中 config-file package 表示 target 使用 package CMake 构建,可以直接拿来使用。而 Module package 的表示使用的 target 没有使用 CMake 构建,需要下游的使用者编写 CMake 文件。

Modern CMake 中提供了制作安装包的脚本。在 cmake 文件中加入 include(CMakePackageConfigHelpers),就可以使用封装方法来生成 ConfigVersion.CMake 文件,其中已经自动设置好了包的相关信息。

更多关于 Package 使用介绍请参考:Modern CMake Package 使用手册

推荐 5: Modern CMake 中 推荐使用 config-file package 的方式将 target 发布成 package,利用 package 机制将对依赖库的使用标准化。

使用 find_package 仅可以使用安装到本地的仓库,但很多时候还需要使用远程仓库上的 库,可以有下面几种做法:

  • 借助 git 的功能,例如:

    git submodule add https://github.com/test.git
    可以使用 git 把远程仓库的代码更新到本地目录之后,就可以像本地目录一样使用库了。

  • 在 CMake 3.14 之后,可以直接使用 FetchContent_Declare(libName) 和 FetchContent_MakeAvailable(libName) 来导入一个 git 上的库后,就可以像其他 target 一样使用了。

    如下例所示:

    include(FetchContent)
    # FetchContent_MakeAvailable was not added until CMake 3.14
    if(${CMake_VERSION} VERSION_LESS 3.14)
        include(add_FetchContent_MakeAvailable.CMake)
    endif()
    
    set(SPDLOG_GIT_TAG  v1.4.1)  # 指定版本
    set(SPDLOG_GIT_URL  https://github.com/gabime/spdlog.git)  # 指定git仓库地址
    
    FetchContent_Declare(
      spdlog
      GIT_REPOSITORY    ${SPDLOG_GIT_URL}
      GIT_TAG           ${SPDLOG_GIT_TAG}
    )
    
    FetchContent_MakeAvailable(spdlog)
    

在 Modern CMake 中,借助 FetchContent 可以直接使用远程 git 库中的组件,然后借助 find_package 来使用安装到本地的组件库,从而实现比较高效管理和使用第三方库。

如果 CMake 管理使用 target 方面功能还不能满足需求,可以考虑结合 concan 与 CMake 一起使用。concan 是业界目前 C/C++功能最完善的包管理器,具体请参考:concan 官网介绍

CMake Module


Module 在很多场景下有不同的解释,这里的 CMake module 代表可以被导入的 cmake 源码文件。在 CMake 中使用 include(module)之后,就可以使用 module 中定义函数方法了。CMake 内置的 Module, 提供了很多有价值的功能方法,可以直接导入使用。

1.用于平台检查功能

检查使用存在指定头文件:

  • Module: INCLUDE(CheckIncludeFiles)

  • Usage: CHECK_INCLUDE_FILES(headers variable)

  • Example: CHECK_INCLUDE_FILES(strings.h HAVE_STRINGS_H)

检查方法是否存在:

  • Module: INCLUDE(CheckFunctionExists)

  • Usage: CHECK_FUNCTION_EXISTS(function variable)

  • Example: CHECK_FUNCTION_EXISTS(madvise HAVE_MADVISE)

还提供了很多其他的检测:CheckSymbolExists, CheckLibraryExists,CheckTypeSize,CheckCXXSourceCompilesd。

CMake 还提供了很多系统和编译器的检查,这些特性在构建支持跨平台应用时发挥非常大的作用。

2. 直接生成 RPM 包

CMake 中提供了 module 用于直接生成 rpm 包,需要设置一系列的变量,下面是一个最简单的 rmp 包的 CMake 脚本如下:

CMake_minimum_required (VERSION 2.8)

set(VERSION "1.0.1")
<----snip your usual build instructions snip--->
set(CPACK_PACKAGE_VERSION ${VERSION})
set(CPACK_GENERATOR "RPM")
set(CPACK_PACKAGE_NAME "my_project")
set(CPACK_PACKAGE_RELEASE 1)
set(CPACK_PACKAGE_CONTACT "John Explainer")
set(CPACK_PACKAGE_VENDOR "My Company")
set(CPACK_PACKAGING_INSTALL_PREFIX ${CMake_INSTALL_PREFIX})
set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CPACK_PACKAGE_RELEASE}.${CMake_SYSTEM_PROCESSOR}")
include(CPack)

具体请参考: CMake rpm 生成手册

3. 外部开源 CMake module

CMake 本身也是一门编程语言,也可以封装实现一些功能方法来提供一些更加友好的功能 API,当然也可以引入一些第三方 CMake Module 来使用。

Bazel.CMake 库就是一个非常好用的库,导入之后可以在 CMake 中想类似 Bazel 的方式来定义 target,减少了很多重复的定义。例如:

project(testcase VERSION 0.1.0)
include(bazel)

cc_library(cpu_id SRCS cpu_id.cc)
cc_test(cpu_id_test SRCS cpu_id_test.cc DEPS cpu_id glog)
cc_test(hello SRCS hello.cc)

Bazel 是一个支持多语言、跨平台的高效构建工具,对 C++的支持非常友好,是目前 Google 主推的构建工具,具体请参考好友「刘光聪」的系列文章:Bazel build 介绍

推荐 6:推荐复用 CMake 内置的 module 与第三方开源 module 中的功能实现,避免重复去造轮子。

扩展补充


1. 交叉编译

交叉编译表示编译构建所在操作系统与运行时操作系统不同,为了实现这个目标需要做到两点:

  • CMake 不能自己去检测目标系统
  • 编译系统也不能用原生系统的头文件和库。

当 CMake 不能检测目标系统和编译器,需要设置变量的方式来告诉 CMake, 目前 CMake 与目标系统相关的部分变量如下:

  • CMake_HOST_SYSTEM_NAME
  • CMake_HOST_SYSTEM_VERSION
  • CMake_HOST_SYSTEM_PROCESSOR
  • CMake_HOST_SYSTEM
  • CMake_C_COMPILER

在进行交叉编译时,可以定义一个目标系统配置的 CMake 文件,如下所示。

# this one is important

SET(CMake_SYSTEM_NAME Linux)
#this one not so much
SET(CMake_SYSTEM_VERSION 1)

# specify the cross compiler

SET(CMake_C_COMPILER /opt/eldk-2007-01-19/usr/bin/ppc_74xx-gcc)
SET(CMake_CXX_COMPILER /opt/eldk-2007-01-19/usr/bin/ppc_74xx-g++)

# where is the target environment

SET(CMake_FIND_ROOT_PATH /opt/eldk-2007-01-19/ppc_74xx /home/alex/eldk-ppc74xx-inst)

# search for programs in the build host directories

SET(CMake_FIND_ROOT_PATH_MODE_PROGRAM NEVER)

# for libraries and headers in the target directories

SET(CMake_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
SET(CMake_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

然后在执行 CMake build 时, 通过显示的指定工具链的配置文件来生成目标系统上的可执行程序,如下所示:

~/src$ cd build
~/src/build$ CMake -DCMake_TOOLCHAIN_FILE=~/Toolchain-eldk-ppc74xx.CMake ..

具体请参考:交叉编译介绍官方文档

2. Generator expressions

CMake 本质上是一个构建工程生成器,Generator expression 是在 build 过程中执行的表达式,从而实现根据不同配置生成不同的构建工程。

现代 IDE 很多都支持 Multi-configuration,例如 debug, release 等,在 Modern CMake 中,可以通过 generator-expression 来更好的支持这个特性。

Generator expression 可以在 target_link_libraries(), target_include_directories(), target_compile_definitions()中使用,从而可以实现根据条件来添加链接依赖,头文件路径依赖或者宏定义等。

generator-expression 定义为$<...>的形式,该表达式实现有多种形式,并且支持嵌套使用,下面以 debug,release 的链接配置示例来简单说明:

target_link_directories(${PROJECT_NAME} PUBLIC
  $<$<CONFIG:Debug>:${LIB_DIRS_DEBUG}>
  $<$<CONFIG:Release>:${LIB_DIRS_RELEASE}>)

具体请参考:编译生成式官方介绍

总结


Modern CMake 3.0 功能和特性与 CMake2.0 上有很大的变化,而且新版本还在不断完善中。希望每位开发者可以拥抱变化,优先选用 Modern CMake 来构建系统,并在编写构建脚本过程中可以参考下面的一些实践总结。

  • 在 Modern CMake 中强烈推荐抛弃旧的 directory 方式,使用 target 的方式构建整个工程。
  • 在 Modern CMake 中强烈建议为 target 添加依赖接口时,从使用者角度考虑写明 INTERFACE, PRIVATE, PUBLIC。
  • 在 Modern CMake 中推荐使用 target_sources 来添加源文件依赖,保持每个接口的职责单一。
  • 充分利用 Modern CMake 强大的依赖传递功能,合理设计每个 target 间的依赖关系。
  • 推荐使用 config-file package 的方式将 target 发布成 package,利用 package 机制将对依赖库的使用标准化。
  • 推荐复用 CMake 内置的 module 与第三方开源 module 中的功能实现,避免重复去造轮子。

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 向您的项目添加 C 和 C++ 代码 本文内容 下载 NDK 和构建工具 创建支持 C/C++ 的新项目 构建和运...
    会飞的大象_阅读 3,750评论 0 3
  • CMake学习 本篇分享一下有关CMake的一些学习心得以及相关使用。 本文目录如下: [1、CMake介绍] [...
    AlphaGL阅读 12,219评论 11 79
  • 为了将C/C++代码转换为可以在硬件上运行的程序,需要经过编译和链接。编译是将高级语言所写的源程序翻译成等价的机器...
    WalkeR_ZG阅读 9,669评论 0 15
  • 本文不介绍cmake命令使用方法,也不讲CMakeLists.txt的语法,有需要的读者可以看我另外相关的文章即可...
    konishi5202阅读 1,101评论 0 5
  • 最近在负责一个大型工程的CMake编译系统管理,整理一些工作过程中积累下来的知识片段和技巧。CMake是一个跨平台...
    啊呀哟嘿阅读 8,772评论 0 2