Chapter 14
Multi-project builds
The powerful support for multi-project builds is one of Gradles unique selling points. This topic is also the intellectually
most challenging.
14.1 Cross Project Configuration
Let’s start with a very simple multi-project build. After all Gradle is a general purpose build tool at its core. So the
projects don’t have to be java projects. Our first examples are about marine life.
14.1.1 Defining Common Behavior
We have the following project tree .
This is a multi-project build with a root project water and a subproject bluewhale.
D- water
F- build.gradle
F- settings.gradle
D- bluewhale
|
And where is the build script for the bluewhale project? In Gradle build scripts are optional. Obviously for a single
project build, a project without a build script doesn’t make much sense. For multiproject builds the situation is different.
Let’s look at the build script for the water project and execute it.:
Closure cl = { task -> println "I'm $task.project.name" }
createTask('hello', cl)
project(':bluewhale').createTask('hello', cl)
>gradle -q hello
I'm water
I'm bluewhale
Gradle allows you to access any project of the multi-project build from any build script. The Project API provides a method
called project, which takes a path as an argument and returns the project object for this path. The capability to configure
a project build from any build script we call Cross Project Configuration. Gradle implements this via Configuration
Injection.
We are not that happy with the build script of the water project. It is inconvenient to add the task
explicitly for every project. We can do better. Let’s first add another project called krill to our multi-project
build.
D- water
F- build.gradle
F- settings.gradle
D- bluewhale
D- krill
|
include 'bluewhale', 'krill'
|
Now we rewrite the water build script and boil it down to a single line.
allprojects {
createTask('hello') { task -> println "I'm $task.project.name" }
}
>gradle -q hello
I'm water
I'm bluewhale
I'm krill
Is this cool or is this cool? And how does this work? The Project API provides a property allprojects which returns a list
with the current project and all its subprojects underneath it. If you call allprojects with a closure, the statements of
the closure are delegated to the projects associated with allprojects. You could also do an iteration via
allprojects.each, but that would be more verbose.
Other build systems use inheritance as the primary means for defining common behavior. We also offer inheritance
for projects as you will see later. But Gradle uses Configuration Injection as the usual way of defining common
behavior. We think it provides a very powerful and flexible way of configuring multiproject builds.
14.2 Subproject Configuration
The Project API also provides a property for accessing the subprojects only.
14.2.1 Defining Common Behavior
allprojects {
createTask('hello') {task -> println "I'm $task.project.name" }
}
subprojects {
hello.doLast {println "- I depend on water"}
}
>gradle -q hello
I'm water
I'm bluewhale
- I depend on water
I'm krill
- I depend on water
14.2.2 Adding Specific Behavior
You can add specific behavior on top of the common behavior. Usually we put the project specific behavior
in the build script of the project where we want to apply this specific behavior. But as we have already
seen, we don’t have to do it this way. We could add project specific behavior for the bluewhale project like
this:
allprojects {
createTask('hello') {task -> println "I'm $task.project.name" }
}
subprojects {
hello.doLast {println "- I depend on water"}
}
project(':bluewhale').hello.doLast {
println "I'm the largest animal that has ever lived on this planet."
}
>gradle -q hello
I'm water
I'm bluewhale
- I depend on water
I'm the largest animal that has ever lived on this planet.
I'm krill
- I depend on water
As we have said, we usually prefer to put project specific behavior into the build script of this project. Let’s refactor and also
add some project specific behavior to the krill project.
D- water
F- build.gradle
F- settings.gradle
D- bluewhale
F- build.gradle
D- krill
F- build.gradle
|
include 'bluewhale', 'krill'
|
hello.doLast { println "- I'm the largest animal that has ever lived on this planet." }
hello.doLast {
println "- The weight of my species in summer is twice as heavy as all human beings."
}
allprojects {
createTask('hello') {task -> println "I'm $task.project.name" }
}
subprojects {
hello.doLast {println "- I depend on water"}
}
>gradle -q hello
I'm water
I'm bluewhale
- I depend on water
- I'm the largest animal that has ever lived on this planet.
I'm krill
- I depend on water
- The weight of my species in summer is twice as heavy as all human beings.
14.2.3 Project Filtering
To show more of the power of Configuration Injection, lets’ add another project called tropicalFish and add more
behavior to the build via the build script of the water project.
Filtering By Name
D- water
F- build.gradle
F- settings.gradle
D- bluewhale
F- build.gradle
D- krill
F- build.gradle
D- tropicalFish
|
include 'bluewhale', 'krill', 'tropicalFish'
|
allprojects {
createTask('hello') {task -> println "I'm $task.project.name" }
}
subprojects {
hello.doLast {println "- I depend on water"}
}
configureProjects(subprojects.findAll {it.name != 'tropicalFish'}) {
hello.doLast {println '- I love to spend time in the arctic waters.'}
}
>gradle -q hello
I'm water
I'm bluewhale
- I depend on water
- I love to spend time in the arctic waters.
- I'm the largest animal that has ever lived on this planet.
I'm krill
- I depend on water
- I love to spend time in the arctic waters.
- The weight of my species in summer is twice as heavy as all human beings.
I'm tropicalFish
- I depend on water
The configureProjects takes a list as an argument and applies the configuration to the projects in this
list.
Filtering By Properties
Using the projectname for filtering is one option. Using dynamic project properties is another.
D- water
F- build.gradle
F- settings.gradle
D- bluewhale
F- build.gradle
D- krill
F- build.gradle
D- tropicalFish
F- build.gradle
|
include 'bluewhale', 'krill', 'tropicalFish'
|
arctic = true
hello.doLast { println "- I'm the largest animal that has ever lived on this planet." }
arctic = true
hello.doLast {
println "- The weight of my species in summer is twice as heavy as all human beings."
}
allprojects {
createTask('hello') {task -> println "I'm $task.project.name" }
}
subprojects {
hello {
doLast {println "- I depend on water"}
lateInitialize {
if (project.arctic) { doLast {
println '- I love to spend time in the arctic waters.' }
}
}
}
}
>gradle -q hello
I'm water
I'm bluewhale
- I depend on water
- I'm the largest animal that has ever lived on this planet.
- I love to spend time in the arctic waters.
I'm krill
- I depend on water
- The weight of my species in summer is twice as heavy as all human beings.
- I love to spend time in the arctic waters.
I'm tropicalFish
- I depend on water
In the gradefile of the water project we use lateInitialize. This means that the closure we are passing get 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. You will find more on this topic in section 14.5
14.3 Execution rules for multi-project builds
When we have executed the hello task from the root project dir things behaved in an intuitive way. All the hello tasks
of the different projects were executed. Let’s switch to the bluewhale dir and see what happens if we execute Gradle from
there.
>gradle -q hello
I'm bluewhale
- I depend on water
- I'm the largest animal that has ever lived on this planet.
- I love to spend time in the arctic waters.
The basic rule behind Gradles behavior is simple. Gradle looks down the hierarchy, starting with the current dir, for tasks
with the name hello an 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 Gradles Cross Project
Configuration every project has to be evaluated before any task gets executed. We will have a closer look at this
in the next section. Let’s now have our last marine example. Let’s add a task to bluewhale and krill.
arctic = true
hello.doLast { println "- I'm the largest animal that has ever lived on this planet." }
createTask('distanceToIceberg') {
println '20 nautical miles'
}
arctic = true
hello.doLast { println "- The weight of my species in summer is twice as heavy as all human beings." }
createTask('distanceToIceberg') {
println '5 nautical miles'
}
>gradle -q distanceToIceberg
20 nautical miles
5 nautical miles
Here the output without the -q option
>gradle distanceToIceberg
Modern compiler found.
Recursive: true
Buildfilename: build.gradle
No build sources found.
:: loading settings :: url = jar:file:/Users/hans/java/gradle-SNAPSHOT/lib/ivy-2.0.0.beta2_20080305165542.jar!/org/apache/ivy/core/settings/ivysettings.xml
:: resolving dependencies :: org.gradle#build;SNAPSHOT
confs: [build]
++++ Starting build for primary task: distanceToIceberg
++ Loading Project objects
++ Configuring Project objects
Project=: evaluated.
Project=:bluewhale evaluated.
Project=:krill evaluated.
Project=:tropicalFish evaluated.
++ Executing: distanceToIceberg Recursive:true Startproject: :
Executing: :bluewhale:distanceToIceberg
20 nautical miles
Executing: :krill:distanceToIceberg
5 nautical miles
BUILD SUCCESSFUL
Total time: 1 seconds
The build is executed from the water project. Neither water nor tropicalFish have a task with the name
distanceToIceberg. Gradle does not care. The simple rule mentioned already above is: Execute all tasks down the
hierarchy which have this name. Only complain if there is no such task!
14.4 Project and Task Paths
A project path has the following pattern: It starts always with a colon, which denotes the root project. The root project is
the only project in a path that is not specified by its name. The path :bluewhale corresponds to the file system path
water/project in the case of the example above.
The path of a task is simply its project path plus the task name. For example :bluewhale:hello. Within
a project you can address a task of the same project just by its name. This is interpreted as a relative
path.
Originally Gradle has used the ’/’ character as a natural path separator. With the introduction of
directory tasks (see 4.3) this was no longer possible, as the name of the directory task contains the ’/’
character.
14.5 Dependencies - Which dependencies?
The examples from the last section were special, as the projects had no Execution Dependencies. They had only
Configuration Dependencies. Here is an example where this is different:
14.5.1 Execution Dependencies
Dependencies and Execution Order
D- messages
F- settings.gradle
D- consumer
F- build.gradle
D- producer
F- build.gradle
|
include 'consumer', 'producer'
|
createTask('action') {
println "Consuming message: " + System.getProperty('org.gradle.message')
}
createTask('action') {
println "Producing message:"
System.setProperty('org.gradle.message', 'Watch the order of execution.')
}
>gradle -q action
Consuming message: null
Producing message:
This did not work out. If nothing else is defined, Gradle executes the task in alphanumeric order. Therefore
:consumer:action is executed before :producer:action. Let’s try to solve this with a hack and rename the producer
project to aProducer.
D- messages
F- settings.gradle
D- aProducer
F- build.gradle
D- consumer
F- build.gradle
|
include 'consumer', 'aProducer'
|
createTask('action') {
println "Producing message:"
System.setProperty('org.gradle.message', 'Watch the order of execution.')
}
createTask('action') {
println "Consuming message: " + System.getProperty('org.gradle.message')
}
>gradle -q action
Producing message:
Consuming message: Watch the order of execution.
Now we take the air out of this hack. We simply switch to the consumer dir and execute the build.
>gradle -q action
Consuming message: null
For Gradle the two action tasks are just not related. If you execute the build from the messages project Gradle executes
them both because they have the same name and they are down the hierarchy. In the last example only one action was
down the hierarchy and therefore it was the only task that got executed. We need something better than this
hack.
Declaring Dependencies
D- messages
F- settings.gradle
D- consumer
F- build.gradle
D- producer
F- build.gradle
|
include 'consumer', 'producer'
|
dependsOn(':producer')
createTask('action') {
println "Consuming message: " + System.getProperty('org.gradle.message')
}
createTask('action') {
println "Producing message:"
System.setProperty('org.gradle.message', 'Watch the order of execution.')
}
>gradle -q action
Producing message:
Consuming message: Watch the order of execution.
>gradle -q action
Producing message:
Consuming message: Watch the order of execution.
We have now declared that the consumer project has an execution dependency on the producer project. For Gradle declaring
execution dependencies between projects is syntactic sugar. Under the hood Gradle creates task dependencies out of
them. You can also create cross project tasks dependencies manually by using the absolute path of the
tasks.
The Nature of Project Dependencies
Let’s change the naming of our tasks and execute the build.
dependsOn(':producer')
createTask('consume') {
println "Consuming message: " + System.getProperty('org.gradle.message')
}
createTask('produce') {
println "Producing message:"
System.setProperty('org.gradle.message', 'Watch the order of execution.')
}
>gradle -q consume
Consuming message: null
Uhps. Why does this not work? The dependsOn command is created for projects with a common lifecycle. Provided
you have two Java projects were one depends on the other. If you trigger a compile for for the dependent
project you don’t want that all tasks of the other project get executed. Therefore a dependsOn creates
dependencies between tasks with equal names. To deal with the scenario above you would do the following:
createTask('consume', dependsOn: ':producer:produce') {
println "Consuming message: " + System.getProperty('org.gradle.message')
}
createTask('produce') {
println "Producing message:"
System.setProperty('org.gradle.message', 'Watch the order of execution.')
}
>gradle -q consume
Producing message:
Consuming message: Watch the order of execution.
14.5.2 Configuration Time Dependencies
Let’s have one more example with our producer-consumer build before we enter Java land. We add a property to the
producer project and create now a configuration time dependency from consumer on producer.
key = 'unknown'
if (project(':producer').hasProperty('key')) {
key = project(':producer').key
}
createTask('consume', dependsOn: ':producer:produce') {
println "Consuming message from key '$key': " + System.getProperty(key)
}
key = 'org.gradle.message'
createTask('produce') {
println "Producing message:"
System.setProperty(key, 'Watch the order of execution.')
}
>gradle -q consume
Producing message:
Consuming message from key 'unknown': null
The default evaluation order of the projects is alphanumeric (for the same nesting level). Therefore the consumer project is
evaluated before the producer project and the key value of the producer is set after it is read by the consumer project.
Gradle offers a solution for this.
evaluationDependsOn(':producer')
key = 'unknown'
if (project(':producer').hasProperty('key')) {
key = project(':producer').key
}
createTask('consume', dependsOn: ':producer:produce') {
println "Consuming message from key '$key': " + System.getProperty(key)
}
>gradle -q consume
Producing message:
Consuming message from key 'org.gradle.message': Watch the order of execution.
The command evaluationDependsOn triggers the evaluation of producer before consumer is evaluated. The example is a bit
contrived for the sake of showing the mechanism. In this case there would be an easier solution by reading the key
property at execution time.
createTask('consume', dependsOn: ':producer:produce') {
String key = project(':producer').key
println "Consuming message from key '$key': " + System.getProperty(key)
}
>gradle -q consume
Producing message:
Consuming message from key 'org.gradle.message': Watch the order of execution.
Configuration dependencies are very different to execution dependencies. Configuration dependencies are between projects
whereas execution dependencies are always resolved to task dependencies. Another difference is that always all projects
are configured, even when you start the build from a subproject. The default configuration order is top down, which is
usually what is needed.
On the same nesting level the configuration order depends on the alphanumeric position. The most common use case
is to have multi-project builds that share a common lifecycle (e.g. all projects use the Java plugin). If you declare with
dependsOn a execution dependency between different projects, the default behavior of this method is to create also a
configuration dependency between the two projects. Therefore it is likely that you don’t have to define configuration
dependencies explicitly.
14.5.3 Real Life examples
Gradles multi-project features are driven by real life use cases. The first example for describing such a
use case, consists of two webapplication projects and a parent project that creates a distribution out of
them.
For the example we use only one build script and do cross project configuration.
D- webDist
F- settings.gradle
F- build.gradle
D- date
F- src/main/java/org/gradle/sample/DateServlet.java
D- hello
F- src/main/java/org/gradle/sample/HelloServlet.java
dependsOnChildren()
allprojects {
usePlugin('java')