9
0
mirror of https://github.com/VolmitSoftware/Iris.git synced 2025-12-19 15:09:18 +00:00

add shared class loader support for script dependencies

This commit is contained in:
Julian Krings
2025-09-05 12:21:28 +02:00
parent 3a13f5a7c1
commit 571dde608c
3 changed files with 112 additions and 22 deletions

View File

@@ -1,10 +1,9 @@
package com.volmit.iris.core.scripting.kotlin.base
import com.volmit.iris.core.scripting.kotlin.runner.configureMavenDepsOnAnnotations
import com.volmit.iris.core.scripting.kotlin.runner.configure
import kotlin.script.experimental.annotations.KotlinScript
import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.defaultImports
import kotlin.script.experimental.api.refineConfiguration
import kotlin.script.experimental.dependencies.DependsOn
import kotlin.script.experimental.dependencies.Repository
import kotlin.script.experimental.jvm.dependenciesFromClassContext
@@ -28,9 +27,7 @@ object SimpleScriptDefinition : ScriptCompilationConfiguration({
dependenciesFromClassContext(SimpleScript::class, wholeClasspath = true)
}
refineConfiguration {
onAnnotations(DependsOn::class, Repository::class, handler = ::configureMavenDepsOnAnnotations)
}
configure()
}) {
private fun readResolve(): Any = SimpleScriptDefinition
}

View File

@@ -5,9 +5,10 @@ import java.io.File
import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KClass
import kotlin.script.experimental.annotations.KotlinScript
import kotlin.script.experimental.api.*
import kotlin.script.experimental.dependencies.DependsOn
import kotlin.script.experimental.dependencies.Repository
import kotlin.script.experimental.api.KotlinType
import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.SourceCode
import kotlin.script.experimental.host.FileScriptSource
import kotlin.script.experimental.host.createCompilationConfigurationFromTemplate
import kotlin.script.experimental.host.toScriptSource
@@ -25,6 +26,7 @@ class ScriptRunner(
private val configs = ConcurrentHashMap<KClass<*>, ScriptCompilationConfiguration>()
private val hostConfig = host.baseHostConfiguration.withDefaultsFrom(defaultJvmScriptingHostConfiguration)
private val sharedClassLoader = SharedClassLoader()
private var resolver = createResolver(baseDir)
fun compile(type: KClass<*>, raw: String, name: String? = null) = compile(type, raw.toScriptSource(name))
@@ -50,7 +52,8 @@ class ScriptRunner(
) {
dependencyResolver(resolver)
packDirectory(baseDir)
sharedClassloader(sharedClassLoader)
if (SimpleScript::class.java.isAssignableFrom(type.java))
return@createCompilationConfigurationFromTemplate
@@ -60,8 +63,6 @@ class ScriptRunner(
dependenciesFromClassContext(KotlinScript::class, wholeClasspath = true)
}
refineConfiguration {
onAnnotations(DependsOn::class, Repository::class, handler = ::configureMavenDepsOnAnnotations)
}
configure()
}
}

View File

@@ -3,11 +3,18 @@ package com.volmit.iris.core.scripting.kotlin.runner
import com.volmit.iris.core.scripting.kotlin.runner.resolver.CompoundDependenciesResolver
import kotlinx.coroutines.runBlocking
import java.io.File
import java.net.URLClassLoader
import kotlin.script.experimental.api.*
import kotlin.script.experimental.dependencies.resolveFromScriptSourceAnnotations
import kotlin.script.experimental.jvm.updateClasspath
import kotlin.script.experimental.dependencies.DependsOn
import kotlin.script.experimental.dependencies.ExternalDependenciesResolver
import kotlin.script.experimental.dependencies.Repository
import kotlin.script.experimental.dependencies.addRepository
import kotlin.script.experimental.dependencies.impl.SimpleExternalDependenciesResolverOptionsParser
import kotlin.script.experimental.jvm.JvmDependency
import kotlin.script.experimental.jvm.JvmDependencyFromClassLoader
import kotlin.script.experimental.jvm.util.classpathFromClassloader
import kotlin.script.experimental.util.PropertiesCollection
import kotlin.script.experimental.util.filterByAnnotationType
internal fun <T, R> ResultWithDiagnostics<T>.map(transformer: (T) -> R): ResultWithDiagnostics<R> = when (this) {
is ResultWithDiagnostics.Success -> ResultWithDiagnostics.Success(transformer(value), reports)
@@ -25,11 +32,14 @@ private val workDir = File(".").normalize()
internal fun createResolver(baseDir: File = workDir) = CompoundDependenciesResolver(baseDir)
private val resolver = createResolver()
internal fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics<ScriptCompilationConfiguration> {
private val loader = SharedClassLoader()
private fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics<ScriptCompilationConfiguration> = runCatching {
val annotations = context.collectedData?.get(ScriptCollectedData.collectedAnnotations)?.takeIf { it.isNotEmpty() }
?: return context.compilationConfiguration.asSuccess()
val reports = mutableListOf<ScriptDiagnostic>()
val loader = context.compilationConfiguration[ScriptCompilationConfiguration.sharedClassloader] ?: loader
val resolver = context.compilationConfiguration[ScriptCompilationConfiguration.dependencyResolver] ?: resolver
context.compilationConfiguration[ScriptCompilationConfiguration.packDirectory]
?.addPack(resolver)
@@ -55,22 +65,82 @@ internal fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinem
}
return runBlocking {
resolver.resolveFromScriptSourceAnnotations(annotations)
}.onSuccess {
resolver.resolveDependencies(annotations)
}.onSuccess { classpath ->
context.compilationConfiguration.with {
updateClasspath(it)
val newClasspath = classpath.filterNewClasspath(this[ScriptCompilationConfiguration.dependencies])
?: return@with
val shared = classpath.mapNotNull { p -> p.first.takeIf { p.second } }
if (shared.isNotEmpty()) loader.addFiles(shared)
val regular = newClasspath
.map { p -> p.first }
.let { JvmDependency(it) }
ScriptCompilationConfiguration.dependencies.append(regular)
}.asSuccess()
}.appendReports(reports)
}.getOrElse { ResultWithDiagnostics.Failure(it.asDiagnostics()) }
private fun Collection<Pair<File, Boolean>>.filterNewClasspath(known: Collection<ScriptDependency>?): List<Pair<File, Boolean>>? {
if (isEmpty()) return null
val knownClasspath = known?.flatMapTo(hashSetOf()) {
(it as? JvmDependency)?.classpath ?: emptyList()
}
return filterNot { knownClasspath?.contains(it.first) == true }.takeIf { it.isNotEmpty() }
}
private suspend fun ExternalDependenciesResolver.resolveDependencies(
annotations: Iterable<ScriptSourceAnnotation<*>>
): ResultWithDiagnostics<List<Pair<File, Boolean>>> {
val reports = mutableListOf<ScriptDiagnostic>()
annotations.forEach { (annotation, locationWithId) ->
when (annotation) {
is Repository -> {
val options = SimpleExternalDependenciesResolverOptionsParser(*annotation.options, locationWithId = locationWithId)
.valueOr { return it }
for (coordinates in annotation.repositoriesCoordinates) {
val added = addRepository(coordinates, options, locationWithId)
.also { reports.addAll(it.reports) }
.valueOr { return it }
if (!added)
return reports + makeFailureResult(
"Unrecognized repository coordinates: $coordinates",
locationWithId = locationWithId
)
}
}
is DependsOn -> {}
else -> return reports + makeFailureResult("Unknown annotation ${annotation.javaClass}", locationWithId = locationWithId)
}
}
return reports + annotations.filterByAnnotationType<DependsOn>()
.flatMapSuccess { (annotation, locationWithId) ->
SimpleExternalDependenciesResolverOptionsParser(
*annotation.options,
locationWithId = locationWithId
).onSuccess { options ->
annotation.artifactsCoordinates.asIterable().flatMapSuccess { artifactCoordinates ->
resolve(artifactCoordinates, options, locationWithId)
}.map { files -> files.map { it to (options.shared ?: false) } }
}
}
}
private val ExternalDependenciesResolver.Options.shared get() = flag("shared")
internal val ClassLoader.classpath get() = classpathFromClassloader(this) ?: emptyList()
fun <R> ResultWithDiagnostics<R>.valueOrThrow(message: CharSequence): R = valueOr {
internal fun <R> ResultWithDiagnostics<R>.valueOrThrow(message: CharSequence): R = valueOr {
throw RuntimeException(it.reports.joinToString("\n", "$message\n") { r -> r.render(withStackTrace = true) })
}
val ScriptCompilationConfigurationKeys.dependencyResolver by PropertiesCollection.key(resolver)
val ScriptCompilationConfigurationKeys.packDirectory by PropertiesCollection.key<File>()
internal val ScriptCompilationConfigurationKeys.dependencyResolver by PropertiesCollection.key(resolver, true)
internal val ScriptCompilationConfigurationKeys.packDirectory by PropertiesCollection.key(workDir, true)
internal val ScriptCompilationConfigurationKeys.sharedClassloader by PropertiesCollection.key(loader, true)
private fun File.addPack(resolver: CompoundDependenciesResolver) = resolver.addPack(this)
private fun <R> ResultWithDiagnostics<R>.appendReports(reports : Collection<ScriptDiagnostic>) =
@@ -78,4 +148,26 @@ private fun <R> ResultWithDiagnostics<R>.appendReports(reports : Collection<Scri
else when (this) {
is ResultWithDiagnostics.Success -> ResultWithDiagnostics.Success(value, this.reports + reports)
is ResultWithDiagnostics.Failure -> ResultWithDiagnostics.Failure(this.reports + reports)
}
}
internal class SharedClassLoader(parent: ClassLoader = SharedClassLoader::class.java.classLoader) : URLClassLoader(arrayOf(), parent) {
val dependency = JvmDependencyFromClassLoader { this }
fun addFiles(files: List<File>) {
files.forEach { addURL(it.toURI().toURL()) }
}
}
internal fun ScriptCompilationConfiguration.Builder.configure() {
refineConfiguration {
beforeParsing { context -> try {
context.compilationConfiguration.with {
ScriptCompilationConfiguration.dependencies.append((this[ScriptCompilationConfiguration.sharedClassloader] ?: loader).dependency)
}.asSuccess()
} catch (e: Throwable) {
ResultWithDiagnostics.Failure(e.asDiagnostics())
}}
onAnnotations(DependsOn::class, Repository::class, handler = ::configureMavenDepsOnAnnotations)
}
}