当运行CMake时,开发人员倾向于认为它是一个简单的步骤,需要读取项目的CMakeLists.txt文件,并生成相关的特定于生成器的项目文件集(例如Visual Studio解决方案和项目文件,Xcode项目,Unix Makefiles或Ninja输入文件)。然而,这涉及两个截然不同的步骤。当运行CMake时,输出日志的末尾通常看起来像这样:
-- Configuring done
-- Generating done
-- Build files have been written to: /some/path/build
当CMake被调用时,它首先读取并处理源树顶部的CMakeLists.txt文件,包括它拉进来的任何其他文件。在执行命令、函数等时,创建项目的内部表示。这称为配置步骤。控制台日志的大部分输出都是在此阶段生成的,包括message()命令的任何内容。在configure步骤的末尾,–configure done消息被打印到日志中。
一旦CMake完成了读取和处理CMakeLists.txt文件,它就会执行生成步骤。这是使用在配置步骤中构建的内部表示创建构建工具的项目文件的地方。在大多数情况下,开发人员倾向于忽略生成步骤,只是将其视为配置的最终结果。控制台日志几乎总是在配置步骤完成后立即显示–Generating done消息,所以这是可以理解的。但在某些情况下,将其分为两个不同的阶段尤为重要。
考虑一个项目处理一个多配置的CMake生成器,如Xcode或Visual Studio。当读取CMakeLists.txt文件时,CMake并不知道要为哪个配置构建目标。这是一个多配置的设置,所以有不止一个选择(例如调试,发布,等等)。开发人员在构建时选择配置,在CMake完成之后。如果CMakeLists.txt文件想要做一些事情,比如将文件复制到与给定目标的最终可执行文件相同的目录中,这似乎会出现一个问题,因为该目录的位置取决于正在构建的配置。需要一些占位符来告诉CMake“对于正在构建的任何配置,使用最终可执行文件的目录”。
这是生成器表达式提供的功能的一个主要示例。它们提供了一种对某些逻辑进行编码的方法,这些逻辑在配置时不会计算,而是推迟到项目文件被写入时的生成阶段。它们可以用来执行条件逻辑,输出字符串,提供关于构建的各个方面的信息,如目录、名称、平台细节等。它们甚至可以用于根据正在执行的构建或安装提供不同的内容。
9.1 简单的布尔逻辑
生成器表达式不能在任何地方使用,但是在很多地方都支持它们。在CMake参考文档中,如果一个特定的命令或属性支持生成器表达式,文档中会提到它。随着时间的推移,支持生成器表达式的属性集已经扩展,一些CMake版本也扩展了支持的表达式集。项目应该确认,对于他们所需要的最小CMake版本,被修改的属性确实支持所使用的生成器表达式。
使用语法$<…>指定一个生成器表达式,其中尖括号之间的内容可以采用几种不同的形式。很快就会清楚,一个基本特征是有条件地包含内容。下面是最基本的生成器表达式:
$<1:...>
$<0:...>
$<BOOL:...>
对于$<1:…>,表达式的结果将是…部分,而对于$<0:…>,…部分将被忽略,表达式将产生一个空字符串。 $<BOOL:…>表达式可以用来将任何被CMake识别为布尔型假值的值转换为0,其他值转换为1。这些生成器表达式一起提供了一种简单而强大的方法来选择性地包含内容。还支持逻辑操作:
$<AND:expr[,expr...]>
$<OR:expr[,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 | |
$<$<BOOL:true>:foo> | foo | |
$<$<NOT: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_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>
相关代码:https://gitee.com/jiangli01/cmake-learning
更多请关注微信公众号【Hope Hut】: