Gradle and AGP Build APIs: Taking your plugin to the next step!

Murat Yener
Dec 1, 2021 · 7 min read

Welcome to a new article in the MAD skills series on Gradle and Android Gradle plugin APIs. In the last article you learned about writing your own plugin and using the new Variants API. If you prefer watching instead of reading, here is something to check out:

In this article, you’ll learn about Gradle tasks, providers, properties and using task inputs and outputs. You’ll also take your plugin a step further and learn how to get access to various build artifacts using the new Artifact API.

Properties

Let’s say I want to build a plugin which automatically updates the version code specified in the app manifest with the git version. To achieve that, I need to add two tasks to my build. The first task will obtain the git version and the second task will use that git version to update the manifest file.

Let’s start with creating a new task called . This class needs to extend and implement the annotated function. Here is some code below which queries the tip of the git tree.

abstract class GitVersionTask: DefaultTask() {    @TaskAction
fun taskAction(){
// this would be the code to get the tip of tree version,
val process = ProcessBuilder(
"git",
"rev-parse --short HEAD"
).start()
val error = process.errorStream.readBytes().toString()
if (error.isNotBlank()) {
System.err.println("Git error : $error")
}
var gitVersion = process.inputStream.readBytes().toString()
//... }
}

I can’t cache this value, I want to store the version info in an intermediate file so other tasks can read and use this value. To do this, I need to use a . Properties can be used for task inputs and outputs. In this case the will act as a container to represent the task output. I create a and annotate it with . is a marker annotation which is attached to the getter function. With this, the is marked as the output file for this task.

@get:OutputFile
abstract val gitVersionOutputFile: RegularFileProperty

Now that I’ve declared my task output, let’s jump back into the where I can access the file and write the text which I want to store. In this case I’ll store the git version that will be the output of this task. To keep things simple, I replace the git version query with a hardcoded string.

abstract class GitVersionTask: DefaultTask() {
@get:OutputFile
abstract val gitVersionOutputFile: RegularFileProperty
@TaskAction
fun taskAction() {
gitVersionOutputFile.get().asFile.writeText("1234")
}
}

Now that the task is ready, let’s register this task in the plugin code. First I’ll create a new plugin class called and implement . If you’re not familiar with creating plugins in the folder, make sure to take a look at the previous videos [1], [2] in this series.

buildSrc folder

Next I’ll register the and set the output file to an intermediate file in the build folder. I also set to so that the outputs from previous executions of this task will not be reused. This also means that the task will be executed on every build since it is never up to date.

override fun apply(project: Project) {
project.tasks.register(
"gitVersionProvider",
GitVersionTask::class.java
) {
it.gitVersionOutputFile.set(
File(
project.buildDir,
"intermediates/gitVersionProvider/output"
)
)
it.outputs.upToDateWhen { false }
}
}

After executing the task, I can check the file under the folder and verify that the task stored the value which I hardcoded in the task.

Now let’s switch to the second task which will update the version in the manifest file. I call that task and use two objects as the inputs to this task.

abstract class ManifestTransformerTask: DefaultTask() {    @get:InputFile
abstract val gitInfoFile: RegularFileProperty
@get:InputFile
abstract val mergedManifest: RegularFileProperty
}

I’ll use the first to read the contents of the output file which is generated by the . I’ll use the second to read the manifest file of the app. Next I can replace the version code in the manifest with the version code stored in the variable from the .

@TaskAction
fun taskAction() {
val gitVersion = gitInfoFile.get().asFile.readText()
var manifest = mergedManifest.asFile.get().readText()
manifest = manifest.replace(
"android:versionCode=\"1\"",
"android:versionCode=\"${gitVersion}\""
)

}

Now I can write the updated manifest. To do that, first I’ll create another for the output and annotate it with .

@get:OutputFile
abstract val updatedManifest: RegularFileProperty

Note, I could actually use the VariantOutput to set the versionCode directly, without having to rewrite the manifest but I’ll use this sample to show you how to use artifact transformations for the same effect.

Let’s go back to the plugin to wire things up. First I get the . I want this new task to run after AGP decides which variants to create, but before values are locked and cannot be modified. The callback follows the callback which you might remember from the previous article.

val androidComponents = project.extensions.getByType(
AndroidComponentsExtension::class.java
)
androidComponents.onVariants { variant ->
//...
}

Providers

You can use s to wire the to other tasks that need to perform time consuming operations such as reading from external inputs like files or the network.

I’ll start with registering the . This task requires the file which is the output of the previous task. To access this , I will use a .

val manifestUpdater: TaskProvider = project.tasks.register(
variant.name + "ManifestUpdater",
ManifestTransformerTask::class.java
) {
it.gitInfoFile.set(
//...
)
}

A can be used to access a value of a given type, either directly by using the function or by converting the value to a new using the operator functions such as and . When I look back at the interface, it implements the interface. You can lazily set values on a and later lazily access these values using s.

When I look at the return type of the function, this returns a of the given type. I’ll assign this to a new .

val gitVersionProvider = project.tasks.register(
"gitVersionProvider",
GitVersionTask::class.java
) {
it.gitVersionOutputFile.set(
File(
project.buildDir,
"intermediates/gitVersionProvider/output"
)
)
it.outputs.upToDateWhen { false }
}

Now back to setting the input to the . I get an error when I try to map the value from the to the input . The lambda argument to takes one value such as and produces another value type such as .

Error when using

However, in this case the set function expects a . I can use the function which similarly takes a value such as but this time produces a of type , instead of producing the value directly.

it.gitInfoFile.set(
gitVersionProvider.flatMap(
GitVersionTask::gitVersionOutputFile
)
)

Transformations

Next I need to tell the artifacts of this variant to use and wire the manifest as the input and the updated manifest as the output. Finally I call the function to transform a single artifact type.

variant.artifacts.use(manifestUpdater)
.wiredWithFiles(
ManifestTransformerTask::mergedManifest,
ManifestTransformerTask::updatedManifest
).toTransform(SingleArtifact.MERGED_MANIFEST)

When I run this task, I can see that the version code in the app manifest is now updated with the value in the file. Note that, I didn’t explicitly ask the to run. The is executed because this task’s output is used as the input to the which I requested to run.

BuiltArtifactsLoader

Let’s add another task to see how we can access the updated manifest and verify that it is updated successfully. I’ll create a new task called . In order to read the manifest, I need to access the APK file which is an artifact of the build task. To do this I need to set the build APK folder as the input to this task.

Notice this time I am using instead of because the object represents the directory where APK files are placed after the build.

As the second input of this task I need another of type which I’ll use to load instances from metadata files that describe the files in the APK directory. Builds can produce several APKs when you have native components, various languages and so on. The abstracts the process of identifying each APKs as well as their attributes like ABI and languages.

@get:Internal
abstract val builtArtifactsLoader: Property<BuiltArtifactsLoader>

Now it is time to implement the task. First I load the , make sure that it contains only one APK and then load this APK as a instance.

val builtArtifacts = builtArtifactsLoader.get().load(
apkFolder.get()
)?: throw RuntimeException("Cannot load APKs")
if (builtArtifacts.elements.size != 1)
throw RuntimeException("Expected one APK !")
val apk = File(builtArtifacts.elements.single().outputFile).toPath()

At this point, I can access the manifest in the APK and verify that the version is updated. To keep things simple I’ll only check if the APK exists, add a reminder to check the manifest here and print a success message.

println("Insert code to verify manifest file in ${apk}")
println("SUCCESS")

Now back to the plugin code to register this task. In the plugin code, I register the new task as , pass in the APK folder and the object of the current variant’s artifacts.

project.tasks.register(
variant.name + "Verifier",
VerifyManifestTask::class.java
) {
it.apkFolder.set(variant.artifacts.get(SingleArtifact.APK))
it.builtArtifactsLoader.set(
variant.artifacts.getBuiltArtifactsLoader()
)
}

This time when I run the task I can see that the new task loads the APK and prints the success message. Notice, again I didn’t explicitly request the manifest transformation to happen but because the requested the final version of the manifest artifact, the transformation happened automatically.

Summary

My plugin has three tasks: My plugin first checks the current git version tree, and stores the version in an intermediate file. Next, my plugin uses the output of this task lazily, using a provider to update the current manifest with the version code. Finally, my plugin uses another task to access the build artifacts and check if the manifest is updated.

That’s it! Starting with version 7.0 Android Gradle plugin offers official extension points so that you can write your own plugins! With these new APIs you can control build inputs, read, modify or even replace intermediate and final artifacts.

To learn more and to keep your builds efficient, make sure to check out the documentation and recipes.

Android Developers

The official Android Developers publication on Medium