Skip to content
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

Fix collection of KotlinCompilation classpath using stable Kotlin Gradle Plugin API #114

Merged
2 changes: 1 addition & 1 deletion examples/multiplatform-example/dokkatoo/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
plugins {
kotlin("multiplatform") version "1.8.22"
kotlin("multiplatform") version "1.9.0"
aSemy marked this conversation as resolved.
Show resolved Hide resolved
id("dev.adamko.dokkatoo") version "2.0.0-SNAPSHOT"
}

Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ gradlePlugin-dokkatoo = { module = "dev.adamko.dokkatoo:dokkatoo-plugin", versio
gradlePlugin-bcvMu = { module = "dev.adamko.kotlin.binary_compatibility_validator:bcv-gradle-plugin", version.ref = "gradlePlugin-bcvMu" }
gradlePlugin-gradlePublishPlugin = { module = "com.gradle.publish:plugin-publish-plugin", version.ref = "gradlePlugin-gradlePublishPlugin" }
gradlePlugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
gradlePlugin-kotlin-klibCommonizerApi = { module = "org.jetbrains.kotlin:kotlin-klib-commonizer-api", version.ref = "kotlin" }

[plugins]
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ class KotlinMultiplatformExampleTest : FunSpec({
output shouldContainAll listOf(
"> Task :dokkatooGeneratePublicationHtml UP-TO-DATE",
"BUILD SUCCESSFUL",
"3 actionable tasks: 3 up-to-date",
"2 actionable tasks: 2 up-to-date",
)
withClue("Dokka Generator should not be triggered, so check it doesn't log anything") {
output shouldNotContain "Generation completed successfully"
Expand Down
1 change: 1 addition & 0 deletions modules/dokkatoo-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies {
implementation(libs.kotlin.dokkaCore)

compileOnly(libs.gradlePlugin.kotlin)
compileOnly(libs.gradlePlugin.kotlin.klibCommonizerApi)
compileOnly(libs.gradlePlugin.android)
compileOnly(libs.gradlePlugin.androidApi)

Expand Down
21 changes: 20 additions & 1 deletion modules/dokkatoo-plugin/src/main/kotlin/DokkatooBasePlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import org.gradle.api.file.ProjectLayout
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.TaskContainer
import org.gradle.kotlin.dsl.*
Expand Down Expand Up @@ -98,7 +99,12 @@ constructor(
publicationEnabled.convention(true)
onlyIf("publication must be enabled") { publicationEnabled.getOrElse(true) }

generator.dokkaSourceSets.addAllLater(providers.provider { dokkatooExtension.dokkatooSourceSets })
generator.dokkaSourceSets.addAllLater(
providers.provider {
// exclude suppressed source sets as early as possible, to avoid unnecessary dependency resolution
dokkatooExtension.dokkatooSourceSets.filterNot { it.suppress.get() }
}
)

generator.dokkaSourceSets.configureDefaults(
sourceSetScopeConvention = dokkatooExtension.sourceSetScopeDefault
Expand All @@ -115,6 +121,15 @@ constructor(
moduleName.convention(providers.provider { project.name })
moduleVersion.convention(providers.provider { project.version.toString() })
modulePath.convention(project.pathAsFilePath())
konanHome.convention(
providers
.provider {
// konanHome is set into in extraProperties:
// https://github.com/JetBrains/kotlin/blob/v1.9.0/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/KotlinNativeTargetPreset.kt#L35-L38
project.extensions.extraProperties.get("konanHome") as? String?
}
.map { File(it) }
)

sourceSetScopeDefault.convention(project.path)
dokkatooPublicationDirectory.convention(layout.buildDirectory.dir("dokka"))
Expand Down Expand Up @@ -254,6 +269,10 @@ constructor(
private fun RegularFileProperty.convention(file: File): RegularFileProperty =
convention(objects.fileProperty().fileValue(file))

// workaround for https://github.com/gradle/gradle/issues/23708
private fun RegularFileProperty.convention(file: Provider<File>): RegularFileProperty =
convention(objects.fileProperty().fileProvider(file))

companion object {

const val EXTENSION_NAME = "dokkatoo"
Expand Down
11 changes: 11 additions & 0 deletions modules/dokkatoo-plugin/src/main/kotlin/DokkatooExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dev.adamko.dokkatoo.internal.*
import java.io.Serializable
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.ExtensionAware
import org.gradle.api.provider.Property
Expand Down Expand Up @@ -45,6 +46,16 @@ constructor(
*/
abstract val sourceSetScopeDefault: Property<String>

/**
* The Konan home directory, which contains libraries for Kotlin/Native development.
*
* This is only required as a workaround to fetch the compile-time dependencies in Kotlin/Native
* projects with a version below 2.0.
*/
// This property should be removed when Dokkatoo only supports KGP 2 or higher.
@DokkatooInternalApi
abstract val konanHome: RegularFileProperty

/**
* Configuration for creating Dokka Publications.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,19 @@ import com.android.build.gradle.api.ApplicationVariant
import com.android.build.gradle.api.LibraryVariant
import dev.adamko.dokkatoo.DokkatooBasePlugin
import dev.adamko.dokkatoo.DokkatooExtension
import dev.adamko.dokkatoo.adapters.DokkatooKotlinAdapter.Companion.currentKotlinToolingVersion
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 java.io.File
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_API
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
Expand All @@ -29,14 +27,21 @@ 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.commonizer.KonanDistribution
import org.jetbrains.kotlin.commonizer.platformLibsDir
import org.jetbrains.kotlin.commonizer.stdlib
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.KotlinCompilation.Companion.MAIN_COMPILATION_NAME
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
import org.jetbrains.kotlin.gradle.plugin.mpp.AbstractKotlinNativeCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMetadataCompilation
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.tooling.core.KotlinToolingVersion

/**
* The [DokkatooKotlinAdapter] plugin will automatically register Kotlin source sets as Dokka source sets.
Expand Down Expand Up @@ -75,8 +80,7 @@ abstract class DokkatooKotlinAdapter @Inject constructor(
val compilationDetailsBuilder = KotlinCompilationDetailsBuilder(
providers = providers,
objects = objects,
configurations = project.configurations,
projectPath = project.path,
konanHome = dokkatooExtension.konanHome.asFile,
)
val allKotlinCompilationDetails: ListProperty<KotlinCompilationDetails> =
compilationDetailsBuilder.createCompilationDetails(
Expand Down Expand Up @@ -127,7 +131,7 @@ abstract class DokkatooKotlinAdapter @Inject constructor(
val kssClasspath = determineClasspath(details)

register(details.name) dss@{
suppress.set(!details.isMainSourceSet())
suppress.set(!details.isPublishedSourceSet())
sourceRoots.from(details.sourceDirectories)
classpath.from(kssClasspath)
analysisPlatform.set(kssPlatform)
Expand Down Expand Up @@ -157,9 +161,9 @@ abstract class DokkatooKotlinAdapter @Inject constructor(

@DokkatooInternalApi
companion object {

private val logger = Logging.getLogger(DokkatooKotlinAdapter::class.java)

/** Try and get [KotlinProjectExtension], or `null` if it's not present */
private fun ExtensionContainer.findKotlinExtension(): KotlinProjectExtension? =
try {
findByType()
Expand All @@ -175,6 +179,13 @@ abstract class DokkatooKotlinAdapter @Inject constructor(
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 {
val kgpVersion = getKotlinPluginVersion(logger)
KotlinToolingVersion(kgpVersion)
}
}
}

Expand All @@ -190,21 +201,18 @@ private data class KotlinCompilationDetails(
val target: String,
val kotlinPlatform: KotlinPlatform,
val allKotlinSourceSetsNames: Set<String>,
val mainCompilation: Boolean,
val compileDependencyFiles: FileCollection,
val publishedCompilation: Boolean,
val dependentSourceSetNames: Set<String>,
val compilationClasspath: FileCollection,
val defaultSourceSetName: String,
)

/** 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 konanHome: Provider<File>,
) {
private val logger = Logging.getLogger(KotlinCompilationDetails::class.java)

fun createCompilationDetails(
kotlinProjectExtension: KotlinProjectExtension,
Expand All @@ -231,9 +239,6 @@ private class KotlinCompilationDetailsBuilder(
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 }

Expand All @@ -244,10 +249,10 @@ private class KotlinCompilationDetailsBuilder(
target = compilation.target.name,
kotlinPlatform = KotlinPlatform.fromString(compilation.platformType.name),
allKotlinSourceSetsNames = allKotlinSourceSetsNames.toSet(),
mainCompilation = compilation.isMain(),
compileDependencyFiles = compileDependencyFiles,
publishedCompilation = compilation.isPublished(),
dependentSourceSetNames = dependentSourceSetNames.toSet(),
compilationClasspath = compilationClasspath,
defaultSourceSetName = compilation.defaultSourceSet.name
)
}

Expand All @@ -266,55 +271,66 @@ private class KotlinCompilationDetailsBuilder(
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_API))

// collect dependency files from 'regular' Kotlin compilations
compilationClasspath.from(providers.provider { compilation.compileDependencyFiles })

// apply workaround for Kotlin/Native, which will be fixed in Kotlin 2.0
// (see KT-61559: K/N dependencies will be part of `compilation.compileDependencyFiles`)
if (
currentKotlinToolingVersion < KotlinToolingVersion("2.0.0")
&&
compilation is AbstractKotlinNativeCompilation
) {
compilationClasspath.from(
konanHome.map { konanHome ->
kotlinNativeDependencies(konanHome, compilation.konanTarget)
}
lenient(true)
}
)
}

val standardConfigurations = buildSet {
addAll(compilation.relatedConfigurationNames)
addAll(compilation.kotlinSourceSets.flatMap { it.relatedConfigurationNames })
}
return compilationClasspath
}

logger.info("[$projectPath] compilation ${compilation.name} has ${standardConfigurations.size} standard configurations $standardConfigurations")
private fun kotlinNativeDependencies(konanHome: File, target: KonanTarget): FileCollection {
val konanDistribution = KonanDistribution(konanHome)

standardConfigurations.forEach { collectConfiguration(it) }
val dependencies = objects.fileCollection()

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
)
}
dependencies.from(konanDistribution.stdlib)

return compilationClasspath
// Konan library files for a specific target
dependencies.from(
konanDistribution.platformLibsDir
.resolve(target.name)
.listFiles()
.orEmpty()
.filter { it.isDirectory || it.extension == "klib" }
)

return dependencies
}

companion object {
private fun KotlinCompilation<*>.isMain(): Boolean {

/**
* 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 KotlinJvmAndroidCompilation ->
is KotlinMetadataCompilation<*> -> true

is KotlinJvmAndroidCompilation ->
androidVariant is LibraryVariant || androidVariant is ApplicationVariant

else ->
else ->
name == MAIN_COMPILATION_NAME
}
}
Expand All @@ -340,10 +356,10 @@ private abstract class KotlinSourceSetDetails @Inject constructor(
/** The specific compilations used to build this source set */
abstract val compilations: ListProperty<KotlinCompilationDetails>

/** Estimate if this Kotlin source set are 'main' sources (as opposed to 'test' sources). */
fun isMainSourceSet(): Provider<Boolean> =
/** Estimate if this Kotlin source set contains 'published' sources */
fun isPublishedSourceSet(): Provider<Boolean> =
compilations.map { values ->
values.any { it.mainCompilation }
values.any { it.publishedCompilation }
}

override fun getName(): String = named
Expand Down