Multi-project builds in Gradle provide a better way to organise your project in the event that your have multiple artifacts to be created or deployed. In simple use cases everything is easy to understand and works without issue. But, as soon as you start to use some more advanced Gradle features we need to look under the cover to better understand how the multiple projects work together within Gradle’s lifecycle.
In this article you’ll learn about the evaluation order of a multi-project Gradle build, and how to set things up to work the way you want them to.
1. Multi-project builds: a recap
Gradle multi-project builds, or multi-module builds as they are known in Maven, are pretty easy to setup and use. Here’s a quick recap, where we’ll build up and example project called gradle-evaluation-order.
1.1. Adding sub-projects to a Gradle project
Let’s create a new Gradle project gradle-evaluation-order. The easiest way to do this is to run gradle init
, which will add a build.gradle file in the root directory and setup the Gradle wrapper.
Edit the settings.gradle file in the project root directory. Add the include
statement to include whatever sub-projects you want:
rootProject.name = 'gradle-evaluation-order'
include 'sub-project-1', 'sub-project-2'
This is in fact all that is required. Note that:
-
there’s no need for a separate directory
-
there’s no need for a separate build.gradle build file
You can now run ./gradlew projects
to show that the new sub-projects have been added:
1.2. Configuring the sub-projects to do something useful
Now that we have some sub-projects, there are 2 approaches to getting them to do something useful:
-
Configure the sub-project’s build in the parent-project’s build.gradle. There’s no need to add a directory or separate build.gradle for the sub-project.
-
Create a separate directory and build.gradle for the sub-project, and configure the sub-project’s build within there.
-
Combine approach 1 and 2. This is really useful if you have common functionality between your sub-projects, but also have some specific behaviour.
Configuring a sub-project from the parent build.gradle
To configure sub-projects from the parent build.gradle (option 1 above), we could add this code:
allprojects {
task('hello').doLast {
println "I'm $project.name"
}
}
When we execute ./gradlew hello
we get the following output, printing the name of each project:
Configuring a sub-project from its own build.gradle
Extending our example, we’ll add a directory sub-project-1, and add a build.gradle within there, containing this code.
task('goodbye').doLast {
println "Goodbye from $project.name"
}
Now when we execute ./gradlew hello goodbye
we get the following output:
Now you can see that all the projects have the hello task defined from the parent build.gradle, but sub-project-1 alone has the goodbye task defined in its own build.gradle.
2. Gradle Lifecycle Evaluation Order
Time to lift off the covers and see what’s really happening with our multi-project builds. First though, as a reminder, remember that the Gradle build lifecycle includes three distinct phases:
Gradle lifecycle phases
-
Initialization: Gradle determines which projects are going to take part in the build. This is determined by any
include
statements in your settings.gradle. -
Configuration: Gradle executes the code in the build files, creating everything that will be required to run the tasks
-
Execution: Gradle determines which tasks should be executed and in which order, based on which tasks were passed in on the command line
To show these phases in action, we’ll add a println
statement to the settings.gradle:
println 'This is executed during the initialization phase.'
And add a println
statement to sub-project-1/build.gradle:
task('goodbye').doLast {
println "Goodbye from $project.name"
}
println "This is executed during the $project.name configuration phase."
Lastly let’s add a println
statement in the parent build.gradle, in the allprojects block:
group 'com.tom'
version '1.0-SNAPSHOT'
allprojects {
task('hello').doLast {
println "I'm $project.name"
}
println "This is executed during the $project.name configuration phase."
}
Now if we run ./gradlew hello
we get the following output:
You can clearly see the 3 distinct phases, including tasks being executed in the execution phase.
There are 2 keys things to note here though:
-
The parent project configuration phase happens before that of a sub-project. Gradle calls this a breadth-wise ordering
-
The allprojects block in the parent build.gradle means that we can configure a sub-project during the parent’s configuration phase i.e. before the sub-project’s own build.gradle has been evaluated
3. Using afterEvaluate
Now that we understand how multi-project builds get executed, what can we do about it? Consider a scenario where we have a plugin definition as follows, defined in buildSrc/src/main/groovy/com/tom/SmallTalkPlugin.groovy:
package com.tom
import org.gradle.api.Plugin
import org.gradle.api.Project
class SmallTalkPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
def extension = project.extensions.create('smallTalk', SmallTalkExtension)
project.task("makeSmallTalkTo$extension.recipient") {
doLast {
println 'How are you?'
}
}
}
}
class SmallTalkExtension {
public String recipient
}
This plugin prints out a How are you? greeting.
It needs to be applied in the parent build.gradle. The task name can be customised, based on a configuration which also goes in the parent build.gradle:
import com.tom.SmallTalkPlugin
apply plugin: SmallTalkPlugin
smallTalk {
recipient = 'Tom'
}
So take a guess, what should we get when we run ./gradlew makeSmallTalkToTom
?
Well, actually something like the following:
It turns out that since the plugin is being applied before the configuration properties have been applied to the SmallTalkExtension
class, recipient is null.
We can verify this from the output of ./gradlew tasks --all
:
Clearly the recipient configuration property has not been created by the point when the task is created, resulting in a task named makeSmallTalkTonull.
A solution
Thankfully the boffins over at Gradle HQ thought of this scenario and have provided us with this method signature available on the Project class:
void afterEvaluate(Closure var1);
Whatever closure is passed in will be executed after the project has been evaluated i.e. at the end of the configuration phase of that project.
Let’s change the plugin definition to include the afterEvaluate
method:
package com.tom
import org.gradle.api.Plugin
import org.gradle.api.Project
class SmallTalkPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
def extension = project.extensions.create('smallTalk', SmallTalkExtension)
project.afterEvaluate {
project.task("makeSmallTalkTo$extension.recipient") {
doLast {
println 'How are you?'
}
}
}
}
}
class SmallTalkExtension {
public String recipient
}
Now the task won’t be created until the end of the configuration phase, after the recipient property has been set. When we run ./gradlew makeSmallTalkToTom
we get the expected output:
4. Turning things upside down with depth-first ordering
We saw earlier that the configuration phase of the Gradle build evaluates the parent project before any sub-projects, in what Gradle calls a breadth-wise ordering.
This is good for most scenarios, but what if we wanted the sub-projects to be configured first, before the parent? Why would we want to do this in the first place?
An example
Let’s take a brand new example (code available in a separate GitHub repository). We have a parent project with 2 sub-projects, each with their own build.gradle:
Sub-project-1 has task doThing1 defined in its build.gradle:
task('doThing1').doLast {
println "Doing thing1 in $project.name"
}
And sub-project-2 has a definition for task doThing2:
task('doThing2').doLast {
println "Doing thing2 in $project.name"
}
Simples. When we run ./gradlew doThing1 doThing2
we see the following output:
No surprises here then. However, say we have a new requirement:
doThing2 must run before doThing1
We may want to configure the order in the parent build.gradle using the mustRunAfter
method, like this:
Project subProject1 = project('sub-project-1')
Project subProject2 = project('sub-project-2')
subProject1.tasks['doThing1'].mustRunAfter(subProject2.tasks['doThing2'])
Let’s run ./gradlew doThing1 doThing2
again:
What happened? Well, unfortunately we tried to configure the ordering of the tasks before the tasks themselves had been created. That’s what you get with breadth-wise evaluation.
A solution
As always, the clever Gradle folk have a solution to tame the elephant. This time, we have an option to force Gradle to evaluate the child build.gradle files before the parent:
void evaluationDependsOnChildren()
Declares that this project has an evaluation dependency on each of its child projects.
From Gradle API docs
Let’s try this out then, but adding this new method call to the top of the parent project’s build.gradle:
evaluationDependsOnChildren()
Project subProject1 = project('sub-project-1')
Project subProject2 = project('sub-project-2')
subProject1.tasks['doThing1'].mustRunAfter(subProject2.tasks['doThing2'])
Now when we run ./gradlew doThing1 doThing2
we get the desired output, with doThing2 executing before doThing1:
Awesome! Now look who’s pulling the strings.
5. Conclusion
There’s a lot to take in here, but let’s wrap it up into 3 takeaways to remember:
-
Always keep in mind the Gradle lifecycle build phases, initialization, configuration and execution: when something’s not happening like you expect, think about what phase of the lifecycle you’re in
-
Use
afterEvaluate
to delay execution until the end of the current project’s configuration phase: this can be handy in several scenarios, including waiting for plugin properties to be configured -
Gradle project evaluation during the configuration phase is in breadth-wise ordering (parent first): it’s worth bearing this in mind whenever you have code in the parent project that depends on tasks defined in the sub-projects. You can switch the evaluation order using
evaluationDependsOnChildren
.
Why not try to apply these concepts in your own project to control the execution of your build?
6. Resources
GITHUB REPOSITORY
Follow along with this article by checking out the accompanying GitHub repository
Here’s the GitHub repository for the evaluationDependsOnChildren()
example.
GRADLE
Multi-project builds docs
Build lifecycle docs
evaluationDependsOnChildren
docs
Missing Gradle knowledge makes you slow
A broken build is easy to fix with a quick Google search, but a misconfigured build costs hours of time each week.
Time to build. Time to test. Time to fix.
Knowing the "right" way to build a project isn't obvious, especially with pages of hard-to-follow documentation. But for your project to scale, you must master the build script and configure it effectively.
That's now a lot easier with Gradle Build Bible.