Chapter 7. Multi-project Builds

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.

Working with multi-project builds

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

Executing tasks by project path

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:

  1. First, Gradle looks for a file settings.gradle in a directory with the name master, at the same level as the current directory.
  2. If settings.gradle is not found, the parent directories of the current directory are searched for a settings.gradle file.
  3. If settings.gradle is found, the project is executed as a single project build.
  4. If a 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).

Using a flat layout

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

Defining projects

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.

Filtering projects

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:

master $ gradle printInfo
:printInfo
This is master
:flower:printInfo
This is flower
:tree:printInfo
This is tree
Has leaves

BUILD SUCCESSFUL

Total time: 2.386 secs

Defining task dependencies between projects

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:

master $ gradle printInfo
:printInfo
This is master
:tree:printInfo
This is tree
:flower:printInfo
This is flower

BUILD SUCCESSFUL

Total time: 2.188 secs

Defining configuration dependencies

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
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.97.216