chapter04 - targets
创建 target
三种方式:
add_executable # 默认在 ALL 中
add_library # 默认在 ALL 中
add_custom_target # 默认不在 ALL 中
# add_custom_command 是创建文件的方式,但是不是创建 target
add_library
: https://cmake.org/cmake/help/latest/command/add_library.html#object-libraries
- Normal library: 如果不指定
STATIC
/SHARED
/MODULE
,add_library
会根据BUILD_SHARED_LIBS
变量去决定是否使用SHARED
还是STATIC
。 - Object library: 生成目标文件。
- Interface library:不生成文件。用来打包传播属性。
- Imported library:
IMPORTED
属性和STATIC
等是可以叠加的。IMPORTED
库是已经被编译好的库,不需要另外编译,但是需要用set_target_property
来设置其位置。 - Alias library
add_custom_target
可以有多条命令,而且不一定有产物。add_custom_target
创建的 target 是默认不 ALL 的!和 add_custom_command
的对比看后文。
可以手动用 add_dependencies
创建新的依赖关系。主要是用来维护编译顺序的(比如检查 checksum 的自定义任务必须得在目标已经生成之后)。
target 的依赖声明可以不按照顺序,cmake 会扫描整个文件再去构建依赖。
读写 target 的属性
set_target_properties
get_target_property
cmake 的有些命令是等价的,但是存在一层封装关系。比如用 get_property
和 set_property
也可以,但是不如上面的命令方便。
set_property
除了设置 target 还能设置 source 的属性。
其实用 add_dependencies
添加依赖关系也只是写入了 target 的属性,因而依赖是属性的一种。
属性传播
传播(propagation)有几种方式:
- PRIVATE
- INTERFACE 不会将属性设置给 source,但是设置给需要依赖的目标!
- PUBLIC
通过属性传播得到的属性发生冲突时,cmake 会报错。因而也有将库版本号作为 INTERFACE 类型的属性给库的做法,这样能够保证项目从多方获得同一个库时版本相同。
cmake_minimum_required(VERSION 3.20.0)
project(PropagatedProperties CXX)
add_library(source1 empty.cpp)
set_property(TARGET source1 PROPERTY INTERFACE_LIB_VERSION 3)
set_target_properties(source1 PROPERTIES
COMPATIBLE_INTERFACE_STRING LIB_VERSION
)
add_library(source2 empty.cpp)
set_property(TARGET source2 PROPERTY INTERFACE_LIB_VERSION 4)
add_library(destination empty.cpp)
target_link_libraries(destination source1 source2)
伪目标
这些目标在配置过程分析依赖图时是可见的,但是在 buildsystem 中不一定可见。
buildsystem 是一个单词而不是两个。
Imported targets
是用 find_package 找到的。
Alias targets
因为是伪目标,所以生成的目标在 buildsystem 中找不到(add_executable
和 add_library
都有 ALIAS
选项)。如果有些项目非要找某个特定名称的 target,而且 target 存在但只是名称不同,可以给个 alias。
Interface libraries
这样的库本身是不会被编译的。因为写成了 target,那么 link 这个 target 的就能获得传播来的属性。
主要作用:
- 创建纯头文件的库。
- 将多个要传播的属性打包在一个 target 里。
看不懂这里往 INTERFACE
里面加文件是什么意思,我觉得网上的这种更容易理解:
https://stackoverflow.com/a/45212811/
add_library(foo INTERFACE)
target_link_libraries(foo INTERFACE ${FOO_LIBRARIES})
target_include_directories(foo INTERFACE ${FOO_INCLUDE_DIR})
target_compile_definitions(foo INTERFACE "-DENABLE_FOO")
CMAKE_CURRENT_SOURCE_DIR
vsCMAKE_CURRENT_LIST_DIR
: https://stackoverflow.com/questions/15662497/difference-between-cmake-current-source-dir-and-cmake-current-list-dir 后者会因为include
而改变(记住include
和add_subdirectory
不同)。被include
的子 list 内部的CMAKE_CURRENT_LIST_DIR
是会改变的,但是 source dir 不变。
Build targets
可以在构建过程(cmake --build build
)中通过 --target
/-t
参数传入。
all(小写)就是一个伪 target。不指定时就会使用这个 target。
这也是为什么用 add_executable
和 add_library
创建库/可执行文件时,有一个选项是 EXCLUDE_FROM_ALL
,可能作为库使用的项目的文档和测试代码可以用这个选项来排除编译?而 add_custom_target
则相反,默认是排除在 all 外,除非指定 ALL 选项。
clean 也是伪 target。
Chapter 1 里面提到过 --clean-first
选项,可以在构建前先跑一下 clean。因为 clean 和其他的 target 不能共存,所以这个选项显得有必要,不然就只能调用两次 cmake 命令来构建了。
为了让 clean 能够正确删除文件,add_custom_target
中生成的文件需要显式用 BYPRODUCTS
选项标注出来。
Custom commands
和 custom target 有一点点区别。custom target 如果提供的只是命令,那么就没有明显的依赖关系,所以每次都需要重新构建(即不能 cache)。如果 custom target 被其他构建 target 依赖,那么很大一块都需要重新构建。custom target 也有另外一种方式,就是只说明依赖哪些文件,而不是使用命令。比如 add_custom_target(generate_foo DEPENDS foo.h foo.cpp)
。
add_custom_command
不会创建 target,但是可以有输出文件(按需构建)和依赖。通过将其输出文件作为其他库或者可执行文件的源码,可以构成依赖关系,从而将 custom command 纳入依赖图中。如果没有被加入依赖图,那么用 add_custom_command
声明的文件是不会运行的哦!
add_custom_command
的输出路径是相对于 ${CMAKE_CURRENT_BINARY_DIR}
的。
第一种用法:动态生成源码
例子:让 protobuf 在构建时生成。
书上的很多写法都像这样: https://stackoverflow.com/a/13703745/ 被认为 source list 中加入头文件是不必要的。但是这样其实有用!可以加上依赖关系,使得用 add_custom_command
能够被纳入依赖图中。
include_directories(include)
add_executable(MyExec
src/main.c
src/other_source.c
include/header1.h
include/header2.h
)
如果不给前缀,那么生成的文件应该是在 binary 目录里面。这样就出现了源码在 binary 路径的情况。书中代码生成的是头文件,所以还额外包含了输出目标的头文件:
target_include_directories(main PRIVATE ${CMAKE_BINARY_DIR})
OUTPUT
的相对路径是当前的 build 路径或 source 路径。build 路径的被优先考虑:
If an output file name is a relative path, its absolute path is determined by interpreting it relative to:
- the build directory corresponding to the current source directory (), or
- the current source directory ().
The path in the build directory is preferred unless the path in the source tree is mentioned as an absolute source file path elsewhere in the current directory.
OUTPUT
的源码默认带有 GENERATED
属性。对于普通的代码也可以标记此属性,比如:
set_source_files_properties( details/part.out PROPERTIES GENERATED TRUE )
2024 年 1 月 27 日:的确是可以依赖子文件夹的未生成文件,但是生成该文件的方式(add_custom_command
)必须在当前 LIST 可见。那么如何把生成方法放在用 add_subdirectory
包含进来的子目录呢?
按照网上的说法,如果要依赖来自子文件夹的文件而该文件的生成方式又在子文件夹的 CMakeLists.txt 中,那么需要用
set_source_files_properties
手动给该文件设置GENERATED
属性。cmake 在 3.20 之前,GENERATED
属性是局限于文件夹内的,对外不可见(见 https://stackoverflow.com/a/72018266/ )。但是我在尝试后没有成功。大概是因为尽管手动设置GENERATED
为TRUE
之后成功 tweak 了源码依赖检查,但是 cmake 依然无法看到来自子目录的构建命令。
实在无法解决又非要将 add_custom_command
放在子目录时,可以用 add_custom_target
创建一个依赖文件的 target 让当前目录的 target 去依赖。(尝试过,已成功。)
第二种用法:给 target 加 hook
可以创建一个在 target 构建前后自动运行的 command,但是给定的 target 必须在这个文件夹里面。使用钩子去指定缺失文件生成规则是无效的,因为缺失文件生成规则是要写进子构建系统的(比如 make),写了钩子也不会被转换成子构建系统能够识别的生成规则,即便改了 GENERATED
属性也没用。
Generator expressions
$<>
,也是能够放在字符串中被转义的。感觉就像执行了一个命令然后取其输出,就像 bash 的 $()
一样。
主要是用来解决 the chicken and the eggs problem。生成表达式是在生成阶段展开的,所以能够解决配置阶段无法解决的先后问题。
Note
因为生成表达式是在生成阶段才展开的(不能用 message 在 配置过程 打印),所以很难调试他的值。可以用 add_custom_command
将值 echo (${CMAKE_COMMAND} -E echo $<...>
)出来。这样在运行自定义 target 的时候可以打印。
生成表达式语法:
由于生成表达式是在生成阶段才可用,所以生成表达式不能用一般的字符串处理命令。因而有一些专门的字符串处理的生成表达式,同时生成表达式还能够嵌套。
# 输出 linux
add_custom_target(sometarget ALL
COMMAND ${CMAKE_COMMAND} -E echo $<LOWER_CASE:$<PLATFORM_ID>>)
条件、比较和字符串
link 相当于 $<IF:condition,true_string,>
也就是第三个参数为空。(和 bash 的 ${var:+true_string}
很像。)和命令中的评估不同,这里条件只认 0 和 1,其他值会报错。将其他值送入条件需要用 $<BOOL:string>
,评估真值的方式和对待变量是相同的。
还有 AND
/OR
/NOT
运算、版本比较、字符串和数值比较、字符串大小写转换、创建 C 标识符等。
列表和路径
比较高级的有列表操作、路径操作。
$<SHELL_PATH:path>
可以转换路径为目标平台的路径(比如 Windows 上用反斜杠)。其参数 path
必须为绝对路径。感觉像是给 Windows 准备的。
信息检查
甚至一些在 cmake 中有对应变量的东西在生成表达式也有对应版本(比如 $<C_COMPILER_ID>
)
add_executable(myapp main.cpp foo.c bar.cpp zot.cu)
target_compile_options(myapp
PRIVATE $<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
)
target_compile_definitions(myapp
PRIVATE $<$<COMPILE_LANGUAGE:CXX>:COMPILING_CXX>
$<$<COMPILE_LANGUAGE:CUDA>:COMPILING_CUDA>
)
target_include_directories(myapp
PRIVATE $<$<COMPILE_LANGUAGE:CXX,CUDA>:/opt/foo/headers>
)
这里用生成表达式而不是变量,是因为文件的后面可能还会用 enable_language
添加新的语言。
$<$<CONFIG:Debug>:DEBUG_MODE>
根据当前是否为 Debug 模式来展开(也就是判断变量 CMAKE_BUILD_TYPE
或者因为 Multi-Config 的 --config
的值)。对于 Unix Makefiles,如果不传入 CMAKE_BUILD_TYPE
,结果将是空。对于 Ninja Multi-Config,不传入则使用 Debug。
CONFIG
支持多个参数,也可以把空字符串加进去。
- 如果
CMAKE_BUILD_TYPE
为空,那么会使用 “toolchain-specific default build type”。听上去不太妙。- 似乎 Unix Makefiles 认为
CMAKE_BUILD_TYPE
不是 Debug,也不是认识的其他模式时,就使用 Release。- 在 Multi-Config 生成器的配置阶段传
CMAKE_BUILD_TYPE
,会被警告其没有被使用。
我们实验室的项目有这样的代码:
# 默认为 Debug 编译模式
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
虽然这样是防止了还需要额外判断 CMAKE_BUILD_TYPE
为空的坑,但是这对于多配置生成器在构建阶段才选择 Debug or Release 不友好。
$<TARGET_EXISTS:arg>
判断目标是否存在。
$<COMPILE_LANGUAGE:lang>
判断是否正在用某种语言编译。比如某个库既兼容 C 又兼容 C++,但是想在编译 C++ 时使用 -fno-exceptions
禁用异常,就可以用这个。此外还有 $<LINK_LANGUAGE:lang>
。(有点不懂为什么 link 阶段还有语言。)
$<TARGET_NAME_IF_EXISTS:target>
有就提供,没有就为空。
$<TARGET_FILE:target>
查询 target 的输出文件。
$<TARGET_PROPERTY:target,prop>
查询 target 的属性。
Escaping
$<ANGLE-R>
$<COMMA>
$<SEMICOLON>
由于生成时间晚,可以防止转义。
将变量和生成表达式混用是不妙的。比如变量中可能包含 “>",展开后提前终止了生成表达式。