Skip to content

Commit

Permalink
Hack attributes to workaround Gradle bugs (#173)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
aSemy authored Feb 23, 2024
1 parent d241307 commit 31502f9
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<Configuration> =
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
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T : Named> : AttributeCompatibilityRule<T> {
override fun execute(details: CompatibilityCheckDetails<T>): 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<Usage>()
internal class CategoryHackRule : AttributeHackCompatibilityRule<Category>()
internal class BundlingHackRule : AttributeHackCompatibilityRule<Bundling>()
internal class TargetJvmEnvironmentHackRule : AttributeHackCompatibilityRule<TargetJvmEnvironment>()
internal class LibraryElementsHackRule : AttributeHackCompatibilityRule<LibraryElements>()

/**
* @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)
}
}
}
113 changes: 113 additions & 0 deletions modules/dokkatoo-plugin/src/testFunctional/kotlin/AttributeHackTest.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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" })
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,18 +240,18 @@ class MultiModuleFunctionalTest : FunSpec({
}

context("relocated project") {
originalProject.runner
relocatedProject.runner
.addArguments(
"clean",
"--build-cache",
)
.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),
)
}
}
Expand All @@ -261,6 +261,7 @@ class MultiModuleFunctionalTest : FunSpec({
":dokkatooGenerate",
"--stacktrace",
"--build-cache",
"-D" + "org.gradle.caching.debug=true"
)
.forwardOutput()
.build {
Expand Down

0 comments on commit 31502f9

Please sign in to comment.