基础知识简介
最低版本要求
这是每个 CMakeLists.txt 都必须包含的第一行
cmake_minimum_required(VERSION 3.1)
顺便提一下关于 CMake 的语法。命令 cmake_minimum_required 是不区分大小写的,所以常用的做法是使用小写1。 VERSION 和它后面的版本号是这个函数的特殊关键字。在这本书中,你可以点击命令的名称来查看它的官方文档,并且可以使用下拉菜单来切换 CMake 的版本。
这一行很特殊2! CMake 的版本与它的特性(policies)相互关联,这意味着它也定义了 CMake 行为的变化。因此,如果你将 cmake_minimum_required 中的 VERSION 设定为 2.8,那么你将会在 macOS 上产生链接错误,例如,即使在 CMake 最新的版本中,如果你将它设置为 3.3 或者更低,那么你将会得到一个隐藏的标志行为(symbols behaviour)错误等。你可以在 policies 中得到一系列 policies 与 versions 的说明。
从 CMake 3.12 开始,版本号可以声明为一个范围,例如 VERSION 3.1...3.15;这意味着这个工程最低可以支持 3.1 版本,但是也最高在 3.15 版本上测试成功过。这对需要更精确(better)设置的用户体验很好,并且由于一个语法上的小技巧,它可以向后兼容更低版本的 CMake (尽管在这里例子中虽然声明为 CMake 3.1-3.15 实际只会设置为 3.1 版本的特性,因为这些版本处理这个工程没有什么差异)。新的版本特性往往对 macOS 和 Windows 用户是最重要的,他们通常使用非常新版本的 CMake。
当你开始一个新项目,起始推荐这么写:
cmake_minimum_required(VERSION 3.7...3.21)
if(${CMAKE_VERSION} VERSION_LESS 3.12)
cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
endif()
如果 CMake 的版本低于3.12,if 块条件为真,CMake 将会被设置为当前版本。如果 CMake 版本是 3.12 或者更高,if 块条件为假,将会遵守 cmake_minimum_required 中的规定,程序将继续正常运行。
WARNING: MSVC 的 CMake 服务器模式起初解析这个语法的时候有一个bug,所以如果你需要支持旧版本的 MSVC 的非命令行的 Windows 构建,你应该这么写:
cmake_minimum_required(VERSION 3.7)
if(${CMAKE_VERSION} VERSION_LESS 3.21)
cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
else()
cmake_policy(VERSION 3.21)
endif()
如果你真的需要在这里设置为一个低版本,你可以使用 cmake_policy 来有条件的提高特性级别或者设置一个特殊的特性。请至少为你的 macOS 用户进行设置!
设置一个项目
接下来,每一个顶层 CMakelists 文件都应该加入下面这一行:
project(MyProject VERSION 1.0
DESCRIPTION "Very nice project"
LANGUAGES CXX)
现在我们看到了更多的语法。这里的字符串是带引号的,因此内容中可以带有空格。项目名称是这里的第一个参数。所有的关键字参数都可选的。VERSION 设置了一系列变量,例如 MyProject_VERSION 和 PROJECT_VERSION。语言可以是 C,CXX,Fortran,ASM,CUDA(CMake 3.8+),CSharp(3.8+),SWIFT(CMake 3.15+ experimental),默认是C CXX。在 CMake 3.9,可以通过DESCRIPTION 关键词来添加项目的描述。这个关于 project 的文档可能会有用。
你可以用 # 来添加注释。CMake 也有一个用于注释的内联语法,但是那极少用到。
项目名称就没有什么特别要注意的。目前为止,我们还没有添加任何的目标(target)。
生成一个可执行文件
尽管库要有趣的多,并且我们会将把大部分时间花在其上。但是现在,先让我们从一个简单的可执行文件开始吧!
add_executable(one two.cpp three.h)
这里有一些语法需要解释。one 既是生成的可执行文件的名称,也是创建的 CMake 目标(target)的名称(我保证,你很快会听到更多关于目标的内容)。紧接着的是源文件的列表,你想列多少个都可以。CMake 很聪明 ,它根据拓展名只编译源文件。在大多数情况下,头文件将会被忽略;列出他们的唯一原因是为了让他们在 IDE 中被展示出来,目标文件在许多 IDE 中被显示为文件夹。你可以在 buildsystem 中找到更多关于一般构建系统与目标的信息。
生成一个库
制作一个库是通过 add_library 命令完成的,并且非常简单:
add_library(one STATIC two.cpp three.h)
你可以选择库的类型,可以是 STATIC,SHARED, 或者MODULE.如果你不选择它,CMake 将会通过 BUILD_SHARED_LIBS 的值来选择构建 STATIC 还是 SHARED 类型的库。
在下面的章节中你将会看到,你经常需要生成一个虚构的目标,也就是说,一个不需要编译的目标。例如,只有一个头文件的库。这被叫做 INTERFACE 库,这是另一种选择,和上面唯一的区别是后面不能有文件名。
你也可以用一个现有的库做一个 ALIAS 库,这只是给已有的目标起一个别名。这么做的一个好处是,你可以制作名称中带有 :: 的库(你将会在后面看到)3 。
目标时常伴随着你
现在我们已经指定了一个目标,那我们如何添加关于它的信息呢?例如,它可能需要包含一个目录:
target_include_directories(one PUBLIC include)
target_include_directories 为目标添加了一个目录。 PUBLIC 对于一个可执行文件目标没有什么含义;但对于库来说,它让 CMake 知道,任何链接到这个目标的目标也必须包含这个目录。其他选项还有 PRIVATE(只影响当前目标,不影响依赖),以及 INTERFACE(只影响依赖)。
接下来我们可以将目标之间链接起来:
add_library(another STATIC another.cpp another.h)
target_link_libraries(another PUBLIC one)
target_link_libraries 可能是 CMake 中最有用也最令人迷惑的命令。这个命令需要指定一个目标 another,并且在给出该目标的名字( another )后为此目标添加一个依赖 one。如果 CMake 项目中不存在名称为 one 的目标(没有定义该 target/目标),那它会直接添加名字为 one 的库到依赖中(一般而言,会去 /usr、CMake 项目指定寻找库的路径等所有能找的路径找到叫 one 的库——译者注)(这也是命令叫 target_link_libraries 的原因)。或者你可以给定一个库的完整路径,或者是链接器标志。最后再说一个有些迷惑性的知识:),经典的 CMake 允许你省略 PUBLIC 关键字,但是你在目标链中省略与不省略混用,那么 CMake 会报出错误。
只要记得在任何使用目标的地方都指定关键字,那么就不会有问题。
目标可以有包含的目录、链接库(或链接目标)、编译选项、编译定义、编译特性(见C++11 章节)等等。正如你将在之后的两个项目章节中看到的,你经常可以得到目标(并且经常是指定目标)来代表所有你使用的库。甚至有些不是真正的库,像 OpenMP,就可以用目标来表示。这也是为什么现代 CMake 如此的棒!
更进一步
看看你是否能理解以下文件。它生成了一个简单的 C++11 的库并且在程序中使用了它。没有依赖。我将在之后讨论更多的 C++ 标准选项,代码中使用的是 CMake 3.8。
cmake_minimum_required(VERSION 3.8)
project(Calculator LANGUAGES CXX)
add_library(calclib STATIC src/calclib.cpp include/calc/lib.hpp)
target_include_directories(calclib PUBLIC include)
target_compile_features(calclib PUBLIC cxx_std_11)
add_executable(calc apps/calc.cpp)
target_link_libraries(calc PUBLIC calclib)
- 在这本书中,我主要避免向你展示错误的做事方式。你可以在网上找到很多关于这个的例子。我偶尔会提到替代方法,但除非是绝对必要,否则不推荐使用这些替代的方法,通常他们只是为了帮助你阅读更旧的 CMake 代码。 ↩
- 有时你会在这里看到
FATAL_ERROR,那是为了支持在 CMake < 2.6 时的错误,现在应该不会有问题了。 ↩
::语法最初是为了INTERFACE IMPORTED库准备的,这些库应该是在当前项目之外定义的。但是,因为如此,大多数的target_*命令对IMPORTED库不起作用,这使得它们难以自己设置。所以,暂时不要使用IMPORTED关键字,而使用ALIAS目标;它在你开始导出目标之前,都表现的很好。这个限制在 CMake 3.11 中得以修复。 ↩
变量与缓存
本地变量
我们首先讨论变量。你可以这样声明一个本地 ( local ) 变量:
set(MY_VARIABLE "value")
变量名通常全部用大写,变量值跟在其后。你可以通过 ${} 来解析一个变量,例如 ${MY_VARIABLE}.1 CMake 有作用域的概念,在声明一个变量后,你只可以在它的作用域内访问这个变量。如果你将一个函数或一个文件放到一个子目录中,这个变量将不再被定义。你可以通过在变量声明末尾添加 PARENT_SCOPE 来将它的作用域置定为当前的上一级作用域。
列表就是简单地包含一系列变量:
set(MY_LIST "one" "two")
你也可以通过 ; 分隔变量,这和空格的作用是一样的:
set(MY_LIST "one;two")
有一些和 list( 进行协同的命令, separate_arguments 可以把一个以空格分隔的字符串分割成一个列表。需要注意的是,在 CMake 中如果一个值没有空格,那么加和不加引号的效果是一样的。这使你可以在处理知道不可能含有空格的值时不加引号。
当一个变量用 ${} 括起来的时候,空格的解析规则和上述相同。对于路径来说要特别小心,路径很有可能会包含空格,因此你应该总是将解析变量得到的值用引号括起来,也就是,应该这样 "${MY_PATH}" 。
缓存变量
CMake 提供了一个缓存变量来允许你从命令行中设置变量。CMake 中已经有一些预置的变量,像 CMAKE_BUILD_TYPE 。如果一个变量还没有被定义,你可以这样声明并设置它。
set(MY_CACHE_VARIABLE "VALUE" CACHE STRING "Description")
这么写不会覆盖已定义的值。这是为了让你只能在命令行中设置这些变量,而不会在 CMake 文件执行的时候被重新覆盖。如果你想把这些变量作为一个临时的全局变量,你可以这样做:
set(MY_CACHE_VARIABLE "VALUE" CACHE STRING "" FORCE)
mark_as_advanced(MY_CACHE_VARIABLE)
第一行将会强制设置该变量的值,第二行将使得用户运行 cmake -L .. 或使用 GUI 界面的时候不会列出该变量。此外,你也可以通过 INTERNAL 这个类型来达到同样的目的(尽管在技术上他会强制使用 STRING 类型,这不会产生任何的影响):
set(MY_CACHE_VARIABLE "VALUE" CACHE INTERNAL "")
因为 BOOL 类型非常常见,你可以这样非常容易的设置它:
option(MY_OPTION "This is settable from the command line" OFF)
对于 BOOL 这种数据类型,对于它的 ON 和 OFF 有几种不同的说辞 (wordings) 。
你可以查看 cmake-variables 来查看 CMake 中已知变量的清单。
环境变量
你也可以通过 set(ENV{variable_name} value) 和 $ENV{variable_name} 来设置和获取环境变量,不过一般来说,我们最好避免这么用。
缓存
缓存实际上就是个文本文件,CMakeCache.txt ,当你运行 CMake 构建目录时会创建它。 CMake 可以通过它来记住你设置的所有东西,因此你可以不必在重新运行 CMake 的时候再次列出所有的选项。
属性
CMake 也可以通过属性来存储信息。这就像是一个变量,但它被附加到一些其他的物体 ( item ) 上,像是一个目录或者是一个目标。一个全局的属性可以是一个有用的非缓存的全局变量。许多目标属性都是被以 CMAKE_ 为前缀的变量来初始化的。例如你设置 CMAKE_CXX_STANDARD 这个变量,这意味着你之后创建的所有目标的 CXX_STANDARD 都将被设为CMAKE_CXX_STANDARD 变量的值。
你可以这样来设置属性:
set_property(TARGET TargetName
PROPERTY CXX_STANDARD 11)
set_target_properties(TargetName PROPERTIES
CXX_STANDARD 11)
第一种方式更加通用 ( general ) ,它可以一次性设置多个目标、文件、或测试,并且有一些非常有用的选项。第二种方式是为一个目标设置多个属性的快捷方式。此外,你可以通过类似于下面的方式来获得属性:
get_property(ResultVariable TARGET TargetName PROPERTY CXX_STANDARD)
可以查看 cmake-properties 获得所有已知属性的列表。在某些情况下,你也可以自己定义一些属性2。
if的条件部分语法有一些奇怪,因为if语法比${}出现的更早,所以它既可以加${}也可以不加${}。 ↩
- 对于接口类的目标,可能对允许自定义的属性有一些限制。 ↩
用 CMake 进行编程
控制流程
CMake 有一个 if 语句,尽管经过多次版本迭代它已经变得非常复杂。这里有一些全大写的变量你可以在 if 语句中使用,并且你既可以直接引用也可以利用 ${} 来对他进行解析( if 语句在历史上比变量拓展出现的更早 )。这是一个 if 语句的例子:
if(variable)
# If variable is `ON`, `YES`, `TRUE`, `Y`, or non zero number
else()
# If variable is `0`, `OFF`, `NO`, `FALSE`, `N`, `IGNORE`, `NOTFOUND`, `""`, or ends in `-NOTFOUND`
endif()
# If variable does not expand to one of the above, CMake will expand it then try again
如果你在这里使用 ${variable} 可能会有一些奇怪,因为看起来它好像 variable 被展开 ( expansion ) 了两次。在 CMake 3.1+ 版本中加入了一个新的特性 ( CMP0054 ) ,CMake 不会再展开已经被引号括起来的展开变量。也就是说,如果你的 CMake 版本大于 3.1 ,那么你可以这么写:
if("${variable}")
# True if variable is not false-like
else()
# Note that undefined variables would be `""` thus false
endif()
这里还有一些关键字可以设置,例如:
- 一元的:
NOT,TARGET,EXISTS(文件),DEFINED, 等。 - 二元的:
STREQUAL,AND,OR,MATCHES( 正则表达式 ),VERSION_LESS,VERSION_LESS_EQUAL( CMake 3.7+ ), 等。 - 括号可以用来分组
generator-expressions
generator-expressions 语句十分强大,不过有点奇怪和专业 ( specialized ) 。大多数 CMake 命令在配置的时候执行,包括我们上面看到的 if 语句。但是如果你想要他们在构建或者安装的时候运行呢,应该怎么写? 生成器表达式就是为此而生1。它们在目标属性中被评估( evaluate ):
最简单的生成器表达式是信息表达式,其形式为 $<KEYWORD>;它会评估和当前配置相关的一系列信息。信息表达式的另一个形式是 $<KEYWORD:value>,其中 KEYWORD 是一个控制评估的关键字,而 value 则是被评估的对象( 这里的 value 中也允许使用信息表达式,如下面的 ${CMAKE_CURRENT_SOURCE_DIR}/include )。如果 KEYWORD 是一个可以被评估为0或1的生成器表达式或者变量,如果(KEYWORD被评估)为1则 value 会在这里被保留下来,而反之则不会。你可以使用嵌套的生成器表达式,你也可以使用变量来使得自己更容易理解嵌套的变量。一些表达式也可以有多个值,值之间通过逗号分隔2。
如果你有一个只想在配置阶段的 DEBUG 模式下开启的编译标志( flag ),你可以这样做:
target_compile_options(MyTarget PRIVATE "$<$<CONFIG:Debug>:--my-flag>")
译者注:这里有点迷惑性,这里其实包含了两种 generator-expression,分别是 configuration-expression 和 conditional-expression,前者使用的形式是 $<CONFIG:cfgs>,这里的 cfgs 是一个 List,如果 CONFIG 满足 cfgs 列表中的任何一个值,这个表达式会被评估(evaluate)为 1,否则为 0。后者使用的形式是 $<condition:true_string>,如果 condition 值为 1,则表达式被评估为 true_string,否则为空值。因此这里表达的含义是,如果这里是一个 DEBUG 的 configuration,就设置 –my-flag。可参见官方文档。
这是一个相比与指定一些形如 *_DEBUG 这样的变量更加新颖并且更加优雅的方式,并且这对所有支持生成器表达式的设置都通用。需要注意的是,你永远不要在配置阶段(configuration phase)使用配置有关值(configure time value),因为在使用像 IDE 这种多配置生成器时你没法在配置阶段获取到这些值,只有在构建阶段使用生成器表达式或者形如 *_<CONFIG> 的变量才能获得。
一些生成器表达式的其他用途:
- 限制某个项目的语言,例如可以限制其语言为 CXX 来避免它和 CUDA 等语言混在一起,或者可以通过封装它来使得他对不同的语言有不同的表现。
- 获得与属性相关的配置,例如文件的位置。
- 为构建和安装生成不同的位置。
最后一个是常见的。你几乎会在所有支持安装的软件包中看到如下代码:
译者注:表示在目标对于直接 BUILD 使用的目标包含的头文件目录为 ${CMAKE_CURRENT_SOURCE_DIR}/include,而安装的目标包含的头文件目录为 include,是一个相对位置(同时需要 install 对应的头文件才可以)。
target_include_directories(
MyTarget
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
宏定义与函数
你可以轻松地定义你自己的 CMake function 或 macro 。函数和宏只有作用域上存在区别,宏没有作用域的限制。所以说,如果你想让函数中定义的变量对外部可见,你需要使用 PARENT_SCOPE 来改变其作用域。如果是在嵌套函数中,这会变得异常繁琐,因为你必须在想要变量对外的可见的所有函数中添加 PARENT_SCOPE 标志。但是这样也有好处,函数不会像宏那样对外“泄漏”所有的变量。接下来用函数举一个例子:
下面是一个简单的函数的例子:
function(SIMPLE REQUIRED_ARG)
message(STATUS "Simple arguments: ${REQUIRED_ARG}, followed by ${ARGN}")
set(${REQUIRED_ARG} "From SIMPLE" PARENT_SCOPE)
endfunction()
simple(This Foo Bar)
message("Output: ${This}")
输出如下:
-- Simple arguments: This, followed by Foo;Bar
Output: From SIMPLE
如果你想要有一个指定的参数,你应该在列表中明确的列出,除此之外的所有参数都会被存储在 ARGN 这个变量中( ARGV 中存储了所有的变量,包括你明确列出的 )。CMake 的函数没有返回值,你可以通过设定变量值的形式来达到同样地目的。在上面的例子中,你可以通过指定变量名来设置一个变量的值。
参数的控制
你应该已经在很多 CMake 函数中见到过,CMake 拥有一个变量命名系统。你可以通过 cmake_parse_arguments 函数来对变量进行命名与解析。如果你想在低于 3.5 版本的CMake 系统中使用它,你应该包含 CMakeParseArguments 模块,此函数在 CMake 3.5 之前一直存在与上述模块中。这是使用它的一个例子:
function(COMPLEX)
cmake_parse_arguments(
COMPLEX_PREFIX
"SINGLE;ANOTHER"
"ONE_VALUE;ALSO_ONE_VALUE"
"MULTI_VALUES"
${ARGN}
)
endfunction()
complex(SINGLE ONE_VALUE value MULTI_VALUES some other values)
在调用这个函数后,会生成以下变量:
COMPLEX_PREFIX_SINGLE = TRUE
COMPLEX_PREFIX_ANOTHER = FALSE
COMPLEX_PREFIX_ONE_VALUE = "value"
COMPLEX_PREFIX_ALSO_ONE_VALUE = <UNDEFINED>
COMPLEX_PREFIX_MULTI_VALUES = "some;other;values"
如果你查看了官方文档,你会发现可以通过 set 来避免在 list 中使用分号,你可以根据个人喜好来确定使用哪种结构。你可以在上面列出的位置参数中混用这两种写法。此外,其他剩余的参数(因此参数的指定是可选的)都会被保存在 COMPLEX_PREFIX_UNPARSED_ARGUMENTS 变量中。
- 他们看起来像是在构建或安装时被评估的,但实际上他们只对每个构建中的配置进行评估。 ↩
- CMake 官方文档中将表达式分为信息表达式,逻辑表达式和输出表达式。 ↩
与你的代码交互
通过 CMake 配置文件
CMake 允许你在代码中使用 configure_file 来访问 CMake 变量。该命令将一个文件( 一般以 .in 结尾 )的内容复制到另一个文件中,并替换其中它找到的所有 CMake 变量。如果你想要在你的输入文件中避免替换掉使用 ${} 包含的内容,你可以使用 @ONLY 关键字。还有一个关键字 COPY_ONLY 可以用来作为 file(COPY 的替代字。
这个功能在 CMake 中使用的相当频繁,例如在下面的 Version.h.in 中:
Version.h.in
#pragma once
#define MY_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define MY_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define MY_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define MY_VERSION_TWEAK @PROJECT_VERSION_TWEAK@
#define MY_VERSION "@PROJECT_VERSION@"
CMake lines:
configure_file (
"${PROJECT_SOURCE_DIR}/include/My/Version.h.in"
"${PROJECT_BINARY_DIR}/include/My/Version.h"
)
在构建你的项目时,你也应该包括二进制头文件路径。如果你想要在头文件中包含一些 true/false 类型的变量,CMake 对 C 语言有特有的 #cmakedefine 和 #cmakedefine01 替换符来完成上述需求。
你也可以使用( 并且是常用 )这个来生成 .cmake 文件,例如配置文件( 见 installing )。
读入文件
另外一个方向也是行得通的, 你也可以从源文件中读取一些东西( 例如版本号 )。例如,你有一个仅包含头文件的库,你想要其在无论有无 CMake 的情况下都可以使用,上述方式将是你处理版本的最优方案。可以像下面这么写:
# Assuming the canonical version is listed in a single line
# This would be in several parts if picking up from MAJOR, MINOR, etc.
set(VERSION_REGEX "#define MY_VERSION[ \t]+\"(.+)\"")
# Read in the line containing the version
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/include/My/Version.hpp"
VERSION_STRING REGEX ${VERSION_REGEX})
# Pick out just the version
string(REGEX REPLACE ${VERSION_REGEX} "\\1" VERSION_STRING "${VERSION_STRING}")
# Automatically getting PROJECT_VERSION_MAJOR, My_VERSION_MAJOR, etc.
project(My LANGUAGES CXX VERSION ${VERSION_STRING})
如上所示, file(STRINGS file_name variable_name REGEX regex) 选择了与正则表达式相匹配的行,并且使用了相同的正则表达式来匹配出其中版本号的部分。
如何组织你的项目
下面的说法可能存在一些偏见,但我认为这是一种好的组织方式。我将会讲解如何组织项目的目录结构,这是基于以往的惯例来写的,这么做对你有以下好处:
- 可以很容易阅读以相同模式组织的项目
- 避免可能造成冲突的组织形式
- 避免使目录结构变得混乱和复杂
首先,如果你创建一个名为 project 的项目,它有一个名为 lib 的库,有一个名为 app 的可执行文件,那么目录结构应该如下所示:
- project
- .gitignore
- README.md
- LICENCE.md
- CMakeLists.txt
- cmake
- FindSomeLib.cmake
- something_else.cmake
- include
- project
- lib.hpp
- src
- CMakeLists.txt
- lib.cpp
- apps
- CMakeLists.txt
- app.cpp
- tests
- CMakeLists.txt
- testlib.cpp
- docs
- CMakeLists.txt
- extern
- googletest
- scripts
- helper.py
其中,文件的名称不是绝对的,你可能会看到关于文件夹名称为 tests 还是 test 的争论,并且应用程序所在的文件夹可能为其他的名称( 或者一个项目只有库文件 )。你也许也会看到一个名为 python 的文件夹,那里存储关于 python 绑定器的内容,或者是一个 cmake 文件夹用于存储如 Find<library>.cmake 这样的 .cmake 辅助文件。但是一些比较基础的东西都在上面包括了。
可以注意到一些很明显的问题, CMakeLists.txt 文件被分割到除了 include 目录外的所有源代码目录下。这是为了能够将 include 目录下的所有文件拷贝到 /usr/include 目录或其他类似的目录下(除了配置的头文件,这个我将会在另一章讲到),因此为了避免冲突等问题,其中不能有除了头文件外的其他文件。这也是为什么在 include 目录下有一个名为项目名的目录。顶层 CMakeLists.txt 中应使用 add_subdirectory 命令来添加一个包含 CMakeLists.txt 的子目录。
你经常会需要一个 cmake 文件夹,里面包含所有用到的辅助模块。这是你放置所有 Find*.cmake 的文件。你可以在 github.com/CLIUtils/cmake 找到一些常见的辅助模块集合。你可以通过以下语句将此目录添加到你的 CMake Path 中:
set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH})
你的 extern 应该几乎只包含 git 子模块( submodule )。通过此方式,你可以明确地控制依赖的版本,并且可以非常轻松地升级。关于添加子模块的例子,可以参见 Testing 章节。
你应该在 .gitignore 中添加形如 /build* 的规则,这样用户就可以在源代码目录下创建 build 目录来构建项目,而不用担心将生成的目标文件添加到 .git 中。有一些软件包禁止这么做,不过这还是相比做一个真正的外部构建并且针对不同的包来使用不同的构建要好的多。
如果你想要避免构建目录在有效的( valid )源代码目录中,你可以在顶层 CMakeLists.txt 文件头部添加如下语句:
### Require out-of-source builds
file(TO_CMAKE_PATH "${PROJECT_BINARY_DIR}/CMakeLists.txt" LOC_PATH)
if(EXISTS "${LOC_PATH}")
message(FATAL_ERROR "You cannot build in a source directory (or any directory with a CMakeLists.txt file). Please make a build subdirectory. Feel free to remove CMakeCache.txt and CMakeFiles.")
endif()
在 CMake 中运行其他的程序
在配置时运行一条命令
在配置时运行一条命令是相对比较容易的。可以使用 execute_process 来运行一条命令并获得他的结果。一般来说,在 CMake 中避免使用硬编码路径是一个好的习惯,你也可以使用 ${CMAKE_COMMAND} , find_package(Git) , 或者find_program 来获取命令的运行权限。可以使用 RESULT_VARIABLE 变量来检查返回值,使用 OUTPUT_VARIABLE 来获得命令的输出。
下面是一个更新所有 git 子模块的例子:
find_package(Git QUIET)
if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git")
execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
RESULT_VARIABLE GIT_SUBMOD_RESULT)
if(NOT GIT_SUBMOD_RESULT EQUAL "0")
message(FATAL_ERROR "git submodule update --init --recursive failed with ${GIT_SUBMOD_RESULT}, please checkout submodules")
endif()
endif()
在构建时运行一条命令
在构建时运行一条命令有点难。主要是目标系统(target system)使这变的很难,你希望你的命令在什么时候运行?它是否会产生另一个目标需要的输出?记住这些需求,然后我们来看一个关于调用 Python 脚本生成头文件的例子:
find_package(PythonInterp REQUIRED)
add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp"
COMMAND "${PYTHON_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/GenerateHeader.py" --argument
DEPENDS some_target)
add_custom_target(generate_header ALL
DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp")
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp DESTINATION include)
在这里,当你在 add_custom_target 命令中添加 ALL 关键字,头文件的生成过程会在 some_target 这些依赖目标完成后自动执行。当你把这个目标作为另一个目标的依赖,你也可以不加 ALL 关键字,那这样他会在被依赖目标构建时会自动执行。或者,你也可以显式地直接构建 generate_header 这个目标。
CMake 中包含的常用的工具
在编写跨平台的 CMake 工程时,一个有用的工具是 cmake -E <mode>(在 CMakeLists.txt 中被写作 ${CMAKE_COMMAND} -E)。通过指定后面的 <mode> 允许 CMake 在不显式调用系统工具的情况下完成一系列事情,例如 copy(复制),make_directory(创建文件夹),和 remove(移除) 。这都是构建时经常使用的命令。 需要注意的是,一个非常有用的 mode——create_symlink,只有在基于 Unix 的系统上可用,但是在 CMake 3.13 后的 Windows 版本中也存在此 mode。点击这里查看对应文档。
一个简单的例子
这是一个简单、完整并且合理的 CMakeLists.txt 的例子。对于这个程序,我们有一个带有头文件与源文件的库文件( MyLibExample ),以及一个带有源文件的应用程序( MyExample )。
# Almost all CMake files should start with this
# You should always specify a range with the newest
# and oldest tested versions of CMake. This will ensure
# you pick up the best policies.
cmake_minimum_required(VERSION 3.1...3.21)
# This is your project statement. You should always list languages;
# Listing the version is nice here since it sets lots of useful variables
project(
ModernCMakeExample
VERSION 1.0
LANGUAGES CXX)
# If you set any CMAKE_ variables, that can go here.
# (But usually don't do this, except maybe for C++ standard)
# Find packages go here.
# You should usually split this into folders, but this is a simple example
# This is a "default" library, and will match the *** variable setting.
# Other common choices are STATIC, SHARED, and MODULE
# Including header files here helps IDEs but is not required.
# Output libname matches target name, with the usual extensions on your system
add_library(MyLibExample simple_lib.cpp simple_lib.hpp)
# Link each target with other targets or add options, etc.
# Adding something we can run - Output name matches target name
add_executable(MyExample simple_example.cpp)
# Make sure you link your targets with this command. It can also link libraries and
# even flags, so linking a target that does not exist will not give a configure-time error.
target_link_libraries(MyExample PRIVATE MyLibExample)
为 CMake 项目添加特性
本节将会涵盖如何为你的 CMake 项目添加特性。你将会学到如何为你的 C++ 项目添加一些常用的选项,如 C++11 支持,以及如何支持 IDE 工具等。
默认的构建类型
CMake 通常会设置一个 “既不是 Release 也不是Debug” 的空构建类型来作为默认的构建类型,如果你想要自己设置默认的构建类型,你可以参考 Kitware blog 中指出的方法。
set(default_build_type "Release")
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
message(STATUS "Setting build type to '${default_build_type}' as none was specified.")
set(CMAKE_BUILD_TYPE "${default_build_type}" CACHE
STRING "Choose the type of build." FORCE)
# Set the possible values of build type for cmake-gui
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Release" "MinSizeRel" "RelWithDebInfo")
endif()
C++11 及后续版本
CMake 中支持 C++11,但是这是针对于 CMake 2.8 及以后的版本来说的。这是为什么?很容易可以猜到, C++11 在 2009年——CMake 2.0 发布的时候还不存在。只要你使用 CMake 的是 CMake 3.1 或者更新的版本,你将会得到 C++11 的完美支持,不过这里有两种不同的方式来启用支持。 并且你将看到,在 CMake 3.8+ 中对 C++11 有着更好的支持。我将会在 CMake 3.8+ 的基础上讲解,因为这才叫做 Modern CMake。
CMake 3.8+: 元编译器选项
只要你使用新版的 CMake 来组织你的项目,那你就能够使用最新的方式来启用 C++ 的标准。这个方式功能强大,语法优美,并且对最新的标准有着很好的支持。此外,它对目标 (target) 进行混合标准与选项设置有着非常优秀的表现。假设你有一个名叫 myTarget 的目标,它看起来像这样:
target_compile_features(myTarget PUBLIC cxx_std_11)
set_target_properties(myTarget PROPERTIES CXX_EXTENSIONS OFF)
对于第一行,我们可以在 cxx_std_11、cxx_std_14 和 cxx_std_17 之间选择。第二行是可选的,但是添加了可以避免 CMake 对选项进行拓展。如果不添加它,CMake 将会添加选项 -std=g++11 而不是 -std=c++11 。第一行对 INTERFACE 这种目标 (target) 也会起作用,第二行只会对实际被编译的目标有效。
如果在目标的依赖链中有目标指定了更高的 C++ 标准,上述代码也可以很好的生效。这只是下述方法的一个更高级的版本,因此可以很好的生效。
CMake 3.1+: 编译器选项
你可以指定开启某个特定的编译器选项。这相比与直接指定 C++ 编译器的版本更加细化,尽管去指定一个包使用的所有编译器选项可能有点困难,除非这个包是你自己写的或者你的记忆力非凡。最后 CMake 会检查你编译器支持的所有选项,并默认设置使用其中每个最新的版本。因此,你不必指定所有你需要的选项,只需要指定那些和默认有出入的。设置的语法和上一部分相同,只是你需要挑选一个列表里面存在的选项而不像是 cxx_std_* 。这里有包含所有选项的列表。
如果你需要可选的选项,在 CMake 3.3+ 中你可以使用列表 CMAKE_CXX_COMPILE_FEATURES 及 if(... INLIST ...) 来查看此选项是否在此项目中被选用,然后来决定是否添加它。可以 在此 查看一些其他的使用情况。
CMake 3.1+: 全局设置以及属性设置
这是支持 C++ 标准的另一种方式,(在目标及全局级别)设置三个特定属性的值。这是全局的属性:
set(CMAKE_CXX_STANDARD 11 CACHE STRING "The C++ standard to use")
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
第一行设置了 C++ 标准的级别, 第二行告诉 CMake 使用上述设置, 最后一行关闭了拓展,来明确自己使用了 -std=c++11 还是 -std=g++11 。这个方法中可以在最终包 (final package) 中使用,但是不推荐在库中使用。你应该总是把它设置为一个缓存变量,这样你就可以很容易地重写其内容来尝试新的标准(或者如果你在库中使用它的话,这是重写它的唯一方式。不过再重申一遍,不要在库中使用此方式)。你也可以对目标来设置这些属性:
set_target_properties(myTarget PROPERTIES
CXX_STANDARD 11
CXX_STANDARD_REQUIRED YES
CXX_EXTENSIONS NO
)
这种方式相比于上面来说更好,但是仍然没法对 PRIVATE 和 INTERFACE 目标的属性有明确的控制,所以他们也仍然只对最终目标 (final targets) 有用。
你可以在 Craig Scott’s useful blog post 这里找到更多关于后面两种方法的信息。
不要自己设置手动标志。如果这么做,你必须对每个编译器的每个发行版设置正确的标志,你无法通过不支持的编译器的报错信息来解决错误,并且 IDE 可能不会去关心手动设置的标志。
为 CMake 项目添加选项
CMake 中有许多关于编译器和链接器的设置。当你需要添加一些特殊的需求,你应该首先检查 CMake 是否支持这个需求,如果支持的话,你就可以不用关心编译器的版本,一切交给 CMake 来做即可。 更好的是,你可以在 CMakeLists.txt 表明你的意图,而不是通过开启一系列标志 (flag) 。
其中最首要,并且最普遍的需求是对 C++ 标准的设定与支持,这个将会单独开一章节讲解。
地址无关代码(Position independent code)
用标志 -fPIC 来设置这个是最常见的。大部分情况下,你不需要去显式地声明它的值。CMake 将会在 SHARED 以及 MODULE 类型的库中自动的包含此标志。如果你需要显式地声明,可以这么写:
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
这样会对全局的目标进行此设置,或者可以这么写:
set_target_properties(lib1 PROPERTIES POSITION_INDEPENDENT_CODE ON)
来对某个目标进行设置是否开启此标志。
Little libraries
如果你需要链接到 dl 库,在 Linux 上可以使用 -ldl 标志,不过在 CMake 中只需要在 target_link_libraries 命令中使用内置的 CMake 变量 ${CMAKE_DL_LIBS} 。这里不需要模组或者使用 find_package 来寻找它。(这个命令包含了调用 dlopen 与 dlclose 的一切依赖)
不幸的是,想要链接到数学库没这么简单。如果你需要明确地链接到它,你可以使用 target_link_libraries(MyTarget PUBLIC m),但是使用 CMake 通用的 find_library 可能更好,如下是一个例子:
find_library(MATH_LIBRARY m)
if(MATH_LIBRARY)
target_link_libraries(MyTarget PUBLIC ${MATH_LIBRARY})
endif()
通过快速搜索,你可以很容易地找到这个和其他你需要的库的 Find*.cmake 文件,大多数主要软件包都具有这个 CMake 模组的辅助库。更多信息请参见包含现有软件包的章节。
程序间优化(Interprocedural optimization)
INTERPROCEDURAL_OPTIMIZATION,最有名的是 链接时间优化 以及 -flto 标志,这在最新的几个 CMake 版本中可用。你可以通过变量 CMAKE_INTERPROCEDURAL_OPTIMIZATION( CMake 3.9+ 可用)或对目标指定 INTERPROCEDURAL_OPTIMIZATION 属性来打开它。在 CMake 3.8 中添加了对 GCC 及 Clang 的支持。如果你设置了 cmake_minimum_required(VERSION 3.9) 或者更高的版本(参考 CMP0069),当在编译器不支持 INTERPROCEDURAL_OPTIMIZATION 时,通过变量或属性启用该优化会产生报错。你可以使用内置模块 CheckIPOSupported 中的 check_ipo_supported() 来检查编译器是否支持 IPO 。下面是基于 CMake 3.9 的一个例子:
include(CheckIPOSupported)
check_ipo_supported(RESULT result)
if(result)
set_target_properties(foo PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()
CCache 和一些其他的实用工具
在过去的一些版本中,一些能够帮助你写好代码的实用工具已经被添加到了 CMake 中。往往是通过为目标指定属性,或是设定形如 CMAKE_* 的初始化变量的值的形式启用相应工具。这个启用的规则不只是对某个特定的工具(program)起作用,一些行为相似的工具都符合此规则。
当需要启用多个工具时,所有的这些变量都通过 ; 分隔(CMake 中列表的分隔标准)来描述你在目标源程序上需要使用的工具( program) 以及选项。
CCache1
通过设置变量 CMAKE_<LANG>_COMPILER_LAUNCHER 或设置目标的 <LANG>_COMPILER_LAUNCHER 属性来使用一些像 CCache 的方式来“封装”目标的编译。在 CMake 的最新版本中拓展了对 CCache 的支持。在使用时,可以这么写:
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
set(CMAKE_CUDA_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") # CMake 3.9+
endif()
一些实用工具
设置以下属性或是在命令行中设置以 CMAKE_* 为起始的变量来启动这些功能。它们大部分只在 make 或 ninja 生成器生成 C 和 CXX 项目时起作用。
<LANG>_CLANG_TIDY: CMake 3.6+<LANG>_CPPCHECK<LANG>_CPPLINT<LANG>_INCLUDE_WHAT_YOU_USE
Clang tidy2
这是在命令行中运行 clang-tidy 的方法,使用的是一个列表(记住,用分号分隔的字符串是一个列表)。
这是一个使用 Clang-Tidy 的简单例子:
~/package # cmake -S . -B build-tidy -DCMAKE_CXX_CLANG_TIDY="$(which clang-tidy);-fix" ~/package # cmake --build build -j 1
这里的 -fix 部分是可选的,将会修改你的源文件来尝试修复 clang-tidy 警告 (warning) 的问题。如果你在一个 git 仓库中工作的话,使用 -fix 是相当安全的,因为你可以看到代码中哪部分被改变了。不过,请确保不要同时运行你的 makefile/ninja 来进行构建!如果它尝试修复一个相同的头文件两次,可能会出现预期外的错误。
如果你想明确的使用目标的形式来确保自己对某些特定的目标调用了 clang-tidy,为可以设置一个变量(例如像 DO_CLANG_TIDY,而不是名为 CMAKE_CXX_CLANG_TIDY 的变量),然后在创建目标时,将它添加为目标的属性。你可以通过以下方式找到路径中的 clang-tidy:
find_program(
CLANG_TIDY_EXE
NAMES "clang-tidy"
DOC "Path to clang-tidy executable"
)
Include what you use3
这是一个使用 include what you use 的例子。首先,你需要确保系统中有这个工具,例如在一个 docker 容器中或者通过 macOS 上的 brew 利用 brew install include-what-you-use 来安装它。然后,你可以通过此方式使用此工具,而不需要修改你的源代码:
~/package # cmake -S . -B build-iwyu -DCMAKE_CXX_INCLUDE_WHAT_YOU_USE=include-what-you-use
最后,你可以重定向输出到文件,然后选择是否应用此修复:
~/package # cmake --build build-iwyu 2> iwyu.out ~/package # fix_includes.py < iwyu.out
(你应该先检查一下这些修复的正确性,或者在修复后对代码进行润色!)
Link what you use
这是一个布尔类型的目标属性,LINK_WHAT_YOU_USE,它将会在链接时检查与目标不相干的文件。
Clang-format4
不幸的是,Clang-format 并没有真正的与 CMake 集成。你可以制作一个自定义的目标(参考 这篇文章,或者你可以尝试自己手动的去运行它。)一个有趣的项目/想法 在这里,不过我还没有亲自尝试过。它添加了一个格式化 (format) 的目标,并且你甚至没法提交没有格式化过的文件。
下面的两行可以在一个 git 仓库中,在 bash 中使用 clang-format 工具(假设你有一个 .clang-format 文件):
gitbook $ git ls-files -- '*.cpp' '*.h' | xargs clang-format -i -style=file gitbook $ git diff --exit-code --color
译者注:以下所有的脚注说明都为译者添加,原文并不包含此信息。脚注的说明资料均来自于互联网。
- Ccache(或 ”ccache“)是一个编译器缓存。它通过缓存之前的编译文件并且利用之前已经完成的编译过程来加速重编译
- clang-tidy 是一个基于 clang 的 C++ 代码分析工具。它意图提供一个可扩展的框架,用于诊断和修复典型的编程错误,如样式违规、接口误用、或通过静态分析推断出的错误。clang-tidy 是一个模块化的程序,为编写新的检查规则提供了方便的接口。 ↩
- 一个与 clang 一起使用,用于分析 C 和 C++ 源文件中 #include 的工具。 ↩
CMake 中一些有用的模组
在 CMake 的 modules 集合了很多有用的模组,但是有一些模块相比于其他的更有用。以下是一些比较出彩的:
CMakeDependentOption
这增加了命令 cmake_dependent_option ,它根据另外一组变量是否为真来(决定是否)开启一个选项。下面是一个例子:
include(CMakeDependentOption)
cmake_dependent_option(BUILD_TESTS "Build your tests" ON "VAL1;VAL2" OFF)
如上代码是下面的一个缩写:
if(VAL1 AND VAL2)
set(BUILD_TESTS_DEFAULT ON)
else()
set(BUILD_TESTS_DEFAULT OFF)
endif()
option(BUILD_TESTS "Build your tests" ${BUILD_TESTS_DEFAULT})
if(NOT BUILD_TESTS_DEFAULT)
mark_as_advanced(BUILD_TESTS)
endif()
需要注意的是,如果你使用了 include(CTest) ,用 BUILD_TESTING 来检测是否启用是更好的方式,因为它就是为此功能而生的。这里只是一个 CMakeDependentOption 的例子。
CMakePrintHelpers
这个模块包含了几个方便的输出函数。cmake_print_properties 可以让你轻松的打印属性,而 cmake_print_variables 将打印出你给它任意变量的名称和值。
CheckCXXCompilerFlag
这个模块允许你检查编译器是否支持某个标志,例如:
include(CheckCXXCompilerFlag)
check_cxx_compiler_flag(-someflag OUTPUT_VARIABLE)
需要注意的是 OUTPUT_VARIABLE 也会出现在打印的配置输出中,所以请选个不错的变量名。
这只是许多类似模块中的一个,例如 CheckIncludeFileCXX、CheckStructHasMember、TestBigEndian 以及CheckTypeSize,它们允许你检查系统的信息(并且你可以在代码中使用这些信息)。
try_compile/try_run
准确的说,这不是一个模块,但是它们对上述列出的许多模块至关重要。通过它你可以在配置时尝试编译(也可能是运行)一部分代码。这可以让你在配置时获取关于系统能力的信息。基本的语法如下:
try_compile(
RESULT_VAR
bindir
SOURCES
source.cpp
)
这里有很多可以添加的选项,例如 COMPILE_DEFINITIONS。在 CMake 3.8+ 中, 这将默认遵循 CMake 中 C/C++/CUDA 的标准设置。如果你使用的是 try_run 而不是 try_compile,它将运行生成的程序并将运行结果存储在 RUN_OUTPUT_VARIABLE 中。
FeatureSummary
这是一个十分有用但是也有些奇怪的模块。它能够让你打印出找到的所有软件包以及你明确设定的所有选项。它和 find_package 有一些联系。像其他模块一样,你首先要包括模块:
include(FeatureSummary)
然后,对于任何你已经运行或者将要运行的 find_package ,你可以这样拓展它的默认信息:
set_package_properties(OpenMP PROPERTIES
URL "http://www.openmp.org"
DESCRIPTION "Parallel compiler directives"
PURPOSE "This is what it does in my package")
你也可以将包的 TYPE 设置为 RUNTIME、OPTIONAL、RECOMMENDED 或者 REQUIRED。但是你不能降低包的类型,如果你已经通过 find_package 添加了一个 REQUIRED 类型的包,你将会看到你不能改变它的 TYPE:
并且,你可以添加任何选项让其成为 feature summary 的一部分。如果你添加的选项名与包的名字一样,他们之间会互相产生影响:
add_feature_info(WITH_OPENMP OpenMP_CXX_FOUND "OpenMP (Thread safe FCNs only)")
然后,你可以将所有特性 (features) 的集合打印到屏幕或日志文件中:
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
feature_summary(WHAT ENABLED_FEATURES DISABLED_FEATURES PACKAGES_FOUND)
feature_summary(FILENAME ${CMAKE_CURRENT_BINARY_DIR}/features.log WHAT ALL)
endif()
你可以建立一个 WHAT 目标来集合任何你想查看的特性 (features),或者直接使用 ALL 目标也行。
CMake 对 IDE 的支持
一般来说,IDE 已经被标准的 CMake 的项目支持。不过这里有一些额外的东西可以帮助 IDE 表现得更好:
用文件夹来组织目标 (target)
一些 IDE,例如 Xcode,支持文件夹。你需要手动的设定 USE_FOLDERS 这个全局属性来允许 CMake 使用文件夹组织你的文件:
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
然后,你可以在创建目标后,为目标添加文件夹属性,即将其目标 MyFile 归入到 Scripts 文件夹中:
set_property(TARGET MyFile PROPERTY FOLDER "Scripts")
文件夹可以使用 / 进行嵌套。
你可以使用正则表达式或在 source_group 使用列表来控制文件在文件夹中是否可见。
用文件夹来组织文件
你也可以控制文件夹对目标是否可见。有两种方式,都是使用 source_group 命令,传统的方式是:
source_group("Source Files\\New Directory" REGULAR_EXPRESSION ".*\\.c[ucp]p?")
你可以用 FILES 来明确的列出文件列表,或者使用 REGULAR_EXPRESSION 来进行筛选。通过这个方式你可以完全的掌控文件夹的结构。不过,如果你的文件已经在硬盘中组织的很好,你可能只是想在 CMake 中复现这种组织。在 CMake 3.8+ 中,你可以用新版的 source_group 命令非常容易的做到上述情形:
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/base/dir" PREFIX "Header Files" FILES ${FILE_LIST})
对于 TREE 选项,通常应该给出一个以 ${CMAKE_CURRENT_SOURCE_DIR} 起始的完整路径(因为此命令的文件解析路径是相对于构建目录的)。这个 PREFIX 设置文件将在 IDE 结构中的位置,而 FILES 选项是包含一些文件的列表 (FILE_LIST)。CMake 将会解析 TREE 路径下 FILE_LIST 中包含的文件,并将每个文件添加到 PREFIX 结构下,这构成了 IDE 的文件夹结构。
注意:如果你需要支持低于 3.8 版本的CMake,我不建议你使用上述命令,只建议在 CMake 3.8+ 中使用上述文件夹布局。对于做这种文件夹布局的旧方法,请参见 这篇博文。
在 IDE 中运行CMake
要使用 IDE,如果 CMake 可以生成对应 IDE 的文件(例如 Xcode,Visual Studio),可以通过 -G"name of IDE" 来完成,或者如果 IDE 已经内置了对 CMake 的支持(例如 CLion,QtCreator 和一些其他的 IDE),你可以直接在 IDE 中打开 CMakeLists.txt 来运行 CMake。
调试代码
你可能需要对你的 CMake 构建过程或你的 C++ 代码进行调试。本文将介绍这两者。
调试 CMake
首先,让我们来盘点一下调试 CMakeLists 和其他 CMake 文件的方法。
打印变量
通常我们使用的打印语句如下:
message(STATUS "MY_VARIABLE=${MY_VARIABLE}")
然而,通过一个内置的模组 CMakePrintHelpoers 可以更方便的打印变量:
include(CMakePrintHelpers)
cmake_print_variables(MY_VARIABLE)
如果你只是想要打印一个变量,那么上述方法已经很好用了!如果你想要打印一些关于某些目标 (或者是其他拥有变量的项目,比如 SOURCES、DIRECTORIES、TESTS , 或 CACHE_ENTRIES - 全局变量好像因为某些原因缺失了) 的变量,与其一个一个打印它们,你可以简单的列举并打印它们:
cmake_print_properties(
TARGETS my_target
PROPERTIES POSITION_INDEPENDENT_CODE
)
跟踪运行
你可能想知道构建项目的时候你的 CMake 文件究竟发生了什么,以及这些都是如何发生的?用 --trace-source="filename" 就很不错,它会打印出你指定的文件现在运行到哪一行,让你可以知道当前具体在发生什么。另外还有一些类似的选项,但这些命令通常给出一大堆输出,让你找不着头脑。
例子:
cmake -S . -B build --trace-source=CMakeLists.txt
如果你添加了 --trace-expand 选项,变量会直接展开成它们的值。
以 debug 模式构建
对于单一构建模式的生成器 (single-configuration generators),你可以使用参数 -DCMAKE_BUILD_TYPE=Debug 来构建项目,以获得调试标志 (debugging flags)。对于支持多个构建模式的生成器 (multi-configuration generators),像是多数IDE,你可以在 IDE 里打开调试模式。这种模式有不同的标志(变量以 _DEBUG 结尾,而不是 _RELEASE 结尾),以及生成器表达式的值 CONFIG:Debug 或 CONFIG:Release。
如果你使用了 debug 模式构建,你就可以在上面运行调试器了,比如 gdb 或 lldb。
包含子项目
这就是将一个好的 Git 系统与 CMake 共同使用的优势所在。虽然靠这种方法无法解决世界上所有的问题,但可以解决大部分基于 C++ 的工程包含子项目的问题!
本章中列出了几种包含子项目的方法。
Git 子模组(Submodule)
如果你想要添加一个 Git 仓库,它与你的项目仓库使用相同的 Git 托管服务(诸如 GitHub、GitLab、BitBucker 等等),下面是正确的添加一个子模组到 extern 目录中的命令:
gitbook $ git submodule add ../../owner/repo.git extern/repo
此处的关键是使用相对于你的项目仓库的相对路径,它可以保证你使用与主仓库相同的访问方式( ssh 或 https )访问子模组。这在大多数情况都能工作得相当好。当你在一个子模组里的时候,你可以把它看作一个正常的仓库,而当你在主仓库里时,你可以用 add 来改变当前的提交指针。
但缺点是你的用户必须懂 git submodule 命令,这样他们才可以 init 和 update 仓库,或者他们可以在最开始克隆你的仓库的时候加上 --recursive 选项。针对这种情况,CMake 提供了一种解决方案:
find_package(Git QUIET)
if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git")
# Update submodules as needed
option(GIT_SUBMODULE "Check submodules during build" ON)
if(GIT_SUBMODULE)
message(STATUS "Submodule update")
execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
RESULT_VARIABLE GIT_SUBMOD_RESULT)
if(NOT GIT_SUBMOD_RESULT EQUAL "0")
message(FATAL_ERROR "git submodule update --init --recursive failed with ${GIT_SUBMOD_RESULT}, please checkout submodules")
endif()
endif()
endif()
if(NOT EXISTS "${PROJECT_SOURCE_DIR}/extern/repo/CMakeLists.txt")
message(FATAL_ERROR "The submodules were not downloaded! GIT_SUBMODULE was turned off or failed. Please update submodules and try again.")
endif()
第一行使用 CMake 自带的 FindGit.cmake 检测是否安装了 Git 。然后,如果项目源目录是一个 git 仓库,则添加一个选项(默认值为 ON),用户可以自行决定是否打开这个功能。然后我们运行命令来获取所有需要的仓库,如果该命令出错了,则 CMake 配置失败,同时会有一份很好的报错信息。最后无论我们以什么方式获取了子模组,CMake 都会检查仓库是否已经被拉取到本地。你也可以使用 OR 来列举其中的几个。
现在,你的用户可以完全忽视子模组的存在了,而你同时可以拥有良好的开发体验!唯一需要开发者注意的一点是,如果你正在子模组里开发,你会在重新运行 CMake 的时候重置你的子模组。只需要添加一个新的提交到主仓库的暂存区,就可以避免这个问题。
然后你就可以添加对 CMake 有良好支持的项目了:
add_subdirectory(extern/repo)
或者,如果这是一个只有头文件的库,你可以创建一个接口库目标 (interface library target) 。或者,如果支持的话,你可以使用find_package,可能初始的搜索目录就是你所添加的目录(查看文档或你所使用的Find*.cmake文件)。如果你追加到你的CMAKE_MODULE_PATH,你也可以包括一个CMake帮助文件目录,例如添加pybind11改进过的FindPython*.cmake文件。
小贴士:获取 Git 版本号
将下面的命令加入到上述 Git 更新子仓库的那段中:
execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
OUTPUT_VARIABLE PACKAGE_GIT_VERSION
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE)
使用 CMake 下载项目
在构建时 (build time) 下载
直到 CMake 3.11, 主流的下载包的方法都在构建时进行。这(在构建时下载)会造成几个问题;其中最主要问题的是 add_subdirectory 不能对一个尚不存在的文件夹使用!因此,我们导入的外部项目内置的工具必须自己构建自己(这个外部项目)来解决这个问题。(同时,这种方法也能用于构建不支持 CMake 的包)1
- 注意,外部数据就是不在包内的数据的工具。 ↩
在配置时 (configure time) 下载
如果你更喜欢在配置时下载,看看这个仓库 Crascit/DownloadProject ,它提供了插件式(不需要改变你原有的 CMakeLists.txt)的解决方案。但是,子模块 (submodules) 很好用,以至于我已经停止了使用 CMake 对诸如 GoogleTest 之类的项目的下载,并把他们加入到了子模块中。自动下载在没有网络访问的环境下也是难以实现的,并且外部项目经常被下载到构建目录中,如果你有多个构建目录,这就既浪费时间又浪费空间。
获取软件包(FetchContent) (CMake 3.11+)
有时你想要在配置的时候下载数据或者是包,而不是在编译的时候下载。这种方法已经被第三方包重复“发明”了好几次。最终,这种方法在 CMake 3.11 中以 FetchContent 模块的形式出现。
FetchContent 模块有出色的文档,我在此不会赘述。我会阐述这样几个步骤:
- 使用
FetchContent_Declare(MyName)来从 URL、Git 仓库等地方获取数据或者是软件包。 - 使用
FetchContent_GetProperties(MyName)来获取MyName_*等变量的值,这里的MyName是上一步获取的软件包的名字。 - 检查
MyName_POPULATED是否已经导出,否则使用FetchContent_Populate(MyName)来导出变量(如果这是一个软件包,则使用add_subdirectory("${MyName_SOURCE_DIR}" "${MyName_BINARY_DIR}"))
比如,下载 Catch2 :
FetchContent_Declare(
catch
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v2.13.6
)
# CMake 3.14+
FetchContent_MakeAvailable(catch)
如果你不能使用 CMake 3.14+ ,可以使用适用于低版本的方式来加载:
# CMake 3.11+
FetchContent_GetProperties(catch)
if(NOT catch_POPULATED)
FetchContent_Populate(catch)
add_subdirectory(${catch_SOURCE_DIR} ${catch_BINARY_DIR})
endif()
当然,你可以将这些语句封装到一个宏内:
if(${CMAKE_VERSION} VERSION_LESS 3.14)
macro(FetchContent_MakeAvailable NAME)
FetchContent_GetProperties(${NAME})
if(NOT ${NAME}_POPULATED)
FetchContent_Populate(${NAME})
add_subdirectory(${${NAME}_SOURCE_DIR} ${${NAME}_BINARY_DIR})
endif()
endmacro()
endif()
这样,你就可以在 CMake 3.11+ 里使用 CMake 3.14+ 的语法了。
可以在这里查看例子。
测试
General Testing Information
你需要在你的主 CMakeLists.txt 文件中添加如下函数调用(而不是在子文件夹 CMakeLists.txt 中):
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
include(CTest)
endif()
这么做将可以使得具有 CMake 测试功能,并且具有一个 BUILD_TESTING 选项使得用户可以选择开启或关闭测试(还有一些其他的设置)。或者你可以直接通过调用 enable_testing() 函数来开启测试。
当你添加你自己的测试文件夹时,你应该这么做:
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME AND BUILD_TESTING)
add_subdirectory(tests)
endif()
这么做的(译者注:需要添加 CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)的原因是,如果有他人包含了你的包,并且他们开启了 BUILD_TESTING 选项,但他们并不想构建你包内的测试单元,这样会很有用。在极少数的情况下他们可能真的想要开启所有包的测试功能,你可以提供给他们一个可以覆盖的变量(如下例的 MYPROJECT_BUILD_TESTING,当设置 MYPROJECT_BUILD_TESTING 为 ON 时,会开启该项目的测试功能):
if((CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME OR MYPROJECT_BUILD_TESTING) AND BUILD_TESTING)
add_subdirectory(tests)
endif()
你可以这样注册一个测试目标(targets):
add_test(NAME TestName COMMAND TargetName)
如果你在 COMMAND 后写了除 TargetName 之外的东西,他将会被注册为在命令行运行的指令。在这里写生成器表达式(generator-expression)也是有效的:
add_test(NAME TestName COMMAND $<TARGET_FILE:${TESTNAME}>)
这么写将会使用该目标生成的文件(也就是生成的可执行文件)的路径作为参数。
将构建作为测试的一部分
如果你想在测试时运行 CMake 构建一个项目,这也是可以的(事实上,这也是 CMake 如何进行自我测试的)。例如,如果你的主项目名为 MyProject 并且你有一个 examples/simple 项目需要在测试时构建,那么可以这么写:
add_test(
NAME
ExampleCMakeBuild
COMMAND
"${CMAKE_CTEST_COMMAND}"
--build-and-test "${My_SOURCE_DIR}/examples/simple"
"${CMAKE_CURRENT_BINARY_DIR}/simple"
--build-generator "${CMAKE_GENERATOR}"
--test-command "${CMAKE_CTEST_COMMAND}"
)
测试框架
可以查看子章节了解主流测试框架的使用方式(recipes):
- GoogleTest: 一个 Google 出品的主流测试框架。不过开发可能有点慢。
- Catch2: 一个现代的,具有灵巧的宏的 PyTest-like 的测试框架。
- DocTest: 一个 Catch2 框架的替代品,并且编译速度更快、更干净(cleaner)。See Catch2 chapter and replace with DocTest.
GoogleTest
GoogleTest 和 GoogleMock 是非常经典的选择;不过就我个人经验而言,我会推荐你使用 Catch2,因为 GoogleTest 十分遵循谷歌的发展理念;它假定用户总是想使用最新的技术,因此会很快的抛弃旧的编译器(不对其适配)等等。添加 GoogleMock 也常常令人头疼,并且你需要使用 GoogleMock 来获得匹配器(matchers),这在 Catch2 是一个默认特性,而不需要手动添加(但 docstest 没有这个特性)。
子模块(Submodule)的方式(首选)
当使用这种方式,只需要将 GoogleTest 设定(checkout) 为一个子模块:1
git submodule add --branch=release-1.8.0 ../../google/googletest.git extern/googletest
然后,在你的主 CMakeLists.txt 中:
option(PACKAGE_TESTS "Build the tests" ON)
if(PACKAGE_TESTS)
enable_testing()
include(GoogleTest)
add_subdirectory(tests)
endif()
我推荐你使用一些像 PROJECT_NAME STREQUAL CMAKE_PROJECT_NAME 来设置 PACKAGE_TEST 选项的默认值,因为这样只会在项目为主项目时才构建测试单元。
像之前提到的,你必须在你的主 CMakeLists.txt 文件中调用 enable_testing() 函数。 现在,在你的 tests 目录中:
add_subdirectory("${PROJECT_SOURCE_DIR}/extern/googletest" "extern/googletest")
如果你在你的主 CMakeLists.txt 中调用它,你可以使用普通的 add_subdirectory;这里因为我们是从子目录中调用的,所以我们需要一个额外的路径选项来更正构建路径。
下面的代码是可选的,它可以让你的 CACHE 更干净:
mark_as_advanced(
BUILD_GMOCK BUILD_GTEST BUILD_SHARED_LIBS
gmock_build_tests gtest_build_samples gtest_build_tests
gtest_disable_pthreads gtest_force_shared_crt gtest_hide_internal_symbols
)
If you are interested in keeping IDEs that support folders clean, I would also add these lines:
set_target_properties(gtest PROPERTIES FOLDER extern)
set_target_properties(gtest_main PROPERTIES FOLDER extern)
set_target_properties(gmock PROPERTIES FOLDER extern)
set_target_properties(gmock_main PROPERTIES FOLDER extern)
然后,为了增加一个测试,推荐使用下面的宏:
macro(package_add_test TESTNAME)
# create an exectuable in which the tests will be stored
add_executable(${TESTNAME} ${ARGN})
# link the Google test infrastructure, mocking library, and a default main fuction to
# the test executable. Remove g_test_main if writing your own main function.
target_link_libraries(${TESTNAME} gtest gmock gtest_main)
# gtest_discover_tests replaces gtest_add_tests,
# see https://cmake.org/cmake/help/v3.10/module/GoogleTest.html for more options to pass to it
gtest_discover_tests(${TESTNAME}
# set a working directory so your project root so that you can find test data via paths relative to the project root
WORKING_DIRECTORY ${PROJECT_DIR}
PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "${PROJECT_DIR}"
)
set_target_properties(${TESTNAME} PROPERTIES FOLDER tests)
endmacro()
package_add_test(test1 test1.cpp)
这可以简单、快速的添加测试单元。你可以随意更改来满足你的需求。如果你之前没有了解过 ARGN,ARGN 是显式声明的参数外的所有参数。如 package_add_test(test1 test1.cpp a b c),ARGN 包含除 test1 与 test1.cpp 外的所有参数。
可以更改宏来满足你的要求。例如,如果你需要链接不同的库来进行不同的测试,你可以这么写:
macro(package_add_test_with_libraries TESTNAME FILES LIBRARIES TEST_WORKING_DIRECTORY)
add_executable(${TESTNAME} ${FILES})
target_link_libraries(${TESTNAME} gtest gmock gtest_main ${LIBRARIES})
gtest_discover_tests(${TESTNAME}
WORKING_DIRECTORY ${TEST_WORKING_DIRECTORY}
PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "${TEST_WORKING_DIRECTORY}"
)
set_target_properties(${TESTNAME} PROPERTIES FOLDER tests)
endmacro()
package_add_test_with_libraries(test1 test1.cpp lib_to_test "${PROJECT_DIR}/european-test-data/")
下载的方式
你可以通过 CMake 的 include 指令使用使用我在 CMake helper repository 中的下载器,
这是一个 GoogleTest 的下载器,基于优秀的 DownloadProject 工具。为每个项目下载一个副本是使用 GoogleTest 的推荐方式(so much so, in fact, that they have disabled the automatic CMake install target), so this respects that design decision. 这个方式在项目配置时下载 GoogleTest,所以 IDEs 可以正确的找到这些库。这样使用起来很简单:
cmake_minimum_required(VERSION 3.10)
project(MyProject CXX)
list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
enable_testing() # Must be in main file
include(AddGoogleTest) # Could be in /tests/CMakeLists.txt
add_executable(SimpleTest SimpleTest.cu)
add_gtest(SimpleTest)
提示:
add_gtest只是一个添加gtest,gmock以及gtest_main的宏,然后运行add_test来创建一个具有相同名字的测试单元target_link_libraries(SimpleTest gtest gmock gtest_main) add_test(SimpleTest SimpleTest)
FetchContent: CMake 3.11
这个例子是用 FetchContent 来添加 GoogleTest:
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.8.0
)
FetchContent_GetProperties(googletest)
if(NOT googletest_POPULATED)
FetchContent_Populate(googletest)
add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR})
endif()
- 在这里我假设你在 Github 仓库中使用 googletest,然后使用的是 googletest 的相对路径。 ↩
Catch
Catch2(只有 C++11 版本)是一个独立且强大的测试工具,它的理念(philosophy)类似于 Python 中的 Pytest。他比 GTest 支持更多的编译器版本,并且会紧跟潮流支持新的事物,比如支持在 M1 版本 MacOS 上使用 Catch。他也有一个相似但是更加快速的双胞胎兄弟,doctest,他编译十分迅速但是缺少了一些类似于匹配器(features)的特性。为了在 CMake 项目中使用 Catch,下面是一些可选的方式:
如何配置
Catch 对 CMake 支持很友好,不过你还是需要下载整个仓库来使用他。无论是使用 submodules 还是FetchContent 都可以。extended-project 与 fetch 这两个示例用的都是 FetchContent 的方式。更多的可以参考官方文档。
Quick download
这可能是最简单并且对老版本 CMake 适配性更好的方式。你可以一步到位地直接下载一个 All-in-one 的头文件:
add_library(catch_main main.cpp)
target_include_directories(catch_main PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
set(url https://github.com/philsquared/Catch/releases/download/v2.13.6/catch.hpp)
file(
DOWNLOAD ${url} "${CMAKE_CURRENT_BINARY_DIR}/catch.hpp"
STATUS status
EXPECTED_HASH SHA256=681e7505a50887c9085539e5135794fc8f66d8e5de28eadf13a30978627b0f47)
list(GET status 0 error)
if(error)
message(FATAL_ERROR "Could not download ${url}")
endif()
target_include_directories(catch_main PUBLIC "${CMAKE_CURRENT_BINARY_DIR}")
在 Catch 3 发布后,你可能需要下载两个文件,因为现在需要两个文件进行测试(但是你不再需要自己写 main.cpp 文件)。这个 main.cpp 文件看起来像这样:
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
Vendoring
如果你已经把 Catch 加入到你项目的一部分(放到了一个单独的文件夹中),你可以这样来使用 Catch:
# Prepare "Catch" library for other executables
set(CATCH_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/extern/catch)
add_library(Catch2::Catch IMPORTED INTERFACE)
set_property(Catch2::Catch PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${CATCH_INCLUDE_DIR}")
然后,你需要链接到 Catch2::Catch。你也可以把它作为一个 INTERFACE 目标,因为你不会导出你的测试模块。
Direct inclusion
如果你使用 ExternalProject,FetchContent 或者 git submodules 的形式来添加库,你也可以使用 add_subdirectory 。(CMake 3.1+)
Catch 还提供了两个 CMake 模块(modules),你可以通过这个来注册独立的测试。
导出与安装
让别人使用库有三种好方法和一种坏方法:
查找模块(不好的方式)
Find<mypackage>.cmake 脚本是为那些不支持 CMake 的库所设计,所以已经使用 CMake 的库,不要创建这个脚本文件!可以使用 Config<mypackage>.cmake,具体方式如下所示。
添加子项目
可以将项目作为一个子目录放置于包中,接着使用 add_subdirectory 添加相应的子目录,这适用于纯头文件和快速编译的库。还需要注意的是,安装命令可能会干扰父项目,因此可以使用 add_subdirectory 的EXCLUDE_FROM_ALL选项;当显式使用的目标时,仍然会进行构建。
作为库的作者,请使用 CMAKE_CURRENT_SOURCE_DIR 而非 PROJECT_SOURCE_DIR (对于其他变量也是如此,比如CMAKE_CURRRENT_BINARY_DIR)。通过检查 CMAKE_PROJECT_NAME 和 PROJECT_NAME 的内容是否相同 (STREQUAL),可以只添加对项目有意义的选项或默认值。
此外,使用命名空间也是不错的方式。使用库的方式应该与下面的一致,应该对所有方法的使用进行标准化。
add_library(MyLib::MyLib ALIAS MyLib)
这里的 ALIAS(别名)目标不会在后面导出。
导出
第三种方法是 *Config.cmake 脚本,这将是下一章的主题。
安装
进行安装时,比如执行 make install,安装命令会将文件或目标“安装”到安装树中。简单使用目标安装指令的方式:
install(TARGETS MyLib
EXPORT MyLibTargets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include
)
当有一个库、静态库或程序要安装时,才需要将不同的文件安装到不同的目的地。由于目标不安装包含目录,所以 包含(INCLUDES)目标是特殊的。只能在导出的目标上设置包含目录(通常由target_include_directories设置,若想要清理 cmake 文件,需要检查 MyLibTargets 文件,确定没有多次包含同一个包含目录)。
给定 CMake 可访问的版本是个不错的方式。使用 find_package 时,可以这样指定版本信息:
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
MyLibConfigVersion.cmake
VERSION ${PACKAGE_VERSION}
COMPATIBILITY AnyNewerVersion
)
接下来有两个选择。创建MyLibConfig.cmake,可以直接将目标导出放在这个文件中,或者手动写入,然后包目标文件。若有依赖项(可能只是 OpenMP),则需要添加相应的选项。下面是个例子:
首先,创建一个安装目标文件(类似于在构建目录中创建的文件):
install(EXPORT MyLibTargets
FILE MyLibTargets.cmake
NAMESPACE MyLib::
DESTINATION lib/cmake/MyLib
)
该文件将获取导出目标,并将其放入文件中。若没有依赖项,只需使用 MyLibConfig.cmake 代替 MyLibTargets.cmake 即可。然后,在源码树的某处,创建一个自定义 MyLibConfig.cmake 文件。若想要捕获配置时的变量,可以使用 .in 文件,并且可以使用 @var@ 语法。具体方式如下所示:
include(CMakeFindDependencyMacro)
# Capturing values from configure (optional)
set(my-config-var @my-config-var@)
# Same syntax as find_package
find_dependency(MYDEP REQUIRED)
# Any extra setup
# Add the targets file
include("${CMAKE_CURRENT_LIST_DIR}/MyLibTargets.cmake")
现在,可以使用配置文件(若使用 .in 文件),然后安装已生成的文件。因为创建了ConfigVersion文件,所以可以在这里安装它。
configure_file(MyLibConfig.cmake.in MyLibConfig.cmake @ONLY)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/MyLibConfigVersion.cmake"
DESTINATION lib/cmake/MyLib
)
就是这样!现在,当包安装完成后,lib/cmake/MyLib 中就出现了 CMake 搜索所需的文件(特别是MyLibConfig.cmake和MyLibConfigVersion.cmake),配置时使用的目标文件应该也在那里。
当 CMake 搜索包时,将在当前安装目录,以及几个标准位置中进行查找。可以手动将相应的目录添加到搜索路径中,包括 MyLib_PATH。若没有找到配置文件,CMake 会输出相应的信息,告知用户当前的情况。
导出
CMake 3.15 中,导出的默认行为发生了变化。由于更改用户主目录中的文件是“令人惊讶的”(确实如此,这就是本章存在的原因),因此不再是默认行为。若将 CMake 的最小或最大版本设置为 3.15+,这种情况将不再发生,除非将 CMAKE_EXPORT_PACKAGE_REGISTRY 设置为ON。
CMake 访问项目有三种方式:子目录、导出构建目录和安装。要使用项目的构建目录,就需要导出目标。正确的安装需要导出目标,使用构建目录只需要再增加了两行代码,但这并不是我推荐的工作方式。不过,对于开发和安装过程来说的确好用。
还需要创建导出集,可能要放在主 CMakeLists.txt 文件的末尾:
export(TARGETS MyLib1 MyLib2 NAMESPACE MyLib:: FILE MyLibTargets.cmake)
这将把列出的目标放到构建目录的文件中,还可以给添加一个命名空间作为前缀。现在,CMake 可以找到这个包了,并将这个包导出到 $HOME/.cmake/packages 文件夹下:
set(CMAKE_EXPORT_PACKAGE_REGISTRY ON)
export(PACKAGE MyLib)
现在,find_package(MyLib)就可以找到构建文件夹了。来看看生成的MyLibTargets.cmake文件到底做了什么。它只是一个普通的CMake文件,但带有导出的目标。
注意,这种方式有一个缺点:若导入了依赖项,则需要在 find_package 之前导入它们。这个问题将在后面的章节中解决。
打包
CMake 有两种打包方式:一是使用CPackConfig.cmake文件;二是将 CPack 变量放置在 CMakeLists.txt 文件中。若想要包含主构建的相关变量(比如:版本号),可以使用配置文件的方式。这里,我将展示第二种方式:
# Packaging support
set(CPACK_PACKAGE_VENDOR "Vendor name")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Some summary")
set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR})
set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH})
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENCE")
set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
这些是生成二进制包时最常见的变量。二进制包使用 CMake 的安装机制,已经安装的东西都会显示出来。
当然,还可以制作源码包。可以将相应的正则表达式添加到 CMAKE_SOURCE_IGNORE_FILES 中,以确保只打包期望的文件(排除构建目录或 git 信息);否则,package_source 会将源目录中的所有内容打包在一起。这里,也可以根据自己的喜欢的文件类型,对源码包生成器进行设置:
set(CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set(CPACK_SOURCE_IGNORE_FILES
/.git
/dist
/.*build.*
/\\\\.DS_Store
)
注意,这种方式无法在 Windows 系统中正常运行,但是生成的源码包可以在 Windows 系统中正常使用。
最后,需要包含一下 CPack 模块:
include(CPack)
查找包
CMake 中有两种方式查找包:“模块”模式(Module)和“配置”模式(Config)。
CUDA
使用 CUDA 有两种方式:CMake 3.8(Windows为3.9)中引入的新方法,应该比旧的方法更受欢迎 —— 可能会在旧包中使用这种方法,所以本节会提一下。与旧语言不同,CUDA 的支持一直在快速发展,因为构建 CUDA 非常困难,所以建议使用最新版本的 CMake!CMake 3.17 和 3.18 有很多直接针对 CUDA 的改进。
对于 CUDA 和现代 CMake 来说,一个很好的参考是 CMake 开发者 Robert Maynard 在 GTC 2017 的 演讲 ppt。
启用 CUDA 语言
有两种方法可以启用 CUDA(若 CUDA 的支持不可选):
project(MY_PROJECT LANGUAGES CUDA CXX)
这里可能需要在将 CXX 一并列出。
若 CUDA 的支持可选,可以将其放在条件语句中:
enable_language(CUDA)
要检查 CUDA 是否可用,可使用 CheckLanuage:
include(CheckLanguage)
check_language(CUDA)
可以通过检查 CMAKE_CUDA_COMPILER(CMake 3.11 之前没有)来查看 CUDA 开发包是否存在。
可以检查 CMAKE_CUDA_COMPILER_ID(对于 nvcc,其值为 "NVIDIA",Clang 将在 CMake 3.18 支持)。可以用 CMAKE_CUDA_COMPILER_VERSION 检查 CUDA 版本。
CUDA 的变量
CMake 中许多名称中带有 CXX 的变量都有 CUDA 版本。例如,要设置 CUDA 所需的 C++ 标准,
if(NOT DEFINED CMAKE_CUDA_STANDARD)
set(CMAKE_CUDA_STANDARD 11)
set(CMAKE_CUDA_STANDARD_REQUIRED ON)
endif()
若正在查找和设置 CUDA 的标准级别,CMake 3.17 中添加了一组新的编译器特性,比如:cuda_std_11。这些版本特性与 cxx 的版本特性使用方式相同。
添加库/可执行文件
这部分很简单;使用 CUDA 文件 .cu,就像平常添加库一样。
也可以使用分离编译的方式:
set_target_properties(mylib PROPERTIES
CUDA_SEPARABLE_COMPILATION ON)
也可以直接使用 CUDA_PTX_COMPILATION 属性创建一个 PTX(Parallel Thread eXecution)文件。
目标架构
构建 CUDA 代码时,应该以架构为目标。若没有明确的架构信息,也可以编译成 ptx,nvcc 编译器会提供基本的指令,所以 PTX 还需要在运行时进行编译,这会使 GPU kernel 的加载速度慢得多。
所有 NVIDA 显卡都有一个架构级别,比如:7.2。在处理架构时,有两个选择:
- 代码层:将向正在编译的代码预报一个版本(如:5.0),这将使用 5.0 之前的所有特性,但不会超过 5.0(假设代码/标准库编写良好)。
- 目标架构:必须等于或大于架构版本。这需要有与目标显卡相同的主版本号,并且等于或小于目标显卡。所以使用架构为 7.2 的显卡时,在编译时将代码架构版本设置为 7.0 将是首选。最后,还可以生成 PTX;PTX 将在架构版本大于当前架构的所有显卡上工作,不过需要在运行时再对 PTX 进行编译。
CMake 3.18 中,设置目标架构变得非常容易。若 CMake 的版本范围为 3.18+,可以对目标使用 CMAKE_CUDA_ARCHITECTURES 变量和 CUDA_ARCHITECTURES 属性。允许直接写值(不带 .),比如:架构 5.0,可以就写为 50。若设置为 OFF,将不会传递任何架构信息。
使用目标
使用目标与 CXX 类似,但有一个问题。若目标包含编译器选项(或标志),大多数情况下,这些选项将无法正确使用(很难正确的封装在 CUDA 包装宏或函数中)。正确的编译器选项设置应该如下所示:
"$<$<BUILD_INTERFACE:$<COMPILE_LANGUAGE:CXX>>:-fopenmp>$<$<BUILD_INTERFACE:$<COMPILE_LANGUAGE:CUDA>>:-Xcompiler=-fopenmp>"
然而,不管是使用传统 CMake 的 find_package 方法,还是使用现代 CMake 的目标和继承方法,都不好使。这是我吃了不少苦头总结出来的经验。
目前,有一个合适的解决方案,只要知道未别名的目标名称即可。这是一个函数,若使用 CUDA 编译器,可以通过包装编译选项(标志)来修复仅处理 C++ 的目标:
function(CUDA_CONVERT_FLAGS EXISTING_TARGET)
get_property(old_flags TARGET ${EXISTING_TARGET} PROPERTY INTERFACE_COMPILE_OPTIONS)
if(NOT "${old_flags}" STREQUAL "")
string(REPLACE ";" "," CUDA_flags "${old_flags}")
set_property(TARGET ${EXISTING_TARGET} PROPERTY INTERFACE_COMPILE_OPTIONS
"$<$<BUILD_INTERFACE:$<COMPILE_LANGUAGE:CXX>>:${old_flags}>$<$<BUILD_INTERFACE:$<COMPILE_LANGUAGE:CUDA>>:-Xcompiler=${CUDA_flags}>"
)
endif()
endfunction()
内置变量
CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES:指示 CUDA 开发包内置 Thrust 等工具的目录CMAKE_CUDA_COMPILER:NVCC 的具体路径
即使不启用 CUDA 语言,也可以使用 FindCUDAToolkit 来查找 CUDA 的各种目标和变量。
注意,FindCUDA 已弃用,但对于低于 3.18 的 CMake,以下函数需要 FindCUDA:
- CUDA 版本检查/选择版本
- 架构检测(注意:3.12 部分修复了这个问题)
- 为 CUDA 库连接非
.cu文件
FindCUDA [警告:不要使用] (仅供参考)
若要支持旧版 CMake,建议至少在 CMake 文件夹中包含来自 CMake 3.9 版本的 FindCUDA(参见 CLIUtils github 组织中的 git库)。需要添加两个特性:CUDA_LINK_LIBRARIES_KEYWORD 和cuda_select_nvcc_arch_flags,以及较新的架构和 CUDA 版本。
要使用旧版 CUDA 支持方式,可以使用 find_package:
find_package(CUDA 7.0 REQUIRED)
message(STATUS "Found CUDA ${CUDA_VERSION_STRING} at ${CUDA_TOOLKIT_ROOT_DIR}")
可以用 CUDA_NVCC_FLAGS(使用列表添加的方式,list(APPEND))控制 CUDA 标志,通过 CUDA_SEPARABLE_COMPILATION 控制分离编译。若想确保 CUDA 的正常工作,需要将关键字添加到目标中(CMake 3.9+):
set(CUDA_LINK_LIBRARIES_KEYWORD PUBLIC)
若想让用户检查当前硬件的架构标志,可以使用以下方式:
cuda_select_nvcc_arch_flags(ARCH_FLAGS) # optional argument for arch to add
OpenMP
CMake 3.9+ 中对 OpenMP 的支持进行了极大的改善。现代(TM)CMake 使用 OpenMP 链接到一个目标的方法如下:
find_package(OpenMP)
if(OpenMP_CXX_FOUND)
target_link_libraries(MyTarget PUBLIC OpenMP::OpenMP_CXX)
endif()
这不仅比传统方法简单,若需要的话,还可以将库链接与编译的设置分开。CMake 3.12+ 中,甚至支持了 macOS 系统中的 OpenMP(需要对库文件进行安装,例如 brew install libomp)。若需要支持旧版 CMake,下面的代码可以在 CMake 3.1+ 上正常运行:
# For CMake < 3.9, we need to make the target ourselves
if(NOT TARGET OpenMP::OpenMP_CXX)
find_package(Threads REQUIRED)
add_library(OpenMP::OpenMP_CXX IMPORTED INTERFACE)
set_property(TARGET OpenMP::OpenMP_CXX
PROPERTY INTERFACE_COMPILE_OPTIONS ${OpenMP_CXX_FLAGS})
# Only works if the same flag is passed to the linker; use CMake 3.9+ otherwise (Intel, AppleClang)
set_property(TARGET OpenMP::OpenMP_CXX
PROPERTY INTERFACE_LINK_LIBRARIES ${OpenMP_CXX_FLAGS} Threads::Threads)
endif()
target_link_libraries(MyTarget PUBLIC OpenMP::OpenMP_CXX)
警告:CMake 小于 3.4 的版本中,Threads 包中有一个 bug,需要启用C语言。