I have this custom gradle plugin to replace the implementation of a class. When I build the apk I do see that the original class is modified as expected. However, the class packaged in the apk is still the original one (confirmed by decompiling and checking the byte code from apk file).
Here is the sequence of tasks run:
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
> Task :app:checkKotlinGradlePluginConfigurationErrors SKIPPED
> Task :app:generateDebugResValues
> Task :app:checkDebugAarMetadata
> Task :app:mapDebugSourceSetPaths
> Task :app:generateDebugResources
> Task :app:packageDebugResources
> Task :app:createDebugCompatibleScreenManifests
> Task :app:extractDeepLinksDebug
> Task :app:mergeDebugResources
> Task :app:parseDebugLocalResources
> Task :app:processDebugMainManifest
> Task :app:processDebugManifest
> Task :app:javaPreCompileDebug
> Task :app:mergeDebugShaders
> Task :app:compileDebugShaders NO-SOURCE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:mergeDebugAssets
> Task :app:compressDebugAssets
> Task :app:desugarDebugFileDependencies
> Task :app:mergeDebugJniLibFolders
> Task :app:checkDebugDuplicateClasses
> Task :app:mergeDebugNativeLibs NO-SOURCE
> Task :app:stripDebugDebugSymbols NO-SOURCE
> Task :app:validateSigningDebug
> Task :app:writeDebugAppMetadata
> Task :app:mergeLibDexDebug
> Task :app:writeDebugSigningConfigVersions
> Task :app:processDebugManifestForPackage
> Task :app:processDebugResources
> Task :app:mergeExtDexDebug
> Task :app:compileDebugKotlin
> Task :app:compileDebugJavaWithJavac NO-SOURCE
> Task :app:dexBuilderDebug
> Task :app:processDebugJavaRes
> Task :app:mergeDebugGlobalSynthetics
> Task :app:processBytecode <--- class replaced here
> Task :app:mergeProjectDexDebug
> Task :app:mergeDebugJavaResource
> Task :app:packageDebug
> Task :app:createDebugApkListingFileRedirect
> Task :app:assembleDebug
BUILD SUCCESSFUL in 11s
Classes created are of same size as expected:
% find . -name "*.class" -exec ls -l {} \; | grep Hello
-rw-r--r--@ 1 user staff 1036 Jan 21 23:12 ./app/build/tmp/kotlin-classes/debug/com/example/HelloModified.class
-rw-r--r--@ 1 user staff 1036 Jan 21 23:12 ./app/build/tmp/kotlin-classes/debug/com/example/Hello.class
Any idea why packaging step misses the instrumented class? If it is due to time/async nature of file writing how do I synchronize?
Here is my plugin code:
lass MyInterceptorPlugin : Plugin<Project> {
override fun apply(project: Project) {
val classReplacements = mapOf("com.example.Hello" to "com.example.HelloModified")
val processClassesTask: TaskProvider<out Task> = project.tasks.register("processBytecode") { task ->
task.group = "custom"
task.description = "Replace specific classes with new ones"
task.doLast {
val classDir = File(project.buildDir, "tmp/kotlin-classes/debug")
if (classDir.exists()) {
println("Searching for classes in: ${classDir.absolutePath}")
// Walk through the class directory and replace specified classes
classDir.walkTopDown().forEach { file ->
if (file.extension == "class") {
val className = file.relativeTo(classDir).path.replace(File.separatorChar, '.').removeSuffix(".class")
if (classReplacements.containsKey(className)) {
println("Replacing class: $className with ${classReplacements[className]}")
// Find and replace the corresponding replacement class file
val replacementClassName = classReplacements[className]?.replace('.', File.separatorChar)
val replacementClassFile = File(project.buildDir, "tmp/kotlin-classes/debug/$replacementClassName.class")
replaceClass(file, replacementClassFile)
}
}
}
}
}
}
project.afterEvaluate {
val androidExtension = project.extensions.findByName("android")
if (androidExtension is com.android.build.gradle.AppExtension) {
androidExtension.applicationVariants.all { variant ->
// Access the Java compile task for the variant
val javaCompileTaskProvider = project.tasks.named(variant.javaCompileProvider.get().name, JavaCompile::class.java)
// Access the Kotlin compile task for each variant
val kotlinCompileTaskProvider = project.tasks.withType(KotlinCompile::class.java)
.matching { task -> task.name == "compile${variant.name.capitalize()}Kotlin" }
// Ensure processClassesTask runs after Java and Kotlin compilation tasks
javaCompileTaskProvider.configure { task ->
task.finalizedBy(processClassesTask) // Make sure class processing happens after Java compile
}
kotlinCompileTaskProvider.configureEach { task ->
task.finalizedBy(processClassesTask) // Make sure class processing happens after Kotlin compile
}
// Hook into the dexing process using mergeProjectDexDebug task
val mergeDexTaskProvider = project.tasks.named("mergeProjectDexDebug")
mergeDexTaskProvider.configure {
it.dependsOn(processClassesTask) // Ensure bytecode manipulation happens before dexing
}
// Ensure `processClassesTask` runs before packaging the APK
val assembleTaskProvider = project.tasks.named("assemble${variant.name.capitalize()}")
assembleTaskProvider.configure {
it.dependsOn(processClassesTask) // Ensure processClassesTask runs before assembling the APK
}
}
}
}
}
private fun replaceClass(originalClassFile: File, replacementClassFile: File) {} // Skipped
}
Source: View source