diff --git a/compile/AnalyzingCompiler.scala b/compile/AnalyzingCompiler.scala index ce67edbd3..e812fdf89 100644 --- a/compile/AnalyzingCompiler.scala +++ b/compile/AnalyzingCompiler.scala @@ -5,6 +5,7 @@ package sbt package compiler import xsbti.{AnalysisCallback, Logger => xLogger, Reporter} + import xsbti.compile.{CachedCompiler, CachedCompilerProvider, DependencyChanges, GlobalsCache} import java.io.File import java.net.{URL, URLClassLoader} @@ -12,22 +13,37 @@ package compiler * provided by scalaInstance. 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 than the one used for this class, they must * be compiled for the version of Scala being used.*/ -class AnalyzingCompiler(val scalaInstance: xsbti.compile.ScalaInstance, val provider: CompilerInterfaceProvider, val cp: xsbti.compile.ClasspathOptions, log: Logger) +final class AnalyzingCompiler(val scalaInstance: xsbti.compile.ScalaInstance, val provider: CompilerInterfaceProvider, val cp: xsbti.compile.ClasspathOptions, log: Logger) extends CachedCompilerProvider { def this(scalaInstance: ScalaInstance, provider: CompilerInterfaceProvider, log: Logger) = this(scalaInstance, provider, ClasspathOptions.auto, log) - def apply(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], callback: AnalysisCallback, maximumErrors: Int, log: Logger) + def apply(sources: Seq[File], changes: DependencyChanges, classpath: Seq[File], outputDirectory: File, options: Seq[String], callback: AnalysisCallback, maximumErrors: Int, cache: GlobalsCache, log: Logger) { - val arguments = (new CompilerArguments(scalaInstance, cp))(sources, classpath, outputDirectory, options) - compile(arguments, callback, maximumErrors, log) + val arguments = (new CompilerArguments(scalaInstance, cp))(Nil, classpath, outputDirectory, options) + compile(sources, changes, arguments, callback, maximumErrors, cache, log) } - def compile(arguments: Seq[String], callback: AnalysisCallback, maximumErrors: Int, log: Logger): Unit = - compile(arguments, callback, log, new LoggerReporter(maximumErrors, log)) - def compile(arguments: Seq[String], callback: AnalysisCallback, log: Logger, reporter: Reporter) + def compile(sources: Seq[File], changes: DependencyChanges, options: Seq[String], callback: AnalysisCallback, maximumErrors: Int, cache: GlobalsCache, log: Logger): Unit = { - call("xsbt.CompilerInterface", log)( - classOf[Array[String]], classOf[AnalysisCallback], classOf[xLogger], classOf[Reporter] ) ( - arguments.toArray[String] : Array[String], callback, log, reporter ) + val reporter = new LoggerReporter(maximumErrors, log) + val cached = cache(options.toArray, !changes.isEmpty, this, log, reporter) + compile(sources, changes, callback, log, reporter, cached) + } + + def compile(sources: Seq[File], changes: DependencyChanges, callback: AnalysisCallback, log: Logger, reporter: Reporter, compiler: CachedCompiler) + { + call("xsbt.CompilerInterface", "run", log)( + classOf[Array[File]], classOf[DependencyChanges], classOf[AnalysisCallback], classOf[xLogger], classOf[Reporter], classOf[CachedCompiler]) ( + sources.toArray, changes, callback, log, reporter, compiler ) + } + def newCachedCompiler(arguments: Array[String], log: xLogger, reporter: Reporter): CachedCompiler = + newCachedCompiler(arguments: Seq[String], log, reporter) + + def newCachedCompiler(arguments: Seq[String], log: xLogger, reporter: Reporter): CachedCompiler = + { + call("xsbt.CompilerInterface", "newCompiler", log)( + classOf[Array[String]], classOf[xLogger], classOf[Reporter] ) ( + arguments.toArray[String] : Array[String], log, reporter ). + asInstanceOf[CachedCompiler] } def doc(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], maximumErrors: Int, log: Logger): Unit = @@ -35,7 +51,7 @@ class AnalyzingCompiler(val scalaInstance: xsbti.compile.ScalaInstance, val prov def doc(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], log: Logger, reporter: Reporter): Unit = { val arguments = (new CompilerArguments(scalaInstance, cp))(sources, classpath, outputDirectory, options) - call("xsbt.ScaladocInterface", log) (classOf[Array[String]], classOf[xLogger], classOf[Reporter]) ( + call("xsbt.ScaladocInterface", "run", log) (classOf[Array[String]], classOf[xLogger], classOf[Reporter]) ( arguments.toArray[String] : Array[String], log, reporter) } def console(classpath: Seq[File], options: Seq[String], initialCommands: String, cleanupCommands: String, log: Logger)(loader: Option[ClassLoader] = None, bindings: Seq[(String, Any)] = Nil): Unit = @@ -44,16 +60,16 @@ class AnalyzingCompiler(val scalaInstance: xsbti.compile.ScalaInstance, val prov val classpathString = CompilerArguments.absString(arguments.finishClasspath(classpath)) val bootClasspath = if(cp.autoBoot) arguments.createBootClasspath else "" val (names, values) = bindings.unzip - call("xsbt.ConsoleInterface", log)( + call("xsbt.ConsoleInterface", "run", log)( classOf[Array[String]], classOf[String], classOf[String], classOf[String], classOf[String], classOf[ClassLoader], classOf[Array[String]], classOf[Array[Any]], classOf[xLogger])( options.toArray[String]: Array[String], bootClasspath, classpathString, initialCommands, cleanupCommands, loader.orNull, names.toArray[String], values.toArray[Any], log) } def force(log: Logger): Unit = provider(scalaInstance, log) - private def call(interfaceClassName: String, log: Logger)(argTypes: Class[_]*)(args: AnyRef*) + private def call(interfaceClassName: String, methodName: String, log: Logger)(argTypes: Class[_]*)(args: AnyRef*): AnyRef = { val interfaceClass = getInterfaceClass(interfaceClassName, log) val interface = interfaceClass.newInstance.asInstanceOf[AnyRef] - val method = interfaceClass.getMethod("run", argTypes : _*) + val method = interfaceClass.getMethod(methodName, argTypes : _*) try { method.invoke(interface, args: _*) } catch { case e: java.lang.reflect.InvocationTargetException => throw e.getCause } } diff --git a/compile/CompilerCache.scala b/compile/CompilerCache.scala new file mode 100644 index 000000000..7a0d828ee --- /dev/null +++ b/compile/CompilerCache.scala @@ -0,0 +1,52 @@ +package sbt +package compiler + + import xsbti.{Logger => xLogger, Reporter} + import xsbti.compile.{CachedCompiler, CachedCompilerProvider, GlobalsCache} + import Logger.f0 + import java.io.File + import java.util.{LinkedHashMap,Map} + +private final class CompilerCache(val maxInstances: Int) extends GlobalsCache +{ + private[this] val cache = lru[CompilerKey, CachedCompiler](maxInstances) + private[this] def lru[A,B](max: Int) = new LinkedHashMap[A,B](8, 0.75f, true) { + override def removeEldestEntry(eldest: Map.Entry[A,B]): Boolean = size > max + } + def apply(args: Array[String], forceNew: Boolean, c: CachedCompilerProvider, log: xLogger, reporter: Reporter): CachedCompiler = synchronized + { + val key = CompilerKey(dropSources(args.toList), c.scalaInstance.actualVersion) + if(forceNew) cache.remove(key) + cache.get(key) match { + case null => + log.debug(f0("Compiler cache miss. " + key.toString)) + put(key, c.newCachedCompiler(args, log, reporter)) + case cc => + log.debug(f0("Compiler cache hit (" + cc.hashCode.toHexString + "). " + key.toString)) + cc + } + } + def clear(): Unit = synchronized { cache.clear() } + + private[this] def dropSources(args: Seq[String]): Seq[String] = + args.filterNot(arg => arg.endsWith(".scala") || arg.endsWith(".java")) + + private[this] def put(key: CompilerKey, cc: CachedCompiler): CachedCompiler = + { + cache.put(key, cc) + cc + } + private[this] final case class CompilerKey(args: Seq[String], scalaVersion: String) { + override def toString = "scala " + scalaVersion + ", args: " + args.mkString(" ") + } +} +object CompilerCache +{ + def apply(maxInstances: Int): GlobalsCache = new CompilerCache(maxInstances) + + val fresh: GlobalsCache = new GlobalsCache { + def clear() {} + def apply(args: Array[String], forceNew: Boolean, c: CachedCompilerProvider, log: xLogger, reporter: Reporter): CachedCompiler = + c.newCachedCompiler(args, log, reporter) + } +} \ No newline at end of file diff --git a/compile/inc/Compile.scala b/compile/inc/Compile.scala index f5d867352..6b3761ca0 100644 --- a/compile/inc/Compile.scala +++ b/compile/inc/Compile.scala @@ -5,22 +5,23 @@ package sbt package inc import xsbti.api.{Source, SourceAPI} +import xsbti.compile.DependencyChanges import xsbti.{Position,Problem,Severity} import Logger.{m2o, problem} import java.io.File object IncrementalCompile { - def apply(sources: Set[File], entry: String => Option[File], compile: (Set[File], xsbti.AnalysisCallback) => Unit, previous: Analysis, forEntry: File => Option[Analysis], outputPath: File, log: Logger): (Boolean, Analysis) = + def apply(sources: Set[File], entry: String => Option[File], compile: (Set[File], DependencyChanges, xsbti.AnalysisCallback) => Unit, previous: Analysis, forEntry: File => Option[Analysis], outputPath: File, log: Logger): (Boolean, Analysis) = { val current = Stamps.initial(Stamp.exists, Stamp.hash, Stamp.lastModified) val internalMap = (f: File) => previous.relations.produced(f).headOption val externalAPI = getExternalAPI(entry, forEntry) Incremental.compile(sources, entry, previous, current, forEntry, doCompile(compile, internalMap, externalAPI, current, outputPath), log) } - def doCompile(compile: (Set[File], xsbti.AnalysisCallback) => Unit, internalMap: File => Option[File], externalAPI: (File, String) => Option[Source], current: ReadStamps, outputPath: File) = (srcs: Set[File]) => { + def doCompile(compile: (Set[File], DependencyChanges, xsbti.AnalysisCallback) => Unit, internalMap: File => Option[File], externalAPI: (File, String) => Option[Source], current: ReadStamps, outputPath: File) = (srcs: Set[File], changes: DependencyChanges) => { val callback = new AnalysisCallback(internalMap, externalAPI, current, outputPath) - compile(srcs, callback) + compile(srcs, changes, callback) callback.get } def getExternalAPI(entry: String => Option[File], forEntry: File => Option[Analysis]): (File, String) => Option[Source] = diff --git a/compile/inc/Incremental.scala b/compile/inc/Incremental.scala index 60fe84d69..3e3069542 100644 --- a/compile/inc/Incremental.scala +++ b/compile/inc/Incremental.scala @@ -7,30 +7,36 @@ package inc import xsbt.api.{NameChanges, SameAPI, TopLevel} import annotation.tailrec import xsbti.api.{Compilation, Source} +import xsbti.compile.DependencyChanges import java.io.File object Incremental { def debug(s: => String) = if(java.lang.Boolean.getBoolean("xsbt.inc.debug")) println(s) else () - def compile(sources: Set[File], entry: String => Option[File], previous: Analysis, current: ReadStamps, forEntry: File => Option[Analysis], doCompile: Set[File] => Analysis, log: Logger)(implicit equivS: Equiv[Stamp]): (Boolean, Analysis) = + def compile(sources: Set[File], entry: String => Option[File], previous: Analysis, current: ReadStamps, forEntry: File => Option[Analysis], doCompile: (Set[File], DependencyChanges) => Analysis, log: Logger)(implicit equivS: Equiv[Stamp]): (Boolean, Analysis) = { val initialChanges = changedInitial(entry, sources, previous, current, forEntry) + val binaryChanges = new DependencyChanges { + val modifiedBinaries = initialChanges.binaryDeps.toArray + val modifiedClasses = initialChanges.external.modified.toArray + def isEmpty = modifiedBinaries.isEmpty && modifiedClasses.isEmpty + } val initialInv = invalidateInitial(previous.relations, initialChanges, log) log.debug("Initially invalidated: " + initialInv) - val analysis = cycle(initialInv, previous, doCompile, log) + val analysis = cycle(initialInv, binaryChanges, previous, doCompile, log) (!initialInv.isEmpty, analysis) } // TODO: the Analysis for the last successful compilation should get returned + Boolean indicating success // TODO: full external name changes, scopeInvalidations - def cycle(invalidated: Set[File], previous: Analysis, doCompile: Set[File] => Analysis, log: Logger): Analysis = + def cycle(invalidated: Set[File], binaryChanges: DependencyChanges, previous: Analysis, doCompile: (Set[File], DependencyChanges) => Analysis, log: Logger): Analysis = if(invalidated.isEmpty) previous else { val pruned = prune(invalidated, previous) debug("********* Pruned: \n" + pruned.relations + "\n*********") - val fresh = doCompile(invalidated) + val fresh = doCompile(invalidated, binaryChanges) debug("********* Fresh: \n" + fresh.relations + "\n*********") val merged = pruned ++ fresh//.copy(relations = pruned.relations ++ fresh.relations, apis = pruned.apis ++ fresh.apis) debug("********* Merged: \n" + merged.relations + "\n*********") @@ -38,8 +44,13 @@ object Incremental debug("Changes:\n" + incChanges) val incInv = invalidateIncremental(merged.relations, incChanges, invalidated, log) log.debug("Incrementally invalidated: " + incInv) - cycle(incInv, merged, doCompile, log) + cycle(incInv, emptyChanges, merged, doCompile, log) } + private[this] def emptyChanges: DependencyChanges = new DependencyChanges { + val modifiedBinaries = new Array[File](0) + val modifiedClasses = new Array[String](0) + def isEmpty = true + } /** diff --git a/compile/integration/AggressiveCompile.scala b/compile/integration/AggressiveCompile.scala index 97d29d436..dbd1baa7a 100644 --- a/compile/integration/AggressiveCompile.scala +++ b/compile/integration/AggressiveCompile.scala @@ -9,37 +9,38 @@ import inc._ import java.io.File import classpath.ClasspathUtilities import classfile.Analyze - import xsbti.api.Source - import xsbti.compile.CompileOrder - import CompileOrder.{JavaThenScala, Mixed, ScalaThenJava} - import xsbti.AnalysisCallback import inc.Locate.DefinesClass import CompileSetup._ import sbinary.DefaultProtocol.{ immutableMapFormat, immutableSetFormat, StringFormat } import Types.const + import xsbti.AnalysisCallback + import xsbti.api.Source + import xsbti.compile.{CompileOrder, DependencyChanges, GlobalsCache} + import CompileOrder.{JavaThenScala, Mixed, ScalaThenJava} + final class CompileConfiguration(val sources: Seq[File], val classpath: Seq[File], val previousAnalysis: Analysis, val previousSetup: Option[CompileSetup], val currentSetup: CompileSetup, val getAnalysis: File => Option[Analysis], val definesClass: DefinesClass, - val maxErrors: Int, val compiler: AnalyzingCompiler, val javac: xsbti.compile.JavaCompiler) + val maxErrors: Int, val compiler: AnalyzingCompiler, val javac: xsbti.compile.JavaCompiler, val cache: GlobalsCache) class AggressiveCompile(cacheFile: File) { - def apply(compiler: AnalyzingCompiler, javac: xsbti.compile.JavaCompiler, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String] = Nil, javacOptions: Seq[String] = Nil, analysisMap: File => Option[Analysis] = const(None), definesClass: DefinesClass = Locate.definesClass _, maxErrors: Int = 100, compileOrder: CompileOrder = Mixed, skip: Boolean = false)(implicit log: Logger): Analysis = + def apply(compiler: AnalyzingCompiler, javac: xsbti.compile.JavaCompiler, sources: Seq[File], classpath: Seq[File], outputDirectory: File, cache: GlobalsCache, options: Seq[String] = Nil, javacOptions: Seq[String] = Nil, analysisMap: File => Option[Analysis] = const(None), definesClass: DefinesClass = Locate.definesClass _, maxErrors: Int = 100, compileOrder: CompileOrder = Mixed, skip: Boolean = false)(implicit log: Logger): Analysis = { val setup = new CompileSetup(outputDirectory, new CompileOptions(options, javacOptions), compiler.scalaInstance.actualVersion, compileOrder) - compile1(sources, classpath, setup, store, analysisMap, definesClass, compiler, javac, maxErrors, skip) + compile1(sources, classpath, setup, store, analysisMap, definesClass, compiler, javac, maxErrors, skip, cache) } def withBootclasspath(args: CompilerArguments, classpath: Seq[File]): Seq[File] = args.bootClasspath ++ args.finishClasspath(classpath) - def compile1(sources: Seq[File], classpath: Seq[File], setup: CompileSetup, store: AnalysisStore, analysis: File => Option[Analysis], definesClass: DefinesClass, compiler: AnalyzingCompiler, javac: xsbti.compile.JavaCompiler, maxErrors: Int, skip: Boolean)(implicit log: Logger): Analysis = + def compile1(sources: Seq[File], classpath: Seq[File], setup: CompileSetup, store: AnalysisStore, analysis: File => Option[Analysis], definesClass: DefinesClass, compiler: AnalyzingCompiler, javac: xsbti.compile.JavaCompiler, maxErrors: Int, skip: Boolean, cache: GlobalsCache)(implicit log: Logger): Analysis = { val (previousAnalysis, previousSetup) = extract(store.get()) if(skip) previousAnalysis else { - val config = new CompileConfiguration(sources, classpath, previousAnalysis, previousSetup, setup, analysis, definesClass, maxErrors, compiler, javac) + val config = new CompileConfiguration(sources, classpath, previousAnalysis, previousSetup, setup, analysis, definesClass, maxErrors, compiler, javac, cache) val (modified, result) = compile2(config) if(modified) store.set(result, setup) @@ -56,7 +57,7 @@ class AggressiveCompile(cacheFile: File) val searchClasspath = explicitBootClasspath(options.options) ++ withBootclasspath(cArgs, absClasspath) val entry = Locate.entry(searchClasspath, definesClass) - val compile0 = (include: Set[File], callback: AnalysisCallback) => { + val compile0 = (include: Set[File], changes: DependencyChanges, callback: AnalysisCallback) => { IO.createDirectory(outputDirectory) val incSrc = sources.filter(include) val (javaSrcs, scalaSrcs) = incSrc partition javaOnly @@ -65,8 +66,8 @@ class AggressiveCompile(cacheFile: File) if(!scalaSrcs.isEmpty) { val sources = if(order == Mixed) incSrc else scalaSrcs - val arguments = cArgs(sources, absClasspath, outputDirectory, options.options) - compiler.compile(arguments, callback, maxErrors, log) + val arguments = cArgs(Nil, absClasspath, outputDirectory, options.options) + compiler.compile(sources, changes, arguments, callback, maxErrors, cache, log) } def compileJava() = if(!javaSrcs.isEmpty) diff --git a/compile/integration/IncrementalCompiler.scala b/compile/integration/IncrementalCompiler.scala index b6bda5dee..c3a3ff322 100644 --- a/compile/integration/IncrementalCompiler.scala +++ b/compile/integration/IncrementalCompiler.scala @@ -16,7 +16,7 @@ object IC extends IncrementalCompiler[Analysis, AnalyzingCompiler] val agg = new AggressiveCompile(setup.cacheFile) val aMap = (f: File) => m2o(analysisMap(f)) val defClass = (f: File) => { val dc = definesClass(f); (name: String) => dc.apply(name) } - agg(scalac, javac, sources, classpath, classesDirectory, scalacOptions, javacOptions, aMap, defClass, maxErrors, order, skip)(log) + agg(scalac, javac, sources, classpath, classesDirectory, cache, scalacOptions, javacOptions, aMap, defClass, maxErrors, order, skip)(log) } private[this] def m2o[S](opt: Maybe[S]): Option[S] = if(opt.isEmpty) None else Some(opt.get) diff --git a/compile/interface/API.scala b/compile/interface/API.scala index 3c174201f..1baa6ee55 100644 --- a/compile/interface/API.scala +++ b/compile/interface/API.scala @@ -16,7 +16,7 @@ object API val name = "xsbt-api" } -final class API(val global: Global, val callback: xsbti.AnalysisCallback) extends Compat +final class API(val global: CallbackGlobal) extends Compat { import global._ def error(msg: String) = throw new RuntimeException(msg) diff --git a/compile/interface/Analyzer.scala b/compile/interface/Analyzer.scala index 32c79d764..f582aef75 100644 --- a/compile/interface/Analyzer.scala +++ b/compile/interface/Analyzer.scala @@ -17,7 +17,7 @@ object Analyzer { def name = "xsbt-analyzer" } -final class Analyzer(val global: Global, val callback: AnalysisCallback) extends Compat +final class Analyzer(val global: CallbackGlobal) extends Compat { import global._ @@ -35,10 +35,12 @@ final class Analyzer(val global: Global, val callback: AnalysisCallback) extends // build dependencies structure val sourceFile = unit.source.file.file callback.beginSource(sourceFile) + println("Dependencies of " + sourceFile) for(on <- unit.depends) { def binaryDependency(file: File, className: String) = callback.binaryDependency(file, className, sourceFile) val onSource = on.sourceFile + println("\t" + on + ", src: " + onSource + ", class: " + classFile(on)) if(onSource == null) { classFile(on) match @@ -82,7 +84,6 @@ final class Analyzer(val global: Global, val callback: AnalysisCallback) extends } private[this] final val classSeparator = '.' - private[this] def findClass(name: String): Option[AbstractFile] = classPath.findClass(name).flatMap(_.binary.asInstanceOf[Option[AbstractFile]]) private[this] def classFile(sym: Symbol): Option[(AbstractFile, String)] = { import scala.tools.nsc.symtab.Flags diff --git a/compile/interface/CompilerInterface.scala b/compile/interface/CompilerInterface.scala index d830e9df5..4d7cc5e97 100644 --- a/compile/interface/CompilerInterface.scala +++ b/compile/interface/CompilerInterface.scala @@ -4,101 +4,227 @@ package xsbt import xsbti.{AnalysisCallback,Logger,Problem,Reporter,Severity} -import scala.tools.nsc.{Phase, SubComponent} +import xsbti.compile.{CachedCompiler, DependencyChanges} +import scala.tools.nsc.{io, reporters, util, Phase, Global, Settings, SubComponent} +import util.{ClassPath,DirectoryClassPath,MergedClassPath,JavaClassPath} +import ClassPath.{ClassPathContext,JavaContext} +import io.AbstractFile +import scala.annotation.tailrec +import scala.collection.mutable import Log.debug +import java.io.File -class CompilerInterface +final class CompilerInterface { - def run(args: Array[String], callback: AnalysisCallback, log: Logger, delegate: Reporter) + def newCompiler(options: Array[String], initialLog: Logger, initialDelegate: Reporter): CachedCompiler = + new CachedCompiler0(options, new WeakLog(initialLog, initialDelegate)) + def run(sources: Array[File], changes: DependencyChanges, callback: AnalysisCallback, log: Logger, delegate: Reporter, cached: CachedCompiler): Unit = + cached.run(sources, changes, callback, log, delegate) +} +sealed abstract class CallbackGlobal(settings: Settings, reporter: reporters.Reporter) extends Global(settings, reporter) { + def callback: AnalysisCallback + def findClass(name: String): Option[AbstractFile] +} +class InterfaceCompileFailed(val arguments: Array[String], val problems: Array[Problem], override val toString: String) extends xsbti.CompileFailed + +private final class WeakLog(private[this] var log: Logger, private[this] var delegate: Reporter) +{ + def apply(message: String) { + assert(log ne null, "Stale reference to logger") + log.error(Message(message)) + } + def logger: Logger = log + def reporter: Reporter = delegate + def clear() { + log = null + delegate = null + } +} + +private final class CachedCompiler0(args: Array[String], initialLog: WeakLog) extends CachedCompiler +{ + val settings = new Settings(s => initialLog(s)) + val command = Command(args.toList, settings) + private[this] val dreporter = DelegatingReporter(settings, initialLog.reporter) + try { + compiler // force compiler internal structures + if(!noErrors(dreporter)) { + dreporter.printSummary() + handleErrors(dreporter, initialLog.logger) + } + } finally + initialLog.clear() + + def noErrors(dreporter: DelegatingReporter) = !dreporter.hasErrors && command.ok + + def run(sources: Array[File], changes: DependencyChanges, callback: AnalysisCallback, log: Logger, delegate: Reporter): Unit = synchronized { - import scala.tools.nsc.{Global, Settings} - + println("Running cached compiler " + hashCode.toHexString) debug(log, "Interfacing (CompilerInterface) with Scala compiler " + scala.tools.nsc.Properties.versionString) - - val settings = new Settings(Log.settingsError(log)) - val command = Command(args.toList, settings) val dreporter = DelegatingReporter(settings, delegate) - def noErrors = !dreporter.hasErrors && command.ok - - object compiler extends Global(command.settings, dreporter) - { - object dummy // temporary fix for #4426 - object sbtAnalyzer extends - { - val global: compiler.type = compiler - val phaseName = Analyzer.name - val runsAfter = List("jvm") - override val runsBefore = List("terminal") - val runsRightAfter = None - } - with SubComponent - { - val analyzer = new Analyzer(global, callback) - def newPhase(prev: Phase) = analyzer.newPhase(prev) - def name = phaseName - } - object apiExtractor extends - { - val global: compiler.type = compiler - val phaseName = API.name - val runsAfter = List("typer") - override val runsBefore = List("erasure") - val runsRightAfter = Some("typer") - } - with SubComponent - { - val api = new API(global, callback) - def newPhase(prev: Phase) = api.newPhase(prev) - def name = phaseName - } - - override lazy val phaseDescriptors = - { - phasesSet += sbtAnalyzer - phasesSet += apiExtractor - superComputePhaseDescriptors - } - // Required because computePhaseDescriptors is private in 2.8 (changed to protected sometime later). - private def superComputePhaseDescriptors() = - { - val meth = classOf[Global].getDeclaredMethod("computePhaseDescriptors") - meth.setAccessible(true) - meth.invoke(this).asInstanceOf[List[SubComponent]] - } - def logUnreportedWarnings(seq: List[(Position,String)]): Unit = // Scala 2.10.x and later - { - for( (pos, msg) <- seq) yield - callback.problem(dreporter.convert(pos), msg, Severity.Warn, false) - } - def logUnreportedWarnings(count: Boolean): Unit = () // for source compatibility with Scala 2.8.x - def logUnreportedWarnings(count: Int): Unit = () // for source compatibility with Scala 2.9.x - } - def processUnreportedWarnings(run: compiler.Run) - { - implicit def listToBoolean[T](l: List[T]): Boolean = error("source compatibility only, should never be called") - implicit def listToInt[T](l: List[T]): Int = error("source compatibility only, should never be called") - compiler.logUnreportedWarnings(run.deprecationWarnings) - compiler.logUnreportedWarnings(run.uncheckedWarnings) - } + try { run(sources.toList, changes, callback, log, dreporter) } + finally { dreporter.dropDelegate() } + } + private[this] def run(sources: List[File], changes: DependencyChanges, callback: AnalysisCallback, log: Logger, dreporter: DelegatingReporter) + { if(command.shouldStopWithInfo) { dreporter.info(null, command.getInfoMessage(compiler), true) throw new InterfaceCompileFailed(args, Array(), "Compiler option supplied that disabled actual compilation.") } - if(noErrors) + if(noErrors(dreporter)) { - val run = new compiler.Run debug(log, args.mkString("Calling Scala compiler with arguments (CompilerInterface):\n\t", "\n\t", "")) - run compile command.files - processUnreportedWarnings(run) + compiler.set(callback, dreporter) + try { + val run = new compiler.Run + compiler.reload(changes) + val sortedSourceFiles = sources.map(_.getAbsolutePath).sortWith(_ < _) + run compile sortedSourceFiles + processUnreportedWarnings(run) + } finally { + compiler.clear() + } dreporter.problems foreach { p => callback.problem(p.position, p.message, p.severity, true) } } dreporter.printSummary() - if(!noErrors) - { - debug(log, "Compilation failed (CompilerInterface)") - throw new InterfaceCompileFailed(args, dreporter.problems, "Compilation failed") - } + if(!noErrors(dreporter)) handleErrors(dreporter, log) } -} -class InterfaceCompileFailed(val arguments: Array[String], val problems: Array[Problem], override val toString: String) extends xsbti.CompileFailed \ No newline at end of file + def handleErrors(dreporter: DelegatingReporter, log: Logger): Nothing = + { + debug(log, "Compilation failed (CompilerInterface)") + throw new InterfaceCompileFailed(args, dreporter.problems, "Compilation failed") + } + def processUnreportedWarnings(run: compiler.Run) + { + implicit def listToBoolean[T](l: List[T]): Boolean = error("source compatibility only, should never be called") + implicit def listToInt[T](l: List[T]): Int = error("source compatibility only, should never be called") + compiler.logUnreportedWarnings(run.deprecationWarnings) + compiler.logUnreportedWarnings(run.uncheckedWarnings) + } + object compiler extends CallbackGlobal(command.settings, dreporter) + { + object dummy // temporary fix for #4426 + object sbtAnalyzer extends + { + val global: compiler.type = compiler + val phaseName = Analyzer.name + val runsAfter = List("jvm") + override val runsBefore = List("terminal") + val runsRightAfter = None + } + with SubComponent + { + val analyzer = new Analyzer(global) + def newPhase(prev: Phase) = analyzer.newPhase(prev) + def name = phaseName + } + object apiExtractor extends + { + val global: compiler.type = compiler + val phaseName = API.name + val runsAfter = List("typer") + override val runsBefore = List("erasure") + val runsRightAfter = Some("typer") + } + with SubComponent + { + val api = new API(global) + def newPhase(prev: Phase) = api.newPhase(prev) + def name = phaseName + } + + val out = new File(settings.outdir.value) + override lazy val phaseDescriptors = + { + phasesSet += sbtAnalyzer + phasesSet += apiExtractor + superComputePhaseDescriptors + } + // Required because computePhaseDescriptors is private in 2.8 (changed to protected sometime later). + private[this] def superComputePhaseDescriptors() = superCall("computePhaseDescriptors").asInstanceOf[List[SubComponent]] + private[this] def superDropRun(): Unit = superCall("dropRun") + private[this] def superCall(methodName: String): AnyRef = + { + val meth = classOf[Global].getDeclaredMethod(methodName) + meth.setAccessible(true) + meth.invoke(this) + } + def logUnreportedWarnings(seq: List[(Position,String)]): Unit = // Scala 2.10.x and later + { + for( (pos, msg) <- seq) yield + callback.problem(reporter.asInstanceOf[DelegatingReporter].convert(pos), msg, Severity.Warn, false) + } + def logUnreportedWarnings(count: Boolean): Unit = () // for source compatibility with Scala 2.8.x + def logUnreportedWarnings(count: Int): Unit = () // for source compatibility with Scala 2.9.x + + def set(callback: AnalysisCallback, dreporter: DelegatingReporter) + { + this.callback0 = callback + reporter = dreporter + } + def clear() + { + callback0 = null + atPhase(currentRun.namerPhase) { forgetAll() } + superDropRun() + reporter = null + } + + override def registerTopLevelSym(sym: Symbol) = toForget += sym + + def findClass(name: String): Option[AbstractFile] = + getOutputClass(name) orElse findOnClassPath(name) + + def getOutputClass(name: String): Option[AbstractFile] = + { + val f = new File(out, name.replace('.', '/') + ".class") + if(f.exists) Some(AbstractFile.getFile(f)) else None + } + + def findOnClassPath(name: String): Option[AbstractFile] = classPath.findClass(name).flatMap(_.binary.asInstanceOf[Option[AbstractFile]]) + + final def unlinkAll(m: Symbol) { + val scope = m.owner.info.decls + scope unlink m + scope unlink m.companionSymbol +// if(scope.isEmpty && m.owner != definitions.EmptyPackageClass && m.owner != definitions.RootClass) +// emptyPackages += m.owner + } + def reloadClass(pkg: Symbol, simpleName: String, bin: AbstractFile) + { + val loader = new loaders.ClassfileLoader(bin) + toForget += loaders.enterClass(pkg, simpleName, loader) + toForget += loaders.enterModule(pkg, simpleName, loader) + } + + def forgetAll() + { + for(sym <- toForget) { + unlinkAll(sym) + toReload.put(sym.fullName, (sym.owner, sym.name.toString)) + } + toForget = mutable.Set() + } + + // fine-control over external changes is unimplemented: + // must drop whole CachedCompiler when !changes.isEmpty + def reload(changes: DependencyChanges) + { + for { + (fullName,(pkg,simpleName)) <- toReload + classFile <- getOutputClass(fullName) + } + reloadClass(pkg, simpleName, classFile) + + toReload = newReloadMap() + } + + private [this] def newReloadMap() = mutable.Map[String,(Symbol,String)]() + private[this] var emptyPackages = mutable.Set[Symbol]() + private[this] var toReload = newReloadMap() + private[this] var toForget = mutable.Set[Symbol]() + private[this] var callback0: AnalysisCallback = null + def callback: AnalysisCallback = callback0 + } +} \ No newline at end of file diff --git a/compile/interface/DelegatingReporter.scala b/compile/interface/DelegatingReporter.scala index 9dcb1520d..67f25873b 100644 --- a/compile/interface/DelegatingReporter.scala +++ b/compile/interface/DelegatingReporter.scala @@ -15,10 +15,11 @@ private object DelegatingReporter // The following code is based on scala.tools.nsc.reporters.{AbstractReporter, ConsoleReporter} // Copyright 2002-2009 LAMP/EPFL // Original author: Martin Odersky -private final class DelegatingReporter(warnFatal: Boolean, delegate: xsbti.Reporter) extends scala.tools.nsc.reporters.Reporter +private final class DelegatingReporter(warnFatal: Boolean, private[this] var delegate: xsbti.Reporter) extends scala.tools.nsc.reporters.Reporter { import scala.tools.nsc.util.{FakePos,NoPosition,Position} + def dropDelegate() { delegate = null } def error(msg: String) { error(FakePos("scalac"), msg) } def printSummary() = delegate.printSummary() diff --git a/interface/src/main/java/xsbti/compile/CachedCompiler.java b/interface/src/main/java/xsbti/compile/CachedCompiler.java new file mode 100644 index 000000000..2f97e395b --- /dev/null +++ b/interface/src/main/java/xsbti/compile/CachedCompiler.java @@ -0,0 +1,11 @@ +package xsbti.compile; + +import xsbti.AnalysisCallback; +import xsbti.Logger; +import xsbti.Reporter; +import java.io.File; + +public interface CachedCompiler +{ + public void run(File[] sources, DependencyChanges cpChanges, AnalysisCallback callback, Logger log, Reporter delegate); +} diff --git a/interface/src/main/java/xsbti/compile/CachedCompilerProvider.java b/interface/src/main/java/xsbti/compile/CachedCompilerProvider.java new file mode 100644 index 000000000..43d3aaf7e --- /dev/null +++ b/interface/src/main/java/xsbti/compile/CachedCompilerProvider.java @@ -0,0 +1,10 @@ +package xsbti.compile; + +import xsbti.Logger; +import xsbti.Reporter; + +public interface CachedCompilerProvider +{ + ScalaInstance scalaInstance(); + CachedCompiler newCachedCompiler(String[] arguments, Logger log, Reporter reporter); +} \ No newline at end of file diff --git a/interface/src/main/java/xsbti/compile/DependencyChanges.java b/interface/src/main/java/xsbti/compile/DependencyChanges.java new file mode 100644 index 000000000..4f6bda55a --- /dev/null +++ b/interface/src/main/java/xsbti/compile/DependencyChanges.java @@ -0,0 +1,13 @@ +package xsbti.compile; + + import java.io.File; + +// only includes changes to dependencies outside of the project +public interface DependencyChanges +{ + boolean isEmpty(); + // class files or jar files + File[] modifiedBinaries(); + // class names + String[] modifiedClasses(); +} \ No newline at end of file diff --git a/interface/src/main/java/xsbti/compile/GlobalsCache.java b/interface/src/main/java/xsbti/compile/GlobalsCache.java new file mode 100644 index 000000000..1e070a1f1 --- /dev/null +++ b/interface/src/main/java/xsbti/compile/GlobalsCache.java @@ -0,0 +1,10 @@ +package xsbti.compile; + +import xsbti.Logger; +import xsbti.Reporter; + +public interface GlobalsCache +{ + public CachedCompiler apply(String[] args, boolean forceNew, CachedCompilerProvider provider, Logger log, Reporter reporter); + public void clear(); +} diff --git a/interface/src/main/java/xsbti/compile/Setup.java b/interface/src/main/java/xsbti/compile/Setup.java index a1a9a1ad8..cf261aa7a 100644 --- a/interface/src/main/java/xsbti/compile/Setup.java +++ b/interface/src/main/java/xsbti/compile/Setup.java @@ -21,4 +21,6 @@ public interface Setup * This file can be removed to force a full recompilation. * The file should be unique and not shared between compilations. */ File cacheFile(); + + GlobalsCache cache(); } diff --git a/main/Defaults.scala b/main/Defaults.scala index 589da45a1..6b9def2b8 100755 --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -55,6 +55,7 @@ object Defaults extends BuildCommon managedDirectory <<= baseDirectory(_ / "lib_managed") )) def globalCore: Seq[Setting[_]] = inScope(GlobalScope)(defaultTestTasks(test) ++ defaultTestTasks(testOnly) ++ defaultTestTasks(testQuick) ++ Seq( + compilerCache <<= state map { _ get Keys.stateCompilerCache getOrElse compiler.CompilerCache.fresh }, crossVersion :== CrossVersion.Disabled, scalaOrganization :== ScalaArtifacts.Organization, buildDependencies <<= buildDependencies or Classpaths.constructBuildDependencies, @@ -574,8 +575,8 @@ object Defaults extends BuildCommon def compileTask = (compileInputs in compile, streams) map { (i,s) => Compiler(i,s.log) } def compileIncSetupTask = - (dependencyClasspath, cacheDirectory, skip in compile, definesClass) map { (cp, cacheDir, skip, definesC) => - Compiler.IncSetup(analysisMap(cp), definesC, skip, cacheDir / "inc_compile") + (dependencyClasspath, cacheDirectory, skip in compile, definesClass, compilerCache) map { (cp, cacheDir, skip, definesC, cache) => + Compiler.IncSetup(analysisMap(cp), definesC, skip, cacheDir / "inc_compile", cache) } def compileInputsSettings: Seq[Setting[_]] = { val optionsPair = TaskKey.local[(Seq[String], Seq[String])] diff --git a/main/Keys.scala b/main/Keys.scala index 74a6b2a92..96afdb9a1 100644 --- a/main/Keys.scala +++ b/main/Keys.scala @@ -10,7 +10,7 @@ package sbt import inc.Analysis import inc.Locate.DefinesClass import std.TaskExtra._ - import xsbti.compile.CompileOrder + import xsbti.compile.{CompileOrder, GlobalsCache} import scala.xml.{Node => XNode, NodeSeq} import org.apache.ivy.core.module.{descriptor, id} import descriptor.ModuleDescriptor, id.ModuleRevisionId @@ -153,6 +153,8 @@ object Keys val compile = TaskKey[Analysis]("compile", "Compiles sources.", APlusTask) val compilers = TaskKey[Compiler.Compilers]("compilers", "Defines the Scala and Java compilers to use for compilation.", DTask) val compileIncSetup = TaskKey[Compiler.IncSetup]("inc-compile-setup", "Configures aspects of incremental compilation.", DTask) + val compilerCache = TaskKey[GlobalsCache]("compiler-cache", "Cache of scala.tools.nsc.Global instances. This should typically be cached so that it isn't recreated every task run.", DTask) + val stateCompilerCache = AttributeKey[GlobalsCache]("compiler-cache", "Internal use: Global cache.") val definesClass = TaskKey[DefinesClass]("defines-class", "Internal use: provides a function that determines whether the provided file contains a given class.", Invisible) val doc = TaskKey[File]("doc", "Generates API documentation.", AMinusTask) val copyResources = TaskKey[Seq[(File,File)]]("copy-resources", "Copies resources to the output directory.", AMinusTask) diff --git a/main/Main.scala b/main/Main.scala index ef8d20402..598fc7375 100644 --- a/main/Main.scala +++ b/main/Main.scala @@ -4,7 +4,7 @@ package sbt import complete.{DefaultParsers, Parser} - import compiler.EvalImports + import compiler.{CompilerCache,EvalImports} import Types.{const,idFun} import Aggregation.AnyKeys import Project.LoadAction @@ -394,11 +394,27 @@ object BuiltinCommands def loadProjectImpl = Command(LoadProjectImpl)(_ => Project.loadActionParser)( doLoadProject ) def doLoadProject(s0: State, action: LoadAction.Value): State = { - val (s, base) = Project.loadAction(SessionVar.clear(s0), action) + val (s1, base) = Project.loadAction(SessionVar.clear(s0), action) IO.createDirectory(base) + val s = if(s1 has Keys.stateCompilerCache) s1 else registerCompilerCache(s1) val (eval, structure) = Load.defaultLoad(s, base, s.log, Project.inPluginProject(s), Project.extraBuilds(s)) val session = Load.initialSession(structure, eval, s0) SessionSettings.checkSession(session, s) Project.setProject(session, structure, s) } + def registerCompilerCache(s: State): State = + { + val maxCompilers = System.getProperty("sbt.resident.limit") + val cache = + if(maxCompilers == null) + CompilerCache.fresh + else + { + val num = try maxCompilers.toInt catch { + case e: NumberFormatException => throw new RuntimeException("Resident compiler limit must be an integer.", e) + } + if(num <= 0) CompilerCache.fresh else CompilerCache(num) + } + s.put(Keys.stateCompilerCache, cache) + } } diff --git a/main/actions/Compiler.scala b/main/actions/Compiler.scala index e24d63500..89c13cea1 100644 --- a/main/actions/Compiler.scala +++ b/main/actions/Compiler.scala @@ -4,7 +4,7 @@ package sbt import xsbti.{Logger => _,_} - import xsbti.compile.CompileOrder + import xsbti.compile.{CompileOrder,GlobalsCache} import CompileOrder.{JavaThenScala, Mixed, ScalaThenJava} import compiler._ import inc._ @@ -17,7 +17,7 @@ object Compiler final case class Inputs(compilers: Compilers, config: Options, incSetup: IncSetup) final case class Options(classpath: Seq[File], sources: Seq[File], classesDirectory: File, options: Seq[String], javacOptions: Seq[String], maxErrors: Int, order: CompileOrder) - final case class IncSetup(analysisMap: File => Option[Analysis], definesClass: DefinesClass, skip: Boolean, cacheFile: File) + final case class IncSetup(analysisMap: File => Option[Analysis], definesClass: DefinesClass, skip: Boolean, cacheFile: File, cache: GlobalsCache) final case class Compilers(scalac: AnalyzingCompiler, javac: JavaTool) @deprecated("Use the other inputs variant.", "0.12.0") @@ -27,7 +27,7 @@ object Compiler val classesDirectory = outputDirectory / "classes" val cacheFile = outputDirectory / "cache_old_style" val augClasspath = classesDirectory.asFile +: classpath - val incSetup = IncSetup(Map.empty, definesClass, false, cacheFile) + val incSetup = IncSetup(Map.empty, definesClass, false, cacheFile, CompilerCache.fresh) inputs(augClasspath, sources, classesDirectory, options, javacOptions, maxErrors, order)(compilers, incSetup, log) } def inputs(classpath: Seq[File], sources: Seq[File], classesDirectory: File, options: Seq[String], javacOptions: Seq[String], maxErrors: Int, order: CompileOrder)(implicit compilers: Compilers, incSetup: IncSetup, log: Logger): Inputs = @@ -76,6 +76,6 @@ object Compiler import in.incSetup._ val agg = new AggressiveCompile(cacheFile) - agg(scalac, javac, sources, classpath, classesDirectory, options, javacOptions, analysisMap, definesClass, maxErrors, order, skip)(log) + agg(scalac, javac, sources, classpath, classesDirectory, cache, options, javacOptions, analysisMap, definesClass, maxErrors, order, skip)(log) } } diff --git a/sbt/package.scala b/sbt/package.scala index c21ef9715..aac8f986f 100644 --- a/sbt/package.scala +++ b/sbt/package.scala @@ -20,10 +20,13 @@ package object sbt extends sbt.std.TaskExtra with sbt.Types with sbt.ProcessExtr type File = java.io.File type URI = java.net.URI type URL = java.net.URL + + object CompileOrder { + val JavaThenScala = xsbti.compile.CompileOrder.JavaThenScala + val ScalaThenJava = xsbti.compile.CompileOrder.ScalaThenJava + val Mixed = xsbti.compile.CompileOrder.Mixed + } type CompileOrder = xsbti.compile.CompileOrder - val JavaThenScala = xsbti.compile.CompileOrder.JavaThenScala - val ScalaThenJava = xsbti.compile.CompileOrder.ScalaThenJava - val Mixed = xsbti.compile.CompileOrder.Mixed implicit def maybeToOption[S](m: xsbti.Maybe[S]): Option[S] = if(m.isDefined) Some(m.get) else None diff --git a/sbt/src/sbt-test/source-dependencies/added/changes/A1.scala b/sbt/src/sbt-test/source-dependencies/added/changes/A1.scala index 28a0e3398..a58c253c0 100644 --- a/sbt/src/sbt-test/source-dependencies/added/changes/A1.scala +++ b/sbt/src/sbt-test/source-dependencies/added/changes/A1.scala @@ -1,3 +1,5 @@ +package example + object A { val x: Int = 3 diff --git a/sbt/src/sbt-test/source-dependencies/added/changes/A2.scala b/sbt/src/sbt-test/source-dependencies/added/changes/A2.scala index b77a07bf9..48ac8e5b6 100644 --- a/sbt/src/sbt-test/source-dependencies/added/changes/A2.scala +++ b/sbt/src/sbt-test/source-dependencies/added/changes/A2.scala @@ -1,3 +1,5 @@ +package example + object A { val x: Int = B.y diff --git a/sbt/src/sbt-test/source-dependencies/added/changes/A3.scala b/sbt/src/sbt-test/source-dependencies/added/changes/A3.scala index 9c5d648fc..ec450b67e 100644 --- a/sbt/src/sbt-test/source-dependencies/added/changes/A3.scala +++ b/sbt/src/sbt-test/source-dependencies/added/changes/A3.scala @@ -1,3 +1,5 @@ +package example + object A { val x: String = B.y diff --git a/sbt/src/sbt-test/source-dependencies/added/changes/B1.scala b/sbt/src/sbt-test/source-dependencies/added/changes/B1.scala index a4b781aaa..9b5cc4ee9 100644 --- a/sbt/src/sbt-test/source-dependencies/added/changes/B1.scala +++ b/sbt/src/sbt-test/source-dependencies/added/changes/B1.scala @@ -1,3 +1,5 @@ +package example + object B { val y: String = "4" diff --git a/sbt/src/sbt-test/source-dependencies/added/changes/B2.scala b/sbt/src/sbt-test/source-dependencies/added/changes/B2.scala index 484f5581f..fc8c53c9c 100644 --- a/sbt/src/sbt-test/source-dependencies/added/changes/B2.scala +++ b/sbt/src/sbt-test/source-dependencies/added/changes/B2.scala @@ -1,3 +1,5 @@ +package example + object B { val y: Int = 5 diff --git a/sbt/src/sbt-test/source-dependencies/dup-class/changes/A.scala b/sbt/src/sbt-test/source-dependencies/dup-class/changes/A.scala index 69c493db2..264775513 100644 --- a/sbt/src/sbt-test/source-dependencies/dup-class/changes/A.scala +++ b/sbt/src/sbt-test/source-dependencies/dup-class/changes/A.scala @@ -1 +1,3 @@ +package clear + object A diff --git a/sbt/src/sbt-test/source-dependencies/dup-class/changes/A2.scala b/sbt/src/sbt-test/source-dependencies/dup-class/changes/A2.scala index 69c493db2..264775513 100644 --- a/sbt/src/sbt-test/source-dependencies/dup-class/changes/A2.scala +++ b/sbt/src/sbt-test/source-dependencies/dup-class/changes/A2.scala @@ -1 +1,3 @@ +package clear + object A diff --git a/sbt/src/sbt-test/source-dependencies/dup-class/changes/B.scala b/sbt/src/sbt-test/source-dependencies/dup-class/changes/B.scala index 251ef7397..50df7082c 100644 --- a/sbt/src/sbt-test/source-dependencies/dup-class/changes/B.scala +++ b/sbt/src/sbt-test/source-dependencies/dup-class/changes/B.scala @@ -1 +1,3 @@ +package clear + object B