Chapter 10. Composite builds

Table of Contents

10.1. What is a composite build?
10.2. Defining a composite build
10.3. Declaring the dependencies substituted by an included build
10.4. Depending on tasks in an included build
10.5. Current limitations and future plans for composite builds

Composite build is an incubating feature. While useful for many use cases, there are bugs to be discovered, rough edges to smooth, and enhancements we plan to make. Thanks for trying it out!

10.1. What is a composite build?

A composite build is simply a build that includes other builds. In many ways a composite build is similar to a Gradle multi-project build, except that instead of including single projects, complete builds are included.

Composite builds allow you to:
  • combine builds that are usually developed independently, for instance when trying out a bug fix in a library that your application uses

  • decompose a large multi-project build into smaller, more isolated chunks that can be worked in independently or together as needed

A build that is included in a composite build is referred to, naturally enough, as an "included build". Included builds do not share any configuration with the composite build, or the other included builds. Each included build is configured and executed in isolation.

Included builds interact with other builds via dependency substitution. If any build in the composite has a dependency that can be satisfied by the included build, then that dependency will be replaced by a project dependency on the included build.

By default, Gradle will attempt to determine the dependencies that can be substituted by an included build. However for more flexibility, it is possible to explicitly declare these substitutions if the default ones determined by Gradle are not correct for the composite. See Section 10.3, “Declaring the dependencies substituted by an included build”.

As well as consuming outputs via project dependencies, a composite build can directly declare task dependencies on included builds. Included builds are isolated, and are not able to declare task dependencies on the composite build or on other included builds. See Section 10.4, “Depending on tasks in an included build”.

10.2. Defining a composite build

The following examples demonstrate the various ways that 2 Gradle builds that are normally developed separately can be combined into a composite build. For these examples, the my-utils multi-project build produces 2 different java libraries (number-utils and string-utils), and the my-app build produces an executable using functions from those libraries.

The my-app build does not have direct dependencies on my-utils. Instead, it declares binary dependencies on the libraries produced by my-utils.

Example 10.1. Dependencies of my-app

my-app/build.gradle

apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'idea'

group "org.sample"
version "1.0"

mainClassName = "org.sample.myapp.Main"

dependencies {
    compile "org.sample:number-utils:1.0"
    compile "org.sample:string-utils:1.0"
}

repositories {
    jcenter()
}

Note: The code for this example can be found at samples/compositeBuilds/basic in the ‘-all’ distribution of Gradle.


10.2.1. Defining a composite build via --include-build

The --include-build command-line argument turns the executed build into a composite, substituting dependencies from the included build into the executed build.

Example 10.2. Declaring a command-line composite

Output of gradle --include-build ../my-utils run

> gradle --include-build ../my-utils run
[composite-build] Configuring build: /home/user/gradle/samples/compositeBuilds/basic/my-utils
:compileJava
:my-utils:number-utils:compileJava
:my-utils:number-utils:processResources UP-TO-DATE
:my-utils:number-utils:classes
:my-utils:number-utils:jar
:my-utils:string-utils:compileJava
:my-utils:string-utils:processResources UP-TO-DATE
:my-utils:string-utils:classes
:my-utils:string-utils:jar
:processResources UP-TO-DATE
:classes
:run
The answer is 42

BUILD SUCCESSFUL

10.2.2. Defining a composite build via settings.gradle

It's possible to make the above arrangement persistent, by using Settings.includeBuild(java.lang.Object) to declare the included build in the settings.gradle file.

Example 10.3. Declaring a composite via settings.gradle

settings-composite.gradle

rootProject.name = 'my-app'

includeBuild '../my-utils'

10.2.3. Defining a separate composite build

One downside of the above approach is that it requires you to modify an existing build, rendering it less useful as a standalone build. One way to avoid this is to define a separate composite build, whose onyl purpose is to combine otherwise separate builds.

Example 10.4. Declaring a separate composite

settings.gradle

rootProject.name='adhoc'

includeBuild '../my-app'
includeBuild '../my-utils'

In this scenario, the 'main' build that is executed is the composite, and it doesn't define any useful tasks to execute itself. In order to execute the 'run' task in the 'my-app' build, the composite build must define a delegating task.

Example 10.5. Depending on task from included build

build.gradle

task run {
    dependsOn gradle.includedBuild('my-app').task(':run')
}

More details tasks that depend on included build tasks below.

10.2.4. Restrictions on included builds

Most builds can be included into a composite, however there are some limitations.

Every included build:

  • must have a settings.gradle file.

  • must not itself be a composite build.

  • must not have a rootProject.name the same as another included build.

  • must not have a rootProject.name the same as a top-level project of the composite build.

  • must not have a rootProject.name the same as the composite build rootProject.name.

10.3. Declaring the dependencies substituted by an included build

By default, Gradle will configure each included build in order to determine the dependencies it can provide. The algorithm for doing this is very simple: Gradle will inspect the group and name for the projects in the included build, and substitute project dependencies for any external dependency matching ${project.group}:${project.name}.

There are cases when the default substitutions determined by Gradle are not sufficient, or they are not correct for a particular composite. For these cases it is possible to explicitly declare the substitutions for an included build. Take for example a single-project build 'unpublished', that produces a java utility library but does not declare a value for the group attribute:

Example 10.6. Build that does not declare group attribute

build.gradle

apply plugin: 'java'

When this build is included in a composite, it will attempt to substitute for the dependency module "undefined:unpublished" ("undefined" being the default value for project.group, and 'unpublished' being the root project name). Clearly this isn't going to be very useful in a composite build. To use the unpublished library unmodified in a composite build, the composing build can explicitly declare the substitutions that it provides:

Example 10.7. Declaring the substitutions for an included build

settings.gradle

rootProject.name = 'app'

includeBuild('../anonymous-library') {
    dependencySubstitution {
        substitute module('org.sample:number-utils') with project(':')
    }
}

With this configuration, the "my-app" composite build will substitute any dependency on org.sample:number-utils with a dependency on the root project of "unpublished".

10.3.1. Cases where included build substitutions must be declared

Many builds that use the uploadArchives task to publish artifacts will function automatically as an included build, without declared substitutions. Here are some common cases where declared substitutions are required:

  • When the archivesBaseName property is used to set the name of the published artifact.

  • When a configuration other than default is published: this usually means a task other than uploadArchives is used.

  • When the MavenPom.addFilter() is used to publish artifacts that don't match the project name.

  • When the maven-publish or ivy-publish plugins are used for publishing, and the publication coordinates don't match ${project.group}:${project.name}.

10.3.2. Cases where composite build substitutions won't work

Some builds won't function correctly when included in a composite, even when dependency substitutions are explicitly declared. This limitation is due to the fact that a project dependency that is substituted will always point to the default configuration of the target project. Any time that the artifacts and dependencies specified for the default configuration of a project don't match what is actually published to a repository, then the composite build may exhibit different behaviour.

Here are some cases where the publish module metadata may be different from the project default configuration:

  • When a configuration other than default is published.

  • When the maven-publish or ivy-publish plugins are used.

  • When the POM or ivy.xml file is tweaked as part of publication.

Builds using these features function incorrectly when included in a composite build. We plan to improve this in the future.

10.4. Depending on tasks in an included build

While included builds are isolated from one another and cannot declare direct dependencies, a composite build is able to declare task dependencies on it's included builds. The included builds are accessed using Gradle.getIncludedBuilds() or Gradle.includedBuild(java.lang.String), and a task reference is obtained via the IncludedBuild.task(java.lang.String) method.

Using these APIs, it is possible to declare a dependency on a task in a particular included build, or tasks with a certain path in all or some of the included builds.

Example 10.8. Depending on a single task from an included build

build.gradle

task run {
    dependsOn gradle.includedBuild('my-app').task(':run')
}

Example 10.9. Depending on a tasks with path in all included builds

build.gradle

task publishDeps {
    dependsOn gradle.includedBuilds*.task(':uploadArchives')
}

10.5. Current limitations and future plans for composite builds

We think composite builds are pretty useful already. However, there are some things that don't yet work the way we'd like, and other improvements that we think will make things work even better.

Limitations of the current implementation include:

  • No support for included builds that have publications that don't mirror the project default configuration. See Section 10.3.2, “Cases where composite build substitutions won't work”.

  • No native support for composite builds in IntelliJ IDEA or Eclipse Buildship. Generating IDEA metadata with gradle idea is supported.

  • Native builds are not supported. (Binary dependencies are not yet supported for native builds).

Improvements we have planned for upcoming releases include:

  • Better detection of dependency substitution, for build that publish with custom coordinates, builds that produce multiple components, etc. This will reduce the cases where dependency substitution needs to be explicitly declared for an included build.

  • The ability to target a task or tasks in an included build directly from the command line. We are currently exploring syntax options for allowing this functionality, which will remove many cases where a delegating task is required in the composite.

  • Execution of included builds in parallel.

  • Detection of changes to included builds when running with continuous build (-t).

  • Making the implicit buildSrc project an included build.