diff --git a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/dependencies/DokkaAttribute.kt b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/dependencies/DokkaAttribute.kt index 7a170aea7d..d65c253d42 100644 --- a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/dependencies/DokkaAttribute.kt +++ b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/dependencies/DokkaAttribute.kt @@ -65,5 +65,14 @@ interface DokkaAttribute { */ val DokkaClasspathAttribute: Attribute = Attribute("org.jetbrains.dokka.classpath") + + /** + * The usage attribute for Dokka jars. + * We are not using [org.gradle.api.attributes.Usage.JAVA_RUNTIME] because this would create + * two outgoing variants exposing jars and potentially confuse consumers. + * + * See https://github.com/adamko-dev/dokkatoo/issues/165 + */ + val DokkaJavaRuntimeUsage = "dokka-java-runtime" } } diff --git a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/dependencies/FormatDependenciesManager.kt b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/dependencies/FormatDependenciesManager.kt index 6a4d551715..1b92acbaf8 100644 --- a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/dependencies/FormatDependenciesManager.kt +++ b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/dependencies/FormatDependenciesManager.kt @@ -22,6 +22,7 @@ import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.named import org.jetbrains.dokka.gradle.dependencies.DokkaAttribute.Companion.DokkaClasspathAttribute import org.jetbrains.dokka.gradle.dependencies.DokkaAttribute.Companion.DokkaFormatAttribute +import org.jetbrains.dokka.gradle.dependencies.DokkaAttribute.Companion.DokkaJavaRuntimeUsage import org.jetbrains.dokka.gradle.internal.* /** @@ -51,18 +52,13 @@ class FormatDependenciesManager( formatName = formatName, ) - init { - project.dependencies { - applyAttributeHacks() - } - } - private fun AttributeContainer.jvmJar() { - attribute(USAGE_ATTRIBUTE, objects.named(AttributeHackPrefix + JAVA_RUNTIME)) - attribute(CATEGORY_ATTRIBUTE, objects.named(AttributeHackPrefix + LIBRARY)) - attribute(BUNDLING_ATTRIBUTE, objects.named(AttributeHackPrefix + EXTERNAL)) - attribute(TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objects.named(AttributeHackPrefix + STANDARD_JVM)) - attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(AttributeHackPrefix + JAR)) + attribute(USAGE_ATTRIBUTE, objects.named(DokkaAttribute.DokkaJavaRuntimeUsage)) + attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(JAR)) + + attribute(CATEGORY_ATTRIBUTE, objects.named(LIBRARY)) + attribute(BUNDLING_ATTRIBUTE, objects.named(EXTERNAL)) + attribute(TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objects.named(STANDARD_JVM)) } //region Dokka Generator Plugins diff --git a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/dependencies/attributesHack.kt b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/dependencies/attributesHack.kt deleted file mode 100644 index 8961235f19..0000000000 --- a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/dependencies/attributesHack.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ -package org.jetbrains.dokka.gradle.dependencies - -import org.gradle.api.Named -import org.gradle.api.attributes.* -import org.gradle.api.attributes.Bundling.BUNDLING_ATTRIBUTE -import org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE -import org.gradle.api.attributes.LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE -import org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE -import org.gradle.api.attributes.java.TargetJvmEnvironment -import org.gradle.api.attributes.java.TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE -import org.gradle.kotlin.dsl.DependencyHandlerScope -import org.gradle.kotlin.dsl.add - -/** - * Workaround for requiring stricter Gradle dependency resolution. - * - * Basically, even though a [Configuration][org.gradle.api.artifacts.Configuration] might specify - * some [Attribute]s, Gradle can, in some situations, randomly ignore them, leading to - * files leaking between Configurations unexpectedly. This is a particular problem with JARs. - * DGP needs to both resolve JARs from Maven Central, and also provide JARs to other - * subprojects. - * - * To work around this: - * - * 1. When requesting or providing attributes, DGP adds a prefix ([AttributeHackPrefix]) to - * the JAR specific Attribute values. - * 2. When DGP shares files, the prefix prevents Gradle from getting confused with other - * Configurations with similar Attributes. - * 3. DGP adds some [AttributeCompatibilityRule]s for the JAR attributes, so that DGP - * can ignore the prefix when **consuming**. - */ -internal abstract class AttributeHackCompatibilityRule : AttributeCompatibilityRule { - override fun execute(details: CompatibilityCheckDetails): Unit = details.run { - val consumerName = consumerValue?.name?.substringAfter(AttributeHackPrefix) ?: return - val producerName = producerValue?.name?.substringAfter(AttributeHackPrefix) ?: return - if (consumerName == producerName) { - compatible() - } - } -} - -internal const val AttributeHackPrefix = "DGP~" - -internal class UsageHackRule : AttributeHackCompatibilityRule() -internal class CategoryHackRule : AttributeHackCompatibilityRule() -internal class BundlingHackRule : AttributeHackCompatibilityRule() -internal class TargetJvmEnvironmentHackRule : AttributeHackCompatibilityRule() -internal class LibraryElementsHackRule : AttributeHackCompatibilityRule() - -/** - * @see AttributeHackCompatibilityRule - */ -internal fun DependencyHandlerScope.applyAttributeHacks() { - attributesSchema { - attribute(USAGE_ATTRIBUTE) { - compatibilityRules.add(UsageHackRule::class) - } - attribute(CATEGORY_ATTRIBUTE) { - compatibilityRules.add(CategoryHackRule::class) - } - attribute(BUNDLING_ATTRIBUTE) { - compatibilityRules.add(BundlingHackRule::class) - } - attribute(TARGET_JVM_ENVIRONMENT_ATTRIBUTE) { - compatibilityRules.add(TargetJvmEnvironmentHackRule::class) - } - attribute(LIBRARY_ELEMENTS_ATTRIBUTE) { - compatibilityRules.add(LibraryElementsHackRule::class) - } - } -} diff --git a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/formats/DokkaFormatPlugin.kt b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/formats/DokkaFormatPlugin.kt index d2da140930..661062644f 100644 --- a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/formats/DokkaFormatPlugin.kt +++ b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/formats/DokkaFormatPlugin.kt @@ -4,13 +4,17 @@ package org.jetbrains.dokka.gradle.formats import org.gradle.api.Action +import org.gradle.api.Named import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.ExternalModuleDependency import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.attributes.AttributeCompatibilityRule import org.gradle.api.attributes.AttributeContainer +import org.gradle.api.attributes.CompatibilityCheckDetails +import org.gradle.api.attributes.Usage import org.gradle.api.file.FileSystemOperations import org.gradle.api.file.ProjectLayout import org.gradle.api.logging.Logging @@ -24,12 +28,15 @@ import org.jetbrains.dokka.gradle.adapters.AndroidAdapter import org.jetbrains.dokka.gradle.adapters.JavaAdapter import org.jetbrains.dokka.gradle.adapters.KotlinAdapter import org.jetbrains.dokka.gradle.dependencies.DependencyContainerNames +import org.jetbrains.dokka.gradle.dependencies.DokkaAttribute import org.jetbrains.dokka.gradle.dependencies.DokkaAttribute.Companion.DokkaClasspathAttribute import org.jetbrains.dokka.gradle.dependencies.DokkaAttribute.Companion.DokkaFormatAttribute import org.jetbrains.dokka.gradle.dependencies.FormatDependenciesManager +import org.jetbrains.dokka.gradle.internal.Attribute import org.jetbrains.dokka.gradle.internal.InternalDokkaGradlePluginApi import org.jetbrains.dokka.gradle.internal.PluginFeaturesService.Companion.pluginFeaturesService import javax.inject.Inject +import kotlin.jvm.java /** * Base Gradle Plugin for setting up a Dokka Publication for a specific output format. @@ -141,6 +148,44 @@ abstract class DokkaFormatPlugin( } //endregion } + + /** + * When using Dokka, a given project exposes 2 "jar" outgoing variants: + * - The traditional one, containing the code of the project. + * - The Dokka one, containing dokka plugins. + * + * This creates a tension: + * - Dokka wants to resolve transitive dependencies of plugins using `java-runtime` + * usage. + * - But by using `java-runtime`, it potentially confuses all other regular consumers + * that are now going to resolve plugins when they really wanted the "traditional" jar file. + * + * To solve this, we use `dokka-java-runtime` for usage and a compatibility + * rule: + * - Dokka consumers are able to resolve plugins transitive dependencies thanks to + * the compatibility rule. + * - Dokka consumers disambiguate the traditional variants by forcing the `org.jetbrains.dokka.classpath` + * attribute on all consumable configurations to a "none" value. + * - Traditional consumers disambiguate the Dokka plugins variants because the + * compatibility rule is one way. If the consumer asks for `java-runtime`, `dokka-java-runtime` + * is not considered compatible. + * + * See https://github.com/adamko-dev/dokkatoo/issues/165 + */ + target.dependencies.attributesSchema { + attribute(Usage.USAGE_ATTRIBUTE) { + compatibilityRules.add(DokkaCompatibilityRule::class.java) + } + } + target.configurations.configureEach { + if (isCanBeConsumed) { + attributes { + if (this.contains(Usage.USAGE_ATTRIBUTE) && !this.contains(DokkaClasspathAttribute)) { + attribute(DokkaClasspathAttribute, "none") + } + } + } + } } } @@ -232,3 +277,11 @@ abstract class DokkaFormatPlugin( private val logger = Logging.getLogger(DokkaFormatPlugin::class.java) } } + +internal open class DokkaCompatibilityRule : AttributeCompatibilityRule { + override fun execute(details: CompatibilityCheckDetails): Unit = details.run { + if (consumerValue?.name == DokkaAttribute.DokkaJavaRuntimeUsage && producerValue?.name == Usage.JAVA_RUNTIME) { + compatible() + } + } +} diff --git a/dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/DokkaPluginFunctionalTest.kt b/dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/DokkaPluginFunctionalTest.kt index 46a0585a00..d8250c1be9 100644 --- a/dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/DokkaPluginFunctionalTest.kt +++ b/dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/DokkaPluginFunctionalTest.kt @@ -135,7 +135,6 @@ class DokkaPluginFunctionalTest : FunSpec({ fun checkVariant(format: String) { @Suppress("LocalVariableName") val Format = format.uppercaseFirstChar() - variants shouldContain /* language=text */ """ |-------------------------------------------------- |Variant dokka${Format}ModuleOutputDirectoriesConsumable~internal @@ -146,6 +145,7 @@ class DokkaPluginFunctionalTest : FunSpec({ | - :DokkaPluginFunctionalTest:unspecified (default capability) |Attributes | - org.gradle.usage = org.jetbrains.dokka + | - org.jetbrains.dokka.classpath = none | - org.jetbrains.dokka.format = $format | - org.jetbrains.dokka.module-component = ModuleOutputDirectories |Artifacts @@ -195,11 +195,11 @@ class DokkaPluginFunctionalTest : FunSpec({ |[Internal Dokka Configuration] Dokka Generator runtime classpath for $format - will be used in Dokka Worker. Should contain all transitive dependencies, plugins (and their transitive dependencies), so Dokka Worker can run. | |Attributes - | - org.gradle.category = DGP~library - | - org.gradle.dependency.bundling = DGP~external - | - org.gradle.jvm.environment = DGP~standard-jvm - | - org.gradle.libraryelements = DGP~jar - | - org.gradle.usage = DGP~java-runtime + | - org.gradle.category = library + | - org.gradle.dependency.bundling = external + | - org.gradle.jvm.environment = standard-jvm + | - org.gradle.libraryelements = jar + | - org.gradle.usage = dokka-java-runtime | - org.jetbrains.dokka.classpath = dokka-generator | - org.jetbrains.dokka.format = $format |Extended Configurations @@ -221,11 +221,11 @@ class DokkaPluginFunctionalTest : FunSpec({ |[Internal Dokka Configuration] Resolves Dokka Plugins classpath for $format. Fetch only the plugins (no transitive dependencies) for use in the Dokka JSON Configuration. | |Attributes - | - org.gradle.category = DGP~library - | - org.gradle.dependency.bundling = DGP~external - | - org.gradle.jvm.environment = DGP~standard-jvm - | - org.gradle.libraryelements = DGP~jar - | - org.gradle.usage = DGP~java-runtime + | - org.gradle.category = library + | - org.gradle.dependency.bundling = external + | - org.gradle.jvm.environment = standard-jvm + | - org.gradle.libraryelements = jar + | - org.gradle.usage = dokka-java-runtime | - org.jetbrains.dokka.classpath = dokka-plugins | - org.jetbrains.dokka.format = $format |Extended Configurations @@ -236,11 +236,11 @@ class DokkaPluginFunctionalTest : FunSpec({ |[Internal Dokka Configuration] Resolves Dokka Plugins classpath for a $format Publication (consisting of one or more Dokka Modules). | |Attributes - | - org.gradle.category = DGP~library - | - org.gradle.dependency.bundling = DGP~external - | - org.gradle.jvm.environment = DGP~standard-jvm - | - org.gradle.libraryelements = DGP~jar - | - org.gradle.usage = DGP~java-runtime + | - org.gradle.category = library + | - org.gradle.dependency.bundling = external + | - org.gradle.jvm.environment = standard-jvm + | - org.gradle.libraryelements = jar + | - org.gradle.usage = dokka-java-runtime | - org.jetbrains.dokka.classpath = dokka-publication-plugins | - org.jetbrains.dokka.format = $format |Extended Configurations