cmake编译逻辑是通过一系列的顶层逻辑项目构成的,每个顶层项目都对应一个可执行文件、库文件或者用户自定义命令。通过建立完善的依赖关系和规则来达到我们想要的效果。
二进制项目构架
我们可以通过 add_excutable 和 add_library 命令来新建一个可执行项目或库文件项目。cmake会自动根据不同的编译平台来确定程序名称的前缀,后缀和扩展名。在二进制之间的依赖关系,我们可以用 target_link_libraries 来指定。 我们来看一个例程:
add_library(archive archive.cpp zip.cpp lzma.cpp)
add_executable(zipapp zipapp.cpp)
target_link_libraries(zipapp archive)
上面的例子中 archive 是定义的一个静态库项目。然后是使用了 add_executable 创建一个 zipapp 的应用程序编译项目, 然后使用 target_link_libraries 来给 zipapp 添加一个静态库 archive 的依赖。
add_library 在默认情况下,构建的是静态库项目,和下面语句等价:
add_library(archive STATIC archive.cpp zip.cpp lzma.cpp)
如果你想构建一个动态库项目,你可以将 STATIC 关键字改为 SHARED 关键字,如下:
add_library(archive SHARED archive.cpp zip.cpp lzma.cpp)
另外,你可以使用 option 来全局修改 BUILD_SHARED_LIBS 变量,如果该变量的值是 true 的话,那该项目当前的全部默认方式编译的库都编译为共享库, 否则的话,就默认编译为静态库。
不管是 在使用 STATIC 还是 SHARED 类型的库项目,流程其实都差不多,然而 add_library 来编译库项目的时候,我们还会遇到一种不是编译的时候加载的库,而是在项目运行的时候使用类是于 dlopen 类型的函数来加载的库,这类库项目不会在编译的时候使用 target_link_libraries 来产生项目依赖, 我们可以使用 MODULE 来指明。
add_library(archive MODULE 7z.cpp)
如果共享库,并没有导出任何符号,那这个库其实也和 MODULE 库一样, 不难理解,有效的 SHARED 库至少要导出一个能被别人使用的符号。
在苹果的框架下, 一个共享库可能还会被标记 FRAMEWORK 的项目属性,使用全局唯一的 MACOSX_FRAMEWORK_IDENTIFIER 属性来指明项目的 CFBundleIdentifier。如下所示:
add_library(MyFramework SHARED MyFramework.cpp) set_target_properties(MyFramework PROPERTIES FRAMEWORK TRUE FRAMEWORK_VERSION A MACOSX_FRAMEWORK_IDENTIFIER org.cmake.MyFramework )
在编译项目的时候,我们还可能就是将源文件编译成 .o 的中间文件来供其他项目使用, 而不是真的将我们的源文件一次性就编译成库文件或者可执行文件, 这个时候,也可以使用 add_library 来创建一个 OBJECT 项目。
OBJECT 项目的构建和使用方法如下:
add_library(archive OBJECT archive.cpp zip.cpp lzma.cpp)
add_library(archiveExtras STATIC $<TARGET_OBJECTS:archive> extras.cpp)
add_executable(test_exe $<TARGET_OBJECTS:archive> test.cpp)
第一句 add_library 命令就是使用 OBJECT 关键字来构建名字为 archive 的 OBJECT 项目
下面的 $<TARGET_OBJECTS:archive> 生成语句就是对 archive 的 OBJECT 项目进行使用, TARGET_OBJECTS 会罗列出所有的 archive 的文件。
我们也可以使用 target_link_libraries 来建立其他项目和 OBJECT 项目的依赖关系。
使用方法如下所示:
add_library(archive OBJECT archive.cpp zip.cpp lzma.cpp)
add_library(archiveExtras STATIC extras.cpp)
target_link_libraries(archiveExtras PUBLIC archive)
add_executable(test_exe test.cpp)
target_link_libraries(test_exe archive)
这里的 Object 中间项目是不能用于 add_custom_command 的 TARGET 定义的依赖, 但是还是可以用 $<TARGET_OBJECTS:archive> 的方式用于 add_custom_command 的 OUTPUT 定义和 file 的 GENERATE 定义。
编译参数
在编译一个项目的时候,我们除了要给项目指定编译的源文件,还需指定一些其他的参数,在cmake中,用得最多的三个命令为 指定头文件目录的 target_include_directories, 指定编译宏定义的 target_compile_definitions 和指定编译选项的 target_compile_options 命令。
这三个命令会分别填充 INCLUDE_DIRECTORIES, COMPILE_DEFINITIONS, COMPILE_OPTIONS 和 INTERFACE_INCLUDE_DIRECTORIES, INTERFACE_COMPILE_DEFINITIONS, INTERFACE_COMPILE_OPTIONS项目属性。 INTERFACE_* 属性是只如果其他项目对该项目产生依赖,就加上该宏,一般编译库项目的时候会使用。
命令参数和项目属性所示:
命令 | 参数 | 属性 |
---|---|---|
target_include_directories | PRIVATE | INCLUDE_DIRECTORIES |
PUBLIC | INCLUDE_DIRECTORIES, INTERFACE_INCLUDE_DIRECTORIES | |
INTERFACE | INTERFACE_INCLUDE_DIRECTORIES | |
target_compile_definitions | PRIVATE | COMPILE_DEFINITIONS |
PUBLIC | COMPILE_DEFINITIONS, INTERFACE_COMPILE_DEFINITIONS | |
INTERFACE | INTERFACE_COMPILE_DEFINITIONS | |
target_compile_options | PRIVATE | COMPILE_OPTIONS |
PUBLIC | COMPILE_OPTIONS, INTERFACE_COMPILE_OPTIONS | |
INTERFACE | INTERFACE_COMPILE_OPTIONS |
总结下: PRIVATE 只修改 不含 INTERFACE 的项目属性, INTERFACE 只修改含 INTERFACE 的属性, PUBLIC 两者都要修改。
比如下面:
target_compile_definitions(archive
PRIVATE BUILDING_WITH_LZMA
INTERFACE USING_ARCHIVE_LIB
)
运行上面命令,效果是:
COMPILE_DEFINITIONS 添加了 BUILDING_WITH_LZMA 宏定义,
INTERFACE_COMPILE_DEFINITIONS 添加了 USING_ARCHIVE_LIB 宏定义。
由于这三个 cmake 给的项目参数对于我们编译项目来说真的太重要了, 接下来我们来结合编译过程来看看 cmake 通过这三个项目属性为我们做了什么。我在这里整理了一个表格:
项目属性 | linux | windows |
---|---|---|
INCLUDE_DIRECTORIES | -I | -isystem |
COMPILE_DEFINITIONS | -D | /D |
COMPILE_OPTIONS | 取决于工具 |
INTERFACE_* 属性的内容会被依赖该项目的项目用到,我们来看看下面例子:
set(srcs archive.cpp zip.cpp)
if (LZMA_FOUND)
list(APPEND srcs lzma.cpp)
endif()
add_library(archive SHARED ${srcs})
if (LZMA_FOUND)
# 只有编译 archive 的时候会用到 -DBUILDING_WITH_LZMA。
target_compile_definitions(archive PRIVATE BUILDING_WITH_LZMA)
endif()
target_compile_definitions(archive INTERFACE USING_ARCHIVE_LIB)
add_executable(consumer)
# 下面的 consumer 项目由于对 archive 项目产生了依赖,所以会用到 -DUSING_ARCHIVE_LIB。
target_link_libraries(consumer archive)
属性传递
在实际的环境中,可能会要求属性的传递。 target_link_libraries 命令可以通过 PRIVATE, INTERFACE 和 PUBLIC 关键字来控制传递过程,下面来看一个例子:
add_library(archive archive.cpp)
target_compile_definitions(archive INTERFACE USING_ARCHIVE_LIB)
add_library(serialization serialization.cpp)
target_compile_definitions(serialization INTERFACE USING_SERIALIZATION_LIB)
add_library(archiveExtras extras.cpp)
target_link_libraries(archiveExtras PUBLIC archive)
target_link_libraries(archiveExtras PRIVATE serialization)
# archiveExtras 项目编译会用到 -DUSING_ARCHIVE_LIB 和 DUSING_SERIALIZATION_LIB 参数。
add_executable(consumer consumer.cpp)
# consumer 项目编译会用到 -DUSING_ARCHIVE_LIB 参数
target_link_libraries(consumer archiveExtras)
由于 archive 是 archiveExtras 的 PUBLIC 依赖。 所以编译参数会传递给 consumer。 而 serialization 是 archiveExtras 的 PRIVATE 依赖, 所以编译 consumer 项目的时候,没有 serialization 的编译参数。
通常,如果只有库实现而不是头文件使用了依赖项,则应在使用 target_link_libraries 的 PRIVATE 关键字的情况下指定依赖项。如果在库的头文件中额外使用了依赖项(例如,用于类继承),则应将其指定为 PUBLIC 依赖项。库的实现不使用而是仅由其标头使用的依赖关系应指定为INTERFACE依赖关系。
可以多次使用 target_link_libraries 命令来使用每个关键字:
target_link_libraries(archiveExtras
PUBLIC archive
PRIVATE serialization
)
其实 cmake 处理使用 PRIVATE, INTERFACE 和 PUBLIC 三个关键字来指明继承关系外, 还可以手动完成继承关系。如:
target_link_libraries(myExe lib1 lib2 lib3)
target_include_directories(myExe
PRIVATE $<TARGET_PROPERTY:lib3,INTERFACE_INCLUDE_DIRECTORIES>)
兼容属性
有的项目里面可能需要项目和依赖接口之间的兼容,比如 POSITION_INDEPENDENT_CODE 属性指定编译的项目是否是位置无关,这可能会依赖于编译环境。而在这个项目的依赖中可能设置的和这个项目设置的值不一样,如:
add_library(lib1 SHARED lib1.cpp)
set_property(TARGET lib1 PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON)
add_library(lib2 SHARED lib2.cpp)
set_property(TARGET lib2 PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE OFF)
add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 lib1)
set_property(TARGET exe1 PROPERTY POSITION_INDEPENDENT_CODE OFF)
add_executable(exe2 exe2.cpp)
target_link_libraries(exe2 lib1 lib2)
这里我们的依赖中, lib1 和 lib1 是不一样的, 我们 exe1 通过 set_property 的 TARGET 手动设置 POSITION_INDEPENDENT_CODE 的值位 OFF。 而在 exe2 中由于没有指明,并且 lib1 和 lib2 的 INTERFACE_POSITION_INDEPENDENT_CODE 不兼容, 所以编译运行的时候就会打印出诊断信息。
当然某些情况下,cmake也自动兼容,比如看下面例子:
add_library(lib1Version2 SHARED lib1_v2.cpp)
set_property(TARGET lib1Version2 PROPERTY INTERFACE_CONTAINER_SIZE_REQUIRED 200)
set_property(TARGET lib1Version2 APPEND PROPERTY
COMPATIBLE_INTERFACE_NUMBER_MAX CONTAINER_SIZE_REQUIRED
)
add_library(lib1Version3 SHARED lib1_v3.cpp)
set_property(TARGET lib1Version3 PROPERTY INTERFACE_CONTAINER_SIZE_REQUIRED 1000)
add_executable(exe1 exe1.cpp)
# CONTAINER_SIZE_REQUIRED will be "200"
target_link_libraries(exe1 lib1Version2)
add_executable(exe2 exe2.cpp)
# CONTAINER_SIZE_REQUIRED will be "1000"
target_link_libraries(exe2 lib1Version2 lib1Version3)
这里 lib1Version2 指定 INTERFACE_CONTAINER_SIZE_REQUIRED 为 200, 而 lib1Version3 指定 INTERFACE_CONTAINER_SIZE_REQUIRED 为 1000。 再exe2 中 cmake 会自动兼容为 1000。
如果不想出现不可预知的效果,建议大家再项目中明确指明能想到的项目参数,而不是使用传递。
属性调试
由于属性有依赖传递,在项目变大的过程中,可能会出现很多棘手的问题。这个时候,我们可能需要有一个调试的方法。
在 cmake 中提供了 CMAKE_DEBUG_TARGET_PROPERTIES 变量来帮助我们调试, cmake 会跟踪添加到 CMAKE_DEBUG_TARGET_PROPERTIES 中的变量, 使用方式如下:
set(CMAKE_DEBUG_TARGET_PROPERTIES
INCLUDE_DIRECTORIES
COMPILE_DEFINITIONS
POSITION_INDEPENDENT_CODE
CONTAINER_SIZE_REQUIRED
LIB_VERSION
)
add_executable(exe1 exe1.cpp)
对于在 COMPATIBLE_INTERFACE_BOOL 或 COMPATIBLE_INTERFACE_STRING 中列出的属性,调试输出将显示哪个目标负责设置该属性,以及哪些其他依赖项也定义了该属性。对于 COMPATIBLE_INTERFACE_NUMBER_MAX 和 COMPATIBLE_INTERFACE_NUMBER_MIN ,调试输出将显示每个依赖项的属性值,以及该值是否确定新的极值。[我本人并没有太搞明白这是怎么回事,欢迎大家留言讨论!!]
生成表达式的使用向导
生成语句可以根据当时编译环境来确定命令的参数,而不是在我们编写 cmake 的时候, 这样无疑大大加大了 cmake 配置文件的使用的兼容性。
其实我们在上面参数传递的时候也用过生成表达式,其实这是一套成熟完善的生成器,非常强大,比如我们根据是否是debug模式来指定是否展开 debug 编译参数。如果不是 debug 模式,那就是空参数。
target_compile_definitions(exe1 PRIVATE
$<$<CONFIG:Debug>:DEBUG_BUILD>
)
当然,还有很多生成表达式, 我们后面再做详细讨论。
头文件目录与使用
target_include_directories 命令可以接收相对路径和绝对路径, 绝对路径就不用管了,如果是默认的相对路径,就是相对于项目目录。
如果是 IMPORTED 项目,那 INTERFACE_INCLUDE_DIRECTORIES 属性就不能够使用相对路径。
我们也可以使用生成器代码中[INSTALL_INTERFACE]表达式来结合预配置安装路径[INSTALL_PREFIX]来指定,当然很灵活,看下例子:
add_library(ClimbingStats climbingstats.cpp)
target_include_directories(ClimbingStats INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/generated> # 这里 BUILD_INTERFACE 生成器 可以和 INSTALL_INTERFACE 来分离编译环境和运行环境
$<INSTALL_INTERFACE:/absolute/path>
$<INSTALL_INTERFACE:relative/path>
$<INSTALL_INTERFACE:$<INSTALL_PREFIX>/$<CONFIG>/generated>
)
为了满足更加方便的头文件指明,我们可以使用 CMAKE_INCLUDE_CURRENT_DIR_IN_INTERFACE 来书写更加简单的表达式:
set_property(TARGET tgt APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR};${CMAKE_CURRENT_BINARY_DIR}>
)
库的链接与生成表达式
在提供链接库操作的时候,我们也可以使用生成表达式来根据实际情况来给定 target_link_libraries 的参数。
add_library(lib1 lib1.cpp)
add_library(lib2 lib2.cpp)
target_link_libraries(lib1 PUBLIC
$<$<TARGET_PROPERTY:POSITION_INDEPENDENT_CODE>:lib2>
)
add_library(lib3 lib3.cpp)
set_property(TARGET lib3 PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON)
add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 lib1 lib3)
上面语句第4行 TARGET_PROPERTY 那句话表示如果是项目属性 POSITION_INDEPENDENT_CODE, 如果为真才链接 lib2 库,否则不练接。
cmake 输出构建
cmake 编译项目通过一些的配置来生成 add_library 和 add_executable 建立的二进制项目。二进制文件的确切输出位置只能在生成时确定,因为它可能取决于构建配置和链接依赖项的链接语言等。 TARGET_FILE , TARGET_LINKER_FILE 和相关表达式可用于访问文件的名称和位置生成的二进制文件。但是,这些表达式不适用于 OBJECT 类型的库,因为此类库不会生成与表达式相关的单个文件。
cmake 可以构建下面说的三类输出,大致分dll平台和 非dll 平台, 所有的基于windows的系统都是 dll 平台:
-
运行输出
- add_executable 构建的可执行程序(如: .exe)
- 对于 dll 平台: add_library 的 SHARED 选项编译的共享库(如: .dll)。
RUNTIME_OUTPUT_DIRECTORY和RUNTIME_OUTPUT_NAME目标属性可用于控制构建树中的运行时输出工件位置和名称。
-
库输出
- add_library 的 MODULE 编译的可加载模块文件。(如: .dll, .so)
- 对于 非dll 平台: add_library 的 SHARED 选项编译的共享库 (.so, .dylib)
LIBRARY_OUTPUT_DIRECTORY和LIBRARY_OUTPUT_NAME目标属性可用于控制构建树中库输出工件的位置和名称。
-
归档输出
- add_library 的 STATIC 选项创建传递静态库(如: .lib, .a)
- 在 dll 平台上: add_library 的 SHARED 选项构建的导入库, 必须确保该库至少有一个扩展符号(如: .lib)
- 在 dll 平台上; add_executable 在 ENABLE_EXPORTS 设置了的时候构建的导入库(如: .lib)
- 在 AIX 上, add_executable 在 ENABLE_EXPORTS 设置了的时候构建的导入链接(如: .imp)
ARCHIVE_OUTPUT_DIRECTORY和ARCHIVE_OUTPUT_NAME目标属性可用于控制构建树中归档输出工件的位置和名称。
目录范围的命令
target_include_directories , target_compile_definitions 和 target_compile_options 命令一次仅对一个目标有效。命令add_compile_definitions,add_compile_options和include_directories具有相似的功能,但为方便起见,它们在目录范围而不是目标范围内运行。
伪目标
一些目标类型不代表构建系统的输出,而仅代表诸如外部依赖项,别名或其他非构建工件之类的输入。伪目标未在生成的构建系统中表示。
导入项目
导入项目是给本项目添加一个已经存在的项目依赖,通常导入的项目是一个上流,不希望在本项目在做修改的项目。声明导入的项目后,可以像使用其他常规项目一样,通过使用常规命令(例如 target_compile_definitions , target_include_directories , target_compile_options 或 target_link_libraries)来调整其目标属性。
导入的目标可能具有与二进制目标相同的使用需求属性,例如 INTERFACE_INCLUDE_DIRECTORIES , INTERFACE_COMPILE_DEFINITIONS , INTERFACE_COMPILE_OPTIONS , INTERFACE_LINK_LIBRARIES 和 INTERFACE_POSITION_INDEPENDENT_CODE。
位置也可以从导入的目标中读取,尽管很少有理由这样做。诸如 add_custom_command 之类的命令可以透明地将 IMPORTED EXECUTABLE 目标用作 COMMAND 可执行文件。
导入目标的定义范围是定义目标的目录。可以从子目录访问和使用它,但不能从父目录或兄弟目录访问和使用它。作用域类似于cmake变量的作用域。
也可以定义在构建系统中全局可访问的GLOBAL IMPORTED目标。
有关创建具有IMPORTED目标的程序包的更多信息,请参见cmake-packages(7)手册。
项目别名
ALIAS 目标是可以在只读上下文中与二进制目标名称互换使用的名称。 ALIAS 目标的主要用例是,例如,带有库的单元测试可执行文件或库,该库可以是同一构建系统的一部分,也可以根据用户配置单独构建。
add_library(lib1 lib1.cpp)
install(TARGETS lib1 EXPORT lib1Export ${dest_args})
install(EXPORT lib1Export NAMESPACE Upstream:: ${other_args})
add_library(Upstream::lib1 ALIAS lib1)
在另一个目录中,我们可以无条件链接到Upstream :: lib1目标,该目标可以是软件包中的IMPORTED目标,也可以是ALIAS目标(如果作为同一构建系统的一部分构建)。
if (NOT TARGET Upstream::lib1)
find_package(lib1 REQUIRED)
endif()
add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 Upstream::lib1)
ALIAS目标不可更改,不可安装或导出。它们对于构建系统描述而言完全是本地的。可以通过从中读取ALIASED_TARGET属性来测试名称是否为ALIAS名称:
get_target_property(_aliased Upstream::lib1 ALIASED_TARGET)
if(_aliased)
message(STATUS "The name Upstream::lib1 is an ALIAS for ${_aliased}.")
endif()
接口库
接口目标没有定位的,并且是可变的,但在其他方面类似于导入的目标。
它可以指定使用要求,例如INTERFACE_INCLUDE_DIRECTORIES,INTERFACE_COMPILE_DEFINITIONS,INTERFACE_COMPILE_OPTIONS,INTERFACE_LINK_LIBRARIES,INTERFACE_SOURCES和INTERFACE_POSITION_INDEPENDENT_CODE。仅target_include_directories,target_compile_definitions,target_compile_options,target_sources和target_link_libraries命令的INTERFACE模式可与INTERFACE库一起使用。
INTERFACE库的主要用例是仅标头库。
add_library(Eigen INTERFACE)
target_include_directories(Eigen INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
$<INSTALL_INTERFACE:include/Eigen>
)
add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 Eigen)
在这里,编译时会消耗和使用Eigen目标的使用要求,但对链接没有影响。
另一个用例是针对使用需求采用完全针对目标的设计:
add_library(pic_on INTERFACE)
set_property(TARGET pic_on PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON)
add_library(pic_off INTERFACE)
set_property(TARGET pic_off PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE OFF)
add_library(enable_rtti INTERFACE)
target_compile_options(enable_rtti INTERFACE
$<$<OR:$<COMPILER_ID:GNU>,$<COMPILER_ID:Clang>>:-rtti>
)
add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 pic_on enable_rtti)
这样,exe1的构建规范就完全表示为链接的目标,并且特定于编译器的标志的复杂性被封装在INTERFACE库目标中。
允许在INTERFACE库上设置或读取的属性是:
- 与INTERFACE_ *相匹配的属性
- 符合COMPATIBLE_INTERFACE_ *的内置属性
- EXPORT_NAME
- EXPORT_PROPERTIES
- IMPORTED
- MANUALLY_ADDED_DEPENDENCIES
- NAME
- 与IMPORTED_LIBNAME_ *匹配的属性
- 与MAP_IMPORTED_CONFIG_ *匹配的属性
可以安装和导出INTERFACE库。他们引用的任何内容都必须单独安装:
add_library(Eigen INTERFACE)
target_include_directories(Eigen INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
$<INSTALL_INTERFACE:include/Eigen>
)
install(TARGETS Eigen EXPORT eigenExport)
install(EXPORT eigenExport NAMESPACE Upstream::
DESTINATION lib/cmake/Eigen
)
install(FILES
${CMAKE_CURRENT_SOURCE_DIR}/src/eigen.h
${CMAKE_CURRENT_SOURCE_DIR}/src/vector.h
${CMAKE_CURRENT_SOURCE_DIR}/src/matrix.h
DESTINATION include/Eigen
)
参考翻译: