diff options
author | rockjam <5min4eq.unity@gmail.com> | 2016-06-23 20:44:40 +0300 |
---|---|---|
committer | rockjam <5min4eq.unity@gmail.com> | 2016-06-24 07:27:36 +0300 |
commit | 314df6dd5627a55f7e368f6fd62c0ad85922607a (patch) | |
tree | 422501c56b05b02b9b6729726417ef204da23ef7 /plugins | |
parent | fd6c08a1fe03fee7ccb87200bf4ff4b717d42864 (diff) | |
download | cbt-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.scala | 3 | ||||
-rw-r--r-- | plugins/uber-jar/src/UberJar.scala | 128 | ||||
-rw-r--r-- | plugins/uber-jar/src/uberjar/JarUtils.scala | 105 | ||||
-rw-r--r-- | plugins/uber-jar/src/uberjar/TryWithResources.scala | 13 |
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 + } +} |