-
Notifications
You must be signed in to change notification settings - Fork 470
KGP 2.2.10 + AGP: Use Android components to determine if source-sets are 'publishable' #4231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
afb8b31
20cabc8
3296827
6ed6540
144405d
a869239
e8cd21e
4fbeeed
f8a02d6
f08f6ad
3663dce
efa359a
d744657
6642dab
5554f87
5bf6681
6fca413
acd43cb
0758d2e
1f92f05
e10f1e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<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 { | ||
|
|
@@ -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>, | ||
|
|
@@ -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, | ||
|
|
@@ -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<*>, | ||
|
|
@@ -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) | ||
| } | ||
| } | ||
|
|
||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We trigger this function with different plugins, like however, we do not trigger it for the listed If yes, why just having
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The behaviour by AGP isn't well specified, but I think the answer is 'no'.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question... Thinking again, I think For @whyoleg Do you have any opinion on whether DGP should support projects with
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Also, AFAIU, with AGP9, users will use kotlin built into those plugins, and this will be less of an issue in the future.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| * | ||
| * 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>, | ||
| ) { | ||
adam-enko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| val androidComponents = project.findAndroidComponentExtension() | ||
whyoleg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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( | ||
whyoleg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.