Using :buildSrc Kotlin Extensions From Groovy-based Gradle Build Scripts

Nate Ebel
Engineering at Premise
5 min readApr 19, 2022

--

by Nate Ebel, Android Developer

The buildSrc directory of a Gradle project is a special directory that is treated as an included build and provides a convenient location to store common or complex build logic used throughout your Gradle project. A common example of this might be to declare statically typed dependency coordinates in a single location for across build scripts in a multi-module Gradle project.

Within the buildSrc directory, we can write custom build logic using both Groovy and Kotlin. This has the potential to make build scripts more approachable for developers that are unfamiliar with Groovy but comfortable with Kotlin — as is the case for most of the Android developers here at Premise.

When we define custom build logic in Kotlin within the buildSrc directory, we can easily interact with that code from any Gradle build scripts written in Kotlin — we simply import and access as we would from any other Kotlin file.

Recently, however, we wanted to reuse some Kotlin extensions in our Groovy-based build scripts. In doing so, we found it wasn’t quite as straightforward as reusing in Kotlin-based build scripts and thought we’d share a couple of tips in case others have run into the same hangups.

Let’s walk through a small example to illustrate this use case.

We’ll be working out of a basic “hello world” Android app. You can find the code in our mobile-tutorials repo.

Within the root directory of that project, we’ll add our buildSrc directory.

  • /app
  • /buildSrc
  • settings.gradle
  • build.gradle
  • etc…

Now, let’s add some custom build logic to our new buildSrc directory.

We’ll add an extension function on Gradle’s Project type.

// buildSrc/src/main/.../extensions/ProjectPropertyExtensions.ktfun Project.getProjectVersionName(): String? {    
val versionName = findProperty("VERSION_NAME")
return if (versionName is String) versionName else null
}

From a Kotlin build script file, we can leverage this extension quite simply.

// app/build.gradle.ktsimport com.premise.mobile.tutorials.extensions.getProjectVersionName
...
android {
defaultConfig {
versionName = getProjectVersionName()
}
}

However, from a Groovy file, if we try to use the extension the same way, we’ll get a build error during the script evaluation phase.

// app/build.gradleandroid {
defaultConfig {
// Error: groovy.lang.MissingMethodException
versionName getProjectVersionName()
}
}

Notice though, that in the Kotlin build script example, we had explicitly imported the extension function. So, maybe we just need to import our function?

Still not there. If we try explicitly importing the extension first we get a different compilation error during the configuration phase of our build.

// app/build.gradle// Error: unable to resolve class getProjectVersionName
import com.premise.mobile.tutorials.extensions.getProjectVersionName
android {
defaultConfig {
versionName getProjectVersionName()
}
}

How then are we supposed to reference this getProjectVersionName() extension function from our Groovy build scripts? Surely we don’t have to migrate all our build scripts to Kotlin, right?

To answer that, let’s start by finding the lowest common denominator between our Groovy scripts and our Kotlin scripts.

Both Kotlin and Groovy ultimately compile down to JVM-compatible bytecode.

If we examine the /build directory of our :buildSrc module, we can locate the generated code for our getProjectVersionName() extension.

What we find is ultimately not surprising if you’re familiar with Java<>Kotlin interop. Under the hood, the Kotlin extension function generates a Java class named for the file in which it’s defined ProjectPropertyExtensionsKt.class .

So, we have a Kotlin extension, which generates a Java class, which we ultimately then want to consume from our Groovy build script.

If we return to our last example within our Groovy build script, we might now notice a key clue.

// app/build.gradle// Error: unable to resolve class getProjectVersionName
import com.premise.mobile.tutorials.extensions.getProjectVersionName
android {
defaultConfig {
versionName getProjectVersionName()
}
}

When attempting to import the extension function like we did from the Kotlin build script, we get an error indicating that the compiler can’t resolve the class name getProjectVersionName . In other words, it’s trying to import a class, rather than a specific function name. In Kotlin, this is handled for us, but when mixing Groovy and Kotlin here we need to explicitly import the name of the class generated by the compiler in which our extension function resides at the bytecode level.

With that in mind, we can now update the usage of our extension to call it as a static method on the generated class. The receiver type, Project, is then passed as the first argument to the invocation.

// app/build.gradle// import the generated class
import com.premise.mobile.tutorials.extensions.ProjectPropertyExtensionsKt
android {
defaultConfig {
// call the static function - passing in the project
versionName ProjectPropertyExtensionsKt.getProjectVersionName(project)
}
}

Ultimately, we end up invoking the extension using the same syntax we would if we were calling it from Java.

The same thinking applies for extension properties as well.

Here, we’ve defined an extension property on Project to indicate whether or not we should be sending analytics events.

// buildSrc/src/main/.../extensions/ProjectPropertyExtensions.ktval Project.shouldLogAnalytics: Boolean
get() {
val shouldLogAnalytics = findProperty("LOG_ANALYTICS")
return if (shouldLogAnalytics is String) {
shouldLogAnalytics.toBoolean()
} else {
false
}
}

Now, let’s say we want to use this extension in our Android project to generate a custom BuildConfig field.

// app/build.gradlebuildTypes {
debug {
buildConfigField("boolean", "SHOULD_LOG_ANALYTICS", ProjectPropertyExtensionsKt.getShouldLogAnalytics(project).toString())
}
}

Notice a few things about our usage of the extension property here:

  • Again, we access the extension using the generated class name ProjectPropertyExtensionskt.
  • Rather than using Kotlin’s property access syntax, we must use the extension property through a generated getter.
  • That getter prepends “get” to the property name to generate a method named getShouldLogAnalytics().
  • We must again pass the Project instance receive as the first parameter to the invocation of the method.

This is a bit more complicated than with the extension function, but still mirroring how we would access an extension property from Java.

Understanding how to consume our Kotlin build script extensions from both Groovy and Kotlin allows us to reuse those extensions from any of our build scripts, achieving a greater level of consistency throughout our build.

It also allows us to migrate key build logic into these helper extensions without having to completely migrate our build scripts to Kotlin, which has both developer and build time costs.

Check out our mobile-tutorials repo for the full working sample detailed throughout this post.

Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings, data and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our Careers page.

--

--