Multi-Project Build Basics
Gradle supports multi-project builds.
While some small projects and monolithic applications may contain a single build file and source tree, it is often more common for a project to have been split into smaller, interdependent modules. The word "interdependent" is vital, as you typically want to link the many modules together through a single build.
Gradle supports this scenario through multi-project builds. This is sometimes referred to as a multi-module project. Gradle refers to modules as subprojects.
A multi-project build consists of one root project and one or more subprojects.
Multi-Project structure
The following represents the structure of a multi-project build that contains two subprojects:
The directory structure should look as follows:
├── .gradle │ └── ⋮ ├── gradle │ ├── libs.version.toml │ └── wrapper ├── gradlew ├── gradlew.bat ├── settings.gradle.kts (1) ├── sub-project-1 │ └── build.gradle.kts (2) ├── sub-project-2 │ └── build.gradle.kts (2) └── sub-project-3 └── build.gradle.kts (2)
1 | The settings.gradle.kts file should include all subprojects. |
2 | Each subproject should have its own build.gradle.kts file. |
Multi-Project standards
The Gradle community has two standards for multi-project build structures:
-
Multi-Project Builds using buildSrc - where
buildSrc
is a subproject-like directory at the Gradle project root containing all the build logic. -
Composite Builds - a build that includes other builds where
build-logic
is a build directory at the Gradle project root containing reusable build logic.
1. Multi-Project Builds using buildSrc
Multi-project builds allow you to organize projects with many modules, wire dependencies between those modules, and easily share common build logic amongst them.
For example, a build that has many modules called mobile-app
, web-app
, api
, lib
, and documentation
could be structured as follows:
.
├── gradle
├── gradlew
├── settings.gradle.kts
├── buildSrc
│ ├── build.gradle.kts
│ └── src/main/kotlin/shared-build-conventions.gradle.kts
├── mobile-app
│ └── build.gradle.kts
├── web-app
│ └── build.gradle.kts
├── api
│ └── build.gradle.kts
├── lib
│ └── build.gradle.kts
└── documentation
└── build.gradle.kts
The modules will have dependencies between them such as web-app
and mobile-app
depending on lib
.
This means that in order for Gradle to build web-app
or mobile-app
, it must build lib
first.
In this example, the root settings file will look as follows:
include("mobile-app", "web-app", "api", "lib", "documentation")
The order in which the subprojects (modules) are included does not matter. |
The buildSrc
directory is automatically recognized by Gradle.
It is a good place to define and maintain shared configuration or imperative build logic, such as custom tasks or plugins.
buildSrc
is automatically included in your build as a special subproject if a build.gradle(.kts)
file is found under buildSrc
.
If the java
plugin is applied to the buildSrc
project, the compiled code from buildSrc/src/main/java
is put in the classpath of the root build script, making it available to any subproject (web-app
, mobile-app
, lib
, etc…) in the build.
Consult how to declare dependencies between subprojects to learn more.
2. Composite Builds
Composite Builds, also referred to as included builds, are best for sharing logic between builds (not subprojects) or isolating access to shared build logic (i.e., convention plugins).
Let’s take the previous example.
The logic in buildSrc
has been turned into a project that contains plugins and can be published and worked on independently of the root project build.
The plugin is moved to its own build called build-logic
with a build script and settings file:
.
├── gradle
├── gradlew
├── settings.gradle.kts
├── build-logic
│ ├── settings.gradle.kts
│ └── conventions
│ ├── build.gradle.kts
│ └── src/main/kotlin/shared-build-conventions.gradle.kts
├── mobile-app
│ └── build.gradle.kts
├── web-app
│ └── build.gradle.kts
├── api
│ └── build.gradle.kts
├── lib
│ └── build.gradle.kts
└── documentation
└── build.gradle.kts
The fact that build-logic is located in a subdirectory of the root project is irrelevant. The folder could be located outside the root project if desired.
|
The root settings file includes the entire build-logic
build:
pluginManagement {
includeBuild("build-logic")
}
include("mobile-app", "web-app", "api", "lib", "documentation")
Consult how to create composite builds with includeBuild
to learn more.
Multi-Project path
A project path has the following pattern: it starts with an optional colon, which denotes the root project.
The root project, :
, is the only project in a path not specified by its name.
The rest of a project path is a colon-separated sequence of project names, where the next project is a subproject of the previous project:
:sub-project-1
You can see the project paths when running gradle projects
:
------------------------------------------------------------
Root project 'project'
------------------------------------------------------------
Root project 'project'
+--- Project ':sub-project-1'
\--- Project ':sub-project-2'
Project paths usually reflect the filesystem layout, but there are exceptions. Most notably for composite builds.
Identifying project structure
You can use the gradle projects
command to identify the project structure.
As an example, let’s use a multi-project build with the following structure:
> gradle -q projects
Projects: ------------------------------------------------------------ Root project 'multiproject' ------------------------------------------------------------ Root project 'multiproject' +--- Project ':api' +--- Project ':services' | +--- Project ':services:shared' | \--- Project ':services:webservice' \--- Project ':shared' To see a list of the tasks of a project, run gradle <project-path>:tasks For example, try running gradle :api:tasks
Multi-project builds are collections of tasks you can run. The difference is that you may want to control which project’s tasks get executed.
The following sections will cover your two options for executing tasks in a multi-project build.
Executing tasks by name
The command gradle test
will execute the test
task in any subprojects relative to the current working directory that has that task.
If you run the command from the root project directory, you will run test
in api, shared, services:shared and services:webservice.
If you run the command from the services project directory, you will only execute the task in services:shared and services:webservice.
The basic rule behind Gradle’s behavior is to execute all tasks down the hierarchy with this name. And complain if there is no such task found in any of the subprojects traversed.
Some task selectors, like help or dependencies , will only run the task on the project they are invoked on and not on all the subprojects to reduce the amount of information printed on the screen.
|
Executing tasks by fully qualified name
You can use a task’s fully qualified name to execute a specific task in a particular subproject.
For example: gradle :services:webservice:build
will run the build
task of the webservice subproject.
The fully qualified name of a task is its project path plus the task name.
This approach works for any task, so if you want to know what tasks are in a particular subproject, use the tasks
task, e.g. gradle :services:webservice:tasks
.
Multi-Project building and testing
The build
task is typically used to compile, test, and check a single project.
In multi-project builds, you may often want to do all of these tasks across various projects.
The buildNeeded
and buildDependents
tasks can help with this.
In this example, the :services:person-service
project depends on both the :api
and :shared
projects.
The :api
project also depends on the :shared
project.
Assuming you are working on a single project, the :api
project, you have been making changes but have not built the entire project since performing a clean
.
You want to build any necessary supporting JARs but only perform code quality and unit tests on the parts of the project you have changed.
The build
task does this:
$ gradle :api:build > Task :shared:compileJava > Task :shared:processResources > Task :shared:classes > Task :shared:jar > Task :api:compileJava > Task :api:processResources > Task :api:classes > Task :api:jar > Task :api:assemble > Task :api:compileTestJava > Task :api:processTestResources > Task :api:testClasses > Task :api:test > Task :api:check > Task :api:build BUILD SUCCESSFUL in 0s
If you have just gotten the latest version of the source from your version control system, which included changes in other projects that :api
depends on, you might want to build all the projects you depend on AND test them too.
The buildNeeded
task builds AND tests all the projects from the project dependencies of the testRuntime
configuration:
$ gradle :api:buildNeeded > Task :shared:compileJava > Task :shared:processResources > Task :shared:classes > Task :shared:jar > Task :api:compileJava > Task :api:processResources > Task :api:classes > Task :api:jar > Task :api:assemble > Task :api:compileTestJava > Task :api:processTestResources > Task :api:testClasses > Task :api:test > Task :api:check > Task :api:build > Task :shared:assemble > Task :shared:compileTestJava > Task :shared:processTestResources > Task :shared:testClasses > Task :shared:test > Task :shared:check > Task :shared:build > Task :shared:buildNeeded > Task :api:buildNeeded BUILD SUCCESSFUL in 0s
You may want to refactor some part of the :api
project used in other projects.
If you make these changes, testing only the :api
project is insufficient.
You must test all projects that depend on the :api
project.
The buildDependents
task tests ALL the projects that have a project dependency (in the testRuntime configuration) on the specified project:
$ gradle :api:buildDependents > Task :shared:compileJava > Task :shared:processResources > Task :shared:classes > Task :shared:jar > Task :api:compileJava > Task :api:processResources > Task :api:classes > Task :api:jar > Task :api:assemble > Task :api:compileTestJava > Task :api:processTestResources > Task :api:testClasses > Task :api:test > Task :api:check > Task :api:build > Task :services:person-service:compileJava > Task :services:person-service:processResources > Task :services:person-service:classes > Task :services:person-service:jar > Task :services:person-service:assemble > Task :services:person-service:compileTestJava > Task :services:person-service:processTestResources > Task :services:person-service:testClasses > Task :services:person-service:test > Task :services:person-service:check > Task :services:person-service:build > Task :services:person-service:buildDependents > Task :api:buildDependents BUILD SUCCESSFUL in 0s
Finally, you can build and test everything in all projects. Any task you run in the root project folder will cause that same-named task to be run on all the children.
You can run gradle build
to build and test ALL projects.
Consult the Structuring Builds chapter to learn more.
Next Step: Learn about the Gradle Build Lifecycle >>