vlambda博客
学习文章列表

读书笔记《gradle-effective-implementations-guide-second-edition》创建Gradle构建脚本

Chapter 2.  Creating Gradle Build Scripts

在 Gradle 中,项目和任务是两个重要的概念。 Gradle 构建始终包含一个或多个项目。一个项目定义了我们想要构建的某种组件。没有关于组件是什么的定义规则。它可以是一个 JAR 文件,其中包含要在其他项目中使用的实用程序类,或者是要部署到公司 Intranet 的 Web 应用程序。项目不一定是关于构建和打包代码,它也可以是关于在远程服务器上复制文件或将应用程序部署到服务器等事情。

一个项目有一个或多个任务。任务是我们运行构建时执行的一小部分工作,例如编译源代码、将代码打包到存档文件中、生成文档等。

在本章中,我们将讨论如何定义一个包含任务的项目并将其用作 Gradle 构建。我们将涵盖以下主题:

  • 定义任务

  • 定义任务之间的依赖关系

  • 组织任务和处理它的方法

Writing a build script


在第一章中,我们已经编写了我们的第一个构建脚本。让我们用一个简单的任务创建一个类似的构建脚本。 Gradle 将在当前目录中查找名为 build.gradle 的文件。  build.gradle 文件包含构成我们项目的任务。在这个例子中,我们定义了一个简单的任务,它会向控制台打印一条简单的消息:

// Assign value to description property. 
project.description = 'Simple project' 
 
// DSL to create a new task using 
// Groovy << operator. 
task simple << { 
    println 'Running simple task for project ' + 
      project.description 
} 

如果我们运行构建,我们会在控制台中看到以下输出:

:simple
Running simple task for project Simple project
BUILD SUCCESSFUL
Total time: 0.57 secs

这个小的构建脚本会发生一些有趣的事情。 Gradle 读取脚本文件并创建一个 Project 对象。构建脚本配置Project对象,最后确定并执行要执行的任务集。

因此,需要注意的是,Gradle 为我们创建了一个 Project 对象。  Project 对象具有多个属性和方法,可在我们的构建脚本中使用。我们可以使用 project变量名来引用 Project对象,但我们也可以省略这个变量名来引用Project 对象的属性和方法。 Gradle 会自动尝试将构建脚本中的属性和方法映射到 Project 对象。

在我们的简单构建脚本中,我们将 Simple project 值分配给 description 项目属性。我们使用了显式的项目变量名和 Groovy 属性赋值语法。以下构建脚本使用不同的语法(有点像 Java)来获得相同的结果:

// Use setDescription method 
// to assign value instead of 
// Groovy assignment. 
project.setDescription('Simple project') 
 
// Use create method to add new 
// task instead of Groovy << operator. 
project.getTasks().create('simple') { 
    println 'Running simple task for project ' + 
      project.description 
} 

在这里,我们使用 Java 语法来设置和获取 Project 对象的 description 属性的值。我们的语法非常灵活,但是我们将在本书的其余部分坚持使用 Groovy 语法,因为它会产生更易读的构建脚本。

Defining tasks


一个项目有一个或多个任务来执行一些动作,所以一个任务是由动作组成的。这些动作在任务执行时执行。 Gradle 支持多种向我们的任务添加操作的方法。在本节中,我们将讨论向任务添加操作的不同方法。

我们可以使用 doFirst 和 doLast 方法向我们的任务添加动作,我们可以使用左移运算符 ( <<) 作为 doLast 方法的同义词。使用 doLast 方法或左移运算符 (<<),我们在末尾添加操作任务的操作列表。使用 doFirst 方法,我们可以将动作添加到动作列表的开头。以下脚本显示了我们如何使用这几种方法:

task first { 
    doFirst { 
        println 'Running first' 
    } 
} 
 
task second { 
    doLast { Task task -> 
        println "Running ${task.name}" 
    } 
} 
 
// Here we use the << operator 
// as synonym for the doLast method. 
task third << { taskObject -> 
    println 'Running ' + taskObject.name 
} 

当我们运行脚本时,我们得到以下输出:

$ gradle first second third
:first
Running first
:second
Running second
:third
Running third
BUILD SUCCESSFUL
Total time: 0.592 secs

对于 second 任务,我们使用 doLast 方法添加打印文本的操作。该方法接受一个闭包作为参数。  task 对象作为参数传递给闭包。这意味着我们可以在我们的操作中使用 task 对象。在示例构建文件中,我们获取 task的 name属性的值并将其打印到控制台。

也许现在是仔细研究闭包的好时机,因为它们是 Groovy 的重要组成部分,并且在整个 Gradle 构建脚本中都使用过。闭包基本上是可重用的代码片段,可以分配给变量或传递给方法。闭包是通过用大括号 ({... }) 将一段代码括起来来定义的。我们可以将一个或多个参数传递给闭包。如果闭包只有一个参数,则可以使用隐式参数it 来引用参数值。我们可以编写如下的 second 任务,结果仍然是一样的:

task second { 
    doLast { 
        // Using implicit 'it' closure parameter. 
        // The type of 'it' is a Gradle task. 
        println "Running ${it.name}" 
    } 
} 

我们还可以为参数定义一个名称,并在代码中使用这个名称。这就是我们为 second 和 third 任务所做的;其中,我们将闭包参数分别命名为 task和 taskObject。如果我们在闭包中明确定义参数 name,则生成的代码更具可读性,如下所示:

task second { 
    doLast { Task task -> 
        // Using explicit name 'task' as closure parameter. 
        // We also defined the type of the parameter. 
        // This can help the IDE to add code completion. 
        println "Running ${task.name}" 
    } 
} 

Defining actions with the Action interface

正如我们将在整本书中看到的那样,Gradle 通常有不止一种定义方式。除了使用闭包向任务添加动作外,我们还可以采用更详细的方式来传递 org.gradle.api.Action 接口的实现类。  Action 接口有一个方法: execute。执行任务时调用此方法。以下代码显示了我们构建脚本中第一个任务的重新实现:

task first { 
    doFirst( 
        new Action() { 
          void execute(O task) { 
            println "Running ${task.name}" 
          } 
       } 
    ) 
} 

很高兴知道我们在为任务定义动作时可以选择,但闭包语法更密集且更具可读性。在本书中,我们将使用闭包来进一步配置对象。

Build scripts are Groovy code


我们必须记住 Gradle 脚本使用 Groovy。这意味着我们可以在脚本中使用所有 Groovy 的好东西。我们已经在示例脚本中看到了所谓的 Groovy GString 的使用。  GString 对象被定义为带有双引号的 String,并且可以包含对定义在 a ${... } 部分。当我们得到 GString的值时,变量引用就被解析了。

但是,其他优秀的 Groovy 结构也可以在 Gradle 脚本中使用。以下示例脚本显示了其中一些结构:

task numbers << { 
    // To define a range of numbers 
    // we can use the following syntax: 
    // start..end. 
    // The each method executes the code 
    // in the closure for each element 
    // in a collection, like a range. 
    (1..4).each { number -> 
        // def is short for define. 
        // Used to define a variable without 
        // an explicit type of the variable. 
        def squared = number * number 
 
        // Use GString with ${} expression 
        // to get a new string value where 
        // the value references are replaced 
        // with the actual values. 
        println "Square of ${number} = ${squared}" 
    } 
} 
 
task list { 
    doFirst { 
        // Simple notation to define a list of values 
        // in Groovy using square brackets. 
        def list = ['Groovy', 'Gradle'] 
 
        // Groovy makes working with collections 
        // easy and adds utility methods to work 
        // with elements in the collection. 
        // The collect method transform each element 
        // in the original collection with the return 
        // value of the closure. Here all string elements 
        // are turned into lower case values. 
        // The join method joins all elements separated by 
        // the given character. The end result is 
        // groovy&gradle 
        println list.collect { it.toLowerCase() }.join('&') 
    } 
} 

当我们运行脚本时,我们得到以下输出:

$ gradle -q numbers list
Square of 1 = 1
Square of 2 = 4
Square of 3 = 9
Square of 4 = 16
groovy&gradle

Defining dependencies between tasks


到目前为止,我们已经定义了彼此独立的任务。但是,在我们的项目中,我们需要任务之间的依赖关系。例如,打包已编译类文件的任务依赖于编译类文件的任务。然后构建系统应该首先运行编译任务,当任务完成后,必须执行打包任务。

在 Gradle 中,我们可以使用 dependsOn 方法为任务添加任务依赖项。我们可以将任务名称指定为 String 值或task 对象作为参数。我们甚至可以指定多个任务名称或对象来指定多个任务依赖项。首先,我们来看一个简单的任务依赖:

task first << { task -> 
    println "Run ${task.name}" 
} 
 
task second << { task -> 
    println "Run ${task.name}" 
} 
 
// Define dependency of task second on task first 
second.dependsOn 'first' 

请注意,我们在最后一行定义了 second 任务对 first 任务的依赖关系。当我们运行脚本时,我们看到 first任务在 second任务之前执行:

$ gradle second
:first
Run first
:second
Run second
BUILD SUCCESSFUL
Total time: 0.583 secs

定义任务之间依赖关系的另一种方法是设置 dependsOn 属性,而不是使用 dependsOn 方法。有一个细微的差别,Gradle 只是提供了几种方法来实现相同的结果。在下面的代码中,我们使用属性来定义 second任务的依赖关系。对于 第三任务,我们在定义任务时立即定义属性:

task first << { task -> 
    println "Run ${task.name}" 
} 
 
task second << { task -> 
    println "Run ${task.name}" 
} 
 
// Use property syntax to define dependency. 
// dependsOn expects a collection object. 
second.dependsOn = ['first'] 
 
// Define dependsOn when we create the task. 
task third(dependsOn: 'second') << { task -> 
    println "Run ${task.name}" 
} 

当我们在命令行运行 third 任务时,我们看到三个任务都被执行了,如下:

$ gradle -q third
Run first
Run second
Run third

任务之间的依赖是惰性的。我们可以定义对稍后在构建脚本中定义的任务的依赖。 Gradle 将在配置阶段而不是在执行阶段设置所有任务依赖项。以下脚本显示构建脚本中任务的顺序无关紧要:

task third(dependsOn: 'second') << { task -> 
    println "Run ${task.name}" 
} 
 
task second(dependsOn: 'first') << { task -> 
    println "Run ${task.name}" 
} 
 
task first << { task -> 
    println "Run ${task.name}" 
} 

我们现在有了包含三个任务的构建脚本,但是每个任务都做同样的事情:它打印一个带有任务名称的字符串。最好记住,我们的构建脚本只是代码,并且可以组织和重构代码以创建更清晰的代码。这也适用于 Gradle 构建脚本。仔细查看您的构建脚本并查看是否可以更好地组织事情以及代码是否可以重用而不是重复是很重要的。甚至我们简单的构建脚本也可以重写如下:

// We assign the task closure 
// to a variable. We can reuse 
// the variable name in our task definitions. 
def printTaskName = { task -> 
    println "Run ${task.name}" 
} 
 
// We use the variable with the closure. 
task third(dependsOn: 'second') << printTaskName 
 
task second(dependsOn: 'first') << printTaskName 
 
task first << printTaskName 

这可能看起来微不足道,但重要的是要了解我们可以将在应用程序代码中使用的相同编码技术应用于构建代码。

Defining dependencies via tasks

在我们的构建脚本中,我们使用任务名称定义了任务依赖项。但是,还有更多方法可以定义任务依赖关系。我们可以使用 task 对象代替任务名称来定义任务依赖:

def printTaskName = { task -> 
    println "Run ${task.name}" 
} 
 
task first << printTaskName 
 
// Here we use first (not the string value 'first') 
// as a value for dependsOn. 
task second(dependsOn: first) << printTaskName 

Defining dependencies via closures

我们还可以使用闭包来定义任务依赖关系。闭包必须返回单个任务名称或对象,或者任务名称或 task 对象的集合。使用这种技术,我们可以真正微调任务的依赖关系。例如,在下面的构建脚本中,我们定义了  second 任务对项目中所有任务名称具有字母 f 在任务名称中:

def printTaskName = { task -> 
    println "Run ${task.name}" 
} 
 
task second << printTaskName 
 
// We use the dependsOn method 
// with a closure. 
second.dependsOn { 
    // We use the Groovy method findAll 
    // that returns all tasks that 
    // apply to the condition we define 
    // in the closure: the task name 
    // starts with the letter 'f'. 
    project.tasks.findAll { task -> 
        task.name.contains 'f' 
    } 
} 
 
task first << printTaskName 
 
task beforeSecond << printTaskName 

当我们运行构建项目时,我们得到以下输出:

$ gradle second
:beforeSecond
Run beforeSecond
:first
Run first
:second
Run second
BUILD SUCCESSFUL
Total time: 0.602 secs

Setting default tasks


要执行任务,我们在运行 gradle 时在命令行中使用任务名称。因此,如果我们的构建脚本包含具有 first 名称的任务,我们可以使用以下命令运行该任务:

$ gradle first

但是,我们也可以定义一个默认任务或多个需要执行的默认任务,即使我们没有明确设置任务名称。因此,如果我们运行不带参数的 gradle 命令,我们的构建脚本的默认任务将被执行。

要设置默认任务或任务,我们使用 defaultTasks 方法。我们将需要执行的任务的名称传递给该方法。在以下构建脚本中,我们将 firstsecond 任务设为默认任务:

defaultTasks 'first', 'second' 
 
task first { 
    doLast { 
        println "I am first" 
    } 
} 
 
task second { 
    doFirst { 
        println "I am second" 
    } 
} 

我们可以运行我们的构建脚本并获得以下输出:

$ gradle
:first
I am first
:second
I am second
BUILD SUCCESSFUL
Total time: 0.558 secs

Organizing tasks


第 1 章中, 从 Gradle 开始,我们已经讨论过可以使用 Gradle 的 tasks 任务来查看可用于构建的任务。假设我们有以下简单的构建脚本:

defaultTasks 'second' 
 
task first << { 
    println "I am first" 
} 
 
task second(dependsOn: first) << { 
    println "I am second" 
} 

这里没有什么花哨的。 second 任务是默认任务,它依赖于 first 任务。当我们在命令行运行 tasks 任务时,我们得到以下输出:

$ gradle -q tasks
------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------
Default tasks: second
Build Setup tasks
-----------------
init - Initializes a new Gradle build. [incubating]
wrapper - Generates Gradle wrapper files. [incubating]
Help tasks
----------
components - Displays the components produced by root project 'organize'. [incubating]
dependencies - Displays all dependencies declared in root project 'organize'.
dependencyInsight - Displays the insight into a specific dependency in root project 'organize'.
help - Displays a help message.
model - Displays the configuration model of root project 'organize'. [incubating]
projects - Displays the sub-projects of root project 'organize'.
properties - Displays the properties of root project 'organize'.
tasks - Displays the tasks runnable from root project 'organize'.
Other tasks
-----------
second
To see all tasks and more detail, run gradle tasks --all
To see more detail about a task, run gradle help --task <task>

我们在 Other tasks 部分看到名为 second 的任务,但没有看到名为 first. 要查看所有任务,包括其他任务所依赖的任务,我们必须在 tasks 命令中添加选项 --all

$ gradle tasks --all
...
Other tasks
-----------
second
 first

现在我们看到名为 first 的任务。 Gradle 甚至缩进了依赖任务,以便我们可以看到 second 任务取决于 first 任务。

在输出的开头,我们看到以下行:

Default tasks: second

Gradle 向我们展示了构建中的默认任务。

Adding a description to tasks

为了描述我们的任务,我们可以设置任务的 description 属性。 description 属性的值被 Gradle 的 task 使用。让我们为我们的两个任务添加一个描述,如下:

defaultTasks 'second' 
 
// Use description property to set description. 
task first(description: 'Base task') << { 
    println "I am first" 
} 
 
task second( 
    dependsOn: first, 
    description: 'Secondary task') << { 
 
    println "I am second" 
} 

现在,当我们运行 tasks 任务时,我们会得到一个更具描述性的输出:

$ gradle tasks --all
...
Other tasks
-----------
second - Secondary task
    first - Base task

Grouping tasks together

使用 Gradle,我们还可以将任务分组到所谓的 task groups 中。任务组是逻辑上属于一起的一组任务。例如,在我们之前使用的 tasks 任务的输出中使用了任务组。让我们通过将两个任务组合到一个示例任务组中来扩展我们的示例构建脚本。我们必须为任务的 group 属性赋值:

defaultTasks 'second' 
 
// Define name of the 
// task group we want to use. 
def taskGroup = 'base' 
 
task first( 
    description: 'Base task', 
    group: taskGroup) << { 
 
    println "I am first" 
} 
 
task second( 
    dependsOn: first, 
    description: 'Secondary task', 
    group: taskGroup) << { 
 
    println "I am second" 
} 

下次我们运行 tasks 任务时,我们可以看到我们的任务在 Base tasks 部分中分组在一起:

$ gradle -q tasks --all
...
Base tasks
----------
first - Base task
second - Secondary task [first]
...

请注意,任务依赖项附加到 second 任务的 description 属性中。

Getting more information about a task

我们可以通过 Gradle help 任务获取有关任务的更多信息。我们需要为 help 任务指定一个额外的参数:--task,带有我们想要更多的任务名称相关信息。 Gradle 将在控制台中打印有关我们任务的一些详细信息。例如,任务的描述和类型。这对于了解有关任务的更多信息非常有用。

我们将调用 help 任务来获取关于我们的 second 任务的更多信息:

$ gradle help --task second
:help
Detailed task information for second
Path
:second
Type
Task (org.gradle.api.Task)
Description
Secondary task

Group
base
BUILD SUCCESSFUL
Total time: 0.58 secs

Adding tasks in other ways


到目前为止,我们已经使用 task 关键字将任务添加到我们的构建项目中,然后是任务的名称。但是,还有更多方法可以将任务添加到我们的项目中。我们可以使用带有任务名称的 String 值来定义一个新任务,如下所示:

task 'simple' << { task -> 
    println "Running ${task.name}" 
} 

我们还可以使用变量表达式来定义一个新任务。如果这样做,我们必须使用括号,否则表达式无法解析。以下示例脚本使用 simple 字符串值定义了一个 simpleTask 变量。此表达式用于定义任务。结果是我们的项目现在包含一个名为 simple: 的任务

// Define name of task 
// as a variable. 
def simpleTask = 'simple' 
 
// Variable is used for the task name. 
task(simpleTask) << { task -> 
    println "Running ${task.name}" 
} 

我们可以运行 tasks 任务来查看我们新创建的任务:

$ gradle -q tasks
...
Other tasks
-----------
simple
...

我们还可以使用 Groovy 的强大功能来添加新任务。我们可以使用 Groovy 的 GString 表示法来动态创建任务名称。就像在前面的示例中使用表达式一样,但是用 Groovy GString: 表示

// Name of task as variable. 
def simpleTask = 'simple' 
 
// Using Groovy GString with 
// ${} expression to use variable 
// as task name. 
task "${simpleTask}" << { task -> 
    println "Running ${task.name}" 
} 
 
// Or use loops to create multiple tasks. 
['Dev', 'Acc', 'Prod'].each { environment -> 
    // A new task is created for each element 
    // in the list ['Dev', 'Acc', 'Prod']. 
    task "deployTo${environment}" << { task -> 
        println "Deploying to ${environment}" 
    } 
} 

如果我们运行 tasks 任务,我们可以看到我们有四个新任务,如下:

$ gradle -q tasks
...
Other tasks
-----------
deployToAcc
deployToDev
deployToProd
simple
...

添加新任务的另一种方法是通过项目的 tasks 属性。请记住,在我们的构建脚本中,我们可以访问 Project 对象;我们要么显式使用 project 变量,要么隐式使用 Project 对象的方法和属性,而不使用 项目变量。项目的 tasks 属性基本上是我们项目中所有任务的容器。在以下构建脚本中,我们使用 create 方法添加新任务:

def printTaskName = { task -> 
    println "Running ${task.name}" 
} 
 
// Use tasks project variable to get access 
// to the TaskContainer object. 
// Then we use the create method of 
// TaskContainer to create a new task. 
project.tasks.create(name: 'first') << printTaskName 
 
// Let Gradle resolve tasks to project variable. 
tasks.create(name: 'second', dependsOn: 'first') << printTaskName 

Using task rules

我们已经了解了如何将任务动态添加到我们的构建项目中。但是,我们也可以定义所谓的任务规则。这些规则非常灵活,允许我们根据几个参数和项目属性向项目添加任务。

假设,我们想要添加一个额外的任务来显示我们项目中每个任务的描述。如果我们的项目中首先有一个任务,我们想添加一个 descFirst 任务来显示 description 属性class="literal">第一个任务。通过任务规则,我们为新任务定义了一个模式。在我们的示例中,这是 desc<TaskName>;它是 desc 前缀,后跟现有任务的名称。以下构建脚本显示了任务规则的实现:

task first(description: 'First task') 
 
task second(description: 'Second task') 
 
tasks.addRule( 
    "Pattern: desc<TaskName>: " + 
    "show description of a task.") { taskName -> 
 
    if (taskName.startsWith('desc')) { 
        // Remove 'desc' from the task name. 
        def targetTaskName = taskName - 'desc' 
 
      // Uncapitalize the task name. 
      def targetTaskNameUncapitalize = 
        targetTaskName[0].toLowerCase() + 
          targetTaskName[1..-1] 
 
      // Find the task in the project we search 
      // the description for. 
      def targetTask = 
        project.tasks.findByName( 
          targetTaskNameUncapitalize) 
 
      if (targetTask) { 
          task(taskName) << { 
          println "Description of task ${targetTask.name} " + 
            " -> ${targetTask.description}" 
        } 
      } 
    } 
} 

如果我们运行 tasks 任务,我们会在输出中看到一个额外的 Rules 部分:

$ gradle tasks
...
Rules
-----
Pattern: desc<TaskName>: show description of a task.
...

所以,我们知道我们可以为我们的项目调用 descFirstdescSecond。请注意,这两个额外的任务没有显示在 Other tasks 部分,但 Rules 部分显示了我们可以使用的模式。

如果我们执行 descFirstdescSecond 任务,我们会得到以下输出:

$ gradle descFirst descSecond
:descFirst
Description of task first  -> First task
:descSecond
Description of task second  -> Second task
BUILD SUCCESSFUL
Total time: 0.56 secs

Accessing tasks as project properties


我们添加的每个任务也可以作为 project 属性使用,我们可以像引用构建脚本中的任何其他属性一样引用此属性。例如,我们可以通过属性引用调用方法或获取和设置任务的属性值。这意味着我们在如何创建任务和向任务中添加行为方面非常灵活。在以下脚本中,我们使用对任务的 project 属性引用来更改 description 属性:

// Create a simple task. 
task simple << { task -> 
    println "Running ${task.name}" 
} 
 
// The simple task is available as 
// project property. 
simple.description = 'Print task name' 
 
// We can invoke methods from the 
// Task object. 
simple.doLast { 
    println "Done" 
} 
 
// We can also reference the task 
// via the project property 
// explicitly. 
project.simple.doFirst { 
    println "Start" 
} 

当我们从命令行运行我们的任务时,我们得到以下输出:

$ gradle -q simple
Start
Running simple
Done

Adding additional properties to tasks


一个任务对象已经有几个属性和方法。但是,我们可以将任意新属性添加到任务并使用它。 Gradle 为 task 对象提供了一个 ext 命名空间。我们可以设置新属性并在设置后再次使用它们。我们可以直接设置属性,也可以使用闭包为属性设置值。在以下示例中,我们打印 message 任务属性的值。该属性的值是通过 simple.ext.message = 'world' 语句分配的:

// Create simple task. 
task simple << { 
    println "Hello ${message}" 
} 
 
// We set the value for 
// the non-existing message 
// property with the task extension 
// support. 
simple.ext.message = 'world' 

当我们运行任务时,我们得到以下输出:

:simple
Hello world
BUILD SUCCESSFUL
Total time: 0.584 secs

Avoiding common pitfalls


创建任务并为此任务添加操作时的一个常见错误是我们忘记了左移运算符(<<)。然后我们在构建脚本中留下了一个有效的语法,所以我们在执行任务时不会出错。但是,我们没有添加操作,而是配置了我们的任务。然后我们使用的闭包被解释为配置闭包。闭包中的所有方法和属性都应用于任务。我们可以在配置闭包中为我们的任务添加操作,但我们必须使用 doFirstdoLast 方法。我们不能使用左移运算符 (<<)。

以下任务做同样的事情,但在定义任务时请注意细微差别:

def printTaskName = { task -> 
    println "Running ${task.name}" 
} 
 
task 'one' { 
    // Invoke doFirst method to add action. 
    doFirst printTaskName 
} 
 
// Assign action through left-shift operator (<<). 
task 'two' << printTaskName 
 
task 'three' { 
    // This line will be displayed during configuration 
    // and not when we execute the task, 
    // because we use the configuration closure 
    // and forgot the << operator. 
    println "Running three" 
} 
 
defaultTasks 'one', 'two' 

Skipping tasks


有时,我们希望将任务从构建中排除。在某些情况下,我们只想跳过一个任务并继续执行其他任务。我们可以使用几种方法来跳过 Gradle 中的任务。

Using onlyIf predicates

每个任务都有一个 onlyIf 方法,该方法接受一个闭包作为参数。闭包的结果必须是 truefalse。 如果必须跳过任务,闭包的结果必须是 < code class="literal">false,否则执行任务。 task 对象作为参数传递给闭包。 Gradle 在任务执行之前评估闭包。

以下构建文件将跳过 longrunning 任务,如果该文件在工作日执行,但将在周末执行:

import static java.util.Calendar.* 
 
task longrunning { 
    // Only run this task if the 
    // closure returns true. 
    onlyIf { task -> 
        def now = Calendar.instance 
        def weekDay = now[DAY_OF_WEEK] 
        def weekDayInWeekend = weekDay in [SATURDAY, SUNDAY] 
        return weekDayInWeekend 
    } 
 
    // Add an action. 
    doLast { 
        println "Do long running stuff" 
    } 
} 

如果我们在工作日运行构建,我们会得到以下输出:

$ gradle longrunning
:longrunning SKIPPED
BUILD SUCCESSFUL
Total time: 0.581 secs

如果我们在周末运行构建,我们会看到任务已执行:

$ gradle longrunning
:longrunning
Do long running stuff
BUILD SUCCESSFUL
 Total time: 0.561 secs

我们可以为一个任务多次调用 onlyIf 方法。如果谓词之一返回 false,则跳过该任务。除了使用闭包来定义确定任务是否需要执行的条件外,我们还可以使用 org.gradle.api.specs.Spec接口的实现. Spec 接口有一个方法:isSatisfiedBy。如果必须执行任务,我们必须编写一个实现并返回 true,如果我们希望跳过该任务,则返回false .当前的 task 对象作为参数传递给 isSatisfiedBy 方法。

在下面的示例中,我们检查文件是否存在。如果文件存在,我们可以执行任务,否则跳过任务:

// Create a new File object. 
def file = new File('data.sample') 
 
task handleFile { 
    // Use Spec implementation to write 
    // a conditon for the onlyIf method. 
    onlyIf(new Spec() { 
        boolean isSatisfiedBy(task) { 
            file.exists() 
        } 
    }) 
 
    doLast { 
        println "Work with file ${file.name}" 
    } 
} 

Skipping tasks by throwing StopExecutionException

跳过执行任务的另一种方法是抛出 StopExecutionException 异常。如果抛出这样的异常,构建将停止当前任务并继续下一个任务。我们可以使用 doFirst 方法为任务添加前置条件检查。在闭包中,当我们传递给 doFirst 方法时,我们可以检查条件并在必要时抛出 StopExecutionException 异常。

在下面的构建脚本中,我们检查脚本是否在工作时间执行。如果是这样,则抛出异常并跳过 first 任务:

// Define closure with the task actions. 
def printTaskName = { task -> 
    println "Running ${task.name}" 
} 
 
// Create first task. 
task first << printTaskName 
 
// Use doFirst method with closure 
// that throws exception when task 
// is executed during work hours. 
first.doFirst { 
    def today = Calendar.instance 
    def workingHours = today[Calendar.HOUR_OF_DAY] in 8..17 
 
    if (workingHours) { 
        throw new StopExecutionException() 
    } 
} 
 
// Create second task that depends on first task. 
task second(dependsOn: 'first') << printTaskName 

如果我们在工作时间运行脚本并检查构建脚本的输出,我们会注意到我们看不到任务已被跳过。如果我们使用 onlyIf 方法,Gradle 会将 SKIPPED 添加到未执行的任务中:

:first
:second
Running second
BUILD SUCCESSFUL
Total time: 0.637 secs

Enabling and disabling tasks

我们已经看到如何使用 onlyIf 方法或抛出 StopExecutionException 来跳过任务。但是,我们也可以使用另一种方法来跳过任务。每个任务都有一个 enabled 属性。默认情况下,该属性的值为 true, 表示启用并执行任务。我们可以更改该值并将其设置为 false 以禁用任务并跳过其执行。

在下面的示例中,我们检查目录是否存在,如果存在,则将 enabled 属性设置为 true;如果不是,则设置为 false:

task listDirectory { 
    def dir = new File('assemble') 
 
    // Set value for enabled task property. 
    enabled = dir.exists() 
 
    // This is only executed if enabled is true. 
    doLast { 
        println "List directory contents: " + 
                dir.listFiles().join(',') 
    } 
} 

如果我们运行任务并且目录不存在,我们会得到以下输出:

$ gradle listDirectory
:listDirectory SKIPPED
BUILD SUCCESSFUL
Total time: 0.563 secs

如果我们运行任务,并且这次目录存在,包含一个名为 sample.txt 的文件,我们会得到以下输出:

$ gradle listDirectory
:listDirectory
List directory contents: assemble/sample.txt
BUILD SUCCESSFUL
Total time: 0.566 secs

Skipping from the command line

到目前为止,我们已经定义了跳过构建文件中的任务的规则。但是,如果我们运行构建,我们可以使用 --exclude-tasks (-x) 命令行选项。我们必须将要从要执行的任务中排除的任务定义为参数。

以下脚本具有三个具有一些任务依赖性的任务:

// Define closure with task action. 
def printTaskName = { task -> 
    println "Run ${task.name}" 
} 
 
task first << printTaskName 
 
task second(dependsOn: first) << printTaskName 
 
task third(dependsOn: [second, first]) << printTaskName 

如果我们运行 gradle 命令并排除 second 任务,我们会得到以下输出:

$ gradle third -x second
:first
Run first
:third
Run third
BUILD SUCCESSFUL
Total time: 0.573 secs

如果我们的 third 任务不依赖于第一个任务,那么只有 third 任务会被执行。

Skipping tasks that are up to date

到目前为止,我们已经定义了评估条件以确定是否需要跳过任务。但是,使用 Gradle,我们可以更加灵活。假设我们有一个任务处理一个文件并根据该文件生成一些输出。例如,编译任务就适合这种模式。在以下示例构建文件中,我们有 convert 任务,它将获取 XML 文件、解析内容并将数据写入文本文件,如以下代码所示:

task convert { 
    def source = new File('source.xml') 
    def output = new File('output.txt') 
 
    doLast { 
      def xml = new XmlSlurper().parse(source) 
 
      output.withPrintWriter { writer -> 
          xml.person.each { person -> 
            writer.println "${person.name},${person.email}" 
        } 
      } 
 
      println "Converted ${source.name} to ${output.name}" 
    } 
} 

我们可以多次运行这个任务。每次都从 XML 文件中读取数据并写入文本文件:

$ gradle convert
:convert
Converted source.xml to output.txt
BUILD SUCCESSFUL
Total time: 0.592 secs
$ gradle convert
:convert
Converted source.xml to output.txt
BUILD SUCCESSFUL
Total time: 0.592 secs

但是,我们的输入文件在任务调用之间没有改变,因此不必执行任务。我们希望仅在源文件已更改、输出文件丢失或自上次运行任务以来已更改时才执行任务。

Gradle 支持这种模式,这种支持称为 增量构建支持。仅在必要时才需要执行任务。这是 Gradle 的一个非常强大的特性。它将真正加快构建过程,因为只执行需要执行的任务。

我们需要更改任务的定义,以便 Gradle 可以根据任务的输入文件或输出文件的更改来确定是否需要执行任务。任务具有用于此目的的属性 inputsoutputs。要定义输入文件,我们使用输入文件的值调用 inputs 属性的 file 方法。我们通过调用 outputs 属性的 file 方法来设置输出文件。

让我们重写我们的任务,使其支持 Gradle 的增量构建功能:

task convert { 
    def source = new File('source.xml') 
    def output = new File('output.txt') 
 
    // Define input file 
    inputs.file source 
 
    // Define output file 
    outputs.file output 
 
    doLast { 
      def xml = new XmlSlurper().parse(source) 
 
      output.withPrintWriter { writer -> 
        xml.person.each { person -> 
          writer.println "${person.name},${person.email}" 
        } 
      } 
 
      println "Converted ${source.name} to ${output.name}" 
    } 
} 

当我们运行构建文件几次时,我们看到我们的任务在第二次运行时被跳过,因为输入和输出文件没有改变:

$ gradle convert
:convert
Converted source.xml to output.txt
BUILD SUCCESSFUL
Total time: 0.592 secs
$ gradle convert
:convert UP-TO-DATE
BUILD SUCCESSFUL
Total time: 0.581 secs

我们可以使用 --rerun-tasks 命令行选项来忽略增量构建功能。当我们使用此命令行选项时,Gradle 不会检查任何条件,并且无论 inputs输出的条件如何,Gradle 都会执行任务 属性。

让我们使用这个选项并再次运行我们的 convert 任务。这一次,即使源文件和输出文件没有改变,任务也会被执行:

$ gradle --rerun-tasks convert
:convert
Converted source.xml to output.txt
BUILD SUCCESSFUL
Total time: 0.592 secs

我们为 inputsoutputs 属性定义了一个文件。但是,Gradle 支持更多方式来定义这些属性的值。  inputs 属性具有添加目录、多个文件甚至要监视更改的属性的方法。  outputs 属性具有添加要监视更改的目录或多个文件的方法。如果这些方法不适合我们的构建,我们甚至可以将 upToDateWhen 方法用于 outputs 属性。我们通过 org.gradle.api.specs.Spec 接口的闭包或实现来定义判断任务输出是否是最新的谓词。

以下构建脚本使用其中一些方法:

project.version = '1.0' 
 
task createVersionDir { 
    def outputDir = new File('output') 
 
    // If project.version changes then the 
    // task is no longer up-to-date 
    inputs.property 'version', project.version 
 
    outputs.dir outputDir 
 
    doLast { 
        println "Making directory ${outputDir.name}" 
        mkdir outputDir 
    } 
} 
 
task convertFiles { 
    // Define multiple files to be checked as inputs. 
    // Or use inputs.dir 'input' to check a complete directory. 
    inputs.files 'input/input1.xml', 'input/input2.xml' 
 
    // Use upToDateWhen method to define predicate. 
    outputs.upToDateWhen { 
 
        // If output directory contains any file which name 
        // starts with output and has the xml extension, 
        // then the task is up-to-date. 
        // We use the Groovy method any to check 
        // if at least one file applies to the condition. 
        // The ==~ syntax is a Groovy shortcut to 
        // check if a regular expression is true. 
        new File('output') 
            .listFiles() 
            any { it.name ==~ /output.*\.xml$/ } 
    } 
 
    doLast { 
        println "Running convertFiles" 
    } 
} 

Summary


在本章中,我们讨论了如何在构建项目中创建任务。我们以多种方式创建了带有操作的任务,并讨论了如何配置任务。

我们通过使用谓词、抛出 StopExecutionException 以及启用或禁用任务来跳过任务。我们还讨论了如何从命令行跳过任务。

Gradle 的一个非常强大的特性是增量构建支持。如果任务是最新的,则不会执行。我们可以定义规则来确定任务定义中的最新状态。

在下一章中,我们将更深入地了解 Gradle Project 对象。我们将看到如何使用文件和项目属性以及如何使用 Gradle Wrapper。