Skip to content

Instantly share code, notes, and snippets.

@markehammons
Created December 19, 2018 04:22
Show Gist options
  • Save markehammons/42d75709e060625f1a663b442842b461 to your computer and use it in GitHub Desktop.
Save markehammons/42d75709e060625f1a663b442842b461 to your computer and use it in GitHub Desktop.
Packaging your application with a minimized runtime courtesy of jlink
import java.io.{ByteArrayOutputStream, PrintWriter}
import java.util.spi.ToolProvider
enablePlugins(JavaAppPackaging)
//this allows us to run tools like jdeps and jlink from within the JVM
def runTool(name: String, arguments: Seq[String]): Either[String,String] = {
val maybeTool: Option[ToolProvider] = {
val _tool = ToolProvider.findFirst(name)
if(_tool.isPresent) {
Some(_tool.get())
} else {
None
}
}
val result = for(tool <- maybeTool) yield {
val stdOut = new ByteArrayOutputStream()
val errOut = new ByteArrayOutputStream()
tool.run(new PrintWriter(stdOut), new PrintWriter(errOut), arguments: _*)
(new String(stdOut.toByteArray), new String(errOut.toByteArray))
}
result
.toRight(s"Could not find tool $name in your java development environment")
.flatMap{ case (ret,err) =>
if(ret.contains("Error:") || err.nonEmpty) {
Left(ret + err)
} else {
Right(ret -> "")
}
}
.map(_._1)
}
//get the jvm module dependencies (such as java.base) from our application and imported libraries
val moduleDependencies = taskKey[Array[String]]("outputs the jdk module dependency information of our classpath")
moduleDependencies := {
val logger = streams.value
logger.log.info("getting module dependencies from jdeps...")
val classPathValue = (dependencyClasspath in Runtime)
.value
.map(_.data.getAbsolutePath)
val command = Seq("-recursive", "--list-deps") ++ classPathValue
logger.log.info(s"jdeps ${command.mkString(" ")}")
runTool("jdeps", command)
.map(_
.split('\n')
.filter(!_.isEmpty)
.map(_
.filter(!_.isWhitespace)
.split('/')
.head
)
.distinct
.filter(_ != "JDKremovedinternalAPI")
)
.fold(sys.error, mods => {
logger.log.info("done generating module dependencies...")
mods
})
}
//this generates the minimized JRE for our application courtesy of jlink
val jlink = taskKey[File]("generates a java runtime for the project")
jlink := {
val logger = streams.value
logger.log.info("generating runtime with jlink...")
val outputFile = target.value / s"${name.value}-runtime"
if(outputFile.exists()) {
logger.log.info("deleting already generated runtime")
IO.delete(outputFile)
}
val modulesToAdd = Seq("--add-modules", moduleDependencies.value.mkString(","))
val outputArgument = Seq("--output", outputFile.absolutePath)
val command = Seq("--no-header-files","--no-man-pages","--compress=2","--strip-debug") ++ modulesToAdd ++ outputArgument
logger.log.debug(s"command: jlink ${command.mkString(" ")}")
runTool("jlink", command).map(_ => outputFile).fold(sys.error, identity)
}
//this tells sbt-native-packager to bundle our minimized JRE with our application
mappings in Universal ++= {
val dir = jlink.value
(dir.**(AllPassFilter) --- dir).pair(file => IO.relativize(dir.getParentFile, file))
}
//sbt-native-packager launch script modifications necessary for using the internal JRE to run the application
bashScriptExtraDefines ++= Seq(s"JAVA_HOME=$$app_home/../${jlink.value.getName}")
//call sbt universal:packageBin to create a nice packed up .zip of your application with launch script
// and all the dependencies needed to run it! Windows users will need to add a change to the batch script
// to get the batch script launcher to use the built-in jre.
//needed for packaging. remember to put in project/plugins.sbt
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.15")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment