cmake 基础

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
)

参考翻译:

发布了185 篇原创文章 · 获赞 28 · 访问量 7万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: Age of Ai 设计师: meimeiellie

分享到微信朋友圈

×

扫一扫,手机浏览