Skip to content

Commit

Permalink
Fix collection of KotlinCompilation classpath using stable Kotlin Gra…
Browse files Browse the repository at this point in the history
…dle Plugin API (#114)

* Use `compileDependencyFiles` as main source of compilation dependencies

Additionally, for Native compilations explicitly include stdlib and
platform dependencies from Kotlin Native distribution.

Home location of Kotlin Native distribution is collected
via Internal API of Kotlin Gradle Plugin.
It is safe for few reasons:
* konanHome property is accessed only for past Kotlin versions (up to 1.9.X)
* In Kotlin 2.0 `compileDependencyFiles` will include native-specific dependencies

* Don't add suppressed source sets to generator task Inputs

This prevents from unnecessary dependency resolutions,
including compile tasks.

Particularly test source sets depend on compilations output
of main source sets. And if test source sets are not skipped
then Kotlin compilation will be triggered unnecessary.

* Bump kotlin version in multiplatform-example project

Kotlin 1.9.0 supports Gradle Configuration Cache for Multiplatform projects
Dokka generation task now depends on `transform{sourceSetName}DependenciesMetadata`
tasks from Kotlin Gradle Plugin that didn't support Gradle Configuration Cache
in earlier versions of Kotlin. Making whole gradle build incompatible with
configuration cache.

* remove unused 'compileDependencyFiles'

* tidy up GradleTestKitUtils

* test more Kotlin versions and add K/N targets in KMP functional tests

* disable Gradle Daemon in Gradle TestKit tests

* fix Kotlin Native tests in KotlinMultiplatformFunctionalTest

* refactoring & tidying

- use KotlinToolingVersion to get the Kotlin version
- rename 'isMain' properties/functions to 'isPublished', because it makes a bit more sense
- fetch the konanHome prop by using extraProperties

* KotlinMultiplatformFunctionalTest moved to a separate PR because GitHub Actions doesn't like them (OOM), so I'll deal with them in #115

---------

Co-authored-by: Adam <[email protected]>
  • Loading branch information
antohaby and aSemy authored Aug 31, 2023
1 parent 0850e6e commit 6624e8c
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 60 deletions.
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"
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

0 comments on commit 6624e8c

Please sign in to comment.