Skip to content

Commit

Permalink
Update DGP migration opt-in flags and messages (#3736)
Browse files Browse the repository at this point in the history
- Re-write the Dokka Gradle Plugin v1 & v2 messages.
- Update the opt-in flag for v2.
- Create a BuildService, so the messages are only logged once per project.
- re-implement how `gradle.properties` are set for Gradle tests
- Create test for V1 & V2 migration messages
- rename 'suppressV2Message' to 'enableV2.noWarn'
* re-run tasks in some tests now that Build Cache is enabled by default
* update message: V1 will be removed in 2.1.0
* update `getFlag()` to lazily evaluate the properties
* Workaround buggy BuildService behaviour gradle/gradle#17559
- Re-register the BuildService if it fails. Mark the first successfully registered BuildService as 'primary'. Only 'primary' BuildServices will log user-facing messages.
- Update MigrationMessagesTest to test BuildService classloader issues.
- Rename flag properties for consistency.
- Add `.nowarn` and `.noWarn` variants, because I really don't want to be constantly nagged by spellcheck.
- use `lazy(SYNCHRONIZED) {}` to better control logging, only log when necessary, and to avoid potential parallel issues.
* fix gradle properties - Avoid using `Properties()` because it needlessly escapes values, doesn't order values, and adds a timestamp.
  • Loading branch information
adam-enko authored Aug 20, 2024
1 parent 4fd8c0b commit 67c2c9e
Show file tree
Hide file tree
Showing 19 changed files with 684 additions and 142 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/*
* 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

import org.gradle.api.DefaultTask
Expand All @@ -13,11 +12,17 @@ import org.gradle.kotlin.dsl.withType
import org.gradle.util.GradleVersion
import org.jetbrains.dokka.DokkaDefaults

/**
* The OG Dokka Gradle Plugin. A.K.A. DGP Classic, or Dokka V1.
*
* This plugin is planned for removal.
*/
open class DokkaClassicPlugin : Plugin<Project> {
override fun apply(project: Project) {
if (GradleVersion.version(project.gradle.gradleVersion) < GradleVersion.version("5.6")) {
project.logger.warn("Dokka: Build is using unsupported gradle version, expected at least 5.6 but got ${project.gradle.gradleVersion}. This may result in strange errors")
}

if (project.shouldUseK2())
project.logger.warn(
"Dokka's K2 Analysis is being used. " +
Expand Down
36 changes: 18 additions & 18 deletions dokka-runners/dokka-gradle-plugin/src/main/kotlin/DokkaPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ package org.jetbrains.dokka.gradle

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.logging.Logging
import org.gradle.kotlin.dsl.apply
import org.jetbrains.dokka.gradle.formats.DokkaHtmlPlugin
import org.jetbrains.dokka.gradle.internal.DokkaInternalApi
import org.jetbrains.dokka.gradle.internal.ExperimentalFlags.Companion.dokkaMode
import org.jetbrains.dokka.gradle.internal.PluginFeaturesService.Companion.pluginFeaturesService
import javax.inject.Inject
import org.jetbrains.dokka.gradle.DokkaClassicPlugin as ClassicDokkaPlugin

Expand All @@ -23,25 +22,26 @@ abstract class DokkaPlugin
@DokkaInternalApi
constructor() : Plugin<Project> {

override fun apply(target: Project) {
val dokkaMode = target.dokkaMode()
if (dokkaMode.dokkatooEnabled) {
if (!dokkaMode.dokkatooEnabledQuietly) {
logger.lifecycle("[${target.displayName}] Dokka Gradle Plugin: Dokkatoo mode enabled")
}
with(target.pluginManager) {
apply(type = DokkaBasePlugin::class)

// auto-apply the custom format plugins
apply(type = DokkaHtmlPlugin::class)
}
override fun apply(project: Project) {
val pluginFeaturesService = project.pluginFeaturesService
if (pluginFeaturesService.v2PluginEnabled) {
applyV2(project)
} else {
logger.warn("[${target.path}] Dokka Gradle Plugin: classic mode, please migrate, see https://kotl.in/dokka-gradle-migration")
target.pluginManager.apply(ClassicDokkaPlugin::class)
applyV1(project)
}
}

companion object {
private val logger = Logging.getLogger(DokkaPlugin::class.java)
private fun applyV1(project: Project) {
project.pluginManager.apply(ClassicDokkaPlugin::class)
}

private fun applyV2(project: Project) {
with(project.pluginManager) {
apply(type = DokkaBasePlugin::class)
// auto-apply the HTML plugin
apply(type = DokkaHtmlPlugin::class)
}
}

companion object
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
* 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.internal

import org.gradle.api.Action
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.logging.Logging
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import org.gradle.kotlin.dsl.extra
import kotlin.LazyThreadSafetyMode.SYNCHRONIZED

/**
* Internal utility service for managing Dokka Plugin features and warnings.
*
* Using a [BuildService] is most useful for only logging a single warning for the whole project,
* regardless of how many subprojects have applied DGP.
*/
internal abstract class PluginFeaturesService : BuildService<PluginFeaturesService.Params> {

interface Params : BuildServiceParameters {
/** @see PluginFeaturesService.v2PluginEnabled */
val v2PluginEnabled: Property<Boolean>

/** @see [PluginFeaturesService.v2PluginNoWarn] */
val v2PluginNoWarn: Property<Boolean>

/** @see [PluginFeaturesService.primaryService] */
val primaryService: Property<Boolean>
}

/**
* Designate this [BuildService] as 'primary', meaning it should log messages to users.
* Non-primary services should not log messages.
*
* Why? Because Gradle is buggy. Sometimes registering a BuildService fails.
* See https://github.com/gradle/gradle/issues/17559.
* If service registration fails then re-register the service, but with a distinct name
* (so it doesn't clash with the existing but inaccessible BuildService), and don't mark it as 'primary'.
*
* @see org.jetbrains.dokka.gradle.internal.registerIfAbsent
*/
private val primaryService: Boolean get() = parameters.primaryService.getOrElse(false)

/**
* Whether DGP should use V2 [org.jetbrains.dokka.gradle.DokkaBasePlugin].
*
* Otherwise, fallback to V1 [org.jetbrains.dokka.gradle.DokkaClassicPlugin].
*/
internal val v2PluginEnabled: Boolean by lazy(SYNCHRONIZED) {
val v2PluginEnabled = parameters.v2PluginEnabled.getOrElse(false)

if (v2PluginEnabled) {
logV2PluginMessage()
} else {
logV1PluginMessage()
}

v2PluginEnabled
}

/** If `true`, suppress any messages regarding V2 mode. */
private val v2PluginNoWarn: Boolean
get() = parameters.v2PluginNoWarn.getOrElse(false)


private fun logV1PluginMessage() {
if (primaryService) {
logger.warn(
"""
|⚠ Warning: Dokka Gradle Plugin V1 mode is enabled
|
| V1 mode is deprecated, and will be removed in Dokka version 2.1.0
|
| Please migrate Dokka Gradle Plugin to V2. This will require updating your project.
| To get started check out the Dokka Gradle Plugin Migration guide
| https://kotl.in/dokka-gradle-migration
|
| Once you have prepared your project, enable V2 by adding
| $V2_PLUGIN_ENABLED_FLAG=true
| to your project's `gradle.properties`
|
| Please report any feedback or problems to Dokka GitHub Issues
| https://github.com/Kotlin/dokka/issues/
""".trimMargin().surroundWithBorder()
)
}
}

private fun logV2PluginMessage() {
if (primaryService && !v2PluginNoWarn) {
logger.lifecycle(
"""
|Dokka Gradle Plugin V2 is enabled ♡
|
| We would appreciate your feedback!
| Please report any feedback or problems to Dokka GitHub Issues
| https://github.com/Kotlin/dokka/issues/
|
| If you need help or advice, check out the migration guide
| https://kotl.in/dokka-gradle-migration
|
| You can suppress this message by adding
| $V2_PLUGIN_NO_WARN_FLAG=true
| to your project's `gradle.properties`
""".trimMargin().surroundWithBorder()
)
}
}

companion object {
private val logger = Logging.getLogger(PluginFeaturesService::class.java)

/** @see [PluginFeaturesService.v2PluginEnabled] */
internal const val V2_PLUGIN_ENABLED_FLAG =
"org.jetbrains.dokka.experimental.gradlePlugin.enableV2"

/** @see [PluginFeaturesService.v2PluginNoWarn] */
internal const val V2_PLUGIN_NO_WARN_FLAG =
"$V2_PLUGIN_ENABLED_FLAG.nowarn"

/** The same as [V2_PLUGIN_NO_WARN_FLAG], but it doesn't trigger spell-checks. */
private const val V2_PLUGIN_NO_WARN_FLAG_PRETTY =
"$V2_PLUGIN_ENABLED_FLAG.noWarn"

/**
* Register a new [PluginFeaturesService], or get an existing instance.
*/
val Project.pluginFeaturesService: PluginFeaturesService
get() {
val setFlags = Action<Params> {
v2PluginEnabled.set(getFlag(V2_PLUGIN_ENABLED_FLAG))
v2PluginNoWarn.set(getFlag(V2_PLUGIN_NO_WARN_FLAG_PRETTY).orElse(getFlag(V2_PLUGIN_NO_WARN_FLAG)))
}

return try {
gradle.sharedServices.registerIfAbsent(PluginFeaturesService::class) {
parameters(setFlags)
// This service was successfully registered, so it is considered 'primary'.
parameters.primaryService.set(true)
}.get()
} catch (ex: ClassCastException) {
try {
// Recover from Gradle bug: re-register the service, but don't mark it as 'primary'.
gradle.sharedServices.registerIfAbsent(
PluginFeaturesService::class,
classLoaderScoped = true,
) {
parameters(setFlags)
parameters.primaryService.set(false)
}.get()
} catch (ex: ClassCastException) {
throw GradleException(
"Failed to register BuildService. Please report this problem https://github.com/gradle/gradle/issues/17559",
ex
)
}
}
}

private fun Project.getFlag(flag: String): Provider<Boolean> =
providers
.gradleProperty(flag)
.forUseAtConfigurationTimeCompat()
.orElse(
// Note: Enabling/disabling features via extra-properties is only intended for unit tests.
// (Because org.gradle.testfixtures.ProjectBuilder doesn't support mocking Gradle properties.
// But maybe soon! https://github.com/gradle/gradle/pull/30002)
project
.provider { project.extra.properties[flag]?.toString() }
.forUseAtConfigurationTimeCompat()
)
.map(String::toBoolean)

/**
* Draw a pretty ascii border around some text.
* This helps with logging a multiline message, so it is easier to view.
*/
private fun String.surroundWithBorder(): String {
val lines = lineSequence().map { it.trimEnd() }
val maxLength = lines.maxOf { it.length }
val horizontalBorder = "".repeat(maxLength)

return buildString {
appendLine("┌─$horizontalBorder─┐")
lines.forEach { line ->
val paddedLine = line.padEnd(maxLength, padChar = ' ')
appendLine("$paddedLine")
}
appendLine("└─$horizontalBorder─┘")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@ import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.ExtensionAware
import org.gradle.api.plugins.ExtensionContainer
import org.gradle.api.provider.Provider
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import org.gradle.api.services.BuildServiceRegistry
import org.gradle.api.services.BuildServiceSpec
import org.gradle.api.specs.Spec
import org.gradle.api.tasks.TaskProvider
import org.gradle.kotlin.dsl.add
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.domainObjectContainer
import org.gradle.kotlin.dsl.polymorphicDomainObjectContainer
import org.gradle.kotlin.dsl.*
import org.gradle.util.GradleVersion
import org.jetbrains.dokka.gradle.dokka.plugins.DokkaPluginParametersBaseSpec
import kotlin.reflect.KClass
import kotlin.reflect.jvm.jvmName


/**
Expand Down Expand Up @@ -325,3 +329,30 @@ private fun Attribute<*>.matchesTypeOf(other: Attribute<*>): Boolean {
*/
private fun Attribute<*>.typeId(): String? =
type.toString().ifBlank { null }


/**
* Registers a service, named by the [jvmName] of [serviceClass].
*
* @param[classLoaderScoped] If `true`, register a new service with a new name, suffixed with the value of [ClassLoader.hashCode],
* to avoid issues related to Gradle classloaders isolation.
*
* See
* - https://github.com/gradle/gradle/issues/17559
* - https://github.com/JetBrains/kotlin/blob/96205cabfdb14a5aa5b1f0127871cee9c09aaef9/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/gradleUtils.kt#L29-L35
* @see [BuildServiceRegistry.registerIfAbsent]
* @see [PluginFeaturesService.Params.primaryService]
*/
internal fun <T : BuildService<P>, P : BuildServiceParameters> BuildServiceRegistry.registerIfAbsent(
serviceClass: KClass<T>,
classLoaderScoped: Boolean = false,
configureAction: Action<BuildServiceSpec<P>> = Action {},
): Provider<T> {
val serviceName =
if (classLoaderScoped) {
"${serviceClass.jvmName}_${serviceClass.java.classLoader.hashCode()}"
} else {
serviceClass.jvmName
}
return registerIfAbsent(serviceName, serviceClass, configureAction)
}
Original file line number Diff line number Diff line change
Expand Up @@ -195,15 +195,15 @@ constructor(
*
* ```properties
* # $GRADLE_USER_HOME/gradle.properties
* org.jetbrains.dokka.gradle.tasks.logHtmlPublicationLinkEnabled=false
* org.jetbrains.dokka.gradle.enabledLogHtmlPublicationLink=false
* ```
*
* or via an environment variable
*
* ```env
* ORG_GRADLE_PROJECT_org.jetbrains.dokka.gradle.tasks.logHtmlPublicationLinkEnabled=false
* ORG_GRADLE_PROJECT_org.jetbrains.dokka.gradle.enabledLogHtmlPublicationLink=false
* ```
*/
const val ENABLE_TASK_PROPERTY_NAME = "org.jetbrains.dokka.gradle.tasks.logHtmlPublicationLinkEnabled"
const val ENABLE_TASK_PROPERTY_NAME = "org.jetbrains.dokka.gradle.enableLogHtmlPublicationLink"
}
}
Loading

0 comments on commit 67c2c9e

Please sign in to comment.