Is there an implicit hardcoded magic dependency of...
# multiplatform
r
Is there an implicit hardcoded magic dependency of all sourceSets on commonMain? I'm writing a course on how to configure a multiplatform project and I have just this as my starting point:
Copy code
kotlin {
    jvm()
}
Then I put a file in commonMain and can use it fine from jvmMain. Why? I even confirmed there is no dependency set:
Copy code
afterEvaluate {
    kotlin.sourceSets
        .filterNot { it.name.endsWith("Test") }
        .forEach { println("${it.name} [${it.dependsOn.joinToString { it.name }}]") }
}
Copy code
> Configure project :
commonMain []
jvmMain []
How does it work? I was expecting to need to call
applyDefaultHierarchyTemplate()
, which does produce the expected result.
Copy code
> Configure project :
commonMain []
jvmMain [commonMain]
The documentation states that this dependency exists, but why is it not in
dependsOn
? Where is it defined?
m
The
jvm
,
ios
,
android
, etc, automatically adds the dependency on
commonMain
even without the default hierarchy template.
r
Well not really. Not an explicit dependency. I think I just learned implicit dependencies were a thing. It's the same for test source sets, they don't explicitly depend on their main counterpart. I wonder where those dependencies are defined then
Copy code
> Configure project :
commonMain []
commonTest []
jvmMain []
jvmTest []
linuxArm64Main []
linuxArm64Test []
linuxX64Main []
linuxX64Test []
macosArm64Main []
macosArm64Test []
macosX64Main []
macosX64Test []
mingwX64Main []
mingwX64Test []
z
The default hierarchy template is used without having to apply it since 1.9.20. You only need to use
applyDefaultHierarchyTemplate
if you'll add additional source sets to the project.
r
When is it applied then? If I don't call
applyDefaultHierarchyTemplate
I don't see intermediate source sets in
kotlin.sourceSets
a
I've answered similar question once. https://kotlinlang.slack.com/archives/C19FD9681/p1709750624633759?thread_ts=1709052445.136479&cid=C19FD9681 TL;DR: there is no guarantee that Kotlin Gradle Plugin configuration state is final in any
afterEvaluate
block. Please let me know your use case, I'll try to help you.
r
My use case is to log the sourceSets for educational purpose. This is for people who basically use Gradle for the first time, so explaining when which code is executed is really not the topic yet. I decided to use a task instead of just a flying
afterEvaluate
block. Good opportunity to explain how to create a simple custom task. I doubt that this uncertainty will cause problem to anyone doing something serious, as they would probably use a task anyway, it's just a little unexpected.
a
I decided to use a task instead of just a flying
afterEvaluate
block. Good opportunity to explain how to create a simple custom task.
yes for your use case it is the best way to do. During task execution there is a guarantee that Configuration State is final.
👍 1
it's just a little unexpected.
thank you for the input anyway! I'll think on how we can improve the message that state is not final OR finding a way of finalizing state as early as possible.
r
Well I just found a limitation I think
Copy code
kotlin {

    jvm()

    linuxArm64()
    linuxX64()
    macosArm64()
    macosX64()
    mingwX64()

    targets.withType<KotlinNativeTarget> {
        binaries.executable()
    }

}

dependencies {
    "commonMainImplementation"(libs.bundles.common.main)
    "appleMainImplementation"(libs.bundles.apple.main)
    "jvmMainImplementation"(libs.bundles.jvm.main)
    "linuxMainImplementation"(libs.bundles.linux.main)
    "mingwMainImplementation"(libs.bundles.mingw.main)
}
With this configuration, the intermediate source sets do not seem to be defined before the
dependencies
block. It fails to find
"appleMainImplementation"
for example :
Copy code
org.gradle.api.artifacts.UnknownConfigurationException: Configuration with name 'appleMainImplementation' not found.
	at org.gradle.api.internal.artifacts.configurations.DefaultConfigurationContainer.createNotFoundException(DefaultConfigurationContainer.java:124)
	at org.gradle.api.internal.DefaultNamedDomainObjectCollection.getByName(DefaultNamedDomainObjectCollection.java:334)
	at org.gradle.api.internal.artifacts.configurations.DefaultConfigurationContainer.getByName(DefaultConfigurationContainer.java:114)
	at org.gradle.api.internal.artifacts.configurations.DefaultConfigurationContainer.getByName(DefaultConfigurationContainer.java:54)
	at org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler.addProvider(DefaultDependencyHandler.java:128)
	at org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler.addProvider(DefaultDependencyHandler.java:133)
	at org.gradle.kotlin.dsl.support.delegates.DependencyHandlerDelegate.addProvider(DependencyHandlerDelegate.kt:62)
	at org.gradle.kotlin.dsl.DependencyHandlerScope.invoke(DependencyHandlerScope.kt:596)
Calling
applyDefaultHierarchyTemplate()
works around this, but again it's not obvious when the source sets are available
a
for adding dependencies we recommend to do that in related Source Set configuration block:
Copy code
kotlin {
   sourceSets {
      commonMain {
         dependencies {
            api(...)
            implementation(...)
         }
      }
   }
}
also good to know that parent source set dependencies are propagated to the underlying children source sets. in your case it seems like it is enough to define only commonMain dependencies.
r
No, I have multiple different bundles with multiple different dependencies for each platform. How different is it to define dependencies in the dependencies block (I mean, apart from my current issue)? When using a single bundle per source set to define all dependencies, I think that it looks much cleaner the way I did it than using 3 levels of nested DSL calls. In my case I think I prefer to call
applyDefaultHierarchyTemplate()
and use the
dependencies
block unless there are other differences
a
We discourage using string-based APIs (when you know the entity by its name). But I can suggest you some way outs
r
I agree, I would prefer not to use Strings. But that's really a lot more verbose
Copy code
sourceSets {
        commonMain.dependencies {
            implementation(libs.bundles.common.main)
        }
        appleMain.dependencies {
            implementation(libs.bundles.apple.main)
        }
        jvmMain.dependencies {
            implementation(libs.bundles.jvm.main)
        }
        linuxMain.dependencies {
            implementation(libs.bundles.linux.main)
        }
        mingwMain.dependencies {
            implementation(libs.bundles.mingw.main)
        }
    }
a
If you want to add a dependencies by source set name you can use this trick:
Copy code
val dependencyBundles = mapOf("commonMain" to libs.bundles.commonMain, ...)

kotlin.sourceSets.configureEach {
   val bundle = dependencyBundles[name] ?: return@configureEach
   dependencies { implementation(bundle) }
}
And yes we are aware of verboseness of declaring dependencies via source set and we are going to work on improvements there.
r
Would be nice to just name bundles like
commonMain-implementation
in the standard
libs.versions.toml
and it would just work. I like your solution but it goes back to relying on Strings and I don't think it's appropriate in my case as I'm making educational material. It's not to my taste but I should use my last snippet I think