diff --git a/build.gradle.kts b/build.gradle.kts index 2d468371..27ed0f06 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,18 +3,15 @@ import buildsrc.utils.initIdeProjectLogo plugins { buildsrc.conventions.base - idea } group = "dev.adamko.dokkatoo" -version = "1.2.1-SNAPSHOT" +version = "1.3.0-SNAPSHOT" idea { module { - isDownloadSources = true - isDownloadJavadoc = false excludeGeneratedGradleDsl(layout) excludeDirs.apply { diff --git a/examples/custom-format-example/dokkatoo/build.gradle.kts b/examples/custom-format-example/dokkatoo/build.gradle.kts index 4c884e66..6ef45b6e 100644 --- a/examples/custom-format-example/dokkatoo/build.gradle.kts +++ b/examples/custom-format-example/dokkatoo/build.gradle.kts @@ -2,7 +2,7 @@ import dev.adamko.dokkatoo.dokka.plugins.DokkaHtmlPluginParameters plugins { kotlin("jvm") version "1.8.10" - id("dev.adamko.dokkatoo") version "1.2.1-SNAPSHOT" + id("dev.adamko.dokkatoo") version "1.3.0-SNAPSHOT" } dokkatoo { diff --git a/examples/gradle-example/dokkatoo/build.gradle.kts b/examples/gradle-example/dokkatoo/build.gradle.kts index e8388f00..5434ecd6 100644 --- a/examples/gradle-example/dokkatoo/build.gradle.kts +++ b/examples/gradle-example/dokkatoo/build.gradle.kts @@ -1,6 +1,6 @@ plugins { kotlin("jvm") version "1.8.10" - id("dev.adamko.dokkatoo") version "1.2.1-SNAPSHOT" + id("dev.adamko.dokkatoo") version "1.3.0-SNAPSHOT" } dokkatoo { diff --git a/examples/multimodule-example/dokkatoo/parentProject/build.gradle.kts b/examples/multimodule-example/dokkatoo/parentProject/build.gradle.kts index 1a42e01d..8da604c6 100644 --- a/examples/multimodule-example/dokkatoo/parentProject/build.gradle.kts +++ b/examples/multimodule-example/dokkatoo/parentProject/build.gradle.kts @@ -1,6 +1,6 @@ plugins { kotlin("jvm") version "1.7.20" apply false - id("dev.adamko.dokkatoo") version "1.2.1-SNAPSHOT" + id("dev.adamko.dokkatoo") version "1.3.0-SNAPSHOT" } dependencies { diff --git a/examples/multimodule-example/dokkatoo/parentProject/childProjectA/build.gradle.kts b/examples/multimodule-example/dokkatoo/parentProject/childProjectA/build.gradle.kts index 4d145909..f12ef515 100644 --- a/examples/multimodule-example/dokkatoo/parentProject/childProjectA/build.gradle.kts +++ b/examples/multimodule-example/dokkatoo/parentProject/childProjectA/build.gradle.kts @@ -1,6 +1,6 @@ plugins { kotlin("jvm") - id("dev.adamko.dokkatoo") version "1.2.1-SNAPSHOT" + id("dev.adamko.dokkatoo") version "1.3.0-SNAPSHOT" } dokkatoo { diff --git a/examples/multimodule-example/dokkatoo/parentProject/childProjectB/build.gradle.kts b/examples/multimodule-example/dokkatoo/parentProject/childProjectB/build.gradle.kts index 803c583a..b83cd9eb 100644 --- a/examples/multimodule-example/dokkatoo/parentProject/childProjectB/build.gradle.kts +++ b/examples/multimodule-example/dokkatoo/parentProject/childProjectB/build.gradle.kts @@ -1,6 +1,6 @@ plugins { kotlin("jvm") - id("dev.adamko.dokkatoo") version "1.2.1-SNAPSHOT" + id("dev.adamko.dokkatoo") version "1.3.0-SNAPSHOT" } dokkatoo { diff --git a/examples/multiplatform-example/dokkatoo/build.gradle.kts b/examples/multiplatform-example/dokkatoo/build.gradle.kts index 9f9d36e6..7fa7c9ab 100644 --- a/examples/multiplatform-example/dokkatoo/build.gradle.kts +++ b/examples/multiplatform-example/dokkatoo/build.gradle.kts @@ -1,6 +1,6 @@ plugins { kotlin("multiplatform") version "1.8.10" - id("dev.adamko.dokkatoo") version "1.2.1-SNAPSHOT" + id("dev.adamko.dokkatoo") version "1.3.0-SNAPSHOT" } group = "org.dokka.example" @@ -23,7 +23,6 @@ kotlin { } dokkatoo { - // Create a custom source set not known to the Kotlin Gradle Plugin dokkatooSourceSets.register("customSourceSet") { jdkVersion.set(9) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 03f272b6..23ce8027 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -kotlin = "1.7.20" +kotlin = "1.7.10" # should match Gradle's embedded Kotlin version https://docs.gradle.org/current/userguide/compatibility.html#kotlin kotlin-dokka = "1.8.10" kotlinx-serialization = "1.5.0" diff --git a/modules/dokkatoo-plugin-integration-tests/projects/it-basic/dokkatoo/build.gradle.kts b/modules/dokkatoo-plugin-integration-tests/projects/it-basic/dokkatoo/build.gradle.kts index 50aa136c..00a7487d 100644 --- a/modules/dokkatoo-plugin-integration-tests/projects/it-basic/dokkatoo/build.gradle.kts +++ b/modules/dokkatoo-plugin-integration-tests/projects/it-basic/dokkatoo/build.gradle.kts @@ -3,7 +3,7 @@ import dev.adamko.dokkatoo.dokka.plugins.DokkaHtmlPluginParameters plugins { kotlin("jvm") version "1.8.10" - id("dev.adamko.dokkatoo") version "1.2.1-SNAPSHOT" + id("dev.adamko.dokkatoo") version "1.3.0-SNAPSHOT" } version = "1.7.20-SNAPSHOT" diff --git a/modules/dokkatoo-plugin/api/dokkatoo-plugin.api b/modules/dokkatoo-plugin/api/dokkatoo-plugin.api index e8db8b44..17b34efa 100644 --- a/modules/dokkatoo-plugin/api/dokkatoo-plugin.api +++ b/modules/dokkatoo-plugin/api/dokkatoo-plugin.api @@ -26,20 +26,24 @@ public abstract class dev/adamko/dokkatoo/DokkatooExtension : java/io/Serializab } public abstract interface class dev/adamko/dokkatoo/DokkatooExtension$Versions { + public static final field Companion Ldev/adamko/dokkatoo/DokkatooExtension$Versions$Companion; + public static final field VERSIONS_EXTENSION_NAME Ljava/lang/String; public abstract fun getFreemarker ()Lorg/gradle/api/provider/Property; public abstract fun getJetbrainsDokka ()Lorg/gradle/api/provider/Property; public abstract fun getJetbrainsMarkdown ()Lorg/gradle/api/provider/Property; + public abstract fun getKotlinxCoroutines ()Lorg/gradle/api/provider/Property; public abstract fun getKotlinxHtml ()Lorg/gradle/api/provider/Property; } +public final class dev/adamko/dokkatoo/DokkatooExtension$Versions$Companion { + public static final field VERSIONS_EXTENSION_NAME Ljava/lang/String; +} + public abstract class dev/adamko/dokkatoo/DokkatooPlugin : org/gradle/api/Plugin { public synthetic fun apply (Ljava/lang/Object;)V public fun apply (Lorg/gradle/api/Project;)V } -public final class dev/adamko/dokkatoo/adapters/DokkatooKotlinAdapter$Companion { -} - public abstract class dev/adamko/dokkatoo/dokka/DokkaPublication : java/io/Serializable, org/gradle/api/Named { public abstract fun getCacheRoot ()Lorg/gradle/api/file/DirectoryProperty; protected final fun getCacheRootPath ()Lorg/gradle/api/provider/Provider; @@ -218,6 +222,7 @@ public abstract class dev/adamko/dokkatoo/dokka/parameters/DokkaSourceSetIdSpec public final fun getScopeId ()Ljava/lang/String; public abstract fun getSourceSetName ()Ljava/lang/String; public abstract fun setSourceSetName (Ljava/lang/String;)V + public fun toString ()Ljava/lang/String; } public final class dev/adamko/dokkatoo/dokka/parameters/DokkaSourceSetIdSpec$Companion { @@ -265,6 +270,7 @@ public abstract class dev/adamko/dokkatoo/dokka/parameters/DokkaSourceSetSpec : } public final class dev/adamko/dokkatoo/dokka/parameters/KotlinPlatform : java/lang/Enum { + public static final field AndroidJVM Ldev/adamko/dokkatoo/dokka/parameters/KotlinPlatform; public static final field Common Ldev/adamko/dokkatoo/dokka/parameters/KotlinPlatform; public static final field Companion Ldev/adamko/dokkatoo/dokka/parameters/KotlinPlatform$Companion; public static final field JS Ldev/adamko/dokkatoo/dokka/parameters/KotlinPlatform; diff --git a/modules/dokkatoo-plugin/src/main/kotlin/DokkatooBasePlugin.kt b/modules/dokkatoo-plugin/src/main/kotlin/DokkatooBasePlugin.kt index 37214e6f..65b2db58 100644 --- a/modules/dokkatoo-plugin/src/main/kotlin/DokkatooBasePlugin.kt +++ b/modules/dokkatoo-plugin/src/main/kotlin/DokkatooBasePlugin.kt @@ -1,5 +1,6 @@ package dev.adamko.dokkatoo +import dev.adamko.dokkatoo.DokkatooExtension.Versions.Companion.VERSIONS_EXTENSION_NAME import dev.adamko.dokkatoo.distibutions.DokkatooConfigurationAttributes import dev.adamko.dokkatoo.distibutions.DokkatooConfigurationAttributes.Companion.DOKKATOO_BASE_ATTRIBUTE import dev.adamko.dokkatoo.distibutions.DokkatooConfigurationAttributes.Companion.DOKKATOO_CATEGORY_ATTRIBUTE @@ -118,11 +119,12 @@ constructor( dokkatooModuleDirectory.convention(layout.buildDirectory.dir("dokka-module")) dokkatooConfigurationsDirectory.convention(layout.buildDirectory.dir("dokka-config")) - extensions.create("versions").apply { + extensions.create(VERSIONS_EXTENSION_NAME).apply { jetbrainsDokka.convention(DokkatooConstants.DOKKA_VERSION) jetbrainsMarkdown.convention("0.3.1") freemarker.convention("2.3.31") kotlinxHtml.convention("0.8.0") + kotlinxCoroutines.convention("1.6.4") } } } @@ -160,7 +162,7 @@ constructor( name.endsWith("Main") -> name.substringBeforeLast("Main") // indeterminate source sets should be named by the Kotlin platform - else -> platform.key + else -> platform.displayName } } ) @@ -176,7 +178,9 @@ constructor( enableKotlinStdLibDocumentationLink.convention(true) enableJdkDocumentationLink.convention(true) - enableAndroidDocumentationLink.convention(false) + enableAndroidDocumentationLink.convention( + analysisPlatform.map { it == KotlinPlatform.AndroidJVM } + ) reportUndocumented.convention(false) skipDeprecated.convention(false) diff --git a/modules/dokkatoo-plugin/src/main/kotlin/DokkatooExtension.kt b/modules/dokkatoo-plugin/src/main/kotlin/DokkatooExtension.kt index 42ae447e..974b5732 100644 --- a/modules/dokkatoo-plugin/src/main/kotlin/DokkatooExtension.kt +++ b/modules/dokkatoo-plugin/src/main/kotlin/DokkatooExtension.kt @@ -104,5 +104,10 @@ constructor( val jetbrainsMarkdown: Property val freemarker: Property val kotlinxHtml: Property + val kotlinxCoroutines: Property + + companion object { + const val VERSIONS_EXTENSION_NAME = "versions" + } } } diff --git a/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooAndroidAdapter.kt b/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooAndroidAdapter.kt new file mode 100644 index 00000000..7bb05b56 --- /dev/null +++ b/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooAndroidAdapter.kt @@ -0,0 +1,75 @@ +package dev.adamko.dokkatoo.adapters + +import com.android.build.gradle.internal.dependency.VariantDependencies +import com.android.build.gradle.internal.publishing.AndroidArtifacts +import dev.adamko.dokkatoo.DokkatooBasePlugin +import dev.adamko.dokkatoo.DokkatooExtension +import dev.adamko.dokkatoo.dokka.parameters.KotlinPlatform +import dev.adamko.dokkatoo.internal.DokkatooInternalApi +import dev.adamko.dokkatoo.internal.collectIncomingFiles +import javax.inject.Inject +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.FileCollection +import org.gradle.api.logging.Logging +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.* + +@DokkatooInternalApi +abstract class DokkatooAndroidAdapter @Inject constructor( + private val objects: ObjectFactory, +) : Plugin { + + override fun apply(project: Project) { + logger.info("applied DokkatooAndroidAdapter to ${project.path}") + + project.plugins.withType().configureEach { + project.pluginManager.apply { + withPlugin("com.android.base") { configure(project) } + withPlugin("com.android.application") { configure(project) } + withPlugin("com.android.library") { configure(project) } + } + } + } + + protected fun configure(project: Project) { + val dokkatooExtension = project.extensions.getByType() + + dokkatooExtension.dokkatooSourceSets.configureEach { + + val androidClasspath: Provider = + analysisPlatform.map { + val compilationClasspath = objects.fileCollection() + if (it == KotlinPlatform.AndroidJVM) { + + fun collectConfiguration(named: String) { + project.configurations.collectIncomingFiles(named, compilationClasspath) + // need to fetch JARs explicitly, because Android Gradle Plugin is weird + // and doesn't seem to register the attributes properly + @Suppress("UnstableApiUsage") + project.configurations.collectIncomingFiles(named, compilationClasspath) { + withVariantReselection() + attributes { + attribute(AndroidArtifacts.ARTIFACT_TYPE, AndroidArtifacts.ArtifactType.JAR.type) + } + lenient(true) + } + } + + // fetch android.jar + collectConfiguration(named = VariantDependencies.CONFIG_NAME_ANDROID_APIS) + } + + compilationClasspath + } + + classpath.from(androidClasspath) + } + } + + @DokkatooInternalApi + companion object { + private val logger = Logging.getLogger(DokkatooAndroidAdapter::class.java) + } +} diff --git a/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooKotlinAdapter.kt b/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooKotlinAdapter.kt index 8d625d3a..688c201d 100644 --- a/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooKotlinAdapter.kt +++ b/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooKotlinAdapter.kt @@ -6,14 +6,19 @@ import dev.adamko.dokkatoo.DokkatooBasePlugin import dev.adamko.dokkatoo.DokkatooExtension import dev.adamko.dokkatoo.dokka.parameters.DokkaSourceSetIdSpec import dev.adamko.dokkatoo.dokka.parameters.DokkaSourceSetIdSpec.Companion.dokkaSourceSetIdSpec +import dev.adamko.dokkatoo.dokka.parameters.DokkaSourceSetSpec import dev.adamko.dokkatoo.dokka.parameters.KotlinPlatform import dev.adamko.dokkatoo.internal.DokkatooInternalApi +import dev.adamko.dokkatoo.internal.collectIncomingFiles import dev.adamko.dokkatoo.internal.not import javax.inject.Inject import org.gradle.api.Named +import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.ConfigurationContainer +import org.gradle.api.attributes.Usage.JAVA_RUNTIME +import org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.FileCollection import org.gradle.api.logging.Logging @@ -22,12 +27,14 @@ import org.gradle.api.plugins.ExtensionContainer import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Provider import org.gradle.api.provider.ProviderFactory +import org.gradle.api.provider.SetProperty import org.gradle.kotlin.dsl.* import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.mpp.AbstractKotlinNativeCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation /** @@ -63,124 +70,102 @@ abstract class DokkatooKotlinAdapter @Inject constructor( val dokkatooExtension = project.extensions.getByType() - val allKotlinCompilationDetails = kotlinExtension.compilationDetails(objects, providers) - - val sourceSetDetails = objects.domainObjectContainer(KotlinSourceSetDetails::class) - - kotlinExtension.sourceSets.all kss@{ - - val dependentSourceSets = - this@kss - .allDependentSourceSets() - .fold(objects.fileCollection()) { acc, src -> - acc.from(src.kotlin.sourceDirectories) - } - - // TODO: Needs to respect filters. - // We probably need to change from "sourceRoots" to support "sourceFiles" - // https://github.com/Kotlin/dokka/issues/1215 - val extantSourceDirectories = - this@kss.kotlin.sourceDirectories.filter { it.exists() } - - // determine the source sets IDs of _other_ source sets that _this_ source depends on. - val dependentSourceSetIds = - dokkatooExtension.sourceSetScopeDefault.map { sourceSetScope -> - this@kss.dependsOn.map { dependedKss -> - objects.dokkaSourceSetIdSpec(sourceSetScope, dependedKss.name) - } - } - - // find all compilation details that this source set needs - val compilations = allKotlinCompilationDetails.map { allCompilations -> - allCompilations.filter { compilation -> - this@kss.name in compilation.allKotlinSourceSetsNames - } - } - - sourceSetDetails.register(name) { - this.dependentSourceSetIds.addAll(dependentSourceSetIds) - this.sourceSets.from(extantSourceDirectories) - this.dependentSourceSets.from(dependentSourceSets) - this.compilations.addAll(compilations) - } - } + // first fetch the relevant properties of all KotlinCompilations + val compilationDetailsBuilder = KotlinCompilationDetailsBuilder( + providers = providers, + objects = objects, + configurations = project.configurations, + projectPath = project.path, + ) + val allKotlinCompilationDetails: ListProperty = + compilationDetailsBuilder.createCompilationDetails( + kotlinProjectExtension = kotlinExtension, + ) + + // second, fetch the relevant properties of the Kotlin source sets + val sourceSetDetailsBuilder = KotlinSourceSetDetailsBuilder( + providers = providers, + objects = objects, + sourceSetScopeDefault = dokkatooExtension.sourceSetScopeDefault, + projectPath = project.path, + ) + val sourceSetDetails: NamedDomainObjectContainer = + sourceSetDetailsBuilder.createSourceSetDetails( + kotlinSourceSets = kotlinExtension.sourceSets, + allKotlinCompilationDetails = allKotlinCompilationDetails, + ) + + // for each Kotlin source set, register a Dokkatoo source set + registerDokkatooSourceSets( + dokkatooExtension = dokkatooExtension, + sourceSetDetails = sourceSetDetails, + ) + } + /** Register a [DokkaSourceSetSpec] for each element in [sourceSetDetails] */ + private fun registerDokkatooSourceSets( + dokkatooExtension: DokkatooExtension, + sourceSetDetails: NamedDomainObjectContainer, + ) { // proactively use 'all' so source sets will be available in users' build files if they use `named("...")` sourceSetDetails.all details@{ + dokkatooExtension.dokkatooSourceSets.register(details = this@details) + } + } - val kssPlatform = compilations.map { values -> - values.map { it.kotlinPlatform } - .distinct() - .singleOrNull() ?: KotlinPlatform.Common - } + /** Register a single [DokkaSourceSetSpec] for [details] */ + private fun NamedDomainObjectContainer.register( + details: KotlinSourceSetDetails + ) { + val kssPlatform = details.compilations.map { values: List -> + values.map { it.kotlinPlatform } + .distinct() + .singleOrNull() ?: KotlinPlatform.Common + } - val sourceSetConfigurationNames = compilations.map { values -> - values.flatMap { it.configurationNames } - } + val kssClasspath = determineClasspath(details) - val kssClasspath = sourceSetConfigurationNames.map { names -> - getKSSClasspath(project.configurations, objects, names) - } + register(details.name) dss@{ + // only set source-set specific properties, default values for the other properties + // (like displayName) are set in DokkatooBasePlugin + suppress.set(!details.isMainSourceSet()) + sourceRoots.from(details.sourceDirectories) + classpath.from(kssClasspath) + analysisPlatform.set(kssPlatform) + dependentSourceSets.addAllLater(details.dependentSourceSetIds) + } + } - dokkatooExtension.dokkatooSourceSets.register(this@details.name) { - // only set source-set specific properties, default values for the other properties - // (like displayName) are set in DokkatooBasePlugin - this.suppress.set(!isMainSourceSet()) - this.sourceRoots.from(this@details.sourceSets) - this.classpath.from(kssClasspath) - this.analysisPlatform.set(kssPlatform) - this.dependentSourceSets.addAllLater(this@details.dependentSourceSetIds) + private fun determineClasspath( + details: KotlinSourceSetDetails + ): Provider { + return details.compilations.map { compilations: List -> + val classpath = objects.fileCollection() + + if (compilations.isNotEmpty()) { + compilations.fold(classpath) { acc, compilation -> + acc.from(compilation.compilationClasspath) + // can't use compileDependencyFiles, it causes weird dependency resolution errors in Android projects + //acc.from(providers.provider { compilation.compileDependencyFiles }) + } + } else { + classpath + .from(details.sourceDirectories) + .from(details.sourceDirectoriesOfDependents) } } } + @DokkatooInternalApi companion object { private val logger = Logging.getLogger(DokkatooKotlinAdapter::class.java) - private fun KotlinProjectExtension.compilationDetails( - objects: ObjectFactory, - providers: ProviderFactory, - ): ListProperty { - - val details = objects.listProperty() - - details.addAll( - providers.provider { - val allKotlinCompilations: Collection> = when (this) { - is KotlinMultiplatformExtension -> targets.flatMap { it.compilations } - is KotlinSingleTargetExtension<*> -> target.compilations - else -> emptyList() - } - - allKotlinCompilations.map { compilation -> - KotlinCompilationDetails( - target = compilation.target.name, - kotlinPlatform = KotlinPlatform.fromString(compilation.platformType.name), - configurationNames = compilation.allConfigurationNames(), - allKotlinSourceSetsNames = compilation.allKotlinSourceSets.map { it.name }, - mainCompilation = compilation.isMainCompilation(), - ) - } - }) - - return details - } - - - /** Recursively get all [KotlinSourceSet]s that this source set depends on */ - private tailrec fun KotlinSourceSet.allDependentSourceSets( - queue: ArrayDeque = ArrayDeque().apply { addAll(dependsOn) }, - allDependents: List = emptyList(), - ): List { - val next = queue.removeFirstOrNull() ?: return allDependents - queue.addAll(next.dependsOn) - return next.allDependentSourceSets(queue, allDependents + next) - } - 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) { @@ -191,46 +176,6 @@ abstract class DokkatooKotlinAdapter @Inject constructor( else -> throw e } } - - private fun KotlinCompilation<*>.isMainCompilation(): Boolean { - return when (this) { - is KotlinJvmAndroidCompilation -> - androidVariant is LibraryVariant || androidVariant is ApplicationVariant - - else -> - name == "main" - } - } - - /** - * Get the [Configuration][org.gradle.api.artifacts.Configuration] names of all configurations - * used to build this [KotlinCompilation] and - * [its source sets][KotlinCompilation.kotlinSourceSets]. - */ - private fun KotlinCompilation<*>.allConfigurationNames(): Set = - relatedConfigurationNames union kotlinSourceSets.flatMap { it.relatedConfigurationNames } - - /** Get the classpath used to build and run a Kotlin Source Set */ - private fun getKSSClasspath( - configurations: ConfigurationContainer, - objects: ObjectFactory, - kotlinSourceSetConfigurationNames: List, - ): FileCollection { - return kotlinSourceSetConfigurationNames - .mapNotNull { kssConfName -> configurations.findByName(kssConfName) } - .filter { conf -> conf.isCanBeResolved } - .fold(objects.fileCollection()) { classpathCollector, conf -> - classpathCollector.from( - @Suppress("UnstableApiUsage") - conf - .incoming - .artifactView { lenient(true) } - .artifacts - .resolvedArtifacts - .map { artifacts -> artifacts.map { it.file } } - ) - } - } } } @@ -241,27 +186,162 @@ abstract class DokkatooKotlinAdapter @Inject constructor( * The compilation details may come from a multiplatform project ([KotlinMultiplatformExtension]) * or a single-platform project ([KotlinSingleTargetExtension]). */ +@DokkatooInternalApi private data class KotlinCompilationDetails( val target: String, val kotlinPlatform: KotlinPlatform, - val configurationNames: Set, - val allKotlinSourceSetsNames: List, + val allKotlinSourceSetsNames: Set, val mainCompilation: Boolean, + val compileDependencyFiles: FileCollection, + val dependentSourceSetNames: Set, + val compilationClasspath: FileCollection, ) +/** Utility class, encapsulating logic for building [KotlinCompilationDetails] */ +private class KotlinCompilationDetailsBuilder( + private val objects: ObjectFactory, + private val providers: ProviderFactory, + private val configurations: ConfigurationContainer, + /** Used for logging */ + private val projectPath: String, +) { + private val logger = Logging.getLogger(KotlinCompilationDetails::class.java) + + fun createCompilationDetails( + kotlinProjectExtension: KotlinProjectExtension, + ): ListProperty { + + val details = objects.listProperty() + + details.addAll( + providers.provider { + kotlinProjectExtension + .allKotlinCompilations() + .map { compilation -> + createCompilationDetails(compilation = compilation) + } + }) + + return details + } + + /** Create a single [KotlinCompilationDetails] for [compilation] */ + private fun createCompilationDetails( + compilation: KotlinCompilation<*>, + ): KotlinCompilationDetails { + val allKotlinSourceSetsNames = + compilation.allKotlinSourceSets.map { it.name } + compilation.defaultSourceSet.name + + val compileDependencyFiles = objects.fileCollection() + .from(providers.provider { compilation.compileDependencyFiles }) + + val dependentSourceSetNames = + compilation.defaultSourceSet.dependsOn.map { it.name } + + val compilationClasspath: FileCollection = + collectKotlinCompilationClasspath(compilation = compilation) + + return KotlinCompilationDetails( + target = compilation.target.name, + kotlinPlatform = KotlinPlatform.fromString(compilation.platformType.name), + allKotlinSourceSetsNames = allKotlinSourceSetsNames.toSet(), + mainCompilation = compilation.isMain(), + compileDependencyFiles = compileDependencyFiles, + dependentSourceSetNames = dependentSourceSetNames.toSet(), + compilationClasspath = compilationClasspath, + ) + } + + private fun KotlinProjectExtension.allKotlinCompilations(): Collection> = + when (this) { + is KotlinMultiplatformExtension -> targets.flatMap { it.compilations } + is KotlinSingleTargetExtension -> target.compilations + else -> emptyList() // shouldn't happen? + } + + /** + * Get the [Configuration][org.gradle.api.artifacts.Configuration] names of all configurations + * used to build this [KotlinCompilation] and + * [its source sets][KotlinCompilation.kotlinSourceSets]. + */ + private fun collectKotlinCompilationClasspath( + compilation: KotlinCompilation<*>, + ): FileCollection { + + val compilationClasspath = objects.fileCollection() + fun collectConfiguration(named: String) { + configurations.collectIncomingFiles(named = named, collector = compilationClasspath) + + // need to fetch JAVA_RUNTIME files explicitly, because Android Gradle Plugin is weird and + // doesn't seem to register the attributes explicitly on its configurations + @Suppress("UnstableApiUsage") + configurations.collectIncomingFiles(named = named, collector = compilationClasspath) { + withVariantReselection() + attributes { + attribute(USAGE_ATTRIBUTE, objects.named(JAVA_RUNTIME)) + } + lenient(true) + } + } + + val standardConfigurations = mutableListOf().apply { + addAll(compilation.relatedConfigurationNames) + addAll(compilation.kotlinSourceSets.flatMap { it.relatedConfigurationNames }) + }.toSet() + + logger.info("[$projectPath] compilation ${compilation.name} has ${standardConfigurations.size} standard configurations $standardConfigurations") + + standardConfigurations.forEach { collectConfiguration(it) } + + if (compilation is AbstractKotlinNativeCompilation) { + // K/N doesn't correctly set task dependencies, the configuration + // `defaultSourceSet.implementationMetadataConfigurationName` + // will trigger a bunch of Gradle warnings about "using file outputs without task dependencies", + // so K/N compilations need to explicitly depend on the compilation tasks + // UPDATE: actually I think is wrong, it's a bug with the K/N 'commonize for IDE' tasks + // see: https://github.com/Kotlin/dokka/issues/2977 + collectConfiguration( + named = compilation.defaultSourceSet.implementationMetadataConfigurationName, +// builtBy = kotlinCompilation.compileKotlinTaskProvider + ) + } + + return compilationClasspath + } + + companion object { + private fun KotlinCompilation<*>.isMain(): Boolean { + return when (this) { + is KotlinJvmAndroidCompilation -> + androidVariant is LibraryVariant || androidVariant is ApplicationVariant + + else -> + name == KotlinCompilation.Companion.MAIN_COMPILATION_NAME + } + } + } +} + /** * Store the details of all [KotlinSourceSet]s in a configuration cache compatible way. + * + * @param[named] Should be [KotlinSourceSet.getName] */ +@DokkatooInternalApi private abstract class KotlinSourceSetDetails @Inject constructor( private val named: String, ) : Named { - abstract val dependentSourceSetIds: ListProperty - abstract val sourceSets: ConfigurableFileCollection - abstract val dependentSourceSets: ConfigurableFileCollection + /** Direct source sets that this source set depends on */ + abstract val dependentSourceSetIds: SetProperty + abstract val sourceDirectories: ConfigurableFileCollection + /** _All_ source directories from any (recursively) dependant source set */ + abstract val sourceDirectoriesOfDependents: ConfigurableFileCollection + /** The specific compilations used to build this source set */ abstract val compilations: ListProperty + /** Estimate if this Kotlin source set are 'main' sources (as opposed to 'test' sources). */ fun isMainSourceSet(): Provider = compilations.map { values -> values.isEmpty() || values.any { it.mainCompilation } @@ -269,3 +349,96 @@ private abstract class KotlinSourceSetDetails @Inject constructor( override fun getName(): String = named } + +/** Utility class, encapsulating logic for building [KotlinCompilationDetails] */ +private class KotlinSourceSetDetailsBuilder( + private val sourceSetScopeDefault: Provider, + private val objects: ObjectFactory, + private val providers: ProviderFactory, + /** Used for logging */ + private val projectPath: String, +) { + + private val logger = Logging.getLogger(KotlinSourceSetDetails::class.java) + + fun createSourceSetDetails( + kotlinSourceSets: NamedDomainObjectContainer, + allKotlinCompilationDetails: ListProperty, + ): NamedDomainObjectContainer { + + val sourceSetDetails = objects.domainObjectContainer(KotlinSourceSetDetails::class) + + kotlinSourceSets.configureEach kss@{ + sourceSetDetails.register( + kotlinSourceSet = this, + allKotlinCompilationDetails = allKotlinCompilationDetails, + ) + } + + return sourceSetDetails + } + + private fun NamedDomainObjectContainer.register( + kotlinSourceSet: KotlinSourceSet, + allKotlinCompilationDetails: ListProperty, + ) { + + // TODO: Needs to respect filters. + // We probably need to change from "sourceRoots" to support "sourceFiles" + // https://github.com/Kotlin/dokka/issues/1215 + val extantSourceDirectories = providers.provider { + kotlinSourceSet.kotlin.sourceDirectories.filter { it.exists() } + } + + val compilations = allKotlinCompilationDetails.map { allCompilations -> + allCompilations.filter { compilation -> + kotlinSourceSet.name in compilation.allKotlinSourceSetsNames + } + } + + // determine the source sets IDs of _other_ source sets that _this_ source depends on. + val dependentSourceSets = providers.provider { kotlinSourceSet.dependsOn } + val dependentSourceSetIds = + providers.zip( + dependentSourceSets, + sourceSetScopeDefault, + ) { sourceSets, sourceSetScope -> + logger.info("[$projectPath] source set ${kotlinSourceSet.name} has ${sourceSets.size} dependents ${sourceSets.joinToString { it.name }}") + sourceSets.map { dependedKss -> + objects.dokkaSourceSetIdSpec(sourceSetScope, dependedKss.name) + } + } + + val sourceDirectoriesOfDependents = providers.provider { + kotlinSourceSet + .allDependentSourceSets() + .fold(objects.fileCollection()) { acc, sourceSet -> + acc.from(sourceSet.kotlin.sourceDirectories) + } + } + + register(kotlinSourceSet.name) { + this.dependentSourceSetIds.addAll(dependentSourceSetIds) + this.sourceDirectories.from(extantSourceDirectories) + this.sourceDirectoriesOfDependents.from(sourceDirectoriesOfDependents) + this.compilations.addAll(compilations) + } + } + + /** + * Return a list containing _all_ source sets that this source set depends on, + * searching recursively. + * + * @see KotlinSourceSet.dependsOn + */ + private tailrec fun KotlinSourceSet.allDependentSourceSets( + queue: Set = dependsOn.toSet(), + allDependents: List = emptyList(), + ): List { + val next = queue.firstOrNull() ?: return allDependents + return next.allDependentSourceSets( + queue = (queue - next) union next.dependsOn, + allDependents = allDependents + next, + ) + } +} diff --git a/modules/dokkatoo-plugin/src/main/kotlin/dokka/DokkaPublication.kt b/modules/dokkatoo-plugin/src/main/kotlin/dokka/DokkaPublication.kt index e6399757..0f6fca14 100644 --- a/modules/dokkatoo-plugin/src/main/kotlin/dokka/DokkaPublication.kt +++ b/modules/dokkatoo-plugin/src/main/kotlin/dokka/DokkaPublication.kt @@ -64,7 +64,8 @@ constructor( get() = outputDir.map { it.asFile.invariantSeparatorsPath } @get:Internal - // marked as Internal because this task does not use the directory contents, only the location + // Marked as Internal because this task does not use the directory contents, only the location. + // Note that `cacheRoot` is not used by Dokka, and will probably be deprecated. abstract val cacheRoot: DirectoryProperty /** diff --git a/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaSourceSetIdSpec.kt b/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaSourceSetIdSpec.kt index d2ea7b42..68cab7d2 100644 --- a/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaSourceSetIdSpec.kt +++ b/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaSourceSetIdSpec.kt @@ -35,7 +35,9 @@ constructor( DokkaParametersKxs.SourceSetIdKxs(scopeId, sourceSetName) @Internal - override fun getName(): String = scopeId + override fun getName(): String = "$scopeId/$sourceSetName" + + override fun toString(): String = "DokkaSourceSetIdSpec($name)" companion object { @@ -48,6 +50,5 @@ constructor( newInstance(scopeId).apply { this.sourceSetName = sourceSetName } - } } diff --git a/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/KotlinPlatform.kt b/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/KotlinPlatform.kt index 1ad6ee0b..7d4fc77d 100644 --- a/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/KotlinPlatform.kt +++ b/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/KotlinPlatform.kt @@ -7,31 +7,40 @@ import org.jetbrains.dokka.Platform * The Kotlin * * @see org.jetbrains.dokka.Platform + * @param[displayName] The display name, eventually used in the rendered Dokka publication. */ -enum class KotlinPlatform { - JVM, - JS, - WASM, - Native, - Common, +enum class KotlinPlatform( + internal val displayName: String +) { + AndroidJVM("androidJvm"), + Common("common"), + JS("js"), + JVM("jvm"), + Native("native"), + WASM("wasm"), ; + @Deprecated("Unused", ReplaceWith("name.toLowerCase()")) + @Suppress("unused") val key: String = name.toLowerCase() companion object { - internal val entries: Set = values().toSet() + internal val values: Set = values().toSet() val DEFAULT: KotlinPlatform = JVM fun fromString(key: String): KotlinPlatform { + val keyMatch = values.firstOrNull { + it.name.equals(key, ignoreCase = true) || it.displayName.equals(key, ignoreCase = true) + } + if (keyMatch != null) { + return keyMatch + } return when (key.toLowerCase()) { - JVM.key, "androidjvm", "android" -> JVM - JS.key -> JS - WASM.key -> WASM - Native.key -> Native - Common.key, "metadata" -> Common - else -> error("Unrecognized platform: $key") + "android" -> AndroidJVM + "metadata" -> Common + else -> error("Unrecognized platform: $key") } } @@ -39,11 +48,11 @@ enum class KotlinPlatform { internal val KotlinPlatform.dokkaType: Platform get() = when (this) { - JVM -> Platform.jvm - JS -> Platform.js - WASM -> Platform.wasm - Native -> Platform.native - Common -> Platform.common + AndroidJVM, JVM -> Platform.jvm + JS -> Platform.js + WASM -> Platform.wasm + Native -> Platform.native + Common -> Platform.common } } } diff --git a/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooFormatPlugin.kt b/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooFormatPlugin.kt index a6abfb94..08f8db53 100644 --- a/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooFormatPlugin.kt +++ b/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooFormatPlugin.kt @@ -2,6 +2,7 @@ package dev.adamko.dokkatoo.formats import dev.adamko.dokkatoo.DokkatooBasePlugin import dev.adamko.dokkatoo.DokkatooExtension +import dev.adamko.dokkatoo.adapters.DokkatooAndroidAdapter import dev.adamko.dokkatoo.adapters.DokkatooJavaAdapter import dev.adamko.dokkatoo.adapters.DokkatooKotlinAdapter import dev.adamko.dokkatoo.distibutions.DokkatooConfigurationAttributes @@ -68,6 +69,7 @@ abstract class DokkatooFormatPlugin( // apply the plugin that will autoconfigure Dokkatoo to use the sources of a Kotlin project target.pluginManager.apply(type = DokkatooKotlinAdapter::class) target.pluginManager.apply(type = DokkatooJavaAdapter::class) + target.pluginManager.apply(type = DokkatooAndroidAdapter::class) target.plugins.withType().configureEach { val dokkatooExtension = target.extensions.getByType(DokkatooExtension::class) @@ -191,8 +193,9 @@ abstract class DokkatooFormatPlugin( dokkaPlugin("org.freemarker:freemarker" version freemarker) dokkaGenerator(dokka("dokka-core")) - // TODO why does this need a -jvm suffix? + // TODO why does org.jetbrains:markdown need a -jvm suffix? dokkaGenerator("org.jetbrains:markdown-jvm" version jetbrainsMarkdown) + dokkaGenerator("org.jetbrains.kotlinx:kotlinx-coroutines-core" version kotlinxCoroutines) } } } diff --git a/modules/dokkatoo-plugin/src/main/kotlin/internal/gradleUtils.kt b/modules/dokkatoo-plugin/src/main/kotlin/internal/gradleUtils.kt index c699a03f..84263a7f 100644 --- a/modules/dokkatoo-plugin/src/main/kotlin/internal/gradleUtils.kt +++ b/modules/dokkatoo-plugin/src/main/kotlin/internal/gradleUtils.kt @@ -4,9 +4,12 @@ import org.gradle.api.Action import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.Project import org.gradle.api.Task +import org.gradle.api.artifacts.ArtifactView import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.component.ComponentIdentifier import org.gradle.api.artifacts.component.ProjectComponentIdentifier +import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.provider.Provider import org.gradle.api.specs.Spec import org.gradle.api.tasks.TaskProvider @@ -72,3 +75,44 @@ internal fun NamedDomainObjectContainer.maybeCreate( name: String, configure: T.() -> Unit, ): T = maybeCreate(name).apply(configure) + + +/** + * Aggregate the incoming files from a [Configuration] + * (with name [named]) into [collector]. + * + * Configurations that cannot be + * [resolved][org.gradle.api.artifacts.Configuration.isCanBeResolved] + * will be ignored. + * + * @param[builtBy] An optional [TaskProvider], used to set [ConfigurableFileCollection.builtBy]. + * This should not typically be used, and is only necessary in rare cases where a Gradle Plugin is + * misconfigured. + */ +internal fun ConfigurationContainer.collectIncomingFiles( + named: String, + collector: ConfigurableFileCollection, + builtBy: TaskProvider<*>? = null, + artifactViewConfiguration: ArtifactView.ViewConfiguration.() -> Unit = { + // ignore failures: it's usually okay if fetching files is best-effort because + // maybe Dokka doesn't need _all_ dependencies + lenient(true) + }, +) { + val conf = findByName(named) + if (conf != null && conf.isCanBeResolved) { + + val incomingFiles = + conf + .incoming + .artifactView(artifactViewConfiguration) + .artifacts + .artifactFiles + + collector.from(incomingFiles) + + if (builtBy != null) { + collector.builtBy(builtBy) + } + } +} diff --git a/modules/dokkatoo-plugin/src/main/kotlin/tasks/DokkatooPrepareParametersTask.kt b/modules/dokkatoo-plugin/src/main/kotlin/tasks/DokkatooPrepareParametersTask.kt index 5e578a08..3ebceab0 100644 --- a/modules/dokkatoo-plugin/src/main/kotlin/tasks/DokkatooPrepareParametersTask.kt +++ b/modules/dokkatoo-plugin/src/main/kotlin/tasks/DokkatooPrepareParametersTask.kt @@ -58,6 +58,7 @@ constructor( abstract val dokkaModuleFiles: ConfigurableFileCollection @get:LocalState + // cacheRoot is not used by Dokka, and will probably be deprecated abstract val cacheRoot: DirectoryProperty @get:Input diff --git a/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/KotlinPlatformTest.kt b/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/KotlinPlatformTest.kt index 1cd46828..4a92295a 100644 --- a/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/KotlinPlatformTest.kt +++ b/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/KotlinPlatformTest.kt @@ -16,7 +16,7 @@ class KotlinPlatformTest : FunSpec({ test("Dokka platform should have equivalent KotlinPlatform") { Platform.values().shouldForAll { dokkaPlatform -> - dokkaPlatform shouldBeIn KotlinPlatform.entries.map { it.dokkaType } + dokkaPlatform shouldBeIn KotlinPlatform.values.map { it.dokkaType } } } diff --git a/modules/dokkatoo-plugin/src/testFixtures/kotlin/samWithReceiverWorkarounds.kt b/modules/dokkatoo-plugin/src/testFixtures/kotlin/samWithReceiverWorkarounds.kt index 0d2201f7..390d9468 100644 --- a/modules/dokkatoo-plugin/src/testFixtures/kotlin/samWithReceiverWorkarounds.kt +++ b/modules/dokkatoo-plugin/src/testFixtures/kotlin/samWithReceiverWorkarounds.kt @@ -2,11 +2,13 @@ package dev.adamko.dokkatoo.utils -import dev.adamko.dokkatoo.dokka.parameters.DokkaExternalDocumentationLinkSpec import dev.adamko.dokkatoo.dokka.parameters.DokkaPackageOptionsSpec import dev.adamko.dokkatoo.dokka.parameters.DokkaSourceLinkSpec import dev.adamko.dokkatoo.dokka.parameters.DokkaSourceSetSpec -import org.gradle.api.* +import org.gradle.api.DomainObjectCollection +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.NamedDomainObjectProvider +import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.DependencySet @@ -68,10 +70,6 @@ fun NamedDomainObjectContainer.register_( register(name, configure) -fun DokkaSourceSetSpec.externalDocumentationLink_( - action: DokkaExternalDocumentationLinkSpec.() -> Unit -) = externalDocumentationLink(action) - fun DokkaSourceSetSpec.sourceLink_( action: DokkaSourceLinkSpec.() -> Unit ) = sourceLink(action)