Improve classpath extraction from classloader:

- implement opt-in unpacking/caching of the jar collection archives,
  such as spring boot fat jars and WARs, to the temp dir, and creating
  a valid classpath from extracted archive
- turning it on for the new default jsr223 engine
- fallback to extracting classpath from resource URLs if the classloader
  is not known url-based one
- refactor and optimize extraction
- cache the last retrieved classpath in the default JSR-223 script engine
  factory
This commit is contained in:
Ilya Chernikov
2019-05-28 19:18:13 +02:00
parent e95569a273
commit f986856d03
6 changed files with 252 additions and 65 deletions
@@ -6,10 +6,14 @@
package kotlin.script.experimental.jsr223 package kotlin.script.experimental.jsr223
import org.jetbrains.kotlin.cli.common.repl.KotlinJsr223JvmScriptEngineFactoryBase import org.jetbrains.kotlin.cli.common.repl.KotlinJsr223JvmScriptEngineFactoryBase
import java.io.File
import javax.script.ScriptEngine import javax.script.ScriptEngine
import kotlin.script.experimental.api.ScriptCompilationConfiguration import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.jvm.JvmScriptCompilationConfigurationBuilder
import kotlin.script.experimental.jvm.dependenciesFromCurrentContext import kotlin.script.experimental.jvm.dependenciesFromCurrentContext
import kotlin.script.experimental.jvm.jvm import kotlin.script.experimental.jvm.jvm
import kotlin.script.experimental.jvm.updateClasspath
import kotlin.script.experimental.jvm.util.scriptCompilationClasspathFromContext
import kotlin.script.experimental.jvmhost.createJvmCompilationConfigurationFromTemplate import kotlin.script.experimental.jvmhost.createJvmCompilationConfigurationFromTemplate
import kotlin.script.experimental.jvmhost.createJvmEvaluationConfigurationFromTemplate import kotlin.script.experimental.jvmhost.createJvmEvaluationConfigurationFromTemplate
import kotlin.script.experimental.jvmhost.jsr223.KotlinJsr223ScriptEngineImpl import kotlin.script.experimental.jvmhost.jsr223.KotlinJsr223ScriptEngineImpl
@@ -18,13 +22,31 @@ class KotlinJsr223DefaultScriptEngineFactory : KotlinJsr223JvmScriptEngineFactor
private val compilationConfiguration = createJvmCompilationConfigurationFromTemplate<KotlinJsr223DefaultScript>() private val compilationConfiguration = createJvmCompilationConfigurationFromTemplate<KotlinJsr223DefaultScript>()
private val evaluationConfiguration = createJvmEvaluationConfigurationFromTemplate<KotlinJsr223DefaultScript>() private val evaluationConfiguration = createJvmEvaluationConfigurationFromTemplate<KotlinJsr223DefaultScript>()
private var lastClassLoader: ClassLoader? = null
private var lastClassPath: List<File>? = null
@Synchronized
protected fun JvmScriptCompilationConfigurationBuilder.dependenciesFromCurrentContext() {
val currentClassLoader = Thread.currentThread().contextClassLoader
val classPath = if (lastClassLoader == null || lastClassLoader != currentClassLoader) {
scriptCompilationClasspathFromContext(
classLoader = currentClassLoader,
wholeClasspath = true,
unpackJarCollections = true
).also {
lastClassLoader = currentClassLoader
lastClassPath = it
}
} else lastClassPath!!
updateClasspath(classPath)
}
override fun getScriptEngine(): ScriptEngine = override fun getScriptEngine(): ScriptEngine =
KotlinJsr223ScriptEngineImpl( KotlinJsr223ScriptEngineImpl(
this, this,
ScriptCompilationConfiguration(compilationConfiguration) { ScriptCompilationConfiguration(compilationConfiguration) {
jvm { jvm {
dependenciesFromCurrentContext(wholeClasspath = true) dependenciesFromCurrentContext()
} }
}, },
evaluationConfiguration evaluationConfiguration
@@ -0,0 +1,62 @@
/*
* Copyright 2010-2019 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 kotlin.script.experimental.jvmhost.test
import junit.framework.TestCase
import org.junit.Assert
import org.junit.Test
import java.io.File
import java.io.FileOutputStream
import java.net.URLClassLoader
import java.util.jar.JarEntry
import java.util.jar.JarOutputStream
import kotlin.script.experimental.jvm.util.classpathFromClassloader
class ClassPathTest : TestCase() {
@Test
fun testExtractFromFat() {
val collection = createTempFile("col", ".jar").apply { createCollectionJar(emulatedCollectionFiles, "BOOT-INF") }
val cl = URLClassLoader(arrayOf(collection.toURI().toURL()), null)
val cp = classpathFromClassloader(cl, true)
Assert.assertTrue(cp != null && cp.isNotEmpty())
testUnpackedCollection(cp!!, emulatedCollectionFiles)
}
}
private val emulatedCollectionFiles = arrayOf(
"classes/a/b.class",
"lib/c-d.jar"
)
fun File.createCollectionJar(fileNames: Array<String>, infDirName: String) {
FileOutputStream(this).use { fileStream ->
val jarStream = JarOutputStream(fileStream)
jarStream.putNextEntry(JarEntry("$infDirName/classes/"))
jarStream.putNextEntry(JarEntry("$infDirName/lib/"))
for (name in fileNames) {
jarStream.putNextEntry(JarEntry("$infDirName/$name"))
jarStream.write(name.toByteArray())
}
jarStream.finish()
}
}
fun testUnpackedCollection(classpath: List<File>, fileNames: Array<String>) {
fun List<String>.checkFiles(root: File) = forEach {
val file = File(root, it)
Assert.assertTrue(file.exists())
Assert.assertEquals(it, file.readText())
}
val (classes, jars) = fileNames.partition { it.startsWith("classes") }
val (cpClasses, cpJars) = classpath.partition { it.isDirectory && it.name == "classes" }
Assert.assertTrue(cpClasses.size == 1)
classes.checkFiles(cpClasses.first().parentFile)
jars.checkFiles(cpJars.first().parentFile.parentFile)
}
@@ -105,7 +105,7 @@ internal fun List<ScriptDependency>?.toClassPathOrEmpty() = this?.flatMap { (it
internal fun List<SourceCode>?.toFilesOrEmpty() = this?.map { internal fun List<SourceCode>?.toFilesOrEmpty() = this?.map {
val externalSource = it as? ExternalSourceCode val externalSource = it as? ExternalSourceCode
externalSource?.externalLocation?.toFile() externalSource?.externalLocation?.toFileOrNull()
?: throw RuntimeException("Unsupported source in requireSources parameter - only local files are supported now (${externalSource?.externalLocation})") ?: throw RuntimeException("Unsupported source in requireSources parameter - only local files are supported now (${externalSource?.externalLocation})")
} ?: emptyList() } ?: emptyList()
@@ -6,6 +6,7 @@
package kotlin.script.experimental.jvm.impl package kotlin.script.experimental.jvm.impl
import java.io.File import java.io.File
import java.net.JarURLConnection
import java.net.URL import java.net.URL
// Based on an implementation in com.intellij.openapi.application.PathManager.getResourceRoot // Based on an implementation in com.intellij.openapi.application.PathManager.getResourceRoot
@@ -29,7 +30,7 @@ private fun extractRoot(resourceURL: URL, resourcePath: String): String? {
var resultPath: String? = null var resultPath: String? = null
val protocol = resourceURL.protocol val protocol = resourceURL.protocol
if (protocol == FILE_PROTOCOL) { if (protocol == FILE_PROTOCOL) {
val path = resourceURL.toFile()!!.path val path = resourceURL.toFileOrNull()!!.path
val testPath = path.replace('\\', '/') val testPath = path.replace('\\', '/')
val testResourcePath = resourcePath.replace('\\', '/') val testResourcePath = resourcePath.replace('\\', '/')
if (testPath.endsWith(testResourcePath, ignoreCase = true)) { if (testPath.endsWith(testResourcePath, ignoreCase = true)) {
@@ -57,7 +58,7 @@ private fun splitJarUrl(url: String): Pair<String, String>? {
if (jarPath.startsWith(FILE_PROTOCOL)) { if (jarPath.startsWith(FILE_PROTOCOL)) {
try { try {
jarPath = URL(jarPath).toFile()!!.path.replace('\\', '/') jarPath = URL(jarPath).toFileOrNull()!!.path.replace('\\', '/')
} catch (e: Exception) { } catch (e: Exception) {
jarPath = jarPath.substring(FILE_PROTOCOL.length) jarPath = jarPath.substring(FILE_PROTOCOL.length)
if (jarPath.startsWith(SCHEME_SEPARATOR)) { if (jarPath.startsWith(SCHEME_SEPARATOR)) {
@@ -91,11 +92,15 @@ fun tryGetResourcePathForClassByName(name: String, classLoader: ClassLoader): Fi
null null
} }
internal fun URL.toFile() = internal fun URL.toFileOrNull() =
try { try {
File(toURI().schemeSpecificPart) File(toURI().schemeSpecificPart).canonicalFile
} catch (e: java.net.URISyntaxException) { } catch (e: java.net.URISyntaxException) {
if (protocol != "file") null if (protocol != "file") null
else File(file) else File(file).canonicalFile
} }
internal fun URL.toContainingFileOrNull(): File? =
if (protocol == "jar") {
(openConnection() as? JarURLConnection)?.jarFileURL?.toFileOrNull()
} else toFileOrNull()
@@ -31,17 +31,24 @@ fun JvmScriptCompilationConfigurationBuilder.dependenciesFromClassContext(
dependenciesFromClassloader(*libraries, classLoader = contextClass.java.classLoader, wholeClasspath = wholeClasspath) dependenciesFromClassloader(*libraries, classLoader = contextClass.java.classLoader, wholeClasspath = wholeClasspath)
} }
fun JvmScriptCompilationConfigurationBuilder.dependenciesFromCurrentContext(vararg libraries: String, wholeClasspath: Boolean = false) { fun JvmScriptCompilationConfigurationBuilder.dependenciesFromCurrentContext(
dependenciesFromClassloader(*libraries, wholeClasspath = wholeClasspath) vararg libraries: String,
wholeClasspath: Boolean = false,
unpackJarCollections: Boolean = false
) {
dependenciesFromClassloader(*libraries, wholeClasspath = wholeClasspath, unpackJarCollections = unpackJarCollections)
} }
fun JvmScriptCompilationConfigurationBuilder.dependenciesFromClassloader( fun JvmScriptCompilationConfigurationBuilder.dependenciesFromClassloader(
vararg libraries: String, vararg libraries: String,
classLoader: ClassLoader = Thread.currentThread().contextClassLoader, classLoader: ClassLoader = Thread.currentThread().contextClassLoader,
wholeClasspath: Boolean = false wholeClasspath: Boolean = false,
unpackJarCollections: Boolean = false
) { ) {
updateClasspath( updateClasspath(
scriptCompilationClasspathFromContext(*libraries, classLoader = classLoader, wholeClasspath = wholeClasspath) scriptCompilationClasspathFromContext(
*libraries, classLoader = classLoader, wholeClasspath = wholeClasspath, unpackJarCollections = unpackJarCollections
)
) )
} }
@@ -6,11 +6,13 @@
package kotlin.script.experimental.jvm.util package kotlin.script.experimental.jvm.util
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.net.URL import java.net.URL
import java.net.URLClassLoader import java.net.URLClassLoader
import java.util.jar.JarInputStream
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.script.experimental.jvm.impl.toFile import kotlin.script.experimental.jvm.impl.toContainingFileOrNull
import kotlin.script.experimental.jvm.impl.tryGetResourcePathForClass import kotlin.script.experimental.jvm.impl.tryGetResourcePathForClass
import kotlin.script.experimental.jvm.impl.tryGetResourcePathForClassByName import kotlin.script.experimental.jvm.impl.tryGetResourcePathForClassByName
import kotlin.script.templates.standard.ScriptTemplateWithArgs import kotlin.script.templates.standard.ScriptTemplateWithArgs
@@ -32,6 +34,11 @@ internal const val KOTLIN_SCRIPTING_JVM_JAR = "kotlin-scripting-jvm.jar"
internal const val KOTLIN_COMPILER_NAME = "kotlin-compiler" internal const val KOTLIN_COMPILER_NAME = "kotlin-compiler"
internal const val KOTLIN_COMPILER_JAR = "$KOTLIN_COMPILER_NAME.jar" internal const val KOTLIN_COMPILER_JAR = "$KOTLIN_COMPILER_NAME.jar"
private val JAR_COLLECTIONS_CLASSES_PATHS = arrayOf("BOOT-INF/classes", "WEB-INF/classes")
private val JAR_COLLECTIONS_LIB_PATHS = arrayOf("BOOT-INF/lib", "WEB-INF/lib")
private val JAR_COLLECTIONS_KEY_PATHS = JAR_COLLECTIONS_CLASSES_PATHS + JAR_COLLECTIONS_LIB_PATHS
private const val JAR_MANIFEST_RESOURCE_NAME = "META-INF/MANIFEST.MF"
internal const val KOTLIN_SCRIPT_CLASSPATH_PROPERTY = "kotlin.script.classpath" internal const val KOTLIN_SCRIPT_CLASSPATH_PROPERTY = "kotlin.script.classpath"
internal const val KOTLIN_COMPILER_CLASSPATH_PROPERTY = "kotlin.compiler.classpath" internal const val KOTLIN_COMPILER_CLASSPATH_PROPERTY = "kotlin.compiler.classpath"
internal const val KOTLIN_COMPILER_JAR_PROPERTY = "kotlin.compiler.jar" internal const val KOTLIN_COMPILER_JAR_PROPERTY = "kotlin.compiler.jar"
@@ -43,32 +50,105 @@ internal const val KOTLIN_RUNTIME_JAR_PROPERTY = "kotlin.java.runtime.jar"
internal const val KOTLIN_SCRIPT_RUNTIME_JAR_PROPERTY = "kotlin.script.runtime.jar" internal const val KOTLIN_SCRIPT_RUNTIME_JAR_PROPERTY = "kotlin.script.runtime.jar"
private val validClasspathFilesExtensions = setOf("jar", "zip", "java") private val validClasspathFilesExtensions = setOf("jar", "zip", "java")
private val validJarCollectionFilesExtensions = setOf("jar", "war", "zip")
fun classpathFromClassloader(classLoader: ClassLoader): List<File>? = fun classpathFromClassloader(currentClassLoader: ClassLoader, unpackJarCollections: Boolean = false): List<File>? {
allRelatedClassLoaders(classLoader).toList().flatMap { val processedJars = hashSetOf<File>()
val urls = (it as? URLClassLoader)?.urLs?.asList() val unpackJarCollectionsDir by lazy {
?: try { createTempDir("unpackedJarCollections").canonicalFile.also {
// e.g. for IDEA platform UrlClassLoader Runtime.getRuntime().addShutdownHook(Thread {
val getUrls = it::class.java.getMethod("getUrls") it.deleteRecursively()
getUrls.isAccessible = true })
val result = getUrls.invoke(it) as? List<Any?>
result?.filterIsInstance<URL>()
} catch (e: Throwable) {
null
}
urls?.mapNotNull {
// taking only classpath elements pointing to dirs (presumably with classes) or jars, because this classpath is intended for
// usage with the kotlin compiler, which cannot process other types of entries, e.g. jni libs
it.toFile()?.takeIf { el -> el.isDirectory || validClasspathFilesExtensions.any { el.extension == it } }
} }
?: emptyList() }
}.distinct().takeIf { it.isNotEmpty() } return allRelatedClassLoaders(currentClassLoader).flatMap { classLoader ->
var classPath = emptySequence<File>()
if (unpackJarCollections && JAR_COLLECTIONS_KEY_PATHS.any { classLoader.getResource(it)?.file?.isNotEmpty() == true }) {
// if cache dir is specified, find all jar collections (spring boot fat jars and WARs so far, and unpack it accordingly
val jarCollections = JAR_COLLECTIONS_KEY_PATHS.asSequence().flatMap { currentClassLoader.getResources(it).asSequence() }
.mapNotNull {
it.toContainingFileOrNull()?.takeIf { file ->
// additionally mark/check processed collection jars since unpacking is expensive
file.extension in validJarCollectionFilesExtensions && processedJars.add(file)
}
}
classPath += jarCollections.flatMap { it.unpackJarCollection(unpackJarCollectionsDir) }.filter { it.isValidClasspathFile() }
}
classPath += when (classLoader) {
is URLClassLoader -> {
classLoader.urLs.asSequence().mapNotNull { url -> url.toValidClasspathFileOrNull() }
}
else -> {
classLoader.classPathFromGetUrlsMethodOrNull()
?: classLoader.classPathFromManifestResourceUrls()
}
}
classPath
}.filter { processedJars.add(it) }
.toList().takeIf { it.isNotEmpty() }
}
internal fun URL.toValidClasspathFileOrNull(): File? = toContainingFileOrNull()?.takeIf { it.isValidClasspathFile() }
internal fun File.isValidClasspathFile(): Boolean =
isDirectory || (isFile && extension in validClasspathFilesExtensions)
private fun ClassLoader.classPathFromGetUrlsMethodOrNull(): Sequence<File>? {
return try {
// e.g. for IDEA platform UrlClassLoader
val getUrls = this::class.java.getMethod("getUrls")
getUrls.isAccessible = true
val result = getUrls.invoke(this) as? List<Any?>
result?.asSequence()?.filterIsInstance<URL>()?.mapNotNull { it.toValidClasspathFileOrNull() }
} catch (e: Throwable) {
null
}
}
internal fun ClassLoader.classPathFromManifestResourceUrls(): Sequence<File> =
getResources(JAR_MANIFEST_RESOURCE_NAME).asSequence().distinct().mapNotNull { it.toValidClasspathFileOrNull() }
private fun File.unpackJarCollection(rootTempDir: File?): Sequence<File> {
val targetDir = createTempDir(nameWithoutExtension, directory = rootTempDir)
return try {
ArrayList<File>().apply {
JarInputStream(FileInputStream(this@unpackJarCollection)).use { jarInputStream ->
for (classesDir in JAR_COLLECTIONS_CLASSES_PATHS) {
add(File(targetDir, classesDir))
}
do {
val entry = jarInputStream.nextJarEntry
if (entry != null) {
try {
if (!entry.isDirectory) {
val file = File(targetDir, entry.name)
if (JAR_COLLECTIONS_LIB_PATHS.any { entry.name.startsWith("$it/") }) {
add(file)
}
file.parentFile.mkdirs()
file.outputStream().use { outputStream ->
jarInputStream.copyTo(outputStream)
outputStream.flush()
}
}
} finally {
jarInputStream.closeEntry()
}
}
} while (entry != null)
}
}.asSequence()
} catch (e: Throwable) {
targetDir.deleteRecursively()
throw e
}
}
fun classpathFromClasspathProperty(): List<File>? = fun classpathFromClasspathProperty(): List<File>? =
System.getProperty("java.class.path") System.getProperty("java.class.path")
?.split(String.format("\\%s", File.pathSeparatorChar).toRegex()) ?.split(String.format("\\%s", File.pathSeparatorChar).toRegex())
?.dropLastWhile(String::isEmpty) ?.dropLastWhile(String::isEmpty)
?.map(::File) ?.map(::File)
fun classpathFromClass(classLoader: ClassLoader, klass: KClass<out Any>): List<File>? = fun classpathFromClass(classLoader: ClassLoader, klass: KClass<out Any>): List<File>? =
classpathFromFQN(classLoader, klass.qualifiedName!!) classpathFromFQN(classLoader, klass.qualifiedName!!)
@@ -82,21 +162,22 @@ fun classpathFromFQN(classLoader: ClassLoader, fqn: String): List<File>? {
} }
fun File.matchMaybeVersionedFile(baseName: String) = fun File.matchMaybeVersionedFile(baseName: String) =
name == baseName || name == baseName ||
name == baseName.removeSuffix(".jar") || // for classes dirs name == baseName.removeSuffix(".jar") || // for classes dirs
Regex(Regex.escape(baseName.removeSuffix(".jar")) + "(-\\d.*)?\\.jar").matches(name) Regex(Regex.escape(baseName.removeSuffix(".jar")) + "(-\\d.*)?\\.jar").matches(name)
fun File.hasParentNamed(baseName: String): Boolean = fun File.hasParentNamed(baseName: String): Boolean =
nameWithoutExtension == baseName || parentFile?.hasParentNamed(baseName) ?: false nameWithoutExtension == baseName || parentFile?.hasParentNamed(baseName) ?: false
private const val KOTLIN_COMPILER_EMBEDDABLE_JAR = "$KOTLIN_COMPILER_NAME-embeddable.jar" private const val KOTLIN_COMPILER_EMBEDDABLE_JAR = "$KOTLIN_COMPILER_NAME-embeddable.jar"
// Iterating over classloaders tree in a regular, parent-first order
private fun allRelatedClassLoaders(clsLoader: ClassLoader, visited: MutableSet<ClassLoader> = HashSet()): Sequence<ClassLoader> { private fun allRelatedClassLoaders(clsLoader: ClassLoader, visited: MutableSet<ClassLoader> = HashSet()): Sequence<ClassLoader> {
if (!visited.add(clsLoader)) return emptySequence() if (!visited.add(clsLoader)) return emptySequence()
val singleParent = clsLoader.parent val singleParent = clsLoader.parent
if (singleParent != null) if (singleParent != null)
return sequenceOf(clsLoader) + sequenceOf(singleParent).flatMap { allRelatedClassLoaders(it, visited) } return sequenceOf(singleParent).flatMap { allRelatedClassLoaders(it, visited) } + clsLoader
return try { return try {
val field = clsLoader.javaClass.getDeclaredField("myParents") // com.intellij.ide.plugins.cl.PluginClassLoader val field = clsLoader.javaClass.getDeclaredField("myParents") // com.intellij.ide.plugins.cl.PluginClassLoader
@@ -104,7 +185,9 @@ private fun allRelatedClassLoaders(clsLoader: ClassLoader, visited: MutableSet<C
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val arrayOfClassLoaders = field.get(clsLoader) as Array<ClassLoader> val arrayOfClassLoaders = field.get(clsLoader) as Array<ClassLoader>
sequenceOf(clsLoader) + arrayOfClassLoaders.asSequence().flatMap { allRelatedClassLoaders(it, visited) } // TODO: PluginClassLoader uses filtering (mustBeLoadedByPlatform), consider using the same logic, if possible
// (untill proper compiling from classloader instead of classpath is implemented)
arrayOfClassLoaders.asSequence().flatMap { allRelatedClassLoaders(it, visited) } + clsLoader
} catch (e: Throwable) { } catch (e: Throwable) {
sequenceOf(clsLoader) sequenceOf(clsLoader)
} }
@@ -112,9 +195,9 @@ private fun allRelatedClassLoaders(clsLoader: ClassLoader, visited: MutableSet<C
internal fun List<File>.takeIfContainsAll(vararg keyNames: String): List<File>? = internal fun List<File>.takeIfContainsAll(vararg keyNames: String): List<File>? =
takeIf { classpath -> takeIf { classpath ->
keyNames.all { key -> classpath.any { it.matchMaybeVersionedFile(key) } } keyNames.all { key -> classpath.any { it.matchMaybeVersionedFile(key) } }
} }
internal fun List<File>.filterIfContainsAll(vararg keyNames: String): List<File>? { internal fun List<File>.filterIfContainsAll(vararg keyNames: String): List<File>? {
val res = hashMapOf<String, File>() val res = hashMapOf<String, File>()
@@ -133,25 +216,30 @@ internal fun List<File>.filterIfContainsAll(vararg keyNames: String): List<File>
} }
internal fun List<File>.takeIfContainsAny(vararg keyNames: String): List<File>? = internal fun List<File>.takeIfContainsAny(vararg keyNames: String): List<File>? =
takeIf { classpath -> takeIf { classpath ->
keyNames.any { key -> classpath.any { it.matchMaybeVersionedFile(key) } } keyNames.any { key -> classpath.any { it.matchMaybeVersionedFile(key) } }
} }
fun scriptCompilationClasspathFromContextOrNull( fun scriptCompilationClasspathFromContextOrNull(
vararg keyNames: String, vararg keyNames: String,
classLoader: ClassLoader = Thread.currentThread().contextClassLoader, classLoader: ClassLoader = Thread.currentThread().contextClassLoader,
wholeClasspath: Boolean = false wholeClasspath: Boolean = false,
unpackJarCollections: Boolean = false
): List<File>? { ): List<File>? {
fun List<File>.takeAndFilter() = when { fun List<File>.takeAndFilter() = when {
isEmpty() -> null isEmpty() -> null
wholeClasspath -> takeIfContainsAll(*keyNames) wholeClasspath -> takeIfContainsAll(*keyNames)
else -> filterIfContainsAll(*keyNames) else -> filterIfContainsAll(*keyNames)
} }
return System.getProperty(KOTLIN_SCRIPT_CLASSPATH_PROPERTY)?.split(File.pathSeparator)?.map(::File)
?: classpathFromClassloader(classLoader)?.takeAndFilter() val fromProperty = System.getProperty(KOTLIN_SCRIPT_CLASSPATH_PROPERTY)?.split(File.pathSeparator)?.map(::File)
if (fromProperty != null) return fromProperty
return classpathFromClassloader(classLoader, unpackJarCollections)?.takeAndFilter()
?: classpathFromClasspathProperty()?.takeAndFilter() ?: classpathFromClasspathProperty()?.takeAndFilter()
} }
fun scriptCompilationClasspathFromContextOrStdlib( fun scriptCompilationClasspathFromContextOrStdlib(
vararg keyNames: String, vararg keyNames: String,
classLoader: ClassLoader = Thread.currentThread().contextClassLoader, classLoader: ClassLoader = Thread.currentThread().contextClassLoader,
@@ -162,25 +250,27 @@ fun scriptCompilationClasspathFromContextOrStdlib(
classLoader = classLoader, classLoader = classLoader,
wholeClasspath = wholeClasspath wholeClasspath = wholeClasspath
) )
?: KotlinJars.kotlinScriptStandardJars ?: KotlinJars.kotlinScriptStandardJars
fun scriptCompilationClasspathFromContext( fun scriptCompilationClasspathFromContext(
vararg keyNames: String, vararg keyNames: String,
classLoader: ClassLoader = Thread.currentThread().contextClassLoader, classLoader: ClassLoader = Thread.currentThread().contextClassLoader,
wholeClasspath: Boolean = false wholeClasspath: Boolean = false,
unpackJarCollections: Boolean = false
): List<File> = ): List<File> =
scriptCompilationClasspathFromContextOrNull( scriptCompilationClasspathFromContextOrNull(
*keyNames, *keyNames,
classLoader = classLoader, classLoader = classLoader,
wholeClasspath = wholeClasspath wholeClasspath = wholeClasspath,
unpackJarCollections = unpackJarCollections
) )
?: throw Exception("Unable to get script compilation classpath from context, please specify explicit classpath via \"$KOTLIN_SCRIPT_CLASSPATH_PROPERTY\" property") ?: throw Exception("Unable to get script compilation classpath from context, please specify explicit classpath via \"$KOTLIN_SCRIPT_CLASSPATH_PROPERTY\" property")
object KotlinJars { object KotlinJars {
private val explicitCompilerClasspath: List<File>? by lazy { private val explicitCompilerClasspath: List<File>? by lazy {
System.getProperty(KOTLIN_COMPILER_CLASSPATH_PROPERTY)?.split(File.pathSeparator)?.map(::File) System.getProperty(KOTLIN_COMPILER_CLASSPATH_PROPERTY)?.split(File.pathSeparator)?.map(::File)
?: System.getProperty(KOTLIN_COMPILER_JAR_PROPERTY)?.let(::File)?.takeIf(File::exists)?.let { listOf(it) } ?: System.getProperty(KOTLIN_COMPILER_JAR_PROPERTY)?.let(::File)?.takeIf(File::exists)?.let { listOf(it) }
} }
val compilerClasspath: List<File> by lazy { val compilerClasspath: List<File> by lazy {
@@ -215,7 +305,7 @@ object KotlinJars {
val classpath = explicitCompilerClasspath val classpath = explicitCompilerClasspath
// search classpath from context classloader and `java.class.path` property // search classpath from context classloader and `java.class.path` property
?: (classpathFromFQN( Thread.currentThread().contextClassLoader, "org.jetbrains.kotlin.cli.jvm.K2JVMCompiler" ) ?: (classpathFromFQN(Thread.currentThread().contextClassLoader, "org.jetbrains.kotlin.cli.jvm.K2JVMCompiler")
?: classpathFromClassloader(Thread.currentThread().contextClassLoader)?.takeIf { it.isNotEmpty() } ?: classpathFromClassloader(Thread.currentThread().contextClassLoader)?.takeIf { it.isNotEmpty() }
?: classpathFromClasspathProperty() ?: classpathFromClasspathProperty()
)?.filter { f -> kotlinBaseJars.any { f.matchMaybeVersionedFile(it) } }?.takeIf { it.isNotEmpty() } )?.filter { f -> kotlinBaseJars.any { f.matchMaybeVersionedFile(it) } }?.takeIf { it.isNotEmpty() }
@@ -254,16 +344,16 @@ object KotlinJars {
val stdlibOrNull: File? by lazy { val stdlibOrNull: File? by lazy {
System.getProperty(KOTLIN_STDLIB_JAR_PROPERTY)?.let(::File)?.takeIf(File::exists) System.getProperty(KOTLIN_STDLIB_JAR_PROPERTY)?.let(::File)?.takeIf(File::exists)
?: getLib( ?: getLib(
KOTLIN_RUNTIME_JAR_PROPERTY, KOTLIN_RUNTIME_JAR_PROPERTY,
KOTLIN_JAVA_STDLIB_JAR, KOTLIN_JAVA_STDLIB_JAR,
JvmStatic::class JvmStatic::class
) )
} }
val stdlib: File by lazy { val stdlib: File by lazy {
stdlibOrNull stdlibOrNull
?: throw Exception("Unable to find kotlin stdlib, please specify it explicitly via \"$KOTLIN_STDLIB_JAR_PROPERTY\" property") ?: throw Exception("Unable to find kotlin stdlib, please specify it explicitly via \"$KOTLIN_STDLIB_JAR_PROPERTY\" property")
} }
val reflectOrNull: File? by lazy { val reflectOrNull: File? by lazy {
@@ -284,11 +374,12 @@ object KotlinJars {
val scriptRuntime: File by lazy { val scriptRuntime: File by lazy {
scriptRuntimeOrNull scriptRuntimeOrNull
?: throw Exception("Unable to find kotlin script runtime, please specify it explicitly via \"$KOTLIN_SCRIPT_RUNTIME_JAR_PROPERTY\" property") ?: throw Exception("Unable to find kotlin script runtime, please specify it explicitly via \"$KOTLIN_SCRIPT_RUNTIME_JAR_PROPERTY\" property")
} }
val kotlinScriptStandardJars get() = listOf( val kotlinScriptStandardJars
stdlibOrNull, get() = listOf(
scriptRuntimeOrNull stdlibOrNull,
).filterNotNull() scriptRuntimeOrNull
).filterNotNull()
} }