diff --git a/.github/workflows/standalone.yml b/.github/workflows/standalone.yml new file mode 100644 index 0000000000..fe968c1651 --- /dev/null +++ b/.github/workflows/standalone.yml @@ -0,0 +1,74 @@ +# Workflow to run tests + +name: standalone compiler runner + +on: + # runs on 2:00 AM PST or 3:00 AM PDT + schedule: + - cron: '0 10 * * *' + +jobs: + build-and-test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + branch: [ main, 1.0.7-release ] + + # The type of runner that the job will run on + runs-on: ${{ matrix.os }} + + steps: + - name: Setup Java 9 + uses: actions/setup-java@v1.4.3 + with: + java-version: '9' + java-package: jdk + architecture: x64 + - name: set JDK_9 environment variable for kotlin compiler + shell: bash + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + run: echo ::set-env name=JDK_9::$(echo $JAVA_HOME) + - name: Setup Java 11 + uses: actions/setup-java@v1.4.3 + with: + java-version: '11' + java-package: jdk + architecture: x64 + + # Checkout + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + ref: ${{ matrix.branch }} + + # Build cache + - name: Cache Gradle Cache + uses: actions/cache@v2 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}-${{ hashFiles('**/gradle.properties') }} + # An ordered list of keys to use for restoring the cache if no cache hit occurred for key + restore-keys: | + ${{ runner.os }}-gradle- + - name: Cache gradle wrapper + uses: actions/cache@v2 + with: + path: ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }} + + # Run tests + - name: test + shell: bash + run: ./gradlew -Pksp.compiler.runner=standalone --stacktrace --info :integration-tests:test :gradle-plugin:test + - name: Upload test results + if: always() + uses: actions/upload-artifact@v3 + with: + name: test-reports-standalone-${{ matrix.os }}-${{ matrix.branch }} + path: | + compiler-plugin/build/reports + integration-tests/build/reports + gradle-plugin/build/reports + common-util/build/reports diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts index aa67a18f00..6322082c61 100644 --- a/gradle-plugin/build.gradle.kts +++ b/gradle-plugin/build.gradle.kts @@ -24,6 +24,7 @@ plugins { dependencies { implementation("org.jetbrains.kotlin:kotlin-gradle-plugin-api:$kotlinBaseVersion") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinBaseVersion") + implementation("org.jetbrains.kotlin:kotlin-compiler-runner:$kotlinBaseVersion") compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlinBaseVersion") // replace AGP dependency w/ gradle-api when we have source registering API available. compileOnly("com.android.tools.build:gradle:$agpBaseVersion") @@ -105,6 +106,7 @@ val writeTestPropsTask = tasks.register("prepareTestConfigurati property("mavenRepoDir", File(rootProject.buildDir, "repos/test").absolutePath) property("kspProjectRootDir", rootProject.projectDir.absolutePath) property("processorClasspath", project.tasks["compileTestKotlin"].outputs.files.asPath) + property("kspCompilerRunner", project.properties.getOrDefault("ksp.compiler.runner", "inherited") as String) } java { diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/AndroidPluginIntegration.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/AndroidPluginIntegration.kt index a78a610ee6..862dc1f364 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/AndroidPluginIntegration.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/AndroidPluginIntegration.kt @@ -19,6 +19,7 @@ package com.google.devtools.ksp.gradle import com.android.build.api.dsl.CommonExtension import com.android.build.gradle.BaseExtension import org.gradle.api.Project +import org.gradle.api.Task import org.gradle.api.file.FileCollection import org.gradle.api.tasks.TaskProvider import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation @@ -65,7 +66,7 @@ object AndroidPluginIntegration { fun registerGeneratedJavaSources( project: Project, kotlinCompilation: KotlinJvmAndroidCompilation, - kspTaskProvider: TaskProvider, + kspTaskProvider: TaskProvider, javaOutputDir: File, classOutputDir: File, resourcesOutputDir: FileCollection, diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/CompilerOptionsFactory.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/CompilerOptionsFactory.kt new file mode 100644 index 0000000000..7e4dd330d8 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/CompilerOptionsFactory.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2022 Google LLC + * Copyright 2010-2022 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 org.gradle.api.model.ObjectFactory +import org.jetbrains.kotlin.gradle.dsl.CompilerCommonOptions +import org.jetbrains.kotlin.gradle.dsl.CompilerCommonOptionsDefault +import org.jetbrains.kotlin.gradle.dsl.CompilerJsOptions +import org.jetbrains.kotlin.gradle.dsl.CompilerJsOptionsDefault +import org.jetbrains.kotlin.gradle.dsl.CompilerJvmOptions +import org.jetbrains.kotlin.gradle.dsl.CompilerJvmOptionsDefault +import org.jetbrains.kotlin.gradle.utils.newInstance + +// TODO: to be replaced by KotlinJvmFactory, etc. +class CompilerOptionsFactory { + companion object { + fun createCompilerJvmOptions(objectFactory: ObjectFactory): CompilerJvmOptions = + objectFactory.newInstance() + + fun createCompilerJsOptions(objectFactory: ObjectFactory): CompilerJsOptions = + objectFactory.newInstance() + + fun createCompilerCommonOptions(objectFactory: ObjectFactory): CompilerCommonOptions = + objectFactory.newInstance() + } +} diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KotlinCompilerRunner.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KotlinCompilerRunner.kt new file mode 100644 index 0000000000..9c6e0ee7dc --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KotlinCompilerRunner.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2022 Google LLC + * Copyright 2010-2022 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 org.gradle.api.Task +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Internal +import org.jetbrains.kotlin.gradle.dsl.CompilerCommonOptions +import org.jetbrains.kotlin.gradle.dsl.CompilerJsOptions +import org.jetbrains.kotlin.gradle.dsl.CompilerJvmOptions +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy +import org.jetbrains.kotlin.gradle.utils.newInstance +import java.io.File + +interface KotlinCompilerRunner { + // TODO: Remove those properties when getting into KGP. + // They should be configured by KGP. For now, they allow KSP to copy the settings from compilation task. + + @get:Internal + val compilerExecutionStrategy: Property + + @get:Internal + val useDaemonFallbackStrategy: Property + + @get:Internal + val kotlinDaemonJvmArguments: ListProperty + + @get:Classpath + val compilerClasspath: ConfigurableFileCollection +} + +interface KotlinJvmCompilerRunner : KotlinCompilerRunner { + fun runJvmCompilerAsync( + options: CompilerJvmOptions, + freeArgs: List, + sources: List, + commonSources: List, + friendPaths: List, + libraries: List, + destination: File + ) +} + +interface KotlinJsCompilerRunner : KotlinCompilerRunner { + fun runJsCompilerAsync( + options: CompilerJsOptions, + freeArgs: List, + sources: List, + commonSources: List, + friendPaths: List, + libraries: List, + destination: File + ) +} + +interface KotlinMetadataCompilerRunner : KotlinCompilerRunner { + fun runMetadataCompilerAsync( + options: CompilerCommonOptions, + freeArgs: List, + sources: List, + commonSources: List, + friendPaths: List, + libraries: List, + destination: File + ) +} + +interface KotlinNativeCompilerRunner : KotlinCompilerRunner { + fun runNativeCompilerAsync( + options: CompilerCommonOptions, + freeArgs: List, + sources: List, + commonSources: List, + friendPaths: List, + libraries: List, + destination: File, + target: String + ) +} + +// TODO: Maybe move those functions into proper KGP class when getting into KGP. + +// TODO: Remove objectFactory when getting into KGP. +fun createKotlinJvmCompilerRunner( + task: Task, + objectFactory: ObjectFactory, +): KotlinJvmCompilerRunner { + return objectFactory.newInstance(task) +} + +// TODO: Remove objectFactory when getting into KGP. +fun createKotlinJsCompilerRunner( + task: Task, + objectFactory: ObjectFactory, +): KotlinJsCompilerRunner { + return objectFactory.newInstance(task) +} + +// TODO: Remove objectFactory when getting into KGP. +fun createKotlinMetadataCompilerRunner( + task: Task, + objectFactory: ObjectFactory, +): KotlinMetadataCompilerRunner { + return objectFactory.newInstance(task) +} + +// TODO: Remove objectFactory when getting into KGP. +fun createKotlinNativeCompilerRunner( + task: Task, + objectFactory: ObjectFactory, +): KotlinNativeCompilerRunner { + return objectFactory.newInstance(task) +} diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KotlinCompilerRunnerImpls.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KotlinCompilerRunnerImpls.kt new file mode 100644 index 0000000000..a8f7bdd5c9 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KotlinCompilerRunnerImpls.kt @@ -0,0 +1,308 @@ +/* + * Copyright 2022 Google LLC + * Copyright 2010-2022 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 org.gradle.api.Task +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Internal +import org.gradle.process.ExecOperations +import org.gradle.workers.WorkerExecutor +import org.jetbrains.kotlin.build.report.metrics.BuildMetricsReporter +import org.jetbrains.kotlin.build.report.metrics.BuildMetricsReporterImpl +import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments +import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments +import org.jetbrains.kotlin.cli.common.arguments.K2MetadataCompilerArguments +import org.jetbrains.kotlin.cli.common.arguments.isIrBackendEnabled +import org.jetbrains.kotlin.cli.common.arguments.isPreIrBackendDisabled +import org.jetbrains.kotlin.compilerRunner.CompilerExecutionSettings +import org.jetbrains.kotlin.compilerRunner.GradleCompilerEnvironment +import org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers +import org.jetbrains.kotlin.compilerRunner.KotlinToolRunner +import org.jetbrains.kotlin.compilerRunner.OutputItemsCollectorImpl +import org.jetbrains.kotlin.gradle.logging.GradleKotlinLogger +import org.jetbrains.kotlin.gradle.logging.GradlePrintingMessageCollector +import org.jetbrains.kotlin.gradle.report.ReportingSettings +import org.jetbrains.kotlin.gradle.tasks.DefaultKotlinJavaToolchain +import org.jetbrains.kotlin.gradle.tasks.GradleCompileTaskProvider +import org.jetbrains.kotlin.gradle.utils.newInstance +import org.jetbrains.kotlin.gradle.utils.propertyWithNewInstance +import org.jetbrains.kotlin.konan.target.KonanTarget +import org.jetbrains.kotlin.library.impl.isKotlinLibrary +import org.jetbrains.kotlin.utils.JsLibraryUtils +import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty +import java.io.File +import javax.inject.Inject +import org.jetbrains.kotlin.gradle.dsl.CompilerCommonOptions +import org.jetbrains.kotlin.gradle.dsl.CompilerCommonOptionsDefault +import org.jetbrains.kotlin.gradle.dsl.CompilerJsOptions +import org.jetbrains.kotlin.gradle.dsl.CompilerJsOptionsDefault +import org.jetbrains.kotlin.gradle.dsl.CompilerJvmOptions +import org.jetbrains.kotlin.gradle.dsl.CompilerJvmOptionsDefault +import org.jetbrains.kotlin.gradle.logging.GradleErrorMessageCollector + +internal inline fun ObjectFactory.property() = property(T::class.java) +internal inline fun ObjectFactory.property(initialValue: T) = property().value(initialValue) + +// TODO: All the properties should be configured by KGP +abstract class KotlinCompilerRunnerImpl @Inject constructor( + task: Task, + objectFactory: ObjectFactory, + @get:Internal val workerExecutor: WorkerExecutor +) : KotlinCompilerRunner { + @get:Internal + internal val taskProvider: Provider = objectFactory.property( + objectFactory.newInstance(task.project.gradle, task, task.project) + ) + + @get:Internal + internal val defaultKotlinJavaToolchain: Provider = + objectFactory.propertyWithNewInstance({ null }) + + @get:Internal + internal val metrics: Property = objectFactory.property(BuildMetricsReporterImpl()) + + @get:Internal + val normalizedKotlinDaemonJvmArguments: Provider> + get() = kotlinDaemonJvmArguments.map { + it.map { arg -> arg.trim().removePrefix("-") } + } + + @get:Internal + internal val logger = task.logger + + @Internal + internal fun prepareEnvironment(allWarningsAsErrors: Boolean, outputs: List): GradleCompilerEnvironment { + val messageCollector = GradlePrintingMessageCollector(GradleKotlinLogger(logger), allWarningsAsErrors) + val errorMessageCollector = GradleErrorMessageCollector(messageCollector) + val outputItemCollector = OutputItemsCollectorImpl() + return GradleCompilerEnvironment( + compilerClasspath.files.toList(), errorMessageCollector, outputItemCollector, + reportingSettings = ReportingSettings(), + outputFiles = outputs + ) + } + + @Internal + internal fun prepareCompilerRunner(): GradleCompilerRunnerWithWorkers { + return GradleCompilerRunnerWithWorkers( + taskProvider.get(), + defaultKotlinJavaToolchain.get().currentJvmJdkToolsJar.orNull, + CompilerExecutionSettings( + normalizedKotlinDaemonJvmArguments.get(), + compilerExecutionStrategy.get(), + useDaemonFallbackStrategy.get() + ), + metrics.get(), + workerExecutor + ) + } +} + +abstract class KotlinJvmCompilerRunnerImpl @Inject constructor( + private val task: Task, + objectFactory: ObjectFactory, + workerExecutor: WorkerExecutor +) : KotlinCompilerRunnerImpl(task, objectFactory, workerExecutor), KotlinJvmCompilerRunner { + + override fun runJvmCompilerAsync( + options: CompilerJvmOptions, + freeArgs: List, + sources: List, + commonSources: List, + friendPaths: List, + libraries: List, + destination: File + ) { + val environment = prepareEnvironment(options.allWarningsAsErrors.get(), task.outputs.files.toList()) + val compilerRunner = prepareCompilerRunner() + val compilerArgs = K2JVMCompilerArguments().apply { + options as CompilerJvmOptionsDefault + options.fillCompilerArguments(this) + + this@apply.friendPaths = friendPaths.map { it.absolutePath }.toTypedArray() + this@apply.classpath = libraries.map { it.absolutePath }.joinToString(File.pathSeparator) + this@apply.freeArgs = options.freeCompilerArgs.get() + freeArgs + this@apply.destination = destination.absolutePath + } + + compilerRunner.runJvmCompilerAsync( + sourcesToCompile = sources, + commonSources = commonSources, + javaPackagePrefix = null, + args = compilerArgs, + environment = environment, + jdkHome = defaultKotlinJavaToolchain.get().providedJvm.get().javaHome, + null + ) + } +} + +abstract class KotlinJsCompilerRunnerImpl @Inject constructor( + private val task: Task, + objectFactory: ObjectFactory, + workerExecutor: WorkerExecutor +) : KotlinCompilerRunnerImpl(task, objectFactory, workerExecutor), KotlinJsCompilerRunner { + + private fun libFilter(args: K2JSCompilerArguments, file: File): Boolean = + file.exists() && when { + // JS_IR + args.isIrBackendEnabled() && args.isPreIrBackendDisabled() -> + isKotlinLibrary(file) + + // JS_LEGACY + !args.isIrBackendEnabled() && !args.isPreIrBackendDisabled() -> + JsLibraryUtils.isKotlinJavascriptLibrary(file) + + // JS_BOTH + args.isIrBackendEnabled() && !args.isPreIrBackendDisabled() -> + isKotlinLibrary(file) && JsLibraryUtils.isKotlinJavascriptLibrary(file) + + else -> throw IllegalArgumentException("Cannot determine JS backend.") + } + + override fun runJsCompilerAsync( + options: CompilerJsOptions, + freeArgs: List, + sources: List, + commonSources: List, + friendPaths: List, + libraries: List, + destination: File + ) { + val environment = prepareEnvironment(options.allWarningsAsErrors.get(), task.outputs.files.toList()) + val compilerRunner = prepareCompilerRunner() + val compilerArgs = K2JSCompilerArguments().apply { + options as CompilerJsOptionsDefault + options.fillCompilerArguments(this) + + this@apply.freeArgs = options.freeCompilerArgs.get() + freeArgs + this@apply.outputFile = File(destination, "dummy.js").absolutePath + + irOnly = this@apply.freeArgs.contains("-Xir-only") + irProduceJs = this@apply.freeArgs.contains("-Xir-produce-js") + irProduceKlibDir = this@apply.freeArgs.contains("-Xir-produce-klib-dir") + irProduceKlibFile = this@apply.freeArgs.contains("-Xir-produce-klib-file") + irBuildCache = this@apply.freeArgs.contains("-Xir-build-cache") + wasm = this@apply.freeArgs.contains("-Xwasm") + + this@apply.friendModules = friendPaths.filter { libFilter(this, it) } + .map { it.absolutePath }.joinToString(File.pathSeparator) + this@apply.libraries = libraries.filter { libFilter(this, it) } + .map { it.absolutePath }.joinToString(File.pathSeparator) + } + + compilerRunner.runJsCompilerAsync(sources, commonSources, compilerArgs, environment, null) + } +} + +abstract class KotlinMetadataCompilerRunnerImpl @Inject constructor( + private val task: Task, + objectFactory: ObjectFactory, + workerExecutor: WorkerExecutor +) : KotlinCompilerRunnerImpl(task, objectFactory, workerExecutor), KotlinMetadataCompilerRunner { + + override fun runMetadataCompilerAsync( + options: CompilerCommonOptions, + freeArgs: List, + sources: List, + commonSources: List, + friendPaths: List, + libraries: List, + destination: File + ) { + val environment = prepareEnvironment(options.allWarningsAsErrors.get(), task.outputs.files.toList()) + val compilerRunner = prepareCompilerRunner() + val compilerArgs = K2MetadataCompilerArguments().apply { + options as CompilerCommonOptionsDefault + options.fillCompilerArguments(this) + + this@apply.friendPaths = friendPaths.map { it.absolutePath }.toTypedArray() + this@apply.classpath = libraries.map { it.absolutePath }.joinToString(File.pathSeparator) + this@apply.freeArgs = options.freeCompilerArgs.get() + freeArgs + this@apply.destination = destination.absolutePath + } + + compilerRunner.runMetadataCompilerAsync(sources, commonSources, compilerArgs, environment) + } +} + +abstract class KotlinNativeCompilerRunnerImpl @Inject constructor( + private val task: Task, + private val objectFactory: ObjectFactory, + workerExecutor: WorkerExecutor, + private val execOperations: ExecOperations +) : KotlinCompilerRunnerImpl(task, objectFactory, workerExecutor), KotlinNativeCompilerRunner { + + private val runnerSettings = org.jetbrains.kotlin.compilerRunner + .KotlinNativeCompilerRunner.Settings.fromProject(task.project) + + override fun runNativeCompilerAsync( + options: CompilerCommonOptions, + freeArgs: List, + sources: List, + commonSources: List, + friendPaths: List, + libraries: List, + destination: File, + target: String + ) { + val target = KonanTarget.predefinedTargets.get(target)!! + val buildArgs: MutableList = mutableListOf( + "-o", destination.path, + "-target", target.name, + "-p", "library", + "-Xmulti-platform" + ) + libraries.flatMap { listOf("-l", it.absolutePath) }.let { buildArgs.addAll(it) } + friendPaths.ifNotEmpty { + buildArgs.add("-friend-modules") + buildArgs.add(joinToString(File.pathSeparator)) + } + + if (options.verbose.get()) + buildArgs.add("-verbose") + if (options.allWarningsAsErrors.get()) + buildArgs.add("-Werror") + + options.languageVersion.getOrNull()?.let { + buildArgs.add("-language-version") + buildArgs.add(it.version) + } + options.apiVersion.getOrNull()?.let { + buildArgs.add("-api-version") + buildArgs.add(it.version) + } + + buildArgs.addAll(sources.map { it.absolutePath }) + buildArgs.addAll(freeArgs) + buildArgs.addAll(commonSources.map { it.absolutePath }) + + org.jetbrains.kotlin.compilerRunner.KotlinNativeCompilerRunner( + settings = runnerSettings, + executionContext = KotlinToolRunner.GradleExecutionContext.fromTaskContext( + objectFactory, + execOperations, + logger + ) + ).run(buildArgs) + } +} 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 08caa23ac8..ed13258170 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 @@ -15,11 +15,10 @@ * 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 com.google.devtools.ksp.gradle.tasks.KspTaskFactory import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.UnknownTaskException @@ -27,76 +26,36 @@ 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.provider.ProviderFactory -import org.gradle.api.tasks.* +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.LocalState +import org.gradle.api.tasks.Nested import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.compile.JavaCompile import org.gradle.language.jvm.tasks.ProcessResources import org.gradle.process.CommandLineArgumentProvider -import org.gradle.process.ExecOperations import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry -import org.gradle.util.GradleVersion -import org.gradle.work.Incremental -import org.gradle.work.InputChanges -import org.gradle.workers.WorkerExecutor -import org.jetbrains.kotlin.cli.common.arguments.* import org.jetbrains.kotlin.config.ApiVersion -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.KotlinCommonCompilation +import org.jetbrains.kotlin.gradle.plugin.FilesSubpluginOption +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilationWithResources +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin +import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact +import org.jetbrains.kotlin.gradle.plugin.SubpluginOption +import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion 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.AbstractKotlinCompileTool import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import org.jetbrains.kotlin.gradle.tasks.configuration.AbstractKotlinCompileConfig -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 org.jetbrains.kotlin.utils.addToStdlib.safeAs import java.io.File -import java.nio.file.Paths -import java.util.concurrent.Callable import javax.inject.Inject -import kotlin.reflect.KProperty1 - -@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "EXPOSED_PARAMETER_TYPE") -internal class Configurator : AbstractKotlinCompileConfig> { - constructor(compilation: KotlinCompilationData<*>, kotlinCompile: AbstractKotlinCompile<*>) : super(compilation) { - configureTask { task -> - if (task is KspTaskJvm) { - // Assign ownModuleName different from kotlin compilation to - // work around https://github.com/google/ksp/issues/647 - // This will not be necessary once https://youtrack.jetbrains.com/issue/KT-45777 lands - task.ownModuleName.value(kotlinCompile.ownModuleName.map { "$it-ksp" }) - } - if (task is KspTaskJS) { - val libraryCacheService = project.rootProject.gradle.sharedServices.registerIfAbsent( - Kotlin2JsCompile.LibraryFilterCachingService::class.java.canonicalName + - "_${Kotlin2JsCompile.LibraryFilterCachingService::class.java.classLoader.hashCode()}", - Kotlin2JsCompile.LibraryFilterCachingService::class.java - ) {} - task.libraryCache.set(libraryCacheService).also { task.libraryCache.disallowChanges() } - task.pluginClasspath.setFrom(objectFactory.fileCollection()) - } - } - } -} class KspGradleSubplugin @Inject internal constructor(private val registry: ToolingModelBuilderRegistry) : KotlinCompilerPluginSupportPlugin { @@ -110,6 +69,8 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool const val KSP_GROUP_ID = "com.google.devtools.ksp" const val KSP_PLUGIN_CLASSPATH_CONFIGURATION_NAME = "kspPluginClasspath" + val apiArtifact = "com.google.devtools.ksp:symbol-processing-api:$KSP_VERSION" + @JvmStatic fun getKspOutputDir(project: Project, sourceSetName: String, target: String) = File(project.project.buildDir, "generated/ksp/$target/$sourceSetName") @@ -135,7 +96,7 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool File(project.project.buildDir, "kspCaches/$target/$sourceSetName") @JvmStatic - private fun getSubpluginOptions( + fun getSubpluginOptions( project: Project, kspExtension: KspExtension, classpath: Configuration, @@ -245,7 +206,6 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool ) project.dependencies.add( KSP_PLUGIN_CLASSPATH_CONFIGURATION_NAME, - "$KSP_GROUP_ID:$KSP_COMPILER_PLUGIN_ID:$KSP_VERSION" ) @@ -261,129 +221,28 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool val kotlinCompileTask = kotlinCompileProvider.get() - 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()) - kspTask.processorClasspath.from(processorClasspath) - kspTask.dependsOn(processorClasspath.buildDependencies) - - kspTask.options.addAll( - kspTask.project.provider { - getSubpluginOptions( - project, - kspExtension, - processorClasspath, - sourceSetName, - target, - isIncremental, - kspExtension.allWarningsAsErrors - ) - } - ) - kspTask.commandLineArgumentProviders.addAll(kspExtension.commandLineArgumentProviders) - kspTask.destination = kspOutputDir - kspTask.blockOtherCompilerPlugins = kspExtension.blockOtherCompilerPlugins - kspTask.apOptions.value(kspExtension.arguments).disallowChanges() - kspTask.kspCacheDir.fileValue(getKspCachesDir(project, sourceSetName, target)).disallowChanges() - - if (kspExtension.blockOtherCompilerPlugins) { - kspTask.overridePluginClasspath.from(kspClasspathCfg) - } - kspTask.isKspIncremental = isIncremental - } - - fun configureAsAbstractKotlinCompileTool(kspTask: AbstractKotlinCompileTool<*>) { - kspTask.destinationDirectory.set(kspOutputDir) - kspTask.outputs.dirs( - kotlinOutputDir, - javaOutputDir, - classOutputDir, - resourceOutputDir - ) - - if (kspExtension.allowSourcesFromOtherPlugins) { - val deps = kotlinCompileTask.dependsOn.filterNot { - it.safeAs>()?.name == kspTaskName || - it.safeAs()?.name == kspTaskName - } - kspTask.dependsOn(deps) - kspTask.setSource(kotlinCompileTask.sources) - if (kotlinCompileTask is KotlinCompile) { - kspTask.setSource(kotlinCompileTask.javaSources) - } - } else { - kotlinCompilation.allKotlinSourceSets.forEach { sourceSet -> - kspTask.setSource(sourceSet.kotlin) - } - if (kotlinCompilation is KotlinCommonCompilation) { - kspTask.setSource(kotlinCompilation.defaultSourceSet.kotlin) - } - } - - // Don't support binary generation for non-JVM platforms yet. - // FIXME: figure out how to add user generated libraries. - if (kspTask is KspTaskJvm) { - 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) - configureAsAbstractKotlinCompileTool(kspTask) - - kspTask.libraries.setFrom(kotlinCompileTask.project.files(Callable { kotlinCompileTask.libraries })) - 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) - configureAsAbstractKotlinCompileTool(kspTask) - - // KotlinNativeCompile computes -Xplugin=... from compilerPluginClasspath. - kspTask.compilerPluginClasspath = project.configurations.getByName(pluginConfigurationName) - kspTask.commonSources.from(kotlinCompileTask.commonSources) - kspTask.compilerPluginOptions.addPluginArgument(kotlinCompileTask.compilerPluginOptions) - } - } - } - else -> return project.provider { emptyList() } - } - - kotlinCompileTask.safeAs>()?.let { - Configurator(kotlinCompilation as KotlinCompilationData<*>, kotlinCompileTask as AbstractKotlinCompile<*>) - .execute(kspTaskProvider as TaskProvider>) - } + val kspTaskProvider = KspTaskFactory.createKspTask( + project, + kotlinCompilation, + kotlinCompileTask, + kspExtension, + nonEmptyKspConfigurations, + kspTaskName, + target, + sourceSetName, + classOutputDir, + javaOutputDir, + kotlinOutputDir, + resourceOutputDir, + kspOutputDir + ) kotlinCompileProvider.configure { kotlinCompile -> kotlinCompile.dependsOn(kspTaskProvider) kotlinCompile.setSource(kotlinOutputDir, javaOutputDir) when (kotlinCompile) { - is AbstractKotlinCompile<*> -> kotlinCompile.libraries.from(project.files(classOutputDir)) - // is KotlinNativeCompile -> TODO: support binary generation? + is KotlinCompile -> kotlinCompile.libraries.from(project.files(classOutputDir)) + // TODO: support binary generation on other platforms? } } @@ -399,7 +258,7 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool AndroidPluginIntegration.registerGeneratedJavaSources( project = project, kotlinCompilation = kotlinCompilation, - kspTaskProvider = kspTaskProvider as TaskProvider, + kspTaskProvider = kspTaskProvider, javaOutputDir = javaOutputDir, classOutputDir = classOutputDir, resourcesOutputDir = project.files(resourceOutputDir) @@ -417,14 +276,12 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool version = KSP_VERSION ) - override fun getPluginArtifactForNative(): SubpluginArtifact? = + override fun getPluginArtifactForNative(): SubpluginArtifact = SubpluginArtifact( groupId = "com.google.devtools.ksp", artifactId = KSP_ARTIFACT_NAME_NATIVE, version = KSP_VERSION ) - - val apiArtifact = "com.google.devtools.ksp:symbol-processing-api:$KSP_VERSION" } private val artifactType = Attribute.of("artifactType", String::class.java) @@ -441,7 +298,7 @@ internal inline fun Project.locateTask(name: String): TaskPro internal fun findJavaTaskForKotlinCompilation(compilation: KotlinCompilation<*>): TaskProvider? = when (compilation) { is KotlinJvmAndroidCompilation -> compilation.compileJavaTaskProvider - is KotlinWithJavaCompilation -> compilation.compileJavaTaskProvider + is KotlinWithJavaCompilation<*, *> -> compilation.compileJavaTaskProvider is KotlinJvmCompilation -> compilation.compileJavaTaskProvider // may be null for Kotlin-only JVM target in MPP else -> null } @@ -477,523 +334,4 @@ interface KspTask : Task { @get:Input var isKspIncremental: Boolean - - fun configureCompilation( - kotlinCompilation: KotlinCompilationData<*>, - kotlinCompile: AbstractKotlinCompile<*>, - ) -} - -@CacheableTask -abstract class KspTaskJvm @Inject constructor( - workerExecutor: WorkerExecutor, - objectFactory: ObjectFactory -) : KotlinCompile(KotlinJvmOptionsImpl(), workerExecutor, objectFactory), KspTask { - @get:PathSensitive(PathSensitivity.NONE) - @get:Optional - @get:InputFiles - @get:Incremental - abstract val classpathStructure: ConfigurableFileCollection - - @get:Input - var isIntermoduleIncremental: Boolean = false - - override fun configureCompilation( - kotlinCompilation: KotlinCompilationData<*>, - kotlinCompile: AbstractKotlinCompile<*>, - ) { - 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.libraries })) - ) - 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() - } - - // Used only in incremental compilation and is not applicable to KSP. - useKotlinAbiSnapshot.value(false) - } - - 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, - libraries.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> - - 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 - - // Mute a warning from ScriptingGradleSubplugin, which tries to get `sourceSetName` before this task is - // configured. - sourceSetName.set("main") - } - - 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) - } - args.addPluginOptions(options.get()) - args.destinationAsFile = destination - args.allowNoSourceFiles = true - args.useK2 = false - } - - // 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_common`( - args: K2JVMCompilerArguments, - kotlinSources: Set, - inputChanges: InputChanges, - taskOutputsBackup: TaskOutputsBackup? - ) { - val changedFiles = getChangedFiles(inputChanges, incrementalProps) - 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, kotlinSources, inputChanges, taskOutputsBackup) - } - - override fun skipCondition(): Boolean = sources.isEmpty && javaSources.isEmpty - - override val incrementalProps: List - get() = listOf( - sources, - javaSources, - commonSourceSet, - classpathSnapshotProperties.classpath, - classpathSnapshotProperties.classpathSnapshot - ) - - @get:InputFiles - @get:SkipWhenEmpty - @get:IgnoreEmptyDirectories - @get:PathSensitive(PathSensitivity.RELATIVE) - override val sources: FileCollection = super.sources.filter { - !destination.isParentOf(it) - } - - @get:InputFiles - @get:SkipWhenEmpty - @get:IgnoreEmptyDirectories - @get:PathSensitive(PathSensitivity.RELATIVE) - override val javaSources: FileCollection = super.javaSources.filter { - !destination.isParentOf(it) - } -} - -@CacheableTask -abstract class KspTaskJS @Inject constructor( - objectFactory: ObjectFactory, - workerExecutor: WorkerExecutor -) : Kotlin2JsCompile(KotlinJsOptionsImpl(), objectFactory, workerExecutor), KspTask { - private val backendSelectionArgs = listOf( - "-Xir-only", - "-Xir-produce-js", - "-Xir-produce-klib-dir", - "-Xir-produce-klib-file" - ) - - override fun configureCompilation( - kotlinCompilation: KotlinCompilationData<*>, - kotlinCompile: AbstractKotlinCompile<*>, - ) { - kotlinCompile as Kotlin2JsCompile - kotlinOptions.freeCompilerArgs = kotlinCompile.kotlinOptions.freeCompilerArgs.filter { - it in backendSelectionArgs - } - val providerFactory = kotlinCompile.project.providers - compileKotlinArgumentsContributor.set( - providerFactory.provider { - kotlinCompile.abstractKotlinCompileArgumentsContributor - } - ) - } - - @get:Internal - internal abstract val compileKotlinArgumentsContributor: - Property> - - 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) - } - args.addPluginOptions(options.get()) - args.outputFile = File(destination, "dummyOutput.js").canonicalPath - kotlinOptions.copyFreeCompilerArgsToArgs(args) - args.useK2 = false - } - - // 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_common`( - args: K2JSCompilerArguments, - kotlinSources: Set, - inputChanges: InputChanges, - taskOutputsBackup: TaskOutputsBackup? - ) { - val changedFiles = getChangedFiles(inputChanges, incrementalProps) - if (!isKspIncremental || changedFiles.hasNonSourceChange()) { - clearIncCache() - } else { - args.addChangedFiles(changedFiles) - } - super.callCompilerAsync(args, kotlinSources, inputChanges, taskOutputsBackup) - } - - // Overrding an internal function is hacky. - // TODO: Ask upstream to open it. - @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "EXPOSED_PARAMETER_TYPE") - fun `isIncrementalCompilationEnabled$kotlin_gradle_plugin_common`(): Boolean = false - - @get:InputFiles - @get:SkipWhenEmpty - @get:IgnoreEmptyDirectories - @get:PathSensitive(PathSensitivity.RELATIVE) - override val sources: FileCollection = super.sources.filter { - !destination.isParentOf(it) - } -} - -@CacheableTask -abstract class KspTaskMetadata @Inject constructor( - workerExecutor: WorkerExecutor, - objectFactory: ObjectFactory -) : KotlinCompileCommon(KotlinMultiplatformCommonOptionsImpl(), workerExecutor, objectFactory), KspTask { - override fun configureCompilation( - kotlinCompilation: KotlinCompilationData<*>, - kotlinCompile: AbstractKotlinCompile<*>, - ) { - kotlinCompile as KotlinCompileCommon - val providerFactory = kotlinCompile.project.providers - compileKotlinArgumentsContributor.set( - providerFactory.provider { - kotlinCompile.abstractKotlinCompileArgumentsContributor - } - ) - } - - @get:Internal - internal abstract val compileKotlinArgumentsContributor: - Property> - - 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) - } - args.addPluginOptions(options.get()) - args.destination = destination.canonicalPath - val classpathList = libraries.files.filter { it.exists() }.toMutableList() - args.classpath = classpathList.joinToString(File.pathSeparator) - args.friendPaths = friendPaths.files.map { it.absolutePath }.toTypedArray() - args.refinesPaths = refinesMetadataPaths.map { it.absolutePath }.toTypedArray() - args.expectActualLinker = true - args.useK2 = false - } - - // 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_common`( - args: K2MetadataCompilerArguments, - kotlinSources: Set, - inputChanges: InputChanges, - taskOutputsBackup: TaskOutputsBackup? - ) { - val changedFiles = getChangedFiles(inputChanges, incrementalProps) - if (!isKspIncremental || changedFiles.hasNonSourceChange()) { - clearIncCache() - } else { - args.addChangedFiles(changedFiles) - } - super.callCompilerAsync(args, kotlinSources, inputChanges, taskOutputsBackup) - } - - @get:InputFiles - @get:SkipWhenEmpty - @get:IgnoreEmptyDirectories - @get:PathSensitive(PathSensitivity.RELATIVE) - override val sources: FileCollection = super.sources.filter { - !destination.isParentOf(it) - } -} - -@CacheableTask -abstract class KspTaskNative @Inject constructor( - compilation: KotlinNativeCompilationData<*>, - objectFactory: ObjectFactory, - providerFactory: ProviderFactory, - execOperations: ExecOperations -) : KotlinNativeCompile(compilation, objectFactory, providerFactory, execOperations), KspTask { - override val additionalCompilerOptions: Provider> - get() { - return project.provider { - val kspOptions = options.get().flatMap { listOf("-P", it.toArg()) } - super.additionalCompilerOptions.get() + kspOptions - } - } - - override var compilerPluginClasspath: FileCollection? = null - get() { - if (blockOtherCompilerPlugins) { - field = overridePluginClasspath - } - return field - } - - 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(). - // Use a name that gets sorted in the front just in case. `_$` is lower, but it might be too hacky. - @TaskAction - fun _0() { - options.get().single { it.key == "kspOutputDir" }.value.let { - File(it).deleteRecursively() - } - super.compile() - } - - @get:InputFiles - @get:SkipWhenEmpty - @get:IgnoreEmptyDirectories - @get:PathSensitive(PathSensitivity.RELATIVE) - override val sources: FileCollection = super.sources.filter { - !destination.isParentOf(it) - } -} - -// 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) { - pluginOptions = (options.map { it.toArg() } + pluginOptions!!).toTypedArray() -} - -fun CommonCompilerArguments.addChangedFiles(changedFiles: ChangedFiles) { - if (changedFiles is ChangedFiles.Known) { - val options = mutableListOf() - 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 T.toPair(property: KProperty1): Pair { - @Suppress("UNCHECKED_CAST") - val value = (property as KProperty1).get(this) - return property.name to if (value is Array<*>) - value.asList().toString() - else - value.toString() -} - -@Suppress("unused") -internal inline fun dumpArgs(args: T): Map { - @Suppress("UNCHECKED_CAST") - val argumentProperties = - args::class.members.mapNotNull { member -> - (member as? KProperty1)?.takeIf { it.annotations.any { ann -> ann is Argument } } - } - - return argumentProperties.associate(args::toPair).toSortedMap() -} - -internal fun File.isParentOf(childCandidate: File): Boolean { - val parentPath = Paths.get(this.absolutePath).normalize() - val childCandidatePath = Paths.get(childCandidate.absolutePath).normalize() - - return childCandidatePath.startsWith(parentPath) } diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/tasks/KspTaskFactory.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/tasks/KspTaskFactory.kt new file mode 100644 index 0000000000..ef60e13bf6 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/tasks/KspTaskFactory.kt @@ -0,0 +1,70 @@ +package com.google.devtools.ksp.gradle.tasks + +import com.google.devtools.ksp.gradle.KspExtension +import com.google.devtools.ksp.gradle.tasks.inherited.InheritedTasks +import com.google.devtools.ksp.gradle.tasks.standalone.StandaloneTasks +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.artifacts.Configuration +import org.gradle.api.tasks.TaskProvider +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool +import java.io.File + +object KspTaskFactory { + private fun getKspTaskCreator(project: Project): KspTaskCreator { + return if (project.findProperty("ksp.compiler.runner")?.toString() == "standalone") { + StandaloneTasks + } else { + InheritedTasks + } + } + + fun createKspTask( + project: Project, + kotlinCompilation: KotlinCompilation<*>, + kotlinCompileTask: AbstractKotlinCompileTool<*>, + kspExtension: KspExtension, + kspConfigurations: List, + kspTaskName: String, + target: String, + sourceSetName: String, + classOutputDir: File, + javaOutputDir: File, + kotlinOutputDir: File, + resourceOutputDir: File, + kspOutputDir: File, + ): TaskProvider = getKspTaskCreator(project).createKspTask( + project, + kotlinCompilation, + kotlinCompileTask, + kspExtension, + kspConfigurations, + kspTaskName, + target, + sourceSetName, + classOutputDir, + javaOutputDir, + kotlinOutputDir, + resourceOutputDir, + kspOutputDir + ) +} + +interface KspTaskCreator { + fun createKspTask( + project: Project, + kotlinCompilation: KotlinCompilation<*>, + kotlinCompileTask: AbstractKotlinCompileTool<*>, + kspExtension: KspExtension, + kspConfigurations: List, + kspTaskName: String, + target: String, + sourceSetName: String, + classOutputDir: File, + javaOutputDir: File, + kotlinOutputDir: File, + resourceOutputDir: File, + kspOutputDir: File, + ): TaskProvider +} diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/tasks/inherited/InheritedTasks.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/tasks/inherited/InheritedTasks.kt new file mode 100644 index 0000000000..7a0586e823 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/tasks/inherited/InheritedTasks.kt @@ -0,0 +1,781 @@ +/* + * Copyright 2022 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.tasks.inherited + +import com.google.devtools.ksp.gradle.KspExtension +import com.google.devtools.ksp.gradle.KspGradleSubplugin +import com.google.devtools.ksp.gradle.KspGradleSubplugin.Companion.getKspCachesDir +import com.google.devtools.ksp.gradle.KspTask +import com.google.devtools.ksp.gradle.tasks.KspTaskCreator +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.artifacts.Configuration +import org.gradle.api.attributes.Attribute +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.FileCollection +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.IgnoreEmptyDirectories +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.SkipWhenEmpty +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import org.gradle.process.ExecOperations +import org.gradle.util.GradleVersion +import org.gradle.work.Incremental +import org.gradle.work.InputChanges +import org.gradle.workers.WorkerExecutor +import org.jetbrains.kotlin.cli.common.arguments.Argument +import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments +import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments +import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments +import org.jetbrains.kotlin.cli.common.arguments.K2MetadataCompilerArguments +import org.jetbrains.kotlin.gradle.dsl.CompilerJsOptionsDefault +import org.jetbrains.kotlin.gradle.dsl.CompilerJvmOptionsDefault +import org.jetbrains.kotlin.gradle.dsl.CompilerMultiplatformCommonOptionsDefault +import org.jetbrains.kotlin.gradle.dsl.copyFreeCompilerArgsToArgs +import org.jetbrains.kotlin.gradle.internal.CompilerArgumentsContributor +import org.jetbrains.kotlin.gradle.internal.compilerArgumentsConfigurationFlags +import org.jetbrains.kotlin.gradle.internal.kapt.incremental.CLASS_STRUCTURE_ARTIFACT_TYPE +import org.jetbrains.kotlin.gradle.internal.kapt.incremental.ClasspathSnapshot +import org.jetbrains.kotlin.gradle.internal.kapt.incremental.KaptClasspathChanges +import org.jetbrains.kotlin.gradle.internal.kapt.incremental.StructureTransformAction +import org.jetbrains.kotlin.gradle.internal.kapt.incremental.StructureTransformLegacyAction +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.SubpluginOption +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinCommonCompilation +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.AbstractKotlinCompile +import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool +import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.tasks.KotlinCompileCommon +import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile +import org.jetbrains.kotlin.gradle.tasks.TaskOutputsBackup +import org.jetbrains.kotlin.gradle.tasks.configuration.AbstractKotlinCompileConfig +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 org.jetbrains.kotlin.utils.addToStdlib.safeAs +import java.io.File +import java.nio.file.Paths +import java.util.concurrent.Callable +import javax.inject.Inject +import kotlin.reflect.KProperty1 +import org.jetbrains.kotlin.gradle.dsl.CompilerJsOptions +import org.jetbrains.kotlin.gradle.dsl.CompilerJvmOptions +import org.jetbrains.kotlin.gradle.dsl.CompilerMultiplatformCommonOptions + +@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "EXPOSED_PARAMETER_TYPE") +internal class Configurator : AbstractKotlinCompileConfig> { + constructor(compilation: KotlinCompilationData<*>, kotlinCompile: AbstractKotlinCompile<*>) : super(compilation) { + configureTask { task -> + if (task is KspTaskJvm) { + // Assign ownModuleName different from kotlin compilation to + // work around https://github.com/google/ksp/issues/647 + // This will not be necessary once https://youtrack.jetbrains.com/issue/KT-45777 lands + task.ownModuleName.value(kotlinCompile.ownModuleName.map { "$it-ksp" }) + } + if (task is KspTaskJS) { + val libraryCacheService = project.rootProject.gradle.sharedServices.registerIfAbsent( + Kotlin2JsCompile.LibraryFilterCachingService::class.java.canonicalName + + "_${Kotlin2JsCompile.LibraryFilterCachingService::class.java.classLoader.hashCode()}", + Kotlin2JsCompile.LibraryFilterCachingService::class.java + ) {} + task.libraryCache.set(libraryCacheService).also { task.libraryCache.disallowChanges() } + task.pluginClasspath.setFrom(objectFactory.fileCollection()) + } + } + } +} + +object InheritedTasks : KspTaskCreator { + override fun createKspTask( + project: Project, + kotlinCompilation: KotlinCompilation<*>, + kotlinCompileTask: AbstractKotlinCompileTool<*>, + kspExtension: KspExtension, + kspConfigurations: List, + kspTaskName: String, + target: String, + sourceSetName: String, + classOutputDir: File, + javaOutputDir: File, + kotlinOutputDir: File, + resourceOutputDir: File, + kspOutputDir: File, + ): TaskProvider { + + val kspClasspathConfiguration = project.configurations.getByName( + KspGradleSubplugin.KSP_PLUGIN_CLASSPATH_CONFIGURATION_NAME + ) + + fun configureAsKspTask(kspTask: KspTaskInherited, isIncremental: Boolean) { + // depends on the processor; if the processor changes, it needs to be reprocessed. + val processorClasspath = project.configurations.maybeCreate("${kspTaskName}ProcessorClasspath") + .extendsFrom(*kspConfigurations.toTypedArray()) + kspTask.processorClasspath.from(processorClasspath) + kspTask.dependsOn(processorClasspath.buildDependencies) + + kspTask.options.addAll( + kspTask.project.provider { + KspGradleSubplugin.getSubpluginOptions( + project, + kspExtension, + processorClasspath, + sourceSetName, + target, + isIncremental, + kspExtension.allWarningsAsErrors + ) + } + ) + kspTask.commandLineArgumentProviders.addAll(kspExtension.commandLineArgumentProviders) + kspTask.destination = kspOutputDir + kspTask.blockOtherCompilerPlugins = kspExtension.blockOtherCompilerPlugins + kspTask.apOptions.value(kspExtension.arguments).disallowChanges() + kspTask.kspCacheDir.fileValue(getKspCachesDir(project, sourceSetName, target)).disallowChanges() + + if (kspExtension.blockOtherCompilerPlugins) { + kspTask.overridePluginClasspath.from(kspClasspathConfiguration) + } + kspTask.isKspIncremental = isIncremental + } + + fun configureAsAbstractKotlinCompileTool(kspTask: AbstractKotlinCompileTool<*>) { + kspTask.destinationDirectory.set(kspOutputDir) + kspTask.outputs.dirs( + kotlinOutputDir, + javaOutputDir, + classOutputDir, + resourceOutputDir + ) + + if (kspExtension.allowSourcesFromOtherPlugins) { + val deps = kotlinCompileTask.dependsOn.filterNot { + it.safeAs>()?.name == kspTaskName || + it.safeAs()?.name == kspTaskName + } + kspTask.dependsOn(deps) + kspTask.setSource(kotlinCompileTask.sources) + if (kotlinCompileTask is KotlinCompile) { + kspTask.setSource(kotlinCompileTask.javaSources) + } + } else { + kotlinCompilation.allKotlinSourceSets.forEach { sourceSet -> + kspTask.setSource(sourceSet.kotlin) + } + if (kotlinCompilation is KotlinCommonCompilation) { + kspTask.setSource(kotlinCompilation.defaultSourceSet.kotlin) + } + } + + // Don't support binary generation for non-JVM platforms yet. + // FIXME: figure out how to add user generated libraries. + if (kspTask is KspTaskJvm) { + 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 -> throw IllegalArgumentException("Unknown Kotlin compilation task $kotlinCompileTask") + } + val isIncremental = project.findProperty("ksp.incremental")?.toString()?.toBoolean() ?: true + project.tasks.register(kspTaskName, kspTaskClass) { kspTask -> + configureAsKspTask(kspTask, isIncremental) + configureAsAbstractKotlinCompileTool(kspTask) + + kspTask.libraries.setFrom(kotlinCompileTask.project.files(Callable { kotlinCompileTask.libraries })) + kspTask.configureCompilation( + kotlinCompilation as KotlinCompilationData<*>, + kotlinCompileTask, + ) + } + } + is KotlinNativeCompile -> { + val kspTaskClass = KspTaskNative::class.java + project.tasks.register(kspTaskName, kspTaskClass, kotlinCompileTask.compilation).apply { + configure { kspTask -> + kspTask.onlyIf { + kotlinCompileTask.compilation.konanTarget.enabledOnCurrentHost + } + configureAsKspTask(kspTask, false) + configureAsAbstractKotlinCompileTool(kspTask) + + // KotlinNativeCompile computes -Xplugin=... from compilerPluginClasspath. + kspTask.compilerPluginClasspath = kspClasspathConfiguration + kspTask.commonSources.from(kotlinCompileTask.commonSources) + kspTask.compilerPluginOptions.addPluginArgument(kotlinCompileTask.compilerPluginOptions) + } + } + } + else -> throw IllegalArgumentException("Unknown Kotlin compilation task $kotlinCompileTask") + } + + kotlinCompileTask.safeAs>()?.let { + Configurator(kotlinCompilation as KotlinCompilationData<*>, kotlinCompileTask as AbstractKotlinCompile<*>) + .execute(kspTaskProvider as TaskProvider>) + } + + return kspTaskProvider + } +} + +private val artifactType = Attribute.of("artifactType", String::class.java) + +interface KspTaskInherited : KspTask { + fun configureCompilation( + kotlinCompilation: KotlinCompilationData<*>, + kotlinCompile: AbstractKotlinCompile<*>, + ) +} + +@CacheableTask +abstract class KspTaskJvm @Inject constructor( + compilerOptions: CompilerJvmOptions, + workerExecutor: WorkerExecutor, + objectFactory: ObjectFactory +) : KotlinCompile(compilerOptions, workerExecutor, objectFactory), KspTaskInherited { + @get:PathSensitive(PathSensitivity.NONE) + @get:Optional + @get:InputFiles + @get:Incremental + abstract val classpathStructure: ConfigurableFileCollection + + @get:Input + var isIntermoduleIncremental: Boolean = false + + override fun configureCompilation( + kotlinCompilation: KotlinCompilationData<*>, + kotlinCompile: AbstractKotlinCompile<*>, + ) { + 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.libraries })) + ) + 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() + } + + // Used only in incremental compilation and is not applicable to KSP. + useKotlinAbiSnapshot.value(false) + } + + 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, + libraries.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> + + 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 + + // Mute a warning from ScriptingGradleSubplugin, which tries to get `sourceSetName` before this task is + // configured. + sourceSetName.set("main") + } + + 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) + } + args.addPluginOptions(options.get()) + args.destinationAsFile = destination + args.allowNoSourceFiles = true + args.useK2 = false + } + + // 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_common`( + args: K2JVMCompilerArguments, + kotlinSources: Set, + inputChanges: InputChanges, + taskOutputsBackup: TaskOutputsBackup? + ) { + val changedFiles = getChangedFiles(inputChanges, incrementalProps) + 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, kotlinSources, inputChanges, taskOutputsBackup) + } + + override fun skipCondition(): Boolean = sources.isEmpty && javaSources.isEmpty + + override val incrementalProps: List + get() = listOf( + sources, + javaSources, + commonSourceSet, + classpathSnapshotProperties.classpath, + classpathSnapshotProperties.classpathSnapshot + ) + + @get:InputFiles + @get:SkipWhenEmpty + @get:IgnoreEmptyDirectories + @get:PathSensitive(PathSensitivity.RELATIVE) + override val sources: FileCollection = super.sources.filter { + !destination.isParentOf(it) + } + + @get:InputFiles + @get:SkipWhenEmpty + @get:IgnoreEmptyDirectories + @get:PathSensitive(PathSensitivity.RELATIVE) + override val javaSources: FileCollection = super.javaSources.filter { + !destination.isParentOf(it) + } +} + +@CacheableTask +abstract class KspTaskJS @Inject constructor( + compilerOptions: CompilerJsOptions, + @get:Internal val objectFactory: ObjectFactory, + workerExecutor: WorkerExecutor +) : Kotlin2JsCompile(compilerOptions, objectFactory, workerExecutor), KspTaskInherited { + private val backendSelectionArgs = listOf( + "-Xir-only", + "-Xir-produce-js", + "-Xir-produce-klib-dir", + "-Xir-produce-klib-file" + ) + + override fun configureCompilation( + kotlinCompilation: KotlinCompilationData<*>, + kotlinCompile: AbstractKotlinCompile<*>, + ) { + kotlinCompile as Kotlin2JsCompile + kotlinOptions.freeCompilerArgs = kotlinCompile.kotlinOptions.freeCompilerArgs.filter { + it in backendSelectionArgs + } + val providerFactory = kotlinCompile.project.providers + compileKotlinArgumentsContributor.set( + providerFactory.provider { + kotlinCompile.abstractKotlinCompileArgumentsContributor + } + ) + } + + @get:Internal + internal abstract val compileKotlinArgumentsContributor: + Property> + + 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. + (compilerOptions as CompilerJsOptionsDefault).fillDefaultValues(args) + compileKotlinArgumentsContributor.get().contributeArguments( + args, + compilerArgumentsConfigurationFlags( + defaultsOnly, + ignoreClasspathResolutionErrors + ) + ) + if (blockOtherCompilerPlugins) { + args.blockOtherPlugins(overridePluginClasspath) + } + args.addPluginOptions(options.get()) + args.outputFile = File(destination, "dummyOutput.js").canonicalPath + kotlinOptions.copyFreeCompilerArgsToArgs(args) + args.useK2 = false + } + + // 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_common`( + args: K2JSCompilerArguments, + kotlinSources: Set, + inputChanges: InputChanges, + taskOutputsBackup: TaskOutputsBackup? + ) { + val changedFiles = getChangedFiles(inputChanges, incrementalProps) + if (!isKspIncremental || changedFiles.hasNonSourceChange()) { + clearIncCache() + } else { + args.addChangedFiles(changedFiles) + } + super.callCompilerAsync(args, kotlinSources, inputChanges, taskOutputsBackup) + } + + // Overrding an internal function is hacky. + // TODO: Ask upstream to open it. + @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "EXPOSED_PARAMETER_TYPE") + fun `isIncrementalCompilationEnabled$kotlin_gradle_plugin_common`(): Boolean = false + + @get:InputFiles + @get:SkipWhenEmpty + @get:IgnoreEmptyDirectories + @get:PathSensitive(PathSensitivity.RELATIVE) + override val sources: FileCollection = super.sources.filter { + !destination.isParentOf(it) + } +} + +@CacheableTask +abstract class KspTaskMetadata @Inject constructor( + compilerOptions: CompilerMultiplatformCommonOptions, + workerExecutor: WorkerExecutor, + objectFactory: ObjectFactory +) : KotlinCompileCommon(compilerOptions, workerExecutor, objectFactory), KspTaskInherited { + override fun configureCompilation( + kotlinCompilation: KotlinCompilationData<*>, + kotlinCompile: AbstractKotlinCompile<*>, + ) { + kotlinCompile as KotlinCompileCommon + val providerFactory = kotlinCompile.project.providers + compileKotlinArgumentsContributor.set( + providerFactory.provider { + kotlinCompile.abstractKotlinCompileArgumentsContributor + } + ) + } + + @get:Internal + internal abstract val compileKotlinArgumentsContributor: + Property> + + 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. + (compilerOptions as CompilerJsOptionsDefault).fillDefaultValues(args) + compileKotlinArgumentsContributor.get().contributeArguments( + args, + compilerArgumentsConfigurationFlags( + defaultsOnly, + ignoreClasspathResolutionErrors + ) + ) + if (blockOtherCompilerPlugins) { + args.blockOtherPlugins(overridePluginClasspath) + } + args.addPluginOptions(options.get()) + args.destination = destination.canonicalPath + val classpathList = libraries.files.filter { it.exists() }.toMutableList() + args.classpath = classpathList.joinToString(File.pathSeparator) + args.friendPaths = friendPaths.files.map { it.absolutePath }.toTypedArray() + args.refinesPaths = refinesMetadataPaths.map { it.absolutePath }.toTypedArray() + args.expectActualLinker = true + args.useK2 = false + } + + // 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_common`( + args: K2MetadataCompilerArguments, + kotlinSources: Set, + inputChanges: InputChanges, + taskOutputsBackup: TaskOutputsBackup? + ) { + val changedFiles = getChangedFiles(inputChanges, incrementalProps) + if (!isKspIncremental || changedFiles.hasNonSourceChange()) { + clearIncCache() + } else { + args.addChangedFiles(changedFiles) + } + super.callCompilerAsync(args, kotlinSources, inputChanges, taskOutputsBackup) + } + + @get:InputFiles + @get:SkipWhenEmpty + @get:IgnoreEmptyDirectories + @get:PathSensitive(PathSensitivity.RELATIVE) + override val sources: FileCollection = super.sources.filter { + !destination.isParentOf(it) + } +} + +@CacheableTask +abstract class KspTaskNative @Inject constructor( + compilation: KotlinNativeCompilationData<*>, + objectFactory: ObjectFactory, + providerFactory: ProviderFactory, + execOperations: ExecOperations +) : KotlinNativeCompile(compilation, objectFactory, providerFactory, execOperations), KspTaskInherited { + override val additionalCompilerOptions: Provider> + get() { + return project.provider { + val kspOptions = options.get().flatMap { listOf("-P", it.toArg()) } + super.additionalCompilerOptions.get() + kspOptions + } + } + + override var compilerPluginClasspath: FileCollection? = null + get() { + if (blockOtherCompilerPlugins) { + field = overridePluginClasspath + } + return field + } + + 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(). + // Use a name that gets sorted in the front just in case. `_$` is lower, but it might be too hacky. + @TaskAction + fun _0() { + options.get().single { it.key == "kspOutputDir" }.value.let { + File(it).deleteRecursively() + } + super.compile() + } + + @get:InputFiles + @get:SkipWhenEmpty + @get:IgnoreEmptyDirectories + @get:PathSensitive(PathSensitivity.RELATIVE) + override val sources: FileCollection = super.sources.filter { + !destination.isParentOf(it) + } +} + +// This forces rebuild. +private fun KspTaskInherited.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) { + pluginOptions = (options.map { it.toArg() } + pluginOptions!!).toTypedArray() +} + +fun CommonCompilerArguments.addChangedFiles(changedFiles: ChangedFiles) { + if (changedFiles is ChangedFiles.Known) { + val options = mutableListOf() + 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 T.toPair(property: KProperty1): Pair { + @Suppress("UNCHECKED_CAST") + val value = (property as KProperty1).get(this) + return property.name to if (value is Array<*>) + value.asList().toString() + else + value.toString() +} + +@Suppress("unused") +internal inline fun dumpArgs(args: T): Map { + @Suppress("UNCHECKED_CAST") + val argumentProperties = + args::class.members.mapNotNull { member -> + (member as? KProperty1)?.takeIf { it.annotations.any { ann -> ann is Argument } } + } + + return argumentProperties.associate(args::toPair).toSortedMap() +} + +internal fun File.isParentOf(childCandidate: File): Boolean { + val parentPath = Paths.get(this.absolutePath).normalize() + val childCandidatePath = Paths.get(childCandidate.absolutePath).normalize() + + return childCandidatePath.startsWith(parentPath) +} diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/tasks/standalone/StandaloneTasks.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/tasks/standalone/StandaloneTasks.kt new file mode 100644 index 0000000000..d0a66ea4b4 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/tasks/standalone/StandaloneTasks.kt @@ -0,0 +1,830 @@ +/* + * 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.tasks.standalone + +import com.google.devtools.ksp.gradle.CompilerOptionsFactory +import com.google.devtools.ksp.gradle.KspExtension +import com.google.devtools.ksp.gradle.KspGradleSubplugin +import com.google.devtools.ksp.gradle.KspGradleSubplugin.Companion.getKspCachesDir +import com.google.devtools.ksp.gradle.KspGradleSubplugin.Companion.getSubpluginOptions +import com.google.devtools.ksp.gradle.KspTask +import com.google.devtools.ksp.gradle.createKotlinJsCompilerRunner +import com.google.devtools.ksp.gradle.createKotlinJvmCompilerRunner +import com.google.devtools.ksp.gradle.createKotlinMetadataCompilerRunner +import com.google.devtools.ksp.gradle.createKotlinNativeCompilerRunner +import com.google.devtools.ksp.gradle.tasks.KspTaskCreator +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.Task +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.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.IgnoreEmptyDirectories +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.SkipWhenEmpty +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import org.gradle.util.GradleVersion +import org.gradle.work.ChangeType +import org.gradle.work.Incremental +import org.gradle.work.InputChanges +import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions +import org.jetbrains.kotlin.gradle.internal.kapt.incremental.CLASS_STRUCTURE_ARTIFACT_TYPE +import org.jetbrains.kotlin.gradle.internal.kapt.incremental.ClasspathSnapshot +import org.jetbrains.kotlin.gradle.internal.kapt.incremental.KaptClasspathChanges +import org.jetbrains.kotlin.gradle.internal.kapt.incremental.StructureTransformAction +import org.jetbrains.kotlin.gradle.internal.kapt.incremental.StructureTransformLegacyAction +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.SubpluginOption +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.enabledOnCurrentHost +import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile +import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool +import org.jetbrains.kotlin.gradle.tasks.CompileUsingKotlinDaemon +import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.tasks.KotlinCompileCommon +import org.jetbrains.kotlin.gradle.tasks.KotlinCompileTool +import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile +import org.jetbrains.kotlin.incremental.ChangedFiles +import org.jetbrains.kotlin.incremental.isJavaFile +import org.jetbrains.kotlin.incremental.isKotlinFile +import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import java.io.File +import java.nio.file.Paths +import javax.inject.Inject +import org.jetbrains.kotlin.gradle.dsl.CompilerCommonOptions +import org.jetbrains.kotlin.gradle.dsl.CompilerCommonToolOptions +import org.jetbrains.kotlin.gradle.dsl.CompilerJsOptions +import org.jetbrains.kotlin.gradle.dsl.CompilerJvmOptions +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask + +object StandaloneTasks : KspTaskCreator { + override fun createKspTask( + project: Project, + kotlinCompilation: KotlinCompilation<*>, + kotlinCompileTask: AbstractKotlinCompileTool<*>, + kspExtension: KspExtension, + kspConfigurations: List, + kspTaskName: String, + target: String, + sourceSetName: String, + classOutputDir: File, + javaOutputDir: File, + kotlinOutputDir: File, + resourceOutputDir: File, + kspOutputDir: File, + ): TaskProvider { + + val kspClasspathConfiguration = project.configurations.getByName( + KspGradleSubplugin.KSP_PLUGIN_CLASSPATH_CONFIGURATION_NAME + ) + + fun configureKspTask(kspTask: KspTaskStandalone, kotlinCompile: KotlinCompileTool, isIncremental: Boolean) { + // depends on the processor; if the processor changes, it needs to be reprocessed. + val processorClasspath = project.configurations.maybeCreate("${kspTaskName}ProcessorClasspath") + .extendsFrom(*kspConfigurations.toTypedArray()) + kspTask.processorClasspath.from(processorClasspath) + kspTask.dependsOn(processorClasspath.buildDependencies) + + kspTask.options.addAll( + kspTask.project.provider { + getSubpluginOptions( + project, + kspExtension, + processorClasspath, + sourceSetName, + target, + isIncremental, + kspExtension.allWarningsAsErrors + ) + } + ) + kspTask.commandLineArgumentProviders.addAll(kspExtension.commandLineArgumentProviders) + kspTask.destination = kspOutputDir + kspTask.apOptions.value(kspExtension.arguments).disallowChanges() + kspTask.kspCacheDir.fileValue(getKspCachesDir(project, sourceSetName, target)).disallowChanges() + kspTask.allWarningAsErrors.value(kspExtension.allWarningsAsErrors) + + kspTask.isKspIncremental = isIncremental + + val providers = project.providers + kspTask.outputs.dirs( + kotlinOutputDir, + javaOutputDir, + classOutputDir, + resourceOutputDir + ) + + fun FileCollection.excludeKSP(): FileCollection = filter { + !kspOutputDir.isParentOf(it) + } + + if (kspExtension.allowSourcesFromOtherPlugins) { + val deps = kotlinCompile.dependsOn.filterNot { + it.safeAs>()?.name == kspTaskName || + it.safeAs()?.name == kspTaskName + } + kspTask.dependsOn(deps) + kspTask.allSources.from(kotlinCompile.sources.excludeKSP()) + if (kotlinCompile is KotlinCompile) { + kspTask.allSources.from(kotlinCompile.javaSources.excludeKSP()) + } + } else { + kotlinCompilation.allKotlinSourceSets.forEach { sourceSet -> + kspTask.allSources.from(sourceSet.kotlin.excludeKSP()) + } + if (kotlinCompilation is KotlinCommonCompilation) { + kspTask.allSources.from(kotlinCompilation.defaultSourceSet.kotlin.excludeKSP()) + } + } + + if (kotlinCompile is KotlinNativeCompile) { + kspTask.commonSources.from(providers.provider { kotlinCompile.commonSources }) + kspTask.friendPaths.from(kotlinCompile.compilation.friendPaths) + kspTask.multiplatform.value(true) + } else if (kotlinCompile is AbstractKotlinCompile<*>) { + kspTask.commonSources.from(providers.provider { kotlinCompile.commonSourceSet }) + kspTask.friendPaths.from(kotlinCompile.friendPaths) + kspTask.multiplatform.set(kotlinCompile.multiPlatformEnabled) + } else { + throw IllegalArgumentException("Unknown compilation task type") + } + kspTask.pluginClasspath.from(kspClasspathConfiguration) + kspTask.libraries.from(providers.provider { kotlinCompile.libraries.excludeKSP() }) + kspTask.kotlinApiVersion.value(kotlinCompilation.kotlinOptions.apiVersion) + kspTask.kotlinLanguageVersion.value(kotlinCompilation.kotlinOptions.languageVersion) + kspTask.verbose.value(kotlinCompilation.kotlinOptions.verbose || project.logger.isDebugEnabled) + + kspTask.configureCompilation(kotlinCompilation as KotlinCompilation, kotlinCompile) + + // Don't support binary generation for non-JVM platforms yet. + // FIXME: figure out how to add user generated libraries. + if (kspTask is KspTaskJvm) { + kotlinCompilation.output.classesDirs.from(classOutputDir) + } + } + + val kspTaskProvider = when (kotlinCompileTask) { + is KotlinCompileTool -> { + val kspTaskClass = when (kotlinCompileTask) { + is KotlinCompile -> KspTaskJvm::class.java + is Kotlin2JsCompile -> KspTaskJs::class.java + is KotlinCompileCommon -> KspTaskMetadata::class.java + is KotlinNativeCompile -> KspTaskNative::class.java + else -> throw IllegalArgumentException("Unknown Kotlin compilation task $kotlinCompileTask") + } + val isNative = kspTaskClass == KspTaskNative::class.java + val isIncremental = project.findProperty("ksp.incremental")?.toString()?.toBoolean() ?: !isNative + project.tasks.register(kspTaskName, kspTaskClass) { kspTask -> + if (isNative) { + // KotlinNativeCompile computes -Xplugin=... from compilerPluginClasspath. + kspTask.pluginClasspath.from(kspClasspathConfiguration) + // The lambda to Task.onlyIf is evaluated at evaluation time. + // It will try to serialize the entire closure and break configuration cache. + val isEnabled = + (kotlinCompilation as AbstractKotlinNativeCompilation).konanTarget.enabledOnCurrentHost + kspTask.onlyIf { isEnabled } + } + configureKspTask(kspTask, kotlinCompileTask, isIncremental) + } + } + else -> throw IllegalArgumentException("Unknown Kotlin compilation task $kotlinCompileTask") + } + + return kspTaskProvider + } +} + +private val artifactType = Attribute.of("artifactType", String::class.java) + +interface KspTaskStandalone : KspTask { + @get:Classpath + @get:Incremental + val libraries: ConfigurableFileCollection + + @get:Classpath + val pluginClasspath: ConfigurableFileCollection + + @get:InputFiles + @get:IgnoreEmptyDirectories + @get:Incremental + @get:PathSensitive(PathSensitivity.RELATIVE) + val commonSources: ConfigurableFileCollection + + @get:InputFiles + @get:SkipWhenEmpty + @get:IgnoreEmptyDirectories + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:Incremental + val allSources: ConfigurableFileCollection + + @get:Input + @get:Optional + val kotlinApiVersion: Property + + @get:Input + @get:Optional + val kotlinLanguageVersion: Property + + @get:Input + val verbose: Property + + @get:Internal + val friendPaths: ConfigurableFileCollection + + @get:Input + val multiplatform: Property + + @get:Input + val allWarningAsErrors: Property + + fun configureCompilation( + kotlinCompilation: KotlinCompilation<*>, + kotlinCompile: KotlinCompileTool, + ) +} + +// This forces rebuild. +private fun KspTaskStandalone.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 MutableList.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 MutableList.addPluginOptions(options: List) { + this.addAll(options.flatMap { listOf("-P", it.toArg()) }) +} + +fun MutableList.addChangedFiles(changedFiles: ChangedFiles) { + if (changedFiles is ChangedFiles.Known) { + val options = mutableListOf() + 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) } + } +} + +internal fun File.isParentOf(childCandidate: File): Boolean { + val parentPath = Paths.get(this.absolutePath).normalize() + val childCandidatePath = Paths.get(childCandidate.absolutePath).normalize() + + return childCandidatePath.startsWith(parentPath) +} + +private fun CompilerCommonToolOptions.from(that: CompilerCommonToolOptions) { + // Copy from compilation task + allWarningsAsErrors.value(that.allWarningsAsErrors) + suppressWarnings.value(that.suppressWarnings) + verbose.value(that.verbose) + + // NOT COPIED: freeCompilerArgs +} + +private fun CompilerCommonOptions.from(that: CompilerCommonOptions) { + // Copy from compilation task + (this as CompilerCommonToolOptions).from(that) + apiVersion.value(that.apiVersion) + languageVersion.value(that.languageVersion) + + // NOT COPIED: + useK2.value(false) +} + +private fun CompilerJvmOptions.from(that: CompilerJvmOptions) { + // Copy from compilation task + (this as CompilerCommonOptions).from(that) + javaParameters.value(that.javaParameters) + jvmTarget.value(that.jvmTarget) + moduleName.value(that.moduleName) + + // NOT COPIED: + noJdk.value(true) +} + +private fun CompilerJsOptions.from(that: CompilerJsOptions) { + // Copy from compilation task + (this as CompilerCommonOptions).from(that) + friendModulesDisabled.value(that.friendModulesDisabled) + main.value(that.main) + metaInfo.value(that.metaInfo) + moduleKind.value(that.moduleKind) + moduleName.value(that.moduleName) + noStdlib.value(that.noStdlib) + target.value(that.target) + + // NOT COPIED: outputFile, sourceMap, sourceMapEmbedSources, sourceMapPrefix, typedArrays +} + +@CacheableTask +abstract class KspTaskJvm @Inject constructor( + objectFactory: ObjectFactory, +) : KspTaskStandalone, CompileUsingKotlinDaemon, DefaultTask() { + @get:Nested + val compilerRunner = createKotlinJvmCompilerRunner(this, objectFactory) + + @get:Input + var isIntermoduleIncremental: Boolean = false + + @get:PathSensitive(PathSensitivity.NONE) + @get:Optional + @get:InputFiles + @get:Incremental + abstract val classpathStructure: ConfigurableFileCollection + + @get:Nested + abstract val classpathSnapshotProperties: ClasspathSnapshotProperties + + @get:Internal + val incrementalProps: List + get() = listOf( + allSources, + commonSources, + classpathSnapshotProperties.classpath, + classpathSnapshotProperties.classpathSnapshot + ) + + @get:Nested + val compilerOptions: CompilerJvmOptions = CompilerOptionsFactory.createCompilerJvmOptions(objectFactory) + + @TaskAction + fun execute(inputChanges: InputChanges) { + val freeArgs = mutableListOf( + "-Xallow-no-source-files", + "-no-stdlib", + ) + + if (multiplatform.get()) + freeArgs.add("-Xmulti-platform") + + freeArgs.addAll(pluginClasspath.map { "-Xplugin=${it.absolutePath}" }) + freeArgs.addPluginOptions(options.get()) + + // Clean outputs. Backups will be copied back if incremental. + // TODO: leave outputs untouched and only restore outputs if build fails. + outputs.files.forEach { + it.deleteRecursively() + } + val changedFiles = getChangedFiles(inputChanges, incrementalProps) + if (isKspIncremental) { + if (isIntermoduleIncremental) { + // findClasspathChanges may clear caches, if there are + // 1. unknown changes, or + // 2. changes in annotation processors. + val classpathChanges = findClasspathChanges(changedFiles) + freeArgs.addChangedClasses(classpathChanges) + } else { + if (changedFiles.hasNonSourceChange()) { + clearIncCache() + } + } + } else { + clearIncCache() + } + freeArgs.addChangedFiles(changedFiles) + + compilerRunner.runJvmCompilerAsync( + compilerOptions, + freeArgs, + allSources.files.filter { !destination.isParentOf(it) }, + commonSources.files.filter { !destination.isParentOf(it) }, + friendPaths.files.toList(), + libraries.files.toList(), + destination + ) + } + + override fun configureCompilation( + kotlinCompilation: KotlinCompilation<*>, + kotlinCompile: KotlinCompileTool, + ) { + val providers = project.providers + + kotlinCompile as AbstractKotlinCompile<*> + compilerRunner.compilerClasspath.from(providers.provider { kotlinCompile.defaultCompilerClasspath }) + compilerRunner.compilerExecutionStrategy.set(kotlinCompile.compilerExecutionStrategy) + compilerRunner.useDaemonFallbackStrategy.set(kotlinCompile.useDaemonFallbackStrategy) + compilerRunner.kotlinDaemonJvmArguments.set(kotlinCompile.kotlinDaemonJvmArguments) + + kotlinCompile as KotlinCompilationTask + compilerOptions.from(kotlinCompile.compilerOptions) + // Android compilation doesn't include JDK libraries. + // Copying the settings from KotlinCompilation is complicated and implementation dependent. So let's check + // for Android explicitly. + compilerOptions.noJdk.value(kotlinCompilation is KotlinJvmAndroidCompilation) + + 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.libraries })) + ) + 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, + libraries.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 + } +} + +@CacheableTask +abstract class KspTaskJs @Inject constructor( + objectFactory: ObjectFactory, +) : KspTaskStandalone, CompileUsingKotlinDaemon, DefaultTask() { + @get:Nested + val compilerRunner = createKotlinJsCompilerRunner(this, objectFactory) + + @get:Nested + val compilerOptions: CompilerJsOptions = CompilerOptionsFactory.createCompilerJsOptions(objectFactory) + + @get:Internal + abstract val freeArgs: ListProperty + + @get:Internal + val incrementalProps: List + get() = listOf( + allSources, + commonSources, + ) + + @TaskAction + fun execute(inputChanges: InputChanges) { + val freeArgs = mutableListOf() + + if (multiplatform.get()) + freeArgs.add("-Xmulti-platform") + + freeArgs.addAll(pluginClasspath.map { "-Xplugin=${it.absolutePath}" }) + freeArgs.addPluginOptions(options.get()) + freeArgs.addAll(this@KspTaskJs.freeArgs.get().filter { it in backendSelectionArgs }) + + // Clean outputs. Backups will be copied back if incremental. + // TODO: leave outputs untouched and only restore outputs if build fails. + outputs.files.forEach { + it.deleteRecursively() + } + val changedFiles = getChangedFiles(inputChanges, incrementalProps) + if (isKspIncremental) { + if (changedFiles.hasNonSourceChange()) { + clearIncCache() + } + } else { + clearIncCache() + } + freeArgs.addChangedFiles(changedFiles) + + compilerRunner.runJsCompilerAsync( + compilerOptions, + freeArgs, + allSources.files.toList(), + commonSources.files.toList(), + friendPaths.files.toList(), + libraries.files.toList(), + destination + ) + } + + private val backendSelectionArgs = setOf( + "-Xir-only", + "-Xir-produce-js", + "-Xir-produce-klib-file", + "-Xir-produce-klib-dir", + "-Xir-build-cache", + "-Xwasm", + ) + + override fun configureCompilation( + kotlinCompilation: KotlinCompilation<*>, + kotlinCompile: KotlinCompileTool, + ) { + val providers = project.providers + + kotlinCompile as AbstractKotlinCompile<*> + compilerRunner.compilerClasspath.from(providers.provider { kotlinCompile.defaultCompilerClasspath }) + compilerRunner.compilerExecutionStrategy.set(kotlinCompile.compilerExecutionStrategy) + compilerRunner.useDaemonFallbackStrategy.set(kotlinCompile.useDaemonFallbackStrategy) + compilerRunner.kotlinDaemonJvmArguments.set(kotlinCompile.kotlinDaemonJvmArguments) + + kotlinCompile as KotlinCompilationTask + compilerOptions.from(kotlinCompile.compilerOptions) + freeArgs.value(kotlinCompile.compilerOptions.freeCompilerArgs) + } +} + +@CacheableTask +abstract class KspTaskMetadata @Inject constructor( + objectFactory: ObjectFactory, +) : KspTaskStandalone, CompileUsingKotlinDaemon, DefaultTask() { + @get:Nested + val compilerRunner = createKotlinMetadataCompilerRunner(this, objectFactory) + + @get:Internal + val incrementalProps: List + get() = listOf( + allSources, + commonSources, + ) + + @get:Nested + val compilerOptions: CompilerCommonOptions = CompilerOptionsFactory.createCompilerCommonOptions(objectFactory) + + @TaskAction + fun execute(inputChanges: InputChanges) { + val freeArgs = mutableListOf( + "-Xexpect-actual-linker" + ) + + if (multiplatform.get()) + freeArgs.add("-Xmulti-platform") + + freeArgs.addAll(pluginClasspath.map { "-Xplugin=${it.absolutePath}" }) + freeArgs.addPluginOptions(options.get()) + + // Clean outputs. Backups will be copied back if incremental. + // TODO: leave outputs untouched and only restore outputs if build fails. + outputs.files.forEach { + it.deleteRecursively() + } + val changedFiles = getChangedFiles(inputChanges, incrementalProps) + if (isKspIncremental) { + if (changedFiles.hasNonSourceChange()) { + clearIncCache() + } + } else { + clearIncCache() + } + freeArgs.addChangedFiles(changedFiles) + + compilerRunner.runMetadataCompilerAsync( + compilerOptions, + freeArgs, + allSources.files.filter { !destination.isParentOf(it) }, + commonSources.files.filter { !destination.isParentOf(it) }, + friendPaths.files.toList(), + libraries.files.toList(), + destination + ) + } + + override fun configureCompilation( + kotlinCompilation: KotlinCompilation, + kotlinCompile: KotlinCompileTool, + ) { + val providers = project.providers + + kotlinCompile as AbstractKotlinCompile<*> + compilerRunner.compilerClasspath.from(providers.provider { kotlinCompile.defaultCompilerClasspath }) + compilerRunner.compilerExecutionStrategy.set(kotlinCompile.compilerExecutionStrategy) + compilerRunner.useDaemonFallbackStrategy.set(kotlinCompile.useDaemonFallbackStrategy) + compilerRunner.kotlinDaemonJvmArguments.set(kotlinCompile.kotlinDaemonJvmArguments) + + kotlinCompile as KotlinCompilationTask + compilerOptions.from(kotlinCompile.compilerOptions) + } +} + +@CacheableTask +abstract class KspTaskNative @Inject constructor( + objectFactory: ObjectFactory, +) : KspTaskStandalone, DefaultTask() { + @get:Nested + val compilerRunner = createKotlinNativeCompilerRunner(this, objectFactory) + + @get:Internal + val incrementalProps: List + get() = listOf( + allSources, + commonSources, + ) + + @get:Input + abstract val target: Property + + @get:Nested + val compilerOptions: CompilerCommonOptions = CompilerOptionsFactory.createCompilerCommonOptions(objectFactory) + + @TaskAction + fun execute(inputChanges: InputChanges) { + val freeArgs = mutableListOf( + "-Xexpect-actual-linker" + ) + + if (multiplatform.get()) + freeArgs.add("-Xmulti-platform") + + freeArgs.addAll(pluginClasspath.map { "-Xplugin=${it.absolutePath}" }) + freeArgs.addPluginOptions(options.get()) + + // Clean outputs. Backups will be copied back if incremental. + // TODO: leave outputs untouched and only restore outputs if build fails. + outputs.files.forEach { + it.deleteRecursively() + } + val changedFiles = getChangedFiles(inputChanges, incrementalProps) + if (isKspIncremental) { + if (changedFiles.hasNonSourceChange()) { + clearIncCache() + } + } else { + clearIncCache() + } + freeArgs.addChangedFiles(changedFiles) + + compilerRunner.runNativeCompilerAsync( + compilerOptions, + freeArgs, + allSources.files.filter { !destination.isParentOf(it) }, + commonSources.files.filter { !destination.isParentOf(it) }, + friendPaths.files.toList(), + libraries.files.toList(), + File(destination, "dummy.out"), + target.get() + ) + } + + override fun configureCompilation( + kotlinCompilation: KotlinCompilation, + kotlinCompile: KotlinCompileTool, + ) { + target.value((kotlinCompile as KotlinNativeCompile).target) + + kotlinCompile as KotlinCompilationTask + compilerOptions.from(kotlinCompile.compilerOptions) + } +} + +internal fun getChangedFiles( + inputChanges: InputChanges, + incrementalProps: List, +) = if (!inputChanges.isIncremental) { + ChangedFiles.Unknown() +} else { + incrementalProps + .fold(mutableListOf() to mutableListOf()) { (modified, removed), prop -> + inputChanges.getFileChanges(prop).forEach { + when (it.changeType) { + ChangeType.ADDED, ChangeType.MODIFIED -> modified.add(it.file) + ChangeType.REMOVED -> removed.add(it.file) + else -> Unit + } + } + modified to removed + } + .run { + ChangedFiles.Known(first, second) + } +} + +/** Properties related to the `kotlin.incremental.useClasspathSnapshot` feature. */ +abstract class ClasspathSnapshotProperties { + @get:Input + abstract val useClasspathSnapshot: Property + + @get:Classpath + @get:Incremental + @get:Optional // Set if useClasspathSnapshot == true + abstract val classpathSnapshot: ConfigurableFileCollection + + // Optional: Set if useClasspathSnapshot == false + // (to restore the existing classpath annotations when the feature is disabled) + @get:Classpath + @get:Incremental + @get:Optional + abstract val classpath: ConfigurableFileCollection + + @get:OutputDirectory + @get:Optional // Set if useClasspathSnapshot == true + abstract val classpathSnapshotDir: DirectoryProperty +} 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..b4e3d39612 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 @@ -88,6 +88,7 @@ class KspIntegrationTestRule( PluginDeclaration.id("com.google.devtools.ksp", testConfig.kspVersion) ) ) + addKspCompilerRunner() } /** @@ -103,6 +104,7 @@ class KspIntegrationTestRule( ) ) addAndroidBoilerplate() + addKspCompilerRunner() } /** @@ -118,6 +120,12 @@ class KspIntegrationTestRule( ) testProject.appModule.buildFileAdditions.add(targets) addAndroidBoilerplate() + addKspCompilerRunner() + } + + private fun addKspCompilerRunner() { + val contents = "\nksp.compiler.runner=${testConfig.kspCompilerRunner}\n" + testProject.rootDir.resolve("gradle.properties").appendText(contents) } private fun addAndroidBoilerplate() { diff --git a/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/testing/TestConfig.kt b/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/testing/TestConfig.kt index 1c048da64d..69faceb28b 100644 --- a/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/testing/TestConfig.kt +++ b/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/testing/TestConfig.kt @@ -41,7 +41,11 @@ data class TestConfig( /** * The version of KSP. */ - val kspVersion: String + val kspVersion: String, + /** + * The compiler runner; Can be standalone or inherited + */ + val kspCompilerRunner: String ) { private val kspProjectProperties by lazy { Properties().also { props -> @@ -73,7 +77,8 @@ data class TestConfig( kspProjectDir = File(props.get("kspProjectRootDir") as String), processorClasspath = props.get("processorClasspath") as String, mavenRepoDir = File(props.get("mavenRepoDir") as String), - kspVersion = props.get("kspVersion") as String + kspVersion = props.get("kspVersion") as String, + kspCompilerRunner = props.get("kspCompilerRunner") as String, ) } } diff --git a/gradle.properties b/gradle.properties index 667d4679a2..a1dd09220c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ # Copied from kotlinc org.gradle.jvmargs=-Duser.country=US -Dkotlin.daemon.jvm.options=-Xmx2200m -Dfile.encoding=UTF-8 -kotlinBaseVersion=1.8.0-dev-2843 +kotlinBaseVersion=1.8.20-dev-649 agpBaseVersion=7.0.0 intellijVersion=203.8084.24 junitVersion=4.12 diff --git a/integration-tests/build.gradle.kts b/integration-tests/build.gradle.kts index 305592c4d5..2fb58572a8 100644 --- a/integration-tests/build.gradle.kts +++ b/integration-tests/build.gradle.kts @@ -17,6 +17,7 @@ tasks.named("test") { systemProperty("kspVersion", version) systemProperty("agpVersion", agpBaseVersion) systemProperty("testRepo", File(rootProject.buildDir, "repos/test").absolutePath) + systemProperty("kspCompilerRunner", project.properties.getOrDefault("ksp.compiler.runner", "inherited") as String) dependsOn(":api:publishAllPublicationsToTestRepository") dependsOn(":gradle-plugin:publishAllPublicationsToTestRepository") dependsOn(":symbol-processing:publishAllPublicationsToTestRepository") diff --git a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/PlaygroundIT.kt b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/PlaygroundIT.kt index 912ae97fe5..0fc3df5cc5 100644 --- a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/PlaygroundIT.kt +++ b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/PlaygroundIT.kt @@ -97,9 +97,7 @@ class PlaygroundIT { @Test fun testBuildCache() { val gradleRunner = GradleRunner.create().withProjectDir(project.root) - gradleRunner.buildAndCheck("--build-cache", ":workload:clean", "build") { - Assert.assertEquals(TaskOutcome.SUCCESS, it.task(":workload:kspKotlin")?.outcome) - } + gradleRunner.buildAndCheck("--build-cache", ":workload:clean", "build") gradleRunner.buildAndCheck("--build-cache", ":workload:clean", "build") { Assert.assertEquals(TaskOutcome.FROM_CACHE, it.task(":workload:kspKotlin")?.outcome) } diff --git a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/TemporaryTestProject.kt b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/TemporaryTestProject.kt index df9cc5b57b..cd8702fc5a 100644 --- a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/TemporaryTestProject.kt +++ b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/TemporaryTestProject.kt @@ -17,12 +17,14 @@ class TemporaryTestProject(projectName: String, baseProject: String? = null) : T val kspVersion = System.getProperty("kspVersion") val agpVersion = System.getProperty("agpVersion") val testRepo = System.getProperty("testRepo").replace(File.separator, "/") + val kspCompilerRunner = System.getProperty("kspCompilerRunner") val gradleProperties = File(root, "gradle.properties") gradleProperties.appendText("\nkotlinVersion=$kotlinVersion") gradleProperties.appendText("\nkspVersion=$kspVersion") gradleProperties.appendText("\nagpVersion=$agpVersion") gradleProperties.appendText("\ntestRepo=$testRepo") gradleProperties.appendText("\norg.gradle.unsafe.configuration-cache=true") + gradleProperties.appendText("\nksp.compiler.runner=$kspCompilerRunner") // Uncomment this to debug compiler and compiler plugin. // gradleProperties.appendText("\nsystemProp.kotlin.compiler.execution.strategy=in-process") }