Create a new API for calling Java toolchains.

* Create a new sbt.compiler.javac package
* Create new interfaces to control running `javac` and `javadoc` whether forked or local.
* Ensure new interfaces make use of `xsbti.Reporter`.
* Create new method on `xsbti.compiler.JavaCompiler` which takes a `xsbti.Reporter`
* Create a new mechanism to parse (more accurately) Warnings + Errors, to distinguish the two.
* Ensure older xsbti.Compiler implementations still succeed via catcing NoSuchMethodError.
* Feed new toolchain through sbt.actions.Compiler API via dirty hackery until we can break things in sbt 1.0
* Added a set of unit tests for parsing errors from Javac/Javadoc
* Added a new integration test for hidden compilerReporter key, including testing threading of javac reports.

Fixes #875, Fixes #1542,  Related #1178 could be looked into/cleaned up.
This commit is contained in:
Josh Suereth 2014-10-28 16:44:23 -04:00
parent e72fbff20a
commit 70cdce0830
21 changed files with 1019 additions and 9 deletions

View File

@ -120,7 +120,12 @@ class AggressiveCompile(cacheFile: File) {
val loader = ClasspathUtilities.toLoader(searchClasspath)
timed("Java compilation", log) {
javac.compile(javaSrcs.toArray, absClasspath.toArray, output, options.javacOptions.toArray, log)
try javac.compileWithReporter(javaSrcs.toArray, absClasspath.toArray, output, options.javacOptions.toArray, reporter, log)
catch {
// Handle older APIs
case _: NoSuchMethodError =>
javac.compile(javaSrcs.toArray, absClasspath.toArray, output, options.javacOptions.toArray, log)
}
}
def readAPI(source: File, classes: Seq[Class[_]]): Set[String] = {

View File

@ -81,6 +81,12 @@ private final class DelegatingReporter(warnFatal: Boolean, noWarn: Boolean, priv
val sourceFile = o2m(sourceFile0)
val pointer = o2mi(pointer0)
val pointerSpace = o2m(pointerSpace0)
override def toString =
(sourcePath0, line0) match {
case (Some(s), Some(l)) => s + ":" + l
case (Some(s), _) => s + ":"
case _ => ""
}
}
import xsbti.Severity.{ Info, Warn, Error }

View File

@ -6,11 +6,26 @@ package compiler
import java.io.{ File, PrintWriter }
import xsbti.{ Severity, Reporter }
import xsbti.compile.Output
@deprecated("0.13.8", "Please use the new set of compilers in sbt.compilers.javac")
abstract class JavacContract(val name: String, val clazz: String) {
def exec(args: Array[String], writer: PrintWriter): Int
}
/** An interface we use to call the Java compiler. */
@deprecated("0.13.8", "Please use the new set of compilers in sbt.compilers.javac")
trait JavaCompiler extends xsbti.compile.JavaCompiler {
def apply(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String])(implicit log: Logger)
/**
* Runs the java compiler
*
* @param sources The source files to compile
* @param classpath The classpath for the compiler
* @param outputDirectory The output directory for class files
* @param options The arguments to pass into Javac
* @param log A log in which we write all the output from Javac.
*/
def apply(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String])(implicit log: Logger): Unit
def compile(sources: Array[File], classpath: Array[File], output: xsbti.compile.Output, options: Array[String], log: xsbti.Logger): Unit = {
val outputDirectory = output match {
@ -20,13 +35,20 @@ trait JavaCompiler extends xsbti.compile.JavaCompiler {
apply(sources, classpath, outputDirectory, options)(log)
}
// TODO - Fix this so that the reporter is actually used.
def compileWithReporter(sources: Array[File], classpath: Array[File], output: Output, options: Array[String], reporter: Reporter, log: xsbti.Logger): Unit = {
compile(sources, classpath, output, options, log)
}
def onArgs(f: Seq[String] => Unit): JavaCompiler
}
@deprecated("0.13.8", "Please use the new set of compilers in sbt.compilers.javac")
trait Javadoc {
def doc(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], maximumErrors: Int, log: Logger)
def onArgs(f: Seq[String] => Unit): Javadoc
}
@deprecated("0.13.8", "Please use the new set of compilers in sbt.compilers.javac")
trait JavaTool extends Javadoc with JavaCompiler {
def apply(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String])(implicit log: Logger) =
compile(JavaCompiler.javac, sources, classpath, outputDirectory, options)(log)
@ -38,7 +60,9 @@ trait JavaTool extends Javadoc with JavaCompiler {
def onArgs(f: Seq[String] => Unit): JavaTool
}
@deprecated("0.13.8", "Please use the new set of compilers in sbt.compilers.javac")
object JavaCompiler {
@deprecated("0.13.8", "Please use the new set of compilers in sbt.compilers.javac")
type Fork = (JavacContract, Seq[String], Logger) => Int
val javac = new JavacContract("javac", "com.sun.tools.javac.Main") {
@ -56,6 +80,7 @@ object JavaCompiler {
def construct(f: Fork, cp: ClasspathOptions, scalaInstance: ScalaInstance): JavaTool = new JavaTool0(f, cp, scalaInstance, _ => ())
/** The actual implementation of a JavaTool (javadoc + javac). */
private[this] class JavaTool0(f: Fork, cp: ClasspathOptions, scalaInstance: ScalaInstance, onArgsF: Seq[String] => Unit) extends JavaTool {
def onArgs(g: Seq[String] => Unit): JavaTool = new JavaTool0(f, cp, scalaInstance, g)
def commandArguments(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], log: Logger): Seq[String] =
@ -93,7 +118,8 @@ object JavaCompiler {
def forkJavac(implicit doFork: Fork) = (contract: JavacContract, arguments: Seq[String], log: Logger) =>
{
val (jArgs, nonJArgs) = arguments.partition(_.startsWith("-J"))
def externalJavac(argFile: File) = doFork(contract, jArgs :+ ("@" + normalizeSlash(argFile.getAbsolutePath)), log)
def externalJavac(argFile: File) =
doFork(contract, jArgs :+ ("@" + normalizeSlash(argFile.getAbsolutePath)), log)
withArgumentFile(nonJArgs)(externalJavac)
}
val directJavac = (contract: JavacContract, arguments: Seq[String], log: Logger) =>
@ -108,6 +134,15 @@ object JavaCompiler {
finally { logger.flushLines(if (exitCode == 0) Level.Warn else Level.Error) }
exitCode
}
/**
* Helper method to create an argument file that we pass to Javac. Gets over the windows
* command line length limitation.
* @param args The string arguments to pass to Javac.
* @param f A function which is passed the arg file.
* @tparam T The return type.
* @return The result of using the argument file.
*/
def withArgumentFile[T](args: Seq[String])(f: File => T): T =
{
import IO.{ Newline, withTemporaryDirectory, write }

View File

@ -0,0 +1,67 @@
package sbt.compiler.javac
import java.io.File
import javax.tools.{ Diagnostic, JavaFileObject, DiagnosticListener }
import sbt.Logger
import xsbti.{ Severity, Reporter }
/**
* A diagnostics listener that feeds all messages into the given reporter.
* @param reporter
*/
final class DiagnosticsReporter(reporter: Reporter) extends DiagnosticListener[JavaFileObject] {
private def fixedDiagnosticMessage(d: Diagnostic[_ <: JavaFileObject]): String = {
def getRawMessage = d.getMessage(null)
def fixWarnOrErrorMessage = {
val tmp = getRawMessage
// we fragment off the line/source/type report from the message.
val lines: Seq[String] =
tmp.split("[\r\n]") match {
case Array(head, tail @ _*) =>
val newHead = head.split(":").last
newHead +: tail
case Array(head) =>
head.split(":").last :: Nil
case Array() => Seq.empty[String]
}
// TODO - Real EOL
lines.mkString("\n")
}
d.getKind match {
case Diagnostic.Kind.ERROR | Diagnostic.Kind.WARNING | Diagnostic.Kind.MANDATORY_WARNING => fixWarnOrErrorMessage
case _ => getRawMessage
}
}
override def report(d: Diagnostic[_ <: JavaFileObject]) {
val severity =
d.getKind match {
case Diagnostic.Kind.ERROR => Severity.Error
case Diagnostic.Kind.WARNING | Diagnostic.Kind.MANDATORY_WARNING => Severity.Warn
case _ => Severity.Info
}
val msg = fixedDiagnosticMessage(d)
val pos: xsbti.Position =
new xsbti.Position {
override val line =
Logger.o2m(if (d.getLineNumber == -1) None
else Option(new Integer(d.getLineNumber.toInt)))
override def lineContent = {
// TODO - Is this pulling error correctly? Is null an ok return value?
Option(d.getSource).
flatMap(s => Option(s.getCharContent(true))).
map(_.subSequence(d.getStartPosition.intValue, d.getEndPosition.intValue).toString).
getOrElse("")
}
override val offset = Logger.o2m(Option(Integer.valueOf(d.getPosition.toInt)))
private val sourceUri = Option(d.getSource).map(_.toUri.toString)
override val sourcePath = Logger.o2m(sourceUri)
override val sourceFile = Logger.o2m(sourceUri.map(new File(_)))
override val pointer = Logger.o2m(Option.empty[Integer])
override val pointerSpace = Logger.o2m(Option.empty[String])
override def toString = s"${d.getSource}:${d.getLineNumber}"
}
reporter.log(pos, msg, severity)
}
}

View File

@ -0,0 +1,73 @@
package sbt.compiler.javac
import java.io.File
import sbt.IO._
import sbt.{ IO, Process, Logger }
import xsbti.Reporter
import xsbti.compile.{ ClasspathOptions, ScalaInstance }
/** Helper methods for running the java toolchain by forking. */
object ForkedJava {
/** Helper method to launch programs. */
private[javac] def launch(javaHome: Option[File], program: String, sources: Seq[File], options: Seq[String], log: Logger, reporter: Reporter): Boolean = {
val (jArgs, nonJArgs) = options.partition(_.startsWith("-J"))
val allArguments = nonJArgs ++ sources.map(_.getAbsolutePath)
withArgumentFile(allArguments) { argsFile =>
val forkArgs = jArgs :+ s"@${normalizeSlash(argsFile.getAbsolutePath)}"
val exe = getJavaExecutable(javaHome, program)
val cwd = new File(new File(".").getAbsolutePath).getCanonicalFile
val javacLogger = new JavacLogger(log, reporter, cwd)
var exitCode = -1
try {
exitCode = Process(exe +: forkArgs, cwd) ! javacLogger
} finally {
javacLogger.flush(exitCode)
}
// We return true or false, depending on success.
exitCode == 0
}
}
/**
* Helper method to create an argument file that we pass to Javac. Gets over the windows
* command line length limitation.
* @param args The string arguments to pass to Javac.
* @param f A function which is passed the arg file.
* @tparam T The return type.
* @return The result of using the argument file.
*/
def withArgumentFile[T](args: Seq[String])(f: File => T): T =
{
import IO.{ Newline, withTemporaryDirectory, write }
withTemporaryDirectory { tmp =>
val argFile = new File(tmp, "argfile")
write(argFile, args.map(escapeSpaces).mkString(Newline))
f(argFile)
}
}
// javac's argument file seems to allow naive space escaping with quotes. escaping a quote with a backslash does not work
private def escapeSpaces(s: String): String = '\"' + normalizeSlash(s) + '\"'
private def normalizeSlash(s: String) = s.replace(File.separatorChar, '/')
import sbt.Path._
/** create the executable name for java */
private[javac] def getJavaExecutable(javaHome: Option[File], name: String): String =
javaHome match {
case None => name
case Some(jh) =>
// TODO - Was there any hackery for windows before?
(jh / "bin" / name).getAbsolutePath
}
}
/** An implementation of compiling java which forks a Javac instance. */
final class ForkedJavaCompiler(javaHome: Option[File]) extends NewJavaCompiler {
def run(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean =
ForkedJava.launch(javaHome, "javac", sources, options, log, reporter)
}
final class ForkedJavadoc(javaHome: Option[File]) extends NewJavadoc {
def run(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean =
ForkedJava.launch(javaHome, "javadoc", sources, options, log, reporter)
}

View File

@ -0,0 +1,43 @@
package sbt.compiler.javac
import java.io.File
import sbt.compiler.{ CompileFailed, CompilerArguments }
import sbt.{ ClasspathOptions, Logger, LoggerReporter }
import xsbti.Reporter
import xsbti.compile.{ MultipleOutput, SingleOutput, Output }
/**
* This class adapts the new java compiler with the classpath/argument option hackery needed to handle scala.
*
* The xsbti.Compiler interface is used by the IncrementalCompiler classes, so this lets us adapt a more generic
* wrapper around running Javac (forked or direct) into the interfaces used by incremental compiler.
*
*/
class JavaCompilerAdapter(delegate: NewJavaTool, scalaInstance: xsbti.compile.ScalaInstance, cpOptions: xsbti.compile.ClasspathOptions) extends xsbti.compile.JavaCompiler {
override final def compile(sources: Array[File], classpath: Array[File], output: Output, options: Array[String], log: xsbti.Logger): Unit = {
// TODO - 5 max errors ok?
val reporter = new LoggerReporter(5, log)
compileWithReporter(sources, classpath, output, options, reporter, log)
}
override final def compileWithReporter(sources: Array[File], classpath: Array[File], output: Output, options: Array[String], reporter: Reporter, log: xsbti.Logger): Unit = {
val target = output match {
case so: SingleOutput => so.outputDirectory
case mo: MultipleOutput => throw new RuntimeException("Javac doesn't support multiple output directories")
}
val args = commandArguments(Seq(), classpath, target, options, log)
// TODO - is sorting the sources correct here?
val success = delegate.run(sources.sortBy(_.getAbsolutePath), args)(log, reporter)
// TODO - What should this error message be, and how do we ensure things are correct here?
if (!success) {
// TODO - Should we track only the problems from this javac?
throw new CompileFailed(args.toArray, "javac returned nonzero exit code", reporter.problems())
}
}
private[this] def commandArguments(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], log: Logger): Seq[String] =
{
val augmentedClasspath = if (cpOptions.autoBoot) classpath ++ Seq(scalaInstance.libraryJar) else classpath
val javaCp = ClasspathOptions.javac(cpOptions.compiler)
(new CompilerArguments(scalaInstance, javaCp))(sources, augmentedClasspath, Some(outputDirectory), options)
}
}

View File

@ -0,0 +1,242 @@
package sbt
package compiler
package javac
import java.util.StringTokenizer
import xsbti._
import java.io.File
/**
* An adapted process logger which can feed semantic error events from Javac as well as just
* dump logs.
*
*
* @param log The logger where all input will go.
* @param reporter A reporter for semantic Javac error messages.
* @param cwd The current working directory of the Javac process, used when parsing Filenames.
*/
final class JavacLogger(log: sbt.Logger, reporter: Reporter, cwd: File) extends ProcessLogger {
import scala.collection.mutable.ListBuffer
import Level.{ Info, Warn, Error, Value => LogLevel }
private val msgs: ListBuffer[(LogLevel, String)] = new ListBuffer()
def info(s: => String): Unit =
synchronized { msgs += ((Info, s)) }
def error(s: => String): Unit =
synchronized { msgs += ((Error, s)) }
def buffer[T](f: => T): T = f
private def print(desiredLevel: LogLevel)(t: (LogLevel, String)) = t match {
case (Info, msg) => log.info(msg)
case (Error, msg) => log.log(desiredLevel, msg)
}
// Helper method to dump all semantic errors.
private def parseAndDumpSemanticErrors(): Unit = {
val input =
msgs collect {
case (Error, msg) => msg
} mkString "\n"
val parser = new JavaErrorParser(cwd)
parser.parseProblems(input, log) foreach { e =>
reporter.log(e.position, e.message, e.severity)
}
}
def flush(exitCode: Int): Unit = {
parseAndDumpSemanticErrors()
val level = if (exitCode == 0) Warn else Error
// Here we only display things that wouldn't otherwise be output by the error reporter.
// TODO - NOTES may not be displayed correctly!
msgs collect {
case (Info, msg) => msg
} foreach { msg =>
log.info(msg)
}
msgs.clear()
}
}
import sbt.Logger.o2m
/** A wrapper around xsbti.Position so we can pass in Java input. */
final case class JavaPosition(_sourceFile: File, _line: Int, _contents: String) extends Position {
def line: Maybe[Integer] = o2m(Option(Integer.valueOf(_line)))
def lineContent: String = _contents
def offset: Maybe[Integer] = o2m(None)
def pointer: Maybe[Integer] = o2m(None)
def pointerSpace: Maybe[String] = o2m(None)
def sourcePath: Maybe[String] = o2m(Option(_sourceFile.getCanonicalPath))
def sourceFile: Maybe[File] = o2m(Option(_sourceFile))
override def toString = s"${_sourceFile}:${_line}"
}
/** A position which has no information, because there is none. */
object JavaNoPosition extends Position {
def line: Maybe[Integer] = o2m(None)
def lineContent: String = ""
def offset: Maybe[Integer] = o2m(None)
def pointer: Maybe[Integer] = o2m(None)
def pointerSpace: Maybe[String] = o2m(None)
def sourcePath: Maybe[String] = o2m(None)
def sourceFile: Maybe[File] = o2m(None)
override def toString = "NoPosition"
}
/** A wrapper around xsbti.Problem with java-specific options. */
final case class JavaProblem(val position: Position, val severity: Severity, val message: String) extends xsbti.Problem {
override def category: String = null // TODO - what is this even supposed to be?
override def toString = s"$severity @ $position - $message"
}
/** A parser that is able to parse java's error output successfully. */
class JavaErrorParser(relativeDir: File) extends util.parsing.combinator.RegexParsers {
// Here we track special handlers to catch "Note:" and "Warning:" lines.
private val NOTE_LINE_PREFIXES = Array("Note: ", "\u6ce8: ", "\u6ce8\u610f\uff1a ")
private val WARNING_PREFIXES = Array("warning", "\u8b66\u544a", "\u8b66\u544a\uff1a")
override val skipWhitespace = false
val CHARAT: Parser[String] = literal("^")
val SEMICOLON: Parser[String] = literal(":") | literal("\uff1a")
val SYMBOL: Parser[String] = allUntilChar(':') // We ignore whether it actually says "symbol" for i18n
val LOCATION: Parser[String] = allUntilChar(':') // We ignore whether it actually says "location" for i18n.
val WARNING: Parser[String] = allUntilChar(':') ^? {
case x if WARNING_PREFIXES.exists(x.trim.startsWith) => x
}
// Parses the rest of an input line.
val restOfLine: Parser[String] =
allUntilChars(Array('\n', '\r')) ~ "[\r]?[\n]?".r ^^ {
case msg ~ _ => msg
}
val NOTE: Parser[String] = restOfLine ^? {
case x if NOTE_LINE_PREFIXES exists x.startsWith => x
}
// Parses ALL characters until an expected character is met.
def allUntilChar(c: Char): Parser[String] = allUntilChars(Array(c))
def allUntilChars(chars: Array[Char]): Parser[String] = new Parser[String] {
def isStopChar(c: Char): Boolean = {
var i = 0
while (i < chars.length) {
if (c == chars(i)) return true
i += 1
}
false
}
def apply(in: Input) = {
val source = in.source
val offset = in.offset
val start = handleWhiteSpace(source, offset)
var i = start
while (i < source.length && !isStopChar(source.charAt(i))) {
i += 1
}
Success(source.subSequence(start, i).toString, in.drop(i - offset))
}
}
//parses a file name (no checks)
val file: Parser[String] = allUntilChar(':') ^^ { _.trim() }
// Checks if a string is an integer
def isInteger(s: String): Boolean =
try {
Integer.parseInt(s)
true
} catch {
case e: NumberFormatException => false
}
// Parses a line number
val line: Parser[Int] = allUntilChar(':') ^? {
case x if isInteger(x) => Integer.parseInt(x)
}
val allUntilCharat: Parser[String] = allUntilChar('^')
// Helper method to try to handle relative vs. absolute file pathing....
// NOTE - this is probably wrong...
private def findFileSource(f: String): File = {
val tmp = new File(f)
if (tmp.exists) tmp
else new File(relativeDir, f)
}
/** Parses an error message (not this WILL parse warning messages as error messages if used incorrectly. */
val errorMessage: Parser[Problem] = {
val fileLineMessage = file ~ SEMICOLON ~ line ~ SEMICOLON ~ restOfLine ^^ {
case file ~ _ ~ line ~ _ ~ msg => (file, line, msg)
}
fileLineMessage ~ allUntilCharat ~ restOfLine ^^ {
case (file, line, msg) ~ contents ~ _ =>
new JavaProblem(
new JavaPosition(
findFileSource(file),
line,
contents + '^' // TODO - Actually parse charat position out of here.
),
Severity.Error,
msg
)
}
}
/** Parses javac warning messages. */
val warningMessage: Parser[Problem] = {
val fileLineMessage = file ~ SEMICOLON ~ line ~ SEMICOLON ~ WARNING ~ SEMICOLON ~ restOfLine ^^ {
case file ~ _ ~ line ~ _ ~ _ ~ _ ~ msg => (file, line, msg)
}
fileLineMessage ~ allUntilCharat ~ restOfLine ^^ {
case (file, line, msg) ~ contents ~ _ =>
new JavaProblem(
new JavaPosition(
findFileSource(file),
line,
contents + "^"
),
Severity.Warn,
msg
)
}
}
val noteMessage: Parser[Problem] =
NOTE ^^ { msg =>
new JavaProblem(
JavaNoPosition,
Severity.Info,
msg
)
}
val potentialProblem: Parser[Problem] = warningMessage | errorMessage | noteMessage
val javacOutput: Parser[Seq[Problem]] = rep(potentialProblem)
/**
* Example:
*
* Test.java:4: cannot find symbol
* symbol : method baz()
* location: class Foo
* return baz();
* ^
*
* Test.java:8: warning: [deprecation] RMISecurityException(java.lang.String) in java.rmi.RMISecurityException has been deprecated
* throw new java.rmi.RMISecurityException("O NOES");
* ^
*/
final def parseProblems(in: String, logger: sbt.Logger): Seq[Problem] =
parse(javacOutput, in) match {
case Success(result, _) => result
case Failure(msg, n) =>
logger.warn("Unexpected javac output at:${n.pos.longString}. Please report to sbt-dev@googlegroups.com.")
Seq.empty
case Error(msg, n) =>
logger.warn("Unexpected javac output at:${n.pos.longString}. Please report to sbt-dev@googlegroups.com.")
Seq.empty
}
}

View File

@ -0,0 +1,69 @@
package sbt.compiler.javac
import java.io.{ File, PrintWriter }
import sbt.{ LoggerWriter, Level, Logger }
import xsbti.Reporter
import xsbti.compile.{ ScalaInstance, ClasspathOptions }
/**
* Helper methods for trying to run the java toolchain out of our own classloaders.
*/
object LocalJava {
private[this] val javadocClass = "com.sun.tools.javadoc.Main"
private[this] def javadocMethod =
try {
Option(Class.forName(javadocClass).getDeclaredMethod("execute", classOf[String], classOf[PrintWriter], classOf[PrintWriter], classOf[PrintWriter], classOf[String], classOf[Array[String]]))
} catch {
case e @ (_: ClassNotFoundException | _: NoSuchMethodException) => None
}
/** True if we can call a forked Javadoc. */
def hasLocalJavadoc: Boolean = javadocMethod.isDefined
/** A mechanism to call the javadoc tool via reflection. */
private[javac] def unsafeJavadoc(args: Array[String], err: PrintWriter, warn: PrintWriter, notice: PrintWriter): Int = {
javadocMethod match {
case Some(m) =>
System.err.println("Running javadoc tool!")
m.invoke(null, "javadoc", err, warn, notice, "com.sun.tools.doclets.standard.Standard", args).asInstanceOf[java.lang.Integer].intValue
case _ =>
System.err.println("Unable to reflectively invoke javadoc, cannot find it on the current classloader!")
-1
}
}
}
/** Implementation of javadoc tool which attempts to run it locally (in-class). */
final class LocalJavadoc() extends NewJavadoc {
override def run(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean = {
val cwd = new File(new File(".").getAbsolutePath).getCanonicalFile
val (jArgs, nonJArgs) = options.partition(_.startsWith("-J"))
val allArguments = nonJArgs ++ sources.map(_.getAbsolutePath)
val javacLogger = new JavacLogger(log, reporter, cwd)
val warnOrError = new PrintWriter(new ProcessLoggerWriter(javacLogger, Level.Error))
val infoWriter = new PrintWriter(new ProcessLoggerWriter(javacLogger, Level.Info))
var exitCode = -1
try {
exitCode = LocalJava.unsafeJavadoc(allArguments.toArray, warnOrError, warnOrError, infoWriter)
} finally {
javacLogger.flush(exitCode)
}
// We return true or false, depending on success.
exitCode == 0
}
}
/** An implementation of compiling java which delegates to the JVM resident java compiler. */
final class LocalJavaCompiler(compiler: javax.tools.JavaCompiler) extends NewJavaCompiler {
override def run(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean = {
import collection.JavaConverters._
val logger = new LoggerWriter(log)
val logWriter = new PrintWriter(logger)
log.debug("Attempting to call " + compiler + " directly...")
val diagnostics = new DiagnosticsReporter(reporter)
val fileManager = compiler.getStandardFileManager(diagnostics, null, null)
val jfiles = fileManager.getJavaFileObjectsFromFiles(sources.asJava)
compiler.getTask(logWriter, fileManager, diagnostics, options.asJava, null, jfiles).call()
}
}

View File

@ -0,0 +1,151 @@
package sbt.compiler.javac
import sbt.ClasspathOptions
import sbt.{ ClasspathOptions => _, _ }
import sbt.compiler._
import java.io.{ PrintWriter, File }
import javax.tools.{ DiagnosticListener, Diagnostic, JavaFileObject, DiagnosticCollector }
import xsbti.compile.ScalaInstance
import xsbti.compile._
import xsbti.{ Severity, Reporter }
/**
* An interface to the toolchain of Java.
*
* Specifically, access to run javadoc + javac.
*/
sealed trait JavaTools {
/** The raw interface of the java compiler for direct access. */
def compiler: NewJavaTool
/**
* This will run a java compiler.
*
*
* @param sources The list of java source files to compile.
* @param options The set of options to pass to the java compiler (includes the classpath).
* @param log The logger to dump output into.
* @param reporter The reporter for semantic error messages.
* @return true if no errors, false otherwise.
*/
def compile(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean
/**
* This will run a java compiler.
*
*
* @param sources The list of java source files to compile.
* @param options The set of options to pass to the java compiler (includes the classpath).
* @param log The logger to dump output into.
* @param reporter The reporter for semantic error messages.
* @return true if no errors, false otherwise.
*/
def doc(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean
}
/**
* An extension of the JavaTools trait that also includes interfaces specific to running
* the java compiler inside of the incremental comppiler.
*/
sealed trait IncrementalCompilerJavaTools extends JavaTools {
/** An instance of the java Compiler for use with incremental compilation. */
def xsbtiCompiler: xsbti.compile.JavaCompiler
}
/** Factory methods for getting a java toolchain. */
object JavaTools {
/** Create a new aggregate tool from existing tools. */
def apply(c: NewJavaCompiler, docgen: NewJavadoc): JavaTools =
new JavaTools {
override def compiler = c
def compile(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean =
c.run(sources, options)
def doc(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean =
docgen.run(sources, options)
}
/**
* Constructs a new set of java toolchain for incremental compilation.
*
* @param instance
* The scalaInstance being used in this incremental compile. Used if we need to append
* scala to the classpath (yeah.... the classpath doesn't already have it).
* @param cpOptions
* Classpath options configured for this incremental compiler. Basically, should we append scala or not.
* @param javaHome
* If this is defined, the location where we should look for javac when we run.
* @return
* A new set of the Java toolchain that also includes and instance of xsbti.compile.JavaCompiler
*/
def directOrFork(instance: xsbti.compile.ScalaInstance, cpOptions: xsbti.compile.ClasspathOptions, javaHome: Option[File]): IncrementalCompilerJavaTools = {
val (compiler, doc) = javaHome match {
case Some(_) => (NewJavaCompiler.fork(javaHome), NewJavadoc.fork(javaHome))
case _ =>
val c = NewJavaCompiler.local.getOrElse(NewJavaCompiler.fork(None))
val d = NewJavadoc.local.getOrElse(NewJavadoc.fork())
(c, d)
}
val delegate = apply(compiler, doc)
new IncrementalCompilerJavaTools {
val xsbtiCompiler = new JavaCompilerAdapter(delegate.compiler, instance, cpOptions)
def compiler = delegate.compiler
def compile(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean =
delegate.compile(sources, options)
def doc(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean =
delegate.doc(sources, options)
}
}
}
/**
* An interface for on of the tools in the java tool chain.
*
* We assume the following is true of tools:
* - The all take sources and options and log error messages
* - They return success or failure.
*/
sealed trait NewJavaTool {
/**
* This will run a java compiler / or other like tool (e.g. javadoc).
*
*
* @param sources The list of java source files to compile.
* @param options The set of options to pass to the java compiler (includes the classpath).
* @param log The logger to dump output into.
* @param reporter The reporter for semantic error messages.
* @return true if no errors, false otherwise.
*/
def run(sources: Seq[File], options: Seq[String])(implicit log: Logger, reporter: Reporter): Boolean
}
/** Interface we use to compile java code. This is mostly a tag over the raw JavaTool interface. */
trait NewJavaCompiler extends NewJavaTool {}
/** Factory methods for constructing a java compiler. */
object NewJavaCompiler {
/** Returns a local compiler, if the current runtime supports it. */
def local: Option[NewJavaCompiler] =
for {
compiler <- Option(javax.tools.ToolProvider.getSystemJavaCompiler)
} yield new LocalJavaCompiler(compiler)
/** Returns a local compiler that will fork javac when needed. */
def fork(javaHome: Option[File] = None): NewJavaCompiler =
new ForkedJavaCompiler(javaHome)
}
/** Interface we use to document java code. This is a tag over the raw JavaTool interface. */
trait NewJavadoc extends NewJavaTool {}
/** Factory methods for constructing a javadoc. */
object NewJavadoc {
/** Returns a local compiler, if the current runtime supports it. */
def local: Option[NewJavadoc] =
// TODO - javax doc tool not supported in JDK6
//Option(javax.tools.ToolProvider.getSystemDocumentationTool)
if (LocalJava.hasLocalJavadoc) Some(new LocalJavadoc)
else None
/** Returns a local compiler that will fork javac when needed. */
def fork(javaHome: Option[File] = None): NewJavadoc =
new ForkedJavadoc(javaHome)
}

View File

@ -0,0 +1,43 @@
package sbt.compiler.javac
import sbt.{ Level, ProcessLogger }
/** Delegates a stream into a process logger. Mimics LoggerWriter, but for the ProcessLogger interface which differs. */
private class ProcessLoggerWriter(delegate: ProcessLogger, level: Level.Value, nl: String = System.getProperty("line.separator")) extends java.io.Writer {
private[this] val buffer = new StringBuilder
private[this] val lines = new collection.mutable.ListBuffer[String]
override def close() = flush()
override def flush(): Unit =
synchronized {
if (buffer.length > 0) {
log(buffer.toString)
buffer.clear()
}
}
def flushLines(level: Level.Value): Unit =
synchronized {
for (line <- lines)
log(line)
lines.clear()
}
override def write(content: Array[Char], offset: Int, length: Int): Unit =
synchronized {
buffer.appendAll(content, offset, length)
process()
}
private[this] def process() {
val i = buffer.indexOf(nl)
if (i >= 0) {
log(buffer.substring(0, i))
buffer.delete(0, i + nl.length)
process()
}
}
private[this] def log(s: String): Unit = level match {
case Level.Warn | Level.Error => delegate.error(s)
case Level.Info => delegate.info(s)
}
}

View File

@ -0,0 +1,7 @@
public class good {
public static String test() {
return "Hello";
}
}

View File

@ -0,0 +1,9 @@
import java.rmi.RMISecurityException;
public class Test {
public NotFound foo() { return 5; }
public String warning() {
throw new RMISecurityException("O NOES");
}
}

View File

@ -0,0 +1,134 @@
package sbt.compiler.javac
import java.io.File
import java.net.URLClassLoader
import sbt._
import org.specs2.Specification
import xsbti.{ Severity, Problem }
object NewJavaCompilerSpec extends Specification {
def is = s2"""
This is a specification for forking + inline-running of the java compiler, and catching Error messages
Compiling a java file with local javac should
compile a java file ${works(local)}
issue errors and warnings ${findsErrors(local)}
Compiling a file with forked javac should
compile a java file ${works(forked)}
issue errors and warnings ${findsErrors(forked)}
yield the same errors as local javac $forkSameAsLocal
Documenting a file with forked javadoc should
document a java file ${docWorks(forked)}
find errors in a java file ${findsDocErrors(forked)}
"""
// TODO - write a test to ensure that java .class files wind up in the right spot, and we can call the compiled java code.
def docWorks(compiler: JavaTools) = IO.withTemporaryDirectory { out =>
val (result, problems) = doc(compiler, Seq(knownSampleGoodFile), Seq("-d", out.getAbsolutePath))
val compiled = result must beTrue
val indexExists = (new File(out, "index.html")).exists must beTrue setMessage ("index.html does not exist!")
val classExists = (new File(out, "good.html")).exists must beTrue setMessage ("good.html does not exist!")
compiled and classExists and indexExists
}
def works(compiler: JavaTools) = IO.withTemporaryDirectory { out =>
val (result, problems) = compile(compiler, Seq(knownSampleGoodFile), Seq("-deprecation", "-d", out.getAbsolutePath))
val compiled = result must beTrue
val classExists = (new File(out, "good.class")).exists must beTrue
val cl = new URLClassLoader(Array(out.toURI.toURL))
val clazzz = cl.loadClass("good")
val mthd = clazzz.getDeclaredMethod("test")
val testResult = mthd.invoke(null)
val canRun = mthd.invoke(null) must equalTo("Hello")
compiled and classExists and canRun
}
def findsErrors(compiler: JavaTools) = {
val (result, problems) = compile(compiler, Seq(knownSampleErrorFile), Seq("-deprecation"))
val errored = result must beFalse
val foundErrorAndWarning = problems must haveSize(5)
val hasKnownErrors = problems.toSeq must contain(errorOnLine(1), warnOnLine(7))
errored and foundErrorAndWarning and hasKnownErrors
}
def findsDocErrors(compiler: JavaTools) = IO.withTemporaryDirectory { out =>
val (result, problems) = doc(compiler, Seq(knownSampleErrorFile), Seq("-d", out.getAbsolutePath))
val errored = result must beTrue
val foundErrorAndWarning = problems must haveSize(2)
val hasKnownErrors = problems.toSeq must contain(errorOnLine(3), errorOnLine(4))
errored and foundErrorAndWarning and hasKnownErrors
}
def lineMatches(p: Problem, lineno: Int): Boolean =
p.position.line.isDefined && (p.position.line.get == lineno)
def isError(p: Problem): Boolean = p.severity == Severity.Error
def isWarn(p: Problem): Boolean = p.severity == Severity.Warn
def errorOnLine(lineno: Int) =
beLike[Problem]({
case p if lineMatches(p, lineno) && isError(p) => ok
case _ => ko
})
def warnOnLine(lineno: Int) =
beLike[Problem]({
case p if lineMatches(p, lineno) && isWarn(p) => ok
case _ => ko
})
def forkSameAsLocal = {
val (fresult, fproblems) = compile(forked, Seq(knownSampleErrorFile), Seq("-deprecation"))
val (lresult, lproblems) = compile(local, Seq(knownSampleErrorFile), Seq("-deprecation"))
val sameResult = fresult must beEqualTo(lresult)
val pResults = for ((f, l) <- fproblems zip lproblems) yield {
val sourceIsSame =
if (f.position.sourcePath.isDefined) (f.position.sourcePath.get must beEqualTo(l.position.sourcePath.get)).setMessage(s"${f.position} != ${l.position}")
else l.position.sourcePath.isDefined must beFalse
val lineIsSame =
if (f.position.line.isDefined) f.position.line.get must beEqualTo(l.position.line.get)
else l.position.line.isDefined must beFalse
val severityIsSame = f.severity must beEqualTo(l.severity)
// TODO - We should check to see if the levenshtein distance of the messages is close...
sourceIsSame and lineIsSame and severityIsSame
}
val errorsAreTheSame = pResults.reduce(_ and _)
sameResult and errorsAreTheSame
}
def compile(c: JavaTools, sources: Seq[File], args: Seq[String]): (Boolean, Array[Problem]) = {
val log = Logger.Null
val reporter = new LoggerReporter(10, log)
val result = c.compile(sources, args)(log, reporter)
(result, reporter.problems)
}
def doc(c: JavaTools, sources: Seq[File], args: Seq[String]): (Boolean, Array[Problem]) = {
val log = Logger.Null
val reporter = new LoggerReporter(10, log)
val result = c.doc(sources, args)(log, reporter)
(result, reporter.problems)
}
// TODO - Create one with known JAVA HOME.
def forked = JavaTools(NewJavaCompiler.fork(), NewJavadoc.fork())
def local =
JavaTools(
NewJavaCompiler.local.getOrElse(sys.error("This test cannot be run on a JRE, but only a JDK.")),
NewJavadoc.local.getOrElse(NewJavadoc.fork())
)
def cwd =
(new File(new File(".").getAbsolutePath)).getCanonicalFile
def knownSampleErrorFile =
new java.io.File(getClass.getResource("test1.java").toURI)
def knownSampleGoodFile =
new java.io.File(getClass.getResource("good.java").toURI)
}

View File

@ -2,6 +2,7 @@ package xsbti.compile;
import java.io.File;
import xsbti.Logger;
import xsbti.Reporter;
/**
* Interface to a Java compiler.
@ -9,6 +10,17 @@ import xsbti.Logger;
public interface JavaCompiler
{
/** Compiles Java sources using the provided classpath, output directory, and additional options.
* Output should be sent to the provided logger.*/
* Output should be sent to the provided logger.
*
* @deprecated 0.13.8 - Use compileWithReporter instead
*/
void compile(File[] sources, File[] classpath, Output output, String[] options, Logger log);
}
/**
* Compiles java sources using the provided classpath, output directory and additional options.
*
* Output should be sent to the provided logger.
* Failures should be passed to the provided Reporter.
*/
void compileWithReporter(File[] sources, File[] classpath, Output output, String[] options, Reporter reporter, Logger log);
}

View File

@ -3,6 +3,7 @@
*/
package sbt
import sbt.compiler.javac.{ IncrementalCompilerJavaTools, NewJavaCompiler, JavaTools }
import xsbti.{ Logger => _, _ }
import xsbti.compile.{ CompileOrder, GlobalsCache }
import CompileOrder.{ JavaThenScala, Mixed, ScalaThenJava }
@ -17,7 +18,17 @@ 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, sourcePositionMapper: Position => Position, order: CompileOrder)
final case class IncSetup(analysisMap: File => Option[Analysis], definesClass: DefinesClass, skip: Boolean, cacheFile: File, cache: GlobalsCache, incOptions: IncOptions)
final case class Compilers(scalac: AnalyzingCompiler, javac: JavaTool)
private[sbt] trait JavaToolWithNewInterface extends JavaTool {
def newJavac: IncrementalCompilerJavaTools
}
final case class Compilers(scalac: AnalyzingCompiler, javac: JavaTool) {
final def newJavac: Option[IncrementalCompilerJavaTools] =
javac match {
case x: JavaToolWithNewInterface => Some(x.newJavac)
case _ => None
}
}
final case class NewCompilers(scalac: AnalyzingCompiler, javac: JavaTools)
def inputs(classpath: Seq[File], sources: Seq[File], classesDirectory: File, options: Seq[String], javacOptions: Seq[String], maxErrors: Int, sourcePositionMappers: Seq[Position => Option[Position]], order: CompileOrder)(implicit compilers: Compilers, incSetup: IncSetup, log: Logger): Inputs =
new Inputs(
@ -37,8 +48,17 @@ object Compiler {
def compilers(instance: ScalaInstance, cpOptions: ClasspathOptions, javaHome: Option[File])(implicit app: AppConfiguration, log: Logger): Compilers =
{
val javac = AggressiveCompile.directOrFork(instance, cpOptions, javaHome)
compilers(instance, cpOptions, javac)
val javac =
AggressiveCompile.directOrFork(instance, cpOptions, javaHome)
val javac2 =
JavaTools.directOrFork(instance, cpOptions, javaHome)
// Hackery to enable both the new and deprecated APIs to coexist peacefully.
case class CheaterJavaTool(newJavac: IncrementalCompilerJavaTools, delegate: JavaTool) extends JavaTool with JavaToolWithNewInterface {
def compile(contract: JavacContract, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String])(implicit log: Logger): Unit =
javac.compile(contract, sources, classpath, outputDirectory, options)(log)
def onArgs(f: Seq[String] => Unit): JavaTool = CheaterJavaTool(newJavac, delegate.onArgs(f))
}
compilers(instance, cpOptions, CheaterJavaTool(javac2, javac))
}
def compilers(instance: ScalaInstance, cpOptions: ClasspathOptions, javac: JavaCompiler.Fork)(implicit app: AppConfiguration, log: Logger): Compilers =
{
@ -70,7 +90,12 @@ object Compiler {
import in.config._
import in.incSetup._
val agg = new AggressiveCompile(cacheFile)
agg(scalac, javac, sources, classpath, CompileOutput(classesDirectory), cache, None, options, javacOptions,
// Here is some trickery to choose the more recent (reporter-using) java compiler rather
// than the previously defined versions.
// TODO - Remove this hackery in sbt 1.0.
val javacChosen: xsbti.compile.JavaCompiler =
in.compilers.newJavac.map(_.xsbtiCompiler).getOrElse(in.compilers.javac)
agg(scalac, javacChosen, sources, classpath, CompileOutput(classesDirectory), cache, None, options, javacOptions,
analysisMap, definesClass, reporter, order, skip, incOptions)(log)
}

View File

@ -0,0 +1,27 @@
TaskKey[Unit]("checkJavaFailures") := {
val reporter = savedReporter.value
val ignore = (compile in Compile).failure.value
val ps = reporter.problems
assert(!ps.isEmpty, "Failed to report any problems!")
// First error should be on a specific line/file
val first = ps(0)
assert(first.position.line.get == 3, s"First failure position is not line 3, failure = $first")
val javaFile = baseDirectory.value / "src/main/java/bad.java"
assert(first.position.sourceFile.get == javaFile, s"First failure file location is not $javaFile, $first")
}
TaskKey[Unit]("checkScalaFailures") := {
val reporter = savedReporter.value
val ignore = (compile in Compile).failure.value
val ps = reporter.problems
assert(!ps.isEmpty, "Failed to report any problems!")
// First error should be on a specific line/file
val first = ps(0)
assert(first.position.line.get == 2, s"First failure position is not line 2, failure = $first")
val scalaFile = baseDirectory.value / "src/main/scala/bad.scala"
assert(first.position.sourceFile.get == scalaFile, s"First failure file location is not $scalaFile, $first")
}
compileOrder := CompileOrder.Mixed

View File

@ -0,0 +1,51 @@
package sbt
import Keys._
import xsbti.{Position, Severity}
object TestPlugin extends AutoPlugin {
override def requires = plugins.JvmPlugin
override def trigger = allRequirements
object autoImport {
val savedReporter = settingKey[xsbti.Reporter]("Saved reporter that collects compilation failures.")
val problems = taskKey[Array[xsbti.Problem]]("Problems reported during compilation.")
}
import autoImport._
override def projectSettings = Seq(
savedReporter := new CollectingReporter,
compilerReporter in (Compile, compile) := Some(savedReporter.value),
problems := savedReporter.value.problems
)
}
class CollectingReporter extends xsbti.Reporter {
val buffer = collection.mutable.ArrayBuffer.empty[xsbti.Problem]
def reset(): Unit = {
System.err.println(s"DEBUGME: Clearing errors: $buffer")
buffer.clear()
}
def hasErrors: Boolean = buffer.exists(_.severity == Severity.Error)
def hasWarnings: Boolean = buffer.exists(_.severity == Severity.Warn)
def printSummary(): Unit = ()
def problems: Array[xsbti.Problem] = buffer.toArray
/** Logs a message. */
def log(pos: xsbti.Position, msg: String, sev: xsbti.Severity): Unit = {
object MyProblem extends xsbti.Problem {
def category: String = null
def severity: Severity = sev
def message: String = msg
def position: Position = pos
override def toString = s"$position:$severity: $message"
}
System.err.println(s"DEBUGME: Logging: $MyProblem")
buffer.append(MyProblem)
}
/** Reports a comment. */
def comment(pos: xsbti.Position, msg: String): Unit = ()
override def toString = "CollectingReporter"
}

View File

@ -0,0 +1,4 @@
class bad {
public bad foo() { return 1; }
}

View File

@ -0,0 +1,3 @@
trait badScala {
def foo: Int = false
}

View File

@ -0,0 +1,3 @@
> checkScalaFailures
> set compileOrder := CompileOrder.JavaThenScala
> checkJavaFailures

View File

@ -104,6 +104,7 @@ object Logger {
val position = pos
val message = msg
val severity = sev
override def toString = s"[$severity] $pos: $message"
}
}