第十四章:构建类型

Posted by lili on

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

14.1 构建类型基础

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

开发人员通常将构建视为两种安排之一:调试或发布。对于调试构建,编译器标志用于启用记录调试器可以使用的信息,以将机器指令与源代码关联起来。在这种构建中通常禁用优化,以便在执行程序时通过源代码位置直接而容易地进行跟踪。另一方面,发布构建通常启用了完全的优化并且不生成调试信息。

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

  • Debug 没有优化和完整的调试信息,通常在开发和调试过程中使用,因为它通常提供最快的构建时间和最好的交互式调试体验。

  • Release 这种构建类型通常提供了用于速度的完整优化和没有调试信息,尽管在某些情况下某些平台仍可能生成调试符号。通常用于构建软件的最终生产发布。

  • RelWithDebInfo 这在某种程度上是前两者的妥协。它旨在提供接近Release构建的性能,但仍然允许一定程度的调试。通常会应用大多数用于速度的优化,但也启用大多数调试功能。因此,当Debug构建的性能对于调试会话甚至不可接受时,此构建类型最有用。请注意,RelWithDebInfo的默认设置将禁用断言。

  • MinSizeRel 这种构建类型通常仅用于受限资源环境,如嵌入式设备。代码优化是为了大小而不是速度,并且不生成调试信息。

每种构建类型都会导致不同的编译器和链接器标志集。它还可能改变其他行为,例如更改哪些源文件被编译或链接到哪些库。这些细节将在接下来的几节中介绍,但在进入这些讨论之前,了解如何选择构建类型以及如何避免一些常见问题是至关重要的。

14.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应用程序中而不是从命令行中更改,但最终效果是相同的。对于CMake 3.22或更高版本,如果未设置CMAKE_BUILD_TYPE缓存变量,它将从CMAKE_BUILD_TYPE环境变量(如果已定义)初始化。与在同一构建目录中在不同构建类型之间切换不同,另一种策略是为每种构建类型设置单独的构建目录,仍然使用相同的源。这样的目录结构可能如下所示:

如果经常在构建类型之间切换,这种安排避免了由于编译器标志的变化而不断重新编译相同的源代码的情况。它还允许单个配置生成器有效地像多配置生成器一样运行。像Qt Creator这样的IDE环境支持在构建目录之间轻松切换,就像Xcode或Visual Studio允许在构建方案或配置之间轻松切换一样。

14.1.2 多配置生成器

一些生成器,特别是Xcode和Visual Studio,支持在单个构建目录中有多个配置。从CMake 3.17开始,Ninja Multi-Config生成器也可用。这些多配置生成器忽略CMAKE_BUILD_TYPE缓存变量,而是要求开发人员在IDE中或在构建时使用命令行选项选择构建类型。配置和构建这样的项目通常会看起来像这样:

cmake -S path/to/source -B path/to/build
cmake --build path/to/build

在Xcode IDE中构建时,构建类型由构建方案控制,而在Visual Studio IDE中,当前解决方案配置控制构建类型。这两个环境都为不同的构建类型保留单独的目录,因此在构建之间切换不会引起不断的重建。实际上,与单一配置生成器的上述多构建目录安排所做的事情相同,只是IDE代表开发人员处理目录结构。

对于命令行构建,Ninja Multi-Config生成器与其他多配置生成器相比具有更多的灵活性。可以使用CMAKE_DEFAULT_BUILD_TYPE缓存变量来更改在构建命令行上没有指定配置时要使用的默认配置。Xcode和Visual Studio生成器在此场景中有其自己的固定逻辑来确定默认配置。Ninja Multi-Config生成器还支持高级功能,允许将自定义命令作为一个配置执行,而使用一个或多个其他配置构建其他目标。大多数项目通常不需要或受益于这些更高级的功能,但Ninja Multi-Config生成器的CMake文档提供了基本的详细信息,并附有示例。

14.2. 常见错误

请注意,对于单一配置生成器,构建类型在配置时指定,而对于多配置生成器,构建类型在构建时指定。这个区别是关键的,因为这意味着在CMake处理项目的CMakeLists.txt文件时,并不总是知道构建类型。考虑下面这段CMake代码,不幸的是,这是相当常见的,但演示了一种不正确的模式:

# 警告:不要这样做!
if(CMAKE_BUILD_TYPE STREQUAL "Debug") 
 # 仅对调试构建执行某些操作
endif()

上述代码对于基于Makefile的生成器和Ninja是可以正常工作的,但对于Xcode、Visual Studio或Ninja Multi-Config则不行。实际上,在项目中基于CMAKE_BUILD_TYPE的任何逻辑都是值得质疑的,除非它受到确认正在使用单一配置生成器的检查的保护。对于多配置生成器,这个变量可能为空,但即使不为空,其值也应该被视为不可靠,因为构建将忽略它。项目在CMakeLists.txt文件中不应该引用CMAKE_BUILD_TYPE,而应该使用其他更健壮的替代技术,比如基于 $<CONFIG:…>的生成器表达式。

在脚本化构建时,常见的不足之处是假设使用了特定的生成器,或者没有正确考虑单一和多配置生成器之间的差异。理想情况下,开发人员应该能够在一个地方更改生成器,而脚本的其余部分仍然能够正常运行。方便的是,单一配置生成器将忽略任何构建时的指定,而多配置生成器将忽略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构建类型的行为,但这并不是绝对保证的。

对于使用单一配置生成器的Visual Studio编译器,这是一种特殊情况。对于该工具链,调试和非调试构建有不同的运行时库。空的构建类型会使得不清楚应该使用哪个运行时库。为了避免这种模糊性,对于这种组合,构建类型将默认为Debug。

cmake_minimum_required(3.11)
project(Foo)
# Only make changes if we are the top level project
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
 
 get_property(isMultiConfig GLOBAL
 
 PROPERTY GENERATOR_IS_MULTI_CONFIG
 
 )
 
 if(isMultiConfig)
 
 if(NOT "Profile" IN_LIST CMAKE_CONFIGURATION_TYPES)
 
 list(APPEND CMAKE_CONFIGURATION_TYPES Profile)
 
 endif()
 
 endif()
 
 # Set Profile-specific flag variables as needed...
endif()

14.3. 自定义构建类型

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

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

多配置生成器知道的构建类型集合由CMAKE_CONFIGURATION_TYPES缓存变量控制,更准确地说,由该变量在处理顶层CMakeLists.txt文件结束时的值控制。如果尚未定义,那么在第一次遇到的project()命令会填充该缓存变量。在CMake 3.22或更高版本中,CMAKE_CONFIGURATION_TYPES环境变量可以提供默认值。如果未设置该环境变量或使用较早版本的CMake,则默认值将是(可能是)第14.1节“构建类型基础”中提到的四个标准配置的子集(Debug、Release、RelWithDebInfo和MinSizeRel)。

项目可以在第一个project()命令之后修改CMAKE_CONFIGURATION_TYPES变量,但只能在顶层CMakeLists.txt文件中进行。一些CMake生成器依赖于整个项目中这个变量具有一致的值。可以通过将自定义构建类型添加到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 3.11。

此问题的另一个方面是,开发人员可能会将自己的类型添加到CMAKE_CONFIGURATION_TYPES缓存变量中,或者删除他们不感兴趣的类型。因此,项目不应该对哪些配置类型被定义或未定义做出任何假设。

考虑到上述观点,以下模式展示了项目为多配置生成器添加自定义构建类型的首选方式:

cmake_minimum_required(3.11)
project(Foo)
# Only make changes if we are the top level project
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
 
 get_property(isMultiConfig GLOBAL
 
 PROPERTY GENERATOR_IS_MULTI_CONFIG
 
 )
 
 if(isMultiConfig)
 
 if(NOT "Profile" IN_LIST CMAKE_CONFIGURATION_TYPES)
 
 list(APPEND CMAKE_CONFIGURATION_TYPES Profile)
 
 endif()
 
 endif()
 
 # Set Profile-specific flag variables as needed...
endif()

对于单一配置生成器,只有一种构建类型。这由CMAKE_BUILD_TYPE缓存变量指定,它是一个字符串。在CMake GUI中,这通常显示为一个文本编辑字段,因此开发人员可以编辑它以包含他们希望的任意内容。正如在第9.6节“缓存变量属性”中讨论的那样,缓存变量可以定义其STRINGS属性,以保存一组有效值。然后,CMake GUI应用程序将以包含有效值的组合框的形式呈现该变量,而不是作为文本编辑字段。

set_property(CACHE CMAKE_BUILD_TYPE PROPERTY
 STRINGS Debug Release Profile
)

属性只能在项目的CMakeLists.txt文件中更改,因此它们可以安全地设置STRINGS属性,而无需担心保留任何开发人员的更改。但是,请注意,设置缓存变量的STRINGS属性并不能保证缓存变量将保持其中一个定义的值,它只控制变量在CMake GUI应用程序中的呈现方式。开发人员仍然可以将CMAKE_BUILD_TYPE设置为任何值,无论是在cmake命令行中还是手动编辑CMakeCache.txt文件。为了严格要求变量具有定义的值之一,项目必须显式地执行该测试。

set(allowedBuildTypes Debug Release Profile)
# WARNING: This logic is not sufficient
if(NOT CMAKE_BUILD_TYPE IN_LIST allowedBuildTypes)
 message(FATAL_ERROR "${CMAKE_BUILD_TYPE} is not a known build type")
endif()

CMAKE_BUILD_TYPE的默认值是一个空字符串,因此除非开发人员明确设置它,否则上述情况会导致单一和多配置生成器都出现致命错误。这是不可取的,特别是对于甚至不使用CMAKE_BUILD_TYPE变量值的多配置生成器而言。可以通过在CMAKE_BUILD_TYPE未设置时由项目提供默认值来处理此问题。此外,应该将多配置和单一配置生成器的技术结合起来,以实现在所有生成器类型上的健壮行为。最终结果可能看起来像这样:

cmake_minimum_required(3.11)
project(Foo)
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
 get_property(isMultiConfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
 if(isMultiConfig) 
   if(NOT "Profile" IN_LIST CMAKE_CONFIGURATION_TYPES)
     list(APPEND CMAKE_CONFIGURATION_TYPES Profile)
   endif()
 else()
   set(allowedBuildTypes Debug Release Profile)
   set_property(CACHE CMAKE_BUILD_TYPE PROPERTY
          STRINGS "${allowedBuildTypes}"
   )
 
   if(NOT CMAKE_BUILD_TYPE)
     set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
   elseif(NOT CMAKE_BUILD_TYPE IN_LIST allowedBuildTypes)
     message(FATAL_ERROR "Unknown build type: ${CMAKE_BUILD_TYPE}")
   endif()
 endif()
 
 # Set relevant Profile-specific flag variables as needed...
endif()

上述技术允许选择自定义构建类型,但它们并未定义关于该构建类型的任何信息。选择构建类型指定要使用的特定于配置的变量。它还影响任何生成器表达式,其逻辑取决于当前配置($<CONFIG>和$<CONFIG:…>)。这些变量和生成器表达式将在下一章中讨论。目前,以下两个变量族是主要关注的。

  • CMAKE_<LANG>FLAGS<CONFIG>
  • CMAKE_<TARGETTYPE>LINKER_FLAGS<CONFIG>

这些变量中指定的标志将添加到相同名称的变量(不带 _<CONFIG> 后缀)提供的默认集合中。可以如下定义自定义的 Profile 构建类型:

set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE} -pg -g")
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE} -pg -g")
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} -pg")
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} -pg")
set(CMAKE_STATIC_LINKER_FLAGS_PROFILE "${CMAKE_STATIC_LINKER_FLAGS_RELEASE} -pg")

上述假设了一个与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}"
 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 的值,特别是对于需要在调试和非调试构建中使用不同运行时DLL的 Visual Studio 构建。因此,软件包可能需要包含两种构建类型的库。对于上面定义的自定义 Profile 构建类型,示例可能是:

set(CMAKE_PROFILE_POSTFIX _profile)

如果创建包含多个构建类型的软件包,强烈建议为每个构建类型设置 CMAKE_<CONFIG>_POSTFIX。按照惯例,Release 构建的后缀通常为空。但请注意,Apple 平台上会忽略 <CONFIG>_POSTFIX 目标属性。

由于历史原因,传递给 target_link_libraries() 命令的项目可以使用 debug 或 optimized 关键字前缀,以指示命名项目应仅在调试或非调试构建中链接。如果构建类型在 DEBUG_CONFIGURATIONS 全局属性中列出,则认为它是调试构建,否则认为它是优化的。对于自定义构建类型,如果它们应该在这种情况下被视为调试构建,则应将其名称添加到此全局属性。例如,如果项目定义了自己的名为 StrictChecker 的自定义构建类型,并且该构建类型应被视为非优化的调试构建类型,则可以(也应该)清晰地表示如下:

set_property(GLOBAL APPEND PROPERTY DEBUG_CONFIGURATIONS StrictChecker)

新项目通常应该优先使用生成器表达式,而不是在 target_link_libraries() 命令中使用 debug 和 optimized 关键字。下一章将更详细地讨论这个领域。

14.4. 推荐实践

开发人员不应假设正在使用特定的CMake生成器构建他们的项目。同一项目中的其他开发人员可能更喜欢使用不同的生成器,因为它与他们的IDE工具更好地集成,或者CMake的将来版本可能会添加对新生成器类型的支持,这可能带来其他好处。某些构建工具可能包含项目后来可能受到影响的错误,因此在此类错误修复之前,有备用生成器可能会很有用。如果假定了特定的CMake生成器,可能会阻碍扩大项目支持的平台集。

在使用单一配置生成器(如Makefiles或Ninja)时,考虑使用多个构建目录,每个目录对应一个感兴趣的构建类型。这样可以在不强制进行完整重新编译的情况下切换构建类型。这提供了与多配置生成器固有提供的类似行为,并且是一种使得像Qt Creator这样的IDE工具能够模拟多配置功能的有用方法。

对于单一配置生成器,考虑在CMAKE_BUILD_TYPE为空时将其设置为更好的默认值。虽然空的构建类型在技术上是有效的,但开发人员通常会误解它,认为它是调试构建而不是其自己独特的构建类型。此外,避免基于CMAKE_BUILD_TYPE创建逻辑,除非首先确认正在使用单一配置生成器。即使在这种情况下,这样的逻辑可能会很脆弱,而且可能可以使用生成器表达式以更通用和稳健的方式表达。

只有在已知正在使用多配置生成器或变量已经存在的情况下,才考虑修改CMAKE_CONFIGURATION_TYPES变量。如果要添加自定义构建类型或删除默认构建类型之一,请不要修改缓存变量,而是更改相同名称的常规变量(它将优先于缓存变量)。添加和删除单个项,而不是完全替换列表。这两个措施将有助于避免干扰开发人员对缓存变量所做更改。只在顶层CMakeLists.txt文件中进行此类更改。

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

一种常见但不正确的做法是查询LOCATION目标属性以了解目标的输出文件名。一个相关的错误是在自定义命令中假设特定的构建输出目录结构(请参阅第19章“自定义任务”)。这些方法对于所有构建类型都不起作用,因为LOCATION在多配置生成器的配置时间未知,并且在各种CMake生成器类型之间,构建输出目录结构通常是不同的。

应该使用生成器表达式,如$<TARGET_FILE:…>,因为它们可以为所有生成器提供所需的路径,无论它们是单一配置还是多配置。