构建脚本的基础知识

这一章主要讲解以下内容

  • Projects 和 tasks
  • Hello world
  • 快捷的任务定义
  • 构建脚本代码
  • 任务依赖
  • 动态任务
  • 使用已经存在的任务
  • 快捷注释
  • 附加的 task 属性
  • 使用 Ant 任务
  • 使用方法
  • 默认的任务
  • 通过 DAG 配置

Projects 和 tasks

Gradle 里的任何东西都是基于这两个基础概念:

  • projects ( 项目 )
  • tasks ( 任务 )

每一个构建都是由一个或多个 projects 构成的. 一个 project 到底代表什么取决于你想用 Gradle 做什么. 举个例子, 一个 project 可以代表一个 JAR 或者一个网页应用. 它也可能代表一个发布的 ZIP 压缩包, 这个 ZIP 可能是由许多其他项目的 JARs 构成的. 但是一个 project 不一定非要代表被构建的某个东西. 它可以代表一件**要做的事, 比如部署你的应用.

不要担心现在看不懂这些说明. Gradle 的合约构建可以让你来具体定义一个 project 到底该做什么.

每一个 project 是由一个或多个 tasks 构成的. 一个 task 代表一些更加细化的构建. 可能是编译一些 classes, 创建一个 JAR, 生成 javadoc, 或者生成某个目录的压缩文件.

目前, 我们先来看看定义构建里的一些简单的 task. 以后的章节会讲解多项目构建以及如何通过 projects 和 tasks 工作.

Hello world

你可以通过 gradle 命令运行一个 Gradle 构建.

gradle 命令会在当前目录中查找一个叫 build.gradle 的文件. 我们称这个 build.gradle 文件为一个构建脚本 (build script), 但是严格来说它是一个构建配置脚本 (build configuration script). 这个脚本定义了一个 project 和它的 tasks.

让我们来先看一个例子, 创建一个名为build.gradle的构建脚本.

例子 6.1 第一个构建脚本

build.gradle

task hello {
    doLast {
        println 'Hello world!'
    }
}

在命令行里, 进入脚本所在的文件夹然后输入 gradle -q hello 来执行构建脚本:

gradle -q hello 的输出 > gradle -q hello Hello world!

这里发生了什么? 这个构建脚本定义了一个独立的 task, 叫做 hello, 并且加入了一个 action. 当你运行 gradle hello, Gradle 执行叫做 hello 的 task, 也就是执行了你所提供的 action. 这个 action 是一个包含了一些 Groovy 代码的闭包(closure 这个概念不清楚的同学好好谷歌下).

如果你认为这些看上去和 Ant 的 targets 很相像, 好吧, 你是对的. Gradle tasks 和 Ant 的 targets 是对等的. 但是你将会会看到, Gradle tasks 更加强大. 我们使用一个不同于 Ant 的术语 task, 看上去比 target 更加能直白. 不幸的是这带来了一个术语冲突, 因为 Ant 称它的命令, 比如 javac 或者 copy, 叫 tasks. 所以当我们谈论 tasks, 是指 Gradle 的 tasks. 如果我们讨论 Ant 的 tasks (Ant 命令), 我们会直接称呼 ant task.

补充一点命令里的 -q 是干什么的?

这个指南里绝大多说的例子会在命令里加入 -q. 代表 quiet 模式. 它不会生成 Gradle 的日志信息 (log messages), 所以用户只能看到 tasks 的输出. 它使得的输出更加清晰. 你并不一定需要加入这个选项. 参考第 18 章, 日志的 Gradle 影响输出的详细信息.

快捷的任务定义

有一种比我们之前定义的 hello 任务更简明的方法

例子 6.3. 快捷的任务定义

build.gradle

task hello << {
    println 'Hello world!'
}

它定义了一个叫做 hello 的任务, 这个任务是一个可以执行的闭包. 我们将使用这种方式来定义这本指南里所有的任务.

翻译者补充

与前面的例子比较, doLast 被替换成了 <<. 它们有一样的功能, 但看上去更加简洁了, 会在后续章节 (6.7) 详细讲解它们的功能.

构建脚本代码

Gradle 的构建脚本展示了 Groovy 的所有能力. 作为开胃菜, 来看看这个:

例子 6.4. 在 Gradle 任务里使用 Groovy

build.gradle

task upper << {
    String someString = 'mY_nAmE'
    println "Original: " + someString
    println "Upper case: " + someString.toUpperCase()
}

gradle -q upper 命令的输出

> gradle -q upper
Original: mY_nAmE
Upper case: MY_NAME

或者

例子 6.5. 在 Gradle 任务里使用 Groovy

build.gradle

task count << {
    4.times { print "$it " }
}

gradle -q count 命令的输出

> gradle -q count
0 1 2 3

任务依赖

就像你所猜想的那样, 你可以声明任务之间的依赖关系.

例子 6.6. 申明任务之间的依赖关系

build.gradle

task hello << {
    println 'Hello world!'
}

task intro(dependsOn: hello) << {
    println "I'm Gradle"
}

gradle -q intro 命令的输出

> gradle -q intro
Hello world!
I'm Gradle

intro 依赖于 hello, 所以执行 intro 的时候 hello 命令会被优先执行来作为启动 intro 任务的条件.

在加入一个依赖之前, 这个依赖的任务不需要提前定义, 来看下面的例子.

例子 6.7. Lazy dependsOn - 其他的任务还没有存在

build.gradle

task taskX(dependsOn: 'taskY') << {
    println 'taskX'
}
task taskY << {
    println 'taskY'
}

gradle -q taskX 命令的输出

> gradle -q taskX
taskY
taskX

taskX 到 taskY 的依赖在 taskY 被定义之前就已经声明了. 这一点对于我们之后讲到的多任务构建是非常重要的. 任务依赖将会在 14.4 具体讨论.

请注意你不能使用快捷注释 (参考 6.8, “快捷注释”) 当所关联的任务还没有被定义.

动态任务

Groovy 不仅仅被用来定义一个任务可以做什么. 举个例子, 你可以使用它来动态的创建任务.

例子 6.8. 动态的创建一个任务

build.gradle

4.times { counter ->
    task "task$counter" << {
        println "I'm task number $counter"
    }
}

这里动态的创建了 task0, task1, task2, task3

gradle -q task1 命令的输出

> gradle -q task1
I'm task number 1

使用已经存在的任务

当任务创建之后, 它可以通过API来访问. 这个和 Ant 不一样. 举个例子, 你可以创建额外的依赖.

例子 6.9. 通过API访问一个任务 - 加入一个依赖

build.gradle

4.times { counter ->
    task "task$counter" << {
        println "I'm task number $counter"
    }
}
task0.dependsOn task2, task3

gradle -q task0 命令的输出

> gradle -q task0
I'm task number 2
I'm task number 3
I'm task number 0

或者你可以给一个已经存在的任务加入行为.

例子 6.10. 通过API访问一个任务 - 加入行为

build.gradle

task hello << {
    println 'Hello Earth'
}
hello.doFirst {
    println 'Hello Venus'
}
hello.doLast {
    println 'Hello Mars'
}
hello << {
    println 'Hello Jupiter'
}

gradle -q hello 命令的输出

> gradle -q hello
Hello Venus
Hello Earth
Hello Mars
Hello Jupiter

doFirst 和 doLast 可以被执行许多次. 他们分别可以在任务动作列表的开始和结束加入动作. 当任务执行的时候, 在动作列表里的动作将被按顺序执行. 这里第四个行为中 << 操作符是 doLast 的简单别称.

短标记法

正如同你已经在之前的示例里看到, 有一个短标记 $ 可以访问一个存在的任务. 也就是说每个任务都可以作为构建脚本的属性:

例子 6.11. 当成构建脚本的属性来访问一个任务

build.gradle

task hello << {
    println 'Hello world!'
}
hello.doLast {
    println "Greetings from the $hello.name task."
}

gradle -q hello 命令的输出

> gradle -q hello
Hello world!
Greetings from the hello task.

这里的 name 是任务的默认属性, 代表当前任务的名称, 这里是 hello.

这使得代码易于读取, 特别是当使用了由插件(如编译)提供的任务时尤其如此.

自定义任务属性

你可以给任务加入自定义的属性. 列如加入一个叫做 myProperty 属性, 设置一个初始值给 ext.myProperty. 然后, 该属性就可以像一个预定义的任务属性那样被读取和设置了.

例子 6.12. 给任务加入自定义属性

build.gradle

task myTask {
    ext.myProperty = "myValue"
}

task printTaskProperties << {
    println myTask.myProperty
}

gradle -q printTaskProperties 命令的输出

> gradle -q printTaskProperties
myValue

给任务加自定义属性是没有限制的. 你可以在 13.4.2, “自定义属性” 里获得更多的信息.

调用 Ant 任务

Ant 任务是 Gradle 的一等公民. Gradle 通过 Groovy 出色的集成了 Ant 任务. Groovy 自带了一个 AntBuilder. 相比于从一个 build.xml 文件中使用 Ant 任务, 在 Gradle 里使用 Ant 任务更为方便和强大. 从下面的例子中, 你可以学习如何执行 Ant 任务以及如何访问 ant 属性:

例子 6.13. 使用 AntBuilder 来执行 ant.loadfile 任务

build.gradle

task loadfile << {
    def files = file('../antLoadfileResources').listFiles().sort()
    files.each { File file ->
        if (file.isFile()) {
            ant.loadfile(srcFile: file, property: file.name)
            println " *** $file.name ***"
            println "${ant.properties[file.name]}"
        }
    }
}

gradle -q loadfile 命令的输出

> gradle -q loadfile
*** agile.manifesto.txt ***
Individuals and interactions over processes and tools
Working software over comprehensive documentation
Customer collaboration  over contract negotiation
Responding to change over following a plan
 *** gradle.manifesto.txt ***

使用方法

Gradle 能很好地衡量你编写脚本的逻辑能力. 首先要做的是如何提取一个方法.

例子 6.14. 使用方法组织脚本逻辑

build.gradle

task checksum << {
    fileList('../antLoadfileResources').each {File file ->
        ant.checksum(file: file, property: "cs_$file.name")
        println "$file.name Checksum: ${ant.properties["cs_$file.name"]}"
    }
}

task loadfile << {
    fileList('../antLoadfileResources').each {File file ->
        ant.loadfile(srcFile: file, property: file.name)
        println "I'm fond of $file.name"
    }
}

File[] fileList(String dir) {
    file(dir).listFiles({file -> file.isFile() } as FileFilter).sort()
}

adle -q loadfile 命令的输出

> gradle -q loadfile
I'm fond of agile.manifesto.txt
I'm fond of gradle.manifesto.txt

稍后你看到, 这种方法可以在多项目构建的子项目之间共享. 如果你的构建逻辑变得更加复杂, Gradle 为你提供了其他非常方便的方法. 请参见第59章,组织构建逻辑。

默认任务

Gradle 允许在脚本中定义一个或多个默认任务.

例子 6.15. 定义默认任务

build.gradle

defaultTasks 'clean', 'run'

task clean << {
    println 'Default Cleaning!'
}

task run << {
    println 'Default Running!'
}

task other << {
    println "I'm not a default task!"
}

gradle -q 命令的输出

> gradle -q
Default Cleaning!
Default Running!

等价于 gradle -q clean run. 在一个多项目构建中, 每一个子项目都可以有它特别的默认任务. 如果一个子项目没有特别的默认任务, 父项目的默认任务将会被执行.

通过 DAG 配置

正如我们之后的详细描述 (参见第55章,构建的生命周期), Gradle 有一个配置阶段和执行阶段. 在配置阶段后, Gradle 将会知道应执行的所有任务. Gradle 为你提供一个"钩子", 以便利用这些信息. 举个例子, 判断发布的任务是否在要被执行的任务当中. 根据这一点, 你可以给一些变量指定不同的值.

在接下来的例子中, distribution 任务和 release 任务将根据变量的版本产生不同的值.

例子 6.16. 根据选择的任务产生不同的输出

build.gradle

task distribution << {
    println "We build the zip with version=$version"
}

task release(dependsOn: 'distribution') << {
    println 'We release now'
}

gradle.taskGraph.whenReady {taskGraph ->
    if (taskGraph.hasTask(release)) {
        version = '1.0'
    } else {
        version = '1.0-SNAPSHOT'
    }
}

gradle -q distribution 命令的输出

> gradle -q distribution
We build the zip with version=1.0-SNAPSHOT
Output of gradle -q release
> gradle -q release
We build the zip with version=1.0
We release now

最重要的是 whenReady 在 release 任务执行之前就已经影响了 release 任务. 甚至 release 任务不是首要任务 (i.e., 首要任务是指通过 gradle 命令的任务).