diff --git a/dokka-integration-tests/gradle/src/main/kotlin/org/jetbrains/dokka/it/gradle/junit/TestedVersionsSource.kt b/dokka-integration-tests/gradle/src/main/kotlin/org/jetbrains/dokka/it/gradle/junit/TestedVersionsSource.kt index 2b56655420..c5719174cc 100644 --- a/dokka-integration-tests/gradle/src/main/kotlin/org/jetbrains/dokka/it/gradle/junit/TestedVersionsSource.kt +++ b/dokka-integration-tests/gradle/src/main/kotlin/org/jetbrains/dokka/it/gradle/junit/TestedVersionsSource.kt @@ -47,7 +47,8 @@ fun interface TestedVersionsSource { "1.9.25", "2.0.21", "2.1.21", - "2.2.20", + "2.2.21", + "2.3.0-Beta2", ) /** diff --git a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/adapters/KotlinAdapter.kt b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/adapters/KotlinAdapter.kt index ff4d95b1cb..bdf2bf7007 100644 --- a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/adapters/KotlinAdapter.kt +++ b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/adapters/KotlinAdapter.kt @@ -3,15 +3,17 @@ */ package org.jetbrains.dokka.gradle.adapters +import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.api.variant.Variant import org.gradle.api.Named import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.FileCollection +import org.gradle.api.logging.Logger import org.gradle.api.logging.Logging import org.gradle.api.model.ObjectFactory -import org.gradle.api.plugins.ExtensionContainer import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Provider import org.gradle.api.provider.ProviderFactory @@ -44,13 +46,12 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMetadataTarget import org.jetbrains.kotlin.tooling.core.KotlinToolingVersion import java.io.File import javax.inject.Inject -import kotlin.reflect.jvm.jvmName /** * The [KotlinAdapter] plugin will automatically register Kotlin source sets as Dokka source sets. * * This is an internal Dokka plugin and should not be used externally. - * It is not a standalone plugin, it requires [org.jetbrains.dokka.gradle.DokkaBasePlugin] is also applied. + * It is not a standalone plugin, it requires [DokkaBasePlugin] is also applied. */ @InternalDokkaGradlePluginApi abstract class KotlinAdapter @Inject constructor( @@ -72,25 +73,8 @@ abstract class KotlinAdapter @Inject constructor( } private fun exec(project: Project) { - val kotlinExtension = project.extensions.findKotlinExtension() + val kotlinExtension = project.findKotlinExtension() if (kotlinExtension == null) { - if (project.extensions.findByName("kotlin") != null) { - // uh oh - the Kotlin extension is present but findKotlinExtension() failed. - // Is there a class loader issue? https://github.com/gradle/gradle/issues/27218 - logger.warn { - val allPlugins = - project.plugins.joinToString { it::class.qualifiedName ?: "${it::class}" } - val allExtensions = - project.extensions.extensionsSchema.elements.joinToString { "${it.name} ${it.publicType}" } - - /* language=TEXT */ - """ - |$dkaName failed to get KotlinProjectExtension in ${project.path} - | Applied plugins: $allPlugins - | Available extensions: $allExtensions - """.trimMargin() - } - } logger.info("Skipping applying $dkaName in ${project.path} - could not find KotlinProjectExtension") return } @@ -220,26 +204,6 @@ abstract class KotlinAdapter @Inject constructor( private val logger = Logging.getLogger(KotlinAdapter::class.java) - /** Try and get [KotlinProjectExtension], or `null` if it's not present. */ - private fun ExtensionContainer.findKotlinExtension(): KotlinProjectExtension? = - try { - findByType() - // fallback to trying to get the JVM extension - // (not sure why I did this... maybe to be compatible with really old versions?) - ?: findByType() - } catch (e: Throwable) { - when (e) { - is TypeNotPresentException, - is ClassNotFoundException, - is NoClassDefFoundError -> { - logger.info("$dkaName failed to find KotlinExtension ${e::class} ${e.message}") - null - } - - else -> throw e - } - } - /** Get the version of the Kotlin Gradle Plugin currently used to compile the project. */ // Must be lazy, else tests fail (because the KGP plugin isn't accessible) internal val currentKotlinToolingVersion: KotlinToolingVersion by lazy { @@ -280,7 +244,7 @@ private data class KotlinCompilationDetails( * * (E.g. 'main' compilations are published, 'test' compilations are not.) */ - val publishedCompilation: Boolean, + val publishedCompilation: Provider, /** [KotlinCompilation.kotlinSourceSets] → [KotlinSourceSet.dependsOn] names. */ val dependentSourceSetNames: Set, @@ -299,6 +263,7 @@ private class KotlinCompilationDetailsBuilder( private val konanHome: Provider, private val project: Project, ) { + private val androidComponentsInfo: Provider> = getAgpVariantInfo(project) fun createCompilationDetails( kotlinProjectExtension: KotlinProjectExtension, @@ -318,6 +283,30 @@ private class KotlinCompilationDetailsBuilder( return details } + /** + * Collect information about Android variants. + * Used to determine whether a source set is published or not. + * See [KotlinSourceSetDetails.isPublishedSourceSet]. + * + * Android variant info must be fetched eagerly, + * since AGP doesn't provide a lazy way of accessing component information. + * + * @see collectAndroidVariants + */ + private fun getAgpVariantInfo( + project: Project, + ): Provider> { + val androidVariants = objects.setProperty(AndroidVariantInfo::class) + + project.pluginManager.apply { + withPlugin(PluginId.AndroidBase) { collectAndroidVariants(project, androidVariants) } + withPlugin(PluginId.AndroidApplication) { collectAndroidVariants(project, androidVariants) } + withPlugin(PluginId.AndroidLibrary) { collectAndroidVariants(project, androidVariants) } + } + + return androidVariants + } + /** Create a single [KotlinCompilationDetails] for [compilation]. */ private fun createCompilationDetails( compilation: KotlinCompilation<*>, @@ -444,34 +433,45 @@ private class KotlinCompilationDetailsBuilder( } } - companion object { + /** + * Determine if a [KotlinCompilation] is 'publishable', and so should be enabled by default + * when creating a Dokka publication. + * + * Typically, 'main' compilations are publishable and 'test' compilations should be suppressed. + * This can be overridden manually, though. + * + * @see DokkaSourceSetSpec.suppress + */ + private fun KotlinCompilation<*>.isPublished(): Provider { + return when (this) { + is KotlinMetadataCompilation<*> -> + providers.provider { true } - /** - * Determine if a [KotlinCompilation] is 'publishable', and so should be enabled by default - * when creating a Dokka publication. - * - * Typically, 'main' compilations are publishable and 'test' compilations should be suppressed. - * This can be overridden manually, though. - * - * @see DokkaSourceSetSpec.suppress - */ - private fun KotlinCompilation<*>.isPublished(): Boolean { - return when (this) { - is KotlinMetadataCompilation<*> -> true - - is KotlinJvmAndroidCompilation -> { - // Use string-based comparison, not the actual classes, because AGP has deprecated and - // moved the Library/Application classes to a different package. - // Using strings is more widely compatible. - val variantName = androidVariant::class.jvmName - "LibraryVariant" in variantName || "ApplicationVariant" in variantName - } + is KotlinJvmAndroidCompilation -> { + isJvmAndroidPublished(this) + } - else -> - name == MAIN_COMPILATION_NAME + else -> + providers.provider { name == MAIN_COMPILATION_NAME } + } + } + + private fun isJvmAndroidPublished( + compilation: KotlinJvmAndroidCompilation, + ): Provider { + return androidComponentsInfo.map { components -> + val compilationComponents = components.filter { it.name == compilation.name } + val result = compilationComponents.any { component -> component.hasPublishedComponent } + logger.info { + "[KotlinAdapter isJvmAndroidPublished] ${compilation.name} publishable:$result, compilationComponents:$compilationComponents" } + result } } + + companion object { + private val logger: Logger = Logging.getLogger(KotlinAdapter::class.java) + } } @@ -514,7 +514,7 @@ private abstract class KotlinSourceSetDetails @Inject constructor( */ fun isPublishedSourceSet(): Provider = allCompilations.map { values -> - values.any { it.publishedCompilation } + values.any { it.publishedCompilation.get() } } override fun getName(): String = named @@ -624,3 +624,89 @@ private class KotlinSourceSetDetailsBuilder( ) } } + + +/** Try and get [KotlinProjectExtension], or `null` if it's not present. */ +private fun Project.findKotlinExtension(): KotlinProjectExtension? = + findExtensionLenient("kotlin") + + +/** Try and get [AndroidComponentsExtension], or `null` if it's not present. */ +private fun Project.findAndroidComponentExtension(): AndroidComponentsExtension<*, *, *>? = + findExtensionLenient>("androidComponents") + + +/** + * Store details about a [Variant]. + * + * @param[name] [Variant.name]. + * @param[hasPublishedComponent] `true` if any component of the variant is 'published', + * i.e. it is an instance of [Variant]. + */ +private data class AndroidVariantInfo( + val name: String, + val hasPublishedComponent: Boolean, +) + +/** + * Collect [AndroidVariantInfo]s of the Android [Variant]s in this Android project. + * + * We store the collected data in a custom class to aid with Configuration Cache compatibility. + * + * This function must only be called when AGP is applied + * (otherwise [findAndroidComponentExtension] will return `null`), + * i.e. inside a `withPlugin(...) {}` block. + * + * ## How to determine publishability of AGP Variants + * + * There are several Android Gradle plugins. + * Each AGP has a specific associated [Variant]: + * - `com.android.application` - [com.android.build.api.variant.ApplicationVariant] + * - `com.android.library` - [com.android.build.api.variant.DynamicFeatureVariant] + * - `com.android.test` - [com.android.build.api.variant.LibraryVariant] + * - `com.android.dynamic-feature` - [com.android.build.api.variant.TestVariant] + * + * A [Variant] is 'published' (or otherwise shared with other projects). + * Note that a [Variant] might have [nestedComponents][Variant.nestedComponents]. + * If any of these [com.android.build.api.variant.Component]s are [Variant]s, + * then the [Variant] itself should be considered 'publishable'. + * + * If a [KotlinSourceSet] has an associated [Variant], + * it should therefore be documented by Dokka by default. + * + * ### Associating Variants with Compilations with SourceSets + * + * So, how can we associate a [KotlinSourceSet] with a [Variant]? + * + * Fortunately, Dokka already knows about the [KotlinCompilation]s associated with a specific [KotlinSourceSet]. + * + * So, for each [KotlinCompilation], find a [Variant] with the same name, + * i.e. [KotlinCompilation.getName] is the same as [Variant.name]. + * + * Next, determine if the [Variant] associated with a [KotlinCompilation] is 'publishable' by + * checking if it _or_ any of its [nestedComponents][Variant.nestedComponents] + * are 'publishable' (i.e. is an instance of [Variant]). + * (We can we use [Variant.components] to check both the [Variant] and its `nestedComponents` the same time.) + */ +private fun collectAndroidVariants( + project: Project, + androidVariants: SetProperty, +) { + val androidComponents = project.findAndroidComponentExtension() + + androidComponents?.onVariants { variant -> + val hasPublishedComponent = + variant.components.any { component -> + // a Variant is a subtype of a Component that is shared with consumers, + // so Dokka should consider it 'publishable' + component is Variant + } + + androidVariants.add( + AndroidVariantInfo( + name = variant.name, + hasPublishedComponent = hasPublishedComponent, + ) + ) + } +} diff --git a/dokka-runners/dokka-gradle-plugin/src/main/kotlin/internal/findExtensionLenient.kt b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/internal/findExtensionLenient.kt new file mode 100644 index 0000000000..2a6b001943 --- /dev/null +++ b/dokka-runners/dokka-gradle-plugin/src/main/kotlin/internal/findExtensionLenient.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2014-2025 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.Project + +/** + * Try and get an extension from [Project.getExtensions], or `null` if it's not present. + * + * If [T] is not accessible in the current classloader, returns `null`. + * + * Logs a warning if the extension is present, but the wrong type + * (probably caused by an inconsistent buildscript classpath https://github.com/gradle/gradle/issues/27218) + */ +internal inline fun Project.findExtensionLenient( + extensionName: String, +): T? { + + val extensionByName = extensions.findByName(extensionName) + if (extensionByName == null) { + logger.info("Dokka Gradle plugin failed to find extension $extensionName by name ${T::class.java}") + return null + } + + try { + return extensions.findByType(T::class.java) + } catch (e: Throwable) { + when (e) { + is TypeNotPresentException, + is ClassNotFoundException, + is NoClassDefFoundError -> { + + // uh oh - extension is present, but it's the wrong type + // Is there a class loader issue? https://github.com/gradle/gradle/issues/27218 + logger.warn { + // If we're here, then T isn't available, so don't use T::class. + // Instead, use the available extension's class. + val actualExtensionFqn = + extensions.extensionsSchema.firstOrNull { it.name == extensionName }?.publicType?.fullyQualifiedName + + val allPlugins = + project.plugins.joinToString { it::class.qualifiedName ?: "${it::class.java}" } + val allExtensions = + project.extensions.extensionsSchema.elements.joinToString { "${it.name} ${it.publicType}" } + + """ + |Dokka Gradle plugin failed to get extension $extensionName $actualExtensionFqn in ${project.path} + |Please make sure plugins in all subprojects are consistent. See https://github.com/gradle/gradle/issues/27218 + | Applied plugins: $allPlugins + | Available extensions: $allExtensions + """.trimMargin() + } + + return null + } + + else -> throw e + } + } +} diff --git a/dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/MultiModuleFunctionalTest.kt b/dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/MultiModuleFunctionalTest.kt index 9d0c278150..7452c9370a 100644 --- a/dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/MultiModuleFunctionalTest.kt +++ b/dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/MultiModuleFunctionalTest.kt @@ -520,14 +520,17 @@ class MultiModuleFunctionalTest : FunSpec({ test("expect warning regarding KotlinProjectExtension") { project.runner - .addArguments("clean") + .addArguments( + "clean", + "--stacktrace", + ) .forwardOutput() .build { // the root project doesn't have the KGP applied, so KotlinProjectExtension shouldn't be applied - output shouldNotContain "KotlinAdapter failed to get KotlinProjectExtension in :\n" + output shouldNotContain "Dokka Gradle plugin failed to get extension kotlin org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension in :\n" - output shouldContain "KotlinAdapter failed to get KotlinProjectExtension in :subproject-hello\n" - output shouldContain "KotlinAdapter failed to get KotlinProjectExtension in :subproject-goodbye\n" + output shouldContain "Dokka Gradle plugin failed to get extension kotlin org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension in :subproject-hello\n" + output shouldContain "Dokka Gradle plugin failed to get extension kotlin org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension in :subproject-goodbye\n" } } }