本文参考:

本文先从一个find_package()的例子出发,然后逐渐引出对find_package()命令搜包过程的介绍,以便深入理解find_package()的具体操作原理,帮助大家理解和消化。

1. 一个使用find_package()命令的例子

为了能够帮助大家理解find_package()命令的用法,此处首先用opencv库举例子,示范如何通过find_package()命令找到opencv库并配置,从而能够在我们自己的项目中调用OpenCV库,实现特定的功能。

下面的代码主要实现了利用OpenCV载入一张图片并显示的简单功能:

ps: 此处假设你已经安装了opencv库,并对opencv有稍许的了解即可

  • opencv_test.cpp
#include <cstdio>
#include <iostream>
#include <opencv2/opencv.hpp>

using namespace cv;

int main(int argc, char *argv[])
{
	Mat image;
	image = imread("./opencv_test.jpg");

	if(!image.data){
		printf("no image data\n");
		
		return -1;
	}

	namedWindow("Display Image", CV_WINDOW_AUTOSIZE);
	imShow("Display Image", image);
	waitKey(0);

	return 0x0;
}
  • CMakeLists.txt
cmake_mininum_required(VERSION 2.8)
project(find_package_learning)

find_package(OpenCV 3 REQUIRED)

message(STATUS "OpenCV_DIR = ${OpenCV_DIR}")
message(STATUS "OpenCV_INCLUDE_DIRS = ${OpenCV_INCLUDE_DIRS}")
message(STATUS "OpenCV_LIBS = ${OpenCV_LIBS})")

include_directories(${OpenCV_INCLUDE_DIRS})
add_executable(opencv_test opencv_test.cpp)
target_link_libraries(opencv_test ${OpenCV_LIBS})

1) 编译运行

在源码路径打开终端,执行:

# mkdir build
# cd build
# cmake ..
# make -j4
# ./opencv_test

2) 编译输出与分析

我的Ubuntu18.04系统在usr/local路径下安装了OpencCV3.4.4,在执行上述cmake ..命令时输出为:

-- The C compiler identification is GNU 7.5.0
-- The CXX compiler identification is GNU 7.5.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found OpenCV: /usr/local (found suitable version "3.4.4", minimum required is "3") 
-- OpenCV_DIR = /usr/local/share/OpenCV
-- OpenCV_INCLUDE_DIRS = /usr/local/include;/usr/local/include/opencv
-- OpenCV_LIBS = opencv_calib3d;opencv_core;opencv_dnn;opencv_features2d;opencv_flann;opencv_highgui;opencv_imgcodecs;opencv_imgproc;opencv_ml;opencv_objdetect;opencv_photo;opencv_shape;opencv_stitching;opencv_superres;opencv_video;opencv_videoio;opencv_videostab;opencv_viz;opencv_aruco;opencv_bgsegm;opencv_bioinspired;opencv_ccalib;opencv_cvv;opencv_datasets;opencv_dnn_objdetect;opencv_dpm;opencv_face;opencv_freetype;opencv_fuzzy;opencv_hdf;opencv_hfs;opencv_img_hash;opencv_line_descriptor;opencv_optflow;opencv_phase_unwrapping;opencv_plot;opencv_reg;opencv_rgbd;opencv_saliency;opencv_stereo;opencv_structured_light;opencv_surface_matching;opencv_text;opencv_tracking;opencv_xfeatures2d;opencv_ximgproc;opencv_xobjdetect;opencv_xphoto
-- Configuring done
-- Generating done
-- Build files have been written to: /workspace/Programming/programming-learning-examples/cmake_learning/learn_cmake_easily/find_package_learning/build

重点看下其中OpenCV_DIROpenCV_INCLUDE_DIRSOpenCV_LIBS打印的结果,这是我在CMakeLists.txt中用message命令输出这三个变量的值的结果。

可以看到在执行find_package(OpenCV 3 REQUIRED)命令后,CMake找到了我们安装在/usr/local下的OpenCV库,并设置了CMake变量OpenCV_DIR为OpenCV库的配置文件所在路径,正是通过载入这个路径下的OpenCVConfig.cmake配置文件才能配置好OpenCV库。然后在OpenCVConfig.cmake配置文件中定义了变量OpenCV_INCLUDE_DIRS为OpenCV库头文件包含路径,这样我们才能在代码中使用#include <opencv2/opencv.hpp>而不会出现编译错误,同时定义了变量OpenCV_LIBS为OpenCV链接库路径,这样我们才能正确链接到OpenCV中的库文件,而不会出现未定义的引用这样的链接错误。

所以整个过程可总结为:执行find_package()命令 => 进入到搜索路径/usr/local/share/OpenCV => 找到OpenCVConfig.cmake文件并执行该文件 => 定义OpenCV_INCLUDE_DIR、OpenCV_LIBS、OpenCV_DIR等变量 => 在CMakeLists.txt文件中引用变量完成编译。

通过上面这个例子就可以看出find_package()本质上就是一个搜包命令,通过一些特定的规则找到<package_name>Config.cmake包配置文件,然后执行该配置文件,从而定义了一系列的变量,通过这些变量就可以准确的定位到OpenCV库的头文件和库文件,完成编译。

那么关键的问题来了,find_package命令是怎么能够定位并载入指定库的配置文件呢?这就需要梳理一下find_package()命令的搜包过程。

2. find_package()的典型用法

绝大部分情况下我们可以使用如下形式来调用find_package():

find_package(<PackageName> [<version>] [REQUIRED] [COMPONENTS <components>...])

其中<PackageName>是唯一强制性的参数;<version>通常可以省略,REQUIRED参数用于指定工程的构建必须依赖于此包。一些更复杂的package支持COMPONENTS参数。

上面展示的是find_package()基本使用形式(basic signature)的简化版本,绝大部分情况下我们可以使用此形式来查找包。

通常情况下,理解find_package()的基本使用形式就已经足够了。而对于期望提供config package的工程维护人员来说,则需要对find_package()有更深的视角,我们会在下面的Full Signature一节进行说明。

3. find_package()的包搜索模式

find_package()命令有三种包搜索模式:

  • Module Mode

  • Config Mode

  • FetchContent redirection mode

1) Module Mode

在此种模式下,CMake会查找一个名叫Find<PackageName>.cmake的文件。查找顺序如下:

  • CMAKE_MODULE_PATH指定的路径下查找

  • CMake安装时所提供的Find Modules路径(ps: 一般为/usr/local/cmake-3.28/modules,其中3.28为cmake版本号)

假如对应的Find<PackageName>.cmake文件找到了,CMake就会读取其中的内容并进行处理。CMake就是通过Find<PackageName>.cmake文件来负责package的查找、version的检查、以及产生其他相关必要的信息。

通常情况下package本身并不提供Find<PackageName>.cmake文件,反而经常能够看到由一些外部系统来提供(例如:操作系统、CMake自身、执行find_package命令的project)。通过由外部系统提供Find<PackageName>.cmake文件,这就使得CMake的Find Modules实现启发式查找,但同时也容易导致文件的过时。

ps: Module Mode只支持在basic signature形式下使用

2) Config Mode

此种模式下,CMake会查找一个名叫<lowercasePackageName>-config.cmake<PackageName>Config.cmake的文件。如果指定了version参数的话,则会查找名叫<lowercasePackageName>-config-version.cmake<PackageName>ConfigVersion.cmake的文件。

在config模式下,该命令可以通过NAMES参数指定一系列的名称以用于包的查找。config模式下的查找路径也会比module模式复杂的多,我们会在后面的Config Mode Search Procedure一节进行说明。

通常情况下,前述的这些config与version文件会作为package的一部分被安装,因此这使得它们会比Find Modules更可靠。

ps: Config Mode支持在basic signature和full signature形式下使用

3)FetchContent redirection mode

此中模式不做介绍。


下面给出find_package()命令工作模式流程图:

ps: 只有在find_package()中指定CONFIG、NO_MODULE等关键字,或者Module模式查找失败后才会进入到Config模式。

cmake-find-package

3.1 Basic signature使用形式

find_package(<PackageName> [version] [EXACT] [QUIET] [MODULE]
             [REQUIRED] [[COMPONENTS] [components...]]
             [OPTIONAL_COMPONENTS components...]
             [REGISTRY_VIEW  (64|32|64_32|32_64|HOST|TARGET|BOTH)]
             [GLOBAL]
             [NO_POLICY_SCOPE]
             [BYPASS_PROVIDER])

basic signature使用形式同时支持Module、Config查找模式。其中MODULE关键字指示只使用Module Mode来查找包,而不会在查找失败的情况下回退到Config Mode。

在此种使用形式下不管采用哪种查找模式(Module Mode、Config Mode、FetchContent redirection Mode),都会有一个_FOUND变量被设置,用于指示对应的package是否查找成功。当package查找成功,package特有的相关信息会通过其他变量来提供。如果指定了`QUIET`选项,会禁用相关的信息message,其中也包括那些指示package是否被找到的的信息(ps: 需要未指定`REQUIRED`选项)。当`REQUIRED`选项被指定,如果package未找到则会停止后续的流程,并打印出相关的error message

对于特定package所需要的components,则可以通过COMPONENTS关键字来列出。假如有任何一个component不被满足的话,则整个package会被认为是not found。假如同时指定了REQUIRED选项,则会被当做一个fatal error并停止后续的所有处理流程,否则后续的执行流程仍然会继续。一种简化的使用形式是,如果REQUIRED选项被指定,那么COMPONENTS关键字可以省略,可以直接在REQUIRED关键字后面列出对应的components。

version参数用于指定需要查找哪个版本的package,有两种不同的指定形式:

  • 一个特定的单独版本号,格式为major[.minor[.patch[.tweak]]]

  • 指定一个版本号范围,格式为versionMin...[<]versionMax

EXACT选项用于指明是否要严格匹配对应的version,该选项不能与范围版本号同时使用。

3.2 Full signature使用形式

find_package(<PackageName> [version] [EXACT] [QUIET]
             [REQUIRED] [[COMPONENTS] [components...]]
             [OPTIONAL_COMPONENTS components...]
             [CONFIG|NO_MODULE]
             [GLOBAL]
             [NO_POLICY_SCOPE]
             [BYPASS_PROVIDER]
             [NAMES name1 [name2 ...]]
             [CONFIGS config1 [config2 ...]]
             [HINTS path1 [path2 ... ]]
             [PATHS path1 [path2 ... ]]
             [REGISTRY_VIEW  (64|32|64_32|32_64|HOST|TARGET|BOTH)]
             [PATH_SUFFIXES suffix1 [suffix2 ...]]
             [NO_DEFAULT_PATH]
             [NO_PACKAGE_ROOT_PATH]
             [NO_CMAKE_PATH]
             [NO_CMAKE_ENVIRONMENT_PATH]
             [NO_SYSTEM_ENVIRONMENT_PATH]
             [NO_CMAKE_PACKAGE_REGISTRY]
             [NO_CMAKE_BUILDS_PATH] # Deprecated; does nothing.
             [NO_CMAKE_SYSTEM_PATH]
             [NO_CMAKE_INSTALL_PREFIX]
             [NO_CMAKE_SYSTEM_PACKAGE_REGISTRY]
             [CMAKE_FIND_ROOT_PATH_BOTH |
              ONLY_CMAKE_FIND_ROOT_PATH |
              NO_CMAKE_FIND_ROOT_PATH])

在纯CONFIG模式下,find_package()会跳过module搜索模式,并立即执行config模式搜索操作。

在Config模式下,find_package()会尝试查找对应的配置文件,并且会创建一个名叫<PackageName>_DIR的缓存条目来保存所查找到的配置文件目录。

4. Config Mode搜包流程

在config mode搜包模式下,CMake会为要搜索的package构建一系列可能的搜索路径。在每一个<prefix>前缀下会在多个目录下搜索配置文件,下面列出Windows、Unix操作系统环境下其会搜索的路径:

Entry                                                        Convention
------------------------------------------------------------------------
<prefix>/                                                         W
<prefix>/(cmake|CMake)/                                           W
<prefix>/<name>*/                                                 W
<prefix>/<name>*/(cmake|CMake)/                                   W
<prefix>/<name>*/(cmake|CMake)/<name>*/                           W

<prefix>/(lib/<arch>|lib*|share)/cmake/<name>*/                   U
<prefix>/(lib/<arch>|lib*|share)/<name>*/                         U
<prefix>/(lib/<arch>|lib*|share)/<name>*/(cmake|CMake)/           U

<prefix>/<name>*/(lib/<arch>|lib*|share)/cmake/<name>*/           W/U
<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/                 W/U
<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/(cmake|CMake)/   W/U

其中<name>不区分大小写,可以是<PackageName>NAMES选项所指定的所有names。<name>*意思是包名后接一些版本号,比如name是pcl的话,那么pcl-1.9也会被找到。

搜素<prefix>按如下步骤来构建:

  • 搜索以<PackageName>为前缀所定义的一些变量所指定的目录

    • <PackageName>_ROOT CMake变量所指定的目录,其中为要搜索的package的名称

    • <PACKAGENAME>_ROOT CMake变量所指定的目录,其中为要搜索的package的名称的大写

    • <PackageName>_ROOT 环境变量所指定的目录,其中为要搜索的package的名称

    • <PACKAGENAME>_ROOT 环境变量所指定的目录,其中为要搜索的package的名称的大写

  • 搜索cmake特定的缓存变量所指定的路径。这些缓冲变量通常倾向于通过命令行来传递(ps: 命令行传递方式为-DVAR=VALUE)。这些所传递的值以分号作为分隔。

    • CMAKE_PREFIX_PATH
    • CMAKE_FRAMEWORK_PATH
    • CMAKE_APPBUNDLE_PATH
  • 搜索cmake特定的环境变量所指定的路径。这些变量倾向于通过用户shell来进行设置(比如Linux中export某个变量)

    • _DIR
    • CMAKE_PREFIX_PATH
    • CMAKE_FRAMEWORK_PATH
    • CMAKE_APPBUNDLE_PATH
  • 搜索HINTS选项所指定的路径

  • PATH环境变量路径

ps: 还有一些其他不太常用的搜索路径,这里不再细述.

5. 内置(Built in)Find Modules介绍

我们可以在CMake的Modules子目录中(ps: 通常是cmake的默认安装目录/usr/share/cmake-/Module)找到大量预定义好的modules。我们可以通过这些Modules查找到大量的通用软件包。参看[cmake-modules(7)](https://cmake.org/cmake/help/latest/manual/cmake-modules.7.html#manual:cmake-modules(7))手册以了解详细信息。

在每一个Find<XX>.cmake文件中都定义了一系列的变量,在对应的package被找到后,就可以直接在工程中使用这些变量。所有的这些变量都以所查找的package的名称作为前缀。CMake也尝试建立一些准则来命名这些变量,但最好我们还是直接阅读对应的Find<XX>.cmake文件。下面我们简单介绍一下cmake准则所建议的一些变量名称:

  • <XX>_INCLUDE_DIRS: 用于包含对应package头文件的目录

  • <XX>_LIBRARIES: 指定要使用<XX>时需要链接哪些库。这些库包含完整的路径

  • <XX>_FOUND: 假如对应的package未找到的话,该变量会被设置为false,或未定义

还有一些其他的cmake准则建议的变量我们未列出,请参考相应的文档。

值得指出的是,Find<XX>.cmake中可能只定义了部分变量。然而,在大多数场景下<XX>_FOUND变量应该是存在的。假如<XX>是一个library,那么<XX>_LIBRARIES变量也应该被定义,<XX>_INCLUDE_DIR变量通常也要被定义。

我们可以在一个工程中通过include命令或find_package()命令来将modules包含进来:

find_package(OpenGL)

与如下两个等价:

` include(${CMAKE_ROOT}/Modules/FindOpenGL.cmake) `

include(FindOpenGL)

6. 创建cmake package configuration文件

如果要想外部的应用程序找到对应的package的话,那么对应的工程就得提供package配置文件。考虑一个最简单的工程Gromit,其提供一个可产生源代码的可执行文件,以及这些所产生的源代码所必须链接的库文件。对应的CMakeLists.txt文件内容如下:

cmake_minimum_required(VERSION 3.20)
project(Gromit C)
set(version 1.0)

# Create library and executable.
add_library(gromit STATIC gromit.c gromit.h)
add_executable(gromit-gen gromit-gen.c)

为了安装Gromit并导出targets供外部工程使用,添加如下代码:

# Install and export the targets.
install(FILES gromit.h DESTINATION include/gromit-${version})
install(TARGETS gromit gromit-gen
        DESTINATION lib/gromit-${version}
        EXPORT gromit-targets)
install(EXPORT gromit-targets
        DESTINATION lib/gromit-${version})

最后Gromit必须在其安装目录树中提供一个package configuration文件,这样外部的工程才可通过find_package()来找到它:

# Create and install package configuration and version files.
configure_file(
   ${Gromit_SOURCE_DIR}/pkg/gromit-config.cmake.in
   ${Gromit_BINARY_DIR}/pkg/gromit-config.cmake @ONLY)

configure_file(
   ${Gromit_SOURCE_DIR}/gromit-config-version.cmake.in
   ${Gromit_BINARY_DIR}/gromit-config-version.cmake @ONLY)

install(FILES ${Gromit_BINARY_DIR}/pkg/gromit-config.cmake
         ${Gromit_BINARY_DIR}/gromit-config-version.cmake
         DESTINATION lib/gromit-${version})

上面的命令用于配置并安装package configuration文件和package version文件。对应的配置输入文件gromit-config.cmake.in如下:

# Compute installation prefix relative to this file.
get_filename_component(_dir "${CMAKE_CURRENT_LIST_FILE}" PATH)
get_filename_component(_prefix "${_dir}/../.." ABSOLUTE)

# Import the targets.
include("${_prefix}/lib/gromit-@version@/gromit-targets.cmake")

# Report other information.
set(gromit_INCLUDE_DIRS "${_prefix}/include/gromit-@version@")