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:

  1. 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.

  2. Create a separate directory and build.gradle for the sub-project, and configure the sub-project’s build within there.

  3. 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

  1. Initialization: Gradle determines which projects are going to take part in the build. This is determined by any include statements in your settings.gradle.

  2. Configuration: Gradle executes the code in the build files, creating everything that will be required to run the tasks

  3. 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:

  1. The parent project configuration phase happens before that of a sub-project. Gradle calls this a breadth-wise ordering

  2. 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:

  1. 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

  2. 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

  3. 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

Video

Check out the accompanying video from the Tom Gregory Tech YouTube channel.