Gradle and AGP build API: further improve your plug-in

Posted by beachdaze on Thu, 06 Jan 2022 07:13:08 +0100

Welcome to read MAD Skills Collection The third article on building API between Gradle and AGP. In the last article< Gradle and AGP build API: how to write plug-ins >You learned how to write your own plug-ins and how to use them Variants API.

If you prefer to learn about this through video, please Click here see.

In this article, you will learn about Gradle's Task, Provider, Property, and using Task for input and output. At the same time, you will further improve your plug-in and learn how to use the new plug-in Artifact API Access various build artifacts.

Property

Suppose I want to create a plug-in that can automatically update the version number specified in the application manifest file using the Git version. To achieve this goal, I need to add two tasks to the build. The first Task will get the Git version, and the second Task will use the Git version to update the manifest file.

Let's start by creating a new task called GitVersionTask. GitVersionTask needs to inherit DefaultTask and implement the annotated taskAction function. The following is the code to query the information at the top of the Git tree.

abstract class GitVersionTask: DefaultTask() {
   @TaskAction
   fun taskAction(){
       // Here is the code to get the top of the 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 the version information directly because I want to store it in an intermediate file so that other tasks can read and use this value. To do this, I need to use RegularFileProperty . Property can be used for Task input and output. In this case, Property Will act as a container for rendering Task output. I created a RegularFileProperty and annotated it with @ get:OutputFile. OutputFile Is a tag annotation attached to the getter function. This annotation marks the Property as the output file of the Task.

@get:OutputFile
abstract val gitVersionOutputFile: RegularFileProperty

Now that I have declared the output of the Task, let's go back to the taskAction() function, where I will access the file and write the text I want to store. In this case, I will store the Git version, that is, the output of the Task. To simplify the example, I replaced the code for querying the Git version with a hard coded 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 it in the plug-in code. First, I will create a new plug-in class called ExamplePlugin and implement it in it Plugin . If you are not familiar with the process of creating plug-ins in the buildSrc folder, you can review the first two articles in this series:< Gradle and AGP build API: configure your build file>,<Gradle and AGP build API: how to write plug-ins>.

△ buildSrc folder

Next, I will register GitVersionTask and set the file Property to output to an intermediate file in the build folder. I also set upToDateWhen to false so that the output of the previous execution of this Task will not be reused. This also means that since the Task will not be in the latest state, it will be executed every time it is built.

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 the Task is executed, I can check the output file located in the build/intermediates folder. I just need to verify that the Task stores my hard coded values.

Let's move on to the second Task, which updates the version information in the manifest file. I named it ManifestTransformTask and used two RegularFileProperty objects as its input values.

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

I will use the first RegularFileProperty to read the contents of the output file generated by GitVersionTask; Read the manifest file of the application with the second RegularFileProperty. Then I can replace the version number in the manifest file with the version number stored in the gitVersion variable in the gitInfoFile file file.

@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 file. First, I'll create another RegularFileProperty for the output and annotate it with @ get:OutputFile.

@get:OutputFile
abstract val updatedManifest: RegularFileProperty

be careful : I could have set versionCode directly using variant output without rewriting the manifest file. But to show you how to use the build product transformation, I'll get the same effect in the way of this example.

Let's go back to plug-ins and connect everything. I got it first AndroidComponentsExtension . I want to execute this new Task after AGP decides which variant to create and before the values of various objects are locked and cannot be modified. The callback of onVariants() will be called after beforeVariants() callback, which may remind you of it. Previous article.

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

Provider

You can use Provider Connect the Property to other tasks that need to perform time-consuming operations, such as reading files or external inputs such as the network.

I'll start by registering manifesttransformer Task. This Task relies on the gitVersionOutput file, which is the output of the previous Task. I will access this Property by using the Provider.

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

Provider can be used to access the value of the specified type. You can use it directly get() Function, you can also use operator functions (such as map() and flatMap() )Converts the value to a new Provider. When I review the Property interface, I find that it implements the Property interface. You can lazily set values to Property and lazily access them later using the Provider.

When I check register() Found that it returned the return type of the given type TaskProvider . I assigned it 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 let's go back and set the input of manifesttransformer task. An error occurred when I tried to map the value from the Provider to the input Property. The lambda parameter of map() receives a value of one type (such as T), and the function produces a value of another type (such as S).

△ error caused by using map()

However, in this case, the set function requires the Provider type. I can use the flatMap() function, which also receives a T-type value, but will generate an S-type Provider instead of directly generating an S-type value.

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

transformation

Next, I need to tell the variant product to use manifestUpdater, take the manifest file as input and the updated manifest file as output. Finally, I call toTransform() Function converts the type of a single product.

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

When running this Task, I can see that the version number in the application manifest file is updated to the value in the gitVersion file. Note that I did not explicitly ask GitProviderTask to run. The Task is executed because its output is the input of manifesttransformer Task, which I requested to run.

BuiltArtifactsLoader

Let's add another Task to see how to access the updated manifest file and verify that it has been successfully updated. I will create a new Task called VerifyManifestTask. In order to read the manifest file, I need to access the APK file, which is the product of building the Task. To do this, I need to use the build APK folder as input to the Task.

Note that I used it this time DirectoryProperty instead of FileProperty Because SingleArticfact.APK Object can represent the directory where APK files are stored after construction.

I also need a type of BuiltArtifactsLoader As the second input to the Task, I will use it to load from the metadata file BuiltArtifacts Object. The metadata file describes the file information in the APK directory. If your project contains elements such as native components and multiple languages, several apks can be generated per build. Built artifacts loader abstracts the process of identifying each APK and its attributes, such as ABI and language.

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

It's time to implement the Task. First, I loaded buildArtifacts and ensured that it contained only one APK, and then loaded the 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 time, I can access the manifest file in APK and verify whether the version has been updated successfully. In order to keep the example concise, I will only check whether APK exists here. I also added a "check list file here" reminder and printed a successful message.

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

Now let's go back to the plug-in code to register the Task. In the plug-in code, I register this Task as "Verifier" and pass in the APK folder and the buildArtifactLoader object of the current variant product.

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

When I run the Task again, I can see that the new Task loads the APK and prints the success information. Note that this time I still did not explicitly request the execution of the list conversion, but because VerifierTask requested the final version of the list product, the conversion was performed automatically.

summary

my plug-in unit It contains three tasks: first , the plug-in will check the current Git tree and store the version in an intermediate file; subsequently , the plug-in will use the output of the previous step and use a Provider to update the version number to the current manifest file; last , the plug-in uses another Task to access the build product and check that the manifest file is updated correctly.

That's all! Starting with version 7.0, the Android Gradle plug-in provides an official extension point for you to write your own plug-in. With these new API s, you can control build input, read, modify, and even replace intermediate and end products.

For more information on how to keep your build efficient, see Official documents and gradle-recipes.

Welcome click here Submit feedback to us, or share your favorite content and found problems. Your feedback is very important to us. Thank you for your support!

Topics: Android Gradle