上一篇文章剖析过groovy script的加载
这篇文章首要基于gradle 8.0源码解说
kts script脚本的加载及运转
kts脚本加载流程
和groovy脚本相同,kts脚本也分为2个阶段,kts这叫stage
- stage 1
履行buildscript和plugins部分,履行结果会对stage2 program的classpath有影响
- stage 2
eval脚本剩下的部分
2个阶段都会生成Program子类,目录在$HOME/.gradle/caches/gradle-version/kotlin-dsl/scripts/hashcode/classes
下
和groovy语言自身供给了丰厚的动态才能不同,gradle对kts的解析有大部分作业是自己完结的,涉及到编程原理部分,比如从lex阶段开端对kts内的token做解析等
全体流程如下图所示
Lexer
Lexer
接纳脚本文件内容及脚本对应的TopLevelBlockIds(buildscript/initscript/pluginManagement/plugins)这些,回来经过lex处理过的Packaged<LexedScript>
,里边包括注释及提取的第一轮需求履行的顶层block
fun lex(script: String, vararg topLevelBlockIds: TopLevelBlockId): Packaged<LexedScript>
class Packaged<T>(
val packageName: String?,
val document: T
)
class LexedScript(
val comments: List<IntRange>,
val topLevelBlocks: List<TopLevelBlock>
)
其是经过org.jetbrains.kotlin.lexer.KotlinLexer
的才能进行解析的,
start()
advance()
tokenType
tokenText
tokenStart
tokenEnd
start
接纳文本内容,开端进行词法剖析,编译的第一步便是进行词法剖析,程序的内容会被全部解析为token
advance
跳至下一个token部分
tokenType
token的类型,
例如空格为KtTokens.WHITE_SPACE
注释为KtTokens.COMMENTS
(这是个调集,包括了EOL_COMMENT-行尾注释,BLOCK_COMMENT-块注释等)
LBRACE
-> {
左花括号
RBRACE
-> }
右花括号
IDENTIFIER
,这个包括的范围特别广,以下面的statement为例
val a = buildscript()
val等关键字,变量如a、b,=等号,办法名buildscript等都属于此
tokenText
token的内容,剖析IDENTIFIER
特别需求,还是上面的statement为例,val的tokenType是IDENTIFIER,它的tokenText为val
tokenStart
,tokenEnd
token的开端,完毕方位
gradle kts的Lexer
意图是为了将顶层的特定block解析出来,这些block比较特别,是需求优先履行的。解析的结果便是包括哪些特别的block以及它们的开端完毕方位
Lexer
靠不断的迭代tokenType
来解析脚步内容,遇到WHITE_SPACE或许注释类型的token就跳过,它以state记载当时所在状况,state有3种类型
SearchingTopLevelBlock
SearchingBlockStart
SearchingBlockEnd
默许处于搜索SearchingTopLevelBlock
状况,在这个状况下假如碰到了PACKAGE,会把包名解析出来并保存,假如碰到IDENTIFIER,则判别是否有契合的toplevelblock,且当时depth为0,当时depth是对花括号的记载,碰到花括号depth会+1,出花括号-1,由于这儿是要收集顶层的block,所以进入花括号属于内层的需求疏忽
当SearchingTopLevelBlock
状况下检测到契合条件的block时会进入SearchingBlockStart
,很容易看出它和SearchingBlockEnd
是一对,意图便是为了将block的闭包开端完毕方位记载下来,start便是在找 { 的方位,找到后就进入,
start状况下只需不是IDENTIFIER或许LBRACE的状况,都会重置回SearchingTopLevelBlock
默许状况从头开端查找
SearchingBlockEnd
状况,由于内部还或许有闭包,所以end里仍需求进入{对depth+1,退出}时-1,当depth为0时将start和end点记载下来
还有个细节,是在start状况遇到IDENTIFIER时,还会检测block是否是契合条件的,假如不契合是会重置状况的,由于或许呈现下面这种状况,buildscript
能够被引用,当解析到第一个buildscript
时会进入start状况,假如start不对IDENTIFIER查看,到第二个buildscript
时就会重置状况,而错失buildscript的解析了
val a = buildscript
buildscript {}
ProgramParser
ProgramParser
是把lex剖析后的Packaged<LexedScript>
解析成Packaged<Program>
- 查看特定的顶层block每个只出一次(buildscript, plugins等)
- 查看特定的顶层block的顺序,pluginManagement一定要在第一个,plugins和buildscript优先级相同不做要求
- 将源码中的注释擦除,留意擦出不是删去,而是替换换行之外的字符为WHITESPACE,避免开端完毕方位错乱
- 将特定的顶层block解析出来,若block内容不为空的话转为对应的
Program
子类,里边会记载其block的开端完毕方位,将它们统一调集到stage1
中 - 再将特定block的代码擦除,假如有剩下代码的话,则将其包在
Program.Script
内作为stage2
- 若stage1、2都存在,将其包在
Program.Staged
回来,若只存在一个则只回来单个,若都没有则回来Program.Empty
// 只要stage1的block,且block内没有内容,解析后为Empty
buildscript { }
plugins {
// comments
}
// stage1的block是空的,解析后为Program.Script
buildscript { }
println "stage2"
Program
Empty
Script
Staged(Stage1, Script)
Stage1
Buildscript
PluginManagement
Plugins
Stage1Sequence(Buildscript, PluginManagement, Plugins)
PartialEvaluator
PartialEvaluator的意图是为了将stage1的部分先reduce,作为stage2的prelude,stage2部分在运转时再编译运转
它将parser解析后的Program
转为ResidualProgram
,residual有剩下,残留的意思
ResidualProgram
Static(instructions:List<Instruction>
)
Dynamic(prelude: Static, source: ProgramSource)
ResidualProgram有2个子类,Static和Dynamic
Static只包括了instructions的调集
Dynamic是由作为prelude的Static和剩下部分的源码组成的
在parser后其实被划分为7种状况,empty,3种特定顶层block只呈现一个的场景,3种中有恣意2个或以上呈现的场景Stage1Sequence,没有特别顶层block呈现的场景Script,stage1和stage2并存的场景Staged
Empty
PluginManagement
Buildscript
Plugins
Stage1Sequence
Script
Staged
只要Staged的状况才会被evaluate为Dynamic,其中stage1部分作为static,剩下的源码部分为source
其他状况都是Static的
能够看出Static首要是服务于stage1中特别的顶层block的,比较重要的一些instruction有
SetupEmbeddedKotlin,ApplyPluginRequestsOf,Eval
值得留意的一点是,这儿对plugins的evaluate有优化,对其独自进行了lex,将插件id等提取出来
ResidualProgramCompiler
residual有剩下,残留的意思
PartialEvaluator
生成的ResidualProgram
实践上是一堆大略的指令,ResidualProgramCompiler
便是将这堆指令运用asm技能翻译成字节码
关于Static和Dynamic的处理方式差不多,首要差异在于Dynamic的分为2个阶段,第一个阶段也是static的,生成的第二个阶段代码实践仅仅将源码先保存在字节码中,等实践履行的时分再去调用Interpreter走整个流程
Static编译为ExecutableProgram
的子类
Dynamic编译为ExecutableProgram.StagedProgram
的子类
下面为删减的代码,编译后的Program需求完结execute办法,这儿只会有部分特定顶层block的履行代码,脚本内其余部分代码是经过kotlin compiler供给的才能编译的。
假如kts脚本被PartialEvaluator
reduce 为了Static,那它会被编译为ExecutableProgram
的子类,若此时文件内只要plugins时,它在这个execute中就能完结一切作业了,不会经过kotlin compiler生成其他代码,假如不是,那会将其他代码编译为相似Build_gradle
的文件,并在execute中对其进行初始化
abstract class ExecutableProgram {
abstract fun execute(host: ExecutableProgram.Host, scriptHost: KotlinScriptHost<?>)
}
abstract class StagedProgram : ExecutableProgram() {
abstract val secondStageScriptText: String
abstract fun loadSecondStageFor(...): CompiledScript
fun loadScriptResource(resourcePath: String): String
当kts脚本被reduce为Dynamic时,这种状况或许更加常见,也更复杂,以下面这个较为简单,除plugins、buildscript外仅有repositories block的脚本为例
plugins {
kotlin("jvm") version "1.8.10"
}
buildscript {
print("test")
}
repositories {
mavenCentral()
}
将会生成下面的代码(伪代码)
stage1部分
Program.kt
class Program: ExecutableProgram.StagedProgram() {
fun execute(host: ExecutableProgram.Host, scriptHost: KotlinScriptHost<Project>) {
host.setupEmbeddedKotlinFor(scriptHost)
val requestCollector = PluginRequestCollector(scriptHost.getScriptSource())
Build_gradle(scriptHost, requestCollector.createSpec(1), scriptHost.getTarget() as Project)
host.applyPluginsTo(scriptHost, requestCollector.getPluginRequests())
host.applyBasePluginsTo(scriptHost.getTarget() as Project)
host.evaluateSecondStageOf(this, scriptHost, "Project/TopLevel/stage2", sourceHash, host.accessorsClassPathFor(scriptHost))
}
// secondStageScriptText和loadScriptResource都是为了加载stage2的脚本文件内容,由于常量池巨细64k的约束,假如超出这个巨细才会用loadScriptResource,不然运用字面量
fun getSecondStageScriptText(): String {
return "repositories { mavenCentral() }"
}
fun loadSecondStageFor(host: ExecutableProgram.Host, scriptHost: KotlinScriptHost<Project>, scriptTemplateId: String, sourceHash: HashCode, accessorsClassPath: ClassPath): CompiledScript {
return host.compileSecondStageOf(this, project, scriptTemplateId, sourceHash, ProgramKind.TopLevel, ProgramTarget.Project, accessorsClassPath);
}
}
Build_gradle.kt
class Build_gradle(
val host: KotlinScriptHost ,
val pluginDependencies: PluginDependenciesSpec,
val project: Project
): CompiledKotlinBuildscriptAndPluginsBlock(host, pluginDependencies) {
init {
plugins {
pluginDependencies.kotlin("jvm").version("1.8.10")
}
buildscript {
print("test")
}
}
}
代码比较多,核心部分在于plugins、buildscript这2个顶层block编译在了Build_gradle中,execute中先履行了setupEmbeddedKotlinFor
,后面又初始化了Build_gradle,因Build_gradle初始化时就履行了顶层block,后续便是applyPluginsTo去运用plugins引进的插件,终究是对stage2部分的编译及运转
setupEmbeddedKotlinFor
是为了统一embededKotlin版别,都是用gradle自带的kotlin版别的,precompile脚本用的kotlin版别和build.gradle.kts保持一致
stage1部分的Build_gradle也不一定会有,这一步和Static生成的逻辑相同,仅仅多了加载stage2的loadSecondStageFor
等部分,下面列了stage2编译后的伪代码,但是实践上此时并没有产生stage2的编译,虽然也是由ResidualProgramCompile
来完结的,但stage2部分的编译产生stage1履行时,由Interpreter
来触发的
stage2
Program.kt
class Program: ExecutableProgram() {
fun execute(host: ExecutableProgram.Host, scriptHost: KotlinScriptHost<Project>) {
Build_gradle(host, scriptHost.getTarget() as Project)
}
}
Build_gradle.kt
class Build_gradle(
val host: KotlinScriptHost ,
val pluginDependencies: PluginDependenciesSpec,
val project: Project
): CompiledKotlinBuildscriptAndPluginsBlock(host, pluginDependencies) {
init {
project.repositories {
mavenCentral()
}
}
}
从伪代码能够看出stage2部分才履行了repositories
的mavenCentral()
办法
stage2的履行是stage1在其execute办法中触发的evaluateSecondStageOf
,而且它还调用了accessorsClassPathFor
去获取accessors
的classpath,accessors
在后面会进行详细论述
Program由ResidualProgramCompiler
运用字节码技能生成的,Build_gradle
是由kotlin script的KotlinToJVMBytecodeCompiler
生成的
KotlinCompiler
gradle kts实践是用kotlin-compiler-embeddable
去编译kts脚本的
Kotlin Script最重要的是Script Defination,这是一组用来界说并装备script类型的参数,首要有
baseClass 和groovy的scriptBaseClass相似,作为script的基类
defaultImports 和groovy自带了许多默许导包不同,kts需求自己增加导包
hostConfiguration 增加classpath等
Script Defination有几种界说办法,官方比如中以注解方式装备参数较为常见,gradle中也有注解部分,但更多还是经过代码手动设置的参数
KotlinScript
相似groovy script的org.gradle.api.Script
,承继联系如下
classDiagram
KotlinScript <|-- DefaultKotlinScript
DefaultKotlinScript <|-- CompiledKotlinBuildScript
DefaultKotlinScript <|-- CompiledKotlinInitScript
DefaultKotlinScript <|-- CompiledKotlinSettingsScript
DefaultKotlinScript <|-- PrecompiledInitScript
DefaultKotlinScript <|-- PrecompiledSettingsScript
DefaultKotlinScript <|-- PrecompiledProjectScript
CompiledKotlinBuildScript <|-- CompiledKotlinBuildscriptBlock
CompiledKotlinBuildScript <|-- CompiledKotlinBuildscriptAndPluginsBlock
CompiledKotlinInitScript <|-- CompiledKotlinInitscriptBlock
CompiledKotlinSettingsScript <|-- CompiledKotlinSettingsBuildscriptBlock
CompiledKotlinSettingsScript <|-- CompiledKotlinSettingsPluginManagementBlock
除此以外还需求装备outputDirectory(生成的class的输出目录),jvm版别,脚本源码途径等等。装备完全体的compiler环境后,调用compileBunchOfSources(environment)
即可对kts文件进行编译
能够参阅Get started with Kotlin custom scripting, kts script加载 来自界说kts脚本
Plugins
plugins block在stage1中会被生成为applyPlugins代码,之后和groovy脚本对plugin的加载相同
plugin首要能够分为两类, 一种是precompile script,这又能够细分为buildSrc和includeBuild引进的 一种是external plugin,这是经过设置plugin的repository后apply进来的,也能够细分为两种,gradle官方供给的例如java,kotlin等,另外是用户自界说的
precompile script
buildSrc和includeBuild中的src目录内的script都能够作为precompile script,详细参见官方文档 Developing Custom Gradle Plugins
这类脚本运用的语言不受约束,只需是JVM的就行,实践上这些脚本会被编译为Plugin的子类,gradle供给了kotlin-dsl
和groovy-gradle-plugin
的插件让咱们能够kts或groovy来编写此类脚本,用java也是能够的
kotlin-dsl
的完结为KotlinDslPlugin
,它会apply PrecompiledScriptPlugins
,这个plugin的效果便是Kotlin source-sets下的*.gradle.kts脚本文件输出为Gradle plugin产物,详细是经过DefaultPrecompiledScriptPluginsSupport
来完结的
DefaultPrecompiledScriptPluginsSupport
- ExtractPrecompiledScriptPluginPlugins
把precompiled script里边的plugins
block提取出来,独自输出到outputDir里 - GenerateExternalPluginSpecBuilders
从compile classpath中找到带有gradle plugins的jar包,判别依据是其有META-INF/gradle-plugins/*.properties
文件,为这些plugin生成accessor,这部分accessor是用在plugins
block内的,例如java,kotlin等PluginSpecBuilders
- CompilePrecompiledScriptPluginPlugins
用ExtractPrecompiledScriptPluginPlugins
提取的plugins block和GenerateExternalPluginSpecBuilders
生成的PluginSpecBuilders
来编译plugins block,这儿也是运用KotlinCompiler.compileKotlinScriptModuleTo
来处理的,到这一步的处理其实相似stage1的进程 - GeneratePrecompiledScriptPluginAccessors
有了前面compile往后的plugins block,就能够来生成type safe accessors了。Plugin供给能够用在脚本里边的有extension(如java,kotlin),task,convention(已经Deprecated了,和extension差不多),containerElement(named办法),configuration(如implementation,api),gradle是经过构建一个虚拟的build,然后对其project
apply这些plugin,之后就能够在project对象中获取到上述的extension等信息,提取的代码见DefaultProjectSchemaProvider.schemaFor
,收集到的信息被封装在ProjectSchema
里,再便是经过org.gradle.kotlin.dsl.accessors.Emitter
利用ASM字节码手法生成accessor了。这一步的操作是为了给正式编译kts脚本中运用到的extension等供给accessors源码
在经过上面一系列操作之后,gradle才会开端履行compileKotlin task,这儿和编译build.gradle.kts不同,并不是运用KotlinCompiler
来完结的,而是经过为freeCompilerArgs
特点增加-script-templates
,-Xscript-resolver-environment
这些kts脚本编译参数来完结的,和单纯编译kts脚本不同,src目录下除了kts脚本外还能够有正常的kotlin代码,需求混编。参阅Kotlin compiler options | Kotlin Documentation
至此precompiled script编译流程完结,buildSrc和includeBuild在履行的时机上有所不同,buildSrc在root/build.gradle.kts之前,includeBuild实践上是介于root/build.gradle.kts编译进程的stage1与stage2之间,大体上并不影响,由于precompiled script便是为了stage2的编译做准备
apply plugin引进的plugin不是在stage1处理过的,所以没有为其生成accessor类,在build脚本里边是无法运用它的extension的
includeBuild里边的precompiled script由于是在stage1,2之间生成,所以没有对应的PluginSpecBuilders
生成,所以只能用id(“”)办法去调用
plugins {
java
`my-build-src`// buidSrc下的precompiled script
id("my-include-build")// includBuild的precompiled script
}
Accessors
Accessor是什么
- gradle自身供给的一些才能,例如plugins、files、repositories、dependencies
- plugin引进的extension,如java,publishing
这些才能是如何能在脚本中被运用到的呢,特别extension是plugin自己界说的,gradle无法进行约束,不像groovy语言自身供给了动态才能在运转时去派发这些办法调用到详细的extension上去,kotlin是强类型静态语言,在编译时这些类就需求在classpath里边找得到,不然无法compile gradle会生成对应的accessor,来让脚本能够运用到这些才能
举个详细的比如,咱们能够在build.gradle.kts脚本里边运用其他插件供给的extension,例如在引进插件java后能够运用java
extension对其进行一些装备
// build.gradle.kts
plugins {
id("java")
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(11))
}
}
咱们也能够自界说plugin,并供给自己的extension,例如下面
val proguard = extensions.create<ProguardExtension>("proguard")
界说了proguard extension
,这行代码引进了2个东西,一个是extension自身,在这儿也便是ProguardExtension
,里边能够供给办法来进行装备,另一个是extension的name,这儿是proguard,也便是咱们引进了这个plugin后,在脚本中能够运用的名字,相似上面java插件中的java
extension。
CompilationClassPath
分为2类,一类是ScopeClassPath,一类是accessors classpath,compilationClassPath便是将2者加起来
- ScopeClassPath
ScopeClassPath是固有的一些classpath,不必编译plugin就自带,能够分为3种
用gradleLib表明$HOME/.gradle/wrapper/dists/gradle-version/hashcode/gradle-version/lib
gradleApi
.gradle/caches/version/generated-gradle-jars/gradle-api-version.jar
gradleLib/groovy相关jar包
gradleLib/kotlin规范库相关jar包
gradleLib/gradle-installation-beacon-version.jar
等等
gradleApiExtensions
.gradle/cache/version/generated-gradle-jars/gradle-kotlin-dsl-extensions.jar
给gradle api生成kotlin拓宽办法的源码,例如files、repositories等,细节见ApiExtensionsJarGenerator
gradleKotlinDslJars
gradleLib/kotlin规范库相关jar包
gradleLib/gradle-kotlin-dsl-version.jar(这个jar包是gradle供给的基础的api的kotlin拓宽办法,例如apply,dependencies,maven等。实践上这个jar包便是kotlin dsl的源码,上述的Interpreter、ResidualProgramCompiler等也在这个jar包里)
gradleLib/gradle-kotlin-dsl-tooling-models-version.jar
exportedClassPath
buildSrc jar包的classpath是在project对象prepare的进程中就给导入了的
includeBuild和plugins的classpath是在stage1代码在eval时,经过履行applyPlugin,调用DefaultPluginRequestApplicator.defineScriptHandlerClassScope
导入的
详细代码见KotlinScriptClassPathProvider
- accessors classpath
stage1
由InterpreterHost.pluginAccessorsFor
触发,终究会调用
GeneratePluginAccessors
,从project的buildSrcClassLoaderScope找到pluginDescriptorsClassPath,和precompiled script流程中的GenerateExternalPluginSpecBuilders
找plugin相同,之后对这些plugin生成accessor,以便在plugins block中能够被调用到,这儿是为buildSrc引进的plugin生成PluginAccessors
其次是在DefaultPluginRequestApplicator.applyPlugins
中触发includeBuild的构建,生成对应的accessors
stage2
由ProgramHost.accessorsClassPathFor
触发,终究调用GenerateProjectAccessors
,和precompiled script中的GeneratePrecompiledScriptPluginAccessorsGeneratePrecompiledScriptPluginAccessors
相似,由于plugin在stage1阶段已经被apply了,所以这儿能够从project对象获取到插件引进的task、extension等,为其生成accessors
生成的代码坐落$HOME/.gradle/caches/*version*/kotlin-dsl/accessors
缓存
对setting.gradle.kts
和build.gradle.kts
进行evaluate是Configuration阶段,被称为configuration cache
有2层缓存,内存缓存和文件缓存
文件缓存靠gradle自身task的履行流程机制保证,没有内存缓存时,gradle会创建一个相似task的履行流程,来加载kts脚本
内存缓存在StandardKotlinScriptEvaluator.classloadingCache
里,假如是daemon运转方式(默许方式),这个进程是常驻的所以能够作为内存缓存,假如之前没有daemon进程例如首次启动,或许手动封闭了daemon进程例如-Dorg.gradle.daemon的话,就相当于没有内存缓存
参阅链接
Get started with Kotlin custom scripting
KEEP/scripting-support.md at master Kotlin/KEEP GitHub
GitHub – Kotlin/kotlin-script-examples: Examples of Kotlin Scripts and usages of the Kotlin Scripting API
KotlinConf 2019: Implementing the Gradle Kotlin DSL by Rodrigo Oliveira – YouTube