How do I use AspectJ with both Kotlin and Java on Android?

I want to preface this by saying that I’m a Gradle newbie and this is my best undestanding of the underlying issue, please don’t judge too harshly~

I’m using AspectJ in my Android app and I’m weaving it like this:

def weaveCats(String classPath, String aspectPath, String inputOutput) {
    String[] args = [
            "-showWeaveInfo",
            "-1.8",
            "-inpath", inputOutput,
            "-aspectpath", aspectPath,
            "-d", inputOutput,
            "-classpath", classPath,
            "-bootclasspath", android.bootClasspath.join(File.pathSeparator)
    ]

    MessageHandler handler = new MessageHandler(true)
    new Main().run(args, handler)

    def log = project.logger
    for (IMessage message : handler.getMessages(null, true)) {
        switch (message.getKind()) {
            case IMessage.ABORT:
            case IMessage.ERROR:
            case IMessage.FAIL:
                log.error "ERROR: " + message.message, message.thrown
                break
            case IMessage.WARNING:
                log.warn "WARN: " + message.message, message.thrown
                break
            case IMessage.INFO:
                log.info "INFO: " + message.message, message.thrown
                break
            case IMessage.DEBUG:
                log.info "DEBUG " + message.message, message.thrown
                break
        }
    }
}

tasks.withType(JavaCompile) {
    if (name == "compileDebugJavaWithJavac") {
        doLast {
            println "weaving cats into the java part of the app..."
            weaveCats(classpath.asPath,
                      classpath.asPath,
                      destinationDir.toString())
        }
    }
}

This part seems to be working fine. However, I added a bit of Kotlin code into my app and this didn’t work for Kotlin. The problem appears to consist of the following parts:

  • Java is compiled by task compileDebugJavaWithJavac
  • Kotlin is compiled by task compileDebugKotlin
  • gradle can run either one of these tasks, or both of them
  • compileDebugJavaWithJavac depends on compileDebugKotlin
  • weaving Kotlin requires Java classes.

So if valid Java classes exist, this code works:

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) {
    if (name == "compileDebugKotlin") {
        doLast {
            println "weaving cats into the kotlin part of the app..."
            weaveCats(classpath.asPath + File.pathSeparator + project.buildDir.path + "/intermediates/javac/debug/classes",
                      classpath.asPath,
                      destinationDir.toString())
        }
    }
}

The problem is when project.buildDir.path + "/intermediates/javac/debug/classes" doesn’t exist (or perhaps it could contain old classes?). I get a warning,

WARN: incorrect classpath: C:\Users\oakkitten\StudioProjects\myapp\app\build\intermediates\javac\debug\classes

and a bunch of errors like

ERROR: can’t determine modifiers of missing type mapp.Foo.Bar

I guess I should postpone execution of the weaving Kotlin until compileDebugJavaWithJavac is run, but compileDebugJavaWithJavac might be up-to-date and never run!

How can I resolve this?

Also, is there a way to not hardcode "/intermediates/javac/debug/classes" by chance?

So I found out that gradle.taskGraph.afterTask gets run regardless of whether the task is executed or not. The following code seems to be doing the job, then

ext.javaPath = null
ext.kotlinPath = null
ext.classPath = null

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) {
    if (name == "compileDebugKotlin") {
        doLast {
            println "scheduling kotlin weaving..."
            kotlinPath = destinationDir.toString()
            classPath = classpath.asPath
        }
    }
}

tasks.withType(JavaCompile) {
    if (name == "compileDebugJavaWithJavac") {
        doLast {
            println "scheduling java weaving..."
            javaPath = destinationDir.toString()
            classPath = classpath.asPath
        }
    }
}

gradle.taskGraph.afterTask { Task task, TaskState state ->
    if (task.name == "compileDebugJavaWithJavac" && !state.failure) {
        if (kotlinPath) {
            println "weaving cats into the kotlin part of the app..."
            if (!javaPath) javaPath = project.buildDir.path + "/intermediates/javac/debug/classes"
            weaveCats(classPath + File.pathSeparator + javaPath, classPath, kotlinPath)
        }
        if (javaPath) {
            println "weaving cats into the java part of the app..."
            weaveCats(classPath, classPath, javaPath)
        }
    }
}

Although the side effect is that weaving is happening outside of the task and due to the fact that it’s modifying the sources in place the tasks are never up to date. With the Java-only code, I somehow could get away without recompiling.

…I think I’ve found a perfect solution for Android. I used Transform to easily create another task for weaving; it works on merged classes and has its own inputs and outputs so this solves all issues.

Full answer on SO: https://stackoverflow.com/a/64673644/1449683