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

sbt_inc.SbtIncrementalCompiler Maven / Gradle / Ivy

/*
 * This is free and unencumbered software released into the public domain.
 * See UNLICENSE.
 */
package sbt_inc;

import static scala.jdk.CollectionConverters.IterableHasAsScala;
import static scala.jdk.FunctionWrappers.FromJavaConsumer;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.logging.Log;
import sbt.internal.inc.*;
import sbt.internal.inc.FileAnalysisStore;
import sbt.internal.inc.ScalaInstance;
import sbt.io.AllPassFilter$;
import sbt.io.IO;
import sbt.util.Logger;
import scala.Option;
import scala.Tuple2;
import scala_maven.MavenArtifactResolver;
import util.FileUtils;
import xsbti.PathBasedFile;
import xsbti.T2;
import xsbti.VirtualFile;
import xsbti.compile.*;

public class SbtIncrementalCompiler {

  private static final String SBT_GROUP_ID = "org.scala-sbt";
  private static final String SBT_GROUP_ID_SCALA3 = "org.scala-lang";
  private static final String JAVA_CLASS_VERSION = System.getProperty("java.class.version");
  private static final File DEFAULT_SECONDARY_CACHE_DIR =
      Paths.get(System.getProperty("user.home"), ".sbt", "1.0", "zinc", "org.scala-sbt").toFile();

  private final IncrementalCompiler compiler = ZincUtil.defaultIncrementalCompiler();
  private final CompileOrder compileOrder;
  private final Logger logger;
  private final Compilers compilers;
  private final Setup setup;
  private final AnalysisStore analysisStore;
  private final MavenArtifactResolver resolver;
  private final File secondaryCacheDir;

  public SbtIncrementalCompiler(
      File javaHome,
      MavenArtifactResolver resolver,
      File secondaryCacheDir,
      Log mavenLogger,
      File cacheFile,
      CompileOrder compileOrder,
      ScalaInstance scalaInstance)
      throws Exception {
    this.compileOrder = compileOrder;
    this.logger = new SbtLogger(mavenLogger);
    mavenLogger.info("Using incremental compilation using " + compileOrder + " compile order");
    this.resolver = resolver;
    this.secondaryCacheDir =
        secondaryCacheDir != null ? secondaryCacheDir : DEFAULT_SECONDARY_CACHE_DIR;
    this.secondaryCacheDir.mkdirs();

    File compilerBridgeJar = getCompiledBridgeJar(scalaInstance, mavenLogger);

    ScalaCompiler scalaCompiler =
        new AnalyzingCompiler(
            scalaInstance, // scalaInstance
            ZincCompilerUtil.constantBridgeProvider(scalaInstance, compilerBridgeJar), // provider
            ClasspathOptionsUtil.auto(), // classpathOptions
            new FromJavaConsumer(noop -> {}), // onArgsHandler
            Option.apply(null) // classLoaderCache
            );

    compilers =
        ZincUtil.compilers(
            scalaInstance,
            ClasspathOptionsUtil.boot(),
            Option.apply(javaHome.toPath()),
            scalaCompiler);

    PerClasspathEntryLookup lookup =
        new PerClasspathEntryLookup() {
          @Override
          public Optional analysis(VirtualFile classpathEntry) {
            Path path = ((PathBasedFile) classpathEntry).toPath();

            String analysisStoreFileName = null;
            if (Files.isDirectory(path)) {
              if (path.getFileName().toString().equals("classes")) {
                analysisStoreFileName = "compile";

              } else if (path.getFileName().toString().equals("test-classes")) {
                analysisStoreFileName = "test-compile";
              }
            }

            if (analysisStoreFileName != null) {
              File analysisStoreFile =
                  path.getParent().resolve("analysis").resolve(analysisStoreFileName).toFile();
              if (analysisStoreFile.exists()) {
                return AnalysisStore.getCachedStore(FileAnalysisStore.binary(analysisStoreFile))
                    .get()
                    .map(AnalysisContents::getAnalysis);
              }
            }
            return Optional.empty();
          }

          @Override
          public DefinesClass definesClass(VirtualFile classpathEntry) {
            return classpathEntry.name().equals("rt.jar")
                ? className -> false
                : Locate.definesClass(classpathEntry);
          }
        };

    analysisStore = AnalysisStore.getCachedStore(FileAnalysisStore.binary(cacheFile));

    setup =
        Setup.of(
            lookup, // lookup
            false, // skip
            cacheFile, // cacheFile
            CompilerCache.fresh(), // cache
            IncOptions.of(), // incOptions
            new LoggedReporter(100, logger, pos -> pos), // reporter
            Optional.empty(), // optionProgress
            new T2[] {});
  }

  private PreviousResult previousResult() {
    Optional analysisContents = analysisStore.get();
    if (analysisContents.isPresent()) {
      AnalysisContents analysisContents0 = analysisContents.get();
      CompileAnalysis previousAnalysis = analysisContents0.getAnalysis();
      MiniSetup previousSetup = analysisContents0.getMiniSetup();
      return PreviousResult.of(Optional.of(previousAnalysis), Optional.of(previousSetup));
    } else {
      return PreviousResult.of(Optional.empty(), Optional.empty());
    }
  }

  public void compile(
      Set classpathElements,
      List sources,
      Path classesDirectory,
      List scalacOptions,
      List javacOptions) {
    List fullClasspath = new ArrayList<>();
    fullClasspath.add(classesDirectory);
    fullClasspath.addAll(classpathElements);

    CompileOptions options =
        CompileOptions.of(
            fullClasspath.stream()
                .map(PlainVirtualFile::new)
                .toArray(VirtualFile[]::new), // classpath
            sources.stream().map(PlainVirtualFile::new).toArray(VirtualFile[]::new), // sources
            classesDirectory, //
            scalacOptions.toArray(new String[] {}), // scalacOptions
            javacOptions.toArray(new String[] {}), // javacOptions
            100, // maxErrors
            pos -> pos, // sourcePositionMappers
            compileOrder, // order
            Optional.empty(), // temporaryClassesDirectory
            Optional.empty(), // _converter
            Optional.empty(), // _stamper
            Optional.empty() // _earlyOutput
            );

    Inputs inputs = Inputs.of(compilers, options, setup, previousResult());

    CompileResult newResult = compiler.compile(inputs, logger);
    analysisStore.set(AnalysisContents.create(newResult.analysis(), newResult.setup()));
  }

  private String compilerBridgeArtifactId(String scalaVersion) {
    if (scalaVersion.startsWith("2.10.")) {
      return "compiler-bridge_2.10";
    } else if (scalaVersion.startsWith("2.11.")) {
      return "compiler-bridge_2.11";
    } else if (scalaVersion.startsWith("2.12.") || scalaVersion.equals("2.13.0-M1")) {
      return "compiler-bridge_2.12";
    } else if (scalaVersion.startsWith("2.13.")) {
      return "compiler-bridge_2.13";
    } else {
      return "scala3-sbt-bridge";
    }
  }

  private static List> computeZipEntries(List paths, Path rootDir) {
    int rootDirLength = rootDir.toString().length();
    Stream> stream =
        paths.stream()
            .map(
                path -> {
                  String zipPath =
                      path.toString().substring(rootDirLength + 1).replace(File.separator, "/");
                  if (Files.isDirectory(path)) {
                    zipPath = zipPath + "/";
                  }
                  return new Tuple2(path.toFile(), zipPath);
                });
    return stream.collect(Collectors.toList());
  }

  private File getCompiledBridgeJar(ScalaInstance scalaInstance, Log mavenLogger) throws Exception {

    // eg
    // org.scala-sbt-compiler-bridge_2.12-1.2.4-bin_2.12.10__52.0-1.2.4_20181015T090407.jar
    String bridgeArtifactId = compilerBridgeArtifactId(scalaInstance.actualVersion());

    boolean isScala3 = scalaInstance.actualVersion().startsWith("3");

    // this file is localed in compiler-interface
    Properties properties = new Properties();
    try (InputStream is =
        getClass().getClassLoader().getResourceAsStream("incrementalcompiler.version.properties")) {
      properties.load(is);
    }

    String zincVersion = properties.getProperty("version");
    String timestamp = properties.getProperty("timestamp");

    String groupId = isScala3 ? SBT_GROUP_ID_SCALA3 : SBT_GROUP_ID;
    String version = isScala3 ? scalaInstance.actualVersion() : zincVersion;

    if (isScala3) {
      return resolver.getJar(groupId, bridgeArtifactId, version, "").getFile();
    }

    String cacheFileName =
        groupId
            + '-'
            + bridgeArtifactId
            + '-'
            + version
            + "-bin_"
            + scalaInstance.actualVersion()
            + "__"
            + JAVA_CLASS_VERSION
            + '-'
            + version
            + '_'
            + timestamp
            + ".jar";

    File cachedCompiledBridgeJar = new File(secondaryCacheDir, cacheFileName);

    if (mavenLogger.isInfoEnabled()) {
      mavenLogger.info("Compiler bridge file: " + cachedCompiledBridgeJar);
    }

    if (!cachedCompiledBridgeJar.exists()) {
      mavenLogger.info("Compiler bridge file is not installed yet");
      // compile and install
      RawCompiler rawCompiler = new RawCompiler(scalaInstance, ClasspathOptionsUtil.auto(), logger);

      File bridgeSources = resolver.getJar(groupId, bridgeArtifactId, version, "sources").getFile();

      Set bridgeSourcesDependencies =
          resolver.getJarAndDependencies(groupId, bridgeArtifactId, version, "sources").stream()
              .filter(
                  artifact ->
                      artifact.getScope() != null && !artifact.getScope().equals("provided"))
              .map(Artifact::getFile)
              .map(File::toPath)
              .collect(Collectors.toSet());

      bridgeSourcesDependencies.addAll(
          Arrays.stream(scalaInstance.allJars())
              .sequential()
              .map(File::toPath)
              .collect(Collectors.toList()));

      Path sourcesDir = Files.createTempDirectory("scala-maven-plugin-compiler-bridge-sources");
      Path classesDir = Files.createTempDirectory("scala-maven-plugin-compiler-bridge-classes");

      IO.unzip(bridgeSources, sourcesDir.toFile(), AllPassFilter$.MODULE$, true);

      List bridgeSourcesScalaFiles =
          FileUtils.listDirectoryContent(
              sourcesDir,
              file ->
                  Files.isRegularFile(file) && file.getFileName().toString().endsWith(".scala"));
      List bridgeSourcesNonScalaFiles =
          FileUtils.listDirectoryContent(
              sourcesDir,
              file ->
                  Files.isRegularFile(file)
                      && !file.getFileName().toString().endsWith(".scala")
                      && !file.getFileName().toString().equals("MANIFEST.MF"));

      try {
        rawCompiler.apply(
            IterableHasAsScala(bridgeSourcesScalaFiles).asScala().toSeq(), // sources:Seq[File]
            IterableHasAsScala(bridgeSourcesDependencies).asScala().toSeq(), // classpath:Seq[File],
            classesDir, // outputDirectory:Path,
            IterableHasAsScala(Collections.emptyList())
                .asScala()
                .toSeq() // options:Seq[String]
            );

        Manifest manifest = new Manifest();
        Path sourcesManifestFile = sourcesDir.resolve("META-INF").resolve("MANIFEST.MF");
        try (InputStream is = new FileInputStream(sourcesManifestFile.toFile())) {
          manifest.read(is);
        }

        List> scalaCompiledClasses =
            computeZipEntries(FileUtils.listDirectoryContent(classesDir, file -> true), classesDir);
        List> resources =
            computeZipEntries(bridgeSourcesNonScalaFiles, sourcesDir);
        List> allZipEntries = new ArrayList<>();
        allZipEntries.addAll(scalaCompiledClasses);
        allZipEntries.addAll(resources);

        IO.jar(
            IterableHasAsScala(
                    allZipEntries.stream()
                        .map(x -> scala.Tuple2.apply(x._1, x._2))
                        .collect(Collectors.toList()))
                .asScala(),
            cachedCompiledBridgeJar,
            manifest);

        mavenLogger.info("Compiler bridge installed");

      } finally {
        FileUtils.deleteDirectory(sourcesDir);
        FileUtils.deleteDirectory(classesDir);
      }
    }

    return cachedCompiledBridgeJar;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy