When applications and projects get bigger, we usually split up several parts of the application into separate projects. Gradle has great support for multi-project builds. We can configure multiple projects in an easy way. Gradle is also able to resolve dependencies between projects and will build the necessary projects in the right order. So, we don't have to switch to a specific directory to build the code; Gradle will resolve the correct project order for us.
In this chapter we will learn about multi-project configuration and dependencies. First, we will look at how we can configure projects and tasks. Then we will use a multi-project Java application to learn how we can have inter-project dependencies and how Gradle resolves these for us.
Let's start with a simple multi-project structure. We have a root project called garden
with two other projects, tree
and flower
. The project structure is as follows:
garden/ tree/ flower/
We will add a new task printInfo
to each of these projects. The task will print out the name of the project to System.out
. We must add a file build.gradle
to each project, with the following contents:
task printInfo << { println "This is ${project.name}" }
To execute the task for each project, we must first enter the correct directory and then invoke the task with Gradle. Or, we run build.gradle
for a specific project with the -b
argument of Gradle. We get the following output, if we run the task for each project:
garden $ gradle -q printInfo This is garden garden $ cd tree tree $ gradle -q printInfo This is tree tree $ cd .. garden $ gradle -b flower/build.gradle printInfo This is flower
We have multiple projects, but we haven't used Gradle's support for multi-project builds yet. Let's reconfigure our projects and use Gradle multi-project support. We need to add a new file, settings.gradle
, in the garden
directory. In this file, we define the projects that are part of our multi-project build. We use the
include()
method to set the projects that are part of our multi-project build. The project with the file settings.gradle
is automatically part of the build. We will use the following line in the settings.gradle
file to define our multi-project build:
include 'tree', 'flower'
Now, we can execute the printInfo
task for each project with a single command. We get the following output if we execute the task:
garden $ gradle printInfo :printInfo This is garden :flower:printInfo This is flower :tree:printInfo This is tree BUILD SUCCESSFUL Total time: 1.778 secs
We see the output of each invocation of the task printInfo
. The path of the project task is also displayed. The root project is denoted by a colon (:
) and has no explicit name. The flower
project is referenced as :flower
, and the task printInfo
of the flower
project is referenced as :flower:printInfo
. The path of a task is the name of the project and a colon (:
) followed by the task name. The colon separates the project and task name. We can reference a specific task in a project using this syntax as well, from the command line. If we want to invoke the printInfo
task of the flower
project, we can run the following command:
graden $ gradle :flower:printInfo :flower:printInfo This is flower BUILD SUCCESSFUL Total time: 2.335 secs
This also works for executing tasks in a root project from another project directory. If we first go to the flower
project directory and want to execute the printInfo
task of the root project, we must use the syntax :printInfo
. We get the following output, if we execute the printInfo
task of the root project, the current project, and the flower
project, from the tree
project directory:
garden $ cd tree tree $ gradle :printInfo printInfo :flower:printInfo :printInfo This is garden :tree:printInfo This is tree :flower:printInfo This is flower BUILD SUCCESSFUL Total time: 1.707 secs
Gradle takes a couple of steps to determine whether a project must be executed as a single or multi-project build:
settings.gradle
in a directory with the name master
, at the same level as the current directory.settings.gradle
is not found, the parent directories of the current directory are searched for a settings.gradle
file. settings.gradle
is found, the project is executed as a single project build.settings.gradle
file is found, and the current project is part of the multi-project definition, the project is executed as part of the multi-project build. Otherwise, the project is executed as a single project build.We can force Gradle to not look for a settings.gradle
file in parent directories, with the command-line argument --no-search-upward
(or -u
).
In our current project setup, we have defined a hierarchical layout of the projects. We placed the settings.gradle
file in the parent directory, and with the include()
method, we added the tree
and flower
projects to our multi-project build.
We can also use a flat layout to set up our multi-project build. We must first create a master
directory in the garden
directory. We move our build.gradle
and settings.gradle
file from the garden
directory to the master
directory. Because we don't have a hierarchical layout any more, we must replace the include()
method with the includeFlat()
method. Our settings.gradle
file now looks like this:
includeFlat 'tree', 'flower'
The projects are referenced via the parent directory of the master
directory. So, if we define tree
as an argument for the include()
method, the actual path that is used to resolve the project directory is master/../tree
.
To invoke the printInfo
task for each project, we run Gradle from the master
directory with the following command:
master $ gradle printInfo :printInfo This is master :flower:printInfo This is flower :tree:printInfo This is tree BUILD SUCCESSFUL Total time: 2.373 secs
We have added a build.gradle
file to the tree
and flower
projects, with an implementation of the printInfo
task. But, with the multi-project support of Gradle, we don't have to do that. We can define all project tasks and properties in the root build.gradle
file. We can use this to define common functionality for all projects, in a single place.
We can reference a project with the project()
method and use the complete name of the project as an argument. We must use a closure to define the tasks and properties of the project.
For our example project, we first remove the build.gradle
files from the tree
and flower
directories. Next, we change the build.gradle
file in the master
directory. Here, we define the printInfo
tasks with the project()
method for the tree
and flower
projects:
task printInfo << { println "This is ${project.name}" } project(':flower') { task printInfo << { println "This is ${project.name}" } } project(':tree') { task printInfo << { println "This is ${project.name}" } }
If we execute the printInfo
task from the master
directory, we see that all printInfo
tasks of the projects are invoked:
master $ gradle printInfo :printInfo This is master :flower:printInfo This is flower :tree:printInfo This is tree BUILD SUCCESSFUL Total time: 2.434 secs
Gradle also has the allprojects{}
script block to apply project tasks and properties to all projects that are part of the multi-project build. We can rewrite our build.gradle
file and use the allprojects{}
script block to get a clean definition of the task without repeating ourselves:
allprojects { task printInfo << { println "This is ${project.name}" } }
If we invoke the printInfo
task from the master
directory, we see that each project has the newly added task:
master $ gradle -q printInfo This is master This is flower This is tree
If we only want to configure the subprojects tree
and flower
, we must use the subprojects{}
script block. With this script block, only tasks and properties of the subprojects of a multi-project build are configured. In the following example build file, we only configure the subprojects:
subprojects { task printInfo << { println "This is ${project.name}" } }
If we invoke the printInfo
task, we see that our master
project no longer has the printInfo
task:
master mrhaki$ gradle -q printInfo This is flower This is tree
Gradle will not throw an exception if the printInfo
task is not defined for a single project. Gradle will first build a complete task graph for all the projects that are part of the multi-project build. If any of the projects contains the task we want to run, the task for that project is executed. Only when none of the projects has the task, will Gradle fail the build.
We can combine the allprojects{}
and subprojects{}
script blocks, and the project()
method, to define common behavior and apply specific behavior for specific projects. In the following sample build file, we add extra functionality to the printInfo
task, at different levels:
allprojects { task printInfo << { println "This is ${project.name}" } } subprojects { printInfo << { println "Can be planted" } } project(':tree').printInfo << { println "Has leaves" } project(':flower') { printInfo.doLast { println 'Smells nice' } }
Now when we execute the printInfo
task, we get the following output:
$ gradle printInfo :printInfo This is master :flower:printInfo This is flower Can be planted Smells nice :tree:printInfo This is tree Can be planted Has leaves BUILD SUCCESSFUL Total time: 1.692 secs
We have added specific behavior to the tree
and flower
projects, with the project()
method. But, we could also have added a build.gradle
file to the tree
and flower
projects and added the extra functionality there.
To apply specific configuration to more than one project, we can also use project filtering. In our build.gradle
file, we must use the configure()
method. We define a filter based on the project names as argument of the method. In a closure, we define the configuration for each found project.
In the following sample build file, we use a project filter to find the projects that have names that start with an f
and then apply a configuration to the project:
allprojects { task printInfo << { println "This is ${project.name}" } } configure(allprojects.findAll { it.name.startsWith('f') }) { printInfo << { println 'Smells nice' } }
We have used the project name as a filter. We can also use project properties to define a filter. Because project properties are only set after the build is defined, either with a build.gradle
file or with the project()
method, we must use the
afterEvaluate()
method. This method is invoked once all projects are configured and project properties are set. We pass our custom configuration as a closure to the afterEvaluate()
method.
In the following example build file, we read the project property hasLeaves
for the projects tree
and flower
. If the property is true
, we customize the printInfo
task for that project:
allprojects { task printInfo << { println "This is ${project.name}" } } subprojects { afterEvaluate { project -> if (project.hasLeaves) { project.printInfo << { println 'Has leaves' } } } } project(':flower') { ext.hasLeaves = false } project(':tree') { ext.hasLeaves = true }
When we execute the printInfo
task from the master directory, we get the following output:
If we invoke the printInfo
task, we see that the printInfo
task of the flower
project is executed before the tree
project. Gradle uses the alphabetical order of the projects, by default, to determine the execution order of the tasks. We can change this execution order by defining explicit dependencies between tasks in different projects.
If we first want to execute the printInfo
task of the tree
project before the flower
project, we can define that the printInfo
task of the flower
project depends on the printInfo
task of the tree
project. In the following example build file, we will change the dependency of the printInfo
task in the flower
project. We will use the dependsOn()
method to reference the printInfo
task of the tree
project:
allprojects { task printInfo << { println "This is ${project.name}" } } project(':flower') { printInfo.dependsOn ':tree:printInfo' }
If we execute the printInfo
task, we see in the output that the printInfo
task of the tree
project is executed before the printInfo
task of the flower
project:
Besides task dependencies between projects, we can also include other configuration dependencies. For example, we could have a project property, set by one project, that is used by another project. Gradle will evaluate the projects in alphabetical order. In the next example, we create a new build.gradle
file in the tree
directory and set a property on the root project:
rootProject.ext.treeMessage = 'I am a tree'
We also create a build.gradle
file in the flower
project and set a project property with a value based on the root project property set by the tree
project:
ext.message = rootProject.hasProperty('treeMessage') ? rootProject.treeMessage : 'is not set' printInfo.doLast { println "Tree say ${message}" }
When we execute the printInfo
task, we get the following output:
master $ gradle printInfo :printInfo This is master :flower:printInfo This is flower Tree say is not set :tree:printInfo This is tree BUILD SUCCESSFUL Total time: 2.254 secs
Note that the printInfo
task in the flower
project cannot display the value of the root project property, because the value is not yet set by the tree
project. To change the evaluation order of the project, we can explicitly define that the flower
project depends on the tree
project, with the
evaluationDependsOn()
method. We change the build.gradle
file in the flower
directory and add evaluationDependsOn(':tree')
to the top of the file:
evaluationDependsOn ':tree' ext.message = rootProject.hasProperty('treeMessage') ? rootProject.treeMessage : 'is not set' printInfo.doLast { println "Tree say ${message}" }
When we execute the printInfo
task again, we see in the output that the value of the root project property is available in the flower
project:
master $ gradle printInfo :printInfo This is master :flower:printInfo This is flower Tree say I am a tree :tree:printInfo This is tree BUILD SUCCESSFUL Total time: 2.303 secs
18.117.75.70