首次接触声明式 Gradle

2024 年 7 月,我们发布了 Declarative Gradle 的早期访问预览 (EAP)。了解有关此版本、尝试示例并分享您的反馈!

目录

简介 #

在 2023 年 11 月的更新中,我们宣布了一个名为 Declarative Gradle 的新实验性项目。那篇文章介绍了我们关于开发者优先的软件定义以及如何实现 Gradle 声明式构建语言愿景的想法。从那时起,我们一直努力创建 Declarative Gradle 的第一个早期访问预览 (EAP)。

这篇博客文章提供了项目进度的更新,并概述了您如何试用、提供反馈并影响我们的后续步骤。

First look at Declarative Gradle

什么是 Declarative Gradle? #

Gradle 构建工具愿景的一部分是提供一种优雅且可扩展的声明式构建语言,允许开发人员以清晰易懂的方式描述任何类型的软件。Gradle 的构建语言已经在最基本的方式上是可扩展的,这带来了高度的灵活性,但它并不总是完全声明式、清晰和易懂的。

我们相信 Declarative Gradle 将为软件开发人员带来 Gradle 用户体验上的根本性进步,这得益于开发者优先的软件定义、声明式 DSL 以及因此带来的开发工具的改进。

请注意,Declarative Gradle 仍处于实验阶段,不适用于生产环境。我们提供早期访问预览以收集社区的初步反馈。

声明式配置语言 #

Gradle 现有的 Kotlin 和 Groovy DSL 让用户可以访问完整的编程语言和生态系统。这使得构建脚本非常强大,但对于初学者来说,理解它们以及供应商在 Gradle 之上提供工具也变得更加困难。

通过 Declarative Gradle,我们引入了一种新的配置语言。除了 .gradle.gradle.kts 构建文件之外,Gradle 还会识别 .gradle.dcl,其中 DCL 代表“声明式配置语言”。

这种语言属于声明式配置语言家族,这意味着它是非过程性的,并且禁止常见的命令式构造,如循环、条件和函数。这与软件定义应表达“它应该做什么”,而不是“它应该如何做”的思想非常吻合。

声明式语言语法在技术上是 Kotlin 语言的一个小子集。声明式语言禁止任意代码和大多数 Kotlin 语言特性,同时保留了 Kotlin 语法的基本要素。解释声明式文件不涉及 Kotlin 编译器,并且速度极快。

我们的目标之一是,无需学习全新的语言,就能轻松地从非声明式文件迁移到 .gradle.dcl。声明式文件的语法与 Kotlin DSL 构建脚本语法几乎相同。

构建中的一个子项目只能使用一种 DSL:Kotlin、Groovy 或 DCL。为了方便将来进行增量迁移,构建可以在子项目之间混合和匹配不同的 DSL。例如,一个构建可以使用 settings.gradle.dcl 文件、一个 build.gradle.kts 根构建脚本,而所有其他子项目可以使用 build.gradle.dcl。某些 Declarative Gradle 功能可能仅在声明式 DSL 中有效,但构建可以使用任何 DSL 组合运行并由 IDE 导入。

开发者优先的配置 #

Declarative Gradle 的关键目标之一是让软件开发者更容易配置构建。开发者优先的配置将构建文件中软件定义的抽象级别与开发者熟悉的软件领域相匹配。这实质上意味着创建更高级别的构建模型,并在软件开发者和构建工程师之间提供更好的关注点分离。

即使采用当前的最佳实践,软件开发人员也常常需要具备 Gradle 特定知识才能更改对他们来说重要的事情。

为了解决这个问题,我们开发了**软件类型**的概念。软件类型是要构建的软件的高级模型。所有与软件开发人员相关的配置,如依赖项或目标平台,都位于一个地方。软件类型的示例包括 Java/Kotlin/Android 应用程序或库。在声明式构建文件中,软件开发人员只能使用软件类型来配置项目。

为了说明软件类型的概念,我们从一个典型的项目构建脚本今天可能的样子开始:

build.gradle.kts

plugins {
   id("application")
}

application.mainClass = "com.example.Main"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
    }
}

dependencies {
    implementation("com.google.guava:guava:31.0-jre")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}

tasks.named<Test>("test") {
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

除了 Test 任务配置之外,构建脚本已经非常声明式了。显而易见,它构建了一个带有主类定义、特定 Java 版本、一些依赖项的应用程序,并使用不同的 Java 版本运行测试。然而,它暴露了一些 Gradle 特定的概念,例如插件和任务,这些概念需要一些 Gradle 工作原理的知识。

如果多个项目以相同的方式配置,您可以通过将配置移动到构建逻辑中并将其打包为约定插件来共享配置。这是现代 Gradle 构建中组织构建逻辑的推荐方法。

约定插件看起来会是这样

com.example.java-application-conventions.gradle.kts

plugins {
   id("application")
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
    }
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}

tasks.named<Test>("test") {
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

项目的构建脚本将更改为

build.gradle.kts

plugins {
   id("com.example.java-application-conventions")
}

application.mainClass = "com.example.Main"

dependencies {
    implementation("com.google.guava:guava:31.0-jre")
}

与前面的示例相比,这是一个不错的改进。不同项目之间的共同点被提取并可重用,主构建文件更短且更具声明性。然而,软件开发人员仍然需要理解插件才能在所有子项目中进行更改,例如升级目标 Java 版本或 JUnit 版本。软件开发人员关注的问题,如 Java 版本的配置,仍然与构建工程师关注的问题,如 Test 任务配置,混杂在一起。

使用 Declarative Gradle 提供的软件类型,开发人员特定的配置部分会移动到声明式文件中,如下例所示。

build.gradle.dcl

javaApplication {
    mainClass = "com.example.Main"
    javaVersion = 11

    dependencies {
        implementation("com.google.guava:guava:31.0-jre")
    }

    testing {
        testJavaVersion = 17
        dependencies {
            implementation("org.junit.jupiter:junit-jupiter:5.10.2")
        }
    }
}

这确保了软件开发人员和构建工程师关注点之间的清晰分离。对于常见任务,开发人员根本不需要触及插件。他们只需要处理声明式配置,而不是编写构建脚本。请注意测试配置是如何简化并变得完全声明式的。此外,声明式格式对 IDE 集成也有好处,我们将在下一节中解释。

使用软件类型,常见的配置通过声明式设置文件表达。通过这种方法,您可以声明该软件类型的默认值。使用该软件类型的项目仍然可以根据需要覆盖属性。

这将看起来像

settings.gradle.dcl

plugins {
    id("org.gradle.experimental.jvm-ecosystem") version "0.1.7"
}

defaults {
    javaApplication {
        javaVersion = 11

        testing {
            testJavaVersion = 17
            dependencies {
                implementation("org.junit.jupiter:junit-jupiter:5.10.2")
            }
        }
    }
}

该项目将简化为

build.gradle.dcl

javaApplication {
    mainClass = "com.example.Main"

    dependencies {
        implementation("com.google.guava:guava:31.0-jre")
    }
}

单个项目只能声明一个软件类型。不再在每个项目中应用约定插件,而是在设置文件中应用新的生态系统插件,并在每个项目中提供可用的软件类型。根据项目中使用的软件类型,Gradle 将隐式应用支持该软件类型的相应插件。例如,如果一个项目声明它正在构建一个 Java 库,那么 org.gradle.java-library 插件将被隐式应用。

与现有的 Kotlin 或 Groovy DSL 构建脚本不同,在声明式软件定义文件中只提供软件类型,而不提供任何其他块,如 repositories {}configurations {}。我们的目的是消除在仅为特定类型的项目定义默认值的情况下对约定插件的需求。

软件类型配置起来更简单,因为高级模型更接近软件开发人员需要理解的内容,并且构建工程师的关注点被清晰地分离。在项目之间共享配置也无需额外的繁琐操作和编写 Gradle 插件。此外,这种方法还支持下面描述的 IDE 和工具改进。

更好的 IDE 体验 #

IDE 集成是开发者体验的基本组成部分,也是 Declarative Gradle 关注的重点领域。我们一直在探索如何通过更高级别的模型和更严格的配置语言来改进 IDE 体验。

得益于一个非常健壮的解析器,下面描述的所有功能在项目配置错误的情况下也能工作。这将允许 IDE 在损坏的构建上运行并更容易地修复错误。

与 IDE 对 Kotlin DSL 的支持一样,第一步是覆盖新声明式语言的基础——语法高亮和代码补全。第一个 EAP 在 Android Studio 中提供了基本的编辑器支持。IDE 为 .gradle.dcl 文件提供语法高亮以及软件类型及其属性的代码补全。代码补全是上下文感知的,这意味着 IDE 只提供相关的建议。由于声明式格式,IDE 辅助工作速度更快,因为它无需等待整个构建完成配置。

Declarative Gradle 倡议的另一个主要驱动力是,常规构建脚本的高度灵活性使得 Gradle 之外的工具难以理解和更改构建配置。变量、局部方法和条件表达式等代码构造的存在加剧了这些困难。通过将软件定义声明化,我们使供应商更容易提供更好的 IDE 支持。

这不仅仅是文件语法的问题。即使使用声明性文件,IDE 仍然需要编码大量知识来近似 Gradle 和 Gradle 插件对构建的理解。每个工具都倾向于使用自己的一套边缘情况和限制来重复这些知识。

我们相信有更好的方法。通过将此类更改的实现集中在 Gradle 及其插件生态系统中,我们可以使新工具更容易与 Gradle 集成,并在更改构建配置时使现有集成更加健壮。

声明式 DSL 和软件类型的结合允许外部工具(如 IDE)快速查询 Gradle 中的信息。这些信息以类似文档的数据结构表示,并跟踪信息回到其在声明式文件中的来源。这是 IDE 供应商创建工具的基础,这些工具允许他们更好地向软件开发人员呈现有关构建的信息。通过 Declarative Gradle,IDE 可以轻松地在 UI 中显示构建文件中定义的信息,并允许用户导航到信息定义的精确位置。

以下视频展示了我们为演示 IDE 中尚未提供的功能而构建的 GUI 应用程序 Gradle Client。它用于检查声明式软件定义,并提供配置模型的交互式可导航视图。它完全依赖于 Gradle 提供的 API。

最后,我们还提供了一种更简单的方式,让工具可以使用“变异”的概念自动更改软件定义。变异的简单示例包括添加依赖项、升级 Gradle 版本、添加特定类型的新子项目等。这个 EAP 提供了 API 来表达和运行此类软件定义的特定领域变异。

下一个视频展示了《Gradle 客户端》的实际操作,展示了可用的突变并修改了软件定义。

突变可以由 Gradle 本身、第三方插件、IDE 和其他外部工具定义。突变基础设施可以处理复杂情况,其中单个概念性更改导致构建的多个部分发生更改。

立即尝试 Declarative Gradle #

Declarative Gradle 项目由几个实验部分组成

  • Gradle 中支持 DCL 文件的更改
  • Android Studio 中支持 DCL 文件的更改
  • 原型插件演示软件类型和高级模型
  • 一个 Gradle 测试客户端演示了 IDE 中尚未实现的功能

我们下面链接的功能和示例需要 Gradle、Android Studio 和我们的 声明式 Gradle 原型插件 的每夜构建版本。这些插件将现有插件(如 Android Gradle 插件 (AGP) 和 Kotlin Multiplatform (KMP))封装到一个新的声明式模型中,该模型与 EAP 中所有可用的 Declarative Gradle 功能协同工作。请注意,我们的原型插件是一种临时措施,直到我们将这些想法重新整合到上游插件或 Gradle 本身中。

尝试声明式 Gradle

试用后,如果您能提交您的反馈,我们将不胜感激。这只需要几分钟,并且是匿名的,除非您选择提供您的电子邮件地址。

下一步是什么? #

这篇博客文章解释了我们迄今为止所做的工作。

在接下来的几个月里,我们将直接与 Google 的合作伙伴合作,通过直接暴露相关软件类型,使 Android Gradle 插件 (AGP) 与声明式 DSL 兼容,而无需额外插件。这将使非常简单的 Android 库和应用程序能够完全使用声明式 Gradle 构建。作为此过程的一部分,我们还将探索解决声明式 DSL 中 Gradle 插件中存在的构造(如集合、容器、枚举和可变参数)的方案。

稍后我们还将研究支持其他生态系统,如 Kotlin Multiplatform 或原生语言,并持续关注实验和展示新想法。

我们正在研究软件类型可扩展性和可组合性方面的问题。我们希望能够在构建逻辑插件中扩展软件类型模型,并在设置和项目声明文件中组合可选功能和共享配置。我们还在研究使软件类型在 Kotlin 和 Groovy DSL 中可用。使用软件类型的 Kotlin DSL 构建脚本应该与声明式 DSL 版本几乎相同。

我们正在与 Google 和 JetBrains 合作,在 Intellij IDEA 和 Android Studio 中提供出色的 IDE 集成,包括利用突变框架。我们还在与微软合作,并在我们自己的 Eclipse 插件 Buildship 中试验支持其他 IDE,如 Visual Studio Code。

最后,我们还将努力使使用声明式 DSL 启动新构建变得更加容易,就像 gradle init 今天使用 Kotlin 和 Groovy DSL 所做的那样。这将使尝试 Declarative Gradle 变得更加容易。

我们目前正在探索这些领域及更多。请在 declarative.gradle.org 关注我们的进展。

讨论