blob: 8eb18adb0ad70b279fd66b4765750115d4d306bc [file] [log] [blame]
/*
* Copyright 2016 the original author or authors.
*
* 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.gradle.kotlin.dsl.support
import org.gradle.internal.SystemProperties
import org.gradle.internal.io.NullOutputStream
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.KOTLIN_COMPILER_ENVIRONMENT_KEEPALIVE_PROPERTY
import org.jetbrains.kotlin.cli.common.config.addKotlinSourceRoot
import org.jetbrains.kotlin.cli.common.config.addKotlinSourceRoots
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.cli.common.messages.MessageUtil
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileBunchOfSources
import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoot
import org.jetbrains.kotlin.codegen.CompilationException
import org.jetbrains.kotlin.com.intellij.openapi.Disposable
import org.jetbrains.kotlin.com.intellij.openapi.project.Project
import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer.dispose
import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer.newDisposable
import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
import org.jetbrains.kotlin.config.AnalysisFlags
import org.jetbrains.kotlin.config.ApiVersion
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.JVMConfigurationKeys.JVM_TARGET
import org.jetbrains.kotlin.config.JVMConfigurationKeys.OUTPUT_DIRECTORY
import org.jetbrains.kotlin.config.JVMConfigurationKeys.RETAIN_OUTPUT_IN_MEMORY
import org.jetbrains.kotlin.config.JvmTarget.JVM_1_8
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.config.LanguageVersion
import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl
import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor.Companion.registerExtension
import org.jetbrains.kotlin.name.NameUtils
import org.jetbrains.kotlin.samWithReceiver.CliSamWithReceiverComponentContributor
import org.jetbrains.kotlin.scripting.compiler.plugin.ScriptingCompilerConfigurationComponentRegistrar
import org.jetbrains.kotlin.scripting.configuration.ScriptingConfigurationKeys.SCRIPT_DEFINITIONS
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.utils.PathUtil
import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult
import org.slf4j.Logger
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.OutputStream
import java.io.PrintStream
import kotlin.reflect.KClass
import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.baseClass
import kotlin.script.experimental.api.defaultImports
import kotlin.script.experimental.api.hostConfiguration
import kotlin.script.experimental.api.implicitReceivers
import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration
fun compileKotlinScriptModuleTo(
outputDirectory: File,
moduleName: String,
scriptFiles: Collection<String>,
scriptDef: ScriptDefinition,
classPath: Iterable<File>,
logger: Logger,
pathTranslation: (String) -> String
) = compileKotlinScriptModuleTo(
outputDirectory,
moduleName,
scriptFiles,
scriptDef,
classPath,
LoggingMessageCollector(logger, pathTranslation)
)
fun scriptDefinitionFromTemplate(
template: KClass<out Any>,
implicitImports: List<String>,
implicitReceiver: KClass<*>? = null
): ScriptDefinition {
val hostConfiguration = defaultJvmScriptingHostConfiguration
return ScriptDefinition.FromConfigurations(
hostConfiguration = hostConfiguration,
compilationConfiguration = ScriptCompilationConfiguration {
baseClass(template)
defaultImports(implicitImports)
hostConfiguration(hostConfiguration)
implicitReceiver?.let {
implicitReceivers(it)
}
},
evaluationConfiguration = null
)
}
internal
fun compileKotlinScriptToDirectory(
outputDirectory: File,
scriptFile: File,
scriptDef: ScriptDefinition,
classPath: List<File>,
messageCollector: LoggingMessageCollector
): String {
compileKotlinScriptModuleTo(
outputDirectory,
"buildscript",
listOf(scriptFile.path),
scriptDef,
classPath,
messageCollector
)
return NameUtils.getScriptNameForFile(scriptFile.name).asString()
}
private
fun compileKotlinScriptModuleTo(
outputDirectory: File,
moduleName: String,
scriptFiles: Collection<String>,
scriptDef: ScriptDefinition,
classPath: Iterable<File>,
messageCollector: LoggingMessageCollector
) {
withRootDisposable {
withCompilationExceptionHandler(messageCollector) {
val configuration = compilerConfigurationFor(messageCollector).apply {
put(RETAIN_OUTPUT_IN_MEMORY, false)
put(OUTPUT_DIRECTORY, outputDirectory)
setModuleName(moduleName)
addScriptingCompilerComponents()
addScriptDefinition(scriptDef)
scriptFiles.forEach { addKotlinSourceRoot(it) }
classPath.forEach { addJvmClasspathRoot(it) }
}
val environment = kotlinCoreEnvironmentFor(configuration).apply {
HasImplicitReceiverCompilerPlugin.apply(project)
}
compileBunchOfSources(environment)
|| throw ScriptCompilationException(messageCollector.errors)
}
}
}
private
object HasImplicitReceiverCompilerPlugin {
fun apply(project: Project) {
registerExtension(project, samWithReceiverComponentContributor)
}
val samWithReceiverComponentContributor = CliSamWithReceiverComponentContributor(
listOf("org.gradle.api.HasImplicitReceiver")
)
}
internal
fun compileToDirectory(
outputDirectory: File,
moduleName: String,
sourceFiles: Iterable<File>,
logger: Logger,
classPath: Iterable<File>
): Boolean {
withRootDisposable {
withMessageCollectorFor(logger) { messageCollector ->
val configuration = compilerConfigurationFor(messageCollector).apply {
addKotlinSourceRoots(sourceFiles.map { it.canonicalPath })
put(OUTPUT_DIRECTORY, outputDirectory)
setModuleName(moduleName)
classPath.forEach { addJvmClasspathRoot(it) }
addJvmClasspathRoot(kotlinStdlibJar)
}
val environment = kotlinCoreEnvironmentFor(configuration)
return compileBunchOfSources(environment)
}
}
}
private
val kotlinStdlibJar: File
get() = PathUtil.getResourcePathForClass(Unit::class.java)
private
inline fun <T> withRootDisposable(action: Disposable.() -> T): T {
val rootDisposable = newDisposable()
try {
return action(rootDisposable)
} finally {
dispose(rootDisposable)
}
}
private
inline fun <T> withMessageCollectorFor(log: Logger, action: (MessageCollector) -> T): T {
val messageCollector = messageCollectorFor(log)
withCompilationExceptionHandler(messageCollector) {
return action(messageCollector)
}
}
private
inline fun <T> withCompilationExceptionHandler(messageCollector: LoggingMessageCollector, action: () -> T): T {
try {
val log = messageCollector.log
return when {
log.isDebugEnabled -> {
loggingOutputTo(log::debug) { action() }
}
else -> {
ignoringOutputOf { action() }
}
}
} catch (ex: CompilationException) {
messageCollector.report(
CompilerMessageSeverity.EXCEPTION,
ex.localizedMessage,
MessageUtil.psiElementToMessageLocation(ex.element)
)
throw IllegalStateException("Internal compiler error: ${ex.localizedMessage}", ex)
}
}
private
inline fun <T> loggingOutputTo(noinline log: (String) -> Unit, action: () -> T): T =
redirectingOutputTo({ LoggingOutputStream(log) }, action)
private
inline fun <T> ignoringOutputOf(action: () -> T): T =
redirectingOutputTo({ NullOutputStream.INSTANCE }, action)
private
inline fun <T> redirectingOutputTo(noinline outputStream: () -> OutputStream, action: () -> T): T =
redirecting(System.err, System::setErr, outputStream()) {
redirecting(System.out, System::setOut, outputStream()) {
action()
}
}
private
inline fun <T> redirecting(
stream: PrintStream,
set: (PrintStream) -> Unit,
to: OutputStream,
action: () -> T
): T = try {
set(PrintStream(to, true))
action()
} finally {
set(stream)
to.flush()
}
private
class LoggingOutputStream(val log: (String) -> Unit) : OutputStream() {
private
val buffer = ByteArrayOutputStream()
override fun write(b: Int) = buffer.write(b)
override fun write(b: ByteArray, off: Int, len: Int) = buffer.write(b, off, len)
override fun flush() {
buffer.run {
val string = toString("utf8")
if (string.isNotBlank()) {
log(string)
}
reset()
}
}
override fun close() {
flush()
}
}
private
fun compilerConfigurationFor(messageCollector: MessageCollector): CompilerConfiguration =
CompilerConfiguration().apply {
put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector)
put(JVM_TARGET, JVM_1_8)
put(CommonConfigurationKeys.LANGUAGE_VERSION_SETTINGS, gradleKotlinDslLanguageVersionSettings)
}
private
val gradleKotlinDslLanguageVersionSettings = LanguageVersionSettingsImpl(
languageVersion = LanguageVersion.KOTLIN_1_3,
apiVersion = ApiVersion.KOTLIN_1_3,
specificFeatures = mapOf(
LanguageFeature.NewInference to LanguageFeature.State.ENABLED,
LanguageFeature.DisableCompatibilityModeForNewInference to LanguageFeature.State.ENABLED,
LanguageFeature.SamConversionForKotlinFunctions to LanguageFeature.State.ENABLED,
LanguageFeature.SamConversionPerArgument to LanguageFeature.State.ENABLED,
LanguageFeature.ReferencesToSyntheticJavaProperties to LanguageFeature.State.ENABLED
),
analysisFlags = mapOf(
AnalysisFlags.skipMetadataVersionCheck to true
)
)
private
fun CompilerConfiguration.setModuleName(name: String) {
put(CommonConfigurationKeys.MODULE_NAME, name)
}
private
fun CompilerConfiguration.addScriptingCompilerComponents() {
add(
ComponentRegistrar.PLUGIN_COMPONENT_REGISTRARS,
ScriptingCompilerConfigurationComponentRegistrar()
)
}
private
fun CompilerConfiguration.addScriptDefinition(scriptDef: ScriptDefinition) {
add(SCRIPT_DEFINITIONS, scriptDef)
}
private
fun Disposable.kotlinCoreEnvironmentFor(configuration: CompilerConfiguration): KotlinCoreEnvironment {
org.jetbrains.kotlin.cli.common.environment.setIdeaIoUseFallback()
return SystemProperties.getInstance().withSystemProperty(
KOTLIN_COMPILER_ENVIRONMENT_KEEPALIVE_PROPERTY,
"true"
) {
KotlinCoreEnvironment.createForProduction(
this,
configuration,
EnvironmentConfigFiles.JVM_CONFIG_FILES
)
}
}
internal
fun messageCollectorFor(log: Logger, pathTranslation: (String) -> String = { it }): LoggingMessageCollector =
LoggingMessageCollector(log, pathTranslation)
internal
data class ScriptCompilationError(val message: String, val location: CompilerMessageSourceLocation?)
internal
data class ScriptCompilationException(val errors: List<ScriptCompilationError>) : RuntimeException() {
init {
require(errors.isNotEmpty())
}
val firstErrorLine
get() = errors.firstNotNullResult { it.location?.line }
override val message: String
get() = (
listOf("Script compilation $errorPlural:")
+ indentedErrorMessages()
+ "${errors.size} $errorPlural"
)
.joinToString("\n\n")
private
fun indentedErrorMessages() =
errors.asSequence().map(::errorMessage).map(::prependIndent).toList()
private
fun errorMessage(error: ScriptCompilationError): String =
error.location?.let { location ->
errorAt(location, error.message)
} ?: error.message
private
fun errorAt(location: CompilerMessageSourceLocation, message: String): String {
val columnIndent = " ".repeat(5 + maxLineNumberStringLength + 1 + location.column)
return "Line ${lineNumber(location)}: ${location.lineContent}\n" +
"^ $message".lines().joinToString(
prefix = columnIndent,
separator = "\n$columnIndent $indent"
)
}
private
fun lineNumber(location: CompilerMessageSourceLocation) =
location.line.toString().padStart(maxLineNumberStringLength, '0')
private
fun prependIndent(it: String) = it.prependIndent(indent)
private
val errorPlural
get() = if (errors.size > 1) "errors" else "error"
private
val maxLineNumberStringLength: Int by lazy {
errors.mapNotNull { it.location?.line }.maxOrNull()?.toString()?.length ?: 0
}
}
private
const val indent = " "
internal
class LoggingMessageCollector(
internal val log: Logger,
private val pathTranslation: (String) -> String
) : MessageCollector {
val errors = arrayListOf<ScriptCompilationError>()
override fun hasErrors() = errors.isNotEmpty()
override fun clear() = errors.clear()
override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageSourceLocation?) {
fun msg() =
location?.run {
path.let(pathTranslation).let { path ->
when {
line >= 0 && column >= 0 -> compilerMessageFor(path, line, column, message)
else -> "$path: $message"
}
}
} ?: message
fun taggedMsg() =
"${severity.presentableName[0]}: ${msg()}"
when (severity) {
CompilerMessageSeverity.ERROR, CompilerMessageSeverity.EXCEPTION -> {
errors += ScriptCompilationError(message, location)
log.error { taggedMsg() }
}
in CompilerMessageSeverity.VERBOSE -> log.trace { msg() }
CompilerMessageSeverity.STRONG_WARNING -> log.info { taggedMsg() }
CompilerMessageSeverity.WARNING -> log.info { taggedMsg() }
CompilerMessageSeverity.INFO -> log.info { msg() }
else -> log.debug { taggedMsg() }
}
}
}
internal
fun compilerMessageFor(path: String, line: Int, column: Int, message: String) =
"$path:$line:$column: $message"