Mill:比Maven快10倍的JVM构建工具

Mill 是一款快速、可扩展、支持多语言的构建工具,支持 Java、Scala 和 Kotlin。尽管 Java 编译器速度快且 Java 语言简单易用,但 JVM 构建工具却以运行缓慢和混乱著称。Mill 旨在让您的构建系统充分利用 JVM 的性能和可用性:

  • Mill 构建相同 Java 代码库的速度比 Maven 快 5-10 倍比 Gradle 快 2-4 倍
  • Mill 的类型化配置语言和不可变任务图 有助于保持构建的简洁和易懂
  • Mill 可以从小型单模块项目扩展到具有数百个模块的大型单一存储库

Mill 通过以下方式实现这一目标:

  • 性能:Mill 的构建图会自动 缓存 和并行化构建任务,让您的工作流程快速且响应迅速。Mill 在构建项目所需的逻辑上增加的开销最小,同时提供工具让您识别和解决构建中的瓶颈
  • 可维护性:Mill 超越了 YAML 和 Bash,其配置和自定义逻辑以 简洁的类型检查代码编写,并具有不可变的模块树和任务图。这可以尽早发现配置问题,并帮助 IDE(IntelliJ或 VSCode)比任何其他构建系统更好地理解您的 Mill 构建
  • 灵活性:Mill 的任务和模块允许添加任何内容,从添加 简单的构建步骤到添加整个语言工具链。您可以在构建中导入任何 JVM 库,使用 Mill 丰富的[url=https://mill-build.org/mill/0.12.1/extending/thirdparty-plugins.html\]第三方 Mill 插件[/url]生态系统,或者自己 编写插件并将[url=https://mill-build.org/mill/0.12.1/extending/writing-plugins.html#\_publishing\]其发布\[/url\]到 Maven Central 供其他人使用。

要开始使用 Mill,请查看每种语言的介绍文档:

  • 使用 Mill 构建 Java 项目
  • 使用 Mill 构建 Scala 项目
  • (实验性)Kotlin 与 Mill

Mill 用于构建许多实际项目,例如 C3P0 JDBC 连接池、 Coursier JVM 依赖项解析器、 Ammonite REPL以及 SpinalHDL和 Chisel硬件设计框架。Mill 可用于在常见 JVM 框架(如Spring Boot或 Micronaut)之上构建的应用程序 。

Mill 借鉴了Maven、 Gradle、Bazel等其他工具的理念,但尝试学习每种工具的优点并改进其缺点。如需与现有构建工具进行比较,请查看以下页面:

  • Mill 与 Maven

  • Mill 与 Gradle

  • Mill 与 SBT 的对决

设计原则
1、依赖图优先
Mill 最重要的抽象是 s 的依赖图Task。使用语法构造T {…​} Task.Anon {…​} Task.Command {…​},它们跟踪构建步骤之间的依赖关系,因此这些步骤可以按正确的顺序执行、查询或并行化。

虽然 Mill 提供了诸如 之类的帮助程序ScalaModule,您可以使用它来快速实例化一系列相关任务(解析依赖项、查找源、编译、打包到 jar 中……),但这些都是次要的。当 Mill 执行时,依赖关系图才是最重要的:任何其他组织模式(层次结构、模块、继承等)仅对创建此依赖关系图很重要Task。

2、构建是分层的
从命令行运行任务的语法与在 Scala 代码中引用任务的语法相同,即 Foo.bar.baz 可以从命令行运行的所有任务都位于 build.mill 文件的对象层次结构中。

层次结构的不同部分可以有不同的任务:只需在某个地方添加一个新的 def foo = Task {...},就能运行它。 使用 Cross 数据结构的交叉编译只是对象层次结构中的另一种节点。

唯一的区别在于语法:由于 Scala/Bash 语法的不同限制,从命令行运行时需要使用 mill core.cross[a].printIt,而从代码中运行时则需要使用 core.cross("a").printIt。

3、默认缓存
构建中的每个任务(由 def foo = Task {...} 定义)都会被默认缓存。 目前,缓存是通过 out/ 文件夹中的 foo.json 文件完成的。 任务还将在文件系统中获得一个 foo.dest/ 路径,用于存储输出文件等。

每个任务都会被缓存,而不仅仅是编译或汇编等 "慢 "任务。

缓存以返回值的 .hashCode 为关键。 对于返回磁盘上文件/文件夹内容的任务,它们会返回 PathRef 实例,其散列码基于磁盘内容的散列码。 返回值的序列化使用 uPickle 完成。

4、纯函数
在很大程度上依赖于构建任务的 "纯净":它们只依赖于输入任务,唯一的输出就是返回值。

  • 它们不会在文件系统中到处乱窜,随意读写。

这正是我们在构建过程中积极缓存和并行评估构建任务的原因。

许多类型的构建步骤都需要磁盘上的文件,为此 Mill 提供了 Task.dest 文件夹。 这是磁盘上专属于每个编译任务的文件夹,因此编译任务可以在其中读写文件,而不必担心与其他任务的 Task.dest 文件夹发生冲突。

实际上,这使得文件输出也变得 "纯粹":当我们需要使文件失效时,可以精确地知道任务的输出文件在哪里,并允许多个任务同时读写文件系统,即使是并行读写也很安全。

5、较短的编译进程
Mill 的编译进程可以反复运行,而不仅仅是一个寿命较长的守护进程/控制台。

这就意味着我们必须尽量缩短进程的启动时间,而且新进程必须能够在前一个进程离开的地方重新构建内存中的数据结构,以便继续编译。

重新构建是通过编译的层次结构来完成的:每个任务 foo.bar.baz 在编译层次结构中都有一个固定的位置,因此在磁盘 out/foo/bar/baz.json 上也有一个固定的位置。

当老进程死亡、新进程启动时,会有一个新的 Task 实例,它具有相同的执行代码,在构建层次结构中也处于相同的位置:这个新的 Task 可以加载 out/foo/bar/baz.json 文件,并从上一个进程离开的地方继续运行。

尽量缩短启动时间意味着要积极缓存,以及尽量减少字节码的使用总量: Mill 目前 1-2s 的启动时间主要由 JVM classloading 控制。

默认情况下,Mill 使用长寿命编译服务器来进一步加快速度,但确保 "从头开始 "的性能保持良好是一项持续的核心要求。

6、静态依赖关系图和应用性任务
任务是应用性的,而不是 Monadic 的。 有 .map、.zip,但没有 .flatMap 操作。

这意味着我们可以在开始执行任务之前就知道整个依赖关系图的结构。

这样,我们就能在运行前对依赖关系图执行各种有用的操作:

  • 给定一个用户希望运行的任务,预先计算并显示将评估哪些任务("干运行"),而无需运行这些任务
  • 自动并行化依赖关系图中互不依赖的不同部分,甚至可以像 Bazel/Pants 一样将其分配给不同的工作机器
  • 可视化依赖关系图,例如通过转储到 DOT 文件。
  • 查询图表,例如 "为什么这件事依赖于另一件事?
  • 避免 "中途 "运行任务:如果一个任务的上游任务失败,我们可以完全跳过该任务,而不是运行到一半时出现异常而退出

Mill 如何实现“简单”
为什么您应该期望 Mill 构建工具能够实现简单、轻松和灵活,而过去的其他构建工具却未能做到这一点?
构建工具本身包含大量不同的概念:

  • 什么“任务”取决于什么?
  • 我该如何定义我自己的任务?
  • 源文件来自哪里?
  • 需要按什么顺序运行才能完成我想要的操作?
  • 什么可以并行化,什么不可以?
  • 任务之间如何传递数据?它们传递什么数据?
  • 哪些任务被缓存了?缓存在哪里?
  • 如何从命令行运行任务?
  • 如何处理构建中固有的重复?(例如,每个“模块”的编译、运行和测试任务)
  • 什么是“模块”?它们与“任务”有何关系?
  • 如何配置模块来执行不同的操作?
  • 如何处理交叉构建(跨不同配置)?

Mill 旨在使用尽可能少且熟悉的核心概念来回答这些问题。整个 Mill 构建都围绕以下几个概念:

  • 对象层次结构
  • 调用图
  • 实例化特征和类

对于熟悉 Scala(或任何其他编程语言……)的人来说,这些概念已经很熟悉,但足以回答上面列出的所有复杂的构建相关问题。

SBT
Mill 是作为 SBT 的替代品而构建的,其问题 在此处进行了描述。尽管如此,Mill 还是在有意义的部分采用了 SBT 的某些部分(用 Scala 编写的构建,带有 Applicative“成语括号”宏的任务图)

Bazel
Mill 很大程度上受到了Bazel的启发。具体来说,单构建层次结构(其中每个目标根据其在层次结构中的位置都有一个磁盘缓存/输出文件夹)源自 Bazel。

借助 Mill,我尝试将 Bazel 的 Python 层 1 和 2 合并为一层 Scala,并让它通过返回值来定义其依赖关系图/层次结构,而不是通过调用全局副作用 API。我在尝试教人们如何在工作中使用 Bazel 时遇到了麻烦,但我很确定我们可以制作出更易于使用的东西。

Scala.Rx
Mill 的“直接式”应用语法灵感来自我的旧 Scala.Rx项目。虽然存在差异(Mill 使用宏以词汇方式捕获依赖关系图,Scala.Rx 在运行时捕获它),但它们非常相似。

最终目标是相同的:以“直接风格”编写代码,并将其自动“提升”到依赖图中,您可以在运行时进行自省并用于增量更新。
Scala.Rx 本身是基于 2010 年的论文 《弃用观察者模式》构建的。

CBT
Mill 看起来很像 CBT。 基于继承的 Modules/ScalaModules 定制模型和 "命令行路径匹配 Scala 选择器路径 "的想法都直接来自于 CBT。 但其他大部分东西都不一样:重新整合的依赖关系图、执行模型和缓存模块都更多地遵循 Bazel 而不是 CBT。

https://www.jdon.com/76215.html

上一篇:探索人工智能的不同形态与未来方向:从ANI到AGI,再到ASI


下一篇:漏洞与攻击技术详解