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

    • 【01】安装与基本介绍
    • 【02】CMake 的文件分布
    • 【03】变量的设置与引用
    • 【04】运算符与条件、循环语句
    • 【05】目标构建
    • 【06】变量参与 C++ 程序的编译
    • 【07】宏与函数
    • 【08】find_package 详解
    • 【09】生成器表达式
    • 【10】项目的导出与安装
      • 1. 前言
      • 2. 文件、目标的安装规则
        • 2.1 文件安装
        • 2.2 目录(文件夹)安装
        • 2.3 目标安装
      • 3. 导出目标的安装规则
        • 3.1 何为导出目标、导出配置
        • 3.2 基本语法
        • 3.3 导出配置文件的基本内容
        • 3.4 回顾:为何要使用 target_xxx
      • 4. XxxConfig.cmake 文件的一般写法
        • 4.1 写法
        • 4.2 剖析 OpenCVConfig.cmake
      • 5. CMake 项目导出与安装的内容总结
      • 思考🤔
    • 【11】项目实战
    • 【12】CTest
    • 【13】CPack

【10】项目的导出与安装

1. 前言

在【08】find_package 详解一文中我们了解到 find_package 的两种工作方式,第一种的 Module 模式需要我们为现成的第三方头文件库文件编写 FindXxx.cmake 从而能够使用 find_package,第二种的 Config 模式则是打包项目时采用的主流方式,即通过编写 XxxConfig.cmake 文件来使用 find_package。大多数开源项目,若是使用CMake提供了编译安装方式,一定会涉及到这一文件,例如 OpenCV 的 cmake/templates 文件夹中就有 OpenCVConfig.cmake.in 的模板文件,这一文件在CMake的配置阶段会生成对应的 OpenCVConfig.cmake 文件。

对于 XxxConfig.cmake 文件的写法会在最后进行介绍,在一开始让我们先了解一下安装的基本命令。

2. 文件、目标的安装规则

—— install 命令

在现代 CMake 中,所有内容的安装,均来自 install 命令,包括文件(夹)、二进制目标、伪目标以及后文的导出目标。在 CMakeLists.txt 文件中添加了 install 命令并不会直接执行安装操作,实际上 install 只是给出了安装的规则,并发生在生成阶段。可以回顾【01】安装与基本介绍的内容,CMake 的作用是生成建构档,而需要安装的具体内容将会生成在该建构档中,如 Unix Makefile。

我们可以执行

make install

命令,完成实际的安装操作。

提示

此处只介绍具有基本文件结构的安装,导出目标的安装在下一节进行介绍。

2.1 文件安装

即:install(FILES)

这里先直接给出文件安装的 install 命令用法,下面列举一些常见的可选内容。

  • DESTINATION:指定要安装到的目标目录。
  • PERMISSIONS:指定要安装文件的权限,权限参数要求是一系列 OWNER_*、GROUP_* 和 WORLD_* 权限值的组合,例如 OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ。
  • CONFIGURATIONS:指定要在哪些构建配置下安装文件。可以使用一个或多个构建配置的名称来限定。
  • COMPONENT:指定要将文件归类到的组件名称,以便于后续进行管理和打包(见【13】CPack)。
  • RENAME:指定在安装时重命名文件或目录的名称。
  • OPTIONAL:表示如果文件不存在则不会抛出错误。
# 定义于 <path-to-opencv>/include/CMakeLists.txt 中

install(FILES "opencv2/opencv.hpp"
    DESTINATION ${OPENCV_INCLUDE_INSTALL_PATH}/opencv2
    COMPONENT dev)

这句命令添加了将单一文件 opencv2/opencv.hpp 安装到指定目录的安装规则,默认情况下是 /usr/local/include/opencv4/opencv2 中,并归类到 dev 组件的安装规则中,在执行 sudo make install 命令的时候会执行该安装操作。

其余可选内容如 RENAME 用法与上文给出的例子类似。

2.2 目录(文件夹)安装

即 install(DIRECTORY)

使用方式与文件安装基本一致,但相比于文件安装,主要多出了 PATTERN、EXCLUDE_PATTERN 和 RECURSE 关键字,通过 PATTERN 和 EXCLUDE_PATTERN 参数指定要包含和排除的文件,使用 RECURSE 参数进行递归安装。下面给出一个简单的,OpenCV 中安装文档所在文件夹的例子。

# 定义于 <path-to-opencv>/doc/CMakeLists.txt 中

install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doxygen/html
  DESTINATION "${OPENCV_DOC_INSTALL_PATH}"
  COMPONENT "docs" OPTIONAL
  ${compatible_MESSAGE_NEVER} # 可以不用管这句话
)

这句命令将当前二进制生成目录中的 doxygen/html 文件夹安装到 ${OPENCV_DOC_INSTALL_PATH} 中,并归类到 docs 中,OPTIONAL 说明了即使这个目录不存在,也不会抛出安装的错误。同样,需要在终端执行 make install 命令以完成安装。

2.3 目标安装

即 install(TARGETS)

用法与文件、文件夹安装基本一致,但由于 CMake 中的目标不仅仅包含动态、静态库,也有可执行文件,也有接口库、导入库等伪目标,因此 CMake 目标安装是个广泛的概念,接口上也能适配多种目标的安装,例如以下命令

install(
  TARGETS my_target
  LIBRARY DESTINATION lib
  ARCHIVE DESTINATION lib
  RUNTIME DESTINATION bin
)

先来考虑二进制目标的情况

  • LIBRARY 指代的是 Unix 文件系统的共享库
  • ARCHIVE 指代的是 Unix 文件系统的静态库,以及 Windows 下的静态链接库和导入库
  • RUNTIME 指代的是可执行文件以及 Windows 下的动态链接库

当 my_target 是普通库目标时,在 Linux 下,对应的 *.so 或 *.a 目标会被安装至 <prefix>/lib 下,同样,在 Windows 下,对应的 *.dll 和DLL导入库 *.lib 也会被安装到 <prefix>/lib 下。当 my_target 是可执行文件时,会被安装到 <prefix>/bin 下

3. 导出目标的安装规则

3.1 何为导出目标、导出配置

在【05】目标构建 #2.4我们介绍过 IMPORTED 导入目标的概念,虽然 IMPORTED 目标本身很有用,但我们仍然需要知道这些 IMPORTED 目标所指代或者包含的文件在磁盘上的位置。 IMPORTED 目标的真正强大之处在于,当提供目标文件的项目计划提供一个 CMake 文件来帮助导入它们时,可以设置项目以生成,以便其他 CMake 项目可以轻松地使用它,这一必要的信息就是所谓的导出目标。

在开发完一个库的时候,需要将其打包给他人使用(注意不是打包让他人源码编译,而是直接拿来用),我们需要提供一个包含这个库里面所有待导出目标信息的 *.cmake 文件,称为导出配置文件。该文件正是在 CMake 的生成阶段由导出目标生成的。不过,这一文件我们并不会直接使用,而是间接的通过使用 XxxConfig.cmake 的方式使用该导出目标对应的 *.cmake 导出配置文件。我们先回到最基本的,如何添加导出目标和生成导出配置文件。

3.2 基本语法

首先我们需要指定哪些目标需要导出,并且设置导出目标的名称,假设还是上文的 my_target 目标,我们在使用 install 命令时可以指定 EXPORT,即

install(
  TARGETS my_target
  LIBRARY DESTINATION lib
  ARCHIVE DESTINATION lib
  RUNTIME DESTINATION bin
  EXPORT MyModules
)

这里的 MyModules 就是导出目标名,并且该导出目标包含了关于 my_target 的所有信息,包括头文件路径、库文件路径等内容。如果我在此之上继续写入

install(
  TARGETS my_target_2
  LIBRARY DESTINATION lib
  ARCHIVE DESTINATION lib
  EXPORT MyModules
)

则此时导出目标 MyModules 包含了两个目标的内容,即 my_target 和 my_target_2。

最后需要将该导出目标转换为对应的 *.cmake 文件,即指定导出目标的安装策略,语法采用 install(EXPORTS)

install(
  EXPORT MyModules
  FILE MyModules.cmake
  DESTINATION "lib/cmake/${PROJECT_NAME}" # 一般安装路径会设置在此处
)

如果不额外指定 OpenCV 的安装路径,可以在 /usr/local/lib/cmake/opencv4 下找到 OpenCVModules.cmake 以及 OpenCVModules-release.cmake这两个文件,这个就是 OpenCV 的导出配置文件。

3.3 导出配置文件的基本内容

我们就以 OpenCV 4.8.0 在执行完安装命令之后生成的导出配置文件

  1. OpenCVModules.cmake,点击此处跳转至该文件
  2. OpenCVModules-release.cmake,点击此处跳转至该文件

两个文件中的内容进行介绍,因为这类文件是由导出目标在安装后自动生成的,所以不论是什么项目,在内容上都具有相似性

上文中的 OpenCVModules.cmake 文件,具有以下几个部分

  1. 防止重复包含,来自如下注释:

    # Protect against multiple inclusion, which would fail when already imported targets are added once more.

    使用 foreach 遍历所有待导入目标的名字(即已被安装的目标的名字),判断这些变量是否已经是目标,如果是说明已经重复包含。

  2. 获取该项目的安装路径,并设置为一个变量 _IMPORT_PREFIX,该变量我们可以看到执行了好几次重复的命令

    get_filename_component(_IMPORT_PREFIX "${CMAKE_CURRENT_LIST_FILE}" PATH)
    get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH)
    get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH)
    get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH)
    

    如果当前文件路径是 /usr/local/lib/cmake/opencv4,那么四次设置 get_filename_component 分别表示

    • 得到/usr/local/lib/cmake/opencv4
    • 得到/usr/local/lib/cmake
    • 得到/usr/local/lib
    • 得到/usr/local

    最终的 _IMPORT_PREFIX 正是项目的安装路径 /usr/local

  3. 创建导入目标,并初步设置属性

    这是该导出配置文件的核心所在,包含了类似如下的一系列语句

    # Create imported target opencv_core
    add_library(opencv_core SHARED IMPORTED)
    
    # Create imported target opencv_flann
    add_library(opencv_flann SHARED IMPORTED)
    
    set_target_properties(opencv_flann PROPERTIES
      INTERFACE_LINK_LIBRARIES "opencv_core;opencv_core"
    )
    

    可能有读者会注意到,这里并没有为导入目标设置 IMPORTED_LOCATION 属性。没错,所以说这里是设置属性。

  4. 包含 OpenCVModules-<config>.cmake 文件,在此文件中设置属性。如果该项目是

    • Release 模式编译的,那么会包含 OpenCVModules-release.cmake 文件
    • Debug 模式编译的,那么会包含 OpenCVModules-debug.cmake 文件
    • 不指定配置模式的,那么会包含 OpenCVModules-noconfig.cmake 文件

    在 OpenCVModules-<config>.cmake 文件中会为导入目标设置 IMPORTED_LOCATION_<config> 的相关属性,例如

    # Import target "opencv_core" for configuration "Release"
    set_property(TARGET opencv_core APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
    set_target_properties(opencv_core PROPERTIES
      IMPORTED_LOCATION_RELEASE "${_IMPORT_PREFIX}/lib/libopencv_core.so.4.8.0"
      IMPORTED_SONAME_RELEASE "libopencv_core.so.408"
      )
    
    list(APPEND _IMPORT_CHECK_TARGETS opencv_core )
    

    无论是这里的 IMPORTED_LOCATION_RELEASE 还是自己设置导入目标中的 IMPORTED_LOCATION,都会为目标设置 LOCATION 属性,这正是目标能够被正确链接的关键。

  5. 检查所有导入目标所指定的库文件(*.a 或者 *.so)是否存在。

    OpenCVModules-<config>.cmake 文件中为所有的导入目标都设置了库文件的路径,这个路径的存在与否将在此进行判断,不存在将会立刻终止 CMake 程序。

3.4 回顾:为何要使用 target_xxx

提示

下面介绍一个经常被提及的话题,为什么要使用 target_xxx

在【05】目标构建 #2一文中介绍了 target_include_directories 和 target_link_libraries 的用法,并指出尽量不要使用 include_directories 和 link_libraries 两种,下面将根据本文导出配置的内容介绍为何要使用 target_xxx。

一般使用 target_include_directories 的时候,需要包含的目录会与指定的 CMake 目标绑定起来(这里的目标同样可以是库、可执行文件、接口库等内容),比方说以下命令

add_library(aa aaa.cpp)
target_include_directories(
  aa PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/include>
  $<INSTALL_INTERFACE:include/MyLibrary>
)

####################
###  other code  ###
####################

add_library(bb bbb.cpp)
target_link_libraries(
  bb
  PUBLIC aa
)

根据以前的知识

  • 会把 include 目录添加到 aa 目标下
  • aa 会被 bb 所链接

此外,如果 aa 和 bb 目标被安装至导出目标 MyModules 上,并且安装,即使用以下代码

install(
  TARGETS aa bb
  EXPORT MyModules
)
install(
  EXPORT MyModules
  FILE MyModules.cmake
  DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/MyLibrary"
)

运行 make install 后可以在安装路径 <path-to-install>/cmake/MyLibrary/MyModules.cmake 下找到如下类似的语句

# Create imported target aa
add_library(aa STATIC IMPORTED)

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

## Create imported target bb
add_library(bb STATIC IMPORTED)

set_target_properties(bb PROPERTIES
  INTERFACE_LINK_LIBRARIES "aa"
)

这便是使用 target_xxx 命令的作用,在导出并安装时,会将这些链接、包含关系继续绑定在该目标上,而使用 include_directories 和 link_libraries 则没有这些效果。当然,在不需要进行导出安装的项目中,完全可以使用这些不带 target_ 前缀的这些命令。不过这些不带 target_ 为前缀的命令会作为全局包含的形式给其他目标使用,有一定的 “污染” 嫌疑。

4. XxxConfig.cmake 文件的一般写法

4.1 写法

在使用

find_package(OpenCV REQUIRED)

的时候,就是按照搜索原则,在系统路径下寻找 OpenCVConfig.cmake 文件,若我们想通过使用 Config 模式,使用

find_package(MyLibrary REQUIRED)

来发现包,我们也需要在合适的位置创建 MyLibraryConfig.cmake 文件,这个文件一般是通过使用文件安装(即 install(FILES) 的方式安装到指定位置的)。一个 XxxConfig.cmake 文件与 FindXxx.cmake 包含的内容基本一致,包含

  1. 搜索头文件
  2. 搜索库文件
  3. 提供 CMake 目标变量
  4. 搜寻的结果、状态

这里直接给出内容

# ======================================================
#  MyLibrary CMake 配置文件
# ======================================================

# ======================================================
#  头文件搜索
# ======================================================
# 获取没有 ../.. 相对路径标记的绝对路径
get_filename_component(ML_CONFIG_PATH "${CMAKE_CURRENT_LIST_DIR}" REALPATH)
get_filename_component(ML_INSTALL_PATH "${ML_CONFIG_PATH}/../../../" REALPATH)

# 搜索,添加至全局变量 MyLibrary_INCLUDE_DIRS
set(ML_INCLUDE_COMPONENTS "${ML_INSTALL_PATH}/include/MyLibrary")
set(MyLibrary_INCLUDE_DIRS "")
foreach(d ${ML_INCLUDE_COMPONENTS})
  get_filename_component(_d "${d}" REALPATH)
  if(NOT EXISTS "${_d}")
    message(WARNING "MyLibrary: Include directory doesn't exist: '${d}'. MyLibrary installation may be broken. Skip...")
  else()
    list(APPEND MyLibrary_INCLUDE_DIRS "${_d}")
  endif()
endforeach()
unset(_d)

# ======================================================
# 库文件搜索
# ======================================================
# 包含导出配置的 *.cmake 文件
include(${CMAKE_CURRENT_LIST_DIR}/MyModules.cmake)

# 添加至全局变量 MyLibrary_LIBS
set(ML_LIB_COMPONENTS my_target;my_target_2)
foreach(_mlcomponent ${ML_LIB_COMPONENTS})
  set(MyLibrary_LIBS ${MyLibrary_LIBS} "${_mlcomponent}")
endforeach()

# ======================================================
# 搜寻的结果、状态
# ======================================================
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(
  MyLibrary
  REQUIRED_VARS ML_INSTALL_PATH
)

可以留意到这里的库文件搜索不再像 FindXxx.cmake 那样需要自己指定动态库或静态库的路径,这里是否是动态库或者静态库完全不需要由导出者指定,导出者只需要包含导出配置的文件即可,因为与库目标相关的设置正是由导出配置完成。理论上,如果不需要全局变量 xxx_INLCUDE_DIRS 或者 xxx_LIBS 以及搜寻的状态的话,XxxConfig.cmake 文件可以短到只有几行(注释删掉就是 1 行):

# ======================================================
#  MyLibrary CMake 配置文件
# ======================================================

include(${CMAKE_CURRENT_LIST_DIR}/MyModules.cmake)

因为在 MyLibrary 中,目标只有 my_target 和 my_target_2,并且与其相关的配置全部设置在了导出目标的文件 MyModules.cmake 中。

4.2 剖析 OpenCVConfig.cmake

以下展示了 OpenCV 4.8.0 的用于 find_package 的 Config 文件,点击此处跳转至该文件。

按照文件中的顺序,主要包含

  1. 提供注释

    OpenCVConfig.cmake 文件提供了很长的注释内容,用户可以直接对照此注释内容进行使用

  2. 提供版本信息

    在此文件的一开头,OpenCV 就设置了版本号信息

  3. 搜索头文件

    常规操作,与上文的基本一致

  4. 搜索库文件、提供 CMake 目标变量

    常规操作,同样分为两个部分。第一部分直接包含导出配置文件 OpenCVModules.cmake,用于配置所有的导入目标,第二部分设置 OpenCV_LIBS 变量,用于包含所有的导入目标。

    注意

    • 与上文一般写法有所不同的是,OpenCV 没有在 OpenCVModules.cmake 中为目标配置包含头文件的搜索路径,因为 OpenCV 在项目中使用的 ocv_target_include_directories 命令使用了 PRIVATE 作为传播属性,那么也就不会在 OpenCVModules.cmake 中有包含头文件路径的配置;

    • 不过在 OpenCVConfig.cmake 中是通过手动 foreach 每个导入目标并进行 set_target_properties 操作,为每个导入目标配置 INTERFACE_INCLUDE_DIRECTORIES 属性的。

  5. 搜寻的结果、状态

    常规操作,同样是使用 find_package_handle_standard_args 命令获取搜索的结果与状态

5. CMake 项目导出与安装的内容总结

总的来说需要完成

  • 添加常规目标的安装规则,并指定导出目标
  • 添加导出目标的安装规则
  • 编写 XxxConfig.cmake 文件,并添加安装规则
  • 编译项目,执行安装步骤,例如 make install

思考🤔

  1. 导入目标和导出目标有什么不同?使用场景是什么?
  2. 使用以下命令安装接口库,会有什么效果?
    install(
      TARGETS my_interface
      LIBRARY DESTINATION lib
      ARCHIVE DESTINATION lib
      RUNTIME DESTINATION bin
    )
    
Prev
【09】生成器表达式
Next
【11】项目实战