From a46dd5b30e30a897ed544d0b2091d3fb06a2b0a8 Mon Sep 17 00:00:00 2001 From: Ilya Chernikov Date: Tue, 22 May 2018 19:22:42 +0200 Subject: [PATCH] Avoid using reflected types in the scripting API since it causes numerous classloading issues. Using the wrapping types and reload them in the proper context when needed. Note: this version supports only classes, but the wrapping type could be extended to support other types in the future. + numerous fixes related to proper loading and handling of the templates. --- .../jetbrains/kotlin/cli/jvm/K2JVMCompiler.kt | 8 +- .../kotlin/cli/jvm/plugins/PluginCliParser.kt | 1 + .../AbstractCustomScriptCodegenTest.kt | 8 +- .../generators/tests/GenerateCompilerTests.kt | 1 + .../org/jetbrains/kotlin/utils/PathUtil.kt | 1 + .../core/script/ScriptDefinitionsManager.kt | 32 +-- .../examples/jvm/resolve/maven/host/host.kt | 7 +- .../script/examples/jvm/simple/host/host.kt | 5 +- .../script/experimental/api/kotlinType.kt | 20 ++ .../api/scriptConfigurationProperties.kt | 12 +- .../script/experimental/api/scriptData.kt | 2 - .../experimental/api/scriptingEnvironment.kt | 30 ++- .../script/experimental/basic/basicScript.kt | 7 +- .../definitions/definitionFromAnnotation.kt | 41 +++- .../script/experimental/util/propertyBag.kt | 20 +- .../jvmhost/impl/KJVMCompilerImpl.kt | 9 +- .../experimental/jvm/jvmScriptEnvironment.kt | 61 ++++++ .../script/experimental/misc/propertiesDsl.kt | 35 +++- .../tools/kotlin-gradle-plugin/build.gradle | 1 - .../kotlin/maven/ExecuteKotlinScriptMojo.java | 2 +- .../scripting/scripting-cli/build.gradle.kts | 2 + ...KotlinScriptDefinitionAdapterFromNewAPI.kt | 40 ++-- ...LazyScriptDefinitionFromDiscoveredClass.kt | 69 ++---- ...DefinitionsFromClasspathDiscoverySource.kt | 196 +++++++++++++++--- ...ScriptingCompilerConfigurationExtension.kt | 26 +-- .../plugin/asmBasedAnnotationsLoading.kt | 52 +++++ .../plugin/ScriptingCompilerPluginTest.kt | 2 +- .../scripting-gradle/build.gradle.kts | 1 + 28 files changed, 506 insertions(+), 185 deletions(-) create mode 100644 libraries/scripting/common/src/kotlin/script/experimental/api/kotlinType.kt create mode 100644 libraries/scripting/jvm/src/kotlin/script/experimental/jvm/jvmScriptEnvironment.kt create mode 100644 plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/asmBasedAnnotationsLoading.kt diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/K2JVMCompiler.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/K2JVMCompiler.kt index ba71c55bd90..efdcdbaadb0 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/K2JVMCompiler.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/K2JVMCompiler.kt @@ -235,9 +235,11 @@ class K2JVMCompiler : CLICompiler() { if (!explicitOrLoadedScriptingPlugin) { val libPath = paths?.libPath?.takeIf { it.exists() } ?: File(".") with(PathUtil) { - val jars = arrayOf(KOTLIN_SCRIPTING_COMPILER_PLUGIN_JAR, KOTLIN_SCRIPTING_COMMON_JAR, KOTLIN_SCRIPTING_JVM_JAR) - .mapNotNull { File(libPath, it).takeIf { it.exists() }?.canonicalPath } - if (jars.size == 3) { + val jars = arrayOf( + KOTLIN_SCRIPTING_COMPILER_PLUGIN_JAR, KOTLIN_SCRIPTING_COMMON_JAR, + KOTLIN_SCRIPTING_JVM_JAR, KOTLIN_SCRIPTING_MISC_JAR + ).mapNotNull { File(libPath, it).takeIf { it.exists() }?.canonicalPath } + if (jars.size == 4) { pluginClasspaths = jars + pluginClasspaths } } 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 c5d0e380ce8..3cd9cc81fb9 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 @@ -27,6 +27,7 @@ import org.jetbrains.kotlin.compiler.plugin.* import org.jetbrains.kotlin.config.CompilerConfiguration import java.io.File import java.net.URL +import java.net.URLClassLoader import java.util.* object PluginCliParser { diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/AbstractCustomScriptCodegenTest.kt b/compiler/tests/org/jetbrains/kotlin/codegen/AbstractCustomScriptCodegenTest.kt index 2dfaf26cc85..7d32b320c23 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/AbstractCustomScriptCodegenTest.kt +++ b/compiler/tests/org/jetbrains/kotlin/codegen/AbstractCustomScriptCodegenTest.kt @@ -15,9 +15,9 @@ import org.jetbrains.kotlin.test.InTextDirectivesUtils import org.jetbrains.kotlin.test.TestJdkKind import org.junit.Assert import java.io.File -import kotlin.reflect.full.starProjectedType import kotlin.script.experimental.annotations.KotlinScript import kotlin.script.experimental.annotations.KotlinScriptDefaultCompilationConfiguration +import kotlin.script.experimental.api.KotlinType import kotlin.script.experimental.api.ScriptCompileConfigurationProperties import kotlin.script.experimental.util.TypedKey @@ -32,7 +32,7 @@ abstract class AbstractCustomScriptCodegenTest : CodegenTestCase() { override fun updateConfiguration(configuration: CompilerConfiguration) { if (scriptDefinitions.isNotEmpty()) { - configureScriptDefinitions(scriptDefinitions, configuration, MessageCollector.NONE, emptyMap()) + configureScriptDefinitions(scriptDefinitions, configuration, this::class.java.classLoader, MessageCollector.NONE, emptyMap()) } configuration.addJvmClasspathRoots(additionalDependencies.orEmpty()) @@ -116,7 +116,7 @@ abstract class AbstractCustomScriptCodegenTest : CodegenTestCase() { object TestScriptWithReceiversConfiguration : ArrayList, Any?>>( listOf( - ScriptCompileConfigurationProperties.scriptImplicitReceivers to listOf(String::class.starProjectedType) + ScriptCompileConfigurationProperties.scriptImplicitReceivers to listOf(KotlinType(String::class)) ) ) @@ -127,7 +127,7 @@ abstract class TestScriptWithReceivers object TestScriptWithSimpleEnvVarsConfiguration : ArrayList, Any?>>( listOf( - ScriptCompileConfigurationProperties.contextVariables to mapOf("stringVar1" to String::class.starProjectedType) + ScriptCompileConfigurationProperties.contextVariables to mapOf("stringVar1" to KotlinType(String::class)) ) ) diff --git a/compiler/tests/org/jetbrains/kotlin/generators/tests/GenerateCompilerTests.kt b/compiler/tests/org/jetbrains/kotlin/generators/tests/GenerateCompilerTests.kt index 1f4644275dd..f528185af80 100644 --- a/compiler/tests/org/jetbrains/kotlin/generators/tests/GenerateCompilerTests.kt +++ b/compiler/tests/org/jetbrains/kotlin/generators/tests/GenerateCompilerTests.kt @@ -27,6 +27,7 @@ import org.jetbrains.kotlin.cli.AbstractCliTest import org.jetbrains.kotlin.codegen.* import org.jetbrains.kotlin.codegen.defaultConstructor.AbstractDefaultArgumentsReflectionTest import org.jetbrains.kotlin.codegen.flags.AbstractWriteFlagsTest +import org.jetbrains.kotlin.codegen.AbstractCustomScriptCodegenTest import org.jetbrains.kotlin.codegen.ir.AbstractIrBlackBoxCodegenTest import org.jetbrains.kotlin.codegen.ir.AbstractIrBlackBoxInlineCodegenTest import org.jetbrains.kotlin.codegen.ir.AbstractIrCompileKotlinAgainstInlineKotlinTest diff --git a/compiler/util/src/org/jetbrains/kotlin/utils/PathUtil.kt b/compiler/util/src/org/jetbrains/kotlin/utils/PathUtil.kt index c3df0c64740..33dd61f3193 100644 --- a/compiler/util/src/org/jetbrains/kotlin/utils/PathUtil.kt +++ b/compiler/util/src/org/jetbrains/kotlin/utils/PathUtil.kt @@ -59,6 +59,7 @@ object PathUtil { const val KOTLIN_JAVA_SCRIPT_RUNTIME_JAR = "kotlin-script-runtime.jar" const val KOTLIN_SCRIPTING_COMMON_JAR = "kotlin-scripting-common.jar" const val KOTLIN_SCRIPTING_JVM_JAR = "kotlin-scripting-jvm.jar" + const val KOTLIN_SCRIPTING_MISC_JAR = "kotlin-scripting-misc.jar" const val KOTLIN_SCRIPTING_COMPILER_PLUGIN_JAR = "kotlin-scripting-compiler.jar" const val KOTLIN_TEST_NAME = "kotlin-test" diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptDefinitionsManager.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptDefinitionsManager.kt index 422619d3519..5fccf5112a6 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptDefinitionsManager.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptDefinitionsManager.kt @@ -33,7 +33,6 @@ import org.jetbrains.kotlin.idea.caches.project.SdkInfo import org.jetbrains.kotlin.idea.caches.project.getScriptRelatedModuleInfo import org.jetbrains.kotlin.script.* import org.jetbrains.kotlin.scripting.compiler.plugin.KotlinScriptDefinitionAdapterFromNewAPI -import org.jetbrains.kotlin.script.* import org.jetbrains.kotlin.idea.util.ProjectRootsUtil.isInContent import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.script.KotlinScriptDefinition @@ -49,12 +48,15 @@ import java.net.URLClassLoader import kotlin.concurrent.write import kotlin.script.dependencies.Environment import kotlin.script.dependencies.ScriptContents +import kotlin.script.experimental.api.KotlinType import kotlin.script.experimental.api.ScriptingEnvironment import kotlin.script.experimental.api.ScriptingEnvironmentProperties import kotlin.script.experimental.definitions.ScriptDefinitionFromAnnotatedBaseClass import kotlin.script.experimental.dependencies.DependenciesResolver import kotlin.script.experimental.dependencies.ScriptDependencies import kotlin.script.experimental.dependencies.asSuccess +import kotlin.script.experimental.jvm.JvmDependency +import kotlin.script.experimental.jvm.JvmGetScriptingClass import kotlin.script.experimental.location.ScriptExpectedLocation import kotlin.script.templates.standard.ScriptTemplateWithArgs @@ -161,15 +163,17 @@ fun loadDefinitionsFromTemplates( * i.e. gradle resolver may depend on some jars that 'built.gradle.kts' files should not depend on. */ additionalResolverClasspath: List = emptyList() -): List = try { +): List { val classpath = templateClasspath + additionalResolverClasspath LOG.info("[kts] loading script definitions $templateClassNames using cp: ${classpath.joinToString(File.pathSeparator)}") val baseLoader = ScriptDefinitionContributor::class.java.classLoader val loader = if (classpath.isEmpty()) baseLoader else URLClassLoader(classpath.map { it.toURI().toURL() }.toTypedArray(), baseLoader) - templateClassNames.mapNotNull { + return templateClassNames.mapNotNull { templateClassName -> try { - val template = loader.loadClass(it).kotlin + // TODO: drop class loading here - it should be handled downstream + // as a compatibility measure, the asm based reading of annotations should be implemented to filter classes before classloading + val template = loader.loadClass(templateClassName).kotlin when { template.annotations.firstIsInstanceOrNull() != null || template.annotations.firstIsInstanceOrNull() != null -> { @@ -181,7 +185,13 @@ fun loadDefinitionsFromTemplates( } template.annotations.firstIsInstanceOrNull() != null -> { KotlinScriptDefinitionAdapterFromNewAPI( - ScriptDefinitionFromAnnotatedBaseClass(ScriptingEnvironment(ScriptingEnvironmentProperties.baseClass to template)) + ScriptDefinitionFromAnnotatedBaseClass( + ScriptingEnvironment( + ScriptingEnvironmentProperties.baseClass to KotlinType(template), + ScriptingEnvironmentProperties.configurationDependencies to listOf(JvmDependency(classpath)), + ScriptingEnvironmentProperties.getScriptingClass to JvmGetScriptingClass() + ) + ) ) } else -> { @@ -192,19 +202,13 @@ fun loadDefinitionsFromTemplates( } catch (e: ClassNotFoundException) { // Assuming that direct ClassNotFoundException is the result of versions mismatch and missing subsystems, e.g. gradle // so, it only results in warning, while other errors are severe misconfigurations, resulting it user-visible error - LOG.warn("[kts] cannot load script definition class $it", e) + LOG.warn("[kts] cannot load script definition class $templateClassName", e) null - } catch (e: NoClassDefFoundError) { - LOG.error("[kts] cannot load script definition class $it", e) - null - } catch (e: InvocationTargetException) { - LOG.error("[kts] cannot load script definition class $it", e) + } catch (e: Throwable) { + LOG.error("[kts] cannot load script definition class $templateClassName", e) null } } -} catch (ex: Throwable) { - // TODO: review exception handling - emptyList() } interface ScriptDefinitionContributor { diff --git a/libraries/examples/scripting/jvm-maven-deps/host/src/org/jetbrains/kotlin/script/examples/jvm/resolve/maven/host/host.kt b/libraries/examples/scripting/jvm-maven-deps/host/src/org/jetbrains/kotlin/script/examples/jvm/resolve/maven/host/host.kt index 0695376016c..f3426a58273 100644 --- a/libraries/examples/scripting/jvm-maven-deps/host/src/org/jetbrains/kotlin/script/examples/jvm/resolve/maven/host/host.kt +++ b/libraries/examples/scripting/jvm-maven-deps/host/src/org/jetbrains/kotlin/script/examples/jvm/resolve/maven/host/host.kt @@ -13,13 +13,18 @@ import kotlin.script.experimental.definitions.ScriptDefinitionFromAnnotatedBaseC import kotlin.script.experimental.host.toScriptSource import kotlin.script.experimental.jvm.DummyCompiledJvmScriptCache import kotlin.script.experimental.jvm.JvmBasicScriptingHost +import kotlin.script.experimental.jvm.JvmGetScriptingClass import kotlin.script.experimental.jvm.JvmScriptCompiler import kotlin.script.experimental.jvmhost.impl.KJVMCompilerImpl +import kotlin.script.experimental.misc.* fun evalFile(scriptFile: File): ResultWithDiagnostics { val scriptCompiler = JvmScriptCompiler(KJVMCompilerImpl(), DummyCompiledJvmScriptCache()) val scriptDefinition = ScriptDefinitionFromAnnotatedBaseClass( - ScriptingEnvironment(ScriptingEnvironmentProperties.baseClass to MyScriptWithMavenDeps::class) + ScriptingEnvironment( + ScriptingEnvironmentProperties.baseClass(), + ScriptingEnvironmentProperties.getScriptingClass(JvmGetScriptingClass()) + ) ) val host = JvmBasicScriptingHost( diff --git a/libraries/examples/scripting/jvm-simple-script/host/src/org/jetbrains/kotlin/script/examples/jvm/simple/host/host.kt b/libraries/examples/scripting/jvm-simple-script/host/src/org/jetbrains/kotlin/script/examples/jvm/simple/host/host.kt index 30446a87128..31f13fd9018 100644 --- a/libraries/examples/scripting/jvm-simple-script/host/src/org/jetbrains/kotlin/script/examples/jvm/simple/host/host.kt +++ b/libraries/examples/scripting/jvm-simple-script/host/src/org/jetbrains/kotlin/script/examples/jvm/simple/host/host.kt @@ -25,7 +25,10 @@ val myJvmConfigParams = jvmJavaHomeParams + with(ScriptCompileConfigurationPrope fun evalFile(scriptFile: File): ResultWithDiagnostics { val scriptCompiler = JvmScriptCompiler(KJVMCompilerImpl(), DummyCompiledJvmScriptCache()) val scriptDefinition = ScriptDefinitionFromAnnotatedBaseClass( - ScriptingEnvironment(ScriptingEnvironmentProperties.baseClass to MyScript::class) + ScriptingEnvironment( + ScriptingEnvironmentProperties.baseClass(), + ScriptingEnvironmentProperties.getScriptingClass(JvmGetScriptingClass()) + ) ) val host = JvmBasicScriptingHost( diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/kotlinType.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/kotlinType.kt new file mode 100644 index 00000000000..20360891c84 --- /dev/null +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/kotlinType.kt @@ -0,0 +1,20 @@ +/* + * 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 kotlin.script.experimental.api + +import kotlin.reflect.KClass +import kotlin.reflect.KType + +class KotlinType( + val typeName: String, + val fromClass: KClass<*>? = null + // TODO: copy properties from KType +) { + // TODO: implement other approach for non-class types + constructor(type: KType) : this((type.classifier as KClass<*>).qualifiedName!!, type.classifier as KClass<*>) + + constructor(kclass: KClass<*>) : this(kclass.qualifiedName!!, kclass) +} diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptConfigurationProperties.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptConfigurationProperties.kt index 9a8c269f288..ab91d0a20ae 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptConfigurationProperties.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptConfigurationProperties.kt @@ -7,8 +7,6 @@ package kotlin.script.experimental.api -import kotlin.reflect.KClass -import kotlin.reflect.KType import kotlin.script.experimental.util.typedKey object ScriptCompileConfigurationProperties { @@ -19,9 +17,9 @@ object ScriptCompileConfigurationProperties { val scriptBodyTarget by typedKey() - val scriptImplicitReceivers by typedKey>() // in the order from outer to inner scope + val scriptImplicitReceivers by typedKey>() // in the order from outer to inner scope - val contextVariables by typedKey>() // external variables + val contextVariables by typedKey>() // external variables val defaultImports by typedKey>() @@ -31,15 +29,15 @@ object ScriptCompileConfigurationProperties { val dependencies by typedKey>() - val generatedClassAnnotations by typedKey>>() + val generatedClassAnnotations by typedKey>() - val generatedMethodAnnotations by typedKey>>() + val generatedMethodAnnotations by typedKey>() val compilerOptions by typedKey>() // Q: CommonCompilerOptions instead? val refineBeforeParsing by typedKey() // default: false - val refineConfigurationOnAnnotations by typedKey>>() + val refineConfigurationOnAnnotations by typedKey>() val refineConfigurationOnSections by typedKey>() } diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptData.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptData.kt index f7a9f5750a0..a29cb93814d 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptData.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptData.kt @@ -8,8 +8,6 @@ package kotlin.script.experimental.api import java.net.URL -import kotlin.reflect.KClass -import kotlin.reflect.KType interface ScriptSource { val location: URL? diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptingEnvironment.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptingEnvironment.kt index 88e18e88f87..ec17b1e7bc9 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptingEnvironment.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptingEnvironment.kt @@ -14,6 +14,34 @@ typealias ScriptingEnvironment = ChainedPropertyBag object ScriptingEnvironmentProperties { // required by definitions that extract data from script base class annotations - val baseClass by typedKey>() + val baseClass by typedKey() + + // should contain all dependencies needed for baseClass and compilationConfigurator + val configurationDependencies by typedKey>() + + // do not use configurationDependencies as script dependencies, so only the dependencies defined by compilationConfigurator will be used + // (NOTE: in this case they should include the dependencies for the base class anyway, since this class is needed for script + // compilation and instantiation, but compilationConfigurator could be excluded) + val isolatedDependencies by typedKey(false) + + // a "class loader" for KotlinTypes + val getScriptingClass by typedKey() } +interface GetScriptingClass { + operator fun invoke(classType: KotlinType, contextClass: KClass<*>, environment: ScriptingEnvironment): KClass<*> +} + +fun ScriptingEnvironment.getScriptingClass(type: KotlinType, contextClass: KClass<*>): KClass<*> { + val getClass = getOrNull(ScriptingEnvironmentProperties.getScriptingClass) + ?: throw IllegalArgumentException("Expecting 'getScriptingClass' property in the scripting environment: unable to load scripting class $type") + return getClass(type, contextClass, this) +} + +fun ScriptingEnvironment.getScriptingClass(type: KotlinType, context: Any): KClass<*> = getScriptingClass(type, context::class) + +fun ScriptingEnvironment.getScriptBaseClass(contextClass: KClass<*>): KClass<*> = + getScriptingClass(get(ScriptingEnvironmentProperties.baseClass), contextClass) + +fun ScriptingEnvironment.getScriptBaseClass(context: Any): KClass<*> = + getScriptingClass(get(ScriptingEnvironmentProperties.baseClass), context::class) diff --git a/libraries/scripting/common/src/kotlin/script/experimental/basic/basicScript.kt b/libraries/scripting/common/src/kotlin/script/experimental/basic/basicScript.kt index cadb432ab69..ff6c05ee75f 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/basic/basicScript.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/basic/basicScript.kt @@ -10,15 +10,14 @@ import kotlin.script.experimental.annotations.KotlinScriptDefaultCompilationConf import kotlin.script.experimental.api.* import kotlin.script.experimental.util.TypedKey - private const val ILLEGAL_CONFIG_ANN_ARG = "Illegal argument to KotlinScriptDefaultCompilationConfiguration annotation: expecting List-derived object or default-constructed class of configuration parameters" open class AnnotationsBasedCompilationConfigurator(val environment: ScriptingEnvironment) : ScriptCompilationConfigurator { - override val defaultConfiguration = run { - val base = environment[ScriptingEnvironmentProperties.baseClass] - val cfg = base.annotations.filterIsInstance(KotlinScriptDefaultCompilationConfiguration::class.java).flatMap { ann -> + override val defaultConfiguration by lazy { + val baseClass = environment.getScriptBaseClass(this) + val cfg = baseClass.annotations.filterIsInstance(KotlinScriptDefaultCompilationConfiguration::class.java).flatMap { ann -> val params = try { ann.compilationConfiguration.objectInstance ?: ann.compilationConfiguration.createInstance() } catch (e: Throwable) { diff --git a/libraries/scripting/common/src/kotlin/script/experimental/definitions/definitionFromAnnotation.kt b/libraries/scripting/common/src/kotlin/script/experimental/definitions/definitionFromAnnotation.kt index ec1fdae98cd..4137a85de96 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/definitions/definitionFromAnnotation.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/definitions/definitionFromAnnotation.kt @@ -18,8 +18,18 @@ private const val ERROR_MSG_PREFIX = "Unable to construct script definition: " open class ScriptDefinitionFromAnnotatedBaseClass(val environment: ScriptingEnvironment) : ScriptDefinition { - private val baseClass: KClass<*> = environment.getOrNull(ScriptingEnvironmentProperties.baseClass) - ?: throw IllegalArgumentException("${ERROR_MSG_PREFIX}Expecting baseClass parameter in the scripting environment") + private val getScriptingClass = environment.getOrNull(ScriptingEnvironmentProperties.getScriptingClass) + ?: throw IllegalArgumentException("${ERROR_MSG_PREFIX}Expecting 'getClass' parameter in the scripting environment") + + private val baseClass: KClass<*> = run { + val baseClassType = environment.getOrNull(ScriptingEnvironmentProperties.baseClass) + ?: throw IllegalArgumentException("${ERROR_MSG_PREFIX}Expecting 'baseClass' parameter in the scripting environment") + try { + getScriptingClass(baseClassType, this::class, environment) + } catch (e: Throwable) { + throw IllegalArgumentException("${ERROR_MSG_PREFIX}Unable to load base class $baseClassType", e) + } + } private val mainAnnotation = baseClass.findAnnotation() ?: throw IllegalArgumentException("${ERROR_MSG_PREFIX}Expecting KotlinScript annotation on the $baseClass") @@ -27,13 +37,17 @@ open class ScriptDefinitionFromAnnotatedBaseClass(val environment: ScriptingEnvi private val explicitDefinition: ScriptDefinition? = baseClass.findAnnotation()?.definition.takeIf { it != this::class }?.let { it.instantiateScriptHandler() } - override val properties = (explicitDefinition?.properties ?: ScriptingEnvironment()).also { properties -> + override val properties = run { + val baseProperties = explicitDefinition?.properties ?: environment val toAdd = arrayListOf, Any>>() - baseClass.findAnnotation()?.let { toAdd += ScriptDefinitionProperties.fileExtension to it } - if (properties.getOrNull(ScriptDefinitionProperties.name) == null) { + baseClass.findAnnotation()?.let { + toAdd += ScriptDefinitionProperties.fileExtension to it.extension + } + if (baseProperties.getOrNull(ScriptDefinitionProperties.name) == null) { toAdd += ScriptDefinitionProperties.name to mainAnnotation.name } - ScriptingEnvironment(properties, toAdd) + if (toAdd.isEmpty()) baseProperties + else ScriptingEnvironment(baseProperties, toAdd) } override val compilationConfigurator = @@ -47,10 +61,17 @@ open class ScriptDefinitionFromAnnotatedBaseClass(val environment: ScriptingEnvi ?: DummyEvaluator::class.instantiateScriptHandler() private fun KClass.instantiateScriptHandler(): T { - val fqn = this.qualifiedName!! - val klass: KClass = (baseClass.java.classLoader.loadClass(fqn) as Class).kotlin - // TODO: fix call after deciding on constructor parameters - return klass.objectInstance ?: klass.primaryConstructor!!.call(environment) + val klass: KClass = try { + getScriptingClass(KotlinType(this), this@ScriptDefinitionFromAnnotatedBaseClass::class, environment) as KClass + } catch (e: Throwable) { + throw IllegalArgumentException("${ERROR_MSG_PREFIX}Unable to load handler $this: $e", e) + } + try { + // TODO: fix call after deciding on constructor parameters + return klass.objectInstance ?: klass.primaryConstructor!!.call(environment) + } catch (e: Throwable) { + throw IllegalArgumentException("${ERROR_MSG_PREFIX}Unable to instantiate handler $this: $e", e) + } } } diff --git a/libraries/scripting/common/src/kotlin/script/experimental/util/propertyBag.kt b/libraries/scripting/common/src/kotlin/script/experimental/util/propertyBag.kt index 141d77154d8..1f4ee38abcb 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/util/propertyBag.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/util/propertyBag.kt @@ -9,13 +9,13 @@ package kotlin.script.experimental.util import kotlin.reflect.KProperty -data class TypedKey(val name: String) +data class TypedKey(val name: String, val defaultValue: T? = null) -class TypedKeyDelegate { - operator fun getValue(thisRef: Any?, property: KProperty<*>): TypedKey = TypedKey(property.name) +class TypedKeyDelegate(val defaultValue: T? = null) { + operator fun getValue(thisRef: Any?, property: KProperty<*>): TypedKey = TypedKey(property.name, defaultValue) } -fun typedKey() = TypedKeyDelegate() +fun typedKey(defaultValue: T? = null) = TypedKeyDelegate(defaultValue) open class ChainedPropertyBag private constructor(private val parent: ChainedPropertyBag?, private val data: Map, Any?>) { constructor(parent: ChainedPropertyBag? = null, pairs: Iterable, Any?>>) : @@ -31,17 +31,17 @@ open class ChainedPropertyBag private constructor(private val parent: ChainedPro else -> ChainedPropertyBag(parent.cloneWithNewParent(newParent), data) } - inline operator fun get(key: TypedKey): T = getUnchecked(key) as T + inline operator fun get(key: TypedKey): T = getRaw(key) as T - fun getUnchecked(key: TypedKey): Any? = + fun getRaw(key: TypedKey): Any? = when { data.containsKey(key) -> data[key] - parent != null -> parent.getUnchecked(key) + parent != null -> parent.getRaw(key) + key.defaultValue != null -> key.defaultValue else -> throw IllegalArgumentException("Unknown key $key") } - inline fun getOrNull(key: TypedKey): T? = getOrNullUnchecked(key)?.let { it as T } + inline fun getOrNull(key: TypedKey): T? = getOrNullRaw(key)?.let { it as T } - fun getOrNullUnchecked(key: TypedKey): Any? = data[key] ?: parent?.getOrNullUnchecked(key) + fun getOrNullRaw(key: TypedKey): Any? = data[key] ?: parent?.getOrNullRaw(key) ?: key.defaultValue } - diff --git a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJVMCompilerImpl.kt b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJVMCompilerImpl.kt index 7fb381f020d..6e7d267c194 100644 --- a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJVMCompilerImpl.kt +++ b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJVMCompilerImpl.kt @@ -236,10 +236,13 @@ internal class BridgeScriptDefinition( scriptCompilerConfiguration: ScriptCompileConfiguration, scriptConfigurator: ScriptCompilationConfigurator?, updateClasspath: (List) -> Unit -) : KotlinScriptDefinition(scriptCompilerConfiguration[ScriptingEnvironmentProperties.baseClass] as KClass) { - override val acceptedAnnotations = - scriptCompilerConfiguration.getOrNull(ScriptCompileConfigurationProperties.refineConfigurationOnAnnotations)?.toList() +) : KotlinScriptDefinition(scriptCompilerConfiguration.getScriptBaseClass(BridgeScriptDefinition::class)) { + override val acceptedAnnotations = run { + val cl = this::class.java.classLoader + scriptCompilerConfiguration.getOrNull(ScriptCompileConfigurationProperties.refineConfigurationOnAnnotations) + ?.map { (cl.loadClass(it.typeName) as Class).kotlin } ?: emptyList() + } override val dependencyResolver: DependenciesResolver = BridgeDependenciesResolver(scriptConfigurator, scriptCompilerConfiguration, updateClasspath) diff --git a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/jvmScriptEnvironment.kt b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/jvmScriptEnvironment.kt new file mode 100644 index 00000000000..4eca3aa3dbb --- /dev/null +++ b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/jvmScriptEnvironment.kt @@ -0,0 +1,61 @@ +/* + * 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 kotlin.script.experimental.jvm + +import java.net.URLClassLoader +import kotlin.reflect.KClass +import kotlin.script.experimental.api.* + +class JvmGetScriptingClass : GetScriptingClass { + + private var dependencies: List? = null + private var classLoader: ClassLoader? = null + private var baseClassLoader: ClassLoader? = null + + @Synchronized + override fun invoke(classType: KotlinType, contextClass: KClass<*>, environment: ScriptingEnvironment): KClass<*> { + + // checking if class already loaded in the same context + val contextClassloader = contextClass.java.classLoader + if (classType.fromClass != null) { + if (classType.fromClass!!.java.classLoader == null) return classType.fromClass!! // root classloader + val actualClassLoadersChain = generateSequence(classType.fromClass!!.java.classLoader) { it.parent } + if (actualClassLoadersChain.any { it == contextClassloader }) return classType.fromClass!! + } + + val newDeps = environment.getOrNull(ScriptingEnvironmentProperties.configurationDependencies) + if (dependencies == null) { + dependencies = newDeps + } else { + if (newDeps != dependencies) throw IllegalArgumentException("scripting environment dependencies changed") + } + + if (baseClassLoader == null) { + baseClassLoader = contextClassloader + } else { + val baseClassLoadersChain = generateSequence(baseClassLoader) { it.parent } + if (baseClassLoadersChain.none { it == contextClassloader }) throw IllegalArgumentException("scripting class instantiation context changed") + } + + if (classLoader == null) { + val classpath = dependencies?.flatMap { + when(it) { + is JvmDependency -> it.classpath.map { it.toURI().toURL() } + else -> throw IllegalArgumentException("unknown dependency type $it") + } + } + classLoader = + if (classpath == null || classpath.isEmpty()) baseClassLoader + else URLClassLoader(classpath.toTypedArray(), baseClassLoader) + } + + return try { + classLoader!!.loadClass(classType.typeName).kotlin + } catch (e: Throwable) { + throw IllegalArgumentException("unable to load class $classType", e) + } + } +} diff --git a/libraries/scripting/misc/src/kotlin/script/experimental/misc/propertiesDsl.kt b/libraries/scripting/misc/src/kotlin/script/experimental/misc/propertiesDsl.kt index 28a50ad64c5..e824f035f28 100644 --- a/libraries/scripting/misc/src/kotlin/script/experimental/misc/propertiesDsl.kt +++ b/libraries/scripting/misc/src/kotlin/script/experimental/misc/propertiesDsl.kt @@ -6,15 +6,48 @@ package kotlin.script.experimental.misc import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.script.experimental.api.KotlinType import kotlin.script.experimental.util.TypedKey inline operator fun TypedKey.invoke(v: T): Pair, T> = this to v -inline operator fun TypedKey>.invoke(): Pair>, KClass<*>> = this to K::class +inline operator fun TypedKey.invoke(): Pair, KotlinType> = + this to KotlinType(K::class) + +operator fun TypedKey.invoke(kclass: KClass<*>): Pair, KotlinType> = + this to KotlinType(kclass) + +operator fun TypedKey.invoke(ktype: KType): Pair, KotlinType> = + this to KotlinType(ktype) + +operator fun TypedKey.invoke(fqname: String): Pair, KotlinType> = + this to KotlinType(fqname) + +operator fun TypedKey>.invoke(vararg classes: KClass<*>): Pair>, List> = + this to classes.map { KotlinType(it) } + +operator fun TypedKey>.invoke(vararg types: KType): Pair>, List> = + this to types.map { KotlinType(it) } + +operator fun TypedKey>.invoke(vararg fqnames: String): Pair>, List> = + this to fqnames.map { KotlinType(it) } inline operator fun TypedKey>.invoke(vararg vs: E): Pair>, List> = this to vs.toList() +@JvmName("invoke_kotlintype_map_from_kclass") +inline operator fun TypedKey>.invoke(vararg classes: Pair>): Pair>, Map> = + this to HashMap().also { it.putAll(classes.asSequence().map { (k, v) -> k to KotlinType(v) }) } + +@JvmName("invoke_kotlintype_map_from_ktype") +inline operator fun TypedKey>.invoke(vararg types: Pair): Pair>, Map> = + this to HashMap().also { it.putAll(types.asSequence().map { (k, v) -> k to KotlinType(v) }) } + +@JvmName("invoke_kotlintype_map_from_fqname") +inline operator fun TypedKey>.invoke(vararg fqnames: Pair): Pair>, Map> = + this to HashMap().also { it.putAll(fqnames.asSequence().map { (k, v) -> k to KotlinType(v) }) } + inline operator fun TypedKey>.invoke(vararg vs: Pair): Pair>, Map> = this to hashMapOf(*vs) diff --git a/libraries/tools/kotlin-gradle-plugin/build.gradle b/libraries/tools/kotlin-gradle-plugin/build.gradle index aad9b4e690f..31eca71d757 100644 --- a/libraries/tools/kotlin-gradle-plugin/build.gradle +++ b/libraries/tools/kotlin-gradle-plugin/build.gradle @@ -65,7 +65,6 @@ dependencies { runtime project(':kotlin-reflect') runtime project(':kotlin-scripting-common') runtime project(':kotlin-scripting-compiler') - runtime project(':kotlin-scripting-jvm') runtime project(path: ':kotlin-scripting-gradle', configuration: 'runtimeJar') // com.android.tools.build:gradle has ~50 unneeded transitive dependencies diff --git a/libraries/tools/kotlin-maven-plugin/src/main/java/org/jetbrains/kotlin/maven/ExecuteKotlinScriptMojo.java b/libraries/tools/kotlin-maven-plugin/src/main/java/org/jetbrains/kotlin/maven/ExecuteKotlinScriptMojo.java index 37dcb0daac8..a82a890ed2a 100644 --- a/libraries/tools/kotlin-maven-plugin/src/main/java/org/jetbrains/kotlin/maven/ExecuteKotlinScriptMojo.java +++ b/libraries/tools/kotlin-maven-plugin/src/main/java/org/jetbrains/kotlin/maven/ExecuteKotlinScriptMojo.java @@ -185,7 +185,7 @@ public class ExecuteKotlinScriptMojo extends AbstractMojo { configuration.put(CommonConfigurationKeys.MODULE_NAME, JvmAbi.DEFAULT_MODULE_NAME); ScriptingCompilerConfigurationExtensionKt.configureScriptDefinitions( - scriptTemplates, configuration, messageCollector, new HashMap<>() + scriptTemplates, configuration, this.getClass().getClassLoader(), messageCollector, new HashMap<>() ); KotlinCoreEnvironment environment = KotlinCoreEnvironment.createForProduction(rootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES); diff --git a/plugins/scripting/scripting-cli/build.gradle.kts b/plugins/scripting/scripting-cli/build.gradle.kts index d89069a9d31..fdd464eee6d 100644 --- a/plugins/scripting/scripting-cli/build.gradle.kts +++ b/plugins/scripting/scripting-cli/build.gradle.kts @@ -14,6 +14,8 @@ dependencies { compileOnly(project(":compiler:cli")) compile(project(":kotlin-scripting-common")) compile(project(":kotlin-scripting-jvm")) + compile(project(":kotlin-scripting-misc")) + compileOnly(project(":kotlin-reflect-api")) compileOnly(intellijCoreDep()) { includeJars("intellij-core") } compileOnly(intellijDep()) { includeJars("asm-all") } diff --git a/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/KotlinScriptDefinitionAdapterFromNewAPI.kt b/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/KotlinScriptDefinitionAdapterFromNewAPI.kt index b68fb051ca3..58bea68f21a 100644 --- a/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/KotlinScriptDefinitionAdapterFromNewAPI.kt +++ b/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/KotlinScriptDefinitionAdapterFromNewAPI.kt @@ -10,13 +10,11 @@ import org.jetbrains.kotlin.idea.KotlinFileType import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.name.NameUtils import org.jetbrains.kotlin.psi.KtScript -import org.jetbrains.kotlin.script.* +import org.jetbrains.kotlin.script.KotlinScriptDefinition import kotlin.reflect.KClass import kotlin.reflect.KType -import kotlin.script.experimental.api.ScriptCompileConfigurationProperties -import kotlin.script.experimental.api.ScriptDefinition -import kotlin.script.experimental.api.ScriptDefinitionProperties -import kotlin.script.experimental.api.ScriptingEnvironmentProperties +import kotlin.reflect.full.starProjectedType +import kotlin.script.experimental.api.* import kotlin.script.experimental.dependencies.DependenciesResolver import kotlin.script.experimental.jvm.impl.BridgeDependenciesResolver import kotlin.script.experimental.location.ScriptExpectedLocation @@ -28,8 +26,9 @@ abstract class KotlinScriptDefinitionAdapterFromNewAPIBase : KotlinScriptDefinit protected abstract val scriptFileExtensionWithDot: String - open val baseClass: KClass<*> - get() = scriptDefinition.compilationConfigurator.defaultConfiguration[ScriptingEnvironmentProperties.baseClass] + open val baseClass: KClass<*> by lazy { + getScriptingClass(scriptDefinition.compilationConfigurator.defaultConfiguration[ScriptingEnvironmentProperties.baseClass]) + } override val template: KClass<*> get() = baseClass @@ -54,17 +53,22 @@ abstract class KotlinScriptDefinitionAdapterFromNewAPIBase : KotlinScriptDefinit } override val acceptedAnnotations: List> by lazy { - scriptDefinition.compilationConfigurator.defaultConfiguration.getOrNull(ScriptCompileConfigurationProperties.refineConfigurationOnAnnotations) - ?: emptyList() + val annNames = + scriptDefinition.compilationConfigurator.defaultConfiguration.getOrNull(ScriptCompileConfigurationProperties.refineConfigurationOnAnnotations) + ?: emptyList() + annNames.map { getScriptingClass(it) as KClass } } override val implicitReceivers: List by lazy { - scriptDefinition.compilationConfigurator.defaultConfiguration.getOrNull(ScriptCompileConfigurationProperties.scriptImplicitReceivers) - ?: emptyList() + val rcNames = + scriptDefinition.compilationConfigurator.defaultConfiguration.getOrNull(ScriptCompileConfigurationProperties.scriptImplicitReceivers) + ?: emptyList() + rcNames.map { getScriptingClass(it).starProjectedType } } override val environmentVariables: List> by lazy { - scriptDefinition.compilationConfigurator.defaultConfiguration.getOrNull(ScriptCompileConfigurationProperties.contextVariables)?.map { (k, v) -> k to v } + scriptDefinition.compilationConfigurator.defaultConfiguration.getOrNull(ScriptCompileConfigurationProperties.contextVariables) + ?.map { (k, v) -> k to getScriptingClass(v).starProjectedType } ?: emptyList() } @@ -77,6 +81,18 @@ abstract class KotlinScriptDefinitionAdapterFromNewAPIBase : KotlinScriptDefinit ScriptExpectedLocation.SourcesOnly, ScriptExpectedLocation.TestsOnly ) + + private val scriptingClassGetter by lazy { + scriptDefinition.properties.getOrNull(ScriptingEnvironmentProperties.getScriptingClass) + ?: throw IllegalArgumentException("Expecting 'getScriptingClass' property in the scripting environment") + } + + private fun getScriptingClass(type: KotlinType) = + scriptingClassGetter( + type, + KotlinScriptDefinition::class, // Assuming that the KotlinScriptDefinition class is loaded in the proper classloader + scriptDefinition.properties + ) } diff --git a/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/LazyScriptDefinitionFromDiscoveredClass.kt b/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/LazyScriptDefinitionFromDiscoveredClass.kt index ef1fdc1f681..38fd2ba7a61 100644 --- a/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/LazyScriptDefinitionFromDiscoveredClass.kt +++ b/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/LazyScriptDefinitionFromDiscoveredClass.kt @@ -12,33 +12,27 @@ package org.jetbrains.kotlin.scripting.compiler.plugin import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.common.messages.MessageCollector -import org.jetbrains.kotlin.name.ClassId -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.org.objectweb.asm.AnnotationVisitor -import org.jetbrains.org.objectweb.asm.ClassReader -import org.jetbrains.org.objectweb.asm.ClassVisitor -import org.jetbrains.org.objectweb.asm.Opcodes import java.io.File -import java.net.URLClassLoader import kotlin.script.experimental.annotations.KotlinScript import kotlin.script.experimental.annotations.KotlinScriptFileExtension import kotlin.script.experimental.api.* import kotlin.script.experimental.definitions.ScriptDefinitionFromAnnotatedBaseClass +import kotlin.script.experimental.jvm.JvmDependency +import kotlin.script.experimental.jvm.JvmGetScriptingClass -class LazyScriptDefinitionFromDiscoveredClass( - classBytes: ByteArray, +class LazyScriptDefinitionFromDiscoveredClass internal constructor( + private val annotationsFromAsm: ArrayList, private val className: String, private val classpath: List, private val messageCollector: MessageCollector ) : KotlinScriptDefinitionAdapterFromNewAPIBase() { - private val annotationsFromAsm = loadAnnotationsFromClass(classBytes) - private val classloader by lazy { - // should use this cl to allow smooth interop with classes explicitly mentioned here, see e.g. scriptDefinition body - val parentClassloader = LazyScriptDefinitionFromDiscoveredClass::class.java.classLoader - if (classpath.isEmpty()) parentClassloader - else URLClassLoader(classpath.map { it.toURI().toURL() }.toTypedArray(), parentClassloader) - } + constructor( + classBytes: ByteArray, + className: String, + classpath: List, + messageCollector: MessageCollector + ) : this(loadAnnotationsFromClass(classBytes), className, classpath, messageCollector) override val scriptDefinition: ScriptDefinition by lazy { messageCollector.report( @@ -46,10 +40,11 @@ class LazyScriptDefinitionFromDiscoveredClass( "Configure scripting: loading script definition class $className using classpath $classpath\n. ${Thread.currentThread().stackTrace}" ) try { - val cls = classloader.loadClass(className).kotlin ScriptDefinitionFromAnnotatedBaseClass( ScriptingEnvironment( - ScriptingEnvironmentProperties.baseClass to cls + ScriptingEnvironmentProperties.baseClass to KotlinType(className), + ScriptingEnvironmentProperties.configurationDependencies to listOf(JvmDependency(classpath)), + ScriptingEnvironmentProperties.getScriptingClass to JvmGetScriptingClass() ) ) } catch (ex: ClassNotFoundException) { @@ -86,41 +81,3 @@ object InvalidScriptDefinition : ScriptDefinition { override val evaluator: ScriptEvaluator<*>? = null } -private class BinAnnData( - val name: String, - val args: ArrayList = arrayListOf() -) - -private class TemplateAnnotationVisitor(val anns: ArrayList = arrayListOf()) : AnnotationVisitor(Opcodes.ASM5) { - override fun visit(name: String?, value: Any?) { - anns.last().args.add(value.toString()) - } -} - -private class TemplateClassVisitor(val annVisitor: TemplateAnnotationVisitor) : ClassVisitor(Opcodes.ASM5) { - override fun visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor { - val shortName = jvmDescToClassId(desc).shortClassName.asString() - if (shortName.startsWith("KotlinScript")) { - annVisitor.anns.add(BinAnnData(shortName)) - } - return annVisitor - } -} - -private fun jvmDescToClassId(desc: String): ClassId { - assert(desc.startsWith("L") && desc.endsWith(";")) { "Not a JVM descriptor: $desc" } - val name = desc.substring(1, desc.length - 1) - val cid = ClassId.topLevel(FqName(name.replace('/', '.'))) - return cid -} - -private fun loadAnnotationsFromClass(fileContents: ByteArray): ArrayList { - - val visitor = - TemplateClassVisitor(TemplateAnnotationVisitor()) - - ClassReader(fileContents).accept(visitor, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES) - - return visitor.annVisitor.anns -} - diff --git a/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptiDefinitionsFromClasspathDiscoverySource.kt b/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptiDefinitionsFromClasspathDiscoverySource.kt index c9bae673e1e..acbf053d3ac 100644 --- a/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptiDefinitionsFromClasspathDiscoverySource.kt +++ b/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptiDefinitionsFromClasspathDiscoverySource.kt @@ -17,15 +17,19 @@ import java.net.URLClassLoader import java.util.jar.JarFile import kotlin.coroutines.experimental.buildSequence import kotlin.script.experimental.annotations.KotlinScript +import kotlin.script.experimental.api.KotlinType import kotlin.script.experimental.api.ScriptingEnvironment import kotlin.script.experimental.api.ScriptingEnvironmentProperties import kotlin.script.experimental.definitions.ScriptDefinitionFromAnnotatedBaseClass +import kotlin.script.experimental.jvm.JvmGetScriptingClass +import kotlin.script.templates.ScriptTemplateDefinition internal const val SCRIPT_DEFINITION_MARKERS_PATH = "META-INF/kotlin/script/templates/" class ScriptDefinitionsFromClasspathDiscoverySource( private val classpath: List, private val defaultScriptDefinitionClasspath: List, + private val scriptResolverEnv: Map, private val messageCollector: MessageCollector ) : ScriptDefinitionsSource { @@ -33,21 +37,28 @@ class ScriptDefinitionsFromClasspathDiscoverySource( discoverScriptTemplatesInClasspath( classpath, defaultScriptDefinitionClasspath, + this::class.java.classLoader, + scriptResolverEnv, messageCollector ) } } internal fun discoverScriptTemplatesInClasspath( - classpath: Iterable, + classpath: List, defaultScriptDefinitionClasspath: List, + baseClassLoader: ClassLoader, + scriptResolverEnv: Map, messageCollector: MessageCollector -): Sequence = buildSequence { +): Sequence = buildSequence { + // TODO: try to find a way to reduce classpath (and classloader) to minimal one needed to load script definition and its dependencies + val classLoader by lazy { + URLClassLoader(classpath.map { it.toURI().toURL() }.toTypedArray(), baseClassLoader) + } for (dep in classpath) { try { when { - // checking for extension is the compiler current behaviour, so the same logic is implemented here - dep.isFile && dep.extension == "jar" -> { + dep.isFile && dep.extension == "jar" -> { // checking for extension is the compiler current behaviour, so the same logic is implemented here val jar = JarFile(dep) if (jar.getJarEntry(SCRIPT_DEFINITION_MARKERS_PATH) != null) { for (template in jar.entries()) { @@ -60,17 +71,16 @@ internal fun discoverScriptTemplatesInClasspath( "Configure scripting: class not found $templateClassName" ) } else { - messageCollector.report( - CompilerMessageSeverity.LOGGING, - "Configure scripting: Added template $templateClassName from $dep" - ) - yield( - LazyScriptDefinitionFromDiscoveredClass( - jar.getInputStream(templateClass).readBytes(), - templateClassName, listOf(dep) + jar.extractClasspath(defaultScriptDefinitionClasspath), - messageCollector + loadScriptDefinition( + jar.getInputStream(templateClass).readBytes(), + templateClassName, classpath, { classLoader }, scriptResolverEnv, messageCollector + )?.let { + messageCollector.report( + CompilerMessageSeverity.LOGGING, + "Configure scripting: Added template $templateClassName from $dep" ) - ) + yield(it) + } } } } @@ -79,25 +89,30 @@ internal fun discoverScriptTemplatesInClasspath( dep.isDirectory -> { val dir = File(dep, SCRIPT_DEFINITION_MARKERS_PATH) if (dir.isDirectory) { - dir.listFiles().forEach { - val templateClass = File(dep, "${it.name.replace('.', '/')}.class") - if (!templateClass.exists() || !templateClass.isFile) { + val templateClasspath by lazy { + listOf(dep) + defaultScriptDefinitionClasspath + } + val classLoader by lazy { + URLClassLoader(templateClasspath.map { it.toURI().toURL() }.toTypedArray(), baseClassLoader) + } + dir.listFiles().forEach { templateClassNmae -> + val templateClassFile = File(dep, "${templateClassNmae.name.replace('.', '/')}.class") + if (!templateClassFile.exists() || !templateClassFile.isFile) { messageCollector.report( CompilerMessageSeverity.WARNING, - "Configure scripting: class not found ${it.name}" + "Configure scripting: class not found ${templateClassNmae.name}" ) } else { - messageCollector.report( - CompilerMessageSeverity.LOGGING, - "Configure scripting: Added template ${it.name} from $dep" - ) - yield( - LazyScriptDefinitionFromDiscoveredClass( - templateClass.readBytes(), - it.name, listOf(dep) + defaultScriptDefinitionClasspath, - messageCollector + loadScriptDefinition( + templateClassFile.readBytes(), + templateClassNmae.name, templateClasspath, { classLoader }, scriptResolverEnv, messageCollector + )?.let { + messageCollector.report( + CompilerMessageSeverity.LOGGING, + "Configure scripting: Added template ${templateClassNmae.name} from $dep" ) - ) + yield(it) + } } } } @@ -119,23 +134,138 @@ internal fun discoverScriptTemplatesInClasspath( } } +internal fun loadScriptTemplatesFromClasspath( + scriptTemplates: List, + classpath: List, + dependenciesClasspath: List, + baseClassLoader: ClassLoader, + scriptResolverEnv: Map, + messageCollector: MessageCollector +): Sequence = buildSequence { + val templatesLeftToFind = ArrayList() + // trying the direct classloading from baseClassloader first, since this is the most performant variant + for (template in scriptTemplates) { + val def = loadScriptDefinition(baseClassLoader, template, scriptResolverEnv, messageCollector) + if (def == null) { + templatesLeftToFind.add(template) + } else { + yield(def!!) + } + } + // then searching the remaining templates in the supplied classpath + if (templatesLeftToFind.isNotEmpty()) { + val templateClasspath by lazy { + classpath + dependenciesClasspath + } + val classLoader by lazy { + URLClassLoader(templateClasspath.map { it.toURI().toURL() }.toTypedArray(), baseClassLoader) + } + for (dep in classpath) { + try { + when { + dep.isFile && dep.extension == "jar" -> { // checking for extension is the compiler current behaviour, so the same logic is implemented here + val jar = JarFile(dep) + for (templateClassName in templatesLeftToFind) { + val templateClassEntry = jar.getJarEntry("${templateClassName.replace('.', '/')}.class") + if (templateClassEntry != null) { + loadScriptDefinition( + jar.getInputStream(templateClassEntry).readBytes(), + templateClassName, templateClasspath, { classLoader }, scriptResolverEnv, messageCollector + )?.let { + templatesLeftToFind.remove(templateClassName) + yield(it) + } + } + } + } + dep.isDirectory -> { + for (templateClassName in scriptTemplates) { + val templateClassFile = File(dep, "${templateClassName.replace('.', '/')}.class") + if (templateClassFile.exists()) { + loadScriptDefinition( + templateClassFile.readBytes(), + templateClassName, templateClasspath, { classLoader }, scriptResolverEnv, messageCollector + )?.let { + templatesLeftToFind.remove(templateClassName) + yield(it) + } + } + } + } + else -> { + // assuming that invalid classpath entries will be reported elsewhere anyway, so do not spam user with additional warnings here + messageCollector.report( + CompilerMessageSeverity.LOGGING, + "Configure scripting: Unknown classpath entry $dep" + ) + } + } + } catch (e: IOException) { + messageCollector.report( + CompilerMessageSeverity.WARNING, + "Configure scripting: unable to process classpath entry $dep: $e" + ) + } + } + } + if (templatesLeftToFind.isNotEmpty()) { + messageCollector.report( + CompilerMessageSeverity.WARNING, + "Configure scripting: unable to find script definition classes: $templatesLeftToFind" + ) + } +} + +private fun loadScriptDefinition( + templateClassBytes: ByteArray, + templateClassName: String, + templateClasspath: List, + getClassLoader: () -> ClassLoader, + scriptResolverEnv: Map, + messageCollector: MessageCollector +): KotlinScriptDefinition? { + val anns = loadAnnotationsFromClass(templateClassBytes) + for (ann in anns) { + var def: KotlinScriptDefinition? = null + if (ann.name == KotlinScript::class.simpleName) { + def = LazyScriptDefinitionFromDiscoveredClass(anns, templateClassName, templateClasspath, messageCollector) + } else if (ann.name == ScriptTemplateDefinition::class.simpleName) { + val templateClass = getClassLoader().loadClass(templateClassName).kotlin + def = KotlinScriptDefinitionFromAnnotatedTemplate(templateClass, scriptResolverEnv, templateClasspath) + } + if (def != null) { + messageCollector.report( + CompilerMessageSeverity.LOGGING, + "Configure scripting: Added template $templateClassName from $templateClasspath" + ) + return def + } + } + messageCollector.report( + CompilerMessageSeverity.WARNING, + "Configure scripting: $templateClassName is not marked with any known kotlin script annotation" + ) + return null +} + private fun JarFile.extractClasspath(defaultClasspath: List): List = manifest.mainAttributes.getValue("Class-Path")?.split(" ")?.map(::File) ?: defaultClasspath -internal fun loadScriptDefinition( - classloader: URLClassLoader, +private fun loadScriptDefinition( + classLoader: ClassLoader, template: String, scriptResolverEnv: Map, messageCollector: MessageCollector ): KotlinScriptDefinition? { try { - val cls = classloader.loadClass(template) + val cls = classLoader.loadClass(template) val def = if (cls.annotations.firstIsInstanceOrNull() != null) { KotlinScriptDefinitionAdapterFromNewAPI( ScriptDefinitionFromAnnotatedBaseClass( ScriptingEnvironment( - ScriptingEnvironmentProperties.baseClass to cls.kotlin + ScriptingEnvironmentProperties.baseClass to KotlinType(cls.kotlin), + ScriptingEnvironmentProperties.getScriptingClass to JvmGetScriptingClass() ) ) ) @@ -149,7 +279,7 @@ internal fun loadScriptDefinition( ) return def } catch (ex: ClassNotFoundException) { - messageCollector.report(CompilerMessageSeverity.ERROR, "Cannot find script definition template class $template") + // return null } catch (ex: Exception) { messageCollector.report( CompilerMessageSeverity.ERROR, diff --git a/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptingCompilerConfigurationExtension.kt b/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptingCompilerConfigurationExtension.kt index 9ef59b54b60..3ac59a44e16 100644 --- a/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptingCompilerConfigurationExtension.kt +++ b/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptingCompilerConfigurationExtension.kt @@ -7,7 +7,6 @@ package org.jetbrains.kotlin.scripting.compiler.plugin import com.intellij.mock.MockProject import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys -import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.cli.jvm.config.jvmClasspathRoots import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar @@ -16,7 +15,6 @@ import org.jetbrains.kotlin.config.JVMConfigurationKeys import org.jetbrains.kotlin.extensions.CompilerConfigurationExtension import org.jetbrains.kotlin.script.StandardScriptDefinition import java.io.File -import java.net.URLClassLoader class ScriptingCompilerConfigurationExtension(val project: MockProject) : CompilerConfigurationExtension { @@ -41,6 +39,7 @@ class ScriptingCompilerConfigurationExtension(val project: MockProject) : Compil configureScriptDefinitions( explicitScriptDefinitions, configuration, + this::class.java.classLoader, messageCollector, scriptResolverEnv ) @@ -57,6 +56,7 @@ class ScriptingCompilerConfigurationExtension(val project: MockProject) : Compil ScriptDefinitionsFromClasspathDiscoverySource( configuration.jvmClasspathRoots, emptyList(), + configuration.get(ScriptingConfigurationKeys.LEGACY_SCRIPT_RESOLVER_ENVIRONMENT_OPTION) ?: emptyMap(), messageCollector ) ) @@ -74,31 +74,17 @@ class ScriptingCompilerConfigurationComponentRegistrar : ComponentRegistrar { fun configureScriptDefinitions( scriptTemplates: List, configuration: CompilerConfiguration, + baseClassloader: ClassLoader, messageCollector: MessageCollector, scriptResolverEnv: Map ) { val classpath = configuration.jvmClasspathRoots // TODO: consider using escaping to allow kotlin escaped names in class names if (scriptTemplates.isNotEmpty()) { - val classloader = - URLClassLoader(classpath.map { it.toURI().toURL() }.toTypedArray(), Thread.currentThread().contextClassLoader) - var hasErrors = false - for (template in scriptTemplates) { - val def = loadScriptDefinition( - classloader, - template, - scriptResolverEnv, - messageCollector - ) - if (!hasErrors && def == null) hasErrors = true - if (def != null) { - configuration.add(JVMConfigurationKeys.SCRIPT_DEFINITIONS, def) + loadScriptTemplatesFromClasspath(scriptTemplates, classpath, emptyList(), baseClassloader, scriptResolverEnv, messageCollector) + .forEach { + configuration.add(JVMConfigurationKeys.SCRIPT_DEFINITIONS, it) } - } - if (hasErrors) { - messageCollector.report(CompilerMessageSeverity.LOGGING, "(Classpath used for templates loading: $classpath)") - return - } } } diff --git a/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/asmBasedAnnotationsLoading.kt b/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/asmBasedAnnotationsLoading.kt new file mode 100644 index 00000000000..0c3672029df --- /dev/null +++ b/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/asmBasedAnnotationsLoading.kt @@ -0,0 +1,52 @@ +/* + * 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.scripting.compiler.plugin + +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.org.objectweb.asm.AnnotationVisitor +import org.jetbrains.org.objectweb.asm.ClassReader +import org.jetbrains.org.objectweb.asm.ClassVisitor +import org.jetbrains.org.objectweb.asm.Opcodes + +internal class BinAnnData( + val name: String, + val args: ArrayList = arrayListOf() +) + +private class TemplateAnnotationVisitor(val anns: ArrayList = arrayListOf()) : AnnotationVisitor(Opcodes.ASM5) { + override fun visit(name: String?, value: Any?) { + anns.last().args.add(value.toString()) + } +} + +private class TemplateClassVisitor(val annVisitor: TemplateAnnotationVisitor) : ClassVisitor(Opcodes.ASM5) { + override fun visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor { + val shortName = jvmDescToClassId(desc).shortClassName.asString() + if (shortName.startsWith("KotlinScript")) { + annVisitor.anns.add(BinAnnData(shortName)) + } + return annVisitor + } +} + +private fun jvmDescToClassId(desc: String): ClassId { + assert(desc.startsWith("L") && desc.endsWith(";")) { "Not a JVM descriptor: $desc" } + val name = desc.substring(1, desc.length - 1) + val cid = ClassId.topLevel(FqName(name.replace('/', '.'))) + return cid +} + +internal fun loadAnnotationsFromClass(fileContents: ByteArray): ArrayList { + + val visitor = + TemplateClassVisitor(TemplateAnnotationVisitor()) + + ClassReader(fileContents).accept(visitor, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES) + + return visitor.annVisitor.anns +} + diff --git a/plugins/scripting/scripting-cli/tests/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptingCompilerPluginTest.kt b/plugins/scripting/scripting-cli/tests/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptingCompilerPluginTest.kt index 301a125c0b5..00043317a45 100644 --- a/plugins/scripting/scripting-cli/tests/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptingCompilerPluginTest.kt +++ b/plugins/scripting/scripting-cli/tests/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptingCompilerPluginTest.kt @@ -113,7 +113,7 @@ class ScriptingCompilerPluginTest : TestCaseWithTmpdir() { messageCollector.clear() val lazyDefsSeq = - discoverScriptTemplatesInClasspath(listOf(defsOut), emptyList(), messageCollector) + discoverScriptTemplatesInClasspath(listOf(defsOut), emptyList(), this::class.java.classLoader, emptyMap(), messageCollector) assertTrue(messageCollector.messages.isEmpty()) { "Unexpected messages from discovery sequence (should be empty):\n$messageCollector" diff --git a/plugins/scripting/scripting-gradle/build.gradle.kts b/plugins/scripting/scripting-gradle/build.gradle.kts index 13d4a960d04..20de4ca9ebb 100644 --- a/plugins/scripting/scripting-gradle/build.gradle.kts +++ b/plugins/scripting/scripting-gradle/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { packedJars(project(":kotlin-scripting-compiler")) { isTransitive = false } runtime(project(":kotlin-scripting-common")) runtime(project(":kotlin-scripting-jvm")) + runtime(project(":kotlin-scripting-misc")) } sourceSets {