From 6624e8c91c8a5d0903feba781ba1c460e2df9630 Mon Sep 17 00:00:00 2001 From: Anton Lakotko Date: Thu, 31 Aug 2023 11:41:09 +0200 Subject: [PATCH] Fix collection of KotlinCompilation classpath using stable Kotlin Gradle 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 https://github.com/adamko-dev/dokkatoo/pull/115 --------- Co-authored-by: Adam <897017+aSemy@users.noreply.github.com> --- .../dokkatoo/build.gradle.kts | 2 +- gradle/libs.versions.toml | 1 + .../kotlin/KotlinMultiplatformExampleTest.kt | 2 +- modules/dokkatoo-plugin/build.gradle.kts | 1 + .../src/main/kotlin/DokkatooBasePlugin.kt | 21 ++- .../src/main/kotlin/DokkatooExtension.kt | 11 ++ .../kotlin/adapters/DokkatooKotlinAdapter.kt | 130 ++++++++++-------- 7 files changed, 108 insertions(+), 60 deletions(-) diff --git a/examples/multiplatform-example/dokkatoo/build.gradle.kts b/examples/multiplatform-example/dokkatoo/build.gradle.kts index 3d95fb15..2b7080d9 100644 --- a/examples/multiplatform-example/dokkatoo/build.gradle.kts +++ b/examples/multiplatform-example/dokkatoo/build.gradle.kts @@ -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" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 691addb6..a1554406 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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] diff --git a/modules/dokkatoo-plugin-integration-tests/src/testExamples/kotlin/KotlinMultiplatformExampleTest.kt b/modules/dokkatoo-plugin-integration-tests/src/testExamples/kotlin/KotlinMultiplatformExampleTest.kt index 3548a3a4..d43c9133 100644 --- a/modules/dokkatoo-plugin-integration-tests/src/testExamples/kotlin/KotlinMultiplatformExampleTest.kt +++ b/modules/dokkatoo-plugin-integration-tests/src/testExamples/kotlin/KotlinMultiplatformExampleTest.kt @@ -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" diff --git a/modules/dokkatoo-plugin/build.gradle.kts b/modules/dokkatoo-plugin/build.gradle.kts index bfba705b..d7bd7bbc 100644 --- a/modules/dokkatoo-plugin/build.gradle.kts +++ b/modules/dokkatoo-plugin/build.gradle.kts @@ -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) diff --git a/modules/dokkatoo-plugin/src/main/kotlin/DokkatooBasePlugin.kt b/modules/dokkatoo-plugin/src/main/kotlin/DokkatooBasePlugin.kt index 614a041b..4e1b850d 100644 --- a/modules/dokkatoo-plugin/src/main/kotlin/DokkatooBasePlugin.kt +++ b/modules/dokkatoo-plugin/src/main/kotlin/DokkatooBasePlugin.kt @@ -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.* @@ -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 @@ -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")) @@ -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): RegularFileProperty = + convention(objects.fileProperty().fileProvider(file)) + companion object { const val EXTENSION_NAME = "dokkatoo" diff --git a/modules/dokkatoo-plugin/src/main/kotlin/DokkatooExtension.kt b/modules/dokkatoo-plugin/src/main/kotlin/DokkatooExtension.kt index 4a80549a..a64be5d2 100644 --- a/modules/dokkatoo-plugin/src/main/kotlin/DokkatooExtension.kt +++ b/modules/dokkatoo-plugin/src/main/kotlin/DokkatooExtension.kt @@ -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 @@ -45,6 +46,16 @@ constructor( */ abstract val sourceSetScopeDefault: Property + /** + * 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. * diff --git a/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooKotlinAdapter.kt b/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooKotlinAdapter.kt index 7dfaf596..bc18b92a 100644 --- a/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooKotlinAdapter.kt +++ b/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooKotlinAdapter.kt @@ -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 @@ -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. @@ -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 = compilationDetailsBuilder.createCompilationDetails( @@ -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) @@ -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() @@ -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) + } } } @@ -190,21 +201,18 @@ private data class KotlinCompilationDetails( val target: String, val kotlinPlatform: KotlinPlatform, val allKotlinSourceSetsNames: Set, - val mainCompilation: Boolean, - val compileDependencyFiles: FileCollection, + val publishedCompilation: Boolean, val dependentSourceSetNames: Set, 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, ) { - private val logger = Logging.getLogger(KotlinCompilationDetails::class.java) fun createCompilationDetails( kotlinProjectExtension: KotlinProjectExtension, @@ -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 } @@ -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 ) } @@ -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 } } @@ -340,10 +356,10 @@ private abstract class KotlinSourceSetDetails @Inject constructor( /** 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 = + /** Estimate if this Kotlin source set contains 'published' sources */ + fun isPublishedSourceSet(): Provider = compilations.map { values -> - values.any { it.mainCompilation } + values.any { it.publishedCompilation } } override fun getName(): String = named