> For the complete documentation index, see [llms.txt](https://chenxiaowei.gitbook.io/professional-cmake/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://chenxiaowei.gitbook.io/professional-cmake/part3-chinese/27-chinese.md).

# 第27章：扩展

对于中等复杂度的项目，可能依赖于一个或多个外部依赖项。这些依赖通常是常用的工具包，如zlib、OpenSSL、Boost等，以及由同一组织或作为资源、测试数据等使用的私有项目。某些情况下，项目可以期望操作系统提供必要依赖。例如，项目是作为操作系统的一部分分发的，那么是合适的。对于独立的项目，项目应该控制其依赖项的确切版本，以确保构建可重复，并且发布包有已知的来源。当在持续集成系统上构建时，这一点尤其重要。

CMake为外部内容引入构建提供了一些选择。file(DOWNLOAD)命令可以用于检索特定文件，可以在配置阶段使用，也可以作为脚本模式(即CMake -P)进行。但这个命令通常还不够集成整个项目所需的功能级别。为了下载和构建完整的依赖关系，CMake中的传统方法是使用ExternalProject模块。这是CMake的一部分，除了简单地下载和构建之外，它还有很多用途。CMake 3.11中添加的FetchContent模块构建在ExternalProject之上，开启了各种新的用例，包括处理项目之间共享的依赖关系，以及在一个构建中支持整个项目层次结构。ExternalData模块为在构建时处理外部内容提供了另一种选择，它会关注测试用例使用的数据。

## 27.1. ExternalProject

ExternalProject模块的主要目的是支持下载和构建外部项目，这些外部项目不容易直接成为主项目的一部分。外部项目添加为它自己独立的子构建，有效地从主项目中分离出来，并当作黑盒处理。这意味着可以用于不同的体系结构、不同的构建项目，甚至可以使用CMake以外的系统构建项目。还可以用于处理定义目标或安装与主项目冲突的组件。

ExternalProject通过在主要项目中定义一组构建目标来工作，这些目标代表了获取和构建外部项目的不同阶段。然后在执行整个序列时，CMake会使用相应的目标收集这些信息。时间戳用于跟踪已执行阶段，并且不需要重复，除非相关信息发生更改。默认的阶段设置如下:

**Download**

可以使用多种方法获取外部项目。这包括从URL下载打包文件，并自动解压缩它，或者从源代码存储库(如git、subversion、mercurial或CVS)复制/签出。另外，如果支持的下载选项都不合适，项目也可以定义自己的命令。

**Update/Patch**

一旦下载了源码，就可以对其应用补丁(打包文件下载)，或者将其更新(对于源代码存储库)。如果需要，可以提供定制命令来覆盖默认行为。

**Configure**

如果外部项目使用CMake作为构建系统，这一步在下载的源代码上执行CMake。一些信息从主构建中传递过来，使外部CMake项目的配置无缝。对于非cmake的外部项目，可以提供一个自定义命令来运行等效的步骤，比如运行配置脚本和适当的选项。

**Build**

默认情况下，如果使用CMake配置构建，则使用与主项目相同的构建工具构建已配置的外部项目。可以为构建阶段提供定制命令，以使用不同的构建工具或执行其他任务。

**Install**

外部项目可以安装到本地目录中，通常安装到主项目的构建树中的某个位置。然后，主项目知道外部项目的构建工件在哪里，并且可以将它们合并到自己的构建中。默认行为取决于配置阶段是否使用CMake构建。

**Test**

外部项目可能带有自己的测试，主项目可能希望或不希望运行这些测试。ExternalProject模块在是否运行测试阶段(默认情况下不运行)，并且测试应该在安装阶段之前还是之后运行也可以选择。如果启用测试阶段，默认的测试目标将假定存在于外部项目中，y也可以指定自定义命令来对测试阶段进行控制。

该模块允许定义自定义阶段，并插入到上述工作流中，但是默认的阶段集对于大多数项目来说已经足够了。默认阶段的详细信息都由模块提供的主函数ExternalProject\_Add()设置。这个函数接受许多选项，所有这些在模块的文档中都有详细说明。下面给出了一些常用的场景和一些典型的场景，以指导读者如何充分利用ExternalProject提供的功能。

### 27.1.1. 主要特性

最简单的情况包括从URL下载源归档文件，并将其构建为CMake项目。实现这一点所需的信息就是URL，如下所示：

```
include(ExternalProject)
ExternalProject_Add(someExtProj
 URL http://somecompany.com/releases/myproj_1.2.3.tar.gz
)
```

该函数的第一个参数要在主项目中创建构建目标的名称。此目标将用于记录引用外部项目的整个构建过程。默认情况下，它会添加到主项目的all目标中，也可以通过添加EXCLUDE\_FROM\_ALL选项来禁用它，该选项与add\_executable()、add\_custom\_target()等命令具有相同的效果。上面的例子中，构建someExtProj目标将导致在主项目的构建阶段执行以下操作:

* 下载压缩包并解压缩它。
* 基于主版本使用默认选项运行cmake。
* 为默认目标与主项目相同的构建工具。
* 构建外部项目的安装目标。

这些步骤都会在构建目录中创建的一组单独的目录来保存源文件、构建输出、时间戳和与外部项目的构建相关联的其他临时文件。这些目录的结构取决于几个不同的因素，模块文档提供了如何选择目录结构的详细说明。更简单的起点是展示主项目如何控制位置，而不依赖于默认值。可以使用PREFIX选项设置目录的基本位置。

```
ExternalProject_Add(someExtProj
 PREFIX prefixDir
 URL http://somecompany.com/releases/myproj_1.2.3.tar.gz
)
```

使用这种方式时，目录布局将基于prefixDir，通常应该使用绝对路径，并且位于主项目的构建区域。这个位置下创建的默认相对目录布局如下所示。解压缩的归档文件将放在prefixDir/src/someExtProj中，CMake构建将使用prefixDir/src/someExtProj-build作为构建目录。

![](https://211235224-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MJHzSm7gYyKXsrbfeFZ%2Fsync%2Faa3ec52e75c1f83a90fdd33839ca5f2c2ba00f77.png?generation=1604235294566757\&alt=media)

可以设置EP\_PREFIX和EP\_BASE目录属性来影响上述布局，请参阅ExternalProject文档了解详细信息。前缀和这些目录属性只提供对目录结构的粗略控制。对于那些需要的情况，ExternalProject\_Add()允许直接设置部分或所有目录:

```
ExternalProject_Add(someExtProj
 DOWNLOAD_DIR downloadDir
 SOURCE_DIR sourceDir
 BINARY_DIR binaryDir
 INSTALL_DIR installDir
 TMP_DIR tmpDir
 STAMP_DIR stampDir
 URL http://somecompany.com/releases/myproj_1.2.3.tar.gz
)
```

实际上，很少使用TMP\_DIR和STAMP\_DIR。默认安装位置由外部项目决定，这通常是系统范围的位置，所以可以指定INSTALL\_DIR来收集所有外部项目的构建目录(要使外部项目使用指定的INSTALL\_DIR，还需要执行进一步的设置，稍后的示例将会展示)。

另一个是提供SOURCE\_DIR，并给出现有目录的位置。当使用这种方式时，不需要提供下载方法，命令将简单地使用指定源目录的现有内容。这是为不同平台构建主项目源代码树的一种方法。例如:

```
ExternalProject_Add(firmware
 SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/firmware
 INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}/firmware-artifacts
 #... other options to configure differently
)
```

当外部项目也使用CMake作为其构建系统时，可以添加CMake命令行选项来影响配置。实现这一点的最直接方法是使用CMAKE\_ARGS选项，后面应该跟着要传递给外部项目的cmake命令的参数。上面的例子可以扩展到工具链文件中，配置版本构建和使用指定的安装目录，像这样:

```
ExternalProject_Add(firmware
 SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/firmware
 INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}/firmware-artifacts
 CMAKE_ARGS -D CMAKE_TOOLCHAIN_FILE=${CMAKE_CURRENT_LIST_DIR}/fwtoolchain.cmake
 -D CMAKE_BUILD_TYPE=Release
 -D CMAKE_INSTALL_PREFIX=<INSTALL_DIR> # See further below
)
```

如果需要设置多个CMake选项，则生成的CMake命令的长度可能会成为问题。另一种方法是使用CMAKE\_CACHE\_ARGS指定缓存变量，而不是通过CMAKE\_ARGS定义。这些参数的形式应该是`-Dvariable:TYPE=value`，并转换为这样的方式`set(variable value CACHE TYPE "" FORCE)`。然后使用-C选项将该文件传递给cmake命令行。其效果与通过-D选项在cmake命令行上直接设置变量是一样的。还有其他选项可以用来更改CMake生成器，以及使用CMake一些不太常见的功能。可以参阅模块文档以了解更多信息。

如果外部项目不使用CMake作为构建系统，可以给出CONFIGURE\_COMMAND选项来提供可执行的自定义命令，而不是运行CMake。例如，许多项目提供一个配置脚本，可以这样设置:

```
ExternalProject_Add(someAutotoolsProj
 URL someUrl
 CONFIGURE_COMMAND <SOURCE_DIR>/configure
 ...
)
```

configure命令在构建目录中运行，但configure脚本将在源目录中运行。上面演示了另一种策略，使用默认的结构，而不是显式地定义用于外部项目的目录布局，但是命令的占位符支持提供了源目录的位置。前面的示例还使用了占位符，作为`CMAKE_INSTALL_PREFIX`的值传递安装目录。占位符就是用尖括号括起来的特定目录的选项名，最常用的是`<SOURCE_DIR>`, `<BINARY_DIR>`和`<INSTALL_DIR>`。`<DOWNLOAD_DIR>`可以在CMake 3.11或更高版本中使用。占位符的更多信息可以通过模块文档进行了解。

如果没有使用CONFIGURE\_COMMAND选项，则假定该项目s使用CMake构建，并且外部项目的构建步骤将使用与主项目相同的构建工具。对于这种情况，默认的构建步骤是合适的，不需要特殊的处理。当提供CONFIGURE\_COMMAND时，默认构建工具假定为make。如果应该构建非默认目标，或者需要构建工具而不是make，必须提供自定义构建命令。例如:

```
find_program(MAKE_EXECUTABLE NAMES nmake gmake make)
ExternalProject_Add(someAutotoolsProj
 URL someUrl
 CONFIGURE_COMMAND <SOURCE_DIR>/configure
 BUILD_COMMAND ${MAKE_EXECUTABLE} specialTool
)
```

自定义构建命令可以做任何事情，甚至可以设置为空字符串，以有效地绕过构建阶段。可以预见的是，同样的模式在安装阶段也可行。对于CMake项目，主项目的构建工具用来构建一个默认名为install的目标，而对于非CMake项目，默认命令是简单的make install。INSTALL\_COMMAND选项可用于自定义安装命令，也可以将其设置为空字符串，以完全禁用安装。当主项目不需要进一步安装时，通常会使用这种方法。

```
ExternalProject_Add(someAutotoolsProj
 URL someUrl
 CONFIGURE_COMMAND <SOURCE_DIR>/configure
 BUILD_COMMAND ${MAKE_EXECUTABLE} specialTool
 INSTALL_COMMAND "" # Effectively disable the install stage
)
```

注意正确处理安装阶段。如果外部项目使用CMake作为其构建系统，则默认安装规则的目的地由CMAKE\_INSTALL\_PREFIX缓存变量控制。如果没有设置此变量，则将使用默认位置，这通常会导致将外部项目安装到系统目录中，而这通常不是期望的结果(如果项目是在持续集成系统中构建的，则肯定不是)。类似地，如果外部项目使用的是CMake以外的构建系统，则默认安装命令将是make install，也可能尝试将目标安装到系统目录。对于CMake，通过CMAKE\_ARGS设置缓存变量(如前面示例中所示)就可以解决这种情况，而对于基于Makefile的项目，下面这样的设置通常是可用的:

```
ExternalProject_Add(otherProj
 URL ...
 INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}/otherProj-install
 CONFIGURE_COMMAND <SOURCE_DIR>/configure
 INSTALL_COMMAND ${MAKE_EXECUTABLE} DESTDIR=<INSTALL_DIR> install
)
```

`INSTALL_DIR`选项除了为`<INSTALL_DIR>`占位符定义值外，不做任何事情。调用者可以使用`<INSTALL_DIR>`占位符将信息传递到需要的地方。项目应该使用`INSTALL_DIR`定义位置，然后使用`<INSTALL_DIR>`占位符，而不是直接将路径嵌入到`INSTALL_COMMAND`选项中。

测试阶段的处理方式略有不同，默认情况下不执行任何操作。要启用它，必须给出特定于测试的选项，比如TEST\_BEFORE\_INSTALL YES或TEST\_AFTER\_INSTALL YES。启用后，与构建和安装阶段相同，使用适当的构建工具调用测试目标，也可以提供TEST\_COMMAND来替代。

当然，ExternalProject提供的下载支持远不止URL。对于压缩包，它支持主项目给出要下载的文件的哈希值。这不仅有验证下载内容的优势，而且还允许模块检查以前下载过的文件，如果知道已经有一个具有正确哈希值的文件，就可以避免再次重新下载。哈希值可以是file()命令支持的任何算法，MD5或SHA1更为常见。哈希值用URL\_HASH选项给出，如下面的例子所示:

```
ExternalProject_Add(someAutotoolsProj
 URL someUrl
 URL_HASH MD5=b4a78fe5c9f2ef73cd3a6b07e79f2283
 #... other options
)
```

强烈建议指定哈希值。如果使用的URL选项没有附带URL\_HASH选项，CMake将发出警告(作为与旧版本CMake保持向后兼容性的特殊情况，可以使用URL\_MD5选项来提供MD5值，不过项目应该避免使用URL\_MD5，而使用更灵活的URL\_HASH)。

还可以指定多个URL，让项目依次尝试每个URL，直到成功为止。当连接的可用服务器因网络连接、VPN设置等的不同而改变时，或者在可能比较慢的远程服务器之前尝试本地服务器时，这个功能非常有用。该特性不能用于file://url。

```
ExternalProject_Add(someProj
 URL http://mirrors.mycompany.com/releases/someproj-1.2.3.tar.gz
 https://somewhereelse.com/artifacts/someproj-1.2.3.tar.gz
 URL_HASH MD5=b4a78fe5c9f2ef73cd3a6b07e79f2283
 #... other options
)
```

下载打包文件时，根据下载后的文件内容检测打包格式，自动解压文件。如果需要，可以禁用自动解包，并且可以控制下载配置。有关这些不太常见的相关选项，请参阅模块文档。

下载的内容不一定来自打包文件，该模块还可以直接与git、subversion、mercurial或CVS的源代码存储库一起工作。每个选项都要求使用`<REPOTYPE>_REPOSITORY`选项来命名存储库，还可以提供其他特定于存储库的选项。

```
ExternalProject_Add(myProj
 GIT_REPOSITORY git@somecompany.com/git/myproj.git
 GIT_TAG 3a281711d1243351190bdee50a40d81694aa630a
)
```

上面的示例显示了克隆git存储库和切换特定提交所需的信息。如果省略了GIT\_TAG选项，则将使用缺省分支(通常是主分支)上的最新提交。标记或分支的名称也可以通过GIT\_TAG而不是提交散列给定。虽然GIT\_TAG支持这些不同的选择，只有提交的哈希值才是明确的。git中，分支或标记名称引用的提交会随着时间的推移而移动，因此并不能保证可重复构建。类似地，完全省略GIT\_TAG与提供默认分支的名称相同，因此也不会总是指向同一提交。

对于GIT\_TAG只使用提交哈希值还有另一个原因。因为标签或分支名称可能会随时间变化，所以每次运行CMake时，ExternalProject\_Add()都需要与远程端联系，即使已经克隆并切换了标签或分支。因为如果不从远程获取，它不能确保标记或分支没有移动。每次重新运行CMake时，这种往返的代价可能很大，特别是当项目使用许多外部项目时。如果使用了提交哈希值，那么ExternalProject\_Add()可以确定它是否已经在本地完成了提交，而不需要与远程服务器联系。因此，一旦成功获取了提交，CMake的后续运行都不需要网络连接。

可以使用其他选项来定制git行为，包括指定不同的默认远程名称、控制git子模块、浅克隆和任意的git配置选项。请参阅模块文档以了解更多细节。

subversion库类似于git:

```
ExternalProject_Add(myProj
 SVN_REPOSITORY svn+ssh@somecompany.com/svn/myproj/trunk
 SVN_REVISION -r31227
)
```

SVN\_REVISION选项指定一个svn命令行选项，预期该选项将指定需要切换的提交。通常是用-r选项指定修订号，但在技术上可以是任何有效的命令行选项。如果省略SVN\_REVISION，则使用最新的修订，但是项目应该提供此选项，以确保构建可重复。ExternalProject\_Add()还支持一些与安全相关的subversion选项，比如对存储库进行身份验证和指定证书信任设置。请参考ExternalProject模块文档，了解这些这些选项的详细信息。

相比之下，对Mercurial和CVS的支持非常基础。对于Mercurial，只能指定存储库和标记，而对于CVS，还需要指定模块:

```
ExternalProject_Add(myProjHg
 HG_REPOSITORY http://somecompany.com/hg/myproj
 HG_TAG dd2ce38a6b8a
)
ExternalProject_Add(myProjCVS
 CVS_REPOSITORY http://somecompany.com/cvs/myproj
 CVS_MODULE someModule
 CVS_TAG -rsomeTag
)
```

CVS\_TAG选项类似于SVN\_REVISION选项，因为直接放在cvs命令行上的，所以必须包含必需的命令前缀，如上面所示。

### 27.1.2. 步骤管理

有时引用ExternalProject序列中的步骤是有用的，甚至是必要的，比如添加对另一个CMake目标的依赖，该目标为特定的步骤提供了输入。可以将STEP\_TARGETS选项交给ExternalProject\_Add()，为指定的步骤集创建CMake目标。这些目标具有mainName-step形式的名称，其中mainName是作为ExternalProject\_Add()的第一个参数给定的目标名称，step是目标的步骤。例如，以下操作将导致定义myProj-configure和myProj-install目标:

```
ExternalProject_Add(myProj
 GIT_REPOSITORY git@somecompany.com/git/myproj.git
 GIT_TAG 3a281711d1243351190bdee50a40d81694aa630a
 STEP_TARGETS configure install
)
```

这些步骤目标添加依赖关系需要多加小心。要使步骤目标依赖于其他CMake目标，项目应该使用模块提供的ExternalProject\_Add\_StepDependencies()函数，而不是调用add\_dependencies()。该命令的形式如下:

```
ExternalProject_Add_StepDependencies(mainName step otherTarget1...)
```

下面的例子展示了如何使用这个功能，使配置步骤的上一个例子依赖于可执行的主要项目:

```
add_executable(preConfigure ...)
ExternalProject_Add_StepDependencies(myProj configure preConfigure)
```

要使普通的CMake目标依赖于一个step目标，可以使用add\_dependencies():

```
add_executable(postInstall ...)
add_dependencies(postInstall myProj-install)
```

如果外部项目的特定步骤需要依赖于另一个外部项目的步骤，则必须再次使用ExternalProject\_Add\_StepDependencies():

```
ExternalProject_Add(earlier
 STEP_TARGETS build
 ...
)
ExternalProject_Add(later
 STEP_TARGETS build
 ...
)
ExternalProject_Add_StepDependencies(later build earlier-build)
```

如果前面定义的测试运行起来很耗时，上面的安排可能很有用，但是在并行构建中，后面的项目不需要等待这些测试，只需要构建前面的测试。

当需要为多个外部项目定义相同的step目标集，可以通过设置EP\_STEP\_TARGETS目录属性将它们设置为默认值。

```
set_property(DIRECTORY PROPERTY EP_STEP_TARGETS build)
ExternalProject_Add(earlier ...)
ExternalProject_Add(later ...)
ExternalProject_Add_StepDependencies(later build earlier-build)
```

对于许多项目来说，这种依赖的粒度在复杂性过高的项目中可能不值得。通过使用ExternalProject\_Add()的DEPENDS选项，整个外部项目可以依赖于另一个目标，这个操作要简单得多:

```
add_executable(preConfigure ...)
ExternalProject_Add(myProj
 DEPENDS preConfigure
 ...
)
```

DEPENDS选项负责确保所有step依赖项都得到正确处理，就像设置更细粒度依赖项时ExternalProject\_Add\_StepDependencies()所做的那样。

项目并不局限于默认的步骤，可以创建自定义步骤，并插入到工作流中，并根据需要建立依赖关系。ExternalProject\_Add\_Step()函数提供了以下功能:

```
ExternalProject_Add_Step(mainName step
 [COMMAND command [args...] ]
 [COMMENT comment]
 [WORKING_DIRECTORY dir]
 [DEPENDS filesWeDependOn...]
 [DEPENDEES stepsWeDependOn...]
 [DEPENDERS stepsDependOnUs...]
 [BYPRODUCTS byproducts...]
 [ALWAYS bool]
 [EXCLUDE_FROM_MAIN bool]
 [LOG bool]
 [USES_TERMINAL bool]
)
```

COMMAND用于定义执行步骤时要采取的操作。类似于为每个默认步骤指定的自定义命令。可以在执行该步骤时提供注释来提供自定义消息，这样的注释并不总是显示出来，所以可以认为注释是有用的，但不是必需的。WORKING\_DIRECTORY选项与add\_custom\_target()命令具有相同的含义。

可以通过自定义步骤提供全面的依赖信息。如果命令依赖于特定文件或一组文件，则应该使用DEPENDS选项列出。对于构建生成的文件，必须由相同目录范围中创建的自定义命令生成。DEPENDEES和DEPENDERS选项定义了此自定义步骤在外部项目工作流中的位置。必须注意完全指定所有的直接依赖项，否则自定义步骤不会按顺序执行。如果自定义步骤生成了外部项目或主项目中其他项目所依赖的文件，也应该使用BYPRODUCTS选项。如果不这样做，可能会使Ninja生成器缺少构建规则。

将ALWAYS 选项设置为true，可以使自定义步骤显示为过期。项目通常只在没有其他step依赖的情况下才这样做，因为任何依赖都认为是过时的，这可能会导致构建要做更多的工作。如果定制step仅用于按需构建，通常需要将ALWAYS和EXCLUDE\_FROM\_MAIN同时设置为true。选项LOG和USES\_TERMINAL将在下一节中讨论。

所有默认步骤都通过从ExternalProject\_Add()内部调用ExternalProject\_Add\_Step()创建的。项目不能重新定义它们，这意味着定制步骤不能命名为创建文件夹、下载、更新、跳过更新、打补丁、配置、构建、安装或测试。

操作和步骤间的依赖关系由ExternalProject\_Add\_Step()定义，但为自定义步骤创建目标，必须调用ExternalProject\_Add\_StepTargets()函数。ExternalProject\_Add()在内部调用该函数，为STEP\_TARGETS选项中列出的step，或通过EP\_STEP\_TARGETS目录属性设置的step创建目标。

```
ExternalProject_Add_StepTargets(mainName [NO_DEPENDS] steps...)
```

NO\_DEPENDS选项在大多数情况下不推荐使用(请参阅模块文档中对该选项的详细讨论)。下面的示例演示如何定义包的自定义步骤，该步骤依赖于构建步骤，但仅在显式请求时执行。

```
ExternalProject_Add_Step(myProj package
 COMMAND ${CMAKE_COMMAND} --build <BINARY_DIR> --target package
 DEPENDEES build
 ALWAYS YES
 EXCLUDE_FROM_MAIN YES
)
ExternalProject_Add_StepTargets(myProj package)
```

### 27.1.3. 其他特性

对于默认或自定义步骤，可以指定自定义命令。对于ExternalProject\_Add()，相关选项是那些以`_COMMAND`结尾，而对于External\_Project\_Add\_Step()，选项要提供要执行的自定义命令。这两个函数都允许通过在第一个命令后面附加更多命令选项来给出多个命令。然后按照顺序执行每个命令。

```
ExternalProject_Add(myProj
 CONFIGURE_COMMAND ${CMAKE_COMMAND} -E echo "Starting custom configure"
 COMMAND ./configure
 COMMAND ${CMAKE_COMMAND} -E echo "Custom configure completed"
 BUILD_COMMAND ${CMAKE_COMMAND} -E echo "Starting custom build"
 COMMAND ${MAKE_EXECUTABLE} mySpecialTarget
 COMMAND ${CMAKE_COMMAND} -E echo "Custom build completed"
)
ExternalProject_Add_Step(myProj package
 COMMAND ${CMAKE_COMMAND} -E echo "Starting packaging step"
 COMMAND ${CMAKE_COMMAND} --build <BINARY_DIR> --target package
 COMMAND ${CMAKE_COMMAND} -E echo "Packaging completed"
 DEPENDEES build
 ALWAYS YES
 EXCLUDE_FROM_MAIN YES
)
ExternalProject_Add_StepTargets(myProj package)
```

命令的另一个特性是访问终端的能力，对需要用户提供密码来访问存储库访问来说非常重要。虽然这并不适合在没有终端可用的情况下进行持续集成构建，但有时对开发人员很有用。对于默认步骤，通过使用ExternalProject*Add()的\`USES\_TERMINAL*`的选项来控制对终端的访问，其中<STEP>是大写的步骤名，选项给定的值是true或false。对于自定义步骤，ExternalProject_Add_Step()命令的`USES\_TERMINAL\`选项具有相同的效果。如果使用git或subversion存储库下载，建议允许下载和更新步骤访问终端。

```
ExternalProject_Add(myProj
 GIT_REPOSITORY git@somecompany.com/git/myproj.git
 GIT_TAG 3a281711d1243351190bdee50a40d81694aa630a
 USES_TERMINAL_DOWNLOAD YES
 USES_TERMINAL_UPDATE YES
)
```

只有在需要时，才允许访问终端。这样做的效果主要与Ninja生成器相关，其中自定义步骤将放置到控制台任务池中。分配给控制台的所有目标都必须串行运行，并缓冲在其他任务池中并行运行的任务输出，直到当前控制台作业完成为止。除非绝对必要，否则不要让构建步骤访问终端，因为这可能对项目的总体构建性能产生严重的负面影响。

某些情况下，捕获文件各个步骤的输出，而不是将其放到终端(或重定向到的任何位置)会很有用。这在存在大量输出的情况下特别有用，这些输出只有在出现错误或其他意外问题时才有意义。要将步骤的输出重定向到文件，可以将`LOG_<STEP>`选项设置为ExternalProject\_Add()，或将`LOG`选项设置为ExternalProject\_Add\_Step()为true。然后，终端输出将只显示一条短消息，指示该步骤是否成功，以及日志文件可以在哪里找到，这些日志文件将保存在timestamp目录中(即STAMP\_DIR)。

```
ExternalProject_Add(myProj
 GIT_REPOSITORY git@somecompany.com/git/myproj.git
 GIT_TAG 3a281711d1243351190bdee50a40d81694aa630a
 LOG_BUILD YES
 LOG_TEST YES
)
```

某些情况下，项目想知道是否向ExternalProject\_Add()提供了某个特定选项，或者某个特定选项的值是多少。诸如`<SOURCE_DIR>`等占位符涵盖了许多需要在ExternalProject\_Add()中引用的常见场景，但是对于其他情况，模块提供了ExternalProject\_Get\_Property()命令。语法明显不同于其他属性检索命令，如get\_property():

```
ExternalProject_Get_Property(mainName propertyName...)
```

没有给出输出变量名，而是创建一个与要检索的属性名匹配的变量。允许在一个调用中检索多个属性。

```
ExternalProject_Get_Property(myProj SOURCE_DIR LOG_BUILD)
set(msg "myProj source will be in ${SOURCE_DIR}")
if(LOG_BUILD)
 string(APPEND msg " and its build output will be redirected to log files")
endif()
message(STATUS "${msg}")
```

### 27.1.4. 常见问题

ExternalProject模块既强大又有效，但有时也会导致问题难以跟踪。最常见的问题是在尝试设置多个外部项目时，其中一个项目希望使用来自另一个项目的生成输出。通常需要主项目做两件事:

* 指定两个项目之间的依赖关系。
* 向depender项目提供查找到的信息。

通过为依赖项目的配置步骤，并在被依赖项目的主要目标上创建依赖关系，可以很容易完成第一件事。第二件事要求理解依赖项目如何想要知道被依赖项目的位置。例如，如果它使用find\_package()、find\_library()等来定位依赖项目，那么设置`CMAKE_PREFIX_PATH`就足够了。下面的示例演示了这种方式，将zlib和libpng构建为外部项目，并安装到相同的目录中。因为libpng需要zlib，所以将公共安装区域赋给`CMAKE_PREFIX_PATH`就可以找到zlib。当libpng使用CMAKE\_PREFIX\_PATH时，需要保证在libpng运行其配置步骤之前安装zlib。

```
cmake_minimum_required(VERSION 3.0)
project(ExtProjDeps)
include(ExternalProject)

set(installDir ${CMAKE_CURRENT_BINARY_DIR}/install)

ExternalProject_Add(zlib
 INSTALL_DIR ${installDir}
 URL https://zlib.net/zlib-1.2.11.tar.gz
 URL_HASH SHA256=c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1
 CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
)
ExternalProject_Add(libpng
 INSTALL_DIR ${installDir}
 URL ftp://ftp-osl.osuosl.org/pub/libpng/src/libpng16/libpng-1.6.34.tar.gz
 URL_HASH MD5=03fbc5134830240104e96d3cda648e71
 CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
 -DCMAKE_PREFIX_PATH:PATH=<INSTALL_DIR>
)
ExternalProject_Add_StepDependencies(libpng configure zlib)
```

上面的项目除了定义一组外部项目之外，什么也不做的方式，通常被称为超级构建，这个主题将在下一章进一步讨论。

使用Ninja生成器时可能出现的另一个与依赖相关的问题，Ninja不知道如何构建外部项目时，应该提供的特定文件。下面的示例演示了这种情况。

```
ExternalProject_Add(myProj
 # Relevant options to download and build a library "someLib"
 ...
)

ExternalProject_Get_Property(myProj INSTALL_DIR)

add_library(MyProj::someLib STATIC IMPORTED)

set_target_properties(MyProj::someLib PROPERTIES
 # Platform-specific due to hard-coded library location and file name
 IMPORTED_LOCATION ${INSTALL_DIR}/lib/libsomeLib.a
)

add_dependencies(MyProj::someLib myProj)
```

Ninja生成器会尝试查找libsomeLib。在第一次构建myProj外部项目之前，它还不存在。Ninja会暂停，并出现错误，说明它不知道如何构建缺失的依赖项。其他生成器可能轻松地进行依赖项检查而不抱怨，但不应将其视为对正确指定依赖项。解决上述问题的一种方法是在ExternalProject\_Add()中添加BUILD\_BYPRODUCTS选项，以指定构建输出(在CMake 3.2或更高版本中可用)。Ninja将得到所有的信息，以及需要满足的依赖关系。

```
ExternalProject_Add(myProj
 BUILD_BYPRODUCTS <INSTALL_DIR>/lib/libsomeLib.a
 # Relevant options to download and build the above library
 ...
)
```

上述情况就是当外部项目与主项目中的目标混合在一起时，出现题的一个例子。通常需要手工指定CMake通常代表项目处理的平台特定信息(例如库名和位置)。使用ExternalProject时，项目应该考虑超级构建是否更合适，而不是尝试创建自己的构建目标。

其他情况下也会出现依赖问题。请考虑前面的示例，其中ExternalProject用于使用与主构建不同的工具链来进行的构建。

```
ExternalProject_Add(firmware
 SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/firmware
 INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}/firmware-artifacts
 CMAKE_ARGS -D CMAKE_TOOLCHAIN_FILE=${CMAKE_CURRENT_LIST_DIR}/fwtoolchain.cmake
 -D CMAKE_BUILD_TYPE=Release
 -D CMAKE_INSTALL_PREFIX=<INSTALL_DIR>
)
```

上面的代码将成功构建，并且看起来一切正常。如果开发人员随后对源目录中的源代码进行了更改，主项目将不会重新构建目标。这是因为ExternalProject使用时间戳来记录这些步骤的成功完成，所以除非计算依赖关系的方式发生了变化，否则主项目认为项目仍然是最新的。这可以通过使用`BUILD_ALWAYS`选项强制构建目标进行构建:

```
ExternalProject_Add(firmware
 SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/firmware
 INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}/firmware-artifacts
 CMAKE_ARGS -D CMAKE_TOOLCHAIN_FILE=${CMAKE_CURRENT_LIST_DIR}/fwtoolchain.cmake
 -D CMAKE_BUILD_TYPE=Release
 -D CMAKE_INSTALL_PREFIX=<INSTALL_DIR>
 BUILD_ALWAYS YES
)
```

这将导致在每次构建主项目时，需要调用项目的构建工具。如果项目中没有任何更改，则构建步骤将不起作用，但如果有更改，则将按照预期重新构建。

## 27.2. FetchContent

ExternalProject的一些优点，也是它的弱点。它允许外部项目构建与主项目完全隔离，因此可以使用不同的工具链、针对不同的平台、使用不同的构建类型，甚至是完全不同的构建系统。这些收益的代价是，项目对外部项目产生的产品一无所知。如果主构建中的任何内容需要引用外部项目的输出，则必须手动向主构建提供该信息。这是使用CMake项目要做的事情，因此以这种方式使用ExternalProject可能不太合适。

对于使用CMake作为构建系统的外部项目，使用不同设置来构建主项目通常是不必要的。事实上，更常见的情况可能是外部项目使用与主项目相同的设置来构建，但是使用ExternalProject并不那么容易做到的。更方便的方法是使用add\_subdirectory()将其添加到主构建中，就好像是主项目自己源代码的一部分。使用ExternalProject无法做到这一点，因为源代码直到构建时才进行下载。项目可以使用其他策略(如git子模块)来避免这一问题，但也有自己的缺点。

CMake 3.11中添加了FetchContent模块来解决上述问题。在内部使用ExternalProject来设置子构建，子构建在配置阶段将下载和更新外部内容。这意味着下载的内容立即可用，因此主项目可以通过add\_subdirectory()将其导入主构建，并将其作为资源使用。

依赖于许多外部项目的项目中，这些外部项目有时会共享一些依赖关系。多次下载和构建这些公共依赖关系是不可取的，但是ExternalProject本身并没有直接提供处理这种情况的方法。FetchContent模块也为这个场景提供了解决方案，允许将外部项目的依赖项细节与初始化下载命令分开定义。第一次为给定的依赖项指定下载信息时，会在内部保存，之后定义它们的任何尝试都会忽略。当项目要求填充依赖项时，项目会使用保存的信息，并且项目的任何部分可以简单地重用这些内容，而不是再次下载。这种“首选设置胜出”的方法意味着父项目可以覆盖通过add\_subdirectory()拉入的外部子项目的依赖信息。

下面的示例演示了FetchContent模块的使用规范:

```
include(FetchContent)
FetchContent_Declare(googletest ①
 GIT_REPOSITORY https://github.com/google/googletest.git
 GIT_TAG ec44c6c1675c25b9827aacd08c02433cccde7780 # release-1.8.0
)

FetchContent_GetProperties(googletest) ②
if(NOT googletest_POPULATED)
 FetchContent_Populate(googletest)
 add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR}) ③
endif()
```

① 记录从GoogleTest获得的信息。如果项目中的其他地方已经这样做了，这里声明的信息将会忽略。

② 填充GoogleTest的内容，只有在项目的其他部分还没有这样做的情况下。

③ 始终为add\_subdirectory()提供`xxx_SOURCE_DIR`和`xxx_BINARY_DIR`。当`xxx_SOURCE_DIR`不指向在当前二进制目录位置时(通常会出现这种情况)，add\_subdirectory()会要求给出相关的二进制目录。

FetchContent\_Declare()命令的第一个参数是依赖项的名称(这个名称大小写不敏感)。名称后面的参数是ExternalProject\_Add()所支持的任何选项，除了与配置、构建、安装或测试步骤相关的选项。实际上，通常给出的选项只有定义下载方法的选项，比如上面GoogleTest示例中的git细节。

FetchContent\_GetProperties()命令允许项目检查是否已经填充了依赖项，还可以检索一些目录信息。该命令的详情如下:

```
FetchContent_GetProperties(name
 [SOURCE_DIR srcDirVar]
 [BINARY_DIR binDirVar]
 [POPULATED doneVar]
)
```

可以使用`SOURCE_DIR`、`BINARY_DIR`和填充选项指定变量的名称，其中属性与存储名称依赖项的关联。如果这些选项都没有给出，那命令将在调用作用域中设置变量`<lcname>_SOURCE_DIR`、`<lcname>_BINARY_DIR`和`<lcname>_POPULATED`，其中`<lcname>`是转换为小写的名称。如果遵循规范模式，则不需要可选参数。

如果FetchContent\_Populate()已经在项目的某个地方以指定的名称调用了，那么填充的属性将为true。如果为true，则`SOURCE_DIR`属性指定下载内容的位置。由于下载内容可能不是FetchContent\_Populate()调用位置的直接子目录，因此对于add\_subdirectory()的调用，需要使用`BINARY_DIR`属性。

如果FetchContent\_GetProperties()确认指定的内容尚未填充，则调用FetchContent\_Populate()来填充内容。当使用上面所示的规范形式作为项目的一部分时，将只接受一个参数，即要填充的依赖项名称。使用前面声明的详细信息，如果之前的cmake运行还没有填充内容，则填充内容。`<lcname>_POPULATED`， `<lcname>_SOURCE_DIR`和`<lcname>_BINARY_DIR`变量也将在调用范围内设置，与调用FetchContent\_GetProperties(name)的方式完全相同。

下面的示例展示了FetchContent模块允许顶层项目覆盖较低级别依赖项，并设置信息的方式。考虑一个顶层项目TopProj，依赖于外部项目Foo和Bar。Foo和Bar都依赖于另一个外部项目Jerry，但是他们都想要的版本不同。

![](https://211235224-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MJHzSm7gYyKXsrbfeFZ%2Fsync%2Fcd77983a8fd7c20aba3a903d87838246e03ea86e.png?generation=1604235293827392\&alt=media)

只需要下载并构建一个Jerry的副本，Foo和Bar就可以使用这个副本。当这些项目被合并到一个构建中，所选的Jerry版本必须覆盖Foo或Bar通常使用的版本，或者可能两者都使用。顶级项目负责确保选择了一个有效的版本，这样Foo和Bar就可以根据它进行构建。这个例子假设Foo在自己构建时使用1.3版本，可以安全地使用后续版本。实现的示例如下:

![](https://211235224-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MJHzSm7gYyKXsrbfeFZ%2Fsync%2F45d617cf8e1e55ac85d7782ed20c2ea7d3ef8249.png?generation=1604235292534025\&alt=media)

*TopProj CMakeLists.txt*

```
# Declare the direct dependencies
include(FetchContent)
FetchContent_Declare(foo GIT_REPOSITORY ... GIT_TAG ...)
FetchContent_Declare(bar GIT_REPOSITORY ... GIT_TAG ...)

# Override the Jerry dependency to ensure we get what we want
FetchContent_Declare(jerry
 URL https://somecompany.com/releases/jerry-1.5.tar.gz
 URL_HASH ...
)

# Populate the direct dependencies but leave Jerry to be populated by foo
FetchContent_GetProperties(foo)
if(NOT foo_POPULATED)
 FetchContent_Populate(foo)
 add_subdirectory(${foo_SOURCE_DIR} ${foo_BINARY_DIR})
endif()
FetchContent_GetProperties(bar)
if(NOT bar_POPULATED)
 FetchContent_Populate(bar)
 add_subdirectory(${bar_SOURCE_DIR} ${bar_BINARY_DIR})
endif()
```

*Foo CMakeLists.txt*

```
include(FetchContent)
FetchContent_Declare(jerry
 URL https://somecompany.com/releases/jerry-1.3.tar.gz
 URL_HASH ...
)
FetchContent_GetProperties(jerry)
if(NOT jerry_POPULATED)
 FetchContent_Populate(jerry)
 add_subdirectory(${jerry_SOURCE_DIR} ${jerry_BINARY_DIR})
endif()
```

Bar的CMakeLists.txt文件与Foo的相同，只是URL指定的是jerry-1.5.tar.gz，而不是jerry-1.3.tar.gz。上面的框架示例允许Foo和Bar独立进行项目构建，或者可以合并到另一个项目中，比如TopProj。

### 27.2.1. 开发人员须知

有时，开发人员可能想要同时处理多个项目，在主项目及其依赖项中或者跨多个依赖项进行更改。当更改外部项目的某些部分时，开发人员希望使用本地副本，而不是每次都更新下载。FetchContent模块允许使用CMake缓存变量覆盖任何外部依赖项的源目录，为这种操作模式提供了直接支持。这些变量的名称形式为`FETCHCONTENT_SOURCE_DIR_<DEPNAME>`，其中是依赖项的大写名称。

前面的示例中，可以考虑这样一种情况：开发人员想要对Foo进行更改，看看如何影响主项目的。可以在mainproject之外创建一个单独的Foo克隆，然后将相应的位置设置到FETCHCONTENT\_SOURCE\_DIR\_FOO中。TopProj项目将使用该本地副本的源文件，并且不会以任何方式修改它，但是它仍然会在TopProj构建区域中为它使用相同的构建目录。唯一的区别是源来自哪里，通过设置FETCHCONTENT\_SOURCE\_DIR\_FOO，开发人员将接管对内容的控制。可以自由地更改本地副本中的任何内容、进行进一步的提交、切换分支或其他任何可能需要的内容，然后重新构建主TopProj项目，而完全不需要更改TopProj。

适合上述用法的一种方式是设置共目录，开发人员可以在该目录下切换想要使用的不同项目。需要时，可以将主项目指向这些本地签出，但是仍然使用默认的下载内容。上面的例子中，这样的方式看起来是这样的:

![](https://211235224-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MJHzSm7gYyKXsrbfeFZ%2Fsync%2Feabf373f627a625dcbc5591f6113ba293ac89563.png?generation=1604235295627841\&alt=media)

如果开发人员想对Foo做一些更改，使用TopProj的构建来测试，可以将FETCHCONTENT\_SOURCE\_DIR\_FOO设置为`/…/Projects/Foo`，但是Foo依赖项的所有构建输出仍然在`Projects/build /TopProj-debug`下。如果FETCHCONTENT\_SOURCE\_DIR\_BAR未设置，Bar仍然会下载，而不是使用项目`/Bar`中的已下载代码。开发人员可以随时通过设置FETCHCONTENT\_SOURCE\_DIR\_BAR切换到本地内容。因为相关的缓存变量都共享相同的前缀，所以很容易在CMake GUI或ccmake工具中找到。这使得查看哪些项目当前正在使用本地副本，而不是默认下载的内容变得非常简单。

![](https://211235224-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MJHzSm7gYyKXsrbfeFZ%2Fsync%2Fef7342ad0571b95540a60fdce964bb177a705dd9.png?generation=1604235296859467\&alt=media)

上述场景的显著优势是，特性很好地与IDE集成，如代码重构工具等。IDE可以看到整个项目，包括依赖项，因此使用这些依赖项的本地切换时，可以透明地跨多个项目执行重构，就像是同一个项目那样容易。即使不使用任何本地切花依赖关系，IDE也有更大的机会来自动完成构建更完整的代码模型，跟随符号等等功能。

### 27.2.2. FetchContent的其他用途

FetchContent支持的不仅仅是下载外部项目的源代码，并通过add\_subdirectory()将其添加到主项目中。另一中用法是将CMake模块收集到中央存储库中，并在许多项目中重用它们。通过这种机制可以引入多个集合，这使得其他项目合并有用的CMake脚本会相对简单，而无需在主项目源中嵌入副本。下面的示例演示了下载外部git存储库，并将其cmake子目录添加到主项目的cmake模块搜索路径的示例。

```
include(FetchContent)
FetchContent_Declare(JoeSmithUtils GIT_REPOSITORY ... GIT_TAG ...)
FetchContent_GetProperties(JoeSmithUtils)
if(NOT joesmithutils_POPULATED)
 FetchContent_Populate(JoeSmithUtils)
 list(APPEND CMAKE_MODULE_PATH ${joesmithutils_SOURCE_DIR}/cmake)
endif()
```

FetchContent模块可以在第一个project()调用之前使用。该特性允许模块提供工具链文件，开发人员可以在主项目中使用这些文件。

```
cmake_minimum_required(VERSION 3.11)

include(FetchContent)
FetchContent_Declare(CompanyXToolchains
 GIT_REPOSITORY ...
 GIT_TAG ...
 SOURCE_DIR ${CMAKE_BINARY_DIR}/toolchains
)

FetchContent_GetProperties(CompanyXToolchains)
if(NOT companyxtoolchains_POPULATED)
 FetchContent_Populate(CompanyXToolchains)
endif()

project(MyProj)
```

```
cmake -DCMAKE_TOOLCHAIN_FILE=toolchains/toolchain_betacxx.cmake ...
```

上面的示例中，使用`SOURCE_DIR`选项显式覆盖工具链的下载目录。假设CompanyXToolchains项目是没有子目录工具链文件的简单集合，则它们的位置可以预测，并且易于开发人员使用。如果使用非常特定的工具链，并且希望这些工具链安装在相同的位置，这是一种非常有效的方法，可以帮助整个团队使用通用的构建设置。这项技术甚至可以扩展到下载实际的工具链。

### 27.2.3. 限制

大多数情况下，FetchContent模块具有一些强大的优势，但也有一些需要注意的限制。主要的限制是CMake目标名称必须在合并项目的集合中是唯一的，所以如果两个外部项目定义了相同名称的目标，则不能通过add\_subdirectory()同时添加。如果项目遵循的命名约定，使用了特定于项目的前缀或类似的东西，这个限制很容易规避。困难往往来自于从未期望以这种方式使用的项目，以及使用普通名称的项目。对于那些使用特定于项目目标名称的项目，可以使用`OUTPUT_NAME`目标属性控制所创建的二进制文件的名称。例如:

```
add_library(BagOfBeans_varieties ...)
set_target_properties(BagOfBeans_varieties PROPERTIES
 OUTPUT_NAME beantypes
)

add_executable(BagOfBeans_planter )
set_target_properties(BagOfBeans_planter PROPERTIES
 OUTPUT_NAME planter
)
```

`OUTPUT_NAME`属性和其他相关属性将在28.5.2节中更详细地介绍。

安装组件也有类似，但不那么严格的限制。每个项目使用特定于项目的前缀或相同的惟一名称来命名它们的安装组件，这允许父项目挑选出想要包含的组件。如果两个或多个外部项目依赖项使用相同的安装组件名称，父项目则不能区分。这是否重要取决于具体情况，但是通过确保项目对其安装组件使用良好的命名约定，可以很容易地避免该情况的发生。

通过add\_subdirectory()将外部依赖项吸收到更大的父构建中的做法还不是很普遍。许多项目从来没有考虑过用例，并且遇到使项目难以合并的也很常见。一个常见的例子是，一个项目假设是顶层项目，使用像`CMAKE_SOURCE_DIR`和`CMAKE_BINARY_DIR`这样的变量，而像`CMAKE_CURRENT_SOURCE_DIR`和`CMAKE_CURRENT_BINARY_DIR`这样的替代方案可能更合适。这样问题通常很容易修复，但需要对项目进行写访问，让项目维护人员接受变更，或者保留项目副本，在副本中进行相关修复或实施类似的措施。

## 27.3. ExternalData

另一个名为ExternalData的模块提供了一种在构建时下载的文件的方法。此模块的重点是在构建特定目标时下载测试数据。类似于ExternalProject的工作方式，但两个模块定义要下载内容的方式有很大不同。ExternalProject模块允许显式定义下载信息，并支持多种方法。ExternalData模块采用一种不同的方法，单个文件在一组项目定义的基本URL位置下可用，路径和文件名使用特定的哈希算法编码。实际的文件在项目的源树中是由同名的占位符文件表示，除非附加了一个哈希算法的名称作为文件名后缀。该模块提供了一个函数，用于将特殊的字符串参数转换为最终的下载位置和名称，以及一个add\_test()函数的包装器，以便更容易地将这些解析后的位置传递给测试命令。

实践中，为ExternalData设置必要的支持所涉及的步骤往往会降低其吸引力。要从其中下载数据的服务器必须使用定义结构并分别处理每个文件，每次添加新文件或更新现有文件时，都必须手动对其进行哈希计算，并将其上传到与该哈希值匹配的路径和文件名中。如果文件很大，但是与前一次迭代只有很小的区别，仍需要完全复制该文件。相比之下，ExternalProject模块可以通过基于存储库的下载方法实现相同的功能，但是所涉及的步骤对于大多数开发人员来说比较简单。选择适当的存储库方法还可以有效地处理大文件中的小更改。

考虑使用ExternalData的原因是它支持一系列文件，而不仅仅是单个文件。这更像是处理一系列文件的测试会出现的场景。即使这样，也可以通过ExternalProject和foreach()循环实现类似的功能，这可能比ExternalData更容易设置。如果项目的测试主要关注时间序列数据或其他类似的顺序数据集，至少评估一下ExternalData是否有在构建时按需获取数据的更好方法。请参考模块文档以获得更多信息，或获得更实用的介绍，与本书相同的站点上提供的关于这个主题的[文章](https://crascit.com/2015/04/03/handling-binary-assets-and-test-data-with-cmake/)，可能会对使用者有帮助。

## 27.4. 总结

ExternalProject和FetchContent提供了将外部内容合并到父项目中的方法。ExternalProject很适合引入成熟的外部项目，这些项目具有良好的打包功能，并且提供了定义良好的配置文件，find\_package()可以使用这些配置文件导入相关的目标。它还有一个优点，即只有在构建需要(外部依赖项)时才下载它们，而且下载可以与其他构建任务并行完成。当开发人员需要跨多个项目进行工作并进行更改时，就不太方便了，特别是涉及到少量的重构。由于ExternalProject是CMake的一部分，网上也有现成的资料，但尽管如此，经常会看到开发人员为如何使用它进行挣扎。常见的缺点是在特定平台中硬编码库的路径和文件名，由于在主项目中混合了ExternalProject和其他目标，而不是传统的超级构建。选择使用外部项目之前，仔细考虑外部依赖打包的成熟度和质量，以及主项目是否可以使用超级构建。如果主要项目不能转化为超级构建，就不要使用它。

FetchContent模块是一个很好的选择，将其他项目添加到构建中，允许同时处理它们。为开发人员提供了跨项目工作的自由，可以临时切换到本地修改、更改分支、使用不同的发布版本和各种其他用例进行无缝测试。对IDE工具也很友好，因为整个构建看起来就像一个单独的项目，所以像代码完成之类的东西通常提供了更好的洞察力，可能比单独加载项目更可靠。如果将依赖关系添加到已经成熟项目中，FetchContent的破坏性要比ExternalProject小得多，因为它不需要对主项目进行任何重组。还非常适合于合并那些相对不成熟的、还没有实现安装组件和打包的外部项目。FetchContent的另一个优点是，会在整个项目层次结构中使用相同的编译器和设置。如果可以接受3.11或更高的CMake为最低版本，请考虑FetchContent是否比ExternalProject更方便、更自然地适合这个项目。强烈建议开发者熟悉ccache之类的工具，以加快构建速度，这样使用FetchContent的好处更加明显

无论使用ExternalProject还是FetchContent，如果下载信息是为git存储库定义，最好将GIT\_TAG设置为提交哈希，而不是分支或标记名称。这样做效率更高，因为如果本地克隆已经有了提交，可以避免建立任何网络连接。

如果项目想按需下载测试数据，请检查ExternalData模块是否是合适的选择。ExternalProject模块可能使用起来更简单，也更容易被大多数开发人员理解，但是在特定的情况下，比如处理文件序列，ExternalData可能会更简单。如果有疑问，请选择ExternalProject，因为它的接口更简单，并且可以更有效地处理对大数据集的小更改。

当处理项目时，总是假设有一天它当做子项目包含。这为将来如何使用项目提供了最大的灵活性。常见的问题包括:

* 不要假设这个项目是最高级别的项目。使用像`CMAKE_CURRENT_SOURCE_DIR`和`CMAKE_CURRENT_BINARY_DIR`这样的变量来引用相对于项目自身目录结构的位置，而不是`CMAKE_SOURCE_DIR`和`CMAKE_BINARY_DIR`。
* 使用特定于项目的目标名称。避免使用通用名称，即使是内部实用目标，因为CMake需要在整个项目层次结构中对所有非全局导入目标使用全局唯一的目标名称。
* 类似地，使用特定于项目的安装组件名称并避免通用名称。
* 最好在项目定义的安装组件集中提供一些粒度，以便父项目可以选择想要安装的部件。考虑整个或部分部署项目的不同方式，并确保安装组件允许捕获安装内容的不同组合。
* 如果别名目标可用，那么总是使用一个目标的名称空间别名链接(例如，更倾向链接到`MyProj::mpfoo`而不是只链接mpfoo)。这允许项目在ExternalProject和FetchContent场景中使用。
* 避免强制设置缓存变量。不是使用常规的CMake变量来覆盖当前作用域，及其作用域下的任何缓存变量，替代方案是使用目标或目录属性。
