第10章:表达式

运行CMake时,开发人员倾向于认为它是一个简单的步骤,包括读取项目的CMakeLists.txt文件,并生成相关的一组特定于生成器的项目文件(例如:Visual Studio解决方案和项目文件,一个Xcode项目,Unix makefile或Ninja输入文件)。然而,这是两个截然不同的步骤。当运行CMake时,输出日志的结尾通常是这样的:

-- Configuring done
-- Generating done
-- Build files have been written to: /some/path/build

当调用CMake时,首先读入并处理位于源顶部的CMakeLists.txt文件,包括其他文件,在执行命令、函数等时创建项目的内部表示,这称为配置步骤。控制台日志的大多数输出都是在此阶段生成的,包括来自message()命令的内容。配置步骤的最后,将--configuration done打印在日志中。

当CMake完成了对CMakeLists.txt文件的读取和处理,就会执行生成步骤,这是使用配置步骤中构建的内部表示创建构建工具的项目文件的地方。大多数情况下,开发人员倾向于忽略生成步骤,只是将其视为配置的最终结果。控制台日志几乎总是在配置步骤完成后立即显示生成的done消息,因此这是可以理解的。但在某些情况下,理解这两个阶段是特别重要的。

考虑一个为多配置CMake生成器(如Xcode或Visual Studio)处理的项目。当读取CMakeLists.txt文件时,CMake不知道将为哪个配置构建目标。多配置设置,有不止一个选择(如调试、发布等)。开发人员在构建时选择配置,此时CMake已经完成。如果CMakeLists.txt文件想要做一些事情,比如:将一个文件复制到给定目标的最终可执行文件所在的目录中,这似乎就会出现问题,因为该目录的位置取决于正在构建的配置。需要某种占位符来告诉CMake“正在构建的配置,最终可执行文件的目录”。

这是生成器表达式提供的功能的一个主要示例,提供了一种编码某些逻辑的方法,这些逻辑在配置时不会生成,会延迟到生成阶段,即编写项目文件时。他们可以用来执行条件逻辑,输出字符串的各个方面提供信息,比如构建目录,项目名称、平台和更多的细节。甚至可以根据正在执行的构建或安装,提供不同的内容。

生成器表达式很多地方都受支持,但也并非哪里都能使用。CMake参考文档中,如果一个特定的命令或属性支持生成器表达式,文档将会提到。随着时间的推移,支持生成器表达式的属性集已经越来越多,一些CMake版本也扩展了支持的表达式集。项目应该确认,需要的最小CMake版本,以确保使用的CMake支持使用的生成器表达式属性。

10.1. 简单的布尔逻辑

生成器表达式使用语法$<…>指定,其中尖括号之间的内容可以采用几种不同的形式。下面是最基本的生成器表达式:

$<1:...>
$<0:...>
$<BOOL:...>

对于$<1:…>,表达式的结果将是…部分,而对于$<0:…>,…部分将被忽略,表达式会产生一个空字符串。$<BOOL:…>表达式可将CMake识别的布尔值转换为0,其他都转换为1(关于CMake认为的错误值的细节,请参阅6.1.1节“基本表达式”的讨论)。这些生成器表达式提供了简单而强大的可选包含内容的方法。还支持逻辑操作:

$<AND:expr[,expr...]>
$<OR:expr[,expr...]>
$<NOT:expr>

期望每个表达式的值为1或0。AND和OR表达式可以接受任意数量的用逗号分隔的参数并提供相应的逻辑结果,但不接受单个表达式并给出其参数的否定。由于AND、OR和NOT要求表达式的计算值仅为0或1,可以考虑将这些表达式包装在$<BOOL:…>中,以强制执行对正确或错误表达式具有更大鲁棒性的逻辑。

CMake 3.8及以后版本中,使用$<IF:…>表达式也可以非常方便地表达IF -then-else逻辑:

$<IF:expr,val1,val0>

通常,表达式的值必须为1或0。如果表达式计算结果为1,则结果为val1。如果expr计算结果为0,则结果为val0。CMake 3.8之前,等价逻辑必须用以下更详细的方式来表达,需要给出表达式两次:

$<expr:val1>$<$<NOT:expr>:val0>

生成器表达式可以嵌套,允许构造任意复杂度的表达式。上面的示例显示了嵌套条件,生成器表达式的任何部分都可以嵌套。下面的例子演示了到目前为止所讨论的特性:

与if()命令一样,CMake提供对生成器表达式中字符串、数字和版本的测试支持,尽管语法略有不同。如果满足相应的条件,值都为1,否则为0。

$<STREQUAL:string1,string2>
$<EQUAL:number1,number2>
$<VERSION_EQUAL:version1,version2>
$<VERSION_GREATER:version1,version2>
$<VERSION_LESS:version1,version2>

另一个非常有用的条件表达式是测试构建类型:

$<CONFIG:arg>

如果arg对应于实际构建的构建类型,则该值将为1,对于其他构建类型,则为0。它的常见用途是仅为调试构建提供编译器标志,或者为不同的构建类型选择不同的实现。例如:

add_executable(myApp src1.cpp src2.cpp)

# Before CMake 3.8
target_link_libraries(myApp PRIVATE
 $<$<CONFIG:Debug>:checkedAlgo>
 $<$<NOT:$<CONFIG:Debug>>:fastAlgo>
)

# CMake 3.8 or later allows a more concise form
target_link_libraries(myApp PRIVATE
 $<IF:$<CONFIG:Debug>,checkedAlgo,fastAlgo>
)

上面的代码会将可执行文件链接到用于调试构建的checkedAlgo库,以及用于所有其他构建类型的fastAlgo库。$<CONFIG:…>生成器表达式是唯一能够提供这种功能的方法,它适用于所有CMake项目生成器,包括像Xcode或Visual Studio这样的多配置生成器。这个主题在13.2节“常见错误”中有更详细的介绍。

CMake提供了更多的基于平台和编译器细节、CMake策略设置等的条件测试。开发人员应该查阅CMake参考文档,以获得支持的条件表达式的完整集。

10.2. 目标的信息

生成器表达式的另一个常见的用法是提供目标的信息。目标的任何属性可以通过以下两种形式获得:

$<TARGET_PROPERTY:target,property>
$<TARGET_PROPERTY:property>

第一个方式提供指定目标的命名属性的值,而第二个方式通过生成器表达式检索目标的属性。

虽然TARGET_PROPERTY是一种非常灵活的表达式类型,但并不总是获取目标信息的最佳方式。CMake还提供了其他表达式,这些表达式提供了关于目标构建的二进制文件的目录和文件名的详细信息。这些更直接表达提取部分的一些属性或计算值基于原始属性。以下是最常见的TARGET_FILE生成器表达式集:

TARGET_FILE

这将获取生成目标二进制文件的绝对路径和文件名,包括与平台相关的任何文件前缀和后缀(例如.exe、.dylib)。对于基于Unix的平台,其中动态库的文件名中通常包含版本信息。

TARGET_FILE_NAME

和TARGET_FILE一样,但不获取路径(即只提供文件名)。

TARGET_FILE_DIR

与TARGET_FILE相同,但没有文件名。这是获取构建最终可执行文件或库的目录最健壮的方法。使用像Xcode或Visual Studio这样的多配置生成器时,它的价值会有所不同。

以上三个TARGET_FILE表达式在后期构建步骤中,复制文件的自定义构建规则时特别有用(参见17.2节,“将构建步骤添加到现有目标”)。除了TARGET_FILE表达式之外,CMake还提供了特定于库的表达式,它们具有类似的方法,只是在处理文件名前缀和/或后缀的细节略有不同。这些表达式的名称以TARGET_LINKER_FILE和TARGET_SONAME_FILE开始,但不像TARGET_FILE表达式那样常用。

支持Windows平台的项目,还可以获得目标PDB文件的详细信息。同样,这些主要用于自定义构建。以TARGET_PDB_FILE开始的表达式遵循与TARGET_PROPERTY类似的模式,提供用于生成器表达式(目标的)PDB文件的路径和文件名详细信息。

另一个与目标相关的生成器表达式也需要提一下。CMake允许将库目标定义为对象库,它只是CMake与目标关联的对象文件的集合,并不会创建最终的库文件。因为是目标文件,所以不能作为单个单元进行链接(尽管CMake 3.12放松了这一限制)。相反,必须像添加源一样将它们添加到目标中。CMake会在链接阶段包括那些对象文件,就像编译目标源创建的对象文件一样。这是使用$<TARGET_OBJECTS:…>生成器表达式完成的,该表达式适合使用add_executable()或add_library()的形式列出对象文件。

# Define an object library
add_library(objLib OBJECT src1.cpp src2.cpp)

# Define two executables which each have their own source
# file as well as the object files from objLib
add_executable(app1 app1.cpp $<TARGET_OBJECTS:objLib>)
add_executable(app2 app2.cpp $<TARGET_OBJECTS:objLib>)

上面的示例中,没有为objLib创建单独的库,但src1.cpp和src2.cpp源文件仍然只编译一次。可以避免创建静态库的构建时间成本,或者动态库的符号解析的时间成本,同时还可以避免多次编译相同的源。

10.3. 基本信息

生成器表达式可以提供的不仅是目标信息,还可以获取有关所使用的编译器、目标平台、构建配置名称等信息。这些表达式倾向于在更特殊情况下使用,例如:处理自定义编译器或解决特定于特定编译器或工具链的问题。这些表达式也会引起误用,比如:构造了一些东西的路径,而这些东西原本可以使用更健壮的方法(如使用TARGET_FILE表达式或其他CMake特性)获得。依赖更通用的信息生成器表达式作为解决问题的方法之前,开发人员应该仔细考虑这些表达的意图。这里列出了一些比较常见的表达式和一些实际用例表达式,作为进阶阅读的起点:

$<CONFIG>

估计为生成类型,使用CMAKE_BUILD_TYPE变量并非是用于Xcode或Visual Studio这样的可多宗配置的生成器。早期的CMake使用了现在已经废弃的$<CONFIGURATION>表达式,现在项目使用$<CONFIG>

$<PLATFORM_ID>

标识目标构建平台。这在交叉编译时非常有用,特别是当构建可能支持多个平台的时候(例如:设备和模拟器构建)。这个生成器表达式与CMAKE_SYSTEM_NAME变量密切相关,项目应该考虑在特定情况下使用该变量。

$<C_COMPILER_VERSION>, $<CXX_COMPILER_VERSION>

只在某些情况下,如果编译器版本是老比一些特定版本或更新。这可以通过$<VERSION_???:……>生成器表达式。例如,如果C++编译器版本小于4.2.0,要生成字符串OLD_COMPILER,可以使用下面的表达式:

$<$<VERSION_LESS:$<CXX_COMPILER_VERSION>,4.2.0>:OLD_COMPILER>

只有在已知编译器类型,且编译器的特定行为需要由项目以某种特殊方式处理的情况下,才会使用这种表达式。这可能是一种有用的技术,但如果它过于依赖此类表达式,则会降低项目的可移植性。

$<LOWER_CASE:…>, $<UPPER_CASE:…>

作为执行字符串比较之前的一个步骤,任何内容都可以通过这些表达式转换为大写或小写。例如:

$<STREQUAL:$<UPPER_CASE:${someVar}>,FOOBAR>

10.4. 推荐

与其他功能相比,生成器表达式是新添加的CMake特性。正因为如此,网上关于CMake的很多资料都没有使用它们,因为生成器表达式通常比旧的方法更健壮,提供的通用性更强。在一些常见的例子中出于好意的指导,导致的逻辑只适用于受支持的项目生成器或平台的子集,但是使用合适的生成器表达式就能消除这种限制。对于试图为不同构建类型做不同事情的项目来说,这一点尤为重要。因此,开发人员应该熟悉生成器表达式提供的功能。上面提到的那些表达式只是CMake支持的一个子集,但为覆盖大多数开发人员可能面临的情况打下了良好的基础。

如果使用得当,生成器表达式可以生成更简洁的CMakeLists.txt文件。例如,根据构建类型有条件地包含源文件,可以相对简洁地完成,正如前面给出的$<config:…>的示例所示。这样的使用减少了if-then-else逻辑的数量,只要生成器表达式不太复杂,会有更好的可读性。生成器表达式也非常适合处理根据目标或构建类型而变化的内容。

相反,当试图将所有东西都变成生成器表达式。可能导致表达式过于复杂,最终模糊了逻辑,并难以调试。与以往一样,开发人员应该更喜欢清晰而不是聪明的表达,对于生成器表达式更是如此。首先CMake是否已经提供了一个专门的命令来实现同样的结果,各种CMake模块提供了针对特定第三方包或执行特定任务的针对性功能。还有各种各样的变量和属性可以简化或完全取代对生成器表达式的需要。花几分钟查阅CMake参考文档,可以节省大量不必要的时间来构建复杂的生成器表达式。

Last updated