Skip to content
This repository has been archived by the owner on Aug 19, 2020. It is now read-only.

Commit

Permalink
Use isolated ClassLoader for Kotlin jars
Browse files Browse the repository at this point in the history
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
bamboo committed Jul 19, 2016
1 parent ee30b3a commit a37751a
Show file tree
Hide file tree
Showing 13 changed files with 442 additions and 168 deletions.
5 changes: 5 additions & 0 deletions build.gradle
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions fixtures/.gitignore
@@ -0,0 +1 @@
/repository
13 changes: 13 additions & 0 deletions fixtures/build.gradle.kts
@@ -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 })
21 changes: 21 additions & 0 deletions fixtures/plugin-compiled-against-kotlin-1.0/build.gradle
@@ -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" })
}
}
1 change: 1 addition & 0 deletions fixtures/settings.gradle
@@ -0,0 +1 @@
rootProject.buildFileName = 'build.gradle.kts'
2 changes: 1 addition & 1 deletion samples/hello-kotlin/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 {
Expand Down
@@ -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
}
}

0 comments on commit a37751a

Please sign in to comment.