CMake(十二):构建类型

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

12.1 构建基础类型

​ 构建类型有可能以某种方式影响与构建相关的几乎所有内容。虽然它主要对编译器和链接器的行为有直接影响,但它也对项目使用的目录结构有影响。这反过来又会影响开发人员如何设置他们自己的本地开发环境,因此构建类型的影响可能相当深远。

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

​ 这些是CMake所说的构建类型的例子。虽然项目可以定义他们想要的任何构建类型,但CMake提供的默认构建类型通常对大多数项目来说已经足够了:

  • Debug

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

  • Release

    这种构建类型通常快速的提供了充分的优化,并且没有调试信息,尽管一些平台在某些情况下仍然可能生成调试符号。它通常是为最终产品版本构建软件时使用的构建类型。

  • RelWithDebInfo

    这在某种程度上是前两者的折衷。它的目标是使性能接近于发布版本,但仍然允许一定程度的调试。通常应用大多数速度优化,但也启用了大多数调试功能。因此,当调试构建的性能甚至对调试会话来说都是不可接受的时,这种构建类型最有用。请注意,RelWithDebInfo的默认设置将禁用断言。

  • MinSizeRel

    这种构建类型通常只用于受限制的资源环境,如嵌入式设备。代码是针对大小而不是速度进行优化的,并且没有创建调试信息。每种构建类型都会产生一组不同的编译器和链接器标志。它还可能改变其他行为,比如改变编译的源文件或链接到的库。

(1)单一配置生成器

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

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

​ CMAKE_BUILD_TYPE缓存变量也可以在CMake GUI应用程序中更改,而不是在命令行中更改,但最终效果是一样的。但是,一种替代策略是为每种构建类型设置单独的构建目录,而不是在不同的构建类型之间切换,所有的构建类型仍然使用相同的源。这样的目录结构可能看起来像这样:

CMake(十二):构建类型

​ 如果频繁地在构建类型之间切换,这种安排可以避免仅仅因为编译器标志改变而不断地重新编译相同的源代码。它还允许单个配置生成器像多个配置生成器一样有效地运行,像Qt Creator这样的IDE环境支持在构建目录之间切换,就像Xcode或Visual Studio支持在构建方案或配置之间切换一样简单。

(2)多个配置生成器

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

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

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

12.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的逻辑都是有问题的,除非它受到检查的保护,以确认正在使用单个配置生成器。对于多个配置生成器,这个变量可能是空的,但即使不是,它的值也应该被认为是不可靠的,因为构建将忽略它。而不是在CMakeLists.txt文件中引用CMAKE_BUILD_TYPE,项目应该使用其他更健壮的替代技术,例如基于 $<CONFIG:…>的生成器表达式。

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构建类型的行为类似,但绝不能保证。

12.3 自定义构建类型

​ 有时,项目可能希望将构建类型集限制为默认值的一个子集,或者希望添加其他具有特殊编译器和链接器标志集的定制构建类型。后者的一个很好的例子是为分析或代码覆盖添加构建类型,这两种类型都需要特定的编译器和链接器设置。

​ 开发人员可以在两个主要的地方看到构建类型集。当使用像Xcode和Visual Studio这样的多配置生成器时,IDE环境提供了一个下拉列表或类似的东西,开发人员可以从中选择他们想要构建的配置。对于像Makefiles或Ninja这样的单个配置生成器,构建类型是直接为CMAKE_BUILD_TYPE缓存变量输入的,但是CMake GUI应用程序可以显示一个有效选择的组合框,而不是简单的文本编辑字段。这两种情况背后的机制是不同的,因此必须分别处理。

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

​ 但是,如果CMAKE_CONFIGURATION_TYPES还没有定义,则需要注意避免设置它。在CMake 3.9之前,确定是否使用了多配置生成器的一个非常常见的方法是检查CMAKE_CONFIGURATION_TYPES是否为非空。甚至在3.11之前,CMake的某些部分也这样做了。虽然这种方法通常是准确的,但即使使用单个配置生成器,项目也会单方面设置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 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中,这通常是一个文本编辑字段,所以开发人员可以编辑它以包含任何他们想要的任意内容。但是,正如在“缓存变量属性”中所讨论的,缓存变量可以定义它们的string属性来保存一组有效的值。然后,CMake GUI应用程序将该变量显示为一个包含有效值的组合框,而不是一个文本编辑字段。

set_property(CACHE CMAKE_BUILD_TYPE PROPERTY
  STRINGS Debug Release Profile)

​ 属性只能从项目的CMakeLists.txt文件中更改,所以他们可以安全地设置string属性,而不必担心保留任何开发人员的更改。注意,然而,设置一个缓存变量的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>

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

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构建中,Debug和non-Debug构建必须使用不同的运行时dll,所以包可能需要包含两种构建类型的库。在上面定义的自定义概要文件构建类型的情况下,一个例子可能是:

set(CMAKE_PROFILE_POSTFIX _profile)

​ 如果创建包含多个构建类型的包,强烈建议为每种构建类型设置CMAKE_<CONFIG>_POSTFIX。按照惯例,Release构建的后缀通常是空的。注意,在苹果平台上<CONFIG>_POSTFIX目标属性被忽略了。

​ 由于历史原因,传递给target_link_libraries()命令的项可以用debug或optimized的关键字作为前缀,以指示命名的项只能分别用于调试或非调试构建。如果构建类型在DEBUG_CONFIGURATIONS全局属性中列出,则认为它是调试构建,否则认为它是优化的。对于自定义构建类型,如果在此场景中它们应被视为调试构建,则应将其名称添加到此全局属性中。例如,如果一个项目定义了自己的自定义构建类型StrictChecker,并且该构建类型应该被认为是一个未优化的调试构建类型,那么它可以(也应该)这样明确:

set_property(GLOBAL PROPERTY APPEND DEBUG_CONFIGURATIONS StrictChecker)

​ 新项目通常应该更喜欢使用生成器表达式,而不是使用target_link_libraries()命令debug和optimized的关键字。
相关代码:https://gitee.com/jiangli01/cmake-learning
更多请关注微信公众号【Hope Hut】:
CMake(十二):构建类型

上一篇:String 转化Calendar


下一篇:CMAKE_BUILD_TYPE