diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/ExecutionStrategyIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/ExecutionStrategyIT.kt new file mode 100644 index 00000000000..ec44d708662 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/ExecutionStrategyIT.kt @@ -0,0 +1,54 @@ +package org.jetbrains.kotlin.gradle + +import org.jetbrains.kotlin.gradle.util.getFileByName +import org.junit.Test + +class ExecutionStrategyIT(): BaseGradleIT() { + companion object { + private const val GRADLE_VERSION = "2.10" + } + + @Test + fun testDaemon() { + doTestExecutionStrategy("daemon") + } + + @Test + fun testInProcess() { + doTestExecutionStrategy("in-process") + } + + @Test + fun testOutOfProcess() { + doTestExecutionStrategy("out-of-process") + } + + private fun doTestExecutionStrategy(executionStrategy: String) { + val project = Project("kotlinBuiltins", GRADLE_VERSION) + setupProject(project) + + val strategyCLIArg = "-Dkotlin.compiler.execution.strategy=$executionStrategy" + val finishMessage = "Finished executing kotlin compiler using $executionStrategy strategy" + + project.build("build", strategyCLIArg) { + assertSuccessful() + assertContains(finishMessage) + checkOutput() + } + + val fKt = project.projectDir.getFileByName("f.kt") + fKt.delete() + project.build("build", strategyCLIArg) { + assertFailed() + assertContains(finishMessage) + assert(output.contains("Unresolved reference: f", ignoreCase = true)) + } + } + + protected open fun setupProject(project: Project) { + } + + protected open fun CompiledProject.checkOutput() { + assertFileExists("app/build/classes/main/foo/MainKt.class") + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/SimpleKotlinGradleIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/SimpleKotlinGradleIT.kt index c4ad45dd8e0..aa4bfc994e9 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/SimpleKotlinGradleIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/SimpleKotlinGradleIT.kt @@ -17,6 +17,7 @@ class SimpleKotlinGradleIT : BaseGradleIT() { project.build("compileDeployKotlin", "build") { assertSuccessful() + assertContains("Finished executing kotlin compiler using daemon strategy") assertReportExists("build/reports/tests/classes/demo.TestSource.html") assertContains(":compileKotlin", ":compileTestKotlin", ":compileDeployKotlin") } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlinBuiltins/app/src/main/kotlin/f.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlinBuiltins/app/src/main/kotlin/f.kt new file mode 100644 index 00000000000..bec949b8acf --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlinBuiltins/app/src/main/kotlin/f.kt @@ -0,0 +1,3 @@ +package foo + +fun f(fn: (x: Int)->Unit) {} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlinBuiltins/app/src/main/kotlin/main.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlinBuiltins/app/src/main/kotlin/main.kt index bec949b8acf..a12a807ae44 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlinBuiltins/app/src/main/kotlin/main.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlinBuiltins/app/src/main/kotlin/main.kt @@ -1,3 +1,5 @@ package foo -fun f(fn: (x: Int)->Unit) {} \ No newline at end of file +fun main() { + f { it * 2 } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/compilerRunner/GradleKotlinCompilerRunner.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/compilerRunner/GradleKotlinCompilerRunner.kt index af25893afd9..21d7489b5be 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/compilerRunner/GradleKotlinCompilerRunner.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/compilerRunner/GradleKotlinCompilerRunner.kt @@ -33,8 +33,12 @@ import java.net.URL import java.util.zip.ZipFile import kotlin.concurrent.thread +internal const val KOTLIN_COMPILER_EXECUTION_STRATEGY_PROPERTY = "kotlin.compiler.execution.strategy" +internal const val DAEMON_EXECUTION_STRATEGY = "daemon" +internal const val IN_PROCESS_EXECUTION_STRATEGY = "in-process" +internal const val OUT_OF_PROCESS_EXECUTION_STRATEGY = "out-of-process" -const val KOTLIN_COMPILER_JAR_PATH_PROPERTY = "kotlin.compiler.jar.path" +internal const val KOTLIN_COMPILER_JAR_PATH_PROPERTY = "kotlin.compiler.jar.path" internal class GradleCompilerRunner(private val project: Project) : KotlinCompilerRunner() { override val log = GradleKotlinLogger(project.logger) @@ -80,6 +84,95 @@ internal class GradleCompilerRunner(private val project: Project) : KotlinCompil } } + override fun doRunCompiler(compilerClassName: String, argsArray: Array, environment: GradleCompilerEnvironment, messageCollector: MessageCollector, collector: OutputItemsCollector): ExitCode { + val executionStrategy = System.getProperty(KOTLIN_COMPILER_EXECUTION_STRATEGY_PROPERTY) ?: DAEMON_EXECUTION_STRATEGY + if (executionStrategy == DAEMON_EXECUTION_STRATEGY) { + val daemonExitCode = compileWithDaemon(compilerClassName, argsArray, environment, messageCollector, collector) + + if (daemonExitCode != null) { + return daemonExitCode + } + else { + log.warn("Could not connect to kotlin daemon. Using fallback strategy.") + } + } + + val isGradleDaemonUsed = System.getProperty("org.gradle.daemon")?.let(String::toBoolean) + return if (executionStrategy == IN_PROCESS_EXECUTION_STRATEGY || isGradleDaemonUsed == false) { + compileInProcess(argsArray, collector, compilerClassName, environment, messageCollector) + } + else { + compileOutOfProcess(argsArray, compilerClassName, environment) + } + } + + override fun compileWithDaemon(compilerClassName: String, argsArray: Array, environment: GradleCompilerEnvironment, messageCollector: MessageCollector, collector: OutputItemsCollector, retryOnConnectionError: Boolean): ExitCode? { + val exitCode = super.compileWithDaemon(compilerClassName, argsArray, environment, messageCollector, collector, retryOnConnectionError) + exitCode?.let { + logFinish(DAEMON_EXECUTION_STRATEGY) + } + return exitCode + } + + private fun compileOutOfProcess( + argsArray: Array, + compilerClassName: String, + environment: GradleCompilerEnvironment + ): ExitCode { + val javaBin = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java" + val classpathString = environment.compilerClasspath.map {it.absolutePath}.joinToString(separator = File.pathSeparator) + val builder = ProcessBuilder(javaBin, "-cp", classpathString, compilerClassName, *argsArray) + val process = builder.start() + + // important to read inputStream, otherwise the process may hang on some systems + val readErrThread = thread { + process.errorStream!!.bufferedReader().forEachLine { + System.err.println(it) + } + } + process.inputStream!!.bufferedReader().forEachLine { + System.out.println(it) + } + readErrThread.join() + + val exitCode = process.waitFor() + logFinish(OUT_OF_PROCESS_EXECUTION_STRATEGY) + return exitCodeFromProcessExitCode(exitCode) + } + + private fun compileInProcess( + argsArray: Array, + collector: OutputItemsCollector, + compilerClassName: String, + environment: GradleCompilerEnvironment, + messageCollector: MessageCollector + ): ExitCode { + val stream = ByteArrayOutputStream() + val out = PrintStream(stream) + // todo: cache classloader? + val classLoader = ParentLastURLClassLoader(environment.compilerClasspathURLs, this.javaClass.classLoader) + val servicesClass = Class.forName(Services::class.java.canonicalName, true, classLoader) + val emptyServices = servicesClass.getField("EMPTY").get(servicesClass) + val compiler = Class.forName(compilerClassName, true, classLoader) + + val exec = compiler.getMethod( + "execAndOutputXml", + PrintStream::class.java, + servicesClass, + Array::class.java + ) + + val res = exec.invoke(compiler.newInstance(), out, emptyServices, argsArray) + val exitCode = ExitCode.valueOf(res.toString()) + processCompilerOutput(messageCollector, collector, stream, exitCode) + logFinish(IN_PROCESS_EXECUTION_STRATEGY) + return exitCode + } + + private fun logFinish(strategy: String) { + log.debug("Finished executing kotlin compiler using $strategy strategy") + } + @Synchronized override fun getDaemonConnection(environment: GradleCompilerEnvironment, messageCollector: MessageCollector): DaemonConnection { return newDaemonConnection(environment.compilerJar, messageCollector, flagFile) @@ -93,13 +186,18 @@ internal class GradleCompilerEnvironment( val compilerJar: File by lazy { val file = findKotlinCompilerJar(project, compilerClassName) ?: throw IllegalStateException("Could not found Kotlin compiler jar. " + - "As a workaround you may specify path to compiler jar using " + - "\"$KOTLIN_COMPILER_JAR_PATH_PROPERTY\" system property") + "As a workaround you may specify path to compiler jar using " + + "\"$KOTLIN_COMPILER_JAR_PATH_PROPERTY\" system property") project.logger.kotlinInfo("Using kotlin compiler jar: $file") file } + val compilerClasspath: List + get() = listOf(compilerJar).filterNotNull() + + val compilerClasspathURLs: List + get() = compilerClasspath.map { it.toURI().toURL() } } fun findKotlinCompilerJar(project: Project, compilerClassName: String): File? { diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/ParentLastURLClassLoader.java b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/ParentLastURLClassLoader.java new file mode 100644 index 00000000000..31ef07b1d86 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/ParentLastURLClassLoader.java @@ -0,0 +1,103 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * 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. + */ + +package org.jetbrains.kotlin.gradle.plugin; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.List; + +/** + * A parent-last classloader that will try the child classloader first and then the parent. + * This takes a fair bit of doing because java really prefers parent-first. + *

+ * For those not familiar with class loading trickery, be wary + * + * http://stackoverflow.com/questions/5445511/how-do-i-create-a-parent-last-child-first-classloader-in-java-or-how-to-overr + */ +public class ParentLastURLClassLoader extends ClassLoader { + private final ChildURLClassLoader childClassLoader; + + public ParentLastURLClassLoader(@NotNull List classpath, @Nullable ClassLoader parent) { + super(Thread.currentThread().getContextClassLoader()); + + URL[] urls = classpath.toArray(new URL[classpath.size()]); + + childClassLoader = new ChildURLClassLoader(urls, new FindClassClassLoader(parent)); + } + + @Override + protected synchronized Class loadClass(@NotNull String name, boolean resolve) throws ClassNotFoundException { + try { + // first we try to find a class inside the child classloader + return childClassLoader.findClass(name); + } + catch (ClassNotFoundException e) { + // didn't find it, try the parent + return super.loadClass(name, resolve); + } + } + + /** + * This class allows me to call findClass on a classloader + */ + private static class FindClassClassLoader extends ClassLoader { + public FindClassClassLoader(@Nullable ClassLoader parent) { + super(parent); + } + + @NotNull + @Override + public Class findClass(@NotNull String name) throws ClassNotFoundException { + return super.findClass(name); + } + } + + /** + * This class delegates (child then parent) for the findClass method for a URLClassLoader. + * We need this because findClass is protected in URLClassLoader + */ + static class ChildURLClassLoader extends URLClassLoader { + private final FindClassClassLoader realParent; + + public ChildURLClassLoader(@NotNull URL[] urls, @NotNull FindClassClassLoader realParent) { + super(urls, null); + + this.realParent = realParent; + } + + + @NotNull + @Override + public Class findClass(@NotNull String name) throws ClassNotFoundException { + Class loaded = findLoadedClass(name); + if (loaded != null) { + return loaded; + } + + try { + return super.findClass(name); + } + catch (ClassNotFoundException e) { + // if that fails, we ask our real parent classloader to load the class (we give up) + return realParent.loadClass(name); + } + } + } +} \ No newline at end of file