Browse Source
Add runtime libraries compatibility check for Compose projects Fixes [CMP-9288](https://youtrack.jetbrains.com/issue/CMP-9288) Check compose libraries compatibility <img width="1083" height="264" alt="image" src="https://github.com/user-attachments/assets/4b321025-7b76-4a8e-a1f8-12a7ddabbd56" /> To disable the check there is a new gradle property: `org.jetbrains.compose.library.compatibility.check.disable` ## Testing - Added integration tests to verify version mismatches and override behavior. ## Release Notes ### Features - Gradle Plugin - Add a compatibility check for runtime libraries to ensure consistency with the expected Compose version.data-source-prototype-1.10.0-beta02
10 changed files with 315 additions and 3 deletions
@ -0,0 +1,138 @@
@@ -0,0 +1,138 @@
|
||||
package org.jetbrains.compose |
||||
|
||||
import org.gradle.api.DefaultTask |
||||
import org.gradle.api.Project |
||||
import org.gradle.api.artifacts.result.ResolvedComponentResult |
||||
import org.gradle.api.provider.Property |
||||
import org.gradle.api.provider.ProviderFactory |
||||
import org.gradle.api.provider.SetProperty |
||||
import org.gradle.api.tasks.Input |
||||
import org.gradle.api.tasks.TaskAction |
||||
import org.jetbrains.compose.desktop.application.internal.ComposeProperties |
||||
import org.jetbrains.compose.internal.KOTLIN_JVM_PLUGIN_ID |
||||
import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID |
||||
import org.jetbrains.compose.internal.kotlinJvmExt |
||||
import org.jetbrains.compose.internal.mppExt |
||||
import org.jetbrains.compose.internal.utils.dependsOn |
||||
import org.jetbrains.compose.internal.utils.joinLowerCamelCase |
||||
import org.jetbrains.compose.internal.utils.provider |
||||
import org.jetbrains.compose.internal.utils.registerOrConfigure |
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType |
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget |
||||
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget |
||||
import javax.inject.Inject |
||||
|
||||
internal fun Project.configureRuntimeLibrariesCompatibilityCheck() { |
||||
plugins.withId(KOTLIN_MPP_PLUGIN_ID) { |
||||
mppExt.targets.configureEach { target -> target.configureRuntimeLibrariesCompatibilityCheck() } |
||||
} |
||||
plugins.withId(KOTLIN_JVM_PLUGIN_ID) { |
||||
kotlinJvmExt.target.configureRuntimeLibrariesCompatibilityCheck() |
||||
} |
||||
} |
||||
|
||||
private fun KotlinTarget.configureRuntimeLibrariesCompatibilityCheck() { |
||||
val target = this |
||||
if ( |
||||
target.platformType == KotlinPlatformType.common || |
||||
target.platformType == KotlinPlatformType.androidJvm || |
||||
(target.platformType == KotlinPlatformType.jvm && target !is KotlinJvmTarget) //new AGP |
||||
) { |
||||
return |
||||
} |
||||
compilations.configureEach { compilation -> |
||||
val runtimeDependencyConfigurationName = if (target.platformType != KotlinPlatformType.native) { |
||||
compilation.runtimeDependencyConfigurationName |
||||
} else { |
||||
compilation.compileDependencyConfigurationName |
||||
} ?: return@configureEach |
||||
val config = project.configurations.getByName(runtimeDependencyConfigurationName) |
||||
|
||||
val task = project.tasks.registerOrConfigure<RuntimeLibrariesCompatibilityCheck>( |
||||
joinLowerCamelCase("check", target.name, compilation.name, "composeLibrariesCompatibility"), |
||||
) { |
||||
expectedVersion.set(composeVersion) |
||||
projectPath.set(project.path) |
||||
configurationName.set(runtimeDependencyConfigurationName) |
||||
runtimeDependencies.set(provider { config.incoming.resolutionResult.allComponents }) |
||||
} |
||||
compilation.compileTaskProvider.dependsOn(task) |
||||
} |
||||
} |
||||
|
||||
internal abstract class RuntimeLibrariesCompatibilityCheck : DefaultTask() { |
||||
private companion object { |
||||
val librariesForCheck = listOf( |
||||
"org.jetbrains.compose.foundation:foundation", |
||||
"org.jetbrains.compose.ui:ui" |
||||
) |
||||
} |
||||
|
||||
@get:Inject |
||||
protected abstract val providers: ProviderFactory |
||||
|
||||
@get:Input |
||||
abstract val expectedVersion: Property<String> |
||||
|
||||
@get:Input |
||||
abstract val projectPath: Property<String> |
||||
|
||||
@get:Input |
||||
abstract val configurationName: Property<String> |
||||
|
||||
@get:Input |
||||
abstract val runtimeDependencies: SetProperty<ResolvedComponentResult> |
||||
|
||||
init { |
||||
onlyIf { |
||||
!ComposeProperties.disableLibraryCompatibilityCheck(providers).get() |
||||
} |
||||
} |
||||
|
||||
@TaskAction |
||||
fun run() { |
||||
val expectedRuntimeVersion = expectedVersion.get() |
||||
val foundLibs = runtimeDependencies.get().filter { component -> |
||||
component.moduleVersion?.let { lib -> lib.group + ":" + lib.name } in librariesForCheck |
||||
} |
||||
val problems = foundLibs.mapNotNull { component -> |
||||
val module = component.moduleVersion ?: return@mapNotNull null |
||||
if (module.version == expectedRuntimeVersion) return@mapNotNull null |
||||
ProblemLibrary(module.group + ":" + module.name, module.version) |
||||
} |
||||
|
||||
if (problems.isNotEmpty()) { |
||||
logger.warn( |
||||
getMessage( |
||||
projectPath.get(), |
||||
configurationName.get(), |
||||
problems, |
||||
expectedRuntimeVersion |
||||
) |
||||
) |
||||
} |
||||
} |
||||
|
||||
private data class ProblemLibrary(val name: String, val version: String) |
||||
|
||||
private fun getMessage( |
||||
projectName: String, |
||||
configurationName: String, |
||||
problemLibs: List<ProblemLibrary>, |
||||
expectedVersion: String |
||||
): String = buildString { |
||||
appendLine("w: Compose Multiplatform runtime dependencies' versions don't match with plugin version.") |
||||
problemLibs.forEach { lib -> |
||||
appendLine(" expected: '${lib.name}:$expectedVersion'") |
||||
appendLine(" actual: '${lib.name}:${lib.version}'") |
||||
appendLine() |
||||
} |
||||
appendLine("This may lead to compilation errors or unexpected behavior at runtime.") |
||||
appendLine("Such version mismatch might be caused by dependency constraints in one of the included libraries.") |
||||
val taskName = if (projectName.isNotEmpty() && !projectName.endsWith(":")) "$projectName:dependencies" else "${projectName}dependencies" |
||||
appendLine("You can inspect resulted dependencies tree via `./gradlew $taskName --configuration ${configurationName}`.") |
||||
appendLine("See more details in Gradle documentation: https://docs.gradle.org/current/userguide/viewing_debugging_dependencies.html#sec:listing-dependencies") |
||||
appendLine() |
||||
appendLine("Please update Compose Multiplatform Gradle plugin's version or align dependencies' versions to match the current plugin version.") |
||||
} |
||||
} |
||||
@ -0,0 +1,79 @@
@@ -0,0 +1,79 @@
|
||||
package org.jetbrains.compose.test.tests.integration |
||||
|
||||
import org.jetbrains.compose.desktop.application.internal.ComposeProperties |
||||
import org.jetbrains.compose.internal.utils.OS |
||||
import org.jetbrains.compose.internal.utils.currentOS |
||||
import org.jetbrains.compose.test.utils.GradlePluginTestBase |
||||
import org.jetbrains.compose.test.utils.checks |
||||
import org.jetbrains.compose.test.utils.modify |
||||
import kotlin.test.Test |
||||
|
||||
class RuntimeLibrariesCompatibilityCheckTest : GradlePluginTestBase() { |
||||
|
||||
@Test |
||||
fun correctConfigurationDoesntPrintWarning(): Unit = with( |
||||
testProject("misc/compatibilityLibCheck") |
||||
) { |
||||
val logMsg = "w: Compose Multiplatform runtime dependencies' versions don't match with plugin version." |
||||
gradle("assembleAndroidMain").checks { |
||||
check.logDoesntContain("checkAndroidMainComposeLibrariesCompatibility") |
||||
check.logDoesntContain(logMsg) |
||||
} |
||||
gradle("metadataMainClasses").checks { |
||||
check.logDoesntContain("checkMetadataMainComposeLibrariesCompatibility") |
||||
check.logDoesntContain(logMsg) |
||||
} |
||||
gradle("jvmMainClasses").checks { |
||||
check.taskSuccessful(":checkJvmMainComposeLibrariesCompatibility") |
||||
check.logDoesntContain(logMsg) |
||||
} |
||||
gradle("jvmTestClasses").checks { |
||||
check.taskSuccessful(":checkJvmMainComposeLibrariesCompatibility") |
||||
check.taskSuccessful(":checkJvmTestComposeLibrariesCompatibility") |
||||
check.logDoesntContain(logMsg) |
||||
} |
||||
gradle("wasmJsMainClasses").checks { |
||||
check.taskSuccessful(":checkWasmJsMainComposeLibrariesCompatibility") |
||||
check.logDoesntContain(logMsg) |
||||
} |
||||
|
||||
if (currentOS == OS.MacOS) { |
||||
gradle("compileKotlinIosSimulatorArm64").checks { |
||||
check.taskSuccessful(":checkIosSimulatorArm64MainComposeLibrariesCompatibility") |
||||
check.logDoesntContain(logMsg) |
||||
} |
||||
} |
||||
|
||||
file("build.gradle.kts").modify { |
||||
it.replace( |
||||
"api(\"org.jetbrains.compose.ui:ui:${defaultTestEnvironment.composeVersion}\")", |
||||
"api(\"org.jetbrains.compose.ui:ui\") { version { strictly(\"1.9.3\") } }" |
||||
) |
||||
} |
||||
val msg = buildString { |
||||
appendLine("w: Compose Multiplatform runtime dependencies' versions don't match with plugin version.") |
||||
appendLine(" expected: 'org.jetbrains.compose.ui:ui:${defaultTestEnvironment.composeVersion}'") |
||||
appendLine(" actual: 'org.jetbrains.compose.ui:ui:1.9.3'") |
||||
} |
||||
gradle("jvmMainClasses").checks { |
||||
check.taskSuccessful(":checkJvmMainComposeLibrariesCompatibility") |
||||
check.logContains(msg) |
||||
} |
||||
gradle("wasmJsMainClasses").checks { |
||||
check.taskSuccessful(":checkWasmJsMainComposeLibrariesCompatibility") |
||||
check.logContains(msg) |
||||
} |
||||
|
||||
if (currentOS == OS.MacOS) { |
||||
gradle("compileKotlinIosSimulatorArm64").checks { |
||||
check.taskSuccessful(":checkIosSimulatorArm64MainComposeLibrariesCompatibility") |
||||
check.logContains(msg) |
||||
} |
||||
} |
||||
val disableProperty = ComposeProperties.DISABLE_LIBRARY_COMPATIBILITY_CHECK |
||||
gradle("jvmMainClasses", "-P${disableProperty}=true").checks { |
||||
check.taskSkipped(":checkJvmMainComposeLibrariesCompatibility") |
||||
check.logDoesntContain(msg) |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
plugins { |
||||
id("com.android.kotlin.multiplatform.library") |
||||
id("org.jetbrains.kotlin.multiplatform") |
||||
id("org.jetbrains.kotlin.plugin.compose") |
||||
id("org.jetbrains.compose") |
||||
} |
||||
|
||||
kotlin { |
||||
androidLibrary { |
||||
namespace = "org.company.app" |
||||
compileSdk = 35 |
||||
minSdk = 23 |
||||
androidResources.enable = true |
||||
} |
||||
|
||||
jvm() |
||||
|
||||
js { browser() } |
||||
wasmJs { browser() } |
||||
|
||||
iosX64() |
||||
iosArm64() |
||||
iosSimulatorArm64() |
||||
|
||||
sourceSets { |
||||
commonMain.dependencies { |
||||
api("org.jetbrains.compose.runtime:runtime:COMPOSE_VERSION_PLACEHOLDER") |
||||
api("org.jetbrains.compose.ui:ui:COMPOSE_VERSION_PLACEHOLDER") |
||||
api("org.jetbrains.compose.foundation:foundation:COMPOSE_VERSION_PLACEHOLDER") |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
#Gradle |
||||
org.gradle.jvmargs=-Xmx4G |
||||
org.gradle.caching=true |
||||
org.gradle.configuration-cache=true |
||||
org.gradle.daemon=true |
||||
org.gradle.parallel=true |
||||
|
||||
#Kotlin |
||||
kotlin.code.style=official |
||||
kotlin.daemon.jvmargs=-Xmx4G |
||||
kotlin.native.binary.gc=cms |
||||
kotlin.incremental.wasm=true |
||||
|
||||
#Android |
||||
android.useAndroidX=true |
||||
android.nonTransitiveRClass=true |
||||
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
pluginManagement { |
||||
repositories { |
||||
mavenLocal() |
||||
gradlePluginPortal() |
||||
google() |
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") |
||||
} |
||||
plugins { |
||||
id("com.android.kotlin.multiplatform.library").version("AGP_VERSION_PLACEHOLDER") |
||||
id("com.android.application").version("AGP_VERSION_PLACEHOLDER") |
||||
id("org.jetbrains.kotlin.multiplatform").version("KOTLIN_VERSION_PLACEHOLDER") |
||||
id("org.jetbrains.kotlin.android").version("KOTLIN_VERSION_PLACEHOLDER") |
||||
id("org.jetbrains.kotlin.jvm").version("KOTLIN_VERSION_PLACEHOLDER") |
||||
id("org.jetbrains.kotlin.plugin.compose").version("KOTLIN_VERSION_PLACEHOLDER") |
||||
id("org.jetbrains.compose").version("COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER") |
||||
} |
||||
} |
||||
dependencyResolutionManagement { |
||||
repositories { |
||||
mavenLocal() |
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") |
||||
mavenCentral() |
||||
gradlePluginPortal() |
||||
google() |
||||
} |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
package org.company.app |
||||
|
||||
import androidx.compose.foundation.text.BasicText |
||||
import androidx.compose.runtime.Composable |
||||
|
||||
@Composable |
||||
fun App() { |
||||
BasicText("test") |
||||
} |
||||
Loading…
Reference in new issue