diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index c7ba22bf88f..11f57fde820 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1510,6 +1510,12 @@ + + + + + + @@ -1656,6 +1662,12 @@ + + + + + + @@ -8564,6 +8576,12 @@ + + + + + + diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt index 9e38d478786..239d8fd6f29 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt @@ -258,7 +258,8 @@ abstract class BaseGradleIT { val verbose: Boolean, val useWorkers: Boolean, val incrementalKapt: Boolean = false, - val includeCompileClasspath: Boolean = true + val includeCompileClasspath: Boolean = true, + val classLoadersCacheSize: Int? = null ) open inner class Project( @@ -913,6 +914,9 @@ Finished executing task ':$taskName'| add("-Pkapt.use.worker.api=${kaptOptions.useWorkers}") add("-Pkapt.incremental.apt=${kaptOptions.incrementalKapt}") add("-Pkapt.include.compile.classpath=${kaptOptions.includeCompileClasspath}") + kaptOptions.classLoadersCacheSize?.also { cacheSize -> + add("-Pkapt.classloaders.cache.size=$cacheSize") + } } options.parallelTasksInProject?.let { diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kapt3IT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kapt3IT.kt index c42ec42b075..13c184b585e 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kapt3IT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kapt3IT.kt @@ -21,11 +21,14 @@ import org.jetbrains.kotlin.gradle.tasks.USING_JVM_INCREMENTAL_COMPILATION_MESSA import org.jetbrains.kotlin.gradle.util.* import org.junit.Assert import org.junit.Assume +import org.junit.Ignore import org.junit.Test import java.io.File +import java.util.regex.Pattern import java.util.zip.ZipFile import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertTrue abstract class Kapt3BaseIT : BaseGradleIT() { companion object { @@ -43,7 +46,7 @@ abstract class Kapt3BaseIT : BaseGradleIT() { } } -class Kapt3WorkersIT : Kapt3IT() { +open class Kapt3WorkersIT : Kapt3IT() { override fun kaptOptions(): KaptOptions = super.kaptOptions().copy(useWorkers = true) @@ -53,7 +56,9 @@ class Kapt3WorkersIT : Kapt3IT() { Project("javacIsLoadedOnce", directoryPrefix = "kapt2") project.build("build") { assertSuccessful() - assertSubstringCount("Loaded com.sun.tools.javac.util.Context from", 1) + + val loadsCount = Pattern.quote("Loaded com.sun.tools.javac.util.Context from").toRegex().findAll(output).count() + assertTrue(loadsCount <= 1, "javac is loaded more than once") } } @@ -95,9 +100,65 @@ class Kapt3WorkersIT : Kapt3IT() { } } +class Kapt3ClassLoadersCacheIT : Kapt3WorkersIT() { + override fun kaptOptions(): KaptOptions = + super.kaptOptions().copy(classLoadersCacheSize = 10, includeCompileClasspath = false) + + @Ignore + override fun testDisableDiscoveryInCompileClasspath() { + //classloaders cache is incompatible with AP discovery in classpath + } + + @Test + override fun testAnnotationProcessorAsFqName() { + val project = Project("annotationProcessorAsFqName", directoryPrefix = "kapt2").also { it.setupWorkingDir() } + + //classloaders caching is not compatible with includeCompileClasspath + project.projectDir.getFileByName("build.gradle").modify { + it.addBeforeSubstring( + "kapt \"org.jetbrains.kotlin:annotation-processor-example:\$kotlin_version\"\n", + "implementation \"org.jetbrains.kotlin:annotation-processor-example" + ) + } + + project.build("build") { + assertSuccessful() + assertKaptSuccessful() + assertTasksExecuted(":compileKotlin", ":compileJava") + assertFileExists("build/generated/source/kapt/main/example/TestClassGenerated.java") + assertFileExists(kotlinClassesDir() + "example/TestClass.class") + assertFileExists(javaClassesDir() + "example/TestClassGenerated.class") + } + } + + @Test + fun testAnnotationProcessorClassIsLoadedOnce() { + val project = Project("javacIsLoadedOnce", directoryPrefix = "kapt2") + + fun CompiledProject.classLoadingCount() = + Pattern.quote("Loaded example.ExampleAnnotationProcessor from").toRegex().findAll(output).count() + + project.build("build") { + assertSuccessful() + assertTasksExecuted(":module1:kaptKotlin", ":module2:kaptKotlin") + assertTrue(classLoadingCount() <= 1, "AP class is loaded more than once") + } + + project.projectDir.getFilesByNames("Module1Class.kt", "Module2Class.kt").forEach { + it.appendText("\n fun touch() = null") + } + + project.build("build") { + assertSuccessful() + assertTasksExecuted(":module1:kaptKotlin", ":module2:kaptKotlin") + assertTrue(classLoadingCount() == 0, "AP class shouldn't be loaded on the second build") + } + } +} + open class Kapt3IT : Kapt3BaseIT() { @Test - fun testAnnotationProcessorAsFqName() { + open fun testAnnotationProcessorAsFqName() { val project = Project("annotationProcessorAsFqName", directoryPrefix = "kapt2") project.build("build") { @@ -487,7 +548,7 @@ open class Kapt3IT : Kapt3BaseIT() { } @Test - fun testDisableDiscoveryInCompileClasspath() = with(Project("kaptAvoidance", directoryPrefix = "kapt2")) { + open fun testDisableDiscoveryInCompileClasspath() = with(Project("kaptAvoidance", directoryPrefix = "kapt2")) { setupWorkingDir() val buildGradle = projectDir.resolve("app/build.gradle") buildGradle.modify { @@ -510,6 +571,12 @@ open class Kapt3IT : Kapt3BaseIT() { @Test fun testKaptAvoidance() = with(Project("kaptAvoidance", directoryPrefix = "kapt2")) { + setupWorkingDir() + + projectDir.resolve("app/build.gradle").modify { + "$it\n\nkapt.includeCompileClasspath = true" + } + build("assemble") { assertSuccessful() assertTasksExecuted( @@ -538,7 +605,10 @@ open class Kapt3IT : Kapt3BaseIT() { // enable discovery projectDir.resolve("app/build.gradle").modify { - "$it\n\nkapt.includeCompileClasspath = false" + it.replace( + "kapt.includeCompileClasspath = true", + "kapt.includeCompileClasspath = false" + ) } build("assemble") { assertSuccessful() @@ -710,7 +780,7 @@ open class Kapt3IT : Kapt3BaseIT() { assertSuccessful() assertKaptSuccessful() assertTasksExecuted(":compileKotlin", ":compileJava") - + } } } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kapt2/mpp-kapt-presence/build.gradle b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kapt2/mpp-kapt-presence/build.gradle index bf41cdefeac..4117c3ba0ef 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kapt2/mpp-kapt-presence/build.gradle +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kapt2/mpp-kapt-presence/build.gradle @@ -3,9 +3,12 @@ buildscript { 'kotlin': [ 'stdlib': [ 'common': "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version", - 'jdk': "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version", - ], + 'jdk': "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + ] ], + 'other' : [ + 'ap_example': "org.jetbrains.kotlin:annotation-processor-example:$kotlin_version" + ] ] repositories { diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kapt2/mpp-kapt-presence/dac/jdk/build.gradle b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kapt2/mpp-kapt-presence/dac/jdk/build.gradle index 065c65527d2..d74da0fe0fd 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kapt2/mpp-kapt-presence/dac/jdk/build.gradle +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kapt2/mpp-kapt-presence/dac/jdk/build.gradle @@ -7,4 +7,5 @@ targetCompatibility = JavaVersion.VERSION_1_8 dependencies { expectedBy project(':dac') implementation deps.kotlin.stdlib.jdk + kapt deps.other.ap_example } diff --git a/libraries/tools/kotlin-gradle-plugin/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin/build.gradle.kts index eb9b8ac6eb3..cf82719c354 100644 --- a/libraries/tools/kotlin-gradle-plugin/build.gradle.kts +++ b/libraries/tools/kotlin-gradle-plugin/build.gradle.kts @@ -58,6 +58,7 @@ dependencies { embedded(project(":kotlin-gradle-build-metrics")) implementation("com.google.code.gson:gson:${rootProject.extra["versions.jar.gson"]}") + implementation("com.google.guava:guava:${rootProject.extra["versions.jar.guava"]}") implementation("de.undercouch:gradle-download-task:4.1.1") implementation("com.github.gundy:semver4j:0.16.4:nodeps") { exclude(group = "*") @@ -81,7 +82,7 @@ dependencies { runtimeOnly(project(":kotlin-reflect")) jarContents(compileOnly(intellijDep()) { - includeJars("asm-all", "gson", "serviceMessages", rootProject = rootProject) + includeJars("asm-all", "gson", "guava", "serviceMessages", rootProject = rootProject) }) // com.android.tools.build:gradle has ~50 unneeded transitive dependencies diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/Kapt3KotlinGradleSubplugin.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/Kapt3KotlinGradleSubplugin.kt index 13f6f46881f..1e039491999 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/Kapt3KotlinGradleSubplugin.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/Kapt3KotlinGradleSubplugin.kt @@ -15,6 +15,7 @@ import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.ExternalDependency import org.gradle.api.attributes.Attribute import org.gradle.api.attributes.Usage import org.gradle.api.file.FileCollection @@ -78,6 +79,9 @@ class Kapt3GradleSubplugin @Inject internal constructor(private val registry: To private val KAPT_KOTLIN_GENERATED = "kapt.kotlin.generated" + private val CLASSLOADERS_CACHE_SIZE = "kapt.classloaders.cache.size" + private val CLASSLOADERS_CACHE_DISABLE_FOR_PROCESSORS = "kapt.classloaders.cache.disableForProcessors" + val MAIN_KAPT_CONFIGURATION_NAME = "kapt" const val KAPT_ARTIFACT_NAME = "kotlin-annotation-processing-gradle" @@ -118,6 +122,28 @@ class Kapt3GradleSubplugin @Inject internal constructor(private val registry: To return getBooleanOptionValue(BooleanOption.KAPT_KEEP_KDOC_COMMENTS_IN_STUBS) } + fun Project.classLoadersCacheSize(): Int = findPropertySafe(CLASSLOADERS_CACHE_SIZE)?.toString()?.toInt() ?: 0 + + fun Project.disableClassloaderCacheForProcessors(): Set { + val value = findPropertySafe(CLASSLOADERS_CACHE_DISABLE_FOR_PROCESSORS)?.toString() ?: "" + return value + .split(",") + .map { it.trim() } + .filter { it.isNotEmpty() } + .toSet() + } + + /** + * In case [Project.findProperty] can throw exception, this version catch it and return null + */ + private fun Project.findPropertySafe(propertyName: String): Any? = + try { + findProperty(propertyName) + } catch (ex: Exception) { + logger.warn("Error getting property $propertyName", ex) + null + } + fun findMainKaptConfiguration(project: Project) = project.findKaptConfiguration(SourceSet.MAIN_SOURCE_SET_NAME) fun createAptConfigurationIfNeeded(project: Project, sourceSetName: String): Configuration { @@ -511,7 +537,9 @@ class Kapt3GradleSubplugin @Inject internal constructor(private val registry: To it.isVisible = false it.isCanBeConsumed = false } + kaptTask.kaptClasspath.from(kaptClasspathConfiguration) + kaptTask.kaptExternalClasspath.from(*kaptClasspathConfiguration.files { it is ExternalDependency }.toTypedArray()) kaptTask.kaptClasspathConfigurationNames.set(kaptClasspathConfigurations.map { it.name }) KaptWithAndroid.androidVariantData(this)?.annotationProcessorOptionProviders?.let { @@ -569,6 +597,15 @@ class Kapt3GradleSubplugin @Inject internal constructor(private val registry: To it.mapDiagnosticLocations = kaptExtension.mapDiagnosticLocations it.annotationProcessorFqNames = kaptExtension.processors.split(',').filter { it.isNotEmpty() } it.javacOptions = dslJavacOptions.get() + if (includeCompileClasspath && project.classLoadersCacheSize() > 0) { + project.logger.warn( + "ClassLoaders cache can't be enabled together with AP discovery in compilation classpath." + + "\nSet 'kapt.includeCompileClasspath = false' to disable discovery" + ) + } else { + it.classLoadersCacheSize = project.classLoadersCacheSize() + } + it.disableClassloaderCacheForProcessors = project.disableClassloaderCacheForProcessors() } val subpluginOptions = getKaptApOptions() diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptTask.kt index 66f049e0561..8506a386f52 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptTask.kt @@ -53,6 +53,12 @@ abstract class KaptTask @Inject constructor( @get:InputFiles abstract val kaptClasspath: ConfigurableFileCollection + //part of kaptClasspath consisting from external artifacts only + //basically kaptClasspath = kaptExternalClasspath + artifacts built locally + @get:Classpath + @get:InputFiles + abstract val kaptExternalClasspath: ConfigurableFileCollection + @get:Classpath @get:InputFiles val compilerClasspath: List by project.provider { @@ -316,4 +322,4 @@ abstract class KaptTask @Inject constructor( } return false } -} \ No newline at end of file +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptWithoutKotlincTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptWithoutKotlincTask.kt index ed4d4923856..5b62b1dbf6d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptWithoutKotlincTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptWithoutKotlincTask.kt @@ -17,6 +17,8 @@ import org.gradle.workers.WorkAction import org.gradle.workers.WorkParameters import org.gradle.workers.WorkerExecutor import org.jetbrains.kotlin.gradle.internal.Kapt3GradleSubplugin.Companion.KAPT_WORKER_DEPENDENCIES_CONFIGURATION_NAME +import org.jetbrains.kotlin.gradle.internal.kapt.classloaders.ClassLoadersCache +import org.jetbrains.kotlin.gradle.internal.kapt.classloaders.rootOrSelf import org.jetbrains.kotlin.gradle.internal.kapt.incremental.KaptIncrementalChanges import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper import org.jetbrains.kotlin.gradle.tasks.CompilerPluginOptions @@ -24,6 +26,7 @@ import org.jetbrains.kotlin.gradle.tasks.findKotlinStdlibClasspath import org.jetbrains.kotlin.gradle.tasks.findToolsJar import org.jetbrains.kotlin.gradle.utils.isGradleVersionAtLeast import org.jetbrains.kotlin.utils.PathUtil +import org.slf4j.LoggerFactory import java.io.File import java.io.Serializable import java.net.URL @@ -44,6 +47,12 @@ abstract class KaptWithoutKotlincTask @Inject constructor( @get:Input var isVerbose: Boolean = false + @get:Input + var classLoadersCacheSize: Int = 0 + + @get:Input + var disableClassloaderCacheForProcessors: Set = emptySet() + @get:Input var mapDiagnosticLocations: Boolean = false @@ -124,12 +133,15 @@ abstract class KaptWithoutKotlincTask @Inject constructor( stubsDir.asFile.get(), kaptClasspath.files.toList(), + kaptExternalClasspath.files.toList(), annotationProcessorFqNames, getAnnotationProcessorOptions(), javacOptions, - kaptFlagsForWorker + kaptFlagsForWorker, + + disableClassloaderCacheForProcessors ) // Skip annotation processing if no annotation processors were provided. @@ -184,6 +196,7 @@ abstract class KaptWithoutKotlincTask @Inject constructor( it.workerOptions.set(optionsForWorker) it.toolsJarURLSpec.set(toolsJarURLSpec) it.kaptClasspath.setFrom(kaptClasspath) + it.classloadersCacheSize.set(classLoadersCacheSize) } } @@ -198,6 +211,7 @@ abstract class KaptWithoutKotlincTask @Inject constructor( val workerOptions: Property val toolsJarURLSpec: Property val kaptClasspath: ConfigurableFileCollection + val classloadersCacheSize: Property } internal abstract class KaptExecutionWorkAction : WorkAction { @@ -205,7 +219,8 @@ abstract class KaptWithoutKotlincTask @Inject constructor( KaptExecution( parameters.workerOptions.get(), parameters.toolsJarURLSpec.get(), - parameters.kaptClasspath.toList() + parameters.kaptClasspath.toList(), + parameters.classloadersCacheSize.get() ).run() } } @@ -215,29 +230,40 @@ abstract class KaptWithoutKotlincTask @Inject constructor( private class KaptExecution @Inject constructor( val optionsForWorker: KaptOptionsForWorker, val toolsJarURLSpec: String, - val kaptClasspath: List + val kaptClasspath: List, + val classloadersCacheSize: Int ) : Runnable { private companion object { private const val JAVAC_CONTEXT_CLASS = "com.sun.tools.javac.util.Context" private fun kaptClass(classLoader: ClassLoader) = Class.forName("org.jetbrains.kotlin.kapt3.base.Kapt", true, classLoader) - private var cachedClassLoaderWithToolsJar: ClassLoader? = null + + private var classLoadersCache: ClassLoadersCache? = null + private var cachedKaptClassLoader: ClassLoader? = null } + private val logger = LoggerFactory.getLogger(KaptExecution::class.java) + override fun run(): Unit = with(optionsForWorker) { val kaptClasspathUrls = kaptClasspath.map { it.toURI().toURL() }.toTypedArray() val rootClassLoader = findRootClassLoader() - val classLoaderWithToolsJar = cachedClassLoaderWithToolsJar ?: if (!toolsJarURLSpec.isEmpty() && !javacIsAlreadyHere()) { - URLClassLoader(arrayOf(URL(toolsJarURLSpec)), rootClassLoader) - } else { - rootClassLoader + val kaptClassLoader = cachedKaptClassLoader ?: run { + val classLoaderWithToolsJar = if (toolsJarURLSpec.isNotEmpty() && !javacIsAlreadyHere()) { + URLClassLoader(arrayOf(URL(toolsJarURLSpec)), rootClassLoader) + } else { + rootClassLoader + } + val result = URLClassLoader(kaptClasspathUrls, classLoaderWithToolsJar) + cachedKaptClassLoader = result + result } - cachedClassLoaderWithToolsJar = classLoaderWithToolsJar - val kaptClassLoader = cachedKaptClassLoader ?: URLClassLoader(kaptClasspathUrls, classLoaderWithToolsJar) - cachedKaptClassLoader = kaptClassLoader + if (classLoadersCache == null && classloadersCacheSize > 0) { + logger.info("Initializing KAPT classloaders cache with size = $classloadersCacheSize") + classLoadersCache = ClassLoadersCache(classloadersCacheSize, kaptClassLoader) + } val kaptMethod = kaptClass(kaptClassLoader).declaredMethods.single { it.name == "kapt" } kaptMethod.invoke(null, createKaptOptions(kaptClassLoader)) @@ -260,6 +286,15 @@ private class KaptExecution @Inject constructor( val detectMemoryLeaksMode = Class.forName("org.jetbrains.kotlin.base.kapt3.DetectMemoryLeaksMode", true, classLoader) .enumConstants.single { (it as Enum<*>).name == "NONE" } + //in case cache was enabled and then disabled + //or disabled for some modules + val processingClassLoader = + if (classloadersCacheSize > 0) { + classLoadersCache!!.getForSplitPaths(processingClasspath - processingExternalClasspath, processingExternalClasspath) + } else { + null + } + Class.forName("org.jetbrains.kotlin.base.kapt3.KaptOptions", true, classLoader).constructors.single().newInstance( projectBaseDir, compileClasspath, @@ -283,17 +318,14 @@ private class KaptExecution @Inject constructor( flags, mode, - detectMemoryLeaksMode + detectMemoryLeaksMode, + + processingClassLoader, + disableClassloaderCacheForProcessors ) } - private fun findRootClassLoader(): ClassLoader { - tailrec fun parentOrSelf(classLoader: ClassLoader): ClassLoader { - val parent = classLoader.parent ?: return classLoader - return parentOrSelf(parent) - } - return parentOrSelf(KaptExecution::class.java.classLoader) - } + private fun findRootClassLoader(): ClassLoader = KaptExecution::class.java.classLoader.rootOrSelf() } internal data class KaptOptionsForWorker( @@ -311,10 +343,13 @@ internal data class KaptOptionsForWorker( val stubsOutputDir: File, val processingClasspath: List, + val processingExternalClasspath: List, val processors: List, val processingOptions: Map, val javacOptions: Map, - val flags: Set + val flags: Set, + + val disableClassloaderCacheForProcessors: Set ) : Serializable diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/classloaders/ClassLoadersCache.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/classloaders/ClassLoadersCache.kt new file mode 100644 index 00000000000..2c0bc35d4c0 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/classloaders/ClassLoadersCache.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.gradle.internal.kapt.classloaders + +import com.google.common.cache.CacheBuilder +import org.slf4j.LoggerFactory +import java.io.File +import java.net.URL +import java.net.URLClassLoader +import java.time.Duration +import java.util.concurrent.ConcurrentMap + +/** + * LRU cache for [ClassLoader]s by class path. + */ +class ClassLoadersCache( + size: Int, + private val parentClassLoader: ClassLoader = ClassLoader.getSystemClassLoader(), + ttl: Duration = Duration.ofHours(1) +) : AutoCloseable { + + private val logger = LoggerFactory.getLogger(ClassLoadersCache::class.java) + + private val cache: ConcurrentMap = + CacheBuilder + .newBuilder() + .maximumSize(size.toLong()) + .expireAfterAccess(ttl) + .removalListener { (key, cl) -> + logger.info("Removing classloader from cache: ${key.entries.map { it.path }}") + cl.close() + } + .build() + .asMap() + + fun getForClassPath(files: List): ClassLoader = getForClassPath(files, parentClassLoader) + + private fun getForClassPath(files: List, parent: ClassLoader): ClassLoader { + val key = makeKey(files) + return cache.getOrPut(key) { + makeClassLoader(key, parent) + } + } + + /** + * Gets or creates [ClassLoader] from [bottom] + [top] files. + * When creating new [ClassLoader] it tries to get [top] from cache first and then create new ClassLoader from [bottom] files, + * providing [top] [ClassLoader] as parent. + * Useful when you have internal and external artifacts and internal ones can be references from other internal artefacts only. + * So you can safely cache [ClassLoader] from external artifacts and use it for internal ones. + */ + fun getForSplitPaths(bottom: List, top: List): ClassLoader { + return if (bottom.isEmpty() || top.isEmpty()) { + getForClassPath(bottom + top) + } else { + val key = makeKey(bottom + top) + cache.getOrPut(key) { + val parent = getForClassPath(top) + makeClassLoader(makeKey(bottom), parent) + } + } + } + + override fun close() { + cache.clear() + } + + private fun makeClassLoader(key: CacheKey, parent: ClassLoader): URLClassLoader { + val cp = key.entries.map { it.path } + logger.info("Creating new classloader for classpath: $cp") + return URLClassLoader(cp.toTypedArray(), parent) + } + + private fun makeKey(files: List): CacheKey { + //probably should walk dirs content for actual last modified + val entries = files.map { f -> ClasspathEntry(f.toURI().toURL(), f.lastModified()) } + return CacheKey(entries) + } + + private data class ClasspathEntry(val path: URL, val modificationTimestamp: Long) + + private data class CacheKey(val entries: List) +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/classloaders/classLoadersUtils.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/classloaders/classLoadersUtils.kt new file mode 100644 index 00000000000..1a842d995d1 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/classloaders/classLoadersUtils.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.gradle.internal.kapt.classloaders + + +internal fun ClassLoader.rootOrSelf(): ClassLoader { + tailrec fun parentOrSelf(classLoader: ClassLoader): ClassLoader { + val parent = classLoader.parent ?: return classLoader + return parentOrSelf(parent) + } + + return parentOrSelf(this) +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/jarSearchingUtil.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/jarSearchingUtil.kt index 2dc57c81f0f..dc64cdeca9c 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/jarSearchingUtil.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/jarSearchingUtil.kt @@ -119,7 +119,7 @@ internal fun findToolsJar(): File? { return javacUtilContextClass?.let(::findJarByClass) } -private fun findJarByClass(klass: Class<*>): File? { +internal fun findJarByClass(klass: Class<*>): File? { val classFileName = klass.name.substringAfterLast(".") + ".class" val resource = klass.getResource(classFileName) ?: return null val uri = resource.toString() diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/classloaders/ClassLoadersCacheTest.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/classloaders/ClassLoadersCacheTest.kt new file mode 100644 index 00000000000..bd477f8c07b --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/classloaders/ClassLoadersCacheTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.gradle.internal.kapt.classloaders + +import com.google.gson.Gson +import org.jetbrains.kotlin.gradle.tasks.findJarByClass +import org.junit.Test +import kotlin.test.assertNotSame +import kotlin.test.assertSame + +class ClassLoadersCacheTest { + + private val rootClassLoader = this.javaClass.classLoader.rootOrSelf() + + private val someClass = Test::class.java + private val someJar = findJarByClass(someClass)!! + + private val otherClass = Gson::class.java + private val otherJar = findJarByClass(otherClass)!! + + @Test + fun testNewClassLoader() { + val cache = ClassLoadersCache(10, rootClassLoader) + val cl = cache.getForClassPath(listOf(someJar)) + val loaded = cl.loadClass(someClass.name) + assertNotSame(someClass, loaded, "Class should be from different ClassLoader") + } + + @Test + fun testCacheClassLoader() { + val cache = ClassLoadersCache(10, rootClassLoader) + val cp = listOf(someJar) + + val cl1 = cache.getForClassPath(cp) + val loaded1 = cl1.loadClass(someClass.name) + + val cl2 = cache.getForClassPath(cp) + val loaded2 = cl2.loadClass(someClass.name) + + assertSame(loaded2, loaded1, "Should return the same ClassLoader for same class path") + } + + @Test + fun testDifferentClassPath() { + val cache = ClassLoadersCache(10, rootClassLoader) + + val cl1 = cache.getForClassPath(listOf(someJar)) + val loaded1 = cl1.loadClass(someClass.name) + + val cl2 = cache.getForClassPath(listOf(someJar, otherJar)) + val loaded2 = cl2.loadClass(someClass.name) + + assertNotSame(loaded2, loaded1, "Should create different ClassLoaders for different class paths") + } + + @Test + fun testSplitClassPath() { + val cache = ClassLoadersCache(10, rootClassLoader) + val topCp = listOf(someJar) + val bottomCp1 = listOf(otherJar) + val bottomCp2 = listOf(otherJar, findJarByClass(JvmField::class.java)!!) + + val cl1 = cache.getForSplitPaths(bottomCp1, topCp) + val cl2 = cache.getForSplitPaths(bottomCp2, topCp) + + assertSame( + cl1.loadClass(someClass.name), + cl2.loadClass(someClass.name), + "Top classpath should be cached separately. ClassLoader shouldn't change if top classpath stays the same" + ) + assertNotSame( + cl1.loadClass(otherClass.name), + cl2.loadClass(otherClass.name), + "Bottom ClassLoader should be recreated as class path changed" + ) + } +} diff --git a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/KaptOptions.kt b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/KaptOptions.kt index 8e5a6b17ffb..d58541ad76d 100644 --- a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/KaptOptions.kt +++ b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/KaptOptions.kt @@ -35,7 +35,14 @@ class KaptOptions( val flags: KaptFlags, val mode: AptMode, - val detectMemoryLeaks: DetectMemoryLeaksMode + val detectMemoryLeaks: DetectMemoryLeaksMode, + + //these two config can be replaced with single function-like interface (ProcessorName -> ClassLoader), + // but it is hard to pass function between different classloaders + //if defined use it to run processors instead of creating new one + val processingClassLoader: ClassLoader?, + //construct new classloader for these processors instead of using one defined in processingClassLoader + val separateClassloaderForProcessors: Set ) : KaptFlags { override fun get(flag: KaptFlag) = flags[flag] @@ -76,7 +83,9 @@ class KaptOptions( changedFiles, compiledSources, incrementalCache, classpathChanges, sourcesOutputDir, classesOutputDir, stubsOutputDir, incrementalDataOutputDir, processingClasspath, processors, processingOptions, javacOptions, KaptFlags.fromSet(flags), - mode, detectMemoryLeaks + mode, detectMemoryLeaks, + processingClassLoader = null, + separateClassloaderForProcessors = emptySet() ) } } @@ -203,4 +212,4 @@ fun KaptOptions.logString(additionalInfo: String = "") = buildString { appendLine("[incremental apt] Compiled sources directories: ${compiledSources.joinToString()}") appendLine("[incremental apt] Cache directory for incremental compilation: $incrementalCache") appendLine("[incremental apt] Changed classpath names: ${classpathChanges.joinToString()}") -} \ No newline at end of file +} diff --git a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/ProcessorLoader.kt b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/ProcessorLoader.kt index 01bcb6beedb..5ba70354d43 100644 --- a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/ProcessorLoader.kt +++ b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/ProcessorLoader.kt @@ -32,6 +32,7 @@ open class ProcessorLoader(private val options: KaptOptions, private val logger: addAll(options.compileClasspath) } } + val classLoader = URLClassLoader(classpath.map { it.toURI().toURL() }.toTypedArray(), parentClassLoader) this.annotationProcessingClassLoader = classLoader @@ -74,7 +75,7 @@ open class ProcessorLoader(private val options: KaptOptions, private val logger: } } - open fun doLoadProcessors(classpath: LinkedHashSet, classLoader: URLClassLoader): List { + open fun doLoadProcessors(classpath: LinkedHashSet, classLoader: ClassLoader): List { val processorNames = mutableSetOf() fun processSingleInput(input: InputStream) { @@ -119,8 +120,17 @@ open class ProcessorLoader(private val options: KaptOptions, private val logger: } private fun tryLoadProcessor(fqName: String, classLoader: ClassLoader): Processor? { + val providedClassloader = options.processingClassLoader?.takeIf { !options.separateClassloaderForProcessors.contains(fqName) } + val classLoaderToUse = if (providedClassloader != null) { + logger.info { "Use provided ClassLoader for processor '$fqName'" } + providedClassloader + } else { + logger.info { "Use own ClassLoader for processor '$fqName'" } + classLoader + } + val annotationProcessorClass = try { - Class.forName(fqName, true, classLoader) + Class.forName(fqName, true, classLoaderToUse) } catch (e: Throwable) { logger.warn("Can't find annotation processor class $fqName: ${e.message}") return null @@ -143,4 +153,4 @@ open class ProcessorLoader(private val options: KaptOptions, private val logger: override fun close() { annotationProcessingClassLoader?.close() } -} \ No newline at end of file +} diff --git a/plugins/kapt3/kapt3-compiler/src/org/jetbrains/kotlin/kapt3/Kapt3Extension.kt b/plugins/kapt3/kapt3-compiler/src/org/jetbrains/kotlin/kapt3/Kapt3Extension.kt index 2e00ae90d63..57dee5a7180 100644 --- a/plugins/kapt3/kapt3-compiler/src/org/jetbrains/kotlin/kapt3/Kapt3Extension.kt +++ b/plugins/kapt3/kapt3-compiler/src/org/jetbrains/kotlin/kapt3/Kapt3Extension.kt @@ -81,9 +81,11 @@ class ClasspathBasedKapt3Extension( override fun loadProcessors(): LoadedProcessors { val efficientProcessorLoader = object : ProcessorLoader(options, logger) { - override fun doLoadProcessors(classpath: LinkedHashSet, classLoader: URLClassLoader): List { - return ServiceLoaderLite.loadImplementations(Processor::class.java, classLoader) - } + override fun doLoadProcessors(classpath: LinkedHashSet, classLoader: ClassLoader): List = + when (classLoader) { + is URLClassLoader -> ServiceLoaderLite.loadImplementations(Processor::class.java, classLoader) + else -> super.doLoadProcessors(classpath, classLoader) + } } this.processorLoader = efficientProcessorLoader