【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 在执行完安装命令之后生成的导出配置文件
两个文件中的内容进行介绍,因为这类文件是由导出目标在安装后自动生成的,所以不论是什么项目,在内容上都具有相似性
上文中的 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"
)
运行 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
包含的内容基本一致,包含
- 搜索头文件
- 搜索库文件
- 提供 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
文件,并添加安装规则 - 编译项目,执行安装步骤,例如
make install
思考🤔
- 导入目标和导出目标有什么不同?使用场景是什么?
- 使用以下命令安装接口库,会有什么效果?
install( TARGETS my_interface LIBRARY DESTINATION lib ARCHIVE DESTINATION lib RUNTIME DESTINATION bin )