本文是对简书 CMake命令之find_package介绍、CMake中find_package命令的搜索模式之模块模式(Module mode)、CMake中find_package命令的搜索模式之配置模式(Config mode)、pkg-config用法详解 四篇文章的整合,并根据自己的环境修改。
搜索模式
find_package
用于查找包(通常是使用三方库),并返回关于包的细节(使用包所依赖的头文件、库文件、编译选项、链接选项等)。
与 find_libaray
直接在指定搜索目录下搜索库不同,find_package
命令可以获取更多的信息,那么它的搜索方式也是与 find_libaray
不一样,它有两种不同的搜索方式,因此在介绍这个命令的细节之前,先简单介绍一下 find_package
命令的两种搜索模式:模块模式(Module mode)和配置模式(Config mode)。
我们将以两个例子分别展示两种搜索模式。本例中我会利用自己系统(Ubuntu 22.04)自带的 python 和自己编写的 mymath 库(代码在 pkg-config用法详解 中给出),尝试搜索这两个库。
模块模式(Module mode)
在该模式下,CMake 会搜索一个名为 Find<PackageName>.cmake
的文件,其中 <PackageName>
为待搜索包的名称。
搜索路径的顺序依次是:
- 从变量 CMAKE_MODULE_PATH 指定的路径中进行查找
- 从 CMake 安装路径中查找。CMake 会在其安装路径下提供很多
.cmake
文件,例如/XXX/cmake/Modules/
目录下(不同的系统安装目录可能不一致)
如果找到文件 Find<PackageName>.cmake
,CMake 会读取并处理该文件,简而言之,它负责检查一些条件(如版本号是否满足等)是否满足,并在找到包后,返回给调用者一些变量,用以获取包的详细信息。。
一般来说,Find<PackageName>.cmake
文件不是随包本身一起提供的,更多的是外部针对已有包的重新包装,例如操作系统、CMake 程序、甚至是调用 find_package
命令的工程针对已有的包提供针对该包的 .cmake
文件。
模块模式查找 Python 库
模块模式的 CMakeLists.txt
内容如下
1 | # CMakeLists.txt |
在终端执行 cmake
命令后,输出如下:
1 | ~/Code/cmake/python/build ❯ cmake .. |
模块模式查找自己编写的 mymath 库
我们知道在 Module
模式下,当调用 find_package
命令查找 <PackageName>
包的时候,实际上会去查找一个名为 Find<PackageName>.cmake
的文件,这个文件的主要任务就是确定一个包是否可用,查找的结果会反映在变量 <PackageName>_FOUND
上供 find_package
的调用者使用。当找到可用的包,同时也会提供使用这个包所需要的变量、宏和导入目标(例如库文件)。
而我们自己写的 mymath 库目前是没有 Findmymath.cmake
文件的,需要自己写一个。
标准变量名称
Find<PackageName>.cmake
文件承担了定义 <PackageName>
包相关的变量的作用,这些变量称作"标准变量"。一旦 find_package
调用成功,这些变量将返回给调用者使用,为了保证不同的包之间返回的变量不冲突,对编写的 Find<PackageName>.cmake
返回的标准变量名称有如下约束:所有的变量都是以 PackageName_
开头,PackageName
就是文件 Find<PackageName>.cmake
中的 <PackageName>
,必须完全一致,大小写敏感。
PackageName_INCLUDE_DIRS
:使用包需要包含的头文件。PackageName_LIBRARIES
:使用包所需要的库文件,是全路径或者链接器能在库搜索目录下找到的库文件名称。PackageName_DEFINITIONS
:使用包所需要的编译选项。PackageName_LIBRARY
:库的路径,只有当包提供的是单个库的时候才能使用这形式。PackageName_INCLUDE_DIR
:使用包所需要包含的头文件目录,只能在单个库的使用,使用者需要将该路径加入到搜索路径中。
.cmake
文件编写步骤说明
接下来我们来写一个 .cmake
文件,假设我们的包名为 mymath
,该包提供一个 libmymath.a
的库,其中包含一个 add
接口,简单计算两个整数的和并打印出结果。在编写 Findmymath.cmake
之前,我们先来看下 .cmake
文件的格式。
- 文件开头是 license 信息
1 | # Distributed under the OSI-approved BSD 3-Clause License. See accompanying |
- 接着是一个
CMake
支持的多行注释(单行注释以#
开头;多行注释是指:以#[
开头,紧接着跟 0 个或多个=
,之后是[
,接下来就是注释内容,注释可以跨越多行,然后以]
、0 个或多个=
、]
组成结束,开头的=
个数要和结尾的=
个数相等),.cmake
要求注释以.rst
开头:
1 | #[=======================================================================[.rst: |
-
接下来在注释中声明
find_package
的标准变量及相关说明- 首先是包的名字,分为两行,第一行是包名字,第二行是包名字下方的下划线
---
,与包名字长度相等(这里以 python 的FindPython3.cmake
进行说明)。
1
2FindPython3
------------ 接着是对包的一个简要描述,这个没有特殊要求
- 接下来分为几个部分,主要是对几类变量的声明(导入变量、结果变量、缓存变量),格式都是一致的:首先是变量类型的说明,并在其下方以等长
^^^
标识,接着是一段对变量内容的简要描述,再接下来就是我们要定义的标准变量了。变量以``标准变量``
的格式定义,每个变量下可以对变量做一个简短的说明(这里以 python 的FindPython3.cmake
进行说明)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16Imported Targets
^^^^^^^^^^^^^^^^
This module defines the following :ref:`Imported Targets <Imported Targets>`:
.. versionchanged:: 3.14
:ref:`Imported Targets <Imported Targets>` are only created when
:prop_gbl:`CMAKE_ROLE` is ``PROJECT``.
``Python3::Interpreter``
Python 3 interpreter. Target defined if component ``Interpreter`` is found.
``Python3::Compiler``
Python 3 compiler. Target defined if component ``Compiler`` is found.
``Python3::Module``
.. versionadded:: 3.15 - 首先是包的名字,分为两行,第一行是包名字,第二行是包名字下方的下划线
-
注释部分到此结束,接下来是对库进行真正查找,并把注释部分申明的变量进行赋值的过程。
- 先尝试使用
pkg-config
来找到真正的库,pkg-config
是系统提供的命令用于找系统中是否存在相关的库,在CMake
中使用如下两条,CMake
会从<PackageName>.pc
文件中读取对应的变量(这里以我们自己的mymath
库进行说明)。
1
2find_package(PkgConfig)
pkg_check_modules(PC_mymath QUIET mymath)- 如果能找到库,那么变量
PC_mymath_FOUND
存在,并且可以得到mymath
相关的头文件和库目录,并且是存储在以PC_mymath_XXX
开头的变量中,例如
C_mymath_INCLUDE_DIRS
(头文件目录)、PC_mymath_LIBRARY_DIRS
(库文件目录)、PC_mymath_LIBRARIES
(库名称)等等(具体有哪些变量可以参考man pkg-config
或者在 CMakeCache.txt 中过滤 PC_mymath 查看)。 - 先尝试使用
-
上一步利用了
pkg-config
获得的变量还不是最终要给find_package
返回的变量,我们要对返回的变量做正确的赋值,并最终调用include(FindPackageHandleStandardArgs)
和find_package_handle_standard_args
将变量返回给find_package
调用处。
1 | include(FindPackageHandleStandardArgs) |
.cmake
文件编写
假定我们自己的库 mymath
已经写好了 .pc
文件(参考 pkg-config用法详解),并能够通过 pkg-config
方式找到它(可以通过 pkg-onfig --list-all
查看到 mymath
库)。
接下来我们来编写库 mymath
的 Findmymath.cmake
文件,参照前面对 .cmake
文件说明,内容如下,示例至提供了库目录、库文件、头文件等少量变量信息:
1 | # Distributed under the OSI-approved BSD 3-Clause License. See accompanying |
测试代码
最后我们编写一个 test.cpp
文件来测试下是否能找到 mymath
库。
1 |
|
此外,还需要在 test.cpp
的同级目录下编写一个 CMakeLists.txt
来使用 find_package
来找到并使用 mymath
库。
1 | cmake_minimum_required(VERSION 3.22.1) |
在 build
目录下执行 cmake ..
命令,此处为了演示,直接指定 CMAKE_MODULE_PATH
的值为当前的 .cmake
所在路径,这样 find_package
命令会直接找到我们编写的 .cmake
文件并读取其中的内容,编译并运行最终程序(只摘取了我们关注的显示信息):
1 | ~/Code/cmake/myproj/build ❯ pwd |
配置模式(Config mode)
该模式下,CMake 会搜索 <lowercasePackageName>-config.cmake
文件或 <PackageName>Config.cmake
文件。如果 find_package
命令中指定了具体的版本,也会搜索 <lowercasePackageName>-config-version.cmake
或 <PackageName>ConfigVersion.cmake
文件,因此配置模式下通常会提供配置文件和版本文件(注意形式上要保持一致),并且作为包的一部分一起提供给使用者。
同样的,当 find_package
调用返回时,一系列跟包相关的变量也会提供给调用者。例如 <PackageName>_FOUND
标识包是否找到、<PackageName>_DIR
变量用于指示包配置文件所在的位置。实际上,返回的变量并没有特别的限制,但是还是建议遵循模块模式的标准变量名称的命名规则。
该模式下对 .cmake
文件的搜索路径的顺序比较复杂,具体见本文的配置模式的查找细节说明小节。
配置模式查找 Python 库
由于 Python 库本身未提供 python3Config.cmake
,我们简单的编写一个。但是在这之前,需要先安装一下 Python 的头文件:
1 | sudo apt install python3-dev |
创建 python3Config.cmake
,内容如下:
1 | # python3Config.cmake |
我们来新建一个测试文件 test.cpp
:
1 | // test.cpp |
同级目录下新建 CMakeLists.txt
内容如下:
1 | # CMakeLists.txt |
配置模式查找自己编写的 mymath 库
一个配置文件方式提供的包由包配置文件(必须包含,名为 <lowercasePackageName>-config.cmake
文件或 <PackageName>Config.cmake
)和包版本文件(可选,名为 <lowercasePackageName>-config-version.cmake
或 <PackageName>ConfigVersion.cmake
)组成。配置文件和版本文件的命名要配对出现,也就是:
1 | <lowercasePackageName>-config.cmake |
或者是:
1 | <lowercasePackageName>Config.cmake |
仍然是以我们自己编写的 mymath
库为例,假设 mymath
库提供了如下的文件:
1 | ├── CMakeLists.txt |
配置文件 mymathConfig.cmake
mymathConfig.cmake
文件有两种方式可以生成:
-
在 CMake 中
include(CMakePackageConfigHelpers)
,用CMakePackageConfigHelpers
提供的configure_package_config_file()
来生成。 -
直接通过
set
设置对应的变量,本文采用这种方式提供,如下:
1 | # mymathConfig.cmake |
可以参考 配置模式的查找细节说明 小节来查看 .cmake
文件的搜索路径,我们的例子将在 find_package
中通过 PATHS
选项来指定。
版本文件 mymathConfigVersion.cmake
find_package
找到一个配置文件后,会尝试去查找版本文件。版本文件的主要作用是用来验证包的版本是否与 find_package
命令中指定的版本信息匹配。如果匹配的话,就会使用配置文件中的内容,否则会忽略配置文件中的内容。
和配置文件一样,版本文件也有两种方式生成:
- 在 CMake 中
include(CMakePackageConfigHelpers)
,用CMakePackageConfigHelpers
提供的write_basic_package_version_file()
来生成。我们的例子采用自动生成的方式,在mymath
库的CMakeLists.txt
中添加:
1 | include(CMakePackageConfigHelpers) |
- 自己手动写版本文件的校验规则。
生成自己的库
mymath
库的 CMakeLists.txt
内容如下:
1 | cmake_minimum_required(VERSION 3.18) |
在 build
目录中执行 cmake ..
和 make
生成 libmymath.a
库。
测试代码
最后我们编写一个 test.cpp
文件来测试下是否能找到 mymath
库。
1 |
|
1 | # 顶层的CMakeLists.txt:/XXX/CMakeLists.txt |
测试结果如下:
1 | ~/Code/cmake/myproj/build ❯ pwd |
配置模式的查找细节说明
这一部分可在官方文档上找到 find_package——config-mode-search-procedure。
配置模式的查找目录
CMake 会从如下从几个目录中取搜索配置文件,下面列出了将会搜索的目录,每一个目录后面通过字母来标记不同的操作系统(W 表示 Windows,U 表示 UNIX,A 表示 Apple),目录中的 <prefix>
是目录的前缀,将在下一节介绍是怎么生成的。
1 | <prefix>/ (W) |
CMake 3.25 版本里还加入了 <prefix>/<name>*/(cmake|CMake)/<name>*/ (W)
在支持 macOS 的 FRAMEWORK 和 BUNDLE 系统中,会搜索如下框架和应用程序包目录是否包含配置文件:
1 | <prefix>/<name>.framework/Resources/ (A) |
上面列举的目录中,<name>
是大小写不敏感的,并且会跟 <PackageName>
或者由 NAMES
选项指定的名字进行匹配。
CMAKE_LIBRARY_ARCHITECTURE
变量指定的时候,也会搜索 lib/<arch>
相关的路径,会按照如下顺序搜索:
- 如果
FIND_LIBRARY_USE_LIB64_PATHS
属性被设置为true
,那么在 64 位系统上,带lib64
的路径会被搜索 - 如果
FIND_LIBRARY_USE_LIB32_PATHS
属性被设置为true
,那么在 32 位系统上,带lib32
的路径会被搜索 - 如果
FIND_LIBRARY_USE_LIBX32_PATHS
属性被设置为true
,那么在使用 X32 ABI 的平台上,带libx32
的路径会被搜索 - lib路径总是会被搜索的
- 可以通过
PATH_SUFFIXES
变量指定搜索路径的后缀,会在上述的每一个路径中都添加后缀路径进行查找。
配置模式查找目录的前缀 <prefix>
如何生成
如果指定了 NO_DEFAULT_PATH
选项的话,则启用所有 NO_*
选项,<prefix>
的构造规则如下:
- 查找名为
<PackageName>_ROOT
的 CMake 变量和名为<PackageName>_ROOT
的环境变量,<PackageName>
是待查找的包名。包的根变量是通过栈维护的,因此如果在当前查找模块中调用find_package
,来自父查找模块的根路径也会被搜索。当然可以通过NO_PACKAGE_ROOT_PATH
选项或者将CMAKE_FIND_USE_PACKAGE_ROOT_PATH
设置为FALSE
来跳过。(备注,该查找过程是 CMake 3.12 版本新增) - 通过选项
-DVAR=value
传递进来的路径,多个路径需要以分号隔开。包含CMAKE_PFEFIX_PATH
、CMAKE_FRAMEWORK_PATH
、CMAKE_APPBUNDLE_PATH
三个变量;例如cmake -DCMAKE_PREFIX_PATH=/tmp/test
。可以通过NO_CMAKE_PATH
选项或将CMAKE_FIND_USE_CMAKE_PATH
设置为FALSE
来跳过 - 特定的 CMake 环境变量,包含:
<PackageName>_DIR
、CMAKE_PREFIX_PATH
、CMAKE_FRAMEWORK_PATH
、CMAKE_APPBUNDLE_PATH
,可以通过NO_CMAKE_ENVIRONMENT_PATH
选项或将CMAKE_FIND_USE_CMAKE_ENVIRONMENT_PATH
设置为FALSE
来跳过 HINTS
选项指定的路径- 标准系统环境变量,例如
PATH
,注意,/bin
和/sbin
会自动添加到PATH
指定的目录中 - CMake 的 User Package Registry 中存储的路径
- 当前系统的平台路径,典型的就是软件安装目录,例如 Linux 下的
/usr/local
,包含如下几个变量:
CMAKE_SYSTEM_PREFIX_PATH
、CMAKE_SYSTEM_FRAMEWORK_PATH
、CMAKE_SYSTEM_APPBUNDLE_PATH
。 - CMake 的 System Package Registry 中存储的路径
PATHS
选项指定的路径
CMAKE_FIND_ROOT_PATH
变量用于指定搜索的根路径。
在 find_package
命令调用之前设置 CMAKE_FIND_PACKAGE_RESOLVE_SYMLINKS
为 TRUE
,这样如果查找到的路径是一个符号链接,会将符号链接对应的真实路径存起来。
配置模式下的版本配置文件
当指定 version
参数,配置模式将仅会查找能兼容指定版本的包,如果指定了 EXACT
,则只会查找精确匹配指定版本的包。CMake 本身不会对版本号做任何转换,而是通过查找到包的版本校验文件(包自身提供的)<PackageName>ConfigVersion.cmake
(或 <PackageName>-config-version.cmake
),调用版本配置文件做校验,版本配置文件可以通过 CMakePackageConfigHelpers
模块来辅助创建。可以参考之前的例子。
当 find_package
命令中指定 version
参数后,会把 version
参数分解出来,赋值到 PACKAGE_FIND_XXX
中,供版本配置文件校验版本号使用,具体赋值的变量如下:
PACKAGE_FIND_NAME
:包名PACKAGE_FIND_VERSION
:全版本字符串PACKAGE_FIND_VERSION_MAJOR
:主版本,默认值为0PACKAGE_FIND_VERSION_MINOR
:次版本,默认为0PACKAGE_FIND_VERSION_PATCH
:补丁版本,默认为0PACKAGE_FIND_VERSION_TWEAK
:小版本,默认为0
当指定的版本是一个范围时,上述变量会存放范围中较小的那个版本号,这个主要是为了保证对没有实现版本范围的兼容,此外,也会赋值如下变量:
当版本配置文件完成版本校验后,会设置如下 PACKAGE_VERSION_XXX
变量供 find_package
使用,具体的变量如下:
PACKAGE_VERSION
:全版本字符串PACKAGE_VERSION_EXACT
:版本是否精确匹配,True表示精确匹配PACKAGE_VERSION_COMPATIBLE
:版本是否兼容,True表示兼容PACKAGE_VERSION_UNSUITABLE
:版本是否合适,True表示不合适
上面的 PACKAGE_VERSION_XXX
几个变量仅用于 find_package
命令检查配置文件是否提供了一个可接受的版本,一旦 find_package
命令返回后,这些变量就失效了。如果版本校验通过,那么如下 <PackageName>_VERSION_XXX
变量会被设置,供 find_package
调用者使用:
<PackageName>_VERSION
:包的全版本字符串<PackageName>_VERSION_MAJOR
:主版本<PackageName>_VERSION_MINOR
:次版本<PackageName>_VERSION_PATCH
:补丁版本<PackageName>_VERSION_TWEAK
:小版本<PackageName>_VERSION_COUNT
:点分版本组成的数量,范围0~4
命令格式
find_package
命令有两种格式,基本命令格式和完整命令格式。
基本命令
1 | find_package(<PackageName> [version] [EXACT] [QUIET] [MODULE] |
几个重要的参数介绍:
- PackageName:待查找包的名称。此外它还决定两种搜索模下的
.cmake
文件名称:例如模块模式下的名称为Find<PackageName>.cmake
,而配置模式下为<lowercasePackageName>-config.cmake/<lowercasePackageName>-config-version.cmake
- MODULE:该选项指定
find_package
命令只使用模块模式搜索方式查找。未指定该选项时,find_package
会优先使用模块模式搜索,仍未找到包时,会切换成配置模式搜索。 - version:待查找包的版本号要求,版本号为点分格式,由四个部分组成,每个部分都是一个数字,均为可选:
major[.minor[.patch[.tweak]]]
,例如1.1.1.1
、1.0
、等。同样也可以指定版本范围(CMake 3.19及之后才支持),格式为:versionMin...[<]versionMax
,versionMin
和versionMax
均是major[.minor[.patch[.tweak]]]
形式的版本号,默认情况下会包含这个指定区间两端的版本号,但如果指定了<
,那么会排除掉versionMax
,例如1.1.1.1...1.1.2.0
、1.1.1.1...<1.1.2.0
等。 - EXACT:该选项要求待查找包的版本必须与指定的版本精确匹配,因此如果指定的是一个版本范围,不能使用该参数。
- QUIET:禁止输出信息,正常情况当找到包时,CMake 会打印一些信息,指定该选项时会禁止掉这些打印。例外是当同时指定
QUIET
时,如果找不到包,仍然会输出错误信息并终止执行过程。 - REQUIRED:当未找到满足条件的包(例如版本号不匹配,或指定组件未找到等),会终止 CMake 的执行过程,并输出一条错误信息。如果未指定该选项,即使未找到满足条件的包,CMake 的执行过程也会继续。
- COMPONENTS:指定要查找的组件。通常一个包可能包含多个组件(可以理解为多个库,例如把 C++ 的 std 看成一个包的概念,那么 vector 就是 std 下的其中一个组件),我们的工程可能会依赖包下的具体某个组件,因此可以通过这个选项来检测这些组件是否存在。通常的约定是,该选项后的组件应该都找到时才认为包找到,否则认为未找到满足条件的包。这个约束会依赖包的
.cmake
来实现,通过find_package
命令传入的COMPONENTS 可以通过<PackName>_FIND_COMPONENTS
这个变量来获得。举个简单的例子:
1 | # mymathConfig.cmake,假定它位于./mymath/mymath目录下 |
1 | # 顶层目录的CMakeLists.txt |
- OPTIONAL_COMPONENTS:与 COMPONENTS 的区别是,不强制要求这些组件必须存在。不影响 CMake 的执行。
完整命令
1 | find_package(<PackageName> [version] [EXACT] [QUIET] |
这里介绍一下与基本命令有差异的地方:
- CONFIG|NO_MODULE:这两个选项二选一即可,表示强制
find_package
命令使用配置模式搜索,忽略模块模式搜索。 - NAMES:默认情况下
find_package
命令会查找名为<PackageName>
的包。如果 NAMES 选项后指定了名称,则会使用这些名字来查找包而忽略<PackageName>
参数。 - PATHS/HINTS:配置模式下指定
.cmake
文件的搜索路径。 - NO_XXX_PATH:配置模式下忽略指定的路径,具体的含义可以参考4.1.2节。
命令返回的结果
<PackageName>_FOUND
变量用来表示包是否找到,True
表示包找到了,False
表示未找到满足条件的包。如果包被找到,那么还会提供其他与这个包相关的变量供调用者使用,例如包的头文件、库文件等。这些变量都是以<PackageName>_
开头的,具体的命名格式请参考 Cmake 中 find_package
命令的搜索模式之模块模式(Module mode)的 四、对标准变量名称的更多说明章节。
搜索模式和命令之间的关系
搜索模式有两种:模块模式和配置模式。命令有两种形式:基本命令和完整命令。他们之间的关系是:
基本命令: 首先使用模块模式,如果没找到包,则会切换到配置模式。可以通过将 CMAKE_FIND_PACKAGE_PREFER_CONFIG 变量设置为 true
来改变顺序,这样会优先使用配置模式,如果没找到再切换到模块模式。此外,基本命令可以通过 MODULE 选项来强制指定只使用模块模式,也可以指定 NO_MODULE|CONFIG 来表示只使用配置模式搜索。
完整命令:只支持配置模式搜索。