Skip to content

Commit 80ab7e9

Browse files
Rework the Gradle attributes to make them integrate better with the ecosystem (#4334)
Unfortunately, Gradle's matching algorithm prioritises the number of attributes matched over the actual attribute values. (Note that if a candidate does not have a requested attribute this still counts as a match!) This behaviour makes it impossible to strictly match Configurations, and leads to situations like this where two 'ecosystems' step on each other's toes. The solution is in two parts: 1. Reduce the number of attributes DGP uses. This prevents the matching algorithm from over-matching based on attributes alone. The Bundling attribute is removed, since this is not important for DGP classpaths. 4. (As suggested by @martinbonnin) DGP configurations use a custom value for the `Usage` attribute, and use a one-way `AttributeCompatibilityRule` so DGP can still resolve dependencies. (A one-way rule should minimise ugly issues [where rules can leak](gradle/gradle#30921).) See the KDoc of `DokkaJavaRuntimeUsageCompatibilityRule` for details. --------- Co-authored-by: Martin Bonnin <[email protected]>
1 parent f3e834c commit 80ab7e9

File tree

7 files changed

+155
-120
lines changed

7 files changed

+155
-120
lines changed

dokka-runners/dokka-gradle-plugin/src/main/kotlin/DokkaBasePlugin.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import org.gradle.api.NamedDomainObjectContainer
1010
import org.gradle.api.Plugin
1111
import org.gradle.api.Project
1212
import org.gradle.api.artifacts.Configuration
13+
import org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE
1314
import org.gradle.api.file.ProjectLayout
1415
import org.gradle.api.file.RegularFileProperty
1516
import org.gradle.api.logging.Logger
@@ -25,6 +26,7 @@ import org.jetbrains.dokka.gradle.dependencies.BaseDependencyManager
2526
import org.jetbrains.dokka.gradle.dependencies.DokkaAttribute.Companion.DokkaClasspathAttribute
2627
import org.jetbrains.dokka.gradle.dependencies.DokkaAttribute.Companion.DokkaFormatAttribute
2728
import org.jetbrains.dokka.gradle.dependencies.DokkaAttribute.Companion.DokkaModuleComponentAttribute
29+
import org.jetbrains.dokka.gradle.dependencies.DokkaJavaRuntimeUsageCompatibilityRule
2830
import org.jetbrains.dokka.gradle.engine.parameters.DokkaSourceSetSpec
2931
import org.jetbrains.dokka.gradle.engine.parameters.KotlinPlatform
3032
import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier
@@ -152,6 +154,10 @@ constructor(
152154
attribute(DokkaFormatAttribute)
153155
attribute(DokkaModuleComponentAttribute)
154156
attribute(DokkaClasspathAttribute)
157+
158+
attribute(USAGE_ATTRIBUTE) {
159+
compatibilityRules.add(DokkaJavaRuntimeUsageCompatibilityRule::class)
160+
}
155161
}
156162
}
157163

dokka-runners/dokka-gradle-plugin/src/main/kotlin/dependencies/DokkaAttribute.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ interface DokkaAttribute {
3939
}
4040

4141
@InternalDokkaGradlePluginApi
42+
@Suppress("ConstPropertyName")
4243
companion object {
4344
/**
4445
* Describes the type of generated output that Dokka generates.
@@ -65,5 +66,15 @@ interface DokkaAttribute {
6566
*/
6667
val DokkaClasspathAttribute: Attribute<String> =
6768
Attribute("org.jetbrains.dokka.classpath")
69+
70+
/**
71+
* The [org.gradle.api.attributes.Usage] attribute for Dokka JARs.
72+
*
73+
* We are not using [org.gradle.api.attributes.Usage.JAVA_RUNTIME] because this would create
74+
* two outgoing variants exposing JARs and potentially confuse consumers.
75+
*
76+
* See https://github.com/adamko-dev/dokkatoo/issues/165
77+
*/
78+
const val DokkaJavaRuntimeUsage = "dokka-java-runtime"
6879
}
6980
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2014 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
package org.jetbrains.dokka.gradle.dependencies
5+
6+
import org.gradle.api.attributes.AttributeCompatibilityRule
7+
import org.gradle.api.attributes.CompatibilityCheckDetails
8+
import org.gradle.api.attributes.Usage
9+
10+
/**
11+
* # Dokka Java Runtime Usage Compatibility Rule
12+
*
13+
* If the consumer requests [DokkaAttribute.DokkaJavaRuntimeUsage],
14+
* then [Usage.JAVA_RUNTIME] is compatible.
15+
*
16+
* If the consumer requests [Usage.JAVA_RUNTIME], then do not apply the rule.
17+
*
18+
* ### Context
19+
*
20+
* When a project has both a JVM (e.g. Java or Kotlin/JVM) plugin and Dokka plugin,
21+
* both plugins add consumable Configurations:
22+
* - The traditional 'project JAR' one, containing the compiled JAR of the project
23+
* (e.g. `runtimeElements`).
24+
* - The Dokka one, containing Dokka plugins required when aggregating the consumable Dokka module
25+
* (e.g. [org.jetbrains.dokka.gradle.dependencies.DependencyContainerNames.publicationPluginClasspathApiOnlyConsumable]).
26+
*
27+
* Since both Configurations contain JARs, and have transitive dependencies on JARs,
28+
* it is logical for both to use attributes that describe them as 'contains Java classpath JARs'
29+
* (e.g. [Usage.JAVA_RUNTIME]).
30+
* However, this creates a tension when a consuming project, also with Dokka and JVM plugins applied,
31+
* depends on this project:
32+
* - The Dokka plugin wants the 'Dokka plugins' Configuration.
33+
* - The Java plugin wants the 'project JAR' Configuration.
34+
* - Both must resolve the dependencies with [Usage.JAVA_RUNTIME] to include transitive dependencies.
35+
* - Since both have 'contains Java classpath JARs' attributes, when they are resolved
36+
* Gradle can't differentiate between them.
37+
* - What happens is the Java plugin ends up resolving the 'Dokka plugins' Configuration,
38+
* which leads to extremely confusing errors and behaviours that are very hard to diagnose.
39+
* See https://github.com/adamko-dev/dokkatoo/issues/165
40+
*
41+
* Basically, Gradle's attribute-matching algorithm is too lenient,
42+
* and does not provide a mechanism of making matching strictly require on a specific attribute.
43+
*
44+
* ### Fix
45+
*
46+
* So, how can Dokka fix this?
47+
*
48+
* First, here's what won't work:
49+
* Adding _more_ attributes to the 'Dokka plugins' Configuration,
50+
* like [org.jetbrains.dokka.gradle.dependencies.DokkaAttribute.DokkaClasspathAttribute],
51+
* - The Dokka plugin will be able to correctly resolve the 'Dokka plugins' Configuration.
52+
* - But the JVM plugin will still get confused, because the consumer will _ignore_ unknown attributes.
53+
* - It doesn't make sense for the JVM plugin to modify its attributes, since they are
54+
* already established and used in the general ecosystem.
55+
* (For similar reasons, Dokka shouldn't modify the Configurations of other plugins.)
56+
*
57+
* Instead, Dokka can use a custom [Usage] attribute value:
58+
* [org.jetbrains.dokka.gradle.dependencies.DokkaAttribute],
59+
* and a compatibility rule
60+
* [org.jetbrains.dokka.gradle.dependencies.DokkaJavaRuntimeUsageCompatibilityRule].
61+
*
62+
* - Dokka consumers are able to resolve the transitive dependencies of plugins
63+
* thanks to the compatibility rule.
64+
* - Traditional consumers disambiguate the Dokka plugins variants because the
65+
* compatibility rule is one way.
66+
* If the consumer asks for [Usage.JAVA_RUNTIME],
67+
* then the rule expresses no opinion.
68+
*/
69+
internal abstract class DokkaJavaRuntimeUsageCompatibilityRule : AttributeCompatibilityRule<Usage> {
70+
override fun execute(details: CompatibilityCheckDetails<Usage>) {
71+
details.run {
72+
if (
73+
consumerValue?.name == DokkaAttribute.DokkaJavaRuntimeUsage
74+
&&
75+
producerValue?.name == Usage.JAVA_RUNTIME
76+
) {
77+
compatible()
78+
}
79+
}
80+
}
81+
}

dokka-runners/dokka-gradle-plugin/src/main/kotlin/dependencies/FormatDependenciesManager.kt

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,18 @@ import org.gradle.api.NamedDomainObjectProvider
77
import org.gradle.api.Project
88
import org.gradle.api.artifacts.Configuration
99
import org.gradle.api.attributes.AttributeContainer
10-
import org.gradle.api.attributes.Bundling.BUNDLING_ATTRIBUTE
11-
import org.gradle.api.attributes.Bundling.EXTERNAL
1210
import org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE
1311
import org.gradle.api.attributes.Category.LIBRARY
1412
import org.gradle.api.attributes.LibraryElements.JAR
1513
import org.gradle.api.attributes.LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE
16-
import org.gradle.api.attributes.Usage.JAVA_RUNTIME
1714
import org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE
1815
import org.gradle.api.attributes.java.TargetJvmEnvironment.STANDARD_JVM
1916
import org.gradle.api.attributes.java.TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE
2017
import org.gradle.api.model.ObjectFactory
21-
import org.gradle.kotlin.dsl.dependencies
2218
import org.gradle.kotlin.dsl.named
2319
import org.jetbrains.dokka.gradle.dependencies.DokkaAttribute.Companion.DokkaClasspathAttribute
2420
import org.jetbrains.dokka.gradle.dependencies.DokkaAttribute.Companion.DokkaFormatAttribute
21+
import org.jetbrains.dokka.gradle.dependencies.DokkaAttribute.Companion.DokkaJavaRuntimeUsage
2522
import org.jetbrains.dokka.gradle.internal.*
2623

2724
/**
@@ -51,18 +48,18 @@ class FormatDependenciesManager(
5148
formatName = formatName,
5249
)
5350

54-
init {
55-
project.dependencies {
56-
applyAttributeHacks()
57-
}
58-
}
59-
60-
private fun AttributeContainer.jvmJar() {
61-
attribute(USAGE_ATTRIBUTE, objects.named(AttributeHackPrefix + JAVA_RUNTIME))
62-
attribute(CATEGORY_ATTRIBUTE, objects.named(AttributeHackPrefix + LIBRARY))
63-
attribute(BUNDLING_ATTRIBUTE, objects.named(AttributeHackPrefix + EXTERNAL))
64-
attribute(TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objects.named(AttributeHackPrefix + STANDARD_JVM))
65-
attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(AttributeHackPrefix + JAR))
51+
/**
52+
* Specifically request JVM JARs for any Dokka classpath.
53+
* I.e., uses [DokkaJavaRuntimeUsage] instead of [org.gradle.api.attributes.Usage.JAVA_RUNTIME].
54+
*
55+
* @see DokkaJavaRuntimeUsage
56+
* @see DokkaJavaRuntimeUsage
57+
*/
58+
private fun AttributeContainer.dokkaJvmJar() {
59+
attribute(USAGE_ATTRIBUTE, objects.named(DokkaJavaRuntimeUsage))
60+
attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(JAR))
61+
attribute(CATEGORY_ATTRIBUTE, objects.named(LIBRARY))
62+
attribute(TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objects.named(STANDARD_JVM))
6663
}
6764

6865
//region Dokka Generator Plugins
@@ -93,7 +90,7 @@ class FormatDependenciesManager(
9390
extendsFrom(dokkaPluginsClasspath)
9491
isTransitive = false
9592
attributes {
96-
jvmJar()
93+
dokkaJvmJar()
9794
attribute(DokkaFormatAttribute, formatAttributes.format.name)
9895
attribute(DokkaClasspathAttribute, baseAttributes.dokkaPlugins.name)
9996
}
@@ -122,7 +119,7 @@ class FormatDependenciesManager(
122119
resolvable()
123120
extendsFrom(dokkaPublicationPluginClasspath.get())
124121
attributes {
125-
jvmJar()
122+
dokkaJvmJar()
126123
attribute(DokkaFormatAttribute, formatAttributes.format.name)
127124
attribute(DokkaClasspathAttribute, baseAttributes.dokkaPublicationPlugins.name)
128125
}
@@ -153,7 +150,7 @@ class FormatDependenciesManager(
153150
consumable()
154151
extendsFrom(dokkaPublicationPluginClasspathApiOnly)
155152
attributes {
156-
jvmJar()
153+
dokkaJvmJar()
157154
attribute(DokkaFormatAttribute, formatAttributes.format.name)
158155
attribute(DokkaClasspathAttribute, baseAttributes.dokkaPublicationPlugins.name)
159156
}
@@ -202,7 +199,7 @@ class FormatDependenciesManager(
202199
extendsFrom(dokkaGeneratorClasspath.get())
203200

204201
attributes {
205-
jvmJar()
202+
dokkaJvmJar()
206203
attribute(DokkaFormatAttribute, formatAttributes.format.name)
207204
attribute(DokkaClasspathAttribute, baseAttributes.dokkaGenerator.name)
208205
}

dokka-runners/dokka-gradle-plugin/src/main/kotlin/dependencies/attributesHack.kt

Lines changed: 0 additions & 74 deletions
This file was deleted.

dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/AttributeHackTest.kt renamed to dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/DokkaDependenciesCompatibilityTest.kt

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44
package org.jetbrains.dokka.gradle
55

66
import io.kotest.core.spec.style.FunSpec
7+
import io.kotest.matchers.string.shouldNotBeBlank
78
import org.jetbrains.dokka.gradle.internal.DokkaConstants.DOKKA_VERSION
89
import org.jetbrains.dokka.gradle.utils.*
910

10-
11-
class AttributeHackTest : FunSpec({
12-
context("verify that DGP does not interfere with JAR Configurations") {
11+
/**
12+
* Test Dokka creates Configurations that do not interfere with
13+
* the dependency resolution of other JVM plugins.
14+
*/
15+
class DokkaDependenciesCompatibilityTest : FunSpec({
16+
context("verify that DGP does not interfere with resolving JAR Configurations") {
1317

1418
val project = initProject()
1519

@@ -23,7 +27,19 @@ class AttributeHackTest : FunSpec({
2327
.forwardOutput()
2428
.build {
2529
test("resolving JARs from a Dokka-enabled project should not contain Dokka plugin JARs") {
26-
output.shouldNotContainAnyOf(
30+
31+
val fileCoords = output
32+
.substringAfter("--- fileCoords ---", "")
33+
.substringBefore("--- fileCoords ---", "")
34+
35+
fileCoords.shouldNotBeBlank()
36+
37+
fileCoords.shouldContainAll(
38+
"project :subproject-with-dokka",
39+
"org.jetbrains.kotlin:kotlin-stdlib",
40+
)
41+
42+
fileCoords.shouldNotContainAnyOf(
2743
"org.jetbrains.dokka",
2844
"all-modules-page-plugin",
2945
)
@@ -105,7 +121,9 @@ private fun initProject(
105121
| }
106122
| inputs.files(jarFilesResolver).withPropertyName("jarFilesResolver")
107123
| doLast {
124+
| println("--- fileCoords ---")
108125
| println(fileCoords.get().joinToString("\n"))
126+
| println("--- fileCoords ---")
109127
| }
110128
|}
111129
|

0 commit comments

Comments
 (0)