This repository has been archived by the owner on Aug 19, 2020. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
13 changed files
with
442 additions
and
168 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/repository |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
// Mark the folder as a Gradle project |
25 changes: 25 additions & 0 deletions
25
fixtures/plugin-compiled-against-kotlin-1.0/src/main/kotlin/fixtures/ThePlugin.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" }) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
rootProject.buildFileName = 'build.gradle.kts' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
200 changes: 200 additions & 0 deletions
200
src/main/kotlin/org/gradle/script/lang/kotlin/provider/KotlinBuildScriptCompiler.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.