Imagine a bacon-wrapped Ferrari. Still not better than our free technical reports.

Using buildSrc for custom logic in Gradle builds

If somebody asked about the Gradle feature that everybody should know about, I would probably point them towards buildSrc. It is a magic Gradle Java/Groovy project inside your repository, available as a library to all of your build.gradle files.

This approach allows you to write code in your favorite JVM language and to use it right there in your build scripts. As a bonus, you can also cover trickier parts of your build logic with unit tests!

Hello buildSrc!

Getting started is easy! Let us create a folder named buildSrc in the root folder of your project (the same folder that contains your settings.gradle):

$ mkdir buildSrc

The project will automatically have both the java and groovy plugins applied to it. We can simply create a standard src/main/java folder and start writing code.

$ mkdir -p buildSrc/src/main/java

Now we can re-sync the project in the IDE and create a new Java class in our freshly minted buildSrc project:

package myapp.gradle;

public class Fun {
  public static void sayHello() {
    System.out.println("Hello from buildSrc!");
  }
}

All that’s left to do is to call the Fun.sayHello method from any build.gradle file. Let’s create a simple hello task in the root project to try this out:

import myapp.gradle.Fun

task hello {
  doLast {
    Fun.sayHello()
  }
}
$ ./gradlew hello
> Task :hello
Hello from buildSrc!

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

That’s all there is to it. sayHello is a normal Java method like any other. Since our build.gradle file is like a JVM application, we can call it like any other Java method in Groovy.

Writing custom tasks

Reusing the code is nice, but all this doLast {} boilerplate when creating new tasks feels a bit iffy. With buildSrc we can remove this boilerplate by creating a custom task class instead. A custom Gradle task is simply a Java class that:

  • Extends org.gradle.api.DefaultTask.
  • Has a public void method with the @TaskAction annotation. This is the code that will run when our task gets executed.

Let’s refactor our previous Fun.sayHello method into such a task:

package myapp.gradle;

import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.TaskAction;

public class HelloTask extends DefaultTask {
    @TaskAction
    public void run() {
        System.out.println("Hello from task " + getPath() + "!");
    }
}

Using a custom task from our Gradle scripts is easy:

import myapp.gradle.HelloTask

task hello(type: HelloTask)
$ ./gradlew hello
> Task :hello
Hello from task :hello!

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

Turning custom tasks into full-blown plugins

With a larger project it could happen that you end up defining tens of custom tasks with intricate dependency graphs. In this case it makes sense to create a custom plugin instead. That plugin creates all the tasks and wires them together.

Here’s what a simple plugin adding our hello task would look like:

package myapp.gradle;

import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class MyPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        project.getTasks().create("hello", HelloTask.class);
    }
}

And now we can apply the custom plugin in our build file instead of creating a custom task:

apply plugin: myapp.gradle.MyPlugin

Yes, it still works!

$ ./gradlew hello
> Task :hello
Hello from task :hello!

BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed

We now have a working custom Gradle plugin that we can use from any of our subprojects. Of course, printing some text to the terminal is barely scratching the surface of what you can do with Gradle plugins. If you want to know more about writing plugins, I heartily recommend these talks from Gradle Summit 2017:

Bonus trick: managing dependencies with buildSrc

During the day I work as a developer on JRebel for Android. It’s a development tool that contains an IDE plugin, a Gradle plugin, an Android app and a normal Java SE app. To build these artifacts, we have a single large Gradle project consisting of 90+ subprojects. These share a lot of common dependencies, for example commons-io or slf4j-api. Writing compile 'org.slf4j:slf4j-api:1.7.25' over and over again quickly became a chore — as well as a guessing game of what the correct version to use is.

The common way to manage these is to have an ext {} block in your root project. Refer to this Stack Overflow answer for an example. The downside of this approach is that you don’t get proper autocompletion or code navigation.

We took a different route: defining our dependencies as plain old String constants in a Deps.java file inside buildSrc. This way we get full code completion and code navigation support right in the IDE! Here’s an example of this trick for a really simple Android project:

package myapp.gradle;

public class Deps {
    public static final String androidPlugin = "com.android.tools.build:gradle:3.0.0-beta6";

    public static final String kotlinVersion = "1.1.50";
    public static final String kotlinPlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:" + kotlinVersion;
    public static final String kotlinRuntime = "org.jetbrains.kotlin:kotlin-stdlib-jre7:" + kotlinVersion;

    public static final String appCompat = "com.android.support:appcompat-v7:26.1.0";
    public static final String constraintLayout = "com.android.support.constraint:constraint-layout:1.0.2";

    public static final String junit = "junit:junit:4.12";
}

And now we can reference these in our build.gradle files:

The downside of this approach is that it feels slightly wrong to put build configuration inside the buildSrc project. I think the benefits are still worth it though.

Do you have a neat trick regarding buildSrc? Let us know in the comments. Or ping me directly on Twitter: @madisp. All of the example code is available on GitHub.


Read next: