diff --git a/compiler/build.gradle.kts b/compiler/build.gradle.kts index 81fcf9f92b7..02f386d7d7e 100644 --- a/compiler/build.gradle.kts +++ b/compiler/build.gradle.kts @@ -1,14 +1,20 @@ - import java.io.File import org.gradle.api.tasks.bundling.Jar import org.jetbrains.kotlin.gradle.dsl.KotlinCompile +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile plugins { kotlin("jvm") id("jps-compatible") } -jvmTarget = "1.6" +tasks.named("compileKotlin") { + kotlinOptions.jvmTarget = "1.6" +} + +tasks.named("compileTestKotlin") { + kotlinOptions.jvmTarget = "1.8" +} val compilerModules: Array by rootProject.extra val otherCompilerModules = compilerModules.filter { it != path } diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/plugins/PluginCliParser.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/plugins/PluginCliParser.kt index 3695e724100..5974a2285f6 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/plugins/PluginCliParser.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/plugins/PluginCliParser.kt @@ -67,7 +67,7 @@ object PluginCliParser { this::class.java.classLoader ) - val componentRegistrars = ServiceLoader.load(ComponentRegistrar::class.java, classLoader).toMutableList() + val componentRegistrars = ServiceLoaderLite.loadImplementations(ComponentRegistrar::class.java, classLoader) configuration.addAll(ComponentRegistrar.PLUGIN_COMPONENT_REGISTRARS, componentRegistrars) processPluginOptions(pluginOptions, configuration, classLoader) diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/plugins/ServiceLoaderLite.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/plugins/ServiceLoaderLite.kt new file mode 100644 index 00000000000..807a6ed40ef --- /dev/null +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/plugins/ServiceLoaderLite.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. 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.cli.jvm.plugins + +import java.io.File +import java.io.IOError +import java.lang.Character.isJavaIdentifierPart +import java.lang.Character.isJavaIdentifierStart +import java.lang.IllegalArgumentException +import java.lang.RuntimeException +import java.net.URLClassLoader +import java.util.zip.ZipFile + +/** + * ServiceLoader has a file handle leak in JDK8: https://bugs.openjdk.java.net/browse/JDK-8156014. + * This class, hopefully, doesn't. :) + */ +object ServiceLoaderLite { + private const val SERVICE_DIRECTORY_LOCATION = "META-INF/services/" + + class ServiceLoadingException(val file: File, cause: Throwable) : RuntimeException("Error loading services from $file", cause) + + /** + * Returns implementations for the given `service` declared in META-INF/services of the `classLoader` roots. + * + * Note that the behavior is radically different from what Java ServiceLoader does. + * ServiceLoaderLite doesn't iterate over the whole ClassLoader hierarchy, it takes only the immediate roots of `classLoader`. + * In fact, this is often the desired behavior. + */ + fun loadImplementations(service: Class, classLoader: URLClassLoader): List { + val files = classLoader.urLs.map { url -> + if (url.protocol.toLowerCase() != "file") throw IllegalArgumentException("Only local files are supported, got $url") + val path = url.path.takeIf { it.isNotEmpty() } ?: throw IllegalArgumentException("Path is empty for $url") + File(path) + } + + val implementations = mutableListOf() + + for (className in findImplementations(service, files)) { + val instance = Class.forName(className, false, classLoader).newInstance() + implementations += service.cast(instance) + } + + return implementations + } + + inline fun findImplementations(files: List): Set { + return findImplementations(Service::class.java, files) + } + + inline fun loadImplementations(classLoader: URLClassLoader): List { + return loadImplementations(Service::class.java, classLoader) + } + + fun findImplementations(service: Class<*>, files: List): Set { + return files.flatMapTo(linkedSetOf()) { findImplementations(service, it) } + } + + private fun findImplementations(service: Class<*>, file: File): Set { + val classIdentifier = getClassIdentifier(service) + + return when { + file.isDirectory -> findImplementationsInDirectory(classIdentifier, file) + file.isFile -> findImplementationsInJar(classIdentifier, file) + else -> emptySet() + } + } + + private fun findImplementationsInDirectory(classId: String, file: File): Set { + val serviceFile = File(file, SERVICE_DIRECTORY_LOCATION + classId).takeIf { it.isFile } ?: return emptySet() + + try { + return serviceFile.useLines { parseLines(file, it) } + } catch (e: IOError) { + throw ServiceLoadingException(file, e) + } + } + + private fun findImplementationsInJar(classId: String, file: File): Set { + ZipFile(file).use { zipFile -> + val entry = zipFile.getEntry(SERVICE_DIRECTORY_LOCATION + classId) ?: return emptySet() + zipFile.getInputStream(entry).use { inputStream -> + return inputStream.bufferedReader().useLines { parseLines(file, it) } + } + } + } + + private fun parseLines(file: File, lines: Sequence): Set { + return lines.mapNotNullTo(linkedSetOf()) { parseLine(file, it) } + } + + private fun parseLine(file: File, line: String): String? { + val actualLine = line.substringBefore('#').trim().takeIf { it.isNotEmpty() } ?: return null + + actualLine.forEachIndexed { index: Int, c: Char -> + val isValid = if (index == 0) isJavaIdentifierStart(c) else isJavaIdentifierPart(c) || c == '.' + if (!isValid) { + val errorText = "Invalid Java identifier: $line" + throw ServiceLoadingException(file, RuntimeException(errorText)) + } + } + + return actualLine + } + + private fun getClassIdentifier(service: Class<*>): String { + return service.name + } +} \ No newline at end of file diff --git a/compiler/tests/org/jetbrains/kotlin/cli/serviceLoaderLite/AbstractServiceLoaderLiteTest.kt b/compiler/tests/org/jetbrains/kotlin/cli/serviceLoaderLite/AbstractServiceLoaderLiteTest.kt new file mode 100644 index 00000000000..72760b7233e --- /dev/null +++ b/compiler/tests/org/jetbrains/kotlin/cli/serviceLoaderLite/AbstractServiceLoaderLiteTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. 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.cli.serviceLoaderLite + +import org.jetbrains.kotlin.test.TestCaseWithTmpdir +import java.io.File +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import javax.annotation.processing.Processor + +abstract class AbstractServiceLoaderLiteTest : TestCaseWithTmpdir() { + protected fun applyForDirAndJar(name: String, vararg entries: Entry, block: (File) -> Unit) { + val zip = writeJar("$name.jar", *entries) + block(zip) + + val dir = writeDir(name, *entries) + block(dir) + } + + protected fun writeDir(dirName: String, vararg entries: Entry): File { + val dir = File(tmpdir, dirName) + if (dir.exists()) { + throw IllegalStateException("Directory $dirName already exists") + } + + dir.mkdir() + + for ((name, content) in entries) { + val file = File(dir, name).also { it.parentFile.mkdirs() } + file.writeBytes(content) + } + + return dir + } + + protected fun writeJar(fileName: String, vararg entries: Entry): File { + val file = File(tmpdir, fileName) + if (file.exists()) { + throw IllegalStateException("File $fileName already exists") + } + + file.outputStream().use { os -> + ZipOutputStream(os).use { zos -> + for ((name, content) in entries) { + zos.putNextEntry(ZipEntry(name)) + zos.write(content) + } + } + } + + return file + } + + protected inline fun assertThrows(block: () -> Unit) { + try { + block() + } catch (e: Throwable) { + if (e !is E) { + fail(E::class.java.name + " exception expected, got " + e.javaClass.name) + } + return + } + + fail(E::class.java.name + " exception expected, got nothing") + } + + protected data class Entry(val name: String, val content: ByteArray) { + constructor(name: String, content: String) : this(name, content.toByteArray()) + } + + protected fun processors(content: String) = Entry( + "META-INF/services/" + Processor::class.java.name, + content + ) +} \ No newline at end of file diff --git a/compiler/tests/org/jetbrains/kotlin/cli/serviceLoaderLite/ServiceLoaderLiteTest.kt b/compiler/tests/org/jetbrains/kotlin/cli/serviceLoaderLite/ServiceLoaderLiteTest.kt new file mode 100644 index 00000000000..2fed602ec18 --- /dev/null +++ b/compiler/tests/org/jetbrains/kotlin/cli/serviceLoaderLite/ServiceLoaderLiteTest.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. 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.cli.serviceLoaderLite + +import org.jetbrains.kotlin.cli.jvm.plugins.ServiceLoaderLite +import org.jetbrains.kotlin.cli.jvm.plugins.ServiceLoaderLite.ServiceLoadingException +import javax.annotation.processing.Processor + +class ServiceLoaderLiteTest : AbstractServiceLoaderLiteTest() { + fun testSimple() = applyForDirAndJar("test", processors("test.Foo")) { file -> + val impls = ServiceLoaderLite.findImplementations(listOf(file)) + assertEquals("test.Foo", impls.single()) + } + + fun testEmpty() = applyForDirAndJar("test") { file -> + val impls = ServiceLoaderLite.findImplementations(listOf(file)) + assertEquals(0, impls.size) + } + + fun testEmpty2() = applyForDirAndJar("test", Entry("foo", "bar")) { file -> + val impls = ServiceLoaderLite.findImplementations(listOf(file)) + assertEquals(0, impls.size) + } + + fun testEmpty3() = applyForDirAndJar("test", processors("")) { file -> + val impls = ServiceLoaderLite.findImplementations(listOf(file)) + assertEquals(0, impls.size) + } + + fun testSeveralProcessors() { + val processorsContent = buildString { appendln("test.Foo").appendln("test.Bar") } + + applyForDirAndJar("test", processors(processorsContent)) { file -> + val impls = ServiceLoaderLite.findImplementations(Processor::class.java, listOf(file)) + assertEquals(2, impls.size) + assertTrue("test.Foo" in impls) + assertTrue("test.Bar" in impls) + } + } + + fun testSeveralEntries() = applyForDirAndJar("test", processors("test.Foo"), Entry("foo", "bar")) { file -> + val impls = ServiceLoaderLite.findImplementations(listOf(file)) + assertEquals("test.Foo", impls.single()) + } + + fun testSeveralJars() { + val jar1 = writeJar("test.jar", processors("test.Foo")) + val jar2 = writeJar("test2.jar", processors("ap.Bar")) + + val impls = ServiceLoaderLite.findImplementations(listOf(jar1, jar2)) + + assertEquals(2, impls.size) + assertTrue("test.Foo" in impls) + assertTrue("ap.Bar" in impls) + } + + fun testSeveralDirs() { + val dir1 = writeDir("test", processors("test.Foo")) + val dir2 = writeDir("test2", processors("ap.Bar")) + + val impls = ServiceLoaderLite.findImplementations(listOf(dir1, dir2)) + + assertEquals(2, impls.size) + assertTrue("test.Foo" in impls) + assertTrue("ap.Bar" in impls) + } + + fun testDirAndJar() { + val jar = writeJar("test", processors("test.Foo")) + val dir = writeDir("test2", processors("ap.Bar")) + + val impls = ServiceLoaderLite.findImplementations(listOf(jar, dir)) + + assertEquals(2, impls.size) + assertTrue("test.Foo" in impls) + assertTrue("ap.Bar" in impls) + } + + fun testParsingError() { + applyForDirAndJar("test", processors("5")) { file -> + assertThrows { + ServiceLoaderLite.findImplementations(Processor::class.java, listOf(file)) + } + } + } + + fun testParsingError2() { + applyForDirAndJar("test", processors("a b c")) { file -> + assertThrows { + ServiceLoaderLite.findImplementations(Processor::class.java, listOf(file)) + } + } + } + + fun testCommentsAndWhitespaces() { + val processorsContent = buildString { + appendln(" test.Foo #comment") + appendln("#comment2") + appendln().appendln() + appendln("test.Bar #anotherComemnt") + appendln("test.Zoo ") + } + + applyForDirAndJar("test", processors(processorsContent)) { file -> + val impls = ServiceLoaderLite.findImplementations(Processor::class.java, listOf(file)) + assertEquals(3, impls.size) + assertTrue("test.Foo" in impls) + assertTrue("test.Bar" in impls) + assertTrue("test.Zoo" in impls) + } + } +} + diff --git a/compiler/tests/org/jetbrains/kotlin/cli/serviceLoaderLite/ServiceLoaderLiteTestWithClassLoader.kt b/compiler/tests/org/jetbrains/kotlin/cli/serviceLoaderLite/ServiceLoaderLiteTestWithClassLoader.kt new file mode 100644 index 00000000000..55e952c0770 --- /dev/null +++ b/compiler/tests/org/jetbrains/kotlin/cli/serviceLoaderLite/ServiceLoaderLiteTestWithClassLoader.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. 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.cli.serviceLoaderLite + +import org.jetbrains.kotlin.cli.jvm.plugins.ServiceLoaderLite +import java.net.URLClassLoader +import kotlin.reflect.KClass + +interface Intf +class Component1 : Intf +class Component2 : Intf +class ComponentWithParameters(val a: String) : Intf +class UnrelatedComponent +enum class EnumComponent : Intf + +class ServiceLoaderLiteTestWithClassLoader : AbstractServiceLoaderLiteTest() { + class NestedComponent : Intf + inner class InnerComponent : Intf + + fun testClassloader1() { + @Suppress("RemoveExplicitTypeArguments") + val entries = arrayOf(impls(Component1::class, Component2::class), clazz(), clazz()) + + classLoaderTest("test", *entries) { classLoader -> + val impls = ServiceLoaderLite.loadImplementations(classLoader) + assertTrue(impls.any { it is Component1 }) + assertTrue(impls.any { it is Component2 }) + } + } + + fun testNestedComponent() { + classLoaderTest("test", impls(NestedComponent::class), clazz()) { classLoader -> + val impls = ServiceLoaderLite.loadImplementations(classLoader) + assertTrue(impls.single() is NestedComponent) + } + } + + fun testInnerComponent() { + classLoaderTest("test", impls(InnerComponent::class), clazz()) { classLoader -> + assertThrows { + ServiceLoaderLite.loadImplementations(classLoader) + } + } + } + + fun testComponentWithParameters() { + classLoaderTest("test", impls(ComponentWithParameters::class), clazz()) { classLoader -> + assertThrows { + ServiceLoaderLite.loadImplementations(classLoader) + } + } + } + + fun testInterface() { + @Suppress("RemoveExplicitTypeArguments") + classLoaderTest("test", impls(Intf::class), clazz()) { classLoader -> + assertThrows { + ServiceLoaderLite.loadImplementations(classLoader) + } + } + } + + fun testEnum() { + classLoaderTest("test", impls(EnumComponent::class), clazz()) { classLoader -> + assertThrows { + ServiceLoaderLite.loadImplementations(classLoader) + } + } + } + + fun testUnrelatedComponent() { + val implsEntry = Entry("META-INF/services/" + Intf::class.java.name, UnrelatedComponent::class.java.name) + classLoaderTest("test", implsEntry, clazz()) { classLoader -> + assertThrows { + ServiceLoaderLite.loadImplementations(classLoader) + } + } + } + + fun testNestedClassLoaders() { + val entries1 = arrayOf(impls(Component1::class), clazz()) + val entries2 = arrayOf(impls(Component2::class), clazz()) + + var index = 0 + classLoaderTest("test" + index++, *entries1) { classLoader1 -> + val impls1 = ServiceLoaderLite.loadImplementations(classLoader1) + assertTrue(impls1.single() is Component1) + + classLoaderTest("test2" + index++, *entries2, parent = classLoader1) { classLoader2 -> + val impls2 = ServiceLoaderLite.loadImplementations(classLoader2) + assertTrue(impls2.single() is Component2) + } + } + } + + fun testEmpty() { + val classLoader = URLClassLoader(emptyArray(), ServiceLoaderLiteTestWithClassLoader::class.java.classLoader) + val impls = ServiceLoaderLite.loadImplementations(classLoader) + assertTrue(impls.isEmpty()) + } + + private fun classLoaderTest(name: String, vararg entries: Entry, parent: ClassLoader? = null, block: (URLClassLoader) -> Unit) { + applyForDirAndJar(name, *entries) { file -> + val parentClassLoader = parent ?: ServiceLoaderLiteTestWithClassLoader::class.java.classLoader + val classLoader = URLClassLoader(arrayOf(file.toURI().toURL()), parentClassLoader) + block(classLoader) + } + } + + private inline fun clazz() = Entry(T::class.java.name.replace('.', '/'), bytecode(T::class.java)) + + private fun bytecode(clazz: Class<*>): ByteArray { + val resourcePath = clazz.name.replace('.', '/') + ".class" + return clazz.classLoader.getResource(resourcePath).readBytes() + } + + private inline fun impls(vararg impls: KClass): Entry { + val content = buildString { + for (impl in impls) { + appendln(impl.java.name) + } + } + return Entry("META-INF/services/" + Intf::class.java.name, content) + } +} \ 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 3c4d5fa0179..497f43f7cd2 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 @@ -14,7 +14,7 @@ import java.net.URLClassLoader import java.util.* import javax.annotation.processing.Processor -class ProcessorLoader( +open class ProcessorLoader( private val paths: KaptPaths, private val annotationProcessorFqNames: List, private val logger: KaptLogger @@ -33,7 +33,7 @@ class ProcessorLoader( annotationProcessorFqNames.mapNotNull { tryLoadProcessor(it, classLoader) } } else { logger.info("Need to discovery annotation processors in the AP classpath") - ServiceLoader.load(Processor::class.java, classLoader).toList() + doLoadProcessors(classLoader) } if (processors.isEmpty()) { @@ -45,6 +45,10 @@ class ProcessorLoader( return processors } + open fun doLoadProcessors(classLoader: URLClassLoader): List { + return ServiceLoader.load(Processor::class.java, classLoader).toList() + } + private fun tryLoadProcessor(fqName: String, classLoader: ClassLoader): Processor? { val annotationProcessorClass = try { Class.forName(fqName, true, classLoader) 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 21ee0cb8bd3..ee63df5bde5 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 @@ -28,6 +28,7 @@ import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.OUTPUT import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.cli.common.messages.OutputMessageUtil import org.jetbrains.kotlin.cli.common.output.writeAll +import org.jetbrains.kotlin.cli.jvm.plugins.ServiceLoaderLite import org.jetbrains.kotlin.codegen.ClassBuilderMode import org.jetbrains.kotlin.codegen.CompilationErrorHandler import org.jetbrains.kotlin.codegen.KotlinCodegenFacade @@ -60,6 +61,7 @@ import org.jetbrains.kotlin.resolve.jvm.extensions.PartialAnalysisHandlerExtensi import java.io.File import java.io.StringWriter import java.io.Writer +import java.net.URLClassLoader import javax.annotation.processing.Processor import com.sun.tools.javac.util.List as JavacList @@ -87,7 +89,14 @@ class ClasspathBasedKapt3Extension( private var processorLoader: ProcessorLoader? = null override fun loadProcessors(): List { - return ProcessorLoader(paths, annotationProcessorFqNames, logger).also { this.processorLoader = it }.loadProcessors() + val efficientProcessorLoader = object : ProcessorLoader(paths, annotationProcessorFqNames, logger) { + override fun doLoadProcessors(classLoader: URLClassLoader): List { + return ServiceLoaderLite.loadImplementations(Processor::class.java, classLoader) + } + } + + this.processorLoader = efficientProcessorLoader + return efficientProcessorLoader.loadProcessors() } override fun analysisCompleted(