aboutsummaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
authorrockjam <5min4eq.unity@gmail.com>2016-06-23 20:44:40 +0300
committerrockjam <5min4eq.unity@gmail.com>2016-06-24 07:27:36 +0300
commit314df6dd5627a55f7e368f6fd62c0ad85922607a (patch)
tree422501c56b05b02b9b6729726417ef204da23ef7 /plugins
parentfd6c08a1fe03fee7ccb87200bf4ff4b717d42864 (diff)
downloadcbt-314df6dd5627a55f7e368f6fd62c0ad85922607a.tar.gz
cbt-314df6dd5627a55f7e368f6fd62c0ad85922607a.tar.bz2
cbt-314df6dd5627a55f7e368f6fd62c0ad85922607a.zip
uber-jar plugin implementation
Diffstat (limited to 'plugins')
-rw-r--r--plugins/uber-jar/build/build.scala3
-rw-r--r--plugins/uber-jar/src/UberJar.scala128
-rw-r--r--plugins/uber-jar/src/uberjar/JarUtils.scala105
-rw-r--r--plugins/uber-jar/src/uberjar/TryWithResources.scala13
4 files changed, 249 insertions, 0 deletions
diff --git a/plugins/uber-jar/build/build.scala b/plugins/uber-jar/build/build.scala
new file mode 100644
index 0000000..0205cf8
--- /dev/null
+++ b/plugins/uber-jar/build/build.scala
@@ -0,0 +1,3 @@
+import cbt._
+
+class Build(val context: Context) extends Plugin
diff --git a/plugins/uber-jar/src/UberJar.scala b/plugins/uber-jar/src/UberJar.scala
new file mode 100644
index 0000000..32b5c4a
--- /dev/null
+++ b/plugins/uber-jar/src/UberJar.scala
@@ -0,0 +1,128 @@
+package cbt
+
+import java.io.{File, FileOutputStream}
+import java.nio.file._
+import java.util.jar
+import java.util.jar.JarOutputStream
+
+import cbt.uberjar._
+
+import scala.util.{Failure, Success, Try}
+
+trait UberJar extends BaseBuild {
+
+ private val log: String => Unit = logger.log("uber-jar", _)
+
+ final def uberJar: ExitCode = {
+ // we need compile first to produce target directory
+ compile
+ System.err.println("Creating uber jar...")
+ UberJar.createUberJar(
+ targetDir = target,
+ compilerTarget = compileTarget,
+ classpath = classpath,
+ mainClass = uberJarMainClass,
+ jarName = uberJarName
+ )(log) match {
+ case Success(_) =>
+ System.err.println("Creating uber jar - DONE")
+ ExitCode.Success
+ case Failure(e) =>
+ System.err.println(s"Failed to create uber jar, cause: $e")
+ ExitCode.Failure
+ }
+ }
+
+ def uberJarMainClass: Option[String] = Some(runClass)
+
+ def uberJarName: String = projectName
+
+}
+
+object UberJar extends JarUtils {
+ import TryWithResources._
+
+ /**
+ * Creates uber jar for given build.
+ * Uber jar construction steps:
+ * 1. create jar file with our customized MANIFEST.MF
+ * 2. write files from `compilerTarget` to jar file
+ * 3. get all jars from `classpath`
+ * 4. extract all jars, filter out their MANIFEST.MF and signatures files
+ * 5. write content of all jars to target jar file
+ * 6. Finalize everything, and return `ExitCode`
+ * @param targetDir build's target directory
+ * @param compilerTarget directory where compiled classfiles are
+ * @param classpath build's classpath
+ * @param mainClass main class name(optional)
+ * @param jarName name of resulting jar file
+ * @param log logger
+ * @return `ExitCode.Success` if uber jar created and `ExitCode.Failure` otherwise
+ */
+ def createUberJar(targetDir: File,
+ compilerTarget: File,
+ classpath: ClassPath,
+ mainClass: Option[String],
+ jarName: String
+ )(log: String => Unit): Try[Unit] = {
+ val targetPath = targetDir.toPath
+ log(s"Target directory is: $targetPath")
+ log(s"Compiler targer directory is: $compilerTarget")
+ log(s"Classpath is: $classpath")
+ mainClass foreach (c => log(s"Main class is is: $c"))
+
+ val jarPath = {
+ log("Creating jar file...")
+ val validJarName = if (jarName.endsWith("*.jar")) jarName else jarName + ".jar"
+ log(s"Jar name is: $validJarName")
+ val path = targetPath.resolve(validJarName)
+ Files.deleteIfExists(path)
+ Files.createFile(path)
+ log("Creating jar file - DONE")
+ path
+ }
+ withCloseable(new JarOutputStream(new FileOutputStream(jarPath.toFile), createManifest(mainClass))) { out =>
+ writeTarget(compilerTarget, out)(log)
+
+ // it will contain all jar files, including jars, that cbt depend on! Not good!
+ val jars = classpath.files filter (f => jarFileMatcher.matches(f.toPath))
+ log(s"Found ${jars.length} jar dependencies: \n ${jars mkString "\n"}")
+ writeExtractedJars(jars, targetPath, out)(log)
+
+ // TODO: make it in try-catch-finally style
+ out.close()
+ System.err.println(s"Uber jar created. You can grab it at $jarPath")
+ }
+ }
+
+ // Main-Class: classname
+ // Manifest-Version: 1.0
+ // Created-By: 1.7.0_06 (Oracle Corporation)
+ // this template is taken from java tutorial oracle site. replace with actual value, if possible...if needed
+ private def createManifest(mainClass: Option[String]): jar.Manifest = {
+ val m = new jar.Manifest()
+ m.getMainAttributes.putValue("Manifest-Version", "1.0")
+ m.getMainAttributes.putValue("Created-By", "1.7.0_06 (Oracle Corporation)")
+ mainClass foreach { className =>
+ m.getMainAttributes.putValue("Main-Class", className)
+ }
+ m
+ }
+
+ private def writeTarget(compilerTargetDir: File, out: JarOutputStream)(log: String => Unit): Unit = {
+ log("Writing target directory...")
+ writeFilesToJar(compilerTargetDir.toPath, out)(log)
+ log("Writing target directory - DONE")
+ }
+
+ private def writeExtractedJars(jars: Seq[File], targetDir: Path, out: JarOutputStream)(log: String => Unit): Unit = {
+ log("Extracting jars")
+ val extractedJarsRoot = extractJars(jars)(log)
+ log("Extracting jars - DONE")
+
+ log("Writing dependencies...")
+ writeFilesToJar(extractedJarsRoot, out)(log)
+ log("Writing dependencies - DONE")
+ }
+
+}
diff --git a/plugins/uber-jar/src/uberjar/JarUtils.scala b/plugins/uber-jar/src/uberjar/JarUtils.scala
new file mode 100644
index 0000000..f73947e
--- /dev/null
+++ b/plugins/uber-jar/src/uberjar/JarUtils.scala
@@ -0,0 +1,105 @@
+package cbt.uberjar
+
+import java.io.File
+import java.nio.file._
+import java.nio.file.attribute.BasicFileAttributes
+import java.util.jar.{JarFile, JarOutputStream}
+import java.util.zip.{ZipEntry, ZipException}
+
+private[cbt] trait JarUtils {
+
+ protected val (pathSeparator, jarFileMatcher, excludeFileMatcher) = {
+ val fs = FileSystems.getDefault
+ (fs.getSeparator, fs.getPathMatcher("glob:**.jar"), fs.getPathMatcher("glob:**{.RSA,.DSA,.SF,.MF}"))
+ }
+
+ /**
+ * If `root` is directory: writes content of directory to jar with original mapping
+ * If `root` is file: writes this file to jar
+ * @param root parent directory, with content should go to jar, or file to be written
+ * @param out jar output stream
+ * @param log logger
+ * @return returns `root`
+ */
+ protected def writeFilesToJar(root: Path, out: JarOutputStream)(log: String => Unit): Path = {
+ Files.walkFileTree(root, new SimpleFileVisitor[Path]() {
+ override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = {
+ // when we put part of compiled classes, zip already contains some entries. We should not duplicate them.
+ try {
+ out.putNextEntry(new ZipEntry(root.relativize(file).toString))
+ Files.copy(file, out)
+ out.closeEntry()
+ } catch {
+ case e: ZipException => log(s"Failed to add entry, skipping cause: $e")
+ }
+ FileVisitResult.CONTINUE
+ }
+
+ override def preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult = {
+ // when we put part compiled classes, zip already contains some entries. We should not duplicate them.
+ try {
+ out.putNextEntry(new ZipEntry(root.relativize(dir).toString + pathSeparator))
+ out.closeEntry()
+ } catch {
+ case e: ZipException => log(s"Failed to add entry, skipping cause: $e")
+ }
+ FileVisitResult.CONTINUE
+ }
+ })
+ }
+
+ /**
+ * Extracts jars, and writes them on disk. Returns root directory of extracted jars
+ * TODO: in future we probably should save extracted jars in target directory, to reuse them on second run
+ * @param jars list of *.jar files
+ * @param log logger
+ * @return root directory of extracted jars
+ */
+ protected def extractJars(jars: Seq[File])(log: String => Unit): Path = {
+ val destDir = {
+ val path = Files.createTempDirectory("unjars")
+ path.toFile.deleteOnExit()
+ log(s"Unjars directory: $path")
+ path
+ }
+ jars foreach { jar => extractJar(jar, destDir)(log) }
+ destDir
+ }
+
+ /**
+ * Extracts content of single jar file to destination directory.
+ * When extracting jar, if same file already exists, we skip(don't write) this file.
+ * TODO: maybe skipping duplicates is not best strategy. Figure out duplicate strategy.
+ * @param jarFile jar file to extract
+ * @param destDir destination directory
+ * @param log logger
+ */
+ private def extractJar(jarFile: File, destDir: Path)(log: String => Unit): Unit = {
+ log(s"Extracting jar: $jarFile")
+ val jar = new JarFile(jarFile)
+ val enumEntries = jar.entries
+ while (enumEntries.hasMoreElements) {
+ val entry = enumEntries.nextElement()
+ // log(s"Entry name: ${entry.getName}")
+ val entryPath = destDir.resolve(entry.getName)
+ if (excludeFileMatcher.matches(entryPath)) {
+ log(s"Excluded file ${entryPath.getFileName} from jar: $jarFile")
+ } else {
+ if (Files.exists(entryPath)) {
+ log(s"File $entryPath already exists, skipping.")
+ } else {
+ if (entry.isDirectory) {
+ Files.createDirectory(entryPath)
+ // log(s"Created directory: $entryPath")
+ } else {
+ val is = jar.getInputStream(entry)
+ Files.copy(is, entryPath)
+ is.close()
+ // log(s"Wrote file: $entryPath")
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/plugins/uber-jar/src/uberjar/TryWithResources.scala b/plugins/uber-jar/src/uberjar/TryWithResources.scala
new file mode 100644
index 0000000..c2f11f9
--- /dev/null
+++ b/plugins/uber-jar/src/uberjar/TryWithResources.scala
@@ -0,0 +1,13 @@
+package cbt.uberjar
+
+import java.io.Closeable
+
+import scala.util.Try
+
+private[cbt] object TryWithResources {
+ def withCloseable[T <: Closeable, R](t: T)(f: T => R): Try[R] = {
+ val result = Try(f(t))
+ t.close()
+ result
+ }
+}