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

Murat Yener
Android Developers
Published in
7 min readDec 1, 2021

--

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 GitVersionTask. This class needs to extend DefaultTask and implement the annotated taskAction 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 RegularFileProperty. Properties can be used for task inputs and outputs. In this case the Property will act as a container to represent the task output. I create a RegularFileProperty and annotate it with @get:OutputFile. OutputFile is a marker annotation which is attached to the getter function. With this, the Property 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 taskAction() 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 ExamplePlugin and implement Plugin. If you’re not familiar with creating plugins in the buildSrc folder, make sure to take a look at the previous videos [1], [2] in this series.

buildSrc folder

Next I’ll register the GitVersionTask and set the output file Property to an intermediate file in the build folder. I also set upToDateWhen to false 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 output file under the build/intermediates 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 ManifestTransformTask and use two RegularFileProperty 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 RegularFileProperty to read the contents of the output file which is generated by the GitVersionTask. I’ll use the second RegularFileProperty 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 gitVersion variable from the gitInfoFile.

@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 RegularFileProperty for the output and annotate it with @get:OutputFile.

@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 AndroidComponentsExtension. I want this new task to run after AGP decides which variants to create, but before values are locked and cannot be modified. The onVariants() callback follows the beforeVariants() callback which you might remember from the previous article.

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

Providers

You can use Providers to wire the Property 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 ManifestTransformerTask. This task requires the gitVersionOutput file which is the output of the previous task. To access this Property, I will use a Provider.

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

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

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

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 ManifestTransformerTask. I get an error when I try to map the value from the Provider to the input Property. The lambda argument to map() takes one value such as T and produces another value type such as S.

Error when using map()

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

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

Transformations

Next I need to tell the artifacts of this variant to use manifestUpdater and wire the manifest as the input and the updated manifest as the output. Finally I call the toTransform() 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 gitVersion file. Note that, I didn’t explicitly ask the GitProviderTask to run. The GitProviderTask is executed because this task’s output is used as the input to the ManifestTransformerTask 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 VerifyManifestTask. 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 DirectoryProperty instead of FileProperty because the SingleArticfact.APK object represents the directory where APK files are placed after the build.

As the second input of this task I need another Property of type BuiltArtifactsLoader which I’ll use to load BuiltArtifacts 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 BuiltArtifactsLoader 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 buildArtifacts, make sure that it contains only one APK and then load this APK as a File 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 Verifier, pass in the APK folder and the buildArtifactLoader 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 VerifierTask 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.

--

--