跳到主要内容

JEP 138:基于 Autoconf 的构建系统

QWen Max 中英对照

概述

引入 autoconf(./configure 样式)构建设置,重构 Makefile 以消除递归,并利用 JEP 139:增强 javac 以提高构建速度

目标

我们试图实现的最高目标是:

  1. 极大地提高构建速度
  2. 简化构建系统的源代码(Makefiles 等)
  3. 简化开发者的工作
  4. 获取精确且可重现的构建输出
  5. 简化构建机器的配置(JPRT 等)

我们将通过四个子项目来实现这些目标,这些子项目或多或少紧密交织在一起。

  1. 更新 Makefile 结构
  2. 使用 autoconf(配置脚本)
  3. 添加并行 Java 编译支持
  4. 使 Java 构建增量进行

我们需要充分理解现有的开发者工作流程,从而尽量降低这一变更对所有人的影响。

这个项目是一个改善 JDK 构建基础设施的更大计划的一部分。我们预计该项目将会与未来的步骤紧密相连。这些步骤之间的区别有些随意,只是为了尽快从改善 JDK 构建基础设施的初期工作中受益而做出的区分。

非目标

由于我们将使用新的结构更新 Makefile,未来我们想要解决的几个问题可能会随着更新而自动得到解决。然而,在本项目中我们并未专门针对这些问题进行处理,也不会测试它们,更不会保证它们能够正常工作。(不过,我们会尽量确保不会破坏任何当前正常运作的功能。)这些问题包括:

  • 使其易于移植到新平台
  • 使得在没有网络连接的情况下进行 JDK 开发成为可能
  • 提供对交叉编译的适当支持,包括在 64 位主机上编译 32 位二进制文件
  • 改进警告处理方式

我们也不会解决计划在将来步骤中处理的问题。(然而,这项工作中的一部分将为这些未来的改进奠定基础。)这些问题包括:

  • 加速 Hotspot 编译
  • 升级编译器
  • 支持 IDE 项目
  • 重新考虑源码投放机制

成功指标

构建简洁性

鉴于所有先决条件都已具备,构建工作应通过以下方式完成:

  1. 从 Mercurial 仓库获取源代码
  2. ./configure
  3. make

构建速度

构建速度取决于硬件因素,改进的效果也会有所不同。我们的目标是在一台 8 核机器上编译 Linux 系统。在这种情况下,经过我们的改进后,构建 JDK 所花费的时间应最多为当前时间的 33%。(通常这意味着从约 15 分钟减少到约 5 分钟或更短)。一个更具挑战性的目标是,对于 JDK 的构建时间应最多为 20%(约 3 分钟)。

请注意,这只是针对 JDK 的。它不包括构建 Hotspot,也不包括创建 Javadoc。

Makefile 清理

JDK(不包括 Hotspot)中所有小于 3 kB 的递归 Makefile 都应该移除,并且其功能应集中到中央 Makefile 中。(少量的 Makefile 本身并不是目标,然而,将代码集中在一个(或少数几个)地方有助于整体概览和理解。)

动机

构建完整的 JDK 实在是太慢了,这给开发者和构建系统带来了额外的负担。结果就是,由于整个产品的构建时间过长,开发者通常只会检出并构建部分源代码。

当前构建系统的实现方式是在整个产品中分布着 350 多个极简的递归 Makefile,这使得对构建系统进行更改变得困难。当前的解决方案有时还要求仅为了添加新的源文件或目录而更新 Makefile;实际上不应该需要这样做。

如今,构建系统是通过使用多个环境变量来配置的。这与使用 ./configure 来设置构建系统的流行方法形成了对比。除了熟悉程度之外,相较于环境变量,这种方法还有几个优点。传递给 configure 的参数会被检查 —— 拼写错误的参数会导致错误,而拼写错误的环境变量则会被忽略。./configure --help 会显示可用参数的列表,而几乎不可能获得影响当前构建系统的所有环境变量的完整列表。

描述

这些变化不会导致最终产品发生任何改变;它们只会影响内部开发流程。

更新 Makefile 结构

背景

将旧的 Makefile 更新为新的简化架构,是本文所述所有其他工作的基础。

实现

当前每个目录一个文件的递归 Makefile 样式将被移除。取而代之的是,Makefile 将通过递归查看源代码目录来发现需要编译的文件。不应该编编译的文件则会被列为明确的排除项。这是为了能够使用新的并行 javac 编译器所必需的。

几个子系统共用的代码将存储在一个新的顶级目录 “common/make” 中。设计思路是,这些共用文件将提供一个带有辅助功能的库,以便各个子系统的 Makefile 尽可能简单、清晰地编写。如果这些库的更高代码复杂性能够换来各个子系统 Makefile 的简化,那么我们将接受这种复杂性。

由于良好的编码实践并不是 Makefile 语法自动强制执行的,因此我们将格外小心,以确保我们编写出恰当且可读的代码。

作为更新的一部分,我们将编写一份文档,描述我们在重写过程中发现有用并遵循的编码指南,以便为未来 Makefiles 的更改提供指导。我们还将编写一份文档,描述 Makefiles 的整体架构。

Makefile 除了构建最终的二进制文件外,还做了其他事情,或者构建了非典型的二进制变体。其中一些目标看起来晦涩难懂,并且已经不再使用。如果所有利益相关者都同意,那么我们将不会将这些目标移植到新系统中。以下是我们目前考虑移除的功能列表:

  • (目前为空)

新旧混合

可能可以将旧的 Makefile 系统与新重写的 Makefile 系统并行保留一段时间,这样我们就可以有两种构建产品的方式(新和旧)。这并不是特别理想,因为它可能导致代码重复和普遍的混淆,并且会让我们错失移除旧系统的益处。然而,保留旧系统,或者拥有一个轻松恢复旧系统的方法,将有助于我们管理相关风险。

转换

大多数开发者不会与实际的 makefile 有太多交互,因此工作流程不会有大的变化。

以前,有时候每当源文件或目录被添加或删除时,都需要更新 Makefile。现在这将不再需要了,而且必须将这一点传达给所有开发人员。

想要更改实际 Makefile 的开发者需要了解所使用的总体设计和编码原则。这些内容会记录在文档中,但需要传达这些文档的存在。

使用 autoconf(配置脚本)

背景

Autoconf 背后的基本思想是,使用一个单一且简单的接口来处理用户系统配置与 Makefile 需求之间的“粘合”问题。这个接口就是 ./configure shell 脚本。

使用 autoconf 有两个方面——创建和使用 ./configure 脚本。configure 脚本是由 autoconf 工具根据 configure.ac(以及附带的辅助文件)中的源代码生成的,这些源代码是使用 M4 宏编写的。从这些源代码中,会生成一个 configure shell 脚本。这个脚本(虽然是生成的)会被提交到代码仓库中。每当 configure.ac 源代码发生变化时,就需要重新生成 configure 脚本并更新到仓库中。要重新生成 configure,系统上需要安装 autoconf 工具。

然而,普通用户不需要这样做。因为 configure 已被检入版本控制,他/她只需要运行 ./configure。为此,并不需要 autoconf 工具。这将生成一个采用 Makefile 语法的 config.spec 文件,其中包含构建细节,并且会被 Makefile 包含。

Autoconf 实现

配置脚本有三个主要任务:

  1. 确定所有构建依赖项都已存在。
  2. 分析平台之间的已知差异,并确定当前情况下适用的差异。
  3. 应用用户提供的参数来定制构建。

尽管 autoconf 框架有助于完成所有这些任务,但它们都必须通过了解 OpenJDK 的具体细节来显式编码。这意味着我们需要明确我们实际拥有的构建依赖项、需要确定哪些差异,以及用户可以通过哪些方式影响构建结果。

构建依赖之前已经在 README 文件中描述过了。

已知的差异之前已编码在 Makefile 中,或者存在于“常识”中。

用户的影响历史上是通过使用环境变量来实现的,而对这些变量的检查则是在 Makefile 中进行的。

配置脚本可以像旧 Makefile 的“包装器”一样工作,并在 config.spec 中设置与 Makefile 一直使用的相同变量。在这种情况下,对于 Makefile 来说,变量是来自配置脚本还是用户几乎是透明的。然而,在许多情况下,更好的解决方案可能是输出一个更“干净”的变量,并重写 Makefile 的相应部分。

作为使用 autoconf 的一部分,我们需要在 JDK 8 源代码库中包含来自 autoconf 的三个文件。这三个文件分别是 pkg.m4config.guessconfig.sub。已经请求了将这些文件包含在 OpenJDK 中的法律许可。我们相信这应该不会成为问题,因为 autoconf 的许可证明确写明支持这种使用情况(基本上允许我们以任何方式分发它们,只要它们是作为配置脚本的一部分使用即可)。

转换

构建 OpenJDK 时的当前工作流程基本上是:

  1. 从代码库中检索源代码
  2. 设置一系列环境变量
  3. 运行 make
  4. 每次需要重新构建时,重复步骤 2 和 3

许多团队成员已经创建了个人的 shell 脚本和类似的解决方案来帮助解决这个问题。

使用配置脚本的新工作流程将改为导致:

  1. 从代码仓库中获取源代码
  2. 运行 ./configure,可能需要带特定参数
  3. 运行 make
  4. 每次需要重新构建时重复步骤 3

由于第 3 步非常简单,因此不需要使用 shell 脚本来重新构建。但是,如果用户对其设置进行了高度定制,他们可能希望创建脚本来帮助他们使用正确的参数运行 configure。

我们应该提供一个从旧环境变量到新 configure 参数的翻译表。

讨论: 也许我们在运行 configure/make 时应该检查一些常用的老式环境变量,并提醒用户?

使用支持并行编译的服务器模式加速 javac

对于 JEP 139:增强 javac 以提高构建速度,我们将编写一个 javac 的扩展,以支持并行编译。为了使用此功能,我们必须在 makefile 中添加对它的支持。

转换

切换到使用 javac 服务器进行 Java 编译,对开发者来说不会产生明显的影响(当然,除了显著的速度提升之外)。不需要过渡计划。

通过使用依赖项输出增强 javac 来实现 Java 构建的增量

Make 工具具有增量构建的能力,也就是说,当发生更改时,只重新编译所有文件的一个子集。理想情况下,这个子集应该是所需的最小子集。为了实现这一点,make 需要以可用的格式获取依赖信息。

对于JEP 139:增强 javac 以提高构建速度,我们将编写一个 javac 的扩展,该扩展将允许对 Java 代码进行增量构建。为了使用它,我们必须在 makefile 中添加对它的支持。

转换

增量构建将对开发者可用,且无需任何特定操作。理论上,开发者唯一能注意到的不同之处应该是在重新编译时速度的提升。然而,如果依赖生成失败或出现混乱,构建可能会不正确,这时就需要进行完全重建。虽然这种情况发生的可能性极小,但告知开发者这一潜在问题以及如何进行完全重建仍然很有必要。

此外,编译速度现在将与源代码依赖的复杂性相关联。告知开发者这一点可能会激励他们编写依赖关系不那么复杂的好代码。

替代方案

与其正确地并行化 javac,我们可以并行启动几个单线程的不同且独立的 Java 包的编译任务。这不需要对 javac 做任何更改,但要确保 Makefile 的正确性会困难得多,并且速度提升也不会那么显著。

我们可以跳过重写 Makefile,但如果不先正确清理 Makefile 就引入这些更改,将会是一项艰巨且耗时的任务。

测试

由于我们不会更改最终的二进制文件,因此不需要添加或更改产品的任何测试。

然而,我们应该确保兑现不改变最终二进制文件的承诺。作为该项目的一部分,我们应该创建一个构建比较工具,该工具可以在所有相关方面比较旧系统和新系统的构建结果。这是一个比听起来更难的问题,因为即使使用相同的构建系统,两次连续的构建也不会完全相同,这是由于短暂且无关的因素造成的。为了有用,这样的工具需要忽略这些无关的方面,专注于那些不应该改变的部分。

该工具应该针对各种平台和构建类型运行,比较旧系统和新系统。

此工具还可用于测试增量构建是否与完全重新构建相同。

讨论: 理想情况下,构建系统应该像最终产品一样经过测试。然而,目前并不存在用于测试构建系统的框架,而创建一个合适的测试框架很可能超出了本次工作的范围。

讨论: 我们应该探讨添加某种基本的 Makefile 测试的可能性。通过精心设计和“恶意”的依赖关系来测试增量构建,可以是添加的一种测试类型。是否有一个现有的 javac 测试套件,可以用来添加这样的测试?

风险与假设

移除非构建项目

  • 风险:错误地移除某些组所需的工作流或流程支持
  • 缓解计划:与所有组进行沟通,收集需求
  • 应急计划:立即重新实施对工作流的支持

稀有平台上的问题

  • 风险:在某些罕见的情况下,新的构建系统可能无法正常工作
  • 缓解计划:在部署之前测试多种场景(不同的硬件和软件,针对不同的群体);确保如果需要,我们可以并行使用新旧两套系统
  • 应急计划:保留旧系统,以便可以并行使用两套系统

结果产品不正确

  • 风险:构建更改导致构建出错误的比特位
  • 缓解计划:正确测试生成的构建
  • 应急计划:保留旧系统并在问题解决前使用它替代

依赖

如前所述,此 JEP 依赖于 JEP 139:增强 javac 以提高构建速度

这个 JEP 将对代码进行大量修改,而这些代码也会被 BSD/MacOS X 移植版本所修改。构建方面的更改很可能在 JDK 8 中会比该项目更早到达,因此我们必须处理它们引入的更改。然而,大部分更改将与 Hotspot 相关,而这并不是我们在这个项目中考虑的内容。

未来的 JEP 将基于此 JEP 来改进 HotSpot 和 Javadoc 的构建过程。

影响

这一变化对最终产品的影响很小。

  • 兼容性:产品的构建方式将会有所不同。现有的个人或团体构建脚本在不修改的情况下将无法使用。
  • 可移植性:我们必须确保新的构建系统在所有支持的平台上都能正常工作。如果可能的话,应该编写代码以尽量减少移植到新系统时的工作量。
  • 文档:现有的文档(如构建 README)需要更新。