Gradle 工作原理第 3 部分 - 构建脚本

目录

简介

先前在 Gradle 工作原理 系列中

  1. Gradle 工作原理第 1 部分 - 启动
  2. Gradle 工作原理第 2 部分 - Daemon 内部

这是 Gradle 工作原理 系列的第三篇博客。在这篇博客中,我们将解释构建脚本执行期间发生的事情。

Kotlin & Groovy DSL #

如果您是一名 Java 开发人员,当您打开任何 Gradle 构建脚本(例如 build.gradle.ktsbuild.gradle)时,首先可能会让您感到困惑的是花括号的特殊语法

// Kotlin DSL:

plugins {
    id("some.plugin") version "0.0.1"
}

// or Groovy DSL:

plugins {
    id "some.plugin" version "0.0.1"
}

这是什么?当 Gradle 执行这些类型的脚本时会发生什么?

简短的答案是:它们是 DSL(领域特定语言),基于 Kotlin 编程语言用于 .gradle.kts 文件,或基于 Groovy 编程语言用于 .gradle 文件。这些 DSL 有一些隐式规则,使它们看起来非常令人困惑。

隐式规则 1:Lambda/闭包 #

首先,{ ... } 在 Groovy 和 Kotlin 中都是一个特殊的对象。此对象在 Kotlin 中称为 lambda,或在 Groovy 中称为闭包。它们类似于其他编程语言中的函数对象,例如 Java 的 lambda 或 JavaScript 的函数对象。

您可以将 plugins { ... } 视为方法调用,其中 Kotlin lambda 对象或 Groovy 闭包对象作为参数传递,因为 Groovy 和 Kotlin 都允许您省略括号

plugins(function() {
    ...
})

此外,还有一个值得注意的 DSL:在 Kotlin/Groovy 中,如果函数的最后一个参数是 lambda/闭包,则允许将其放在括号之外。例如,以下代码片段

tasks.register("myTask") {
    ...
    doLast {
        ...
    }
}

等效于

tasks.register("myTask", function() {
    ...
    doLast(function() {
        ...
    })
})

函数内部的代码可能会立即执行,也可能会稍后执行,具体取决于特定方法的实现。

隐式规则 2:链式方法调用 #

在上面的 plugins { } 示例中,Kotlin 版本:id("some.plugin") version "0.0.1" 和 Groovy 版本:id "some.plugin" version "0.0.1" 都等效于链式方法调用 id("some.plugin").version("0.0.1")

等等,为什么?

因为 id("some.plugin") version "0.0.1" 中的 version 实际上是 Kotlin 中的一个 中缀函数,它在 这里 定义,而 id "some.plugin" version "0.0.1" 是 Groovy 中的 “命令链”

我们不会解释 Groovy 和 Kotlin DSL 中的所有隐式规则(因为那需要另一个完整的博客系列 :-P),您应该简单地理解它们以某种方式映射到 Gradle API 方法。请参阅 Kotlin DSL 入门Groovy DSL 入门

但是方法调用 id("some.plugin")this 是什么?

函数内部的代码是针对 this 对象执行的,该对象在 Kotlin lambda 中称为“接收器”,或在 Groovy 闭包中称为“委托”。Gradle 确定正确的 this 对象,并针对该 this 对象调用该方法。在此示例中,this 对象的 类型为 PluginDependenciesSpec

构建脚本执行 #

一旦我们揭示了 DSL 的所有机制,Gradle 构建脚本就只是基于 DSL 构建的一些 Gradle API 调用。像几乎所有其他编程语言一样,Gradle 逐行、从上到下执行构建脚本。

当然,构建脚本必须先编译成字节码,然后才能在 JVM 中执行。Gradle 透明地执行此操作,给人一种构建脚本正在被解释和执行的印象。

构建脚本中的外部依赖 #

考虑以下构建脚本

// build.gradle.kts
import com.android.build.gradle.tasks.LintGlobalTask

plugins {
    id("com.android.application") version "7.4.0"
}

tasks.withType<LintGlobalTask>().configureEach {
    ...
}

如何在不指定 com.android.build.gradle.tasks.LintGlobalTask 依赖项的情况下编译此构建脚本?您可能会说,“它不是来自下面的 plugins { } 块吗?”

但请记住,要执行 plugins { } 块,必须首先编译构建脚本。现在这是一个鸡和蛋的问题:要获取 LintGlobalTask 的依赖项,我们必须编译并运行构建脚本,但要编译构建脚本,我们必须获取 LintGlobalTask 的依赖项。

Gradle 特别处理 plugins { },如下所示

  1. plugins 块首先被提取并执行;
  2. 已解析的依赖项被添加到整个构建脚本的类路径中;
  3. 构建脚本被编译并执行。

类似的事情也发生在 buildscript { } 块中:您可以显式指定构建脚本编译和执行的依赖项。通过这种方式,您可以利用 JVM 生态系统中的许多可用库来增强您的构建脚本。

下一步是什么 #

构建脚本调用 Gradle API 来配置构建,其中会发生令人惊奇的事情。可以将构建脚本打包到 Gradle 插件中,以获得更好的可重用性和性能。

在本系列的下一篇博客中,我们将解释 Gradle 插件的幕后工作原理。

讨论