CMake Superbuild 和 Git 子模块

版权声明:本文翻译自 CMake Superbuilds and Git Submodules,作者 Marcus D. Hanwell

简介

很久以前,在我加入 Kitware 后不久,我有点震惊,因为我们还在使用 CVS 来管理代码,并且没有公司博客。我被要求参与一个名为 Titan 的项目(据我所知,它不再是一个活跃的开源项目)。作为该项目的一部分,我们将 VTK 从 CVS 转换到了 Git(2010 年),并且与社区合作更新开发实践。完成这些工作后,Titan 存储库成为了一个包含许多 git 子模块的 git 存储库。

当时的计划是尽可能轻松地为新开发人员构建一个具有许多依赖项的项目。Titan 主要代码库建立在 VTK 之上,也大量使用了 Qt、[Boost](http:// /www.boost.org/),以及大量需要在 Windows、macOS 和 Linux 上测试和工作的其他依赖项。当时我们决定使用 CMake 中的 ExternalProject 功能来协调构建过程,这是我们许多人所说的超级构建(superbuild)的核心。我不记得我们是否有专门的外部存储库或带有子模块的混合代码。

多年来,我一直想写一些这样的东西,有位同事在最近的一次研讨会上鼓励我这样做,所以开始吧。让我给你们将更多一些细节。

ExternalProject 特性

ExternalProject: Never Cross the Streams

如果你从《Ghostbusters》中学到任何东西(至少在原版中),那么你永远不会越过溪流(streams)。在我使用 ExternalProject 的早期,我很想将 ExternalProject 目标构建依赖项与普通目标构建库和/或可执行文件混合在一起。虽然在某些时候我们可以这样让它工作起来,但我们还是决定避免这种做法,保持了外部协调构建(outer coordinating build)和内部项目(inner projects)的明确分离,内部项目按照它们的依赖关系指定的顺序构建。

对于任何 Superbuild 方法,你应该牢记的一个重要概念是 —— 你将有一个外部构建,并且这个构建应该只关注于构建其他项目。到目前为止,构建 CMake 项目是最简单的,同时也可以驱动其他构建工具。这里的主要挑战是将 CMake 的所有内容映射到外部构建工具,以便获得一致的结果。为此,我们采取了在构建树中镜像源码树布局的方式,这样当源码树顶层有 VTK 目录时,构建树顶层就有 VTK 构建目录。

为什么要用 Superbuild

你可能会问自己为什么我们需要使用 Superbuild 的方式,它们听起来更复杂,为什么不直接使用包管理器?很久以前,我是一个 Gentoo 包维护者,从事科学包和移植到 64 位处理器(当时是新的,我想我已经老了)。

我们讨厌的一件事是将第三方库打包到其源代码树中以“使事情变得更容易”的项目。这是一种流行的做法,例如,VTK 有很多第三方库,我们已经做了很多工作来轻松切换到那里的系统库。执行此操作时,你必须将这些包转换为构建系统的一部分,并定期更新它们。包维护者讨厌这一点,他们花费大量时间让所有东西都使用相同的版本,或者当他们没有维护稳定的 API 时将多个版本插入。

Superbuilds 可以从项目的源代码库中删除所有这些垃圾,并使你能够更直接地使用上游项目的构建系统作为独立构建的软件组件。它基本上是一个乞丐版的包管理器,你可以在你的目标构建平台上始终如一地工作,如果你的目标平台有一个通用的包管理器,你可以考虑使用它。

我认为它们实现了两全其美,一个项目依赖并重用了许多有意义的库,但开发人员基本上可以克隆项目并通过一两个步骤构建它。当然,知道自己在做什么的人完全能够忽略 Superbuild —— 经验丰富的开发人员、打包人员等。而大多数开发人员应该能够使用 Superbuild 来设置他们的环境。

Superbuild 的类型

我想说至少有三种方法可以创建一个超级构建,有很多混合体,可能有些我还没有遇到过。我将尝试总结我在这里知道的那些,以及为什么你可能会考虑使用每种类型。我有自己的喜好,我会尽我所能客观地概述每一种的优缺点。与许多事情一样,可能没有一种真正的方法,而是一组对你的项目有意义的妥协。

开发人员构建:这里的主要重点是帮助开发人员启动和运行,并使用源代码树进行开发。在这里,我强烈建议将 Git 与子模块或等效项一起用于所有可能经常更改的项目。这种类型的布局使用版本控制系统来控制版本,使用构建系统(CMake)来协调这些子模块的构建,使用来自外部项目的指令,并下载未积极开发(not actively developed)的源文件的 tarball。

Titan 构建系统就是一个很好的例子,Open Chemistry supermodule 就是一个很好的例子。它在顶层具有化学项目的子模块,以及第三方目录中更改更频繁的一些内容。 它还使用“cmake/projects.cmake”来下载一些源代码压缩包,用于诸如 Eigen、GLEW 等移动频率较低/倾向于使用这些项目的已发布版本的东西。“cmake/External_*.cmake”文件包含构建逻辑和依赖信息。

这里的一个特点是所有可能被编辑的源目录都是永久的,并且在构建树之外。如果你更改这些,你可以依靠构建系统而不是覆盖/更改它们,并且你可以安全地在这些项目中开发分支。合并更改后,你可以将子模块 SHA 向前移动以供外部构建查看更改,主要使用版本控制来管理这些更新。这个特点同样适用于软件打包,这一直是我开发这种超级构建风格的强大动力。

打包构建:这里的主要重点是打包二进制文件/测试仪表板提交。存储库通常更简单,大多数布局逻辑都在 CMake 构建系统中。在这种情况下,下载 tarball 和源代码树由 CMake 负责,并且几乎所有源代码(在 Superbuild 存储库之外)都包含在构建树中。这通常可以确保构建树是干净的,但意味着很难使用它来主动开发代码,因此它往往是对其他一些开发人员构建指令的补充。

Tomviz superbuild 就是一个很好的例子,它源自 ParaView superbuild 的早期版本。在 Tomviz 的场景中,你通常需要将项目的 SHA 从本地测试的源代码树复制到“versions.cmake”(以及上面引用的发布 tarball),一旦推送,这些将由构建者构建。在这两种情况下,超级构建实际上包含 CPack 打包代码,而在 Open Chemistry 的场景中,各个软件存储库包含打包代码。这些包含构建按需创建或作为发布的一部分提供的安装程序的所有说明。

依赖构建:我最近看到的第三种超级构建就是我所说的依赖构建。这通常遵循打包构建的模式,并且通常也是具有第二种模式的打包构建,它构建除了超级构建所针对的项目之外的所有内容。所以在 CMB 的 superbuild 中有一个开发者模式的概念,它可以构建除实际项目之外的所有内容。然后它可能会编写一些配置文件或类似文件来帮助开发人员的构建找到为他们构建的依赖项。

通用前缀

大多数 Superbuild 项目使用一个通用的安装前缀或一组前缀来安装构建工件。在 Titan 中,我认为我们从每个外部项目一个前缀开始,但后来移动到所有项目的通用前缀(common prefix)。在 Open Chemistry 中,我们为所有项目使用一个通用前缀,在构建树的顶层命名为“prefix”。单个公共前缀可能非常有用,因为你可以简单地添加 CMAKE_PREFIX_PATH 来引用该前缀,并让项目支持其中找到的任何内容,该路径也可以填充为一个列表。

这种方法的主要缺点是前缀可能会随着时间的推移而变脏,安装了多个版本,并且过时的文件会导致问题。这也是构建树通常会出现的问题,从干净的构建目录开始通常是避免这种情况的最佳解决方案。这也意味着你无法分离构建和安装的不同依赖项,但通常开发 Superbuild 以支持一个(或少数)项目。

构建一切?

当我们进入开发 Superbuild 的心态时,出现的一个问题是我们是否应该从源头构建所有东西。从概念上讲,这是最好/最简单的方法,但它也会导致尽可能长的构建时间。在为 Titan、后来的 Open Chemistry 和 Tomviz 考虑了很多时间之后,我得出的结论是,这取决于……

有些依赖项非常小,而且构建起来很可靠,几乎可以肯定你应该只构建它。其中一些更大,如果不经常更改,你几乎肯定应该尝试仅在更新时构建它们。其他人坐在中间的某个地方,你会发现现实世界比我们想象的要模糊得多。理想情况下,外部项目代码将是健壮的,并且可以在所有平台上可靠地生成二进制文件。

随着我们转向使用越来越多的持续集成,我认为我们需要考虑如何自动保存/上传二进制工件以加速大型项目/超级构建的构建过程。主要的 Tomviz 超级构建使用 Qt 的系统版本和预编译的 ITK,因为它们都需要很长时间才能构建并且更新不那么频繁。ParaView/VTK 也需要很长时间才能构建,它们更新更频繁。他们将受益于更自动化的构建/缓存过程,然后我们可以将其用于 ITK 和其他人。

这也让我想起了我作为 Gentoo Linux 开发人员的日子,我们让从源代码在本地构建所有东西变得容易(或者如果你确定的话就足够简单了),但推动了可选地提供二进制文件。我迁移到 Arch Linux 的部分原因是在滚动发布发行版中易于使用二进制文件,该发行版始终是最新的。 SDK 安装程序的可用性也有很大帮助,因为它们可以放置在 CMake 构建的路径中。

总结

超级构建对于现代项目非常有用。在高层次上,它们使目标项目能够避免在其源代码树中复制第三方代码。这通常会导致一个更干净的项目,它专注于开发该项目,以及一个在我们需要构建和/或打包项目时协调依赖项构建的超级构建。我们在使用这些帮助开发人员快速启动和运行以及打包具有 Windows、macOS 和 Linux 的许多依赖项的复杂项目方面取得了很大的成功。

理想情况下,大多数依赖构建将由跨平台包管理器替换,但到目前为止还没有创建任何合适的。我从事的大多数项目都希望使用每个平台的本机编译器在 Windows、macOS 和 Linux 上构建/打包 —— 这意味着 MSVC、Clang、GCC 等。我已经指出了我使用过的两种高级风格的 Superbuild,在开发人员和以打包为重点的 Superbuild 中,使用以打包为重点的构建跳过构建实际目标项目的第三个变体。

在 Linux 和 macOS 上,我通常可以使用包管理器来处理大部分依赖项,并使用灵活的 Superbuild 来填充不太常用的打包项目。这就是即使对于超级构建也使用系统标志非常有用的地方,并且可以在引导开发环境时减少构建时间。