-
-
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
Conversation
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.
Looks great :) Didn't manage to plug this into the python code, but will hopefully try this out soon :)
|
||
val jarPath = Paths.get(classesDirectory.toString, settings.outputJar.toString) | ||
val target = new JarOutputStream(Files.newOutputStream(jarPath)) | ||
val entryTime = System.currentTimeMillis() |
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.
We should probably either have a flag to set the time to use (so that pants can set a fixed value, maybe falling back to the current time if no value is specified), or always set this to a fixed value, for determinism.
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.
absolutely!
FileVisitResult.CONTINUE | ||
} | ||
|
||
Files.walkFileTree(classesDirectory.toPath, FileVisitor) |
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.
IIRC walkFileTree makes no guarantees about which order it visits files; we may want to manually list directory contents, sort them, and visit in sorted order, for determinism
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.
+1
@@ -57,12 +58,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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
I think either
is accurate here.
Unfortunately, doing that requires doing a local publish: there are instructions in the "dry-run" section here: https://www.pantsbuild.org/release_jvm.html#dry-run |
1 similar comment
Unfortunately, doing that requires doing a local publish: there are instructions in the "dry-run" section here: https://www.pantsbuild.org/release_jvm.html#dry-run |
/** | ||
* Jar the contents of output classes (settings.classesDirectory) and copy to settings.outputJar | ||
*/ | ||
def createClassesJar(settings: Settings, log: Logger) = { |
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.
It might be a good idea to put this in a separate object
somewhere (could preserve our silly naming convention here and call it OutputUtil
or something, heh).
jarPath | ||
} | ||
|
||
val jarPath = Paths.get(classesDirectory.toString, settings.outputJar.toString) |
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.
...I really can't believe that this is the recommended way to join paths in Java. But it is!
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.
I've found the ammonite ops library very neat for hygienic path operations (it has implicits that let you do e.g. Path(classesDirectory) / "intermediate-folder-i-just-made-up-for-this-example" / RelPath(settings.outputJar)
). Its process creation mechanism isn't as useful as scala.sys.process
for general use though, imho (although quite concise for synchronous process execution for testing). Not sure if I'm missing any reason that can't be used here.
*/ | ||
def createClassesJar(settings: Settings, log: Logger) = { | ||
val classesDirectory = settings.classesDirectory | ||
object FileVisitor extends SimpleFileVisitor[Path]() { |
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.
Since this has mutable state inside, I'd recommend doing something like:
val jarCaptureVisitier = new SimpleFileVisitor[Path]() {
...
}
FileVisitResult.CONTINUE | ||
} | ||
|
||
Files.walkFileTree(classesDirectory.toPath, FileVisitor) |
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.
+1
@@ -57,12 +58,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 comment
The reason will be displayed to describe this comment to others. Learn more.
I think either
is accurate here.
Just saw this and realized why my test locally wasnt working. Since we need to publish, is there any way to write an integration test for this? |
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.
Looks great!
I definitely defer to @stuhood for how to write tests here :)
@@ -121,6 +121,11 @@ object Main { | |||
} | |||
|
|||
log.info("Compile success " + Util.timing(startTime)) | |||
|
|||
// TODO(ity): if compile successful, jar the contents of classesDirectory and copy to outputJar |
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.
Drop TODO?
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.
Thanks Ity. Please add a quick unit test and then this should be good.
FileVisitResult.CONTINUE | ||
} | ||
sorted.map(createJar(_)) | ||
} |
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.
This does not explicitly call close
on the JarOutputStream
, but should.
// deterministic jar creation | ||
Files.walkFileTree(classesDirectory.toPath, fileSortVisitor) | ||
|
||
val jarPath = Paths.get(classesDirectory.toString, settings.outputJar.toString) |
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.
It looks like this will make this a relative path under the classesDirectory?
Both of these should probably be (assumed to be) absolute, so that they don't need to be located within one another.
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.
^ as mentioned below, I think that this is still a thing... the test is currently assuming this behaviour.
/** | ||
* Jar the contents of output classes (settings.classesDirectory) and copy to settings.outputJar | ||
*/ | ||
def createClassesJar(settings: Settings, log: Logger) = { |
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.
I'd recommend creating one test that sanity checks this method: to make that easier, I'd recommend replacing Settings
with explicit arguments for the classesDirectory and outputJar (and then maybe make the timestamp optional).
An example unit test for this code is over here: https://github.com/pantsbuild/pants/tree/master/tests/scala/org/pantsbuild/zinc/analysis
@@ -47,7 +48,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 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...
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.
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 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.
Only post merge, unfortunately. |
Is this a "can't be done practically for reasons" or "someone needs to make this functionality"? It seems like publishing a jar is something that would be useful to have the ability to test (I'm thinking of a mixin or something which could then be extended for e.g. internal tasks which need to test publishing a jar for some reason). Is there context I'm missing? |
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.
I've got one question below.
I'd also like to see a test that asserts things about the resulting jar contents. I'd be ok with deferring adding automated testing to the python side, but I have a strong preference for having a scala test if possible.
|
||
val fileSortVisitor = new SimpleFileVisitor[Path]() { | ||
override def preVisitDirectory(path: Path, attrs: BasicFileAttributes): FileVisitResult = { | ||
sorted.add(path) |
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.
Are these paths relative to the classDirectory? If they're absolute, they might create the wrong entries in the jar file because they're used as the path for the JarEntry
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.
Thanks @ity !
I think that there is one remaining issue around relative paths. Should update the test to use two separate temp dirs before merging.
* @return | ||
*/ | ||
def existsClass(jarPath: Path, clazz: String): Boolean = { | ||
val jis = new JarInputStream(Files.newInputStream(jarPath)) |
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.
This stream needs to be closed (probably via try { .. } finally { .. }
).
} | ||
"JarCreationWithClasses" should { | ||
"succeed when input classes are provided" in { | ||
IO.withTemporaryDirectory { tempDir => |
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.
I think that the code and this test assume that the jar is always created under the classes directory... but that won't be the case.
Can you change this test to use two temporary directories: one containing the input classes, and another containing the output jar?
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.
I got rid of this assumption in the previous revision - added 2 separate dirs, PTAL when you can
val jarOutputPath = Paths.get(tempDirPath.toString, "spec-valid-output.jar") | ||
|
||
OutputUtils.createJar(filePaths, jarOutputPath, System.currentTimeMillis()) | ||
OutputUtils.existsClass(jarOutputPath, tempFile.toString) 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.
Thanks for writing the test. Things are a bit clearer to me now.
I think createJar should take a root for the input file paths, that way the entry paths can be relative to that root.
for example, if you had an input dir like temp-dir
that looked like this.
temp-dir/org/example/Clazz.class
temp-dir/org/example/inner/Clazz2.class
I think you'd want the resulting jar to contain
org/example/Clazz.class
org/example/inner/Clazz2.class
I'm not sure it does this right now.
Does that make sense? I might be missing something.
val jarOutputPath = Paths.get(tempOutputDir.toString, "spec-valid-output.jar") | ||
|
||
OutputUtils.createJar(filePaths, jarOutputPath, System.currentTimeMillis()) | ||
OutputUtils.existsClass(jarOutputPath, tempFile.toString) 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.
So, tempFile.toString
is going to be an absolute filename, I think? In this case we're expecting literally Clazz.class
.
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.
Thanks for updating the test and clearing up the relative path thing. It looks good to me now, apart from a copyright year that might need updating and possibly a method rename.
@@ -0,0 +1,12 @@ | |||
# Copyright 2017 Pants project contributors (see CONTRIBUTORS.md). |
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.
nit: 2018
|
||
OutputUtils.createJar(tempInputDir.toString, filePaths, jarOutputPath, System.currentTimeMillis()) | ||
OutputUtils.existsClass(jarOutputPath, tempFile.toString) must be(false) | ||
OutputUtils.existsClass(jarOutputPath, tempFile.getName) 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.
Thanks 👍
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.
getName
is only the final component of a filename, so this test would fail if the classfile were more deeply nested... it looks like the underlying code properly relativizes things though.
* Determines if a Class exists in a JAR provided. | ||
* | ||
* @param jarPath Absolute Path to the JAR being inspected | ||
* @param clazz Name of the Class, the existence of which is to be inspected |
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.
I think this function checks the existence of files in the jar and not the existence of classes by name. It might make sense to rename it and the clazz
parameter to make that clearer.
jarEntry.setTime(jarEntryTime) | ||
|
||
target.putNextEntry(jarEntry) | ||
Files.copy(source, target) |
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.
Hm... it looks like you're properly collecting directories above (ie, collecting directories in pre-visit), but I feel like this method will fail if the source: Path
is a directory? It's important to actually store directory entries in the zip file, so I think that this case needs to add the jarEntry for a directory as a directory, and then skip appending the content.
You might consider having the FileVisitor store a tuple of (path: Path, isFile: Boolean)
(which can still be sorted) to avoid needing to check whether the path is a directory here.
Between this and my comment in the test, I think the test needs an update to create a classfile under a directory.
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.
thanks for catching this - ptal when you can :)
👍 |
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.
I led you astray on one thing, sorry! Once that's fixed, please try out integrating with zinc, and then feel free to merge!
|
||
val fileSortVisitor = new SimpleFileVisitor[Path]() { | ||
override def preVisitDirectory(path: Path, attrs: BasicFileAttributes): FileVisitResult = { | ||
sorted.add(path, false) |
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.)
I should have read that myself really, thanks - been trying to test this locally but hitting some local caching issue with the zinc changes. will push once I have local integration tested. |
### Problem Invocations of zinc do not support jars. ### Solution Add functionality to the zinc wrapper to jar up the contents of _classesDirectory when an option is specified.
Problem
Invocations of zinc do not support jars. See #6080 for details
Solution
Add functionality to the zinc wrapper to jar up the contents of
_classesDirectory
when an option is specified.