<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/
与其他 find_…() 命令一样,对包根变量的支持是在 CMake 3.9.0 中作为搜索位置添加的,在 3.9.1 中由于向后兼容性问题而被移除,并在 CMake 3.12 中重新添加。每次调用 find_package(),都会将 <packageName>ROOT CMake 和环境变量推送到一个内部维护的路径堆栈上。这些路径的使用方式与 CMAKE_PREFIX_PATH 完全相同,不仅用于当前调用 find_package(),还用于所有可能作为 find_package() 处理的 find..() 命令。实际上,这意味着如果一个 find_package() 调用加载了一个 Find 模块,那么 Find 模块内部调用的任何 find_…() 命令都会将堆栈中的每个路径视为首先是 CMAKE_PREFIX_PATH,然后再检查其他路径。
例如,假设一个 find_package(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-specific)
CMake-specific 的缓存变量位置是从缓存变量 CMAKE_PREFIX_PATH、CMAKE_FRAMEWORK_PATH 和 CMAKE_APPBUNDLE_PATH 派生的。它们的工作方式与其他 find_…() 命令相同,只是 CMAKE_PREFIX_PATH 条目已经对应到包安装基点,因此不会附加像 bin、lib、include 等目录。
环境变量(CMake-specific)
这些与上述缓存变量具有相同的关系,就像其他 find_…() 命令一样。环境变量 CMAKE_PREFIX_PATH、CMAKE_INCLUDE_PATH 和 CMAKE_FRAMEWORK_PATH 都使用平台特定的路径分隔符(Unix 平台上是冒号,Windows 上是分号)。还会在其他三个变量之前检查一个附加的环境变量 <packageName>_DIR。
环境变量(系统特定)
唯一支持的系统特定环境变量是 PATH。每个条目都被用作包安装基点,但会移除任何尾部的 bin 或 sbin。这是大多数系统上可能会搜索的默认系统位置。
缓存变量(平台特定)
平台特定的缓存变量位置遵循与其他 find_…() 命令相同的模式,提供 …SYSTEM… 等效项。这些系统变量的名称是 CMAKE_SYSTEM_PREFIX_PATH、CMAKE_SYSTEM_FRAMEWORK_PATH 和 CMAKE_SYSTEM_APPBUNDLE_PATH,不打算由项目设置。
HINTS 和 PATHS
这些与其他 find_…() 命令的工作方式完全相同,只是它们不支持形式为 ENV someVar 的项目。
与 find_package() 特有的是,用户和系统包注册表旨在提供一种使包易于在没有安装在标准系统位置的情况下被找到的方法。有关更详细的讨论,请参阅下文的 25.5.1 节,“包注册表”。
各种 NO_… 选项的工作方式与其他 find_…() 命令相同,允许单独跳过每个搜索位置组。NO_DEFAULT_PATH 关键字会导致除了 HINTS 和 PATHS 之外的所有位置都被跳过。
在 CMake 3.16 或更高版本中,各种 CMAKE_FIND_USE_… 变量也具有与其他 find_…() 命令相同的效果。这些变量允许分别控制每个搜索位置的默认行为。CMAKE_FIND_USE_INSTALL_PREFIX 在 CMake 3.24 或更高版本中也受支持。PATH_SUFFIXES 选项也具有预期的效果,接受以下每个搜索位置下要检查的更多子目录。
各种 NO_… 选项与其他 find_…() 命令的工作方式相同,允许单独跳过每个搜索位置组。NO_DEFAULT_PATH 关键字会导致除了 HINTS 和 PATHS 之外的所有位置都被跳过。
在 CMake 3.16 或更高版本中,各种 CMAKE_FIND_USE_… 变量也具有与其他 find_…() 命令相同的效果。这些变量允许分别控制每个搜索位置的默认行为。CMAKE_FIND_USE_INSTALL_PREFIX 在 CMake 3.24 或更高版本中也受支持。PATH_SUFFIXES 选项也具有预期的效果,接受以下每个搜索位置下要检查的更多子目录。
find_package() 命令还支持与其他 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_DIRECTION
支持的排序方向值为 DEC(降序选择最新的)或 ASC(升序选择最旧的)。如果未设置此变量,则 DEC 是默认行为。
CMAKE_FIND_PACKAGE_SORT_ORDER
这控制排序的类型。支持的值为 NAME、NATURAL 或 NONE。如果设置为 NONE 或根本没有设置,则不执行排序,将使用找到的第一个有效包。NAME 设置按字典顺序排序,而 NATURAL 按整数序列比较排序。下表演示了在降序排序时最后两种方法的差异:
在 CMake 3.24 或更高版本中,特殊目录始终首先被检查,而不管上述提到的任何其他位置。这个位置由 CMAKE_FIND_PACKAGE_REDIRECTS_DIR 变量给出,无法禁用。有关其目的和用法的讨论,请参阅第 30.4.3 节“重定向目录”。
实际上,搜索逻辑的复杂性通常远远超出了有效使用 find_package() 命令所需的细节级别。只要一个包遵循较常见的目录布局之一,并位于较高级别的基本安装位置之一,find_package() 命令通常会在没有进一步帮助的情况下找到其配置文件。
一旦找到一个包的合适配置文件,<packageName>_DIR 缓存变量将设置为包含该文件的目录。随后对 find_package() 的调用将首先查找该目录,如果配置文件仍然存在,则会在没有进一步搜索的情况下使用。如果该位置的包配置文件不再存在,则忽略 <packageName>_DIR。这种安排确保了对同一包的后续 find_package() 调用要快得多,即使是从一次 CMake 调用到下一次,但如果移除了该包,搜索仍将执行。然而,请注意,包位置的缓存也可能意味着 CMake 可能无法在更理想的位置发现新添加的包。例如,操作系统可能预装了一个相当旧版本的包。当首次在项目上运行 CMake 时,它找到了旧版本并将其位置存储在缓存中。用户看到正在使用旧版本,并决定在其他目录下安装新版本的包,并将该位置添加到 CMAKE_PREFIX_PATH,然后重新运行 CMake。在这种情况下,仍将使用旧版本,因为缓存仍指向旧包的位置。必须移除 <packageName>_DIR 缓存条目或卸载旧版本,才能考虑新版本的位置。
还有更多的控制可用于影响特定包的处理方式。可以通过将 CMAKE_DISABLE_FIND_PACKAGE_<packageName> 变量设为 true 来禁用给定包名的每个非 REQUIRED 调用 find_package(),最好在项目的顶层或作为一个缓存变量中。这可以被视为关闭可选包的一种方式,防止它通过 find_package() 调用被找到。请注意,如果这些调用包含 REQUIRED 关键字,则不会阻止这些调用。
在 CMake 3.22 或更高版本中,也支持 CMAKE_REQUIRE_FIND_PACKAGE_<packageName> 变量。将其设置为 true 可以强制针对特定 <packageName> 的所有 find_package() 调用行为与使用 REQUIRED 关键字的调用相同。这可用于捕获期望可用的包的情况,如果缺少该包,强制 CMake 停止并显示错误。依赖于可选包的逻辑测试是该变量可能有用的示例场景,但它也有其局限性。有些情况下,这个变量会破坏项目逻辑。例如,以下是一种常见的方式,即首选在特定位置找到包(如果可用),否则按照常规搜索顺序进行:
find_package(MyThing PATHS /some/location NO_DEFAULT_PATH)
find_package(MyThing)
将 CMAKE_REQUIRE_FIND_PACKAGE_MyThing 设置为 true 会破坏上述逻辑。必须在 /some/location 找到包,否则第一次调用会产生致命错误,并且永远不会到达第二次调用。
25.5.1. 包注册表
包通常存放在标准系统位置或通过 CMAKE_PREFIX_PATH 或类似方法告知 CMake 的目录中。对于非系统包,如果它们不共享一个常见的安装前缀,为每个包指定位置可能会很繁琐或不可取。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>\
对于给定的包名,该点下的每个条目都是持有 REG_SZ 值的任意名称。该值应为包的配置文件所在的目录。在 Unix 平台上,没有系统包注册表,只有存储在用户主目录下的用户包注册表,该点下的条目与 Windows 的含义相同:
~/.cmake/packages/<packageName>/
CMake几乎没有提供如何在任何平台上创建这些条目的支持。没有为已安装的包提供自动化机制,但 export() 命令可以在项目的 CMakeLists.txt 文件中使用,将项目的构建树的部分添加到用户注册表中:
export(PACKAGE packageName)
此命令可以将指定的包添加到用户包注册表,并将该注册表条目指向与 export() 调用关联的当前二进制目录(查看下面的条件,以防止此操作)。然后,由项目负责确保该目录中存在该包的适当配置文件。如果不存在此类配置文件,并且为该包的任何项目进行了 find_package() 调用,那么如果权限允许,注册表条目将自动删除。通常,包注册表中每个条目的名称都是指向路径的 MD5 哈希值。这样可以避免名称冲突,这也是 export(PACKAGE) 命令采用的命名策略。
将来自构建树的位置添加到包注册表存在风险。虽然 export(PACKAGE) 可用于将位置添加到注册表中,但除手动删除注册表条目或从构建目录中删除包配置文件外,没有相应的机制可以将其删除。很容易忘记这样做,因此可能会意外地捡起过去实验留下的旧构建树。使用 export(PACKAGE) 还有可能对连续集成系统造成影响,因为它会使项目捡起在同一台机器上构建的其他项目的构建树。
由于与 export(PACKAGE) 相关的危险,开发人员通常希望禁用它。CMake提供了两种方法来实现这一点,一种是使用自CMake 3.1以来可用的 opt-out 方法,另一种是在CMake 3.15中引入的使用更高级的 opt-in 机制。对于 CMake 3.14 或更早的版本,export(PACKAGE) 命令会修改包注册表,除非将 CMAKE_EXPORT_NO_PACKAGE_REGISTRY 变量设置为 true。因为该变量默认未定义,所以 export(PACKAGE) 命令默认会修改包注册表。在 CMake 3.15 中,通过策略 CMP0090 更改了默认行为,使得当该策略设置为 NEW 时,export(PACKAGE) 命令将被禁用,除非将 CMAKE_EXPORT_PACKAGE_REGISTRY 变量设置为 true(注意不同的变量名称)。如果策略 CMP0090 设置为 OLD 或未设置,则使用CMake 3.14及更早版本的行为。对于大多数实际场景,开发人员可以将 CMAKE_EXPORT_NO_PACKAGE_REGISTRY 设置为 true,无论策略设置或 CMake 版本如何,export(PACKAGE) 命令都将被禁用。
虽然两组 CMAKE_EXPORT_… 和 CMAKE_FIND_… 变量是互补的,但 CMAKE_FIND_… 变量更有效地隔离了包注册表与构建,并且通常对开发人员更相关。
实际上,包注册表并不经常使用。由于添加和删除条目的帮助有限,因此维护注册表在某种程度上是一种手动过程。当通过主机的标准包管理系统安装包时,它可能会将自己添加到适当的系统或用户注册表中,然后包的卸载程序可以删除相同的条目。虽然包的位置是明确定义的,并且它们的定义在概念上很容易,但是很少有包费力地注册和注销自己。包可能以各种不同的方式出现在最终用户的机器上,这使得实现这种注册/注销功能有些困难。
25.5.2. FindPkgConfig
通常,find_package() 命令将是查找并将包集成到 CMake 项目中的首选方法,但在某些情况下,结果可能不尽如人意。一些 Find 模块尚未更新到更现代的实践方式,并且没有提供导入的目标,而是依赖于定义一系列变量,需要消费项目手动处理。其他模块可能会落后于最新的包发布,导致不兼容性或提供的信息不正确。
在某些情况下,包可能具有对 pkg-config 的支持,这是一种提供类似信息于 find_package() 但形式不同的工具。如果有这样的 pkg-config 详细信息可用,则可以使用 PkgConfig Find 模块来读取该信息,并以更适合 CMake 的方式提供。导入的目标可以自动创建,使项目不必手动处理各种变量。pkg-config 详细信息也可能与包的已安装版本匹配,因为它们通常由包本身提供。
FindPkgConfig 模块定位 pkg-config 可执行文件,并定义几个函数来调用它以查找并提取具有 pkg-config 支持的包的详细信息。如果模块找到可执行文件,则将 PKG_CONFIG_FOUND 变量设置为 true,并将 PKG_CONFIG_VERSION_STRING 变量设置为工具的版本(CMake 版本低于 2.8.8 除外)。PKG_CONFIG_EXECUTABLE 变量设置为工具的位置。CMake 3.22 及更高版本还将 PKG_CONFIG_ARGN 设置为每个调用时传递给可执行文件的其他参数。如果需要覆盖模块的默认设置,用户可以显式设置 PKG_CONFIG_EXECUTABLE 和 PKG_CONFIG_ARGN。
实际上,项目很少需要使用 PKG_CONFIG_EXECUTABLE 或 PKG_CONFIG_ARGN 变量。该模块定义了两个函数,这些函数包装工具以提供一种更方便的方式来查询包详细信息。这两个函数 pkg_check_modules() 和 pkg_search_module() 接受完全相同的选项集,并具有类似的行为。两者之间的主要区别在于 pkg_check_modules() 检查其参数列表中给定的所有模块,而 pkg_search_module() 则会在找到满足条件的第一个模块时停止。虽然使用术语模块而不是包已经在这些命令的历史中确立,可能会引起一些混淆,但它们与常规的 CMake 模块没有直接关系,基本上可以视为包。
pkg_check_modules(prefix
[REQUIRED] [QUIET]
[IMPORTED_TARGET [GLOBAL] ]
[NO_CMAKE_PATH]
[NO_CMAKE_ENVIRONMENT_PATH]
moduleSpec1 [moduleSpec2...]
pkg_search_module(prefix
[REQUIRED] [QUIET]
[IMPORTED_TARGET [GLOBAL] ]
[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。
IMPORTED_TARGET 选项仅在 CMake 3.6 或更高版本中受支持。如果给定了该选项并且找到了请求的模块,则将创建一个名为 PkgConfig:: 的导入目标。这个导入目标将从模块的 .pc 文件中填充接口详细信息,提供诸如头文件搜索路径、编译器标志等内容。因此,如果项目所需的最低 CMake 版本为 3.6 或更高版本,则强烈建议使用此选项。如果使用的是 CMake 3.13 或更高版本,则还可以添加 GLOBAL 关键字,使导入的目标具有全局可见性,而不仅限于当前目录范围及其以下。
这些函数期望一个或多个 moduleSpec 参数来定义搜索的内容。它们可以是一个纯模块/包名称,也可以将名称与版本要求结合使用。这些版本要求的形式为 name=version、name<=version 或 name>=version。从 CMake 3.13 开始,还支持 < 和 >。当不包含版本要求时,任何版本都将被接受。
在返回时,这些函数通过调用 pkg-config 以合适的选项提取包详细信息的相关部分来设置调用范围内的一些变量。当一组选项返回多个项目(例如多个库或多个搜索路径)时,相应的变量将保存为一个 CMake 列表。
上述变量仅在满足模块要求时设置。检查此条件的规范方法是使用 prefix_FOUND 和 prefix_STATIC_FOUND 变量。对于 pkg_check_modules(),所有 moduleSpec 要求必须满足才能使这些变量的值为 true,而 pkg_search_module() 只需找到一个匹配的 moduleSpec 即可。从 CMake 3.16 开始,pkg_search_module() 还将 _MODULE_NAME 填充为找到的模块。
对于 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() 使用,用于查询前缀(prefix)、包含目录(includedir)和库目录(libdir)变量的值,但项目可以使用它来查询任意变量的值。请注意,在 CMake 3.15 之前,pkg_get_variable() 存在一个 bug,导致它实际上忽略了 CMAKE_PREFIX_PATH,因此在依赖此功能时考虑将 CMake 3.15 设置为最低版本。对于大多数常见的系统,FindPkgConfig 模块提供的函数相当可靠。
但是,这些函数的实现依赖于 pkg-config 0.20.0 版本引入的功能。一些较旧的系统(例如 Solaris 10)附带较旧版本的 pkg-config,导致对 FindPkgConfig 函数的所有调用均无法成功找到任何模块,且没有错误消息记录以突出显示 pkg-config 版本过旧的问题。
25.6. 忽略搜索路径
在某些情况下,强制 find_…() 命令忽略特定搜索路径可能是有必要的。这在跨编译时特别相关,因为可能需要忽略一些特定的主机路径,以便找到目标平台的文件,而不是主机平台的文件。下面描述的变量不论是否进行交叉编译都适用,但在非交叉编译时设置它们可能不太常见。
CMAKE_IGNORE_PATH 变量应由用户或项目设置。它可以设置为要排除搜索的目录列表。CMAKE_SYSTEM_IGNORE_PATH 变量执行相同的操作,但意图是由工具链设置填充。
对于 find_file()、find_path()、find_library() 和 find_program(),被忽略的目录应该是正在搜索的文件所在的目录。被忽略的路径不是递归的,因此不能用于排除目录结构的整个部分。它们必须指定要忽略的每个单独目录的绝对路径。
对于 find_package(),这些变量仅影响 CONFIG 模式中的搜索。它们可用于忽略包含配置包文件(PackageNameConfig.cmake 或 packageName-config.cmake)的特定目录。它们还可以用于忽略搜索前缀(例如由 CMAKE_PREFIX_PATH、CMAKE_SYSTEM_PREFIX_PATH 等定义的前缀)。
重要的是,CMAKE_IGNORE_PATH 和 CMAKE_SYSTEM_IGNORE_PATH 不影响查找 Find 模块,但它们确实影响了在 Find 模块实现中调用的 find_…() 命令。
CMake 3.23 增加了对另外两个变量的支持,CMAKE_IGNORE_PREFIX_PATH 和 CMAKE_SYSTEM_IGNORE_PREFIX_PATH。这些影响所有 find_…() 命令的搜索前缀,而不仅仅是 find_package()。由于这种更一致的行为,当指定要忽略的搜索前缀时,应优先使用这两个新变量,而不是使用 CMAKE_IGNORE_PATH 或 CMAKE_SYSTEM_IGNORE_PATH。请注意,这两个较新的变量也不影响 Find 模块的搜索前缀。
所有被忽略的目录和前缀将会自动重新定位,方式与搜索路径相同,如第 25.1.2 节“交叉编译控制”所述。意图忽略主机位置的路径也可能导致重新定位位置中相应路径被忽略。
请仔细考虑被忽略路径与诸如 CMAKE_FIND_ROOT_PATH、CMAKE_SYSROOT、CMAKE_STAGING_PREFIX 等变量的交互作用,以避免意外忽略目标平台的路径。
25.7. 调试 find_…() 调用
正如前面的章节所展示的,CMake 搜索各种 find_…() 命令的位置和名称的逻辑是复杂的。当搜索返回意外结果或无法找到预期存在的内容时,很难确定出现了什么问题。为了帮助解决这个问题,CMake 3.17 添加了一个新的 --debug-find 命令行选项,它可以启用对内置 find_…() 命令的调用进行日志记录。这个输出可能包括搜索设置的简要摘要以及每个已检查的位置和名称的列表。如果一个 find_…() 命令调用使用了缓存值而不是实际执行搜索,那么该调用可能不会产生调试输出。
CMake 3.23 添加了一些更有针对性的选项,以帮助将调试输出集中在特定感兴趣的内容上。--debug-find-pkg=pkg1,pkg2,… 选项仅显示与指定包的 find_package() 调用相关的调试输出。--debug-find-var=var1,var2,… 选项对其他 find_…() 命令执行相同的操作,其中调用使用了指定的结果变量之一。
cmake --debug-find-pkg=Boost,fmt ...
cmake --debug-find-var=CCACHE_EXECUTABLE ...
第一个示例将显示寻找 Boost 或 fmt 的 find_package() 调用的调试输出。这包括作为这些 find_package() 调用的一部分而执行的任何其他 find_…() 命令。第二个示例将为像 find_program(CCACHE_EXECUTABLE ccache) 这样的调用提供调试输出。
--debug-find 选项适用于整个构建过程,因此对于包含许多 find_…() 调用的大型项目来说,详细输出可能会让人不知所措。更有针对性的 --debug-find-pkg 和 --debug-find-var 选项可能有助于减少输出量,但它们可能并不总是足够。如果开发人员只想针对特定调用或项目的某个部分进行调试,更有效的策略是只在感兴趣的具体调用周围启用 find_…() 命令调试。这可以通过在感兴趣的调用之前将一个名为 CMAKE_FIND_DEBUG_MODE 的变量设置为 true,并在它们之后设置为 false 来实现(CMake 3.17 添加了对该变量的支持)。例如:
set(CMAKE_FIND_DEBUG_MODE TRUE)
find_program(...)
set(CMAKE_FIND_DEBUG_MODE FALSE)
调试输出旨在作为人类使用的开发辅助工具。它不应该作为任何脚本或其他形式的自动处理的输入,因为格式和内容可能会随着 CMake 版本的变化而变化。
25.8. 推荐做法
从 CMake 3.0 开始,已经有意向使用导入目标来表示外部库和程序,而不是填充变量。这使得这些库和程序可以被视为一个统一的单元,不仅收集相关二进制文件的位置,而且对于库来说,还包括相关的头文件搜索路径、编译器定义以及消费目标所需的更多库依赖都是导入目标的一部分。这使得外部库和程序在项目中与任何其他常规目标一样容易使用。这种关注点的转移意味着找到包变得比找到单个文件、路径等更为重要,同时也越来越倾向于让项目可以被其他 CMake 项目作为包来消费。找到单个文件等仍然有其用处,了解如何实现这些也是有帮助的,但开发人员应将其视为转向包和/或导入目标的一种过渡,而不是终点。在可能的情况下,优先找到包,而不是包中的单个元素。
在查找包时,大多数出现的复杂情况与安装了不同位置的多个版本相关。用户可能不知道所有已安装的版本,或者可能对应该首先找到的版本有期望。与其让项目试图预测这种情况,通常更明智的做法是不要偏离默认的搜索行为太远,并允许用户通过缓存或环境变量提供自己的覆盖设置。由于 CMake 自动搜索每个前缀路径下的一系列常见目录布局的方式,CMAKE_PREFIX_PATH 通常是最便捷的方式来实现这一点。
请注意,find_package() 调用可能会重定向到完全不同的机制来满足这些请求。CMake 3.24 添加了一些功能,将 find_package() 与 FetchContent 模块集成起来,并提供了相关支持,通过自定义的开发者指定的依赖项提供支持。第 30.4 节“与 find_package() 集成”和第 32 章“依赖项提供者”详细讨论了这些话题。
不要过度依赖 find_package() 的版本范围支持,甚至可能完全不依赖版本约束。版本范围仅适用于 CMake 3.19 或更高版本,并且旧版本的包通常会忽略版本范围约束的上限。考虑将上限视为建议而非严格强制执行。在某些情况下,整个版本约束可能会被忽略,如第 30.4 节“与 find_package() 集成”和第 32 章“依赖项提供者”所述。最终,指定任何形式的版本约束可能并不值得努力。
除了 find_package() 之外的所有 find_…() 命令都以类似的方式工作。默认情况下,它们会缓存成功的结果,以避免在下一次需要查找相同内容的 find_…() 命令时重复整个查找操作。这种缓存甚至跨多个 CMake 调用保持。由于每个调用可能搜索的位置和目录条目的数量可能很大,缓存机制可以节省大量时间,尤其是在整个项目中存在许多这样的 find_…() 调用时。然而,这种查找行为有两个需要开发人员注意的后果。首先,一旦 find_file()、find_path()、find_program() 或 find_library() 命令成功,它将停止对所有后续调用的搜索,即使运行命令可能会返回不同的结果,或者之前找到的实体已经不存在。如果实体被移除,这可能会导致构建错误,只能通过从缓存中删除过时条目来纠正。开发人员通常会删除整个缓存并重新从头开始构建,而不是尝试弄清楚哪些缓存变量需要被删除。开发人员还应该注意这种查找行为的另一方面,即如果这些 find_…() 命令中的任何一个调用无法找到所需的实体,则每个调用都会重复搜索,即使在同一个项目内也是如此。不成功的调用不会被缓存。如果一个项目有许多这样的调用,这可能会减慢配置步骤。在极端情况下,每个调用可能检查数万个位置。因此,开发人员应仔细考虑项目如何使用 find_…() 命令,并尽量减少不成功搜索的可能性和数量。如果最低 CMake 版本可以设置为 3.21 或更高,那么策略 CMP0125 也允许避免一些微妙的令人惊讶的行为。
find_package() 的情况略微复杂一些。如果通过 Find 模块找到包,那么很可能所有上述关注点也适用于该包,因为逻辑可能是基于其他 find_…() 命令构建的。如果包是通过配置模式而不是 Find 模块找到的,则 find_package() 将缓存成功的结果,并在后续调用中首先检查该位置。如果该位置上不再有适当的配置文件,则命令将按照其正常的搜索逻辑继续执行。配置模式的这种独特行为更为健壮,更接近开发人员自然想要的行为。
find_…() 结果的缓存化可能会导致微妙的问题,特别是在持续集成系统中。如果正在使用增量构建,在上一次运行的 CMake 缓存中保留了更改项目搜索内容方式的更改,则这些更改可能不会反映在构建中。只有当清除了 CMake 缓存时,这些更改才会生效。缓存通常也意味着不会记录有关找到实体的任何详细信息,因此构建输出对于旧搜索详情的使用提供了很少的线索。因此,人们可能会希望要求所有 CI 构建从头开始构建,但对于较长的构建时间可能并不可行。可能有助于减少问题的策略是在低 CI 负载时间安排每日构建任务,清除构建树,然后按照正常方式构建项目。这样可以保持常规工作时间的增量行为,并且通常会在一天内解决任何与缓存相关的问题。这种策略的有效性在进行分支更改并且 CI 构建在该分支和其他分支之间交替时会降低,但人们希望这样的情况并不常见,并且可以在此期间告知开发人员潜在后果。
find_package() 命令的包注册功能应该谨慎使用。它们可能会对持续集成系统产生意外结果,因为项目可能希望找到也在同一台机器上构建的包。不幸的是,没有环境变量可以设置来禁用注册表的使用,但可以由项目自身通过将 CMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY CMake 变量设置为 OFF 来强制执行(CI 作业通常没有必要的权限来修改系统包注册表,因此设置 CMAKE_FIND_PACKAGE_NO_SYSTEM_PACKAGE_REGISTRY 也应该是不必要的)。实际上,很少有项目写入包注册表,因此除非知道某个项目可能在使用 CI 系统,否则将这个 CMake 变量添加到每个可能受到影响的项目中的需求较低。项目还应避免在 CI 作业中调用 export(PACKAGE)(可以说它们应该在一般情况下避免这样的调用)。
仅在 find_package() 不适用的情况下才保留使用 FindPkgConfig 模块。通常情况下,这适用于 CMake 提供了一个查找模块的包,但该查找模块比较老旧且没有提供导入目标,或者它落后于较新的包发布。FindPkgConfig 模块还适用于搜索 CMake 完全不了解的包,且包没有提供自己的 CMake 配置文件,但提供了一个 pkg-config(即 .pc)文件。
在进行交叉编译时,更倾向于设置 CMAKE_SYSROOT 而不是 CMAKE_FIND_ROOT_PATH。虽然两者都以相同的方式影响各种 find_…() 命令的搜索路径,但只有 CMAKE_SYSROOT 还会确保正确增加编译器和链接器标志,以便正确地处理头文件包含和库链接。
在交叉编译场景中,搜索程序通常期望找到在主机上运行的二进制文件,而搜索文件和库通常期望找到目标平台的内容。因此,很常见地可以在工具链文件中看到以下内容,以通过默认方式强制执行这种行为:
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER)
这里可以说,这个设置应该在项目中设置,而不是依赖于它在工具链文件中设置。从技术上讲,开发者可以自由选择任何工具链文件,项目隐式依赖于默认行为,然后选择是否覆盖它。这里增加的复杂性是,工具链文件可能会在每个project()或enable_language()调用时重新读取,所以如果一个项目想要强制执行特定的默认组合,它需要在每次这样的调用之后这样做。因此,一个合理的妥协是,项目在第一个project()调用之前包含上述代码块,工具链编写者也要包含在内。然后,如果工具链作者没有包含这样一个代码块,至少项目仍然得到合理的默认值。如果工具链文件将默认值更改为其他值,那么它们将在整个项目中一致地应用。因为这是一个如此常见的模式,项目经常假设它。
对于开发者可以在不重新运行CMake的情况下切换设备和模拟器构建(例如,在iOS项目中使用Xcode时),应避免调用find_library()。这样的调用获得的结果只能指向设备库或模拟器库中的一个,而不能同时指向两者。在这种情况下,添加仅通过名称而不是路径链接的基础链接器标志,例如-framework ARKit,-lz或$<LINK_LIBRARY:FRAMEWORK,abc>。如果默认链接器搜索路径上找不到框架或库,则项目还需要提供链接器选项来扩展搜索路径以使其可以被找到。
在线示例和博客文章通常会对是否使用CMAKE_MODULE_PATH或CMAKE_PREFIX_PATH来控制CMake搜索位置提出相互冲突的建议。记住区别的一个简单方法是,当CMake搜索FindXXX.cmake文件或通过include()命令引入模块时,只有CMAKE_MODULE_PATH才会被CMake使用。对于其他所有情况,包括搜索配置包文件,都会使用CMAKE_PREFIX_PATH。在find_…()命令进行搜索时指定要忽略的目录时,最好使用CMAKE_IGNORE_PREFIX_PATH当忽略搜索前缀足够时。
这适用于CMake 3.23或更高版本的所有find_…()命令,并且避免了在前缀下添加每个可能的搜索位置的需要。对于较早的CMake版本,CMAKE_IGNORE_PATH可以用于仅在find_package()中忽略前缀。对于其他find_…()命令,每个要忽略的目录必须单独添加。这两个变量都不会阻止在Find模块的位置进行搜索,尽管它们可能会影响Find模块的实现,如果它在内部调用了find_…()命令。