Linux Progamming

Modern CMake 最佳实践

2020-03-09  本文已影响0人  尉刚强

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。

定义库具体指令如下:

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

2. target 闭包性

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

在 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 添加头文件依赖路径时:

推荐 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>...]...)

推荐 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 可以在用户级包仓库和系统级包仓库中寻找到已经安装的包。

使用 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 仅可以使用安装到本地的仓库,但很多时候还需要使用远程仓库上的 库,可以有下面几种做法:

在 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.用于平台检查功能

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

检查方法是否存在:

还提供了很多其他的检测: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 文件,如下所示。

# 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 来构建系统,并在编写构建脚本过程中可以参考下面的一些实践总结。

参考资料

上一篇下一篇

猜你喜欢

热点阅读