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

目录

引言

《Gradle 工作原理》系列前文回顾

  1. Gradle 工作原理第 1 部分 - 启动
  2. Gradle 工作原理第二部分 - 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 执行这些脚本时会发生什么?

简短的回答是:它们是基于 Kotlin 编程语言(用于 .gradle.kts 文件)或基于 Groovy 编程语言(用于 .gradle 文件)的 DSL(领域特定语言)。这些 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 插件的内部工作原理。

讨论