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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
#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 @@ |
|||||||
|
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() |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue