CMake系列专题--find_package

CMake系列专题–find_package

内容目录

1. find_packakge命令介绍

在我们实际开发过程中,经常不可避免会使用到第三方开源库,这些开源库可能是通过apt-get install命令自动安装到系统目录中,也可能是由我们自己下载库的源码然后通过编译安装到指令目录下的。

不管哪种方式安装的库文件,如果我们需要自己的项目中使用这些库,首先面临的第一个问题就是如何找到这些库。所谓“找到”这些库,其实是根据我们的需要找到指定版本的库头文件包含路径、链接库路径等,从而能够满足我们开发项目的编译链接需要。

在没有CMake的时代,这种库查找链接的工作都需要借助MakeFile中的各种命令来完成,非常的繁琐,而且不方便移植,到了CMake时代,CMake给我们提供了find_package()命令用来查找依赖包,理想情况下,一句find_package()命令就能把一整个依赖包的头文件包含路径、库路径、库名字、版本号等情况都获取到,后续只管用就好了。但实际使用过程可能会出现这样那样的问题,因此需要我们对find_package()这个强大的命令有个大概的理解。

本文首先以一个简单Demo案例出发,打印find_package()配置的环境变量值,以引入find_package()工作原理(注:IDE CLion)。

main.cpp

#include <cstdio>
#include <iostream>
#include <opencv2/opencv.hpp>

int main() {
    cv::Mat img;
    img = cv::imread("../test.png");

    namedWindow("Display Image", cv::WINDOW_AUTOSIZE);
    imshow("Display Image", img);
    cv::waitKey(0);
    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.17)
project(find_package_learning)

set(CMAKE_CXX_STANDARD 14)

add_executable(find_package_learning main.cpp)

find_package(OpenCV REQUIRED)

if(OpenCV_FOUND)
    message(STATUS "INFO: Found OpenCV library.")
    message(STATUS "INFO: OpenCV_DIR = ${OpenCV_DIR}")
    message(STATUS "INFO: OpenCV_INCLUDE_DIRS = ${OpenCV_INCLUDE_DIRS}")
    message(STATUS "INFO: OpenCV_LIBS = ${OpenCV_LIBS}")
else()
    message("WARNING: OpenCV not found.")
endif()

include_directories(${OPENCV_INCLUDE_DIRS})
target_link_libraries(find_package_learning ${OpenCV_LIBS})

CMake编译(C++源码未发生编译):

-- INFO: Found OpenCV library.
-- INFO: OpenCV_DIR = /usr/local/lib/cmake/opencv4
-- INFO: OpenCV_INCLUDE_DIRS = /usr/local/Cellar/opencv/4.6.0/include/opencv4
-- INFO: OpenCV_LIBS = opencv_calib3d;opencv_core;opencv_dnn;opencv_features2d;opencv_flann;opencv_gapi;opencv_highgui;opencv_imgcodecs;opencv_imgproc;opencv_ml;opencv_objdetect;opencv_photo;opencv_stitching;opencv_video;opencv_videoio;opencv_alphamat;opencv_aruco;opencv_barcode;opencv_bgsegm;opencv_bioinspired;opencv_ccalib;opencv_datasets;opencv_dnn_objdetect;opencv_dnn_superres;opencv_dpm;opencv_face;opencv_freetype;opencv_fuzzy;opencv_hfs;opencv_img_hash;opencv_intensity_transform;opencv_line_descriptor;opencv_mcc;opencv_optflow;opencv_phase_unwrapping;opencv_plot;opencv_quality;opencv_rapid;opencv_reg;opencv_rgbd;opencv_saliency;opencv_sfm;opencv_shape;opencv_stereo;opencv_structured_light;opencv_superres;opencv_surface_matching;opencv_text;opencv_tracking;opencv_videostab;opencv_viz;opencv_wechat_qrcode;opencv_xfeatures2d;opencv_ximgproc;opencv_xobjdetect;opencv_xphoto
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/jb.pan/Desktop/cmake_test/cmake-build-debug

从CMake编译打印的信息可以发现:

  • find_package()找到包后会对OpenCV相关的OpenCV_FOUNDOpenCV_DIROpenCV_INCLUDE_DIRSOpenCV_LIBS等环境变量进行赋值;

  • 环境变量OpenCV_DIR指向OpenCV库的配置文件.cmake(名为OpenCVConfig.cmake)所在的路径,OpenCV_INCLUDE_DIRS指向OpenCV头文件路径,OpenCV_LIBS给出所有被包含库文件的名称。

  • find_package本质上就是一个搜包的命令,通过一些特定的规则找到包的配置文件,通过执行该配置文件,从而定义了一系列的变量,通过这些变量就可以准确定位到OpenCV库的头文件和库文件,完成编译。

因此find_package工作模式流程大致为:

find_package
搜索找到OpenCV配置文件
解析OpenCV配置文件并赋值相关环境变量
CMakeList.txt中引用变量完成变异

2. find_packakge工作原理

2.1 find_packakge工作模式

find_package指令典型的调用形式为:

find_package(<Package> [version])

其中<Package>指需要寻的包名称,[version]是可选择的库版本号。find_package指令执行有两种工作模式,这两种工作模式的不同决定了其搜包路径的不同。

Module模式:

find_package命令基础工作模式(Basic Signature),也是默认工作模式。

指令将依照顺序查找名为Find<Package>.cmakefind-module模块文件(查找顺序如下图),如果find-module模块文件被找到,它将会被加载以进一步寻找包的组件。find-module模块文件(即Find<Package>.cmake)包含了包的特定配置,包括函数库以及一些其他期望被找到的文件的配置,其内部通过find_library定位到它们。

环境变量`CMAKE_MODULE_PATH`
CMake安装目录

Config模式:

find_package命令高级工作模式(Full Signature)。 只有在find_package()中指定CONFIGNO_MODULE等关键字,或者Module模式查找失败后才会进入到Config模式。

该模式通常在定位find-module模块文件或者在指令中被明确定义时才会执行,该模式下find_package指令查找包的名为<Package>Config.cmake或者<Package>-config.cmake的配置文件,与Module模式不同,Config模式需要查找的路径非常多,也要匹配很多的可能性,因此有些路径是首先作为根目录,然后进行子目录的匹配。

`< Package >_DIR`的CMake变量或环境变量路径终目录(非根目录)
`CMAKE_PREFIX_PATH`, `CMAKE_FRAMEWORK_PATH`, `CMAKE_APPBUNDLE_PATH`的CMake变量或环境变量为根目录
PATH环境变量路径根目录, 默认为系统环境PATH环境变量值

需要注意的是:1)在第一个路径下查找时,<Package>_DIR应当指向包配置文件<Package>Config.cmake或者<Package>-config.cmake所在的路径,如果不指定的话,默认为空,也就是如果不设置<Package>_DIR变量,find_package()无法通过第一个非根目录找到包;2)在随后的两种路径中寻找时,上述路径会被指定为根目录,CMake会首先检查这些根目录路径下是否有名为<Package>Config.cmake<Package>-config.cmake的模块文件,如果没有,CMake会继续检查或匹配这些根目录<prefix>下的以下路径:

<prefix>/(lib/<arch>|lib|share)/cmake/<name>*/
<prefix>/(lib/<arch>|lib|share)/<name>*/ 
<prefix>/(lib/<arch>|lib|share)/<name>*/(cmake|CMake)/
2.2 包配置文件/模块文件

find_package通过找到包配置文件/模块文件,进而定义一些列环境变量以实现头文件和库文件的定位,这些环境变量包括<Package>_FOUND<Package>_DIR<Package>_INCLUDE_DIRS<Package>_LIBS等等,实际上这些环境变量的设置是由包配置文件/模块文件(即.cmake文件)完成的,以Eigen库为例,下面给出Eigen库的包配置文件。

Eigen3Config.cmake

# This file exports the Eigen3::Eigen CMake target which should be passed to the
# target_link_libraries command.
####### Expanded from @PACKAGE_INIT@ by configure_package_config_file() #######
####### Any changes to this file will be overwritten by the next CMake run ####
####### The input file was Eigen3Config.cmake.in                            ########
get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../" ABSOLUTE)
macro(set_and_check _var _file)
  set(${_var} "${_file}")
  if(NOT EXISTS "${_file}")
    message(FATAL_ERROR "File or directory ${_file} referenced by variable ${_var} does not exist !")
  endif()
endmacro()
####################################################################################
if (NOT TARGET eigen)
  include ("${CMAKE_CURRENT_LIST_DIR}/Eigen3Targets.cmake")
endif ()

# Legacy variables, do *not* use. May be removed in the future.
set (EIGEN3_FOUND 1)
set (EIGEN3_USE_FILE    "${CMAKE_CURRENT_LIST_DIR}/UseEigen3.cmake")
set (EIGEN3_DEFINITIONS  "")
set (EIGEN3_INCLUDE_DIR  "${PACKAGE_PREFIX_DIR}/include/eigen3")
set (EIGEN3_INCLUDE_DIRS "${PACKAGE_PREFIX_DIR}/include/eigen3")
set (EIGEN3_ROOT_DIR     "${PACKAGE_PREFIX_DIR}")
set (EIGEN3_VERSION_STRING "3.4.0")
set (EIGEN3_VERSION_MAJOR  "3")
set (EIGEN3_VERSION_MINOR  "4")
set (EIGEN3_VERSION_PATCH  "0")

由上面代码可以看出,Eigen3Config.cmake文件完成了关于Eigen的所有环境变量设置(Eigen包没有库文件,EIGEN3_LIBS未进行设置)。在一些工程项目中,开发人员会依据自己需求自己编写库配置文件,如Robotics Library 中作者自己编写Eigen3的模块文件FindEigen3,实现项目中Eigen3库的定位与环境变量设置,这些cmake文件通常较为复杂并且需要一定经验知识,因此一般开发时通常将库安装好,通过find_packageCMake自己寻库即可,具体使用方法请参考Section 3。

2.2 Module和Config命令参数

1)Module模式的参数为:

find_package(<package> [version] [EXACT] [QUIET] [MODULE]
             [REQUIRED] [[COMPONENTS] [components...]]
             [OPTIONAL_COMPONENTS components...]
             [NO_POLICY_SCOPE])

参数说明:

package:必填参数。需要查找的包名,在CMake有大小写区分。

versionEXACT:可选参数,version指定的是版本,如果指定就必须检查找到的包的版本是否和version兼容。如果指定EXACT则表示必须完全匹配的版本而不是兼容版本就可以。

QUIET:可选参数,表示如果查找失败,不会在屏幕进行输出(但是如果指定了REQUIRED字段,则QUIET无效,仍然会输出查找失败提示语)。

MODULE:可选字段。前面提到说“如果Module模式查找失败则回退到Config模式进行查找”,但是假如加入了MODULE选项,那么就只在Module模式查找,如果Module模式下查找失败并不切换到Config模式查找。

REQUIRED:可选字段。表示一定要找到包,找不到的话就立即停掉整个CMake。而如果不指定REQUIREDCMake会继续执行。

COMPONENTScomponents:可选字段,表示查找的包中必须要找到的组件(components),如果有任何一个找不到就算失败,类似于REQUIRED,导致CMake停止执行。

2)Config模式的参数为:

find_package(<package> [version] [EXACT] [QUIET]
             [REQUIRED] [[COMPONENTS] [components...]]
             [CONFIG|NO_MODULE]
             [NO_POLICY_SCOPE]
             [NAMES name1 [name2 ...]]
             [CONFIGS config1 [config2 ...]]
             [HINTS path1 [path2 ... ]]
             [PATHS path1 [path2 ... ]]
             [PATH_SUFFIXES suffix1 [suffix2 ...]]
             [NO_DEFAULT_PATH]
             [NO_CMAKE_ENVIRONMENT_PATH]
             [NO_CMAKE_PATH]
             [NO_SYSTEM_ENVIRONMENT_PATH]
             [NO_CMAKE_PACKAGE_REGISTRY]
             [NO_CMAKE_BUILDS_PATH] # Deprecated; does nothing.
             [NO_CMAKE_SYSTEM_PATH]
             [NO_CMAKE_SYSTEM_PACKAGE_REGISTRY]
             [CMAKE_FIND_ROOT_PATH_BOTH |
              ONLY_CMAKE_FIND_ROOT_PATH |
              NO_CMAKE_FIND_ROOT_PATH])

相比于Module模式,Config模式的参数更多,也更复杂,但实际在使用过程中我们并不会用到所有参数,大部分参数都是可选的,因此一般使用时只需要对Module模式参数了解即可,如需深入了解Config模式参数设置,可参考CMake官方手册(Link)。

3. find_packakge使用方法

3.1 find_package–>问题思考

在学会使用find_package之前,需要充分了解find_package寻包原理,前面提到find_package寻包主要有两种方式,Module模式和Config模式,为了充分了解这两种模式,先思考两个问题:

1)find_package首先会依据Module模式寻找包的模块文件,其名称具有形式Find<Package>.cmake,该文件通常存在于CMAKE_MODULE_PATHCMake安装目录下,因此find_package会依次在这两个目录下寻找。那么疑问来了,如果开发人员需要依赖Qt5库,而Qt5库是用户自定义路径安装的,那么CMake为什么会到CMake的安装目录里找Qt5库的模块文件,不是应该在Qt5的安装目录下么?

答:在用CMake进行某些库编译安装时,CMake有时会将该库的模块文件(Module模式)拷贝至CMake安装目录下,比如我的CMake安装时的模块文件路径为:C:\Program Files\CMake\share\cmake-3.24\Modules,在这其中,我可以找到当初我用CMake编译安装的一些库的模块文件。至于在库的安装路径下,也存在配置文件,不过是用于Config模式寻包的,即<Package>Config.cmake<Package>-config.cmake

2)在Config模式寻包时,我的库是自定义位置安装的,find_package是如何找到这个安装位置,并定位到包配置文件<Package>Config.cmake<Package>-config.cmake

答:当Module模式无法定位到安装的依赖包时,计算机便会通过Config模式查找,包配置文件通常是安装在包目录下的(不是CMake目录下),由于依赖包在进行安装时并不一定将系统的环境变量设置好,因此Module模式下计算机很可能找不到包具体安装位置,当无法定位到依赖包时,我们通常可以通过设置CMake变量,去指引CMake获得到包配置文件<Package>Config.cmake<Package>-config.cmake

3.2 find_package–>设置CMake变量

当配置开发环境,调用find_package显示无法找到依赖包时,这说明:1)(Module模式)所依赖的包在安装时没有进行模块文件的安装,或者说包并没有编写模块文件Find<Package>.cmake;2)(Config模式)在系统变量中并没有配置包的环境,也没有相关的CMake变量指向包配置文件。因此,为了定位到包通常做法是给定CMake变量,根据Config模式寻包原理,我们可设置的CMake变量包括CMAKE_PREFIX_PATH等,如:

set(CMAKE_PREFIX_PATH "C:/SFR_SDK/qt-5.12.8")
find_package(Qt5 REQUIRED COMPONENTS ${QT_COMPONENTS})

这种方式并不能保证百分之百奏效,这是由于设置的CMake变量CMAKE_PREFIX_PATH,指向的是包的根目录,为了定位到包配置文件,CMake还会根据一定的逻辑(详情见章节2.1)去搜寻子目录,但是每个包的安装后目录结构有可能并不遵循这样的逻辑,甚至同一个包在不同系统下的目录结构都会不一样,因此,我们通常还可以直接定位到包配置文件所在目录,如:

set(EIGEN_DIR "C:/SFR_SDK/Eigen3")
find_package(Eigen3 REQUIRED PATHS ${EIGEN_DIR}/share/eigen3/cmake)

或者直接将EIGEN_DIR设置指向cmake文件夹,然后在寻包时PATHS参数给定${EIGEN_DIR}也可。

除此之外,仍需要注意的一点是:find_package中参数包的名称一定要和<Package>Config.cmake<Package>-config.cmake的模块文件或者Find<Package>.cmake<Package>名称对应一致(包括大小写),否则即便find_package给定包的cmake路径,也不可能找到模块/配置文件。


专题blog系列

  • CMake库编译安装指南
  • CMake常用变量汇总
  • find_package指令应用
  • 开发库的包模块/配置文件
  • 未完待续。。。

——————————————————————————-——————————————————————

G . P a n G.Pan G.Pan 先生原创文章,禁止抄袭,转载请注明出处,详情请参考版权声明

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值