Chapter 6. Multiproject Builds

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.

Multiproject Build Structure

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.

Project-Specific 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.

Example 6-1. The settings.gradle file for a simple multiproject build.

include 'codec', 'content'

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.

Example 6-2. The root project build file, when subprojects have individual build files
evaluationDependsOn(':codec')

apply plugin: 'java'

dependencies {
  compile project(':codec')
  compile project(':content')
}

[ 'shakespeare', 'williams', 'shelley', 'chesterton' ].each { poet -> 1
  task "${poet}"(type: JavaExec) { 2
    group = 'Encoded Poetry'
    args = [ poet ] 3
    main = 'org.gradle.example.codedpoet.CommandLine'
    classpath sourceSets.main.runtimeClasspath,
                   project(':codec').sourceSets.main.runtimeClasspath
  }
}
1

This Groovy code declares a list of four strings (each of which is the name of a poet), then iterates over that list.

2

This line creates a dynamic task in the top-level project named after each poet.

3

The args parameter expects a List, so we use Groovy list literal syntax to wrap the poet variable.

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

Example 6-3. The build file of the content subproject.

apply plugin: 'java'

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

Example 6-4. The build file of the codec subproject.

apply plugin: 'java'

repositories {
  mavenCentral()
}

dependencies {
  compile 'commons-codec:commons-codec:1.5'
}

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.

One Master Build 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).

Example 6-5. The settings.gradle for the unified multiproject build case.

include 'codec', 'content'

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

Example 6-6. A unified build file for a mutli-project build
 evaluationDependsOn(':codec') 1

allprojects { 2
  apply plugin: 'java'
}

project(':codec') { 3
  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
  }
}
1

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.

2

The allprojects method passes all of its configuration to all projects in the build, including the root and all subprojects.

3

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.

A Hybrid Multiproject Build

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.

Example 6-7. The build file of the root-level project in the hybrid multiproject build case.

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:

Example 6-8. The build file for the codec subproject.

repositories {
  mavenCentral()
}

dependencies {
  compile 'commons-codec:commons-codec:1.5'
}

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.

Individual, Unified, or Hybrid?

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.

Multiproject Task Structure

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.

Example 6-9. Running a task in the root project from within a subproject directory
[~/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).

Example 6-10. Running one subproject’s tasks while in a second subproject
[~/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).

Example 6-11. Running a subproject task from the root project
[~/mutiproject] $ gradle codec:jar
:codec:compileJava
:codec:processResources
:codec:classes
:codec:jar

BUILD SUCCESSFUL

Total time: 0.996 secs

Multiple Projects Your Way

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.

..................Content has been hidden....................

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