[English] [中文]
🚧 It's currently under incubating...
Polyfill is a middleware to assist writing Gradle Plugins for Android build system.
As its name suggests, the lib is a middle-ware between AGP (Android Gradle Plugin) and 3rd Gradle Plugin based on AGP context. For example, the ScratchPaper is a plugin to add an overlay to your app icons which based on AGP, it consumes:
- SDK Locations / BuildToolInfo instance (to run aapt commands)
- Merged AndroidManifest.xml (to get the resolved icon name)
Those inputs usually caused problems:
- They are open-source but not exposed directly, sometimes you may need to use reflect to retrieve the input you need.
- They may change when updating to a new AGP version, again, because those are mainly for internal usage.
In 2018, I started to consider if we can make a Polyfill layer for 3rd Android Gradle Plugin developers, and finally released the first version in 2020 as you can see here. The name "Polyfill" comes from the FrontEnd tech stack, which makes the JS code compatible with old/odd browser APIs.
To be noticed, starting from AGP 4.1.0, the AGP team provides a new public API set "Artifacts". However, this is a very early stage that only provides less than 10 artifacts' API for developers to use, and since AGP released 2-3 minor versions per year, developers may not expect their own problems got quickly fixed in recent 2-3 years.
That's why I still insist to create Polyfill lib and wish one day we can 100% migrate to Artifacts API.
Find more Artifaces API news from links below:
- gradle-recipes: which is the official Artifacts API showcases.
- New APIs in the Android Gradle Plugin : a brief orientation for new Artifacts API.
- It encapsulates AGP (Android Gradle Plugin) APIs, turn them to Task Hook Points (action) and Task Inputs for all 3rd plugin developer to easily interact with.
- Task Hook Points: for instance, if the developer wants to intercept manifest merge input files, he/she should find producer task(s) and consumer task of input files, then add a new custom task that will be executed between them; here the Hook Points means we define an action who process the task order stuffs and make sure the new-added runs on the true timing, Polyfill provides many
AGPTaskAction.kt
(the impl such asManifestBeforeMergeAction.kt
) to complete this job. - Task Inputs: with the help of Hook Points, your task logic can now be executed during the right time, what you might miss is task input(s), for example, Manifest files, Android SDK locations, AGP versions, etc. To configure them easily, Polyfill provides
SelfManageableProvider.kt
(the impl such asManifestMergeInputProvider.kt
) to fetch the task input(s).
- Task Hook Points: for instance, if the developer wants to intercept manifest merge input files, he/she should find producer task(s) and consumer task of input files, then add a new custom task that will be executed between them; here the Hook Points means we define an action who process the task order stuffs and make sure the new-added runs on the true timing, Polyfill provides many
- Meanwhile, it provides a bunch of tools working on build intermediates, such as binary parser and builder for
resources.arsc
,AndroidManifest.xml
.
- Add Polyfill to build classpath:
// Root project's build.gradle.kts
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:4.2.0")
classpath("me.2bab:polyfill:0.3.1")
}
}
// If add to /buildSrc/build.gradle.kts or standalone plugin project, then:
// dependencies {
// implementation("me.2bab:polyfill:0.3.1")
// }
- Write some custom tasks based on Polyfill (follow the steps of comment):
// 0. Create Polyfill instance (per project):
// Create for application module
val polyfill = PolyfillFactory.createApplicationPolyfill(project)
// Create for library module
// Polyfill.createLibraryPolyfill(project)
// 1. Start onVariantProperties
polyfill.onVariants {
val variant = this
// 3. Create & Config the hook task.
val preUpdateTask = project.tasks.register("preUpdate${variant.name.capitalize()}Manifest",
ManifestBeforeMergeTask::class.java) {
beforeMergeInputs.set(polyfill.getProvider(variant, ManifestMergeInputProvider::class.java).get())
}
// 4. Add it with the action (which plays the role of entry for a hook).
val beforeMergeAction = ManifestBeforeMergeAction(preUpdateTask)
polyfill.addAGPTaskAction(variant, beforeMergeAction)
// Let's try again with after merge hook
val postUpdateTask = project.tasks.register("postUpdate${variant.name.capitalize()}Manifest",
ManifestAfterMergeTask::class.java) {
afterMergeInputs.set(polyfill.getProvider(variant, ManifestMergeOutputProvider::class.java).get())
}
val afterMergeAction = ManifestAfterMergeAction(postUpdateTask)
polyfill.addAGPTaskAction(variant, afterMergeAction)
}
// 2. Prepare the task containing specific hook logic.
abstract class ManifestBeforeMergeTask : DefaultTask() {
@get:InputFiles
abstract val beforeMergeInputs: SetProperty<FileSystemLocation>
@TaskAction
fun beforeMerge() {
val manifestPathsOutput = FunctionTestFixtures.getOutputFile(project, "manifest-merge-input.json")
manifestPathsOutput.createNewFile()
beforeMergeInputs.get().let { set ->
manifestPathsOutput.writeText(JSON.toJSONString(set.map { it.asFile.absolutePath }))
}
}
}
abstract class ManifestAfterMergeTask : DefaultTask() {
@get:InputFiles
abstract val afterMergeInputs: RegularFileProperty
@TaskAction
fun afterMerge() {
if (afterMergeInputs.isPresent) {
val file = afterMergeInputs.get().asFile
val modifiedManifest = file.readText()
.replace("allowBackup=\"true\"", "allowBackup=\"false\"")
file.writeText(modifiedManifest)
}
}
}
// Optional: if the new Variant API can not fulfill the requirement
// or you want to migrate from an old project to Polyfill smoothly,
// you can use onClassicVariants{} instead.
// Here are 2 samples which their used APIs are accessible for ApplicationVariant only,
// though `polyfill.onVariants` is preferred as it uses new Variant API (Old APIs may get depracted).
//
// @see com.android.build.api.variant.ApplicationVariant
polyfill.onClassicVariants {
val applicationVariant = this
project.tasks
.register("makeCacheDir") {
// The versionName is only accessible by ApplicationVariant
project.logger.info(applicationVariant.versionName)
}
.dependsOn(this.preBuildProvider) // The AGP task providers is only available by ApplicationVariant
}
Check more in ./test-plugin
and ./polyfill/src/functionalTest
.
Polyfill is only supported & tested on latest 2 Minor versions of Android Gradle Plugin.
Changelog can be found from Github Releases.
AGP Version | Latest Support Version |
---|---|
4.2.0 | 0.3.1 (MavenCentral) |
(The project currently compiles with the latest version of AGP 4.2, and compiles and tests against the both AGP 4.2 and 7.0 on CI.)
Check this link to make sure everyone will make a meaningful commit message.
So far we haven't added any hook tool, but follow the regex below:
(chore|feat|docs|fix|refactor|style|test|hack|release)(:)( )(.{0,80})
Copyright 2018-2021 2BAB
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.