Skip to content

Commit

Permalink
Add support for mixed Scala/Java projects
Browse files Browse the repository at this point in the history
Optimized versions of the code for JDK7 and JDK6
are available. This plugin will currently compile
directly to jar only the main artifacts, while
the tests will still be compiled as individual
classfiles.
  • Loading branch information
Antonio Cunei committed Mar 22, 2016
1 parent d931772 commit d9e9669
Show file tree
Hide file tree
Showing 2 changed files with 204 additions and 7 deletions.
22 changes: 16 additions & 6 deletions README.md
Expand Up @@ -17,7 +17,7 @@ intermediate creation of the separate classfiles.
## Usage

* add to your project/plugin.sbt the line:
`addSbtPlugin("com.typesafe.sbt" % "sbt-tojar" % "0.1")`
`addSbtPlugin("com.typesafe.sbt" % "sbt-tojar" % "0.2")`
* then add to the settings of the subprojects for which you would like to enable this functionality:
`straightToJar := true`

Expand All @@ -27,14 +27,24 @@ needed beforehand).

## Notes

Since the packaged jar file is generated directly by the Scala compiler, it will not be possible
to have in the same jar also files that are generated from other sources. Therefore, `straightTojar` cannot
be enabled on mixed Java/Scala subprojects, or on those subprojects that use `copyResources`.
During the compilation of a mixed Java/Scala project, both the Scala and the Java compilers are
called. The Java compiler is unable to produce a jar file directly, therefore initially a first
jar is generated by scalac, then the remaining classes created by javac are appended to it.

When running under JDK7 and newer, the plugin is able to open directly the existing jar and append
the new classfiles. The facilities used for this operation are not avaliable on JDK6 and older;
instead, a less efficient implementation will create a new jar file, copy the entries from the
first jar, and finally append the new classes.

Since the packaged jar file is generated from the compiled classes as described above, the
archive cannot contain files that are generated from other sources. Therefore, `straightTojar`
is currently incompatible with bytecode manipulations via the `manipulateBytecode` facility,
and the resulting jar archive cannot contain files copied via `copyResources`.
However, in an sbt project that contains many different subprojects, the straight-to-jar compilation can
be enabled on all the subprojects that do not use those features.
be selectively disabled on just the subprojects that use those features.

It should also be noted that straight-to-jar compilation will disable the usual incremental compilation
mechanisms of sbt: if any source file in a straight-to-jar subproject is modified in a way that makes
recompilation necessary, then all of the source files of that subproject will be recompiled.

This plugin requires sbt 0.13.10, final or RC2+.
This plugin requires sbt 0.13.10, final or RC2, or a more recent version..
189 changes: 188 additions & 1 deletion src/main/scala/com/typesafe/sbttojar/Plugin.scala
Expand Up @@ -52,12 +52,35 @@ import sbt.inc.Analysis
on 0.13.10-RC1, or previous version of sbt.
Instead 0.13.10-RC2 or more recent will be needed.
--
In order to support mixed Java/Scala compilation,
the Java compiler is called after the Scala compiler;
if this plugin is in use, however, the jar file has
already been generated at that point.
We let javac compile as normal, generating further
classfiles, and we then insert these newer files
into the existing jar.
If we are running under JDK7 or more recent, we use
a zip-based virtual file system, using the existing
jar file and adding new entries. On JDK6 and older,
however, such support is not available; in that case,
an alternate implementation creates a new jar that
contains the old entries plus the newer ones.
The default value of the class directory is passed
to the Java compiler via a separate key.
*/

// TODO: add support for straight-to-jar for tests as well
object ToJar extends AutoPlugin {
object autoImport {
val straightToJar = settingKey[Boolean]("Enables straight-to-jar compilation")
}
val defaultClassDir = settingKey[File]("Default class output directory")
import autoImport._
override def globalSettings = Seq(
straightToJar := false
Expand Down Expand Up @@ -98,8 +121,172 @@ object ToJar extends AutoPlugin {
}
}
clean.value
}
},
compilers := {
case class Y(in:sbt.compiler.JavaTool) extends sbt.compiler.JavaTool {
def compile(contract: sbt.compiler.JavacContract,sources: Seq[java.io.File],classpath: Seq[java.io.File],
outputDirectory: java.io.File,options: Seq[String])(implicit log: sbt.Logger): Unit = {
// TODO: defaultClassDir could be in Compile or Test, or something else
val newDir = defaultClassDir.value
newDir.mkdirs()
in.compile(contract, sources, classpath, newDir, options)(log)
}
def onArgs(f: Seq[String] => Unit): sbt.compiler.JavaTool = {
Y(in.onArgs(f))
}
}
val x = compilers.value
if (straightToJar.value) {
x.copy(javac=Y(x.javac))
} else x
},
manipulateBytecode in Compile := {
if (straightToJar.value) {
val classDir = defaultClassDir.value
val analysisResult: Compiler.CompileResult = (manipulateBytecode in Compile).value
if (analysisResult.hasModified) {
// now, append the java classfiles to the scalac jar
val jarFile = (classDirectory in Compile).value
injectJar(jarFile, classDir)
}
analysisResult
} else (manipulateBytecode in Compile).value
},
defaultClassDir := new File(((crossTarget in Compile).value /
((if ((configuration in Compile).value.name == Configurations.Compile.name) "" else (configuration in Compile).value.name + "-") + "classes")).getCanonicalPath)
)

override def trigger = allRequirements
override def requires = plugins.JvmPlugin

//--------------------------------------------

import sbt.IO._
import sbt.DirectoryFilter
import collection.JavaConversions._

import java.io.File
import java.io.InputStream
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.PrintWriter

// Two versions of injectJar are provided.
// The first is old-style, and creates a new jar file in order
// to be able to append to it, copying the old content first.
// The second version uses the nio zip filesystem, available w/ Java 7.

abstract class Injector {
// injects all of the files in dir into the jar
def injectJar(jar: File, dir: File): Unit
}

val injector = {
// We have separate classes for JDK6 and JDK7; if we run on
// JDK6, the JDK7 injector class is never initialized (which
// is good, since required classes would be missing from the
// classpath).

class InjectorJDK6 extends Injector {
import java.util.jar.JarInputStream
import java.util.jar.JarOutputStream
import java.util.jar.JarFile
import java.util.jar.JarEntry

def injectJar(jar: File, dir: File) = {
withTemporaryFile("inject", "tempJar") { temp =>
val out = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(temp)))

// Initially we copy the old jar content, and later we append the new entries
val bufferSize = 131072
val buffer = new Array[Byte](bufferSize)
def writeEntry(where: JarEntry, source: InputStream) = {
out.putNextEntry(where)
Stream.continually(source.read(buffer, 0, bufferSize)).takeWhile(_ != -1).
foreach { size =>
out.write(buffer, 0, size)
}
}

val in = new JarFile(jar)
//
// The jar may contain duplicate entries (even though it shouldn't);
// for example, the scalap jar in Scala 2.11.4 is broken.
// Rather than aborting, we print a warning and try to continue
val list = in.entries.toSeq
val uniques = list.foldLeft(Map[String, JarEntry]()) { (map, entry) =>
if (map.isDefinedAt(entry.getName)) {
map
} else
map.updated(entry.getName, entry)
}

val files = dir.**(-DirectoryFilter).get
val targets = files.map(relativize(dir, _).getOrElse("Internal error while relativizing, please report."))

// Copy all the content, skipping the entries that will be replaced
uniques.valuesIterator.foreach { entry =>
if (!targets.contains(entry.getName())) {
writeEntry(entry, in.getInputStream(entry))
}
}

// Finally, insert the new entries at the appropriate target locations
files.zip(targets).foreach {
case (file, target) =>
writeEntry(new JarEntry(target), new BufferedInputStream(new FileInputStream(file)))
}
in.close()
out.flush()
out.close()

// Time to move the temporary file back to the original location
move(temp, jar)
}
}
}

class InjectorJDK7 extends Injector {
import java.util.{ Map => JMap, HashMap => JHashMap, _ }
import java.net.URI
import java.nio.file.Path
import java.nio.file._

def injectJar(jar: File, dir: File) = {
val files = dir.**(-DirectoryFilter).get
val targets = files.map("/" + relativize(dir, _).getOrElse("Internal error while relativizing, please report."))
val env: JMap[String, String] = new JHashMap[String, String]()
env.put("create", "false")
val uri = URI.create("jar:" + jar.toURI) // for escaping blanks&symbols
// val fs = FileSystems.getFileSystem(uri)
val fs = FileSystems.newFileSystem(uri, env, null)
try {
val fileDirs = files.map { f => Option(f.getCanonicalFile().getParentFile()) }.distinct.flatten
fileDirs.map { d =>
val targetPath = fs.getPath("/" + (relativize(dir, d).getOrElse("")))
Files.createDirectories(targetPath)
}
files.zip(targets).foreach {
case (file, target) =>
val entryPath = fs.getPath(target)
Files.copy(file.toPath, entryPath, StandardCopyOption.REPLACE_EXISTING)
}
} finally {
fs.close()
}
}
}

val required = VersionNumber("1.7")
val current = VersionNumber(sys.props("java.specification.version"))
val hasZipFS = current.numbers.zip(required.numbers).foldRight(required.numbers.size <= current.numbers.size)((a, b) => (a._1 > a._2) || (a._1 == a._2 && b))
if (hasZipFS) (new InjectorJDK7) else (new InjectorJDK6)
}

def injectJar(jar: File, dir: File) = {
injector.injectJar(jar, dir)
}
}

0 comments on commit d9e9669

Please sign in to comment.