Large projects are typically broken up into separate modules with independent build lifecycles. Each subproject has some kind of life of its own, perhaps with a development team assigned to it alone, and certainly with a test suite of its own which is worthwhile to run apart from the project as a whole. However, any large system that is broken into pieces must also be reintegrated together, implying the need for a master build that combines all subprojects into a single integration test suite and set of release artifacts or processes. A next-generation build system will be equipped to provide a flexible framework for dealing with real-world project differentiation and integration.
A multiproject is generally hierarchical in nature: it typically has a master project with one or more subprojects. In some cases, subprojects may be nested. The master project goes in a top-level directory, with subprojects arranged in subdirectories underneath. The master project may add code, resources, tests, and build conventions of its own, or it may simply be build glue that holds the project tree together.
In the most intuitive case, Gradle allows you to define a build file for the top-level project and one for each subproject. In a more interesting and potentially more useful case, it also lets you define the entire build from the top-level build file. Alternatively, if neither a scattered set of individual build files nor a single, integrated build file suits you, you can also put some build configuration in the top-level build file and put project-specific details in the project-specific build files in their respective subdirectories. As usual, Gradle wants to you give you the ability to define your own build conventions, rather than impose its opinions on you.
To tell Gradle which subdirectories of the root project actually
contain projects (and not merely sources or build outputs), you must
provide the settings.gradle
file.
This is a build configuration file which is independent of build.gradle
. In the simplest case, settings.gradle
simply lists the names of the
subdirectories which contain subprojects, and nothing more.
When Gradle runs the Initialization lifecycle phase, it first
looks for settings.gradle
, and from
it finds the list of subdirectories which contain subprojects. If those
subdirectories contain build.gradle
files of their own, they are
processed next and incorporated into the directed acyclic graph (DAG)
that describes the build. The fact that Gradle is building an internal
project DAG reveals a compelling option: given the right Gradle syntax,
we can actually do all of the multiproject build configuration from a
single build.gradle file at the root level. We can also distribute all
of the configuration to the individual build files, or use a hybrid
approach in which common configuration settings are in the master file,
and project-specific settings reside in the individual build
files.
Having a single, project-specific build file per subproject is
easiest to digest for most new users. There is a settings.gradle
file at the project root, and
a build.gradle
file at the root and
in each subproject directory. Each subproject manages its own build
affairs, and the top-level project combines the subproject build outputs
into the integrated build outcome which is the ultimate goal of the
build.
In our example, the root project is a command-line application that takes the name of a poet as a command line argument, then emits a few lines of that poet’s poetry to the console in an encoded form. One of its dependencies is a project containing a simple API for generating the poems, including a Java interface, a factory class, and several implementations containing different poem fragments. The other dependency is an API that encodes arbitrary strings first with the Metaphone algorithm, then the Base64 format. The top-level application has the responsibility of calling both APIs to emit encoded poetry to the console. This example illustrates two dependent subprojects, one of which is a completely standalone API, and one of has external JAR dependencies which must be fetched from an online repository and made available to the root-level project.
The settings.gradle
file looks
as shown in Example 6-1.
This is the most basic use of settings.gradle
possible. It simply names the
subprojects by directory name, relative to the directory of the
top-level project. The settings.gradle
file is interpreted during the
Initialization lifecycle phase, when the skeleton of the build graph is
being constructed. Consequently, a richer API than the include method is
available. In general, any part of the Gradle API that affects the
structure of the build graph can be called in this file. We haven’t
looked in detail at what these API calls are yet, but later on in this
chapter, we’ll look at some of them when we extract parts of the
subproject build files into the top-level build file.
The top-level build file looks as shown in Example 6-2.
evaluationDependsOn(':codec') apply plugin: 'java' dependencies { compile project(':codec') compile project(':content') } [ 'shakespeare', 'williams', 'shelley', 'chesterton' ].each { poet -> task "${poet}"(type: JavaExec) { group = 'Encoded Poetry' args = [ poet ] main = 'org.gradle.example.codedpoet.CommandLine' classpath sourceSets.main.runtimeClasspath, project(':codec').sourceSets.main.runtimeClasspath } }
There are a few things in this build file that we haven’t seen
yet. First of all, note that the dependencies aren’t vectors describing
a JAR file in a repository, but they are projects. The project()
method queries the Gradle DAG and
returns the project object belonging to a subproject. The colon at the
beginning of the project name indicates the root of the project tree, in
a similar way that a forward slash indicates the root directory in a
Unix filesystem, or a backslash indicates the root directory on Windows.
The project name following the colon is the project name as given in
settings.gradle
. This top-level
project’s dependencies block indicates that it depends on the codec
and content
subprojects.
This build file also uses dynamic task creation. The dynamic tasks
created in this build file are JavaExec
tasks, each of which runs the
org.gradle.example.codedpoet.CommandLine
class, passing in the name of the poet as a command-line argument. Most
importantly, each task’s classpath property is given two arguments: the
runtimeClasspath
of the top-level
project, and the runtimeClasspath
of
the :codec
project.
The codec project’s classpath must be added to the JavaExec
task explicitly because the codec
introduces a dependency of its own: namely, the Apache commons-codec
library. It is very typical for subprojects to have their own
dependencies, either stored locally in the project or retrieved from a
Maven- or Ivy-style repository, so including them in the top-level
project’s build is commonplace.
Finally, the first line of build.gradle
introduces a method call we have
not seen before. The evaluationDependsOn()
method, which takes a
subproject name as its argument, indicates that the evaluation of the
current project (the top-level or root project) depends on the named project. This method
call is necessary to enable the dependencies of the codec project to be
available in the top-level project. Calling this method will always
force the named project to be evaluated first, so its contributions to
the project graph will exist before the current project is
evaluated.
The build file of the content project is trivial (Example 6-3).
It is a pure Java project with no external dependencies, following all of the conventions of the Java plug-in. The code itself is only slightly more complex, having an interface, a factory class, and several concrete implementations of the interface. These are all compiled and bundled into a JAR to be used by the top-level project.
The build file of the codec project contains only slightly more configuration (Example 6-4).
In addition to applying the Java plug-in, it also names Maven
Central as a repository, and declares the Apache commons-codec library
version 1.5 as a dependency. The code itself declares a single class
called Encoder
, which exposes a
method that calls both the Metaphone and Base64 codecs on a string
argument.
Splitting a multiproject build into a parent project and multiple subprojects, and giving each subproject its own build file, is an easy-to-understand structure with much to recommend it as a standard approach to complex, composite Gradle projects. However, the fact that Gradle converts the build files internally into a unified project DAG exposes a couple of other options for us when deciding how to organize multiproject builds. Let’s look at how we might refactor this build to put all of the build configuration into one file.
The previous multiproject build can be expressed in a single build
file in the root project. The settings.gradle
remains the same, naming the
subprojects by their directories (Example 6-5).
However, the build.gradle
files
in the codec
and content
subproject directories go away
entirely. They are replaced with configuration in the master build file
(Example 6-6).
evaluationDependsOn(':codec') allprojects { apply plugin: 'java' } project(':codec') { repositories { mavenCentral() } dependencies { compile 'commons-codec:commons-codec:1.5' } } dependencies { compile project(':codec') compile project(':content') } [ 'shakespeare', 'williams', 'shelley', 'chesterton' ].each { poet -> task "${poet}"(type: JavaExec) { group = 'Encoded Poetry' args = [ poet ] main = 'org.gradle.example.codedpoet.CommandLine' classpath sourceSets.main.runtimeClasspath, project(':codec').sourceSets.main.runtimeClasspath } }
The call to evaluationDependsOn()
tells Gradle to
evaluate the :codec
build file
before the root project’s build file. This ensure that codec
build objects will exist in the
graph before the rest of this build file is evaluated.
The allprojects
method
passes all of its configuration to all projects in the build,
including the root and all subprojects.
The project()
method gives
us direct access to the configuration of the codec
subproject. Using this syntax, we
can configure any project in the graph.
In the unified build, all three projects need the Java plug-in, so
we apply that plug-in inside the allprojects
closure. If we had configuration
to apply only to subprojects, we could use the subprojects
method instead.
The codec
project has some
individual configuration needs that don’t apply to the other two
projects in the build, and we can apply this configuration by using the
project()
method. Note that the
parameter passed to project()
is
:codec
. Using this syntax, we can
access the project graph of any configured subproject. The power and
flexibility of this syntax is difficult to overstate. The object
returned by project()
is a Project
object, which is implicitly the object being operated on by all of the
methods normally called in a Gradle build file. Just as repositories and
dependencies are configured in this block, tasks could be created or
modified, new Java SourceSets defined, plug-ins applied, or any other
Gradle configuration operation performed. The ability to access the
Project object from the top-level build file gives us complete control
over the structure of the entire build. This feature is much more
consequential than its ordinary-seeming syntax would suggest.
The rest of the build file is the same as what we saw in the individual build file example: the root project is made to depend on the two subprojects, and a set of tasks is dynamically created to call the CommandLine class with an appropriate argument. This gives us the same functionality as the individual build file example.
So far, we’ve seen two ways of expressing a multiproject build: splitting the build up into several project-specific build files, and combining all build configuration into one master build file. As an alternative to these, you may find that the most expressive way to describe your build is to choose a hybrid approach in which some configuration is placed in the root build file and some is included in project-specific build files. Let’s rework the build files from the previous two examples to reflect such a hybrid configuration.
The new root-level build file still contains an allprojects
configuration, applying the Java
plug-in to all projects. Otherwise, it looks just like the root project
build file from the individual build file example. The resulting file is
shown in Example 6-7.
allprojects
{
apply
plugin:
'
java
'
}
evaluationDependsOn
(
'
:
codec
'
)
dependencies
{
compile
project
(
'
:
codec
'
)
compile
project
(
'
:
content
'
)
}
[
'
shakespeare
'
,
'
williams
'
,
'
shelley
'
,
'
chesterton
'
].
each
{
poet
->
task
"${poet}"
(
type:
JavaExec
)
{
group
=
'
Encoded
Poetry
'
args
=
[
poet
]
main
=
'
org
.
gradle
.
example
.
codedpoet
.
CommandLine
'
classpath
sourceSets
.
main
.
runtimeClasspath
,
project
(
'
:
codec
'
).
sourceSets
.
main
.
runtimeClasspath
}
}
Since the dependency and repository configuration of the codec
project were specific to that project,
we push that configuration back down into its build file as shown in
Example 6-8:
Note that we do not apply the Java plug-in in the code project’s
build file, since the Java plug-in is a common configuration step that
is done in the root-level build.gradle
file. Because of this, there is
no need to provide a build file for the content
subproject, since that very simple
build relies entirely on the defaults provided by the Java plug-in,
which is applied by the root project.
The three approaches we’ve explored here each have unique advantages. Creating a single build file for each subproject project provides for a very clear separation of concerns, and is a very easy-to-understand approach for new users of Gradle. Putting all configuration into a single build file puts the entire description of the build in one easy-to-examine file. A hybrid approach lets us put common configuration in one place, then to factor project-specific configuration into build files associated with their individual projects. Each one of these approaches has something to recommend it.
Each is desirable for a reason of its own, but Gradle imposes no opinion on which approach is correct. As a user of Gradle, you are free to structure your multiproject builds in whatever way best fits your circumstances and build sensibilities.
Gradle considers a multi-project build as a single graph of projects, tasks, and other configuration data structures. As a result, running the build from inside the root project or any subproject gives you access to the entire graph of tasks. If you’re in the directory of a subproject, you don’t have to change directories into another subproject to get access to that project’s tasks.
The task addressing scheme is similar to directory paths in the file system, except it uses colons instead of slashes as delimiters. A colon at the beginning of a fully qualified task name indicates the root project. If the colon is followed by the name of the task in the root project, then that fully qualified task name refers to that task in the root project—whether Gradle is being executed from within the root directory or from within a subproject directory. If the text after the colon is a subproject name, then it should be followed by another colon and the name of a task within that subproject. Again, this task can be invoked no matter what project directory in the tree you’re in when you run Gradle.
If you want to run the whole project build while in the directory of a subproject, you might use the command line shown in Example 6-9.
[~/mutiproject] $ cd codec [~/mutiproject/codec] $ gradle :build :codec:compileJava :codec:processResources :codec:classes :codec:jar :content:compileJava :content:processResources :content:classes :content:jar :compileJava :processResources :classes :jar :assemble :compileTestJava :processTestResources :testClasses :test :check :build BUILD SUCCESSFUL Total time: 12.333 secs
If you want to build another subproject while in one subproject’s directory, you’d use the longer task syntax (Example 6-10).
[~/mutiproject] $ cd content [~/mutiproject/content] $ gradle :codec:compileJava :codec:compileJava BUILD SUCCESSFUL Total time: 1.274 secs
This same syntax applies when you’re in the root project and want to run a subproject task directly, except that you may optionally omit the colon, since you’re already at the root level (Example 6-11).
Gradle’s internal architecture lends itself to a very fluid way of
dealing with multi-project builds. It is a central fact of Gradle’s
architecture that it converts all build.
gradle
files into a unified DAG that
describes the dependencies and associated actions of a build. The
existence of this DAG gives you tremendous flexibility in how you want
to represent the configuration of your multiproject build in your actual
project files. The individual, unified, and hybrid approaches—all of
which are automatically integrated by the time the build executes—offer
options that should appeal to all build developers regardless of project
structure and developer preferences. As usual, Gradle wants to provide
you with the tools to create your own standards, rather than impose its
standards on you.