读书笔记《gradle-effective-implementations-guide-second-edition》创建Gradle构建脚本
在 Gradle 中,项目和任务是两个重要的概念。 Gradle 构建始终包含一个或多个项目。一个项目定义了我们想要构建的某种组件。没有关于组件是什么的定义规则。它可以是一个 JAR 文件,其中包含要在其他项目中使用的实用程序类,或者是要部署到公司 Intranet 的 Web 应用程序。项目不一定是关于构建和打包代码,它也可以是关于在远程服务器上复制文件或将应用程序部署到服务器等事情。
一个项目有一个或多个任务。任务是我们运行构建时执行的一小部分工作,例如编译源代码、将代码打包到存档文件中、生成文档等。
在本章中,我们将讨论如何定义一个包含任务的项目并将其用作 Gradle 构建。我们将涵盖以下主题:
定义任务
定义任务之间的依赖关系
组织任务和处理它的方法
在第一章中,我们已经编写了我们的第一个构建脚本。让我们用一个简单的任务创建一个类似的构建脚本。 Gradle 将在当前目录中查找名为 build.gradle
的文件。 build.gradle
文件包含构成我们项目的任务。在这个例子中,我们定义了一个简单的任务,它会向控制台打印一条简单的消息:
如果我们运行构建,我们会在控制台中看到以下输出:
这个小的构建脚本会发生一些有趣的事情。 Gradle 读取脚本文件并创建一个 Project
对象。构建脚本配置Project
对象,最后确定并执行要执行的任务集。
因此,需要注意的是,Gradle 为我们创建了一个 Project
对象。 Project
对象具有多个属性和方法,可在我们的构建脚本中使用。我们可以使用 project
变量名来引用 Project
对象,但我们也可以省略这个变量名来引用Project
对象的属性和方法。 Gradle 会自动尝试将构建脚本中的属性和方法映射到 Project
对象。
在我们的简单构建脚本中,我们将 Simple project
值分配给 description
项目属性。我们使用了显式的项目变量名和 Groovy 属性赋值语法。以下构建脚本使用不同的语法(有点像 Java)来获得相同的结果:
在这里,我们使用 Java 语法来设置和获取 Project
对象的 description 属性的值。我们的语法非常灵活,但是我们将在本书的其余部分坚持使用 Groovy 语法,因为它会产生更易读的构建脚本。
一个项目有一个或多个任务来执行一些动作,所以一个任务是由动作组成的。这些动作在任务执行时执行。 Gradle 支持多种向我们的任务添加操作的方法。在本节中,我们将讨论向任务添加操作的不同方法。
我们可以使用 doFirst
和 doLast
方法向我们的任务添加动作,我们可以使用左移运算符 ( <<
) 作为 doLast
方法的同义词。使用 doLast
方法或左移运算符 (<<
),我们在末尾添加操作任务的操作列表。使用 doFirst
方法,我们可以将动作添加到动作列表的开头。以下脚本显示了我们如何使用这几种方法:
当我们运行脚本时,我们得到以下输出:
对于 second
任务,我们使用 doLast
方法添加打印文本的操作。该方法接受一个闭包作为参数。 task
对象作为参数传递给闭包。这意味着我们可以在我们的操作中使用 task
对象。在示例构建文件中,我们获取 task
的 name
属性的值并将其打印到控制台。
也许现在是仔细研究闭包的好时机,因为它们是 Groovy 的重要组成部分,并且在整个 Gradle 构建脚本中都使用过。闭包基本上是可重用的代码片段,可以分配给变量或传递给方法。闭包是通过用大括号 ({... }
) 将一段代码括起来来定义的。我们可以将一个或多个参数传递给闭包。如果闭包只有一个参数,则可以使用隐式参数it
来引用参数值。我们可以编写如下的 second
任务,结果仍然是一样的:
我们还可以为参数定义一个名称,并在代码中使用这个名称。这就是我们为 second
和 third
任务所做的;其中,我们将闭包参数分别命名为 task
和 taskObject
。如果我们在闭包中明确定义参数 name
,则生成的代码更具可读性,如下所示:
到目前为止,我们已经定义了彼此独立的任务。但是,在我们的项目中,我们需要任务之间的依赖关系。例如,打包已编译类文件的任务依赖于编译类文件的任务。然后构建系统应该首先运行编译任务,当任务完成后,必须执行打包任务。
在 Gradle 中,我们可以使用 dependsOn
方法为任务添加任务依赖项。我们可以将任务名称指定为 String
值或task
对象作为参数。我们甚至可以指定多个任务名称或对象来指定多个任务依赖项。首先,我们来看一个简单的任务依赖:
请注意,我们在最后一行定义了 second
任务对 first
任务的依赖关系。当我们运行脚本时,我们看到 first
任务在 second
任务之前执行:
定义任务之间依赖关系的另一种方法是设置 dependsOn
属性,而不是使用 dependsOn
方法。有一个细微的差别,Gradle 只是提供了几种方法来实现相同的结果。在下面的代码中,我们使用属性来定义 second
任务的依赖关系。对于 第三
任务,我们在定义任务时立即定义属性:
当我们在命令行运行 third
任务时,我们看到三个任务都被执行了,如下:
任务之间的依赖是惰性的。我们可以定义对稍后在构建脚本中定义的任务的依赖。 Gradle 将在配置阶段而不是在执行阶段设置所有任务依赖项。以下脚本显示构建脚本中任务的顺序无关紧要:
我们现在有了包含三个任务的构建脚本,但是每个任务都做同样的事情:它打印一个带有任务名称的字符串。最好记住,我们的构建脚本只是代码,并且可以组织和重构代码以创建更清晰的代码。这也适用于 Gradle 构建脚本。仔细查看您的构建脚本并查看是否可以更好地组织事情以及代码是否可以重用而不是重复是很重要的。甚至我们简单的构建脚本也可以重写如下:
这可能看起来微不足道,但重要的是要了解我们可以将在应用程序代码中使用的相同编码技术应用于构建代码。
在我们的构建脚本中,我们使用任务名称定义了任务依赖项。但是,还有更多方法可以定义任务依赖关系。我们可以使用 task
对象代替任务名称来定义任务依赖:
在第 1 章中, 从 Gradle 开始,我们已经讨论过可以使用 Gradle 的 tasks 任务来查看可用于构建的任务。假设我们有以下简单的构建脚本:
这里没有什么花哨的。 second
任务是默认任务,它依赖于 first
任务。当我们在命令行运行 tasks
任务时,我们得到以下输出:
我们在 Other tasks
部分看到名为 second
的任务,但没有看到名为 first.
要查看所有任务,包括其他任务所依赖的任务,我们必须在 tasks 命令中添加选项 --all
:
现在我们看到名为 first 的任务。
Gradle 甚至缩进了依赖任务,以便我们可以看到 second
任务取决于 first
任务。
在输出的开头,我们看到以下行:
Gradle 向我们展示了构建中的默认任务。
为了描述我们的任务,我们可以设置任务的 description
属性。 description
属性的值被 Gradle 的 task
使用。让我们为我们的两个任务添加一个描述,如下:
现在,当我们运行 tasks 任务时,我们会得到一个更具描述性的输出:
使用 Gradle,我们还可以将任务分组到所谓的 task groups 中。任务组是逻辑上属于一起的一组任务。例如,在我们之前使用的 tasks
任务的输出中使用了任务组。让我们通过将两个任务组合到一个示例任务组中来扩展我们的示例构建脚本。我们必须为任务的 group
属性赋值:
下次我们运行 tasks
任务时,我们可以看到我们的任务在 Base tasks
部分中分组在一起:
请注意,任务依赖项附加到 second
任务的 description
属性中。
到目前为止,我们已经使用 task
关键字将任务添加到我们的构建项目中,然后是任务的名称。但是,还有更多方法可以将任务添加到我们的项目中。我们可以使用带有任务名称的 String
值来定义一个新任务,如下所示:
我们还可以使用变量表达式来定义一个新任务。如果这样做,我们必须使用括号,否则表达式无法解析。以下示例脚本使用 simple
字符串值定义了一个 simpleTask
变量。此表达式用于定义任务。结果是我们的项目现在包含一个名为 simple:
的任务
我们可以运行 tasks
任务来查看我们新创建的任务:
我们还可以使用 Groovy 的强大功能来添加新任务。我们可以使用 Groovy 的 GString
表示法来动态创建任务名称。就像在前面的示例中使用表达式一样,但是用 Groovy GString:
表示
如果我们运行 tasks
任务,我们可以看到我们有四个新任务,如下:
添加新任务的另一种方法是通过项目的 tasks
属性。请记住,在我们的构建脚本中,我们可以访问 Project
对象;我们要么显式使用 project
变量,要么隐式使用 Project
对象的方法和属性,而不使用 项目
变量。项目的 tasks
属性基本上是我们项目中所有任务的容器。在以下构建脚本中,我们使用 create
方法添加新任务:
我们已经了解了如何将任务动态添加到我们的构建项目中。但是,我们也可以定义所谓的任务规则。这些规则非常灵活,允许我们根据几个参数和项目属性向项目添加任务。
假设,我们想要添加一个额外的任务来显示我们项目中每个任务的描述。如果我们的项目中首先有一个任务,我们想添加一个 descFirst
任务来显示 description
属性class="literal">第一个任务。通过任务规则,我们为新任务定义了一个模式。在我们的示例中,这是 desc<TaskName>
;它是 desc
前缀,后跟现有任务的名称。以下构建脚本显示了任务规则的实现:
如果我们运行 tasks
任务,我们会在输出中看到一个额外的 Rules
部分:
所以,我们知道我们可以为我们的项目调用 descFirst
和 descSecond
。请注意,这两个额外的任务没有显示在 Other tasks
部分,但 Rules
部分显示了我们可以使用的模式。
如果我们执行 descFirst
和 descSecond
任务,我们会得到以下输出:
有时,我们希望将任务从构建中排除。在某些情况下,我们只想跳过一个任务并继续执行其他任务。我们可以使用几种方法来跳过 Gradle 中的任务。
每个任务都有一个 onlyIf
方法,该方法接受一个闭包作为参数。闭包的结果必须是 true
或 false。
如果必须跳过任务,闭包的结果必须是 < code class="literal">false,否则执行任务。 task
对象作为参数传递给闭包。 Gradle 在任务执行之前评估闭包。
以下构建文件将跳过 longrunning
任务,如果该文件在工作日执行,但将在周末执行:
如果我们在工作日运行构建,我们会得到以下输出:
如果我们在周末运行构建,我们会看到任务已执行:
我们可以为一个任务多次调用 onlyIf
方法。如果谓词之一返回 false
,则跳过该任务。除了使用闭包来定义确定任务是否需要执行的条件外,我们还可以使用 org.gradle.api.specs.Spec
接口的实现. Spec
接口有一个方法:isSatisfiedBy
。如果必须执行任务,我们必须编写一个实现并返回 true
,如果我们希望跳过该任务,则返回false
.当前的 task
对象作为参数传递给 isSatisfiedBy
方法。
在下面的示例中,我们检查文件是否存在。如果文件存在,我们可以执行任务,否则跳过任务:
跳过执行任务的另一种方法是抛出 StopExecutionException
异常。如果抛出这样的异常,构建将停止当前任务并继续下一个任务。我们可以使用 doFirst
方法为任务添加前置条件检查。在闭包中,当我们传递给 doFirst
方法时,我们可以检查条件并在必要时抛出 StopExecutionException
异常。
在下面的构建脚本中,我们检查脚本是否在工作时间执行。如果是这样,则抛出异常并跳过 first
任务:
如果我们在工作时间运行脚本并检查构建脚本的输出,我们会注意到我们看不到任务已被跳过。如果我们使用 onlyIf
方法,Gradle 会将 SKIPPED
添加到未执行的任务中:
我们已经看到如何使用 onlyIf
方法或抛出 StopExecutionException
来跳过任务。但是,我们也可以使用另一种方法来跳过任务。每个任务都有一个 enabled
属性。默认情况下,该属性的值为 true,
表示启用并执行任务。我们可以更改该值并将其设置为 false
以禁用任务并跳过其执行。
在下面的示例中,我们检查目录是否存在,如果存在,则将 enabled
属性设置为 true;
如果不是,则设置为 false:
如果我们运行任务并且目录不存在,我们会得到以下输出:
如果我们运行任务,并且这次目录存在,包含一个名为 sample.txt
的文件,我们会得到以下输出:
到目前为止,我们已经定义了跳过构建文件中的任务的规则。但是,如果我们运行构建,我们可以使用 --exclude-tasks
(-x
) 命令行选项。我们必须将要从要执行的任务中排除的任务定义为参数。
以下脚本具有三个具有一些任务依赖性的任务:
如果我们运行 gradle
命令并排除 second
任务,我们会得到以下输出:
如果我们的 third
任务不依赖于第一个任务,那么只有 third
任务会被执行。
到目前为止,我们已经定义了评估条件以确定是否需要跳过任务。但是,使用 Gradle,我们可以更加灵活。假设我们有一个任务处理一个文件并根据该文件生成一些输出。例如,编译任务就适合这种模式。在以下示例构建文件中,我们有 convert
任务,它将获取 XML 文件、解析内容并将数据写入文本文件,如以下代码所示:
我们可以多次运行这个任务。每次都从 XML 文件中读取数据并写入文本文件:
但是,我们的输入文件在任务调用之间没有改变,因此不必执行任务。我们希望仅在源文件已更改、输出文件丢失或自上次运行任务以来已更改时才执行任务。
Gradle 支持这种模式,这种支持称为 增量构建支持。仅在必要时才需要执行任务。这是 Gradle 的一个非常强大的特性。它将真正加快构建过程,因为只执行需要执行的任务。
我们需要更改任务的定义,以便 Gradle 可以根据任务的输入文件或输出文件的更改来确定是否需要执行任务。任务具有用于此目的的属性 inputs
和 outputs
。要定义输入文件,我们使用输入文件的值调用 inputs
属性的 file
方法。我们通过调用 outputs
属性的 file
方法来设置输出文件。
让我们重写我们的任务,使其支持 Gradle 的增量构建功能:
当我们运行构建文件几次时,我们看到我们的任务在第二次运行时被跳过,因为输入和输出文件没有改变:
我们可以使用 --rerun-tasks
命令行选项来忽略增量构建功能。当我们使用此命令行选项时,Gradle 不会检查任何条件,并且无论 inputs
或输出的条件如何,Gradle 都会执行任务
属性。
让我们使用这个选项并再次运行我们的 convert
任务。这一次,即使源文件和输出文件没有改变,任务也会被执行:
我们为 inputs
和 outputs
属性定义了一个文件。但是,Gradle 支持更多方式来定义这些属性的值。 inputs
属性具有添加目录、多个文件甚至要监视更改的属性的方法。 outputs
属性具有添加要监视更改的目录或多个文件的方法。如果这些方法不适合我们的构建,我们甚至可以将 upToDateWhen
方法用于 outputs
属性。我们通过 org.gradle.api.specs.Spec
接口的闭包或实现来定义判断任务输出是否是最新的谓词。
以下构建脚本使用其中一些方法: