diff --git a/compile/JavaCompiler.scala b/compile/JavaCompiler.scala index 6cdb46855..7edd7ab24 100644 --- a/compile/JavaCompiler.scala +++ b/compile/JavaCompiler.scala @@ -4,26 +4,48 @@ package sbt package compiler - import java.io.File +import java.io.{File, PrintWriter} +abstract class JavacContract(val name: String, val clazz: String) { + def exec(args: Array[String], writer: PrintWriter): Int +} trait JavaCompiler { - def apply(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String])(implicit log: Logger): Unit + def apply(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String])(implicit log: Logger) { + compile(JavaCompiler.javac, sources, classpath, outputDirectory, options)(log) + } + def doc(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], maximumErrors: Int, log: Logger) { + compile(JavaCompiler.javadoc, sources, classpath, outputDirectory, options)(log) + } + def compile(contract: JavacContract, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String])(implicit log: Logger): Unit } object JavaCompiler { - type Fork = (Seq[String], Logger) => Int + type Fork = (JavacContract, Seq[String], Logger) => Int - def construct(f: (Seq[String], Logger) => Int, cp: ClasspathOptions, scalaInstance: ScalaInstance): JavaCompiler = + val javac = new JavacContract("javac", "com.sun.tools.javac.Main") { + def exec(args: Array[String], writer: PrintWriter) = { + val m = Class.forName(clazz).getDeclaredMethod("compile", classOf[Array[String]], classOf[PrintWriter]) + m.invoke(null, args, writer).asInstanceOf[java.lang.Integer].intValue + } + } + val javadoc = new JavacContract("javadoc", "com.sun.tools.javadoc.Main") { + def exec(args: Array[String], writer: PrintWriter) = { + val m = Class.forName(clazz).getDeclaredMethod("execute", classOf[String], classOf[PrintWriter], classOf[PrintWriter], classOf[PrintWriter], classOf[String], classOf[Array[String]]) + m.invoke(null, name, writer, writer, writer, "com.sun.tools.doclets.standard.Standard", args).asInstanceOf[java.lang.Integer].intValue + } + } + + def construct(f: Fork, cp: ClasspathOptions, scalaInstance: ScalaInstance): JavaCompiler = new JavaCompiler { - def apply(sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String])(implicit log: Logger) { + def compile(contract: JavacContract, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String])(implicit log: Logger) { val augmentedClasspath = if(cp.autoBoot) classpath ++ Seq(scalaInstance.libraryJar) else classpath val javaCp = ClasspathOptions.javac(cp.compiler) val arguments = (new CompilerArguments(scalaInstance, javaCp))(sources, augmentedClasspath, outputDirectory, options) - log.debug("running javac with arguments:\n\t" + arguments.mkString("\n\t")) - val code: Int = f(arguments, log) - log.debug("javac returned exit code: " + code) - if( code != 0 ) throw new CompileFailed(arguments.toArray, "javac returned nonzero exit code") + log.debug("Calling " + contract.name.capitalize + " with arguments:\n\t" + arguments.mkString("\n\t")) + val code: Int = f(contract, arguments, log) + log.debug(contract.name + " returned exit code: " + code) + if( code != 0 ) throw new CompileFailed(arguments.toArray, contract.name + " returned nonzero exit code") } } def directOrFork(cp: ClasspathOptions, scalaInstance: ScalaInstance)(implicit doFork: Fork): JavaCompiler = @@ -35,31 +57,29 @@ object JavaCompiler def fork(cp: ClasspathOptions, scalaInstance: ScalaInstance)(implicit doFork: Fork): JavaCompiler = construct(forkJavac, cp, scalaInstance) - def directOrForkJavac(implicit doFork: Fork) = (arguments: Seq[String], log: Logger) => - try { directJavac(arguments, log) } - catch { case e: ClassNotFoundException => - log.debug("com.sun.tools.javac.Main not found; forking javac instead") - forkJavac(doFork)(arguments, log) + def directOrForkJavac(implicit doFork: Fork) = (contract: JavacContract, arguments: Seq[String], log: Logger) => + try { directJavac(contract, arguments, log) } + catch { case e @ (_: ClassNotFoundException | _: NoSuchMethodException) => + log.debug(contract.clazz + " not found with appropriate method signature; forking " + contract.name + " instead") + forkJavac(doFork)(contract, arguments, log) } /** `doFork` should be a function that forks javac with the provided arguments and sends output to the given Logger.*/ - def forkJavac(implicit doFork: Fork) = (arguments: Seq[String], log: Logger) => + def forkJavac(implicit doFork: Fork) = (contract: JavacContract, arguments: Seq[String], log: Logger) => { val (jArgs, nonJArgs) = arguments.partition(_.startsWith("-J")) - def externalJavac(argFile: File) = doFork(jArgs :+ ("@" + normalizeSlash(argFile.getAbsolutePath)), log) + def externalJavac(argFile: File) = doFork(contract, jArgs :+ ("@" + normalizeSlash(argFile.getAbsolutePath)), log) withArgumentFile(nonJArgs)(externalJavac) } - val directJavac = (arguments: Seq[String], log: Logger) => + val directJavac = (contract: JavacContract, arguments: Seq[String], log: Logger) => { val logger = new LoggerWriter(log) - val writer = new java.io.PrintWriter(logger) + val writer = new PrintWriter(logger) val argsArray = arguments.toArray - val javac = Class.forName("com.sun.tools.javac.Main") - log.debug("Calling javac directly.") - val compileMethod = javac.getDeclaredMethod("compile", classOf[Array[String]], classOf[java.io.PrintWriter]) + log.debug("Attempting to call " + contract.name + " directly...") var exitCode = -1 - try { exitCode = compileMethod.invoke(null, argsArray, writer).asInstanceOf[java.lang.Integer].intValue } + try { exitCode = contract.exec(argsArray, writer) } finally { logger.flushLines( if(exitCode == 0) Level.Warn else Level.Error) } exitCode } diff --git a/main/Defaults.scala b/main/Defaults.scala index 8cc972204..e245dead1 100644 --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -131,7 +131,7 @@ object Defaults extends BuildCommon lazy val configPaths = sourceConfigPaths ++ resourceConfigPaths ++ outputConfigPaths lazy val sourceConfigPaths = Seq( - sourceDirectory <<= configSrcSub( sourceDirectory), + sourceDirectory <<= configSrcSub(sourceDirectory), sourceManaged <<= configSrcSub(sourceManaged), scalaSource <<= sourceDirectory / "scala", javaSource <<= sourceDirectory / "java", @@ -445,14 +445,21 @@ object Defaults extends BuildCommon def docSetting(key: TaskKey[File]): Seq[Setting[_]] = inTask(key)(Seq( cacheDirectory ~= (_ / key.key.label), target <<= docDirectory, // deprecate docDirectory in favor of 'target in doc'; remove when docDirectory is removed - scaladocOptions <<= scalacOptions, // deprecate scaladocOptions in favor of 'scalacOptions in doc'; remove when scaladocOptions is removed - fullClasspath <<= dependencyClasspath, - key in TaskGlobal <<= (sources, cacheDirectory, maxErrors, compilers, target, configuration, scaladocOptions, fullClasspath, streams) map { (srcs, cache, maxE, cs, out, config, options, cp, s) => - (new Scaladoc(maxE, cs.scalac)).cached(cache, nameForSrc(config.name), srcs, cp.files, out, options, s.log) + scalacOptions <<= scaladocOptions or scalacOptions, // deprecate scaladocOptions in favor of 'scalacOptions in doc'; remove when scaladocOptions is removed + compileInputs <<= compileInputsTask, + key in TaskGlobal <<= (cacheDirectory, compileInputs, target, configuration, streams) map { (cache, in, out, config, s) => + // For Scala/Java hybrid projects, the output docs are rebased to `scala` or `java` sub-directory accordingly. We do hybrid + // mode iff both *.scala and *.java files exist -- other doc resources (package.html, *.jpg etc.) don't influence the decision. + val srcs = in.config.sources + val hybrid = srcs.exists(_.name.endsWith(".scala")) && srcs.exists(_.name.endsWith(".java")) + val (scalaOut, javaOut) = if (hybrid) (out / "scala", out / "java") else (out, out) + val cp = in.config.classpath.toList - in.config.classesDirectory + Doc(in.config.maxErrors, in.compilers.scalac).cached(cache / "scala", nameForSrc(config.name), srcs, cp, scalaOut, in.config.options, s.log) + Doc(in.config.maxErrors, in.compilers.javac).cached(cache / "java", nameForSrc(config.name), srcs, cp, javaOut, in.config.javacOptions, s.log) out } )) - + @deprecated("Use `docSetting` instead", "0.11.0") def docTask: Initialize[Task[File]] = (cacheDirectory, compileInputs, streams, docDirectory, configuration, scaladocOptions) map { (cache, in, s, target, config, options) => val d = new Scaladoc(in.config.maxErrors, in.compilers.scalac) diff --git a/main/actions/Compiler.scala b/main/actions/Compiler.scala index b33137c0b..ee80ac3fa 100644 --- a/main/actions/Compiler.scala +++ b/main/actions/Compiler.scala @@ -30,7 +30,7 @@ object Compiler def inputs(classpath: Seq[File], sources: Seq[File], outputDirectory: File, options: Seq[String], javacOptions: Seq[String], definesClass: DefinesClass, maxErrors: Int, order: CompileOrder.Value)(implicit compilers: Compilers, log: Logger): Inputs = { - import Path._ + import Path._ val classesDirectory = outputDirectory / "classes" val cacheDirectory = outputDirectory / "cache" val augClasspath = classesDirectory.asFile +: classpath @@ -58,7 +58,7 @@ object Compiler val javac = directOrFork(instance, cpOptions, javaHome) compilers(instance, cpOptions, javac) } - def compilers(instance: ScalaInstance, cpOptions: ClasspathOptions, javac: (Seq[String], Logger) => Int)(implicit app: AppConfiguration, log: Logger): Compilers = + def compilers(instance: ScalaInstance, cpOptions: ClasspathOptions, javac: JavaCompiler.Fork)(implicit app: AppConfiguration, log: Logger): Compilers = { val javaCompiler = JavaCompiler.fork(cpOptions, instance)(javac) compilers(instance, cpOptions, javaCompiler) @@ -78,18 +78,18 @@ object Compiler if(javaHome.isDefined) JavaCompiler.fork(cpOptions, instance)(forkJavac(javaHome)) else - JavaCompiler.directOrFork(cpOptions, instance)( forkJavac(None) ) + JavaCompiler.directOrFork(cpOptions, instance)(forkJavac(None)) - def forkJavac(javaHome: Option[File]): (Seq[String], Logger) => Int = + def forkJavac(javaHome: Option[File]): JavaCompiler.Fork = { import Path._ - val exec = javaHome match { case None => "javac"; case Some(jh) => (jh / "bin" / "javac").absolutePath } - (args: Seq[String], log: Logger) => { - log.debug("Forking javac: " + exec + " " + args.mkString(" ")) + def exec(jc: JavacContract) = javaHome match { case None => jc.name; case Some(jh) => (jh / "bin" / jc.name).absolutePath } + (contract: JavacContract, args: Seq[String], log: Logger) => { + log.debug("Forking " + contract.name + ": " + exec(contract) + " " + args.mkString(" ")) val javacLogger = new JavacLogger(log) var exitCode = -1 try { - exitCode = Process(exec, args) ! javacLogger + exitCode = Process(exec(contract), args) ! javacLogger } finally { javacLogger.flush(exitCode) } diff --git a/main/actions/Doc.scala b/main/actions/Doc.scala index e804ae60f..5e3600087 100644 --- a/main/actions/Doc.scala +++ b/main/actions/Doc.scala @@ -1,10 +1,10 @@ /* sbt -- Simple Build Tool - * Copyright 2008, 2009, 2010, 2011 Mark Harrah + * Copyright 2008, 2009, 2010, 2011 Mark Harrah, Indrajit Raychaudhuri */ package sbt import java.io.File - import compiler.AnalyzingCompiler + import compiler.{AnalyzingCompiler, JavaCompiler} import Predef.{conforms => _, _} import Types.:+: @@ -15,24 +15,28 @@ package sbt import Tracked.{inputChanged, outputChanged} import FilesInfo.{exists, hash, lastModified} -final class Scaladoc(maximumErrors: Int, compiler: AnalyzingCompiler) -{ - final def apply(label: String, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], log: Logger) - { - log.info(actionStartMessage(label)) +object Doc { + def apply(maximumErrors: Int, compiler: AnalyzingCompiler) = new Scaladoc(maximumErrors, compiler) + def apply(maximumErrors: Int, compiler: JavaCompiler) = new Javadoc(maximumErrors, compiler) +} +sealed trait Doc { + type Gen = (Seq[File], Seq[File], File, Seq[String], Int, Logger) => Unit + + def apply(label: String, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], log: Logger): Unit + + final def generate(variant: String, label: String, docf: Gen, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], maxErrors: Int, log: Logger) { + val logSnip = variant + " API documentation" if(sources.isEmpty) - log.info(ActionNothingToDoMessage) + log.info("No sources available, skipping " + logSnip + "...") else { + log.info("Generating " + logSnip + " for " + label + " sources to " + outputDirectory.absolutePath + "...") IO.delete(outputDirectory) IO.createDirectory(outputDirectory) - compiler.doc(sources, classpath, outputDirectory, options, maximumErrors, log) - log.info(ActionSuccessfulMessage) + docf(sources, classpath, outputDirectory, options, maxErrors, log) + log.info(logSnip + " generation successful.") } } - def actionStartMessage(label: String) = "Generating API documentation for " + label + " sources..." - val ActionNothingToDoMessage = "No sources specified." - val ActionSuccessfulMessage = "API documentation generation successful." def cached(cache: File, label: String, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], log: Logger) { @@ -48,7 +52,21 @@ final class Scaladoc(maximumErrors: Int, compiler: AnalyzingCompiler) log.debug("Doc uptodate: " + outputDirectory.getAbsolutePath) } } - cachedDoc(inputs)(() => exists(outputDirectory.***.get.toSet)) } } +final class Scaladoc(maximumErrors: Int, compiler: AnalyzingCompiler) extends Doc +{ + def apply(label: String, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], log: Logger) + { + generate("Scala", label, compiler.doc, sources, classpath, outputDirectory, options, maximumErrors, log) + } +} +final class Javadoc(maximumErrors: Int, compiler: JavaCompiler) extends Doc +{ + def apply(label: String, sources: Seq[File], classpath: Seq[File], outputDirectory: File, options: Seq[String], log: Logger) + { + // javadoc doesn't handle *.scala properly, so we evict them from javadoc sources list. + generate("Java", label, compiler.doc, sources.filterNot(_.name.endsWith(".scala")), classpath, outputDirectory, options, maximumErrors, log) + } +} diff --git a/sbt/src/sbt-test/actions/doc/src/main/java/pkg/J.java b/sbt/src/sbt-test/actions/doc/src/main/java/pkg/J.java new file mode 100644 index 000000000..21354494c --- /dev/null +++ b/sbt/src/sbt-test/actions/doc/src/main/java/pkg/J.java @@ -0,0 +1,4 @@ +package pkg; +/** This is class J */ +public class J { +} \ No newline at end of file diff --git a/sbt/src/sbt-test/actions/doc/src/main/java/pkg/K.java b/sbt/src/sbt-test/actions/doc/src/main/java/pkg/K.java new file mode 100644 index 000000000..b819006cf --- /dev/null +++ b/sbt/src/sbt-test/actions/doc/src/main/java/pkg/K.java @@ -0,0 +1,4 @@ +package pkg; +/** This is class K */ +public class K { +} \ No newline at end of file diff --git a/sbt/src/sbt-test/actions/doc/test b/sbt/src/sbt-test/actions/doc/test index 8a5c6fdb6..7a5bdead2 100644 --- a/sbt/src/sbt-test/actions/doc/test +++ b/sbt/src/sbt-test/actions/doc/test @@ -1,7 +1,34 @@ +> 'set crossPaths := false' + > 'set scalacOptions in (Compile, doc) += "-Xfatal-warnings"' -> doc -> 'set sources in (Compile, doc) <<= sources in Compile map { _.filter(_.getName contains "A") }' +> 'set sources in (Compile, doc) <<= sources in Compile map { _.filterNot(_.getName contains "B") }' -> doc \ No newline at end of file +# hybrid project, scaladoc and javadoc in respective directories +> doc +$ exists "target/api/scala" +$ exists "target/api/java" + +> 'set sources in (Compile, doc) <<= sources in (Compile, doc) map { _.filterNot(_.getName endsWith ".java") }' + +> clean +> doc + +# pure scala project, only scaladoc at top level +$ exists "target/api/index.js" +$ absent "target/api/package-list" +$ absent "target/api/scala" +$ absent "target/api/java" + +> 'set sources in (Compile, doc) <<= sources in Compile map { _.filter(_.getName endsWith ".java") }' + +> clean +> doc + +# pure java project, only javadoc at top level +$ exists "target/api/package-list" +$ absent "target/api/index.js" +$ absent "target/api/scala" +$ absent "target/api/java"