14.6 给 task 加入描述
可以给你的任务加入一段描述性的文字. 它将会在任务执行的时候显示出来.
14-18 给任务加入描述
build.gradle
task copy(type: Copy) {description 'Copies the resource directory to the target directory.'from 'resources'into 'target'include('**/*.txt', '**/*.xml', '**/*.properties')
}
14.7 替换 tasks
想要替换一个任务. e.g. 如果想要互换一个通过 java 插件定义的任务和一个自定义的不同类型的任务:
15-19 覆写一个任务
build.gradle
task copy(type: Copy)task copy(overwrite: true) << {println('I am the new one.')
}
输出
> gradle -q copy
I am the new one.
这种方式将用你自己定义的任务替换一个 Copy 类型的任务, 因为它使用了同样的名字. 但你定义一个新的任务时, 你必须设置 overwrite 属性为 true. 否则的话 Gradle 会抛出一个异常, task with that name already exists.
14.8 跳过 tasks
Gradle 提供了好几种跳过一个任务的方式.
1. 使用判断条件 (predicate)
可以使用 onlyIf() 方法来为一个任务加入判断条件. 就和 Java 里的 if 语句一样, 任务只有在条件判断为真时才会执行.
通过一个闭包来实现判断条件. 闭包像变量一样传递任务, 如果任务应该被执行则返回真, 反之亦然. 判断条件在任务执行之前进行判断.
15-20 使用判断条件跳过一个任务
build.gradle
task hello << {println 'hello world' }hello.onlyIf { !project.hasProperty('skipHello') }
输出
> gradle hello -PskipHello
:hello SKIPPED
BUILD SUCCESSFULTotal time: 1 secs
2. 使用 StopExecutionException
如果想要跳过一个任务的逻辑而不能被判断条件通过表达式表达出来, 可以使用 StopExecutionException.
如果这个异常是被一个任务要执行的动作抛出的, 这个动作之后的执行以及所有紧跟它的动作都会被跳过. 构建将会继续执行下一个任务.
15-21 通过 StopExecutionException 跳过任务
build.gradle
task compile << {println 'We are doing the compile.'
}compile.doFirst {// Here you would put arbitrary conditions in real life.// But this is used in an integration test so we want defined behavior.if (true) { throw new StopExecutionException() }
}
task myTask(dependsOn: 'compile') << {println 'I am not affected'
}
输出
> gradle -q myTask
I am not affected
如果直接使用 Gradle 提供的任务, 这项功能还是十分有用的. 它允许为内建的任务加入条件来控制执行.
3. 激活和注销 tasks
每一个任务都有一个已经激活的标记(enabled flag), 这个标记一般默认为真. 将它设置为假, 那它的任何动作都不会被执行.
15-22 激活和注销 tasks
build.gradle
task disableMe << {println 'This should not be printed if the task is disabled.'
}
disableMe.enabled = false
输出
> gradle disableMe
:disableMe SKIPPEDBUILD SUCCESSFULTotal time: 1 secs
14.9 跳过 up-to-date 的任务
如果你正在使用一些附加的任务, 比如通过 Java 插件加入的任务, 可能会注意到 Gradle 会跳过一些任务, 这些任务后面会标注 up-to-date. 代表这个任务已经运行过了或者说是最新的状态, 不再需要产生一次相同的输出. 不仅仅是这些内建任务, 其实在运行自己的任务时, 也会碰到这种情况.
1. 声明一个任务的输入和输出
e.g. 这里任务会根据一个 XML 文件生成好几个输出文件. 运行这个任务 2 次.
15-23 A generator task
build.gradle
task transform {ext.srcFile = file('mountains.xml')ext.destDir = new File(buildDir, 'generated')doLast {println "Transforming source file."destDir.mkdirs()def mountains = new XmlParser().parse(srcFile)mountains.mountain.each { mountain ->def name = mountain.name[0].text()def height = mountain.height[0].text()def destFile = new File(destDir, "${name}.txt")destFile.text = "$name -> ${height}\n"}}
}
输出1
> gradle transform
:transform
Transforming source file.
输出2
> gradle transform
:transform
Transforming source file.
这里 Gradle 执行了这个任务两次, 即使什么都没有改变, 它也没有跳过这个任务.
例子里的任务, 它的行为是通过闭包定义的. Gradle 并不知道闭包会做什么, 也并不能自动指出是否这个任务是 up-to-date. 为了使用 Gradle 的 up-to-date 检测, 你需要定义任务的输入和输出.
每个任务都有输入和输出属性, 你需要使用这些属性来声明任务的输入和输出.
下面的例子中, 将声明 XML 文件作为输入, 并且把输出放在一个指定的目录. 让我们运行这个任务 2 次.
15-24 声明任务的输入和输出
build.gradle
task transform {ext.srcFile = file('mountains.xml')ext.destDir = new File(buildDir, 'generated')inputs.file srcFile // inputoutputs.dir destDir // outputdoLast {println "Transforming source file."destDir.mkdirs()def mountains = new XmlParser().parse(srcFile)mountains.mountain.each { mountain ->def name = mountain.name[0].text()def height = mountain.height[0].text()def destFile = new File(destDir, "${name}.txt")destFile.text = "$name -> ${height}\n"}}
}
输出1
> gradle transform
:transform
Transforming source file.
输出2
> gradle transform
:transform UP-TO-DATE
现在, Gradle 就能够检测出任务是否是 up-to-date 状态.
任务的输入属性是 TaskInputs 类型. 任务的输出属性是 TaskOutputs 类型.
总之, 如果一个任务只定义了输出, 如果输出不变的话, 它就会被视为 up-to-date.
2. 它是如何工作的?
当一个任务是首次执行时, Gradle 会取一个输入的快照 (snapshot). 该快照包含组输入文件和每个文件的内容的散列.
然后当 Gradle 执行任务时, 如果任务成功完成,Gradle 会获得一个输出的快照. 该快照包含输出文件和每个文件的内容的散列. Gradle 会保留这两个快照用来在该任务的下一次执行时进行判断.
之后, 每次在任务执行之前, Gradle 都会为输入和输出取一个新的快照, 如果这个快照和之前的快照一样, Gradle 就会假定这个任务已经是最新的 (up-to-date) 并且跳过任务, 反之亦然.
需要注意的是, 如果一个任务有指定的输出目录, 自从该任务上次执行以来被加入到该目录的任务文件都会被忽略, 并且不会引起任务过时 (out of date). 这是因为不相关任务也许会共用同一个输出目录. 如果这并不是你所想要的情况, 可以考虑使用 TaskOutputs.upToDateWhen())
14.10 Task 规则
有时候也想要一个任务的行为是基于已经定义好的取值范围或者特定规则, 下面的例子提供了一种很直观漂亮的方式:
15-25 任务规则
build.gradle
tasks.addRule("Pattern: ping<ID>") { String taskName ->if (taskName.startsWith("ping")) {task(taskName) << {println "Pinging: " + (taskName - 'ping')}}
}
输出
> gradle -q pingServer1
Pinging: Server1
这里的 String 参数就是用来定义规则的.
规则并不只是在通过命令行使用任务的时候执行. 你也可以基于规则来创建依赖关系:
15-26 基于规则的任务依赖
build.gradle
tasks.addRule("Pattern: ping<ID>") { String taskName ->if (taskName.startsWith("ping")) {task(taskName) << {println "Pinging: " + (taskName - 'ping')}}
}task groupPing {dependsOn pingServer1, pingServer2
}
输出
> gradle -q groupPing
Pinging: Server1
Pinging: Server2
如果运行 “gradle -q tasks”, 你并不能找到名叫 “pingServer1” 或者 “pingServer2” 的任务, 但是这个脚本仍然会执行这些任务.
14.11 终止 tasks
终止任务是一个正在开发的功能
这里的终止任务并不是指终止一个任务, 而是指一个无论运行结果如何最后都会被执行的任务.
15-27 加入一个任务终止器
build.gradle
task taskX << {println 'taskX'
}
task taskY << {println 'taskY'
}taskX.finalizedBy taskY
输出
> gradle -q taskX
taskX
taskY
即使要终止的任务失败了, 终止任务仍会继续执行.
15-28 当任务失败时
build.gradle
task taskX << {println 'taskX'throw new RuntimeException()
}
task taskY << {println 'taskY'
}taskX.finalizedBy taskY
输出
> gradle -q taskX
taskX
taskY
另外, 如果要终止的任务并没有被执行 (比如上一节讲的 up-to-date) 那么终止任务并不会执行.
当构建创建了一个资源, 无论构建失败或成功时这个资源必须被清除的时候, 终止任务就非常有用.
要使用终止任务, 必须使用 Task.finalizedBy() 方法.
一个任务的实例, 任务的名称, 或者任何 Task.dependsOn() 可以接收的输入都可以作为这个任务的输入.
14.12 补充
下面补充的部分原本是第 14 章,最新的 Gradle 文档将其移除,所以将其作为补充放到这一章节。
14.12.1 Gradle 属性 和 system 属性
Gradle 提供了多种的方法让您可以在构建脚本中添加属性. 使用 -D 命令选项,您可以向运行 Gradle 的 JVM 传递一个 system 属性 . Gradle 命令的 -D 选项 和 Java 命令的 -D 选项有些相同的效果.
可以使用属性文件向 Project 对象中添加属性. 可以在 Gradle 用户目录( 如果没有在 USER_HOME/.gradle 配置默认设置, 则由”GRADLE_USER_HOME” 环境变量定义) 或者项目目录放置一个 gradle.properties 文件.
如果是多项目的话,可以在每个子目录里都放置一个 gradle.properties 文件. gradle.properties 文件内容里的属性能够被 Project 对象访问到.
不过有一点,用户目录中的 gradle.properties 文件优先权大于项目目录中的 gradle.properties 文件.
也可以通过 -P 命令选项直接向Project 对象中添加属性.
另外,当 Gradle 看到特别命名的 system 属性或者环境变量时,Gradle 也可以设置项目属性.
e.g. 当您没有管理员权限去持续整合服务,还有需要设置属性值但是不容易时,这个特性非常有用.
出于安全的原因,在这种情况下,没法使用 -P 命令选项,也不能修改系统级别的文件. 确切的策略是改变持续继承构建工作的配置,增加一个环境变量设置令它匹配一个期望的模式.
对于当前系统来说,这种方法对于普通用户来说是不可见的.
Jenkins, Teamcity, or Bamboo 都是 提供这个功能的 CI 服务.
输出
gradle -q -PcommandLineProjectProp=commandLineProjectPropValue -Dorg.gradle.project.systemProjectProp=systemPropertyValue printProps commandLineProjectPropValue gradlePropertiesValue systemPropertyValue envPropertyValue systemValue
如果环境变量的名字是 ORG_GRADLE_PROJECT=somevalue, Gradle 会使用值为 somevalue 在 Project 对象中设定一个支持属性.
另外 Gradle 也支持 system 属性,但是使用不同的名字模式,e.g. org.gradle.project.prop .
可以在 gradle.properties 文件中设置 system 属性.如果一个属性名的前缀为 “systemProp”,那么这个属性和它的属性值会被设置为 system 属性.
如果没有这个前缀,在多项目构建中,除了根项目会被忽略外,“systemProp.” 属性会在任何项目中设置. 也就是说仅仅根项目的 gradle.properties 文件会被检查其属性的前缀是否是 “systemProp”.
14-2 通过 gradle.properties 文件设置属性
gradle.properties
gradlePropertiesProp=gradlePropertiesValue
sysProp=shouldBeOverWrittenBySysProp
envProjectProp=shouldBeOverWrittenByEnvProp
systemProp.system=systemValue
build.gradle
task printProps << {println commandLineProjectPropprintln gradlePropertiesPropprintln systemProjectPropprintln envProjectPropprintln System.properties['system']
}
14.12.2 使用其他的脚本配置项目
可以使用其他的构建脚本来配置当前的项目,Gradle 构建语言的所有的内容对于其他的脚本都是可以使用的. 您甚至可以在别的脚本中再使用其他的脚本.
14-3 使用其他的构建脚本配置项目
build.gradle
apply from: 'other.gradle'
other.gradle
println "configuring $project"
task hello << {println' 'hello form other srcipt' }
输出
> gradle -q hello
configuring root project 'configureProjectUsingScript'
hello from other script
14.12.3 使用其他的脚本配置任意对象
14-5 使用别的脚本配置配置对象
build.gradle
task config << {def pos = new java.text.FieldPosition(10) // 使用另一个脚本apply from: 'other.gradle', to: posprintln pos.beginIndexprintln pos.endIndex}
other.gradle
beginIndex = 1
endIndex = 5
输出
> gradle -q configure
1
5
14.12.4 配置任意对象
14-4 配置任意对象
build.gradle
task configure << {def pos = configure(new java.text.FieldPosition(10)) {beginIndex = 1endIndex = 5}println pos.beginIndexprintln pos.endIndex
}
输出
gradle -q configure
1
5
14.12.5 缓存
为了提高响应能力,Gradle 默认缓存了所有编译后的脚本. 包括所有的构建脚本,初始化脚本,还有其他脚本. Gradle 创建了一个 .gradle 目录来存放编译后的脚本,下次运行构建脚本时,如果这个脚本自从它被编译后就再也没有被改动过,Gradle 会先使用编译后的脚本. 否则 Gradle 会重新编译脚本,然后将新编译后的文件缓存起来.
如果使用 Gradle –recompile–scripts 运行脚本,缓存的脚本就会被删除,然后新编译后的文件就会再被缓存. 这种方法可以强制 Gradle 重新编译脚本并缓存.
15 文件操作
大多数构建工作需要操作文件,Gradle 增加了一些API帮助处理这些工作。
15.1 Locating files
使用 Project.file()
方法能够相对项目目录定位一个文件
16.1. 定位文件
build.gradle
// 使用一个相对路径
File configFile = file('src/config.xml')// 使用一个绝对路径
configFile = file(configFile.absolutePath)// 使用一个项目路径的文件对象
configFile = file(new File('src/config.xml'))`
file()
方法接收任何形式的对象参数.它会将参数值转换为一个绝对文件对象,一般情况下,可以传递一个 String
或者一个 File
实例.
如果传递的路径是个绝对路径,它会被直接构造为一个文件实例. 否则,会被构造为项目目录加上传递的目录的文件对象.另外, file()
函数也能识别 URL,例如 file:/some/path.xml.
这个方法非常有用, 它将参数值转换为一个绝对路径文件. 所以请尽量使用 new File(somePath)
, 因为 file()
总是相对于当前项目路径计算传递的路径 ,然后加以矫正.因为当前工作区间目录依赖于用户以何种方式运行 Gradle.
15.2 文件集合
文件集合表示一组文件,Gradle 使用 FileCollection
接口表示文件集合, Gradle API 中的许多项目都实现了这个接口, 例如 dependency configurations .
获取 FileCollection
实例的一种方法是使用 Project.files()
方法. 可以传递任何数量的对象参数, 这个方法能将你传递的对象集合转换为一组文件对象. files()
方法接收任何类型对象参数.
每一个 file()
方法都依赖于项目目录(在第 15 章,第一小节). files()
方法也接收 collections
, iterables
, maps
和 arrays
类型参数.这些参数的内容会被解析,然后被转换为文件对象.
15.2 创建文件集合
build.gradle
FileCollection collection = files('src/file1.txt',new File('src/file2.txt'),['src/file3.txt', 'src/file4.txt'])
文件集合可以被迭代器,使用迭代操作能够将其转换为其他的一些类型. 可以使用 + 操作将两个文件集合合并, 使用 - 操作能够对一个文件集合做减法.
15.3 使用文件集合
build.gradle
// 对文件集合进行迭代
collection.each {File file ->println file.name
}
// 转换文件集合为其他类型
Set set = collection.files
Set set2 = collection as Set
List list = collection as List
String path = collection.asPath
File file = collection.singleFile
File file2 = collection as File
// 增加和减少文件集合
def union = collection + files('src/file3.txt')
def different = collection - files('src/file3.txt')
也可以向 files()
方法专递一个闭合或者可回调的实例参数.当查询集合的内容时就会调用它, 然后将返回值转换为一些文件实例.
返回值可以是 files()
方法支持的任何类型的对象.
下面有个简单的例子来演示实现 FileCollection
接口
15.4 实现一个文件集合
build.gradle
task list << {File srcDir // 使用闭合创建一个文件集合collection = files { srcDir.listFiles() }srcDir = file('src')println "Contents of $srcDir.name"collection.collect { relativePath(it) }.sort().each { println it }srcDir = file('src2')println "Contents of $srcDir.name"collection.collect { relativePath(it) }.sort().each { println it }
}
输出结果
> gradle -q list
Contents of src
src/dir1
src/file1.txt
Contents of src2
src2/dir1
src2/dir2
另外, files()
方法也接收其他类型的参数:
FileCollection
内容损坏的文件包含在文件集合中.
Task
任务的输出文件包含在文件集合中.
TaskOutputs
TaskOutputs 的输出文件包含在文件集合中
值得注意的是当有需要时文件集合的内容会被被惰性处理, e.g. 一些任务在需要的时候会创建一个 FileCollecion
代表的文件集合.
15.3 文件树
文件树就是一个按照层次结构分布的文件集合, e.g. 一个文件树可以代表一个目录树结构或者一个 ZIP 压缩文件的内容. 它被抽象为 FileTree
结构, FileTree
继承自 FileCollection
, 所以可以像处理文件集合一样处理文件树, Gradle 有些对象实现了FileTree 接口,例如 源集合.
使用 Project.fileTree()
方法可以得到 FileTree
的实例,它会创建一个基于基准目录的对象, 然后视需要使用一些 Ant-style
的包含和去除规则.
Ant-Style http://stackoverflow.com/questions/2952196/learning-ant-path-style
15.5 创建文件树
build.gradle
// 以一个基准目录创建一个文件树
FileTree tree = fileTree(dir: 'src/main')// 添加包含和排除规则
tree.include '**/*.java'
tree.exclude '**/Abstract*'// 使用路径创建一个树
tree = fileTree('src').include('**/*.java')// 使用闭合创建一个数
tree = fileTree('src') {include '**/*.java'
}// 使用map创建一个树
tree = fileTree(dir: 'src', include: '**/*.java')
tree = fileTree(dir: 'src', includes: ['**/*.java', '**/*.xml'])
tree = fileTree(dir: 'src', include: '**/*.java', exclude: '**/*test*/**')
就像使用文件集合一样, 可以访问文件树的内容, 使用 Ant-style
规则选择一个子树。
例 15.6 使用文件树
build.gradle
// 遍历文件树
tree.each {File file ->println file
}
// 过滤文件树
FileTree filtered = tree.matching {include 'org/gradle/api/**'
}
// 合并文件树A
FileTree sum = tree + fileTree(dir: 'src/test')
// 访问文件数的元素
tree.visit {
element ->println "$element.relativePath => $element.file"
}
—TBC—
—YCR—