https://kotlinlang.org logo
Title
o

Oliver.O

06/03/2022, 7:34 PM
I am working on fixing multiplatform issues with the KSP Gradle plugin. Details in 🧵.
🎉 3
I have already created code which • wires KSP tasks with correct dependencies in line with HMPP source sets, • supports user-defined intermediate source sets (e.g. iosMain), • supports all source sets (e.g.
commonTest
), • auto-registers source directories, making the IDE happy. The code works in
build.gradle.kts
, now I'm trying to apply the fixes to the KSP Gradle plugin. If that's OK, I'd come up with a PR (which would initially provide the fixes for multiplatform only, as I do not currently use Android builds). There is one issue to be resolved: Ideally, I'd like to use a single point of configuration like so (which works nicely with build script code):
kotlin {
    // ...
    sourceSets {
        val commonMain by getting {
            withKsp(project(":test-processor"))
        }
}
To achieve that, I need to find a way for Gradle to load this extension function from the plugin into the buildscript classpath:
fun KotlinSourceSet.withKsp(dependencyNotation: Any)
Note:
KotlinSourceSet
is not even extension-aware. Is this at all possible? Any ideas?
f

Fudge

06/04/2022, 8:44 AM
Normally you add ksp dependencies by using the
ksp
configuration right? You should be able to iterate through all dependencies with the ksp configuration in afterEvaluate{}
o

Oliver.O

06/04/2022, 10:36 AM
With multiplatform, you use an
add
invocation inside a
dependencies
block. You have to use a ksp configuration name, which is a modified form of the sourceSet name and not fully documented. For users, it would be easier to just use a sourceSet name directly, ideally in
kotlin.sourceSets
, avoiding repetition. I already got everything working with the Gradle plugin. This is just a matter of offering the easiest to use DSL for configuration to the plugin’s users.
f

Fudge

06/05/2022, 8:49 AM
How about creating a gradle extension called
ksp
and giving it a method e.g.
processor
and then it's used like:
ksp.processor(project(...))
o

Oliver.O

06/05/2022, 1:06 PM
The
ksp
extension already exists and configures KSP globally. So
ksp.processor(...)
would configure a processor globally. Now I'd like to configure a source set-specific processor. Currently, I have implemented it this way:
sourceSets {
    val commonMain by getting {
        ksp {
            processor(project(":test-processor"))
        }
    }
}
Ideally,
KotlinSourceSet
would be extension-aware, so we could create
ksp
extensions for source sets. Unfortunately, this is not the case. So I defined the above
processor
in the
KspExtension
class as
fun KotlinSourceSet.processor(dependencyNotation: Any)
. That way, even though we are using the global
ksp
extension,
processor
can pick up the source set from the context via its receiver. While this is one level of indirection more than I had hoped for originally, it opens up a path for source set-specific options, e.g.
sourceSets {
    val commonMain by getting {
        ksp {
            processor(project(":test-processor"))
            arg("option1", "value1")
            allWarningsAsErrors = true
        }
    }
}
n

natario1

06/06/2022, 1:40 AM
Making
KotlinSourceSet
extension aware looks like a legitimate feature request
f

Fudge

06/06/2022, 7:59 PM
Is there a reason for doing ksp { processor () } instead of ksp.processor() other than a personal preference?
o

Oliver.O

06/06/2022, 8:43 PM
Yes,
ksp { processor(...) }
is mandatory within a source set block.
processor
needs to have two receivers in scope:
KspExtention
via
ksp {...}
and
KotlinSourceSet
via
getting {...}
. See the above definition of the
processor
extension function.
f

Fudge

06/09/2022, 6:27 PM
but the
KspExtension
receiver is given explicitly when you do
ksp.processor()
o

Oliver.O

06/09/2022, 6:32 PM
Yes. And then, how do you bring another receiver (
KotlinSourceSet
) in scope so that we have both?
r

russhwolf

06/09/2022, 6:33 PM
Sounds like a job for context receivers! (But not yet probably)
1
n

natario1

06/10/2022, 3:10 PM
Either way to properly support groovy, AFAIU the only correct solution here is for
KotlinSourceSet
to be extension aware.
1
o

Oliver.O

06/14/2022, 7:37 PM
The source set-based multiplatform configuration, including a number of fixes have just landed in a PR for KSP. Let's see how that goes.
🎉 2
:tnx: 3
👀 4
a

Alex Vanyo

06/14/2022, 8:30 PM
Out of curiosity, do you know how that PR impacts applying the same processor across multiple levels of source sets? For example, applying processor
A
to both
commonMain
and
jvmMain
. Assuming
A
does something simple, like generating code for a class with an annotation, right now (at least with my setup), everything from
commonMain
will be generated twice, which causes duplicate class issues.
Or I think put another way, would a processor applied to
jvmMain
only look at code in
jvmMain
?
o

Oliver.O

06/14/2022, 8:48 PM
Not really: KSP, just like the Kotlin compiler, needs to look at declarations across source set levels to resolve symbols properly. The above PR does not change the way files are processed by KSP across multiple levels of source sets. But nevertheless, there is some help. By enabling source set-specific configuration, the PR provides control on a source set level, e.g. via an option like so:
sourceSets {
        val commonMain by getting {
            ksp {
                processor(project(":test-processor"))
                arg("output", "commonMain")
            }
        }
        val jvmMain by getting {
            ksp {
                processor(project(":test-processor"))
                arg("output", "jvmMain")
            }
        }
    }
In the future, KSP hopefully could provide more information, e.g. each processed file's (input) source set. Combine this with the above
output
source set information, and you could filter out unwanted files. So TL;DR: The above is the first step towards a more convenient solution. Until then, you could use the workarounds mentioned in https://github.com/google/ksp/issues/965.
👍 1
a

Alex Vanyo

06/14/2022, 8:54 PM
That makes sense, thanks for the explanation!
🙂 1
e

evant

06/14/2022, 9:01 PM
In the future, KSP hopefully could provide more information, e.g. each
processed file's (input) source set. Combine this with the above
output
source set information, and you could filter out unwanted files.
To me it makes sense that this would be the default at least for say
getSymbolsWithAnnotation
unless there's usecases I'm missing?
a

Alex Vanyo

06/14/2022, 9:10 PM
Maybe if you wanted to do something like?
@MyAnnotation
expect fun something()
Not sure if that’s an actual use case, just speculating.
e

evant

06/14/2022, 9:12 PM
hm yeah there would have to be some special handling for expect/actual. My first thought is they'd have to be handled on the actual side of things. I would just like to get to a place where every processor doesn't have to implement the same sort of filtering.
👍 1
m

Matthias Geisler

06/14/2022, 9:32 PM
Great news!!!
May I ask something - will this also change the behaviour of
dependencies { add("ksp...") }
when the KMP Plugin is applied? Currently it throws an error when you for example try to register
kspClientTest
- is that preserved?
o

Oliver.O

06/18/2022, 12:45 PM
The behavior for
dependencies {…}
should be unchanged.
👍 1
m

Matthias Geisler

08/22/2022, 5:56 AM
After following the discussions on the PR this far...I really want to say: Thank you @Oliver.O!