Some comments and better organization of compile-related code.

This commit is contained in:
Mark Harrah 2009-09-05 15:01:04 -04:00
parent 8bfb2802fb
commit 39546077ee
8 changed files with 150 additions and 97 deletions

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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)

38
compile/RawCompiler.scala Normal file
View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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}