[关闭]
@xtccc 2016-02-14T14:33:30.000000Z 字数 10572 阅读 4264

多项目构建

给我写信
GitHub

此处输入图片的描述

Gradle




1. Multiproject Build Structure


1.1 目录层次结构

Multiproject通常的层次结构为:它有一个master project,包含了若干个subprojects。其中,subproject也可以是嵌套结构。master project位于顶层目录中,subprojects则位于各自的子目录中。

Gradle允许我们为master project创建一个build file,并为每一个subproject分别创建各自的build file。

1.2 settings.gradle

为了让Gradle知道master project的子目录中,哪些子目录是真正包含项目的,我们必须定义 settings.gradle 文件。这是一个配置文件,它独立于 build.gradle 文件。在最简单的情况下,settings.gradle 可以仅仅列出那些包含了subprojects的子目录的名字。

当Gradle运行其生命周期的Initialization阶段时,它首先去寻找 settings.gradle 文件,并从中找出那些包含了子项目的子目录。如果这些子目录包含了它们自己的 build.gradle 文件,这些子项目会被加入到一个DAG中,这个DAG被用于描述整个构建过程。

例:

如果我们的master project中含有2个subprojects(codec与content),则可以如下定义master project的settings.gradle

  include "codec", "content"

实际上,在master project并不需要真正地存在 codeccontent 这两个subprojects,只要 settings.gradle文件中包含了这个两个子项目,Gradle即认为master project包含了这两个子项目。

通过命令 gradle project,可以查看整个项目的层次结构:
QQ20160211-2@2x.png-114.2kB


Master project中 build.gradle 文件可以用来为subprojects共享配置,例如为所有的subprojects应用相同的plugins和dependencies。因此,当我们想要弄清楚一个subproject的配置情况时,我们不要忘记查看master project的配置

实际上,build files不一定要命名为 build.gradle,它可以其他的 *.gradle 文件名。例如,许多项目会将build file命名为项目的名称,如 service.gradle、api.gradle等等。




1.3 Project Tree

Multi-project build是由一棵single-root tree表示的,树的每一个节点代表一个项目。这个树是由根项目的settings.gradle文件中的include/includeFlat创建的。

Multi-project tree是由 project descriptors 构成的,我们可以在settings文件中访问和修改这些descriptors。

假设我们有一个名为gradle_project的根项目,里面有一个名为Module_JavaExec的子项目,层次结构如下:
QQ20160211-4@2x.png-62.2kB

那么,我们可以在 gradle_project/settings.gradle文件中修改并访问 project descriptors的属性:

  1. include "Module_JavaExec"
  2. println 'rootProject.name = [' + rootProject.name + ']'
  3. rootProject.name = "master project"
  4. println 'rootProject.name = [' + rootProject.name + ']'
  5. println "project(':Module_JavaExec').name = [" + project(':Module_JavaExec').name + "]"
  6. println "rootProject.buildFile = [" + rootProject.buildFile + "]"
  7. println "project(':Module_JavaExec').projectDir = [" + project(':Module_JavaExec').projectDir + "]"




2. Cross Project Configuration


在多项目的构建过程中,Gradle允许我们在任意一个build script中访问任意项目。Project API 提供了一个名为 project 的方法,它接受一个路径作为参数,返回与该路径对应的Project对象。

假设目录 water 的结构如下:

  1. water
  2. ├── build.gradle
  3. └── settings.gradle

其中,settings.gradle的内容为:

  1. include "krill", "bluewhale"

build.gradle的内容为:

  1. Closure cl = {
  2. task -> println "I'm project [$task.project.name]"
  3. }
  4. task hello << cl
  5. project(":krill") {
  6. task hello << cl
  7. }
  8. project(":bluewhale") {
  9. task hello << cl
  10. }

那么,在water目录执行命令 gradle -q hello,可以看到以下输出:
QQ20160211-5@2x.png-47.4kB


可见,在master project中执行 gradle -q hello,不仅master project的hello任务被执行了,而且随后所有subprojects中的hello任务都被执行了。



Project API提供了一个称为 allprojects 的属性,它会返回一个列表,其中包含了当前项目及其所有的子项目。如果在调用 allprojects 时,对其应用一个闭包,则闭包会应用于所有相应的项目。

因此,上面的 build.gradle 文件也可以改写为:

  1. allprojects {
  2. task hello << {
  3. task -> println "I'm project [$task.project.name]" }
  4. }

运行结果是一样的。



3. Subproject Configuration


3.1 Defining Common Behavior

我们不仅可以对所有的项目定义指定的动作,还可以通过 subprojects 实现为所有的子项目定义某些动作。

上面的 settings.gradle 文件保持不变:

  1. include "bluewhale", "krill"

build.gradle 文件内容修改如下:

  1. allprojects {
  2. task hello << { task -> println "I'm project [$task.project.name]" }
  3. }
  4. subprojects {
  5. hello << { println " -- I depend on water" }
  6. }

那么,在water目录下执行 gradle -q hello 的结果如下:
QQ20160212-1@2x.png-56.2kB



allprojects 的闭包内,我们用关键字 task 创建了任务 hello;而在 subprojects 的闭包内,我们不能再使用关键字 task,因为一个任务只能被创建一次,但是可以不断地向其中添加代码和配置。


3.2 Adding Specific Behavior

如果想为某个project添加特定的动作,那么可以在该project的build script中完成。但是,我们也可以在master project的build script中为某个subproject添加特定的动作。

例如,下面我们将在water项目的build script中为子项目bluewhale的hello任务添加一个特定的动作。

  1. // water/build.gradle文件
  2. allprojects {
  3. task hello << { task -> println "I'm project [$task.project.name]" }
  4. }
  5. subprojects {
  6. hello << { println " -- I depend on water" }
  7. }
  8. // 在这里为子项目添加特定的配置
  9. project(':bluewhale').hello << {
  10. println " -- 我是蓝鲸"
  11. }

运行命令 gradle -q hello 后,输出如下:
QQ20160212-2@2x.png-55.6kB



当然,这种方式是不提倡的。提倡的方式是:在bluewhale项目的build script中添加与该项目特定的配置。

所以,这类我们应该改为创建目录 water/bluewhale,并在其中创建文件 water/bluewhale/build.gradle,然后添加内容:

  1. hello << {
  2. println " -- 我是蓝鲸"
  3. }

运行的结果是一样的。

注意:这里不能写成 task hello << { ... } ,因为名为 hello 的任务在 water/build.gradle 中已经被定义了,不能再次定义,只能往里面添加新内容。


3.3 Project Filtering

3.3.1 Filtering by Name

configure 方法接受一个列表作为参数,并将配置应用到列表中的每一个项目。下面,我们添加一个新的子项目 tropical,并将名字为bluewhale的子项目都过滤掉。

settings.gradle文件:

  1. include "krill", "bluewhale", "tropical"

build.gradle文件:

  1. allprojects {
  2. task hello << { task -> println "I'm project [$task.project.name]" }
  3. }
  4. subprojects {
  5. hello << { println " -- I depend on water" }
  6. }
  7. configure(subprojects.findAll{it.name != 'bluewhale'}) {
  8. hello << { println ' -- 我不是蓝鲸,我没被过滤掉'}
  9. }

在water目录中运行命令 gradle -q hello 的结果为:
QQ20160212-3@2x.png-108.7kB


3.3.2 Filtering by Property

Master project hierarchy:

  1. water
  2. ├── build.gradle
  3. ├── settings.gradle
  4. ├── bluewhale
  5.    └── build.gradle
  6. ├── krill
  7.    └── build.gradle
  8. └── tropical
  9. └── build.gradle

water/bluewhale/build.gradle:

  1. ext.arctic = true
  2. hello.doLast {
  3. println " -- 我是蓝鲸"
  4. }

water/krill/build.gradle:

  1. ext.arctic = true
  2. hello.doLast {
  3. println " -- 我是磷虾"
  4. }

water/tropical/build/gradle:

  1. ext.arctic = false

water/settings.gradle:

  1. include "krill", "bluewhale", "tropical"

water/build.gradle:

  1. allprojects {
  2. task hello << { task -> println "I'm project [$task.project.name]" }
  3. }
  4. subprojects {
  5. hello {
  6. doLast { println " -- I depend on [water]"}
  7. afterEvaluate { Project proj ->
  8. if (project.arctic) {
  9. doLast {
  10. println " -- 我来自北极"
  11. }
  12. }
  13. }
  14. }
  15. }

在water目录下运行命令 gradle -q hello 的输出结果为:
QQ20160212-5@2x.png-121.9kB



关于 afterEvaluate:

afterEvaluate means the closure we are passing gets evaluated after the build scripts of the subproject are evaluated. As the property arctic is set in those build scripts, we have to do it this way.


4. Execution Rules for Multi-project Builds


4.1 Executing Tasks By Name

在第3.3.2节的例子中,我们在master project中执行 gradle -q hello 后,不仅master project的hello任务被执行了,而且随后所有subprojects中的hello任务都被执行了。

那么,如果不在master project中执行该命令,而是在 bluewhale 这个subproject中执行该命令,会怎么样呢?

QQ20160212-6@2x.png-79.2kB

可见,这样只会执行当前项目下的hello任务。但是回忆一下,我们在master project中的build.gradle文件中为该子项目的hello任务添加了新的配置,这部分也起作用了。

Gradle looks down the hierarchy, starting with the current dir, for tasks with the name hello and executes them. One thing is very important to note: Gradle always evaluates every project of the multi-project build and creates all existing task objects. Then, according to the task name arguments and the current dir, Gradle filters the tasks which should be executed. Because of Gradle's cross project configuration every project has to be evaluated before any task gets executed.


4.2 Executing Tasks By Absolute Path

现在,我们处于 water/bluewhale 目录下,要分别执行bluewhale子项目的hello任务、water项目的hello任务、krill子项目的hello任务。

QQ20160212-8@2x.png-160kB

其中, hello 是通过任务名指定的,:hello:krill:hello 是通过绝对路径指定的。


4.3 Project and Task Paths

一个project的路径可以用冒号开头,冒号是可选的。冒号本省代表master project;project path的剩余部分是由冒号分割的project name。

例如, :, :bluewhale, :tropical:A

而path of a task 则是project path加上task name。

例如, :hello, :bluewhale:hello




5. Dependencies


5.1 Task Execution Dependencies

我们可以定义各个tasks之间的依赖关系,即要求几个tasks的执行有先后顺序。

假设我们有如下的需求:

根项目为message,其中的两个子项目为producerconsumer,它们都有名为action的任务。其中,:producer:action任务负责为根项目添加一个名为producerMsg的属性,:consumer:action任务负责读取根项目的该属性并显示出来。

提示:为根项目添加额外的属性prop时,要写成 rootProject.ext.prop 的形式



Master Project Hierarchy:

  1. message/
  2. ├── consumer
  3.    └── build.gradle
  4. ├── producer
  5.    └── build.gradle
  6. └── settings.gradle

message/settings.gradle:

  1. include 'consumer', 'producer'

message/producer/build.gradle:

  1. task action << {
  2. println "我是生产者,我正在生产一条消息"
  3. rootProject.ext.producerMsg = "今天初六,小雨,过一会儿回南京"
  4. }

message/consumer/build.gradle:

  1. task action() << {
  2. println "我是Consumer, 来自生产者的消息为 ${rootProject.ext.producerMsg}"
  3. }

在message目录下执行命令 gradle -q action,运行输出如下:
QQ20160213-1@2x.png-128.6kB

原因: If nothing else is defined, Gradle executes the task in alphanumeric order. Therefore, Gradle will execute :consumer:action before :producer:action.


因此,我们需要让 :consumer:action 依赖于 :producer:action,这样可以保证属性rootProject.ext.producerMsg 先被产生,然后才被消费。

message/consumer/build.gradle 改为:

  1. task action(dependsOn: ":producer:action") << {
  2. println "我是Consumer, 来自生产者的消息为 ${rootProject.ext.producerMsg}"
  3. }

现在可以正确输出:
QQ20160213-2@2x.png-46.6kB

Task之间的依赖当然不限于同名的任务,下面我们把action名称改一下,分别改为 :consumer:consumption:producer:production

message/producer/build.gradle:

  1. task production << {
  2. println "我是生产者,我正在生产一条消息"
  3. rootProject.ext.producerMsg = "今天初六,小雨,过一会儿回南京"
  4. }

message/consumer/build.gradle:

  1. task consumption(dependsOn: ":producer:production") << {
  2. println "我是Consumer, 来自生产者的消息为 ${rootProject.ext.producerMsg}"
  3. }

message目录下运行命令 gradle -q consumption,输出结果是一样的。


5.2 Project Evaluation Dependencies

除了设置Tasks之间的依赖关系之外,还可以通过 evaluationDependesOn 来设置Projects之间的依赖关系。

将5.1节中的例子作一些修改:

message/producer/build.gradle:

  1. rootProject.ext.producerMsg = "今天初六,小雨,过一会儿回南京"

message/consumer/build.gradle:

  1. evaluationDependsOn(":producer")
  2. def msg = rootProject.ext.producerMsg
  3. task consumption() << {
  4. println "我是Consumer, 来自生产者的消息为 ${msg}"
  5. }

message目录下运行命令 gradle -q :consumer:consumption,可以得到相同的结果

关于 evaluationDependsOn

The default evaluation order of projects is alphanumeric (for the same nesting level). Therefore the :consumer project is evaluated before the :producer project and the producerMsg value is set after it is read by the :consumer project.

The use of the evaluationDependsOn command results in the evaluation of the :producer project before the :consumer project is evaluated.


5.3 Project Evaluation Guarantee

除了5.1节和5.2节中的解决方法外,还有另外一种更简单的解决方法。

message/producer/build.gradle:

  1. rootProject.ext.producerMsg = "今天初六,小雨,过一会儿回南京"

message/consumer/build.gradle:

  1. task consumption() << {
  2. println "我是Consumer, 来自生产者的消息为 ${rootProject.ext.producerMsg}"
  3. }

message目录下运行命令 gradle -q :consumer:consumption,可以看到输出结果:
QQ20160214-0@2x.png-24.7kB


为什么只执行:consumer:consumption这个任务就能直接读取到由:producer项目设置的属性呢?

即使我们只从某个子项目开始构建,所有的项目都会在此之前被配置。默认的configuration order是自上而下,如果想改变,则可以用evaluationDependsOnChildren方法来实现。在同一个嵌套层次内,configuration order是项目名的字母顺序。


5.4 Execution Dependency 影响 Configuration Dependency

如果Project p1中的Task t1依赖于Project p2中的Task t2,那么,即使我们没有指定p1的配置依赖于p2的配置,p1的配置实际上也是依赖于p2的配置的。


5.5 Project Compilation Dependency

对于一个Project,可以在几个方面指定它对其他JAR/Project的依赖:



举例:

假设根项目message的结构如下:

  1. message/
  2. ├── consumer
  3.    └── build.gradle
  4. ├── producer
  5.    └── build.gradle
  6. ├── build.gradle
  7. └── settings.gradle

message/settings.gradle:

  1. include ':consumer', ':producer'

message/build.gradle:

  1. subprojects {
  2. apply plugin: 'java' // java plugin提供了`build` task
  3. repositories {
  4. mavenCentral()
  5. }
  6. dependencies {
  7. testCompile 'junit:junit:4+' // 所有的子项目都依赖该JAR
  8. }
  9. }
  10. project(':consumer') {
  11. dependencies {
  12. compile project(':producer') // 调用":consumer:build"时,":producer:build"会被首先调用
  13. compile 'log4j:log4j:1.2.17'
  14. }
  15. }

message/consumer/build.gradle:

  1. println "I am [Consumer], I am being evaluated"

message/producer/build.gradle:

  1. println "I am [Producer], I am being evaluated"


5.6 Project Compilation Order

如果Project p1在执行compile task时依赖于Project p2,那么在编译p1时,p2总是会先被编译。

仍然用5.5节中的例子,我们在文件message/build.gradle中通过如下语句指定了":consumer"在编译时依赖于":producer"

  1. project(':consumer') {
  2. dependencies {
  3. compile ':producer'
  4. }
  5. }

因此如果我们只编译子项目:consumer,即在message目录下执行命令gradle -q :consumer:build,那么输出如下:

QQ20160214-1@2x.png-53.7kB

此时,目录message下的内容为:

  1. message/
  2. ├── consumer
  3.    ├── build
  4.       ├── libs
  5.          └── consumer.jar
  6.       └── tmp
  7.       └── jar
  8.       └── MANIFEST.MF
  9.    └── build.gradle
  10. ├── producer
  11.    ├── build
  12.       ├── libs
  13.          └── producer.jar
  14.       └── tmp
  15.       └── jar
  16.       └── MANIFEST.MF
  17.    └── build.gradle
  18. ├── build.gradle
  19. └── settings.gradle
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注