Issue
I'm migrating from Gradle to Bazel.
I had in my gradle build a testImplementation
dependency to io.kotest:kotest-runner-junit5:5.4.2
. It works perfectly.
I add the same dependency to my Bazel config files (WORKSPACE and BUILD), but I get compilation errors, as if the library doesn't exist.
I go and check if Bazel doesn't bring transitive dependencies, but it does.
I check the POM of the library and it turns out it has no dependencies.
I see in maven there's another called io.kotest:kotest-runner-junit5-jvm:5.4.2
.
I use that one instead. Voilá, it works!
But why? how is gradle picking the -jvm
artifact instead?
Solution
There are 3 parts to your question
- How does Gradle what variants are available?
- How does Gradle find the variants?
- How does Gradle figure out how to pick the right variant?
The short answer is that
- Gradle publishes an additional Module Metadata file that contains info on what variants are available, and where to find them.
- Gradle doesn't use just the Maven
group:artifact:version
coordinates to find dependencies - it uses variant attributes to 'tag' available modules and the requests for modules, so it can match the requested dependency with those available in a very fine grained way.
Kotlin Multiplatform Variants
Kotlin Multiplatform libraries publish several artifacts: a 'common' published artifact, and a variant for each platform that it targets.
For io.kotest:kotest-runner-junit5
that means there's
A 'common' library
https://repo1.maven.org/maven2/io/kotest/kotest-runner-junit5/5.4.2/
and a
-jvm
varianthttps://repo1.maven.org/maven2/io/kotest/kotest-runner-junit5-jvm/5.4.2/
Typically dependencies are determined by Maven POM and maven-metadata.xml
files. That will be what Bazel is doing. The same is true for a project that uses Maven. So how does Gradle figure out what variants are available, and which ones to use?
Variant availability: Gradle Module Metadata
Gradle publishes an additional metadata file. https://docs.gradle.org/current/userguide/publishing_gradle_module_metadata.htm. It's like a pom.xml
, but with a lot more info. The extension is .module
, but the content is JSON.
Looking in https://repo1.maven.org/maven2/io/kotest/kotest-runner-junit5/5.4.2/, we can see the file.
Because it's JSON, we can look inside. There's some metadata
{
"formatVersion": "1.1",
"component": {
"group": "io.kotest",
"module": "kotest-runner-junit5",
"version": "5.4.2",
"attributes": {
"org.gradle.status": "release"
}
},
...
There's also a variants
array. One of the variants is the -jvm
variant, as well as a available-at.url
, which is a relative path linking to the available variants within the Maven repository.
...
"variants": [
...
{
"name": "jvmRuntimeElements-published",
"attributes": {
"org.gradle.category": "library",
"org.gradle.libraryelements": "jar",
"org.gradle.usage": "java-runtime",
"org.jetbrains.kotlin.platform.type": "jvm"
},
"available-at": {
"url": "../../kotest-runner-junit5-jvm/5.4.2/kotest-runner-junit5-jvm-5.4.2.module",
"group": "io.kotest",
"module": "kotest-runner-junit5-jvm",
"version": "5.4.2"
}
}
...
]
}
That's how Gradle discovers the available variants.
Variant selection: Attribute matching
There's actually several variants in the module, and there would be even more if more Kotlin Multiplatform targets were enabled, so the final question "how does Gradle figure out what variant is needed?"
The answer comes from the "attributes"
that are associated with the variant. They're just key-value strings that Gradle uses to match what's required, to what's available.
https://docs.gradle.org/current/userguide/variant_attributes.html#attribute_matching
The attributes might say
I want a Java 8 JAR for org.company:some-artifact:1.0.0
or
I want a Kotlin Native 1.7.0 source files for io.kotest:something:2.0.0
They're just key-value strings, so they can really be anything. I've created attributes for sharing TypeScript files, or JaCoCo XML report files.
Why do we never see these attributes when we write Gradle files?
When you add a dependency in Gradle
// build.gradle.kts
plugins {
kotlin("jvm") version "1.7.20"
}
dependencies {
testImplementation("io.kotest:kotest-runner-junit5:5.4.2")
}
There's no attributes. So how does Gradle know to select the -jvm
variant?
Gradle sees you've added a dependency using testImplementation
, which is a Configuration.
(Aside: I think the name 'Configuration' is confusing. It's not configuration for how the project behaves. I like to think of it more like how a group of naval warships might have a 'configuration' for battle, or a 'configuration' for loading supplies. It's more about the 'shape', and it's not about controlling Gradle properties or actions.)
When Configurations are defined, they're also tagged with attributes, which Gradle will use to play matchmaker between the request for "kotest-runner-junit5" and what it discovers in the registered repositories
In the case of testImplementation("io.kotest:kotest-runner-junit5:5.4.2")
, Gradle can see that testImplementation
has Attributes that say "I need a JVM variant", and it can use that to find a matching dependency using the module metadata in Maven Central.
Answered By - aSemy
Answer Checked By - Katrina (JavaFixing Volunteer)