第五十九章. 组织构建逻辑

Chapter 59. Organizing Build Logic

Gradle提供了多种方法来组织构建逻辑。首先,你可以直接将构建逻辑置于任务的操作闭包中。如果有多个任务使用相同的逻辑,那么可以将这段逻辑抽取到一个方法中。如果多项目构建的多个项目有相同的逻辑,你可以把这个方法定义在父项目中。如果构建逻辑过于复杂,无法通过方法进行正确建模,你就要使用面向对象模型了。[25]这对 Gradle 来说很简单,只需要把你的类放到某个目录中,Gradle 就会自动编译它们并将它们加入构建脚本的类路径中。
Gradle offers a variety of ways to organize your build logic. First of all you can put your build logic directly in the action closure of a task. If a couple of tasks share the same logic you can extract this logic into a method. If multiple projects of a multi-project build share some logic you can define this method in the parent project. If the build logic gets too complex for being properly modeled by methods you want have an OO Model. [25] Gradle makes this very easy. Just drop your classes in a certain directory and Gradle automatically compiles them and puts them in the classpath of your build script.

下面可以组织构建逻辑的方法摘要:
Here is a summary of the ways you can organise your build logic:

59.1. 继承的属性和方法

59.1. Inherited properties and methods

项目构建脚本中定义的任何方法或属性,它的所有子项目都是可见的。你可以通过这一特点来定义公共配置,并将构建逻辑抽取到子项目可复用的方法中。
Any method or property defined in a project build script is also visible to all the sub-projects. You can use this to define common configurations, and to extract build logic into methods which can be reused by the sub-projects.

示例59.1. 使用继承的属性和方法 - Example 59.1. Using inherited properties and methods

build.gradle

// Define an extra property
ext.srcDirName = 'src/java'

// Define a method
def getSrcDir(project) {
    return project.file(srcDirName)
}

child/build.gradle

task show << {
    // Use inherited property
    println 'srcDirName: ' + srcDirName

    // Use inherited method
    File srcDir = getSrcDir(project)
    println 'srcDir: ' + rootProject.relativePath(srcDir)
}

gradle -q show 的输出结果
Output of gradle -q show

> gradle -q show
srcDirName: src/java
srcDir: child/src/java

59.2. 注入配置

59.2. Injected configuration

你可以使用在第 56.1 节,《跨项目配置》第 56.2 节,《子项目配置》 中所论述的配置注入技术,将属性和方法注入到各个项目中。通常来说,它是比继承更好的选择,原因如下:在构建脚本中,注入是显式的,你可以将不同的逻辑注入到不同的项目,并且可以注入任何类型的配置,如仓库,插件,任务等等。以下是关于注入的示例。
You can use the configuration injection technique discussed in Section 56.1, “Cross project configuration” and Section 56.2, “Subproject configuration” to inject properties and methods into various projects. This is generally a better option than inheritance, for a number of reasons: The injection is explicit in the build script, You can inject different logic into different projects, And you can inject any kind of configuration such as repositories, plug-ins, tasks, and so on. The following sample shows how this works.

示例 59.2. 使用注入的属性和方法 - Example 59.2. Using injected properties and methods

build.gradle

subprojects {
    // Define a new property
    ext.srcDirName = 'src/java'

    // Define a method using a closure as the method body
    ext.srcDir = { file(srcDirName) }

    // Define a task
    task show << {
        println 'project: ' + project.path
        println 'srcDirName: ' + srcDirName
        File srcDir = srcDir()
        println 'srcDir: ' + rootProject.relativePath(srcDir)
    }
}

// Inject special case configuration into a particular project
project(':child2') {
    ext.srcDirName = "$srcDirName/legacy"
}

child1/build.gradle

// Use injected property and method. Here, we override the injected value
srcDirName = 'java'
def dir = srcDir()

gradle -q show 的输出结果
Output of gradle -q show

> gradle -q show
project: :child1
srcDirName: java
srcDir: child1/java
project: :child2
srcDirName: src/java/legacy
srcDir: child2/src/java/legacy

59.3. 在 buildSrc 项目中的构建源码

59.3. Build sources in the buildSrc project

当你运行 Gradle 时,它会检查 buildSrc 目录是否存在。如果存在,Gradle 会自动编译和测试其代码,并将它加到构建脚本的类路径中。你不需要提供任何进一步的指示信息。这可能是添加自定义任务和插件的好地方。
When you run Gradle, it checks for the existence of a directory called buildSrc. Gradle then automatically compiles and tests this code and puts it in the classpath of your build script. You don't need to provide any further instruction. This can be a good place to add your custom tasks and plugins.

对于多项目构建,只能有一个在根项目目录中的 buildSrc 目录。
For multi-project builds there can be only one buildSrc directory, which has to be in the root project directory.

下面列出的是 Gradle 应用到 buildSrc 项目的默认构建脚本。
Listed below is the default build script that Gradle applies to the buildSrc project:

图 59.1. 默认的 buildSrc 构建脚本 - Figure 59.1. Default buildSrc build script

apply plugin: 'groovy'
dependencies {
    compile gradleApi()
    compile localGroovy()
}

这意味着你只需将构建源代码放到这个目录当中,并保持 Java/Groovy 项目的布局约定(见表 23.4,“Java 插件——默认项目布局”)。
This means that you can just put you build source code in this directory and stick to the layout convention for a Java/Groovy project (see Table 23.4, “Java plugin - default project layout”).

如果你想更灵活,那么可以提供自己的 build.gradle。Gradle 会应用默认的构建脚本,不管是否另有脚本被指定。这意味着你只需要声明你所需要的额外内容。以下是一个例子。请注意,此示例不需要声明对 Gradle API 的依赖,因为这已由默认的构建脚本完成:
If you need more flexibility, you can provide your own build.gradle. Gradle applies the default build script regardless of whether there is one specified. This means you only need to declare the extra things you need. Below is an example. Notice that this example does not need to declare a dependency on the Gradle API, as this is done by the default build script:

示例 59.3. 自定义 buildSrc 构建脚本 - Example 59.3. Custom buildSrc build script

buildSrc/build.gradle

repositories {
    mavenCentral()
}

dependencies {
    testCompile 'junit:junit:4.11'
}

buildSrc 项目可以是一个多项目构建,就像其他常规 Gradle 多项目构建。但是,你需要使你想要的所有项目,在 buildSrc 根项目的实际构建 runtime 依赖的类路径上。你可以通过把它添加到每一个想要导出的项目的配置上来完成:
The buildSrc project can be a multi-project build. This works like any other regular Gradle multi-project build. However, you need to make all of the projects that you wish be on the classpath of the actual build runtime dependencies of the root project in buildSrc. You can do this by adding this to the configuration of each project you wish to export:

示例 59.4. 将子项目添加到根 buildSrc 项目 - Example 59.4. Adding subprojects to the root buildSrc project

buildSrc/build.gradle

rootProject.dependencies {
  runtime project(path)
}

注意: 此示例的代码可以在 Gradle 的二进制文件或源码中的 samples/multiProjectBuildSrc 里找到。
Note: The code for this example can be found at samples/multiProjectBuildSrc which is in both the binary and source distributions of Gradle.


59.4. 从一个构建中运行另一个 Gradle 构建

59.4. Running another Gradle build from a build

你可以使用 GradleBuild 任务。你可以使用 dirbuildFile 属性来指定要执行的构建,以及使用 tasks 属性来指定要执行的任务。
You can use the GradleBuild task. You can use either of the dir or buildFile properties to specify which build to execute, and the tasks property to specify which tasks to execute.

示例 59.5. 从一个构建运行另一个构建 - Example 59.5. Running another build from a build

build.gradle

task build(type: GradleBuild) {
    buildFile = 'other.gradle'
    tasks = ['hello']
}

other.gradle

task hello << {
    println "hello from the other build."
}

gradle -q build 的输出结果
Output of gradle -q build

> gradle -q build
hello from the other build.

59.5. 构建脚本的外部依赖

59.5. External dependencies for the build script

如果构建脚本需要使用外部库,那么可以将它们添加到构建脚本本身的脚本类路径中。你可以通过使用 buildscript() 方法,并传入一个声明构建脚本类路径的闭包来完成。
If your build script needs to use external libraries, you can add them to the script's classpath in the build script itself. You do this using the buildscript() method, passing in a closure which declares the build script classpath.

示例 59.6. 声明构建脚本的外部依赖 - Example 59.6. Declaring external dependencies for the build script

build.gradle

buildscript {
    repositories {
        mavenCentral()
}
    }
    dependencies {
        classpath group: 'commons-codec', name: 'commons-codec', version: '1.2'
    }
}

这个传给 buildscript() 方法的闭包配置了一个 ScriptHandler 实例。你可以通过将依赖添加到 classpath 配置的方式来声明构建脚本类路径。这与 Java 编译类路径的声明方式是一样的。你可以使用第 50.4 节,《如何声明你的依赖》中描述的除项目依赖之外的任何依赖类型。
The closure passed to the buildscript() method configures a ScriptHandler instance. You declare the build script classpath by adding dependencies to the classpath configuration. This is the same way you declare, for example, the Java compilation classpath. You can use any of the dependency types described in Section 50.4, “How to declare your dependencies”, except project dependencies.

声明了构建脚本类路径后,你可以在构建脚本中使用这些类,就像使用其他在类路径上的类一样。以下示例是在上一示例的基础上,增加了使用构建脚本类路径中的类。
Having declared the build script classpath, you can use the classes in your build script as you would any other classes on the classpath. The following example adds to the previous example, and uses classes from the build script classpath.

示例 59.7. 具有外部依赖的构建脚本 - Example 59.7. A build script with external dependencies

build.gradle

import org.apache.commons.codec.binary.Base64

buildscript {
    repositories {
        mavenCentral()
}
    }
    dependencies {
        classpath group: 'commons-codec', name: 'commons-codec', version: '1.2'
    }
}

task encode << {
    def byte[] encodedString = new Base64().encode('hello world\n'.getBytes())
    println new String(encodedString)
}

gradle -q encode 的输出结果
Output of gradle -q encode

> gradle -q encode
aGVsbG8gd29ybGQK

对于多项目构建,定义在一个项目的构建脚本中的依赖,可用于所有子项目的构建脚本。
For multi-project builds, the dependencies declared in the a project's build script, are available to the build scripts of all sub-projects.

59.6. Ant 可选依赖

59.6. Ant optional dependencies

出于我们尚未完全理解的原因,Ant 的可选任务不会选取外部依赖,但你可以很容易地用用另一种方式来实现。[26]
For reasons we don't fully understand yet, external dependencies are not picked up by Ant's optional tasks. But you can easily do it in another way. [26]

示例 59.8. Ant 的可选依赖 - Example 59.8. Ant optional dependencies

build.gradle

configurations {
    ftpAntTask
}

dependencies {
    ftpAntTask("org.apache.ant:ant-commons-net:1.9.3") {
        module("commons-net:commons-net:1.4.1") {
            dependencies "oro:oro:2.0.8:jar"
        }
    }
}

task ftp << {
    ant {
        taskdef(name: 'ftp',
                classname: 'org.apache.tools.ant.taskdefs.optional.net.FTP',
                classpath: configurations.ftpAntTask.asPath)
        ftp(server: "ftp.apache.org", userid: "anonymous", password: "me@myorg.com") {
            fileset(dir: "htdocs/manual")
        }
    }
}

这对于客户端模块的用法也是一个不错的示例。对于这个用例而言,在 Maven 中央仓中针对 ant-commons-net 任务的 POM 文件没有提供到正确的信息。
This is also nice example for the usage of client modules. The POM file in Maven Central for the ant-commons-net task does not provide the right information for this use case.

59.7. 总结

59.7. Summary

Gradle 提供了多种方法来组织构建逻辑。你可以选择适合你的领域的方式,并在不必要的方向之间找到合适的平衡,避免冗余和使代码库难以维护。我们的经验是,即使非常复杂的定制构建逻辑,也很少在不同构建之间共享。其他构建工具强制将此构建逻辑分隔为单独的项目,Gradle 则给你节省了这些不必要的开销和间接性。
Gradle offers you a variety of ways of organizing your build logic. You can choose what is right for your domain and find the right balance between unnecessary indirections, and avoiding redundancy and a hard to maintain code base. It is our experience that even very complex custom build logic is rarely shared between different builds. Other build tools enforce a separation of this build logic into a separate project. Gradle spares you this unnecessary overhead and indirection.



[25] 可能是从一个类到一些非常复杂的东西的范围。
[25] Which might range from a single class to something very complex.

[26] 实际上,我们认为这是更好的解决方案。仅当你的构建脚本和 Ant 的可选任务需要 相同的 库时,你才需要定义它两次。在这种情况下,如果 Ant 的可选任务能自动获取 gradesettings 中定义的类路径,会相当不错。
[26] In fact, we think this is anyway the nicer solution. Only if your buildscript and Ant's optional task need the same library you would have to define it two times. In such a case it would be nice, if Ant's optional task would automatically pickup the classpath defined in the gradesettings.