diff --git a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/internal/PluginFeaturesService.kt b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/internal/PluginFeaturesService.kt index 6bdae5ff53..5f7fc0a434 100644 --- a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/internal/PluginFeaturesService.kt +++ b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/internal/PluginFeaturesService.kt @@ -3,6 +3,7 @@ */ package org.jetbrains.dokka.gradle.internal +import org.gradle.TaskExecutionRequest import org.gradle.api.Action import org.gradle.api.GradleException import org.gradle.api.Project @@ -12,6 +13,8 @@ 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 java.io.File +import java.util.* /** * Internal utility service for managing Dokka Plugin features and warnings. @@ -193,56 +196,125 @@ internal abstract class PluginFeaturesService : BuildService { - v2PluginEnabled.set(getFlag(V2_PLUGIN_ENABLED_FLAG)) - v2PluginNoWarn.set(getFlag(V2_PLUGIN_NO_WARN_FLAG_PRETTY).orElse(getFlag(V2_PLUGIN_NO_WARN_FLAG))) - v2PluginMigrationHelpersEnabled.set(getFlag(V2_PLUGIN_MIGRATION_HELPERS_FLAG)) - k2AnalysisEnabled.set(getFlag(K2_ANALYSIS_ENABLED_FLAG)) - k2AnalysisNoWarn.set( - getFlag(K2_ANALYSIS_NO_WARN_FLAG_PRETTY) - .orElse(getFlag(K2_ANALYSIS_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() = getOrCreateService(project) + + private fun getOrCreateService(project: Project): PluginFeaturesService { + val configureServiceParams = serviceParamsConfiguration(project) + + return try { + project.gradle.sharedServices.registerIfAbsent(PluginFeaturesService::class) { + parameters(configureServiceParams) + // 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'. + project.gradle.sharedServices.registerIfAbsent( + PluginFeaturesService::class, + classLoaderScoped = true, + ) { + parameters(configureServiceParams) + parameters.primaryService.set(false) }.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 - ) - } + 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 = - 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() + /** + * Return an [Action] that will configure [PluginFeaturesService.Params], based on detected plugin flags. + */ + private fun serviceParamsConfiguration( + project: Project + ): Action { + + /** Find a flag for [PluginFeaturesService]. */ + fun getFlag(flag: String): Provider = + project.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) + + + return Action { + v2PluginEnabled.set(getFlag(V2_PLUGIN_ENABLED_FLAG)) + v2PluginNoWarn.set(getFlag(V2_PLUGIN_NO_WARN_FLAG_PRETTY).orElse(getFlag(V2_PLUGIN_NO_WARN_FLAG))) + v2PluginMigrationHelpersEnabled.set(getFlag(V2_PLUGIN_MIGRATION_HELPERS_FLAG)) + k2AnalysisEnabled.set(getFlag(K2_ANALYSIS_ENABLED_FLAG)) + k2AnalysisNoWarn.set( + getFlag(K2_ANALYSIS_NO_WARN_FLAG_PRETTY) + .orElse(getFlag(K2_ANALYSIS_NO_WARN_FLAG)) ) - .map(String::toBoolean) + + configureParamsDuringAccessorsGeneration(project) + } + } + + /** + * We use a Gradle flag to control whether DGP is in v1 or v2 mode. + * This flag dynamically changes the behaviour of DGP at runtime. + * + * However, there is a particular situation where this flag can't be detected: + * When Dokka is applied to a precompiled script plugin and Gradle generates Kotlin DSL accessors. + * + * When Gradle is generating such accessors, it creates a temporary project, totally disconnected + * from the main build. The temporary project has no access to any Gradle properties. + * As such, no Dokka flags can be detected, resulting in unexpected behaviour. + * + * We work around this by first detecting when Gradle is generating accessors + * (see [isGradleGeneratingAccessors]), and secondly by manually discovering a suitable + * `gradle.properties` file (see [findGradlePropertiesFile]) and reading its values. + * + * This is a workaround and can be removed with DGPv1 + * https://youtrack.jetbrains.com/issue/KT-71027/ + */ + private fun Params.configureParamsDuringAccessorsGeneration(project: Project) { + try { + if (project.isGradleGeneratingAccessors()) { + logger.info("Gradle is generating accessors. Discovering Dokka Gradle Plugin flags manually. ${project.gradle.rootProject.name} | ${project.gradle.rootProject.rootDir}") + + // Disable all warnings, regardless of the discovered flag values. + // Log messages will be printed too soon and aren't useful for users. + v2PluginNoWarn.set(true) + + // Because Gradle is generating accessors, it won't give us access to Gradle properties + // defined for the main project. So, we must discover `gradle.properties` ourselves. + val propertiesFile = project.findGradlePropertiesFile() + + val properties = Properties().apply { + propertiesFile?.reader()?.use { reader -> + load(reader) + } + } + + // These are the only flags that are important when Gradle is generating accessors, + // because they control what accessors DGP registers. + properties[V2_PLUGIN_ENABLED_FLAG]?.toString()?.toBoolean()?.let { + v2PluginEnabled.set(it) + } + properties[V2_PLUGIN_MIGRATION_HELPERS_FLAG]?.toString()?.toBoolean()?.let { + v2PluginMigrationHelpersEnabled.set(it) + } + } + } catch (t: Throwable) { + // Ignore all errors. + // This is just a temporary util. It doesn't need to be stable long-term, + // and we don't want to risk breaking people's projects. + } + } /** * Draw a pretty ascii border around some text. @@ -264,3 +336,62 @@ internal abstract class PluginFeaturesService : BuildService/build/tmp/generatePrecompiledScriptPluginAccessors/accessors373648437747350006 + // ^5 ^4 ^3 ^2 ^1 + .drop(5) + + .map { it.resolve("gradle.properties") } + + .firstOrNull { it.exists() && it.isFile } +} diff --git a/dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/BuildSrcKotlinDslAccessorsTest.kt b/dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/BuildSrcKotlinDslAccessorsTest.kt new file mode 100644 index 0000000000..3e3371d2c8 --- /dev/null +++ b/dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/BuildSrcKotlinDslAccessorsTest.kt @@ -0,0 +1,92 @@ +/* + * 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 io.kotest.core.spec.style.FunSpec +import org.gradle.testkit.runner.TaskOutcome.* +import org.jetbrains.dokka.gradle.internal.DokkaConstants +import org.jetbrains.dokka.gradle.utils.* + +class BuildSrcKotlinDslAccessorsTest : FunSpec({ + + val project = initProjectWithBuildSrcConvention() + + context("when DGPv2 is enabled") { + project + .runner + .addArguments( + ":compileKotlin", + "--project-dir", "buildSrc", + ) + .build { + test("expect DGPv2 can be used in a convention plugin") { + shouldHaveTasksWithAnyOutcome(":compileKotlin" to listOf(SUCCESS, UP_TO_DATE, FROM_CACHE)) + } + } + } +}) + +private fun initProjectWithBuildSrcConvention( + rootProjectName: String? = null, + config: GradleProjectTest.() -> Unit = {}, +): GradleProjectTest { + + return gradleKtsProjectTest( + projectLocation = "BuildSrcKotlinDslAccessorsTest", + rootProjectName = rootProjectName, + ) { + + buildGradleKts = """ + |plugins { + | kotlin("jvm") version embeddedKotlinVersion + | id("org.jetbrains.dokka") version "${DokkaConstants.DOKKA_VERSION}" + |} + | + """.trimMargin() + + dir("buildSrc") { + buildGradleKts = """ + |plugins { + | `kotlin-dsl` + |} + | + |dependencies { + | implementation("org.jetbrains.dokka:dokka-gradle-plugin:${DokkaConstants.DOKKA_VERSION}") + |} + | + """.trimMargin() + + + settingsGradleKts = """ + |rootProject.name = "buildSrc" + | + |${settingsRepositories()} + | + """.trimMargin() + + createFile( + "src/main/kotlin/dokka-convention.gradle.kts", + /* language=TEXT */ """ + |plugins { + | id("org.jetbrains.dokka") + |} + | + |dokka { + | moduleName.set("custom-module-name") + |} + | + """.trimMargin() + ) + } + + gradleProperties { + dokka { + v2Plugin = true + v2MigrationHelpers = true + } + } + + config() + } +}