Skip to content

Commit f98203a

Browse files
authored
KGP 2.2.10 + AGP: Use Android components to determine if source-sets are 'publishable' (#4231)
DGP should only document 'publishable' (i.e. main, non-test) source sets. DGP needs to extract this information from KGP and AGP. For AGP projects, DPG currently uses `KotlinJvmAndroidCompilation.androidVariant` to determine publishability of a source set. However, [this is deprecated in KGP 2.2.10](https://github.com/JetBrains/kotlin/blob/v2.2.10/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/jvm/KotlinJvmAndroidCompilation.kt#L38-L39). It will eventually be removed. This is because AGP will implement Kotlin compilation internally (AKA Kotlin built-in). This PR uses an alternative AGP utility: `AndroidComponentsExtension`. DGP extracts Components from this extension and stores it in a separate data structure (to avoid Configuration Cache issues with serialising too much data). If a Component has at least one Variant, it is considered 'publishable'. Each Component is associated with a single KotlinCompilation (matched by name), and each KotlinSourceSet is associated with a list of KotlinCompilations. While using `AndroidComponentsExtension` is required for AGP9, it also works for AGP7 and 8. To reduce complexity, this PR uses the same approach for all AGP versions. --- This PR is the first part of supporting AGP 9 in DGP. The next PR is #4295, which will support and test AGP 9 with and without Kotlin built-in.
1 parent 7903495 commit f98203a

4 files changed

Lines changed: 223 additions & 71 deletions

File tree

dokka-integration-tests/gradle/src/main/kotlin/org/jetbrains/dokka/it/gradle/junit/TestedVersionsSource.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ fun interface TestedVersionsSource<T : TestedVersions> {
4747
"1.9.25",
4848
"2.0.21",
4949
"2.1.21",
50-
"2.2.20",
50+
"2.2.21",
51+
"2.3.0-Beta2",
5152
)
5253

5354
/**

dokka-runners/dokka-gradle-plugin/src/main/kotlin/adapters/KotlinAdapter.kt

Lines changed: 152 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@
33
*/
44
package org.jetbrains.dokka.gradle.adapters
55

6+
import com.android.build.api.variant.AndroidComponentsExtension
7+
import com.android.build.api.variant.Variant
68
import org.gradle.api.Named
79
import org.gradle.api.NamedDomainObjectContainer
810
import org.gradle.api.Plugin
911
import org.gradle.api.Project
1012
import org.gradle.api.file.ConfigurableFileCollection
1113
import org.gradle.api.file.FileCollection
14+
import org.gradle.api.logging.Logger
1215
import org.gradle.api.logging.Logging
1316
import org.gradle.api.model.ObjectFactory
14-
import org.gradle.api.plugins.ExtensionContainer
1517
import org.gradle.api.provider.ListProperty
1618
import org.gradle.api.provider.Provider
1719
import org.gradle.api.provider.ProviderFactory
@@ -44,13 +46,12 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMetadataTarget
4446
import org.jetbrains.kotlin.tooling.core.KotlinToolingVersion
4547
import java.io.File
4648
import javax.inject.Inject
47-
import kotlin.reflect.jvm.jvmName
4849

4950
/**
5051
* The [KotlinAdapter] plugin will automatically register Kotlin source sets as Dokka source sets.
5152
*
5253
* This is an internal Dokka plugin and should not be used externally.
53-
* It is not a standalone plugin, it requires [org.jetbrains.dokka.gradle.DokkaBasePlugin] is also applied.
54+
* It is not a standalone plugin, it requires [DokkaBasePlugin] is also applied.
5455
*/
5556
@InternalDokkaGradlePluginApi
5657
abstract class KotlinAdapter @Inject constructor(
@@ -72,25 +73,8 @@ abstract class KotlinAdapter @Inject constructor(
7273
}
7374

7475
private fun exec(project: Project) {
75-
val kotlinExtension = project.extensions.findKotlinExtension()
76+
val kotlinExtension = project.findKotlinExtension()
7677
if (kotlinExtension == null) {
77-
if (project.extensions.findByName("kotlin") != null) {
78-
// uh oh - the Kotlin extension is present but findKotlinExtension() failed.
79-
// Is there a class loader issue? https://github.com/gradle/gradle/issues/27218
80-
logger.warn {
81-
val allPlugins =
82-
project.plugins.joinToString { it::class.qualifiedName ?: "${it::class}" }
83-
val allExtensions =
84-
project.extensions.extensionsSchema.elements.joinToString { "${it.name} ${it.publicType}" }
85-
86-
/* language=TEXT */
87-
"""
88-
|$dkaName failed to get KotlinProjectExtension in ${project.path}
89-
| Applied plugins: $allPlugins
90-
| Available extensions: $allExtensions
91-
""".trimMargin()
92-
}
93-
}
9478
logger.info("Skipping applying $dkaName in ${project.path} - could not find KotlinProjectExtension")
9579
return
9680
}
@@ -220,26 +204,6 @@ abstract class KotlinAdapter @Inject constructor(
220204

221205
private val logger = Logging.getLogger(KotlinAdapter::class.java)
222206

223-
/** Try and get [KotlinProjectExtension], or `null` if it's not present. */
224-
private fun ExtensionContainer.findKotlinExtension(): KotlinProjectExtension? =
225-
try {
226-
findByType()
227-
// fallback to trying to get the JVM extension
228-
// (not sure why I did this... maybe to be compatible with really old versions?)
229-
?: findByType<org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension>()
230-
} catch (e: Throwable) {
231-
when (e) {
232-
is TypeNotPresentException,
233-
is ClassNotFoundException,
234-
is NoClassDefFoundError -> {
235-
logger.info("$dkaName failed to find KotlinExtension ${e::class} ${e.message}")
236-
null
237-
}
238-
239-
else -> throw e
240-
}
241-
}
242-
243207
/** Get the version of the Kotlin Gradle Plugin currently used to compile the project. */
244208
// Must be lazy, else tests fail (because the KGP plugin isn't accessible)
245209
internal val currentKotlinToolingVersion: KotlinToolingVersion by lazy {
@@ -280,7 +244,7 @@ private data class KotlinCompilationDetails(
280244
*
281245
* (E.g. 'main' compilations are published, 'test' compilations are not.)
282246
*/
283-
val publishedCompilation: Boolean,
247+
val publishedCompilation: Provider<Boolean>,
284248

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

303268
fun createCompilationDetails(
304269
kotlinProjectExtension: KotlinProjectExtension,
@@ -318,6 +283,30 @@ private class KotlinCompilationDetailsBuilder(
318283
return details
319284
}
320285

286+
/**
287+
* Collect information about Android variants.
288+
* Used to determine whether a source set is published or not.
289+
* See [KotlinSourceSetDetails.isPublishedSourceSet].
290+
*
291+
* Android variant info must be fetched eagerly,
292+
* since AGP doesn't provide a lazy way of accessing component information.
293+
*
294+
* @see collectAndroidVariants
295+
*/
296+
private fun getAgpVariantInfo(
297+
project: Project,
298+
): Provider<Set<AndroidVariantInfo>> {
299+
val androidVariants = objects.setProperty(AndroidVariantInfo::class)
300+
301+
project.pluginManager.apply {
302+
withPlugin(PluginId.AndroidBase) { collectAndroidVariants(project, androidVariants) }
303+
withPlugin(PluginId.AndroidApplication) { collectAndroidVariants(project, androidVariants) }
304+
withPlugin(PluginId.AndroidLibrary) { collectAndroidVariants(project, androidVariants) }
305+
}
306+
307+
return androidVariants
308+
}
309+
321310
/** Create a single [KotlinCompilationDetails] for [compilation]. */
322311
private fun createCompilationDetails(
323312
compilation: KotlinCompilation<*>,
@@ -444,34 +433,45 @@ private class KotlinCompilationDetailsBuilder(
444433
}
445434
}
446435

447-
companion object {
436+
/**
437+
* Determine if a [KotlinCompilation] is 'publishable', and so should be enabled by default
438+
* when creating a Dokka publication.
439+
*
440+
* Typically, 'main' compilations are publishable and 'test' compilations should be suppressed.
441+
* This can be overridden manually, though.
442+
*
443+
* @see DokkaSourceSetSpec.suppress
444+
*/
445+
private fun KotlinCompilation<*>.isPublished(): Provider<Boolean> {
446+
return when (this) {
447+
is KotlinMetadataCompilation<*> ->
448+
providers.provider { true }
448449

449-
/**
450-
* Determine if a [KotlinCompilation] is 'publishable', and so should be enabled by default
451-
* when creating a Dokka publication.
452-
*
453-
* Typically, 'main' compilations are publishable and 'test' compilations should be suppressed.
454-
* This can be overridden manually, though.
455-
*
456-
* @see DokkaSourceSetSpec.suppress
457-
*/
458-
private fun KotlinCompilation<*>.isPublished(): Boolean {
459-
return when (this) {
460-
is KotlinMetadataCompilation<*> -> true
461-
462-
is KotlinJvmAndroidCompilation -> {
463-
// Use string-based comparison, not the actual classes, because AGP has deprecated and
464-
// moved the Library/Application classes to a different package.
465-
// Using strings is more widely compatible.
466-
val variantName = androidVariant::class.jvmName
467-
"LibraryVariant" in variantName || "ApplicationVariant" in variantName
468-
}
450+
is KotlinJvmAndroidCompilation -> {
451+
isJvmAndroidPublished(this)
452+
}
469453

470-
else ->
471-
name == MAIN_COMPILATION_NAME
454+
else ->
455+
providers.provider { name == MAIN_COMPILATION_NAME }
456+
}
457+
}
458+
459+
private fun isJvmAndroidPublished(
460+
compilation: KotlinJvmAndroidCompilation,
461+
): Provider<Boolean> {
462+
return androidComponentsInfo.map { components ->
463+
val compilationComponents = components.filter { it.name == compilation.name }
464+
val result = compilationComponents.any { component -> component.hasPublishedComponent }
465+
logger.info {
466+
"[KotlinAdapter isJvmAndroidPublished] ${compilation.name} publishable:$result, compilationComponents:$compilationComponents"
472467
}
468+
result
473469
}
474470
}
471+
472+
companion object {
473+
private val logger: Logger = Logging.getLogger(KotlinAdapter::class.java)
474+
}
475475
}
476476

477477

@@ -514,7 +514,7 @@ private abstract class KotlinSourceSetDetails @Inject constructor(
514514
*/
515515
fun isPublishedSourceSet(): Provider<Boolean> =
516516
allCompilations.map { values ->
517-
values.any { it.publishedCompilation }
517+
values.any { it.publishedCompilation.get() }
518518
}
519519

520520
override fun getName(): String = named
@@ -624,3 +624,89 @@ private class KotlinSourceSetDetailsBuilder(
624624
)
625625
}
626626
}
627+
628+
629+
/** Try and get [KotlinProjectExtension], or `null` if it's not present. */
630+
private fun Project.findKotlinExtension(): KotlinProjectExtension? =
631+
findExtensionLenient<KotlinProjectExtension>("kotlin")
632+
633+
634+
/** Try and get [AndroidComponentsExtension], or `null` if it's not present. */
635+
private fun Project.findAndroidComponentExtension(): AndroidComponentsExtension<*, *, *>? =
636+
findExtensionLenient<AndroidComponentsExtension<*, *, *>>("androidComponents")
637+
638+
639+
/**
640+
* Store details about a [Variant].
641+
*
642+
* @param[name] [Variant.name].
643+
* @param[hasPublishedComponent] `true` if any component of the variant is 'published',
644+
* i.e. it is an instance of [Variant].
645+
*/
646+
private data class AndroidVariantInfo(
647+
val name: String,
648+
val hasPublishedComponent: Boolean,
649+
)
650+
651+
/**
652+
* Collect [AndroidVariantInfo]s of the Android [Variant]s in this Android project.
653+
*
654+
* We store the collected data in a custom class to aid with Configuration Cache compatibility.
655+
*
656+
* This function must only be called when AGP is applied
657+
* (otherwise [findAndroidComponentExtension] will return `null`),
658+
* i.e. inside a `withPlugin(...) {}` block.
659+
*
660+
* ## How to determine publishability of AGP Variants
661+
*
662+
* There are several Android Gradle plugins.
663+
* Each AGP has a specific associated [Variant]:
664+
* - `com.android.application` - [com.android.build.api.variant.ApplicationVariant]
665+
* - `com.android.library` - [com.android.build.api.variant.DynamicFeatureVariant]
666+
* - `com.android.test` - [com.android.build.api.variant.LibraryVariant]
667+
* - `com.android.dynamic-feature` - [com.android.build.api.variant.TestVariant]
668+
*
669+
* A [Variant] is 'published' (or otherwise shared with other projects).
670+
* Note that a [Variant] might have [nestedComponents][Variant.nestedComponents].
671+
* If any of these [com.android.build.api.variant.Component]s are [Variant]s,
672+
* then the [Variant] itself should be considered 'publishable'.
673+
*
674+
* If a [KotlinSourceSet] has an associated [Variant],
675+
* it should therefore be documented by Dokka by default.
676+
*
677+
* ### Associating Variants with Compilations with SourceSets
678+
*
679+
* So, how can we associate a [KotlinSourceSet] with a [Variant]?
680+
*
681+
* Fortunately, Dokka already knows about the [KotlinCompilation]s associated with a specific [KotlinSourceSet].
682+
*
683+
* So, for each [KotlinCompilation], find a [Variant] with the same name,
684+
* i.e. [KotlinCompilation.getName] is the same as [Variant.name].
685+
*
686+
* Next, determine if the [Variant] associated with a [KotlinCompilation] is 'publishable' by
687+
* checking if it _or_ any of its [nestedComponents][Variant.nestedComponents]
688+
* are 'publishable' (i.e. is an instance of [Variant]).
689+
* (We can we use [Variant.components] to check both the [Variant] and its `nestedComponents` the same time.)
690+
*/
691+
private fun collectAndroidVariants(
692+
project: Project,
693+
androidVariants: SetProperty<AndroidVariantInfo>,
694+
) {
695+
val androidComponents = project.findAndroidComponentExtension()
696+
697+
androidComponents?.onVariants { variant ->
698+
val hasPublishedComponent =
699+
variant.components.any { component ->
700+
// a Variant is a subtype of a Component that is shared with consumers,
701+
// so Dokka should consider it 'publishable'
702+
component is Variant
703+
}
704+
705+
androidVariants.add(
706+
AndroidVariantInfo(
707+
name = variant.name,
708+
hasPublishedComponent = hasPublishedComponent,
709+
)
710+
)
711+
}
712+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2014-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package org.jetbrains.dokka.gradle.internal
6+
7+
import org.gradle.api.Project
8+
9+
/**
10+
* Try and get an extension from [Project.getExtensions], or `null` if it's not present.
11+
*
12+
* If [T] is not accessible in the current classloader, returns `null`.
13+
*
14+
* Logs a warning if the extension is present, but the wrong type
15+
* (probably caused by an inconsistent buildscript classpath https://github.com/gradle/gradle/issues/27218)
16+
*/
17+
internal inline fun <reified T : Any> Project.findExtensionLenient(
18+
extensionName: String,
19+
): T? {
20+
21+
val extensionByName = extensions.findByName(extensionName)
22+
if (extensionByName == null) {
23+
logger.info("Dokka Gradle plugin failed to find extension $extensionName by name ${T::class.java}")
24+
return null
25+
}
26+
27+
try {
28+
return extensions.findByType(T::class.java)
29+
} catch (e: Throwable) {
30+
when (e) {
31+
is TypeNotPresentException,
32+
is ClassNotFoundException,
33+
is NoClassDefFoundError -> {
34+
35+
// uh oh - extension is present, but it's the wrong type
36+
// Is there a class loader issue? https://github.com/gradle/gradle/issues/27218
37+
logger.warn {
38+
// If we're here, then T isn't available, so don't use T::class.
39+
// Instead, use the available extension's class.
40+
val actualExtensionFqn =
41+
extensions.extensionsSchema.firstOrNull { it.name == extensionName }?.publicType?.fullyQualifiedName
42+
43+
val allPlugins =
44+
project.plugins.joinToString { it::class.qualifiedName ?: "${it::class.java}" }
45+
val allExtensions =
46+
project.extensions.extensionsSchema.elements.joinToString { "${it.name} ${it.publicType}" }
47+
48+
"""
49+
|Dokka Gradle plugin failed to get extension $extensionName $actualExtensionFqn in ${project.path}
50+
|Please make sure plugins in all subprojects are consistent. See https://github.com/gradle/gradle/issues/27218
51+
| Applied plugins: $allPlugins
52+
| Available extensions: $allExtensions
53+
""".trimMargin()
54+
}
55+
56+
return null
57+
}
58+
59+
else -> throw e
60+
}
61+
}
62+
}

dokka-runners/dokka-gradle-plugin/src/testFunctional/kotlin/MultiModuleFunctionalTest.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -520,14 +520,17 @@ class MultiModuleFunctionalTest : FunSpec({
520520

521521
test("expect warning regarding KotlinProjectExtension") {
522522
project.runner
523-
.addArguments("clean")
523+
.addArguments(
524+
"clean",
525+
"--stacktrace",
526+
)
524527
.forwardOutput()
525528
.build {
526529
// the root project doesn't have the KGP applied, so KotlinProjectExtension shouldn't be applied
527-
output shouldNotContain "KotlinAdapter failed to get KotlinProjectExtension in :\n"
530+
output shouldNotContain "Dokka Gradle plugin failed to get extension kotlin org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension in :\n"
528531

529-
output shouldContain "KotlinAdapter failed to get KotlinProjectExtension in :subproject-hello\n"
530-
output shouldContain "KotlinAdapter failed to get KotlinProjectExtension in :subproject-goodbye\n"
532+
output shouldContain "Dokka Gradle plugin failed to get extension kotlin org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension in :subproject-hello\n"
533+
output shouldContain "Dokka Gradle plugin failed to get extension kotlin org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension in :subproject-goodbye\n"
531534
}
532535
}
533536
}

0 commit comments

Comments
 (0)