Permalink
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...
1 parent ee30b3a commit a37751a29a93beb52fd4833cf3bf1215603c435c @bamboo bamboo committed Jul 13, 2016
View
@@ -97,6 +97,11 @@ tasks.addRule('Pattern: check-<SAMPLE>') { taskName ->
}
}
+task prepareIntegrationTestFixtures(type: GradleBuild) {
+ dir = file('fixtures')
+}
+test.dependsOn prepareIntegrationTestFixtures
+
// --- classpath.properties --------------------------------------------
File generatedResourcesDir = file("$buildDir/generate-resources/main")
View
@@ -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'
@@ -1,6 +1,6 @@
buildscript {
- extra["kotlinVersion"] = "1.1.0-dev-998"
+ extra["kotlinVersion"] = "1.1.0-dev-1159"
extra["repo"] = "https://repo.gradle.org/gradle/repo"
repositories {
@@ -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

Please sign in to comment.