diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeCompilerKotlinSupportPlugin.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeCompilerKotlinSupportPlugin.kt index 2358409d53..7cd7f229f3 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeCompilerKotlinSupportPlugin.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeCompilerKotlinSupportPlugin.kt @@ -7,12 +7,18 @@ package org.jetbrains.compose import org.gradle.api.Project import org.gradle.api.provider.Provider +import org.gradle.build.event.BuildEventsListenerRegistry import org.jetbrains.compose.internal.ComposeCompilerArtifactProvider +import org.jetbrains.compose.internal.mppExtOrNull import org.jetbrains.compose.internal.webExt import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget +import javax.inject.Inject -class ComposeCompilerKotlinSupportPlugin : KotlinCompilerPluginSupportPlugin { +@Suppress("UnstableApiUsage") +class ComposeCompilerKotlinSupportPlugin @Inject constructor( + @Suppress("UnstableApiUsage") private val buildEventsListenerRegistry: BuildEventsListenerRegistry +) : KotlinCompilerPluginSupportPlugin { private lateinit var composeCompilerArtifactProvider: ComposeCompilerArtifactProvider override fun apply(target: Project) { @@ -24,9 +30,35 @@ class ComposeCompilerKotlinSupportPlugin : KotlinCompilerPluginSupportPlugin { composeExt.kotlinCompilerPlugin.orNull ?: ComposeCompilerCompatibility.compilerVersionFor(target.getKotlinPluginVersion()) } + + collectUnsupportedCompilerPluginUsages(target) } } + private fun collectUnsupportedCompilerPluginUsages(target: Project) { + fun Project.hasNonJvmTargets(): Boolean { + val nonJvmTargets = setOf(KotlinPlatformType.native, KotlinPlatformType.js, KotlinPlatformType.wasm) + return mppExtOrNull?.targets?.any { + it.platformType in nonJvmTargets + } ?: false + } + + fun SubpluginArtifact.isNonJBComposeCompiler(): Boolean { + return !groupId.startsWith("org.jetbrains.compose.compiler") + } + + val service = ComposeMultiplatformBuildService.provider(target) + buildEventsListenerRegistry.onTaskCompletion(service) + + service.get().parameters.unsupportedCompilerPlugins.add( + target.provider { + composeCompilerArtifactProvider.compilerArtifact.takeIf { + target.hasNonJvmTargets() && it.isNonJBComposeCompiler() + } + } + ) + } + override fun getCompilerPluginId(): String = "androidx.compose.compiler.plugins.kotlin" @@ -68,4 +100,15 @@ class ComposeCompilerKotlinSupportPlugin : KotlinCompilerPluginSupportPlugin { private fun options(vararg options: Pair): List = options.map { SubpluginOption(it.first, it.second) } +} + +private const val COMPOSE_COMPILER_COMPATIBILITY_LINK = + "https://github.com/JetBrains/compose-jb/blob/master/VERSIONING.md#using-compose-multiplatform-compiler" + +internal fun createWarningAboutNonCompatibleCompiler(currentCompilerPluginGroupId: String): String { + return """ +WARNING: Usage of the Custom Compose Compiler plugin ('$currentCompilerPluginGroupId') +with non-JVM targets (Kotlin/Native, Kotlin/JS, Kotlin/WASM) is not supported. +For more information, please visit: $COMPOSE_COMPILER_COMPATIBILITY_LINK +""".trimMargin() } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeMultiplatformBuildService.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeMultiplatformBuildService.kt new file mode 100644 index 0000000000..5e117da92e --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeMultiplatformBuildService.kt @@ -0,0 +1,62 @@ +package org.jetbrains.compose + +import org.gradle.api.Project +import org.gradle.api.logging.Logging +import org.gradle.api.provider.Provider +import org.gradle.api.provider.SetProperty +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters +import org.gradle.api.services.BuildServiceRegistry +import org.gradle.tooling.events.FinishEvent +import org.gradle.tooling.events.OperationCompletionListener +import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact + +// The service implements OperationCompletionListener just so Gradle would use the service +// even if the service is not used by any task or transformation +abstract class ComposeMultiplatformBuildService : BuildService, + OperationCompletionListener, AutoCloseable { + interface Parameters : BuildServiceParameters { + val unsupportedCompilerPlugins: SetProperty> + } + + private val log = Logging.getLogger(this.javaClass) + + override fun close() { + notifyAboutUnsupportedCompilerPlugin() + } + + private fun notifyAboutUnsupportedCompilerPlugin() { + val unsupportedCompilerPlugin = parameters.unsupportedCompilerPlugins.orNull + ?.firstOrNull() + ?.orNull + + if (unsupportedCompilerPlugin != null) { + log.error(createWarningAboutNonCompatibleCompiler(unsupportedCompilerPlugin.groupId)) + } + } + + override fun onFinish(event: FinishEvent) {} + + companion object { + fun configure(project: Project, fn: Parameters.() -> Unit): Provider = + project.gradle.sharedServices.registerOrConfigure { + fn() + } + + fun provider(project: Project): Provider = configure(project) {} + } +} + +inline fun > BuildServiceRegistry.registerOrConfigure( + crossinline fn: P.() -> Unit +): Provider { + val serviceClass = S::class.java + val serviceFqName = serviceClass.canonicalName + val existingService = registrations.findByName(serviceFqName) + ?.apply { (parameters as? P)?.fn() } + ?.service + return (existingService as? Provider) + ?: registerIfAbsent(serviceFqName, serviceClass) { + it.parameters.fn() + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/UnsupportedCompilerPluginWarningTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/UnsupportedCompilerPluginWarningTest.kt new file mode 100644 index 0000000000..c90acff31f --- /dev/null +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/UnsupportedCompilerPluginWarningTest.kt @@ -0,0 +1,44 @@ +package org.jetbrains.compose.test.tests.integration + +import org.jetbrains.compose.createWarningAboutNonCompatibleCompiler +import org.jetbrains.compose.test.utils.GradlePluginTestBase +import org.jetbrains.compose.test.utils.TestProjects +import org.jetbrains.compose.test.utils.checks +import org.junit.jupiter.api.Test + +class UnsupportedCompilerPluginWarningTest : GradlePluginTestBase() { + + private val androidxComposeCompilerGroupId = "androidx.compose.compiler" + private val androidxComposeCompilerPlugin = "$androidxComposeCompilerGroupId:compiler:1.4.8" + + @Suppress("RedundantUnitExpression") + @Test + fun testKotlinJs_shows_warning_for_androidx_compose_compiler() = testProject( + TestProjects.customCompilerArgs, defaultTestEnvironment.copy( + kotlinVersion = "1.8.22", + composeCompilerPlugin = "\"$androidxComposeCompilerPlugin\"", + composeCompilerArgs = "\"suppressKotlinVersionCompatibilityCheck=1.8.22\"" + ) + ).let { + it.gradle(":compileKotlinJs").checks { + check.taskSuccessful(":compileKotlinJs") + check.logContains(createWarningAboutNonCompatibleCompiler(androidxComposeCompilerGroupId)) + } + Unit + } + + @Suppress("RedundantUnitExpression") + @Test + fun testKotlinJvm_doesnt_show_warning_for_androidx_compose_compiler() = testProject( + TestProjects.customCompiler, defaultTestEnvironment.copy( + kotlinVersion = "1.8.22", + composeCompilerPlugin = "\"$androidxComposeCompilerPlugin\"", + ) + ).let { + it.gradle(":run").checks { + check.taskSuccessful(":run") + check.logDoesntContain(createWarningAboutNonCompatibleCompiler(androidxComposeCompilerGroupId)) + } + Unit + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt index bc8b0ff854..56fbb3fea2 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt @@ -35,6 +35,12 @@ internal class BuildResultChecks(private val result: BuildResult) { } } + fun logDoesntContain(substring: String) { + if (result.output.contains(substring)) { + throw AssertionError("Test output contains the unexpected string: '$substring'") + } + } + fun taskSuccessful(task: String) { taskOutcome(task, TaskOutcome.SUCCESS) }