初识声明式 Gradle

在 2024 年 7 月,我们宣布了声明式 Gradle 的早期访问预览版 (EAP)。了解有关此版本的更多信息,试用示例并分享您的反馈!

目录

简介 #

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

这篇博客文章提供了关于项目进展的最新信息,并概述了您如何试用它,提供反馈,并影响我们的下一步。

First look at Declarative Gradle

什么是声明式 Gradle? #

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

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

请注意,声明式 Gradle 仍处于实验阶段,尚未准备好用于生产环境。我们正在提供早期访问预览版,以收集来自社区的初步反馈。

声明式配置语言 #

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

通过声明式 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。某些声明式 Gradle 功能可能仅在声明式 DSL 中有效,但构建可以使用任何 DSL 组合运行并由 IDE 导入。

开发者优先的配置 #

我们使用声明式 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 任务配置)混杂在一起。

使用声明式 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 集成是开发者体验的基本组成部分,也是声明式 Gradle 的主要关注领域。我们一直在探索如何通过更高级别的模型和更严格的配置语言来改善 IDE 体验。

得益于非常弹性的解析器,即使在项目配置错误的情况下,下面描述的所有功能也都可以工作。这将允许 IDE 在损坏的构建上运行,并使其更容易修复错误。

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

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

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

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

声明式 DSL 和软件类型的结合使外部工具(如 IDE)能够快速从 Gradle 查询信息。此信息以类似文档的数据结构表示,并将信息追溯到其在声明式文件中的来源。这对于 IDE 供应商创建工具至关重要,这些工具使他们能够更好地向软件开发人员呈现有关构建的信息。借助声明式 Gradle,IDE 可以轻松地在 UI 中显示在构建文件中定义的信息,并让用户导航到信息定义的确切位置。

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

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

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

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

立即试用声明式 Gradle #

声明式 Gradle 项目由几个实验性部分组成

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

我们下面链接到的功能和示例需要 Gradle、Android Studio 和我们的声明式 Gradle 原型插件的每夜构建版本。这些插件使用新的声明式模型包装了现有的插件,如 Android Gradle 插件 (AGP) 和 Kotlin Multiplatform (KMP),该模型适用于 EAP 中提供的所有声明式 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 集成,包括利用变更框架。我们还在与 Microsoft 合作的 Visual Studio Code 以及我们自己的 Eclipse 插件 Buildship 中试验支持。

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

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

讨论