Browse Source

Add runtime libraries compatibility check for Compose projects (#5485)

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
Konstantin 3 weeks ago committed by GitHub
parent
commit
880c005b71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt
  2. 138
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/RuntimeLibrariesCompatibilityCheck.kt
  3. 4
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt
  4. 7
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/projectExtensions.kt
  5. 5
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ComposeResources.kt
  6. 79
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/RuntimeLibrariesCompatibilityCheckTest.kt
  7. 32
      gradle-plugins/compose/src/test/test-projects/misc/compatibilityLibCheck/build.gradle.kts
  8. 16
      gradle-plugins/compose/src/test/test-projects/misc/compatibilityLibCheck/gradle.properties
  9. 26
      gradle-plugins/compose/src/test/test-projects/misc/compatibilityLibCheck/settings.gradle.kts
  10. 9
      gradle-plugins/compose/src/test/test-projects/misc/compatibilityLibCheck/src/commonMain/kotlin/org/company/app/App.kt

2
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt

@ -52,6 +52,8 @@ abstract class ComposePlugin : Plugin<Project> { @@ -52,6 +52,8 @@ abstract class ComposePlugin : Plugin<Project> {
project.configureWebCompatibility()
project.configureRuntimeLibrariesCompatibilityCheck()
project.afterEvaluate {
configureDesktop(project, desktopExtension)
project.configureWeb(composeExtension)

138
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/RuntimeLibrariesCompatibilityCheck.kt

@ -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.")
}
}

4
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt

@ -28,6 +28,7 @@ internal object ComposeProperties { @@ -28,6 +28,7 @@ internal object ComposeProperties {
internal const val SYNC_RESOURCES_PROPERTY = "compose.ios.resources.sync"
internal const val DISABLE_HOT_RELOAD = "org.jetbrains.compose.hot.reload.disable"
internal const val DISABLE_RESOURCE_CONTENT_HASH_GENERATION = "org.jetbrains.compose.resources.content.hash.generation.disable"
internal const val DISABLE_LIBRARY_COMPATIBILITY_CHECK = "org.jetbrains.compose.library.compatibility.check.disable"
fun isVerbose(providers: ProviderFactory): Provider<Boolean> =
providers.valueOrNull(VERBOSE).toBooleanProvider(false)
@ -68,6 +69,9 @@ internal object ComposeProperties { @@ -68,6 +69,9 @@ internal object ComposeProperties {
fun disableResourceContentHashGeneration(providers: ProviderFactory): Provider<Boolean> =
providers.valueOrNull(DISABLE_RESOURCE_CONTENT_HASH_GENERATION).toBooleanProvider(false)
fun disableLibraryCompatibilityCheck(providers: ProviderFactory): Provider<Boolean> =
providers.valueOrNull(DISABLE_LIBRARY_COMPATIBILITY_CHECK).toBooleanProvider(false)
//providers.valueOrNull works only with root gradle.properties
fun dontSyncResources(project: Project): Provider<Boolean> =
project.findLocalOrGlobalProperty(SYNC_RESOURCES_PROPERTY).map { it == "false" }

7
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/projectExtensions.kt

@ -13,6 +13,7 @@ import org.gradle.util.GradleVersion @@ -13,6 +13,7 @@ import org.gradle.util.GradleVersion
import org.jetbrains.compose.ComposeExtension
import org.jetbrains.compose.web.WebExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinJsProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
internal val Project.composeExt: ComposeExtension?
@ -27,6 +28,12 @@ internal val Project.mppExt: KotlinMultiplatformExtension @@ -27,6 +28,12 @@ internal val Project.mppExt: KotlinMultiplatformExtension
internal val Project.mppExtOrNull: KotlinMultiplatformExtension?
get() = extensions.findByType(KotlinMultiplatformExtension::class.java)
internal val Project.kotlinJvmExt: KotlinJvmProjectExtension
get() = kotlinJvmExtOrNull ?: error("Could not find KotlinJvmProjectExtension ($project)")
internal val Project.kotlinJvmExtOrNull: KotlinJvmProjectExtension?
get() = extensions.findByType(KotlinJvmProjectExtension::class.java)
internal val Project.kotlinJsExtOrNull: KotlinJsProjectExtension?
get() = extensions.findByType(KotlinJsProjectExtension::class.java)

5
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ComposeResources.kt

@ -6,7 +6,7 @@ import org.gradle.util.GradleVersion @@ -6,7 +6,7 @@ import org.gradle.util.GradleVersion
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.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.compose.internal.kotlinJvmExt
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin
import org.jetbrains.kotlin.gradle.plugin.extraProperties
@ -60,8 +60,7 @@ private fun Project.onKgpApplied(config: Provider<ResourcesExtension>, kgp: Kotl @@ -60,8 +60,7 @@ private fun Project.onKgpApplied(config: Provider<ResourcesExtension>, kgp: Kotl
}
internal fun Project.onKotlinJvmApplied(config: Provider<ResourcesExtension>) {
val kotlinExtension = project.extensions.getByType(KotlinJvmProjectExtension::class.java)
configureJvmOnlyResources(kotlinExtension, config)
configureJvmOnlyResources(kotlinJvmExt, config)
}
internal fun Project.onAgpApplied(block: (pluginId: String) -> Unit) {

79
gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/RuntimeLibrariesCompatibilityCheckTest.kt

@ -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)
}
}
}

32
gradle-plugins/compose/src/test/test-projects/misc/compatibilityLibCheck/build.gradle.kts

@ -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")
}
}
}

16
gradle-plugins/compose/src/test/test-projects/misc/compatibilityLibCheck/gradle.properties

@ -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

26
gradle-plugins/compose/src/test/test-projects/misc/compatibilityLibCheck/settings.gradle.kts

@ -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()
}
}

9
gradle-plugins/compose/src/test/test-projects/misc/compatibilityLibCheck/src/commonMain/kotlin/org/company/app/App.kt

@ -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…
Cancel
Save