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..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 @@ -5,13 +5,12 @@ 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 /** * 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" } @@ -24,18 +23,21 @@ 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 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>() - @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 +46,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 +58,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 +114,120 @@ 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) + } + } + } + + /** + * 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 -> + 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(kspMultiplatformExtension.globalSourceSetOptions()) + } ?: 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/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 826f3ea3e9..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 @@ -135,8 +134,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 +153,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 +161,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 { @@ -176,12 +175,26 @@ 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) { - target.extensions.create("ksp", KspExtension::class.java) - kspConfigurations = KspConfigurations(target) - registry.register(KspModelBuilder()) + 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)) } override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean { @@ -214,17 +227,39 @@ 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 kspConfigurations = kspConfigurations.find(kotlinCompilation) - val nonEmptyKspConfigurations = kspConfigurations.filter { it.allDependencies.isNotEmpty() } - if (nonEmptyKspConfigurations.isEmpty()) { + 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()) { return project.provider { emptyList() } } if (kotlinCompileProvider.name == "compileKotlinMetadata") { 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) @@ -232,22 +267,21 @@ 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) 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) @@ -259,15 +293,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) { @@ -303,11 +337,29 @@ 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 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) { + // 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 -> + if (sourceSet != kotlinCompilation.defaultSourceSet) { + kspTask.source(sourceSet.kotlin) + } + } } } @@ -367,23 +419,49 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool .execute(kspTaskProvider as TaskProvider>) } + if (sourceSetDependenciesEnabled) { + kotlinCompilation.defaultSourceSet.apply { + kotlin.srcDir(project.files(kotlinOutputDir).builtBy(kspTaskProvider)) + resources.srcDir(project.files(resourceOutputDir).builtBy(kspTaskProvider)) + } + } + kotlinCompileProvider.configure { kotlinCompile -> - kotlinCompile.dependsOn(kspTaskProvider) - kotlinCompile.setSource(kotlinOutputDir, javaOutputDir) + 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, @@ -500,8 +578,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/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 6c82b31f5f..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 @@ -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,50 @@ 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, + enableMultiplatformExtension = true, + ) + 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..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 @@ -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,29 @@ 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, + enableMultiplatformExtension: Boolean = false + ) { + if (enableMultiplatformExtension) + testProject.appendGradleProperties("ksp.multiplatform.enabled=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..afd123df8c --- /dev/null +++ b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/KMPWithHmppIT.kt @@ -0,0 +1,398 @@ +package com.google.devtools.ksp.test + +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 + @JvmField + val project: TemporaryTestProject = TemporaryTestProject("kmp-hmpp") + + @Test + fun testCustomSourceSetHierarchyBuild() { + val gradleRunner = GradleRunner.create().withProjectDir(project.root) + + 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( + """ + 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) + } + } + + 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 + fun testCustomSourceSetHierarchyDependencies() { + val gradleRunner = GradleRunner.create().withProjectDir(project.root) + + gradleRunner.withArguments( + "--configuration-cache-problems=warn", + "clean", + ":workload:showMe", + ) + .build() + .let { result -> + val allOutput: String = result.output + val kspOutput = + allOutput.lines() + .mapNotNull { if (it.startsWith("[showMe] ")) it.substringAfter("[showMe] ") else null } + .joinToString("\n") + + kspOutput.shouldContain( + """ + + 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 [] + * `compileCommonMainKotlinMetadata` depends on [] + * `compileDevelopmentExecutableKotlinJs` depends on [`compileKotlinJs`] + * `compileJava` depends on [] + * `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 [] + * `compileTestProductionExecutableKotlinJs` depends on [`compileTestKotlinJs`] + * `kspClientMainKotlinMetadata` depends on [kspClientMainKotlinMetadataProcessorClasspath] + * `kspCommonMainKotlinMetadata` depends on [kspCommonMainKotlinMetadataProcessorClasspath] + * `kspKotlinJs` depends on [kspKotlinJsProcessorClasspath] + * `kspKotlinJvm` depends on [kspKotlinJvmProcessorClasspath] + * `kspTestKotlinJvm` depends on [kspTestKotlinJvmProcessorClasspath] + + """ + ) + } + } +} + +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/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..6833cc3bcd --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx2048M +ksp.multiplatform.enabled=true 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..ad4e7b384c --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/test-processor/src/main/kotlin/TestProcessor.kt @@ -0,0 +1,73 @@ +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 { + 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 + + environment.options.toSortedMap().forEach { (key, value) -> + logger.warn("option: '$key' -> '$value'") + } + + val options = environment.options.toSortedMap().map { (key, value) -> "'$key' -> '$value'" } + + 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 -> + writer.write( + """ + package com.example + + object GeneratedFor${outputSourceSet.replaceFirstChar { it.uppercaseChar() }} { + const val allFiles = "$allFileNamesSorted" + const val newFiles = "$newFileNamesSorted" + 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..3c5698b423 --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/build.gradle.kts @@ -0,0 +1,189 @@ +@file:Suppress("UNUSED_VARIABLE") + +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") + id("com.google.devtools.ksp") + application +} + +version = "1.0-SNAPSHOT" + +application { + mainClass.set("MainKt") +} + +kotlin { + jvm { + withJava() + } + + js(IR) { + nodejs() + binaries.executable() + } + + 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 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") + } + } + } +} + +ksp { + arg("a", "a_global") + arg("b", "b_global") +} + +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) { + 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 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 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..9d2268f826 --- /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 = 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 new file mode 100644 index 0000000000..1d5062861e --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/commonMain/kotlin/com/example/CommonMainAnnotated.kt @@ -0,0 +1,6 @@ +package com.example + +@MyAnnotation +class CommonMainAnnotated { + val allFiles = GeneratedForCommonMain.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/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/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..5aba0d99fe --- /dev/null +++ b/integration-tests/src/test/resources/kmp-hmpp/workload/src/jsMain/kotlin/com/example/JsMainAnnotated.kt @@ -0,0 +1,6 @@ +package com.example + +@MyAnnotation +class JsMainAnnotated { + val allFiles = GeneratedForJsMain.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 new file mode 100644 index 0000000000..c24d8abbd7 --- /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 = "(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/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..97d7c87207 --- /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 = GeneratedForJvmMain.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) + } +} 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..997659372e --- /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 = GeneratedForJvmTest.allFiles +}