diff options
author | David MacIver <david.maciver@gmail.com> | 2009-02-15 17:00:16 +0000 |
---|---|---|
committer | David MacIver <david.maciver@gmail.com> | 2009-02-15 17:00:16 +0000 |
commit | dd368937571ef308989e1388b08e04f55b0b6cd4 (patch) | |
tree | 234564d9c14ff4489e131602540ce444c145f059 /src | |
parent | 266df9f05ecd11e67166e57cc58236646f2f50bb (diff) | |
download | scala-dd368937571ef308989e1388b08e04f55b0b6cd4.tar.gz scala-dd368937571ef308989e1388b08e04f55b0b6cd4.tar.bz2 scala-dd368937571ef308989e1388b08e04f55b0b6cd4.zip |
Added -make flag and supporting functionality f...
Added -make flag and supporting functionality for recompilation of only
changed files and their dependencies.
An email to scala-internals will follow shortly to explain how this
works.
Diffstat (limited to 'src')
-rw-r--r-- | src/compiler/scala/tools/nsc/Global.scala | 17 | ||||
-rw-r--r-- | src/compiler/scala/tools/nsc/Settings.scala | 2 | ||||
-rw-r--r-- | src/compiler/scala/tools/nsc/dependencies/DependencyAnalysis.scala | 91 | ||||
-rw-r--r-- | src/compiler/scala/tools/nsc/dependencies/Files.scala | 205 |
4 files changed, 314 insertions, 1 deletions
diff --git a/src/compiler/scala/tools/nsc/Global.scala b/src/compiler/scala/tools/nsc/Global.scala index 2a4e024a21..bb17780dab 100644 --- a/src/compiler/scala/tools/nsc/Global.scala +++ b/src/compiler/scala/tools/nsc/Global.scala @@ -18,6 +18,7 @@ import scala.collection.mutable.{HashSet, HashMap, ListBuffer} import symtab._ import symtab.classfile.{PickleBuffer, Pickler} +import dependencies.DependencyAnalysis import util.Statistics import plugins.Plugins import ast._ @@ -442,6 +443,12 @@ class Global(var settings: Settings, var reporter: Reporter) extends SymbolTable val runsRightAfter = None } with GenJVM + object dependencyAnalysis extends { + val global: Global.this.type = Global.this + val runsAfter = List("jvm") + val runsRightAfter = None + } with DependencyAnalysis + // phaseName = "msil" object genMSIL extends { val global: Global.this.type = Global.this @@ -519,6 +526,12 @@ class Global(var settings: Settings, var reporter: Reporter) extends SymbolTable if (forJVM) { phasesSet += liftcode // generate reified trees phasesSet += genJVM // generate .class files + if (!dependencyAnalysis.off){ + if(settings.debug.value){ + println("Adding dependency analysis phase"); + } + phasesSet += dependencyAnalysis + } } if (forMSIL) { phasesSet += genMSIL // generate .msil files @@ -661,7 +674,7 @@ class Global(var settings: Settings, var reporter: Reporter) extends SymbolTable else false def compileSources(_sources: List[SourceFile]) { - val sources = _sources.removeDuplicates // bug #1268, scalac confused by duplicated filenames + val sources = dependencyAnalysis.filter(_sources.removeDuplicates) // bug #1268, scalac confused by duplicated filenames if (reporter.hasErrors) return // there is a problem already, e.g. a // plugin was passed a bad option @@ -721,6 +734,8 @@ class Global(var settings: Settings, var reporter: Reporter) extends SymbolTable } for ((sym, file) <- symSource.elements) resetPackageClass(sym.owner) informTime("total", startTime) + + dependencyAnalysis.writeToFile(); } def compileLate(file: AbstractFile) { diff --git a/src/compiler/scala/tools/nsc/Settings.scala b/src/compiler/scala/tools/nsc/Settings.scala index 3b2a8c501d..479e0e81f0 100644 --- a/src/compiler/scala/tools/nsc/Settings.scala +++ b/src/compiler/scala/tools/nsc/Settings.scala @@ -96,6 +96,8 @@ class Settings(error: String => Unit) { val explaintypes = BooleanSetting ("-explaintypes", "Explain type errors in more detail").hideToIDE val uniqid = BooleanSetting ("-uniqid", "Print identifiers with unique names for debugging").hideToIDE val version = BooleanSetting ("-version", "Print product version and exit").hideToIDE + val dependenciesFile = StringSetting ("-dependencyfile", "file", "Specify the file in which dependencies are tracked", ".scala_dependencies") + val make = ChoiceSetting ("-make", "Specify the behaviour for selecting which files need to be recompiled", List("all", "changed", "immediate", "transitive"), "all") val help = BooleanSetting ("-help", "Print a synopsis of standard options").hideToIDE val Xhelp = BooleanSetting ("-X", "Print a synopsis of advanced options").hideToIDE val argfiles = BooleanSetting ("@<file>", "A text file containing compiler arguments (options and source files)") // only for the help message diff --git a/src/compiler/scala/tools/nsc/dependencies/DependencyAnalysis.scala b/src/compiler/scala/tools/nsc/dependencies/DependencyAnalysis.scala new file mode 100644 index 0000000000..5004086c1a --- /dev/null +++ b/src/compiler/scala/tools/nsc/dependencies/DependencyAnalysis.scala @@ -0,0 +1,91 @@ +package scala.tools.nsc.dependencies; +import util.SourceFile; + +trait DependencyAnalysis extends SubComponent with Files{ + import global._ + + val phaseName = "dependencyAnalysis"; + + def off = settings.make.value == "all" + + def newPhase(prev : Phase) = new AnalysisPhase(prev) + + lazy val maxDepth = settings.make.value match { + case "changed" => 0 + case "transitive" => Int.MaxValue + case "immediate" => 1 + } + + def nameToFile(name : Any) = + settings.outdir.value / (name.toString.replace(".", java.io.File.separator) + ".class") + + lazy val dependenciesFile : Option[File] = settings.dependenciesFile.value match { + case "none" => None + case x => Some(toFile(x)) + } + + def classpath = settings.classpath.value + def newDeps = new FileDependencies(classpath); + + lazy val dependencies = + dependenciesFile match { + case Some(f) if f.exists => { + val fd = FileDependencies.readFrom(f); + if (fd.classpath != classpath) { + if(settings.debug.value){ + println("Classpath has changed. Nuking dependencies"); + } + newDeps + } + else fd + } + case _ => newDeps; + } + + def writeToFile() = if(!off){ + dependenciesFile.foreach(dependencies.writeTo(_)); + } + + def filter(files : List[SourceFile]) : List[SourceFile] = + if (off) files + else if (dependencies.isEmpty){ + if(settings.debug.value){ + println("No known dependencies. Compiling everything"); + } + files + } + else { + val (direct, indirect) = dependencies.invalidatedFiles(maxDepth); + val filtered = files.filter(x => { + val f = x.path.absolute; + direct(f) || indirect(f) || !dependencies.containsFile(f); + }) + filtered match { + case Nil => println("No changes to recompile"); + case x => println("Recompiling " + ( + if(settings.debug.value) x.mkString(", ") + else x.length + " files") + ) + } + filtered + } + + class AnalysisPhase(prev : Phase) extends StdPhase(prev){ + def apply(unit : global.CompilationUnit) { + val f = unit.source.file.file; + // When we're passed strings by the interpreter + // they have no source file. We simply ignore this case + // as irrelevant to dependency analysis. + if (f != null){ + val source : File = unit.source.file.file; + for (d <- unit.icode){ + dependencies.emits(source, nameToFile(d)) + } + + for (d <- unit.depends; if (d.sourceFile != null)){ + dependencies.depends(source, d.sourceFile.file); + } + } + } + } +} diff --git a/src/compiler/scala/tools/nsc/dependencies/Files.scala b/src/compiler/scala/tools/nsc/dependencies/Files.scala new file mode 100644 index 0000000000..96e9e0f4be --- /dev/null +++ b/src/compiler/scala/tools/nsc/dependencies/Files.scala @@ -0,0 +1,205 @@ +package scala.tools.nsc.dependencies; + +import java.io.{File => JFile, _} + +import scala.collection.mutable._; + +trait Files{ + + implicit def toFile(name : String) : File = toFile(new JFile(name)); + implicit def toFile(jf : JFile) : File = new File(jf); + + class FileDependencies(val classpath : String){ + class Tracker extends OpenHashMap[File, Set[File]]{ + override def default(key : File) = { + this(key) = new HashSet[File]; + this(key); + } + } + + val dependencies = new Tracker + val targets = new Tracker; + + def isEmpty = dependencies.isEmpty && targets.isEmpty + + def emits(source : File, result : File) = targets(source.absolute) += result.absolute; + def depends(from : File, on : File) = dependencies(from.absolute) += on.absolute; + + def reset(file : File) = dependencies -= file; + + def cleanEmpty() = { + dependencies.foreach({case (key, value) => value.retain(_.exists)}) + dependencies.retain((key, value) => key.exists && !value.isEmpty) + } + + def containsFile(f : File) = targets.contains(f.absolute) + + def invalidatedFiles(maxDepth : Int) = { + val direct = new HashSet[File]; + + for ((file, products) <- targets) { + // This looks a bit odd. It may seem like one should invalidate a file + // if *any* of its dependencies are older than it. The forall is there + // to deal with the fact that a) Some results might have been orphaned + // and b) Some files might not need changing. + direct(file) ||= products.forall(d => d.lastModified < file.lastModified) + } + + + val seen = new HashSet[File]; + val indirect = new HashSet[File]; + val newInvalidations = new HashSet[File]; + + def invalid(file : File) = indirect(file) || direct(file); + + def go(i : Int) : Unit = if(i > 0){ + newInvalidations.clear; + for((target, depends) <- dependencies; + if !invalid(target); + d <- depends){ + newInvalidations(target) ||= invalid(d) + } + indirect ++= newInvalidations; + if(!newInvalidations.isEmpty) go(i - 1); + else () + } + + go(maxDepth) + + indirect --= direct + + for ((source, targets) <- targets; if (invalid(source))){ + targets.foreach(_.rm); + targets -= source; + } + + (direct, indirect); + } + + def writeTo(file : File) : Unit = file.writeTo(out => writeTo(new PrintStream(out))); + def writeTo(print : PrintStream) : Unit = { + cleanEmpty(); + def emit(tracker : Tracker){ + for ((f, ds) <- tracker; + d <- ds){ + print.println(f + " -> " + d); + } + } + + print.println(classpath); + print.println(FileDependencies.Separator) + emit(dependencies); + print.println(FileDependencies.Separator) + emit(targets); + + } + } + + object FileDependencies{ + val Separator = "-------"; + + def readFrom(file : File) = file.readFrom(in => { + val reader = new BufferedReader(new InputStreamReader(in)); + val it = new FileDependencies(reader.readLine); + reader.readLine; + var line : String = null; + while ({line = reader.readLine; (line != null) && (line != Separator)}){ + line.split(" -> ") match { + case Array(from, on) => it.depends(from, on); + case x => error("Parse error: Unrecognised string " + line); + }; + } + + while ({line = reader.readLine; (line != null) && (line != Separator)}){ + line.split(" -> ") match { + case Array(source, target) => it.emits(source, target); + case x => error("Parse error: Unrecognised string " + line); + }; + } + + it; + }) + } + + + def currentDirectory = new File(new JFile(".")) + + case class File private[Files](val underlying : JFile){ + if (underlying == null) throw new NullPointerException(); + + def absolute : File = underlying.getAbsoluteFile; + def canonical : File = underlying.getCanonicalFile + + def assertDirectory = + if (exists && !isDirectory) error(this + " is not a directory") + else this; + + def assertExists = + if (!exists) error(this + " does not exist") + else this; + + def lastModified = underlying.lastModified + + def list : Iterable[File] = + assertExists.assertDirectory.underlying.listFiles.projection.map(toFile) + + def / (file : File) : File = + new JFile(assertDirectory.toString, + file.toString) + + override def toString = { + val it = underlying.getPath; + if (it.length == 0) "." + else it + } + + def exists = underlying.exists + def isDirectory = underlying.isDirectory; + + def parent : File = { + val x = underlying.getParentFile; + if (x == null) currentDirectory + else x + } + + def create : Boolean = {parent.mkdir; underlying.createNewFile } + def mkdir : Boolean = + if (exists) { assertDirectory; false } + else {parent.mkdir; underlying.mkdir; } + + def rm : Boolean = { + if (isDirectory) list.foreach(_.rm); + underlying.delete; + } + + def descendants : Iterable[File] = + list.flatMap(x => if (x.isDirectory) x.descendants else List(x)) + + def extension = { + val name = toString; + val i = name.lastIndexOf('.'); + if (i == -1) "" else name.substring(i + 1) + } + + def writeTo[T](f : OutputStream => T) : T = { + val out = new FileOutputStream(underlying); + try { + f(out); + } finally { + out.close; + } + } + + def readFrom[T](f : InputStream => T) : T = { + val in = new FileInputStream(underlying); + try{ + f(in); + } finally { + in.close; + } + } + } + +} + +object Files extends Files; |