Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add functionality to create jar in zinc wrapper #6094

Merged
merged 11 commits into from
Jul 24, 2018
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,6 @@ def compile(self, ctx, args, classpath, upstream_analysis,
self._verify_zinc_classpath(upstream_analysis.keys())

zinc_args = []

zinc_args.extend([
'-log-level', self.get_options().level,
'-analysis-cache', ctx.analysis_file,
Expand Down
6 changes: 1 addition & 5 deletions src/scala/org/pantsbuild/zinc/compiler/InputUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,10 @@

package org.pantsbuild.zinc.compiler

import java.io.{File, IOException}
import java.lang.{ Boolean => JBoolean }
import java.io.{File}
import java.util.function.{ Function => JFunction }
import java.util.{ List => JList, Map => JMap }

import scala.collection.JavaConverters._
import scala.compat.java8.OptionConverters._
import scala.util.matching.Regex

import sbt.io.IO
import sbt.util.Logger
Expand Down
8 changes: 8 additions & 0 deletions src/scala/org/pantsbuild/zinc/compiler/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ object Main {
}

log.info("Compile success " + Util.timing(startTime))

// if compile is successful, jar the contents of classesDirectory and copy to outputJar
if (settings.outputJar.isDefined) {
val outputJarPath = settings.outputJar.get.toPath
val classesDirectory = settings.classesDirectory
log.debug("Creating JAR at %s, for files at %s" format (outputJarPath, classesDirectory))
OutputUtils.createClassesJar(classesDirectory, outputJarPath, settings.creationTime)
}
} catch {
case e: CompileFailed =>
log.error("Compile failed " + Util.timing(startTime))
Expand Down
117 changes: 117 additions & 0 deletions src/scala/org/pantsbuild/zinc/compiler/OutputUtils.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Copyright (C) 2012 Typesafe, Inc. <http://www.typesafe.com>
*/

package org.pantsbuild.zinc.compiler

import java.io.File
import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.{FileVisitResult, Files, Path, Paths, SimpleFileVisitor}
import java.util.jar.{JarEntry, JarInputStream, JarOutputStream}
import scala.annotation.tailrec
import scala.collection.mutable

object OutputUtils {

/**
* Sort the contents of the `dir` in lexicographic order.
*
* @param dir File handle containing the contents to sort
* @return sorted set of all paths within the `dir`
*/
def sort(dir:File): mutable.TreeSet[(Path, Boolean)] = {
val sorted = new mutable.TreeSet[(Path, Boolean)]()

val fileSortVisitor = new SimpleFileVisitor[Path]() {
override def preVisitDirectory(path: Path, attrs: BasicFileAttributes): FileVisitResult = {
sorted.add(path, false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eek. Sorry. Just looked at the javadocs for this: https://docs.oracle.com/javase/7/docs/api/java/util/zip/ZipEntry.html#isDirectory()

So the boolean isn't really necessary as long as you append a / here, and then later check whether it ends with a slash. Sorry!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(...and the slash is required in order to not confuse consumers of the jar.)

FileVisitResult.CONTINUE
}

override def visitFile(path: Path, attrs: BasicFileAttributes): FileVisitResult = {
sorted.add(path, true)
FileVisitResult.CONTINUE
}
}

Files.walkFileTree(dir.toPath, fileSortVisitor)
sorted
}

def relativize(base: String, path: Path): String = {
new File(base.toString).toURI().relativize(new File(path.toString).toURI()).getPath()
}

/**
* Create a JAR of of filePaths provided.
*
* @param filePaths set of all paths to be added to the JAR
* @param outputJarPath Absolute Path to the output JAR being created
* @param jarEntryTime time to be set for each JAR entry
*/
def createJar(
base: String, filePaths: mutable.TreeSet[(Path, Boolean)], outputJarPath: Path, jarEntryTime: Long) {

val target = new JarOutputStream(Files.newOutputStream(outputJarPath))

def addToJar(source: (Path, Boolean), entryName: String): FileVisitResult = {
val jarEntry = new JarEntry(entryName)
// setting jarEntry time to a fixed value for all entries within the jar so that jars are
// byte-for-byte reproducible.
jarEntry.setTime(jarEntryTime)

target.putNextEntry(jarEntry)
if (source._2) {
Files.copy(source._1, target)
}
target.closeEntry()
FileVisitResult.CONTINUE
}

val pathToName = filePaths.zipWithIndex.map{case(k, v) => (k, relativize(base, k._1))}.toMap
pathToName.map(e => addToJar(e._1, e._2))
target.close()
}

/**
* Jar the contents of output classes (settings.classesDirectory) and copy to settings.outputJar
*
*/
def createClassesJar(classesDirectory: File, outputJarPath: Path, jarCreationTime: Long) = {

// Sort the contents of the classesDirectory for deterministic jar creation
val sortedClasses = sort(classesDirectory)

createJar(classesDirectory.toString, sortedClasses, outputJarPath, jarCreationTime)
}

/**
* Determines if a file exists in a JAR provided.
*
* @param jarPath Absolute Path to the JAR being inspected
* @param fileName Name of the file, the existence of which is to be inspected
* @return
*/
def existsClass(jarPath: Path, fileName: String): Boolean = {
var jis: JarInputStream = null
var found = false
try {
jis = new JarInputStream(Files.newInputStream(jarPath))

@tailrec
def findClass(entry: JarEntry): Boolean = entry match {
case null =>
false
case entry if entry.getName == fileName =>
true
case _ =>
findClass(jis.getNextJarEntry)
}

found = findClass(jis.getNextJarEntry)
} finally {
jis.close()
}
found
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not explicitly call close on the JarOutputStream, but should.

}
34 changes: 24 additions & 10 deletions src/scala/org/pantsbuild/zinc/compiler/Settings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
package org.pantsbuild.zinc.compiler

import java.io.File
import java.nio.file.Path
import sbt.internal.util.{ ConsoleLogger, ConsoleOut }
import java.nio.file.{Files, Path}
import java.lang.{ Boolean => JBoolean }
import java.util.function.{ Function => JFunction }
import java.util.{ List => JList, logging => jlogging }
Expand Down Expand Up @@ -38,6 +39,7 @@ case class Settings(
_sources: Seq[File] = Seq.empty,
classpath: Seq[File] = Seq.empty,
_classesDirectory: Option[File] = None,
outputJar: Option[File] = None,
scala: ScalaLocation = ScalaLocation(),
scalacOptions: Seq[String] = Seq.empty,
javaHome: Option[File] = None,
Expand All @@ -47,7 +49,8 @@ case class Settings(
compileOrder: CompileOrder = CompileOrder.Mixed,
sbt: SbtJars = SbtJars(),
_incOptions: IncOptions = IncOptions(),
analysis: AnalysisOptions = AnalysisOptions()
analysis: AnalysisOptions = AnalysisOptions(),
creationTime: Long = System.currentTimeMillis()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a good default for this arg: would recommend 0 (the unix epoch) or something...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if we want the jars to be deterministic (each JAR entry to have the same creation time), the default needs to be consistent for each JAR created but not the same as every other JAR created. lmk if I misunderstand

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but not the same as every other JAR created

Ideally "when" you create the jar does not matter at all. For the same inputs, creating the same jar twice (one+ second apart) should result in the exact same jar. Hence 0 here.

) {
import Settings._

Expand All @@ -56,13 +59,13 @@ case class Settings(
}

lazy val sources: Seq[File] = _sources map normalise
if (_classesDirectory.isEmpty && outputJar.isEmpty) {
throw new RuntimeException(
s"Either ${Settings.DestinationOpt} or ${Settings.JarDestinationOpt} option is required.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Either/At least one of/?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either is accurate here.

}

lazy val classesDirectory: File =
normalise(
_classesDirectory.getOrElse {
throw new RuntimeException(s"The ${Settings.ZincCacheDirOpt} option is required.")
}
)
normalise(_classesDirectory.getOrElse(defaultClassesDirectory()))

lazy val incOptions: IncOptions = {
_incOptions.copy(
Expand All @@ -88,15 +91,15 @@ case class ConsoleOptions(
) {
def javaLogLevel: jlogging.Level = logLevel match {
case Level.Info =>
jlogging.Level.INFO
jlogging.Level.INFO
case Level.Warn =>
jlogging.Level.WARNING
case Level.Error =>
jlogging.Level.SEVERE
jlogging.Level.SEVERE
case Level.Debug =>
jlogging.Level.FINE
case x =>
sys.error(s"Unsupported log level: $x")
sys.error(s"Unsupported log level: $x")
}

/**
Expand Down Expand Up @@ -214,6 +217,7 @@ case class IncOptions(

object Settings extends OptionSet[Settings] {
val DestinationOpt = "-d"
val JarDestinationOpt = "-jar"
val ZincCacheDirOpt = "-zinc-cache-dir"
val CompilerBridgeOpt = "-compiler-bridge"
val CompilerInterfaceOpt = "-compiler-interface"
Expand Down Expand Up @@ -243,6 +247,8 @@ object Settings extends OptionSet[Settings] {
header("Compile options:"),
path( ("-classpath", "-cp"), "path", "Specify the classpath", (s: Settings, cp: Seq[File]) => s.copy(classpath = cp)),
file( DestinationOpt, "directory", "Destination for compiled classes", (s: Settings, f: File) => s.copy(_classesDirectory = Some(f))),
file( JarDestinationOpt, "directory", "Jar destination for compiled classes", (s: Settings, f: File) => s.copy(outputJar = Some(f))),
long("-jar-creation-time", "n", "Creation timestamp for compiled jars, default is current time", (s: Settings, l: Long) => s.copy(creationTime = l)),

header("Scala options:"),
file( "-scala-home", "directory", "Scala home directory (for locating jars)", (s: Settings, f: File) => s.copy(scala = s.scala.copy(home = Some(f)))),
Expand Down Expand Up @@ -314,4 +320,12 @@ object Settings extends OptionSet[Settings] {
def defaultBackupLocation(classesDir: File) = {
classesDir.getParentFile / "backup" / classesDir.getName
}

/**
* If a settings.classesDirectory option isnt specified, create a temporary directory for output
* classes to be written to.
*/
def defaultClassesDirectory(): File = {
Files.createTempDirectory("temp-zinc-classes").toFile
}
}
1 change: 1 addition & 0 deletions src/scala/org/pantsbuild/zinc/options/OptionSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ trait OptionSet[T] {
def boolean(opts: (String, String), desc: String, action: T => T) = new BooleanOption[T](Seq(opts._1, opts._2), desc, action)
def string(opt: String, arg: String, desc: String, action: (T, String) => T) = new StringOption[T](Seq(opt), arg, desc, action)
def int(opt: String, arg: String, desc: String, action: (T, Int) => T) = new IntOption[T](Seq(opt), arg, desc, action)
def long(opt: String, arg: String, desc: String, action: (T, Long) => T) = new LongOption[T](Seq(opt), arg, desc, action)
def double(opt: String, arg: String, desc: String, action: (T, Double) => T) = new DoubleOption[T](Seq(opt), arg, desc, action)
def fraction(opt: String, arg: String, desc: String, action: (T, Double) => T) = new FractionOption[T](Seq(opt), arg, desc, action)
def file(opt: String, arg: String, desc: String, action: (T, File) => T) = new FileOption[T](Seq(opt), arg, desc, action)
Expand Down
12 changes: 12 additions & 0 deletions src/scala/org/pantsbuild/zinc/options/Options.scala
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ extends ArgumentOption[Int, Context] {
}
}

class LongOption[Context](
val options: Seq[String],
val argument: String,
val description: String,
val action: (Context, Long) => Context)
extends ArgumentOption[Long, Context] {
def parse(arg: String): Option[Long] = {
try { Some(arg.toLong) }
catch { case _: NumberFormatException => None }
}
}

class DoubleOption[Context](
val options: Seq[String],
val argument: String,
Expand Down
12 changes: 12 additions & 0 deletions tests/scala/org/pantsbuild/zinc/compiler/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright 2018 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

junit_tests(
dependencies=[
'3rdparty/jvm/org/scala-sbt:io',
'3rdparty:scalatest',
'src/scala/org/pantsbuild/zinc/compiler',
],
strict_deps=True,
platform='java8',
)
63 changes: 63 additions & 0 deletions tests/scala/org/pantsbuild/zinc/compiler/JarCreationSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.pantsbuild.zinc.compiler

import sbt.io.IO

import java.io.File
import java.nio.file.{Files, Path, Paths}
import scala.collection.mutable
import org.junit.runner.RunWith
import org.scalatest.WordSpec
import org.scalatest.junit.JUnitRunner
import org.scalatest.MustMatchers

@RunWith(classOf[JUnitRunner])
class JarCreationSpec extends WordSpec with MustMatchers {
"JarCreationWithoutClasses" should {
"succeed when input classes are not provided" in {
IO.withTemporaryDirectory { tempInputDir =>
val filePaths = new mutable.TreeSet[(Path, Boolean)]()

IO.withTemporaryDirectory { tempOutputDir =>
val jarOutputPath = Paths.get(tempOutputDir.toString, "spec-empty-output.jar")

OutputUtils.createJar(tempInputDir.toString, filePaths, jarOutputPath, System.currentTimeMillis())
OutputUtils.existsClass(jarOutputPath, "NonExistent.class") must be(false)
}
}
}
}
"JarCreationWithClasses" should {
"succeed when input classes are provided" in {
IO.withTemporaryDirectory { tempInputDir =>
val tempFile = File.createTempFile("Temp", ".class", tempInputDir)
val filePaths = mutable.TreeSet((tempFile.toPath, true))

IO.withTemporaryDirectory { tempOutputDir =>
val jarOutputPath = Paths.get(tempOutputDir.toString, "spec-valid-output.jar")

OutputUtils.createJar(tempInputDir.toString, filePaths, jarOutputPath, System.currentTimeMillis())
OutputUtils.existsClass(jarOutputPath, tempFile.toString) must be(false)
OutputUtils.existsClass(jarOutputPath, OutputUtils.relativize(tempInputDir.toString, tempFile.toPath)) must be(true)
}
}
}
}

"JarCreationWithNestedClasses" should {
"succeed when nested input directory and classes are provided" in {
IO.withTemporaryDirectory { tempInputDir =>
val nestedTempDir = Files.createTempDirectory(tempInputDir.toPath, "tmp")
val nestedTempClass = File.createTempFile("NestedTemp", ".class", nestedTempDir.toFile)
val filePaths = mutable.TreeSet((nestedTempDir, false), (nestedTempClass.toPath, true))
IO.withTemporaryDirectory { tempOutputDir =>
val jarOutputPath = Paths.get(tempOutputDir.toString, "spec-valid-output.jar")

OutputUtils.createJar(tempInputDir.toString, filePaths, jarOutputPath, System.currentTimeMillis())
OutputUtils.existsClass(jarOutputPath, OutputUtils.relativize(tempInputDir.toString, nestedTempDir)) must be(true)
OutputUtils.existsClass(jarOutputPath, OutputUtils.relativize(tempInputDir.toString, nestedTempClass.toPath)) must be(true)
}
}
}
}
}