JEP 138: Autoconf-Based Build System | 基于 Autoconf 的构建系统
摘要
引入 autoconf(./configure-style
)构建设置,重构 Makefile 以消除递归,并利用 JEP 139: 增强 javac 以提高构建速度。
目标
我们试图实现的最顶层目标是:
- 大幅提高构建速度
- 简化构建系统源代码(Makefile 等)
- 简化开发人员的工作
- 获得精确且可重复的构建输出
- 简化构建机器配置(JPRT 等)
我们将通过四个子项目来实现这些目标,这些子项目或多或少紧密相关。
- 更新 Makefile 结构
- 使用 autoconf(配置脚本)
- 添加并行 Java 编译支持
- 使 Java 构建具有增量性
我们需要正确了解现有的开发人员工作流程,以便最大程度地减少此更改对每个人的影响。
该项目是改善 JDK 构建基础架构的更大努力的一部分。我们预计该项目将紧随未来的步骤进行。这些步骤之间的区别有些随意,只是为了快速利用首先完成的 JDK 构建基础架构改进工作。
非目标
由于我们将使用新结构更新 Makefile,因此未来我们想要解决的一些问题可能会由于更新而自行解决。但是,我们不会在此项目中专门解决这些问题,也不会对它们进行测试或保证它们能正常工作。(但是,我们会尽量确保不会破坏任何正常工作的事物。)这些问题包括:
- 简化移植到新平台的工作
- 使在没有网络连接的情况下进行 JDK 开发成为可能
- 为交叉编译提供适当支持,包括在 64 位主机上编译 32 位二进制文件
- 改进警告处理
我们也不会解决计划在未来步骤中解决的问题。(但是,这项工作中的一些内容将为这些未来的改进奠定基础。)这些问题包括:
- 加快 Hotspot 编译速度
- 升级编译器
- 支持 IDE 项目
- 重新考虑源代码删除机制
成功指标
构建简洁性
在所有先决条件都具备的情况下,构建应该通过以下步骤完成:
- 从 Mercurial 仓库中获取源代码
./configure
make
构建速度
构建速度取决于硬件因素,改进程度会有所不同。我们的目标是在 8 核 Linux 机器上进行编译。在这种情况下,经过我们的改进后,构建 JDK 所需的时间应最多为当前时间的 33%。(这通常意味着从大约 15 分钟缩短到大约 5 分钟或更短)。一个更具挑战性的目标是,JDK 的构建时间应最多为 20%(约 3 分钟)。
请注意,这仅针对 JDK。不包括构建 Hotspot 或生成 Javadoc。
Makefile 清理
JDK 中所有小于 3kB 的递归 Makefile(不包括 Hotspot)都应该被移除,并将其功能收集到中央 Makefile 中。(虽然 Makefile 的数量本身不是目标,但将代码集中在一个(或几个)位置有助于概览和理解。)
动机
完整构建 JDK 的速度过慢,这给开发人员和构建系统带来了额外的负担。因此,开发人员只会检出并构建源代码的一部分,因为整个产品的构建时间太长。
当前构建系统的实现方式,在产品的各个角落散布着超过 350 个最小的递归 Makefile,这使得对构建系统进行更改变得困难。当前的解决方案有时还需要更新 Makefile 来添加新的源文件或目录,这实际上是不必要的。
目前,构建系统是通过使用多个环境变量来配置的。这与流行的使用 ./configure
来设置构建系统的方法不同。除了熟悉性之外,这种方法相比环境变量还有多个优点。配置参数会被检查——拼写错误的参数会导致错误,而拼写错误的环境变量则会被忽略。./configure --help
会显示可用参数的列表,而几乎不可能获得影响当前构建系统的所有环境变量的综合列表。
描述
这些更改不会导致构建产品的任何变化;它们仅影响内部开发过程。
更新 Makefile 结构
背景
将旧的 Makefile 更新为新的、简化的架构,对于这里描述的所有其他工作都是基础性的。
实现
当前每个目录一个文件的递归 makefile 风格将被移除。相反,makefile 将通过递归查找源代码目录来发现需要编译的文件。不应编译的文件将被明确列为排除项。这将需要使用新的并行 javac 编译器。
几个子系统共用的代码将被存储在一个新的顶级目录“common/make”中。设计思想是,这些公共文件将提供一个包含辅助函数的库,以便每个子系统的 Makefile 可以尽可能简单和清晰地编写。如果这能让每个子系统的 Makefile 更加简单,我们将接受这些库中的代码复杂度更高。
由于 Makefile 语法不会自动强制执行良好的编码实践,我们将格外注意确保我们编写正确且可读的代码。
作为更新的一部分,我们将编写一份文档,描述我们在重写过程中发现的有用且遵循的编码准则,以便指导 Makefile 的未来更改。我们还将编写一份文档,描述 Makefile 的总体架构。
Makefile 除了构建最终的二进制文件或构建二进制文件的不同变体之外,还执行其他操作。其中一些目标看起来晦涩难懂且不再使用。如果所有利益相关者都同意,我们将不会将这些目标移植到新系统中。以下是我们目前考虑移除的功能列表:
- (目前为空)
新旧混合
我们可能可以保留旧的 Makefile 系统,并与新的重写 Makefile 系统并行存在,这样我们就可以在一段时间内有两种构建产品的方式(新和旧)。但这并不是真正可取的,因为它可能导致代码重复和一般混乱,并使我们错过移除旧内容的好处。然而,保留旧系统或提供一种轻松恢复旧系统的方法将有助于我们管理相关风险。
过渡
大多数开发人员不会与实际的 makefile 有太多交互,因此工作流程不会有太大变化。
以前,每当添加或删除源文件或目录时,有时需要更新 Makefile。这将不再需要,并且需要通知所有开发人员。
想要更改实际 Makefile 的开发人员需要了解整体设计和使用的编码原则。这将被记录下来,但需要通知这些文档的存在。
使用 autoconf(配置脚本)
背景
autoconf 背后的基本思想是,通过一个简单、单一的接口来处理用户系统配置与 Makefile 要求之间的“粘合”问题。这个接口是 ./configure
shell 脚本。
因此,使用 autoconf 有两个方面——创建和使用 ./configure
脚本。配置脚本是由 autoconf 工具从 configure.ac
(以及伴随的辅助文件)中的源代码生成的,这些源代码使用 M4 宏编写。从这个源代码中,会生成一个 configure
shell 脚本。这个脚本(尽管是生成的)会被签入到仓库中。每当 configure.ac
源代码发生变化时,都需要重新生成并更新仓库中的 configure
脚本。要重新生成 configure
,需要在系统上安装 autoconf 工具。
然而,普通用户通常不需要这样做。由于 configure
已被签入,他 / 她只需要运行 ./configure
。为此,不需要 autoconf 工具。这会在 Makefile 语法中生成一个 config.spec
文件,该文件确定构建细节,并被 Makefile 包含。
Autoconf 实现
configure 脚本有三个主要任务:
- 确定所有构建依赖项都存在。
- 分析平台之间的已知差异,并确定哪些差异适用于当前情况。
- 应用用户提供的参数来专门化构建。
尽管 autoconf 框架有助于完成所有这些任务,但都必须明确地使用关于 OpenJDK 特性的知识来编码。这意味着我们需要清楚地知道我们实际拥有哪些构建依赖项,需要确定哪些差异,以及用户可以通过哪些方式影响构建结果。
构建依赖项已在之前的 README 文件中描述。
已知的差异之前已编码在 Makefile 中,或者作为“常识”存在。
用户影响历来是通过使用环境变量来实现的,并且这些检查已放在 Makefile 中。
configure 脚本可以作为旧 Makefile 的“包装器”工作,并在 config.spec 中设置与 Makefile 使用的相同变量。在这种情况下,对于 Makefile 来说,变量来自 configure 脚本而不是用户,这将是几乎透明的。然而,在许多情况下,更好的解决方案可能是输出一个更“干净”的变量,并重写 Makefile 的相应部分。
法律地位
作为使用 autoconf 的一部分,我们需要将 autoconf 中的三个文件包含在 JDK 8 源代码存储库中。这三个文件是 pkg.m4
、config.guess
和 config.sub
。已在 OpenJDK 中请求包含这些文件的法律许可。我们相信这应该不是问题,因为 autoconf 许可证明确支持这种用例(基本上允许我们以任何喜欢的方式分发它们,只要它们作为 configure 脚本的一部分使用)。
转换
当前构建 OpenJDK 的工作流程基本上是:
- 从存储库中检索源代码
- 设置一系列环境变量
- 运行 make
- 每次需要重新构建时重复步骤 2 和 3
许多团队成员已经创建了个人 shell 脚本和类似解决方案来帮助完成此操作。
使用 configure 脚本的新工作流程将导致:
- 从存储库中检索源代码
- 运行
./configure
,可能带有专门化的参数 - 运行 make
- 每次需要重新构建时重复步骤 3
由于步骤 3 非常简单,因此不需要 shell 脚本来重新构建。但是,如果用户对其设置进行了大量专门化,他们可能希望创建脚本来帮助他们使用正确的参数运行 configure。
我们应该提供一个从旧环境变量到新 configure 参数的转换表。
讨论:也许我们应该在运行 configure/make 时检查一些常用的旧式环境变量,并提醒用户?
使用支持并行编译的服务器模式加速 javac
对于 JEP 139: 增强 javac 以提高构建速度,我们将为 javac 编写一个扩展,以支持并行编译。为了使用此功能,我们必须在 makefile 中添加对其的支持。
转换
将 Java 编译切换到使用 javac 服务器模式对开发者来说不会产生明显影响(当然,除了大幅提高速度之外)。因此,不需要转换计划。
通过为 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)。