From 31502f93a5ca13401677c6e1e1f7f27f09f6a3fa Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Fri, 23 Feb 2024 23:22:35 +0100 Subject: [PATCH] Hack attributes to workaround Gradle bugs (#173) * Hack attributes to workaround Gradle bugs Also, remove unused 'incoming' Configuration * fixup! Hack attributes to workaround Gradle bugs * fixup! Hack attributes to workaround Gradle bugs * fixup! Hack attributes to workaround Gradle bugs * fixup! Hack attributes to workaround Gradle bugs --- .../dependencies/FormatDependenciesManager.kt | 29 ++--- .../kotlin/dependencies/attributesHack.kt | 70 +++++++++++ .../kotlin/AttributeHackTest.kt | 113 ++++++++++++++++++ .../kotlin/DokkatooPluginFunctionalTest.kt | 34 ++---- .../kotlin/MultiModuleFunctionalTest.kt | 11 +- 5 files changed, 210 insertions(+), 47 deletions(-) create mode 100644 modules/dokkatoo-plugin/src/main/kotlin/dependencies/attributesHack.kt create mode 100644 modules/dokkatoo-plugin/src/testFunctional/kotlin/AttributeHackTest.kt diff --git a/modules/dokkatoo-plugin/src/main/kotlin/dependencies/FormatDependenciesManager.kt b/modules/dokkatoo-plugin/src/main/kotlin/dependencies/FormatDependenciesManager.kt index af9867f7..b8b6ad15 100644 --- a/modules/dokkatoo-plugin/src/main/kotlin/dependencies/FormatDependenciesManager.kt +++ b/modules/dokkatoo-plugin/src/main/kotlin/dependencies/FormatDependenciesManager.kt @@ -51,26 +51,19 @@ class FormatDependenciesManager( objects = objects, ) - private fun AttributeContainer.jvmJar() { - attribute(USAGE_ATTRIBUTE, objects.named(JAVA_RUNTIME)) - attribute(CATEGORY_ATTRIBUTE, objects.named(LIBRARY)) - attribute(BUNDLING_ATTRIBUTE, objects.named(EXTERNAL)) - attribute(TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objects.named(STANDARD_JVM)) - attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(JAR)) - } - - /** Collect [BaseDependencyManager.declaredDependencies]. */ - val incoming: NamedDomainObjectProvider = - project.configurations.register(configurationNames.dokkatooResolver) { - description = "Resolve Dokkatoo declared dependencies for $formatName." - resolvable() - extendsFrom(baseDependencyManager.declaredDependencies) - attributes { - attribute(USAGE_ATTRIBUTE, baseAttributes.dokkatooUsage) - attribute(DokkatooFormatAttribute, formatAttributes.format) - } + 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)) + } //region Dokka Generator Plugins /** diff --git a/modules/dokkatoo-plugin/src/main/kotlin/dependencies/attributesHack.kt b/modules/dokkatoo-plugin/src/main/kotlin/dependencies/attributesHack.kt new file mode 100644 index 00000000..4eca729e --- /dev/null +++ b/modules/dokkatoo-plugin/src/main/kotlin/dependencies/attributesHack.kt @@ -0,0 +1,70 @@ +package dev.adamko.dokkatoo.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.* + +/** + * Dumb hack to work around Gradle bugs/issues/deficiencies. + * + * 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. + * Dokkatoo 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, Dokkatoo adds a prefix ([AttributeHackPrefix]) to + * the JAR specific Attribute values. + * 2. When Dokkatoo shares files, the prefix prevents Gradle from getting confused with other + * Configurations with similar Attributes. + * 3. Dokkatoo adds some [AttributeCompatibilityRule]s for the JAR attributes, so that Dokkatoo + * 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 = "Dokkatoo~" + +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/modules/dokkatoo-plugin/src/testFunctional/kotlin/AttributeHackTest.kt b/modules/dokkatoo-plugin/src/testFunctional/kotlin/AttributeHackTest.kt new file mode 100644 index 00000000..b6775c1a --- /dev/null +++ b/modules/dokkatoo-plugin/src/testFunctional/kotlin/AttributeHackTest.kt @@ -0,0 +1,113 @@ +package dev.adamko.dokkatoo + +import dev.adamko.dokkatoo.internal.DokkatooConstants +import dev.adamko.dokkatoo.utils.* +import io.kotest.core.spec.style.FunSpec + + +class AttributeHackTest : FunSpec({ + context("verify that Dokkatoo does not interfere with JAR Configurations") { + + val project = initProject() + + project.runner + .addArguments( + ":subproject-without-dokkatoo:printJarFileCoords", + "--quiet", + "--stacktrace", + "--no-configuration-cache", + ) + .forwardOutput() + .build { + test("resolving JARs from a Dokkatoo-enabled project should not contain Dokka plugin JARs") { + output.shouldNotContainAnyOf( + "org.jetbrains.dokka", + "all-modules-page-plugin", + ) + } + } + } +}) + + +private fun initProject( + config: GradleProjectTest.() -> Unit = {}, +): GradleProjectTest { + return gradleKtsProjectTest("attribute-hack-test") { + + settingsGradleKts += """ + | + |include(":subproject-with-dokkatoo") + |include(":subproject-without-dokkatoo") + | + """.trimMargin() + + dir("subproject-with-dokkatoo") { + buildGradleKts = """ + |plugins { + | kotlin("multiplatform") version embeddedKotlinVersion + | id("dev.adamko.dokkatoo-html") version "${DokkatooConstants.DOKKATOO_VERSION}" + |} + | + |kotlin { + | jvm() + |} + """.trimMargin() + } + + dir("subproject-without-dokkatoo") { + + buildGradleKts = """ + |import org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE + |import org.gradle.api.attributes.Category.LIBRARY + |import org.gradle.api.attributes.LibraryElements.JAR + |import org.gradle.api.attributes.LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE + |import org.gradle.api.attributes.Usage.JAVA_RUNTIME + |import org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE + |import org.gradle.api.attributes.java.TargetJvmEnvironment.STANDARD_JVM + |import org.gradle.api.attributes.java.TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE + | + |plugins { + | `java-library` + |} + | + |val jarFiles: Configuration by configurations.creating { + | isCanBeResolved = false + | isCanBeConsumed = false + | isCanBeDeclared = true + |} + | + |val jarFilesResolver: Configuration by configurations.creating { + | isCanBeResolved = true + | isCanBeConsumed = false + | isCanBeDeclared = false + | extendsFrom(jarFiles) + | attributes { + | //attribute(USAGE_ATTRIBUTE, objects.named(JAVA_RUNTIME)) + | //attribute(CATEGORY_ATTRIBUTE, objects.named(LIBRARY)) + | //attribute(TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objects.named(STANDARD_JVM)) + | //attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(JAR)) + | //attribute(Attribute.of("org.jetbrains.kotlin.platform.type", String::class.java), "jvm") + | } + |} + | + |dependencies { + | jarFiles(project(":subproject-with-dokkatoo")) + |} + | + |val printJarFileCoords by tasks.registering { + | val fileCoords = jarFilesResolver.incoming.artifacts.resolvedArtifacts.map { artifacts -> + | artifacts.map { it.id.componentIdentifier.displayName } + | } + | inputs.files(jarFilesResolver).withPropertyName("jarFilesResolver") + | doLast { + | println(fileCoords.get().joinToString("\n")) + | } + |} + | + """.trimMargin() + } + + config() + } +} diff --git a/modules/dokkatoo-plugin/src/testFunctional/kotlin/DokkatooPluginFunctionalTest.kt b/modules/dokkatoo-plugin/src/testFunctional/kotlin/DokkatooPluginFunctionalTest.kt index a97e832c..c679b5b8 100644 --- a/modules/dokkatoo-plugin/src/testFunctional/kotlin/DokkatooPluginFunctionalTest.kt +++ b/modules/dokkatoo-plugin/src/testFunctional/kotlin/DokkatooPluginFunctionalTest.kt @@ -110,7 +110,6 @@ class DokkatooPluginFunctionalTest : FunSpec({ dokkatooConfigurations.shouldContainExactlyInAnyOrder( buildSet { - addAll(expectedFormats.map { "dokkatoo${it}Resolver" }) addAll(expectedFormats.map { "dokkatoo${it}GeneratorClasspathResolver" }) addAll(expectedFormats.map { "dokkatoo${it}ModuleOutputDirectoriesResolver" }) addAll(expectedFormats.map { "dokkatoo${it}PluginsClasspathIntransitiveResolver" }) @@ -124,19 +123,6 @@ class DokkatooPluginFunctionalTest : FunSpec({ ) { val format = Format.lowercase() - allConfigurations shouldContain /* language=text */ """ - |-------------------------------------------------- - |Configuration dokkatoo${Format}Resolver - |-------------------------------------------------- - |Resolve Dokkatoo declared dependencies for $format. - | - |Attributes - | - dev.adamko.dokkatoo.format = $format - | - org.gradle.usage = dev.adamko.dokkatoo - |Extended Configurations - | - dokkatoo - """.trimMargin() - allConfigurations shouldContain /* language=text */ """ |-------------------------------------------------- |Configuration dokkatoo${Format}GeneratorClasspathResolver @@ -146,11 +132,11 @@ class DokkatooPluginFunctionalTest : FunSpec({ |Attributes | - dev.adamko.dokkatoo.classpath = dokka-generator | - dev.adamko.dokkatoo.format = $format - | - org.gradle.category = library - | - org.gradle.dependency.bundling = external - | - org.gradle.jvm.environment = standard-jvm - | - org.gradle.libraryelements = jar - | - org.gradle.usage = java-runtime + | - org.gradle.category = Dokkatoo~library + | - org.gradle.dependency.bundling = Dokkatoo~external + | - org.gradle.jvm.environment = Dokkatoo~standard-jvm + | - org.gradle.libraryelements = Dokkatoo~jar + | - org.gradle.usage = Dokkatoo~java-runtime |Extended Configurations | - dokkatoo${Format}GeneratorClasspath """.trimMargin() @@ -164,11 +150,11 @@ class DokkatooPluginFunctionalTest : FunSpec({ |Attributes | - dev.adamko.dokkatoo.classpath = dokka-plugins | - dev.adamko.dokkatoo.format = $format - | - org.gradle.category = library - | - org.gradle.dependency.bundling = external - | - org.gradle.jvm.environment = standard-jvm - | - org.gradle.libraryelements = jar - | - org.gradle.usage = java-runtime + | - org.gradle.category = Dokkatoo~library + | - org.gradle.dependency.bundling = Dokkatoo~external + | - org.gradle.jvm.environment = Dokkatoo~standard-jvm + | - org.gradle.libraryelements = Dokkatoo~jar + | - org.gradle.usage = Dokkatoo~java-runtime |Extended Configurations | - dokkatooPlugin${Format} """.trimMargin() diff --git a/modules/dokkatoo-plugin/src/testFunctional/kotlin/MultiModuleFunctionalTest.kt b/modules/dokkatoo-plugin/src/testFunctional/kotlin/MultiModuleFunctionalTest.kt index 7e624746..e00be8d0 100644 --- a/modules/dokkatoo-plugin/src/testFunctional/kotlin/MultiModuleFunctionalTest.kt +++ b/modules/dokkatoo-plugin/src/testFunctional/kotlin/MultiModuleFunctionalTest.kt @@ -240,7 +240,7 @@ class MultiModuleFunctionalTest : FunSpec({ } context("relocated project") { - originalProject.runner + relocatedProject.runner .addArguments( "clean", "--build-cache", @@ -248,10 +248,10 @@ class MultiModuleFunctionalTest : FunSpec({ .forwardOutput() .build { test("clean tasks should run successfully") { - shouldHaveTasksWithOutcome( - ":clean" to SUCCESS, - ":subproject-hello:clean" to SUCCESS, - ":subproject-goodbye:clean" to SUCCESS, + shouldHaveTasksWithAnyOutcome( + ":clean" to listOf(UP_TO_DATE, SUCCESS), + ":subproject-hello:clean" to listOf(UP_TO_DATE, SUCCESS), + ":subproject-goodbye:clean" to listOf(UP_TO_DATE, SUCCESS), ) } } @@ -261,6 +261,7 @@ class MultiModuleFunctionalTest : FunSpec({ ":dokkatooGenerate", "--stacktrace", "--build-cache", + "-D" + "org.gradle.caching.debug=true" ) .forwardOutput() .build {