CMake简易指南

2023-09-06 15:08:40 浏览数 (1)

本文并非入门保姆教程,仅是个人使用CMake过程中踩过的坑的一些总结

CMake 详细说明参考官方文档 https://cmake.org/cmake/help/latest/index.html,其中latest为最新版本版本,不同 CMake 版本,API 有差异,请根据当前项目设置的最低版本来参考,高版本 API 在低版本无法使用。3.20之后的文档会标记该 API 的生效版本

cmake 的优势不是性能和易用性,而是通用性跨平台。感谢 C 委员会的大力推广,几乎支持市面上所有通用编译环境,以及大部分开源三方库均支持 cmake

核心概念

  • Target:目标单元,在CMake中,target是一个非常核心的概念,与其他现代化工程系统中的target类似,由 add_library/add_executable/add_custom_target 这三种方式生成,前两者很好理解,库和执行文件,第三种则比较特殊,通常用于执行自定义命令,如:调用protoc编译proto文件、编译完成时拷贝数据等等
  • Generator:生成器,可以理解为编译系统,如:Ninja / Unix Makefiles / Visual Studio / Xcode
  • cmake-commands:cmake 命令,通常写在 CMakeLists.txt / *.cmake 文件中调用的内置语法和函数都称之为 cmake 命令
  • cmake-generator-expressions:生成器表达式,一种特殊的表达式,编译过程才生效
  • Command-Line:cmake 控制台命令,即在终端控制台使用的命令,可以用于触发配置和编译之外,还可以用于文件操作以及解压缩等

版本选择

每个可以独立编译的 CMakeLists.txt 首行都应该加上最低版本限制,避免出现运行的 CMake 版本过低导致不明错误,如:

代码语言:text复制
cmake_minimum_required(VERSION 3.14)

关于 cmake 版本主要需要考虑操作系统以及 IDE 的兼容,实际使用时尽可能使用更新版本的 cmake 可以避免一些不必要的错误。

更高版本的 cmake 意味着可以使用更先进的 API,同时部分 OEM 系统也可能无法支持,根据项目使用场景合理选择 cmake 版本,在选定一个最小版本之后翻阅文档时也应该以该版本的文档为准。

目标编译系统

cmake 与Google GN类似,属于meta-build(源编译)系统,有自己的交互语法,使用时需要先将自身的语法翻译成其他编译系统,这个翻译过程称之为configure(配置),在执行配置命令时可以通过-G XXX来指定翻译的目标编译系统,在未指定目标编译时 cmake 会默认指定一个Generator,如下表:

默认编译系统

Linux

Unix Makefiles(Makefile)

macOS

Unix Makefiles(Makefile)

Windows

Visual Studio(.sln/.vcxproj)

除了上述平台编译系统外,cmake 还支持跨平台编译系统Google Ninja,Ninja 属于目标编译系统,且效率很高,默认会根据系统处理器内核数来分配编译线程数,如果条件允许,尽可能使用Ninja作为目标编译系统,使用时需要安装ninja到环境变量PATH中,在 cmake 配置时加上-G Ninja即可,当存在CMakeCache.txt时修改-G需要先将缓存文件CMakeCache.txt删除,CMakeCache.txt在编译根目录

工作流

cmake 的运行大致分两到三步,下述样例属于cmake 控制台命令

  1. 配置:输入源文件目录,指定目标编译系统,添加编译选项,生成目标编译系统
  2. 编译:输入目标编译系统,执行编译
  3. 安装(可选):将编译产物安装到指定位置(需要 CMakeLists.txt 中编写安装规则)
代码语言:shell复制
# 新建编译缓存路径
mkdir -p out/debug && cd out/debug
# 执行配置
cmake ../.. -G Ninja -DCMAKE_BUILD_TYPE=Debug
# 执行编译
cmake --build .
# 安装(高版本cmake支持)
cmake --install .
# 低版本cmake可用
cmake --build . --target install

步骤二编译时也可以使用目标编译系统的编译命令触发编译,需要编写跨平台编译脚本时,使用cmake --build .可以适配任何环境

配置

配置阶段的参数主要为以下几种:

  • -G Generator:用于指定目标编译系统,未指定时取 cmake默认编译系统。如果条件允许,推荐使用-G Ninja
  • -DCMAKE_BUILD_TYPE=Debug/Release/RelWithDebInfo/MinSizeRel:用于指定编译类型
  • -DCMAKE_<LANG>_COMPILER=clang:用于指定语言编译器,默认由 cmake 搜索指定,如:-DCMAKE_CXX_COMPILER=clang 。一般来说,CMAKE_C_COMPILERCMAKE_CXX_COMPILER分别指定 C 和 C 的编译器,如修改则需要同时指定
  • -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake:用于指定交叉编译工具链,一般用于非本地平台编译,如 Android,ARM 平台编译等
  • -DKey=Value:用于配置CMakeLists.txt或者工具链中的option选项等

cmake 执行配置时从指定路径下的CMakeLists.txt开始加载,遇到第一个project(xxx)时开始检查编译环境中的编译器,执行完所有代码后将全局变量保存至CMakeCache.txt文件,再次执行配置时不会再修改全局变量,所以遇到一些非预期错误时,请先删除缓存路径下的CMakeCache.txt文件。

编译与安装

执行编译时可以通过添加参数 -- 来为目标编译器添加编译选项,如:为 gcc 添加多线程编译可以添加

代码语言:text复制
cmake --build . -- -j8

执行安装时,需要确认CMakeLists.txt文件中已编写安装规则,通常需要指定安装那些文件,以及这些文件相对于CMAKE_PREFIX_PATH的位置

如已指定-G Ninja,则无需使用-j,Ninja默认启用多线程编译

常用语法

以下为最基础的样例

代码语言:text复制
# 指定最低cmake版本要求
cmake_minimum_required(VERSION 3.14)
# 创建项目标识
project(mylib)
# 添加名为mylib的目标,类型为动态库
add_library(mylib SHARED lib.cc lib.h)
# 添加名为myexe的目标,类型为可自行文件
add_executable(myexe main.cc)
# 为myexe添加对mylib的链接关联
target_link_libraries(myexe PUBLIC mylib)

在实际跨平台项目中,由于涉及到平台差分,不同编译器具有不同的编译选项,以及复杂的工程目录结构,远比样例代码复杂,以下为常见项目结构

代码语言:text复制
.
├── CMakeLists.txt            // 入口cmake文件
├── include                   // 本项目用于导出的头文件
├── mylib
│   └── CMakeLists.txt        // 源文件cmake配置
├── test
│   └── CMakeLists.txt        // 测试代码cmake配置
└── third_party
    └── crbase                // crbase三方库

在项目结构较为复杂时,建议使用多个多级 CMakeLists.txt 来描述,如在入口 cmake 文件中通过 add_subdirectory() 来关联子目录,如:

代码语言:text复制
# 入口cmake文件
cmake_minimum_required(VERSION 3.10)
project(mylib)

option(build_with_test "是否编译测试代码" ON)

add_subdirectory(third_party/crbase ${CMAKE_BINARY_DIR}/crbase)
add_subdirectory(mylib)

if (build_with_test)
  add_subdirectory(test)
endif ()

########## 文件分隔符 ##########

# 源文件cmake配置
add_library(mylib SHARED lib.cc lib.h)
target_link_libraries(mulib PUBLIC crbase)

########## 文件分隔符 ##########

# 测试代码cmake配置
add_executable(test tests.cc)
target_link_libraries(test PUBLIC mylib)

每一个add_subdirectory(subpath [subpath])都会在缓存路径新建一个对应的文件夹,定义在<subpath>/CMakeLists.txt中的target产物也在该缓存文件夹

平台差分

跨平台项目中通常会遇到不同平台参与编译的头文件不一样,或者编译选项不同,在 cmake 里有一些描述平台的系统变量:

  • CMAKE_SYSTEM_NAME:描述目标平台名称,可以理解为运行编译产物的操作系统,如: Windows | Darwin | Linux | Android | iOS,交叉编译时由工具链指定
  • CMAKE_SYSTEM_PROCESSOR:描述目标处理器类型,交叉编译时由工具链指定
  • CMAKE_HOST_SYSTEM_NAME:描述本地平台名称,可以理解为执行编译动作的操作系统,如: Windows | Darwin | Linux
  • CMAKE_HOST_SYSTEM_PROCESSOR:描述本地处理器类型

为了跨平台差分使用方便,一般会在一个地方检测当前需要编译的平台变量,如:

代码语言:text复制
# 检测当前编译平台
# iOS没有官方工具链,三方工具链部分设置的名称为iOS,部分为IOS,此处统一改成小写
string(TOLOWER ${CMAKE_SYSTEM_NAME} __system_name)
if (${__system_name} STREQUAL "darwin")
  set(OS_MACOS TRUE)
elseif (${__system_name} STREQUAL "linux")
  set(OS_LINUX TRUE)
elseif (${__system_name} STREQUAL "windows")
  set(OS_WINDOWS TRUE)
elseif (${__system_name} STREQUAL "android")
  set(OS_ANDROID TRUE)
elseif (${__system_name} STREQUAL "ios")
  set(OS_IOS TRUE)
else ()
  message(FATAL_ERROR "Unsupported system : [${CMAKE_SYSTEM_NAME}]")
endif ()
unset(__system_name)

后续需要根据不同平台走进不同分支时可以:

代码语言:text复制
if (OS_MACOS)
  message(STATUS "Target system is macOS")
elseif (OS_LINUX)
  message(STATUS "Target system is Linux")
elseif (OS_WINDOWS)
  message(STATUS "Target system is Windows")
elseif (OS_ANDROID)
  message(STATUS "Target system is Android")
elseif (OS_IOS)
  message(STATUS "Target system is iOS")
endif ()

在交叉编译时,CMAKE_SYSTEM_NAMECMAKE_HOST_SYSTEM_NAME 是不同的,正因为编译环境与运行环境不同,所以才叫 交叉编译

变量以及内置变量

为了便于阅读和维护,通常会使用变量来保存一些内容,cmake 中变量分为常规变量缓存变量环境变量,普通变量直接设置尽在当前 CMakeLists.txt 及子项目(通过 add_subdirectory 添加的项目)中生效,可取消设置,缓存变量则会写到 CMakeCache.txt 缓存文件中全局可用,如:

代码语言:text复制
# 常规变量
# set(<variable> <value>... [PARENT_SCOPE])
set(NORMAL_VAR "normal variable")
unset(NORMAL_VAR)
# 缓存变量
# set(<variable> <value>... CACHE <type> <docstring> [FORCE])
set(CACHE_VAR "cache variable" CAHCE STRING "description")
# 环境变量
# set(ENV{<variable>} [<value>])
set(ENV{PATH} "$ENV{PATH}:${CMAKE_CURRENT_LIST_DIR}")
# 获取变量
message(STATUS "NORMAL_VAR = ${NORMAL_VAR}")
message(STATUS "CACHE_VAR = ${CACHE_VAR}")
message(STATUS "ENV_PATH = $ENV{PATH}")

可以通过 ${<variable>} 获取变量的值,部分命令及表达式使用的是变量名。cmake 中内置很多系统变量,用于查询或修改系统设置,完整文档参考 cmake-variables,除了上述 平台差分中提到的四个变量,常用的变量还有:

  • CMAKE_PROJECT_NAME:顶层项目名称,由project(xxx)指定
  • PROJECT_NAME:多级项目时最后一个项目名称,由project(xxx)指定
  • CMAKE_SOURCE_DIR:获取入口 cmake 文件所在路径,相对路径时建议使用 CMAKE_CURRENT_LIST_DIR
  • CMAKE_CURRENT_LIST_DIR:获取当前 cmake 文件(可以是CMakeLists.txt,也可是xxx.cmake)所在路径,CMAKE_CURRENT_LIST_DIR 更为常用
  • CMAKE_BINARY_DIR:顶层缓存路径,即执行 cmake 配置的路径
  • CMAKE_CURRENT_BINARY_DIR:当前缓存路径,add_subdirectory(subproject subpath) 添加的 subpath
  • PROJECT_BINARY_DIR:当前项目缓存路径,即最后一个 project 所在路径
  • CMAKE_BUILD_TYPE:编译类型,常用有 Debug / ReleaseRelWithDebInfo / MinSizeRel不常用
  • CMAKE_<LANG>_FLAGS:编译选项,<LANG> 为编译语言,如:CMAKE_C_FLAGS / CMAKE_CXX_FLAGS
  • CMAKE_<LANG>_COMPILER:编译器信息

编译选项

在 cmake 中添加编译选项主要通过CMAKE_<LANG>_FLAGS来设置编译选项,CMAKE_C_FLAGS/CMAKE_CXX_FLAGS分别指 C 和 C 编译选项。链接选项有CMAKE_STATIC_LINKER_FLAGS / CMAKE_SHARED_LINKER_FLAGS / CMAKE_EXE_LINKER_FLAGS分别指静态库、动态库、可执行文件的链接选项。CMAKE_XXX_FLAGS为字符串类型,通常使用方式为

代码语言:text复制
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DMACRO_OPTION=1")

上述编译参数为传统方式,整个**CMakeLists.txt**生效,已不推荐使用,新版有类似面向对象的参数模式

该使用方式不利于修改,实际使用中一般会选择 cmake 其他命令还辅助添加,以下为常用命令,注意最低cmake版本要求

  • target_compile_features:编译特征支持检查
  • target_precompile_headers(3.16 ):预编译头文件
  • target_compile_definitions/add_definitions:宏定义
  • target_compile_options/add_compile_options:编译选项,通常用于修改编译器参数,需要搭配编译器一起使用
  • target_include_directories/include_directories:头文件查找路径
  • target_link_directories(3.13 )/link_directories:库文件查找路径
  • target_link_libraries(3.13 )/link_libraries:链接库名称
  • target_link_options(3.13 )/add_link_options(3.13 ):链接选项
代码语言:text复制
# 分别添加`C11`和`C  14`特征支持检查
target_compile_features(mylib PUBLIC c_std_11 cxx_std_14)
# 添加预编译头文件,通常用于编译提速
target_precompile_headers(mylib PRIVATE precompile.h)
# 相当于-DFoo=1
target_compile_definitions(mylib PUBLIC -DFoo=1)
# 表达式编译选项
target_compile_options(mylib PUBLIC -fno-exceptions
  PRIVATE $<$<COMPILE_LANGUAGE:C>:${__CFLAGS_C}>            # C编译选项
  PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${__CFLAGS_CXX}>        # C  编译选项
  PRIVATE $<$<CXX_COMPILER_ID:GNU>:${__CFLAGS_CXX_GNU}>     # GNU编译器生效
          $<$<CXX_COMPILER_ID:Clang>:${__CFLAGS_CXX_CLANG}> # Clang编译器生效
          $<$<CXX_COMPILER_ID:AppleClang>:${__CFLAGS_CXX_CLANG}>
)
# 添加头文件搜索路径,相当于 -Iinclude
target_include_directories(mylib PUBLIC include)
# 添加库文件查找路径,相当于 -Llib
target_link_directories(mylib PUBLIC lib)
# 添加库链接,相当于 -lfoo
target_link_libraries(mylib PUBLIC foo)
# 添加链接选项,启用lld链接器
target_link_options(mylib PUBLIC -fuse-ld=lld)

属性继承 (手动划重点)

target_开头的一些设置参数的函数是CMake3之后支持的,可以理解为它将target定义成了一个对象,对象中包含了若干成员(编译参数)

代码语言:c 复制
// 以下为解释target罗列的伪代码
struct Target {
  std::list<meta> include_directories;
  std::list<meta> compile_options;
  // others
};
std::list<meta> global_include_directories;
std::list<meta> global_compile_options;
  • target_开头的命令表示针对参数指定的目标生效,如:Target.include_directories
  • 不带target_开头的命令表示当前 CMakeLists.txt极其子CMakeLists.txt 全局生效,如:global_include_directories
  • 一般来说尽量避免添加全局参数,容易造成属性污染。合理利用属性继承能让依赖变得清晰

target_include_directories为例,详细说明参考官方文档

代码语言:text复制
target_include_directories(<target> [SYSTEM] [AFTER|BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

其中SYSTEMAFTER|BEFORE参数不常用,重点讨论继承关系PUBLIC|PRIVATE|INTERFACE

  • PUBLIC-显式依赖:表示当前目标生效,且依赖目标也生效。
  • PRIVATE-隐式依赖:表示仅当前目标生效,依赖目标不生效。
  • INTERFACE:用于INTERFACE类型的目标(如:导入库,空库),依赖目标生效,继承方式等同PUBLIC
代码语言:text复制
# mylib实际包含路径为 include src
target_include_directories(mylib
  PUBLIC include
  PRIVATE src
)
# mylib实际依赖库为 libpublic libprivate
target_link_libraries(mylib
  PUBLIC libpublic
  PRIVATE libprivate
)

# 为myexe添加mylib链接
# myexe实际包含的路径为 include,不包含 src
# myexe实际依赖库为 libpublic,不包含 libprivate
target_link_libraries(myexe PUBLIC mylib)

其余 target_ 开头的属性设置均适用该继承方式,一般来说,INTERFACE使用情况较少,通常用于符号导出等特殊场景。在项目 API 中未包含三方库时,可以选择 PRIVATE 依赖以中断该继承,如果不明确依赖关系,可以写成 PUBLIC

  • 强烈建议所有编译选项尽可能以target为单位
  • 强烈建议所有编译选项尽可能以target为单位
  • 强烈建议所有编译选项尽可能以target为单位

如 libA 需要 include pathA,且 libA 的头文件中包含了 pathA 的定义,libB 依赖 libA 时无需再手动添加 include pathA,target 应当做到自给自足

自定义目标

在 cmake 中,除了库和执行文件可以作为目标,一些自定义操作也可以作为目标,例如编译前需要下载数据,编译完成时将数据拷贝至指定目录等。

通过可以使用 add_custom_command / add_custom_target 命令来添加自定义操作。

生成文件的自定义目标

假设用于需要将一个文件转换成另外一个文件,如:protobuf 通过 IDL 生成源文件,样例如下:

代码语言:text复制
add_custom_command(
  OUTPUT out.cc
  COMMAND protoc -i in.proto -o out.cc
  DEPENDS in.proto
  VERBATIM)

COMMAND之后为实际运行的命令,参数需要根据执行程序作响应调整。根据这一个特征,可以扩展很多实用操作,如:编译前下载源代码,下载测试数据等

基于编译事件的自定义目标

假设用户期望在库B编译完成时,将依赖库A拷贝至库B生成路径,样例如下:

代码语言:text复制
add_library(libA SHARED source_a.cc)
add_library(libB SHARED source_b.cc)
target_link_libraries(libB PUBLIC libA)
# 添加编译完成事件 POST_BUILD
add_custom_command(TARGET libB POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_if_different
  $<TARGET_FILE:libA>
  $<TARGET_FILE_DIR:libB>
)

针对编译事件,cmake 支持 PRE_BUILD | PRE_LINK | POST_BUILD 三个时机,

  • PRE_BUILD:在 Visual Studio 编译系统中,时机为所有编译开始之前,其他系统时仅在PRE_LINK之前
  • PRE_LINK:源文件编译成中间之后,链接成目标文件之前
  • POST_BUILD:链接成目标文件之后

生成器表达式

在 cmake 中,除了常规的命令行,如if(xxx),还支持一种特殊语法 生成器表达式,生成器表达式与常规命令不同,常规命令在 配置 阶段生效,而生成器表达式在 编译 阶段才针对生成器进行计算评估。

上文中自定义命令 add_custom_command 里的 $<TARGET_FILE:libA> 就是经典的使用场景,配置时我们并不确定具体生成的文件路径,可以在执行阶段解析为实际变量,再例如:

代码语言:text复制
target_compile_options(mylib PUBLIC -fno-exceptions
  PRIVATE $<$<COMPILE_LANGUAGE:C>:${__CFLAGS_C}>            # C编译选项
  PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${__CFLAGS_CXX}>        # C  编译选项
  PRIVATE $<$<CXX_COMPILER_ID:GNU>:${__CFLAGS_CXX_GNU}>     # GNU编译器生效
          $<$<CXX_COMPILER_ID:Clang>:${__CFLAGS_CXX_CLANG}> # Clang编译器生效
          $<$<CXX_COMPILER_ID:AppleClang>:${__CFLAGS_CXX_CLANG}>
)

$<CXX_COMPILER_ID:Clang> 表示执行时当 CXX_COMPILER_ID == Clang 则返回 1,否则返回 0,再配合外围的 $<cond:flags> 解析得到当编译器为 Clang 时,表达式返回 ${__CFLAGS_CXX_CLANG},否则表达式返回空。表达式支持很多条件,具体参考 cmake-generator-expressions,灵活运用表达式可以让 cmake避免一堆长长的if/else,使代码变得非常清晰简洁

交叉编译与工具链

交叉编译的本质是使用指定的编译器编译生成指定处理器平台的中间文件,并链接指定的系统库文件,生成最终的目标文件。与本地编译的流程并无不同,交叉指的是执行编译过程的操作系统与运行程序的操作系统不是同一个。如:Android 系统中并无可运行的编译器,生成 Android 可执行的 ELF 文件需要借助其他操作系统。

工具链通常用于指定系统名称、目标处理器类型、编译器、库搜索路径以及编译参数等信息,使用时在 cmake 配置阶段使用变量 CMAKE_TOOLCHAIN_FILE 指定,如:

代码语言:shell复制
cmake .. -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake -DCMAKE_BUILD_TYPE=Debug

本地编译时 cmake 会根据系统环境配置一些必要信息,无需指定 CMAKE_TOOLCHAIN_FILE,遇到交叉编译时通常选择交叉编译工具链,部分 SDK 已经提供 cmake 工具链,如:Android NDKTDA4等,未提供工具链的 SDK,可以通过指定编译器路径等信息来编译,也可以基于 SDK 编写工具链文件便于后续项目使用。编写 cmake 工具链参考官方文档 cmake-toolchains。

在 cmake 中,交叉编译与工具链并非因果关系。交叉编译除了可是使用工具链,也可以在配置阶段通过参数指定编译器等信息实现交叉编译;工具链除了可以用于交叉编译,也可用于编译系统扩展,如:vcpkg 中可用于查找内置的三方库的工具链文件

扩展工具链

微软开源项目中的工具链文件 vcpkg.cmake 为扩展查找三方库的经典样例

交叉编译工具链(高阶)

Android NDK 中交叉编译工具链 ${ANDROID_NDK}/build/cmake/android.toolchain.cmake 几乎包含交叉编译中涉及的所有改动,iOS 由于没有 Apple 没有提供官方支持,仅有开源项目 ios-cmake 可用,下文为 Linux 下 ARM 编译工具链样例:

代码语言:text复制
# 指定目标系统名称,不指定时取CMAKE_SYSTEM_HOST_NAME
set(CMAKE_SYSTEM_NAME Linux)
# 指定目标处理器类型,在部分编译器中需要额外添加编译参数,不指定时取CMAKE_SYSTEM_HOST_PROCESSOR
set(CMAKE_SYSTEM_PROCESSOR arm)

# 指定系统库路径,相当于向编译器指定 --sysroot
set(CMAKE_SYSROOT /home/devel/rasp-pi-rootfs)
# 除了指定CMAKE_SYSROOT,还可以通过设置CMAKE_FIND_ROOT_PATH指定搜索路径

# 指定编译器
set(tools /home/devel/gcc-4.7-linaro-rpi-gnueabihf)
set(CMAKE_C_COMPILER ${tools}/bin/arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER ${tools}/bin/arm-linux-gnueabihf-g  )

# 交叉编译中,该选项需要特别注意
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

扩展编译系统(待完善)

  • FetchContent(cmake3.11 )
  • ExternalProject

ExternalProject 扩展编译系统通常用于下载编译导入三方库,一般会配合两段使用,以导入 GoogleTest 为例,在根目录新建文本文件 CMakeLists.txt.in 并输入:

代码语言:text复制
cmake_minimum_required(VERSION 2.8.2)
project(googletest-download NONE)

include(ExternalProject)
ExternalProject_Add(googletest
  GIT_REPOSITORY    https://github.com/google/googletest.git
  GIT_TAG           master
  SOURCE_DIR        "${CMAKE_BINARY_DIR}/googletest-src"
  BINARY_DIR        "${CMAKE_BINARY_DIR}/googletest-build"
  CONFIGURE_COMMAND ""
  BUILD_COMMAND     ""
  INSTALL_COMMAND   ""
  TEST_COMMAND      ""
)

上述用于加载 ExternalProject,下载指定源码的 master 版本,以及存放路径,还可以指定补丁等。在 CMakeLists.txt 中加入:

代码语言:text复制
configure_file(
  ${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt.in
  ${CMAKE_BINARY_DIR}/googletest-download/CMakeLists.txt
)

# Configure and build the downloaded googletest source
execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" .
  RESULT_VARIABLE result
  WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/googletest-download )
if(result)
  message(FATAL_ERROR "CMake step for googletest failed: ${result}")
endif()

execute_process(COMMAND ${CMAKE_COMMAND} --build .
  RESULT_VARIABLE result
  WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/googletest-download)
if(result)
  message(FATAL_ERROR "Build step for googletest failed: ${result}")
endif()

# Prevent overriding the parent project's compiler/linker settings on Windows
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)

# Add googletest directly to our build. This defines the gtest and gtest_main
# targets.
add_subdirectory(${CMAKE_BINARY_DIR}/googletest-src
                 ${CMAKE_BINARY_DIR}/googletest-build
                 EXCLUDE_FROM_ALL)

上述用于触发 GoogleTest 编译,并将产物配置导入当前项目,该方式不需要编译前下载 GoogleTest 源代码,且可以产物形式导入到项目中。ExternalProject_Add 也可以直接用于下载源代码,以源码形式添加到项目中一起编译,具体根据项目需要选择使用方式。

除去ExternalProject,cmake 在 3.11 版本中加入了FetchContent,功能更实用

合理利用扩展系统,避免将一些三方库或者数据直接塞进仓库中,能避免仓库产生不必要的体积膨胀

策略(Policies)

cmake 中有一个策略机制,用于兼容旧版 cmake,在一些特殊场景下,

0 人点赞