-
-
Notifications
You must be signed in to change notification settings - Fork 644
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
Changes from 10 commits
0bbfb99
c44fdff
7ed6e08
2cb21f3
06cb12d
38a3655
94ba8e9
3bba4ca
1b8d8b8
f7a8e6f
98afdd7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
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 | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does not explicitly call |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 } | ||
|
@@ -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, | ||
|
@@ -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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't a good default for this arg: would recommend There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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._ | ||
|
||
|
@@ -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.") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/Either/At least one of/? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||
} | ||
|
||
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( | ||
|
@@ -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") | ||
} | ||
|
||
/** | ||
|
@@ -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" | ||
|
@@ -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)))), | ||
|
@@ -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 | ||
} | ||
} |
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', | ||
) |
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) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
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!There was a problem hiding this comment.
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.)