JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
15.17k stars 1.11k forks source link

Adding any compose dependency breaks resolving the Gradle configuration #1404

Open jakobkmar opened 2 years ago

jakobkmar commented 2 years ago

When I am adding the compose dependency to a (resolvable) Gradle configuration (includeImplementation is my custom configuration) as follows:

includeImplementation(compose.desktop.currentOs) // same with common

and then trying to resolve the dependencies of this configuration

includeImplementation.resolvedConfiguration.firstLevelModuleDependencies

I am getting the following error

Caused by: org.gradle.internal.component.AmbiguousConfigurationSelectionException: Cannot choose between the following variants of org.jetbrains.compose.foundation:foundation:1.0.0-beta5:
  - debugRuntimeElements-published
  - desktopRuntimeElements-published
  - releaseRuntimeElements-published

(probably relates to this)

My custom configuration above is one example, but there are other scenarios where you have to use custom configurations from third-party Gradle plugins, and therefore using this together with Compose does not work.

akurasov commented 2 years ago

Do you use Kotlin multiplatform gradle plugin? Since Compose is published like a multiplatform solution, this plugin is required for dependency resolution

jakobkmar commented 2 years ago

I am not using the multiplatform plugin, because I am having trouble to use the custom configuration from the third party plugin in the scope of KotlinDependencyHandler, as this scope only has some predefined standard configurations available for use.

jakobkmar commented 2 years ago
plugins {
    kotlin("jvm") version "1.5.31"
    id("fabric-loom") version "0.10-SNAPSHOT"
    kotlin("plugin.serialization") version "1.5.31"
    id("org.jetbrains.compose") version "1.0.0-beta5"
}

repositories {
    mavenCentral()
    google()
    maven("https://maven.fabricmc.net/")
    mavenLocal()
    maven("https://oss.sonatype.org/content/repositories/snapshots")
    maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}

val includeImplementation by configurations.creating {
    configurations.implementation.configure { extendsFrom(this@creating) }
}

dependencies {
    minecraft("com.mojang:minecraft:$minecraftVersion")
    mappings("net.fabricmc:yarn:$yarnMappingsVersion")
    modImplementation("net.fabricmc:fabric-loader:$fabricLoaderVersion")
    modImplementation("net.fabricmc.fabric-api:fabric-api:$fabricApiVersion")
    modImplementation("net.fabricmc:fabric-language-kotlin:$fabricLanguageKotlinVersion")

    includeImplementation(compose.desktop.currentOs)

    includeImplementation("org.litote.kmongo:kmongo-coroutine-core:4.3.0")
    includeImplementation("org.litote.kmongo:kmongo-serialization-mapping:4.3.0")
    includeImplementation("org.slf4j:slf4j-simple:1.7.32")
    includeImplementation("org.apache.commons:commons-text:1.9")

    // example usage which triggers the error
    includeImplementation.resolvedConfiguration.firstLevelModuleDependencies
}

Or is there any way I could convert this project using Compose to a project using the multiplatform plugin?

akurasov commented 2 years ago

Or is there any way I could convert this project using Compose to a project using the multiplatform plugin?

Yes, it is quite easy.

Replace kotlin("jvm") version "1.5.31" with kotlin("multiplatform") version "1.5.31"

add the code below and move dependencies there. It should work.

kotlin {
    jvm {
        withJava()
    }
    sourceSets {
        named("jvmMain") {
            dependencies {
>>put dependencies here
            }
        }
    }
}
jakobkmar commented 2 years ago

Yes I already tried that, but the custom configurations such as modImplementation etc are not available there.

akurasov commented 2 years ago

What are these custom functions for?

akurasov commented 2 years ago

As an option, you could split code in two modules. Use Compose+mpp plugin in one module and custom imports in another.

jakobkmar commented 2 years ago

What are these custom functions for?

They are Gradle configurations, minecraft specified the Minecraft dependency, mappings specifies the mappings for Minecraft and modImplementation adds Minecraft mods.

jakobkmar commented 2 years ago

As an option, you could split code in two modules. Use Compose+mpp plugin in one module and custom imports in another.

I could do that, but I would still have to execute includeImplementation.resolvedConfiguration.firstLevelModuleDependencies in the other module dependening on the compose module, which would lead to the same error.

akurasov commented 2 years ago

I could do that, but I would still have to execute includeImplementation.resolvedConfiguration.firstLevelModuleDependencies in the other module dependening on the compose module, which would lead to the same error

Could you share you build scripts? I struggle to understand that is the issue

jakobkmar commented 2 years ago

The simplest example would be just to try and shade the compose dependency using the Gradle shadow plugin in a JVM context. Not that I'd recommend this, but this will trigger the problem I mean, as it tried to resolve the compose dependency as well.

jakobkmar commented 2 years ago

To trigger the problem:

plugins {
    kotlin("jvm") version "1.6.10"
    id("org.jetbrains.compose") version "1.0.1-rc2"
    id("com.github.johnrengelman.shadow") version "7.1.1"
}

repositories {
    mavenCentral()
    google()
    maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}

val embed: Configuration by configurations.creating

dependencies {
    embed(implementation(compose.desktop.currentOs)!!)
}

tasks {
    shadowJar {
        configurations = listOf(embed)
    }
}

Now you might suggest that I can just use the multiplatform plugin instead, but that is not an option as it does provide all the functionality that I need. edit: it does not provide

akurasov commented 2 years ago

I guess you meant don not provide. What exactly is missing?

jakobkmar commented 2 years ago

I guess you meant don not provide.

correct

What exactly is missing?

The custom Gradle configurations (minecraft, modImplementation) and compatiblity with other Gradle plugins as shown in my previous comment.

jakobkmar commented 2 years ago

With release 1.0.1 the issue was resolved for the Compose dependencies themselves, but with 1.1.0 it was reintroduced, this time caused by the skiko dependency:

   > Cannot choose between the following variants of org.jetbrains.skiko:skiko:0.7.12:
       - androidRuntimeElements-published
       - awtRuntimeElements-published
     All of them match the consumer attributes:
       - Variant 'androidRuntimeElements-published' capability org.jetbrains.skiko:skiko:0.7.12:
           - Unmatched attributes:
               - Provides org.gradle.category 'library' but the consumer didn't ask for it
               - Provides org.gradle.libraryelements 'jar' but the consumer didn't ask for it
               - Provides org.gradle.status 'release' but the consumer didn't ask for it
               - Provides org.gradle.usage 'java-runtime' but the consumer didn't ask for it
               - Provides org.jetbrains.kotlin.platform.type 'jvm' but the consumer didn't ask for it
               - Provides ui 'android' but the consumer didn't ask for it
       - Variant 'awtRuntimeElements-published' capability org.jetbrains.skiko:skiko:0.7.12:
           - Unmatched attributes:
               - Provides org.gradle.category 'library' but the consumer didn't ask for it
               - Provides org.gradle.libraryelements 'jar' but the consumer didn't ask for it
               - Provides org.gradle.status 'release' but the consumer didn't ask for it
               - Provides org.gradle.usage 'java-runtime' but the consumer didn't ask for it
               - Provides org.jetbrains.kotlin.platform.type 'jvm' but the consumer didn't ask for it

I know that technically I should use the multiplatform plugin (but it is not possible right now), but I am curious why release 1.0.1 fixed all the issues - and now they are back because of the skiko publication.

trychen commented 2 years ago

Have the same issues building fatjar with compose dependencies

jakobkmar commented 2 years ago

I tried to use a separate project with the Kotlin Multiplatform Gradle plugin, but the same issue comes up there as well.

Asking more generally: How can I get a list of all transitive dependencies of compose.desktop.common, using either the JVM or the Multiplatform Plugin. With all the methods I tried, I always get the "Cannot choose between the following variants" error.

jakobkmar commented 2 years ago

Related issue: https://github.com/JetBrains/skiko/issues/547


Generally speaking, the issue here is not if one uses the Kotlin Multiplatform plugin or not, but that Gradle does not have enough information about which artifact it should use.

To fix this, the Compose Gradle plugin should ask for the correct UI attribute. For this to work correctly, I think skiko has to provide the correct attributes with each artifact first. Also, skiko should add a namespace to the ui attribute - I also added that to the skiko issue.

Temporary fix

I found out that you can apparently "fix" this by using some weird Gradle behaviour, and ask for an ui attribute that does not even exist. Let's use awt, just because it makes sense in this case.

To apply this fix, do this, either using configurations.all or just for your configuration which you use to shade or include dependencies:

configurations.all {
    attributes {
        attribute(Attribute.of("ui", String::class.java), "awt")
    }
}

kirill-grouchnikov commented 2 years ago

Some time ago I was running into Cannot choose between the following variants and came up with this snippet:

tasks.register("printRuntimeDependencies") {
    println("Project runtime dependencies:")
    allprojects {
        println()
        println("-------- ${project.name} --------")
        project.configurations.matching { it.name == "desktopRuntimeClasspath" }
            .matching { !it.allDependencies.isEmpty() }
            .forEach {
                it.allDependencies.forEach { dep ->
                    if (dep.group != null) {
                        println("${dep.group}:${dep.name}:${dep.version}")
                    }
                }
            }
    }
}
mikehearn commented 1 year ago

@jakobkmar Thank you for your workaround!