@@ -2,6 +2,14 @@ package org.jetbrains.compose
22
33import org.gradle.api.DefaultTask
44import org.gradle.api.Project
5+ import org.gradle.api.artifacts.ModuleVersionIdentifier
6+ import org.gradle.api.artifacts.component.ComponentIdentifier
7+ import org.gradle.api.artifacts.component.ComponentSelector
8+ import org.gradle.api.artifacts.component.ModuleComponentIdentifier
9+ import org.gradle.api.artifacts.component.ModuleComponentSelector
10+ import org.gradle.api.artifacts.component.ProjectComponentIdentifier
11+ import org.gradle.api.artifacts.component.ProjectComponentSelector
12+ import org.gradle.api.artifacts.result.ResolvedDependencyResult
513import org.gradle.api.artifacts.result.ResolvedComponentResult
614import org.gradle.api.provider.Property
715import org.gradle.api.provider.ProviderFactory
@@ -54,18 +62,32 @@ private fun KotlinTarget.configureRuntimeLibrariesCompatibilityCheck() {
5462 expectedVersion.set(composeVersion)
5563 projectPath.set(project.path)
5664 configurationName.set(runtimeDependencyConfigurationName)
57- runtimeDependencies.set(provider { config.incoming.resolutionResult.allComponents })
65+ allDependencies.set(
66+ provider {
67+ config.incoming.resolutionResult.allDependencies
68+ .filterIsInstance<ResolvedDependencyResult >()
69+ }
70+ )
5871 }
5972 compilation.compileTaskProvider.dependsOn(task)
6073 }
6174}
6275
6376internal abstract class RuntimeLibrariesCompatibilityCheck : DefaultTask () {
6477 private companion object {
65- val librariesForCheck = listOf (
78+ val composeLibrariesForCheck = setOf (
6679 " org.jetbrains.compose.foundation:foundation" ,
6780 " org.jetbrains.compose.ui:ui"
6881 )
82+ val skikoLibraryForCheck = " org.jetbrains.skiko:skiko"
83+
84+ private val majorMinorRegex = """ ^(\d+)\.(\d+)""" .toRegex()
85+ fun majorMinorVersion (version : String ): String {
86+ val match = majorMinorRegex.find(version) ? : return version
87+ val major = match.groupValues[1 ]
88+ val minor = match.groupValues[2 ]
89+ return " $major .$minor "
90+ }
6991 }
7092
7193 @get:Inject
@@ -81,7 +103,7 @@ internal abstract class RuntimeLibrariesCompatibilityCheck : DefaultTask() {
81103 abstract val configurationName: Property <String >
82104
83105 @get:Input
84- abstract val runtimeDependencies : SetProperty <ResolvedComponentResult >
106+ abstract val allDependencies : SetProperty <ResolvedDependencyResult >
85107
86108 init {
87109 onlyIf {
@@ -92,47 +114,98 @@ internal abstract class RuntimeLibrariesCompatibilityCheck : DefaultTask() {
92114 @TaskAction
93115 fun run () {
94116 val expectedRuntimeVersion = expectedVersion.get()
95- val foundLibs = runtimeDependencies.get().filter { component ->
96- component.moduleVersion?.let { lib -> lib.group + " :" + lib.name } in librariesForCheck
117+ val composeLibraries = allDependencies.get()
118+ .mapNotNull { it.selected.moduleVersion }
119+ .filter { lib -> " ${lib.group} :${lib.name} " in composeLibrariesForCheck }
120+ .distinctBy { lib -> " ${lib.group} :${lib.name} :${lib.version} " }
121+ val composeInconsistentVersions = composeLibraries.filter { lib ->
122+ lib.version != expectedRuntimeVersion
97123 }
98- val problems = foundLibs.mapNotNull { component ->
99- val module = component.moduleVersion ? : return @mapNotNull null
100- if (module.version == expectedRuntimeVersion) return @mapNotNull null
101- ProblemLibrary (module.group + " :" + module.name, module.version)
124+ if (composeInconsistentVersions.isNotEmpty()) {
125+ logger.warn(
126+ getComposeMessage(
127+ projectPath.get(),
128+ configurationName.get(),
129+ composeInconsistentVersions,
130+ expectedRuntimeVersion
131+ )
132+ )
102133 }
103134
104- if (problems.isNotEmpty()) {
135+ val skikoIncompatibleDependencyUsages = allDependencies.get().filter { dependency ->
136+ val requested = dependency.requested as ? ModuleComponentSelector ? : return @filter false
137+ val selected = dependency.selected.moduleVersion ? : return @filter false
138+ if (" ${requested.group} :${requested.module} " != skikoLibraryForCheck) return @filter false
139+ if (" ${selected.group} :${selected.name} " != skikoLibraryForCheck) return @filter false
140+ majorMinorVersion(requested.version) != majorMinorVersion(selected.version)
141+ }
142+ if (skikoIncompatibleDependencyUsages.isNotEmpty()) {
105143 logger.warn(
106- getMessage (
144+ getSkikoMessage (
107145 projectPath.get(),
108146 configurationName.get(),
109- problems,
110- expectedRuntimeVersion
147+ skikoIncompatibleDependencyUsages
111148 )
112149 )
113150 }
114151 }
115152
116- private data class ProblemLibrary (val name : String , val version : String )
117-
118- private fun getMessage (
153+ private fun getComposeMessage (
119154 projectName : String ,
120155 configurationName : String ,
121- problemLibs : List <ProblemLibrary >,
122- expectedVersion : String
156+ composeInconsistentVersions : List <ModuleVersionIdentifier >,
157+ expectedVersion : String ,
123158 ): String = buildString {
124159 appendLine(" w: Compose Multiplatform runtime dependencies' versions don't match with plugin version." )
125- problemLibs .forEach { lib ->
126- appendLine(" expected: '${lib .name} :$expectedVersion '" )
127- appendLine(" actual: '${lib. name} :${lib .version} '" )
160+ composeInconsistentVersions .forEach { library ->
161+ appendLine(" expected: '${library.group} : ${library .name} :$expectedVersion '" )
162+ appendLine(" actual: '${library.group} : ${library. name} :${library .version} '" )
128163 appendLine()
129164 }
165+ appendNoteAboutDependencyMismatch(projectName, configurationName)
166+ appendLine()
167+ appendLine(" Please update Compose Multiplatform Gradle plugin's version or align dependencies' versions to match the current plugin version." )
168+ }
169+
170+ private fun getSkikoMessage (
171+ projectName : String ,
172+ configurationName : String ,
173+ dependencyUsages : List <ResolvedDependencyResult >,
174+ ): String = buildString {
175+ appendLine(" w: Skiko dependencies' versions are incompatible." )
176+ dependencyUsages.forEach { usage ->
177+ val from = usage.from.moduleVersion
178+ val requested = usage.requested
179+ val selected = usage.selected.moduleVersion
180+ appendLine(" ${from.toModuleString()} " )
181+ appendLine(" \\ --- ${requested.toModuleString()} -> ${selected?.version} " )
182+ appendLine()
183+ }
184+ appendNoteAboutDependencyMismatch(projectName, configurationName)
185+ appendLine()
186+ appendLine(" Note: Skiko is considered implementation detail in Compose Multiplatform and might be incompatible across versions." )
187+ appendLine(" Please align Skiko dependencies to the same version. If possible, avoid direct Skiko references and use Compose APIs instead." )
188+ }
189+
190+ private fun StringBuilder.appendNoteAboutDependencyMismatch (
191+ projectName : String ,
192+ configurationName : String ,
193+ ) {
130194 appendLine(" This may lead to compilation errors or unexpected behavior at runtime." )
131195 appendLine(" Such version mismatch might be caused by dependency constraints in one of the included libraries." )
132196 val taskName = if (projectName.isNotEmpty() && ! projectName.endsWith(" :" )) " $projectName :dependencies" else " ${projectName} dependencies"
133197 appendLine(" You can inspect resulted dependencies tree via `./gradlew $taskName --configuration ${configurationName} `." )
134198 appendLine(" See more details in Gradle documentation: https://docs.gradle.org/current/userguide/viewing_debugging_dependencies.html#sec:listing-dependencies" )
135- appendLine()
136- appendLine(" Please update Compose Multiplatform Gradle plugin's version or align dependencies' versions to match the current plugin version." )
137199 }
138- }
200+
201+ private fun ModuleVersionIdentifier?.toModuleString () = when (this ) {
202+ null -> " <unknown>"
203+ else -> " $group :$name :$version "
204+ }
205+ private fun ComponentSelector?.toModuleString (): String = when (this ) {
206+ is ProjectComponentSelector -> " project $projectPath "
207+ is ModuleComponentSelector -> " $group :$module :$version "
208+ null -> " <unknown>"
209+ else -> displayName
210+ }
211+ }
0 commit comments