外观
【10】项目的导出与安装
3994字约13分钟
教学视频
写在前面
如果你觉得本教程对你有所帮助,欢迎一键三连,你的支持是我不断更新的动力!
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。
我们可以执行
cmake --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
组件的安装规则中,在执行 cmake --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
说明了即使这个目录不存在,也不会抛出安装的错误。同样,需要在终端执行 cmake --install .
命令以完成安装。
2.3 目标安装
即 install(TARGETS)
,先给出目标安装的签名
install(TARGETS <target>... [EXPORT <export-name>]
[RUNTIME_DEPENDENCIES <arg>...|RUNTIME_DEPENDENCY_SET <set-name>]
[<artifact-option>...]
[<artifact-kind> <artifact-option>...]...
[INCLUDES DESTINATION [<dir> ...]]
)
用法与文件、文件夹安装基本一致,但由于 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
EXPORT MyModules
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
)
注意
EXPORT
必须在 TARGETS
之后,[<artifact-option>...]
之前,如签名的要求。也就是说,如果写成
install(
EXPORT MyModules
TARGETS my_target
)
或者
install(
TARGETS my_target
LIBRARY DESTINATION lib
EXPORT MyModules
)
都是错误的写法,在 CMake 配置期间会报错。
这里的 MyModules
就是导出目标名,并且该导出目标包含了关于 my_target
的所有信息,包括头文件路径、库文件路径等内容。如果我在此之上继续写入
install(
TARGETS my_target_2
EXPORT MyModules
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
则此时导出目标 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 在执行完安装命令之后生成的导出配置文件
两个文件中的内容进行介绍,因为这类文件是由导出目标在安装后自动生成的,所以不论是什么项目,在内容上都具有相似性
上文中的 OpenCVModules.cmake
文件,具有以下几个部分
防止重复包含,来自如下注释:
# Protect against multiple inclusion, which would fail when already imported targets are added once more.
使用
foreach
遍历所有待导入目标的名字(即已被安装的目标的名字),判断这些变量是否已经是目标,如果是说明已经重复包含。获取该项目的安装路径,并设置为一个变量
_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
- 得到
创建导入目标,并初步设置属性
这是该导出配置文件的核心所在,包含了类似如下的一系列语句
# 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
属性。没错,所以说这里是初步设置属性。包含
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
属性,这正是目标能够被正确链接的关键。- Release 模式编译的,那么会包含
检查所有导入目标所指定的库文件(
*.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"
)
运行 cmake --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
包含的内容基本一致,包含
- 搜索头文件
- 搜索库文件
- 提供 CMake 目标变量
- 搜寻的结果、状态
这里直接给出内容
# ======================================================
# 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 文件,点击此处跳转至该文件。
按照文件中的顺序,主要包含
提供注释
OpenCVConfig.cmake
文件提供了很长的注释内容,用户可以直接对照此注释内容进行使用提供版本信息
在此文件的一开头,OpenCV 就设置了版本号信息
搜索头文件
常规操作,与上文的基本一致
搜索库文件、提供 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
属性的。
搜寻的结果、状态
常规操作,同样是使用
find_package_handle_standard_args
命令获取搜索的结果与状态
5. CMake 项目导出与安装的内容总结
总的来说需要完成
- 添加常规目标的安装规则,并指定导出目标
- 添加导出目标的安装规则
- 编写
XxxConfig.cmake
文件,并添加安装规则 - 编译项目,执行安装步骤,例如
cmake --install .
思考 🤔
- 导入目标和导出目标有什么不同?使用场景是什么?
- 使用以下命令安装接口库,会有什么效果?
install( TARGETS my_interface LIBRARY DESTINATION lib ARCHIVE DESTINATION lib RUNTIME DESTINATION bin )
答案 【点此查看】