Check that dependencies are declared consistently at source set level
Uklib spec prohibits uklibs from filtering their dependencies and
assumes that uklibs are going to be published in a world of library to
library dependencies. To simulate this in KGP we check that dependencies
are declared consistently across compilations that are going to be
published as Uklib fragments.
This check is inaccurate. For example platform and metadata compilations
might resolve to different versions of the same dependency.
^KT-74005
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/kotlinDomApiDependencyManagement.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/kotlinDomApiDependencyManagement.kt
index d14464d..d86fa5c 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/kotlinDomApiDependencyManagement.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/internal/kotlinDomApiDependencyManagement.kt
@@ -20,7 +20,7 @@
import org.jetbrains.kotlin.gradle.targets.js.npm.SemVer
import org.jetbrains.kotlin.gradle.utils.forAllTargets
-private const val KOTLIN_DOM_API_MODULE_NAME = "kotlin-dom-api-compat"
+internal const val KOTLIN_DOM_API_MODULE_NAME = "kotlin-dom-api-compat"
private val Dependency.isKotlinDomApiDependency: Boolean
get() = group == KOTLIN_MODULE_GROUP && (name == KOTLIN_DOM_API_MODULE_NAME)
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/KotlinToolingDiagnostics.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/KotlinToolingDiagnostics.kt
index 27a1f3e..c789d13 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/KotlinToolingDiagnostics.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/KotlinToolingDiagnostics.kt
@@ -6,6 +6,9 @@
package org.jetbrains.kotlin.gradle.plugin.diagnostics
import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.Dependency
+import org.gradle.internal.extensions.stdlib.capitalized
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.InternalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.*
@@ -25,7 +28,7 @@
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_NATIVE_IGNORE_DISABLED_TARGETS
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_NATIVE_SUPPRESS_EXPERIMENTAL_ARTIFACTS_DSL_WARNING
import org.jetbrains.kotlin.gradle.plugin.diagnostics.ToolingDiagnostic.Severity.*
-import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resolve.KotlinTargetResourcesResolution
+import org.jetbrains.kotlin.gradle.plugin.mpp.uklibs.diagnostics.UklibFromKGPSourceSetsDependenciesChecker
import org.jetbrains.kotlin.gradle.plugin.sources.android.multiplatformAndroidSourceSetLayoutV1
import org.jetbrains.kotlin.gradle.plugin.sources.android.multiplatformAndroidSourceSetLayoutV2
import org.jetbrains.kotlin.gradle.utils.prettyName
@@ -92,6 +95,40 @@
}
}
+ internal object UklibInconsistentDependencyDeclarationViolation : ToolingDiagnosticFactory(ERROR, DiagnosticGroups.KGP.Misconfiguration) {
+ operator fun invoke(violations: List<UklibFromKGPSourceSetsDependenciesChecker.DependencyDeclarationViolation>) = build {
+ title("UklibInconsistentDependencyDeclarationViolation")
+ .description(
+ buildString {
+ append(
+ """
+ Uklibs only support consistent dependency declarations. Please declare all dependencies in the root source set:
+
+ kotlin {
+ sourceSets.commonMain {
+ dependencies {
+ ...
+ }
+ }
+ }
+
+ The following configurations declare unique dependencies:
+ """.trimIndent()
+ )
+
+ violations.forEach { violation ->
+ appendLine()
+ appendLine(violation.configuration)
+ violation.uniqueDependencies.map { it.toString() }.sorted().forEach {
+ appendLine(" $it")
+ }
+ }
+ }
+ )
+ .solution("???")
+ }
+ }
+
object DeprecatedKotlinNativeTargetsDiagnostic : ToolingDiagnosticFactory(ERROR, DiagnosticGroups.KGP.Misconfiguration) {
operator fun invoke(usedTargetIds: List<String>) = buildDiagnostic(
title = "Deprecated Kotlin/Native Targets",
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/uklibs/diagnostics/UklibFromKGPSourceSetsDependenciesChecker.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/uklibs/diagnostics/UklibFromKGPSourceSetsDependenciesChecker.kt
new file mode 100644
index 0000000..49cd454
--- /dev/null
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/uklibs/diagnostics/UklibFromKGPSourceSetsDependenciesChecker.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.gradle.plugin.mpp.uklibs.diagnostics
+
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.Dependency
+import org.jetbrains.kotlin.gradle.internal.KOTLIN_DOM_API_MODULE_NAME
+import org.jetbrains.kotlin.gradle.internal.KOTLIN_MODULE_GROUP
+import org.jetbrains.kotlin.gradle.internal.stdlibModules
+import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
+import org.jetbrains.kotlin.gradle.plugin.mpp.internal
+import org.jetbrains.kotlin.gradle.plugin.mpp.resolvableMetadataConfiguration
+import org.jetbrains.kotlin.gradle.plugin.sources.internal
+
+internal object UklibFromKGPSourceSetsDependenciesChecker {
+
+ data class DependencyDeclarationViolation(
+ val configuration: Configuration,
+ val uniqueDependencies: Set<Dependency>,
+ )
+
+ /**
+ * Check that dependencies are specified consistently in all compilations which are going to be published as if we already has library to
+ * library dependencies.
+ *
+ * This check unfortunately can't be accurate unless the entire graph of dependencies resolves to Uklibs, so we don't even try to resolve
+ * these configurations and only look at the declared dependencies.
+ */
+ fun findInconsistentDependencyDeclarations(
+ uklibPublishedPlatformCompilations: List<KotlinCompilation<*>>,
+ publishedMetadataCompilations: List<KotlinCompilation<*>>,
+ ): Set<DependencyDeclarationViolation> {
+ // FIXME: This filtering is not accurate if these are specified manually
+ val ignoreDependenciesInsertedByDefault = setOf(
+ KOTLIN_DOM_API_MODULE_NAME,
+ ) + stdlibModules
+
+ fun Configuration.declaredDependencies() = incoming.dependencies.filterNot {
+ it.group == KOTLIN_MODULE_GROUP && it.name in ignoreDependenciesInsertedByDefault
+ }.toSet()
+
+ val compilationDependencies = uklibPublishedPlatformCompilations.associate {
+ val conf = it.internal.configurations.compileDependencyConfiguration
+ conf to conf.declaredDependencies()
+ } + publishedMetadataCompilations.associate {
+ // For metadata compilations we always resolve resolvableMetadataConfiguration instead of the compileDependencyConfiguration
+ val conf = it.defaultSourceSet.internal.resolvableMetadataConfiguration
+ conf to conf.declaredDependencies()
+ }
+
+ // Ignore dependencies that are shared between all compilations
+ val sharedDependencies = compilationDependencies.values.first().toMutableSet()
+ compilationDependencies.values.forEach {
+ sharedDependencies.retainAll(it)
+ }
+ val violations = mutableListOf<DependencyDeclarationViolation>()
+ compilationDependencies.forEach {
+ val configuration = it.key
+ val dependencies = it.value
+
+ val uniqueDependencies = dependencies - sharedDependencies
+ if (uniqueDependencies.isNotEmpty()) {
+ violations.add(
+ DependencyDeclarationViolation(
+ configuration,
+ uniqueDependencies,
+ )
+ )
+ }
+ }
+ return violations.toSet()
+ }
+
+}
\ No newline at end of file
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/uklibs/publication/UklibFromKGPModel.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/uklibs/publication/UklibFromKGPModel.kt
index dd8bb7b..a793c32 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/uklibs/publication/UklibFromKGPModel.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/uklibs/publication/UklibFromKGPModel.kt
@@ -26,6 +26,7 @@
import org.jetbrains.kotlin.gradle.plugin.diagnostics.*
import org.jetbrains.kotlin.gradle.plugin.mpp.uklibs.UklibFragmentPlatformAttribute
import org.jetbrains.kotlin.gradle.plugin.mpp.uklibs.diagnostics.UklibFragmentsChecker
+import org.jetbrains.kotlin.gradle.plugin.mpp.uklibs.diagnostics.UklibFromKGPSourceSetsDependenciesChecker
import org.jetbrains.kotlin.gradle.plugin.mpp.uklibs.uklibFragmentPlatformAttribute
import java.io.File
@@ -124,6 +125,10 @@
*/
} else {
project.ensureSourceSetStructureIsUklibCompliant(allPublishedCompilations)
+ project.ensureDependenciesAreDeclaredConsistentlyAcrossAllSourceSets(
+ uklibPublishedPlatformCompilations = uklibPublishedPlatformCompilations,
+ publishedMetadataCompilations = publishedMetadataCompilations,
+ )
}
return fragments
@@ -139,6 +144,23 @@
}
}
+private fun Project.ensureDependenciesAreDeclaredConsistentlyAcrossAllSourceSets(
+ uklibPublishedPlatformCompilations: List<KotlinCompilation<*>>,
+ publishedMetadataCompilations: List<KotlinCompilation<*>>,
+) {
+ val violations = UklibFromKGPSourceSetsDependenciesChecker.findInconsistentDependencyDeclarations(
+ uklibPublishedPlatformCompilations = uklibPublishedPlatformCompilations,
+ publishedMetadataCompilations = publishedMetadataCompilations,
+ )
+ if (violations.isNotEmpty()) {
+ project.reportDiagnostic(
+ KotlinToolingDiagnostics.UklibInconsistentDependencyDeclarationViolation(
+ violations.toList()
+ )
+ )
+ }
+}
+
private fun kgpUklibFragment(
mainCompilation: KotlinCompilation<*>,
artifactProvidingTask: TaskProvider<*>,
diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/uklibs/UklibDependencyDeclarationViolations.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/uklibs/UklibDependencyDeclarationViolations.kt
new file mode 100644
index 0000000..13b6a50
--- /dev/null
+++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/uklibs/UklibDependencyDeclarationViolations.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.gradle.unitTests.uklibs
+
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.Dependency
+import org.jetbrains.kotlin.gradle.artifacts.publishedMetadataCompilations
+import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
+import org.jetbrains.kotlin.gradle.dsl.awaitMetadataTarget
+import org.jetbrains.kotlin.gradle.plugin.mpp.internal
+import org.jetbrains.kotlin.gradle.plugin.mpp.resolvableMetadataConfiguration
+import org.jetbrains.kotlin.gradle.plugin.mpp.uklibs.diagnostics.UklibFromKGPSourceSetsDependenciesChecker
+import org.jetbrains.kotlin.gradle.plugin.mpp.uklibs.publication.uklibPublishedPlatformCompilations
+import org.jetbrains.kotlin.gradle.plugin.sources.internal
+import org.jetbrains.kotlin.gradle.util.buildProjectWithMPP
+import org.jetbrains.kotlin.gradle.util.androidLibrary
+import org.jetbrains.kotlin.gradle.util.kotlin
+import org.jetbrains.kotlin.gradle.util.runLifecycleAwareTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class UklibDependencyDeclarationViolations {
+
+ @Test
+ fun `dependency specification validation - is not triggered when there are no metadata compilations`() {
+ runTest(
+ { emptySet() }
+ ) {
+ iosArm64()
+
+ sourceSets.commonMain.dependencies {
+ implementation("a:b:1.0")
+ }
+ sourceSets.iosArm64Main.dependencies {
+ implementation("c:d:1.0")
+ }
+ }
+ }
+
+ @Test
+ fun `dependency specification validation - with shared and differing dependencies across shared and platform source sets`() {
+ runTest(
+ {
+ setOf(
+ violation(
+ iosArm64().compilations.getByName("main").internal.configurations.compileDependencyConfiguration,
+ setOf(
+ project.dependencies.create("a:different_version:2.0"),
+ project.dependencies.create("a:platform:1.0"),
+ ),
+ )
+ )
+ }
+ ) {
+ iosArm64()
+ iosX64()
+
+ sourceSets.commonMain.dependencies {
+ // This dependency is propagated to all compilations
+ implementation("a:common:1.0")
+ // This dependency is shared across all compilations
+ implementation("a:shared:1.0")
+ // This dependency is specified again with a differing version
+ implementation("a:different_version:1.0")
+ }
+ sourceSets.iosArm64Main.dependencies {
+ implementation("a:shared:1.0")
+ implementation("a:different_version:2.0")
+ // This dependency is only specified in the platform compilation
+ implementation("a:platform:1.0")
+ }
+ }
+ }
+
+ @Test
+ fun `dependency specification validation - warns about missing dependency due to potentially filtered shared sources sets`() {
+ runTest(
+ {
+ setOf(
+ violation(
+ iosArm64().compilations.getByName("main").internal.configurations.compileDependencyConfiguration,
+ setOf(
+ project.dependencies.create("a:ios:1.0"),
+ ),
+ ),
+ violation(
+ iosX64().compilations.getByName("main").internal.configurations.compileDependencyConfiguration,
+ setOf(
+ project.dependencies.create("a:ios:1.0"),
+ ),
+ ),
+ violation(
+ sourceSets.appleMain.get().internal.resolvableMetadataConfiguration,
+ setOf(
+ project.dependencies.create("a:ios:1.0"),
+ ),
+ ),
+ violation(
+ sourceSets.iosMain.get().internal.resolvableMetadataConfiguration,
+ setOf(
+ project.dependencies.create("a:ios:1.0"),
+ ),
+ ),
+ )
+ }
+ ) {
+ iosArm64()
+ iosX64()
+
+ // The dependency is missing in potentially published fragments nativeMain and commonMain
+ sourceSets.appleMain.dependencies {
+ implementation("a:ios:1.0")
+ }
+ }
+ }
+
+ @Test
+ fun `dependency specification validation - when platform hierarchy is missing metadata compilations`() {
+ runTest(
+ {
+ setOf(
+ violation(
+ iosArm64().compilations.getByName("main").internal.configurations.compileDependencyConfiguration,
+ setOf(
+ project.dependencies.create("a:apple:1.0"),
+ ),
+ ),
+ violation(
+ linuxArm64().compilations.getByName("main").internal.configurations.compileDependencyConfiguration,
+ setOf(
+ project.dependencies.create("a:linux:1.0"),
+ ),
+ )
+ )
+ }
+ ) {
+ iosArm64()
+ linuxArm64()
+
+ sourceSets.commonMain.dependencies {
+ implementation("a:common:1.0")
+ }
+ sourceSets.appleMain.dependencies {
+ implementation("a:apple:1.0")
+ }
+ sourceSets.linuxMain.dependencies {
+ implementation("a:linux:1.0")
+ }
+ }
+ }
+
+ @Test
+ fun `dependency specification validation - don't emit for dependencies added by default`() {
+ runTest(
+ {
+ emptySet()
+ }
+ ) {
+ project.androidLibrary { compileSdk = 31 }
+
+ jvm()
+ iosArm64()
+ iosX64()
+ js()
+ wasmJs()
+ wasmWasi()
+ mingwX64()
+ androidTarget()
+
+ sourceSets.commonMain.dependencies {
+ implementation("a:common:1.0")
+ }
+ }
+ }
+
+ private fun violation(
+ configuration: Configuration,
+ uniqueDependencies: Set<Dependency>,
+ ): UklibFromKGPSourceSetsDependenciesChecker.DependencyDeclarationViolation = UklibFromKGPSourceSetsDependenciesChecker.DependencyDeclarationViolation(
+ configuration,
+ uniqueDependencies
+ )
+
+ private fun runTest(
+ expectedViolations: KotlinMultiplatformExtension.() -> Set<UklibFromKGPSourceSetsDependenciesChecker.DependencyDeclarationViolation>,
+ configure: KotlinMultiplatformExtension.() -> Unit,
+ ) {
+ buildProjectWithMPP {
+ kotlin {
+ configure()
+
+ runLifecycleAwareTest {
+ assertEquals(
+ expectedViolations(),
+ UklibFromKGPSourceSetsDependenciesChecker.findInconsistentDependencyDeclarations(
+ uklibPublishedPlatformCompilations(),
+ awaitMetadataTarget().publishedMetadataCompilations(),
+ )
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file