1 /*
<lambda>null2 * Copyright 2020 Google LLC
3 * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package com.google.devtools.ksp.gradle
19
20 import com.google.devtools.ksp.gradle.model.builder.KspModelBuilder
21 import org.gradle.api.Action
22 import org.gradle.api.Project
23 import org.gradle.api.Task
24 import org.gradle.api.UnknownTaskException
25 import org.gradle.api.artifacts.Configuration
26 import org.gradle.api.attributes.Attribute
27 import org.gradle.api.file.ConfigurableFileCollection
28 import org.gradle.api.file.FileCollection
29 import org.gradle.api.provider.ListProperty
30 import org.gradle.api.provider.Provider
31 import org.gradle.api.tasks.TaskProvider
32 import org.gradle.api.tasks.compile.JavaCompile
33 import org.gradle.language.jvm.tasks.ProcessResources
34 import org.gradle.process.CommandLineArgumentProvider
35 import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry
36 import org.gradle.util.GradleVersion
37 import org.jetbrains.kotlin.config.ApiVersion
38 import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
39 import org.jetbrains.kotlin.gradle.internal.kapt.incremental.CLASS_STRUCTURE_ARTIFACT_TYPE
40 import org.jetbrains.kotlin.gradle.internal.kapt.incremental.ClasspathSnapshot
41 import org.jetbrains.kotlin.gradle.internal.kapt.incremental.KaptClasspathChanges
42 import org.jetbrains.kotlin.gradle.internal.kapt.incremental.StructureTransformAction
43 import org.jetbrains.kotlin.gradle.internal.kapt.incremental.StructureTransformLegacyAction
44 import org.jetbrains.kotlin.gradle.plugin.CompilerPluginConfig
45 import org.jetbrains.kotlin.gradle.plugin.FilesSubpluginOption
46 import org.jetbrains.kotlin.gradle.plugin.InternalSubpluginOption
47 import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
48 import org.jetbrains.kotlin.gradle.plugin.KotlinCompilationWithResources
49 import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin
50 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
51 import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
52 import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact
53 import org.jetbrains.kotlin.gradle.plugin.SubpluginOption
54 import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
55 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinCommonCompilation
56 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation
57 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmCompilation
58 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinWithJavaCompilation
59 import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.KotlinCompilationData
60 import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
61 import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool
62 import org.jetbrains.kotlin.gradle.tasks.BaseKotlinCompile
63 import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
64 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
65 import org.jetbrains.kotlin.gradle.tasks.KotlinCompileCommon
66 import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile
67 import org.jetbrains.kotlin.incremental.ChangedFiles
68 import org.jetbrains.kotlin.incremental.isJavaFile
69 import org.jetbrains.kotlin.incremental.isKotlinFile
70 import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly
71 import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty
72 import java.io.File
73 import java.util.concurrent.Callable
74 import javax.inject.Inject
75
76 class KspGradleSubplugin @Inject internal constructor(private val registry: ToolingModelBuilderRegistry) :
77 KotlinCompilerPluginSupportPlugin {
78 companion object {
79 const val KSP_PLUGIN_ID = "com.google.devtools.ksp.symbol-processing"
80 const val KSP_API_ID = "symbol-processing-api"
81 const val KSP_COMPILER_PLUGIN_ID = "symbol-processing"
82 const val KSP_COMPILER_PLUGIN_ID_NON_EMBEDDABLE = "symbol-processing-cmdline"
83 const val KSP_GROUP_ID = "com.google.devtools.ksp"
84 const val KSP_PLUGIN_CLASSPATH_CONFIGURATION_NAME = "kspPluginClasspath"
85 const val KSP_PLUGIN_CLASSPATH_CONFIGURATION_NAME_NON_EMBEDDABLE = "kspPluginClasspathNonEmbeddable"
86
87 @JvmStatic
88 fun getKspOutputDir(project: Project, sourceSetName: String, target: String) =
89 File(project.project.buildDir, "generated/ksp/$target/$sourceSetName")
90
91 @JvmStatic
92 fun getKspClassOutputDir(project: Project, sourceSetName: String, target: String) =
93 File(getKspOutputDir(project, sourceSetName, target), "classes")
94
95 @JvmStatic
96 fun getKspJavaOutputDir(project: Project, sourceSetName: String, target: String) =
97 File(getKspOutputDir(project, sourceSetName, target), "java")
98
99 @JvmStatic
100 fun getKspKotlinOutputDir(project: Project, sourceSetName: String, target: String) =
101 File(getKspOutputDir(project, sourceSetName, target), "kotlin")
102
103 @JvmStatic
104 fun getKspResourceOutputDir(project: Project, sourceSetName: String, target: String) =
105 File(getKspOutputDir(project, sourceSetName, target), "resources")
106
107 @JvmStatic
108 fun getKspCachesDir(project: Project, sourceSetName: String, target: String) =
109 File(project.project.buildDir, "kspCaches/$target/$sourceSetName")
110
111 @JvmStatic
112 private fun getSubpluginOptions(
113 project: Project,
114 kspExtension: KspExtension,
115 classpath: Configuration,
116 sourceSetName: String,
117 target: String,
118 isIncremental: Boolean,
119 allWarningsAsErrors: Boolean,
120 commandLineArgumentProviders: ListProperty<CommandLineArgumentProvider>,
121 commonSources: List<File>,
122 ): List<SubpluginOption> {
123 val options = mutableListOf<SubpluginOption>()
124 options +=
125 InternalSubpluginOption("classOutputDir", getKspClassOutputDir(project, sourceSetName, target).path)
126 options +=
127 InternalSubpluginOption("javaOutputDir", getKspJavaOutputDir(project, sourceSetName, target).path)
128 options +=
129 InternalSubpluginOption("kotlinOutputDir", getKspKotlinOutputDir(project, sourceSetName, target).path)
130 options += InternalSubpluginOption(
131 "resourceOutputDir",
132 getKspResourceOutputDir(project, sourceSetName, target).path
133 )
134 options += InternalSubpluginOption("cachesDir", getKspCachesDir(project, sourceSetName, target).path)
135 options += InternalSubpluginOption("kspOutputDir", getKspOutputDir(project, sourceSetName, target).path)
136 options += SubpluginOption("incremental", isIncremental.toString())
137 options += SubpluginOption(
138 "incrementalLog",
139 project.findProperty("ksp.incremental.log")?.toString() ?: "false"
140 )
141 options += InternalSubpluginOption("projectBaseDir", project.project.projectDir.canonicalPath)
142 options += SubpluginOption("allWarningsAsErrors", allWarningsAsErrors.toString())
143 options += FilesSubpluginOption("apclasspath", classpath.toList())
144 // Turn this on by default to work KT-30172 around. It is off by default in the compiler plugin.
145 options += SubpluginOption(
146 "returnOkOnError",
147 project.findProperty("ksp.return.ok.on.error")?.toString() ?: "true"
148 )
149 commonSources.ifNotEmpty {
150 options += FilesSubpluginOption("commonSources", this)
151 }
152
153 kspExtension.apOptions.forEach {
154 options += SubpluginOption("apoption", "${it.key}=${it.value}")
155 }
156 options += SubpluginOption(
157 "excludedProcessors",
158 kspExtension.excludedProcessors.joinToString(":")
159 )
160 options += SubpluginOption(
161 "mapAnnotationArgumentsInJava",
162 project.findProperty("ksp.map.annotation.arguments.in.java")?.toString() ?: "false"
163 )
164 commandLineArgumentProviders.get().forEach {
165 it.asArguments().forEach { argument ->
166 if (!argument.matches(Regex("\\S+=\\S+"))) {
167 throw IllegalArgumentException("KSP apoption does not match \\S+=\\S+: $argument")
168 }
169 options += SubpluginOption("apoption", argument)
170 }
171 }
172 return options
173 }
174 }
175
176 private lateinit var kspConfigurations: KspConfigurations
177
178 override fun apply(target: Project) {
179 target.extensions.create("ksp", KspExtension::class.java)
180 kspConfigurations = KspConfigurations(target)
181 registry.register(KspModelBuilder())
182 }
183
184 override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean {
185 val project = kotlinCompilation.target.project
186 val kspVersion = ApiVersion.parse(KSP_KOTLIN_BASE_VERSION)!!
187 val kotlinVersion = ApiVersion.parse(project.getKotlinPluginVersion())!!
188
189 // Check version and show warning by default.
190 val noVersionCheck = project.findProperty("ksp.version.check")?.toString()?.toBoolean() == false
191 if (!noVersionCheck) {
192 if (kspVersion < kotlinVersion) {
193 project.logger.warn(
194 "ksp-$KSP_VERSION is too old for kotlin-$kotlinVersion. " +
195 "Please upgrade ksp or downgrade kotlin-gradle-plugin to $KSP_KOTLIN_BASE_VERSION."
196 )
197 }
198 if (kspVersion > kotlinVersion) {
199 project.logger.warn(
200 "ksp-$KSP_VERSION is too new for kotlin-$kotlinVersion. " +
201 "Please upgrade kotlin-gradle-plugin to $KSP_KOTLIN_BASE_VERSION."
202 )
203 }
204 }
205
206 return true
207 }
208
209 // TODO: to be future proof, protect with `synchronized`
210 // Map from default input source set to output source set
211 private val sourceSetMap: MutableMap<KotlinSourceSet, KotlinSourceSet> = mutableMapOf()
212
213 override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider<List<SubpluginOption>> {
214 val project = kotlinCompilation.target.project
215 val kotlinCompileProvider: TaskProvider<AbstractKotlinCompileTool<*>> =
216 project.locateTask(kotlinCompilation.compileKotlinTaskName) ?: return project.provider { emptyList() }
217 val kspExtension = project.extensions.getByType(KspExtension::class.java)
218 val kspConfigurations = kspConfigurations.find(kotlinCompilation)
219 val nonEmptyKspConfigurations = kspConfigurations.filter { it.allDependencies.isNotEmpty() }
220 if (nonEmptyKspConfigurations.isEmpty()) {
221 return project.provider { emptyList() }
222 }
223 if (kotlinCompileProvider.name == "compileKotlinMetadata") {
224 return project.provider { emptyList() }
225 }
226
227 val target = kotlinCompilation.target.name
228 val sourceSetName = kotlinCompilation.defaultSourceSet.name
229 val classOutputDir = getKspClassOutputDir(project, sourceSetName, target)
230 val javaOutputDir = getKspJavaOutputDir(project, sourceSetName, target)
231 val kotlinOutputDir = getKspKotlinOutputDir(project, sourceSetName, target)
232 val resourceOutputDir = getKspResourceOutputDir(project, sourceSetName, target)
233 val kspOutputDir = getKspOutputDir(project, sourceSetName, target)
234
235 val kspClasspathCfg = project.configurations.maybeCreate(KSP_PLUGIN_CLASSPATH_CONFIGURATION_NAME)
236 project.dependencies.add(
237 KSP_PLUGIN_CLASSPATH_CONFIGURATION_NAME,
238 "$KSP_GROUP_ID:$KSP_API_ID:$KSP_VERSION"
239 )
240 project.dependencies.add(
241 KSP_PLUGIN_CLASSPATH_CONFIGURATION_NAME,
242 "$KSP_GROUP_ID:$KSP_COMPILER_PLUGIN_ID:$KSP_VERSION"
243 )
244
245 val kspClasspathCfgNonEmbeddable = project.configurations.maybeCreate(
246 KSP_PLUGIN_CLASSPATH_CONFIGURATION_NAME_NON_EMBEDDABLE
247 )
248 project.dependencies.add(
249 KSP_PLUGIN_CLASSPATH_CONFIGURATION_NAME_NON_EMBEDDABLE,
250 "$KSP_GROUP_ID:$KSP_API_ID:$KSP_VERSION"
251 )
252 project.dependencies.add(
253 KSP_PLUGIN_CLASSPATH_CONFIGURATION_NAME_NON_EMBEDDABLE,
254 "$KSP_GROUP_ID:$KSP_COMPILER_PLUGIN_ID_NON_EMBEDDABLE:$KSP_VERSION"
255 )
256
257 findJavaTaskForKotlinCompilation(kotlinCompilation)?.configure { javaCompile ->
258 val generatedJavaSources = javaCompile.project.fileTree(javaOutputDir)
259 generatedJavaSources.include("**/*.java")
260 javaCompile.source(generatedJavaSources)
261 javaCompile.classpath += project.files(classOutputDir)
262 }
263
264 val processingModel = project.findProperty("ksp.experimental.processing.model")?.toString() ?: "traditional"
265
266 assert(kotlinCompileProvider.name.startsWith("compile"))
267 val kspTaskName = kotlinCompileProvider.name.replaceFirst("compile", "ksp")
268
269 val kspGeneratedSourceSet =
270 project.kotlinExtension.sourceSets.create("generatedBy" + kspTaskName.capitalizeAsciiOnly())
271 sourceSetMap.put(kotlinCompilation.defaultSourceSet, kspGeneratedSourceSet)
272
273 val processorClasspath = project.configurations.maybeCreate("${kspTaskName}ProcessorClasspath")
274 .extendsFrom(*nonEmptyKspConfigurations.toTypedArray())
275 fun configureAsKspTask(kspTask: KspTask, isIncremental: Boolean) {
276 // depends on the processor; if the processor changes, it needs to be reprocessed.
277 kspTask.dependsOn(processorClasspath.buildDependencies)
278 kspTask.commandLineArgumentProviders.addAll(kspExtension.commandLineArgumentProviders)
279
280 val commonSources: List<File> = when (processingModel) {
281 "hierarchical" -> {
282 fun unclaimedDeps(roots: Set<KotlinSourceSet>): Set<KotlinSourceSet> {
283 val unclaimedParents =
284 roots.flatMap { it.dependsOn }.filterNot { it in sourceSetMap }.toSet()
285 return if (unclaimedParents.isEmpty()) {
286 unclaimedParents
287 } else {
288 unclaimedParents + unclaimedDeps(unclaimedParents)
289 }
290 }
291 // Source sets that are not claimed by other compilations.
292 // I.e., those that should be processed by this compilation.
293 val unclaimed =
294 kotlinCompilation.kotlinSourceSets + unclaimedDeps(kotlinCompilation.kotlinSourceSets)
295 val commonSourceSets = kotlinCompilation.allKotlinSourceSets - unclaimed
296 commonSourceSets.flatMap { it.kotlin.files }
297 }
298 else -> emptyList()
299 }
300
301 kspTask.options.addAll(
302 kspTask.project.provider {
303 getSubpluginOptions(
304 project,
305 kspExtension,
306 processorClasspath,
307 sourceSetName,
308 target,
309 isIncremental,
310 kspExtension.allWarningsAsErrors,
311 kspTask.commandLineArgumentProviders,
312 commonSources,
313 )
314 }
315 )
316 kspTask.inputs.property("apOptions", kspExtension.arguments)
317 }
318
319 fun configureAsAbstractKotlinCompileTool(kspTask: AbstractKotlinCompileTool<*>) {
320 when (kspTask) {
321 is Kotlin2JsCompile -> {
322 kspTask.outputFileProperty.value(
323 File(kspOutputDir, "dummyOutput.js")
324 )
325 }
326 else -> kspTask.destinationDirectory.set(kspOutputDir)
327 }
328 kspTask.outputs.dirs(
329 kotlinOutputDir,
330 javaOutputDir,
331 classOutputDir,
332 resourceOutputDir
333 )
334
335 val kotlinCompileTask = kotlinCompileProvider.get()
336 if (kspExtension.allowSourcesFromOtherPlugins) {
337 fun FileCollection.nonSelfDeps(): List<Task> =
338 buildDependencies.getDependencies(null).filterNot {
339 it.name == kspTaskName
340 }
341
342 fun setSource(source: FileCollection) {
343 // kspTask.setSource(source) would create circular dependency.
344 // Therefore we need to manually extract input deps, filter them, and tell kspTask.
345 kspTask.setSource(project.provider { source.files })
346 kspTask.dependsOn(project.provider { source.nonSelfDeps() })
347 }
348
349 setSource(kotlinCompileTask.sources - kspGeneratedSourceSet.kotlin)
350 if (kotlinCompileTask is KotlinCompile) {
351 setSource(kotlinCompileTask.javaSources - kspGeneratedSourceSet.kotlin)
352 }
353 } else {
354 kotlinCompilation.allKotlinSourceSets.filterNot { it == kspGeneratedSourceSet }.forEach { sourceSet ->
355 kspTask.setSource(sourceSet.kotlin)
356 }
357 if (kotlinCompilation is KotlinCommonCompilation) {
358 kspTask.setSource(kotlinCompilation.defaultSourceSet.kotlin)
359 }
360 val generated = when (processingModel) {
361 "hierarchical" -> {
362 // boundary parent source sets that are going to be compiled by other compilations
363 fun claimedParents(root: KotlinSourceSet): Set<KotlinSourceSet> {
364 val (claimed, unclaimed) = root.dependsOn.partition { it in sourceSetMap }
365 return claimed.toSet() + unclaimed.flatMap { claimedParents(it) }
366 }
367 kotlinCompilation.kotlinSourceSets.flatMap { claimedParents(it) }.map { sourceSetMap[it]!! }
368 }
369 else -> emptyList()
370 }
371 generated.forEach {
372 kspTask.setSource(it.kotlin)
373 }
374 }
375 kspTask.exclude { kspOutputDir.isParentOf(it.file) }
376
377 kspTask.libraries.setFrom(
378 kotlinCompileTask.project.files(
379 Callable {
380 kotlinCompileTask.libraries.filter {
381 !kspOutputDir.isParentOf(it)
382 }
383 }
384 )
385 )
386 // kotlinc's incremental compilation isn't compatible with symbol processing in a few ways:
387 // * It doesn't consider private / internal changes when computing dirty sets.
388 // * It compiles iteratively; Sources can be compiled in different rounds.
389 (kspTask as? AbstractKotlinCompile<*>)?.incremental = false
390 }
391
392 fun maybeBlockOtherPlugins(kspTask: BaseKotlinCompile) {
393 if (kspExtension.blockOtherCompilerPlugins) {
394 kspTask.pluginClasspath.setFrom(kspClasspathCfg)
395 kspTask.pluginOptions.set(emptyList())
396 }
397 }
398
399 fun configurePluginOptions(kspTask: BaseKotlinCompile) {
400 kspTask.pluginOptions.add(
401 project.provider {
402 CompilerPluginConfig().apply {
403 (kspTask as KspTask).options.get().forEach {
404 addPluginArgument(KSP_PLUGIN_ID, it)
405 }
406 }
407 }
408 )
409 }
410
411 val isIncremental = project.findProperty("ksp.incremental")?.toString()?.toBoolean() ?: true
412
413 // Create and configure KSP tasks.
414 val kspTaskProvider = when (kotlinCompilation.platformType) {
415 KotlinPlatformType.jvm, KotlinPlatformType.androidJvm -> {
416 kotlinCompilation as KotlinCompilationData<*>
417 KotlinFactories.registerKotlinJvmCompileTask(project, kspTaskName, kotlinCompilation).also {
418 it.configure { kspTask ->
419 val kotlinCompileTask = kotlinCompileProvider.get() as KotlinCompile
420 maybeBlockOtherPlugins(kspTask as BaseKotlinCompile)
421 configureAsKspTask(kspTask, isIncremental)
422 configureAsAbstractKotlinCompileTool(kspTask as AbstractKotlinCompileTool<*>)
423 configurePluginOptions(kspTask)
424 kspTask.compilerOptions.noJdk.value(kotlinCompileTask.compilerOptions.noJdk)
425 kspTask.compilerOptions.useK2.value(false)
426 kspTask.ownModuleName.value(kotlinCompileTask.ownModuleName.map { "$it-ksp" })
427 kspTask.moduleName.value(kotlinCompileTask.moduleName.get())
428 kspTask.destination.value(kspOutputDir)
429
430 val isIntermoduleIncremental =
431 (project.findProperty("ksp.incremental.intermodule")?.toString()?.toBoolean() ?: true) &&
432 isIncremental
433 val classStructureFiles = getClassStructureFiles(project, kspTask.libraries)
434 kspTask.incrementalChangesTransformers.add(
435 createIncrementalChangesTransformer(
436 isIncremental,
437 isIntermoduleIncremental,
438 getKspCachesDir(project, sourceSetName, target),
439 project.provider { classStructureFiles },
440 project.provider { kspTask.libraries },
441 project.provider { processorClasspath }
442 )
443 )
444 }
445 // Don't support binary generation for non-JVM platforms yet.
446 // FIXME: figure out how to add user generated libraries.
447 kotlinCompilation.output.classesDirs.from(classOutputDir)
448 }
449 }
450 KotlinPlatformType.js, KotlinPlatformType.wasm -> {
451 kotlinCompilation as KotlinCompilationData<*>
452 KotlinFactories.registerKotlinJSCompileTask(project, kspTaskName, kotlinCompilation).also {
453 it.configure { kspTask ->
454 val kotlinCompileTask = kotlinCompileProvider.get() as Kotlin2JsCompile
455 maybeBlockOtherPlugins(kspTask as BaseKotlinCompile)
456 configureAsKspTask(kspTask, isIncremental)
457 configureAsAbstractKotlinCompileTool(kspTask as AbstractKotlinCompileTool<*>)
458 configurePluginOptions(kspTask)
459 kspTask.compilerOptions.freeCompilerArgs
460 .value(kotlinCompileTask.compilerOptions.freeCompilerArgs)
461 kspTask.compilerOptions.useK2.value(false)
462 kspTask.moduleName.value(kotlinCompileTask.moduleName.map { "$it-ksp" })
463
464 kspTask.incrementalChangesTransformers.add(
465 createIncrementalChangesTransformer(
466 isIncremental,
467 false,
468 getKspCachesDir(project, sourceSetName, target),
469 project.provider { project.files() },
470 project.provider { project.files() },
471 project.provider { project.files() },
472 )
473 )
474 }
475 }
476 }
477 KotlinPlatformType.common -> {
478 kotlinCompilation as KotlinCompilationData<*>
479 KotlinFactories.registerKotlinMetadataCompileTask(project, kspTaskName, kotlinCompilation).also {
480 it.configure { kspTask ->
481 val kotlinCompileTask = kotlinCompileProvider.get() as KotlinCompileCommon
482 maybeBlockOtherPlugins(kspTask as BaseKotlinCompile)
483 configureAsKspTask(kspTask, isIncremental)
484 configureAsAbstractKotlinCompileTool(kspTask as AbstractKotlinCompileTool<*>)
485 configurePluginOptions(kspTask)
486 kspTask.compilerOptions.useK2.value(false)
487
488 kspTask.incrementalChangesTransformers.add(
489 createIncrementalChangesTransformer(
490 isIncremental,
491 false,
492 getKspCachesDir(project, sourceSetName, target),
493 project.provider { project.files() },
494 project.provider { project.files() },
495 project.provider { project.files() },
496 )
497 )
498 }
499 }
500 }
501 KotlinPlatformType.native -> {
502 KotlinFactories.registerKotlinNativeCompileTask(project, kspTaskName, kotlinCompilation).also {
503 it.configure { kspTask ->
504 val kotlinCompileTask = kotlinCompileProvider.get() as KotlinNativeCompile
505 configureAsKspTask(kspTask, false)
506 configureAsAbstractKotlinCompileTool(kspTask)
507
508 val useEmbeddable = project.findProperty("kotlin.native.useEmbeddableCompilerJar")
509 ?.toString()?.toBoolean() ?: true
510 val classpathCfg = if (useEmbeddable) {
511 kspClasspathCfg
512 } else {
513 kspClasspathCfgNonEmbeddable
514 }
515 // KotlinNativeCompile computes -Xplugin=... from compilerPluginClasspath.
516 if (kspExtension.blockOtherCompilerPlugins) {
517 kspTask.compilerPluginClasspath = classpathCfg
518 } else {
519 kspTask.compilerPluginClasspath =
520 classpathCfg + kotlinCompileTask.compilerPluginClasspath!!
521 kspTask.compilerPluginOptions.addPluginArgument(kotlinCompileTask.compilerPluginOptions)
522 }
523 kspTask.commonSources.from(kotlinCompileTask.commonSources)
524 val kspOptions = kspTask.options.get().flatMap { listOf("-P", it.toArg()) }
525 kspTask.compilerOptions.freeCompilerArgs.value(
526 kspOptions + kotlinCompileTask.compilerOptions.freeCompilerArgs.get()
527 )
528 kspTask.compilerOptions.useK2.value(false)
529 // Cannot use lambda; See below for details.
530 // https://docs.gradle.org/7.2/userguide/validation_problems.html#implementation_unknown
531 kspTask.doFirst(object : Action<Task> {
532 override fun execute(t: Task) {
533 kspOutputDir.deleteRecursively()
534 }
535 })
536 }
537 }
538 }
539 // No else; The cases should be exhaustive
540 }
541 kspGeneratedSourceSet.kotlin.srcDir(project.files(kotlinOutputDir, javaOutputDir).builtBy(kspTaskProvider))
542 kotlinCompilation.source(kspGeneratedSourceSet)
543 kotlinCompileProvider.configure { kotlinCompile ->
544 when (kotlinCompile) {
545 is AbstractKotlinCompile<*> -> kotlinCompile.libraries.from(project.files(classOutputDir))
546 // is KotlinNativeCompile -> TODO: support binary generation?
547 }
548 }
549
550 val processResourcesTaskName =
551 (kotlinCompilation as? KotlinCompilationWithResources)?.processResourcesTaskName ?: "processResources"
552 project.locateTask<ProcessResources>(processResourcesTaskName)?.let { provider ->
553 provider.configure { resourcesTask ->
554 resourcesTask.from(project.files(resourceOutputDir).builtBy(kspTaskProvider))
555 }
556 }
557 if (kotlinCompilation is KotlinJvmAndroidCompilation) {
558 AndroidPluginIntegration.registerGeneratedSources(
559 project = project,
560 kotlinCompilation = kotlinCompilation,
561 kspTaskProvider = kspTaskProvider as TaskProvider<KspTaskJvm>,
562 javaOutputDir = javaOutputDir,
563 kotlinOutputDir = kotlinOutputDir,
564 classOutputDir = classOutputDir,
565 resourcesOutputDir = project.files(resourceOutputDir)
566 )
567 }
568
569 return project.provider { emptyList() }
570 }
571
572 override fun getCompilerPluginId() = KSP_PLUGIN_ID
573 override fun getPluginArtifact(): SubpluginArtifact =
574 SubpluginArtifact(
575 groupId = "com.google.devtools.ksp",
576 artifactId = KSP_COMPILER_PLUGIN_ID,
577 version = KSP_VERSION
578 )
579
580 override fun getPluginArtifactForNative(): SubpluginArtifact? =
581 SubpluginArtifact(
582 groupId = "com.google.devtools.ksp",
583 artifactId = KSP_COMPILER_PLUGIN_ID_NON_EMBEDDABLE,
584 version = KSP_VERSION
585 )
586 }
587
588 // Copied from kotlin-gradle-plugin, because they are internal.
locateTasknull589 internal inline fun <reified T : Task> Project.locateTask(name: String): TaskProvider<T>? =
590 try {
591 tasks.withType(T::class.java).named(name)
592 } catch (e: UnknownTaskException) {
593 null
594 }
595
596 // Copied from kotlin-gradle-plugin, because they are internal.
findJavaTaskForKotlinCompilationnull597 internal fun findJavaTaskForKotlinCompilation(compilation: KotlinCompilation<*>): TaskProvider<out JavaCompile>? =
598 when (compilation) {
599 is KotlinJvmAndroidCompilation -> compilation.compileJavaTaskProvider
600 is KotlinWithJavaCompilation<*, *> -> compilation.compileJavaTaskProvider
601 is KotlinJvmCompilation -> compilation.compileJavaTaskProvider // may be null for Kotlin-only JVM target in MPP
602 else -> null
603 }
604
605 internal val artifactType = Attribute.of("artifactType", String::class.java)
606
maybeRegisterTransformnull607 internal fun maybeRegisterTransform(project: Project) {
608 // Use the same flag with KAPT, so as to share the same transformation in case KAPT and KSP are both enabled.
609 if (!project.extensions.extraProperties.has("KaptStructureTransformAdded")) {
610 val transformActionClass =
611 if (GradleVersion.current() >= GradleVersion.version("5.4"))
612 StructureTransformAction::class.java
613 else
614
615 StructureTransformLegacyAction::class.java
616 project.dependencies.registerTransform(transformActionClass) { transformSpec ->
617 transformSpec.from.attribute(artifactType, "jar")
618 transformSpec.to.attribute(artifactType, CLASS_STRUCTURE_ARTIFACT_TYPE)
619 }
620
621 project.dependencies.registerTransform(transformActionClass) { transformSpec ->
622 transformSpec.from.attribute(artifactType, "directory")
623 transformSpec.to.attribute(artifactType, CLASS_STRUCTURE_ARTIFACT_TYPE)
624 }
625
626 project.extensions.extraProperties["KaptStructureTransformAdded"] = true
627 }
628 }
629
getClassStructureFilesnull630 internal fun getClassStructureFiles(
631 project: Project,
632 libraries: ConfigurableFileCollection,
633 ): FileCollection {
634 maybeRegisterTransform(project)
635
636 val classStructureIfIncremental = project.configurations.detachedConfiguration(
637 project.dependencies.create(project.files(project.provider { libraries }))
638 )
639
640 return classStructureIfIncremental.incoming.artifactView { viewConfig ->
641 viewConfig.attributes.attribute(artifactType, CLASS_STRUCTURE_ARTIFACT_TYPE)
642 }.files
643 }
644
645 // Reuse Kapt's infrastructure to compute affected names in classpath.
646 // This is adapted from KaptTask.findClasspathChanges.
findClasspathChangesnull647 internal fun findClasspathChanges(
648 changes: ChangedFiles,
649 cacheDir: File,
650 allDataFiles: Set<File>,
651 libs: List<File>,
652 processorCP: List<File>,
653 ): KaptClasspathChanges {
654 cacheDir.mkdirs()
655
656 val changedFiles = (changes as? ChangedFiles.Known)?.let { it.modified + it.removed }?.toSet() ?: allDataFiles
657
658 val loadedPrevious = ClasspathSnapshot.ClasspathSnapshotFactory.loadFrom(cacheDir)
659 val previousAndCurrentDataFiles = lazy { loadedPrevious.getAllDataFiles() + allDataFiles }
660 val allChangesRecognized = changedFiles.all {
661 val extension = it.extension
662 if (extension.isEmpty() || extension == "kt" || extension == "java" || extension == "jar" ||
663 extension == "class"
664 ) {
665 return@all true
666 }
667 // if not a directory, Java source file, jar, or class, it has to be a structure file, in order to understand changes
668 it in previousAndCurrentDataFiles.value
669 }
670 val previousSnapshot = if (allChangesRecognized) {
671 loadedPrevious
672 } else {
673 ClasspathSnapshot.ClasspathSnapshotFactory.getEmptySnapshot()
674 }
675
676 val currentSnapshot =
677 ClasspathSnapshot.ClasspathSnapshotFactory.createCurrent(
678 cacheDir,
679 libs,
680 processorCP,
681 allDataFiles
682 )
683
684 val classpathChanges = currentSnapshot.diff(previousSnapshot, changedFiles)
685 if (classpathChanges is KaptClasspathChanges.Unknown || changes is ChangedFiles.Unknown) {
686 cacheDir.deleteRecursively()
687 cacheDir.mkdirs()
688 }
689 currentSnapshot.writeToCache()
690
691 return classpathChanges
692 }
693
hasNonSourceChangenull694 internal fun ChangedFiles.hasNonSourceChange(): Boolean {
695 if (this !is ChangedFiles.Known)
696 return true
697
698 return !(this.modified + this.removed).all {
699 it.isKotlinFile(listOf("kt")) || it.isJavaFile()
700 }
701 }
702
toSubpluginOptionsnull703 fun KaptClasspathChanges.toSubpluginOptions(): List<SubpluginOption> {
704 return if (this is KaptClasspathChanges.Known) {
705 this.names.map { it.replace('/', '.').replace('$', '.') }.ifNotEmpty {
706 listOf(SubpluginOption("changedClasses", joinToString(":")))
707 } ?: emptyList()
708 } else {
709 emptyList()
710 }
711 }
712
ChangedFilesnull713 fun ChangedFiles.toSubpluginOptions(): List<SubpluginOption> {
714 return if (this is ChangedFiles.Known) {
715 val options = mutableListOf<SubpluginOption>()
716 this.modified.filter { it.isKotlinFile(listOf("kt")) || it.isJavaFile() }.ifNotEmpty {
717 options += SubpluginOption("knownModified", map { it.path }.joinToString(File.pathSeparator))
718 }
719 this.removed.filter { it.isKotlinFile(listOf("kt")) || it.isJavaFile() }.ifNotEmpty {
720 options += SubpluginOption("knownRemoved", map { it.path }.joinToString(File.pathSeparator))
721 }
722 options
723 } else {
724 emptyList()
725 }
726 }
727
728 // Return a closure that captures required arguments only.
createIncrementalChangesTransformernull729 internal fun createIncrementalChangesTransformer(
730 isKspIncremental: Boolean,
731 isIntermoduleIncremental: Boolean,
732 cacheDir: File,
733 classpathStructure: Provider<FileCollection>,
734 libraries: Provider<FileCollection>,
735 processorCP: Provider<FileCollection>,
736 ): (ChangedFiles) -> List<SubpluginOption> = { changedFiles ->
737 val options = mutableListOf<SubpluginOption>()
738 if (isKspIncremental) {
739 if (isIntermoduleIncremental) {
740 // findClasspathChanges may clear caches, if there are
741 // 1. unknown changes, or
742 // 2. changes in annotation processors.
743 val classpathChanges = findClasspathChanges(
744 changedFiles,
745 cacheDir,
746 classpathStructure.get().files,
747 libraries.get().files.toList(),
748 processorCP.get().files.toList()
749 )
750 options += classpathChanges.toSubpluginOptions()
751 } else {
752 if (changedFiles.hasNonSourceChange()) {
753 cacheDir.deleteRecursively()
754 }
755 }
756 } else {
757 cacheDir.deleteRecursively()
758 }
759 options += changedFiles.toSubpluginOptions()
760
761 options
762 }
763