Gradle 依赖图的真实示例有哪些?
正如文档中所述,Gradle 使用有向无环图 (DAG)构建依赖图。根据我的理解,具有单独的评估和执行周期是构建工具的一个主要功能。例如 Gradle 文档 声明这可以实现一些原本不可能实现的功能。
我对展示此功能强大功能的现实示例感兴趣。依赖图对于哪些用例很重要?我对来自该领域的个人故事特别感兴趣,无论是使用 Gradle 还是类似装备的工具。
我从一开始就制作这个“社区维基”,因为很难评估“正确”的答案。
As noted in the documentation, Gradle uses a directed acyclic graph (DAG) to build a dependency graph. From my understanding, having separate cycles for evaluation and execution is a major feature for a build tool. e.g. The Gradle doc states that this enables some features that would otherwise be impossible.
I'm interested in real-world examples that illustrate the power of this feature. What are some use-cases for which a dependency graph is important? I'm especially interested in personal stories from the field, whether with Gradle or a similarly equipped tool.
I am making this 'community wiki' from the outset, as it will be difficult to assess a 'correct' answer.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
这个具有挑战性的问题为最终研究 Gradle 提供了动力。我还没有使用过它,所以我只能提供浏览文档时记录的分析,而不是个人故事。
我的第一个问题是为什么 Gradle 任务依赖图保证是非循环的。我没有找到答案,但很容易构建相反的情况,因此我假设循环检测是构建图时运行的验证,并且在执行第一个任务之前构建失败,如果存在非法循环依赖。如果不首先构建图表,则在构建接近完成之前可能不会发现这种故障情况。此外,检测例程必须在每个任务执行后运行,这将是非常低效的(只要图是增量构建的并且全局可用,则只需要深度优先搜索来找到起点,然后周期评估只需要很少的工作,但总的工作量仍然比一开始就对整个关系集进行一次缩减要大)。我认为早期检测是一个主要好处。
任务依赖关系可以是惰性的(请参阅:4.3 任务依赖关系,以及 13.14 中的相关示例)。在构建整个图之前,无法正确评估惰性任务依赖关系。对于传递(非任务)依赖项解析也是如此,这可能会导致无数问题,并且在发现和解决其他依赖项时需要重复重新编译(还需要对存储库重复请求)。任务规则功能(13.8)也是不可能的。这些问题以及可能的许多其他问题可以通过考虑 Gradle 使用动态语言来概括,并且可以动态添加和修改任务,因此在首次通过评估之前,结果可能是不确定的,因为执行路径是构建的并且因此,如果存在直到以后才知道的依赖项或行为指令(因为它们尚未创建),则不同的评估序列可能会产生任意不同的结果。 (这可能值得用一些具体的例子来研究。如果这是真的,那么即使两次传递也并不总是足够的。如果 A -> B,B -> C,其中 C 改变了 A 的行为,使得它不再依赖于 B,那么我希望有一些限制非局部范围元编程的最佳实践,以不允许它出现在任意任务中,一个有趣的例子是时间旅行悖论的模拟,其中。孙子杀死了他的祖父或娶了他的祖母,生动地说明了一些实用的道德原则!)
它可以更好地报告当前正在执行的构建的状态和进度。 TaskExecutionListener 为每个任务的处理提供了 before/after 挂钩,但在不知道剩余任务数量的情况下,除了“6 个任务已完成。即将执行任务 foo”之外,它无法说明其他状态。相反,您可以使用 gradle.taskGraph.whenReady 中的任务数初始化 TaskExecutionListener,然后将其附加到 TaskExecutionGraph。现在它可以提供信息来启用报告详细信息,例如“已完成 72 个任务中的 6 个。正在执行任务 foo。预计剩余时间:2 小时 38 分钟。”这对于在持续集成服务器的控制台上显示非常有用,或者如果 Gradle 用于编排大型多项目构建并且时间估计至关重要。
正如 Jerry Bullard 所指出的,生命周期的评估部分对于确定执行计划至关重要,执行计划提供有关环境的信息,因为环境部分由执行上下文决定(通过 DAG 配置部分中的示例 4.15)。此外,我发现这对于执行优化很有用。独立的子路径可以安全地传递给不同的线程。如果执行的步行算法不是天真的的话,那么它们的内存密集程度可能会较低(我的直觉是,总是走具有最多子路径的路径将导致比总是首选具有最少子路径的路径产生更大的堆栈)。
一个有趣的用途可能是系统的许多组件最初被删除以支持演示和增量开发的情况。然后在开发过程中,构建本身可以确定子项目是否已准备好包含(也许它会尝试获取代码,编译它并运行预先确定的测试套件),而不是在实现每个组件时更新构建配置。如果是,评估阶段将揭示这一点,并且将包括适当的任务,否则,它会选择存根的任务。也许对 Oracle 数据库的依赖尚不可用,而您同时正在使用嵌入式数据库。您可以让构建检查可用性,在可以时透明地切换,并告诉您它切换了数据库,而不是您告诉它。沿着这些思路,可能会有很多创造性的用途。
Gradle 看起来棒极了。感谢您引发一些研究!
This provocative question provided motivation for finally looking into Gradle. I still haven't used it, so I can only offer analysis noted while browsing the docs, not personal stories.
My first question was why a Gradle task dependency graph is guaranteed to be acyclic. I didn't find the answer to that, but a contrary case is easily constructed, so I'll presume that cycle detection is a validation that is run when the graph is built, and the build fails prior to execution of the first task if there are illegal cyclical dependencies. Without first building the graph, this failure condition might not be discovered until the build is nearly complete. Additionally, the detection routine would have to run after every task was executed, which would be very inefficient (as long as the graph was built incrementally and available globally, a depth-first search would only be required to find a starting point, and subsequent cycle evaluations would require minimal work, but the total work would still be greater than doing a single reduction on the entire set of relations at the outset). I'd chalk up early detection as a major benefit.
A task dependency can be lazy (see: 4.3 Task dependencies, and a related example in 13.14). Lazy task dependencies could not be evaluated correctly until the entire graph is built. The same is true for transitive (non-task) dependency resolution, which could cause innumerable problems, and require repeated recompilations as additional dependencies are discovered and resolved (also requiring repeated requests to a repository). The task rules feature (13.8) wouldn't be possible either. These issues, and likely many others, can be generalized by considering that Gradle uses a dynamic language, and can dynamically add and modify tasks, so prior to a first-pass evaluation, results could be non-deterministic since the execution path is built and modified during runtime, thus, different sequences of evaluation could produce arbitrarily different results if there are dependencies or behavioral directives that are unknown until later, because they haven't been created yet. (This may be worthy of investigating with some concrete examples. If it is true, then even two passes would not always be sufficient. If A -> B, B -> C, where C changes the behavior of A so that it no longer depends on B, then you have a problem. I hope there are some best practices on restricting metaprogramming with non-local scope, to not allow it in arbitrary tasks. A fun example would be a simulation of a time travel paradox, where a grandchild kills his grandfather or marries his grandmother, vividly illustrating some practical ethical principles!)
It can enable better status and progress reporting on a currently executing build. A TaskExecutionListener provides before/after hooks to the processing of each task, but without knowing the number of remaining tasks, there isn't much it could say about status other than "6 tasks completed. About to execute task foo." Instead, you could initialize a TaskExecutionListener with the number of tasks in gradle.taskGraph.whenReady, and then attach it to the TaskExecutionGraph. Now it could provide information to enable report details like "6 of 72 tasks completed. Now executing task foo. Estimated time remaining: 2h 38m." That would be useful to display on a console for a continuous integration server, or if Gradle was being used to orchestrate a large multi-project build and time estimates were crucial.
As pointed out by Jerry Bullard, the evaluation portion of the lifecycle is critical to determining the execution plan, which provides information about the environment, since the environment is partially determined by the execution context (Example 4.15 in the Configure by DAG section). Additionally, I could see this being useful for execution optimization. Independent subpaths could be safely handed to different threads. Walking algorithms for execution can be less memory intensive if they aren't naive (my intuition says that always walking the path with the most subpaths is going to lead to a larger stack than always preferring paths with the least subpaths).
An interesting use of this might be a situation where many components of a system are initially stubbed out to support demos and incremental development. Then during development, rather than updating the build configuration as each component becomes implemented, the build itself could determine if a subproject is ready for inclusion yet (perhaps it tries to grab the code, compile it, and run a pre-determined test suite). If it is, the evaluation stage would reveal this, and the appropriate tasks would be included, otherwise, it selects the tasks for the stubs. Perhaps there's a dependency on an Oracle database that isn't available yet, and you're using an embedded database in the meantime. You could let the build check the availability, transparently switch over when it can, and tell you that it switched databases, rather than you telling it. There could be a lot of creative uses along those lines.
Gradle looks awesome. Thanks for provoking some research!
同一文档中的示例说明了这种方法的力量:
换句话说,您可以尽早加入构建过程,这样您就可以根据需要改变其过程。如果已经执行了一些实际的构建工作,那么更改可能为时已晚。
An example from the same documentation illustrates the power of this approach:
In other words, you can hook into the build process early, so you can alter its course as needed. If some actual build work had already been performed, it may be too late to change.
我现在正在评估不同的构建系统,并使用 gradle 设法添加丑陋的代码,枚举“jar”类型的所有任务并更改它们,以便每个 jar 清单都包含“Build-Number”属性(稍后用于组成最终的文件名):
I'm evaluating different build systems now and with gradle I managed to add ugly code that enumerates all tasks of type 'jar' and changes them so, that every jar manifest includes 'Build-Number' attribute (which is used later to compose final file names):