9

文档中所述,Gradle 使用有向无环图 (DAG) 来构建依赖图。根据我的理解,有独立的评估和执行周期是构建工具的一个主要特性。例如,Gradle 文档指出,这启用了一些原本不可能的功能。

我对说明此功能强大功能的真实示例感兴趣。依赖图对哪些用例很重要?我对该领域的个人故事特别感兴趣,无论是使用 Gradle 还是类似配备的工具。

我从一开始就制作这个“社区维基”,因为很难评估一个“正确”的答案。

4

3 回答 3

6

这个挑衅性的问题为最终研究 Gradle 提供了动力。我还没有使用它,所以我只能提供浏览文档时记录的分析,而不是个人故事。

我的第一个问题是为什么 Gradle 任务依赖图保证是非循环的。我没有找到答案,但是很容易构造相反的情况,因此我假设循环检测是在构建图形时运行的验证,并且在执行第一个任务之前构建失败如果存在非法的循环依赖。如果不首先构建图表,则在构建几乎完成之前可能不会发现此故障情况。此外,检测例程必须在每个任务执行后运行,这将是非常低效的(只要图形是增量构建的并且全局可用,深度优先搜索只需要找到一个起点,然后周期评估需要最少的工作,但是总的工作量仍然比一开始就对整个关系集进行一次归约要多)。我认为早期发现是一个主要好处。

任务依赖可以是惰性的(参见:4.3 任务依赖,以及 13.14 中的相关示例)。在构建整个图之前,无法正确评估惰性任务依赖关系。传递(非任务)依赖解决方案也是如此,这可能会导致无数问题,并且在发现和解决其他依赖关系时需要重复重新编译(也需要对存储库重复请求)。任务规则功能 (13.8) 也不可能。考虑到 Gradle 使用动态语言并且可以动态添加和修改任务,可以概括这些问题以及可能的许多其他问题,因此在第一次评估之前,结果可能是不确定的,因为执行路径已构建并且在运行时修改,因此,如果存在直到后来才知道的依赖关系或行为指令,不同的评估序列可能会产生任意不同的结果,因为它们尚未创建。(这可能值得用一些具体的例子来研究。如果是真的,那么即使两次传递也并不总是足够的。如果 A -> B,B -> C,其中 C 改变了 A 的行为,使其不再取决于B,那么你就有问题了。我希望有一些最佳实践来限制非本地范围的元编程,以不允许它在任意任务中使用。一个有趣的例子是模拟时间旅行悖论,孙子杀祖或娶祖母,生动地说明了一些实际的伦理原则!)因为它们还没有被创建。(这可能值得用一些具体的例子来研究。如果是真的,那么即使两次传递也并不总是足够的。如果 A -> B,B -> C,其中 C 改变了 A 的行为,使其不再取决于B,那么你就有问题了。我希望有一些最佳实践来限制非本地范围的元编程,以不允许它在任意任务中使用。一个有趣的例子是模拟时间旅行悖论,孙子杀祖或娶祖母,生动地说明了一些实际的伦理原则!)因为它们还没有被创建。(这可能值得用一些具体的例子来研究。如果是真的,那么即使两次传递也并不总是足够的。如果 A -> B,B -> C,其中 C 改变了 A 的行为,使其不再取决于B,那么你就有问题了。我希望有一些最佳实践来限制非本地范围的元编程,以不允许它在任意任务中使用。一个有趣的例子是模拟时间旅行悖论,孙子杀祖或娶祖母,生动地说明了一些实际的伦理原则!)其中 C 改变了 A 的行为,使其不再依赖于 B,那么你就有问题了。我希望有一些最佳实践来限制非本地范围的元编程,以不允许它在任意任务中使用。一个有趣的例子是一个时间旅行悖论的模拟,一个孙子杀死了他的祖父或娶了他的祖母,生动地说明了一些实用的道德原则!)其中 C 改变了 A 的行为,使其不再依赖于 B,那么你就有问题了。我希望有一些最佳实践来限制非本地范围的元编程,以不允许它在任意任务中使用。一个有趣的例子是一个时间旅行悖论的模拟,一个孙子杀死了他的祖父或娶了他的祖母,生动地说明了一些实用的道德原则!)

它可以对当前正在执行的构建提供更好的状态和进度报告。TaskExecutionListener 为每个任务的处理提供了之前/之后的钩子,但在不知道剩余任务的数量的情况下,除了“6 个任务已完成。即将执行任务 foo”之外,它只能说明状态。相反,您可以使用 gradle.taskGraph.whenReady 中的任务数初始化 TaskExecutionListener,然后将其附加到 TaskExecutionGraph。现在它可以提供信息以启用报告详细信息,例如“72 个任务中的 6 个已完成。现在正在执行任务 foo。预计剩余时间:2h 38m。” 这对于在持续集成服务器的控制台上显示很有用,或者如果 Gradle 被用于编排大型多项目构建并且时间估计至关重要。

正如 Jerry Bullard 所指出的,生命周期的评估部分对于确定执行计划至关重要,执行计划提供有关环境的信息,因为环境部分由执行上下文确定(由 DAG 配置部分中的示例 4.15)。此外,我可以看到这对执行优化很有用。独立的子路径可以安全地交给不同的线程。如果它们不是幼稚的,用于执行的步行算法可以减少内存密集度(我的直觉说,总是走具有最多子路径的路径将导致更大的堆栈,而不是总是首选具有最少子路径的路径)。

一个有趣的使用可能是系统的许多组件最初被剔除以支持演示和增量开发。然后在开发过程中,而不是在每个组件实现时更新构建配置,构建本身可以确定子项目是否已准备好包含(也许它会尝试获取代码,编译它,并运行预先确定的测试套件) . 如果是,评估阶段将揭示这一点,并且将包括适当的任务,否则,它将为存根选择任务。可能存在对尚不可用的 Oracle 数据库的依赖,而您同时使用的是嵌入式数据库。您可以让构建检查可用性,在可以时透明地切换,并告诉您它切换了数据库,而不是你告诉它。沿着这些思路可能会有很多创造性的用途。

Gradle 看起来很棒。感谢您激发一些研究!

于 2010-03-18T20:06:57.987 回答
3

同一文档中的一个示例说明了这种方法的强大功能:

正如我们稍后详细描述的(参见第 30 章,构建生命周期)Gradle 有一个配置阶段和一个执行阶段。在配置阶段之后,Gradle 知道应该执行的所有任务。Gradle 为您提供了一个使用这些信息的钩子。一个用例是检查发布任务是否是要执行的任务的一部分。根据这一点,您可以为某些变量分配不同的值。

换句话说,您可以及早加入构建过程,因此您可以根据需要更改其过程。如果已经执行了一些实际的构建工作,那么更改可能为时已晚。

于 2010-03-18T05:23:04.173 回答
1

我现在正在评估不同的构建系统,并且使用 gradle 我设法添加了枚举所有“jar”类型的任务并更改它们的丑陋代码,以便每个 jar 清单都包含“Build-Number”属性(稍后用于组成 final文件名):

gradle.taskGraph.whenReady {
    taskGraph ->
    taskGraph.getAllTasks().findAll {
        it instanceof org.gradle.api.tasks.bundling.Jar
    }.each {
        it.getManifest().getAttributes().put('Build-Number', project.buildVersion.buildNumber)
    }
}
于 2011-04-17T16:48:07.457 回答