Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
afb8b31
Fix compatibility with changes in KGP
whyoleg Aug 15, 2025
20cabc8
Merge branch 'master' into whyoleg/kgp-2.2.20-compatibility
adam-enko Aug 26, 2025
3296827
WIP update KotlinAdapter, to determine whether an AGP source set is '…
adam-enko Aug 26, 2025
6ed6540
tidy
adam-enko Aug 27, 2025
144405d
tidy
adam-enko Aug 27, 2025
a869239
Avoid `java.lang.NoClassDefFoundError` in `findExtensionLenient`.
adam-enko Aug 28, 2025
e8cd21e
limit fetching data unlesss KGP >= 2.2.10
adam-enko Aug 28, 2025
4fbeeed
Merge branch 'master' into adam/kgp-2.2.20-compatibility
adam-enko Aug 28, 2025
f8a02d6
fix android variant typecheck
adam-enko Aug 28, 2025
f08f6ad
rm unnecessary `typealias AndroidComponentsExtension`
adam-enko Sep 15, 2025
3663dce
move comment about 'string based class comparison' to relevant part o…
adam-enko Sep 15, 2025
efa359a
Merge branch 'master' into adam/kgp-2.2.20-compatibility
adam-enko Nov 3, 2025
d744657
bump tested KGP 2.2 version
adam-enko Nov 4, 2025
6642dab
add tested KGP 2.3 version
adam-enko Nov 4, 2025
5554f87
remove condition usage of AndroidComponentsExtension
adam-enko Nov 4, 2025
5bf6681
Merge branch 'master' into adam/kgp-2.2.20-compatibility
adam-enko Nov 4, 2025
6fca413
add more docs for `collectAndroidVariants`
adam-enko Nov 4, 2025
acd43cb
rm old kdoc reference
adam-enko Nov 4, 2025
0758d2e
Merge branch 'master' into adam/kgp-2.2.20-compatibility
adam-enko Nov 4, 2025
1f92f05
Merge branch 'master' into adam/kgp-2.2.20-compatibility
adam-enko Nov 10, 2025
e10f1e5
update `findExtensionLenient` kdoc
adam-enko Nov 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ fun interface TestedVersionsSource<T : TestedVersions> {
"1.9.25",
"2.0.21",
"2.1.21",
"2.2.20",
"2.2.21",
"2.3.0-Beta2",
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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
}
Expand Down Expand Up @@ -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<org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension>()
} 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 {
Expand Down Expand Up @@ -280,7 +244,7 @@ private data class KotlinCompilationDetails(
*
* (E.g. 'main' compilations are published, 'test' compilations are not.)
*/
val publishedCompilation: Boolean,
val publishedCompilation: Provider<Boolean>,

/** [KotlinCompilation.kotlinSourceSets] → [KotlinSourceSet.dependsOn] names. */
val dependentSourceSetNames: Set<String>,
Expand All @@ -299,6 +263,7 @@ private class KotlinCompilationDetailsBuilder(
private val konanHome: Provider<File>,
private val project: Project,
) {
private val androidComponentsInfo: Provider<Set<AndroidVariantInfo>> = getAgpVariantInfo(project)

fun createCompilationDetails(
kotlinProjectExtension: KotlinProjectExtension,
Expand All @@ -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<Set<AndroidVariantInfo>> {
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<*>,
Expand Down Expand Up @@ -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<Boolean> {
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<Boolean> {
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)
}
}


Expand Down Expand Up @@ -514,7 +514,7 @@ private abstract class KotlinSourceSetDetails @Inject constructor(
*/
fun isPublishedSourceSet(): Provider<Boolean> =
allCompilations.map { values ->
values.any { it.publishedCompilation }
values.any { it.publishedCompilation.get() }
}

override fun getName(): String = named
Expand Down Expand Up @@ -624,3 +624,89 @@ private class KotlinSourceSetDetailsBuilder(
)
}
}


/** Try and get [KotlinProjectExtension], or `null` if it's not present. */
private fun Project.findKotlinExtension(): KotlinProjectExtension? =
findExtensionLenient<KotlinProjectExtension>("kotlin")


/** Try and get [AndroidComponentsExtension], or `null` if it's not present. */
private fun Project.findAndroidComponentExtension(): AndroidComponentsExtension<*, *, *>? =
findExtensionLenient<AndroidComponentsExtension<*, *, *>>("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]
Comment on lines +664 to +667
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We trigger this function with different plugins, like

withPlugin(PluginId.AndroidBase) { collectAndroidVariants(project, androidVariants) }
withPlugin(PluginId.AndroidApplication) { collectAndroidVariants(project, androidVariants) }
withPlugin(PluginId.AndroidLibrary) { collectAndroidVariants(project, androidVariants) }

however, we do not trigger it for the listed com.android.dynamic-feature or com.android.test, for example. Does it mean we cover those by com.android.base?

If yes, why just having com.android.base is not enough and we should explicitly react on com.android.application and com.android.library? The API for fetching variants seems lazy, so from my POV this causes collecting variants multiple times, but it has only a performance effect since duplicates are automatically excluded from the set constructed via androidVariants

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do not trigger it for the listed com.android.dynamic-feature or com.android.test, for example. Does it mean we cover those by com.android.base?

The behaviour by AGP isn't well specified, but I think the answer is 'no'.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it mean then that this code does not cover some of the Android family plugins?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question...

Thinking again, I think com.android.base is non-functional here. Only the actual plugins should matter. But I'd prefer to keep it, just to be safe.

For com.android.dynamic-feature and com.android.test: I didn't add them here because we haven't had any requests to support them, we don't have any tests for them, and I'm not sure if anyone would ever want to generate Dokka docs for them.

@whyoleg Do you have any opinion on whether DGP should support projects with com.android.dynamic-feature or com.android.test plugins?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that KGP also does perform checks based on multiple ids (not using com.android.base), and I haven't found any tests which use com.android.dynamic-feature.
So I'm leaning more towards having the same behaviour as in KGP - react on all four plugins, even if we don't have tests for them.

Also, AFAIU, with AGP9, users will use kotlin built into those plugins, and this will be less of an issue in the future.

Copy link
Member Author

@adam-enko adam-enko Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated DGP to react to all Android plugins.

UPDATE: oops, I updated it in #4295, not this PR

9ee88a1

*
* 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<AndroidVariantInfo>,
) {
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,
)
)
}
}
Original file line number Diff line number Diff line change
@@ -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 <reified T : Any> 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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
Expand Down
Loading