Keywords: buildsrc,build-logic

# Sharing Build Logic between Subprojects

version 8.11

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:

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

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

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

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

5. 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:

shared.gradle.kts

    repositories {
        mavenCentral()
    }

    dependencies {
        implementation("org.slf4j:slf4j-api:1.7.32")
    }

shared.gradle

    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:

MyCustomTask.kt

    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")
        }
    }

MyCustomTask.groovy

    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.gradle(.kts)` file is also applied:

build.gradle.kts

    // Apply any other configurations specific to your project

    // Use the build script defined in buildSrc
    apply(from = rootProject.file("buildSrc/shared.gradle.kts"))

    // Use the custom task defined in buildSrc
    tasks.register<MyCustomTask>("myCustomTask")

build.gradle

    // 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:

settings.gradle.kts

    rootProject.name = "dependencies-java"
    include("api", "shared", "services:person-service")

settings.gradle

    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:

buildSrc/src/main/kotlin/myproject.java-conventions.gradle.kts

    plugins {
        id("java")
    }

    group = "com.example"
    version = "1.0"

    repositories {
        mavenCentral()
    }

    dependencies {
        testImplementation("junit:junit:4.13")
    }

buildSrc/src/main/groovy/myproject.java-conventions.gradle

    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:

buildSrc/build.gradle.kts

    plugins {
        `kotlin-dsl`
    }

    repositories {
        mavenCentral()
    }

buildSrc/build.gradle

    plugins {
        id 'groovy-gradle-plugin'
    }

The convention plugin is applied to the `api`, `shared`, and `person-service` subprojects:

api/build.gradle.kts

    plugins {
        id("myproject.java-conventions")
    }

    dependencies {
        implementation(project(":shared"))
    }

shared/build.gradle.kts

    plugins {
        id("myproject.java-conventions")
    }

services/person-service/build.gradle.kts

    plugins {
        id("myproject.java-conventions")
    }

    dependencies {
        implementation(project(":shared"))
        implementation(project(":api"))
    }

api/build.gradle

    plugins {
        id 'myproject.java-conventions'
    }

    dependencies {
        implementation project(':shared')
    }

shared/build.gradle

    plugins {
        id 'myproject.java-conventions'
    }

services/person-service/build.gradle

    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:

1. Applying plugins or other configurations to subprojects of a certain type.
   Often, the cross-configuration logic is `if subproject is of type X, then configure Y`. This is equivalent to applying `X-conventions` plugin directly to a subproject.

2. Extracting information from subprojects of a certain type.
   This use case can be modeled using outgoing configuration variants.