Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to use Compose on multiple Kotlin versions #2366

Merged
merged 4 commits into from
Oct 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

# Compose Multiplatform, by JetBrains
![](artwork/readme/apps.png)
Compose Kotlin UI framework port for desktop platforms (macOS, Linux, Windows) and Web, components outside of the core Compose repository
at https://android.googlesource.com/platform/frameworks/support.
Compose Kotlin UI framework port for desktop platforms (macOS, Linux, Windows) and Web, components outside of the core [Compose repository](https://android.googlesource.com/platform/frameworks/support).

Preview functionality (check your application UI without building/running it) for desktop platforms is available via IDEA plugin (https://plugins.jetbrains.com/plugin/16541-compose-multiplatform-ide-support).
Preview functionality (check your application UI without building/running it) for desktop platforms is available via [IDEA plugin](https://plugins.jetbrains.com/plugin/16541-compose-multiplatform-ide-support).

## Tutorials
### Compose for Desktop
Expand Down Expand Up @@ -70,6 +69,9 @@ Note that when you use Compose Multiplatform, you setup your project differently
* [LWJGL integration](experimental/lwjgl-integration) - An example showing how to integrate Compose with [LWJGL](https://www.lwjgl.org)
* [CLI example](experimental/build_from_cli) - An example showing how to build Compose without Gradle

## Getting latest version of Compose Multiplatform ##
## Versions ##

See https://github.com/JetBrains/compose-jb/releases/latest for the latest stable release or https://github.com/JetBrains/compose-jb/releases for all stable and dev releases.
* [The latest stable release](https://github.com/JetBrains/compose-jb/releases/latest)
* [The latest dev release](https://github.com/JetBrains/compose-jb/releases)
* [Compatability and versioning overview](VERSIONING.md)
* [Changelog](CHANGELOG.md)
19 changes: 10 additions & 9 deletions FEATURES.md → VERSIONING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ Knowing issues on older versions:

### Kotlin compatibility

Compose version | Kotlin version
--- | ---
1.0.0+ | 1.5.31
1.0.1-rc2+ | 1.6.10


### Gradle plugin compatibility

* 1.0.0 works with Gradle 6.7 or later (7.2 is the latest tested version).
A new version of Kotlin may be not supported immediately after its release. But after some time we will release a version of Compose Multiplatform
that supports it.
Starting from 1.2.0, Compose Multiplatform supports multiple versions of Kotlin.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the Compose Multiplatform Gradle plugin could be more precise

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is details, and only obvious for people who knows how our Gradle plugin works. Compose Multiplatform is considered as a whole product. And one of the features of this product is supporting specific Kotlin version. This support can be achieved multiple ways - via Gradle plugin, via Kotlin compiler plugin, via IDE, via runtime dependencies, etc.


Kotlin version | Minimal Compose version | Notes
--- | --- | ---
1.5.31 | 1.0.0
1.6.20 | 1.1.1
1.7.10 | 1.2.0
1.7.20 | 1.2.0 | JS is not supported (will be fixed in the next versions)
3 changes: 0 additions & 3 deletions examples/validateExamples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,4 @@ runGradle issues package
runGradle notepad package
runGradle todoapp-lite package
runGradle visual-effects package
runGradle web-compose-bird build
runGradle web-landing build
runGradle web-with-react build
runGradle widgets-gallery package
24 changes: 24 additions & 0 deletions examples/validateExamplesWithJs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash

# Script to build most of the examples, to verify if they can compile.
# Don't add examples, which don't depend on maven.pkg.jetbrains.space, because they won't be able to compile.

set -euo pipefail

if [ "$#" -ne 2 ]; then
echo "Specify Compose and Kotlin version. For example: ./validateExamplesWithJs.sh 1.1.1 1.6.10"
exit 1
fi
COMPOSE_VERSION=$1
KOTLIN_VERSION=$2


runGradle() {
pushd $1
./gradlew $2 -Pcompose.version=$COMPOSE_VERSION -Pkotlin.version=$KOTLIN_VERSION
popd
}

runGradle web-compose-bird build
runGradle web-landing build
runGradle web-with-react build
4 changes: 0 additions & 4 deletions gradle-plugins/buildSrc/src/main/kotlin/BuildProperties.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ object BuildProperties {
fun composeVersion(project: Project): String =
System.getenv("COMPOSE_GRADLE_PLUGIN_COMPOSE_VERSION")
?: project.findProperty("compose.version") as String
fun composeCompilerVersion(project: Project): String =
project.findProperty("compose.compiler.version") as String
fun composeCompilerCompatibleKotlinVersion(project: Project): String =
project.findProperty("compose.compiler.compatible.kotlin.version") as String
fun testsAndroidxCompilerVersion(project: Project): String =
project.findProperty("compose.tests.androidx.compiler.version") as String
fun testsAndroidxCompilerCompatibleVersion(project: Project): String =
Expand Down
1 change: 0 additions & 1 deletion gradle-plugins/compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ val buildConfig = tasks.register("buildConfig", GenerateBuildConfig::class.java)
classFqName.set("org.jetbrains.compose.ComposeBuildConfig")
generatedOutputDir.set(buildConfigDir)
fieldsToGenerate.put("composeVersion", BuildProperties.composeVersion(project))
fieldsToGenerate.put("composeCompilerVersion", BuildProperties.composeCompilerVersion(project))
fieldsToGenerate.put("composeGradlePluginVersion", BuildProperties.deployVersion(project))
fieldsToGenerate.put("experimentalOELPublication", BuildProperties.experimentalOELPublication(project))
fieldsToGenerate.put("oelAndroidXVersion", BuildProperties.oelAndroidXVersion(project).orEmpty())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.jetbrains.compose

import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType

internal object ComposeCompilerCompatability {
fun compilerVersionFor(kotlinVersion: String): ComposeCompilerVersion? = when (kotlinVersion) {
"1.7.10" -> ComposeCompilerVersion("1.3.0-alpha01")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder I we can use one table/json/xml file to generate both table in the docs and compatibility code

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably it is overkill. If we will have difficulties of supporting docs, we can investigate. Currently, I don't see a solution yet, but already see multiple issues with this code generation :)

Besides this place, we need to keep all other docs in sync too.

"1.7.20" -> ComposeCompilerVersion(
"1.3.2-alpha01",
unsupportedPlatforms = setOf(KotlinPlatformType.js)
)
else -> null
}
}

internal data class ComposeCompilerVersion(
val version: String,
val unsupportedPlatforms: Set<KotlinPlatformType> = emptySet()
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget

class ComposeCompilerKotlinSupportPlugin : KotlinCompilerPluginSupportPlugin {
private var composeCompilerArtifactProvider = ComposeCompilerArtifactProvider { null }
private lateinit var composeCompilerArtifactProvider: ComposeCompilerArtifactProvider

override fun apply(target: Project) {
super.apply(target)
target.plugins.withType(ComposePlugin::class.java) {
val composeExt = target.extensions.getByType(ComposeExtension::class.java)
composeCompilerArtifactProvider = ComposeCompilerArtifactProvider { composeExt.kotlinCompilerPlugin.orNull }

composeCompilerArtifactProvider = ComposeCompilerArtifactProvider(
kotlinVersion = target.getKotlinPluginVersion()
) {
composeExt.kotlinCompilerPlugin.orNull
}
}
}

Expand Down Expand Up @@ -52,6 +57,7 @@ class ComposeCompilerKotlinSupportPlugin : KotlinCompilerPluginSupportPlugin {

override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider<List<SubpluginOption>> {
val target = kotlinCompilation.target
composeCompilerArtifactProvider.checkTargetSupported(target)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overall code design became confusing.
Previously, ComposeCompilerArtifactProvider had a single responsibility of providing a version of ComposeCompilerArtifact. Now ComposeCompilerArtifactProvider also knows how to check if a target is compatible with a Compose Compiler, but only if the default compiler is used.
So the following configuration

plugins {
  id("org.jetbrains.kotlin.multiplatform") version "1.7.20"
  id("org.jetbrains.compose") version "1.2.0"
}

kotlin {
   js(IR) {
        browser()
        binaries.executable()
    }
}

compose {
     kotlinCompilerPlugin.set("1.3.2-alpha01")
     
     web {}
}

will not trigger the check at all, despite that we know they are incompatible.

I suggest to separate compiler inference and compiler compatibility:

  1. The inference should only try to choose a version. If inference fails, it should tell user to try updating the Compose Compiler (if a compatible version exists).
  2. Then we run compiler compatibility checks, regardless of whether the version was inferred or set manually.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to separate compiler inference and compiler compatibility:

See my comment in the other discussion. We can't make compatibility check for an arbitrary compiler in the Gradle plugin. We only can make such check, if the compiler was chosen from our list.

As for code quality, we expand responsibility of ComposeCompilerArtifactProvider from providing a version of ComposeCompilerArtifact to providing info about compatible compiler. It still can be considered as a single responsibility.

return target.project.provider {
platformPluginOptions[target.platformType] ?: emptyList()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,67 @@

package org.jetbrains.compose.internal

import org.jetbrains.compose.ComposeBuildConfig
import org.jetbrains.compose.ComposeCompilerCompatability
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact

internal class ComposeCompilerArtifactProvider(private val customPluginString: () -> String?) {
private const val KOTLIN_COMPATABILITY_LINK =
"https://github.com/JetBrains/compose-jb/blob/master/VERSIONING.md#kotlin-compatibility"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using Kotlin team about kotl.in URL shortener or more general jb.gg URL shortener.
E.g. kotl.in/compose-compatibility would look nicer.
That's obviously not urgent, just nice to have

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can I create such link?


internal class ComposeCompilerArtifactProvider(
private val kotlinVersion: String,
customPluginString: () -> String?
) {
fun checkTargetSupported(target: KotlinTarget) {
require(!unsupportedPlatforms.contains(target.platformType)) {
"This version of Compose Multiplatform doesn't support Kotlin " +
"$kotlinVersion for ${target.platformType} target. " +
"Please see $KOTLIN_COMPATABILITY_LINK " +
"to know the latest supported version of Kotlin."
}
}

private var unsupportedPlatforms: Set<KotlinPlatformType> = emptySet()

val compilerArtifact: SubpluginArtifact
get() {
val customPlugin = customPluginString()
val customCoordinates = customPlugin?.split(":")
return when (customCoordinates?.size) {
null -> DefaultCompiler.pluginArtifact
1 -> {
val customVersion = customCoordinates[0]
check(customVersion.isNotBlank()) { "'compose.kotlinCompilerPlugin' cannot be blank!" }
DefaultCompiler.pluginArtifact.copy(version = customVersion)

init {
val customPlugin = customPluginString()
val customCoordinates = customPlugin?.split(":")
when (customCoordinates?.size) {
null -> {
val version = requireNotNull(
ComposeCompilerCompatability.compilerVersionFor(kotlinVersion)
) {
"This version of Compose Multiplatform doesn't support Kotlin " +
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message is a bit confusing. As I understand it, here we simply don't know, what version of Compose Compiler to choose. Instead the message says "this version of Compose doesn't support Kotlin $kotlinVersion", which may or may not be true. For example, if there is a version of a Compose Compiler, that works with current plugin and the desired version of Kotlin, then in my understanding the Gradle plugin & libraries are compatible, we simply don't know the right version of the compiler.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking about the best way to do this, and I couldn't invent the better than the current way.

The idea is that if the user chooses Compiler themselves, the responsibility is on them.

But if it is chosen by automatic selection, we can display a meaningful message.

If we would add a mapping Compiler version -> supported platforms, then it always be incomplete. There won't be new or dev versions in this mapping. We can solve this issue changing Compose Compiler, but for just showing a message it seems overkill.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This version of Compose doesn't support Kotlin $kotlinVersion

And that is why we tell Compose Multiplatform instead of Compose Compiler :). Compose Multiplatform as a whole, and in a default state doesn't support it, but a custom compiler can theoretically support it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, there is an option to completely prohibit using Kotlin 1.7.20, even if the user sets compatible Compose Compiler. But I don't think it is a good idea.

"$kotlinVersion. " +
"Please see $KOTLIN_COMPATABILITY_LINK " +
"to know the latest supported version of Kotlin."
}
3 -> DefaultCompiler.pluginArtifact.copy(
groupId = customCoordinates[0],
artifactId = customCoordinates[1],
version = customCoordinates[2]

compilerArtifact = DefaultCompiler.pluginArtifact(
version = version.version
)
else -> error("""
unsupportedPlatforms = version.unsupportedPlatforms
}
1 -> {
val customVersion = customCoordinates[0]
check(customVersion.isNotBlank()) { "'compose.kotlinCompilerPlugin' cannot be blank!" }
compilerArtifact = DefaultCompiler.pluginArtifact(version = customVersion)
}
3 -> compilerArtifact = DefaultCompiler.pluginArtifact(
version = customCoordinates[2],
groupId = customCoordinates[0],
artifactId = customCoordinates[1],
)
else -> error("""
Illegal format of 'compose.kotlinCompilerPlugin' property.
Expected format: either '<VERSION>' or '<GROUP_ID>:<ARTIFACT_ID>:<VERSION>'
Actual value: '$customPlugin'
""".trimIndent())
}
}
}

val compilerHostedArtifact: SubpluginArtifact
get() = compilerArtifact.run {
Expand All @@ -47,10 +81,13 @@ internal class ComposeCompilerArtifactProvider(private val customPluginString: (
const val GROUP_ID = "org.jetbrains.compose.compiler"
const val ARTIFACT_ID = "compiler"
const val HOSTED_ARTIFACT_ID = "compiler-hosted"
const val VERSION = ComposeBuildConfig.composeCompilerVersion

val pluginArtifact: SubpluginArtifact
get() = SubpluginArtifact(groupId = GROUP_ID, artifactId = ARTIFACT_ID, version = VERSION)
fun pluginArtifact(
version: String,
groupId: String = GROUP_ID,
artifactId: String = ARTIFACT_ID,
): SubpluginArtifact =
SubpluginArtifact(groupId = groupId, artifactId = artifactId, version = version)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ import org.junit.jupiter.api.Test
class GradlePluginTest : GradlePluginTestBase() {
@Test
fun jsMppIsNotBroken() =
with(testProject(TestProjects.jsMpp)) {
with(
testProject(
TestProjects.jsMpp,
testEnvironment = defaultTestEnvironment.copy(
kotlinVersion = TestProperties.composeJsCompilerCompatibleKotlinVersion
)
)
) {
gradle(":compileKotlinJs").build().checks { check ->
check.taskOutcome(":compileKotlinJs", TaskOutcome.SUCCESS)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/

package org.jetbrains.compose.test.tests.integration

import org.gradle.testkit.runner.TaskOutcome
import org.gradle.testkit.runner.UnexpectedBuildFailure
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
import org.junit.jupiter.api.assertThrows

class KotlinCompatabilityTest : GradlePluginTestBase() {
@Test
fun testKotlinMpp_1_7_10() = testMpp("1.7.10")

@Test
fun testKotlinJsMpp_1_7_10() = testJsMpp("1.7.10")

@Test
fun testKotlinMpp_1_7_20() = testMpp("1.7.20")

@Test
fun testKotlinJsMpp_1_7_20() {
assertThrows<UnexpectedBuildFailure> {
testJsMpp("1.7.20")
}
}

private fun testMpp(kotlinVersion: String) = with(
testProject(
TestProjects.mpp,
testEnvironment = defaultTestEnvironment.copy(kotlinVersion = kotlinVersion)
)
) {
val logLine = "Kotlin MPP app is running!"
gradle("run").build().checks { check ->
check.taskOutcome(":run", TaskOutcome.SUCCESS)
check.logContains(logLine)
}
}

private fun testJsMpp(kotlinVersion: String) = with(
testProject(
TestProjects.jsMpp,
testEnvironment = defaultTestEnvironment.copy(kotlinVersion = kotlinVersion)
)
) {
gradle(":compileKotlinJs").build().checks { check ->
check.taskOutcome(":compileKotlinJs", TaskOutcome.SUCCESS)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,34 @@ internal class ComposeCompilerArtifactProviderTest {
fun defaultCompilerArtifact() {
assertArtifactEquals(
Expected.jbCompiler,
Actual.compiler(null)
Actual.compiler(null, TestProperties.composeCompilerCompatibleKotlinVersion)
)
}

@Test
fun defaultCompilerHostedArtifact() {
assertArtifactEquals(
Expected.jbCompilerHosted,
Actual.compilerHosted(null)
Actual.compilerHosted(null, TestProperties.composeCompilerCompatibleKotlinVersion)
)
}

@Test
fun customVersion() {
assertArtifactEquals(
Expected.jbCompiler.copy(version = "10.20.30"),
Actual.compiler("10.20.30")
Actual.compiler("10.20.30", TestProperties.composeCompilerCompatibleKotlinVersion)
)
}

@Test
fun customCompiler() {
assertArtifactEquals(
Expected.googleCompiler.copy(version = "1.3.1"),
Actual.compiler("androidx.compose.compiler:compiler:1.3.1")
Actual.compiler(
"androidx.compose.compiler:compiler:1.3.1",
TestProperties.androidxCompilerCompatibleKotlinVersion
)
)
}

Expand All @@ -51,7 +54,10 @@ internal class ComposeCompilerArtifactProviderTest {
// check that we don't replace artifactId for non-jb compiler
assertArtifactEquals(
Expected.googleCompiler.copy(version = "1.3.1"),
Actual.compilerHosted("androidx.compose.compiler:compiler:1.3.1")
Actual.compilerHosted(
"androidx.compose.compiler:compiler:1.3.1",
TestProperties.composeCompilerCompatibleKotlinVersion
)
)
}

Expand All @@ -64,7 +70,7 @@ internal class ComposeCompilerArtifactProviderTest {

private fun testIllegalCompiler(pluginString: String?) {
try {
Actual.compiler(pluginString)
Actual.compiler(pluginString, "")
} catch (e: Exception) {
return
}
Expand All @@ -73,11 +79,11 @@ internal class ComposeCompilerArtifactProviderTest {
}

object Actual {
fun compiler(pluginString: String?) =
ComposeCompilerArtifactProvider { pluginString }.compilerArtifact
fun compiler(pluginString: String?, kotlinVersion: String) =
ComposeCompilerArtifactProvider(kotlinVersion) { pluginString }.compilerArtifact

fun compilerHosted(pluginString: String?) =
ComposeCompilerArtifactProvider { pluginString }.compilerHostedArtifact
fun compilerHosted(pluginString: String?, kotlinVersion: String) =
ComposeCompilerArtifactProvider(kotlinVersion) { pluginString }.compilerHostedArtifact
}

object Expected {
Expand Down
Loading