现代 CMake 简明教程(二)- 设计理念与使用

系列文章目录

前言

现代 CMake 主张放弃传统的基于变量的方法,而是采用基于 target 的更具结构的模式。通过新的特性太重塑你的 CMake 系统,使其成为一个更可维护、更直观、更易集成、更具意义的方案。

关于现代 CMake 的概念可以在 《Effective CMake(slides)》 中找到根源。

让我们来看看下面的 cmake,你是否熟悉?

find_package(Boost 1.55 COMPONENTS asio) 
list(APPEND INCLUDE_DIRS ${BOOST_INCLUDE_DIRS})
list(APPEND LIBRARIES ${BOOST_LIBRARIES})

include_directories(${INCLUDE_DIRS})
link_libraries(${LIBRARIES})

从现在起,请别这么做,它在多个方面都是错误的。你只是将头文件目录和链接器标志扔进一个大锅里,然后告诉 CMake:Hi,你去这口大锅里找你自己想要的东西吧。Effective Modern CMake 告诉我们要忘记 include_directorieslink_librarieslink_directories 等命令,因为它们作用在目录层上,也就是说所有在该层定义的目标都会集成这些属性。这增加了隐形依赖的机会,最好的办法是直接对 target 进行操作。

当你在处理不同 target 之间的依赖关系的时候,才会发现上面写法简直就是地狱:你要处理依赖关系、链接顺序、头文件目录等等问题。当你意识到你正在处理这些混乱的关系时,就是你需要重新评估 CMake 系统的时候。让现代 CMake 来拯救你吧。

Targets 和 Properties

CMake 看到了上述的问题,因此引入了新的特性,可以更好的组织你的项目。现代 CMake 设计理念是围绕 target 和 property 的。

从概念上说,它们并不难理解:

  • 一个可执行程序是一个 target
  • 一个 lib 是一个 target
  • 一个应用程序可能依赖多个 lib,而 lib 之间又有依赖

每个 target 都有属性。属性可以是它的源文件、它需要的编译器选项、它所链接的库、它引用的头文件目录等等。在现代 CMake 中,你可以创建 targets,并在它们身上定义很多属性。

怎样?听上去是不是有点像面向对象的思想?如果把一个 target 想象成一个对象,会发现它们之间有很多相似的地方。

Build-Requirements & Usage-Requirements

软件开发中依赖是十分常见的,C/C++通过 include 头文件的方式引入依赖,在动态或静态链接后可以调用依赖实现。 一个可执行程序可能会依赖链接库,链接库也同样可能依赖其他的链接库。 此时一个棘手的问题是,使用者如何知道使用这些外部依赖库需要什么条件? 比方说,其头文件的代码可能需要开启编译器 C++17 的支持、依赖存在许多动态链接库时可能只需要链接其中的一小部分、有哪些间接依赖需要安装、间接依赖的版本要求是什么……

对于这些问题,最简单粗暴的解决方案即文字说明,依赖库的作者可以在某个 README、网站、甚至在头文件里说明使用要求,但这种方式效率显然是很低下的。

CMake 提供的解决方案是,在对 target 进行配置时,可以规定配置的类型,分为 Build-Requirements 和 Usage-Requirements 两类,会影响配置的应用范围。

Build-Requirements 表示仅在编译时需要满足,通过 PRIVATE 关键字声明。
Usage-Requirements 表示在外部使用时需要满足的,即在其他项目,使用了本项目编译好的 target 时需要满足的约束,通过 INTERFACE 关键字申明。
在实际中,有些配置在编译以及被使用时都需要满足,因此通过 PUBLIC 关键字申明。

上面干巴巴的说,不如来看一个例子。我们写了一个音频解码库,它:

  1. 这个链接了静态库 ffmpeg,使用到了 ffmpeg 的头文件和函数,
  2. 在实现文件中使用到了 c++14 的特性。

随后我们发布了这个库,其中有头文件和编译好的动态库链接。尽管我们在代码实现中使用了 c++14 语法,但是对外发布的头文件只用到了 c++03 的特性,也没有引入任何 ffmpeg 的代码。

这种情况下,其他工程在使用我们项目的 library 时,并不需要开启 c++14 特性,也不需要安装 ffmpeg。因此我们 library 可以这么写:

target_compile_feature(my_lib PRIVATE cxx_std_14)
target_link_libraries(my_lib PRIVATE ffmpeg)

这里 PRIVATE 表明 c++14 特性只在编译时需要,ffmpeg 库的链接也仅在编译时需要。但如果我们对外提供的头文件中包括 c++14 的特性,那么需要使用 PUBLIC 进行修饰

target_compile_feature(my_lib PUBLIC cxx_std_14)
target_link_libraries(my_lib PRIVATE ffmpeg)

另一种情况,当我们提供的库是 header only 时,也就不需要编译了,这时候通过 INTERFACE 修改配置,例如

target_compile_feature(my_header_only_lib INTERFACE cxx_std_14)

需要注意的是,通过 PUBLICINTERFACE 声明 Usage-Requirements 是会传递的,例如 libA 依赖 libB 后,会继承 libB 的 Usage-Requirements,此后 libC 再依赖 libA,那么会同时继承 libA 和 libB 的 Usage-Requirements。这种配置的继承可以方便我们管理库与库直接的依赖关系。

总结来说,Build-Requirements 和 Usage-Requirements 名副其实

  • Build-Requirements 即编译时需要的配置,通过例如头文件目录、编译选项、链接库、宏定义等。它的作用是让编译通过。
  • Usage-Requirements 即外部使用我们项目时需要的配置。具体来说,一般通过 install 命令发布我们的库后,我们需要告诉使用者,需要引入的头文件目录在哪、需要添加哪些编译选项等。 它的作用是让使用者能够正确集成。

Show me the code

想象你正在编写一个工具库,对 fftw3 库进行封装,方便傅里叶变化的计算,项目的目录结构长这样(完整代码在github):

├── CMakeLists.txt
├── include
│   └── fftutils
│       └── fftutils.h
└── src
    ├── fftutils.cpp
    ├── helper.cpp
    └── helper.h

fftutils.h 中定义了唯一需要暴露的接口 doFFT()

#include <fftw3.h>
fftw_complex* doFFT(fftw_complex* in, int n);

注意一点,这个头文件同时也暴露和使用了 fftw3 的代码。让我们开始编写 CMakeLists.txt

cmake_minimum_required(VERSION 3.10)

project(libfftutils
        VERSION 1.0.0
        LANGUAGES C CXX
        )

简单的指定 cmake 最低版本,以及 project 信息。没啥可说的。接着,创建 library

add_library(fftutils src/fftutils.cpp src/helper.cpp)

然后,为 fftutils 添加一些属性,例如头文件目录

target_include_directories(fftutils
        PUBLIC
            $<INSTALL_INTERFACE:include>
            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        PRIVATE
            ${CMAKE_CURRENT_SOURCE_DIR}/src
        )

我们的头文件位于两个不同的地方:src/ 其中包括一个 helper.h,它为 helper.cpp 编译提供定义;include/ 内,包括公共头文件 fftutils.h。为了使我们的库编译通过,我们同时需要这两个目录。

但另一方面,fftutils 的使用者只需要知道公共头文件 fftutils.h 的位置既即可。因此 INTERFACE_INCLUDE_DIRS 只需要包括 include/ 而不需要 src/

不过还是有个问题,在构建 fftutils 时,include//Users/admin/Documents/cmake_tutorial_example/fftutils/include 里,但是安装后,它可不在这个位置了。为了解决这个问题,我们使用生成器表达式,它可以根据不同的情况正确设置路径。

为 target 单独设置属性

我们可以为 target 设置属性,例如添加 -Werror 让编译器视警告为错误

target_compile_options(fftutils PRIVATE -Werror)

再或者,设置 c++11 以获取新的特性

target_compile_features(fftutils PRIVATE cxx_std_11)

target_link_libraries 解决依赖

至此,一切似乎都很美好,但你尝试编译的时候,却会发现缺失 fftw 库,你找不到 fftw 的头文件,更是无法链接它。模块之间的依赖关系总是令人头疼。

好在,对于 fftw 这样常用的库,有很多写了 FindFFTW.cmake 来方便我们找到 fftw。例如 findFFTW。非常棒,我们只要把这个文件拷贝到项目中,并添加一些代码,就能够将 fftw 集成进来

list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
find_package(FFTW)

接着,通过 target_link_libraries 对 fftw 进行连接。由于对外暴露的头文件中使用到了 fftw 的代码,因此使用 PUBLIC 关键字声明,以传递依赖。

target_link_libraries(fftutils PUBLIC FFTW::Double)

导出你的库

编译通过后,为了方便别的项目使用,需要将我们的库导出,包括头文件、编译好的二进制库、以及库的基本信息

include(GNUInstallDirs)
install(TARGETS fftutils
        EXPORT fftutils-targets
        LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
        ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})

install(DIRECTORY include/
        DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

# Export the targets to a script
install(EXPORT fftutils-targets
        FILE
            FFTUtilsTargets.cmake
        NAMESPACE
            FFTUtils::
        DESTINATION
            "lib/cmake/"
        )

install(FILES
        ${CMAKE_CURRENT_SOURCE_DIR}/cmake/FFTUtilsConfig.cmake
        ${CMAKE_CURRENT_SOURCE_DIR}/cmake/FindFFTW.cmake
        DESTINATION "lib/cmake/")

注意这两个文件,FFTUtilsTargets.cmake 和 FFTUtilsConfig.cmake。

FFTUtilsTargets.cmake 是由 cmake 生成的,它里头描述了 fftutils 的信息,例如头文件目录、链接库等等。

FFTUtilsConfig.cmake 通常你自己定义的,里头通常描述 fftutils 依赖关系等信息。让我们来看看它的内容

get_filename_component(FFTUtils_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
include(CMakeFindDependencyMacro)

list(APPEND CMAKE_MODULE_PATH ${FFTUtils_CMAKE_DIR})

find_dependency(FFTW)
include("${CMAKE_CURRENT_LIST_DIR}/FFTUtilsTargets.cmake")

其中 find_dependencyfind_package 作用一致。找到 FFTW 后,通过 inlcude 引入 FFTUtilsTargets.cmake,以此获得 fftutils 的库信息。

使用 fftutils

将 fftutils 发布的包拷贝至项目某处,接着通过 find_package() 找到 fftutils 即可。例如

cmake_minimum_required(VERSION 3.10)

project(example_exec)

set(FFTUtils_DIR ${CMAKE_CURRENT_SOURCE_DIR}/fftutils/lib/cmake)
find_package(FFTUtils)

add_executable(example_exec main.cpp)
target_link_libraries(example_exec PRIVATE FFTUtils::fftutils)

总结

现代 CMake 围绕 target 和 properties 设计,通过 INTERFACEPRIVATEPUBLIC 进行依赖的传递与管理。我们应该主动导出库信息,并且编写 config.cmake 文件,以便其他项目能够更好的集成。

完整代码已上传至 github

参考资料

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页