Replaces Script Definition Template proposal.
Discussion of this proposal is held in this issue
- Define Kotlin scripting and its applications
- Describe intended use cases for the Kotlin scripting
- Define scripting support that is:
- applicable to all Kotlin platforms
- provides sufficient control of interpretation and execution of scripts
- simple enough to configure and customize
- provides usable default components and configurations for the typical use cases
- Provide basic examples of the scripting usage and implementation
- Address the issues found during the public usage of the current scripting support
The document is still a draft, and few important parts are still missing, in particular:
- REPL:
- API for REPL host configuration and embedding
- API for REPL plugins (maybe should be covered elsewhere):
- custom repl commands
- highlighting
- completion
- etc.
- IDE plugins API for custom scripting support (besides discovery)
- Build scripts (Gradle/Kobalt)
- Test scripts (Spek)
- Command-line utilities
- Routing scripts (ktor)
- Type-safe configuration files (TeamCity)
- In-process scripting and REPL for IDE
- Consoles like IPython/Jupyter Notebook
- Game scripting engines
- ...
- Script - a text file written in Kotlin language but allowing top-level statements and expressions and having access to some implicit (not directly mentioned in the script text) properties, functions and objects, as if the whole script body is a body of an implicit function placed in some environment (see below)
- Scripting Host - an application or a component which handles script execution
- Scripting Host Environment - a set of parameters that defines an environment for all scripting host services, contains when relevant: project paths, jdk path, etc. It is passed on constructing the services
- REPL snippet - a group of script text lines, executed in a single REPL eval call
- Script compilation configuration - a set of parameters configuring script compilation, such as dependencies, external variables declarations, implicit import statements, etc.
- Script evaluation configuration - a set of parameters configuring script evaluation, such as external variables instances, actual script parameters, etc.
- Script definition - a set of parameters and configurations defining a script type
- Compiled script - a binary compiled code of the script, stored in memory or on disk, which could be loaded and instantiated by appropriate platform
- Dependency - an external library or another project whose declarations are available for the script being compiled and evaluated
- Imported script - another script whose declarations are available for the script being compiled and evaluated
- Configuration refinement - the process of updating script compilation and evaluation configurations during compilation/evaluation with parameter specific to the particular script exemplar, e.g. depending on the script contents
The use case when a scripting host is embedded into user's application, e.g. specialized console like IPython/Jupyter notebook, Spark shell, embedded game scripting, IDE and other application-level scripting.
In this case the script is most likely need to run in a specific execution environment, defined by the scripting host. The default script compilation and evaluation configurations are defined by the scripting host as well. The host may provide script authors with a possibility to customize some configuration parameters, e.g. to add dependencies or specify additional compilation options, e.g. using annotations in the script text:
@file:DependsOn("maven:artifact:1.0", "imported.package.*")
@file:Import("path/to/externalScript.kts")
@file:CompilerOptions("-someCompilerOpt")
To implement this the compilation/evaluation pipeline should provide a callback mechanism to call a host-provided code that could analyze script text directly or parsed annotations in order to update compilation and evaluation configurations with parameters depending on the script contents or other dynamic factors.
If the host need to support several script types, i.e. sets of compilation configurations and customization means, there should be a way for the host to distinguish the scripts and select appropriate set of compilation/configuration/evaluation services and properties. The host authors can implement the selection based on any script property, but due to the difficulties in supporting file type distinction based on anything but filename extension across platforms and IDEs, the default implementations should support only extension-based or file name based selection.
Note: It would be nice to provide an infrastructure (complete hosts or libraries) that support some typical mean for each platform to resolve external libraries from online repositories (e.g. - maven for JVM) out of the box.
In a simple case, the developer wants to implement a scripting host to control script execution and provide the required environment. One may want to write something as simple as:
KotlinScriptingHost().eval(File("path/to/script.kts"))
or
KotlinScriptingHost().eval("println(\"Hello from script!\")")
and the script should be executed in the current environment with some reasonable default compilation and evaluation settings. If things need to be configured explicitly, the code would look like:
val scriptingHost = KotlinScriptingHost(configurationParams...)
scriptingHost.eval(File("path/to/script.kts"), compilationConfiguration, evaluationConfiguration)
This would also allows the developer to control the lifetime of the scripting host.
In cases then there is more control needed for the compilation and evaluation process, the direct use of the script compiler and evaluator could be desirable:
val scriptCompiler = KotlinScriptCompiler(configurationParams...)
val compiledScript = scriptCompiler.invoke(File("path/to/script.kts), compilationConfiguration)
val scriptEvaluator = KotlinScriptEvaluator(configurationParams...)
val evaluationResult = scriptEvaluator.invoke(compiledScript, evaluationConfiguration)
In addition in this case the compiled script could be evaluated more than once.
To be able to run scripts in a user controlled environment, the following information bits could be configured or provided to the host:
- Script Compiler - the service that will compile scripts into a form accepted by the Evaluator
- Script Compilation Configuration - set of properties defining the script compilation settings, including:
- Script Base Class - an interface (or prototype) of the script class, expected by the executor, so the compiler should compile script into the appropriate class
- Dependencies - external libraries that could be used in the script
- Default imports - import statements implicitly added to any compiled script
- Provided properties - properties with types that is assumed visible in the script scope
- etc.
- Script Evaluator - the service that will actually evaluate the compiled scripts in the required execution environment
- Evaluation configuration - set of properties defining an environment for the evaluation, including:
- Provided properties - actual values of the provided properties from the compilation configuration
- etc.
Some of these parameters could be wrapped into a Script Definition for easier identification of the script types by the hosts that may support handling of the several script types simultaneously.
Since calling Kotlin compiler could be quite a heavy operation in comparison with typical script execution, the caching of the compiled script should be supported by the compilation platform.
The script is executed according to the following scheme:
- compilation - the Script Compiler takes the script, it's compilation configuration and provides a compiled class. Inside this process. If the compilation configuration defines refinement callbacks, they are called before or after parsing to update configuration with parameter depending on the script contents or other dynamic data.
- evaluation - the Script Evaluator takes the compiled script instantiates it if needed, and calls the appropriate method, passing arguments from the environment to it; this step could be repeated many times. If evaluation configuration defines refinement callbacks, they will be called before evaluation similarly to the compilation ones.
The IDE support for the scripts should be based on the same Script definition. Basically after recognizing the script type (see Environment and customization section above), the IDE extracts the Compilation Configuration and use its parameters to implement highlighting, navigation and other features. The default implementation of Kotlin IDEA plugin should support the appropriate functionality, based on the standard set of configuration parameters.
Standalone scripting applications include command-line utilities and a standalone Kotlin REPL.
Standalone scripting is a variant of the embedded scripting with hosts provided with the Kotlin distribution or 3rd-party hosts.
The standalone script could be executed e.g. using command line Kotlin compiler:
kotlinc -cp <classpath required by the script definition> -script myscript.kts
Or with the dedicated runner included into the distribution:
kotlin -cp <classpath required by the script definition> myscrtipt.kts
To be able to use the Kotlin scripts in a Unix shell environment, the shebang (#!
) syntax should be supported
at the beginning of the script:
#! /path/to/kotlin/script/runner -some -params
Note: due to lack of clear specification, passing parameters in the shebang line could be problematic, therefore alternative schemes of configuring scripts should be available.
It should be possible to process custom scripts with the standard hosts, e.g. by supplying a custom script definition in the command line, e.g.:
kotlin -script-templates="org.acme.MyScriptDef" -cp myScriptDefLib.jar myscript.myscr.kts
In this case the host loads specified definition class, and extract required definition from it and its annotations.
Another possible mechanism is automatic discovery, with the simplified usage:
kotlin -cp myScriptDefLib.jar myscript.myscr.kts
In this case the host analyses the classpath, discovers script definitions located there and then processes then as
before. Note that in this case it is should be recommended to use dedicated script extension (myscr.kts
) in every
definition to minimize chances of clashes if several script definitions will appear in the classpath. And on top of
that, some selection mechanism based on the file names and/or paths is needed, e.g. to cover cases like gradle's
build.gradle.kts
and setting.gradle.kts
that should be treated like different script types.
For the command line usage the support for script parameters is needed. The simplest form is to assume that the script
has access to the args: Array<String>
property/parameter. More advanced is to have a customization that supports a
declaration of the typed parameters in the script annotations e.g.:
@file:param("name", "String?") // note: stringified types are used for the cases not supported by class literals
@file:param("num", Int::class)
@file:param("list", "List<String>")
// this script could be called with args "-name=abc -num=42 -list=a,b,c"
// and then in the body we can access parsed typed arguments
println("${name ?: "<unknown>"} ${num/6}: ${list.map { it.toUpperCase() } }")
Since in this use case scripts are not part of any project, support for such script should be configured in the IDE explicitly, either via a plugin or via the IDE settings.
Standalone REPL is invoked by a dedicated host the same way as for standalone script but accepts user's input as repl
snippets. It means that the declarations made in the previous snippets are accessible in the subsequent ones.
In this mode, all new scripting features should be accessible as well, including customization.
Applications: project-level REPL, build scripts, source generation scripts, etc.
Project infrastructure scripts are executed by some dedicated scripting host usually embedded into the project build system. So it is a variant of the embedded scripting with the host and the IDE support integrated into build system and/or IDE itself.
From an IDE point of view, they are project-context dependent, but may not be part of the project sources. (In the same sense as e.g. gradle build scripts source is not considered as a part of the project sources.) In this case the support in the IDE is possible only if the definitions are supplied to the IDE explicitly, similarly to standalone scripts, either via a plugin or via the IDE settings.
The IDE needs to be able to extract scripts environment configurations from the project settings, if the scrips are considered a part of the project sources, so the project's classpath could be used for discovery of the supported scripts.
A REPL that has access to the project's compiled classes.
Applications: test definition scripts (Spek), routing scripts (ktor), type safe config files, etc.
In these cases the scripts are considered parts of the kotlin project and are compiled to appropriate binary form by the compiler, and then linked with the rest of the compilation results. They differ from the other project's sources by the possibility to employ script semantic and configurability and therefore avoid some boilerplate and make the sources look more DSL-like.
In this scenario, no dedicated scripting host is used, but the standard compiler is used during the regular compilation
according to configured script recognition logic (e.g. the script type discovery mechanism described above), and the
target application should implement its own logic for instantiating and calling the generated script classes.
Script compiler may also annotate generated classes and methods with user-specified annotations to integrate it with
existing execution logic. E.g. junit test scripts could be annotated accordingly.
From an IDE point of view, these scripts are the part of the project but should be configured according to the recognized script definition.
The scripting support consists of the following components:
ScriptCompiler
- interface for script compilation- compilation:
(scriptSource, compilationConfiguration) -> compiledScript
- predefined script compilers based on the kotlin platforms: /JVM, /JS, /Native
- custom/customized implementation possible
- compiled scripts cashing belongs here
- should not keep the state of the script compilation, the required state for the subsequent compilations, e.g. in the REPL mode, is passed along with the compiled script
- compilation:
ScriptEvaluator
- the component that receives compiled script instantiates it, and then evaluates it in a required environment, supplying any arguments that the script requires:- evaluation:
(compiledScript, evaluationConfiguration) -> Any?
- the
compiledScript
contains the final compilation configuration used - the
evaluationConfiguration
the parameters describing the actual script evaluation configuration of the script - predefined platform-specific evaluators available, but could be provided by the scripting host
- pseudo-evaluators that save compiled script into e.g. an executable jar should be provided as well
- evaluation:
- IDE support - Kotlin IDEA plugin should have support for scripting with script definition selection based on the file name extension, and also includes discovery. The exposed generic ide support that would allow to build rich script editing apps and REPLs is outside of the scope of this proposal and will be covered elsewhere.
ScriptSource
determines the way to access script for other components; it consists of:- the script reference pointer: url
- an accessor to the script text
Both components are optional but at least one is required for a regular script.
The class implements source position and fragment referencing classes used e.g. in error reporting.
KotlinType
a wrapper around Kotlin types, used to decouple script definition and compilation/evaluation environments. It could be constructed either from reflected or from stringified type representation.ScriptCompilationConfiguration
- a heterogeneous container of parameters defining the script compilationScriptEvaluationConfiguration
- a heterogeneous container of parameters defining the script evaluationScriptingHostConfiguration
- a heterogeneous container of host-specific parameters
Script Definition is a way to specify custom script. It is basically consists of a script base class annotated with
KotlinScript
annotation. The arguments of the annotation define the script configuration parameters. For example:
@KotlinScript(
displayName = "My Script",
fileExtension = "myscr.kts",
compilationConfiguration = MyScriptCompilationConfiguration::class,
evaluationConfiguration = MyScriptEvaluationConfiguration::class
)
abstract class MyScript(project: Project, val name: String) {
fun helper1() { ... }
[@ScriptBody]
[suspend] abstract fun <scriptBody>(params...): MyReturnType
}
object MyScriptCompilationConfiguration : ScriptCompilationConfiguration({
defaultImports("java.io.*")
providedProperties(prop1 to Int::class, prop2 to String::class)
})
object MyScriptEvaluationConfiguration : ScriptEvaluationConfiguration({
providedProperties(prop1 to 42, prop2 to "foo")
})
Where:
- any valid method name could be used in place of
<scriptBody>
@ScriptBody
annotation marks the method the script body will be compiled into. In the absence of the explicit annotation, if the configuration requires compilation into a method, the SAM notation will be used, otherwise the script will be generated into the target class constructor.interface
oropen class
could be used in place of theabstract class
- (see also possible compilation configuration properties below)
The annotations have reasonable defaults, so in the minimal case it is enough to mark the class only with the
@KoltinScript
without parameters. But it is recommended to give a dedicated file name extension for every script
type to minimize chances for clashes in case of multiple definitions in one context.
The file name extension could be declared in the compilation configuration as well, but it is highly recommended
to define it in the annotation instead, since it will speed up the discovery process significantly.
Script compiler implements the following interface:
interface ScriptCompiler {
/**
* Compiles the [script] according to the [scriptCompilationConfiguration]
* @param script the interface to the script source code
* @param scriptCompilationConfiguration the script compilation configuration properties
* @return result wrapper, if successful - with compiled script
*/
suspend operator fun invoke(
script: SourceCode,
scriptCompilationConfiguration: ScriptCompilationConfiguration
): ResultWithDiagnostics<CompiledScript<*>>
}
where:
interface CompiledScript<out ScriptBase : Any> {
/**
* The compilation configuration used for script compilation
*/
val compilationConfiguration: ScriptCompilationConfiguration
/**
* The function that loads compiled script class
* @param scriptEvaluationConfiguration the script evaluation configuration properties
* @return result wrapper, if successful - with loaded KClass
*/
suspend fun getClass(scriptEvaluationConfiguration: ScriptEvaluationConfiguration?): ResultWithDiagnostics<KClass<*>>
/**
* The name and the type of the script's result field, if any
*/
val resultField: Pair<String, KotlinType>?
}
The compilers for the supported platforms are supplied by default scripting infrastructure.
The script compilation could be configured dynamically by specifying configuration refining callbacks in the compilation configuration. The callback should be written according to the following signature:
typealias RefineScriptCompilationConfigurationHandler =
(ScriptConfigurationRefinementContext) -> ResultWithDiagnostics<ScriptCompilationConfiguration>
where:
class ScriptConfigurationRefinementContext(
val script: SourceCode,
val compilationConfiguration: ScriptCompilationConfiguration,
val collectedData: ScriptCollectedData? = null
)
and ScriptCollectedData
is a heterogeneous container of properties with the appropriate data collected during parsing.
See the following section for the parameters that declare such callbacks.
The following properties are recognized by the compiler:
baseClass
- target script superclass as well as a source for the script target method signature, constructor parameters and annotations, default -Any
.sourceFragments
- script fragments compile - allows to compile script partiallyscriptBodyTarget
- defines whether script body will be compiled into resulting class constructor or to a method body. In the latter case, there should be either single abstract method defined in the script base class, or single appropriate method should be annotated with theScriptBody
annotationimplicitReceivers
- a list of script types that is assumed to be implicit receivers for the script body, as if the script is wrapped intowith
expressions, in the order from outer to inner scope, i.e.:with(receiver0) { ... with(receiverN) { <script body> } }
providedProperties
- a map (name -> type) of external variables visible for the scriptdefaultImports
- a list of import statements implicitly added to the scriptrestrictions
- a list of allow/deny rules containing qualified identifier wildcards, which are applied after resolving any identifier used in the script to determine whether a particular identifier should be accessible for the script. This allows creating hosts with script functionality restrictionsimportedScripts
- a list of scripts definitions from which should be available for the compiled scriptdependencies
- a list of external libraries or modules available for the scriptcopyAnnotationsFrom
- an external class those annotations should be copied to the generated target class; if not specified, the annotations are copied from the base class (except for known scripting annotations)compilerOptions
- a list of additional compiler options that should be passed to compiler on script compilationresultField
- the name of the generated script class field to assign the script results to, empty means disabled, default -$$result
refineConfigurationBeforeParsing
- a configuration refining callback that should be called before parsing is startedrefineConfigurationOnAnnotations
- a list of script file-level annotations and configuration refining callback, if the specified annotations are found in the parsed script, the callback is called to get an updated configuration.refineConfigurationOnSections
- a list of top-level "sections" - function calls with single lambda parameter, e.g.and the callback that should be called if specified sections are found in the parsed scriptplugins { ... }
Additional properties are possible for particular platforms and compiler implementations.
In case of compiling into constructor, the script compiled into the following class
@CopiedAnnotation0(...)
...
@CopiedAnnotationN(...)
class Script(
baseClassArguments...,
receiver0: ReceiverType0, ..., receiverN: ReceiverTypeN,
val providedProperty0: ProvidedPropertyType0, ..., val providedPropertyN: ProvidedPropertyTypeN
): ScriptBaseClass(baseClassArguments...) {
val val1: V1 by initOnce // for all vals/vars defined in the script body
fun fn1(...): R1 {} // for all funcs defined in the script body
class Cl1(...) {} // for all classes/objects defined in the script body
val $$result: ResultType
init {
with(receiver0) {
...
with(receiverN) {
<script body>
$$result = <last expression>
}
}
}
}
In case of compiling into a method, the internal script properties are not exposed from the class:
@CopiedAnnotation0(...)
...
@CopiedAnnotationN(...)
class Script(
baseClassArguments...,
receiver0: ReceiverType0, ..., receiverN: ReceiverTypeN,
val providedProperty0: ProvidedPropertyType0, ..., val providedPropertyN: ProvidedPropertyTypeN
): ScriptBaseClass(baseClassArguments...) {
[suspend] fun <scriptBody>(lambdaParams...): ReturnType {
with(receiver0) {
...
with(receiverN) {
<script body>
}
}
}}
The actual name of the <scriptBody>
method is defined by the script base class.
The implicitReceivers
may also contain compiled external scripts objects (from the importedScripts
property).
Note: The with (implicitReceivers)
wrapping is needed in all methods and initializers defined in the class.
The script evaluator is an optional service (since the scripting host may choose to instantiate and execute compiled scripts manually) for instantiating and running compiled scripts. The default evaluators for supported platforms are provided by the scripting infrastructure. It should implement the following interface:
interface ScriptEvaluator {
suspend operator fun invoke(
compiledScript: CompiledScript<*>,
scriptEvaluationConfiguration: ScriptEvaluationConfiguration?
): ResultWithDiagnostics<EvaluationResult>
}
The following properties could be recognized by the evaluator, when passed in the scriptEvaluationConfiguration
parameter:
implicitReceivers
- a list of actual implicit receivers objects, corresponding to theimplicitReceivers
parameter in the compilation configurationprovidedProperties
- a map (name -> value) of the actual external variables visible to the script, corresponding to theprovidedProperties
parameter in the compilation configurationconstructorArgs
- a list of constructor parameters corresponding to the script base class constructor, that should be passed to the script constructorrunArgs
- a list of arguments to the script body method, if thescriptBodyTarget
is set to the compiling script into the methodscriptsInstancesSharing
- boolean property defining the way of dealing with duplicates among imported scripts - if true - the imported scripts instances are kept in a container and shared among all use sites in the import hierarchy. (the helperenableScriptsInstancesSharing()
provides another way of turning it on)refineConfigurationBeforeEvaluate
- a configuration refining callback that should be called before evaluation is started
It is possible to define and pass additional properties to a user-defined evaluator.
These properties are defined by the scripting host and contain general parameters needed for all scripting services.
In particular, it is assumed that ScriptCompilationConfigurator
and ScriptEvaluater
implementations are instantiated
by passing the environment property bag to the appropriate constructors.
The following parameters are defined:
configurationDependencies
- a list of dependencies required for script base class and services (configurator and evaluator), but not necessarily for scripts themselves (see below)getScriptingClass
- an interface to an implementation-specific "class loader" for types specified in the configurationsgetEvaluationContext
- optional user-supplied function that can provide data to the evaluation configuration refinement functions
For standard JVM compiler, the command line parameter could be used to specify a script definition class name, and then regular compilation classpath will be used for dependencies.
Additionally, the automatic discovery of the templates marked by @KotlinScript
annotation could be used with
libraries that do not have plugins able to provide an extension. This could, for example, be used for test frameworks.
In this case, the compiler scans all jars in the classpath for the discovery files - files in the folder
META-INF/kotlin/script/templates/
those names correspond to fully qualified names of the annotated script base
classes. (Note: the files' contents are not used, only the name). When it reads the annotations attached to these
classes and lazily constructs actual script definitions from them.
(Note: the process is optimized in a way that if the file name extension could be extracted from the annotations alone, the actual definition is not loaded until we will actually start to compile a script with the appropriate extension. This practically eliminates the overhead of script definitions discovery for projects that are not using scripts.)
To implement scripting support on the JVM one should do the following
- Implement script definition library/module
- define custom script compilation configuration class, inherit it from
ScriptCompilationConfiguration
and pass configuration lambda to the constructor of the latter - define custom script evaluation configuration class, inherit it from
ScriptEvaluationConfiguration
and pass configuration lambda to the constructor of the latter - define script base class with the
@KotlinScript
and pass at least file name extension and the configuration classes to it - optionally add a discovery file named after FQN of the base class to the
META-INF/kotlin/script/templates/
in the target jar
- define custom script compilation configuration class, inherit it from
If discovery file is added to the resulting jar, and this jar is added to the compilation classapth, it becomes possible to use custom script with the default hosts (e.g. Kotlin command-line compiler) and Kotlin IDEA plugin out of the box.
(Note for usage with Gradle: the discovery process is not turned on by default in the Kotlin gradle plugin, to enable
script definitions discovery in particular dependencies, they should be added to the kotlinScriptDef
(or
testKotlinScriptDef
, etc.) configuration)
Optionally the custom host and Idea support could be implemented:
-
Implement the scripting host with the following functionality:
- collect host configuration properties required for the services
- instantiate compiler and evaluator
- create basic host e.g. using platform-specific basic implementation
- in case of custom implementation, on each eval:
- call compiler
- call evaluator
-
Implement IDE support via the appropriate extension
- implement a class with
ScriptDefinitionsProvider
that returns appropriate definition class name and classpath needed to load it. - use
org.jetbrains.kotlin.scriptDefinitionsProvider
extension point to register the provider class in IntelliJ - in the script compilation configuration created as described above, add
ide
section with the following properties:acceptedLocations
- the list of possible project locations where the script of this type will be recognized (see.ScriptAcceptedLocation
enum class for the possible values)dependenciesSources
script source dependencies pointing to the jars with source code
- implement a class with
The experimental implementation of the described scripting infrastructure on the JVM platform is a part of Kotlin starting from the 1.3.0 release. The following API and helper libraries are generally needed for the implementation:
kotlin-scripting-common
- API, interfaces, data structures and propertieskotlin-scripting-jvm
- JVM-specific properties and default implementationskotlin-scripting-jvm-host
- JVM-specific host helpers
The basic examples could be found in the libraries/samples/scripting
folder in the Kotlin source code repository.
The implementation is functional for many use cases, but there are many gaps, in particular:
- the following compilation configuration properties are not supported yet:
sourceFragments
- scripts are only compiled as a whole for a moment; accordingly - the next point:refineConfigurationOnSections
- is not implementedscriptBodyTarget
- the generation into method body is not yet supported, so this parameter is ignoredrestrictions
- not implemented yetcopyAnnotationsFrom
- is not implemented yet, the annotations are copied from the base class
- some properties are required by the current implementation are not part of this proposal, because they exist only
due to some implementation issues, and could soon disappear or change significantly, for example:
getScriptingClass
in the environment - is used for kotlin types instantiation; currently the instance ofJvmGetScriptingClass
should be always used
The standard host functionality including discovery is implemented in the command-line compiler, gradle plugin and IDEA plugin. But the following limitations are known:
- if the script definition module is in the same project as the usage part, the module (and in particular - base class) should be compiled to class files in order to be recognized by the discovery mechanism
- the script compilation in maven project is not supported yet
- the mechanisms for managing script definitions and resolving clashes are not implemented
- additional configuration is needed to enable script definitions discovery in a gradle build (see note in the How to implement scripting support section)
In general the scripting support is in the experimental stage, so we cannot guarantee stability of the interfaces and implementations.
The kotlin-main-kts
artifact contains a script definition and a minimal maven artifacts resolver in a single jar.
It allows to:
- create and use the simple utility scripts that may depend on the external libraries;
- extract the common functionality into “imported” scripts.
It could be used as an example as an advanced script definition or together with standard or custom hosts - as an
actual scripting tool. It implements JSR-223 host and therefore could be used via the javax.script
API.
To extend scripts with functionality from 3rd-party libraries and to organize scripts structure, the kotlin-main-kts
implements support of the following file-level annotations:
@DependsOn
allows specifying library dependencies for the script. Either a direct path to the file or jar, or maven coordinates are accepted as arguments.@Repository
adds the maven repository for resolving dependencies. If none are specified explicitly, the maven central repository is used.@Import
allows importing another script into a given one. All the actions specified in the imported script run before all the actions specified in the given script and all the properties, functions and classes defined in the imported scripts become visible for the given script.
The artifact is distributed along with kotlin command line compiler package and published in Maven
Central as org.jetbrains.kotlin:kotlin-main-kts
.
common.main.kts:
val greeting = "Hello, World!"
sample.main.kts:
@file:Repository("https://jcenter.bintray.com")
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-html-jvm:0.6.11")
@file:Import("common.main.kts")
import kotlinx.html.*
import kotlinx.html.stream.*
print(createHTML().html {
body {
h1 { +greeting }
}
})
kotlinc -cp <path/to/kotlin-main-kts.jar> -script sample.main.kts
fun evalFile(scriptFile: File): ResultWithDiagnostics<EvaluationResult> {
val compilationConfiguration = createJvmCompilationConfigurationFromTemplate<MainKtsScript>()
val evaluationConfiguration = createJvmEvaluationConfigurationFromTemplate<MainKtsScript>()
return BasicJvmScriptingHost().eval(scriptFile.toScriptSource(), compilationConfiguration, evaluationConfiguration)
}
evalFile(File("sample.main.kts")
val engine = ScriptEngineManager().getEngineByExtension("main.kts")!!
engine.eval("""
@file:DependsOn("junit:junit:4.11")
org.junit.Assert.assertTrue(true)
println("Hello, World!")
""")
The complete source code could be found in the libraries/tools/kotlin-main-kts
folder in the Kotlin source
code repository. Here only partial sources are given to illustrate the main implementation points.
@KotlinScript(
fileExtension = "main.kts",
compilationConfiguration = MainKtsScriptDefinition::class,
evaluationConfiguration = MainKtsEvaluationConfiguration::class
)
abstract class MainKtsScript(val args: Array<String>)
object MainKtsScriptDefinition : ScriptCompilationConfiguration(
{
defaultImports(DependsOn::class, Repository::class, Import::class)
jvm {
dependenciesFromClassContext( // extract dependencies from the host environment
MainKtsScriptDefinition::class, // use this class classloader for dependencies search
"kotlin-main-kts", "kotlin-stdlib", "kotlin-reflect" // search these libraries in it and use then as a script compilation classpath
)
}
refineConfiguration {
onAnnotations(
DependsOn::class, Repository::class, Import::class, // if these annotations are found on script parsing
handler = MainKtsConfigurator() // call this handler to refine configuration parameters
)
}
ide {
acceptedLocations(ScriptAcceptedLocation.Everywhere) // these scripts are recognized everywhere in the project structure
}
jsr223 {
importAllBindings(true) // when used as a JSR-223 host, import engine bindings as kotlin properties
}
})
object MainKtsEvaluationConfiguration : ScriptEvaluationConfiguration(
{
scriptsInstancesSharing(true) // if a script is imported multiple times in the import hierarchy, use a single copy
refineConfigurationBeforeEvaluate( // before evaluation, call this handler to refine configuration properties
::configureProvidedPropertiesFromJsr223Context
)
}
)
class MainKtsConfigurator : RefineScriptCompilationConfigurationHandler {
private val resolver = FilesAndIvyResolver()
override operator fun invoke(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics<ScriptCompilationConfiguration> =
val annotations = context.collectedData?.get(ScriptCollectedData.foundAnnotations)?.takeIf { it.isNotEmpty() }
?: return context.compilationConfiguration.asSuccess()
// collect imported scripts paths from Import annotations
val scriptBaseDir = (context.script as? FileBasedScriptSource)?.file?.parentFile
val importedSources = annotations.flatMap {
(it as? Import)?.paths?.map { sourceName ->
FileScriptSource(scriptBaseDir?.resolve(sourceName) ?: File(sourceName))
} ?: emptyList()
}
// pass resolving annotations to resolver
val resolvedClassPath = try {
val scriptContents = object : ScriptContents {
override val annotations: Iterable<Annotation> = annotations.filter { it is DependsOn || it is Repository }
override val file: File? = null
override val text: CharSequence? = null
}
resolver.resolve(scriptContents, emptyMap(), {}, null).get()?.classpath?.toList()
// TODO: add diagnostics
} catch (e: Throwable) {
return ResultWithDiagnostics.Failure(*diagnostics.toTypedArray(), e.asDiagnostics(path = context.script.locationId))
}
// return updated configuration with resolved dependencies and imported scripts
return ScriptCompilationConfiguration(context.compilationConfiguration) {
if (resolvedClassPath != null) updateClasspath(resolvedClassPath)
if (importedSources.isNotEmpty()) importScripts.append(importedSources)
}.asSuccess()
}
}
META-INF/kotlin/script/templates/org.jetbrains.kotlin.mainKts.MainKtsScript.classname