Permalink
Please sign in to comment.
Browse files
Use isolated ClassLoader for Kotlin jars
Due to the lack of isolation between gradle-script-kotlin and the compiled buildscript(s), referenced Kotlin types could leak into different ClassLoader scopes causing all sorts of loader constraint violations. That lack of isolation also meant that only the specific version of Kotlin shipped with gsk could ever be used. This commit mitigates these limitations by subverting the ClassLoader delegation model when Kotlin jars are detected in the buildscript classpath. In that case, all jars in the script classpath together with the Kotlin jars are segregated into a ClassLoader that will first try to load classes locally before delegating to its parent from the ClassLoader scope hierarchy. This solution is only a stepping stone and comes with its own set of limitations, buildscript block and script body cannot exchange Kotlin library values for one. A better solution will demand more isolation between gradle-script-kotlin and core. Resolves #84 Resolves #86 Resolves #25
- Loading branch information...
Showing
with
442 additions
and 168 deletions.
- +5 −0 build.gradle
- +1 −0 fixtures/.gitignore
- +13 −0 fixtures/build.gradle.kts
- +21 −0 fixtures/plugin-compiled-against-kotlin-1.0/build.gradle
- +1 −0 fixtures/plugin-compiled-against-kotlin-1.0/settings.gradle
- +25 −0 fixtures/plugin-compiled-against-kotlin-1.0/src/main/kotlin/fixtures/ThePlugin.kt
- +1 −0 fixtures/settings.gradle
- +1 −1 samples/hello-kotlin/build.gradle.kts
- +200 −0 src/main/kotlin/org/gradle/script/lang/kotlin/provider/KotlinBuildScriptCompiler.kt
- +10 −135 src/main/kotlin/org/gradle/script/lang/kotlin/provider/KotlinScriptPluginFactory.kt
- +38 −0 src/main/kotlin/org/gradle/script/lang/kotlin/provider/PostDelegatingClassLoader.kt
- +13 −5 src/main/kotlin/org/gradle/script/lang/kotlin/support/KotlinScriptDefinitionProvider.kt
- +113 −27 src/test/kotlin/org/gradle/script/lang/kotlin/integration/GradleScriptKotlinIntegrationTest.kt
| @@ -0,0 +1 @@ | ||
| +/repository |
| @@ -0,0 +1,13 @@ | ||
| +import org.gradle.api.tasks.GradleBuild | ||
| +import java.io.File | ||
| + | ||
| +fun isProjectDir(candidate: File) = | ||
| + candidate.isDirectory && File(candidate, "settings.gradle").exists() | ||
| + | ||
| +val subProjectTasks = rootDir.listFiles().filter { isProjectDir(it) }.map { subProjectDir -> | ||
| + task<GradleBuild>("prepare-${subProjectDir.name}") { | ||
| + setDir(subProjectDir) | ||
| + } | ||
| +} | ||
| + | ||
| +setDefaultTasks(subProjectTasks.map { it.name }) |
| @@ -0,0 +1,21 @@ | ||
| +plugins { | ||
| + id 'nebula.kotlin' version '1.0.3' | ||
| +} | ||
| + | ||
| +group 'org.gradle.script.lang.kotlin.fixtures' | ||
| + | ||
| +version '1.0' | ||
| + | ||
| +uploadArchives { | ||
| + repositories.ivy { url '../repository' } | ||
| +} | ||
| + | ||
| +dependencies { | ||
| + compile(gradleApi()) | ||
| +} | ||
| + | ||
| +repositories { | ||
| + jcenter() | ||
| +} | ||
| + | ||
| +defaultTasks 'uploadArchives' |
| @@ -0,0 +1 @@ | ||
| +// Mark the folder as a Gradle project |
| @@ -0,0 +1,25 @@ | ||
| +package fixtures | ||
| + | ||
| +import org.gradle.api.Plugin | ||
| +import org.gradle.api.Project | ||
| +import org.gradle.api.DefaultTask | ||
| +import org.gradle.api.tasks.TaskAction | ||
| + | ||
| +class ThePlugin() : Plugin<Project> { | ||
| + | ||
| + override fun apply(target: Project) { | ||
| + target.tasks.create("the-plugin-task", ThePluginTask::class.java) | ||
| + } | ||
| +} | ||
| + | ||
| +open class ThePluginTask : DefaultTask() { | ||
| + | ||
| + var from: String = "default from value" | ||
| + | ||
| + open fun configure(setup: (String) -> String) = setup(from) | ||
| + | ||
| + @TaskAction | ||
| + fun run() { | ||
| + println(configure { "it = $it" }) | ||
| + } | ||
| +} |
| @@ -0,0 +1 @@ | ||
| +rootProject.buildFileName = 'build.gradle.kts' |
| @@ -0,0 +1,200 @@ | ||
| +/* | ||
| + * Copyright 2016 the original author or authors. | ||
| + * | ||
| + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| + * you may not use this file except in compliance with the License. | ||
| + * You may obtain a copy of the License at | ||
| + * | ||
| + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| + * | ||
| + * Unless required by applicable law or agreed to in writing, software | ||
| + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| + * See the License for the specific language governing permissions and | ||
| + * limitations under the License. | ||
| + */ | ||
| + | ||
| +package org.gradle.script.lang.kotlin.provider | ||
| + | ||
| +import org.gradle.script.lang.kotlin.KotlinBuildScript | ||
| +import org.gradle.script.lang.kotlin.support.KotlinBuildScriptSection | ||
| +import org.gradle.script.lang.kotlin.support.isKotlinJar | ||
| +import org.gradle.script.lang.kotlin.support.kotlinScriptClassPath | ||
| + | ||
| +import org.gradle.api.Project | ||
| +import org.gradle.api.internal.initialization.ClassLoaderScope | ||
| +import org.gradle.api.internal.initialization.ScriptHandlerInternal | ||
| + | ||
| +import org.gradle.groovy.scripts.ScriptSource | ||
| + | ||
| +import org.gradle.internal.classpath.ClassPath | ||
| +import org.gradle.internal.classpath.DefaultClassPath | ||
| + | ||
| +import org.jetbrains.kotlin.script.KotlinScriptDefinitionFromTemplate | ||
| + | ||
| +import org.slf4j.Logger | ||
| + | ||
| +import java.io.File | ||
| + | ||
| +import java.lang.reflect.InvocationTargetException | ||
| + | ||
| +import java.net.URLClassLoader | ||
| + | ||
| +import kotlin.reflect.KClass | ||
| + | ||
| +class KotlinBuildScriptCompiler( | ||
| + val scriptSource: ScriptSource, | ||
| + val topLevelScript: Boolean, | ||
| + val scriptHandler: ScriptHandlerInternal, | ||
| + val targetScope: ClassLoaderScope, | ||
| + val baseScope: ClassLoaderScope, | ||
| + val gradleJars: List<File>, | ||
| + val logger: Logger) { | ||
| + | ||
| + val scriptResource = scriptSource.resource!! | ||
| + val scriptFile = scriptResource.file!! | ||
| + val script = scriptResource.text!! | ||
| + | ||
| + val gradleApi: ClassPath = DefaultClassPath.of(gradleJars) | ||
| + val buildSrc: ClassPath = exportClassPathOf(baseScope) | ||
| + val defaultClassPath: ClassPath = gradleApi + buildSrc | ||
| + | ||
| + val scriptClassPath: ClassPath by lazy { | ||
| + scriptHandler.scriptClassPath | ||
| + } | ||
| + | ||
| + val compilationClassPath: ClassPath by lazy { | ||
| + val classPath = scriptClassPath + defaultClassPath | ||
| + logger.info("Kotlin compilation classpath: {}", classPath) | ||
| + classPath | ||
| + } | ||
| + | ||
| + fun compile(): (Project) -> Unit { | ||
| + val buildscriptRange = extractBuildScriptFrom(script) | ||
| + return when { | ||
| + buildscriptRange != null -> | ||
| + twoPassScript(buildscriptRange) | ||
| + else -> | ||
| + onePassScript() | ||
| + } | ||
| + } | ||
| + | ||
| + private fun onePassScript(): (Project) -> Unit { | ||
| + val scriptClassLoader = scriptBodyClassLoaderFor(baseScope.exportClassLoader) | ||
| + val scriptClass = compileScriptFile(scriptClassLoader) | ||
| + return { target -> | ||
| + shareKotlinScriptClassPathOn(target) | ||
| + executeScriptWithContextClassLoader(scriptClassLoader, scriptClass, target) | ||
| + } | ||
| + } | ||
| + | ||
| + private fun twoPassScript(buildscriptRange: IntRange): (Project) -> Unit { | ||
| + val buildscriptClassLoader = buildscriptClassLoaderFrom(baseScope) | ||
| + val buildscriptClass = compileBuildscriptSection(buildscriptRange, buildscriptClassLoader) | ||
| + return { target -> | ||
| + executeScriptWithContextClassLoader(buildscriptClassLoader, buildscriptClass, target) | ||
| + | ||
| + val scriptClassLoader = scriptBodyClassLoaderFor(buildscriptClassLoader) | ||
| + val scriptClass = compileScriptFile(scriptClassLoader) | ||
| + shareKotlinScriptClassPathOn(target) | ||
| + executeScriptWithContextClassLoader(scriptClassLoader, scriptClass, target) | ||
| + } | ||
| + } | ||
| + | ||
| + private fun scriptBodyClassLoaderFor(parentClassLoader: ClassLoader): ClassLoader = | ||
| + if (scriptClassPath.hasKotlinJar() || buildSrc.hasKotlinJar()) | ||
| + isolatedKotlinClassLoaderFor(parentClassLoader) | ||
| + else | ||
| + defaultClassLoaderFor(targetScope.apply { export(scriptClassPath) }) | ||
| + | ||
| + private fun ClassPath.hasKotlinJar() = | ||
| + asFiles.any { isKotlinJar(it.name) } | ||
| + | ||
| + /** | ||
| + * Creates a ClassLoader that reloads gradle-script-kotlin.jar in the context of | ||
| + * the buildscript classpath so to share the correct version of the Kotlin | ||
| + * standard library types. | ||
| + */ | ||
| + private fun isolatedKotlinClassLoaderFor(buildscriptClassLoader: ClassLoader): PostDelegatingClassLoader { | ||
| + val isolatedClassPath = scriptClassPath + gradleScriptKotlinJars() + buildSrc | ||
| + val isolatedClassLoader = PostDelegatingClassLoader(buildscriptClassLoader, isolatedClassPath) | ||
| + exportTo(targetScope, isolatedClassLoader) | ||
| + return isolatedClassLoader | ||
| + } | ||
| + | ||
| + private fun exportTo(targetScope: ClassLoaderScope, scriptClassLoader: ClassLoader) { | ||
| + targetScope.apply { | ||
| + export(scriptClassLoader) | ||
| + lock() | ||
| + } | ||
| + } | ||
| + | ||
| + private fun gradleScriptKotlinJars() = | ||
| + gradleJars.filter { jar -> | ||
| + val name = jar.name | ||
| + name.startsWith("gradle-script-kotlin-") || isKotlinJar(name) | ||
| + } | ||
| + | ||
| + private fun buildscriptClassLoaderFrom(baseScope: ClassLoaderScope) = | ||
| + defaultClassLoaderFor(baseScope.createChild("buildscript")) | ||
| + | ||
| + private fun defaultClassLoaderFor(scope: ClassLoaderScope) = | ||
| + scope.run { | ||
| + export(KotlinBuildScript::class.java.classLoader) | ||
| + lock() | ||
| + localClassLoader | ||
| + } | ||
| + | ||
| + private fun compileBuildscriptSection(buildscriptRange: IntRange, classLoader: ClassLoader) = | ||
| + compileKotlinScript( | ||
| + tempBuildscriptFileFor(script.substring(buildscriptRange)), | ||
| + scriptDefinitionFromTemplate(KotlinBuildScriptSection::class, defaultClassPath), | ||
| + classLoader, logger) | ||
| + | ||
| + private fun compileScriptFile(classLoader: ClassLoader) = | ||
| + compileKotlinScript( | ||
| + scriptFile, | ||
| + scriptDefinitionFromTemplate(KotlinBuildScript::class, compilationClassPath), | ||
| + classLoader, logger) | ||
| + | ||
| + private fun scriptDefinitionFromTemplate(template: KClass<out Any>, classPath: ClassPath) = | ||
| + KotlinScriptDefinitionFromTemplate(template, mapOf("classPath" to classPath)) | ||
| + | ||
| + private fun executeScriptWithContextClassLoader(classLoader: ClassLoader, scriptClass: Class<*>, target: Any) { | ||
| + withContextClassLoader(classLoader) { | ||
| + executeScriptOf(scriptClass, target) | ||
| + } | ||
| + } | ||
| + | ||
| + private fun executeScriptOf(scriptClass: Class<*>, target: Any) { | ||
| + try { | ||
| + scriptClass.getConstructor(Project::class.java).newInstance(target) | ||
| + } catch(e: InvocationTargetException) { | ||
| + throw e.targetException | ||
| + } | ||
| + } | ||
| + | ||
| + private fun tempBuildscriptFileFor(buildscript: String) = | ||
| + createTempFile("buildscript-section", ".gradle.kts").apply { | ||
| + writeText(buildscript) | ||
| + } | ||
| + | ||
| + private fun exportClassPathOf(baseScope: ClassLoaderScope): ClassPath = | ||
| + DefaultClassPath.of( | ||
| + (baseScope.exportClassLoader as? URLClassLoader)?.urLs?.map { File(it.toURI()) }) | ||
| + | ||
| + private fun shareKotlinScriptClassPathOn(target: Project) { | ||
| + target.kotlinScriptClassPath = compilationClassPath | ||
| + } | ||
| +} | ||
| + | ||
| +inline fun withContextClassLoader(classLoader: ClassLoader, block: () -> Unit) { | ||
| + val currentThread = Thread.currentThread() | ||
| + val previous = currentThread.contextClassLoader | ||
| + try { | ||
| + currentThread.contextClassLoader = classLoader | ||
| + block() | ||
| + } finally { | ||
| + currentThread.contextClassLoader = previous | ||
| + } | ||
| +} |
Oops, something went wrong.
0 comments on commit
a37751a