背景

我觉得所有学习过某种编程语言的人,或多或少都希望自己构建一个可使用标准化流程安装的三方库。当然我也不例外,我平时的大部分精力都会放在SLAM的学习上,我的想法是做一个简单方便的SLAM可视化小工具,用来帮助自己快速验证一个SLAM算法的效果和可行性。几个月前,我确实把一个名为SLAM_VIEWER的小工具开源了出来。为了方便自己也是为了方便大家的使用,我希望我写的三方库可以像主流的三方库一样,能够使用一个标准化的编译和安装流程,并能够在cmake中很方便地导入使用。在我查阅了cmake官方文档并奴役了许多AI后,得到了一套流程化的解决方案:

解决方案

# CMakeLists.txt

cmake_minimum_required(VERSION 3.5)

project(slam_viewer VERSION 0.0.2)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()

if(NOT DEFINED BUILD_EXAMPLES)
    set(BUILD_EXAMPLES ON)
endif()

find_package(Pangolin REQUIRED)
find_package(Sophus REQUIRED)
find_package(PCL REQUIRED)
find_package(TBB REQUIRED)
find_package(OpenCV REQUIRED)
find_package(Eigen3 REQUIRED)

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/lib)
set(PACKAGE_INCLUDE_INSTALL_DIR ${CMAKE_INSTALL_PREFIX}/include)
set(PACKAGE_LIBS_INSTALL_DIR ${CMAKE_INSTALL_PREFIX}/lib)

include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)

file(GLOB SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cc)

add_library(slam_viewer SHARED ${SRC_FILES})
target_link_libraries(
    slam_viewer PUBLIC ${Pangolin_LIBRARY} ${PCL_LIBRARIES} Sophus::Sophus
                       TBB::tbb ${OpenCV_LIBS} Eigen3::Eigen)
target_include_directories(
    slam_viewer PUBLIC ${OpenCV_INCLUDE_DIRS} ${PCL_INCLUDE_DIRS}
                       $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
                       $<INSTALL_INTERFACE:include>)

if(${BUILD_EXAMPLES})
    add_subdirectory(examples)
endif()

# 添加安装指令
install(
    TARGETS slam_viewer
    EXPORT ${PROJECT_NAME}Targets
    LIBRARY DESTINATION ${PACKAGE_LIBS_INSTALL_DIR}
    ARCHIVE DESTINATION ${PACKAGE_LIBS_INSTALL_DIR})
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/slam_viewer
        DESTINATION ${PACKAGE_INCLUDE_INSTALL_DIR})

# 安装导出文件
install(
    EXPORT ${PROJECT_NAME}Targets
    FILE ${PROJECT_NAME}Targets.cmake
    NAMESPACE ${PROJECT_NAME}::
    DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/cmake/${PROJECT_NAME})

# 生成并安装Config文件
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
    ${PROJECT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY SameMajorVersion)

configure_file(Config.cmake.in
               "${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" @ONLY)

install(FILES ${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
              ${PROJECT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
        DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/cmake/${PROJECT_NAME})

# 输出配置信息,以查看是否配置正确
message(====================================================================)
message(STATUS ${PROJECT_NAME} Configure:)
message(STATUS CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE})
message(STATUS BUILD_EXAMPLES: ${BUILD_EXAMPLES})
message(STATUS CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX})
message(====================================================================)

上面的cmake代码是我根据这一套流程配置的slam_viewer库,其主要涵盖以下几个方面的内容:

1. 安装Targets和头文件目录

install(TARGETS slam_viewer
        EXPORT ${PROJECT_NAME}Targets
        LIBRARY DESTINATION ${PACKAGE_LIBS_INSTALL_DIR}
        ARCHIVE DESTINATION ${PACKAGE_LIBS_INSTALL_DIR})
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/slam_viewer
        DESTINATION ${PACKAGE_INCLUDE_INSTALL_DIR})

installcmake中比较常用的一个命令,其作用主要是将某个文件夹某个文件或者某些Target安装到指定的位置。如果你需要安装的是某个目录,就需要使用DIRECTORY关键字,并在后面跟上待文件夹的位置,然后使用DESTINATION关键字来指定安装的位置。在标准工作流程里面,使用安装文件夹的命令,将头文件目录安装到${PACKAGE_INCLUDE_INSTALL_DIR}位置,其中${PACKAGE_INCLUDE_INSTALL_DIR}=${CMAKE_INSTALL_PREFIX}/include,如果不指定CMAKE_INSTALL_PREFIX的话,默认为/usr/local

使用installTARGETS安装到某个位置时就比较繁琐了,首先需要指定TARGETS关键字,并在后面跟上需要安装的target的名称(在cmake中,可执行文件,库文件都是以TARGET的形式维护)。EXPORT关键字代表需要生成一个TARGETS相关的依赖文件,这个依赖文件就是${PROJECT_NAME}Targets.cmake。里面维护了TARGETS中的依赖关系,主要有两个内容,一个是INTERFACE_LINK_LIBRARIES,另一个是INTERFACE_INCLUDE_DIRECTORIES

  • INTERFACE_LINK_LIBRARIES维护的是TARGET所有依赖的动态库的TARGET名字,这部分是通过解析target_link_libraries指令内容生成的。
  • INTERFACE_INCLUDE_DIRECTORIES维护的是TARGET所有依赖的头文件的路径,这部分是通过解析target_include_directories指令内容生成的。
  • 当然,只有使用PUBLICINTERFACE关键字指定的依赖才会被解析并放在INTERFACE_LINK_LIBRARIESINTERFACE_INCLUDE_DIRECTORIES中。
  • 这里并不会对PUBLICINTERFACEPRIVATE关键字进行解析,网上也有很多这方面的资料可以参考。

下面是slam_viewerTargets.cmake文件的部分截图,这部分内容相当重要,或者中可以通过这个xxxTargets.cmake文件来找到三方库的Target信息,并且可以明确这个Target的依赖关系。

xxxTargets.cmake

2. 安装导出的Target依赖文件到指定位置

install(EXPORT ${PROJECT_NAME}Targets
        FILE ${PROJECT_NAME}Targets.cmake
        NAMESPACE ${PROJECT_NAME}::
        DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/cmake/${PROJECT_NAME})

这一步主要是为了将Target的依赖文件和slam_viewerConfig.cmake文件导出到同一目录下方便slam_viewerConfig.cmake文件的引用include()操作。根据cmake官方文档部分的说明,安装TARGET的依赖文件必须使用带有EXPORT关键字的安装命令,并且EXPORT关键字后面与安装TARGETSEXPORT关键字后面的内容保持一致,FILE关键字后指定安装文件的文件名。NAMESPACE关键字是一个可选的参数,如果带有NAMESPACE,那么在其他依赖当前编译的三方库文件时,就需要在Target名字前指定命名空间,在上面slam_viewerTargets.cmake文件的部分截图中也能体现。在一定程度上,可以防止同名TARGET的出现。

3. 生成并安装配置文件

include(CMakePackageConfigHelpers)
write_basic_package_version_file(
    ${PROJECT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY SameMajorVersion)

configure_file(Config.cmake.in "${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" @ONLY)

install(FILES ${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
              ${PROJECT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
        DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/cmake/${PROJECT_NAME})

首先需要引入CMakePackageConfigHelpers模块,需要这个模块的write_basic_package_version_file函数来写版本配置文件xxxConfigVersion.cmake。其中VERSION关键字后面跟项目版本号,COMPATIBILITY关键字后面指定版本兼容信息,这里的版本兼容性可以为后续find_package提供信息。一共有AnyNewerVersion|SameMajorVersion|SameMinorVersion|ExactVersion这四种版本兼容要求,其详细的内容解释可以看cmake文档

configure_file函数配置xxxConfig.cmake文件,当我们使用find_pacakge(xxx)函数来查找xxx三方库时,find_package函数会试图找到一个xxxConfig.cmakexxxConfigVersion.cmake文件来判断是否找到了xxx库。代码中Config.cmake.in为配置source文件路径,而"${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"为配置target文件路径,其中@ONLY限定了替换的变量只能用@符号包裹起来。configure_file函数的一些实例内容解释可以看cmake文档

最后安装xxxConfig.cmakexxxConfigVersion.cmake文件到指定位置即可。这个位置建议xxxTargets.cmake的安装位置保持一致。

4. 配置Config.cmake.in文件

# Config.cmake.in

include(CMakeFindDependencyMacro)

find_dependency(Pangolin REQUIRED)
find_dependency(Sophus REQUIRED)
find_dependency(PCL REQUIRED)
find_dependency(TBB REQUIRED)
find_dependency(OpenCV REQUIRED)
find_dependency(Eigen3 REQUIRED)

include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")

最后需要配置Config.cmake.in文件,在第3步中,configure_file函数将Config.cmake.in文件配置到${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake文件中,其中@PROJECT_NAME@PROJECT_NAME变量,${CMAKE_CURRENT_LIST_DIR}为当前xxxConfig.cmake文件所在的目录。也就是为什么第3步的结尾建议要和xxxTargets.cmake的安装装位置保持一致,如果不一致,这里include()参数路径就需要更改。

除此之外,还需要对自定义的三方库依赖的三方库进行配置,这里需要使用find_dependency函数,而find_dependency函数在模块CMakeFindDependencyMacro中定义。配置好find_dependency后,当其他库文件或者可执行文件依赖当前定义的库文件时,就不需要对自定义的三方库依赖的三方库进行find_package操作了。

5. 配置依赖自身的头文件路径的INTERFACE_INCLUDE_DIRECTORIES

target_include_directories(
    slam_viewer PUBLIC ${OpenCV_INCLUDE_DIRS} ${PCL_INCLUDE_DIRS}
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

在讲解这里的用法之前,我们需要理清楚一个比较重要的问题:

在源码中可以看到,对某些库文件,没有使用include_directories或者target_include_directories明确指定它们的头文件路径,而有些库像OpenCVPCL都需要在CMakeLists.txt明确指定头文件路径。在第一部分讲到,如果你在target_include_directories中指定了某些头文件路径,那么在导出的含有依赖关系的文件xxxTargets.cmake中会有一个INTERFACE_INCLUDE_DIRECTORIES,这里会维护这个Target依赖的头文件路径。当某些库严格按照target_include_directories来维护Target的依赖关系时,导出的xxxTargets.cmake文件中必然保存着该Target依赖的所有头文件路径,因此这种库只需要使用target_link_libraries函数即可搞定头文件依赖和库文件依赖关系。然而总有些三方库因为这样或者那样的原因,选择不使用target_include_directories来维护头文件依赖关系,这种库往往都会提供一个xxx_INCLUDE_DIRS这个变量,供调用者使用,因此遇到这种三方库依赖时,需要将target_include_directories来单独维护这份头文件依赖,然后导入到自身的INTERFACE_INCLUDE_DIRECTORIES中。

我相信通过上面一段阐述的内容,同学们应该能理解为什么OpenCVPCL库需要单独使用target_include_directories来维护头文件依赖关系,而其他库则不需要。除此之外,$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>$<INSTALL_INTERFACE:include>这两个内容应该吸引了你的注意。根据cmake官方文档的解释,以$<BUILD_INTERFACE:开头的内容是声明构建过程中所需要的头文件依赖关系,并且后面需要跟绝对路径,而以$<INSTALL_INTERFACE:开头的内容则是安装过程中的头文件依赖或者使用的头文件依赖,后面需要跟以${CMAKE_INSTALL_PREFIX}$为前缀的相对路径。在xxxTargets.cmake中会忽略$<BUILD_INTERFACE:开头的头文件依赖。而没有$<开头的CMAKE会认定这种头文件依赖发生在编译和安装使用过程,因此也会在xxxTargets.cmake中保存下来。

6. 如何使用?

根据上面的解释,我们不难理解所有的库文件依赖关系都在xxxTargets.cmake维护的INTERFACE_LINK_LIBRARIES中,而所有的头文件依赖关系都在xxxTargets.cmake维护的INTERFACE_INCLUDE_DIRECTORIES中,而在xxxConfig.cmake中使用find_dependency代替了find_package函数来导入依赖库文件的Target。因此使用自定义的slam_viewer库时,只需要使用下面的两行代码就可以搞定,可以说非常的简单:

# your project CMakeLists.txt

find_package(slam_viewer REQUIRED)

target_link_libraries(<your_target> slam_viewer::slam_viewer)

标准流程?

上面的内容,是我自己在看了网上的教程和一些通用三方库的CMakeLists.txt总结来的一套适合我自己使用的建库流程,当然这并不是唯一的建库方法。cmake官方教程step9-step11也是非常好的建库流程。我想说的是,无论使用什么样的方式,只要能实现一般三方库导入项目的功能,就是一个好的标准流程。我也非常希望我写的东西能对你有所帮助。

喜欢这篇文章吗?喜欢就分享吧: TwitterFacebookEmail


孙善路-github Avatar 孙善路-github 对SLAM和DL感兴趣的理工男
孙善路-bilibili Avatar 孙善路-bilibili 对SLAM和DL感兴趣的理工男
Comments

有什么问题吗,有任何问题欢迎你在下面评论留言或者邮件联系我!


Published

Category

随笔

Tags

Contact Me