Sharing Build Logic between Subprojects
Subprojects in a multi-project build typically share some common dependencies.
Instead of copying and pasting the same Java version and libraries in each subproject build script, Gradle provides a special directory for storing shared build logic that can be automatically applied to subprojects.
Share logic in buildSrc
buildSrc
is a Gradle-recognized and protected directory which comes with some benefits:
-
Reusable Build Logic:
buildSrc
allows you to organize and centralize your custom build logic, tasks, and plugins in a structured manner. The code written in buildSrc can be reused across your project, making it easier to maintain and share common build functionality. -
Isolation from the Main Build:
Code placed in
buildSrc
is isolated from the other build scripts of your project. This helps keep the main build scripts cleaner and more focused on project-specific configurations. -
Automatic Compilation and Classpath:
The contents of the
buildSrc
directory are automatically compiled and included in the classpath of your main build. This means that classes and plugins defined in buildSrc can be directly used in your project’s build scripts without any additional configuration. -
Ease of Testing:
Since
buildSrc
is a separate build, it allows for easy testing of your custom build logic. You can write tests for your build code, ensuring that it behaves as expected. -
Gradle Plugin Development:
If you are developing custom Gradle plugins for your project,
buildSrc
is a convenient place to house the plugin code. This makes the plugins easily accessible within your project.
The buildSrc
directory is treated as an included build.
For multi-project builds, there can be only one buildSrc
directory, which must be in the root project directory.
The downside of using buildSrc is that any change to it will invalidate every task in your project and require a rerun.
|
buildSrc
uses the same source code conventions applicable to Java, Groovy, and Kotlin projects.
It also provides direct access to the Gradle API.
A typical project including buildSrc
has the following layout:
.
├── buildSrc
│ ├── src
│ │ └──main
│ │ └──kotlin
│ │ └──MyCustomTask.kt (1)
│ ├── shared.gradle.kts (2)
│ └── build.gradle.kts
├── api
│ ├── src
│ │ └──...
│ └── build.gradle.kts (3)
├── services
│ └── person-service
│ ├── src
│ │ └──...
│ └── build.gradle.kts (3)
├── shared
│ ├── src
│ │ └──...
│ └── build.gradle.kts
└── settings.gradle.kts
1 | Create the MyCustomTask task. |
2 | A shared build script. |
3 | Uses the MyCustomTask task and shared build script. |
.
├── buildSrc
│ ├── src
│ │ └──main
│ │ └──groovy
│ │ └──MyCustomTask.groovy (1)
│ ├── shared.gradle (2)
│ └── build.gradle
├── api
│ ├── src
│ │ └──...
│ └── build.gradle (3)
├── services
│ └── person-service
│ ├── src
│ │ └──...
│ └── build.gradle (3)
├── shared
│ ├── src
│ │ └──...
│ └── build.gradle
└── settings.gradle
1 | Create the MyCustomTask task. |
2 | A shared build script. |
3 | Uses the MyCustomTask task and shared build script. |
In the buildSrc
, the build script shared.gradle(.kts)
is created.
It contains dependencies and other build information that is common to multiple subprojects:
repositories {
mavenCentral()
}
dependencies {
implementation("org.slf4j:slf4j-api:1.7.32")
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.slf4j:slf4j-api:1.7.32'
}
In the buildSrc
, the MyCustomTask
is also created.
It is a helper task that is used as part of the build logic for multiple subprojects:
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
open class MyCustomTask : DefaultTask() {
@TaskAction
fun calculateSum() {
// Custom logic to calculate the sum of two numbers
val num1 = 5
val num2 = 7
val sum = num1 + num2
// Print the result
println("Sum: $sum")
}
}
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
class MyCustomTask extends DefaultTask {
@TaskAction
void calculateSum() {
// Custom logic to calculate the sum of two numbers
int num1 = 5
int num2 = 7
int sum = num1 + num2
// Print the result
println "Sum: $sum"
}
}
The MyCustomTask
task is used in the build script of the api
and shared
projects.
The task is automatically available because it’s part of buildSrc
.
The shared.build(.kts)
file is also applied:
// Apply any other configurations specific to your project
// Use the build script defined in buildSrc
apply(from = rootProject.file("buildSrc/shared.gradle"))
// Use the custom task defined in buildSrc
tasks.register<MyCustomTask>("myCustomTask")
// Apply any other configurations specific to your project
// Use the build script defined in buildSrc
apply from: rootProject.file('buildSrc/shared.gradle')
// Use the custom task defined in buildSrc
tasks.register('myCustomTask', MyCustomTask)
Share logic using convention plugins
Gradle’s recommended way of organizing build logic is to use its plugin system.
We can write a plugin that encapsulates the build logic common to several subprojects in a project. This kind of plugin is called a convention plugin.
While writing plugins is outside the scope of this section, the recommended way to build a Gradle project is to put common build logic in a convention plugin located in the buildSrc
.
Let’s take a look at an example project:
.
├── buildSrc
│ ├── src
│ │ └──main
│ │ └──kotlin
│ │ └──myproject.java-conventions.gradle.kts (1)
│ └── build.gradle.kts
├── api
│ ├── src
│ │ └──...
│ └── build.gradle.kts (2)
├── services
│ └── person-service
│ ├── src
│ │ └──...
│ └── build.gradle.kts (2)
├── shared
│ ├── src
│ │ └──...
│ └── build.gradle.kts (2)
└── settings.gradle.kts
1 | Create the myproject.java-conventions convention plugin. |
2 | Applies the myproject.java-conventions convention plugin. |
.
├── buildSrc
│ ├── src
│ │ └──main
│ │ └──groovy
│ │ └──myproject.java-conventions.gradle (1)
│ └── build.gradle
├── api
│ ├── src
│ │ └──...
│ └── build.gradle (2)
├── services
│ └── person-service
│ ├── src
│ │ └──...
│ └── build.gradle (2)
├── shared
│ ├── src
│ │ └──...
│ └── build.gradle (2)
└── settings.gradle
1 | Create the myproject.java-conventions convention plugin. |
2 | Applies the myproject.java-conventions convention plugin. |
This build contains three subprojects:
rootProject.name = "dependencies-java"
include("api", "shared", "services:person-service")
rootProject.name = 'dependencies-java'
include 'api', 'shared', 'services:person-service'
The source code for the convention plugin created in the buildSrc
directory is as follows:
plugins {
id("java")
}
group = "com.example"
version = "1.0"
repositories {
mavenCentral()
}
dependencies {
testImplementation("junit:junit:4.13")
}
plugins {
id 'java'
}
group = 'com.example'
version = '1.0'
repositories {
mavenCentral()
}
dependencies {
testImplementation "junit:junit:4.13"
}
For the convention plugin to compile, basic configuration needs to be applied in the build file of the buildSrc
directory:
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
plugins {
id 'groovy-gradle-plugin'
}
The convention plugin is applied to the api
, shared
, and person-service
subprojects:
plugins {
id("myproject.java-conventions")
}
dependencies {
implementation(project(":shared"))
}
plugins {
id("myproject.java-conventions")
}
plugins {
id("myproject.java-conventions")
}
dependencies {
implementation(project(":shared"))
implementation(project(":api"))
}
plugins {
id 'myproject.java-conventions'
}
dependencies {
implementation project(':shared')
}
plugins {
id 'myproject.java-conventions'
}
plugins {
id 'myproject.java-conventions'
}
dependencies {
implementation project(':shared')
implementation project(':api')
}
Do not use cross-project configuration
An improper way to share build logic between subprojects is cross-project configuration via the subprojects {}
and allprojects {}
DSL constructs.
Avoid using subprojects {} and allprojects {} .
|
With cross-configuration, build logic can be injected into a subproject which is not obvious when looking at its build script.
In the long run, cross-configuration usually grows in complexity and becomes a burden. Cross-configuration can also introduce configuration-time coupling between projects, which can prevent optimizations like configuration-on-demand from working properly.
Convention plugins versus cross-configuration
The two most common uses of cross-configuration can be better modeled using convention plugins:
-
Applying plugins or other configurations to subprojects of a certain type.
Often, the cross-configuration logic isif subproject is of type X, then configure Y
. This is equivalent to applyingX-conventions
plugin directly to a subproject. -
Extracting information from subprojects of a certain type.
This use case can be modeled using outgoing configuration variants.