目录

CMake构建类型

构建类型基础知识

CMake提供了几种默认的构建类型:

  • Debug:没有优化并附带完整的调试信息,通常在开发和调试过程中使用。
  • Release:没有调试信息,提供全面的速度优化。
  • RelWithDebInfo:在某种程度上是前两者的妥协,它的目的是使性能接近于发布版,但仍允许某种程度的调试。通常会对速度进行大部分优化,但也会启用大部分调试功能。默认禁用断言。
  • MinSizeRel:通常只用于有限的资源环境,如嵌入式设备。代码是针对大小而不是速度进行优化的,并且不创建调试信息。

每种构建类型都会产生一组不同的编译器和链接器标志,因此了解如何选择构建类型以及如何避免一些常见的问题是很重要的。

单一配置生成器

单一配置生成器必须通过设置CMAKE_BUILD_TYPE缓存变量来选择构建类型,例如:

1
2
cmake -G Ninja -DCMAKE_BUILD_TYPE:STRING=Debug ../source
cmake --build .

我们可以为每一种构建类型设置单独的构建目录,这样可以避免切换不同的构建类型从头开始编译代码,其目录结构类似下图:

/images/2022/04/13/cmake-build-dir.png

多配置生成器

多配置生成器如Xcode和Visual Studio,支持在一个构建目录中设置多种构建类型。它们忽略CMAKE_BUILD_TYPE缓存变量,而是要求开发者在IDE中选择构建类型,或者在构建时使用命令行选项指定构建类型,例如:

1
2
cmake -G Xcode ../source
cmake --build . --config Debug

常见错误

注意对于单配置生成器,构建类型是在配置时指定的,而对于多配置生成器,构建类型是在构建时指定的。这个区别意味着在CMake处理CMakeLists.txt文件时,并不总是知道构建类型。下面这段CMake代码,展示了一种不正确的模式:

1
2
3
4
# WARNING: Do not do this!
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
	# Do something only for debug builds
endif()

基于CMAKE_BUILD_TYPE的任何逻辑都是有问题的,除非能够确认正在使用单一配置生成器。而对于多配置生成器来说,构建会忽略CMAKE_BUILD_TYPE变量。因此项目应该使用基于$<CONFIG:...>的生成器表达式,或者在配置和构建阶段显式指定构建类型:

1
2
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ../source
cmake --build . --config Release

自定义构建类型

有时一个项目可能想添加其他的自定义构建类型,以使用一组特殊的编译器和链接器标志。

多配置生成器

多配置生成器的构建类型集合由CMAKE_CONFIGURATION_TYPES缓存变量控制,可以将构建类型添加到CMAKE_CONFIGURATION_TYPES中来自定义构建类型。

在CMake 3.9之前,确定是否正在使用多配置生成器的一个非常常见的方法是检查CMAKE_CONFIGURATION_TYPES是否为非空。但是单一配置生成器的项目设置了CMAKE_CONFIGURATION_TYPES变量的情况也很常见,因此CMake 3.9增加了一个新的GENERATOR_IS_MULTI_CONFIG全局属性用于判断是否使用了多配置生成器。

即便如此,检查CMAKE_CONFIGURATION_TYPES仍然是一个非常普遍的模式,应该继续只修改它,千万不要自己创建它。还应注意的是,在CMake 3.11之前,向CMAKE_CONFIGURATION_TYPES添加自定义构建类型在技术上并不安全。

开发者可能会在CMAKE_CONFIGURATION_TYPES缓存变量中添加自己的类型,或删除一些他们不感兴趣的类型。因此项目不应该对配置类型做任何假设。

考虑到以上几点,多配置生成器添加自定义构建类型可以使用如下模式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cmake_minimum_required(3.11)
project(Foo)

if(CMAKE_CONFIGURATION_TYPES)
    if(NOT "Profile" IN_LIST CMAKE_CONFIGURATION_TYPES)
    	list(APPEND CMAKE_CONFIGURATION_TYPES Profile)
    endif()
endif()

# Set relevant Profile-specific flag variables if not already set...

单一配置生成器

单一配置生成器只有一种构建类型,由CMAKE_BUILD_TYPE缓存变量指定,它是一个字符串。缓存变量可以将其STRINGS属性设置为一组有效值:

1
2
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY
	STRINGS Debug Release Profile)

为了严格要求CMAKE_BUILD_TYPE变量为定义好的值,项目必须明确地进行测试:

1
2
3
4
5
6
set(allowableBuildTypes Debug Release Profile)

# WARNING: This logic is not sufficient
if(NOT CMAKE_BUILD_TYPE IN_LIST allowableBuildTypes)
	message(FATAL_ERROR "${CMAKE_BUILD_TYPE} is not a defined build type")
endif()

CMAKE_BUILD_TYPE的默认值是一个空字符串,所以上述做法对于单配置和多配置的生成器来说都会导致致命的错误,除非开发者明确地设置它。因此多重和单一配置生成器的方法应该结合起来,以便在所有生成器类型中提供稳健的行为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
cmake_minimum_required(3.11)
project(Foo)

if(CMAKE_CONFIGURATION_TYPES)
    if(NOT "Profile" IN_LIST CMAKE_CONFIGURATION_TYPES)
    	list(APPEND CMAKE_CONFIGURATION_TYPES Profile)
    endif()
else()
    set(allowableBuildTypes Debug Release Profile)
    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY
   	    STRINGS "${allowableBuildTypes}")
    if(NOT CMAKE_BUILD_TYPE)
    	set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
    elseif(NOT CMAKE_BUILD_TYPE IN_LIST allowableBuildTypes)
    	message(FATAL_ERROR "Invalid build type: ${CMAKE_BUILD_TYPE}")
    endif()
endif()

# Set relevant Profile-specific flag variables if not already set...

当选择一个构建类型时,它指定了CMake应该使用哪些特定配置的变量,并且会影响到任何逻辑上依赖于当前配置的生成器表达式(比如:$<CONFIG>$<CONFIG:...>)。以下讲一下相关的两个系列的变量:

  • CMAKE_<LANG>_FLAGS_<CONFIG>
  • CMAKE_<TARGETTYPE>_LINKER_FLAGS_<CONFIG>

这些可以用于默认编译器和链接器集合之外添加额外的编译器和链接器标志。例如自定义Profile构建类型的标志可以定义如下:

1
2
3
4
5
6
set(CMAKE_C_FLAGS_PROFILE "-p -g -O2" CACHE STRING "")
set(CMAKE_CXX_FLAGS_PROFILE "-p -g -O2" CACHE STRING "")
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "-p -g -O2" CACHE STRING "")
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "-p -g -O2" CACHE STRING "")
set(CMAKE_STATIC_LINKER_FLAGS_PROFILE "-p -g -O2" CACHE STRING "")
set(CMAKE_MODULE_LINKER_FLAGS_PROFILE "-p -g -O2" CACHE STRING "")

自定义构建类型可能定义的另一个变量是CMAKE_<CONFIG>_POSTFIX。它被用来初始化每个库目标的<CONFIG>_POSTFIX属性,其值将被附加到这些目标的文件名中。这允许将多种构建类型的库放在同一目录下,而不会相互覆盖。例如:

1
set(CMAKE_PROFILE_POSTFIX _profile)

由于历史原因,传给target_link_libraries命令的参数可以以debug或optimized关键字为前缀,以表明参数只能分别被链接到调试或非调试构建中。如果一个构建类型被列在DEBUG_CONFIGURATIONS全局属性中,它就被认为是调试构建,否则就被认为是优化构建。对于自定义的构建类型,如果它们应被视为调试构建,则应将其名称添加到该全局属性中。例如:

1
set_property(GLOBAL PROPERTY APPEND DEBUG_CONFIGURATIONS Profile)