华南虎视觉组主页华南虎视觉组主页
首页
项目
通用
教程
Gitea
首页
项目
通用
教程
Gitea
  • 现代 CMake 教程

    • 【01】安装与基本介绍
    • 【02】CMake 的文件分布
    • 【03】变量的设置与引用
    • 【04】运算符与条件、循环语句
    • 【05】目标构建
    • 【06】变量参与 C++ 程序的编译
    • 【07】宏与函数
    • 【08】find_package 详解
    • 【09】生成器表达式
      • 1. 前言
        • 1.1 CMake 工作流程
        • 1.2 CMake 的生成器表达式是什么
        • 1.3 为什么要引入生成器表达式
      • 2. 常用生成器表达式
        • 2.1 布尔生成器表达式
        • 2.2 字符串生成器表达式
      • 3. 构建安装表达式
        • 3.1 问题引出
        • 3.2 解决方法
    • 【10】项目的导出与安装
    • 【11】项目实战
    • 【12】CTest
    • 【13】CPack

【09】生成器表达式

1. 前言

1.1 CMake 工作流程

CMake 工作流程分为两步,和。我们在命令行中输入cmake ..的时候,可以在输出内容的最后看到这样的两行话

-- Configuring done
-- Generating done

分别指代了这两个阶段

1.1.1 Configure 阶段

在这个阶段,CMake 会读取你的CMakeLists.txt文件,并执行其中的命令。这些命令可能会检查系统的特性(例如,查找库或者编译器特性),设置变量,或者定义构建目标(例如库、可执行文件,或者其他伪目标)。这个阶段的主要目的是确定构建过程需要的所有信息。

1.1.2 Generate 阶段

在此阶段 CMake 会生成实际的建构档。这些文件包含了实际编译和链接你的代码所需要的命令,包括

  • 各种编译选项
  • 与目标相关的操作

的命令都是在生成阶段添加至建构档中的。比方说

target_include_directories(
  demo
  PUBLIC include
)

target_link_libraries(
  demo
  PRIVATE core
)

这两个命令均生效于。而后文准备介绍的生成器表达式,就作用于生成阶段。也正因如此,只有能够在生成阶段生效的命令才能使用生成器表达式,而在配置阶段生效的命令,例如用于设置变量的 set 命令并不支持生成器表达式。

1.2 CMake 的生成器表达式是什么

生成器表达式在生成构建系统期间进行计算,以生成特定于每个生成配置的信息。他们有如下形式:$<...>。可参考Generator Expression。

1.3 为什么要引入生成器表达式

在设置某些编译选项的时候,如果当条件较多,要制作的编译选项版本也相应增多。这时可以使用生成器表达式语法,让一部分编译信息在生成阶段自动完成,无需在配置阶段设置,示例如下:

这里的add_compile_options就属于在生成阶段生效的命令。以上代码属于,即在不同的条件下生成不同的变量、宏等内容。其中这段代码使用生成器表达式的效果是,在不同的构建类型下(Debug、Release),会生成不同的被添加进编译选项的字符串。

2. 常用生成器表达式

2.1 布尔生成器表达式

2.1.1 逻辑运算符

  1. $<BOOL:string>

    如果字符串为空、0、不区分大小写的FALSE、OFF、N、NO、IGNORE、NOTFOUND,或者区分大小写的以-NOTFOUND结尾的字符串,则表达式的值为0,否则为1

    add_library(mylib ${SOURCES})
    
    target_compile_definitions(mylib PUBLIC
      $<$<BOOL:${USE_OTHERLIB}>:USE_OTHERLIB>
    )
    

    在这个例子中,$<BOOL:${USE_OTHERLIB}> 会检查变量 USE_OTHERLIB 是否为假。如果 USE_OTHERLIB 为假,那么这个表达式就会返回 0,否则返回 1。然后,$<1:USE_OTHERLIB> 会在前面的表达式为真时添加一个编译定义 USE_OTHERLIB。

  2. $<AND:conditions>:逻辑与

    conditons是以逗号分割的,一般来说,条件是列表的,都是使用逗号进行分割,后文不再赘述。逻辑运算条件成立,表达式的值为0,否则为1,后文不再赘述。

    add_library(mylib ${SOURCES})
    
    target_compile_definitions(mylib PUBLIC
      $<$<AND:$<CONFIG:Debug>,$<TARGET_EXISTS:otherlib>>:USE_OTHERLIB>
    )
    

    在这个例子中,$<AND:$<CONFIG:Debug>,$<TARGET_EXISTS:otherlib>>会在当前配置为Debug并且目标 otherlib 存在时返回1,否则返回0。然后,$<1:USE_OTHERLIB> 会在前面的表达式为真时添加一个编译定义 USE_OTHERLIB。

  3. $<OR:conditions>:逻辑或

    在给定的任何条件为真时返回1,否则返回0

    add_library(mylib ${SOURCES})
    
    target_compile_definitions(mylib PUBLIC
      $<$<OR:$<CONFIG:Debug>,$<TARGET_EXISTS:otherlib>>:USE_OTHERLIB>
    )
    

    在这个例子中,$<OR:$<CONFIG:Debug>,$<TARGET_EXISTS:otherlib>>会在当前配置为 Debug 或者目标 otherlib 存在时返回 1,否则返回 0。然后,$<1:USE_OTHERLIB> 会在前面的表达式为真时添加一个编译定义 USE_OTHERLIB。

  4. $<NOT:condition>:逻辑非

    在给定的条件为假时返回1,否则返回0

    add_library(mylib ${SOURCES})
    
    target_compile_definitions(mylib PUBLIC
      $<$<NOT:$<TARGET_EXISTS:otherlib>>:USE_OTHERLIB>
    )
    

    在这个例子中,$<NOT:$<TARGET_EXISTS:otherlib>>会在目标otherlib不存在时返回1,否则返回0。然后,$<1:USE_OTHERLIB> 会在前面的表达式为真时添加一个编译定义 USE_OTHERLIB。

2.1.2 字符串比较

  1. $<STREQUAL:string1,string2>

    判断字符串是否相等

  2. $<EQUAL:value1,value2>

    判断数值是否相等

  3. $<IN_LIST:string,list>

    判断string是否包含在list中,list使用分号分割

注意

这里的list是在逗号后面的列表,即${...}所表示的内容,因此其内容需要使用分号分割。

2.1.3 变量查询

  1. $<TARGET_EXISTS:target>

    判断目标是否存在

  2. $<CONFIG:cfgs>

    判断编译类型配置是否包含在cfgs列表(比如"Release,Debug")中;不区分大小写

  3. $<PLATFORM_ID:platform_ids>

    判断CMake定义的平台ID是否包含在platform_ids列表中

  4. $<COMPILE_LANGUAGE:languages>

    判断编译语言是否包含在languages列表中

2.2 字符串生成器表达式

使用生成器表达式的主要目的可能就是因为此功能了。比如 CMake 官方的例子:基于编译器 ID 指定include目录:

include_directories("/usr/include/$<CXX_COMPILER_ID>/")

根据编译器的类型,$<CXX_COMPILER_ID>会被替换成对应的ID(比如“GNU”、“Clang”)。

2.2.1 条件表达式

  • $<condition:true_string>

    如果条件为真,则结果为true_string,否则为空

  • $<IF:condition,str1,str2>

    如果条件为真,则结果为str1,否则为str2

2.2.2 字符串转换

  • $<LOWER_CASE:string>

    将字符串转为小写

    add_library(mylib ${SOURCES})
    
    set(TARGET_NAME "MYLIB")
    
    target_compile_definitions(mylib PUBLIC
      TARGET_NAME=$<LOWER_CASE:${TARGET_NAME}>
    )
    

    在这个例子中, $<LOWER_CASE:${TARGET_NAME}>会将TARGET_NAME变量的值转换为小写。然后,这个小写的值会被赋给编译定义 TARGET_NAME。

    所以,mylib 会被编译时定义 TARGET_NAME 为 mylib。

  • $<UPPER_CASE:string>,将字符串转为大写

生成器表达式种类繁多,这里不再一一列举

3. 构建安装表达式

该生成器表达书属于字符串生成器表达式的一种。这个问题来自于【10】项目的导出与安装,但不看并不影响知识点的讲解,下面给出这个问题的产生原因。

3.1 问题引出

在安装导出目标的时候,如果导出目标中包含的目标使用了 target_include_directories 命令,例如

target_include_directories(
  my_target PUBLIC include
)

则执行 cmake .. 的操作后会出现形如

-- CMake Error in xxx/xxx/CMakeLists.txt:
-- Target "my_target" INTERFACE_INCLUDE_DIRECTORIES property contains
-- path:
--  
--   "/xxx/xxx/include"
-- 
-- which is prefixed in the source directory.

的问题,这表示包含目录具有绝对路径,这其实是 install 指定的头文件安装路径(默认是 /usr/local/include)与 target_include_directories 指定的路径(这里是那串绝对路径 /xxx/xxx/include)互相冲突了

3.2 解决方法

最为有效且通用的解决方法是使用导出安装的生成器表达式,有 $<BUILD_INTERFACE>:xxx 和 $<INSTALL_INTERFACE:xxx,下面给出一个比较常见的示例。

target_include_directories(
  my_target PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIRS}/include>
  $<INSTALL_INTERFACE:include/MyLibrary>
)

这个命令表示,将一系列内容以公开传播方式包含于目标 my_target 中,这里的一系列内容则是会根据当前处于何种方式(或是)进行选择。BUILD_INTERFACE 表示在构建时启用,INSTALL_INTERFACE 表示在安装时启用。

假设导出目标名为 MyModules,可以在安装路径 <path-to-install>/cmake/MyModules.cmake 下找到如下类似的语句

set_target_properties(my_target PROPERTIES
  INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include/MyLibrary"
)

这是INSTALL_INTERFACE生成器表达式生效的产物。

补充说明

这里对上文出现的三个标识符做个简短的介绍

  • my_target,目标,包括二进制目标和伪目标,可参考【05】目标构建
  • MyModules,导出目标,可参考【10】项目的导出与安装
  • MyLibrary,包名,一般用于find_package。可参考【08】find_package 详解
Prev
【08】find_package 详解
Next
【10】项目的导出与安装