I am working on fixing multiplatform issues with t...
# ksp
o
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):
Copy 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:
Copy code
fun KotlinSourceSet.withKsp(dependencyNotation: Any)
Note:
KotlinSourceSet
is not even extension-aware. Is this at all possible? Any ideas?
f
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
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
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
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:
Copy code
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.
Copy code
sourceSets {
    val commonMain by getting {
        ksp {
            processor(project(":test-processor"))
            arg("option1", "value1")
            allWarningsAsErrors = true
        }
    }
}
n
Making
KotlinSourceSet
extension aware looks like a legitimate feature request
f
Is there a reason for doing ksp { processor () } instead of ksp.processor() other than a personal preference?
o
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
but the
KspExtension
receiver is given explicitly when you do
ksp.processor()
o
Yes. And then, how do you bring another receiver (
KotlinSourceSet
) in scope so that we have both?
r
Sounds like a job for context receivers! (But not yet probably)
1
n
Either way to properly support groovy, AFAIU the only correct solution here is for
KotlinSourceSet
to be extension aware.
1
a
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
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:
Copy code
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
That makes sense, thanks for the explanation!
🙂 1
e
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
Maybe if you wanted to do something like?
Copy code
@MyAnnotation
expect fun something()
Not sure if that’s an actual use case, just speculating.
e
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
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
The behavior for
dependencies {…}
should be unchanged.
👍 1
m
After following the discussions on the PR this far...I really want to say: Thank you @Oliver.O!