blob: 30bec0d5586272e9ed494a04d7724b2721c39cf1 [file] [log] [blame]
/*
* 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.
*/
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
package com.google.devtools.ksp.gradle
import com.google.devtools.ksp.gradle.model.builder.KspModelBuilder
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.UnknownTaskException
import org.gradle.api.artifacts.Configuration
import org.gradle.api.attributes.Attribute
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.compile.AbstractCompile
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.language.jvm.tasks.ProcessResources
import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.cli.common.arguments.*
import org.jetbrains.kotlin.gradle.dsl.*
import org.jetbrains.kotlin.gradle.internal.CompilerArgumentsContributor
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.KotlinJvmAndroidCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinWithJavaCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.enabledOnCurrentHost
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.KotlinCompilationData
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.KotlinNativeCompilationData
import org.jetbrains.kotlin.gradle.tasks.*
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile.Configurator
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.incremental.ChangedFiles
import org.jetbrains.kotlin.incremental.destinationAsFile
import org.jetbrains.kotlin.incremental.isJavaFile
import org.jetbrains.kotlin.incremental.isKotlinFile
import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty
import java.io.File
import java.util.*
import java.util.concurrent.Callable
import javax.inject.Inject
import kotlin.reflect.KProperty1
class KspGradleSubplugin @Inject internal constructor(private val registry: ToolingModelBuilderRegistry) :
KotlinCompilerPluginSupportPlugin {
companion object {
const val KSP_MAIN_CONFIGURATION_NAME = "ksp"
const val KSP_ARTIFACT_NAME = "symbol-processing"
const val KSP_ARTIFACT_NAME_NATIVE = "symbol-processing-cmdline"
const val KSP_PLUGIN_ID = "com.google.devtools.ksp.symbol-processing"
@JvmStatic
fun getKspOutputDir(project: Project, sourceSetName: String) =
File(project.project.buildDir, "generated/ksp/$sourceSetName")
@JvmStatic
fun getKspClassOutputDir(project: Project, sourceSetName: String) =
File(getKspOutputDir(project, sourceSetName), "classes")
@JvmStatic
fun getKspJavaOutputDir(project: Project, sourceSetName: String) =
File(getKspOutputDir(project, sourceSetName), "java")
@JvmStatic
fun getKspKotlinOutputDir(project: Project, sourceSetName: String) =
File(getKspOutputDir(project, sourceSetName), "kotlin")
@JvmStatic
fun getKspResourceOutputDir(project: Project, sourceSetName: String) =
File(getKspOutputDir(project, sourceSetName), "resources")
@JvmStatic
fun getKspCachesDir(project: Project, sourceSetName: String) =
File(project.project.buildDir, "kspCaches/$sourceSetName")
@JvmStatic
private fun getSubpluginOptions(
project: Project,
kspExtension: KspExtension,
nonEmptyKspConfigurations: List<Configuration>,
sourceSetName: String,
isIncremental: Boolean,
): List<SubpluginOption> {
val options = mutableListOf<SubpluginOption>()
options += SubpluginOption("classOutputDir", getKspClassOutputDir(project, sourceSetName).path)
options += SubpluginOption("javaOutputDir", getKspJavaOutputDir(project, sourceSetName).path)
options += SubpluginOption("kotlinOutputDir", getKspKotlinOutputDir(project, sourceSetName).path)
options += SubpluginOption("resourceOutputDir", getKspResourceOutputDir(project, sourceSetName).path)
options += SubpluginOption("cachesDir", getKspCachesDir(project, sourceSetName).path)
options += SubpluginOption("kspOutputDir", getKspOutputDir(project, sourceSetName).path)
options += SubpluginOption("incremental", isIncremental.toString())
options += SubpluginOption(
"incrementalLog",
project.findProperty("ksp.incremental.log")?.toString() ?: "false"
)
options += SubpluginOption("projectBaseDir", project.project.projectDir.canonicalPath)
options += FilesSubpluginOption("apclasspath", nonEmptyKspConfigurations.flatten())
kspExtension.apOptions.forEach {
options += SubpluginOption("apoption", "${it.key}=${it.value}")
}
return options
}
}
private val androidIntegration by lazy {
AndroidPluginIntegration(this)
}
@OptIn(ExperimentalStdlibApi::class)
private val KotlinSourceSet.kspConfigurationName: String
get() {
return if (name == SourceSet.MAIN_SOURCE_SET_NAME) {
KSP_MAIN_CONFIGURATION_NAME
} else {
"$KSP_MAIN_CONFIGURATION_NAME${name.capitalize(Locale.US)}"
}
}
private fun KotlinSourceSet.kspConfiguration(project: Project): Configuration? {
return project.configurations.findByName(kspConfigurationName)
}
override fun apply(project: Project) {
project.extensions.create("ksp", KspExtension::class.java)
// Always include the main `ksp` configuration.
// TODO: multiplatform
project.configurations.create(KSP_MAIN_CONFIGURATION_NAME)
project.plugins.withType(KotlinPluginWrapper::class.java) {
// kotlin extension has the compilation target that we need to look for to create configurations
decorateKotlinExtension(project)
}
androidIntegration.applyIfAndroidProject(project)
registry.register(KspModelBuilder())
}
private fun decorateKotlinExtension(project: Project) {
project.extensions.configure(KotlinSingleTargetExtension::class.java) { kotlinExtension ->
kotlinExtension.target.compilations.createKspConfigurations(project) { kotlinCompilation ->
kotlinCompilation.kotlinSourceSets.map {
it.kspConfigurationName
}
}
}
}
/**
* Creates a KSP configuration for each element in the object container.
*/
internal fun <T> NamedDomainObjectContainer<T>.createKspConfigurations(
project: Project,
getKspConfigurationNames: (T) -> List<String>,
) {
val mainConfiguration = project.configurations.maybeCreate(KSP_MAIN_CONFIGURATION_NAME)
all {
getKspConfigurationNames(it).forEach { kspConfigurationName ->
if (kspConfigurationName != KSP_MAIN_CONFIGURATION_NAME) {
val existing = project.configurations.findByName(kspConfigurationName)
if (existing == null) {
project.configurations.create(kspConfigurationName) {
it.extendsFrom(mainConfiguration)
}
}
}
}
}
}
override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean {
val project = kotlinCompilation.target.project
val kspVersion = javaClass.`package`.implementationVersion
val kotlinVersion = project.getKotlinPluginVersion() ?: "N/A"
// Check version and show warning by default.
val noVersionCheck = project.findProperty("ksp.version.check")?.toString()?.toBoolean() == false
if (!noVersionCheck && !kspVersion.startsWith(kotlinVersion))
project.logger.warn(
"ksp-$kspVersion might not work with kotlin-$kotlinVersion properly. " +
"Please pick the same version of ksp and kotlin plugins."
)
return true
}
override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider<List<SubpluginOption>> {
val project = kotlinCompilation.target.project
val kotlinCompileProvider: TaskProvider<AbstractCompile> =
project.locateTask(kotlinCompilation.compileKotlinTaskName) ?: return project.provider { emptyList() }
val javaCompile = findJavaTaskForKotlinCompilation(kotlinCompilation)?.get()
val kspExtension = project.extensions.getByType(KspExtension::class.java)
val kspConfigurations = LinkedHashSet<Configuration>()
kotlinCompilation.allKotlinSourceSets.forEach {
it.kspConfiguration(project)?.let {
kspConfigurations.add(it)
}
}
// Always include the main `ksp` configuration.
// TODO: multiplatform
project.configurations.findByName(KSP_MAIN_CONFIGURATION_NAME)?.let {
kspConfigurations.add(it)
}
val nonEmptyKspConfigurations = kspConfigurations.filter { it.dependencies.isNotEmpty() }
if (nonEmptyKspConfigurations.isEmpty()) {
return project.provider { emptyList() }
}
val sourceSetName = kotlinCompilation.defaultSourceSetName
val classOutputDir = getKspClassOutputDir(project, sourceSetName)
val javaOutputDir = getKspJavaOutputDir(project, sourceSetName)
val kotlinOutputDir = getKspKotlinOutputDir(project, sourceSetName)
val resourceOutputDir = getKspResourceOutputDir(project, sourceSetName)
val kspOutputDir = getKspOutputDir(project, sourceSetName)
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 kotlinCompileTask = kotlinCompileProvider.get()
fun configureAsKspTask(kspTask: KspTask, isIncremental: Boolean) {
kspTask.options.addAll(
kspTask.project.provider {
getSubpluginOptions(
project,
kspExtension,
nonEmptyKspConfigurations,
sourceSetName,
isIncremental
)
}
)
kspTask.destination = kspOutputDir
kspTask.blockOtherCompilerPlugins = kspExtension.blockOtherCompilerPlugins
kspTask.apOptions.value(kspExtension.arguments).disallowChanges()
kspTask.kspCacheDir.fileValue(getKspCachesDir(project, sourceSetName)).disallowChanges()
// depends on the processor; if the processor changes, it needs to be reprocessed.
val processorClasspath = project.configurations.maybeCreate("${kspTaskName}ProcessorClasspath")
.extendsFrom(*nonEmptyKspConfigurations.toTypedArray())
kspTask.processorClasspath.from(processorClasspath)
nonEmptyKspConfigurations.forEach {
kspTask.dependsOn(it.buildDependencies)
}
if (kspExtension.blockOtherCompilerPlugins) {
// FIXME: ask upstream to provide an API to make this not implementation-dependent.
val cfg = project.configurations.getByName(kotlinCompilation.pluginConfigurationName)
kspTask.overridePluginClasspath.value(
kspTask.project.provider {
val dep = cfg.dependencies.single { it.name == KSP_ARTIFACT_NAME }
cfg.fileCollection(dep)
}
)
}
kspTask.isKspIncremental = isIncremental
}
fun configureAsAbstractCompile(kspTask: AbstractCompile) {
kspTask.getDestinationDirectory().set(kspOutputDir)
kspTask.outputs.dirs(
kotlinOutputDir,
javaOutputDir,
classOutputDir,
resourceOutputDir
)
kspTask.source(kotlinCompileTask.source)
if (kotlinCompileTask is AbstractKotlinCompile<*>) {
val sourceRoots = kotlinCompileTask.getSourceRoots()
if (sourceRoots is SourceRoots.ForJvm) {
kspTask.source(sourceRoots.javaSourceRoots)
}
}
// Don't support binary generation for K/N yet.
// FIXME: figure out how to add user generated libraries.
if (kspTask !is KspTaskNative) {
kotlinCompilation.output.classesDirs.from(classOutputDir)
}
}
val kspTaskProvider = when (kotlinCompileTask) {
is AbstractKotlinCompile<*> -> {
val kspTaskClass = when (kotlinCompileTask) {
is KotlinCompile -> KspTaskJvm::class.java
is Kotlin2JsCompile -> KspTaskJS::class.java
is KotlinCompileCommon -> KspTaskMetadata::class.java
else -> return project.provider { emptyList() }
}
val isIncremental = project.findProperty("ksp.incremental")?.toString()?.toBoolean() ?: true
project.tasks.register(kspTaskName, kspTaskClass) { kspTask ->
configureAsKspTask(kspTask, isIncremental)
configureAsAbstractCompile(kspTask)
kspTask.classpath = kotlinCompileTask.project.files(Callable { kotlinCompileTask.classpath })
kspTask.configureCompilation(
kotlinCompilation as KotlinCompilationData<*>,
kotlinCompileTask,
)
}
}
is KotlinNativeCompile -> {
val kspTaskClass = KspTaskNative::class.java
val pluginConfigurationName =
(kotlinCompileTask.compilation as AbstractKotlinNativeCompilation).pluginConfigurationName
project.configurations.getByName(pluginConfigurationName).dependencies.add(
project.dependencies.create(apiArtifact)
)
project.tasks.register(kspTaskName, kspTaskClass, kotlinCompileTask.compilation).apply {
configure { kspTask ->
kspTask.onlyIf {
kotlinCompileTask.compilation.konanTarget.enabledOnCurrentHost
}
configureAsKspTask(kspTask, false)
configureAsAbstractCompile(kspTask)
// KotlinNativeCompile computes -Xplugin=... from compilerPluginClasspath.
kspTask.compilerPluginClasspath = project.configurations.getByName(pluginConfigurationName)
kspTask.commonSources.from(kotlinCompileTask.commonSources)
}
}
}
else -> return project.provider { emptyList() }
}
kotlinCompileProvider.configure { kotlinCompile ->
kotlinCompile.dependsOn(kspTaskProvider)
kotlinCompile.source(kotlinOutputDir, javaOutputDir)
when (kotlinCompile) {
is AbstractKotlinCompile<*> -> kotlinCompile.classpath += project.files(classOutputDir)
// is KotlinNativeCompile -> TODO: support binary generation?
}
}
val processResourcesTaskName =
(kotlinCompilation as? KotlinCompilationWithResources)?.processResourcesTaskName ?: "processResources"
project.locateTask<ProcessResources>(processResourcesTaskName)?.let { provider ->
provider.configure { resourcesTask ->
resourcesTask.dependsOn(kspTaskProvider)
resourcesTask.from(resourceOutputDir)
}
}
if (kotlinCompilation is KotlinJvmAndroidCompilation) {
androidIntegration.registerGeneratedJavaSources(
project = project,
kotlinCompilation = kotlinCompilation,
kspTaskProvider = kspTaskProvider as TaskProvider<KspTaskJvm>,
javaOutputDir = javaOutputDir,
classOutputDir = classOutputDir,
resourcesOutputDir = project.files(resourceOutputDir)
)
}
return project.provider { emptyList() }
}
override fun getCompilerPluginId() = KSP_PLUGIN_ID
override fun getPluginArtifact(): SubpluginArtifact =
SubpluginArtifact(
groupId = "com.google.devtools.ksp",
artifactId = KSP_ARTIFACT_NAME,
version = javaClass.`package`.implementationVersion
)
override fun getPluginArtifactForNative(): SubpluginArtifact? =
SubpluginArtifact(
groupId = "com.google.devtools.ksp",
artifactId = KSP_ARTIFACT_NAME_NATIVE,
version = javaClass.`package`.implementationVersion
)
val apiArtifact = "com.google.devtools.ksp:symbol-processing-api:${javaClass.`package`.implementationVersion}"
}
private val artifactType = Attribute.of("artifactType", String::class.java)
// Copied from kotlin-gradle-plugin, because they are internal.
internal inline fun <reified T : Task> Project.locateTask(name: String): TaskProvider<T>? =
try {
tasks.withType(T::class.java).named(name)
} catch (e: UnknownTaskException) {
null
}
// Copied from kotlin-gradle-plugin, because they are internal.
internal fun findJavaTaskForKotlinCompilation(compilation: KotlinCompilation<*>): TaskProvider<out JavaCompile>? =
when (compilation) {
is KotlinJvmAndroidCompilation -> compilation.compileJavaTaskProvider
is KotlinWithJavaCompilation -> compilation.compileJavaTaskProvider
is KotlinJvmCompilation -> compilation.compileJavaTaskProvider // may be null for Kotlin-only JVM target in MPP
else -> null
}
interface KspTask : Task {
@get:Internal
val options: ListProperty<SubpluginOption>
@get:OutputDirectory
var destination: File
@get:Optional
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputFiles
val overridePluginClasspath: Property<FileCollection>
@get:Input
var blockOtherCompilerPlugins: Boolean
@get:Input
val apOptions: MapProperty<String, String>
// @PathSensitive and @Classpath doesn't seem working together. Effectively, we are forced to choose between
// 1. remote cache, or
// 2. detecting trivial changes in processors.
// Only processor authors need 2. so let's favor 1. for broader audience.
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputFiles
val processorClasspath: ConfigurableFileCollection
/**
* Output directory that contains caches necessary to support incremental annotation processing.
*/
@get:LocalState
val kspCacheDir: DirectoryProperty
@get:Input
var isKspIncremental: Boolean
fun configureCompilation(
kotlinCompilation: KotlinCompilationData<*>,
kotlinCompile: AbstractKotlinCompile<*>,
)
}
@CacheableTask
abstract class KspTaskJvm : KotlinCompile(KotlinJvmOptionsImpl()), KspTask {
@get:PathSensitive(PathSensitivity.NONE)
@get:Optional
@get:InputFiles
abstract val classpathStructure: ConfigurableFileCollection
@get:Input
var isIntermoduleIncremental: Boolean = false
override fun configureCompilation(
kotlinCompilation: KotlinCompilationData<*>,
kotlinCompile: AbstractKotlinCompile<*>,
) {
Configurator<KspTaskJvm>(kotlinCompilation).configure(this)
kotlinCompile as KotlinCompile
val providerFactory = kotlinCompile.project.providers
compileKotlinArgumentsContributor.set(
providerFactory.provider {
kotlinCompile.compilerArgumentsContributor
}
)
isIntermoduleIncremental =
(project.findProperty("ksp.incremental.intermodule")?.toString()?.toBoolean() ?: true) &&
isKspIncremental
if (isIntermoduleIncremental) {
val classStructureIfIncremental = project.configurations.detachedConfiguration(
project.dependencies.create(project.files(project.provider { kotlinCompile.classpath }))
)
maybeRegisterTransform(project)
classpathStructure.from(
classStructureIfIncremental.incoming.artifactView { viewConfig ->
viewConfig.attributes.attribute(artifactType, CLASS_STRUCTURE_ARTIFACT_TYPE)
}.files
).disallowChanges()
classpathSnapshotProperties.useClasspathSnapshot.value(true).disallowChanges()
} else {
classpathSnapshotProperties.useClasspathSnapshot.value(false).disallowChanges()
}
}
private fun maybeRegisterTransform(project: Project) {
// Use the same flag with KAPT, so as to share the same transformation in case KAPT and KSP are both enabled.
if (!project.extensions.extraProperties.has("KaptStructureTransformAdded")) {
val transformActionClass =
if (GradleVersion.current() >= GradleVersion.version("5.4"))
StructureTransformAction::class.java
else
StructureTransformLegacyAction::class.java
project.dependencies.registerTransform(transformActionClass) { transformSpec ->
transformSpec.from.attribute(artifactType, "jar")
transformSpec.to.attribute(artifactType, CLASS_STRUCTURE_ARTIFACT_TYPE)
}
project.dependencies.registerTransform(transformActionClass) { transformSpec ->
transformSpec.from.attribute(artifactType, "directory")
transformSpec.to.attribute(artifactType, CLASS_STRUCTURE_ARTIFACT_TYPE)
}
project.extensions.extraProperties["KaptStructureTransformAdded"] = true
}
}
// Reuse Kapt's infrastructure to compute affected names in classpath.
// This is adapted from KaptTask.findClasspathChanges.
private fun findClasspathChanges(
changes: ChangedFiles,
): KaptClasspathChanges {
val cacheDir = kspCacheDir.asFile.get()
cacheDir.mkdirs()
val allDataFiles = classpathStructure.files
val changedFiles = (changes as? ChangedFiles.Known)?.let { it.modified + it.removed }?.toSet() ?: allDataFiles
val loadedPrevious = ClasspathSnapshot.ClasspathSnapshotFactory.loadFrom(cacheDir)
val previousAndCurrentDataFiles = lazy { loadedPrevious.getAllDataFiles() + allDataFiles }
val allChangesRecognized = changedFiles.all {
val extension = it.extension
if (extension.isEmpty() || extension == "kt" || extension == "java" || extension == "jar" ||
extension == "class"
) {
return@all true
}
// if not a directory, Java source file, jar, or class, it has to be a structure file, in order to understand changes
it in previousAndCurrentDataFiles.value
}
val previousSnapshot = if (allChangesRecognized) {
loadedPrevious
} else {
ClasspathSnapshot.ClasspathSnapshotFactory.getEmptySnapshot()
}
val currentSnapshot =
ClasspathSnapshot.ClasspathSnapshotFactory.createCurrent(
cacheDir,
classpath.files.toList(),
processorClasspath.files.toList(),
allDataFiles
)
val classpathChanges = currentSnapshot.diff(previousSnapshot, changedFiles)
if (classpathChanges is KaptClasspathChanges.Unknown || changes is ChangedFiles.Unknown) {
clearIncCache()
cacheDir.mkdirs()
}
currentSnapshot.writeToCache()
return classpathChanges
}
@get:Internal
internal abstract val compileKotlinArgumentsContributor:
Property<CompilerArgumentsContributor<K2JVMCompilerArguments>>
init {
// kotlinc's incremental compilation isn't compatible with symbol processing in a few ways:
// * It doesn't consider private / internal changes when computing dirty sets.
// * It compiles iteratively; Sources can be compiled in different rounds.
incremental = false
}
override fun setupCompilerArgs(
args: K2JVMCompilerArguments,
defaultsOnly: Boolean,
ignoreClasspathResolutionErrors: Boolean,
) {
// Start with / copy from kotlinCompile.
compileKotlinArgumentsContributor.get().contributeArguments(
args,
compilerArgumentsConfigurationFlags(
defaultsOnly,
ignoreClasspathResolutionErrors
)
)
if (blockOtherCompilerPlugins) {
args.blockOtherPlugins(overridePluginClasspath.get())
}
args.addPluginOptions(options.get())
args.destinationAsFile = destination
args.allowNoSourceFiles = true
}
// Overrding an internal function is hacky.
// TODO: Ask upstream to open it.
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "EXPOSED_PARAMETER_TYPE")
fun `callCompilerAsync$kotlin_gradle_plugin`(
args: K2JVMCompilerArguments,
sourceRoots: SourceRoots,
changedFiles: ChangedFiles,
) {
if (isKspIncremental) {
if (isIntermoduleIncremental) {
// findClasspathChanges may clear caches, if there are
// 1. unknown changes, or
// 2. changes in annotation processors.
val classpathChanges = findClasspathChanges(changedFiles)
args.addChangedClasses(classpathChanges)
} else {
if (changedFiles.hasNonSourceChange()) {
clearIncCache()
}
}
} else {
clearIncCache()
}
args.addChangedFiles(changedFiles)
super.callCompilerAsync(args, sourceRoots, changedFiles)
}
override fun skipCondition(): Boolean = false
}
@CacheableTask
abstract class KspTaskJS @Inject constructor(
objectFactory: ObjectFactory,
) : Kotlin2JsCompile(KotlinJsOptionsImpl(), objectFactory), KspTask {
override fun configureCompilation(
kotlinCompilation: KotlinCompilationData<*>,
kotlinCompile: AbstractKotlinCompile<*>,
) {
Configurator<KspTaskJS>(kotlinCompilation).configure(this)
kotlinCompile as Kotlin2JsCompile
val providerFactory = kotlinCompile.project.providers
compileKotlinArgumentsContributor.set(
providerFactory.provider {
kotlinCompile.abstractKotlinCompileArgumentsContributor
}
)
}
@get:Internal
internal abstract val compileKotlinArgumentsContributor:
Property<CompilerArgumentsContributor<K2JSCompilerArguments>>
init {
// kotlinc's incremental compilation isn't compatible with symbol processing in a few ways:
// * It doesn't consider private / internal changes when computing dirty sets.
// * It compiles iteratively; Sources can be compiled in different rounds.
incremental = false
}
override fun setupCompilerArgs(
args: K2JSCompilerArguments,
defaultsOnly: Boolean,
ignoreClasspathResolutionErrors: Boolean,
) {
// Start with / copy from kotlinCompile.
args.fillDefaultValues()
compileKotlinArgumentsContributor.get().contributeArguments(
args,
compilerArgumentsConfigurationFlags(
defaultsOnly,
ignoreClasspathResolutionErrors
)
)
if (blockOtherCompilerPlugins) {
args.blockOtherPlugins(overridePluginClasspath.get())
}
args.addPluginOptions(options.get())
args.outputFile = File(destination, "dummyOutput.js").canonicalPath
}
// Overrding an internal function is hacky.
// TODO: Ask upstream to open it.
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "EXPOSED_PARAMETER_TYPE")
fun `callCompilerAsync$kotlin_gradle_plugin`(
args: K2JSCompilerArguments,
sourceRoots: SourceRoots,
changedFiles: ChangedFiles,
) {
if (!isKspIncremental || changedFiles.hasNonSourceChange()) {
clearIncCache()
} else {
args.addChangedFiles(changedFiles)
}
super.callCompilerAsync(args, sourceRoots, changedFiles)
}
}
abstract class KspTaskMetadata : KotlinCompileCommon(KotlinMultiplatformCommonOptionsImpl()), KspTask {
override fun configureCompilation(
kotlinCompilation: KotlinCompilationData<*>,
kotlinCompile: AbstractKotlinCompile<*>,
) {
Configurator<KspTaskMetadata>(kotlinCompilation).configure(this)
kotlinCompile as KotlinCompileCommon
val providerFactory = kotlinCompile.project.providers
compileKotlinArgumentsContributor.set(
providerFactory.provider {
kotlinCompile.abstractKotlinCompileArgumentsContributor
}
)
}
@get:Internal
internal abstract val compileKotlinArgumentsContributor:
Property<CompilerArgumentsContributor<K2MetadataCompilerArguments>>
init {
// kotlinc's incremental compilation isn't compatible with symbol processing in a few ways:
// * It doesn't consider private / internal changes when computing dirty sets.
// * It compiles iteratively; Sources can be compiled in different rounds.
incremental = false
}
override fun setupCompilerArgs(
args: K2MetadataCompilerArguments,
defaultsOnly: Boolean,
ignoreClasspathResolutionErrors: Boolean,
) {
// Start with / copy from kotlinCompile.
args.apply { fillDefaultValues() }
compileKotlinArgumentsContributor.get().contributeArguments(
args,
compilerArgumentsConfigurationFlags(
defaultsOnly,
ignoreClasspathResolutionErrors
)
)
if (blockOtherCompilerPlugins) {
args.blockOtherPlugins(overridePluginClasspath.get())
}
args.addPluginOptions(options.get())
args.destination = destination.canonicalPath
}
// Overrding an internal function is hacky.
// TODO: Ask upstream to open it.
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "EXPOSED_PARAMETER_TYPE")
fun `callCompilerAsync$kotlin_gradle_plugin`(
args: K2MetadataCompilerArguments,
sourceRoots: SourceRoots,
changedFiles: ChangedFiles,
) {
if (!isKspIncremental || changedFiles.hasNonSourceChange()) {
clearIncCache()
} else {
args.addChangedFiles(changedFiles)
}
super.callCompilerAsync(args, sourceRoots, changedFiles)
}
}
@CacheableTask
abstract class KspTaskNative @Inject constructor(
injected: KotlinNativeCompilationData<*>,
) : KotlinNativeCompile(injected), KspTask {
override fun buildCompilerArgs(): List<String> {
val kspOptions = options.get().flatMap { listOf("-P", it.toArg()) }
return super.buildCompilerArgs() + kspOptions
}
override fun buildCommonArgs(defaultsOnly: Boolean): List<String> {
if (blockOtherCompilerPlugins)
compilerPluginClasspath = overridePluginClasspath.get()
return super.buildCommonArgs(defaultsOnly)
}
override fun configureCompilation(
kotlinCompilation: KotlinCompilationData<*>,
kotlinCompile: AbstractKotlinCompile<*>,
) = Unit
// KotlinNativeCompile doesn't support Gradle incremental compilation. Therefore, there is no information about
// new / changed / removed files.
// Long term solution: contribute to upstream to support incremental compilation.
// Short term workaround: declare a @TaskAction function and call super.compile().
}
// This forces rebuild.
private fun KspTask.clearIncCache() {
kspCacheDir.get().asFile.deleteRecursively()
}
private fun ChangedFiles.hasNonSourceChange(): Boolean {
if (this !is ChangedFiles.Known)
return true
return !(this.modified + this.removed).all {
it.isKotlinFile(listOf("kt")) || it.isJavaFile()
}
}
fun CommonCompilerArguments.addChangedClasses(changed: KaptClasspathChanges) {
if (changed is KaptClasspathChanges.Known) {
changed.names.map { it.replace('/', '.').replace('$', '.') }.ifNotEmpty {
addPluginOptions(listOf(SubpluginOption("changedClasses", joinToString(":"))))
}
}
}
fun SubpluginOption.toArg() = "plugin:${KspGradleSubplugin.KSP_PLUGIN_ID}:$key=$value"
fun CommonCompilerArguments.addPluginOptions(options: List<SubpluginOption>) {
pluginOptions = (options.map { it.toArg() } + pluginOptions!!).toTypedArray()
}
fun CommonCompilerArguments.addChangedFiles(changedFiles: ChangedFiles) {
if (changedFiles is ChangedFiles.Known) {
val options = mutableListOf<SubpluginOption>()
changedFiles.modified.filter { it.isKotlinFile(listOf("kt")) || it.isJavaFile() }.ifNotEmpty {
options += SubpluginOption("knownModified", map { it.path }.joinToString(File.pathSeparator))
}
changedFiles.removed.filter { it.isKotlinFile(listOf("kt")) || it.isJavaFile() }.ifNotEmpty {
options += SubpluginOption("knownRemoved", map { it.path }.joinToString(File.pathSeparator))
}
options.ifNotEmpty { addPluginOptions(this) }
}
}
private fun CommonCompilerArguments.blockOtherPlugins(kspPluginClasspath: FileCollection) {
pluginClasspaths = kspPluginClasspath.map { it.canonicalPath }.toTypedArray()
pluginOptions = arrayOf()
}
// TODO: Move into dumpArgs after the compiler supports local function in inline functions.
private inline fun <reified T : CommonCompilerArguments> T.toPair(property: KProperty1<T, *>): Pair<String, String> {
@Suppress("UNCHECKED_CAST")
val value = (property as KProperty1<T, *>).get(this)
return property.name to if (value is Array<*>)
value.asList().toString()
else
value.toString()
}
@Suppress("unused")
internal inline fun <reified T : CommonCompilerArguments> dumpArgs(args: T): Map<String, String> {
@Suppress("UNCHECKED_CAST")
val argumentProperties =
args::class.members.mapNotNull { member ->
(member as? KProperty1<T, *>)?.takeIf { it.annotations.any { ann -> ann is Argument } }
}
return argumentProperties.associate(args::toPair).toSortedMap()
}