CMake(九):⽣成器表达式
当运⾏CMake时,开发⼈员倾向于认为它是⼀个简单的步骤,需要读取项⽬的⽂件,并⽣成相关的特定于⽣成器的项⽬⽂件集(例如Visual Studio解决⽅案和项⽬⽂件,Xcode项⽬,Unix Makefiles或Ninja输⼊⽂件)。然⽽,这涉及两个截然不同的步骤。当运⾏CMake时,输出⽇志的末尾通常看起来像这样:
-- Configuring done
-- Generating done
-- Build files have been written to: /some/path/build
当CMake被调⽤时,它⾸先读取并处理源树顶部的⽂件,包括它拉进来的任何其他⽂件。在执⾏命令、函数等时,创建项⽬的内部表⽰。这称为配置步骤。控制台⽇志的⼤部分输出都是在此阶段⽣成的,包括message()命令的任何内容。在configure步骤的末尾,–configure done消息被打印到⽇志中。
⼀旦CMake完成了读取和处理⽂件,它就会执⾏⽣成步骤。这是使⽤在配置步骤中构建的内部表⽰创建构建⼯具的项⽬⽂件的地⽅。在⼤多数情况下,开发⼈员倾向于忽略⽣成步骤,只是将其视
为配置的最终结果。控制台⽇志⼏乎总是在配置步骤完成后⽴即显⽰–Generating done消息,所以这是可以理解的。但在某些情况下,将其分为两个不同的阶段尤为重要。
考虑⼀个项⽬处理⼀个多配置的CMake⽣成器,如Xcode或Visual Studio。当读取⽂件时,CMake并不知道要为哪个配置构建⽬标。这是⼀个多配置的设置,所以有不⽌⼀个选择(例如调试,发布,等等)。开发⼈员在构建时选择配置,在CMake完成之后。如果⽂件想要做⼀些事情,⽐如将⽂件复制到与给定⽬标的最终可执⾏⽂件相同的⽬录中,这似乎会出现⼀个问题,因为该⽬录的位置取决于正在构建的配置。需要⼀些占位符来告诉CMake“对于正在构建的任何配置,使⽤最终可执⾏⽂件的⽬录”。
这是⽣成器表达式提供的功能的⼀个主要⽰例。它们提供了⼀种对某些逻辑进⾏编码的⽅法,这些逻辑在配置时不会计算,⽽是推迟到项⽬⽂件被写⼊时的⽣成阶段。它们可以⽤来执⾏条件逻辑,输出字符串,提供关于构建的各个⽅⾯的信息,如⽬录、名称、平台细节等。它们甚⾄可以⽤于根据正在执⾏的构建或安装提供不同的内容。
9.1 简单的布尔逻辑
⽣成器表达式不能在任何地⽅使⽤,但是在很多地⽅都⽀持它们。在CMake参考⽂档中,如果⼀个特定的命令或属性⽀持⽣成器表达式,⽂档中会提到它。随着时间的推移,⽀持⽣成器表达式的属性集已经
扩展,⼀些CMake版本也扩展了⽀持的表达式集。项⽬应该确认,对于他们所需要的最⼩CMake版本,被修改的属性确实⽀持所使⽤的⽣成器表达式。
使⽤语法$<…>指定⼀个⽣成器表达式,其中尖括号之间的内容可以采⽤⼏种不同的形式。很快就会清楚,⼀个基本特征是有条件地包含内容。下⾯是最基本的⽣成器表达式:
$<1:...>
$<0:...>
$<BOOL:...>
对于$<1:…>,表达式的结果将是…部分,⽽对于$<0:…>,…部分将被忽略,表达式将产⽣⼀个空字符串。 $<BOOL:…>表达式可以⽤来将任何被CMake识别为布尔型假值的值转换为0,其他值转换为1。这些⽣成器表达式⼀起提供了⼀种简单⽽强⼤的⽅法来选择性地包含内容。还⽀持逻辑操作:
$<AND:expr[,]>
$<OR:expr[,]>
$<NOT:expr>
每个expr的值预期为1或0。AND和OR表达式可以接受任意数量的逗号分隔参数,并提供相应的逻辑结果,⽽NOT只接受⼀个表达式,并将产⽣其参数的否定。因为AND、OR和NOT要求它们的表达式的值只能为0或1,所以考虑将这些表达式封装在$中,以强制对被认为是true或false的表达式进⾏更宽容的逻辑处理。
在CMake 3.8及以后的版本中,IF -then-else逻辑也可以⾮常⽅便地⽤⼀个专⽤的$表达式来表达:
$<IF:expr,val1,val0>
通常,expr的值必须为1或0。如果expr的值为1,则结果为val1;如果expr的值为0,则结果为val0。在CMake 3.8之前,等价的逻辑必须以以下更冗长的⽅式表⽰,需要给出两次表达式:
$<expr:val1>$<$<NOT:expr>:val0>
⽣成器表达式可以嵌套,允许构造任意复杂度的表达式。上⾯的例⼦显⽰了⼀个嵌套的条件,但是⽣成器表达式的任何部分都可以嵌套。下⾯的例⼦演⽰了到⽬前为⽌所讨论的特性:
Expression Result Notes $<1:foo>foo
$<0:foo>
$<true:foo>Error, not a 1 or 0 $<$<B OOL:true>:foo>foo
$<$<N OT:0>:foo>foo
$<$<NOT:1>:foo>
$<$<NOT:tree>:foo>foo Error, NOT requires a 1 or 0
$<$<AND:1,0>:foo>
$<$<OR:1,0>:foo>foo
$<1:$<$<BOOL:false>:foo>>
$<IF:$<BOOL:${foo}>,yes,no>Result will be yes or no depending on ${foo}
就像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这样的多配置⽣成器。
CMake提供了更多的基于平台和编译器细节、CMake策略设置等的条件测试。开发⼈员应该查阅CMake参考⽂档,了解⽀持的条件表达式的完整集合。
9.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表达式在定义⾃定义构建规则以在构建后的步骤中复制⽂件时特别有⽤。除了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源⽂件仍然只编译⼀次。对于某些构建来说,这可能更⽅便,因为它可以避免创建静态库的构建时间成本或动态库的符号解析的运⾏时成本,但仍然可以避免多次编译相同的源代码。
9.3 ⼀般的信息
⽣成器表达式可以提供关于⽬标以外的信息。可以获得有关正在使⽤的编译器、正在构建⽬标的平台、构建配置的名称等信息。这类表达式倾向于在更⾼级的情况下使⽤,例如处理⾃定义编译器或解决特定编译器或⼯具链的特定问题。这些表达式也会引起误⽤,因为它们似乎提供了⼀种⽅法来构造路径,⽽这些路径本可以通过更健壮的⽅法(如使⽤TARGET_FILE表达式或其他CMake特性)获得。在依赖更通⽤的信息⽣成器表达式作为解决问题的⽅法之前,开发⼈员应该仔细考虑。也就是说,有些表达确实有正当的⽤途。这⾥列出了⼀些更常见的表达式和⼀些实⽤程序表达式,作为进⼀步阅读的起点:
$<CONFIG>
为什么现在都用cmake
计算结果为⽣成类型。优先使⽤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>