Skip to content

Commit

Permalink
Load global plugins in their own class loader and replace the base lo…
Browse files Browse the repository at this point in the history
…ader with that. Fixes #272.

Also, replace the base classpath with the global classpath.
  • Loading branch information
harrah committed Feb 1, 2013
1 parent 52954b3 commit 8cb7e23
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 10 deletions.
28 changes: 21 additions & 7 deletions main/src/main/scala/sbt/Load.scala
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,25 @@ object Load
}
def buildGlobalSettings(base: File, files: Seq[File], config: LoadBuildConfiguration): ClassLoader => Seq[Setting[_]] =
{
val eval = mkEval(data(config.globalPluginClasspath), base, defaultEvalOptions)
val eval = mkEval(data(config.classpath), base, defaultEvalOptions)
val imports = baseImports ++ importAllRoot(config.globalPluginNames)
loader => EvaluateConfigurations(eval, files, imports)(loader).settings
}
def loadGlobal(state: State, base: File, global: File, config: LoadBuildConfiguration): LoadBuildConfiguration =
if(base != global && global.exists)
config.copy(globalPlugin = Some(GlobalPlugin.load(global, state, config)))
else
if(base != global && global.exists) {
val gp = GlobalPlugin.load(global, state, config)
val pm = setGlobalPluginLoader(gp, config.pluginManagement)
val cp = (gp.data.fullClasspath ++ config.classpath).distinct
config.copy(globalPlugin = Some(gp), pluginManagement = pm, classpath = cp)
} else
config

private[this] def setGlobalPluginLoader(gp: GlobalPlugin, pm: PluginManagement): PluginManagement =
{
val newLoader = ClasspathUtilities.toLoader(Build.data(gp.data.fullClasspath), pm.initialLoader)
pm.copy(initialLoader = newLoader)
}

def defaultDelegates: LoadedBuild => Scope => Seq[Scope] = (lb: LoadedBuild) => {
val rootProject = getRootProject(lb.units)
def resolveRef(project: Reference): ResolvedReference = Scope.resolveReference(lb.root, rootProject, project)
Expand Down Expand Up @@ -441,6 +451,7 @@ object Load
expand(auto)
}

@deprecated("No longer used.", "0.13.0")
def globalPluginClasspath(globalPlugin: Option[GlobalPlugin]): Seq[Attributed[File]] =
globalPlugin match
{
Expand Down Expand Up @@ -483,7 +494,7 @@ object Load
!(dir * -GlobFilter(DefaultTargetName)).get.isEmpty
}
def noPlugins(dir: File, config: LoadBuildConfiguration): LoadedPlugins =
loadPluginDefinition(dir, config, PluginData(config.globalPluginClasspath, None, None))
loadPluginDefinition(dir, config, PluginData(config.classpath, None, None))
def buildPlugins(dir: File, s: State, config: LoadBuildConfiguration): LoadedPlugins =
loadPluginDefinition(dir, config, buildPluginDefinition(dir, s, config))

Expand Down Expand Up @@ -661,8 +672,11 @@ final case class LoadBuildConfiguration(stagingDirectory: File, classpath: Seq[A
pluginManagement: PluginManagement, injectSettings: Load.InjectSettings, globalPlugin: Option[GlobalPlugin], extraBuilds: Seq[URI],
log: Logger)
{
lazy val (globalPluginClasspath, globalPluginLoader) = Load.pluginDefinitionLoader(this, Load.globalPluginClasspath(globalPlugin))
lazy val globalPluginNames = if(globalPluginClasspath.isEmpty) Nil else Load.getPluginNames(globalPluginClasspath, globalPluginLoader)
@deprecated("Use `classpath`.", "0.13.0")
lazy val globalPluginClasspath = classpath
@deprecated("Use `pluginManagement.initialLoader`.", "0.13.0")
lazy val globalPluginLoader = pluginManagement.initialLoader
lazy val globalPluginNames = if(classpath.isEmpty) Nil else Load.getPluginNames(classpath, pluginManagement.initialLoader)
}

final class IncompatiblePluginsException(msg: String, cause: Throwable) extends Exception(msg, cause)
4 changes: 4 additions & 0 deletions sbt/src/sbt-test/project/global-plugin-src/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# tests that a source file in $sbt.global.base/plugins/ is available to the build definition in project/

# dummy to ensure project gets loaded
> name
21 changes: 21 additions & 0 deletions sbt/src/sbt-test/project/global-plugin/changes/Build.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import sbt._
import sbt.Keys._

object MyBuild extends Build {
lazy val mySettings = Defaults.defaultSettings ++ Seq(
name := "my-test-proj",
organization := "com.example",
check <<= update map checkVersion,
version := "0.1.0-SNAPSHOT")

lazy val proj = Project("my-test-proj", file("."), settings = mySettings)

lazy val check = taskKey[Unit]("Verifies that the junit dependency has the older version (4.5)")

def checkVersion(report: UpdateReport) {
for(mod <- report.allModules) {
if(mod.name == "junit") assert(mod.revision == "4.5", s"JUnit version (${mod.revision}) was not overridden")
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// use a small java library instead of a plugin to avoid incompatibilities when upgrading
// use an old version to check that it will override a newer version in a build definition
libraryDependencies += "junit" % "junit" % "4.5"
2 changes: 2 additions & 0 deletions sbt/src/sbt-test/project/global-plugin/changes/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// the version should be overridden by the global plugin
libraryDependencies += "junit" % "junit" % "4.8"
19 changes: 16 additions & 3 deletions sbt/src/sbt-test/project/global-plugin/test
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
# tests that a source file in $sbt.global.base/plugins/ is available to the build definition in project/
$ copy-file changes/Build.scala project/Build.scala
> reload

# dummy to ensure project gets loaded
> name
# ensure that a new global dependency gets picked up
$ copy-file changes/global-plugins.sbt global/plugins/plugins.sbt
> reload

# check that the class can be loaded
> eval Class.forName("org.junit.Test")

# check that it is on the classpath
> eval (x => ()) : (org.junit.Test => Unit)

# ensure that the global plugin version overrides the local version
$ copy-file changes/plugins.sbt project/plugins.sbt
> reload
> check

2 comments on commit 8cb7e23

@dansanduleac
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this behaviour is not what we want. Regarding the scripted test, I think we always want to pick the newer version from (global plugin, project's build). That should be done automatically by Ivy when it resolves the build, because the GP is added as a source dependency of any build. But for that to work, we have to not include GP's classpath & classloader when constructing a build's classpath & classloader.
With this change in place, adding the GP's classpath onto config.classpath, it means that, if a build redefines a dependency of GP, that build ends up with two versions of the same dep on the CP (and the same goes if the dependency is declared only in the GP, but is dynamic). Clearly not what we want.

@jsuereth
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I agree. This is where having full classpath separation would, indeed, be nice. I think this related to the issues you see with global plugins today. If you wanted to take a crack at fixing that please let me know, with the caveat that it would need to go in "early" in a sbt release cycle so we can fully vet it.

Please sign in to comment.