package sbt
package inc
import java.io._
import sbt.{ CompileSetup, Relation }
import xsbti.api.{ Compilation, Source }
import xsbti.compile.{ MultipleOutput, SingleOutput }
import javax.xml.bind.DatatypeConverter
private[inc] object FormatTimer {
private val timers = scala.collection.mutable.Map[String, Long]()
private val printTimings = "true" == System.getProperty("sbt.analysis.debug.timing")
def aggregate[T](key: String)(f: => T) = {
val start = System.nanoTime()
val ret = f
val elapsed = System.nanoTime() - start
timers.update(key, timers.getOrElseUpdate(key, 0) + elapsed)
ret
}
def time[T](key: String)(f: => T) = {
val ret = aggregate(key)(f)
close(key)
ret
}
def close(key: String) {
if (printTimings) {
println("[%s] %dms".format(key, timers.getOrElse(key, 0L) / 1000000))
}
timers.remove(key)
}
}
class ReadException(s: String) extends Exception(s) {
def this(expected: String, found: String) = this("Expected: %s. Found: %s.".format(expected, found))
}
class EOFException extends ReadException("Unexpected EOF.")
object TextAnalysisFormat {
import sbinary.DefaultProtocol.{ immutableMapFormat, immutableSetFormat, StringFormat, tuple2Format }
import AnalysisFormats._
implicit val compilationF = xsbt.api.CompilationFormat
def write(out: Writer, analysis: Analysis, setup: CompileSetup) {
VersionF.write(out)
FormatTimer.time("write setup") { CompileSetupF.write(out, setup) }
FormatTimer.time("write relations") { RelationsF.write(out, analysis.relations) }
FormatTimer.time("write stamps") { StampsF.write(out, analysis.stamps) }
FormatTimer.time("write apis") { APIsF.write(out, analysis.apis) }
FormatTimer.time("write sourceinfos") { SourceInfosF.write(out, analysis.infos) }
FormatTimer.time("write compilations") { CompilationsF.write(out, analysis.compilations) }
out.flush()
}
def read(in: BufferedReader): (Analysis, CompileSetup) = {
VersionF.read(in)
val setup = FormatTimer.time("read setup") { CompileSetupF.read(in) }
val relations = FormatTimer.time("read relations") { RelationsF.read(in, setup.nameHashing) }
val stamps = FormatTimer.time("read stamps") { StampsF.read(in) }
val apis = FormatTimer.time("read apis") { APIsF.read(in) }
val infos = FormatTimer.time("read sourceinfos") { SourceInfosF.read(in) }
val compilations = FormatTimer.time("read compilations") { CompilationsF.read(in) }
(Analysis.Empty.copy(stamps, apis, relations, infos, compilations), setup)
}
private[this] object VersionF {
val currentVersion = "5"
def write(out: Writer) {
out.write("format version: %s\n".format(currentVersion))
}
private val versionPattern = """format version: (\w+)""".r
def read(in: BufferedReader) {
in.readLine() match {
case versionPattern(version) => validateVersion(version)
case s: String => throw new ReadException("\"format version: <version>\"", s)
case null => throw new EOFException
}
}
def validateVersion(version: String) {
if (version != currentVersion) {
throw new ReadException("File uses format version %s, but we are compatible with version %s only.".format(version, currentVersion))
}
}
}
private[this] object RelationsF {
object {
val = "products"
val = "binary dependencies"
val = "direct source dependencies"
val = "direct external dependencies"
val = "public inherited source dependencies"
val = "public inherited external dependencies"
val = "class names"
val = "member reference internal dependencies"
val = "member reference external dependencies"
val = "inheritance internal dependencies"
val = "inheritance external dependencies"
val = "used names"
}
def write(out: Writer, relations: Relations) {
def writeRelation[T](: String, rel: Relation[File, T])(implicit ord: Ordering[T]) {
writeHeader(out, header)
writeSize(out, rel.size)
rel.forwardMap.toSeq.sortBy(_._1).foreach {
case (k, vs) =>
val kStr = k.toString
vs.toSeq.sorted foreach { v =>
out.write(kStr); out.write(" -> "); out.write(v.toString); out.write("\n")
}
}
}
val nameHashing = relations.nameHashing
writeRelation(Headers.srcProd, relations.srcProd)
writeRelation(Headers.binaryDep, relations.binaryDep)
val direct = if (nameHashing) Relations.emptySource else relations.direct
val publicInherited = if (nameHashing)
Relations.emptySource else relations.publicInherited
val memberRef = if (nameHashing)
relations.memberRef else Relations.emptySourceDependencies
val inheritance = if (nameHashing)
relations.inheritance else Relations.emptySourceDependencies
val names = if (nameHashing) relations.names else Relation.empty[File, String]
writeRelation(Headers.directSrcDep, direct.internal)
writeRelation(Headers.directExternalDep, direct.external)
writeRelation(Headers.internalSrcDepPI, publicInherited.internal)
writeRelation(Headers.externalDepPI, publicInherited.external)
writeRelation(Headers.memberRefInternalDep, memberRef.internal)
writeRelation(Headers.memberRefExternalDep, memberRef.external)
writeRelation(Headers.inheritanceInternalDep, inheritance.internal)
writeRelation(Headers.inheritanceExternalDep, inheritance.external)
writeRelation(Headers.classes, relations.classes)
writeRelation(Headers.usedNames, names)
}
def read(in: BufferedReader, nameHashing: Boolean): Relations = {
def readRelation[T](: String, s2t: String => T): Relation[File, T] = {
val items = readPairs(in)(expectedHeader, new File(_), s2t).toIterator
var forward: List[(File, Set[T])] = Nil
var currentItem: (File, T) = null
var currentFile: File = null
var currentVals: List[T] = Nil
def closeEntry() {
if (currentFile != null) forward = (currentFile, currentVals.toSet) :: forward
currentFile = currentItem._1
currentVals = currentItem._2 :: Nil
}
while (items.hasNext) {
currentItem = items.next()
if (currentItem._1 == currentFile) currentVals = currentItem._2 :: currentVals else closeEntry()
}
if (currentItem != null) closeEntry()
Relation.reconstruct(forward.toMap)
}
def readFileRelation(: String) = readRelation(expectedHeader, { new File(_) })
def readStringRelation(: String) = readRelation(expectedHeader, identity[String])
val srcProd = readFileRelation(Headers.srcProd)
val binaryDep = readFileRelation(Headers.binaryDep)
import sbt.inc.Relations.{
Source,
SourceDependencies,
makeSourceDependencies,
emptySource,
makeSource,
emptySourceDependencies
}
val directSrcDeps: Source = {
val internalSrcDep = readFileRelation(Headers.directSrcDep)
val externalDep = readStringRelation(Headers.directExternalDep)
makeSource(internalSrcDep, externalDep)
}
val publicInheritedSrcDeps: Source = {
val internalSrcDepPI = readFileRelation(Headers.internalSrcDepPI)
val externalDepPI = readStringRelation(Headers.externalDepPI)
makeSource(internalSrcDepPI, externalDepPI)
}
val memberRefSrcDeps: SourceDependencies = {
val internalMemberRefDep = readFileRelation(Headers.memberRefInternalDep)
val externalMemberRefDep = readStringRelation(Headers.memberRefExternalDep)
makeSourceDependencies(internalMemberRefDep, externalMemberRefDep)
}
val inheritanceSrcDeps: SourceDependencies = {
val internalInheritanceDep = readFileRelation(Headers.inheritanceInternalDep)
val externalInheritanceDep = readStringRelation(Headers.inheritanceExternalDep)
makeSourceDependencies(internalInheritanceDep, externalInheritanceDep)
}
assert(nameHashing || (memberRefSrcDeps == emptySourceDependencies),
"When name hashing is disabled the `memberRef` relation should be empty.")
assert(!nameHashing || (directSrcDeps == emptySource),
"When name hashing is enabled the `direct` relation should be empty.")
val classes = readStringRelation(Headers.classes)
val names = readStringRelation(Headers.usedNames)
if (nameHashing)
Relations.make(srcProd, binaryDep, memberRefSrcDeps, inheritanceSrcDeps, classes, names)
else {
assert(names.all.isEmpty, "When `nameHashing` is disabled `names` relation " +
s"should be empty: $names")
Relations.make(srcProd, binaryDep, directSrcDeps, publicInheritedSrcDeps, classes)
}
}
}
private[this] object StampsF {
object {
val = "product stamps"
val = "source stamps"
val = "binary stamps"
val = "class names"
}
def write(out: Writer, stamps: Stamps) {
def doWriteMap[V](: String, m: Map[File, V]) = writeMap(out)(header, m, { v: V => v.toString })
doWriteMap(Headers.products, stamps.products)
doWriteMap(Headers.sources, stamps.sources)
doWriteMap(Headers.binaries, stamps.binaries)
doWriteMap(Headers.classNames, stamps.classNames)
}
def read(in: BufferedReader): Stamps = {
def doReadMap[V](: String, s2v: String => V) = readMap(in)(expectedHeader, new File(_), s2v)
val products = doReadMap(Headers.products, Stamp.fromString)
val sources = doReadMap(Headers.sources, Stamp.fromString)
val binaries = doReadMap(Headers.binaries, Stamp.fromString)
val classNames = doReadMap(Headers.classNames, identity[String])
Stamps(products, sources, binaries, classNames)
}
}
private[this] object APIsF {
object {
val = "internal apis"
val = "external apis"
}
val stringToSource = ObjectStringifier.stringToObj[Source] _
val sourceToString = ObjectStringifier.objToString[Source] _
def write(out: Writer, apis: APIs) {
writeMap(out)(Headers.internal, apis.internal, sourceToString, inlineVals = false)
writeMap(out)(Headers.external, apis.external, sourceToString, inlineVals = false)
FormatTimer.close("bytes -> base64")
FormatTimer.close("byte copy")
FormatTimer.close("sbinary write")
}
def read(in: BufferedReader): APIs = {
val internal = readMap(in)(Headers.internal, new File(_), stringToSource)
val external = readMap(in)(Headers.external, identity[String], stringToSource)
FormatTimer.close("base64 -> bytes")
FormatTimer.close("sbinary read")
APIs(internal, external)
}
}
private[this] object SourceInfosF {
object {
val = "source infos"
}
val stringToSourceInfo = ObjectStringifier.stringToObj[SourceInfo] _
val sourceInfoToString = ObjectStringifier.objToString[SourceInfo] _
def write(out: Writer, infos: SourceInfos) { writeMap(out)(Headers.infos, infos.allInfos, sourceInfoToString, inlineVals = false) }
def read(in: BufferedReader): SourceInfos = SourceInfos.make(readMap(in)(Headers.infos, new File(_), stringToSourceInfo))
}
private[this] object CompilationsF {
object {
val = "compilations"
}
val stringToCompilation = ObjectStringifier.stringToObj[Compilation] _
val compilationToString = ObjectStringifier.objToString[Compilation] _
def write(out: Writer, compilations: Compilations) {
writeSeq(out)(Headers.compilations, compilations.allCompilations, compilationToString)
}
def read(in: BufferedReader): Compilations = Compilations.make(
readSeq[Compilation](in)(Headers.compilations, stringToCompilation))
}
private[this] object CompileSetupF {
object {
val = "output mode"
val = "output directories"
val = "compile options"
val = "javac options"
val = "compiler version"
val = "compile order"
val = "name hashing"
}
private[this] val singleOutputMode = "single"
private[this] val multipleOutputMode = "multiple"
private[this] val singleOutputKey = new File("output dir")
def write(out: Writer, setup: CompileSetup) {
val (mode, outputAsMap) = setup.output match {
case s: SingleOutput => (singleOutputMode, Map(singleOutputKey -> s.outputDirectory))
case m: MultipleOutput => (multipleOutputMode, m.outputGroups.map(x => x.sourceDirectory -> x.outputDirectory).toMap)
}
writeSeq(out)(Headers.outputMode, mode :: Nil, identity[String])
writeMap(out)(Headers.outputDir, outputAsMap, { f: File => f.getPath })
writeSeq(out)(Headers.compileOptions, setup.options.options, identity[String])
writeSeq(out)(Headers.javacOptions, setup.options.javacOptions, identity[String])
writeSeq(out)(Headers.compilerVersion, setup.compilerVersion :: Nil, identity[String])
writeSeq(out)(Headers.compileOrder, setup.order.name :: Nil, identity[String])
writeSeq(out)(Headers.nameHashing, setup.nameHashing :: Nil, (b: Boolean) => b.toString)
}
def read(in: BufferedReader): CompileSetup = {
def s2f(s: String) = new File(s)
def s2b(s: String): Boolean = s.toBoolean
val outputDirMode = readSeq(in)(Headers.outputMode, identity[String]).headOption
val outputAsMap = readMap(in)(Headers.outputDir, s2f, s2f)
val compileOptions = readSeq(in)(Headers.compileOptions, identity[String])
val javacOptions = readSeq(in)(Headers.javacOptions, identity[String])
val compilerVersion = readSeq(in)(Headers.compilerVersion, identity[String]).head
val compileOrder = readSeq(in)(Headers.compileOrder, identity[String]).head
val nameHashing = readSeq(in)(Headers.nameHashing, s2b).head
val output = outputDirMode match {
case Some(s) => s match {
case `singleOutputMode` => new SingleOutput {
val outputDirectory = outputAsMap(singleOutputKey)
}
case `multipleOutputMode` => new MultipleOutput {
val outputGroups: Array[MultipleOutput.OutputGroup] = outputAsMap.toArray.map {
case (src: File, out: File) => new MultipleOutput.OutputGroup {
val sourceDirectory = src
val outputDirectory = out
}
}
}
case str: String => throw new ReadException("Unrecognized output mode: " + str)
}
case None => throw new ReadException("No output mode specified")
}
new CompileSetup(output, new CompileOptions(compileOptions, javacOptions), compilerVersion,
xsbti.compile.CompileOrder.valueOf(compileOrder), nameHashing)
}
}
private[this] object ObjectStringifier {
def objToString[T](o: T)(implicit fmt: sbinary.Format[T]) = {
val baos = new ByteArrayOutputStream()
val out = new sbinary.JavaOutput(baos)
FormatTimer.aggregate("sbinary write") { try { fmt.writes(out, o) } finally { baos.close() } }
val bytes = FormatTimer.aggregate("byte copy") { baos.toByteArray }
FormatTimer.aggregate("bytes -> base64") { DatatypeConverter.printBase64Binary(bytes) }
}
def stringToObj[T](s: String)(implicit fmt: sbinary.Format[T]) = {
val bytes = FormatTimer.aggregate("base64 -> bytes") { DatatypeConverter.parseBase64Binary(s) }
val in = new sbinary.JavaInput(new ByteArrayInputStream(bytes))
FormatTimer.aggregate("sbinary read") { fmt.reads(in) }
}
}
private[this] def (: Writer, : String) {
out.write(header + ":\n")
}
private[this] def (: BufferedReader, : String) {
val = in.readLine()
if (header != expectedHeader + ":") throw new ReadException(expectedHeader, if (header == null) "EOF" else header)
}
private[this] def writeSize(out: Writer, n: Int) {
out.write("%d items\n".format(n))
}
private val itemsPattern = """(\d+) items""".r
private[this] def readSize(in: BufferedReader): Int = {
in.readLine() match {
case itemsPattern(nStr) => Integer.parseInt(nStr)
case s: String => throw new ReadException("\"<n> items\"", s)
case null => throw new EOFException
}
}
private[this] def writeSeq[T](out: Writer)(: String, s: Seq[T], t2s: T => String) {
def n = s.length
val numDigits = if (n < 2) 1 else math.log10(n - 1).toInt + 1
val fmtStr = "%%0%dd".format(numDigits)
val m: Map[String, T] = s.zipWithIndex.map(x => fmtStr.format(x._2) -> x._1).toMap
writeMap(out)(header, m, t2s)
}
private[this] def readSeq[T](in: BufferedReader)(: String, s2t: String => T): Seq[T] =
(readPairs(in)(expectedHeader, identity[String], s2t).toSeq.sortBy(_._1) map (_._2))
private[this] def writeMap[K, V](out: Writer)(: String, m: Map[K, V], v2s: V => String, inlineVals: Boolean = true)(implicit ord: Ordering[K]) {
writeHeader(out, header)
writeSize(out, m.size)
m.keys.toSeq.sorted foreach { k =>
out.write(k.toString)
out.write(" -> ")
if (!inlineVals) out.write("\n")
out.write(v2s(m(k)))
out.write("\n")
}
}
private[this] def readPairs[K, V](in: BufferedReader)(: String, s2k: String => K, s2v: String => V): Traversable[(K, V)] = {
def toPair(s: String): (K, V) = {
if (s == null) throw new EOFException
val p = s.indexOf(" -> ")
val k = s2k(s.substring(0, p))
val v = s2v(if (p == s.length - 4) in.readLine() else s.substring(p + 4))
(k, v)
}
expectHeader(in, expectedHeader)
val n = readSize(in)
for (i <- 0 until n) yield toPair(in.readLine())
}
private[this] def readMap[K, V](in: BufferedReader)(: String, s2k: String => K, s2v: String => V): Map[K, V] = {
readPairs(in)(expectedHeader, s2k, s2v).toMap
}
}