Part 3: Multi-Project Builds
Learn the basics of structuring Gradle projects using subprojects and composite builds.
Step 1. About Multi-Project Builds
Typically, builds contain multiple projects, such as shared libraries or separate applications that will be deployed in your ecosystem.
In Gradle, a multi-project build consists of:
-
settings.gradle(.kts)
file representing your Gradle build including required subprojects e.g. include("app", "model", "service") -
build.gradle(.kts)
and source code for each subproject in corresponding subdirectories
Our build currently consists of a root project called authoring-tutorial
, which has a single app
subproject:
. (1)
├── app (2)
│ ... (3)
│ └── build.gradle.kts (4)
└── settings.gradle.kts (5)
1 | The authoring-tutorial root project |
2 | The app subproject |
3 | The app source code |
4 | The app build script |
5 | The optional settings file |
. (1)
├── app (2)
│ ... (3)
│ └── build.gradle (4)
└── settings.gradle (5)
1 | The authoring-tutorial root project |
2 | The app subproject |
3 | The app source code |
4 | The app build script |
5 | The optional settings file |
Step 2. Add another Subproject to the Build
Imagine that our project is growing and requires a custom library to function.
Let’s create this imaginary lib
.
First, create a lib
folder:
mkdir lib
cd lib
Create a file called build.gradle(.kts)
and add the following lines to it:
plugins {
id("java")
}
repositories {
mavenCentral()
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.9.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation("com.google.guava:guava:32.1.1-jre")
}
tasks.named<Test>("test") {
useJUnitPlatform()
}
tasks.register("task3"){
println("REGISTER TASK3: This is executed during the configuration phase")
}
tasks.named("task3"){
println("NAMED TASK3: This is executed during the configuration phase")
doFirst {
println("NAMED TASK3 - doFirst: This is executed during the execution phase")
}
doLast {
println("NAMED TASK3 - doLast: This is executed during the execution phase")
}
}
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.3'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'com.google.guava:guava:32.1.1-jre'
}
test {
useJUnitPlatform()
}
tasks.register('task3') {
println('REGISTER TASK3: This is executed during the configuration phase')
}
tasks.named('task3') {
println('NAMED TASK3: This is executed during the configuration phase')
doFirst {
println('NAMED TASK3 - doFirst: This is executed during the execution phase')
}
doLast {
println('NAMED TASK3 - doLast: This is executed during the execution phase')
}
}
Your project should look like this:
.
├── app
│ ...
│ └── build.gradle.kts
├── lib
│ └── build.gradle.kts
└── settings.gradle.kts
.
├── app
│ ...
│ └── build.gradle
├── lib
│ └── build.gradle
└── settings.gradle
Let’s add some code to the lib
subproject.
Create a new directory:
mkdir -p lib/src/main/java/com/gradle
Create a Java class called CustomLib
in a file called CustomLib.java
with the following source code:
package com.gradle;
public class CustomLib {
public static String identifier = "I'm a String from a lib.";
}
The project should now have the following file and directory structure:
.
├── app
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── java
│ └── authoring
│ └── tutorial
│ └── App.java
├── lib
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── java
│ └── com
│ └── gradle
│ └── CustomLib.java
└── settings.gradle.kts
.
├── app
│ ├── build.gradle
│ └── src
│ └── main
│ └── java
│ └── authoring
│ └── tutorial
│ └── App.java
├── lib
│ ├── build.gradle
│ └── src
│ └── main
│ └── java
│ └── com
│ └── gradle
│ └── CustomLib.java
└── settings.gradle
However, the lib
subproject does not belong to the build, and you won’t be able to execute task3
, until it is added to the settings.gradle(.kts)
file.
To add lib
to the build, update the settings.gradle(.kts)
file in the root accordingly:
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0"
}
rootProject.name = "authoring-tutorial"
include("app")
include("lib") // Add lib to the build
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0'
}
rootProject.name = 'authoring-tutorial'
include('app')
include('lib') // Add lib to the build
Let’s add the lib
subproject as an app
dependency in app/build.gradle(.kts)
:
dependencies {
implementation(project(":lib")) // Add lib as an app dependency
}
dependencies {
implementation(project(':lib')) // Add lib as an app dependency
}
Update the app
source code so that it imports the lib
:
package authoring.tutorial;
import com.gradle.CustomLib;
public class App {
public String getGreeting() {
return "CustomLib identifier is: " + CustomLib.identifier;
}
public static void main(String[] args) {
System.out.println(new App().getGreeting());
}
}
Finally, let’s run the app
with the command ./gradlew run
:
$ ./gradlew run
> Configure project :app
> Task :app:processResources NO-SOURCE
> Task :lib:compileJava
> Task :lib:processResources NO-SOURCE
> Task :lib:classes
> Task :lib:jar
> Task :app:compileJava
> Task :app:classes
> Task :app:run
CustomLib identifier is: I'm a String from a lib.
BUILD SUCCESSFUL in 11s
8 actionable tasks: 6 executed, 2 up-to-date
Our build for the root project authoring-tutorial
now includes two subprojects, app
and lib
.
app
depends on lib
.
You can build lib
independent of app
.
However, to build app
, Gradle will also build lib
.
Step 3. Understand Composite Builds
A composite build is simply a build that includes other builds.
Composite builds allow you to:
-
Extract your build logic from your project build (and re-use it among subprojects)
-
Combine builds that are usually developed independently (such as a plugin and an application)
-
Decompose a large build into smaller, more isolated chunks
Step 4. Add build to the Build
Let’s add a plugin to our build.
First, create a new directory called license-plugin
in the gradle
directory:
cd gradle
mkdir license-plugin
cd license-plugin
Once in the gradle/license-plugin
directory, run gradle init
.
Make sure that you select the Gradle plugin
project as well as the other options for the init
task below:
$ gradle init --dsl kotlin --type kotlin-gradle-plugin --project-name license
$ gradle init --dsl groovy --type groovy-gradle-plugin --project-name license
Select defaults for any additional prompts.
Your project should look like this:
.
├── app
│ ...
│ └── build.gradle.kts
├── lib
│ ...
│ └── build.gradle.kts
├── gradle
│ ├── ...
│ └── license-plugin
│ ├── settings.gradle.kts
│ └── plugin
│ ├── gradle
│ │ └── ....
│ ├── src
│ │ ├── functionalTest
│ │ │ └── ....
│ │ ├── main
│ │ │ └── kotlin
│ │ │ └── license
│ │ │ └── LicensePlugin.kt
│ │ └── test
│ │ └── ...
│ └── build.gradle.kts
│
└── settings.gradle.kts
.
├── app
│ ...
│ └── build.gradle
├── lib
│ ...
│ └── build.gradle
├── gradle
│ ├── ...
│ └── license-plugin
│ ├── settings.gradle
│ └── plugin
│ ├── gradle
│ │ └── ....
│ ├── src
│ │ ├── functionalTest
│ │ │ └── ....
│ │ ├── main
│ │ │ └── groovy
│ │ │ └── license
│ │ │ └── LicensePlugin.groovy
│ │ └── test
│ │ └── ...
│ └── build.gradle
│
└── settings.gradle
Take the time to look at the LicensePlugin.kt
or LicensePlugin.groovy
code and the gradle/license-plugin/settings.gradle(.kts)
file.
It’s important to note that this is an entirely separate build with its own settings file and build script:
rootProject.name = "license"
include("plugin")
rootProject.name = 'license'
include('plugin')
To add our license-plugin
build to the root project, update the root settings.gradle(.kts)
file accordingly:
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0"
}
rootProject.name = "authoring-tutorial"
include("app")
include("subproject")
includeBuild("gradle/license-plugin") // Add the new build
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0'
}
rootProject.name = 'running-tutorial-groovy'
include('app')
include('lib')
includeBuild('gradle/license-plugin')
You can view the structure of the root project by running ./gradlew projects
in the root folder authoring-tutorial
:
$ ./gradlew projects
------------------------------------------------------------
Root project 'authoring-tutorial'
------------------------------------------------------------
Root project 'authoring-tutorial'
+--- Project ':app'
\--- Project ':lib'
Included builds
\--- Included build ':license-plugin'
Our build for the root project authoring-tutorial
now includes two subprojects, app
and lib
, and another build, license-plugin
.
When in the project root, running:
-
./gradlew build
- Buildsapp
andlib
-
./gradlew :app:build
- Buildsapp
andlib
-
./gradlew :lib:build
- Buildslib
only -
./gradlew :license-plugin:plugin:build
- Buildslicense-plugin
only
There are many ways to design a project’s architecture with Gradle.
Multi-project builds are great for organizing projects with many modules such as mobile-app
, web-app
, api
, lib
, and documentation
that have dependencies between them.
Composite (include) builds are great for separating build logic (i.e., convention plugins) or testing systems (i.e., patching a library)
Next Step: Settings File >>