google / ksp

Kotlin Symbol Processing API
https://github.com/google/ksp
Apache License 2.0
2.8k stars 264 forks source link

Multiplatform example in IntelliJ IDEA: "Unresolved reference: Foo" #963

Open OliverO2 opened 2 years ago

OliverO2 commented 2 years ago

I have experimented with KSP's examples/multiplatform with IntelliJ IDEA 2022.1, commenting out androidNative* and mingwX64 targets in examples/multiplatform/workload/build.gradle.kts (sections kotlin and dependencies).

Being unaware of generated files, the IDE complains about examples/multiplatform/workload/src/linuxX64Main/kotlin/Main.kt:

Unresolved reference: Foo

This can be fixed by adding the following to kotlin.sourceSets:

        val commonMain by getting {
            kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
        }

However, then executing gradlew :workload:clean :workload:allTests produces Gradle warnings and compilation errors (excerpt):

> Task :workload:kspKotlinJs FAILED
e: Source file or directory not found: /home/oliver/Repositories/experimental.nobackup/ksp/examples/multiplatform/workload/build/generated/ksp/metadata/commonMain/kotlin/Foo.kt
> Task :workload:kspKotlinJvm FAILED
e: Source file or directory not found: /home/oliver/Repositories/experimental.nobackup/ksp/examples/multiplatform/workload/build/generated/ksp/metadata/commonMain/kotlin/Foo.kt
> Task :workload:kspCommonMainKotlinMetadata
Execution optimizations have been disabled for task ':workload:kspCommonMainKotlinMetadata' to ensure correctness due to the following reasons:
  - Gradle detected a problem with the following location: '/home/oliver/Repositories/experimental.nobackup/ksp/examples/multiplatform/workload/build/generated/ksp/metadata/commonMain'. Reason: Task ':workload:kspKotlinJs' uses this output of task ':workload:kspCommonMainKotlinMetadata' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed. Please refer to https://docs.gradle.org/7.2/userguide/validation_problems.html#implicit_dependency for more details about this problem.
  - Gradle detected a problem with the following location: '/home/oliver/Repositories/experimental.nobackup/ksp/examples/multiplatform/workload/build/generated/ksp/metadata/commonMain'. Reason: Task ':workload:kspKotlinJvm' uses this output of task ':workload:kspCommonMainKotlinMetadata' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed. Please refer to https://docs.gradle.org/7.2/userguide/validation_problems.html#implicit_dependency for more details about this problem.
  - Gradle detected a problem with the following location: '/home/oliver/Repositories/experimental.nobackup/ksp/examples/multiplatform/workload/build/generated/ksp/metadata/commonMain/kotlin'. Reason: Task ':workload:kspKotlinJs' uses this output of task ':workload:kspCommonMainKotlinMetadata' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed. Please refer to https://docs.gradle.org/7.2/userguide/validation_problems.html#implicit_dependency for more details about this problem.
  - Gradle detected a problem with the following location: '/home/oliver/Repositories/experimental.nobackup/ksp/examples/multiplatform/workload/build/generated/ksp/metadata/commonMain/kotlin'. Reason: Task ':workload:kspKotlinJvm' uses this output of task ':workload:kspCommonMainKotlinMetadata' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed. Please refer to https://docs.gradle.org/7.2/userguide/validation_problems.html#implicit_dependency for more details about this problem.
w: [ksp] [Bar.kt, Baz.kt]
w: [ksp] [Bar.kt, Baz.kt, Foo.kt]
> Task :workload:kspKotlinLinuxX64
Execution optimizations have been disabled for task ':workload:kspKotlinLinuxX64' to ensure correctness due to the following reasons:
  - Gradle detected a problem with the following location: '/home/oliver/Repositories/experimental.nobackup/ksp/examples/multiplatform/workload/build/generated/ksp/metadata/commonMain/kotlin'. Reason: Task ':workload:kspKotlinLinuxX64' uses this output of task ':workload:kspCommonMainKotlinMetadata' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed. Please refer to https://docs.gradle.org/7.2/userguide/validation_problems.html#implicit_dependency for more details about this problem.
w: [ksp] [Bar.kt, Baz.kt, Main.kt]
w: [ksp] [Bar.kt, Baz.kt, Main.kt, Foo.kt]
FAILURE: Build completed with 2 failures.

NOTE: Trying to fix the Gradle warnings with the idea plugin's module configuration as suggested in KSP quickstart appears to have no effect with multiplatform.

The Gradle warnings can be fixed by adding the required dependencies:

afterEvaluate {  // WORKAROUND: both register() and named() fail – https://github.com/gradle/gradle/issues/9331
    tasks {
        withType<org.jetbrains.kotlin.gradle.dsl.KotlinCompile<*>> {
            if (name != "kspCommonMainKotlinMetadata")
                dependsOn("kspCommonMainKotlinMetadata")
        }
    }
}

Now, executing gradlew :workload:clean :workload:allTests proceeds without the Gradle warnings, but still exhibits compilation errors (excerpt):

> Task :workload:compileKotlinLinuxX64 FAILED
e: /home/oliver/Repositories/experimental.nobackup/ksp/examples/multiplatform/workload/build/generated/ksp/linuxX64/linuxX64Main/kotlin/Foo.kt: (3, 7): Redeclaration: Foo
e: /home/oliver/Repositories/experimental.nobackup/ksp/examples/multiplatform/workload/build/generated/ksp/metadata/commonMain/kotlin/Foo.kt: (3, 7): Redeclaration: Foo
> Task :workload:compileKotlinJs FAILED
e: /home/oliver/Repositories/experimental.nobackup/ksp/examples/multiplatform/workload/build/generated/ksp/js/jsMain/kotlin/Foo.kt: (3, 7): Redeclaration: Foo
e: /home/oliver/Repositories/experimental.nobackup/ksp/examples/multiplatform/workload/build/generated/ksp/metadata/commonMain/kotlin/Foo.kt: (3, 7): Redeclaration: Foo
FAILURE: Build completed with 2 failures.

This can be fixed by making KSP stop generating the same class multiple times (in commonMain as well as per target), reducing the dependencies section to:

dependencies {
    add("kspCommonMainMetadata", project(":test-processor"))
}

Now gradlew :workload:clean :workload:allTests completes successfully. And IntelliJ IDEA is happy as well.

Could this be integrated into the example and documentation be added to KSP with Kotlin Multiplatform?

dilraj-singh1997 commented 2 years ago

If anyone looking for the same error in android studio, check this once- https://github.com/google/ksp/issues/37#issuecomment-1047366011. I guess the same principle can be extended for KMM.

flaringapp commented 1 year ago

This issue is still there. Couple of clarifications though:

  1. 'Workaround' compiles fine without afterEvaluate
  2. KotlinCompile task is responsible for compiling JVM only. If you compile iOS as well, consider adding the same 'workaround' with KotlinNativeCompile class. I bet the same thing can be done for other platforms.
  3. Removing ksp processor from all configurations except kspCommonMainMetadata means that no code will be generated from shared module source sets except common. E.g., in KMM project androidMain and iosMain won't be able to generate any code.
bcmedeiros commented 1 year ago

If all you care about is generating code for the commonMain source set, You can do the following:

plugins {
    kotlin("multiplatform")
    id("com.google.devtools.ksp")
}

kotlin {
    linuxArm64()
    linuxX64()
    macosArm64()
    macosX64()
    jvm()
    iosX64()
    iosArm64()
    iosSimulatorArm64()
    // ...

    sourceSets {
        val commonMain by getting {
            kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
            // ...
        }
        val commonTest by getting {
            // ...
        }
    }

    // following example here:
    // https://github.com/OliverO2/kotlin-multiplatform-ksp/blob/main/base/build.gradle.kts
    dependencies {
        // Provide symbol processing for each Kotlin '*Main' source set.
        kotlin.sourceSets.forEach { sourceSet ->
            val kspConfiguration = when {
                sourceSet.name == "commonMain" -> "kspCommonMainMetadata"
                // but skip configurations for each platform-specific source set
                // sourceSet.name.endsWith("Main") -> "ksp${sourceSet.name.substringBefore("Main").replaceFirstChar { it.titlecase() }}"
                else -> null
            }
            if (kspConfiguration != null) add(kspConfiguration, project(":gameplay-protocol-converter-processor"))
        }
    }
}

// Fix KSP task dependencies (https://github.com/google/ksp/issues/963)
afterEvaluate {
    tasks {
        val kspCommonMainKotlinMetadata by getting
        withType<org.jetbrains.kotlin.gradle.dsl.KotlinCompile<*>> {
            if (this !== kspCommonMainKotlinMetadata) {
                dependsOn(kspCommonMainKotlinMetadata)
            }
        }
    }
}

This makes all platforms work and IntelliJ recognizes all the generated source files.

bcmedeiros commented 12 months ago

This issue is really bizarre... I just updated to Kotlin 1.9.10, and Gradle started saying that the task kspCommonMainKotlinMetadata didn't exist, but if you println all the tasks names, it obviously exists. I had to change my afterEvaluate to the below, but I have no idea why it works, nor how safe this is.

afterEvaluate {
    val compileTasks = CopyOnWriteArrayList<org.jetbrains.kotlin.gradle.dsl.KotlinCompile<*>>()
    tasks {
        withType<org.jetbrains.kotlin.gradle.dsl.KotlinCompile<*>> {
            if (name != "kspCommonMainKotlinMetadata") {
                compileTasks.add(this)
            } else {
                compileTasks.forEach { t ->
                    t.dependsOn(this)
                }
                compileTasks.clear()
            }
        }
    }
}
supenkwibowo commented 10 months ago

I also got the same problem after updating to Kotlin 1.9.10. The interesting thing is that while trying to get kspCommonMainKotlinMetadata in afterEvaluate is not working, trying to do it after all projects evaluated in gradle.projectsEvaluated works.

gradle.projectsEvaluated {
    tasks {
        val kspCommonMainKotlinMetadata by getting
        withType<org.jetbrains.kotlin.gradle.dsl.KotlinCompile<*>> {
            if (this !== kspCommonMainKotlinMetadata) {
                dependsOn(kspCommonMainKotlinMetadata)
            }
        }
    }
}
danilla commented 9 months ago

What I don't quite understand is if there is planned support for commonMain source set in IntelliJ idea out of the box. I mean, in the same way as currently e.g. jvmMain and jsMain source sets are supported.

In this issue, various gradle tricks are described which allow to reference generated code from user's common source sets. Then, there is advice on how to fix things when those tricks no longer work in newer versions.

But is it planned that generated code can be referenced from common source sets out of the box? Maybe it's not in this but in another issue? Thanks!

OliverO2 commented 9 months ago

I haven't tracked KSP for a while, but once upon a time I had collected all sorts of interconnected issues regarding source set support in KSP. It did not go well... Maybe you could start here to get an impression: https://github.com/google/ksp/pull/1021#issuecomment-1212298132

Lysander commented 7 months ago

We have found a quite simple solution for all the trouble with KSP and Multiplatform-Projects (including publishing - which started to break the known workarounds with gradle 8.x):

// KSP support for Lens generation - our KSP based Code-Generator, replace with your own!
dependencies.kspCommonMainMetadata(project(":lenses-annotation-processor"))

// solves all implicit dependency trouble and IDEs source code detection
kotlin.sourceSets.commonMain { tasks.withType<KspTaskMetadata> { kotlin.srcDir(destinationDirectory) } }

As mentioned in the gradle docs, which are presented on such missing dependencies, a property carries dependency information, whereas simple path-Strings do not!

All trouble with build and publish (!) tasks simply went away - should be added to the documentation of KSP imho!

I hope this helps someone who also get into such trouble as we did...

jamesrapadmi commented 2 months ago

I have this issue as well. None of the workarounds in this thread work for me so far

Lysander commented 2 months ago

I have this issue as well. None of the workarounds in this thread work for me so far

Which Kotlin and Gradle version do you use?

I have tested my proposal not yet with 2.0! Might be possible some things have changed there...

jamesrapadmi commented 2 months ago

Yeah this is with kotlin 2.0 and Gradle 8.7

bcmedeiros commented 2 weeks ago
// KSP support for Lens generation - our KSP based Code-Generator, replace with your own!
dependencies.kspCommonMainMetadata(project(":lenses-annotation-processor"))

// solves all implicit dependency trouble and IDEs source code detection
kotlin.sourceSets.commonMain { tasks.withType<KspTaskMetadata> { kotlin.srcDir(destinationDirectory) } }

This is good stuff, @Lysander, thanks for that. I'm posting my version below which is slightly different in terms of positioning the 2 pieces of config, maybe it will help someone.

import com.google.devtools.ksp.gradle.KspTaskMetadata

plugins {
    kotlin("multiplatform")
    id("multiplatform-module-standard")
    id("com.google.devtools.ksp")
}

kotlin {
    linuxArm64()
    linuxX64()
    macosArm64()
    macosX64()

    sourceSets {
        commonMain {
            // (2) above
            tasks.withType<KspTaskMetadata> { kotlin.srcDir(destinationDirectory) }
            dependencies {
                api(project(":gameplay-protocol-converter-annotation"))
                api(project(":domain"))
                api(project(":gameplay-protocol"))
            }
        }
        commonTest {
            dependencies {
                implementation(kotlin("test"))
            }
        }
    }
}

// (1) above
dependencies {
    kspCommonMainMetadata(project(":gameplay-protocol-converter-processor"))
}
udev commented 2 weeks ago

@bcmedeiros Thank you for this. I've been struggling to migrate a project to k2 and this helped with figuring out Koin ksp generation. Not sure if their instructions are just out of date but what you provided works perfectly.