I have a KMP library with a `compileOnly` dependen...
# gradle
j
I have a KMP library with a
compileOnly
dependency that allows the consumer to use one of a couple variations of a dependency by explicitly declaring which implementation to use. I'd like to test my project against both variations though. What's the best way to configure gradle to do this?
Copy code
kotlin {
    ...
    sourceSets {
        val commonMain by getting {
            dependencies {
                compileOnly("variantA")
            }
        }
        val commonTest by getting {
            dependencies {
                implementation("variantA") // would also like the same tests to run with "variantB" too
            }
        }
    }
}
e
the best way is not to use
compileOnly
, but rather model it properly with https://docs.gradle.org/current/userguide/feature_variants.html
however there's no KMP integration so you'd need to set up the variants manually
note that
compileOnly
isn't supported on KMP anyway, https://youtrack.jetbrains.com/issue/KT-46760
j
Thanks. Yes, I've seen conflicting info about
compileOnly
working with Kotlin/Native. This issue implies it should work if the consumer disables gradle caches. I've been finding it definitely doesn't work like with the JVM though. Most recently I'm finding the
compileOnly
dependency is actually available at runtime (at least in tests even after I remove the test implementation). So yes, I think I'm going to have to avoid this route and publish separate variants explicitly. What I'm trying to do is prevent the need for cascading variants with combinations of my base library, which has two variants, and supplemental extensions to the libraries. I'd prefer not to bundle all of them into the same library because some of the features in the extensions are optional and bring in other dependencies. But if this can be done with a slight modification to a project build.gradle.kts by creating another variant publication with a different dependency, this would work better. Do you know of a good example of doing this manually in a KMP project? And what about cocoapods dependencies for iOS/macOS? Is it possible to use a different cocoapods dependency for a variant in the same way as Kotlin dependencies? My base library variants each require a different cocoapods dependency.
e
did a little experiment, this seems to sorta work (including adding different dependencies to different sourcesets): publisher
Copy code
kotlin {
    for (target in listOf(macosArm64("macosarm64basic"), macosX64("macosx64basic"), macosArm64("macosarm64extra"), macosX64("macosx64extra"))) {
        configurations.matching { it.name.startsWith(target.name) }.configureEach {
            outgoing {
                // Register the same capability in all variants so that downstream consumers will reject using multiple different variants at the same time
                capability("$group:${project.name}:$version")
                // Add a unique capability to each variant so that they can be published distinctly
                capability("$group:${project.name}-${target.name}:$version")
consumer
Copy code
kotlin {
    macosArm64()
    sourceSets {
        getByName("macosArm64Main") {
            dependencies {
                // Choose one or the other; Gradle will fail if both are used
                implementation("my.group:name-macosarm64basic:version")
                implementation("my.group:name-macosarm64extra:version")
it doesn't really seem to play well with other stuff that the Kotlin Gradle Plugin wants to do though
I think the safer thing to do would be to just publish multiple libraries
at least until kotlin multiplatform supports feature variants like the gradle java plugin does (although I don't see a feature request for it, so maybe there's not enough demand for it to happen)
j
That's interesting. Thanks for experimenting with this. To describe my use case a bit more, both variants of my base library provide the same base API, while the second variant adds additional enterprise APIs to the first variant. Each variant depends on a different dependency artifact, either the CE or EE variant. The base API code works with either dependency variant. I'm doing this currently with two separate modules, where the EE variant module symlinks the code from the first module, and then adds its own
srcDir
for the supplemental code. The supplemental code needs access to
internal
APIs, which is why I have to symlink the base variant's source files, rather than just depending on the module. If I could create both variants within the same module, where one uses the base
srcDir
and the other uses both `srcDir`s, as well as changing the dependencies for each, this would work better. The symlinked `srcDir`s are only a problem in that all the files are indexed twice and occasionally experience false merge conflicts from find/replace changes.
e
you can access
internal
code across modules with
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
but a better option would be to use your own
@RequiresOptIn
annotation
j
That's true. I actually had to use this workaround just yesterday when I ran into this Gradle 8 bug with Dokka (the workaround is in Groovy, where
internal
is
public
).
I'm sure I'd also need to inject the gradle module metadata to properly exclude the CE dependency variant from the EE module if it were to depend on the CE module directly, with something similar to what you shared here.
e
a plain old exclude (if it works on native, haven't tried) would be better for exporting to consumers
but given the whole closed-world code model of native, doing things like that feel pretty sketchy to me
j
That would be the question, as native dependencies often don't behave like JVM.
In addition to my base library, I have a couple extension libraries that each depend on the base library variants to publish two variants of their own. The extension libraries don't have differing code between the CE and EE variants, only the dependency on the base library differs. But I still have them split into two modules each with only the build.gradle.kts file differing to change the dependency and artifact name, while symlinking the
src
again. I might be able to rework these extension library modules with similar code to what you tried above, assuming I publish exclusive capabilities for the base library variants.
Which would bring me back to my initial question. Once this is set up this way, how do tests work to run them against both variants?
e
create multiple targets with different test dependencies
getting this far though, I'd really re-consider the approach
IMO it would be more reasonable to have your own
wrapper-api
that your public code builds against, and
wrapper-ce
and
wrapper-ee
libraries that are only needed at runtime to implement
wrapper-api
the API needs to be able to inject the wrapper implementation but this way doesn't require dealing with any of the growing set of issues discussed here
j
Interesting to think about, if my code could be adapted to fit that architecture. EE is a superset of CE's API, both consisting of classes, interfaces, enums, lambdas, etc. Some of EE's API adds functions and properties to classes defined in CE. My EE implementation does this via Kotlin extension functions/properties. Much of the API is implemented with expect/actual code where wrapper implementations call into the platform-specific base library dependency. Depending on the platform, the API is similar to my KMP public API (or even typealiased when possible from the Java dependency), although the native Linux and Windows actual implementations can differ quite a bit utilizing a native C SDK.
As I've thought about this more, I think the real blocker to making any changes to the current project structure is the ability to replace a cocoapods ObjC interop library from another module with a different variant. The same for Linux and Windows native C interop libraries too. Something similar to these mechanisms for Kotlin dependencies would be required.
140 Views