Enable Javadoc generation in `doc` task.

`docSetting` has been updated to do both Scaladoc and Javadoc. In
Scala/Java hybrid projects, the output docs are rebased to `scala`
or `java` sub-directory accordingly. But for pure scala or pure java
projects the subdirectories aren't added to becompliant with user
expectation as much as possible. We do hybrid mode iff both *.scala
and *.java files exist; other doc resources (package.html, *.jpg etc.)
don't influence the decision.
This commit is contained in:
Indrajit Raychaudhuri 2011-11-09 17:49:54 +05:30
parent ca154d9b5e
commit ad7aede533
7 changed files with 132 additions and 52 deletions

View File

@ -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
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -0,0 +1,4 @@
package pkg;
/** This is class J */
public class J {
}

View File

@ -0,0 +1,4 @@
package pkg;
/** This is class K */
public class K {
}

View File

@ -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
# 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"