第13章:构建类型

构建类型(某些IDE工具中也称为构建配置或构建方案)是一种高级控件,它选择不同的编译器和链接器行为集。构建类型的操作是本章的主题,下一章将介绍控制编译器和链接器选项的更具体细节。这两章节涵盖了每个CMake开发人员通常会使用的操作。

13.1. 构建类型的基本概念

构建类型可能以这样或那样的方式影响与构建有关的所有内容。主要对编译器和链接器行为有直接影响,也对项目使用的目录结构有影响,这反过来又会影响开发人员如何设置自己的本地开发环境。

开发人员通常认为构建只有两种:调试或发布。对于调试,编译器标志用于记录信息,调试器可以使用这些信息将机器指令与源代码关联起来。这样的构建中,禁用优化以便在逐步执行程序时,容易直接生成从机器指令到源代码的位置。另一方面,发布版本通常启用了优化,并且没有调试信息。

这些就是CMake构建类型的例子。当项目能定义构建类型时,CMake提供的默认构建类型对于大多数项目来说是足够的:

Debug

由于禁用优化和有完整的调试信息,这种构建通常在开发和调试期间使用,通常提供最快的构建时间和最佳的交互调试体验。

Release

这种构建类型通常提供了完全的优化,并且没有调试信息,不过会在一些平台在某些情况下可能生成调试符号。通常最终产品发布,使用该构建类型。

RelWithDebInfo

这在某种程度上是前两者的混合。它的目标是提供接近发布版本的性能,但仍然允许某种程度的调试。当调试生成的性能甚至对于调试来说不可接受时,此构建类型非常有用。注意,RelWithDebInfo会默认禁用断言。

MinSizeRel

这种构建类型通常只用于受限的资源环境,比如嵌入式设备。代码优化的大小而不是速度,而是调试信息。

每种构建类型都会产生一组不同的编译器和链接器标志。还可能改变其他行为,比如:改变编译哪些源文件或链接到什么库。这些细节将在接下来的几节中讨论, 在开始讨论之前,理解如何选择构建类型,以及如何避免一些常见问题也非常重要。

13.1.1. 单配置生成器

第2.3节“生成项目文件”中,介绍了不同类型的项目生成器。有些文件,如Makefiles和Ninja,对每个构建目录只支持一种构建类型。对于这些生成器,必须通过设置CMAKE_BUILD_TYPE缓存变量来选择构建类型。例如,要使用Ninja配置并构建一个项目,可以使用如下命令:

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

CMAKE_BUILD_TYPE缓存变量可以在CMake GUI中更改。但与在不同的构建类型之间切换不同,另一种策略是为每种构建类型设置单独的构建目录,所有这些目录仍然使用相同的源。目录结构可能像这样:

如果经常在构建类型之间切换,这种安排可以避免因为编译器标记的变化而重新编译相同的源代码。它还可以使得单配置生成器充当多配置生成器的角色,像Qt Creator这样的IDE环境支持在构建目录之间切换,就像Xcode或Visual Studio允许在构建方案或配置之间切换一样简单。

13.1.2. 多配置生成器

一些生成器,特别是Xcode和Visual Studio,支持在单个构建目录中进行多种配置。这些生成器忽略CMAKE_BUILD_TYPE缓存变量,而是要求开发人员在IDE中选择构建类型,或者在构建时使用命令行选项。配置和构建这样的项目如下所示:

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

在Xcode IDE中构建时,构建类型由构建方案控制,而在Visual Studio IDE中,当前解决方案可以控制构建类型。这两个环境都为不同的构建类型保留单独的目录,因此在构建之间切换不会导致重新构建。对于单配置生成器,所做的工作与上面描述的多个构建目录安排是一样的,只是IDE代表开发人员处理了目录结构。

13.2. 常见错误

对于单个配置生成器,生成类型是在配置时指定的,而对于多个配置生成器,生成类型是在生成时指定的。这个区别非常重要,它意味着当CMake处理项目的CMakeLists.txt文件时,构建类型并不总是已知的。考虑下面这段CMake代码,演示了一个不正确的模式:

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

上面的方法对于基于makefile的生成器和Ninja来说很友好,但是对于Xcode和Visual Studio来说就不行了。实际中,项目中任何基于CMAKE_BUILD_TYPE的逻辑都有问题,除非确认开启了单个配置生成器的检查。对于多配置生成器,这个变量很可能是空的,即使不是空的,因为构建将忽略它,它的值也认为是不可靠的。项目应该使用其他更健壮的替代技术,比如基于$<CONFIG:…>的生成器表达式,而不是在CMakeLists.txt文件中引用CMAKE_BUILD_TYPE。

编写脚本构建时,一个常见的问题是假定使用了特定的生成器,或者没有正确考虑单个和多个配置生成器之间的差异。理想情况下,开发人员应该能够更改生成器,并且脚本的其余部分仍然能够正常工作。单配置生成器将忽略任何构建时规范,而多个配置生成器将忽略CMAKE_BUILD_TYPE变量,因此脚本可以考虑这两种情况。例如:

mkdir build
cd build
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ../source
cmake --build . --config Release

对于上面的示例,开发人员可以简单地更改给-G参数的生成器名称,脚本的其余部分将不用更改。

不显式地为单个配置生成器设置CMAKE_BUILD_TYPE也很常见,但通常不是开发人员想要的。单个配置生成器的惟一行为是,如果没有设置CMAKE_BUILD_TYPE,则构建类型为空。这会导致一些开发人员误解为空构建类型等同于Debug,但事实并非如此。空构建类型是唯一、无名的构建类型。这种情况下,不使用特定于配置的编译器或链接器标志,因此行为由编译器和链接器的默认行为决定。虽然这类似于Debug构建类型的行为,但实际情况却无法保证。

13.3. 自定义构建类型

有时项目可能希望将构建类型集限制为缺省值的子集,或者希望添加具有一组特殊编译器和链接器标志为自定义构建类型。后一种方法的例子是添加用于分析或代码覆盖的构建类型,这两者都需要特定的编译器和链接器设置。

开发人员可以在两个主要位置看到构建类型集。当使用Xcode和Visual Studio这样的多配置生成器时,IDE会提供一个下拉列表或者类似的东西,开发者可以从中选择想要的构建配置。对于单配置生成器(如Makefiles或Ninja),直接为CMAKE_BUILD_TYPE缓存变量输入构建类型,而CMake GUI可以提供一个组合框。这两种情况的机制是不同的,必须分别处理它们。

多配置生成器知道的构建类型集,并由CMAKE_CONFIGURATION_TYPES缓存变量控制。第一个遇到的project()命令用一个默认列表填充缓存变量(如果还没有定义的话),但是项目可以在此之后修改同名的非缓存变量(修改缓存变量是不安全的,因为它可能会放弃开发人员所做的更改)。自定义构建类型可以通过将它们添加到CMAKE_CONFIGURATION_TYPES来定义构建,不需要的构建类型可以从该列表中删除。

但是,如果CMAKE_CONFIGURATION_TYPES还没有定义,则要避免设置它。CMake 3.9之前,判断是否使用了多配置生成器的一种常见方法是检查CMAKE_CONFIGURATION_TYPES是否为非空。甚至CMake本身的某些部分在3.11之前也是这样做的。虽然这种方法通常是准确的,但是即使使用单个配置生成器,也可以看到由项目单方面地设置CMAKE_CONFIGURATION_TYPES的情况。这可能导致对正在使用的生成器类型做出错误的判断。为了解决这个问题,CMake 3.9添加了一个新的GENERATOR_IS_MULTI_CONFIG全局属性,当使用多配置生成器时,该属性设置为true,从而提供了一种获得信息的明确方法,而不是依赖于CMAKE_CONFIGURATION_TYPES。即便如此,检查CMAKE_CONFIGURATION_TYPES仍然是一种流行的模式,项目应该继续只在它存在时修改它,而不要自己创建它。还应该注意的是,在CMake 3.11之前,向CMAKE_CONFIGURATION_TYPES添加自定义构建类型在技术上是不安全的。CMake的某些部分只考虑了默认的构建类型,即便如此,项目仍然可以用早期的CMake版本有效地定义自定义构建类型,这取决于如何使用CMake。为了更好的健壮性,如果要定义自定义构建类型,仍然建议使用CMake 3.11以上的版本。

另一个方面,开发人员可能会在CMAKE_CONFIGURATION_TYPES缓存变量中添加自定义的类型,或者删除一些不感兴趣的类型。因此,项目不应该对定义了或没有定义的配置类型做任何假设。

考虑到以上几点,下面的例子展示了项目为多配置生成器添加自定义构建类型的最佳方式:

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缓存变量指定。CMake GUI中,这通常是一个文本编辑字段,因此开发人员可以编辑它以包含任何他们想要的内容。如在9.6节所讨论,缓存变量可以由字符串定义一组有效值。然后CMake GUI将该变量作为包含有效值的组合框,而不是文本编辑字段显示。

set_property(CACHE CMAKE_BUILD_TYPE PROPERTY
 STRINGS Debug Release Profile)

属性只能从项目的CMakeLists.txt文件中更改,因此他们可以安全地设置STRINGS属性。但要注意,设置缓存变量的STRINGS属性并不能保证缓存变量将持有一个定义的值,它只能控制变量在CMake GUI应用程序中的显示方式。开发人员可以在cmake命令行中将CMAKE_BUILD_TYPE设置为任何值,或者手动修改CMakeCache.txt文件。为了严格要求变量具有所定义的值,项目本身必须显式地执行该测试。

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的默认值是一个空字符串,除非开发人员显式地设置它,否则上面的内容将对单/多配置生成器都造成致命错误。特别是对于不使用CMAKE_BUILD_TYPE变量值的多配置生成器。如果CMAKE_BUILD_TYPE没有设置,可以通过让项目提供一个默认值来处理。最终结果是这样的:

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>

这些可以用来在没有提供_<CONFIG>后缀的变量时,在默认设置上添加额外的编译器和链接器标志。例如,自定义配置文件构建类型的标志可以定义如下:

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 "")

上面假设有一个兼容GCC的编译器来保持示例的简单性,并启用分析及调试符号和最大程度优化。另一种方法是将编译器和链接器标志建立在其他构建类型的基础上,并添加额外标志。只要在project()命令之后执行,就可以执行此操作,因为该命令会填充默认的编译器和链接器标志变量。对于性能测试, RelWithDebInfo为默认构建类型是一个很好的选择,因为它支持调试和最大程度优化:

set(CMAKE_C_FLAGS_PROFILE
 "${CMAKE_C_FLAGS_RELWITHDEBINFO} -p" CACHE STRING "")
set(CMAKE_CXX_FLAGS_PROFILE
 "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -p" CACHE STRING "")
set(CMAKE_EXE_LINKER_FLAGS_PROFILE
 "${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO} -p" CACHE STRING "")
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE
 "${CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO} -p" CACHE STRING "")
set(CMAKE_STATIC_LINKER_FLAGS_PROFILE
 "${CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO} -p" CACHE STRING "")
set(CMAKE_MODULE_LINKER_FLAGS_PROFILE
 "${CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO} -p" CACHE STRING "")

每个自定义配置都应该定义相关的编译器和链接器标志变量。对于一些多配置生成器类型,CMake将检查所需的变量存在,如果他们没有设置,构建将失败并报错。

另一个可能为自定义构建类型定义的变量是CMAKE_<CONFIG>_POSTFIX。它用于初始化每个库目标的<CONFIG>_POSTFIX属性,并在指定配置构建时,将其值附加到这些目标的文件名中。这使得来自多种构建类型的库可以放在同一个目录中,从而不会相互覆盖。CMAKE_DEBUG_POSTFIX经常设置为d或_debug这样的值,特别是在Visual Studio构建中,在调试和非调试构建中必须使用不同的dll,因此需要包含这两种构建类型的库。在上面定义的自定义配置构建类型的为Profile情况下,可以写为:

set(CMAKE_PROFILE_POSTFIX _profile)

如果创建包含多种构建类型的包,强烈建议为每种构建类型设置CMAKE_<CONFIG>_POSTFIX。按照惯例,发布版本的后缀通常是空的。注意,<CONFIG>_POSTFIX目标属性不适用于Apple平台。

由于历史原因,传递给target_link_libraries()命令的项可以加上调试或优化关键字作为前缀,以表明命名项应该分别在调试或非调试版本中链接。如果在DEBUG_CONFIGURATIONS全局属性中列出了构建类型,则认为它是调试构建,否则认为是优化的。对于自定义构建类型,如果此场景中应该将其作为调试构建,则应该将其名称添加到此全局属性中。例如,如果一个项目定义了它自己的自定义构建类型StrictChecker,并且该构建类型应该被认为是一个非优化的调试构建类型,可以(也应该)像这样写:

set_property(GLOBAL PROPERTY APPEND DEBUG_CONFIGURATIONS StrictChecker)

新项目通常使用生成器表达式,而不是使用target_link_libraries()命令的debug和optimized关键字。下一章更详细地讨论了这一问题。

13.4. 推荐

开发人员不应该假设某个特定的CMake生成器会用来构建他们的项目。同一项目的其他开发人员可能更喜欢使用不同的生成器,可能是与他们的IDE工具集成得更好,或者在之后的CMake版本中可能会添加对新生成器的支持。某些构建工具可能有一些bug,这些bug会对项目构建产生影响。因此在修复这些bug之前,使用可替代的生成器很有必要。如果假设使用了特定的CMake生成器,扩展项目所支持的平台集也会受阻。

使用单配置生成器(如makefile或Ninja)时,考虑使用多个构建目录,每个构建目录对应一个构建类型。这允许在构建类型之间切换,而不必每次都强制重新编译。这提供了与多配置生成器原生行为相似的特性,并且可以帮助Qt Creator等IDE工具模拟多配置功能。

对于单个配置生成器,如果CMAKE_BUILD_TYPE为空,则考虑将其设置为合适的默认值。虽然空构建类型在技术上是有效的,但开发人员也经常误解为调试构建。此外,避免基于CMAKE_BUILD_TYPE创建逻辑,除非确认会使用单配置生成器。即使这样,这样的逻辑也很很脆弱,并且可能会使用生成器表达式进行更通用和健壮地表达。

如果已知正在使用一个多配置生成器,或者该变量已经存在,只考虑修改CMAKE_CONFIGURATION_TYPES变量。如果添加自定义生成类型或删除默认生成类型之一,请不要修改缓存变量,而要更改同名的常规变量(它将优先于缓存变量)。进行添加和删除单个项目,而不是完全替换列表。这两种方法都有助于避免干扰开发人员对缓存变量所做的修改。

如果需要CMake 3.9或更高版本,请使用GENERATOR_IS_MULTI_CONFIG全局属性来确定查询生成器类型,而不是依赖CMAKE_CONFIGURATION_TYPES来执行不太健壮的检查。

一种常见但不正确的做法是查询LOCATION目标属性,以确定目标的输出文件名,一个相关的错误是在自定义命令中假设一个特定的构建输出目录结构。这些方法并不适用于所有构建类型,因为在配置时,对于多个配置生成器的位置是未知的,而且构建输出目录结构在不同的CMake生成器类型中通常是不同的。应该使用$<TARGET_FILE:…>这样的生成器表达式,因为它们有力地为所有生成器提供了所需的路径,无论是单配置还是多配置。

Last updated