diff --git a/compile/AnalyzingCompiler.scala b/compile/AnalyzingCompiler.scala new file mode 100644 index 000000000..efa698539 --- /dev/null +++ b/compile/AnalyzingCompiler.scala @@ -0,0 +1,45 @@ +package xsbt + + import xsbti.{AnalysisCallback, Logger => xLogger} + import java.io.File + import java.net.URLClassLoader + +/** Interface to the Scala compiler that uses the dependency analysis plugin. This class uses the Scala library and compiler +* obtained through the 'scalaLoader' class loader. This class requires a ComponentManager in order to obtain the +* interface code to scalac and the analysis plugin. Because these call Scala code for a different Scala version, they must +* be compiled for the version of Scala being used. It is essential that the provided 'scalaVersion' be a 1:1 mapping to the +* actual version of Scala being used for compilation (-SNAPSHOT is not acceptable). Otherwise, binary compatibility +* issues will ensue!*/ +class AnalyzingCompiler(scalaInstance: ScalaInstance, manager: ComponentManager) extends NotNull +{ + def apply(sources: Set[File], classpath: Set[File], outputDirectory: File, options: Seq[String], callback: AnalysisCallback, maximumErrors: Int, log: CompileLogger): Unit = + apply(sources, classpath, outputDirectory, options, false, callback, maximumErrors, log) + def apply(sources: Set[File], classpath: Set[File], outputDirectory: File, options: Seq[String], compilerOnClasspath: Boolean, + callback: AnalysisCallback, maximumErrors: Int, log: CompileLogger) + { + val arguments = (new CompilerArguments(scalaInstance))(sources, classpath, outputDirectory, options, compilerOnClasspath) + // this is the instance used to compile the analysis + val componentCompiler = new ComponentCompiler(new RawCompiler(scalaInstance, log), manager) + log.debug("Getting " + ComponentCompiler.compilerInterfaceID + " from component compiler for Scala " + scalaInstance.version) + val interfaceJar = componentCompiler(ComponentCompiler.compilerInterfaceID) + val dual = createDualLoader(scalaInstance.loader, getClass.getClassLoader) // this goes to scalaLoader for scala classes and sbtLoader for xsbti classes + val interfaceLoader = new URLClassLoader(Array(interfaceJar.toURI.toURL), dual) + val interface = Class.forName("xsbt.CompilerInterface", true, interfaceLoader).newInstance.asInstanceOf[AnyRef] + val runnable = interface.asInstanceOf[{ def run(args: Array[String], callback: AnalysisCallback, maximumErrors: Int, log: xLogger): Unit }] + // these arguments are safe to pass across the ClassLoader boundary because the types are defined in Java + // so they will be binary compatible across all versions of Scala + runnable.run(arguments.toArray, callback, maximumErrors, log) + } + protected def createDualLoader(scalaLoader: ClassLoader, sbtLoader: ClassLoader): ClassLoader = + { + val xsbtiFilter = (name: String) => name.startsWith("xsbti.") + val notXsbtiFilter = (name: String) => !xsbtiFilter(name) + new DualLoader(scalaLoader, notXsbtiFilter, x => true, sbtLoader, xsbtiFilter, x => false) + } + override def toString = "Analyzing compiler (Scala " + scalaInstance.actualVersion + ")" +} +object AnalyzingCompiler +{ + def apply(scalaVersion: String, provider: xsbti.ScalaProvider, manager: ComponentManager): AnalyzingCompiler = + new AnalyzingCompiler(ScalaInstance(scalaVersion, provider), manager) +} \ No newline at end of file diff --git a/compile/Compiler.scala b/compile/Compiler.scala deleted file mode 100644 index 588b6b8a5..000000000 --- a/compile/Compiler.scala +++ /dev/null @@ -1,77 +0,0 @@ -package xsbt - -import xsbti.{AnalysisCallback, Logger => xLogger} -import java.io.File -import java.net.URLClassLoader - -/** Interface to the Scala compiler. This class uses the Scala library and compiler obtained through the 'scalaLoader' class -* loader. This class requires a ComponentManager in order to obtain the interface code to scalac and the analysis plugin. Because -* these call Scala code for a different Scala version, they must be compiled for the version of Scala being used. -* It is essential that the provided 'scalaVersion' be a 1:1 mapping to the actual version of Scala being used for compilation -* (-SNAPSHOT is not acceptable). Otherwise, binary compatibility issues will ensue!*/ - -/** A basic interface to the compiler. It is called in the same virtual machine, but no dependency analysis is done. This -* is used, for example, to compile the interface/plugin code.*/ -class RawCompiler(scalaLoader: ClassLoader, scalaLibDirectory: File, log: CompileLogger) -{ - lazy val scalaVersion = Class.forName("scala.tools.nsc.Properties", true, scalaLoader).getMethod("versionString").invoke(null) - def apply(sources: Set[File], classpath: Set[File], outputDirectory: File, options: Seq[String]): Unit = - apply(sources, classpath, outputDirectory, options, false) - def apply(sources: Set[File], classpath: Set[File], outputDirectory: File, options: Seq[String], compilerOnClasspath: Boolean) - { - // reflection is required for binary compatibility - // The following imports ensure there is a compile error if the identifiers change, - // but should not be otherwise directly referenced - import scala.tools.nsc.Main - import scala.tools.nsc.Properties - - val arguments = CompilerArguments(scalaLibDirectory)(sources, classpath, outputDirectory, options, compilerOnClasspath) - log.debug("Vanilla interface to Scala compiler " + scalaVersion + " with arguments: " + arguments.mkString("\n\t", "\n\t", "")) - val mainClass = Class.forName("scala.tools.nsc.Main", true, scalaLoader) - val process = mainClass.getMethod("process", classOf[Array[String]]) - val realArray: Array[String] = arguments.toArray - assert(realArray.getClass eq classOf[Array[String]]) - process.invoke(null, realArray) - checkForFailure(mainClass, arguments.toArray) - } - protected def checkForFailure(mainClass: Class[_], args: Array[String]) - { - val reporter = mainClass.getMethod("reporter").invoke(null) - val failed = reporter.asInstanceOf[{ def hasErrors: Boolean }].hasErrors - if(failed) throw new xsbti.CompileFailed { val arguments = args; override def toString = "Vanilla compile failed" } - } -} -/** Interface to the compiler that uses the dependency analysis plugin.*/ -class AnalyzeCompiler(scalaVersion: String, scalaLoader: ClassLoader, scalaLibDirectory: File, manager: ComponentManager) extends NotNull -{ - def apply(sources: Set[File], classpath: Set[File], outputDirectory: File, options: Seq[String], callback: AnalysisCallback, maximumErrors: Int, log: CompileLogger): Unit = - apply(sources, classpath, outputDirectory, options, false, callback, maximumErrors, log) - def apply(sources: Set[File], classpath: Set[File], outputDirectory: File, options: Seq[String], compilerOnClasspath: Boolean, - callback: AnalysisCallback, maximumErrors: Int, log: CompileLogger) - { - val arguments = CompilerArguments(scalaLibDirectory)(sources, classpath, outputDirectory, options, compilerOnClasspath) - // this is the instance used to compile the analysis - val componentCompiler = new ComponentCompiler(scalaVersion, new RawCompiler(scalaLoader, scalaLibDirectory, log), manager) - log.debug("Getting " + ComponentCompiler.compilerInterfaceID + " from component compiler for Scala " + scalaVersion + " (loader=" + scalaLoader + ")") - val interfaceJar = componentCompiler(ComponentCompiler.compilerInterfaceID) - val dual = createDualLoader(scalaLoader, getClass.getClassLoader) // this goes to scalaLoader for scala classes and sbtLoader for xsbti classes - val interfaceLoader = new URLClassLoader(Array(interfaceJar.toURI.toURL), dual) - val interface = Class.forName("xsbt.CompilerInterface", true, interfaceLoader).newInstance.asInstanceOf[AnyRef] - val runnable = interface.asInstanceOf[{ def run(args: Array[String], callback: AnalysisCallback, maximumErrors: Int, log: xLogger): Unit }] - // these arguments are safe to pass across the ClassLoader boundary because the types are defined in Java - // so they will be binary compatible across all versions of Scala - runnable.run(arguments.toArray, callback, maximumErrors, log) - } - private def createDualLoader(scalaLoader: ClassLoader, sbtLoader: ClassLoader): ClassLoader = - { - val xsbtiFilter = (name: String) => name.startsWith("xsbti.") - val notXsbtiFilter = (name: String) => !xsbtiFilter(name) - new DualLoader(scalaLoader, notXsbtiFilter, x => true, sbtLoader, xsbtiFilter, x => false) - } - override def toString = "Analyzing compiler (Scala " + scalaVersion + ")" -} -object AnalyzeCompiler -{ - def apply(scalaVersion: String, provider: xsbti.ScalaProvider, manager: ComponentManager): AnalyzeCompiler = - new AnalyzeCompiler(scalaVersion, provider.getScalaLoader(scalaVersion), provider.getScalaHome(scalaVersion), manager) -} \ No newline at end of file diff --git a/compile/CompilerArguments.scala b/compile/CompilerArguments.scala index 38d524a00..12da5735e 100644 --- a/compile/CompilerArguments.scala +++ b/compile/CompilerArguments.scala @@ -2,21 +2,33 @@ package xsbt import java.io.File -object CompilerArguments +/** Forms the list of options that is passed to the compiler from the required inputs and other options. +* The directory containing scala-library.jar and scala-compiler.jar (scalaLibDirectory) is required in +* order to add these jars to the boot classpath. The 'scala.home' property must be unset because Scala +* puts jars in that directory on the bootclasspath. Because we use multiple Scala versions, +* this would lead to compiling against the wrong library jar.*/ +class CompilerArguments(scalaInstance: ScalaInstance) { - def apply(scalaLibDirectory: File)(sources: Set[File], classpath: Set[File], outputDirectory: File, options: Seq[String], compilerOnClasspath: Boolean): Seq[String] = + def apply(sources: Set[File], classpath: Set[File], outputDirectory: File, options: Seq[String], compilerOnClasspath: Boolean): Seq[String] = { - val scalaHome = System.getProperty("scala.home") - assert((scalaHome eq null) || scalaHome.isEmpty, "'scala.home' should not be set (was " + scalaHome + ")") - def abs(files: Set[File]) = files.map(_.getAbsolutePath) - val originalBoot = System.getProperty("sun.boot.class.path", "") - val newBootPrefix = if(originalBoot.isEmpty) "" else originalBoot + File.pathSeparator - val bootClasspathOption = Seq("-bootclasspath", newBootPrefix + scalaLibraryJar(scalaLibDirectory).getAbsolutePath) - val cp2 = classpath ++ (if(compilerOnClasspath) scalaCompilerJar(scalaLibDirectory):: Nil else Nil) - val classpathOption = Seq("-cp", abs(cp2).mkString(File.pathSeparator) ) + checkScalaHomeUnset() + val bootClasspathOption = Seq("-bootclasspath", createBootClasspath) + val cpWithCompiler = classpath ++ (if(compilerOnClasspath) scalaInstance.compilerJar :: Nil else Nil) + val classpathOption = Seq("-cp", abs(cpWithCompiler).mkString(File.pathSeparator) ) val outputOption = Seq("-d", outputDirectory.getAbsolutePath) options ++ outputOption ++ bootClasspathOption ++ classpathOption ++ abs(sources) } - private def scalaLibraryJar(scalaLibDirectory: File): File = new File(scalaLibDirectory, "scala-library.jar") - private def scalaCompilerJar(scalaLibDirectory: File): File = new File(scalaLibDirectory, "scala-compiler.jar") + protected def abs(files: Set[File]) = files.map(_.getAbsolutePath) + protected def checkScalaHomeUnset() + { + val scalaHome = System.getProperty("scala.home") + assert((scalaHome eq null) || scalaHome.isEmpty, "'scala.home' should not be set (was " + scalaHome + ")") + } + /** Add the correct Scala library jar to the boot classpath.*/ + protected def createBootClasspath = + { + val originalBoot = System.getProperty("sun.boot.class.path", "") + val newBootPrefix = if(originalBoot.isEmpty) "" else originalBoot + File.pathSeparator + newBootPrefix + scalaInstance.libraryJar.getAbsolutePath + } } \ No newline at end of file diff --git a/compile/ComponentCompiler.scala b/compile/ComponentCompiler.scala index f2b15b889..f491763a0 100644 --- a/compile/ComponentCompiler.scala +++ b/compile/ComponentCompiler.scala @@ -10,18 +10,21 @@ object ComponentCompiler val compilerInterfaceID = "compiler-interface" val compilerInterfaceSrcID = compilerInterfaceID + srcExtension } -class ComponentCompiler(scalaVersion: String, compiler: RawCompiler, manager: ComponentManager) +/** This class provides source components compiled with the provided RawCompiler. +* The compiled classes are cached using the provided component manager according +* to the actualVersion field of the RawCompiler.*/ +class ComponentCompiler(compiler: RawCompiler, manager: ComponentManager) { import ComponentCompiler._ import FileUtilities.{copy, createDirectory, zip, jars, unzip, withTemporaryDirectory} def apply(id: String): File = { - val binID = binaryID(id, scalaVersion) + val binID = binaryID(id) try { manager.file(binID) } catch { case e: InvalidComponent => compileAndInstall(id, binID) } } - private def binaryID(id: String, scalaVersion: String) = id + binSeparator + scalaVersion - private def compileAndInstall(id: String, binID: String): File = + protected def binaryID(id: String) = id + binSeparator + compiler.scalaInstance.actualVersion + protected def compileAndInstall(id: String, binID: String): File = { val srcID = id + srcExtension val binaryDirectory = manager.location(binID) diff --git a/compile/RawCompiler.scala b/compile/RawCompiler.scala new file mode 100644 index 000000000..f61b9787c --- /dev/null +++ b/compile/RawCompiler.scala @@ -0,0 +1,38 @@ +package xsbt + + import java.io.File + +/** A basic interface to the compiler. It is called in the same virtual machine, but no dependency analysis is done. This +* is used, for example, to compile the interface/plugin code.*/ +class RawCompiler(val scalaInstance: ScalaInstance, log: CompileLogger) +{ + def apply(sources: Set[File], classpath: Set[File], outputDirectory: File, options: Seq[String]): Unit = + apply(sources, classpath, outputDirectory, options, false) + def apply(sources: Set[File], classpath: Set[File], outputDirectory: File, options: Seq[String], compilerOnClasspath: Boolean) + { + // reflection is required for binary compatibility + // The following imports ensure there is a compile error if the identifiers change, + // but should not be otherwise directly referenced + import scala.tools.nsc.Main + import scala.tools.nsc.Properties + + val arguments = (new CompilerArguments(scalaInstance))(sources, classpath, outputDirectory, options, compilerOnClasspath) + log.debug("Vanilla interface to Scala compiler " + scalaInstance.actualVersion + " with arguments: " + arguments.mkString("\n\t", "\n\t", "")) + val mainClass = Class.forName("scala.tools.nsc.Main", true, scalaInstance.loader) + val process = mainClass.getMethod("process", classOf[Array[String]]) + process.invoke(null, toJavaArray(arguments)) + checkForFailure(mainClass, arguments.toArray) + } + protected def checkForFailure(mainClass: Class[_], args: Array[String]) + { + val reporter = mainClass.getMethod("reporter").invoke(null) + val failed = reporter.asInstanceOf[{ def hasErrors: Boolean }].hasErrors + if(failed) throw new xsbti.CompileFailed { val arguments = args; override def toString = "Vanilla compile failed" } + } + protected def toJavaArray(arguments: Seq[String]): Array[String] = + { + val realArray: Array[String] = arguments.toArray + assert(realArray.getClass eq classOf[Array[String]]) + realArray + } +} \ No newline at end of file diff --git a/compile/ScalaInstance.scala b/compile/ScalaInstance.scala new file mode 100644 index 000000000..a9127a12b --- /dev/null +++ b/compile/ScalaInstance.scala @@ -0,0 +1,32 @@ +package xsbt + + import java.io.File + +/** Represents the source for Scala classes for a given version. The reason both a ClassLoader and the jars are required +* is that the compiler requires the location of the library/compiler jars on the (boot)classpath and the loader is used +* for the compiler itself. +* The 'version' field is the version used to obtain the Scala classes. This is typically the version for the maven repository. +* The 'actualVersion' field should be used to uniquely identify the compiler. It is obtained from the compiler.properties file.*/ +final class ScalaInstance(val version: String, val loader: ClassLoader, val libraryJar: File, val compilerJar: File) extends NotNull +{ + /** Gets the version of Scala in the compiler.properties file from the loader. This version may be different than that given by 'version'*/ + lazy val actualVersion = + { + import ScalaInstance.VersionPrefix + val v = Class.forName("scala.tools.nsc.Properties", true, loader).getMethod("versionString").invoke(null).toString + if(v.startsWith(VersionPrefix)) v.substring(VersionPrefix.length) else v + } +} +object ScalaInstance +{ + val VersionPrefix = "version " + /** Creates a ScalaInstance using the given provider to obtain the jars and loader.*/ + def apply(version: String, provider: xsbti.ScalaProvider) = + { + val scalaLibDirectory = provider.getScalaHome(version) + // these get the locations of the scala jars given the location of the lib/ directory + val libraryJar = new File(scalaLibDirectory, "scala-library.jar") + val compilerJar = new File(scalaLibDirectory, "scala-compiler.jar") + new ScalaInstance(version, provider.getScalaLoader(version), libraryJar, compilerJar) + } +} \ No newline at end of file diff --git a/compile/src/test/scala/CompileTest.scala b/compile/src/test/scala/CompileTest.scala index eb4d59de0..7c00ba665 100644 --- a/compile/src/test/scala/CompileTest.scala +++ b/compile/src/test/scala/CompileTest.scala @@ -13,7 +13,7 @@ object CompileTest extends Specification WithCompiler( "2.7.2" )(testCompileAnalysis) } } - private def testCompileAnalysis(compiler: AnalyzeCompiler, log: CompileLogger) + private def testCompileAnalysis(compiler: AnalyzingCompiler, log: CompileLogger) { WithFiles( new File("Test.scala") -> "object Test" ) { sources => withTemporaryDirectory { temp => @@ -26,7 +26,7 @@ object CompileTest extends Specification } object WithCompiler { - def apply[T](scalaVersion: String)(f: (AnalyzeCompiler, CompileLogger) => T): T = + def apply[T](scalaVersion: String)(f: (AnalyzingCompiler, CompileLogger) => T): T = { System.setProperty("scala.home", "") // need to make sure scala.home is unset val log = new TestIvyLogger with CompileLogger @@ -38,7 +38,7 @@ object WithCompiler val manager = new ComponentManager(launch.getSbtHome(sbtVersion, scalaVersion), log) prepare(manager, ComponentCompiler.compilerInterfaceSrcID, "CompilerInterface.scala") prepare(manager, ComponentCompiler.xsbtiID, classOf[xsbti.AnalysisCallback]) - f(AnalyzeCompiler(scalaVersion, launch, manager), log) + f(AnalyzingCompiler(scalaVersion, launch, manager), log) } } } diff --git a/tasks/standard/Compile.scala b/tasks/standard/Compile.scala index 09f6e64b2..9d3175d66 100644 --- a/tasks/standard/Compile.scala +++ b/tasks/standard/Compile.scala @@ -39,7 +39,7 @@ trait Compile extends TrackedTaskDefinition[CompileReport] protected def getTracked = Seq(trackedClasspath, trackedSource, trackedOptions, invalidation) } class StandardCompile(val sources: Task[Set[File]], val classpath: Task[Set[File]], val outputDirectory: Task[File], val options: Task[Seq[String]], - val superclassNames: Task[Set[String]], val compilerTask: Task[AnalyzeCompiler], val cacheDirectory: File, val log: CompileLogger) extends Compile + val superclassNames: Task[Set[String]], val compilerTask: Task[AnalyzingCompiler], val cacheDirectory: File, val log: CompileLogger) extends Compile { import Task._ import scala.collection.mutable.{ArrayBuffer, Buffer, HashMap, HashSet, Map, Set => mSet}