From e0f75e4fecebddb4f2b3ff6b196c5987b6d3a5e9 Mon Sep 17 00:00:00 2001 From: OliverO2 Date: Tue, 14 Jun 2022 18:40:23 +0200 Subject: [PATCH 1/6] Add source set-based multiplatform configuration, fixes --- .../devtools/ksp/gradle/KspConfigurations.kt | 195 ++++++++++++------ .../devtools/ksp/gradle/KspExtension.kt | 132 +++++++++++- .../devtools/ksp/gradle/KspSubplugin.kt | 39 +++- .../ksp/gradle/SourceSetConfigurationsTest.kt | 50 ++++- .../gradle/testing/KspIntegrationTestRule.kt | 19 +- .../ksp/gradle/testing/TestProject.kt | 22 +- .../google/devtools/ksp/test/KMPWithHmppIT.kt | 170 +++++++++++++++ .../kmp-hmpp/annotations/build.gradle.kts | 15 ++ .../kotlin/com/example/MyAnnotation.kt | 3 + .../test/resources/kmp-hmpp/build.gradle.kts | 12 ++ .../test/resources/kmp-hmpp/gradle.properties | 1 + .../resources/kmp-hmpp/settings.gradle.kts | 18 ++ .../kmp-hmpp/test-processor/build.gradle.kts | 21 ++ .../src/main/kotlin/TestProcessor.kt | 65 ++++++ ...ols.ksp.processing.SymbolProcessorProvider | 1 + .../kmp-hmpp/workload/build.gradle.kts | 147 +++++++++++++ .../kotlin/com/example/ClientMainAnnotated.kt | 6 + .../kotlin/com/example/CommonMainAnnotated.kt | 7 + .../kotlin/com/example/CommonTestAnnotated.kt | 4 + .../kotlin/com/example/JsMainAnnotated.kt | 7 + .../kotlin/com/example/JsTestAnnotated.kt | 6 + .../kotlin/com/example/JvmMainAnnotated.kt | 6 + .../kotlin/com/example/JvmTestAnnotated.kt | 6 + 23 files changed, 849 insertions(+), 103 deletions(-) create mode 100644 integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt create mode 100644 integration-tests/src/test/resources/kmp-hmpp/annotations/build.gradle.kts create mode 100644 integration-tests/src/test/resources/kmp-hmpp/annotations/src/commonMain/kotlin/com/example/MyAnnotation.kt create mode 100644 integration-tests/src/test/resources/kmp-hmpp/build.gradle.kts create mode 100644 integration-tests/src/test/resources/kmp-hmpp/gradle.properties create mode 100644 integration-tests/src/test/resources/kmp-hmpp/settings.gradle.kts create mode 100644 integration-tests/src/test/resources/kmp-hmpp/test-processor/build.gradle.kts create mode 100644 integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/kotlin/TestProcessor.kt create mode 100644 integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider create mode 100644 integration-tests/src/test/resources/kmp-hmpp/workload/build.gradle.kts create mode 100644 integration-tests/src/test/resources/kmp-hmpp/workload/src/clientMain/kotlin/com/example/ClientMainAnnotated.kt create mode 100644 integration-tests/src/test/resources/kmp-hmpp/workload/src/commonMain/kotlin/com/example/CommonMainAnnotated.kt create mode 100644 integration-tests/src/test/resources/kmp-hmpp/workload/src/commonTest/kotlin/com/example/CommonTestAnnotated.kt create mode 100644 integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/com/example/JsMainAnnotated.kt create mode 100644 integration-tests/src/test/resources/kmp-hmpp/workload/src/jsTest/kotlin/com/example/JsTestAnnotated.kt create mode 100644 integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmMain/kotlin/com/example/JvmMainAnnotated.kt create mode 100644 integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmTest/kotlin/com/example/JvmTestAnnotated.kt diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt index b87983bc1e..652746207d 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt @@ -5,7 +5,6 @@ import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.jetbrains.kotlin.gradle.dsl.* import org.jetbrains.kotlin.gradle.plugin.* -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinCommonCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation /** @@ -24,18 +23,17 @@ class KspConfigurations(private val project: Project) { // The "ksp" configuration, applied to every compilations. private val configurationForAll = project.configurations.create(PREFIX) - private fun configurationNameOf(vararg parts: String): String { - return parts.joinToString("") { - it.replaceFirstChar { it.uppercase() } - }.replaceFirstChar { it.lowercase() } - } + private val kspExtension: KspExtension = project.extensions.getByType(KspExtension::class.java) + + private val resolvedSourceSetOptions = mutableMapOf() + private val compilationsConfiguredOrSkipped = mutableSetOf>() - @OptIn(ExperimentalStdlibApi::class) - private fun createConfiguration( - name: String, - readableSetName: String, - ): Configuration { - // maybeCreate to be future-proof, but we should never have a duplicate with current logic + private fun maybeCreateConfiguration(name: String, readableSetName: String): Configuration { + // Configurations get created lazily + // - when decorating a Kotlin project, and + // - when creating a KSP task. + // This can occur in any order, depending on when a KSP task is referenced, so it is necessary to + // tolerate multiple invocations with idempotence. return project.configurations.maybeCreate(name).apply { description = "KSP dependencies for the '$readableSetName' source set." isCanBeResolved = false // we'll resolve the processor classpath config @@ -44,6 +42,11 @@ class KspConfigurations(private val project: Project) { } } + private fun maybeCreateConfiguration(compilation: KotlinCompilation<*>) { + val kspConfigurationName = getKotlinConfigurationName(compilation) + maybeCreateConfiguration(name = kspConfigurationName, readableSetName = "KSP $compilation") + } + private fun getAndroidConfigurationName(target: KotlinTarget, sourceSet: String): String { val isMain = sourceSet.endsWith("main", ignoreCase = true) val nameWithoutMain = when { @@ -51,41 +54,39 @@ class KspConfigurations(private val project: Project) { else -> sourceSet } // Note: on single-platform, target name is conveniently set to "". - return configurationNameOf(PREFIX, target.name, nameWithoutMain) + return lowerCamelCased(PREFIX, target.name, nameWithoutMain) } - private fun getKotlinConfigurationName(compilation: KotlinCompilation<*>, sourceSet: KotlinSourceSet): String { - val isMain = compilation.name == KotlinCompilation.MAIN_COMPILATION_NAME - val isDefault = sourceSet.name == compilation.defaultSourceSetName && compilation !is KotlinCommonCompilation - // Note: on single-platform, target name is conveniently set to "". - val name = if (isMain && isDefault) { - // For js(IR), js(LEGACY), the target "js" is created. - // - // When js(BOTH) is used, target "jsLegacy" and "jsIr" are created. - // Both targets share the same source set. Therefore configurations other than main compilation - // are shared. E.g., "kspJsTest". - // For simplicity and consistency, let's not distinguish them. - when (val targetName = compilation.target.name) { - "jsLegacy", "jsIr" -> "js" - else -> targetName + private fun getKotlinConfigurationName(compilation: KotlinCompilation<*>): String { + var targetName = compilation.target.targetName + + when (targetName) { + "jsIr", "jsLegacy" -> targetName = "Js" + "metadata" -> { + // This reversal of target and compilation name is unnecessarily complicated, but retains + // backward compatibility for dependency-based configuration via `dependencies { add(...) }`. + when (compilation.name) { + KotlinCompilation.MAIN_COMPILATION_NAME, "commonMain" -> + return "${PREFIX}CommonMainMetadata" + } } - } else if (compilation is KotlinCommonCompilation) { - sourceSet.name + compilation.target.name.capitalize() + } + + return if (compilation.name == KotlinCompilation.MAIN_COMPILATION_NAME) { + lowerCamelCased(PREFIX, targetName) } else { - sourceSet.name + lowerCamelCased(PREFIX, targetName, compilation.name) } - return configurationNameOf(PREFIX, name) } init { project.plugins.withType(KotlinBasePluginWrapper::class.java).configureEach { - // 1.6.0: decorateKotlinProject(project.kotlinExtension)? - decorateKotlinProject(project.extensions.getByName("kotlin") as KotlinProjectExtension, project) + decorateKotlinProject(project) } } - private fun decorateKotlinProject(kotlin: KotlinProjectExtension, project: Project) { - when (kotlin) { + private fun decorateKotlinProject(project: Project) { + when (val kotlin = project.kotlinExtension) { is KotlinSingleTargetExtension -> decorateKotlinTarget(kotlin.target) is KotlinMultiplatformExtension -> { kotlin.targets.configureEach(::decorateKotlinTarget) @@ -109,68 +110,128 @@ class KspConfigurations(private val project: Project) { } /** - * Decorate the [KotlinSourceSet]s belonging to [target] to create one KSP configuration per source set, - * named ksp. The only exception is the main source set, for which we avoid using the - * "main" suffix (so what would be "kspJvmMain" becomes "kspJvm"). + * Decorate [target]'s source sets (Android) or compilations (non-Android), creating one KSP configuration + * per source set or compilation. * * For Android, we prefer to use AndroidSourceSets from AGP rather than [KotlinSourceSet]s. * Even though the Kotlin Plugin does create [KotlinSourceSet]s out of AndroidSourceSets * ( https://kotlinlang.org/docs/mpp-configure-compilations.html#compilation-of-the-source-set-hierarchy ), * there are slight differences between the two - Kotlin creates some extra sets with unexpected word ordering, * and things get worse when you add product flavors. So, we use AGP sets as the source of truth. + * Android configurations are named ksp, stripping a "Main" suffix (so what would be "kspJvmMain" + * becomes "kspJvm"). + * + * Non-Android compilations are named ksp except for main compilations, which are + * named ksp. */ private fun decorateKotlinTarget(target: KotlinTarget) { + // TODO: Check whether special AGP handling is still necessary. if (target.platformType == KotlinPlatformType.androidJvm) { AndroidPluginIntegration.forEachAndroidSourceSet(target.project) { sourceSet -> - createConfiguration( + maybeCreateConfiguration( name = getAndroidConfigurationName(target, sourceSet), readableSetName = "$sourceSet (Android)" ) } } else { - target.compilations.configureEach { compilation -> - compilation.kotlinSourceSets.forEach { sourceSet -> - createConfiguration( - name = getKotlinConfigurationName(compilation, sourceSet), - readableSetName = sourceSet.name - ) - } - } + target.compilations.configureEach(::maybeCreateConfiguration) } } /** - * Returns the user-facing configurations involved in the given compilation. - * We use [KotlinCompilation.kotlinSourceSets], not [KotlinCompilation.allKotlinSourceSets] for a few reasons: - * 1) consistency with how we created the configurations. For example, all* can return user-defined sets - * that don't belong to any compilation, like user-defined intermediate source sets (e.g. iosMain). - * These do not currently have their own ksp configuration. - * 2) all* can return sets belonging to other [KotlinCompilation]s - * - * See test: SourceSetConfigurationsTest.configurationsForMultiplatformApp_doesNotCrossCompilationBoundaries + * Returns the configurations relevant for [compilation]. */ fun find(compilation: KotlinCompilation<*>): Set { - val results = mutableListOf() - if (compilation is KotlinCommonCompilation) { - results.add(getKotlinConfigurationName(compilation, compilation.defaultSourceSet)) - } - compilation.kotlinSourceSets.mapTo(results) { - getKotlinConfigurationName(compilation, it) - } + configureCompilation(compilation) + + val configurationNames = mutableListOf(getKotlinConfigurationName(compilation)) + + // TODO: Check whether special AGP handling is still necessary. if (compilation.platformType == KotlinPlatformType.androidJvm) { compilation as KotlinJvmAndroidCompilation - AndroidPluginIntegration.getCompilationSourceSets(compilation).mapTo(results) { + AndroidPluginIntegration.getCompilationSourceSets(compilation).mapTo(configurationNames) { getAndroidConfigurationName(compilation.target, it) } } // Include the `ksp` configuration, if it exists, for all compilations. - if (allowAllTargetConfiguration) { - results.add(configurationForAll.name) + if (configurationNames.isNotEmpty() && allowAllTargetConfiguration) { + configurationNames.add(configurationForAll.name) } - return results.mapNotNull { - compilation.target.project.configurations.findByName(it) + return configurationNames.mapNotNull { + project.configurations.findByName(it) }.toSet() } + + private fun configureCompilation(compilation: KotlinCompilation<*>) { + if (compilation in compilationsConfiguredOrSkipped) + return + + compilationsConfiguredOrSkipped.add(compilation) + + val sourceSetOptions = resolvedSourceSetOptions(compilation) + if (sourceSetOptions.enabled == true) { + sourceSetOptions.processor?.let { processor -> + maybeCreateConfiguration(compilation) + project.dependencies.add(getKotlinConfigurationName(compilation), processor) + compilation.defaultSourceSet.kotlin.srcDir( + KspGradleSubplugin.getKspKotlinOutputDir( + project, + compilation.defaultSourceSet.name, + compilation.target.name + ) + ) + } + } + } + + /** + * Returns the source set-dependent options for [kotlinCompilation], with hierarchically resolved inheritance. + * + * Source set options are put together by following source set dependencies in bottom-up order. + * (Inheriting incompatible KSP configurations from multiple parents is discouraged as the + * evaluation order in such cases is considered undefined.) + * + * The result's properties are guaranteed to be non-null, as each of them eventually inherits a non-null value + * from global options. + */ + internal fun resolvedSourceSetOptions(kotlinCompilation: KotlinCompilation<*>): SourceSetOptions = + resolvedSourceSetOptions.computeIfAbsent(kotlinCompilation.defaultSourceSet) { compilationSourceSet -> + val result = SourceSetOptions().inheritFrom( + kspExtension.sourceSetOptions(compilationSourceSet), + initializationMode = true + ) + + kotlinCompilation.parentSourceSetsBottomUp() + .map { kspExtension.sourceSetOptions(it) } + .takeWhile { it.inheritable } + .forEach { parentOptions -> + result.inheritFrom(parentOptions) + } + + // Finally, complete missing options with global options (which are always inheritable). + result.inheritFrom(kspExtension.globalSourceSetOptions()) + } +} + + +internal fun KotlinSourceSet.bottomUpDependencies(): Sequence = sequence { + yield(this@bottomUpDependencies) + dependsOn.forEach { + yieldAll(it.bottomUpDependencies()) + } +} + + +internal fun KotlinCompilation<*>.parentSourceSetsBottomUp(): Sequence = + defaultSourceSet.bottomUpDependencies() + .drop(1) // exclude the compilation source set + .distinct() // avoid repetitions if multiple parents are present + + +internal fun lowerCamelCased(vararg parts: String): String { + return parts.joinToString("") { part -> + part.replaceFirstChar { it.uppercase() } + }.replaceFirstChar { it.lowercase() } } diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspExtension.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspExtension.kt index dbb5c7997f..52010d7a4f 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspExtension.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspExtension.kt @@ -18,32 +18,146 @@ package com.google.devtools.ksp.gradle import org.gradle.api.GradleException +import org.gradle.api.Project import org.gradle.process.CommandLineArgumentProvider +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +/** + * A Gradle extension to configure KSP. + */ open class KspExtension { - internal val apOptions = mutableMapOf() + private val apOptions = mutableMapOf() internal val commandLineArgumentProviders = mutableListOf() - open val arguments: Map get() = apOptions.toMap() + private val sourceSetOptions = mutableMapOf() + + // Some options have a global and a source set-specific variant. The latter, if specified, overrides the former. + // The following is necessary due to KotlinSourceSet not being extension-aware: + // - A KotlinSourceSet receiver addresses the source set-specific variant if the `ksp { ... }` block is invoked + // inside a source set block. + // - A Project receiver addresses the corresponding global variant. (This is required as the compiler's name + // resolution would always prefer a receiver-less member and never invoke an extension receiver variant.) + // - A corresponding private property provides the global variant's backing field. + + /** Options passed to the processor (global). */ + private val arguments: Map get() = this@KspExtension.apOptions.toMap() + + /** Options passed to the processor (global). */ + open val Project.arguments: Map get() = this@KspExtension.arguments + + /** Options passed to the processor (source set-specific). */ + open val KotlinSourceSet.arguments: Map + get() = sourceSetOptions(this).apOptions.toMap() + + /** Specifies an option passed to the processor (global). */ + open fun Project.arg(k: String, v: String) { + if ('=' in k) { + throw GradleException("'=' is not allowed in custom option's name.") + } + apOptions[k] = v + } - open fun arg(k: String, v: String) { + /** Specifies an option passed to the processor (source set-specific). */ + open fun KotlinSourceSet.arg(k: String, v: String) = with(sourceSetOptions(this)) { if ('=' in k) { throw GradleException("'=' is not allowed in custom option's name.") } - apOptions.put(k, v) + apOptions[k] = v } + /** Specifies a command line arguments provider (global). */ open fun arg(arg: CommandLineArgumentProvider) { commandLineArgumentProviders.add(arg) } + /** Block other compiler plugins by removing them from the classpath (global option). */ open var blockOtherCompilerPlugins: Boolean = false - // Instruct KSP to pickup sources from compile tasks, instead of source sets. - // Note that it depends on behaviors of other Gradle plugins, that may bring surprises and can be hard to debug. - // Use your discretion. + /** + * Instruct KSP to pickup sources from compile tasks, instead of source sets (global option). + * Note that it depends on behaviors of other Gradle plugins, that may bring surprises and can be hard to debug. + * Use your discretion. + */ open var allowSourcesFromOtherPlugins: Boolean = false - // Treat all warning as errors. - open var allWarningsAsErrors: Boolean = false + /** Treat all warnings as errors (global option). */ + private var allWarningsAsErrors: Boolean = false + + /** Treat all warnings as errors (global option). */ + open var Project.allWarningsAsErrors: Boolean + get() = this@KspExtension.allWarningsAsErrors + set(value) { + this@KspExtension.allWarningsAsErrors = value + } + + /** Treat all warnings as errors (source set-specific option). */ + open var KotlinSourceSet.allWarningsAsErrors: Boolean + get() = sourceSetOptions(this).allWarningsAsErrors ?: this@KspExtension.allWarningsAsErrors + set(value) = with(sourceSetOptions(this)) { allWarningsAsErrors = value } + + /** Specify if this set of source set options is inheritable for dependent source sets (true by default). */ + open var KotlinSourceSet.inheritable: Boolean + get() = sourceSetOptions(this).inheritable + set(value) = with(sourceSetOptions(this)) { inheritable = value } + + /** Specify if KSP processing is enabled (source set-specific option). */ + open var KotlinSourceSet.enabled: Boolean + get() = sourceSetOptions(this).enabled ?: false + set(value) = with(sourceSetOptions(this)) { enabled = value } + + /** Specify the source set's KSP processor (enables KSP processing, if set). */ + open fun KotlinSourceSet.processor(dependencyNotation: Any) { + sourceSetOptions(this).processor = dependencyNotation + sourceSetOptions(this).enabled = true + } + + internal fun sourceSetOptions(sourceSet: KotlinSourceSet): SourceSetOptions = + sourceSetOptions.computeIfAbsent(sourceSet.name) { SourceSetOptions() } + + internal fun globalSourceSetOptions(): SourceSetOptions = SourceSetOptions().also { + it.inheritable = true + it.enabled = false + it.apOptions = apOptions + it.allWarningsAsErrors = allWarningsAsErrors + } +} + +/** + * Source set-specific options. + * + * If [inheritable] is true (the default), a lower-level source set's option with a null value will inherit its + * values from its inheritable parent, while [apOptions] / [arguments] will inherit all key/value pairs for keys + * that are not already present. The [inheritable] property is not inheritable ;-), its purpose is to optionally + * disable inheritance for one level only. + */ +internal data class SourceSetOptions( + /** Specify if this set of source set options is inheritable for dependent source sets. */ + internal var inheritable: Boolean = true, + + /** Specify if KSP processing is enabled. */ + internal var enabled: Boolean? = null, + + /** Specify the source set's KSP processor. */ + internal var processor: Any? = null, + + /** Options passed to the processor. */ + internal var apOptions: MutableMap = mutableMapOf(), + + /** Treat all warnings as errors. */ + internal var allWarningsAsErrors: Boolean? = null, +) { + /** Options passed to the processor. */ + internal val arguments: Map get() = apOptions.toMap() + + /** Inherits options from [other]. */ + internal fun inheritFrom(other: SourceSetOptions, initializationMode: Boolean = false): SourceSetOptions { + require(initializationMode || other.inheritable) + + enabled = enabled ?: other.enabled + processor = processor ?: other.processor + apOptions = (other.apOptions + apOptions).toMutableMap() + allWarningsAsErrors = allWarningsAsErrors ?: other.allWarningsAsErrors + + return this + } } diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt index 826f3ea3e9..4bbbf8db39 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt @@ -135,8 +135,8 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool classpath: Configuration, sourceSetName: String, target: String, + sourceSetOptions: SourceSetOptions, isIncremental: Boolean, - allWarningsAsErrors: Boolean, ): List { val options = mutableListOf() options += SubpluginOption("classOutputDir", getKspClassOutputDir(project, sourceSetName, target).path) @@ -154,7 +154,7 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool project.findProperty("ksp.incremental.log")?.toString() ?: "false" ) options += SubpluginOption("projectBaseDir", project.project.projectDir.canonicalPath) - options += SubpluginOption("allWarningsAsErrors", allWarningsAsErrors.toString()) + options += SubpluginOption("allWarningsAsErrors", sourceSetOptions.allWarningsAsErrors.toString()) options += FilesSubpluginOption("apclasspath", classpath.toList()) // Turn this on by default to work KT-30172 around. It is off by default in the ccompiler plugin. options += SubpluginOption( @@ -162,7 +162,7 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool project.findProperty("ksp.return.ok.on.error")?.toString() ?: "true" ) - kspExtension.apOptions.forEach { + sourceSetOptions.apOptions.forEach { options += SubpluginOption("apoption", "${it.key}=${it.value}") } kspExtension.commandLineArgumentProviders.forEach { @@ -215,9 +215,9 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool project.locateTask(kotlinCompilation.compileKotlinTaskName) ?: return project.provider { emptyList() } val javaCompile = findJavaTaskForKotlinCompilation(kotlinCompilation)?.get() val kspExtension = project.extensions.getByType(KspExtension::class.java) - val kspConfigurations = kspConfigurations.find(kotlinCompilation) - val nonEmptyKspConfigurations = kspConfigurations.filter { it.allDependencies.isNotEmpty() } - if (nonEmptyKspConfigurations.isEmpty()) { + val compilationKspConfigurations = + kspConfigurations.find(kotlinCompilation).filter { it.allDependencies.isNotEmpty() } + if (compilationKspConfigurations.isEmpty()) { return project.provider { emptyList() } } if (kotlinCompileProvider.name == "compileKotlinMetadata") { @@ -243,14 +243,28 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool val kspTaskName = kotlinCompileProvider.name.replaceFirst("compile", "ksp") val kotlinCompileTask = kotlinCompileProvider.get() + val sourceSetOptions = kspConfigurations.resolvedSourceSetOptions(kotlinCompilation) + + fun Task.addKspMetadataDependencyIfExists(sourceSetName: String) { + val parentKspTaskName = lowerCamelCased("ksp", sourceSetName, "KotlinMetadata") + + project.locateTask(parentKspTaskName)?.let { parentKspTask -> + dependsOn(parentKspTask) + } + } fun configureAsKspTask(kspTask: KspTask, isIncremental: Boolean) { // depends on the processor; if the processor changes, it needs to be reprocessed. val processorClasspath = project.configurations.maybeCreate("${kspTaskName}ProcessorClasspath") - .extendsFrom(*nonEmptyKspConfigurations.toTypedArray()) + .extendsFrom(*compilationKspConfigurations.toTypedArray()) kspTask.processorClasspath.from(processorClasspath) kspTask.dependsOn(processorClasspath.buildDependencies) + // depends on KSP tasks for parent source sets + kotlinCompilation.parentSourceSetsBottomUp().forEach { sourceSet -> + kspTask.addKspMetadataDependencyIfExists(sourceSet.name) + } + kspTask.options.addAll( kspTask.project.provider { getSubpluginOptions( @@ -259,15 +273,15 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool processorClasspath, sourceSetName, target, - isIncremental, - kspExtension.allWarningsAsErrors + sourceSetOptions, + isIncremental ) } ) kspTask.commandLineArgumentProviders.addAll(kspExtension.commandLineArgumentProviders) kspTask.destination = kspOutputDir kspTask.blockOtherCompilerPlugins = kspExtension.blockOtherCompilerPlugins - kspTask.apOptions.value(kspExtension.arguments).disallowChanges() + kspTask.apOptions.value(sourceSetOptions.arguments).disallowChanges() kspTask.kspCacheDir.fileValue(getKspCachesDir(project, sourceSetName, target)).disallowChanges() if (kspExtension.blockOtherCompilerPlugins) { @@ -369,6 +383,9 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool kotlinCompileProvider.configure { kotlinCompile -> kotlinCompile.dependsOn(kspTaskProvider) + kotlinCompilation.parentSourceSetsBottomUp().forEach { sourceSet -> + kotlinCompile.addKspMetadataDependencyIfExists(sourceSet.name) + } kotlinCompile.setSource(kotlinOutputDir, javaOutputDir) when (kotlinCompile) { is AbstractKotlinCompile<*> -> kotlinCompile.libraries.from(project.files(classOutputDir)) @@ -501,7 +518,7 @@ abstract class KspTaskJvm @Inject constructor( isIntermoduleIncremental = (project.findProperty("ksp.incremental.intermodule")?.toString()?.toBoolean() ?: true) && - isKspIncremental + isKspIncremental if (isIntermoduleIncremental) { val classStructureIfIncremental = project.configurations.detachedConfiguration( project.dependencies.create(project.files(project.provider { kotlinCompile.libraries })) diff --git a/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/SourceSetConfigurationsTest.kt b/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/SourceSetConfigurationsTest.kt index 6c82b31f5f..5ee3733bec 100644 --- a/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/SourceSetConfigurationsTest.kt +++ b/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/SourceSetConfigurationsTest.kt @@ -121,10 +121,9 @@ class SourceSetConfigurationsTest { } @Test - fun configurationsForMultiplatformApp_doesNotCrossCompilationBoundaries() { - // Adding a ksp dependency on jvmParent should not leak into jvmChild compilation, - // even if the source sets depend on each other. This works because we use - // KotlinCompilation.kotlinSourceSets instead of KotlinCompilation.allKotlinSourceSets + fun configurationsForDependencyConfiguredMultiplatformApp_doesNotCrossCompilationBoundaries() { + // Adding a ksp dependency on jvmParent should not leak into its sibling target compilation jvmChild, + // even if the source sets depend on each other. testRule.setupAppAsMultiplatformApp( """ kotlin { @@ -160,6 +159,49 @@ class SourceSetConfigurationsTest { .build() } + @Test + fun configurationsForSourceSetConfiguredMultiplatformApp_doesNotCrossCompilationBoundaries() { + // Adding a ksp processor on a jvmParent source set should not leak into its sibling target compilation + // jvmChild, even if the source sets depend on each other. + testRule.setupAppAsMultiplatformApp( + """ + kotlin { + jvm("jvmParent") { } + jvm("jvmChild") { } + } + """.trimIndent(), + withAndroid = false, + ) + testRule.appModule.addMultiplatformSource("commonMain", "Foo.kt", "class Foo") + testRule.appModule.buildFileAdditions.add( + """ + kotlin { + sourceSets { + val jvmParentMain by getting { + ksp { + processor("androidx.room:room-compiler:2.4.2") + inheritable = false + } + } + this["jvmChildMain"].dependsOn(jvmParentMain) + } + } + tasks.register("checkConfigurations") { + doLast { + // child has no dependencies, so task is not created. + val parent = tasks.findByName("kspKotlinJvmParent") + val child = tasks.findByName("kspKotlinJvmChild") + require(parent != null) + require(child == null) + } + } + """.trimIndent() + ) + testRule.runner() + .withArguments(":app:checkConfigurations") + .build() + } + @Test fun registerJavaSourcesToAndroid() { testRule.setupAppAsAndroidApp() diff --git a/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/testing/KspIntegrationTestRule.kt b/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/testing/KspIntegrationTestRule.kt index 088fa89b36..0dfd33c7b3 100644 --- a/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/testing/KspIntegrationTestRule.kt +++ b/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/testing/KspIntegrationTestRule.kt @@ -26,7 +26,8 @@ import kotlin.reflect.KClass /** * JUnit test rule to setup a [TestProject] which contains a KSP processor module and an * application. The application can either be an android app or jvm app. - * Test must call [setupAppAsAndroidApp] or [setupAppAsJvmApp] before using the [runner]. + * Test must call [setupAppAsAndroidApp] or [setupAppAsJvmApp] or [setupAppAsMultiplatformApp] + * before using the [runner]. */ class KspIntegrationTestRule( private val tmpFolder: TemporaryFolder @@ -108,20 +109,26 @@ class KspIntegrationTestRule( /** * Sets up the app module as a multiplatform app with the specified [targets], wrapped in a kotlin { } block. */ - fun setupAppAsMultiplatformApp(targets: String) { + fun setupAppAsMultiplatformApp( + targets: String, + withAndroid: Boolean = true, + ) { testProject.appModule.plugins.addAll( - listOf( - PluginDeclaration.id("com.android.application", testConfig.androidBaseVersion), + listOfNotNull( + if (withAndroid) { + PluginDeclaration.id("com.android.application", testConfig.androidBaseVersion) + } else null, PluginDeclaration.kotlin("multiplatform", testConfig.kotlinBaseVersion), PluginDeclaration.id("com.google.devtools.ksp", testConfig.kspVersion) ) ) testProject.appModule.buildFileAdditions.add(targets) - addAndroidBoilerplate() + if (withAndroid) + addAndroidBoilerplate() } private fun addAndroidBoilerplate() { - testProject.writeAndroidGradlePropertiesFile() + testProject.appendAndroidGradleProperties() testProject.appModule.buildFileAdditions.add( """ android { diff --git a/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/testing/TestProject.kt b/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/testing/TestProject.kt index 95bd8f7221..28a14344ef 100644 --- a/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/testing/TestProject.kt +++ b/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/testing/TestProject.kt @@ -47,9 +47,12 @@ class TestProject( rootDir.resolve("app") ) + private val propertiesSections = mutableListOf() + fun writeFiles() { writeBuildFile() writeSettingsFile() + writePropertiesFile() appModule.writeBuildFile() processorModule.writeBuildFile() } @@ -70,12 +73,19 @@ class TestProject( rootDir.resolve("settings.gradle.kts").writeText(contents) } - fun writeAndroidGradlePropertiesFile() { - val contents = """ - android.useAndroidX=true - org.gradle.jvmargs=-Xmx2048M -XX:MaxMetaspaceSize=512m - """.trimIndent() - rootDir.resolve("gradle.properties").writeText(contents) + fun appendAndroidGradleProperties() { + appendGradleProperties( + "android.useAndroidX=true", + "org.gradle.jvmargs=-Xmx2048M -XX:MaxMetaspaceSize=512m" + ) + } + + fun appendGradleProperties(vararg content: String) { + propertiesSections.add(content.joinToString("\n")) + } + + fun writePropertiesFile() { + rootDir.resolve("gradle.properties").writeText(propertiesSections.joinToString("\n", postfix = "\n")) } private fun writeBuildFile() { diff --git a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt new file mode 100644 index 0000000000..d162e58da3 --- /dev/null +++ b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt @@ -0,0 +1,170 @@ +package com.google.devtools.ksp.test + +import org.gradle.testkit.runner.GradleRunner +import org.junit.Assert +import org.junit.Rule +import org.junit.Test + +class KMPWithHmppIT { + @Rule + @JvmField + val project: TemporaryTestProject = TemporaryTestProject("kmp-hmpp") + + @Test + fun testCustomSourceSetHierarchyBuild() { + val gradleRunner = GradleRunner.create().withProjectDir(project.root) + val subprojectName = "workload" + + gradleRunner.withArguments( + "--configuration-cache-problems=warn", + "clean", + ":$subprojectName:assemble", + ":$subprojectName:testClasses", + ) + .build() + .let { result -> + val output: String = result.output + val relevantOutput = + output.lines().filter { it.startsWith("> Task :$subprojectName:ksp") || it.startsWith("w: [ksp] ") } + .joinToString("\n") + + listOf( + """ + > Task :$subprojectName:kspCommonMainKotlinMetadata + w: [ksp] current file: CommonMainAnnotated.kt + w: [ksp] all files: [CommonMainAnnotated.kt] + w: [ksp] option: 'a' -> 'a_commonMain' + w: [ksp] option: 'b' -> 'b_global' + w: [ksp] option: 'c' -> 'c_commonMain' + > Task :$subprojectName:kspClientMainKotlinMetadata + w: [ksp] current file: ClientMainAnnotated.kt + w: [ksp] all files: [ClientMainAnnotated.kt] + w: [ksp] option: 'a' -> 'a_clientMain' + w: [ksp] option: 'b' -> 'b_global' + w: [ksp] option: 'c' -> 'c_commonMain' + w: [ksp] option: 'd' -> 'd_clientMain' + """, + """ + > Task :$subprojectName:kspKotlinJvm + w: [ksp] current file: JvmMainAnnotated.kt + w: [ksp] all files: [ClientMainAnnotated.kt, ClientMainAnnotatedGenerated.kt, CommonMainAnnotated.kt, CommonMainAnnotatedGenerated.kt, JvmMainAnnotated.kt] + w: [ksp] option: 'a' -> 'a_commonMain' + w: [ksp] option: 'b' -> 'b_global' + w: [ksp] option: 'c' -> 'c_commonMain' + """, + """ + > Task :$subprojectName:kspKotlinJs + w: [ksp] current file: JsMainAnnotated.kt + w: [ksp] all files: [ClientMainAnnotated.kt, ClientMainAnnotatedGenerated.kt, CommonMainAnnotated.kt, CommonMainAnnotatedGenerated.kt, JsMainAnnotated.kt] + w: [ksp] option: 'a' -> 'a_commonMain' + w: [ksp] option: 'b' -> 'b_global' + w: [ksp] option: 'c' -> 'c_commonMain' + """, + """ + > Task :$subprojectName:kspTestKotlinJvm + w: [ksp] current file: JvmTestAnnotated.kt + w: [ksp] all files: [CommonTestAnnotated.kt, JvmTestAnnotated.kt] + w: [ksp] option: 'a' -> 'a_global' + w: [ksp] option: 'b' -> 'b_global' + """, + ).forEach { + Assert.assertTrue(it.trimIndent() in relevantOutput) + } + + Assert.assertTrue("> Task :annotations:ksp" !in output) + Assert.assertTrue("Execution optimizations have been disabled" !in output) + } + } + + @Test + fun testCustomSourceSetHierarchyDependencies() { + val gradleRunner = GradleRunner.create().withProjectDir(project.root) + val subprojectName = "workload" + + gradleRunner.withArguments( + "--configuration-cache-problems=warn", + "clean", + ":$subprojectName:showMe", + ) + .build() + .let { result -> + val output: String = result.output + val relevantOutput = + output.lines() + .mapNotNull { if (it.startsWith("[showMe] ")) it.substringAfter("[showMe] ") else null } + .joinToString("\n") + + Assert.assertTrue( + """ + + Kotlin targets/compilations/allKotlinSourceSets: + + * target `js` + * compilation `main`, default sourceSet: `jsMain` + * sourceSet `jsMain`, depends on `clientMain`, `commonMain` + * sourceSet `commonMain` + * sourceSet `clientMain`, depends on `commonMain` + * compilation `test`, default sourceSet: `jsTest` + * sourceSet `jsTest`, depends on `commonTest` + * sourceSet `commonTest` + * target `jvm` + * compilation `main`, default sourceSet: `jvmMain` + * sourceSet `jvmMain`, depends on `clientMain`, `commonMain` + * sourceSet `commonMain` + * sourceSet `clientMain`, depends on `commonMain` + * compilation `test`, default sourceSet: `jvmTest` + * sourceSet `jvmTest`, depends on `commonTest` + * sourceSet `commonTest` + * target `metadata` + * compilation `clientMain` [common], default sourceSet: `clientMain` + * compilation `commonMain` [common], default sourceSet: `commonMain` + * compilation `main` [common], default sourceSet: `commonMain` + * sourceSet `commonMain` + + Kotlin targets/compilations/bottomUpSourceSets: + + * target `js` + * compilation `main`, ordered source sets: `jsMain`, `commonMain`, `clientMain`, `commonMain` + * compilation `test`, ordered source sets: `jsTest`, `commonTest` + * target `jvm` + * compilation `main`, ordered source sets: `jvmMain`, `commonMain`, `clientMain`, `commonMain` + * compilation `test`, ordered source sets: `jvmTest`, `commonTest` + * target `metadata` + * compilation `clientMain` [common], ordered source sets: `clientMain`, `commonMain` + * compilation `commonMain` [common], ordered source sets: `commonMain` + * compilation `main` [common], ordered source sets: `commonMain` + + KSP configurations: + + * `ksp`, artifacts: [], dependencies: [] + * `kspCommonMainMetadata`, artifacts: [], dependencies: [test-processor] + * `kspJs`, artifacts: [], dependencies: [test-processor] + * `kspJsTest`, artifacts: [], dependencies: [] + * `kspJvm`, artifacts: [], dependencies: [test-processor] + * `kspJvmTest`, artifacts: [], dependencies: [test-processor] + * `kspMetadataClientMain`, artifacts: [], dependencies: [test-processor] + + Tasks [compile, ksp] and their ksp/compile dependencies: + + * `compileClientMainKotlinMetadata` depends on [kspClientMainKotlinMetadata, kspCommonMainKotlinMetadata] + * `compileCommonMainKotlinMetadata` depends on [kspCommonMainKotlinMetadata] + * `compileJava` depends on [] + * `compileKotlinJs` depends on [kspClientMainKotlinMetadata, kspCommonMainKotlinMetadata, kspKotlinJs] + * `compileKotlinJvm` depends on [kspClientMainKotlinMetadata, kspCommonMainKotlinMetadata, kspKotlinJvm] + * `compileKotlinMetadata` depends on [] + * `compileTestDevelopmentExecutableKotlinJs` depends on [`compileTestKotlinJs`] + * `compileTestJava` depends on [] + * `compileTestKotlinJs` depends on [] + * `compileTestKotlinJvm` depends on [kspTestKotlinJvm] + * `compileTestProductionExecutableKotlinJs` depends on [`compileTestKotlinJs`] + * `kspClientMainKotlinMetadata` depends on [kspClientMainKotlinMetadataProcessorClasspath, kspCommonMainKotlinMetadata] + * `kspCommonMainKotlinMetadata` depends on [kspCommonMainKotlinMetadataProcessorClasspath] + * `kspKotlinJs` depends on [kspClientMainKotlinMetadata, kspCommonMainKotlinMetadata, kspKotlinJsProcessorClasspath] + * `kspKotlinJvm` depends on [kspClientMainKotlinMetadata, kspCommonMainKotlinMetadata, kspKotlinJvmProcessorClasspath] + * `kspTestKotlinJvm` depends on [kspTestKotlinJvmProcessorClasspath] + + """.trimIndent() in relevantOutput + ) + } + } +} diff --git a/integration-tests/src/test/resources/kmp-hmpp/annotations/build.gradle.kts b/integration-tests/src/test/resources/kmp-hmpp/annotations/build.gradle.kts new file mode 100644 index 0000000000..6afbba2409 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/annotations/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + kotlin("multiplatform") + id("com.google.devtools.ksp") +} + +version = "1.0-SNAPSHOT" + +kotlin { + jvm { + } + + js(IR) { + browser() + } +} diff --git a/integration-tests/src/test/resources/kmp-hmpp/annotations/src/commonMain/kotlin/com/example/MyAnnotation.kt b/integration-tests/src/test/resources/kmp-hmpp/annotations/src/commonMain/kotlin/com/example/MyAnnotation.kt new file mode 100644 index 0000000000..b938f1c1ae --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/annotations/src/commonMain/kotlin/com/example/MyAnnotation.kt @@ -0,0 +1,3 @@ +package com.example + +annotation class MyAnnotation diff --git a/integration-tests/src/test/resources/kmp-hmpp/build.gradle.kts b/integration-tests/src/test/resources/kmp-hmpp/build.gradle.kts new file mode 100644 index 0000000000..8dd6566724 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + kotlin("multiplatform") apply false +} + +val testRepo: String by project +allprojects { + repositories { + maven(testRepo) + mavenCentral() + maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/bootstrap/") + } +} diff --git a/integration-tests/src/test/resources/kmp-hmpp/gradle.properties b/integration-tests/src/test/resources/kmp-hmpp/gradle.properties new file mode 100644 index 0000000000..b3c7a03306 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx2048M diff --git a/integration-tests/src/test/resources/kmp-hmpp/settings.gradle.kts b/integration-tests/src/test/resources/kmp-hmpp/settings.gradle.kts new file mode 100644 index 0000000000..14a45d2147 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + val kotlinVersion: String by settings + val kspVersion: String by settings + val testRepo: String by settings + plugins { + id("com.google.devtools.ksp") version kspVersion apply false + kotlin("multiplatform") version kotlinVersion apply false + } + repositories { + maven(testRepo) + gradlePluginPortal() + maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/bootstrap/") + } +} + +include(":annotations") +include(":workload") +include(":test-processor") diff --git a/integration-tests/src/test/resources/kmp-hmpp/test-processor/build.gradle.kts b/integration-tests/src/test/resources/kmp-hmpp/test-processor/build.gradle.kts new file mode 100644 index 0000000000..e7a943aa02 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/test-processor/build.gradle.kts @@ -0,0 +1,21 @@ +val kspVersion: String by project + +plugins { + kotlin("multiplatform") +} + +group = "com.example" +version = "1.0-SNAPSHOT" + +kotlin { + jvm() + sourceSets { + val jvmMain by getting { + dependencies { + implementation("com.google.devtools.ksp:symbol-processing-api:$kspVersion") + } + kotlin.srcDir("src/main/kotlin") + resources.srcDir("src/main/resources") + } + } +} diff --git a/integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/kotlin/TestProcessor.kt b/integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/kotlin/TestProcessor.kt new file mode 100644 index 0000000000..1794858ce1 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/kotlin/TestProcessor.kt @@ -0,0 +1,65 @@ +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import com.google.devtools.ksp.symbol.KSAnnotated +import java.io.OutputStreamWriter + +class TestProcessor( + private val codeGenerator: CodeGenerator, + private val logger: KSPLogger, + private val environment: SymbolProcessorEnvironment +) : SymbolProcessor { + private var invoked = false + + private fun String.sourceSetBelow(startDirectoryName: String): String = + substringAfter("/$startDirectoryName/").substringBefore("/kotlin/").substringAfterLast('/') + + override fun process(resolver: Resolver): List { + if (invoked) { + return emptyList() + } + invoked = true + + val allFileNames = resolver.getAllFiles().map { it.fileName }.toList() + val allFileNamesSorted = allFileNames.sorted() + val currentFileName = allFileNames.last() + val currentFileBaseName = currentFileName.removeSuffix(".kt") + logger.warn("current file: $currentFileName") + logger.warn("all files: $allFileNamesSorted") + environment.options.toSortedMap().forEach { (key, value) -> + logger.warn("option: '$key' -> '$value'") + } + + val options = environment.options.toSortedMap().map { (key, value) -> "'$key' -> '$value'" } + + codeGenerator.createNewFile(Dependencies(false), "", "${currentFileBaseName}Generated", "kt").use { output -> + val outputSourceSet = codeGenerator.generatedFile.first().toString().sourceSetBelow("ksp") + + OutputStreamWriter(output).use { writer -> + writer.write(""" + package com.example + + object ${currentFileBaseName}For${outputSourceSet.replaceFirstChar { it.uppercaseChar() }} { + const val allFiles = "$allFileNamesSorted" + const val options = "$options" + const val outputSourceSet = "$outputSourceSet" + } + + """.trimIndent() + ) + } + } + + return emptyList() + } +} + +class TestProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return TestProcessor(environment.codeGenerator, environment.logger, environment) + } +} diff --git a/integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 0000000000..c91e3e9e0b --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +TestProcessorProvider diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/build.gradle.kts b/integration-tests/src/test/resources/kmp-hmpp/workload/build.gradle.kts new file mode 100644 index 0000000000..91e8baffda --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/build.gradle.kts @@ -0,0 +1,147 @@ +@file:Suppress("UNUSED_VARIABLE") + +import org.gradle.api.Task +import org.gradle.api.tasks.TaskProvider +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet + +plugins { + kotlin("multiplatform") + id("com.google.devtools.ksp") +} + +version = "1.0-SNAPSHOT" + +kotlin { + jvm { + withJava() + } + + js(IR) { + browser() + } + + sourceSets { + val commonMain by getting { + ksp { + processor(project(":test-processor")) + arg("a", "a_commonMain") + arg("c", "c_commonMain") + } + dependencies { + implementation(project(":annotations")) + } + } + + val clientMain by creating { + ksp { + arg("a", "a_clientMain") + arg("d", "d_clientMain") + inheritable = false + } + dependsOn(commonMain) + } + + val jvmMain by getting { + dependsOn(clientMain) + } + + val jsMain by getting { + dependsOn(clientMain) + } + + val jvmTest by getting { + ksp { + processor(project(":test-processor")) + } + } + } +} + +ksp { + arg("a", "a_global") + arg("b", "b_global") +} + +tasks { + val showMe by registering { + doLast { + fun Any.asText() = when (this) { + is Task -> name + is KotlinSourceSet -> name + is TaskProvider<*> -> get().name + else -> toString() + }.let { + "`$it`" + } + + fun Iterable.asStableText(transformed: String.() -> String? = { this }) = + mapNotNull { it.asText().transformed() }.sorted().joinToString() + + val prefix = "[showMe] " + fun log(message: String = "") = println(message.lines().joinToString("\n") { "$prefix$it" }) + + log("\nKotlin targets/compilations/allKotlinSourceSets:\n") + kotlin.targets.forEach { target -> + log("* target `${target.targetName}`") + target.compilations.forEach { compilation -> + val commonMark = + if (compilation is org.jetbrains.kotlin.gradle.plugin.mpp.KotlinCommonCompilation) " [common]" else "" + log(" * compilation `${compilation.name}`$commonMark, default sourceSet: `${compilation.defaultSourceSet.name}`") + compilation.allKotlinSourceSets.forEach { + val dependencies = + if (it.dependsOn.isEmpty()) "" else ", depends on ${it.dependsOn.asStableText()}" + log(" * sourceSet `${it.name}`$dependencies") + } + } + } + + fun KotlinSourceSet.allDependencies(): List = + if (dependsOn.isEmpty()) { + listOf(this) + } else { + listOf(this) + dependsOn.flatMap { it.allDependencies() } + } + + log("\nKotlin targets/compilations/bottomUpSourceSets:\n") + kotlin.targets.forEach { target -> + log("* target `${target.targetName}`") + target.compilations.forEach { compilation -> + val commonMark = + if (compilation is org.jetbrains.kotlin.gradle.plugin.mpp.KotlinCommonCompilation) " [common]" else "" + log( + " * compilation `${compilation.name}`$commonMark," + + " ordered source sets: ${ + compilation.defaultSourceSet.allDependencies().joinToString { "`${it.name}`" } + }" + ) + } + } + + log("\nKSP configurations:\n") + project.configurations.forEach { config -> + if (config.name.startsWith("ksp")) { + log("* `${config.name}`, artifacts: ${config.allArtifacts.map { it.name }}, dependencies: ${config.dependencies.map { it.name }}") + } + } + + val selection: List? = listOf("compile", "ksp") + log("\nTasks ${selection ?: "(all)"} and their ksp/compile dependencies:\n") + project.tasks.forEach { task -> + if (selection == null || selection.any { task.name.startsWith(it) }) { + log( + "* `${task.name}` depends on [${ + task.dependsOn.asStableText { + when { + "ksp" in this -> Regex("""[^\w](ksp\w+)""").find(this)?.groupValues?.get(1) + startsWith("`compile") -> this + else -> null + } + } + }]" + ) + } + } + log() + } + } +} diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/clientMain/kotlin/com/example/ClientMainAnnotated.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/clientMain/kotlin/com/example/ClientMainAnnotated.kt new file mode 100644 index 0000000000..1b1310e473 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/clientMain/kotlin/com/example/ClientMainAnnotated.kt @@ -0,0 +1,6 @@ +package com.example + +@MyAnnotation +class ClientMainAnnotated { + val allFiles = ClientMainAnnotatedForClientMain.allFiles +} diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/commonMain/kotlin/com/example/CommonMainAnnotated.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/commonMain/kotlin/com/example/CommonMainAnnotated.kt new file mode 100644 index 0000000000..b7054b3ef5 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/commonMain/kotlin/com/example/CommonMainAnnotated.kt @@ -0,0 +1,7 @@ +package com.example + +@MyAnnotation +class CommonMainAnnotated { + val allFiles = CommonMainAnnotatedForCommonMain.allFiles +} + diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/commonTest/kotlin/com/example/CommonTestAnnotated.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/commonTest/kotlin/com/example/CommonTestAnnotated.kt new file mode 100644 index 0000000000..8dcb4aea40 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/commonTest/kotlin/com/example/CommonTestAnnotated.kt @@ -0,0 +1,4 @@ +package com.example + +@MyAnnotation +class CommonTestAnnotated diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/com/example/JsMainAnnotated.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/com/example/JsMainAnnotated.kt new file mode 100644 index 0000000000..9b6e603b17 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/com/example/JsMainAnnotated.kt @@ -0,0 +1,7 @@ +package com.example + +@MyAnnotation +class JsMainAnnotated { + val allFiles = JsMainAnnotatedForJsMain.allFiles +} + diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsTest/kotlin/com/example/JsTestAnnotated.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsTest/kotlin/com/example/JsTestAnnotated.kt new file mode 100644 index 0000000000..a9c4cb89b8 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsTest/kotlin/com/example/JsTestAnnotated.kt @@ -0,0 +1,6 @@ +package com.example + +@MyAnnotation +class JsTestAnnotated { + val allFiles = Foo_jsTest.allFiles +} diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmMain/kotlin/com/example/JvmMainAnnotated.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmMain/kotlin/com/example/JvmMainAnnotated.kt new file mode 100644 index 0000000000..91ef56cc75 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmMain/kotlin/com/example/JvmMainAnnotated.kt @@ -0,0 +1,6 @@ +package com.example + +@MyAnnotation +class JvmMainAnnotated { + val allFiles = JvmMainAnnotatedForJvmMain.allFiles +} diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmTest/kotlin/com/example/JvmTestAnnotated.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmTest/kotlin/com/example/JvmTestAnnotated.kt new file mode 100644 index 0000000000..aeceea6650 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmTest/kotlin/com/example/JvmTestAnnotated.kt @@ -0,0 +1,6 @@ +package com.example + +@MyAnnotation +class JvmTestAnnotated { + val allFiles = JvmTestAnnotatedForJvmTest.allFiles +} From 1297b098b7bf18ae90e6d59e6fc6c107a009eb4d Mon Sep 17 00:00:00 2001 From: OliverO2 Date: Wed, 15 Jun 2022 15:00:50 +0200 Subject: [PATCH 2/6] Reformat for ktlint --- .../devtools/ksp/gradle/KspConfigurations.kt | 7 ++-- .../devtools/ksp/gradle/KspSubplugin.kt | 3 +- .../src/main/kotlin/TestProcessor.kt | 17 +++++----- .../kmp-hmpp/workload/build.gradle.kts | 32 +++++++++++-------- .../kotlin/com/example/CommonMainAnnotated.kt | 1 - .../kotlin/com/example/JsMainAnnotated.kt | 1 - 6 files changed, 31 insertions(+), 30 deletions(-) diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt index 652746207d..f65f1e2016 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt @@ -215,7 +215,6 @@ class KspConfigurations(private val project: Project) { } } - internal fun KotlinSourceSet.bottomUpDependencies(): Sequence = sequence { yield(this@bottomUpDependencies) dependsOn.forEach { @@ -223,12 +222,10 @@ internal fun KotlinSourceSet.bottomUpDependencies(): Sequence = } } - internal fun KotlinCompilation<*>.parentSourceSetsBottomUp(): Sequence = defaultSourceSet.bottomUpDependencies() - .drop(1) // exclude the compilation source set - .distinct() // avoid repetitions if multiple parents are present - + .drop(1) // exclude the compilation source set + .distinct() // avoid repetitions if multiple parents are present internal fun lowerCamelCased(vararg parts: String): String { return parts.joinToString("") { part -> diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt index 4bbbf8db39..9323e98b6e 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt @@ -517,8 +517,7 @@ abstract class KspTaskJvm @Inject constructor( ) isIntermoduleIncremental = - (project.findProperty("ksp.incremental.intermodule")?.toString()?.toBoolean() ?: true) && - isKspIncremental + (project.findProperty("ksp.incremental.intermodule")?.toString()?.toBoolean() ?: true) && isKspIncremental if (isIntermoduleIncremental) { val classStructureIfIncremental = project.configurations.detachedConfiguration( project.dependencies.create(project.files(project.provider { kotlinCompile.libraries })) diff --git a/integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/kotlin/TestProcessor.kt b/integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/kotlin/TestProcessor.kt index 1794858ce1..9f3f253f9f 100644 --- a/integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/kotlin/TestProcessor.kt +++ b/integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/kotlin/TestProcessor.kt @@ -40,14 +40,15 @@ class TestProcessor( val outputSourceSet = codeGenerator.generatedFile.first().toString().sourceSetBelow("ksp") OutputStreamWriter(output).use { writer -> - writer.write(""" - package com.example - - object ${currentFileBaseName}For${outputSourceSet.replaceFirstChar { it.uppercaseChar() }} { - const val allFiles = "$allFileNamesSorted" - const val options = "$options" - const val outputSourceSet = "$outputSourceSet" - } + writer.write( + """ + package com.example + + object ${currentFileBaseName}For${outputSourceSet.replaceFirstChar { it.uppercaseChar() }} { + const val allFiles = "$allFileNamesSorted" + const val options = "$options" + const val outputSourceSet = "$outputSourceSet" + } """.trimIndent() ) diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/build.gradle.kts b/integration-tests/src/test/resources/kmp-hmpp/workload/build.gradle.kts index 91e8baffda..5b076528c7 100644 --- a/integration-tests/src/test/resources/kmp-hmpp/workload/build.gradle.kts +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/build.gradle.kts @@ -3,6 +3,7 @@ import org.gradle.api.Task import org.gradle.api.tasks.TaskProvider import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinCommonCompilation plugins { kotlin("multiplatform") @@ -85,8 +86,11 @@ tasks { log("* target `${target.targetName}`") target.compilations.forEach { compilation -> val commonMark = - if (compilation is org.jetbrains.kotlin.gradle.plugin.mpp.KotlinCommonCompilation) " [common]" else "" - log(" * compilation `${compilation.name}`$commonMark, default sourceSet: `${compilation.defaultSourceSet.name}`") + if (compilation is KotlinCommonCompilation) " [common]" else "" + log( + " * compilation `${compilation.name}`$commonMark," + + " default sourceSet: `${compilation.defaultSourceSet.name}`" + ) compilation.allKotlinSourceSets.forEach { val dependencies = if (it.dependsOn.isEmpty()) "" else ", depends on ${it.dependsOn.asStableText()}" @@ -107,12 +111,11 @@ tasks { log("* target `${target.targetName}`") target.compilations.forEach { compilation -> val commonMark = - if (compilation is org.jetbrains.kotlin.gradle.plugin.mpp.KotlinCommonCompilation) " [common]" else "" + if (compilation is KotlinCommonCompilation) " [common]" else "" log( " * compilation `${compilation.name}`$commonMark," + - " ordered source sets: ${ - compilation.defaultSourceSet.allDependencies().joinToString { "`${it.name}`" } - }" + " ordered source sets: " + + compilation.defaultSourceSet.allDependencies().joinToString { "`${it.name}`" } ) } } @@ -120,7 +123,10 @@ tasks { log("\nKSP configurations:\n") project.configurations.forEach { config -> if (config.name.startsWith("ksp")) { - log("* `${config.name}`, artifacts: ${config.allArtifacts.map { it.name }}, dependencies: ${config.dependencies.map { it.name }}") + log( + "* `${config.name}`, artifacts: ${config.allArtifacts.map { it.name }}," + + " dependencies: ${config.dependencies.map { it.name }}" + ) } } @@ -130,13 +136,13 @@ tasks { if (selection == null || selection.any { task.name.startsWith(it) }) { log( "* `${task.name}` depends on [${ - task.dependsOn.asStableText { - when { - "ksp" in this -> Regex("""[^\w](ksp\w+)""").find(this)?.groupValues?.get(1) - startsWith("`compile") -> this - else -> null - } + task.dependsOn.asStableText { + when { + "ksp" in this -> Regex("""[^\w](ksp\w+)""").find(this)?.groupValues?.get(1) + startsWith("`compile") -> this + else -> null } + } }]" ) } diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/commonMain/kotlin/com/example/CommonMainAnnotated.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/commonMain/kotlin/com/example/CommonMainAnnotated.kt index b7054b3ef5..cd903bc0f9 100644 --- a/integration-tests/src/test/resources/kmp-hmpp/workload/src/commonMain/kotlin/com/example/CommonMainAnnotated.kt +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/commonMain/kotlin/com/example/CommonMainAnnotated.kt @@ -4,4 +4,3 @@ package com.example class CommonMainAnnotated { val allFiles = CommonMainAnnotatedForCommonMain.allFiles } - diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/com/example/JsMainAnnotated.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/com/example/JsMainAnnotated.kt index 9b6e603b17..6e150b6f50 100644 --- a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/com/example/JsMainAnnotated.kt +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/com/example/JsMainAnnotated.kt @@ -4,4 +4,3 @@ package com.example class JsMainAnnotated { val allFiles = JsMainAnnotatedForJsMain.allFiles } - From 9cf4738ec63cac385655b82a9f1af97600028485 Mon Sep 17 00:00:00 2001 From: OliverO2 Date: Wed, 15 Jun 2022 23:22:48 +0200 Subject: [PATCH 3/6] Depend on 'ksp.multiplatform.enabled', restoring Groovy compatibility --- .../devtools/ksp/gradle/KspConfigurations.kt | 36 +++-- .../devtools/ksp/gradle/KspExtension.kt | 132 ++-------------- .../ksp/gradle/KspMultiplatformExtension.kt | 147 ++++++++++++++++++ .../devtools/ksp/gradle/KspSubplugin.kt | 22 ++- .../gradle/model/builder/KspModelBuilder.kt | 10 +- .../ksp/gradle/SourceSetConfigurationsTest.kt | 1 + .../gradle/testing/KspIntegrationTestRule.kt | 3 + .../test/resources/kmp-hmpp/gradle.properties | 1 + 8 files changed, 208 insertions(+), 144 deletions(-) create mode 100644 gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspMultiplatformExtension.kt diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt index f65f1e2016..122d6ea281 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt @@ -10,7 +10,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation /** * Creates and retrieves ksp-related configurations. */ -class KspConfigurations(private val project: Project) { +class KspConfigurations(private val project: Project, multiplatformEnabled: Boolean) { companion object { private const val PREFIX = "ksp" } @@ -23,7 +23,11 @@ class KspConfigurations(private val project: Project) { // The "ksp" configuration, applied to every compilations. private val configurationForAll = project.configurations.create(PREFIX) - private val kspExtension: KspExtension = project.extensions.getByType(KspExtension::class.java) + private val kspMultiplatformExtension: KspMultiplatformExtension? = + if (multiplatformEnabled) project.extensions.getByType(KspMultiplatformExtension::class.java) else null + + private val kspExtension: KspExtension = + kspMultiplatformExtension?.kspExtension ?: project.extensions.getByType(KspExtension::class.java) private val resolvedSourceSetOptions = mutableMapOf() private val compilationsConfiguredOrSkipped = mutableSetOf>() @@ -198,20 +202,22 @@ class KspConfigurations(private val project: Project) { */ internal fun resolvedSourceSetOptions(kotlinCompilation: KotlinCompilation<*>): SourceSetOptions = resolvedSourceSetOptions.computeIfAbsent(kotlinCompilation.defaultSourceSet) { compilationSourceSet -> - val result = SourceSetOptions().inheritFrom( - kspExtension.sourceSetOptions(compilationSourceSet), - initializationMode = true - ) - - kotlinCompilation.parentSourceSetsBottomUp() - .map { kspExtension.sourceSetOptions(it) } - .takeWhile { it.inheritable } - .forEach { parentOptions -> - result.inheritFrom(parentOptions) - } + kspMultiplatformExtension?.let { kspMultiplatformExtension -> + val result = SourceSetOptions().inheritFrom( + kspMultiplatformExtension.sourceSetOptions(compilationSourceSet), + initializationMode = true + ) + + kotlinCompilation.parentSourceSetsBottomUp() + .map { kspMultiplatformExtension.sourceSetOptions(it) } + .takeWhile { it.inheritable } + .forEach { parentOptions -> + result.inheritFrom(parentOptions) + } - // Finally, complete missing options with global options (which are always inheritable). - result.inheritFrom(kspExtension.globalSourceSetOptions()) + // Finally, complete missing options with global options (which are always inheritable). + result.inheritFrom(kspMultiplatformExtension.globalSourceSetOptions()) + } ?: kspExtension.globalSourceSetOptions() } } diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspExtension.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspExtension.kt index 52010d7a4f..dbb5c7997f 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspExtension.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspExtension.kt @@ -18,146 +18,32 @@ package com.google.devtools.ksp.gradle import org.gradle.api.GradleException -import org.gradle.api.Project import org.gradle.process.CommandLineArgumentProvider -import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet -/** - * A Gradle extension to configure KSP. - */ open class KspExtension { - private val apOptions = mutableMapOf() + internal val apOptions = mutableMapOf() internal val commandLineArgumentProviders = mutableListOf() - private val sourceSetOptions = mutableMapOf() - - // Some options have a global and a source set-specific variant. The latter, if specified, overrides the former. - // The following is necessary due to KotlinSourceSet not being extension-aware: - // - A KotlinSourceSet receiver addresses the source set-specific variant if the `ksp { ... }` block is invoked - // inside a source set block. - // - A Project receiver addresses the corresponding global variant. (This is required as the compiler's name - // resolution would always prefer a receiver-less member and never invoke an extension receiver variant.) - // - A corresponding private property provides the global variant's backing field. - - /** Options passed to the processor (global). */ - private val arguments: Map get() = this@KspExtension.apOptions.toMap() - - /** Options passed to the processor (global). */ - open val Project.arguments: Map get() = this@KspExtension.arguments - - /** Options passed to the processor (source set-specific). */ - open val KotlinSourceSet.arguments: Map - get() = sourceSetOptions(this).apOptions.toMap() - - /** Specifies an option passed to the processor (global). */ - open fun Project.arg(k: String, v: String) { - if ('=' in k) { - throw GradleException("'=' is not allowed in custom option's name.") - } - apOptions[k] = v - } + open val arguments: Map get() = apOptions.toMap() - /** Specifies an option passed to the processor (source set-specific). */ - open fun KotlinSourceSet.arg(k: String, v: String) = with(sourceSetOptions(this)) { + open fun arg(k: String, v: String) { if ('=' in k) { throw GradleException("'=' is not allowed in custom option's name.") } - apOptions[k] = v + apOptions.put(k, v) } - /** Specifies a command line arguments provider (global). */ open fun arg(arg: CommandLineArgumentProvider) { commandLineArgumentProviders.add(arg) } - /** Block other compiler plugins by removing them from the classpath (global option). */ open var blockOtherCompilerPlugins: Boolean = false - /** - * Instruct KSP to pickup sources from compile tasks, instead of source sets (global option). - * Note that it depends on behaviors of other Gradle plugins, that may bring surprises and can be hard to debug. - * Use your discretion. - */ + // Instruct KSP to pickup sources from compile tasks, instead of source sets. + // Note that it depends on behaviors of other Gradle plugins, that may bring surprises and can be hard to debug. + // Use your discretion. open var allowSourcesFromOtherPlugins: Boolean = false - /** Treat all warnings as errors (global option). */ - private var allWarningsAsErrors: Boolean = false - - /** Treat all warnings as errors (global option). */ - open var Project.allWarningsAsErrors: Boolean - get() = this@KspExtension.allWarningsAsErrors - set(value) { - this@KspExtension.allWarningsAsErrors = value - } - - /** Treat all warnings as errors (source set-specific option). */ - open var KotlinSourceSet.allWarningsAsErrors: Boolean - get() = sourceSetOptions(this).allWarningsAsErrors ?: this@KspExtension.allWarningsAsErrors - set(value) = with(sourceSetOptions(this)) { allWarningsAsErrors = value } - - /** Specify if this set of source set options is inheritable for dependent source sets (true by default). */ - open var KotlinSourceSet.inheritable: Boolean - get() = sourceSetOptions(this).inheritable - set(value) = with(sourceSetOptions(this)) { inheritable = value } - - /** Specify if KSP processing is enabled (source set-specific option). */ - open var KotlinSourceSet.enabled: Boolean - get() = sourceSetOptions(this).enabled ?: false - set(value) = with(sourceSetOptions(this)) { enabled = value } - - /** Specify the source set's KSP processor (enables KSP processing, if set). */ - open fun KotlinSourceSet.processor(dependencyNotation: Any) { - sourceSetOptions(this).processor = dependencyNotation - sourceSetOptions(this).enabled = true - } - - internal fun sourceSetOptions(sourceSet: KotlinSourceSet): SourceSetOptions = - sourceSetOptions.computeIfAbsent(sourceSet.name) { SourceSetOptions() } - - internal fun globalSourceSetOptions(): SourceSetOptions = SourceSetOptions().also { - it.inheritable = true - it.enabled = false - it.apOptions = apOptions - it.allWarningsAsErrors = allWarningsAsErrors - } -} - -/** - * Source set-specific options. - * - * If [inheritable] is true (the default), a lower-level source set's option with a null value will inherit its - * values from its inheritable parent, while [apOptions] / [arguments] will inherit all key/value pairs for keys - * that are not already present. The [inheritable] property is not inheritable ;-), its purpose is to optionally - * disable inheritance for one level only. - */ -internal data class SourceSetOptions( - /** Specify if this set of source set options is inheritable for dependent source sets. */ - internal var inheritable: Boolean = true, - - /** Specify if KSP processing is enabled. */ - internal var enabled: Boolean? = null, - - /** Specify the source set's KSP processor. */ - internal var processor: Any? = null, - - /** Options passed to the processor. */ - internal var apOptions: MutableMap = mutableMapOf(), - - /** Treat all warnings as errors. */ - internal var allWarningsAsErrors: Boolean? = null, -) { - /** Options passed to the processor. */ - internal val arguments: Map get() = apOptions.toMap() - - /** Inherits options from [other]. */ - internal fun inheritFrom(other: SourceSetOptions, initializationMode: Boolean = false): SourceSetOptions { - require(initializationMode || other.inheritable) - - enabled = enabled ?: other.enabled - processor = processor ?: other.processor - apOptions = (other.apOptions + apOptions).toMutableMap() - allWarningsAsErrors = allWarningsAsErrors ?: other.allWarningsAsErrors - - return this - } + // Treat all warning as errors. + open var allWarningsAsErrors: Boolean = false } diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspMultiplatformExtension.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspMultiplatformExtension.kt new file mode 100644 index 0000000000..32807bdb6f --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspMultiplatformExtension.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.ksp.gradle + +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.process.CommandLineArgumentProvider +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet + +/** + * A Gradle extension to configure KSP. + */ +open class KspMultiplatformExtension { + internal val kspExtension = KspExtension() // global options + + private val sourceSetOptions = mutableMapOf() + + // Some options have a global and a source set-specific variant. The latter, if specified, overrides the former. + // The following is necessary due to KotlinSourceSet not being extension-aware: + // - A KotlinSourceSet receiver addresses the source set-specific variant if the `ksp { ... }` block is invoked + // inside a source set block. + // - A Project receiver addresses the corresponding global variant. (This is required as the compiler's name + // resolution would always prefer a receiver-less member and never invoke an extension receiver variant.) + // - A corresponding property in `kspExtension` provides the global variant's backing field. + + /** Options passed to the processor (global). */ + open val Project.arguments: Map get() = kspExtension.arguments + + /** Options passed to the processor (source set-specific). */ + open val KotlinSourceSet.arguments: Map + get() = sourceSetOptions(this).apOptions.toMap() + + /** Specifies an option passed to the processor (global). */ + open fun Project.arg(k: String, v: String) = kspExtension.arg(k, v) + + /** Specifies an option passed to the processor (source set-specific). */ + open fun KotlinSourceSet.arg(k: String, v: String) = with(sourceSetOptions(this)) { + if ('=' in k) { + throw GradleException("'=' is not allowed in custom option's name.") + } + apOptions[k] = v + } + + /** Specifies a command line arguments provider (global). */ + open fun arg(arg: CommandLineArgumentProvider) = kspExtension.arg(arg) + + /** Block other compiler plugins by removing them from the classpath (global option). */ + open var blockOtherCompilerPlugins: Boolean by kspExtension::blockOtherCompilerPlugins + + /** + * Instruct KSP to pickup sources from compile tasks, instead of source sets (global option). + * Note that it depends on behaviors of other Gradle plugins, that may bring surprises and can be hard to debug. + * Use your discretion. + */ + open var allowSourcesFromOtherPlugins: Boolean by kspExtension::allowSourcesFromOtherPlugins + + /** Treat all warnings as errors (global option). */ + open var Project.allWarningsAsErrors: Boolean by kspExtension::allWarningsAsErrors + + /** Treat all warnings as errors (source set-specific option). */ + open var KotlinSourceSet.allWarningsAsErrors: Boolean + get() = sourceSetOptions(this).allWarningsAsErrors ?: kspExtension.allWarningsAsErrors + set(value) = with(sourceSetOptions(this)) { allWarningsAsErrors = value } + + /** Specify if this set of source set options is inheritable for dependent source sets (true by default). */ + open var KotlinSourceSet.inheritable: Boolean + get() = sourceSetOptions(this).inheritable + set(value) = with(sourceSetOptions(this)) { inheritable = value } + + /** Specify if KSP processing is enabled (source set-specific option). */ + open var KotlinSourceSet.enabled: Boolean + get() = sourceSetOptions(this).enabled ?: false + set(value) = with(sourceSetOptions(this)) { enabled = value } + + /** Specify the source set's KSP processor (enables KSP processing, if set). */ + open fun KotlinSourceSet.processor(dependencyNotation: Any) { + sourceSetOptions(this).processor = dependencyNotation + sourceSetOptions(this).enabled = true + } + + internal fun sourceSetOptions(sourceSet: KotlinSourceSet): SourceSetOptions = + sourceSetOptions.computeIfAbsent(sourceSet.name) { SourceSetOptions() } + + internal fun globalSourceSetOptions(): SourceSetOptions = kspExtension.globalSourceSetOptions() +} + +internal fun KspExtension.globalSourceSetOptions(): SourceSetOptions = SourceSetOptions().also { + it.inheritable = true + it.enabled = false + it.apOptions = apOptions + it.allWarningsAsErrors = allWarningsAsErrors +} + +/** + * Source set-specific options. + * + * If [inheritable] is true (the default), + * – source set options with a null value inherit their values from parent source sets in the source set hierarchy, + * – [apOptions] / [arguments] inherit all key/value pairs for keys that are not already present. + * Inheritance can be disabled for a source set, but[inheritable] can be + */ +internal data class SourceSetOptions( + /** Specify if this set of source set options is inheritable for dependent source sets. */ + internal var inheritable: Boolean = true, + + /** Specify if KSP processing is enabled. */ + internal var enabled: Boolean? = null, + + /** Specify the source set's KSP processor. */ + internal var processor: Any? = null, + + /** Options passed to the processor. */ + internal var apOptions: MutableMap = mutableMapOf(), + + /** Treat all warnings as errors. */ + internal var allWarningsAsErrors: Boolean? = null, +) { + /** Options passed to the processor. */ + internal val arguments: Map get() = apOptions.toMap() + + /** Inherits options from [other]. */ + internal fun inheritFrom(other: SourceSetOptions, initializationMode: Boolean = false): SourceSetOptions { + require(initializationMode || other.inheritable) + + enabled = enabled ?: other.enabled + processor = processor ?: other.processor + apOptions = (other.apOptions + apOptions).toMutableMap() + allWarningsAsErrors = allWarningsAsErrors ?: other.allWarningsAsErrors + + return this + } +} diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt index 9323e98b6e..6238956485 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt @@ -176,12 +176,21 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool } } + private var multiplatformEnabled: Boolean = false + private lateinit var kspConfigurations: KspConfigurations override fun apply(target: Project) { - target.extensions.create("ksp", KspExtension::class.java) - kspConfigurations = KspConfigurations(target) - registry.register(KspModelBuilder()) + multiplatformEnabled = + target.findProperty("ksp.multiplatform.enabled")?.let { it.toString().toBoolean() } ?: false + if (multiplatformEnabled) { + target.logger.warn("[ksp] Enabling the 'ksp' multiplatform extension (supports Kotlin build scripts only)") + target.extensions.create("ksp", KspMultiplatformExtension::class.java) + } else { + target.extensions.create("ksp", KspExtension::class.java) + } + kspConfigurations = KspConfigurations(target, multiplatformEnabled) + registry.register(KspModelBuilder(multiplatformEnabled)) } override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean { @@ -214,7 +223,12 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool val kotlinCompileProvider: TaskProvider> = project.locateTask(kotlinCompilation.compileKotlinTaskName) ?: return project.provider { emptyList() } val javaCompile = findJavaTaskForKotlinCompilation(kotlinCompilation)?.get() - val kspExtension = project.extensions.getByType(KspExtension::class.java) + val kspExtension = + if (multiplatformEnabled) { + project.extensions.getByType(KspMultiplatformExtension::class.java).kspExtension + } else { + project.extensions.getByType(KspExtension::class.java) + } val compilationKspConfigurations = kspConfigurations.find(kotlinCompilation).filter { it.allDependencies.isNotEmpty() } if (compilationKspConfigurations.isEmpty()) { diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/model/builder/KspModelBuilder.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/model/builder/KspModelBuilder.kt index 760d83bc92..78cd1f7109 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/model/builder/KspModelBuilder.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/model/builder/KspModelBuilder.kt @@ -18,6 +18,7 @@ package com.google.devtools.ksp.gradle.model.builder import com.google.devtools.ksp.gradle.KspExtension +import com.google.devtools.ksp.gradle.KspMultiplatformExtension import com.google.devtools.ksp.gradle.model.Ksp import com.google.devtools.ksp.gradle.model.impl.KspImpl import org.gradle.api.Project @@ -27,7 +28,7 @@ import org.gradle.tooling.provider.model.ToolingModelBuilder * [ToolingModelBuilder] for [Ksp] models. * This model builder is registered for Kotlin All Open sub-plugin. */ -class KspModelBuilder : ToolingModelBuilder { +class KspModelBuilder(private val multiplatformEnabled: Boolean = false) : ToolingModelBuilder { override fun canBuild(modelName: String): Boolean { return modelName == Ksp::class.java.name @@ -35,7 +36,12 @@ class KspModelBuilder : ToolingModelBuilder { override fun buildAll(modelName: String, project: Project): Any? { if (modelName == Ksp::class.java.name) { - val extension = project.extensions.getByType(KspExtension::class.java) + val extension = + if (multiplatformEnabled) { + project.extensions.getByType(KspMultiplatformExtension::class.java).kspExtension + } else { + project.extensions.getByType(KspExtension::class.java) + } return KspImpl(project.name) } return null diff --git a/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/SourceSetConfigurationsTest.kt b/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/SourceSetConfigurationsTest.kt index 5ee3733bec..99c39be8d4 100644 --- a/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/SourceSetConfigurationsTest.kt +++ b/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/SourceSetConfigurationsTest.kt @@ -171,6 +171,7 @@ class SourceSetConfigurationsTest { } """.trimIndent(), withAndroid = false, + enableMultiplatformExtension = true, ) testRule.appModule.addMultiplatformSource("commonMain", "Foo.kt", "class Foo") testRule.appModule.buildFileAdditions.add( diff --git a/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/testing/KspIntegrationTestRule.kt b/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/testing/KspIntegrationTestRule.kt index 0dfd33c7b3..c944838d58 100644 --- a/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/testing/KspIntegrationTestRule.kt +++ b/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/testing/KspIntegrationTestRule.kt @@ -112,7 +112,10 @@ class KspIntegrationTestRule( fun setupAppAsMultiplatformApp( targets: String, withAndroid: Boolean = true, + enableMultiplatformExtension: Boolean = false ) { + if (enableMultiplatformExtension) + testProject.appendGradleProperties("ksp.multiplatform.enabled=true") testProject.appModule.plugins.addAll( listOfNotNull( if (withAndroid) { diff --git a/integration-tests/src/test/resources/kmp-hmpp/gradle.properties b/integration-tests/src/test/resources/kmp-hmpp/gradle.properties index b3c7a03306..6833cc3bcd 100644 --- a/integration-tests/src/test/resources/kmp-hmpp/gradle.properties +++ b/integration-tests/src/test/resources/kmp-hmpp/gradle.properties @@ -1 +1,2 @@ org.gradle.jvmargs=-Xmx2048M +ksp.multiplatform.enabled=true From f5fd95d9dc69c7d6833eaf967dbff542f1211575 Mon Sep 17 00:00:00 2001 From: OliverO2 Date: Mon, 20 Jun 2022 13:45:34 +0200 Subject: [PATCH 4/6] Multiplatform: fix picking up sources from intermediate source sets --- .../devtools/ksp/gradle/KspConfigurations.kt | 4 ++ .../devtools/ksp/gradle/KspSubplugin.kt | 22 ++++---- .../google/devtools/ksp/test/KMPWithHmppIT.kt | 50 +++++++++++-------- .../src/main/kotlin/TestProcessor.kt | 23 ++++++--- .../kotlin/com/example/ClientMainAnnotated.kt | 2 +- .../kotlin/com/example/CommonMainAnnotated.kt | 2 +- .../kotlin/com/example/JsMainAnnotated.kt | 2 +- .../kotlin/com/example/JsTestAnnotated.kt | 2 +- .../kotlin/com/example/JvmMainAnnotated.kt | 2 +- .../kotlin/com/example/JvmTestAnnotated.kt | 2 +- 10 files changed, 68 insertions(+), 43 deletions(-) diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt index 122d6ea281..a0e0425ddb 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt @@ -233,6 +233,10 @@ internal fun KotlinCompilation<*>.parentSourceSetsBottomUp(): Sequence.allSourceSetsBottomUp(): Sequence = + defaultSourceSet.bottomUpDependencies() + .distinct() // avoid repetitions if multiple parents are present + internal fun lowerCamelCased(vararg parts: String): String { return parts.joinToString("") { part -> part.replaceFirstChar { it.uppercase() } diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt index 6238956485..184a7291a4 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt @@ -259,7 +259,7 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool val kotlinCompileTask = kotlinCompileProvider.get() val sourceSetOptions = kspConfigurations.resolvedSourceSetOptions(kotlinCompilation) - fun Task.addKspMetadataDependencyIfExists(sourceSetName: String) { + fun Task.addParentKspTaskDependencyIfExists(sourceSetName: String) { val parentKspTaskName = lowerCamelCased("ksp", sourceSetName, "KotlinMetadata") project.locateTask(parentKspTaskName)?.let { parentKspTask -> @@ -274,9 +274,9 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool kspTask.processorClasspath.from(processorClasspath) kspTask.dependsOn(processorClasspath.buildDependencies) - // depends on KSP tasks for parent source sets + // depends on outputs of KSP tasks for parent source sets kotlinCompilation.parentSourceSetsBottomUp().forEach { sourceSet -> - kspTask.addKspMetadataDependencyIfExists(sourceSet.name) + kspTask.addParentKspTaskDependencyIfExists(sourceSet.name) } kspTask.options.addAll( @@ -331,11 +331,15 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool kspTask.setSource(kotlinCompileTask.javaSources) } } else { - kotlinCompilation.allKotlinSourceSets.forEach { sourceSet -> - kspTask.setSource(sourceSet.kotlin) - } - if (kotlinCompilation is KotlinCommonCompilation) { - kspTask.setSource(kotlinCompilation.defaultSourceSet.kotlin) + if (multiplatformEnabled) { + kotlinCompilation.allSourceSetsBottomUp().forEach { sourceSet -> kspTask.source(sourceSet.kotlin) } + } else { + kotlinCompilation.allKotlinSourceSets.forEach { sourceSet -> + kspTask.setSource(sourceSet.kotlin) + } + if (kotlinCompilation is KotlinCommonCompilation) { + kspTask.setSource(kotlinCompilation.defaultSourceSet.kotlin) + } } } @@ -398,7 +402,7 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool kotlinCompileProvider.configure { kotlinCompile -> kotlinCompile.dependsOn(kspTaskProvider) kotlinCompilation.parentSourceSetsBottomUp().forEach { sourceSet -> - kotlinCompile.addKspMetadataDependencyIfExists(sourceSet.name) + kotlinCompile.addParentKspTaskDependencyIfExists(sourceSet.name) } kotlinCompile.setSource(kotlinOutputDir, javaOutputDir) when (kotlinCompile) { diff --git a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt index d162e58da3..29476e627c 100644 --- a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt +++ b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt @@ -13,59 +13,69 @@ class KMPWithHmppIT { @Test fun testCustomSourceSetHierarchyBuild() { val gradleRunner = GradleRunner.create().withProjectDir(project.root) - val subprojectName = "workload" gradleRunner.withArguments( "--configuration-cache-problems=warn", "clean", - ":$subprojectName:assemble", - ":$subprojectName:testClasses", + ":workload:assemble", + ":workload:testClasses", ) + // .withDebug(true) .build() .let { result -> val output: String = result.output val relevantOutput = - output.lines().filter { it.startsWith("> Task :$subprojectName:ksp") || it.startsWith("w: [ksp] ") } + output.lines().filter { it.startsWith("> Task :workload:ksp") || it.startsWith("w: [ksp] ") } .joinToString("\n") listOf( """ - > Task :$subprojectName:kspCommonMainKotlinMetadata - w: [ksp] current file: CommonMainAnnotated.kt - w: [ksp] all files: [CommonMainAnnotated.kt] + > Task :workload:kspCommonMainKotlinMetadata + w: [ksp] all files: [commonMain:CommonMainAnnotated.kt] + w: [ksp] new files: [commonMain:CommonMainAnnotated.kt] w: [ksp] option: 'a' -> 'a_commonMain' w: [ksp] option: 'b' -> 'b_global' w: [ksp] option: 'c' -> 'c_commonMain' - > Task :$subprojectName:kspClientMainKotlinMetadata - w: [ksp] current file: ClientMainAnnotated.kt - w: [ksp] all files: [ClientMainAnnotated.kt] + w: [ksp] all files: [commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt] + w: [ksp] new files: [commonMain:Generated.kt] + > Task :workload:kspClientMainKotlinMetadata + w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt] + w: [ksp] new files: [clientMain:ClientMainAnnotated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt] w: [ksp] option: 'a' -> 'a_clientMain' w: [ksp] option: 'b' -> 'b_global' w: [ksp] option: 'c' -> 'c_commonMain' w: [ksp] option: 'd' -> 'd_clientMain' + w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt] + w: [ksp] new files: [clientMain:Generated.kt] """, """ - > Task :$subprojectName:kspKotlinJvm - w: [ksp] current file: JvmMainAnnotated.kt - w: [ksp] all files: [ClientMainAnnotated.kt, ClientMainAnnotatedGenerated.kt, CommonMainAnnotated.kt, CommonMainAnnotatedGenerated.kt, JvmMainAnnotated.kt] + > Task :workload:kspKotlinJvm + w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jvmMain:JvmMainAnnotated.kt] + w: [ksp] new files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jvmMain:JvmMainAnnotated.kt] w: [ksp] option: 'a' -> 'a_commonMain' w: [ksp] option: 'b' -> 'b_global' w: [ksp] option: 'c' -> 'c_commonMain' + w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jvmMain:Generated.kt, jvmMain:JvmMainAnnotated.kt] + w: [ksp] new files: [jvmMain:Generated.kt] """, """ - > Task :$subprojectName:kspKotlinJs - w: [ksp] current file: JsMainAnnotated.kt - w: [ksp] all files: [ClientMainAnnotated.kt, ClientMainAnnotatedGenerated.kt, CommonMainAnnotated.kt, CommonMainAnnotatedGenerated.kt, JsMainAnnotated.kt] + > Task :workload:kspKotlinJs + w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jsMain:JsMainAnnotated.kt] + w: [ksp] new files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jsMain:JsMainAnnotated.kt] w: [ksp] option: 'a' -> 'a_commonMain' w: [ksp] option: 'b' -> 'b_global' w: [ksp] option: 'c' -> 'c_commonMain' + w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jsMain:Generated.kt, jsMain:JsMainAnnotated.kt] + w: [ksp] new files: [jsMain:Generated.kt] """, """ - > Task :$subprojectName:kspTestKotlinJvm - w: [ksp] current file: JvmTestAnnotated.kt - w: [ksp] all files: [CommonTestAnnotated.kt, JvmTestAnnotated.kt] + > Task :workload:kspTestKotlinJvm + w: [ksp] all files: [commonTest:CommonTestAnnotated.kt, jvmTest:JvmTestAnnotated.kt] + w: [ksp] new files: [commonTest:CommonTestAnnotated.kt, jvmTest:JvmTestAnnotated.kt] w: [ksp] option: 'a' -> 'a_global' w: [ksp] option: 'b' -> 'b_global' + w: [ksp] all files: [commonTest:CommonTestAnnotated.kt, jvmTest:Generated.kt, jvmTest:JvmTestAnnotated.kt] + w: [ksp] new files: [jvmTest:Generated.kt] """, ).forEach { Assert.assertTrue(it.trimIndent() in relevantOutput) @@ -84,7 +94,7 @@ class KMPWithHmppIT { gradleRunner.withArguments( "--configuration-cache-problems=warn", "clean", - ":$subprojectName:showMe", + ":workload:showMe", ) .build() .let { result -> diff --git a/integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/kotlin/TestProcessor.kt b/integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/kotlin/TestProcessor.kt index 9f3f253f9f..ad4e7b384c 100644 --- a/integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/kotlin/TestProcessor.kt +++ b/integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/kotlin/TestProcessor.kt @@ -19,24 +19,30 @@ class TestProcessor( substringAfter("/$startDirectoryName/").substringBefore("/kotlin/").substringAfterLast('/') override fun process(resolver: Resolver): List { + val allFileNamesSorted = + resolver.getAllFiles().map { "${it.filePath.sourceSetBelow("src")}:${it.fileName}" }.toList().sorted() + val newFileNamesSorted = + resolver.getNewFiles().map { "${it.filePath.sourceSetBelow("src")}:${it.fileName}" }.toList().sorted() + logger.warn("all files: $allFileNamesSorted") + logger.warn("new files: $newFileNamesSorted") + if (invoked) { return emptyList() } invoked = true - val allFileNames = resolver.getAllFiles().map { it.fileName }.toList() - val allFileNamesSorted = allFileNames.sorted() - val currentFileName = allFileNames.last() - val currentFileBaseName = currentFileName.removeSuffix(".kt") - logger.warn("current file: $currentFileName") - logger.warn("all files: $allFileNamesSorted") environment.options.toSortedMap().forEach { (key, value) -> logger.warn("option: '$key' -> '$value'") } val options = environment.options.toSortedMap().map { (key, value) -> "'$key' -> '$value'" } - codeGenerator.createNewFile(Dependencies(false), "", "${currentFileBaseName}Generated", "kt").use { output -> + codeGenerator.createNewFile( + Dependencies(aggregating = true, *resolver.getAllFiles().toList().toTypedArray()), + "com.example", + "Generated", + "kt" + ).use { output -> val outputSourceSet = codeGenerator.generatedFile.first().toString().sourceSetBelow("ksp") OutputStreamWriter(output).use { writer -> @@ -44,8 +50,9 @@ class TestProcessor( """ package com.example - object ${currentFileBaseName}For${outputSourceSet.replaceFirstChar { it.uppercaseChar() }} { + object GeneratedFor${outputSourceSet.replaceFirstChar { it.uppercaseChar() }} { const val allFiles = "$allFileNamesSorted" + const val newFiles = "$newFileNamesSorted" const val options = "$options" const val outputSourceSet = "$outputSourceSet" } diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/clientMain/kotlin/com/example/ClientMainAnnotated.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/clientMain/kotlin/com/example/ClientMainAnnotated.kt index 1b1310e473..9d2268f826 100644 --- a/integration-tests/src/test/resources/kmp-hmpp/workload/src/clientMain/kotlin/com/example/ClientMainAnnotated.kt +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/clientMain/kotlin/com/example/ClientMainAnnotated.kt @@ -2,5 +2,5 @@ package com.example @MyAnnotation class ClientMainAnnotated { - val allFiles = ClientMainAnnotatedForClientMain.allFiles + val allFiles = GeneratedForClientMain.allFiles } diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/commonMain/kotlin/com/example/CommonMainAnnotated.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/commonMain/kotlin/com/example/CommonMainAnnotated.kt index cd903bc0f9..1d5062861e 100644 --- a/integration-tests/src/test/resources/kmp-hmpp/workload/src/commonMain/kotlin/com/example/CommonMainAnnotated.kt +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/commonMain/kotlin/com/example/CommonMainAnnotated.kt @@ -2,5 +2,5 @@ package com.example @MyAnnotation class CommonMainAnnotated { - val allFiles = CommonMainAnnotatedForCommonMain.allFiles + val allFiles = GeneratedForCommonMain.allFiles } diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/com/example/JsMainAnnotated.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/com/example/JsMainAnnotated.kt index 6e150b6f50..5aba0d99fe 100644 --- a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/com/example/JsMainAnnotated.kt +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/com/example/JsMainAnnotated.kt @@ -2,5 +2,5 @@ package com.example @MyAnnotation class JsMainAnnotated { - val allFiles = JsMainAnnotatedForJsMain.allFiles + val allFiles = GeneratedForJsMain.allFiles } diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsTest/kotlin/com/example/JsTestAnnotated.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsTest/kotlin/com/example/JsTestAnnotated.kt index a9c4cb89b8..e28a208a90 100644 --- a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsTest/kotlin/com/example/JsTestAnnotated.kt +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsTest/kotlin/com/example/JsTestAnnotated.kt @@ -2,5 +2,5 @@ package com.example @MyAnnotation class JsTestAnnotated { - val allFiles = Foo_jsTest.allFiles + val allFiles = GeneratedForJsTest.allFiles } diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmMain/kotlin/com/example/JvmMainAnnotated.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmMain/kotlin/com/example/JvmMainAnnotated.kt index 91ef56cc75..97d7c87207 100644 --- a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmMain/kotlin/com/example/JvmMainAnnotated.kt +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmMain/kotlin/com/example/JvmMainAnnotated.kt @@ -2,5 +2,5 @@ package com.example @MyAnnotation class JvmMainAnnotated { - val allFiles = JvmMainAnnotatedForJvmMain.allFiles + val allFiles = GeneratedForJvmMain.allFiles } diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmTest/kotlin/com/example/JvmTestAnnotated.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmTest/kotlin/com/example/JvmTestAnnotated.kt index aeceea6650..997659372e 100644 --- a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmTest/kotlin/com/example/JvmTestAnnotated.kt +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmTest/kotlin/com/example/JvmTestAnnotated.kt @@ -2,5 +2,5 @@ package com.example @MyAnnotation class JvmTestAnnotated { - val allFiles = JvmTestAnnotatedForJvmTest.allFiles + val allFiles = GeneratedForJvmTest.allFiles } From 29f416f9fcdd818f359b8fd9f454fa3bec05df10 Mon Sep 17 00:00:00 2001 From: OliverO2 Date: Mon, 20 Jun 2022 13:51:04 +0200 Subject: [PATCH 5/6] Multiplatform: use task inputs instead of explicit task dependencies --- .../devtools/ksp/gradle/KspConfigurations.kt | 4 --- .../devtools/ksp/gradle/KspSubplugin.kt | 30 ++++++++----------- .../google/devtools/ksp/test/KMPWithHmppIT.kt | 13 ++++---- 3 files changed, 18 insertions(+), 29 deletions(-) diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt index a0e0425ddb..122d6ea281 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt @@ -233,10 +233,6 @@ internal fun KotlinCompilation<*>.parentSourceSetsBottomUp(): Sequence.allSourceSetsBottomUp(): Sequence = - defaultSourceSet.bottomUpDependencies() - .distinct() // avoid repetitions if multiple parents are present - internal fun lowerCamelCased(vararg parts: String): String { return parts.joinToString("") { part -> part.replaceFirstChar { it.uppercase() } diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt index 184a7291a4..e63066c32c 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt @@ -259,14 +259,6 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool val kotlinCompileTask = kotlinCompileProvider.get() val sourceSetOptions = kspConfigurations.resolvedSourceSetOptions(kotlinCompilation) - fun Task.addParentKspTaskDependencyIfExists(sourceSetName: String) { - val parentKspTaskName = lowerCamelCased("ksp", sourceSetName, "KotlinMetadata") - - project.locateTask(parentKspTaskName)?.let { parentKspTask -> - dependsOn(parentKspTask) - } - } - fun configureAsKspTask(kspTask: KspTask, isIncremental: Boolean) { // depends on the processor; if the processor changes, it needs to be reprocessed. val processorClasspath = project.configurations.maybeCreate("${kspTaskName}ProcessorClasspath") @@ -274,11 +266,6 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool kspTask.processorClasspath.from(processorClasspath) kspTask.dependsOn(processorClasspath.buildDependencies) - // depends on outputs of KSP tasks for parent source sets - kotlinCompilation.parentSourceSetsBottomUp().forEach { sourceSet -> - kspTask.addParentKspTaskDependencyIfExists(sourceSet.name) - } - kspTask.options.addAll( kspTask.project.provider { getSubpluginOptions( @@ -332,7 +319,16 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool } } else { if (multiplatformEnabled) { - kotlinCompilation.allSourceSetsBottomUp().forEach { sourceSet -> kspTask.source(sourceSet.kotlin) } + kspTask.source(kotlinCompilation.defaultSourceSet.kotlin) + + // Depend on parent source sets and the outputs of parent KSP tasks. + kotlinCompilation.parentSourceSetsBottomUp().forEach { parentSourceSet -> + kspTask.source(parentSourceSet.kotlin) + val parentKspTaskName = lowerCamelCased("ksp", parentSourceSet.name, "KotlinMetadata") + project.locateTask>(parentKspTaskName)?.let { parentKspTask -> + kspTask.source(parentKspTask) + } + } } else { kotlinCompilation.allKotlinSourceSets.forEach { sourceSet -> kspTask.setSource(sourceSet.kotlin) @@ -401,10 +397,8 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool kotlinCompileProvider.configure { kotlinCompile -> kotlinCompile.dependsOn(kspTaskProvider) - kotlinCompilation.parentSourceSetsBottomUp().forEach { sourceSet -> - kotlinCompile.addParentKspTaskDependencyIfExists(sourceSet.name) - } - kotlinCompile.setSource(kotlinOutputDir, javaOutputDir) + + kotlinCompile.source(kotlinOutputDir, javaOutputDir) when (kotlinCompile) { is AbstractKotlinCompile<*> -> kotlinCompile.libraries.from(project.files(classOutputDir)) // is KotlinNativeCompile -> TODO: support binary generation? diff --git a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt index 29476e627c..f7dd29fcd8 100644 --- a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt +++ b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt @@ -89,7 +89,6 @@ class KMPWithHmppIT { @Test fun testCustomSourceSetHierarchyDependencies() { val gradleRunner = GradleRunner.create().withProjectDir(project.root) - val subprojectName = "workload" gradleRunner.withArguments( "--configuration-cache-problems=warn", @@ -156,21 +155,21 @@ class KMPWithHmppIT { Tasks [compile, ksp] and their ksp/compile dependencies: - * `compileClientMainKotlinMetadata` depends on [kspClientMainKotlinMetadata, kspCommonMainKotlinMetadata] + * `compileClientMainKotlinMetadata` depends on [kspClientMainKotlinMetadata] * `compileCommonMainKotlinMetadata` depends on [kspCommonMainKotlinMetadata] * `compileJava` depends on [] - * `compileKotlinJs` depends on [kspClientMainKotlinMetadata, kspCommonMainKotlinMetadata, kspKotlinJs] - * `compileKotlinJvm` depends on [kspClientMainKotlinMetadata, kspCommonMainKotlinMetadata, kspKotlinJvm] + * `compileKotlinJs` depends on [kspKotlinJs] + * `compileKotlinJvm` depends on [kspKotlinJvm] * `compileKotlinMetadata` depends on [] * `compileTestDevelopmentExecutableKotlinJs` depends on [`compileTestKotlinJs`] * `compileTestJava` depends on [] * `compileTestKotlinJs` depends on [] * `compileTestKotlinJvm` depends on [kspTestKotlinJvm] * `compileTestProductionExecutableKotlinJs` depends on [`compileTestKotlinJs`] - * `kspClientMainKotlinMetadata` depends on [kspClientMainKotlinMetadataProcessorClasspath, kspCommonMainKotlinMetadata] + * `kspClientMainKotlinMetadata` depends on [kspClientMainKotlinMetadataProcessorClasspath] * `kspCommonMainKotlinMetadata` depends on [kspCommonMainKotlinMetadataProcessorClasspath] - * `kspKotlinJs` depends on [kspClientMainKotlinMetadata, kspCommonMainKotlinMetadata, kspKotlinJsProcessorClasspath] - * `kspKotlinJvm` depends on [kspClientMainKotlinMetadata, kspCommonMainKotlinMetadata, kspKotlinJvmProcessorClasspath] + * `kspKotlinJs` depends on [kspKotlinJsProcessorClasspath] + * `kspKotlinJvm` depends on [kspKotlinJvmProcessorClasspath] * `kspTestKotlinJvm` depends on [kspTestKotlinJvmProcessorClasspath] """.trimIndent() in relevantOutput From 91fbc762b1654c3c05355dde1c2e3ee7a6cec5a9 Mon Sep 17 00:00:00 2001 From: OliverO2 Date: Sun, 31 Jul 2022 00:42:21 +0200 Subject: [PATCH 6/6] Optionally use source set dependencies instead of task dependencies These can be enabled via setting the Gradle property 'ksp.sourceSetDependencies.enabled=true'. They are enabled by default for 'ksp.multiplatform.enabled=true'. --- .../devtools/ksp/gradle/KspConfigurations.kt | 7 - .../devtools/ksp/gradle/KspSubplugin.kt | 109 +++-- .../google/devtools/ksp/test/KMPWithHmppIT.kt | 371 ++++++++++++++---- .../kmp-hmpp/workload/build.gradle.kts | 38 +- .../workload/src/jsMain/kotlin/Main.kt | 9 + .../workload/src/jsTest/kotlin/JsTest.kt | 15 + .../kotlin/com/example/JsTestAnnotated.kt | 2 +- .../workload/src/jvmMain/kotlin/Main.kt | 9 + .../workload/src/jvmTest/kotlin/JvmTest.kt | 15 + 9 files changed, 460 insertions(+), 115 deletions(-) create mode 100644 integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/Main.kt create mode 100644 integration-tests/src/test/resources/kmp-hmpp/workload/src/jsTest/kotlin/JsTest.kt create mode 100644 integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmMain/kotlin/Main.kt create mode 100644 integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmTest/kotlin/JvmTest.kt diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt index 122d6ea281..06c1128e14 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt @@ -179,13 +179,6 @@ class KspConfigurations(private val project: Project, multiplatformEnabled: Bool sourceSetOptions.processor?.let { processor -> maybeCreateConfiguration(compilation) project.dependencies.add(getKotlinConfigurationName(compilation), processor) - compilation.defaultSourceSet.kotlin.srcDir( - KspGradleSubplugin.getKspKotlinOutputDir( - project, - compilation.defaultSourceSet.name, - compilation.target.name - ) - ) } } } diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt index e63066c32c..f8cb563c7e 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt @@ -51,7 +51,6 @@ import org.jetbrains.kotlin.gradle.internal.compilerArgumentsConfigurationFlags import org.jetbrains.kotlin.gradle.internal.kapt.incremental.* import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.plugin.mpp.AbstractKotlinNativeCompilation -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinCommonCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinWithJavaCompilation @@ -177,18 +176,23 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool } private var multiplatformEnabled: Boolean = false + private var sourceSetDependenciesEnabled: Boolean = false private lateinit var kspConfigurations: KspConfigurations override fun apply(target: Project) { - multiplatformEnabled = - target.findProperty("ksp.multiplatform.enabled")?.let { it.toString().toBoolean() } ?: false + fun propertyFlag(name: String) = target.findProperty(name)?.let { it.toString().toBoolean() } + + multiplatformEnabled = propertyFlag("ksp.multiplatform.enabled") ?: false + sourceSetDependenciesEnabled = propertyFlag("ksp.sourceSetDependencies.enabled") ?: multiplatformEnabled + if (multiplatformEnabled) { target.logger.warn("[ksp] Enabling the 'ksp' multiplatform extension (supports Kotlin build scripts only)") target.extensions.create("ksp", KspMultiplatformExtension::class.java) } else { target.extensions.create("ksp", KspExtension::class.java) } + kspConfigurations = KspConfigurations(target, multiplatformEnabled) registry.register(KspModelBuilder(multiplatformEnabled)) } @@ -238,7 +242,24 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool return project.provider { emptyList() } } - val target = kotlinCompilation.target.name + if (sourceSetDependenciesEnabled && kspExtension.allowSourcesFromOtherPlugins) { + // Source set dependencies are incompatible with task dependencies introduced by + // `allowSourcesFromOtherPlugins`, resulting in a dependency cycle. + project.logger.warn( + "[ksp] Disabling source set dependencies, because they are incompatible with" + + " 'allowSourcesFromOtherPlugins'" + ) + sourceSetDependenciesEnabled = false + } + + fun String.singleJsNameIfPossible(): String = + if (sourceSetDependenciesEnabled) { + replace(Regex("([jJ]s)(Ir|Legacy)"), "$1") + } else { + this + } + + val target = kotlinCompilation.target.name.singleJsNameIfPossible() val sourceSetName = kotlinCompilation.defaultSourceSetName val classOutputDir = getKspClassOutputDir(project, sourceSetName, target) val javaOutputDir = getKspJavaOutputDir(project, sourceSetName, target) @@ -246,15 +267,13 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool val resourceOutputDir = getKspResourceOutputDir(project, sourceSetName, target) val kspOutputDir = getKspOutputDir(project, sourceSetName, target) - if (javaCompile != null) { - val generatedJavaSources = javaCompile.project.fileTree(javaOutputDir) - generatedJavaSources.include("**/*.java") - javaCompile.source(generatedJavaSources) - javaCompile.classpath += project.files(classOutputDir) - } - assert(kotlinCompileProvider.name.startsWith("compile")) - val kspTaskName = kotlinCompileProvider.name.replaceFirst("compile", "ksp") + val kspTaskName = kotlinCompileProvider.name.replaceFirst("compile", "ksp").singleJsNameIfPossible() + + if (kspTaskName.endsWith("Js") && project.locateTask(kspTaskName) != null) { + // If Js variants (Ir and Legacy) share a single KSP task, avoid configuring it twice. + return project.provider { emptyList() } + } val kotlinCompileTask = kotlinCompileProvider.get() val sourceSetOptions = kspConfigurations.resolvedSourceSetOptions(kotlinCompilation) @@ -318,23 +337,28 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool kspTask.setSource(kotlinCompileTask.javaSources) } } else { + // If source set dependencies are enabled, all Kotlin source sets processed by KSP carry a + // `builtBy` dependency on their KSP task. Otherwise the special treatment explained in the + // following comments (A) and (B) is not relevant. + + // (A) Use dependency-free input for KSP's own source set to avoid a cyclic dependency on itself + // via the `builtBy` dependency, which is carried by the `kotlin` SourceDirectorySet. + kspTask.source(kotlinCompilation.defaultSourceSet.kotlin.srcDirTrees) + + // (B) Use regular dependencies (including `builtBy`, if applicable) for the remaining source sets. if (multiplatformEnabled) { - kspTask.source(kotlinCompilation.defaultSourceSet.kotlin) - - // Depend on parent source sets and the outputs of parent KSP tasks. - kotlinCompilation.parentSourceSetsBottomUp().forEach { parentSourceSet -> - kspTask.source(parentSourceSet.kotlin) - val parentKspTaskName = lowerCamelCased("ksp", parentSourceSet.name, "KotlinMetadata") - project.locateTask>(parentKspTaskName)?.let { parentKspTask -> - kspTask.source(parentKspTask) - } + // On multiplatform, `allKotlinSourceSets` for custom source sets will not contain parent + // source sets. TODO: Check with upstream if this is intended or a bug. + kotlinCompilation.parentSourceSetsBottomUp().forEach { sourceSet -> + kspTask.source(sourceSet.kotlin) } } else { + // On Android, climbing upwards from the default source set is insufficient, so we must use + // `allKotlinSourceSets` (which also works for non-Android, non-multiplatform compilations). kotlinCompilation.allKotlinSourceSets.forEach { sourceSet -> - kspTask.setSource(sourceSet.kotlin) - } - if (kotlinCompilation is KotlinCommonCompilation) { - kspTask.setSource(kotlinCompilation.defaultSourceSet.kotlin) + if (sourceSet != kotlinCompilation.defaultSourceSet) { + kspTask.source(sourceSet.kotlin) + } } } } @@ -395,24 +419,49 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool .execute(kspTaskProvider as TaskProvider>) } - kotlinCompileProvider.configure { kotlinCompile -> - kotlinCompile.dependsOn(kspTaskProvider) + if (sourceSetDependenciesEnabled) { + kotlinCompilation.defaultSourceSet.apply { + kotlin.srcDir(project.files(kotlinOutputDir).builtBy(kspTaskProvider)) + resources.srcDir(project.files(resourceOutputDir).builtBy(kspTaskProvider)) + } + } - kotlinCompile.source(kotlinOutputDir, javaOutputDir) + kotlinCompileProvider.configure { kotlinCompile -> + if (sourceSetDependenciesEnabled) { + kotlinCompile.source(project.files(javaOutputDir).builtBy(kspTaskProvider)) + } else { + kotlinCompile.dependsOn(kspTaskProvider) + kotlinCompile.source(kotlinOutputDir, javaOutputDir) + } when (kotlinCompile) { is AbstractKotlinCompile<*> -> kotlinCompile.libraries.from(project.files(classOutputDir)) // is KotlinNativeCompile -> TODO: support binary generation? } } + if (javaCompile != null) { + if (sourceSetDependenciesEnabled) { + javaCompile.source(project.fileTree(javaOutputDir).builtBy(kspTaskProvider).include("**/*.java")) + javaCompile.classpath += project.files(classOutputDir).builtBy(kspTaskProvider) + } else { + javaCompile.source(project.fileTree(javaOutputDir).include("**/*.java")) + javaCompile.classpath += project.files(classOutputDir) + } + } + val processResourcesTaskName = (kotlinCompilation as? KotlinCompilationWithResources)?.processResourcesTaskName ?: "processResources" project.locateTask(processResourcesTaskName)?.let { provider -> provider.configure { resourcesTask -> - resourcesTask.dependsOn(kspTaskProvider) - resourcesTask.from(resourceOutputDir) + if (sourceSetDependenciesEnabled) { + resourcesTask.from(project.files(resourceOutputDir).builtBy(kspTaskProvider)) + } else { + resourcesTask.dependsOn(kspTaskProvider) + resourcesTask.from(resourceOutputDir) + } } } + if (kotlinCompilation is KotlinJvmAndroidCompilation) { AndroidPluginIntegration.registerGeneratedJavaSources( project = project, diff --git a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt index f7dd29fcd8..afd123df8c 100644 --- a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt +++ b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt @@ -4,6 +4,7 @@ import org.gradle.testkit.runner.GradleRunner import org.junit.Assert import org.junit.Rule import org.junit.Test +import java.io.File class KMPWithHmppIT { @Rule @@ -14,76 +15,285 @@ class KMPWithHmppIT { fun testCustomSourceSetHierarchyBuild() { val gradleRunner = GradleRunner.create().withProjectDir(project.root) - gradleRunner.withArguments( - "--configuration-cache-problems=warn", - "clean", - ":workload:assemble", - ":workload:testClasses", - ) - // .withDebug(true) - .build() - .let { result -> - val output: String = result.output - val relevantOutput = - output.lines().filter { it.startsWith("> Task :workload:ksp") || it.startsWith("w: [ksp] ") } - .joinToString("\n") - - listOf( - """ - > Task :workload:kspCommonMainKotlinMetadata - w: [ksp] all files: [commonMain:CommonMainAnnotated.kt] - w: [ksp] new files: [commonMain:CommonMainAnnotated.kt] - w: [ksp] option: 'a' -> 'a_commonMain' - w: [ksp] option: 'b' -> 'b_global' - w: [ksp] option: 'c' -> 'c_commonMain' - w: [ksp] all files: [commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt] - w: [ksp] new files: [commonMain:Generated.kt] - > Task :workload:kspClientMainKotlinMetadata - w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt] - w: [ksp] new files: [clientMain:ClientMainAnnotated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt] - w: [ksp] option: 'a' -> 'a_clientMain' - w: [ksp] option: 'b' -> 'b_global' - w: [ksp] option: 'c' -> 'c_commonMain' - w: [ksp] option: 'd' -> 'd_clientMain' - w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt] - w: [ksp] new files: [clientMain:Generated.kt] - """, - """ - > Task :workload:kspKotlinJvm - w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jvmMain:JvmMainAnnotated.kt] - w: [ksp] new files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jvmMain:JvmMainAnnotated.kt] - w: [ksp] option: 'a' -> 'a_commonMain' - w: [ksp] option: 'b' -> 'b_global' - w: [ksp] option: 'c' -> 'c_commonMain' - w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jvmMain:Generated.kt, jvmMain:JvmMainAnnotated.kt] - w: [ksp] new files: [jvmMain:Generated.kt] - """, - """ - > Task :workload:kspKotlinJs - w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jsMain:JsMainAnnotated.kt] - w: [ksp] new files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jsMain:JsMainAnnotated.kt] - w: [ksp] option: 'a' -> 'a_commonMain' - w: [ksp] option: 'b' -> 'b_global' - w: [ksp] option: 'c' -> 'c_commonMain' - w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jsMain:Generated.kt, jsMain:JsMainAnnotated.kt] - w: [ksp] new files: [jsMain:Generated.kt] - """, + fun checkBuild( + tasks: List = listOf(":workload:assemble", ":workload:testClasses"), + classToAdd: String? = null, + checkBuildResult: (allOutput: String, kspOutput: String) -> Unit = { _, _ -> } + ) { + if (classToAdd != null) { + val (sourceSetName, className) = classToAdd.split(':') + File(project.root, "workload/src/$sourceSetName/kotlin/com/example/$className.kt").appendText( """ - > Task :workload:kspTestKotlinJvm - w: [ksp] all files: [commonTest:CommonTestAnnotated.kt, jvmTest:JvmTestAnnotated.kt] - w: [ksp] new files: [commonTest:CommonTestAnnotated.kt, jvmTest:JvmTestAnnotated.kt] - w: [ksp] option: 'a' -> 'a_global' - w: [ksp] option: 'b' -> 'b_global' - w: [ksp] all files: [commonTest:CommonTestAnnotated.kt, jvmTest:Generated.kt, jvmTest:JvmTestAnnotated.kt] - w: [ksp] new files: [jvmTest:Generated.kt] - """, - ).forEach { - Assert.assertTrue(it.trimIndent() in relevantOutput) + package com.example + + @MyAnnotation + class $className { + val allFiles = GeneratedFor${sourceSetName.replaceFirstChar { it.uppercase() }}.allFiles + } + """.trimIndent() + ) + } + + gradleRunner.withArguments( + "--configuration-cache-problems=warn", + *tasks.toTypedArray(), + ) + // .withDebug(true) + .build() + .let { result -> + val allOutput: String = result.output + val kspOutput = + allOutput.lines().filter { it.startsWith("> Task :workload:ksp") || it.startsWith("w: [ksp] ") } + .joinToString("\n") + + Assert.assertTrue("> Task :annotations:ksp" !in allOutput) + Assert.assertTrue("Execution optimizations have been disabled" !in allOutput) + + checkBuildResult(allOutput, kspOutput) } + } + + checkBuild( + listOf( + "clean", + ":workload:run", + ":workload:jsNodeDevelopmentRun", + ":workload:jvmTest", + ":workload:jsNodeTest" + ) + ) { allOutput, kspOutput -> + listOf( + """ + > Task :workload:kspCommonMainKotlinMetadata + w: [ksp] all files: [commonMain:CommonMainAnnotated.kt] + w: [ksp] new files: [commonMain:CommonMainAnnotated.kt] + w: [ksp] option: 'a' -> 'a_commonMain' + w: [ksp] option: 'b' -> 'b_global' + w: [ksp] option: 'c' -> 'c_commonMain' + w: [ksp] all files: [commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt] + w: [ksp] new files: [commonMain:Generated.kt] + > Task :workload:kspClientMainKotlinMetadata + w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt] + w: [ksp] new files: [clientMain:ClientMainAnnotated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt] + w: [ksp] option: 'a' -> 'a_clientMain' + w: [ksp] option: 'b' -> 'b_global' + w: [ksp] option: 'c' -> 'c_commonMain' + w: [ksp] option: 'd' -> 'd_clientMain' + w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt] + w: [ksp] new files: [clientMain:Generated.kt] + """, + """ + > Task :workload:kspKotlinJvm + w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jvmMain:JvmMainAnnotated.kt, jvmMain:Main.kt] + w: [ksp] new files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jvmMain:JvmMainAnnotated.kt, jvmMain:Main.kt] + w: [ksp] option: 'a' -> 'a_commonMain' + w: [ksp] option: 'b' -> 'b_global' + w: [ksp] option: 'c' -> 'c_commonMain' + w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jvmMain:Generated.kt, jvmMain:JvmMainAnnotated.kt, jvmMain:Main.kt] + w: [ksp] new files: [jvmMain:Generated.kt] + """, + """ + > Task :workload:kspKotlinJs + w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jsMain:JsMainAnnotated.kt, jsMain:Main.kt] + w: [ksp] new files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jsMain:JsMainAnnotated.kt, jsMain:Main.kt] + w: [ksp] option: 'a' -> 'a_commonMain' + w: [ksp] option: 'b' -> 'b_global' + w: [ksp] option: 'c' -> 'c_commonMain' + w: [ksp] all files: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jsMain:Generated.kt, jsMain:JsMainAnnotated.kt, jsMain:Main.kt] + w: [ksp] new files: [jsMain:Generated.kt] + """, + """ + > Task :workload:kspTestKotlinJvm + w: [ksp] all files: [commonTest:CommonTestAnnotated.kt, jvmTest:JvmTest.kt, jvmTest:JvmTestAnnotated.kt] + w: [ksp] new files: [commonTest:CommonTestAnnotated.kt, jvmTest:JvmTest.kt, jvmTest:JvmTestAnnotated.kt] + w: [ksp] option: 'a' -> 'a_global' + w: [ksp] option: 'b' -> 'b_global' + w: [ksp] all files: [commonTest:CommonTestAnnotated.kt, jvmTest:Generated.kt, jvmTest:JvmTest.kt, jvmTest:JvmTestAnnotated.kt] + w: [ksp] new files: [jvmTest:Generated.kt] + """, + ).forEach { + kspOutput.shouldContain(it) + } + listOf( + """ + > Task :workload:run + commonMain: [commonMain:CommonMainAnnotated.kt] + clientMain: [clientMain:ClientMainAnnotated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt] + jvmMain: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jvmMain:JvmMainAnnotated.kt, jvmMain:Main.kt] + """, + """ + > Task :workload:jsNodeDevelopmentRun + commonMain: [commonMain:CommonMainAnnotated.kt] + clientMain: [clientMain:ClientMainAnnotated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt] + jsMain: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jsMain:JsMainAnnotated.kt, jsMain:Main.kt] + """, + """ + > Task :workload:jvmTest + + JvmTest[jvm] > main()[jvm] STANDARD_OUT + commonMain: [commonMain:CommonMainAnnotated.kt] + clientMain: [clientMain:ClientMainAnnotated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt] + jvmMain: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jvmMain:JvmMainAnnotated.kt, jvmMain:Main.kt] + jvmTest: [commonTest:CommonTestAnnotated.kt, jvmTest:JvmTest.kt, jvmTest:JvmTestAnnotated.kt] + """, + """ + > Task :workload:jsNodeTest + + JsTest.main STANDARD_OUT + commonMain: [commonMain:CommonMainAnnotated.kt] + clientMain: [clientMain:ClientMainAnnotated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt] + jsMain: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:Generated.kt, jsMain:JsMainAnnotated.kt, jsMain:Main.kt] + jsTest: (nothing) + """, + ).forEach { + allOutput.shouldContain(it) + } + } + + checkBuild( + listOf(":workload:run", ":workload:jsNodeDevelopmentRun", ":workload:jvmTest", ":workload:jsNodeTest"), + classToAdd = "commonMain:CommonMainAnnotated2" + ) { allOutput, _ -> + listOf( + """ + > Task :workload:run + commonMain: [commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt] + clientMain: [clientMain:ClientMainAnnotated.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt] + jvmMain: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt, jvmMain:JvmMainAnnotated.kt, jvmMain:Main.kt] + """, + """ + > Task :workload:jsNodeDevelopmentRun + commonMain: [commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt] + clientMain: [clientMain:ClientMainAnnotated.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt] + jsMain: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt, jsMain:JsMainAnnotated.kt, jsMain:Main.kt] + """, + """ + > Task :workload:jvmTest + + JvmTest[jvm] > main()[jvm] STANDARD_OUT + commonMain: [commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt] + clientMain: [clientMain:ClientMainAnnotated.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt] + jvmMain: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt, jvmMain:JvmMainAnnotated.kt, jvmMain:Main.kt] + jvmTest: [commonTest:CommonTestAnnotated.kt, jvmTest:JvmTest.kt, jvmTest:JvmTestAnnotated.kt] + """, + """ + > Task :workload:jsNodeTest + + JsTest.main STANDARD_OUT + commonMain: [commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt] + clientMain: [clientMain:ClientMainAnnotated.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt] + jsMain: [clientMain:ClientMainAnnotated.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt, jsMain:JsMainAnnotated.kt, jsMain:Main.kt] + jsTest: (nothing) + """, + ).forEach { + allOutput.shouldContain(it) + } + } + + // TODO: Running ":workload:jsNodeDevelopmentRun" twice with configuration cache fails with: + // Could not load the value of field `values` of + // `org.gradle.api.internal.collections.SortedSetElementSource` bean found in field `store` of + // `org.gradle.api.internal.FactoryNamedDomainObjectContainer` bean found in field `compilations` of + // `org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMetadataTarget` bean found in field `target` of [...] + checkBuild( + listOf( + ":workload:run", + /* ":workload:jsNodeDevelopmentRun", */ + ":workload:jvmTest", + ":workload:jsNodeTest" + ), + classToAdd = "clientMain:ClientMainAnnotated2" + ) { allOutput, _ -> + listOf( + """ + > Task :workload:kspCommonMainKotlinMetadata UP-TO-DATE + """, + """ + > Task :workload:run + commonMain: [commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt] + clientMain: [clientMain:ClientMainAnnotated.kt, clientMain:ClientMainAnnotated2.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt] + jvmMain: [clientMain:ClientMainAnnotated.kt, clientMain:ClientMainAnnotated2.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt, jvmMain:JvmMainAnnotated.kt, jvmMain:Main.kt] + """, +/* + """ + > Task :workload:jsNodeDevelopmentRun + commonMain: [commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt] + clientMain: [clientMain:ClientMainAnnotated.kt, clientMain:ClientMainAnnotated2.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt] + jsMain: [clientMain:ClientMainAnnotated.kt, clientMain:ClientMainAnnotated2.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt, jsMain:JsMainAnnotated.kt, jsMain:Main.kt] + """, +*/ + """ + > Task :workload:jvmTest + + JvmTest[jvm] > main()[jvm] STANDARD_OUT + commonMain: [commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt] + clientMain: [clientMain:ClientMainAnnotated.kt, clientMain:ClientMainAnnotated2.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt] + jvmMain: [clientMain:ClientMainAnnotated.kt, clientMain:ClientMainAnnotated2.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt, jvmMain:JvmMainAnnotated.kt, jvmMain:Main.kt] + jvmTest: [commonTest:CommonTestAnnotated.kt, jvmTest:JvmTest.kt, jvmTest:JvmTestAnnotated.kt] + """, + """ + > Task :workload:jsNodeTest + + JsTest.main STANDARD_OUT + commonMain: [commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt] + clientMain: [clientMain:ClientMainAnnotated.kt, clientMain:ClientMainAnnotated2.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt] + jsMain: [clientMain:ClientMainAnnotated.kt, clientMain:ClientMainAnnotated2.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt, jsMain:JsMainAnnotated.kt, jsMain:Main.kt] + jsTest: (nothing) + """, + ).forEach { + allOutput.shouldContain(it) + } + } - Assert.assertTrue("> Task :annotations:ksp" !in output) - Assert.assertTrue("Execution optimizations have been disabled" !in output) + checkBuild( + listOf( + ":workload:run", + /* ":workload:jsNodeDevelopmentRun", */ + ":workload:jvmTest", + ":workload:jsNodeTest" + ), + classToAdd = "jvmMain:JvmMainAnnotated2" + ) { allOutput, _ -> + listOf( + """ + > Task :workload:kspCommonMainKotlinMetadata UP-TO-DATE + """, + """ + > Task :workload:kspClientMainKotlinMetadata UP-TO-DATE + """, + """ + > Task :workload:kspKotlinJs UP-TO-DATE + """, + """ + > Task :workload:run + commonMain: [commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt] + clientMain: [clientMain:ClientMainAnnotated.kt, clientMain:ClientMainAnnotated2.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt] + jvmMain: [clientMain:ClientMainAnnotated.kt, clientMain:ClientMainAnnotated2.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt, jvmMain:JvmMainAnnotated.kt, jvmMain:JvmMainAnnotated2.kt, jvmMain:Main.kt] + """, +/* + """ + > Task :workload:jsNodeDevelopmentRun + commonMain: [commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt] + clientMain: [clientMain:ClientMainAnnotated.kt, clientMain:ClientMainAnnotated2.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt] + jsMain: [clientMain:ClientMainAnnotated.kt, clientMain:ClientMainAnnotated2.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt, jsMain:JsMainAnnotated.kt, jsMain:Main.kt] + """, +*/ + """ + > Task :workload:jvmTest + + JvmTest[jvm] > main()[jvm] STANDARD_OUT + commonMain: [commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt] + clientMain: [clientMain:ClientMainAnnotated.kt, clientMain:ClientMainAnnotated2.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt] + jvmMain: [clientMain:ClientMainAnnotated.kt, clientMain:ClientMainAnnotated2.kt, clientMain:Generated.kt, commonMain:CommonMainAnnotated.kt, commonMain:CommonMainAnnotated2.kt, commonMain:Generated.kt, jvmMain:JvmMainAnnotated.kt, jvmMain:JvmMainAnnotated2.kt, jvmMain:Main.kt] + jvmTest: [commonTest:CommonTestAnnotated.kt, jvmTest:JvmTest.kt, jvmTest:JvmTestAnnotated.kt] + """, + """ + > Task :workload:jsNodeTest UP-TO-DATE + """, + ).forEach { + allOutput.shouldContain(it) } + } } @Test @@ -97,13 +307,13 @@ class KMPWithHmppIT { ) .build() .let { result -> - val output: String = result.output - val relevantOutput = - output.lines() + val allOutput: String = result.output + val kspOutput = + allOutput.lines() .mapNotNull { if (it.startsWith("[showMe] ")) it.substringAfter("[showMe] ") else null } .joinToString("\n") - Assert.assertTrue( + kspOutput.shouldContain( """ Kotlin targets/compilations/allKotlinSourceSets: @@ -155,16 +365,18 @@ class KMPWithHmppIT { Tasks [compile, ksp] and their ksp/compile dependencies: - * `compileClientMainKotlinMetadata` depends on [kspClientMainKotlinMetadata] - * `compileCommonMainKotlinMetadata` depends on [kspCommonMainKotlinMetadata] + * `compileClientMainKotlinMetadata` depends on [] + * `compileCommonMainKotlinMetadata` depends on [] + * `compileDevelopmentExecutableKotlinJs` depends on [`compileKotlinJs`] * `compileJava` depends on [] - * `compileKotlinJs` depends on [kspKotlinJs] - * `compileKotlinJvm` depends on [kspKotlinJvm] + * `compileKotlinJs` depends on [] + * `compileKotlinJvm` depends on [] * `compileKotlinMetadata` depends on [] + * `compileProductionExecutableKotlinJs` depends on [`compileKotlinJs`] * `compileTestDevelopmentExecutableKotlinJs` depends on [`compileTestKotlinJs`] * `compileTestJava` depends on [] * `compileTestKotlinJs` depends on [] - * `compileTestKotlinJvm` depends on [kspTestKotlinJvm] + * `compileTestKotlinJvm` depends on [] * `compileTestProductionExecutableKotlinJs` depends on [`compileTestKotlinJs`] * `kspClientMainKotlinMetadata` depends on [kspClientMainKotlinMetadataProcessorClasspath] * `kspCommonMainKotlinMetadata` depends on [kspCommonMainKotlinMetadataProcessorClasspath] @@ -172,8 +384,15 @@ class KMPWithHmppIT { * `kspKotlinJvm` depends on [kspKotlinJvmProcessorClasspath] * `kspTestKotlinJvm` depends on [kspTestKotlinJvmProcessorClasspath] - """.trimIndent() in relevantOutput + """ ) } } } + +fun String.shouldContain(expectedRawContent: String) { + val expectedContent = expectedRawContent.trimIndent() + assert(expectedContent in this) { + "Missing expected content:\n$expectedContent\n\nIn output:\n$this" + } +} diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/build.gradle.kts b/integration-tests/src/test/resources/kmp-hmpp/workload/build.gradle.kts index 5b076528c7..3c5698b423 100644 --- a/integration-tests/src/test/resources/kmp-hmpp/workload/build.gradle.kts +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/build.gradle.kts @@ -8,17 +8,23 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinCommonCompilation plugins { kotlin("multiplatform") id("com.google.devtools.ksp") + application } version = "1.0-SNAPSHOT" +application { + mainClass.set("MainKt") +} + kotlin { jvm { withJava() } js(IR) { - browser() + nodejs() + binaries.executable() } sourceSets { @@ -50,10 +56,19 @@ kotlin { dependsOn(clientMain) } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + val jvmTest by getting { ksp { processor(project(":test-processor")) } + dependencies { + implementation("org.junit.jupiter:junit-jupiter-params:5.8.2") + } } } } @@ -64,6 +79,27 @@ ksp { } tasks { + val jvmTest by getting(Test::class) { + useJUnitPlatform() + // Show stdout/stderr and stack traces on console – https://stackoverflow.com/q/65573633/2529022 + testLogging { + events("PASSED", "FAILED", "SKIPPED") + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + showStandardStreams = true + showStackTraces = true + } + } + + val jsNodeTest by getting(org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest::class) { + // Show stdout/stderr and stack traces on console – https://stackoverflow.com/q/65573633/2529022 + testLogging { + events("PASSED", "FAILED", "SKIPPED") + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + showStandardStreams = true + showStackTraces = true + } + } + val showMe by registering { doLast { fun Any.asText() = when (this) { diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/Main.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/Main.kt new file mode 100644 index 0000000000..cd08ca9904 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/Main.kt @@ -0,0 +1,9 @@ +import com.example.ClientMainAnnotated +import com.example.CommonMainAnnotated +import com.example.JsMainAnnotated + +fun main() { + println("commonMain: " + CommonMainAnnotated().allFiles) + println("clientMain: " + ClientMainAnnotated().allFiles) + println("jsMain: " + JsMainAnnotated().allFiles) +} diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsTest/kotlin/JsTest.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsTest/kotlin/JsTest.kt new file mode 100644 index 0000000000..c2da76ba17 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsTest/kotlin/JsTest.kt @@ -0,0 +1,15 @@ +import com.example.ClientMainAnnotated +import com.example.CommonMainAnnotated +import com.example.JsMainAnnotated +import com.example.JsTestAnnotated +import kotlin.test.Test + +class JsTest { + @Test + fun main() { + println("commonMain: " + CommonMainAnnotated().allFiles) + println("clientMain: " + ClientMainAnnotated().allFiles) + println("jsMain: " + JsMainAnnotated().allFiles) + println("jsTest: " + JsTestAnnotated().allFiles) + } +} diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsTest/kotlin/com/example/JsTestAnnotated.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsTest/kotlin/com/example/JsTestAnnotated.kt index e28a208a90..c24d8abbd7 100644 --- a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsTest/kotlin/com/example/JsTestAnnotated.kt +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsTest/kotlin/com/example/JsTestAnnotated.kt @@ -2,5 +2,5 @@ package com.example @MyAnnotation class JsTestAnnotated { - val allFiles = GeneratedForJsTest.allFiles + val allFiles = "(nothing)" } diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmMain/kotlin/Main.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmMain/kotlin/Main.kt new file mode 100644 index 0000000000..2d4410e9ce --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmMain/kotlin/Main.kt @@ -0,0 +1,9 @@ +import com.example.ClientMainAnnotated +import com.example.CommonMainAnnotated +import com.example.JvmMainAnnotated + +fun main() { + println("commonMain: " + CommonMainAnnotated().allFiles) + println("clientMain: " + ClientMainAnnotated().allFiles) + println("jvmMain: " + JvmMainAnnotated().allFiles) +} diff --git a/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmTest/kotlin/JvmTest.kt b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmTest/kotlin/JvmTest.kt new file mode 100644 index 0000000000..57234e1ae8 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jvmTest/kotlin/JvmTest.kt @@ -0,0 +1,15 @@ +import com.example.ClientMainAnnotated +import com.example.CommonMainAnnotated +import com.example.JvmMainAnnotated +import com.example.JvmTestAnnotated +import org.junit.jupiter.api.Test + +class JvmTest { + @Test + fun main() { + println("commonMain: " + CommonMainAnnotated().allFiles) + println("clientMain: " + ClientMainAnnotated().allFiles) + println("jvmMain: " + JvmMainAnnotated().allFiles) + println("jvmTest: " + JvmTestAnnotated().allFiles) + } +}