All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sbt.internal.inc.javac.AnalyzingJavaCompiler.scala Maven / Gradle / Ivy

/*
 * Zinc - The incremental compiler for Scala.
 * Copyright Scala Center, Lightbend, and Mark Harrah
 *
 * Licensed under Apache License 2.0
 * SPDX-License-Identifier: Apache-2.0
 *
 * See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 */

package sbt
package internal
package inc
package javac

import java.nio.file.Path
import java.net.URLClassLoader

import sbt.internal.inc.JavaInterfaceUtil._
import sbt.internal.inc.classfile.JavaAnalyze
import sbt.internal.inc.classpath.ClasspathUtil
import xsbti.compile._
import xsbti.{
  AnalysisCallback,
  FileConverter,
  Reporter => XReporter,
  Logger => XLogger,
  VirtualFile,
  VirtualFileRef
}

import sbt.util.InterfaceUtil
import sbt.util.Logger

/**
 * Define a Java compiler that reports on any discovered source dependencies or
 * APIs found via the incremental compilation and `AnalysisCallback`.
 *
 * Note that this compiler does not implement a `CachedCompilerProvider`
 * because the Java compiler can easily be initialized via reflection.
 *
 * @param javac An instance of a Java compiler.
 * @param classpath The classpath to be used by Javac.
 * @param scalaInstance The Scala instance encapsulating classpath entries
 *                      for a given Scala version.
 * @param classpathOptions The classpath options for a compiler. This instance
 *                         returns false always only for Java compilers.
 * @param classLookup The mechanism to map class files to classpath entries.
 * @param searchClasspath The classpath used to look for binary dependencies.
 */
final class AnalyzingJavaCompiler private[sbt] (
    val javac: xsbti.compile.JavaCompiler,
    val classpath: Seq[VirtualFile],
    val scalaInstance: xsbti.compile.ScalaInstance,
    val classpathOptions: ClasspathOptions,
    val classLookup: (String => Option[VirtualFile]),
    val searchClasspath: Seq[VirtualFile]
) extends JavaCompiler {

  override def supportsDirectToJar: Boolean = javac.supportsDirectToJar

  // // for compatibility
  // def compile(
  //     sources: Seq[File],
  //     options: Seq[String],
  //     output: Output,
  //     callback: AnalysisCallback,
  //     incToolOptions: IncToolOptions,
  //     reporter: XReporter,
  //     log: XLogger,
  //     progressOpt: Option[CompileProgress]
  // ): Unit = {
  //   compile(
  //     sources,
  //     options,
  //     output,
  //     finalJarOutput = None,
  //     callback,
  //     incToolOptions,
  //     reporter,
  //     log,
  //     progressOpt
  //   )
  // }

  /**
   * Compile some java code using the current configured compiler.
   *
   * @param sources  The sources to compile
   * @param options  The options for the Java compiler
   * @param output   The output configuration for this compiler
   * @param finalJarOutput The output that will be used for straight to jar compilation if javac doesn't support it.
   * @param callback  A callback to report discovered source/binary dependencies on.
   * @param incToolOptions The component that manages generated class files.
   * @param reporter  A reporter where semantic compiler failures can be reported.
   * @param log       A place where we can log debugging/error messages.
   * @param progressOpt An optional compilation progress reporter to report
   *                    back what files are currently under compilation.
   */
  def compile(
      sources: Seq[VirtualFile],
      extraClasspath: Seq[VirtualFile],
      converter: FileConverter, // this is needed to thaw ref back to path for stamping
      options: Seq[String],
      output: Output,
      finalJarOutput: Option[Path],
      callback: AnalysisCallback,
      incToolOptions: IncToolOptions,
      reporter: XReporter,
      log: XLogger,
      progressOpt: Option[CompileProgress]
  ): Unit = {
    val sourceDirs = collection.mutable.Map.empty[Path, VirtualFileRef]

    if (sources.nonEmpty) {
      // Make the classpath absolute for Java compilation
      val absClasspath = (extraClasspath ++ classpath).map(VirtualFileUtil.toAbsolute)

      // Outline chunks of compiles so that .class files end up in right location
      val chunks: Map[Option[Path], Seq[VirtualFile]] = output match {
        case single: SingleOutput =>
          Map(Option(single.getOutputDirectoryAsPath) -> sources)
        case multi: MultipleOutput =>
          sources.groupBy { src =>
            multi.getOutputGroups
              .find { out =>
                val sourceDir: VirtualFileRef = sourceDirs.getOrElseUpdate(
                  out.getSourceDirectoryAsPath,
                  converter.toVirtualFile(out.getSourceDirectoryAsPath)
                )
                src.id.startsWith(sourceDir.id)
              }
              .map(_.getOutputDirectoryAsPath)
          }
      }

      // Report warnings about source files that have no output directory
      chunks.get(None) foreach { srcs =>
        val culpritPaths = srcs.map(_.id).mkString(", ")
        log.error(InterfaceUtil.toSupplier(s"No output directory mapped for: $culpritPaths"))
      }

      // Memoize the known class files in the Javac output location
      val memo = for { (Some(outputPath), srcs) <- chunks } yield {
        val classFinder =
          if (outputPath.toString.endsWith(".jar")) new JarClassFinder(outputPath)
          else new DirectoryClassFinder(outputPath)
        (classFinder, classFinder.classes.pathsAndClose(), srcs)
      }

      // Record progress for java compilation
      val somePhase = ""
      val noPhase = ""
      val javaCompilationPhase = "Java compilation"
      progressOpt.map { progress =>
        progress.startUnit(javaCompilationPhase, "")
        progress.advance(0, 2, somePhase, javaCompilationPhase)
      }

      timed(javaCompilationPhase, log) {
        val args = sbt.internal.inc.javac.JavaCompiler.commandArguments(
          absClasspath,
          converter,
          options,
          scalaInstance,
          classpathOptions
        )
        val javaSources: Array[VirtualFile] =
          sources.sortBy(_.id).toArray
        val success =
          javac.run(javaSources, args.toArray, output, incToolOptions, reporter, log)
        if (!success) {
          /* Assume that no Scalac problems are reported for a Javac-related
           * reporter. This relies on the incremental compiler will not run
           * Javac compilation if Scala compilation fails, which means that
           * the same reporter won't be used for `AnalyzingJavaCompiler`. */
          val msg = "javac returned non-zero exit code"
          throw new CompileFailed(args.toArray, msg, reporter.problems())
        }
      }

      // Read the API information from [[Class]] to analyze dependencies.
      def readAPI(source: VirtualFileRef, classes: Seq[Class[?]]): Set[(String, String)] = {
        val (apis, mainClasses, inherits) = ClassToAPI.process(classes)
        apis.foreach(callback.api(source, _))
        mainClasses.foreach(callback.mainClass(source, _))
        inherits.map {
          case (from, to) => (from.getName, to.getName)
        }
      }

      // Record progress for java analysis
      val javaAnalysisPhase = "Java analysis"
      progressOpt.map { progress =>
        progress.startUnit(javaAnalysisPhase, "")
        progress.advance(1, 2, javaCompilationPhase, javaAnalysisPhase)
      }
      // Construct class loader to analyze dependencies of generated class files
      val loader = ClasspathUtil.toLoader(
        output.getSingleOutputAsPath.toOption.toSeq ++
          (extraClasspath ++ searchClasspath).map(converter.toPath)
      )

      timed(javaAnalysisPhase, log) {
        for {
          (classFinder, oldClasses, srcs) <- memo
        } {
          val classes = classFinder.classes
          try {
            val newClasses = Set(classes.paths: _*) -- oldClasses
            JavaAnalyze(newClasses.toSeq, srcs, log, output, finalJarOutput)(
              callback,
              loader,
              readAPI
            )
          } finally classes.close()
        }
      }

      // After using the classloader it should be closed. Otherwise it will keep the accessed
      // jars open. Especially, when zinc is compiling directly to jar, that jar will be locked
      // not allowing to change it in further compilation cycles (on Windows).
      // This also affects jars in the classpath that come from dependency resolution.
      loader match {
        case u: URLClassLoader => u.close()
        case _                 => ()
      }

      // Report that we reached the end
      progressOpt.foreach { progress =>
        progress.advance(2, 2, javaAnalysisPhase, noPhase)
      }
    }
  }

  /**
   * Compile some java code using the current configured compiler. This
   * implements a method from `JavaCompiler` that will **not** perform
   * incremental compilation of any way.
   *
   * @note Don't use if you want incremental compilation.
   *
   * @param sources  The sources to compile
   * @param options  The options for the Java compiler
   * @param incToolOptions The component that manages generated class files.
   * @param reporter  A reporter where semantic compiler failures can be reported.
   * @param log       A place where we can log debugging/error messages.
   */
  override def run(
      sources: Array[VirtualFile],
      options: Array[String],
      output: Output,
      incToolOptions: IncToolOptions,
      reporter: XReporter,
      log: XLogger
  ): Boolean = javac.run(sources, options, output, incToolOptions, reporter, log)

  /** Time how long it takes to run various compilation tasks. */
  private[this] def timed[T](label: String, log: Logger)(t: => T): T = {
    val start = System.nanoTime
    val result = t
    val elapsed = System.nanoTime - start
    log.debug(label + " took " + (elapsed / 1e9) + " s")
    result
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy