The powerful support for multi-project builds is one of Gradle's unique selling points. This topic is also the most intellectually challenging.
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.
We have the following project tree. This is a multi-project build with a root project
water
and a subproject bluewhale
.
Example 49.1. Multi-project tree - water & bluewhale projects
Build layout
water/ build.gradle settings.gradle bluewhale/
Note: The code for this example can be found at samples/userguide/multiproject/firstExample/water
which is in both the binary and source distributions of Gradle.
settings.gradle
include '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:
Example 49.2. Build script of water (parent) project
build.gradle
Closure cl = { task -> println "I'm $task.project.name" } task hello << cl project(':bluewhale') { task hello << cl }
Output of gradle -q hello
> 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.
Example 49.3. Multi-project tree - water, bluewhale & krill projects
Build layout
water/ build.gradle settings.gradle bluewhale/ krill/
Note: The code for this example can be found at samples/userguide/multiproject/addKrill/water
which is in both the binary and source distributions of Gradle.
settings.gradle
include 'bluewhale', 'krill'
Now we rewrite the water
build script and boil it down to a single line.
Example 49.4. Water project build script
build.gradle
allprojects {
task hello << { task -> println "I'm $task.project.name" }
}
Output of gradle -q hello
> 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.
The Project API also provides a property for accessing the subprojects only.
Example 49.5. Defining common behaviour of all projects and subprojects
build.gradle
allprojects { task hello << {task -> println "I'm $task.project.name" } } subprojects { hello << {println "- I depend on water"} }
Output of gradle -q hello
> gradle -q hello I'm water I'm bluewhale - I depend on water I'm krill - I depend on water
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:
Example 49.6. Defining specific behaviour for particular project
build.gradle
allprojects { task hello << {task -> println "I'm $task.project.name" } } subprojects { hello << {println "- I depend on water"} } project(':bluewhale').hello << { println "- I'm the largest animal that has ever lived on this planet." }
Output of gradle -q hello
> 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.
Example 49.7. Defining specific behaviour for project krill
Build layout
water/ build.gradle settings.gradle bluewhale/ build.gradle krill/ build.gradle
Note: The code for this example can be found at samples/userguide/multiproject/spreadSpecifics/water
which is in both the binary and source distributions of Gradle.
settings.gradle
include 'bluewhale', 'krill'
bluewhale/build.gradle
hello.doLast { println "- I'm the largest animal that has ever lived on this planet." }
krill/build.gradle
hello.doLast {
println "- The weight of my species in summer is twice as heavy as all human beings."
}
build.gradle
allprojects { task hello << {task -> println "I'm $task.project.name" } } subprojects { hello << {println "- I depend on water"} }
Output of gradle -q hello
> 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.
To show more of the power of configuration injection, let's add another project
called tropicalFish
and add more behavior to the build via the build script of the
water
project.
Example 49.8. Adding custom behaviour to some projects (filtered by project name)
Build layout
water/ build.gradle settings.gradle bluewhale/ build.gradle krill/ build.gradle tropicalFish/
Note: The code for this example can be found at samples/userguide/multiproject/addTropical/water
which is in both the binary and source distributions of Gradle.
settings.gradle
include 'bluewhale', 'krill', 'tropicalFish'
build.gradle
allprojects { task hello << {task -> println "I'm $task.project.name" } } subprojects { hello << {println "- I depend on water"} } configure(subprojects.findAll {it.name != 'tropicalFish'}) { hello << {println '- I love to spend time in the arctic waters.'} }
Output of gradle -q hello
> 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 configure()
method takes a list as an argument and applies the
configuration to the projects in this list.
Using the project name for filtering is one option. Using extra project properties is another. (See Section 16.4.2, “Extra properties” for more information on extra properties.)
Example 49.9. Adding custom behaviour to some projects (filtered by project properties)
Build layout
water/ build.gradle settings.gradle bluewhale/ build.gradle krill/ build.gradle tropicalFish/ build.gradle
Note: The code for this example can be found at samples/userguide/multiproject/tropicalWithProperties/water
which is in both the binary and source distributions of Gradle.
settings.gradle
include 'bluewhale', 'krill', 'tropicalFish'
bluewhale/build.gradle
ext.arctic = true
hello.doLast { println "- I'm the largest animal that has ever lived on this planet." }
krill/build.gradle
ext.arctic = true
hello.doLast {
println "- The weight of my species in summer is twice as heavy as all human beings."
}
tropicalFish/build.gradle
ext.arctic = false
build.gradle
allprojects { task hello << {task -> println "I'm $task.project.name" } } subprojects { hello { doLast {println "- I depend on water"} afterEvaluate { Project project -> if (project.arctic) { doLast { println '- I love to spend time in the arctic waters.' } } } } }
Output of gradle -q hello
> 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 build file of the water
project we use an afterEvaluate
notification. This means that 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. You will find more on this topic in
Section 49.6, “Dependencies - Which dependencies?”
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.
Example 49.10. Running build from subproject
Output of gradle -q hello
> 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 Gradle's 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 Gradle's
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
.
Example 49.11. Evaluation and execution of projects
bluewhale/build.gradle
ext.arctic = true hello << { println "- I'm the largest animal that has ever lived on this planet." } task distanceToIceberg << { println '20 nautical miles' }
krill/build.gradle
ext.arctic = true hello << { println "- The weight of my species in summer is twice as heavy as all human beings." } task distanceToIceberg << { println '5 nautical miles' }
Output of gradle -q distanceToIceberg
> gradle -q distanceToIceberg 20 nautical miles 5 nautical miles
Here the output without the -q
option:
Example 49.12. Evaluation and execution of projects
Output of gradle distanceToIceberg
> gradle distanceToIceberg :bluewhale:distanceToIceberg 20 nautical miles :krill:distanceToIceberg 5 nautical miles BUILD SUCCESSFUL Total time: 1 secs
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!
As we have seen, you can run a multi-project build by entering any subproject dir and execute the build from there. All matching task names of the project hierarchy starting with the current dir are executed. But Gradle also offers to execute tasks by their absolute path (see also Section 49.5, “Project and task paths”):
Example 49.13. Running tasks by their absolute path
Output of gradle -q :hello :krill:hello hello
> gradle -q :hello :krill:hello hello I'm water 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
The build is executed from the tropicalFish
project. We execute the hello
tasks of the water
, the krill
and the tropicalFish
project. The first two tasks are specified by there absolute path, the last task is executed on the name
matching mechanism described above.
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/bluewhale
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 Section 14.1, “Directory creation”) this was no longer possible, as the name of the directory task
contains the
'/'
character.
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:
Example 49.14. Dependencies and execution order
Build layout
messages/ settings.gradle consumer/ build.gradle producer/ build.gradle
Note: The code for this example can be found at samples/userguide/multiproject/dependencies/firstMessages/messages
which is in both the binary and source distributions of Gradle.
settings.gradle
include 'consumer', 'producer'
consumer/build.gradle
task action << { println("Consuming message: " + (rootProject.hasProperty('producerMessage') ? rootProject.producerMessage : 'null')) }
producer/build.gradle
task action << { println "Producing message:" rootProject.producerMessage = 'Watch the order of execution.' }
Output of gradle -q action
> 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
.
Example 49.15. Dependencies and execution order
Build layout
messages/ settings.gradle aProducer/ build.gradle consumer/ build.gradle
settings.gradle
include 'consumer', 'aProducer'
aProducer/build.gradle
task action << { println "Producing message:" rootProject.producerMessage = 'Watch the order of execution.' }
consumer/build.gradle
task action << { println("Consuming message: " + (rootProject.hasProperty('producerMessage') ? rootProject.producerMessage : 'null')) }
Output of gradle -q action
> 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.
Example 49.16. Dependencies and execution order
Output of gradle -q action
> 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.
Example 49.17. Declaring dependencies
Build layout
messages/ settings.gradle consumer/ build.gradle producer/ build.gradle
Note: The code for this example can be found at samples/userguide/multiproject/dependencies/messagesWithDependencies/messages
which is in both the binary and source distributions of Gradle.
settings.gradle
include 'consumer', 'producer'
consumer/build.gradle
task action(dependsOn: ":producer:action") << { println("Consuming message: " + (rootProject.hasProperty('producerMessage') ? rootProject.producerMessage : 'null')) }
producer/build.gradle
task action << { println "Producing message:" rootProject.producerMessage = 'Watch the order of execution.' }
Output of gradle -q action
> gradle -q action Producing message: Consuming message: Watch the order of execution.
Running this from the consumer
directory gives:
Example 49.18. Declaring dependencies
Output of gradle -q action
> gradle -q action Producing message: Consuming message: Watch the order of execution.
We have now declared that the
action
task in the consumer
project has an
execution dependency
on the action
task on the
producer
project.
Of course, task dependencies across different projects are not limited to tasks with the same name. Let's change the naming of our tasks and execute the build.
Example 49.19. Cross project task dependencies
consumer/build.gradle
task consume(dependsOn: ':producer:produce') << { println("Consuming message: " + (rootProject.hasProperty('producerMessage') ? rootProject.producerMessage : 'null')) }
producer/build.gradle
task produce << { println "Producing message:" rootProject.producerMessage = 'Watch the order of execution.' }
Output of gradle -q consume
> gradle -q consume Producing message: Consuming message: Watch the order of execution.
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.
Example 49.20. Configuration time dependencies
consumer/build.gradle
message = rootProject.hasProperty('producerMessage') ? rootProject.producerMessage : 'null' task consume << { println("Consuming message: " + message) }
producer/build.gradle
rootProject.producerMessage = 'Watch the order of evaluation.'
Output of gradle -q consume
> gradle -q consume Consuming message: 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.
Example 49.21. Configuration time dependencies - evaluationDependsOn
consumer/build.gradle
evaluationDependsOn(':producer') message = rootProject.hasProperty('producerMessage') ? rootProject.producerMessage : 'null' task consume << { println("Consuming message: " + message) }
Output of gradle -q consume
> gradle -q consume Consuming message: Watch the order of evaluation.
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.
Example 49.22. Configuration time dependencies
consumer/build.gradle
task consume << { println("Consuming message: " + (rootProject.hasProperty('producerMessage') ? rootProject.producerMessage : 'null')) }
Output of gradle -q consume
> gradle -q consume Consuming message: Watch the order of evaluation.
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.
To change the the default configuration order to be bottom up, That means that a project configuration
depends on the configuration of its child projects, the evaluationDependsOnChildren()
method can be used.
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.
Gradle's 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. [19] For the example we use only one build script and do cross project configuration.
Example 49.23. Dependencies - real life example - crossproject configuration
Build layout
webDist/ settings.gradle build.gradle date/ src/main/java/ org/gradle/sample/ DateServlet.java hello/ src/main/java/ org/gradle/sample/ HelloServlet.java
Note: The code for this example can be found at samples/userguide/multiproject/dependencies/webDist
which is in both the binary and source distributions of Gradle.
settings.gradle
include 'date', 'hello'
build.gradle
allprojects { apply plugin: 'java' group = 'org.gradle.sample' version = '1.0' } subprojects { apply plugin: 'war' repositories { mavenCentral() } dependencies { compile "javax.servlet:servlet-api:2.5" } } task explodedDist(dependsOn: assemble) << { File explodedDist = mkdir("$buildDir/explodedDist") subprojects.each {project -> project.tasks.withType(Jar).each {archiveTask -> copy { from archiveTask.archivePath into explodedDist } } } }
We have an interesting set of dependencies. Obviously the
date
and
hello
projects have a
configuration
dependency on webDist
, as all the build logic for the webapp projects is injected by
webDist
. The
execution
dependency is in the other direction, as
webDist
depends on the build artifacts of
date
and hello
. There is even a third dependency.
webDist
has a
configuration
dependency on
date
and
hello
because it needs to know the archivePath
. But it asks for this information at
execution time. Therefore we have no circular dependency.
Such and other dependency patterns are daily bread in the problem space of multi-project builds. If a build system does not support such patterns, you either can't solve your problem or you need to do ugly hacks which are hard to maintain and massively afflict your productivity as a build master.