Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Enable straight-to-jar compilation #597

Merged
merged 29 commits into from
Oct 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7b2e998
Straight to jar compilation
lukaszwawrzyk Sep 12, 2018
5932814
Close classloader after analyzing generated classes
lukaszwawrzyk Oct 1, 2018
b9caefd
Initial reworks
lukaszwawrzyk Sep 18, 2018
9f833af
Change the way of injecting javac temp dir to keep binary compatibility
lukaszwawrzyk Sep 18, 2018
b4e54c3
Refactor JaredClass
lukaszwawrzyk Sep 18, 2018
0f54b6d
Added more comments
lukaszwawrzyk Sep 20, 2018
7e4cab5
Simplified product stamps
lukaszwawrzyk Sep 24, 2018
1c93dec
Rename STJ to JarUtils
lukaszwawrzyk Sep 27, 2018
a6aab45
Renamed JaredClass to ClassInJar
lukaszwawrzyk Sep 27, 2018
596d58c
Create JarUtils only once in CallbackGlobal
lukaszwawrzyk Sep 27, 2018
31484ad
Moved prev jar path generation and resolving to AnalysisCallback
lukaszwawrzyk Sep 28, 2018
a680bcd
Add xsbti.AnalysisCallback.previousJar to mima exceptions
lukaszwawrzyk Sep 28, 2018
65cc488
Extract compileToJar as a parameter to scripted tests
lukaszwawrzyk Oct 1, 2018
5f72534
Explicitly normalize path for windows case
lukaszwawrzyk Oct 1, 2018
1d3e60f
Improved error message in checkNoGeneratedClassFiles
lukaszwawrzyk Oct 1, 2018
17bf07b
map + getOrElse => match for readability
lukaszwawrzyk Oct 1, 2018
ff27cac
Changed pause in scripted to print copy friendly message
lukaszwawrzyk Oct 1, 2018
15f631e
Move jar content logic out of bridge
lukaszwawrzyk Oct 2, 2018
a3f31c0
Disable parallel test run for compilation to jar
lukaszwawrzyk Oct 2, 2018
c128200
Switch to IndexBasedZipFsOps to list class files
lukaszwawrzyk Oct 2, 2018
2ddcd8d
Parallelize scripted tests for --to-jar enabled and disabled
lukaszwawrzyk Oct 3, 2018
6257872
Add temporaryClassesDirectory field to CompileOptions
lukaszwawrzyk Oct 3, 2018
0bbc115
Fix binary issues
lukaszwawrzyk Oct 3, 2018
ffbef5d
Revert adding flag to disable compression when storing analysis
lukaszwawrzyk Oct 3, 2018
dffbfd0
Add docs and fix OutputJarContent
lukaszwawrzyk Oct 3, 2018
72639e1
Extract preparation and cleanpu phase for prev jar to methods
lukaszwawrzyk Oct 3, 2018
0130097
Add final modifications to straight-to-jar changes
jvican Oct 4, 2018
e9920ce
Make OutputJarContent a class to allow parallelism.
lukaszwawrzyk Oct 5, 2018
a588991
Use matrix for scripted tests
lukaszwawrzyk Oct 8, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .drone.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
matrix:
CI_SCALA_VERSION:
- 2.12.6
RUN_SCRIPTED:
- ./bin/run-ci-scripted.sh
- ./bin/run-ci-scripted-to-jar.sh

clone:
git:
Expand Down Expand Up @@ -30,6 +33,7 @@ pipeline:
- export DRONE_DIR="/drone"
- git fetch --tags && git log | head -n 20
- ./bin/run-ci.sh
- ${RUN_SCRIPTED}

rebuild_cache:
image: appleboy/drone-sftp-cache
Expand Down
9 changes: 9 additions & 0 deletions bin/run-ci-scripted-to-jar.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -eu
set -o nounset

PROJECT_ROOT="zincRoot"
sbt -Dfile.encoding=UTF-8 \
-J-XX:ReservedCodeCacheSize=512M \
-J-Xms1024M -J-Xmx4096M -J-server \
"zincScripted/test:run --to-jar"
9 changes: 9 additions & 0 deletions bin/run-ci-scripted.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -eu
set -o nounset

PROJECT_ROOT="zincRoot"
sbt -Dfile.encoding=UTF-8 \
-J-XX:ReservedCodeCacheSize=512M \
-J-Xms1024M -J-Xmx4096M -J-server \
"zincScripted/test:run"
3 changes: 1 addition & 2 deletions bin/run-ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,4 @@ sbt -Dfile.encoding=UTF-8 \
"$PROJECT_ROOT/test:compile" \
crossTestBridges \
"publishBridges" \
"$PROJECT_ROOT/test" \
"zincScripted/test:run"
"$PROJECT_ROOT/test"
28 changes: 25 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ lazy val zinc = (project in file("zinc"))
.settings(
name := "zinc",
mimaSettings,
mimaBinaryIssueFilters ++= Seq(
exclude[DirectMissingMethodProblem]("sbt.internal.inc.IncrementalCompilerImpl.compileIncrementally"),
exclude[DirectMissingMethodProblem]("sbt.internal.inc.IncrementalCompilerImpl.inputs"),
exclude[DirectMissingMethodProblem]("sbt.internal.inc.IncrementalCompilerImpl.compile"),
exclude[DirectMissingMethodProblem]("sbt.internal.inc.MixedAnalyzingCompiler.config"),
exclude[DirectMissingMethodProblem]("sbt.internal.inc.MixedAnalyzingCompiler.makeConfig"),
exclude[DirectMissingMethodProblem]("sbt.internal.inc.MixedAnalyzingCompiler.this"),
exclude[DirectMissingMethodProblem]("sbt.internal.inc.CompileConfiguration.this")
)
)

lazy val zincTesting = (project in internalPath / "zinc-testing")
Expand Down Expand Up @@ -281,7 +290,11 @@ lazy val zincCore = (project in internalPath / "zinc-core")
exclude[ReversedMissingMethodProblem]("sbt.internal.inc.IncrementalCommon.findClassDependencies"),
exclude[ReversedMissingMethodProblem]("sbt.internal.inc.IncrementalCommon.invalidateClassesInternally"),
exclude[ReversedMissingMethodProblem]("sbt.internal.inc.IncrementalCommon.invalidateClassesExternally"),
exclude[ReversedMissingMethodProblem]("sbt.internal.inc.IncrementalCommon.findAPIChange")
exclude[ReversedMissingMethodProblem]("sbt.internal.inc.IncrementalCommon.findAPIChange"),
exclude[IncompatibleMethTypeProblem]("sbt.internal.inc.Incremental.prune"),
exclude[DirectMissingMethodProblem]("sbt.internal.inc.IncrementalCompile.apply"),
exclude[DirectMissingMethodProblem]("sbt.internal.inc.AnalysisCallback#Builder.this"),
exclude[DirectMissingMethodProblem]("sbt.internal.inc.AnalysisCallback.this")
)
}
)
Expand Down Expand Up @@ -391,7 +404,10 @@ lazy val compilerInterface212 = (project in internalPath / "compiler-interface")
exclude[ReversedMissingMethodProblem]("xsbti.compile.ExternalHooks#Lookup.hashClasspath"),
exclude[ReversedMissingMethodProblem]("xsbti.compile.ScalaInstance.loaderLibraryOnly"),
exclude[DirectMissingMethodProblem]("xsbti.api.AnalyzedClass.of"),
exclude[DirectMissingMethodProblem]("xsbti.api.AnalyzedClass.create")
exclude[DirectMissingMethodProblem]("xsbti.api.AnalyzedClass.create"),
exclude[ReversedMissingMethodProblem]("xsbti.AnalysisCallback.classesInOutputJar"),
exclude[ReversedMissingMethodProblem]("xsbti.compile.IncrementalCompiler.compile"),
exclude[DirectMissingMethodProblem]("xsbti.compile.IncrementalCompiler.compile")
)
},
)
Expand Down Expand Up @@ -745,7 +761,10 @@ lazy val zincClassfile212 = zincClassfileTemplate
.settings(
scalaVersion := scala212,
crossScalaVersions := Seq(scala212),
target := (target in zincClassfileTemplate).value.getParentFile / "target-2.12"
target := (target in zincClassfileTemplate).value.getParentFile / "target-2.12",
mimaBinaryIssueFilters ++= Seq(
exclude[DirectMissingMethodProblem]("sbt.internal.inc.classfile.Analyze.apply")
)
)

// re-implementation of scripted engine
Expand Down Expand Up @@ -831,6 +850,7 @@ lazy val otherRootSettings = Seq(
Scripted.scriptedPrescripted := { (_: File) => () },
Scripted.scriptedUnpublished := scriptedUnpublishedTask.evaluated,
Scripted.scriptedSource := (sourceDirectory in zinc).value / "sbt-test",
Scripted.scriptedCompileToJar := false,
publishAll := {
val _ = (publishLocal).all(ScopeFilter(inAnyProject)).value
}
Expand All @@ -845,6 +865,7 @@ def scriptedTask: Def.Initialize[InputTask[Unit]] = Def.inputTask {
scriptedSource.value,
result,
scriptedBufferLog.value,
scriptedCompileToJar.value,
scriptedPrescripted.value
)
}
Expand All @@ -857,6 +878,7 @@ def scriptedUnpublishedTask: Def.Initialize[InputTask[Unit]] = Def.inputTask {
scriptedSource.value,
result,
scriptedBufferLog.value,
scriptedCompileToJar.value,
scriptedPrescripted.value
)
}
Expand Down
16 changes: 12 additions & 4 deletions internal/compiler-bridge/src/main/scala/xsbt/API.scala
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ final class API(val global: CallbackGlobal) extends Compat with GlobalHelpers wi
*
* This method only takes care of non-local classes because local classes have no
* relevance in the correctness of the algorithm and can be registered after genbcode.
* Local classes are only used to contruct the relations of products and to produce
* Local classes are only used to construct the relations of products and to produce
* the list of generated files + stamps, but names referring to local classes **never**
* show up in the name hashes of classes' APIs, hence never considered for name hashing.
*
Expand All @@ -116,9 +116,17 @@ final class API(val global: CallbackGlobal) extends Compat with GlobalHelpers wi
def registerProductNames(names: FlattenedNames): Unit = {
// Guard against a local class in case it surreptitiously leaks here
if (!symbol.isLocalClass) {
val classFileName = s"${names.binaryName}.class"
val outputDir = global.settings.outputDirs.outputDirFor(sourceFile).file
val classFile = new java.io.File(outputDir, classFileName)
val pathToClassFile = s"${names.binaryName}.class"
val classFile = {
JarUtils.outputJar match {
case Some(outputJar) =>
new java.io.File(JarUtils.classNameInJar(outputJar, pathToClassFile))
case None =>
val outputDir = global.settings.outputDirs.outputDirFor(sourceFile).file
new java.io.File(outputDir, pathToClassFile)
}
}

val zincClassName = names.className
val srcClassName = classNameAsString(symbol)
callback.generatedNonLocalClass(sourceJavaFile, classFile, zincClassName, srcClassName)
Expand Down
46 changes: 43 additions & 3 deletions internal/compiler-bridge/src/main/scala/xsbt/Analyzer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@

package xsbt

import java.io.File

import scala.tools.nsc.Phase
import scala.collection.JavaConverters._

object Analyzer {
def name = "xsbt-analyzer"
Expand All @@ -22,15 +25,39 @@ final class Analyzer(val global: CallbackGlobal) extends LocateClassFile {
"Finds concrete instances of provided superclasses, and application entry points."
def name = Analyzer.name

/**
* When straight-to-jar compilation is enabled, returns the classes
* that are found in the jar of the last compilation. This method
* gets the existing classes from the analysis callback and adapts
* it for consumption in the compiler bridge.
*
* It's lazy because it triggers a read of the zip, which may be
* unnecessary if there are no local classes in a compilation unit.
*/
private lazy val classesWrittenByGenbcode: Set[String] = {
JarUtils.outputJar match {
case Some(jar) =>
val classes = global.callback.classesInOutputJar().asScala
classes.map(JarUtils.classNameInJar(jar, _)).toSet
case None => Set.empty
}
}

def apply(unit: CompilationUnit): Unit = {
if (!unit.isJava) {
val sourceFile = unit.source.file.file
for (iclass <- unit.icode) {
val sym = iclass.symbol
val outputDir = settings.outputDirs.outputDirFor(sym.sourceFile).file
lazy val outputDir = settings.outputDirs.outputDirFor(sym.sourceFile).file
def addGenerated(separatorRequired: Boolean): Unit = {
val classFile = fileForClass(outputDir, sym, separatorRequired)
if (classFile.exists()) {
val locatedClass = {
JarUtils.outputJar match {
case Some(outputJar) => locateClassInJar(sym, outputJar, separatorRequired)
case None => locatePlainClassFile(sym, separatorRequired)
}
}

locatedClass.foreach { classFile =>
assert(sym.isClass, s"${sym.fullName} is not a class")
// Use own map of local classes computed before lambdalift to ascertain class locality
if (localToNonLocalClass.isLocal(sym).getOrElse(true)) {
Expand All @@ -49,5 +76,18 @@ final class Analyzer(val global: CallbackGlobal) extends LocateClassFile {
}
}
}

private def locatePlainClassFile(sym: Symbol, separatorRequired: Boolean): Option[File] = {
val outputDir = settings.outputDirs.outputDirFor(sym.sourceFile).file
val classFile = fileForClass(outputDir, sym, separatorRequired)
if (classFile.exists()) Some(classFile) else None
}

private def locateClassInJar(sym: Symbol, jar: File, sepRequired: Boolean): Option[File] = {
val classFile = pathToClassFile(sym, sepRequired)
val classInJar = JarUtils.classNameInJar(jar, classFile)
if (!classesWrittenByGenbcode.contains(classInJar)) None
else Some(new File(classInJar))
}
}
}
41 changes: 35 additions & 6 deletions internal/compiler-bridge/src/main/scala/xsbt/CallbackGlobal.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import scala.tools.nsc._
import io.AbstractFile
import java.io.File

import scala.reflect.io.PlainFile

/** Defines the interface of the incremental compiler hiding implementation details. */
sealed abstract class CallbackGlobal(settings: Settings,
reporter: reporters.Reporter,
Expand All @@ -38,6 +40,8 @@ sealed abstract class CallbackGlobal(settings: Settings,
}
}

lazy val JarUtils = new JarUtils(outputDirs)

/**
* Defines the sbt phase in which the dependency analysis is performed.
* The reason why this is exposed in the callback global is because it's used
Expand Down Expand Up @@ -134,19 +138,44 @@ sealed class ZincCompiler(settings: Settings, dreporter: DelegatingReporter, out

private final val fqnsToAssociatedFiles = perRunCaches.newMap[String, (AbstractFile, Boolean)]()

/** Returns the associated file of a fully qualified name and whether it's on the classpath. */
/**
* Returns the associated file of a fully qualified name and whether it's on the classpath.
* Note that the abstract file returned must exist.
*/
def findAssociatedFile(fqn: String): Option[(AbstractFile, Boolean)] = {
def getOutputClass(name: String): Option[AbstractFile] = {
// This could be improved if a hint where to look is given.
val className = name.replace('.', '/') + ".class"
outputDirs.map(new File(_, className)).find((_.exists)).map((AbstractFile.getFile(_)))
def findOnPreviousCompilationProducts(name: String): Option[AbstractFile] = {
// This class file path is relative to the output jar/directory and computed from class name
val classFilePath = name.replace('.', '/') + ".class"

JarUtils.outputJar match {
case Some(outputJar) =>
if (!callback.classesInOutputJar().contains(classFilePath)) None
else {
/*
* Important implementation detail: `classInJar` has the format of `$JAR!$CLASS_REF`
* which is, of course, a path to a file that does not exist. This file path is
* interpreted especially by Zinc to decompose the format under straight-to-jar
* compilation. For this strategy to work, `PlainFile` must **not** check that
* this file does exist or not because, if it does, it will return `null` in
* `processExternalDependency` and the dependency will not be correctly registered.
* If scalac breaks this contract (the check for existence is done when creating
* a normal reflect file but not a plain file), Zinc will not work correctly.
*/
Some(new PlainFile(JarUtils.classNameInJar(outputJar, classFilePath)))
}

case None => // The compiler outputs class files in a classes directory (the default)
// This lookup could be improved if a hint where to look is given.
outputDirs.map(new File(_, classFilePath)).find(_.exists()).map(AbstractFile.getFile(_))
}
}

def findOnClassPath(name: String): Option[AbstractFile] =
classPath.findClass(name).flatMap(_.binary.asInstanceOf[Option[AbstractFile]])

fqnsToAssociatedFiles.get(fqn).orElse {
val newResult = getOutputClass(fqn).map(f => (f, true))
val newResult = findOnPreviousCompilationProducts(fqn)
.map(f => (f, true))
.orElse(findOnClassPath(fqn).map(f => (f, false)))
newResult.foreach(res => fqnsToAssociatedFiles.put(fqn, res))
newResult
Expand Down
37 changes: 37 additions & 0 deletions internal/compiler-bridge/src/main/scala/xsbt/JarUtils.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package xsbt

import java.io.File

/**
* This is a utility class that provides a set of functions that
* are used to implement straight to jar compilation.
*
* [[sbt.internal.inc.JarUtils]] is an object that has similar purpose and
* duplicates some of the code, as it is difficult to share it. Any change
* in the logic of this file must be applied to the other `JarUtils` too!
*/
final class JarUtils(outputDirs: Iterable[File]) {
// This is an equivalent of asking if it runs on Windows where the separator is `\`
private val isSlashSeparator: Boolean = File.separatorChar == '/'

/**
* The jar file that is used as output for classes. If the output is
* not set to a single .jar file, value of this field is [[None]].
*/
val outputJar: Option[File] = {
outputDirs match {
case Seq(file) if file.getName.endsWith(".jar") => Some(file)
case _ => None
}
}

/**
* Creates an identifier for a class located inside a jar.
*
* It follows the format to encode inter-jar dependencies that
* is established in [[sbt.internal.inc.JarUtils.ClassInJar]].
*/
def classNameInJar(jar: File, classFilePath: String): String = {
s"$jar!${if (isSlashSeparator) classFilePath else classFilePath.replace('/', File.separatorChar)}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,7 @@ abstract class LocateClassFile extends Compat with ClassName {

protected def fileForClass(outputDirectory: File, s: Symbol, separatorRequired: Boolean): File =
new File(outputDirectory, flatclassName(s, File.separatorChar, separatorRequired) + ".class")

protected def pathToClassFile(s: Symbol, separatorRequired: Boolean): String =
flatclassName(s, File.separatorChar, separatorRequired) + ".class"
}
Loading