体验Gradle(8):多项目

支持多项目是Gradle的一大特点,而意想不到的简单则是它的一大卖点!

首先,让我们看看多项目是如何定义的。在上一篇里,我们已经知道,多项目构建必须要有一个settings.gradle存在于根项目所在目录。其中定义了本次多项目构建的子项目:

//如果子项目处于根项目的下层目录,则使用include。
//其中的参数是子项目到根项目的虚拟路径,实际上就相当于分隔符为“:”的相对路径。
include 'project1', 'project2', 'project2:child1'

//如果子项目与根项目在同一级,那么就用includeFlat。
//其中参数为目录名
includeFlat 'project3', 'project4'

因为第一种方式更自然,本篇就以第一种为例进行说明。一个典型的Gradle的多项目目录组织如下:

    water/
        build.gradle
        settings.gradle
        bluewhale/
        krill/

其中water是根项目,而bluewhale和krill是子项目,settings.gradle定义的项目包含为:

include 'bluewhale', 'krill'

注意,上面的目录结构里并没有列出每个子项目的构建脚本。这便是Gradle多项目的一个特点:子项目的构建脚本是可选的。在多项目的情况下,你可以在构建脚本里使用路径访问多项目的任意项目,从而对它进行配置。这个能力在Gradle里被称为“配置注入”。

Closure cl = { task -> println "I'm $task.project.name" }
task hello << cl
project(':bluewhale') {
    task hello << cl
}

在命令行下运行“gradle hello”,将打印出两个项目的名字。代码里使用project(':bluewhale')便得到了bluewhale子项目对应的Project对象。

Project对象有两个方法分别可以用来访问当前构建的所有项目和所有子项目:allprojects和subprojects。因此,根据第3篇学到的知识,我们可以在构建脚本里:

allprojects {
    task hello << { task -> println "I'm $task.project.name" }
}

subprojects {
    hello << {println "- I depend on water"}
}

还记得以前学过的内容吗?subprojects内的写法相当于给每个子项目的hello任务添加了一个doLast。

如果想对自己的任务搞点特殊化,那么不妨就用路径来引用相关的任务,然后进行处理:

project(':bluewhale').hello << {
    println "I'm the largest animal that has ever lived on this planet."
}

但这样写毕竟不爽,搞特殊化的东西最好还是待在自己该待的地方。这就是子项目build.gradle的作用了,在这个例子里,它位于bluewhale目录下。把上面代码移到其中就变成:

hello << {
    println "I'm the largest animal that has ever lived on this planet."
}

是不是很清爽呢?通过上面的例子,我们可以得出一个结论:根目录的build.gradle放公共构建逻辑,子项目的build.gradle则包含个性化的构建逻辑

在第一个例子,我们定义了两个hello任务:一个属于根项目;另一个则属于bluewhale。如果在命令行里输入“gradle hello”,这两个任务都会触发运行;而进入bluewhale目录后,执行相同命令,则只会执行属于bluewhale的hello任务。这便是Gradle的执行规则:接受一个任务之后,Gradle会从当前项目开始,执行其以下的同名任务

当然,以上规则对于包含了路径名的任务不适用。所谓任务的路径名,其形式为::项目名:任务名。这是Gradle内绝对路径名的一般形式,由“:”开始,代表根项目,最后一部分是任务名,中间可以有子项目名。简单的讲,项目类似于目录,任务类似于文件,这样更有利于对Gradle里路径的理解。因此:

  • gradle :hello,执行根项目的hello任务
  • gradle :bluewhale:hello,执行bluewhale的hello任务
  • gradle hello,执行当前项目及当前项目以下子项目的hello任务

既然是多项目,那么项目之间存在依赖是必然,否则根本没有必要专门放在一起进行构建。在Gradle里依赖有两种:

  • 配置依赖:某个项目的配置过程依赖另一个项目配置过程的先执行,使用evaluationDependsOn完成。看看摘自参考文档的例子:
  • //producer/build.gradle
    key = 'org.gradle.message'
    task produce << {
        println "Producing message:"
        System.setProperty(key, 'Watch the order of execution.')
    }
    
    //consumer/build.gradle
    evaluationDependsOn(':producer')
    key = 'unknown'
    if (project(':producer').hasProperty('key')) {
        key = project(':producer').key
    }
    task consume(dependsOn: ':producer:produce') << {
        println "Consuming message from key '$key': " + System.getProperty(key)
    }
    
  • 执行依赖:当某个任务执行时,确保另一个任务先执行。

执行依赖听起来是不是有些耳熟?没错,在前面我们已经给出了使用dependsOn属性来定义任务间执行依赖的例子,虽然是单项目环境下。对于多项目,这个属性仍然适用,只不过对于依赖外部项目的任务时,需要使用路径:

task consume(dependsOn: ':producer:produce') << {...}

在多项目环境下,我们还可以直接使用Project的dependsOn和dependsOnChildren来指定依赖。前者表示依赖是项目路径所指项目,后者表示依赖每个子项目。这在两个项目存在有相同任务时特别有用,如两个Java项目A和B,B的编译依赖于A编译的执行完成,此时在B的构建脚本里声明:

dependsOn(':A')

就能达到上述目的,这里假设A和B都是某项目的子项目,处于相同目录级别。

对于多项目的构建,Gradle要灵活得多,它并不是全部都是一股脑地全部编译:

  • -u,不搜索父目录的settings.gradle,直接把当前目录当作单项目处理
  • -a,利用已缓存的编译结果,不编译依赖项目
  • buildNeeded,只编译需要编译的内容
  • buildDepends,编译依赖

参考文档对上述选项有详细的描述,这里就不罗嗦了。同时,参考文档里给出了一个真实的多项目例子,直接参考略作修改即可用于实际项目中。下一篇,本系列将展示一个使用Gradle构建Grails的多项目例子,不要错过哟!

更新(2010-7-24):仔细想想,基于插件划分的Grails多项目工程,Grails本身的脚本就支持得不错了,似乎没有必要专门搞一个这样的例子。而且,本站以前几篇关于用Gradle编译Grails工程的文章,基本套路都是利用Grails现有的脚本架构。还是以后有时间,加上对Gradle的理解深入了,再做个例子吧。


本系列的其它部分:

By foxgem - Posted on 23 七月 2010