Code generation in Kotlin MPP with gradle

Another topic on my favorite subject Gradle Must Die

I need to do a quite trivial thing: a purely code generated module. I.e.:

  • there is a tool (a class with main method), written in Kotlin and deployed to maven repo as someToolLib
  • there is also a couple of other kotlin libraries
  • before any kotlin MPP compilation, a tool must be executed, with a libraryB on its classpath, to code-generate some kotlin common sources, which then have to be processed by kotlin MPP with additional dependencies.

I have spent almost a day, trying to understand what needs to be configured inside gradle to achieve this :frowning: (The similar things always worked for me like a charm with Maven)

Does anybody know, what needs to be fixed in buildscript example below to make it work?

import org.jetbrains.kotlin.gradle.dsl.KotlinCompile

val generatedKotlinSourcesDir = "$buildDir/generated/sources/kotlin"

kotlin {
    sourceSets {
        commonMain {
            kotlin.srcDir(generatedKotlinSourcesDir)

            dependencies {
                api("somegroup:someLibA:0.0-SNAPSHOT")
                api("somegroup:someLibB:0.0-SNAPSHOT")
            }
        }
        jvmMain {
            dependencies {
                runtimeOnly("somegroup:someToolLib:0.0-SNAPSHOT")
            }
        }
    }
}

tasks.register("generateSourceCodeBeforeKotlinCompilation", JavaExec::class) {
    mainClass = "xxx.yyy.MainClass"
    val runtimeClasspath = configurations["jvmRuntimeClasspath"] + kotlin.targets["jvm"].compilations["main"].output.allOutputs //+ kotlin.targets["jvm"].compilations["main"].runtimeDependencyFiles!!
    classpath = runtimeClasspath //sourceSets["commonMain"].runtimeClasspath
    args = listOf(generatedKotlinSourcesDir, "someArgument")

    println("Code generation classpath:\n  ${runtimeClasspath.joinToString("\n  ")}")
}

tasks.withType(KotlinCompile::class.java).configureEach {
    dependsOn("generateSourceCodeBeforeKotlinCompilation")
}

Because right now it fails with:

Circular dependency between the following tasks:

:xyz:compileKotlinJvm
\--- :xyz:generateSourceCodeBeforeKotlinCompilation
     +--- :xyz:compileKotlinJvm (*)
     \--- :xyz:jvmMainClasses
          \--- :xyz:compileKotlinJvm (*)

Wait, do you mean to run a class from project X, which responsibility is to generate the source code of project X? Sounds like a chicken-egg problem to me. For me it makes much more sense to separate the generator code to another module, no matter the build tool.

edit:
Ahh, sorry, xxx.yyy.MainClass is inside one of included libs, correct?

Yes, xxx.yyy.MainClass is inside one of included libs, correct?

–correct

If I make build.dependsOn(“codeGenerationTask”) then everything works, except Kotlin compilation happens BEFORE code generation and geneared code is not compiled :frowning:

This is far from anything I ever did in Gradle, but I suspect the problem is here:

val runtimeClasspath = configurations["jvmRuntimeClasspath"] + kotlin.targets["jvm"].compilations["main"].output.allOutputs //+ kotlin.targets["jvm"].compilations["main"].runtimeDependencyFiles!!

You basically say that for running the source generation you need the classpath of the current module, which includes its own sources. This is a chicken-egg. We should probably put only the dependencies into the classpath. However, I don’t know how to do it and I admit I’m making educated guesses here.

I am pretty sure that what you described is the reason, BUT:

Why configuration part, that just declares dependencies of the module, produces circular dependency between execution tasks, which must be absolutely independent???

This is one those weird things about gradle :frowning: It is very badly designed. I hope this damned tool will sink in oblivion as it deserves.

This pattern (and even more comnplicated, when tool is part of multi-module project), works perfectly fine with maven, because maven is designed properly.

I don’t know :slight_smile: For me it makes perfect sense that if I put e.g. module A in a classpath of module B, then A has to be built before B, because otherwise that classpath dependency wouldn’t make any sense. But I honestly don’t know if this is what’s happening here and if this is how Gradle manages task dependencies.

I would try jvmCompileClasspath instead of jvmRuntimeClasspath, because I assume when compiling we need dependencies and when running: dependencies + self code. But I would also probably start with a simple Java module and only when succeeded, move to MPP, because that common-jvm separation also adds complexity and makes debugging harder.

1 Like

I tried jvmCompileClasspath

It worked somehow, thank you. Though I had to change the type of dependency on the toollib to ‘compileOnly’, from ‘runtimeOnly’ and it stopped pulling transitive dependencies of the tool library, so I had manually include them :frowning:

Thank you for idea, @broot

1 Like