第23章:查找

中等规模的项目很可能依赖于项目本身之外的东西较多。例如,期望有一个特定的库或工具可用,或者需要知道使用库的特定配置文件或头文件位置。更高的层次上,项目可能需要找到一个包,该包可能定义了一系列内容,包括目标、函数、变量,以及常规CMake项目可能定义的任何内容。

为了实现这一点,CMake提供了多种功能,让项目不用为找东西发愁,甚至可以很容易找到想要的东西,并且能合并到其他项目中。find_…()命令提供了搜索文件、库或程序,甚至包的能力。CMake模块还添加了使用pkg-config来提供外部包信息的功能,而其他模块则编写供其他项目使用的包文件。这一章涵盖了CMake对搜索文件系统中可用内容的支持。下载缺失的依赖在第27章中会来详细了解。可被其他项目发现的项目,会在第25.7节“编写配置包文件”中介绍。

搜索的基本思想相对简单,但如何进行搜索相当复杂。许多情况下,默认行为是适当的,但是对搜索位置和顺序的设置可以允许项目进行定制化搜索,以解释非标准行为和特殊的情况。

23.1. 查找文件和路径

最基本的搜索任务是查找指定文件,实现的方法是使用findfile()命令。`find…()`命令族中,共享许多相同的选项并具有类似的行为。该命令完整的使用方法如下:

find_file(outVar
 name | NAMES name1 [name2...]
 [HINTS path1 [path2...] [ENV var]...]
 [PATHS path1 [path2...] [ENV var]...]
 [PATH_SUFFIXES suffix1 [suffix2 ...]]
 [NO_DEFAULT_PATH]
 [NO_PACKAGE_ROOT_PATH]
 [NO_CMAKE_PATH]
 [NO_CMAKE_ENVIRONMENT_PATH]
 [NO_SYSTEM_ENVIRONMENT_PATH]
 [NO_CMAKE_SYSTEM_PATH]
 [CMAKE_FIND_ROOT_PATH_BOTH |
 ONLY_CMAKE_FIND_ROOT_PATH |
 NO_CMAKE_FIND_ROOT_PATH]
 [DOC "description"]
)

该命令可以通过单个文件名搜索,也可以通过NAMES选项提供一个文件名列表。当要搜索的文件的名称可能有一些变化时,列表就会很有用,比如:不同的操作系统发行版选择不同的命名方式、包含或不包含版本号、考虑文件从一个版本更改名称到另一个版本等等。名称应该按优先顺序列出,搜索将在找到的第一个匹配名称处停止。当指定文件名包含某种形式的版本号时,CMake文档建议先列出没有版本细节的名称,这样就可以先在本地构建文件中查找,没找到时再在操作系统提供的文件中查找。

搜索将根据明确的顺序在一组地点进行检查。大多数位置都有一个相关的选项,如果该选项存在,该选项将跳过该位置,从而允许根据需要定制搜索范围。下表总结了搜索顺序:

位置

“跳过”选项

包的root路径

NO_PACKAGE_ROOT_PATH

缓存变量 (CMake)

NO_CMAKE_PATH

环境变量 (CMake)

NO_CMAKE_ENVIRONMENT_PATH

通过HINTS选项指定路径

环境变量 (系统)

NO_SYSTEM_ENVIRONMENT_PATH

缓存变量 (平台)

NO_CMAKE_SYSTEM_PATH

通过PATHS选项指定的路径

包的root路径

第一个搜索的位置在find_file()从Find模块中调用时应用(本章后面将讨论)。最初在CMake 3.9.0中的添加了搜索位置,但在3.9.1中由于向后兼容性问题删除。在问题解决后,CMake 3.12中又重新添加它。关于搜索位置的进一步讨论将在第23.5节“查找包”中进行,这里更加注意其使用方面。

缓存变量 (CMake)

CMake搜索位置的缓存变量是从缓存变量CMAKEPREFIX_PATH、CMAKE_INCLUDE_PATH和CMAKE_FRAMEWORK_PATH派生出来的。在这些命令中,CMAKE_PREFIX_PATH可能是好用的,因为它的设置不仅适用于find_file(),而且适用于所有其他`find…()命令。在这个基准点之下,bin、lib、include等典型的目录结构是需要的,每个find_…()命令都会附加自己的子目录来构造搜索路径。在find_file()的情况下,会搜索CMAKE_PREFIX_PATH中的每个目录,所以目录/include肯定会进行搜索。如果设置了CMAKE_LIBRARY_ARCHITECTURE变量,那么将首先搜索特定架构的目录/include/${CMAKE_LIBRARY_ARCHITECTURE}`,以确保特定架构的位置优先于一般位置。CMAKE_LIBRARY_ARCHITECTURE变量通常由CMake自动设置,项目通常不应该设置它。

对于需要搜索的头文件目录,或需要搜索框架路径,但这个路径不是标准目录布局或包的一部分,这时可以使用CMAKE_INCLUDE_PATH和CMAKE_FRAMEWORK_PATH变量。它们都提供搜索目录列表,但与CMAKE_PREFIX_PATH不同,没有附加头文件目录的子目录。find_file()和find_path()支持CMAKE_INCLUDE_PATH和CMAKE_FRAMEWORK_PATH,并且find_library()也支持CMAKE_FRAMEWORK_PATH。除此之外,这两组路径的处理方式是相同的。请参阅23.1.1节,“Apple特定的行为”了解更多细节。

环境变量 (CMake)

CMake的环境变量位置与缓存变量位置非常相似。三个环境变量CMAKE_PREFIX_PATH、CMAKE_INCLUDE_PATH和CMAKE_FRAMEWORK_PATH的处理方式与同名缓存变量相同,只是在Unix平台上,每个列表项之间用冒号(:)而不是分号(;)分隔。这允许环境变量使用特定于平台的路径列表,这些路径列表以与每个平台路径有相同的样式。

环境变量 (平台)

系统的环境变量包括INCLUDE和PATH。两者都可以包含一个路径分隔符(Unix系统上是冒号,Windows上是分号)分隔的列表,每项都添加到搜索位置集中(INCLUDE添加在PATH之前)。

仅在Windows上(包括Cygwin),PATH将以更复杂的方式处理。对于PATH环境变量中的每个项,将通过从末尾删除bin或sbin子目录来计算基本路径。然后使用这个基本路径向搜索位置添加一个或两个路径。如果定义了CMAKE_LIBRARY_ARCHITECTURE,则添加<base>/include/${CMAKE_LIBRARY_ARCHITECTURE}。之后,不管是否定义了CMAKE_LIBRARY_ARCHITECTURE,<base>/include路径都会添加到搜索路径集中。搜索路径排序中,这些路径会放在其他路径项的前面。例如,如果PATH环境变量设置为C:\foo\bin;D:\bar,并且CMAKE_LIBRARY_ARCHITECTURE设置为somearch,那么搜索路径将按照如下顺序添加:

  • C:\foo\include\somearch

  • C:\foo\include

  • C:\foo\bin

  • D:\bar\include\somearch

  • D:\bar\include

  • D:\bar

缓存变量 (平台)

平台的缓存变量位置与CMake的缓存变量位置非常相似,名字稍有变化,但模式是一样的。变量名是CMAKE_SYSTEM_PREFIX_PATH、CMAKE_SYSTEM_INCLUDE_PATH和CMAKE_SYSTEM_FRAMEWORK_PATH。这些特定于平台的变量不由项目或开发人员设置,作为设置平台工具链的一部分,以便反映特定于所使用的平台和编译器的位置。例外情况是开发人员提供了工具链文件,这种情况下,在工具链文件中设置这些变量可能合适。

HINTS和PATHS

上面讨论的每一组变量都在项目之外设置,但HINTS和PATHS选项是项目添加额外搜索路径的地方。HINTS和PATHS的主要区别在于,PATHS通常是固定的位置,永远不会改变,不依赖于任何其他东西,而HINTS通常是通过计算出来的,比如之前已经找到的东西的位置,或者依赖于变量或属性值的路径。PATHS是最后搜索的目录,HINTS是搜索特定于平台或系统的位置之前搜索的路径。

HINTS和PATHS都支持环境变量,这些环境变量可以包含主机本地格式的路径列表(例如,Unix系统使用冒号分隔,Windows使用分号分隔)。可以通过在变量名前面加ENV来实现,比如PATHS ENV FooDirs。

除了HINTS和PATHS搜索的位置之外,所有的搜索位置都有一个NO…PATH的跳过选项,该选项可用于跳过那组位置。此外,可以使用NO_DEFAULT_PATH选项绕过除了HINTS和PATHS位置之外的所有位置,强制命令只搜索特定位置。

PATH_SUFFIXES选项可用于提供子目录列表,以便检查每个搜索位置下面的子目录。搜索位置依次与每个后缀一起使用,在转移到下一个搜索位置之前完全不带任何后缀。谨慎使用此选项,因为它极大地扩展了搜索的位置。

许多情况下,搜索顺序的复杂性并不特别重要,项目只需要指定要搜索的文件名,只需要提供一些搜索路径(相当于路径选项)。这种情况下,可以使用该命令的简短形式:

find_file(outVar name [path1 [path2...]])

无论使用的是简短形式还是完整形式,搜索位置的顺序都设计为在更具体的位置搜索之前搜索。虽然这通常是希望的行为,但在某些情况下可能不是这样。例如,项目可能希望在通过缓存或环境变量提供的任何搜索位置之前,先查找特定的路径。通过使用控制搜索位置的不同选项多次调用find_file(),项目可以强制执行不同优先级的查找。一旦找到该文件,就缓存该位置,跳过后续所有调用它们的搜索,这就是各种NO…PATH选项最有用的地方。例如,以下命令强制首先搜索/opt/foo/include,只有在没有找到的情况下,才会搜索默认位置:

find_file(FOO_HEADER foo.h PATHS /opt/foo/include NO_DEFAULT_PATH)
find_file(FOO_HEADER foo.h)

这样做的要求是每个调用都必须使用相同的结果变量,设置缓存变量,并在找到文件后跳过后续的调用。DOC选项可用于将缓存变量存储为指定文档,但这个选项几乎没什么人用。一个自解释的缓存变量名应该不需要文档。按照惯例,这些缓存变量通常都是大写,并使用下划线分隔单词。

23.1.1. Apple平台特定的行为

尽管可以使用find_file()命令查找任何文件,但它的最开始是用来搜索头文件的。这就是为什么一些默认搜索路径有include子目录。Apple平台上,框架有时包含它们自己的头文件,并且find_file()命令还有其他的行为,这些行为与在相应的子目录中的搜索相关。对于每个搜索位置,该命令可以将该位置视为框架或普通目录,或两者兼都有。行为是由CMAKE_FIND_FRAMEWORK变量控制的,该变量应该包含以下值之一:

  • FIRST

  • LAST

  • ONLY

  • NEVER

FIRST是将搜索位置视为框架的顶目录,并添加适当的子目录以下移到其中的头文件位置。如果找不到指定的文件,则将搜索位置视为普通目录而不是框架,并再次搜索。LAST反转了这个顺序,ONLY不会将位置视为普通目录,也不会跳过将位置视为框架的步骤。苹果系统的默认值是FIRST,也是期望的行为。

23.1.2. 控制交叉编译

对于交叉编译,搜索位置会变得相当复杂。交叉编译工具链通常收集自己的目录结构下的工具,这样就能与主机默认的构建工具区分开。在搜索特定文件时,通常先查看工具链的目录结构,再查看主机的目录结构,以便找到特定平台的目标文件。这在查找程序和库时尤其重要,即使是查找文件,也可能会出现文件内容在不同平台之间发生变化的情况(例如,特定于平台的配置头)。

为了支持交叉编译,可以将整个搜索位置集的起始位置与文件系统进行区分。可以使用CMAKEFIND_ROOT_PATH设置一个附加目录列表,可以在该列表中更新起始目录的搜索位置(即将列表中的项前置)。CMAKE_SYSROOT变量也可以以类似的方式修改搜索起始目录。这个变量用于为交叉编译指定起始目录的位置,并且只能在工具链文件中设置,而不能由项目设置,还会影响编译使用的标志。CMake 3.9中,CMAKE_SYSROOT_COMPILE和CMAKE_SYSROOT_LINK也具有类似的效果。如果任何非起始位置已经位于CMAKE_FIND_ROOT_PATH、CMAKE_SYSROOT、CMAKE_SYSROOT_COMPILE或CMAKE_SYSROOT_LINK指定的位置中,则不会更新起始目录。位于由CMAKE_STAGING_PREFIX指定的路径下的非起始路径也不会进行更新。此外,所有`find…()`命令的未定义化行为不会更新任何以~字符开头的非起始路径(这是为了避免更新用户主目录)。

更新起始置和非起始位置之间的默认搜索顺序由CMAKE_FIND_ROOT_PATH_MODE_INCLUDE控制。为find_file()命令提供CMAKE_FIND_ROOT_PATH_BOTH、ONLY_CMAKE_FIND_ROOT_PATH或NO_CMAKE_FIND_ROOT_PATH选项,指定的行为也可以在每次调用时重写。下表总结了这个模式变量的影响,相关的选项和最终的搜索顺序:

搜索模式

find_file() 选项

搜索顺序

BOTH

CMAKE_FIND_ROOT_PATH_BOTH

CMAKE_FIND_ROOT_PATH CMAKE_SYSROOT_COMPILE CMAKE_SYSROOT_LINK CMAKE_SYSROOT All non-rooted locations

NEVER

NO_CMAKE_FIND_ROOT_PATH

All non-rooted locations

ONLY

ONLY_CMAKE_FIND_ROOT_PATH

CMAKE_FIND_ROOT_PATH CMAKE_SYSROOT_COMPILE CMAKE_SYSROOT_LINK CMAKE_SYSROOT Any non-rooted locations already under one of the re-rooted locations or under CMAKE_STAGING_PREFIX

可以强制让find_file()忽略不合适的路径。交叉编译场景中,可能需要忽略某些主机路径,以确保找到特定于目标的文件。项目可以将CMAKE_IGNORE_PATH变量设置为要从搜索中排除的目录列表。这些路径不是递归的,所以不能用于排除目录结构的整个部分,需要显式地指定每个目录。CMAKE_SYSTEM_IGNORE_PATH变量做同样的事情,但这个变量算由工具链设置。这两个...IGNORE_PATH变量不管是否在交叉编译环境下都可用,在交叉编译时比较常见 。

find_file()只能提供一个位置,而一些交叉编译支持无需重新运行CMake,也可以在设备和模拟器构建之间切换。如果通过find_file()的结果取决于使用的是哪一个,那无疑是不可靠的。这对于查找库很重要,在23.4节“查找库”中有更详细的讨论。

23.2. 查找路径

项目可能希望找到包含特定文件的目录,而不是实际文件本身。find_path()命令提供了这个功能,可以获得要找到的文件的目录信息,(其他方面都与find_file()相同)。

23.3. 查找程序

查找程序与查找文件略有不同,find_program()使用与find_file()完全相同的参数,外加一个可选参数NAMES_PER_DIR。还支持该命令的短形式。下面描述了find_program()与find_file()的区别,虽然看起来很复杂,在大多数情况下,只是描述了逻辑上预期的区别,这里会展示一些例外情况:

缓存变量 (CMake)

  • 在CMAKE_PREFIX_PATH下搜索时,find_file()将include追加到每个项。find_program()将会追加bin和sbin作为检查搜索位置。CMAKE_LIBRARY_ARCHITECTURE变量对find_program()没有影响。

  • 可以用CMAKE_PROGRAM_PATH替换CMAKE_INCLUDE_PATH,而且使用方式上完全相同。就是CMAKE_PROGRAM_PATH只能在find_program()中使用。

  • 可以用CMAKE_APPBUNDLE_PATH替换CMAKE_FRAMEWORK_PATH,而且使用方式上完全相同。它们只能在find_program()和find_package()中使用。

环境变量(系统)

  • 系统环境变量的搜索位置处理起来相当简单。INCLUDE对于find_program()没有任何意义,路径中的每一项都会检查,而不会进行任何修改。这种行为在所有平台上都是相同的。

通用变量

  • 通常,当NAMES选项用于提供多个名称时,继续搜索列表中的下一个名称前,会检查所有搜索位置。find_program()命令支持NAMES_PER_DIR选项,该选项可以在继续到下一个位置之前,使用每个名称对目录的文件进行匹配。NAMES_PER_DIR选项在CMake 3.4中添加。

  • Windows上(包括Cygwin和MinGW),.com和.exe文件也会自动进行检查,所以不需要提供这些扩展名作为程序名的一部分来查找。首先检查这些扩展,然后是没有扩展的名称。注意,CMake不会自动搜索.bat和.cmd文件。

  • find_file()使用CMAKE_FIND_FRAMEWORK来确定框架和非框架路径之间的搜索顺序,而find_program()使用CMAKE_FIND_APPBUNDLE,在应用包和非包路径之间进行控制。这两个变量支持的值是相同的,并且它们对包的预期具有同等的意义。find_file将在头文件夹子目录中查找,而find_program将在Contents/MacOS子目录中查找,并将结果设置为应用包中的可执行文件。

  • CMAKE_FIND_ROOT_PATH_MODE_INCLUDE对find_program()没有影响,可以替换为具有等价效果的CMAKE_FIND_ROOT_PATH_MODE_PROGRAM,但只适用于find_program()。交叉编译时,通常寻找的是主机平台工具,因此CMAKE_FIND_ROOT_PATH_MODE_PROGRAM常设置为NEVER。

23.4. 查找库

查找库也类似于查找文件,find_library()命令支持与find_file()相同的选项,另外还支持NAMES_PER_DIR选项。差异如下:

缓存变量(CMake)

  • 在CMAKE_PREFIX_PATH下搜索时,find_file()将include追加到每个项,而find_library()将追加lib。

  • CMAKE_LIBRARY_PATH可以替换CMAKE_INCLUDE_PATH,在使用方式完全相同。而CMAKE_LIBRARY_PATH只用在find_library()中。CMAKE_FRAMEWORK_PATH变量的使用方式与find_file()完全相同。

环境变量(系统)

  • 系统环境变量的搜索位置的处理方法与find_file()非常相似。使用LIB环境变量代替INCLUDE。而且,路径的搜索位置遵循与find_file()相同的逻辑,只是lib附加到每个搜索位置后。与find_file()一样,路径逻辑仅适用于Windows。

通用变量

  • NAMES_PER_DIR选项的含义与find_program()完全相同,在CMake 3.4中添加。

  • find_file()和find_library()使用CMAKE_FIND_FRAMEWORK来确定框架和非框架路径之间的搜索顺序。在find_library()的情况下,如果找到了一个框架,那么顶层.framework目录的名称就是存储在结果变量中的名称。

  • CMAKE_FIND_ROOT_PATH_MODE_INCLUDE对find_library()没有影响,可以替换为具有同等的效果的CMAKE_FIND_ROOT_PATH_MODE_LIBRARY变量,但只能于find_library()。在Apple平台上,在将CMAKE_FIND_ROOT_PATH_MODE_LIBRARY设置为ONLY之前要慎重,因为库可能构建为支持多个目标平台的宽二进制文件。这些宽二进制文件可能并不位于特定于目标平台的路径下,因此仍然需要搜索主机平台的路径来找到它们。

使用find_library()时还存一些行为差异。平台对库名称有不同的约定,比如在大多数Unix平台上将lib作为前缀。文件扩展名在不同的平台上也有很大的不同,Windows上的dll也可以有一个相关联的导入库。find_library()命令尽可能抽象出这些差异,允许项目只指定库的基本名称作为搜索名称。如果目录同时包含静态库和动态库,那么使用动态库。大多数时候,这个抽象做的很好,但是在某些情况下,需要重写这个行为。一种常见情况是,在某些平台上将静态库设置为优先于动态库的。下面这个例子希望在Linux上使用foobar的静态库,而不是动态库,但在macOS或Windows上还是使用默认的优先级:

# WARNING: Not robust!
find_library(FOOBAR_LIBRARY NAMES libfoobar.a foobar)

请记住,优先级覆盖仅适用于特定目录中找到的库。如果含动态库的目录在包含静态库的目录之前进行了搜索,那么上述方法将找不到静态库。确保静态库在所有搜索位置上优先于动态库的更健壮的方法是使用多个find_library(),如下所示:

# Better, static library now has priority across
# all search locations
find_library(FOOBAR_LIBRARY libfoobar.a)
find_library(FOOBAR_LIBRARY foobar)

注意,这种技术不能在Windows上使用,因为静态库和动态库的导入库(即dll)具有相同的文件名,包括后缀(如foobar.lib)。因此,不能使用文件名来区分这两种类型的库。

库处理的另一个独特的复杂性是,许多平台同时支持32位和64位体系结构,可能会有32位和64位版本的库安装到不同的位置,但文件名相同的情况。这种多lib系统上,用于分离不同体系结构的目录结构可能会有所不同,甚至在对同一平台的不同发行版之间也是如此。例如,一些发行版将64位库放在lib目录下,32位库放在lib32目录下,而另一些发行版将64位库放在lib64目录下,32位库放在lib目录下,还有一些发行版使用另一种方式,放在libx32子目录。CMake在设置平台默认值时通常会知道这些变化,并填充全局属性FIND_LIBRARY_USE_LIB32_PATHS FIND_LIBRARY_USE_LIB64_PATHS FIND_LIBRARY_USE_LIBX32_PATHS和控制应该首先搜索特定于体系结构的目录(如果有的话)。项目可以使用CMAKE_FIND_LIBRARY_CUSTOM_LIB_SUFFIX变量,来定制前缀,不过这样的需求非常少见。

当某个特定于体系结构的后缀(无论是从上面的一个全局属性还是从CMAKE_FIND_LIBRARY_CUSTOM_LIB_SUFFIX变量),用于用特定于体系结构的位置扩充搜索位置都很重要。搜索位置路径中任何位置以lib结尾的目录都将添加特定于体系结构的等效内容。这种情况在整个路径中递归发生,因此像/opt/mylib/foo/lib这样的搜索位置可能会在某些64位系统上对搜索位置集进行扩展/opt/mylib64/foo/lib64、/opt/mylib64/foo/lib、/opt/mylib/foo/lib64和/opt/mylib/foo/lib。即使搜索位置没有以lib结束,仍然会通过以体系结构为后缀的位置来增强,所以搜索位置/opt/foo可能会在一些64位系统上搜索/opt/foo64和/opt/foo位置。

特定于体系结构的搜索路径的细节通常不需要开发人员关注。在找到了不需要的库或丢失了需要的库的情况下,使用CMAKE_LIBRARY_PATH这样的变量强制执行结果可能比尝试操作特定于体系结构的逻辑更直接。通常不需要对其中的复杂性有详细的了解,如果只是为了减少CMake如何在特定位置找到库的神秘感,那么对以上几点的了解就足够了。

使用在构建时可以在设备和模拟器配置之间切换的CMake生成器时,需要特别注意。任何find_library()结果都是不可用的,因为只能找到设备或模拟器的库,不能同时找到两者都需要的库。即使重新运行CMake,也会保留缓存结果,因此不会更新库的位置,除非手动删除相关的缓存条目。这在Xcode构建中是特别常见,其中项目可能希望使用find_library()来定位各种框架或公共库(如zlib)。对于这些情况,只能直接指定链接器标志(而不指定路径),让链接器在搜索路径上查找库。对于Apple框架,需要指定两个值,因为框架使用-framework <FrameworkName>添加。对于像zlib这样的普通库,-lz足矣。

23.5. 查找包

前面讨论的各种find_…()命令都关注于查找一个特定的项。然而,这些项通常只是包的一部分,作为一个包可能有它自己的项目可能感兴趣的特征,例如版本号或对某些特性的支持。项目通常希望将包作为一个单独的单元来查找,而不是手动地将不同的部分拼凑在一起。

CMake中有两种主要的方法来定义包,一种是将包定义为模块,另一种是通过配置来定义。配置通常作为包本身的一部分提供,它们与前面小节中讨论的各种find_…()命令的功能更加一致。另一方面,模块通常与包无关的东西一起定义(通常由CMake或项目本身定义),当包随着时间的推移而发展时,就很难跟上进度。

加载模块或配置文件时,通常为包定义变量和导入目标。可以提供程序、库、目标所使用的标志的位置等等,包也可以定义函数和宏(尽管只有模块才会这样做)。对于提供什么,没有一组准确的需求,不过CMake开发人员手册中有一些约定,项目作者必须参考每个模块或包配置的文档来理解它提供了什么。一般来说,较老的模块倾向于提供遵循一致模式的变量,而较新的模块和配置实现通常定义导入目标。当变量和导入的目标都提供时,项目应该更喜欢后者,因为它们具有卓越的健壮性,并且更好地与CMake的传递依赖特性进行集成。

项目通常使用find_package()命令查找包,该命令有短格式和长格式。短格式通常应该是首选,因为它更简单,同时支持模块和配置包,而长格式不支持模块。然而,长格式提供了更多的搜索控制,使它在某些情况下灵活。短格式只有几个选项:

find_package(packageName
 [version [EXACT]]
 [QUIET] [REQUIRED]
 [[COMPONENTS] component1 [component2...]]
 [OPTIONAL_COMPONENTS component3 [component4...]]
 [MODULE]
 [NO_POLICY_SCOPE]
)

可选的版本参数指示包必须是指定的版本或更高版本,如果也给出了确切的选项,则需要版本必须完全匹配。一个包是可选的,这意味着项目可以在可用的情况下使用它,或者在找不到包或者没有合适的版本时不使用它。如果包是强制性的,则应该提供必需的选项,以便在找不到包或无法满足版本要求时停止并出现错误信息。通常,如果find_package()无法找到包,会有记录消息,可以使用QUIET选项来禁止记录,这对于可选包特别有用,因为缺少包时的一些警告,会让开发人员困惑。当找到包时,QUIET选项也会阻止状态消息的输出。

与组件相关的选项允许项目指示它们对包的哪些部分感兴趣。并不是所有的包都支持组件,组件是否定义,以及组件代表什么都取决于模块或配置实现。组件可能对于大型包(比如Qt)更有用,其中可能不安装所有的组件。find_package()命令允许项目用COMPONENTS参数指定组件为必选组件,或用OPTIONAL_COMPONENTS参数指定组件为可选组件。例如,下面的调用需要找到Qt 5.9或更高版本,并且Gui组件必须可用,DBus模块是可选的。

find_package(Qt5 5.9 REQUIRED
 COMPONENTS Gui
 OPTIONAL_COMPONENTS DBus
)

当出现REQUIRED选项时,可以省略COMPONENTS关键字,并将强制性组件放置在REQUIRED之后。这在没有可选组件时尤其常见。例如:

find_package(Qt5 5.9 REQUIRED Gui Widgets Network)

如果包定义了组件,但是没有给find_package()提供组件,那么如何处理就取决于模块或配置定义。对于某些包的所有组件都列出了,而对于另一些包,可能不需要任何组件(尽管仍然可以定义包的基本细节,例如基础库、包版本等等)。另一种可能性是,缺少组件可视为错误。考虑到行为上的差异,开发人员应该参考他们相关包的文档。

短格式的其余选项使用频率较低。NO_POLICY_SCOPE关键字是CMake 2.6遗留下来的,应该避免使用它。MODULE关键字将调用限制为只搜索模块,而不搜索配置包。项目通常应该避免使用这个选项,因为不应该关心如何定义包的实现细节,而只是陈述对包的需求即可。当MODULE不存在时,find_package()命令的短格式将首先搜索模块,如果没有匹配的模块,将搜索配置包。

模块首先在第11章模块中讨论。虽然使用include()命令将非包模块合并到项目中,如果包模块的文件名为 Find<packageName>.cmake,就可以通过find_package()来处理,所以通常称为Find模块。include()和find_package()都将CMAKE_MODULE_PATH变量看作是CMake应该在模块集之前搜索的目录列表。

Find模块负责实现find_package()调用的所有,包括查找包、执行版本检查、满足组件需求,以及适当地记录或不记录消息。并不是所有查找模块都会做这些,它们可能选择忽略包名之外提供的部分或全部信息。因此,查阅模块文档以确认模块预期的行为。

Find模块通过调用各种find_…()命令来实现。因此,有时会受到与这些命令相关的缓存和环境变量的影响。CMAKEPREFIX_PATH变量在会影响模块的查找,指定的每个路径作为基点,每个`find…()`命令都会追加自己的子目录。对于遵循标准布局的包,只需向CMAKE_PREFIX_PATH添加包的基本安装位置,find模块通常就能找到所需的所有包组件。

与查找模块相比,带有配置细节的包为项目提供了更丰富、更健壮的方法来检索关于该包的信息。配置模式下,可以使用更广泛的findpackage()选项集,该命令的长格式与其他`find…()`命令相似:

find_package(packageName
 [version [EXACT]]
 [QUIET | REQUIRED]
 [[COMPONENTS] component1 [component2...]]
 [NO_MODULE | CONFIG]
 [NO_POLICY_SCOPE]
 [NAMES name1 [name2 ...]]
 [CONFIGS fileName1 [fileName2...]]
 [HINTS path1 [path2 ... ]]
 [PATHS path1 [path2 ... ]]
 [PATH_SUFFIXES suffix1 [suffix2 ...]]
 [CMAKE_FIND_ROOT_PATH_BOTH |
 ONLY_CMAKE_FIND_ROOT_PATH |
 NO_CMAKE_FIND_ROOT_PATH]
 [<skip-options>] # See further below
)

当使用完整格式支持的选项调用find_package()时,将跳过对模块的搜索。NO_MODULE或CONFIG关键字允许将短格式的匹配调用作为长格式处理,因此只搜索配置信息(两个关键字是等效的)。

搜索配置细节时,find_package()查找名为<packageName>Config.cmake的文件,或较为罕见的<lowercasePackageName>-config.cmake(默认)。CONFIGS选项可以用来指定一组要搜索的文件名,但请尽可能别使用这个选项。

找到配置文件时,find_package()还会在同一目录中查找相关的版本文件。版本文件将Version或-version附加到文件名中,因此如果是查找FooConfig.cmake文件,则会查找一个名为FooConfigVersion.cmake或FooConfig-version.cmake的版本文件,而foo-config.cmake将导致查找foo-configVersion.cmake或foo-config-version.cmake。包不需要版本文件,但通常会提供。如果版本信息包含在find_package()的调用中,而包没有版本文件时,则认为版本获取失败。

搜索的位置遵循与其他find_…()命令类似的模式,也支持包的注册。每个搜索位置视为一个包安装基点,以下各种子目录都可以搜索:

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

# The following are also checked on Apple platforms

<prefix>/<packageName>.framework/Resources/
<prefix>/<packageName>.framework/Resources/CMake/
<prefix>/<packageName>.framework/Versions/*/Resources/
<prefix>/<packageName>.framework/Versions/*/Resources/CMake/
<prefix>/<packageName>.app/Contents/Resources/
<prefix>/<packageName>.app/Contents/Resources/CMake/

在上面,不区分大小写,只有在设置了CMAKE_LIBRARY_ARCHITECTURE后,才会搜索lib/子目录。lib*子目录代表一组目录,包括lib64、lib32、libx32和lib,lib子目录总会去检查。如果将NAMES选项提供给find_package(),那么将针对提供名称检查上述所有目录。

检查的搜索位置集遵循下表中的顺序,它与其他find_…()命令有许多相似之处。通过添加相关的NO_…关键字,可以禁用大多数搜索位置:

位置

跳过选项

包的起始位置

NO_PACKAGE_ROOT_PATH

缓存变量(CMake)

NO_CMAKE_PATH

环境变量(CMake)

NO_CMAKE_ENVIRONMENT_PATH

通过HINTS选项指定的路径

环境变量(系统)

NO_SYSTEM_ENVIRONMENT_PATH

用户包注册位置

NO_CMAKE_PACKAGE_REGISTRY

缓存变量 (平台)

NO_CMAKE_SYSTEM_PATH

系统包注册位置

NO_CMAKE_SYSTEM_PACKAGE_REGISTRY

通过PATHS选项指定的路径

包的起始位置

对于其他find_…()命令,CMake 3.9.0中添加了包起始目录的作为搜索位置,不过由于由于向后兼容问题后面删除了,并在CMake 3.12中重新添加。每次调用findpackage()时,都会将 <packageName>_ROOT CMake和环境变量添加到搜索路径上。路径的使用方式与CMAKE_PREFIX_PATH完全相同,不仅用于当前对find_package()的调用,也可以用于find_package()过程中调用的所有`find..()命令。如果find_package()加载了一个Find模块,那么Find模块内部调用的任何find_…()`命令都将使用中的路径,就好像是CMAKE_PREFIX_PATH,然后再检查其他路径。

例如,findpackage(Foo)调用会加载FindFoo.cmake,在FindFoo.cmake中的任何`find…()命令,将首先搜索${Foo_ROOT}$ENV{Foo_ROOT}(如果设置了),然后再继续检查其他搜索位置。如果FindFoo.cmake包含了一个类似于find_package(Bar)的调用,该调用会加载FindBar.cmake,堆栈将包含${Bar_ROOT}$ENV{Bar_ROOT}${Foo_ROOT}$ENV{Foo_ROOT}`。这个特性意味着嵌套的Find模块将首先搜索父Find模块的前缀位置,这样信息就不必通过CMAKE_PREFIX_PATH或其他类似的方法手动传播。大多数情况下,项目可以忽略此功能,因为应该透明地工作,而不需要项目进行任何操作。

缓存变量(CMake)

CMake的缓存变量location是从缓存变量CMAKEPREFIX_PATH、CMAKE_FRAMEWORK_PATH和CMAKE_APPBUNDLE_PATH派生出来的。这些命令的工作方式基本上与`find…()`命令相同,只是CMAKE_PREFIX_PATH对应于包安装基点,所以不会追加bin、lib、include等目录。

环境变量(CMake)

上述缓存变量的关系与其他find_…()命令相同。环境变量CMAKE_PREFIX_PATH、CMAKE_INCLUDE_PATH和CMAKE_FRAMEWORK_PATH都使用特定于平台的路径分隔符(Unix平台上的冒号,Windows上的分号)。另外一个环境变量<packageName>_DIR也会在其他三个环境变量之前检查。

环境变量(系统)

唯一支持的特定于系统的环境变量是PATH。每个入口都被用作一个包安装基点,任何bin或sbin后缀会删除。大多数系统中可能会搜索/usr等默认系统位置。

缓存变量(平台)

平台的缓存变量location与其他find_…()命令相同,提供与特定于CMake的缓存变量等效的…_SYSTEM_…这些系统变量的名称是CMAKE_SYSTEM_PREFIX_PATH、CMAKE_SYSTEM_FRAMEWORK_PATH和CMAKE_SYSTEM_APPBUNDLE_PATH,这些变量不由项目设置。

HINTS和PATHS

工作方式与其他find_…()命令相同,只是不支持环境变量的方式ENV someVar

注册包

find_package()唯一的特点是,用户和系统包注册是为了提供一种方法,使包可以轻松查找,而无需将它们安装在标准系统位置。

各种NO_…选项的方式与其他find_…()命令相同,允许绕过每组搜索位置。NO_DEFAULT_PATH关键字会传递除了HINTS和PATHS的所有路径。PATH_SUFFIXES选项也有预期的效果,会检查每个搜索位置的子目录。

findpackage()命令还支持与其他`find…()`命令相同的重定位根目录的搜索逻辑。CMAKE_SYSROOT、CMAKE_STAGING_PREFIX和CMAKE_FIND_ROOT_PATH都是按照与相同的方式考虑,并且CMAKE_FIND_ROOT_PATH_BOTH、ONLY_CMAKE_FIND_ROOT_PATH和NO_CMAKE_FIND_ROOT_PATH选项的含义也是等效的。当这三个选项都不提供时,默认的重定位根目录模式由CMAKE_FIND_ROOT_PATH_MODE_PACKAGE变量控制,该变量具有三个有效值(ONLY、NEVER或BOTH)。

find_…()命令不同,在查找配置文件时,find_package()并不一定在找到的第一个匹配条件的包处停止搜索。搜索会考虑一系列搜索位置,搜索结果可能会返回多个匹配的特定搜索子分支。如果公共目录下安装了多个版本的包,每个版本都在公共目录下有一个版本化的子目录,就会发生这种情况。这种情况下,使用CMAKE_FIND_PACKAGE_SORT_ORDER和CMAKE_FIND_PACKAGE_SORT_DIRECTION变量根据候选对象的版本信息,对它们进行排序。CMAKE_FIND_PACKAGE_SORT_DIRECTION必须具有值DEC或ASC,分别指降序(选择最新的)或升序(选择最旧的)排序方向,而CMAKE_FIND_PACKAGE_SORT_ORDER控制排序的类型,并记录了NAME、NATURAL或NONE的值。如果设置为NONE或没有设置,则不执行排序,并且使用找到的第一个有效包。名称设置按字典序排序,而自然排序是将数字序列作为整数进行比较。下表演示了前两种方法在降序排序时的区别,这是没有设置CMAKE_FIND_PACKAGE_SORT_DIRECTION时的默认行为:

名称

默认排序

1.9

1.10

1.10

1.9

1.0

1.0

实践中,搜索逻辑的复杂性通常超出了使用find_package()命令所需的详细程度。只要包遵循更常见的目录布局,并且位于较高级别的安装位置下,find_package()命令通常不需要进一步帮助,就能找到配置文件。

找到适合包的配置文件,<packageName>_DIR缓存变量将设置为包含该文件的目录。然后,对find_package()的后续调用将首先查找该目录,如果配置文件仍然存在,则直接使用配置文件,而不进行进一步搜索。如果在该位置没有包配置文件,忽略<packageName>_DIR。这种方式确保了对同一个包的再次使用,find_package()会快很多,但是如果包删除了,搜索仍然会执行。要注意,包位置的缓存也可能意味着CMake没有机会在更好的位置上发现新添加的包。例如,操作系统预装了一个相当旧的软件包版本。第一次在项目上运行CMake时,它会找到那个旧版本并将其位置存储在缓存中。用户看到使用的是旧版本,决定在其他目录下安装新版本的包,将该位置添加到CMAKE_PREFIX_PATH并重新运行CMake。这个场景中,仍然会使用旧版本,因为缓存仍然指向旧包的位置。考虑新版本的位置之前,需要删除<packageName>_DIR的缓存条目,或者卸载旧版本。

还有一种控制可以影响特定包的处理。通过在项目早期将CMAKE_DISABLE_FIND_PACKAGE_<packageName>变量设置为true,可以禁用给定packageName的每个对find_package()的非REQUIRED调用,理想情况下是在顶层CMake中或作为缓存变量。这可以看作是关闭可选包的一种方法,防止通过find_package()调用找到它。注意,如果这些调用包含REQUIRED关键字,就不会阻止这些调用。

23.5.1. 注册包

包往往在系统位置或CMake通过CMAKE_PREFIX_PATH(或类似的)目录中找到。对于非系统包,如果每个包不都共享一个公共的安装前缀,就必须为每个包指定位置,我们不推荐这样做。CMake支持一种包注册表形式,允许将对任意位置的引用收集在一起。这允许用户维护帐户或系统范围的注册,CMake将自动咨询。注册引用的位置不一定是完整的安装包,也可以是包构建树中的一个目录(或者任何其他目录),只要有所需的文件就行。

在Windows上,提供了两个注册表。用户注册表存储在Windows注册表的HKEY_CURRENT_USER键下,而系统包注册表存储在HKEY_LOCAL_MACHINE键下:

HKEY_CURRENT_USER\Software\Kitware\CMake\Packages\<packageName>\
HKEY_LOCAL_MACHINE\Software\Kitware\CMake\Packages\<packageName>\

对于给定的packageName,该点下的每个条目都是包含REG_SZ值的任意名称。这个值应该是一个目录,其中可以找到该包的配置文件。在Unix平台上,没有系统包注册表,只有一个用户包注册信息存储在用户的主目录下,并且在该点下的条目的含义与Windows相同:

~/.cmake/packages/<packageName>/

CMake对于如何在任何平台上创建这些没提供什么帮助。对于已安装的包,没有提供自动机制,但是export()命令可以在项目的CMakeLists.txt文件中使用,将项目构建树的一部分添加到用户注册中:

export(PACKAGE packageName)

这将指定的包添加到用户包注册表中,并将其指向与export()调用位置相关联的当前二进制目录。然后,由项目来确保该目录中存在适当的配置文件。如果不存在这样的配置文件,并且对任何项目的包进行find_package()调用,如果权限允许,注册项将自动删除。包注册表中的每个条目的名称都是其所指向的目录路径的MD5值,这避免了名称冲突,也是export(PACKAGE)命令所采用的命名策略。

从构建树向包注册表添加位置有其危险。虽然export(PACKAGE)可以将一个位置添加到注册表中,但是除了手动删除注册项或从构建目录中删除包配置文件之外,没有相应的机制来删除它。使用export(PACKAGE)还可能对持续集成系统造成破坏,因为它使项目获得在同一构建从属上的另一构建树。防止这种情况的一种方法是将CMAKE_EXPORT_NO_PACKAGE_REGISTRY变量设置为ON,这将导致禁用所有对export(PACKAGE)的调用。这将防止项目向用户包注册添加构建树。作为补充,项目可以将CMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY或CMAKE_FIND_PACKAGE_NO_SYSTEM_PACKAGE_REGISTRY设置为ON,以使其所有的find_package()调用分别忽略用户和系统包注册表。

实践中,包注册表并不经常使用。为添加和删除条目提供的帮助有限,维护注册中心在某种程度上是手动的。当通过主机的标准包管理系统安装一个包时,它可以适当地将自己添加到系统或用户注册表中,然后包的卸载程序可以删除相同的条目。虽然包位置定义得很好,而且在概念上很容易定义,但很少有包费心去注册和注销。包可以通过各种不同的方式进入终端用户的机器,这使得健壮而简单地实现这种注册/注消特性有些困难。

23.5.2. FindPkgConfig

find_package()命令通常是查找包并将其合并到CMake项目中的首选方法,但在某些情况下,结果可能不太理想。一些Find模块还没有更新,并且没有提供导入目标,而是依赖于定义一个变量集合,项目必须手动处理这些变量。其他模块可能落后于最新的包版本,导致不兼容或提供了不正确的信息。

某些情况下,包可能支持pkg-config,一种与find_package()类似的工具。如果这样的pkg-config可用,那么PkgConfig Find模块可以用来读取该信息,并以一种更友好的方式提供它。导入的目标可以自动创建,使项目不必手动处理各种变量。pkg-config信息也与软件包的安装版本相匹配,因为信息通常由软件包本身提供。

FindPkgConfig模块定位pkg-config可执行文件,并定义几个函数来调用它,以查找和提取有关具有pkg-config支持的包的详细信息。如果模块找到了可执行文件,将PKG_CONFIG_FOUND变量设置为true,将PKG_CONFIG_EXECUTABLE变量设置为工具的位置。PKG_CONFIG_VERSION_STRING也设置为工具的版本(2.8.8之前的CMake版本除外)。

实践中,项目很少需要使用PKG_CONFIG_EXECUTABLE变量,因为模块定义了两个函数包装了工具,以一种更方便的方式来查询包的向欧盟西欧。pkg_check_modules()和pkg_search_module()这两个函数接受完全相同的选项,并具有类似的行为。两者之间的主要区别是,pkg_check_modules()检查其参数列表中给出的所有模块,而pkg_search_module()在它发现的第一个满足条件的模块处停止。这里使用"模块",而不是"包",是因为这些命令的历史原因,可能会造成一些混淆,但是它们与常规的CMake模块没有直接关系,基本上可以认为是包。

pkg_check_modules(prefix
 [REQUIRED] [QUIET]
 [IMPORTED_TARGET]
 [NO_CMAKE_PATH]
 [NO_CMAKE_ENVIRONMENT_PATH]
 moduleSpec1 [moduleSpec2...]
)

pkg_search_module(prefix
 [REQUIRED] [QUIET]
 [IMPORTED_TARGET]
 [NO_CMAKE_PATH]
 [NO_CMAKE_ENVIRONMENT_PATH]
 moduleSpec1 [moduleSpec2...]
)

这些函数的行为与find_package()有些类似。这里的REQUIRED和QUIET参数与find_package()命令具有相同的效果。在CMake 3.1或更高版本中,CMAKE_PREFIX_PATH、CMAKE_FRAMEWORK_PATH和CMAKE_APPBUNDLE_PATH都是搜索位置,NO_CMAKE_PATH和NO_CMAKE_ENVIRONMENT_PATH关键字在这里也具有相同的含义。PKG_CONFIG_USE_CMAKE_PREFIX_PATH变量可以更改默认行为,以决定是否考虑这些搜索位置(视为一个布尔开关,打开或关闭搜索位置),但是项目应该避免使用它,除非需要支持早于3.1的CMake。

只有CMake 3.6或更高版本才支持IMPORTED_TARGET选项。如果找到了所请求的模块,则会创建一个名为PkgConfig::<prefix>的导入目标。这个导入的目标将从模块的.pc文件中填充接口细节,提供诸如头文件搜索路径、编译器标志等内容。如果项目需要的最小CMake版本是3.6或更高,强烈建议使用这个选项。

这些函数需要一个或多个moduleSpec参数来定义搜索内容。它们将查找的包/模块的名称与任何版本需求结合起来。支持的方式有:

  • moduleName

  • moduleName=version

  • moduleName>=version

  • moduleName<=version

注意,缺少>或<比较运算符,唯一支持的不等式运算符是>=和<=。如果不包括版本需求,则接受任何版本。返回时,函数通过使用适当的选项调用pkg-config,从而在调用范围内设置一些变量,以提取包相关的详细信息:

变量

pkg-config使用的选项

prefix_LIBRARIES

--libs-only-l

prefix_LIBRARY_DIRS

--libs-only-L

prefix_LDFLAGS

--libs

prefix_LDFLAGS_OTHER

--libs-only-other

prefix_INCLUDE_DIRS

--cflags-only-I

prefix_CFLAGS

--cflags

prefix_CFLAGS_OTHER

--cflags-only-other

prefix_STATIC_LIBRARIES

--static --libs-only-l

prefix_STATIC_LIBRARY_DIRS

--static --libs-only-L

prefix_STATIC_LDFLAGS

--static --libs

prefix_STATIC_LDFLAGS_OTHER

--static --libs-only-other

prefix_STATIC_INCLUDE_DIRS

--static --cflags-only-I

prefix_STATIC_CFLAGS

--static --cflags

prefix_STATIC_CFLAGS_OTHER

--static --cflags-only-other

当多个项目由一组选项返回时(例如多个库或多个搜索路径),相应的变量将包含一个CMake列表。

只有满足模块需求时,才设置上述变量。检查这一点的标准方法是使用prefix_FOUND和prefix_STATIC_FOUND变量。对于pkg_check_modules(),必须满足所有的moduleSpec要求,才能使这些变量的值为true,而pkg_search_module()只需要找到一个匹配的moduleSpec。

对于pkg_check_modules(),成功找到模块时还会设置一些额外的模块变量。下文中,如果只给出一个moduleSpec,则YYY = prefix,否则YYY = prefix_moduleName。

YYY_VERSION

找到的模块版本,可以从--modversion选项的输出中获取。

YYY_PREFIX

模块的前缀目录。这是通过查询名为prefix的变量获得的,这个变量是大多数.pc文件定义的,也是pkg-config在默认情况下提供的。

YYY_INCLUDEDIR

查询名为includedir的变量的结果。这是一个常见的,但不必需的变量。

YYY_LIBDIR

查询名为libdir的变量的结果。同样,这是一个常见的,但不必需的变量。

CMake 3.4及以后版本中,FindPkgConfig模块提供了一个额外的功能,可以用来从.pc文件中提取任意变量:

pkg_get_variable(resultVar moduleName variableName)

pkg_check_modules()在内部使用它来查询前缀、includedir和libdir变量,但是项目可以使用它来查询任意变量的值。

对于大多数系统,FindPkgConfig模块提供的功能工作非常可靠。然而,这些函数的实现依赖于pkg-config版本0.20.0中引入的特性。一些较旧的系统(例如:Solaris 10)带有较旧的pkg-config,这导致所有对FindPkgConfig函数的调用都不能找到任何模块,并且没有记录任何错误消息来突出pkg-config版本太旧。

23.6. 推荐

从CMake 3.0开始,已经有意识地转向使用导入的目标来表示外部库和程序,而不是变量。这允许将库和程序视为一个单位,而不仅仅是收集相关二进制文件的位置,但在库的情况下,相关的头文件的搜索路、编译器定义和库的依赖关系,以及目标也是导入目标的一部分。这使得项目使用外部库和程序时,就像项目定义的任何其他目标一样容易。这种关注点的转移意味着寻找包已经变得比寻找单个文件、路径等重要得多,而且越来越多的项目可以作为包来使用。寻找单独的文件仍然有它的用处,而且有助于理解如何做到这一点,但开发人员应该把它看作是包和/或导入目标的铺垫。在可能的情况下,最好查找包,而不是包中的单个内容。

查找包时,出现的数复杂情况与多个版本安装在不同位置有关。用户可能不知道所有已安装的版本,与其让项目预测这种情况,不如跳过默认的搜索路径,让用户通过缓存或环境变量提供相应的路径。CMAKE_PREFIX_PATH是最方便的方法,因为CMake会自动搜索列出的每个前缀路径下的公共目录范围。

除了findpackage()之外,所有的`find…()命令都以类似的方式工作,缓存成功的结果,以避免在下次find…()查找相同的内容时,再重复整个查找操作,甚至可以跨多个CMake调用缓存。考虑到每个调用可能搜索大量的位置和目录条目,缓存机制可以节省大量时间,因为在整个项目中有很多这样的find…()。开发人员至少需要注意两个后果,当find_file()、find_path()、find_program()或find_library()调用成功,将停止搜索所有后续调用,即使运行该命令会返回一个不同的结果,或者之前找到的实体不再存在。如果删除了实体,则会导致构建错误,只有从缓存中删除过期条目才能纠正这些错误。开发人员只需要删除整个缓存,然后重新从头开始构建,而不是试图找出哪些缓存变量需要删除。另一个方面是,开发人员应该注意的是,如果对这些find…()命令的调用未能找到所需的实体,那么即使在相同的项目中,也会对每个调用重复搜索。CMake不会缓存不成功的调用。如果一个项目有很多这样的调用,这可能会减慢配置步骤的时间。因此,开发人员应该仔细考虑项目如何使用find…()`命令,以减少不成功搜索的可能性和数量。

findpackage()的情况稍微复杂一些。如果通过Find模块找到包,很可能上述所有关注点都将应用于包,因为逻辑很可能构建在其他`find…()`命令之上。但是,如果通过配置模式找到了包,find_package()将缓存成功的结果,并在后续调用时首先检查该位置。如果包在该位置不再有适当的配置文件,则命令继续执行正常的搜索逻辑。配置模式的这种独特行为更加健壮。

对于持续集成系统,find_…()结果的缓存可能导致一些微妙问题。如果在保留前一次运行的CMake缓存的地方使用增量构建,在项目中对其搜索方式的更改可能不会反映在构建中。只有当CMake缓存清除时,这些更改才会生效。缓存通常没有记录关于所找到实体的详细信息,因此构建输出很少提供关于使用旧搜索详细信息的线索。因此,可能会要求所有的CI构建从头开始,但这对于较长的构建可能不可行。一个可以帮助减少这个问题的策略是在CI负载低的时候安排每日构建工作,在这个时候构建树被清除,然后按照正常的方式构建项目。这将保持常规的增量行为,并且通常会使任何缓存相关的问题自行解决。这种策略的有效性在一段时期内会降低,变更在一个分支上进行,CI构建在该分支和其他分支之间交替进行,但人们希望这样的时期是不常见的,只要开发人员意识到这段时期的起因,就可以容忍带来的后果。

find_package()命令的包注册特性应该谨慎处理。它们可能会给持续集成系统带来意想不到的结果,其中项目可能希望找到同样在一台机器上构建的包。不幸的是,没有环境变量可以禁用注册,但项目本身可以通过设置CMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY CMake变量来进行(CI工作通常不会修改系统包注册表所需的权限,所以设置CMAKE_FIND_PACKAGE_NO_SYSTEM_PACKAGE_REGISTRY是不必要的)。实践中,很少有项目写入到包注册表中,所以除非知道这样的项目可能使用CI系统,否则不需要向受到影响的项目添加这个CMake变量。项目还应该避免在CI作业中调用export(PACKAGE)(有争议的是,它们通常应该避免这样的调用)。

只有在find_package()不适合的情况下才能使用FindPkgConfig模块。通常,这是针对CMake提供查找模块的包,但查找模块相当旧,不提供导入目标,或者它落后于最近的包发行版。FindPkgConfig模块对于搜索CMake不知道的包,以及包没有提供自己的CMake配置文件时很有用(提供了pkg-config(即.pc)文件)。

使用工具链文件进行交叉编译时,最好设置CMAKESYSROOT而不是CMAKE_FIND_ROOT_PATH。虽然两者以相同的方式影响`find…()`命令的搜索路径,但只有CMAKE_SYSROOT能保证编译器和链接器标志,以便头文件的包含和库链接能够正确工作。

交叉编译场景中,搜索程序通常希望找到主机上运行的二进制文件,而搜索文件和库通常希望找到目标文件。因此,通常会在工具链文件中看到以下默认强制执行的行为:

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

有人可能会说,这应该在项目中设置,而不是依赖于在工具链文件中的设置,从技术上讲,开发人员可以自由地使用任何工具链文件,而且项目会隐式地依赖默认行为,然后选择是否覆盖这些行为。这里增加复杂性是对于每个project()或enable_language()调用都要重新读取工具链文件,如果项目想要强制执行特定的默认组合,就必须在每次这样的调用之后执行。因此,合理的折衷是让项目在它的第一个project()调用之前包含上面的块,并且让工具链编写器也包含它。如果工具链作者没有包含这样的块,至少项目能得到合理的默认值,如果工具链文件将默认值更改为其他东西,至少在整个项目中能一致地应用。开发人员在使用上面示例以外的设置时应该非常谨慎,因为这是一个非常常见的模式。

面对不需要重新运行CMake就可以在设备和模拟器构建之间切换的情况(例如在iOS项目中使用Xcode)时,需要避免调用find_library()。通过这样的调用获得的任何结果只能指向设备或模拟器库中的一个,而不能同时指向两者。在这种情况下,可以添加底层链接器标志,仅通过名称而不是通过路径链接,例如-framework ARKit或-lz。如果在默认的链接器搜索路径中找不到框架或库,项目还需要提供链接器选项来扩展搜索路径,以便能够找到它们。

对于使用CMAKE_MODULE_PATH或CMAKE_PREFIX_PATH控制CMake搜索内容的位置,一些在线示例和博客文章中经常出现冲突的情况。一个简单的区别是CMake只在搜索FindXXX.cmake时使用CMAKE_MODULE_PATH。对于其他所有操作,包括搜索配置文件,使用CMAKE_PREFIX_PATH。

Last updated