第二十五章:查找依赖

Posted by lili on

一个至少规模适中的项目很可能会依赖于项目外部提供的内容。例如,它可能希望某个特定的库或工具可用,或者需要知道某个库使用的特定配置或头文件的位置。在更高的层次上,项目可能希望找到一个完整的包,该包可能定义了一系列目标、函数、变量以及其他任何一个普通的CMake项目可能定义的内容。

CMake提供了各种功能,使项目能够找到所需内容,同时也能够被其他项目找到或整合进来。各种find_…()命令提供了搜索特定文件、库或程序,甚至整个包的能力。CMake模块还增加了使用pkg-config提供外部包信息的能力,而其他模块则方便为其他项目编写包文件以供使用。本章介绍了CMake对搜索已经在文件系统上可用的内容的支持。下载缺失的依赖项的能力在第29章“ExternalProject”,第30章“FetchContent”和第32章“Dependency Providers”中进行了介绍,而准备项目以供其他项目找到的内容则在第27.8节“编写配置包文件”和第31章“使项目可用”中进行了讨论。

搜索某些内容的基本想法相对简单,但搜索的具体细节可能会很复杂。在许多情况下,默认行为是合适的,但了解搜索位置及其顺序可以使项目适应非标准行为和不寻常情况。

25.1. 查找文件和路径

在概念上,最基本的搜索任务是查找特定文件。实现这一目标最直接的方法是使用find_file()命令,该命令也是对整个find_…()命令系列的很好的介绍,因为它们都共享许多相同的选项,并且行为类似。

find_file(outVar
	name | NAMES name1 [name2...]
	[HINTS path1 [path2...] [ENV var]...]
	[PATHS path1 [path2...] [ENV var]...]
	[PATH_SUFFIXES suffix1 [suffix2 ...]]
	[REGISTRY_VIEW viewMode] # CMake 3.24 or later
	[NO_DEFAULT_PATH]
	[NO_PACKAGE_ROOT_PATH]
	[NO_CMAKE_PATH]
	[NO_CMAKE_ENVIRONMENT_PATH]
	[NO_SYSTEM_ENVIRONMENT_PATH]
	[NO_CMAKE_SYSTEM_PATH]
	[NO_CMAKE_INSTALL_PREFIX] # CMake 3.24 or later
	[CMAKE_FIND_ROOT_PATH_BOTH | ONLY_CMAKE_FIND_ROOT_PATH | NO_CMAKE_FIND_ROOT_PATH]
	[DOC "description"]
	[REQUIRED] # CMake 3.18 or later
	[NO_CACHE] # CMake 3.21 or later
	[VALIDATOR function] # CMake 3.25 or later
)

该命令可以搜索单个文件名,也可以使用NAMES选项给出一个文件名列表。当搜索的文件可能有几个名称变体时,列表可能会很有用,例如不同的操作系统发行版选择不同的命名约定,包含版本号或不包含版本号,考虑文件从一个发布版本到另一个版本更改名称等。名称应按首选顺序列出,因为搜索将在找到的第一个文件处停止(在移动到下一个名称之前,将完整的搜索位置集合检查特定名称)。当指定包含某种形式版本编号的名称时,CMake文档建议将不包含版本详细信息的名称排在包含版本详细信息的名称之前,以便更可能在本地构建的文件之前找到由操作系统提供的文件。

搜索将根据明确定义的顺序在一组位置上进行。大多数位置都有一个关联选项,如果该选项存在,则将跳过该位置,从而允许根据需要定制搜索。以下表格总结了搜索顺序:

包根变量

首先搜索的位置仅适用于在调用find_package()调用的一部分作为脚本调用的情况下使用find_file()(在本章后面讨论)。最初,它在CMake 3.9.0中被添加为一个搜索位置,但由于向后兼容性问题,在3.9.1中被移除。然后在CMake 3.12中再次添加,并解决了问题。这个搜索位置的进一步讨论将推迟到第25.5节“查找包”中,那里的使用更加相关。

缓存变量(特定于CMake)

CMake特定的缓存变量位置是从缓存变量CMAKE_PREFIX_PATH、CMAKE_INCLUDE_PATH和CMAKE_FRAMEWORK_PATH派生的。其中,CMAKE_PREFIX_PATH可能是最方便的,因为设置它不仅适用于find_file(),而且适用于所有其他find_…()命令。它代表了一个基点,预期在其下面的典型目录结构包括bin、lib、include等,每个find_…()命令都会附加自己的子目录以构建搜索路径。对于find_file(),对于CMAKE_PREFIX_PATH中的每个条目,将会搜索目录<prefix>/include。如果设置了变量CMAKE_LIBRARY_ARCHITECTURE,则会首先搜索体系结构特定目录<prefix>/include/${CMAKE_LIBRARY_ARCHITECTURE},以确保体系结构特定位置优先于通用位置。变量CMAKE_LIBRARY_ARCHITECTURE通常由CMake自动设置,项目通常不应尝试自己设置它。

对于需要搜索更具体的包含或框架路径的情况,并且它不是标准目录布局或包的一部分,可以使用变量CMAKE_INCLUDE_PATH和CMAKE_FRAMEWORK_PATH。它们分别提供要搜索的目录列表,但与CMAKE_PREFIX_PATH不同,不会附加包含子目录。CMAKE_INCLUDE_PATH受find_file()和find_path()支持,而CMAKE_FRAMEWORK_PATH受这两个命令和find_library()支持。除此之外,这两组路径都以相同的方式处理。有关更多详细信息,请参见下面的第25.1.1节“适用于Apple的特定行为”。

在使用CMake 3.16或更高版本时,变量CMAKE_FIND_USE_CMAKE_PATH可以用于控制默认行为,即是否考虑CMake特定的缓存变量在搜索中。如果变量未定义或设置为true,则搜索将考虑缓存变量。如果设置为false,则搜索将忽略它们。

环境变量(特定于CMake)

CMake特定的环境变量位置与缓存变量位置非常相似。三个环境变量CMAKE_PREFIX_PATH、CMAKE_INCLUDE_PATH和CMAKE_FRAMEWORK_PATH的处理方式与同名的缓存变量相同,但在Unix平台上,每个列表项将以冒号(:)而不是分号(;)分隔。这是为了使环境变量能够使用与每个平台上的其他路径列表相同样式定义的特定于平台的路径列表。

在使用CMake 3.16或更高版本时,变量CMAKE_FIND_USE_CMAKE_ENVIRONMENT_PATH可以用于控制默认行为,方式与CMAKE_FIND_USE_CMAKE_PATH类似。

环境变量(特定于系统)

系统特定的环境变量包括INCLUDE和PATH。两者都可以包含由特定于平台的路径分隔符(Unix上为冒号,Windows上为分号)分隔的列表,每个项目都将添加到搜索位置集合(在PATH之前添加INCLUDE)。

仅在Windows(包括Cygwin)上,PATH条目将以更复杂的方式进一步处理。对于PATH环境变量中的每个条目,将通过从末尾删除任何尾随的bin或sbin子目录来计算基本路径。如果定义了变量CMAKE_LIBRARY_ARCHITECTURE,则将添加体系结构特定目录<base>/include/${CMAKE_LIBRARY_ARCHITECTURE}。之后,将无论是否定义CMAKE_LIBRARY_ARCHITECTURE,将<base>/include路径添加到搜索路径集合中,并放置在未修改的PATH项之前。例如,如果将CMAKE_LIBRARY_ARCHITECTURE设置为somearch,并且PATH环境变量包含C:\foo\bin;D:\bar,则将添加以下有序的搜索路径集合:

C:\foo\include\somearch
C:\foo\include
C:\foo\bin
D:\bar\include\somearch
D:\bar\include
D:\bar

在使用CMake 3.16或更高版本时,变量CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH可以用于控制默认行为,方式与CMAKE_FIND_USE_CMAKE_PATH类似。

缓存变量(特定于平台)

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

在使用CMake 3.16或更高版本时,变量CMAKE_FIND_USE_CMAKE_SYSTEM_PATH可以用于控制默认行为,方式与CMAKE_FIND_USE_CMAKE_PATH类似。

对于某些平台,安装前缀(参见第27.1.2节“基本安装位置”)包含在CMAKE_SYSTEM_PREFIX_PATH中。这可能并不总是理想的,它可能导致发现意外内容。CMake 3.24添加了对NO_CMAKE_INSTALL_PREFIX关键字的支持,该关键字使命令忽略CMAKE_SYSTEM_PREFIX_PATH中的安装前缀。可以使用变量CMAKE_FIND_USE_INSTALL_PREFIX设置默认行为(将其设置为true以默认阻止搜索安装前缀)。对于CMake 3.23及更早版本,变量CMAKE_FIND_NO_INSTALL_PREFIX具有类似的用途,但含义相反。尽可能使用新变量,因为如果同时定义了两个变量,则新变量将覆盖旧变量。

HINTS和PATHS

上面讨论的各种组的变量都是由项目外部设置的,但是HINTS和PATHS选项是项目本身应该注入的其他搜索路径。HINTS和PATHS之间的主要区别在于PATHS通常是永远不变且不依赖于其他内容的固定位置,而HINTS通常是从其他值计算的,例如已经找到的某些东西的位置或者依赖于变量或属性值的路径。PATHS是最后搜索的目录,但在任何平台或系统特定位置之前搜索HINTS。

HINTS和PATHS都支持指定环境变量,该变量可能包含主机本地格式的路径列表(即在Unix系统上为冒号分隔,在Windows上为分号分隔)。这是通过在环境变量的名称之前加上ENV来完成的,例如PATHS ENV FooDirs。当环境变量的内容可能使用特定于平台的路径分隔符时,应该使用此形式,而不是$ENV{FooDirs}。

使用CMake 3.24或更高版本时,HINTS和PATHS可以引用Windows主机上的注册表位置。可以查看注册表的32位和64位视图。REGISTRY_VIEW关键字可用于控制这些视图,或者如果省略,则行为将由策略CMP0134决定。包含注册表值和使用注册表视图的详细信息在官方CMake文档中有详细介绍,因此此处不再重复。

除了HINTS和PATHS搜索位置之外,所有其他搜索位置都有一种形式为NO_…PATH的相关跳过选项,可用于跳过该组位置。此外,可以使用选项NO_DEFAULT_PATH来跳过除HINTS和PATHS位置之外的所有位置,从而强制命令仅搜索由项目控制的特定位置。这些NO…选项会覆盖由CMAKE_FIND_USE_…变量提供的任何默认设置。

PATH_SUFFIXES选项可用于提供一个额外的子目录列表,在每个搜索位置下检查。每个搜索位置会依次与每个后缀一起使用,然后在移动到下一个搜索位置之前不使用任何后缀。请谨慎使用此选项,因为它会大大扩展要搜索的总位置数。

在许多情况下,项目只需要指定一个要搜索的文件名,而搜索顺序的复杂性并不特别重要。也许只需要提供一些额外的搜索路径(等同于PATHS选项)。在这种情况下,可以使用命令的较短形式:

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可用于向该缓存变量添加文档,但极少使用。相反,最好选择一个自我描述的变量名称,从而使得显式文档不必要。

在使用CMake 3.20或更早版本时,与结果变量相关的某些边缘情况可能会产生意外的行为。当在调用find_file()之前存在与结果变量同名的非缓存变量时,必须小心。这个非缓存变量可能会被忽略,这取决于是否也存在缓存变量以及这种缓存变量是否具有类型(参见第5.3节“缓存变量”)。CMake 3.21添加了策略CMP0125,确保非缓存变量将更像直观预期的方式处理。还在CMake 3.21中添加了NO_CACHE选项,作为将结果仅分配给非缓存变量的一种方式,但项目通常应避免使用它。NO_CACHE会通过强制find_file()在每次运行时都重复搜索来影响配置阶段的性能。

如果找不到文件,则存储在变量中的值将在if()表达式中求值为false。测试只需在对find_file()的最后调用之后进行。

find_file(FOO_HEADER foo.h
	PATHS /opt/foo/include
	NO_DEFAULT_PATH
)
find_file(FOO_HEADER foo.h)
if(NOT FOO_HEADER)
	message(FATAL_ERROR "Could not find foo.h")
endif()

上述形式适用于任何CMake版本,但从CMake 3.18开始,可以使用REQUIRED选项更简洁地表达逻辑:

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

25.1.1. 苹果特定行为

虽然find_file()命令可用于查找任何文件,但它的起源是搜索头文件。这就是为什么一些默认搜索路径会附加一个include子目录的原因。在苹果平台上,框架有时包含它们自己的头文件(参见第24.3节,“框架”),而find_file()命令具有与在其中的适当子目录中搜索相关的附加行为。对于每个搜索位置,该命令可以将位置视为框架、普通目录或两者兼而有之。

行为由CMAKE_FIND_FRAMEWORK变量控制,该变量应该包含以下值之一:FIRST、LAST、ONLY或NEVER。FIRST意味着将搜索位置视为框架的顶级目录,并将适当的子目录附加到进入其中的Headers位置。如果在那里找不到命名文件,则再次将搜索位置视为普通目录而不是框架并进行搜索。LAST颠倒了这个顺序,ONLY不会将位置视为普通目录,NEVER会跳过将位置视为框架的步骤。苹果系统的默认值是FIRST,这通常是期望的行为。

25.1.2. 跨编译控制

对于跨编译场景,搜索位置集合变得更加复杂。跨编译工具链通常会在自己的目录结构下进行收集,以使其与默认主机工具链分开。在搜索特定文件时,通常希望首先查看工具链的目录结构,然后再查看主机的目录结构,以便找到目标平台特定版本的文件。这在查找程序和库时尤为重要。即使对于查找文件,文件内容在平台之间也可能发生变化(例如,特定于平台的配置头文件)。

为了支持跨编译场景,整个搜索位置集合可以重新定位到文件系统的不同部分。CMAKE_FIND_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()只能提供一个位置,但是某些跨编译情况下支持构建安排,可以在不重新运行CMake的情况下在设备和模拟器构建之间切换。这意味着如果find_file()的结果取决于使用哪种构建方式,那么它们是不可靠的。对于查找库这一方面更为重要,并且在下文的第25.4节,“查找库”中进行了更详细的讨论。 跨编译场景有时也需要在搜索过程中忽略某些路径。请参阅第25.6节,“忽略搜索路径”以进行该主题的讨论。

25.1.3. 验证器

通常,文件的存在就足以被接受为找到的文件。在更高级的情况下,文件可能需要满足其他条件才能被接受。从CMake 3.25或更高版本开始,可以使用VALIDATOR关键字指定一个实现对每个候选文件进行任意检查的函数。验证器函数必须接受两个参数:

  • 要在调用作用域中设置的结果变量的名称。
  • 文件的绝对路径。

除非函数在返回之前将结果变量设置为false,否则候选文件将被接受为find_file()调用的结果。以下两个示例演示了使用方法。

# Only accept files that define a version string
function(has_version result_var file)
	file(STRINGS "${file}" version_line
		REGEX "#define +THING_VERSION" LIMIT_COUNT 1
	)
	if(version_line STREQUAL "")
		set(${result_var} FALSE PARENT_SCOPE)
	endif()
endfunction()

find_file(THING_HEADER thing.h VALIDATOR has_version)
# Require a companion version file in the same directory
function(has_version_file result_var file)
	cmake_path(GET file PARENT_PATH dir)
		if(NOT EXISTS "${dir}/thing_version.h")
			set(${result_var} FALSE PARENT_SCOPE)
		endif()
endfunction()
find_file(THING_HEADER thing.h VALIDATOR has_version_file)

25.2. 查找路径

项目可能希望找到包含特定文件的目录,而不是实际文件本身。find_path() 命令提供了这种功能,与 find_file() 在所有方面都相同,唯一不同的是找到的文件所在目录存储在结果变量中。

25.3. 查找程序

查找程序与查找文件略有不同。find_program() 命令接受与 find_file() 完全相同的参数集,以及一个额外的可选参数 NAMES_PER_DIR。find_program() 命令还支持类似的简短形式。以下描述了与 find_file() 相比,find_program() 的差异,虽然看起来可能有些复杂,但大部分情况下都是可以逻辑推断的,但也有一些特殊情况需要注意:

缓存变量(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() 没有意义,PATH 中的每个项在不进行任何修改的情况下进行检查。在所有平台上行为相同。

通用

  • 通常,在 NAMES 选项提供多个名称时,将在给定名称的所有搜索位置上进行检查,然后再继续搜索列表中的下一个名称。find_program() 命令支持 NAMES_PER_DIR 选项,它颠倒了此顺序,在移动到下一个位置之前会在特定搜索位置上检查每个名称。NAMES_PER_DIR 选项在 CMake 3.4 或更高版本中可用。

  • 在 Windows(包括 Cygwin 和 MinGW)上,文件扩展名 .com 和 .exe 也会自动检查,因此无需将这些扩展名作为程序名称的一部分提供给 find。这些扩展名会优先于没有扩展名的名称进行检查。请注意,.bat 和 .cmd 文件不会自动搜索。

  • 虽然 find_file() 使用 CMAKE_FIND_FRAMEWORK 来确定在框架和非框架路径之间的搜索顺序,但 find_program() 使用 CMAKE_FIND_APPBUNDLE。它提供了在苹果平台上在应用程序捆绑包路径和非捆绑包路径之间进行控制的类似方式。这两个变量支持的值相同,对捆绑包具有预期的等效含义。查找文件时将在 Headers 子目录中查找,而查找程序将在 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。

25.4. 查找库

查找库与查找文件类似。find_library() 命令支持与 find_file() 完全相同的选项集,再加上一个额外的 NAMES_PER_DIR 选项。以下是与 find_file() 相比,find_library() 的不同之处:

缓存变量(CMake 特有)

  • 当在 CMAKE_PREFIX_PATH 下搜索时,find_file() 将 include 附加到每个项后面,而 find_library() 则附加 lib。CMAKE_LIBRARY_ARCHITECTURE 变量也以与 find_file() 相同的方式受到尊重。
  • CMAKE_LIBRARY_PATH 替代了 CMAKE_INCLUDE_PATH,但在使用方式上完全相同。CMAKE_LIBRARY_PATH 仅供 find_library() 使用。CMAKE_FRAMEWORK_PATH 变量与 find_file() 使用方式完全相同。

环境变量(特定于系统)

  • 标准系统环境变量的搜索位置处理方式与 find_file() 非常相似。不同之处在于,会检查 LIB 环境变量而不是 INCLUDE。此外,基于 PATH 的搜索位置遵循与 find_file() 相同的复杂逻辑,只是在每个前缀后面附加 lib,而不是 include。就像对 find_file() 一样,这种复杂的 PATH 逻辑仅适用于 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()。在苹果平台上,设置 CMAKE_FIND_ROOT_PATH_MODE_LIBRARY 为 ONLY 前请仔细考虑,因为库可能构建为支持多个目标平台的 fat 二进制文件。这些 fat 二进制文件可能不位于目标平台特定的路径下,因此可能仍然需要搜索主机平台路径才能找到它们。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)

请注意,在 CMake 3.24 或更早版本中,无法在 Windows 上使用这种技术,因为静态库和共享库的导入库(即 DLL)具有相同的文件名,包括后缀(例如 foobar.lib)。因此,文件名无法区分这两种类型的库。在 CMake 3.25 或更高版本中,可以使用 VALIDATOR 来确定文件是否是导入库。Visual Studio 工具链包括一个 lib 工具,可用于列出静态库或导入库的内容。CMake 将通过 CMAKE_AR 变量提供该工具的位置。如果从该工具获取的内容列表包含相同的文件,但后缀从 .lib 更换为 .dll,那么这就是文件很可能是导入库的一个合理提示。下面展示了该逻辑的基本实现:

function(is_import_lib result_var file)
	cmake_path(GET file FILENAME filename)
	string(TOLOWER "${filename}" filename_lower)
	string(REGEX REPLACE "\\.lib$" ".dll"
		dll_filename_lower "${filename_lower}"
)
# This assumes we are using the MSVC toolchain
execute_process(
	COMMAND ${CMAKE_AR} /nologo /list "${file}"
	RESULT_VARIABLE result
	OUTPUT_VARIABLE output
	ERROR_VARIABLE errors
)
string(TOLOWER "${output}" output_lower)
if(result OR
	  NOT errors STREQUAL "" OR
	  NOT output_lower MATCHES "(^|\n|\\\\)${dll_filename_lower}(\n|$)")
	set(${result_var} FALSE PARENT_SCOPE)
endif()
endfunction()

然后可以进行更健壮的搜索,优先选择静态库,如下所示:

if(MSVC)
	find_library(FOOBAR_LIBRARY foobar.lib VALIDATOR is_import_lib)
else()
	find_library(FOOBAR_LIBRARY libfoobar.a)
endif()

find_library(FOOBAR_LIBRARY foobar)

库处理的另一个独特复杂性是许多平台同时支持 32 位和 64 位架构。可能会在不同位置安装 32 位和 64 位版本的库,但文件名相同。用于在这种多库架构系统上分隔不同架构的目录结构可能有所不同,甚至对于同一平台的不同发行版也是如此。例如,某些发行版将 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 就足够了。

25.5. 查找包

在前面的章节中讨论的各种 find_…() 命令都专注于查找一个特定的项目。然而,这些项目往往只是一个更大包的一部分,整个包可能具有项目可能感兴趣的自身特性,比如版本号或对某些特性的支持。项目通常希望将包作为单个单元来查找,而不是手动拼凑其不同部分。

CMake 中定义包的两种主要方式是作为一个模块或通过配置细节。配置细节通常作为包本身的一部分提供,并且它们与前面章节讨论的各种 find_…() 命令的功能更加接近。另一方面,模块通常由与包无关的其他内容(通常是由 CMake 或项目自身定义)定义,因此,随着包随着时间的推移而发展,它们更难保持更新。

模块和配置文件通常定义了包的变量和导入目标。这些可能提供程序的位置、库、被消费目标使用的标志等。还可以定义函数和宏。并没有一套明确的要求来说明会提供什么,但有一些惯例在 CMake 开发者手册中已经说明。项目作者必须查阅每个模块或包的文档以了解提供的内容。作为一般指南,旧的模块通常提供遵循相当一致模式的变量,而较新的模块和配置实现通常定义导入目标。

如果同时提供了变量和导入目标,项目应该优先选择后者,因为它们更加健壮,并且与 CMake 的传递依赖功能更好地集成。

项目通常使用 find_package() 命令查找包,该命令有短格式和长格式。一般应该优先选择短格式,因为它更简单,并且支持模块和配置包,而长格式不支持模块。然而,在某些情况下,长格式提供了更多的搜索控制,因此在这些情况下更可取。

短格式只有几个选项,可以总结如下:

	find_package(packageName
	[version [EXACT] ]
	[QUIET] [REQUIRED]
	[ [COMPONENTS] component1 [component2...] ]
	[OPTIONAL_COMPONENTS component3 [component4...] ]
	[GLOBAL]  # CMake 3.24 or later
	[REGISTRY_VIEW viewMode] # CMake 3.24 or later
	[MODULE]
	[NO_POLICY_SCOPE]
)

可选的 version 参数指示包必须是指定版本或更高版本,如果还给出了 EXACT,则必须完全匹配。当使用 CMake 3.19 或更高版本时,版本可以指定为版本范围。版本范围以 versionMin…versionMax 或 versionMin…<versionMax 的形式表示。范围的 versionMin 部分被视为单个版本号。它是包的最低要求版本。对于版本约束的上限,第一种形式要求包的版本不得大于 versionMax,而第二种形式要求其严格小于 versionMax。如果使用版本范围,则不能使用 EXACT 关键字。

请注意,包可能不知道版本范围。它们的模块或配置文件可能具有比 CMake 版本范围支持更旧的实现。在这些情况下,包通常会忽略要求的 versionMax 部分。第 27.8.1 节,“CMake 项目的配置文件”进一步讨论了包可能处理版本范围的情况。第 30.4 节,“与 find_package() 集成”和第 32 章,“依赖提供者”讨论了可能完全忽略版本约束的情况。

包可能是可选的,这意味着项目可以在可用时使用它,或者如果找不到合适的包则可以在没有它的情况下工作。如果包是强制性的,则应提供 REQUIRED 选项,以便在找不到合适的包时,CMake 将停止并显示错误。与其他 find_…() 命令不同,所有的 CMake 版本都支持 find_package() 的 REQUIRED 选项。

通常,find_package() 将为失败记录消息,但 QUIET 选项可用于抑制可选包的消息(无法抑制强制性包的失败消息)。QUIET 还会抑制通常在第一次找到包时打印的消息。QUIET 的典型用法是防止针对缺失可选包的消息,以便开发人员不太可能认为这是错误。

与组件相关的选项允许项目指示它们感兴趣的包的哪些部分。并非所有的包都支持组件,这取决于模块或配置实现是否定义了组件以及组件代表什么。组件可能有用的一个例子是大型包如 Qt,其中并非所有组件都可能被安装。项目可能仅仅说它需要 Qt 是不够的,它可能还需要指定 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() 中没有给出任何组件,那么如何处理就取决于模块或配置定义。对于一些包,可能会将其视为列出了所有组件,而对于其他包,可能会将其解释为不需要任何组件(尽管基本的包细节仍然被定义,比如基本库、包版本等)。另一种可能性是缺少组件可能被视为错误。由于行为的差异,开发人员应查阅他们希望查找的包的文档。

包通常会创建导入目标。默认情况下,这些目标仅在当前目录范围及其以下范围内可见。这种历史上的 CMake 行为存在是为了支持在不同目录范围中查找同一包的不同版本。由于不同版本的导入目标是在无关的目录范围中创建的,因此这些目标不会冲突。实际上,这种灵活性的需求非常罕见,并且在构建中混合使用不同版本与现代实践不太匹配。较新的 CMake 功能加强了这样的观点,即在整个构建过程中只有一个版本的依赖。从 CMake 3.24 或更高版本开始,项目可以通过在 find_package() 调用中添加 GLOBAL 关键字来强制包创建全局导入目标。如果没有给出 GLOBAL 关键字,则使用 CMAKE_FIND_PACKAGE_TARGETS_GLOBAL 变量作为默认值,因此项目可以使用该变量来设置其所有 find_package() 调用的行为。项目的消费者可能不希望它强制这些导入目标为全局,因此要谨慎使用此功能。

短格式的其他选项很少使用。REGISTRY_VIEW 关键字控制注册表路径的解释方式(仅在 CMake 3.24 或更高版本中支持)。请查阅官方 CMake 文档以获取该功能的完整解释。NO_POLICY_SCOPE 关键字是来自 CMake 2.6 时代的历史遗留问题,项目应避免使用它。MODULE 关键字将调用限制为仅搜索模块,而不是配置包。项目通常应避免使用此选项,因为它们不应该关心包的定义方式的实现细节,而只应关注对包的要求。当 MODULE 关键字不存在时,find_package() 命令的短格式将首先搜索匹配的模块,然后如果找不到这样的模块,则将搜索配置包。CMake 3.15 添加了对 CMAKE_FIND_PACKAGE_PREFER_CONFIG 变量的支持,可以将其设置为 true 以反转搜索偏好(默认情况下未设置以保持 3.15 之前的行为)。模块最初在第 11 章讨论。虽然非包模块使用 include() 命令纳入项目中,但包模块的文件名形式为 Find<packageName>.cmake,并且意图是通过调用 find_package() 处理。因此,它们通常被称为 Find 模块。include() 和 find_package() 都遵循 CMAKE_MODULE_PATH 变量作为在每个 CMake 发行版中作为一组模块之前搜索的目录列表。

Find 模块负责实现 find_package() 调用的所有方面,包括定位包、执行版本检查、满足组件要求以及根据需要记录或不记录消息。并非所有的 find 模块都遵循这些责任,它们可能选择忽略除了包名称之外的一些或全部提供的信息,因此始终要查阅模块文档以确认预期的行为。

Find 模块通常是通过调用各种 find_…() 命令来实现的。因此,它们有时可能会受到与这些命令相关的缓存和环境变量的影响。CMAKE_PREFIX_PATH 变量特别方便于影响 find 模块,因为每个指定的路径都充当每个 find_…() 命令附加其特定子目录的基点。对于遵循相当标准布局的包,仅将包的基本安装位置添加到 CMAKE_PREFIX_PATH 通常足以使 find 模块找到所需的所有包组件。

与 find 模块相比,具有配置详细信息的包为项目提供了一种更丰富、更强大的方式来检索关于该包的信息。在配置模式下,find_package() 提供了更多的选项,其完整的长格式命令与其他 find_…() 命令有很多相似之处:

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

当使用长格式支持的选项调用 find_package() 时,将跳过对 Find 模块的搜索。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>/<packageName>*/(cmake|CMake)/<packageName>*/ # 3.25 or later only
<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)/

在Apple平台,下面的路径也会被搜索:

<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_…()命令。