org.jetbrains.kotlin.compilerRunner.KotlinToolRunner.kt Maven / Gradle / Ivy
/*
* Copyright 2010-2020 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.compilerRunner
import com.intellij.openapi.util.text.StringUtil.escapeStringCharacters
import org.gradle.api.Project
import org.jetbrains.kotlin.konan.target.HostManager
import java.io.File
import java.lang.reflect.InvocationTargetException
import java.net.URLClassLoader
import java.util.concurrent.ConcurrentHashMap
internal abstract class KotlinToolRunner(
val project: Project
) {
// name that will be used in logs
abstract val displayName: String
abstract val mainClass: String
open val daemonEntryPoint: String get() = "main"
open val execEnvironment: Map = emptyMap()
open val execEnvironmentBlacklist: Set = emptySet()
open val execSystemProperties: Map = emptyMap()
open val execSystemPropertiesBlacklist: Set = setOf(
"java.endorsed.dirs", // Fix for KT-25887
"user.dir", // Don't propagate the working dir of the current Gradle process
"java.system.class.loader" // Don't use custom class loaders
)
abstract val classpath: Set
open fun checkClasspath(): Unit = check(classpath.isNotEmpty()) { "Classpath of the tool is empty: $displayName" }
abstract val isolatedClassLoaderCacheKey: Any
private fun getIsolatedClassLoader(): URLClassLoader = isolatedClassLoadersMap.computeIfAbsent(isolatedClassLoaderCacheKey) {
val arrayOfURLs = classpath.map { File(it.absolutePath).toURI().toURL() }.toTypedArray()
URLClassLoader(arrayOfURLs, null).apply {
setDefaultAssertionStatus(enableAssertions)
}
}
open val defaultMaxHeapSize: String get() = "3G"
open val enableAssertions: Boolean get() = true
open val disableC2: Boolean get() = true
abstract val mustRunViaExec: Boolean
open fun transformArgs(args: List): List = args
// for the purpose if there is a way to specify JVM args, for instance, straight in project configs
open fun getCustomJvmArgs(): List = emptyList()
private val jvmArgs: List by lazy {
mutableListOf().apply {
if (enableAssertions) add("-ea")
val customJvmArgs = getCustomJvmArgs()
if (customJvmArgs.none { it.startsWith("-Xmx") }) add("-Xmx$defaultMaxHeapSize")
// Disable C2 compiler for HotSpot VM to improve compilation speed.
if (disableC2) {
System.getProperty("java.vm.name")?.let { vmName ->
if (vmName.contains("HotSpot", true)) add("-XX:TieredStopAtLevel=1")
}
}
addAll(customJvmArgs)
}
}
fun run(args: List) {
checkClasspath()
if (mustRunViaExec) runViaExec(args) else runInProcess(args)
}
private fun runViaExec(args: List) {
val transformedArgs = transformArgs(args)
val classpath = project.files(classpath)
val systemProperties = System.getProperties().asSequence()
.map { (k, v) -> k.toString() to v.toString() }
.filter { (k, _) -> k !in execSystemPropertiesBlacklist }
.escapeQuotesForWindows()
.toMap() + execSystemProperties
project.logger.info(
"""|Run "$displayName" tool in a separate JVM process
|Main class = $mainClass
|Arguments = ${args.toPrettyString()}
|Transformed arguments = ${if (transformedArgs == args) "same as arguments" else transformedArgs.toPrettyString()}
|Classpath = ${classpath.files.map { it.absolutePath }.toPrettyString()}
|JVM options = ${jvmArgs.toPrettyString()}
|Java system properties = ${systemProperties.toPrettyString()}
|Suppressed ENV variables = ${execEnvironmentBlacklist.toPrettyString()}
|Custom ENV variables = ${execEnvironment.toPrettyString()}
""".trimMargin()
)
project.javaexec { spec ->
spec.main = mainClass
spec.classpath = classpath
spec.jvmArgs(jvmArgs)
spec.systemProperties(systemProperties)
execEnvironmentBlacklist.forEach { spec.environment.remove(it) }
spec.environment(execEnvironment)
spec.args(transformedArgs)
}
}
private fun runInProcess(args: List) {
val transformedArgs = transformArgs(args)
val isolatedClassLoader = getIsolatedClassLoader()
project.logger.info(
"""|Run in-process tool "$displayName"
|Entry point method = $mainClass.$daemonEntryPoint
|Classpath = ${isolatedClassLoader.urLs.map { it.file }.toPrettyString()}
|Arguments = ${args.toPrettyString()}
|Transformed arguments = ${if (transformedArgs == args) "same as arguments" else transformedArgs.toPrettyString()}
""".trimMargin()
)
try {
val mainClass = isolatedClassLoader.loadClass(mainClass)
val entryPoint = mainClass.methods.single { it.name == daemonEntryPoint }
entryPoint.invoke(null, transformedArgs.toTypedArray())
} catch (t: InvocationTargetException) {
throw t.targetException
}
}
companion object {
private fun String.escapeQuotes() = replace("\"", "\\\"")
private fun Sequence>.escapeQuotesForWindows() =
if (HostManager.hostIsMingw) map { (key, value) -> key.escapeQuotes() to value.escapeQuotes() } else this
private val isolatedClassLoadersMap = ConcurrentHashMap()
private fun Map.toPrettyString(): String = buildString {
append('[')
if ([email protected]()) append('\n')
[email protected] { (key, value) ->
append('\t').append(key).append(" = ").append(value.toPrettyString()).append('\n')
}
append(']')
}
private fun Collection.toPrettyString(): String = buildString {
append('[')
if ([email protected]()) append('\n')
[email protected] { append('\t').append(it.toPrettyString()).append('\n') }
append(']')
}
private fun String.toPrettyString(): String =
when {
isEmpty() -> "\"\""
any { it == '"' || it.isWhitespace() } -> '"' + escapeStringCharacters(this) + '"'
else -> this
}
}
}