Skip to content

Commit

Permalink
Support building Graal native images in docker
Browse files Browse the repository at this point in the history
Closes sbt#1250

This provides support for building Graal native images in a docker
container.
  • Loading branch information
jroper committed Aug 8, 2019
1 parent 4f3ac34 commit fd40bca
Show file tree
Hide file tree
Showing 14 changed files with 313 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,11 @@ object MappingsHelper {
file -> s"$target/${file.getName}"
}

/**
* Get the mappings for the given files relative to the given directories.
*/
def relative(files: Seq[File], dirs: Seq[File]): Seq[(File, String)] = {
(files --- dirs) pair (relativeTo(dirs) | Path.flat)
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.typesafe.sbt.packager

import sbt._
import sbt.{Path, _}
import sbt.io._

/** A set of helper methods to simplify the writing of mappings */
Expand Down Expand Up @@ -81,4 +81,10 @@ object MappingsHelper extends Mapper {
file -> s"$target/${file.getName}"
}

/**
* Get the mappings for the given files relative to the given directories.
*/
def relative(files: Seq[File], dirs: Seq[File]): Seq[(File, String)] = {
(files --- dirs) pair (relativeTo(dirs) | flat)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ object DockerPlugin extends AutoPlugin {
},
dockerEntrypoint := Seq(s"${(defaultLinuxInstallLocation in Docker).value}/bin/${executableScriptName.value}"),
dockerCmd := Seq(),
dockerExecCommand := Seq("docker"),
dockerVersion := Try(Process(dockerExecCommand.value ++ Seq("version", "--format", "'{{.Server.Version}}'")).!!).toOption
.map(_.trim)
.flatMap(DockerVersion.parse),
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.typesafe.sbt
package packager
package graalvmnativeimage

import sbt._

/**
* GraalVM settings
*/
trait GraalVMNativeImageKeys {
val graalVMNativeImageOptions =
settingKey[Seq[String]]("GraalVM native-image options")

val graalVMNativeImageGraalVersion = settingKey[Option[String]](
"Version of GraalVM to build with. Setting this has the effect of causing graalVMNativeImageBuilder to default to GeneratedDocker with the Oracle graalvm docker base image for this version."
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package com.typesafe.sbt.packager.graalvmnativeimage

import sbt._
import sbt.Keys.{mainClass, name, _}
import com.typesafe.sbt.packager.{MappingsHelper, Stager}
import com.typesafe.sbt.packager.Keys._
import com.typesafe.sbt.packager.Compat._
import com.typesafe.sbt.packager.archetypes.JavaAppPackaging
import com.typesafe.sbt.packager.docker.{Cmd, DockerPlugin, Dockerfile, ExecCmd}
import com.typesafe.sbt.packager.universal.UniversalPlugin

/**
* Plugin to compile ahead-of-time native executables.
*
* @example Enable the plugin in the `build.sbt`
* {{{
* enablePlugins(GraalVMNativeImagePlugin)
* }}}
*/
object GraalVMNativeImagePlugin extends AutoPlugin {

object autoImport extends GraalVMNativeImageKeys {
val GraalVMNativeImage: Configuration = config("graalvm-native-image")
}

import autoImport._

private val GraalVMBaseImage = "oracle/graalvm-ce"
private val NativeImageCommand = "native-image"

override def requires: Plugins = JavaAppPackaging

override def projectConfigurations: Seq[Configuration] = Seq(GraalVMNativeImage)

override lazy val projectSettings: Seq[Setting[_]] = Seq(
target in GraalVMNativeImage := target.value / "graalvm-native-image",
graalVMNativeImageOptions := Seq.empty,
graalVMNativeImageGraalVersion := None,
resourceDirectory in GraalVMNativeImage := sourceDirectory.value / "graal"
) ++ inConfig(GraalVMNativeImage)(scopedSettings)

private lazy val scopedSettings = Seq[Setting[_]](
resourceDirectories := Seq(resourceDirectory.value),
includeFilter := "*",
resources := resourceDirectories.value.descendantsExcept(includeFilter.value, excludeFilter.value).get,

UniversalPlugin.autoImport.containerBuildImage := Def.taskDyn {
graalVMNativeImageGraalVersion.value match {
case Some(tag) => generateContainerBuildImage(s"$GraalVMBaseImage:$tag")
case None => Def.task(None: Option[String])
}
}.value,

packageBin := {
val targetDirectory = target.value
val binaryName = name.value
val className = mainClass.value.getOrElse(sys.error("Could not find a main class."))
val classpathJars = scriptClasspathOrdering.value
val extraOptions = graalVMNativeImageOptions.value
val streams = Keys.streams.value
val dockerCommand = DockerPlugin.autoImport.dockerExecCommand.value
val graalResourceDirectories = resourceDirectories.value
val graalResources = resources.value

UniversalPlugin.autoImport.containerBuildImage.value match {
case None =>
buildLocal(
targetDirectory,
binaryName,
className,
classpathJars.map(_._1),
extraOptions,
streams.log
)

case Some(image) =>

val resourceMappings = MappingsHelper.relative(graalResources, graalResourceDirectories)

buildInDockerContainer(
targetDirectory,
binaryName,
className,
classpathJars,
extraOptions,
dockerCommand,
resourceMappings,
image,
streams
)
}
}
)

private def buildLocal(targetDirectory: File,
binaryName: String,
className: String,
classpathJars: Seq[File],
extraOptions: Seq[String],
log: ProcessLogger): File = {
targetDirectory.mkdirs()
val command = {
val nativeImageArguments = {
val classpath = classpathJars.mkString(":")
Seq("--class-path", classpath, s"-H:Name=$binaryName") ++ extraOptions ++ Seq(className)
}
Seq(NativeImageCommand) ++ nativeImageArguments
}
sys.process.Process(command, targetDirectory) ! log match {
case 0 => targetDirectory / binaryName
case x => sys.error(s"Failed to run $command, exit status: " + x)
}
}

private def buildInDockerContainer(targetDirectory: File,
binaryName: String,
className: String,
classpathJars: Seq[(File, String)],
extraOptions: Seq[String],
dockerCommand: Seq[String],
resources: Seq[(File, String)],
image: String,
streams: TaskStreams): File = {

stage(targetDirectory, classpathJars, resources, streams)

val command = dockerCommand ++ Seq(
"run",
"--rm",
"-v",
s"${targetDirectory.getAbsolutePath}:/opt/graalvm",
image,
"-cp",
classpathJars.map(jar => "/opt/graalvm/stage/" + jar._2).mkString(":"),
s"-H:Name=$binaryName"
) ++ extraOptions ++ Seq(className)

sys.process.Process(command) ! streams.log match {
case 0 => targetDirectory / binaryName
case x => sys.error(s"Failed to run $command, exit status: " + x)
}
}

/**
* This can be used to build a custom build image starting from a custom base image. Can be used like so:
*
* ```
* (containerBuildImage in GraalVMNativeImage) := generateContainerBuildImage("my-docker-hub-username/my-graalvm").value
* ```
*
* The passed in docker image must have GraalVM installed and on the PATH, including the gu utility.
*/
def generateContainerBuildImage(baseImage: String): Def.Initialize[Task[Option[String]]] = Def.task {
val dockerCommand = (DockerPlugin.autoImport.dockerExecCommand in GraalVMNativeImage).value
val streams = Keys.streams.value
val target = (Keys.target in GraalVMNativeImage).value

val (baseName, tag) = baseImage.split(":", 2) match {
case Array(n, t) => (n, t)
case Array(n) => (n, "latest")
}

val imageName = s"${baseName.replace('/', '-')}-native-image:$tag"
import sys.process._
if ((dockerCommand ++ Seq("image", "ls", imageName, "--quiet")).!!.trim.isEmpty) {
streams.log.info(s"Generating new GraalVM native-image image based on $baseImage: $imageName")

val dockerFile = target / "Dockerfile.graalvm-native-image"

val dockerContent = Dockerfile(
Cmd("FROM", baseImage),
Cmd("WORKDIR", "/opt/graalvm"),
ExecCmd("RUN", "gu", "install", "native-image"),
ExecCmd("ENTRYPOINT", "native-image")
).makeContent

IO.write(dockerFile, dockerContent)

DockerPlugin.publishLocalDocker(
target,
dockerCommand ++ Seq("build", "-f", dockerFile.getAbsolutePath, "-t", imageName, "."),
streams.log
)
} else {
streams.log.info(s"Using existing GraalVM native-image image: $imageName")
}

Some(imageName)
}

private def stage(targetDirectory: File,
classpathJars: Seq[(File, String)],
resources: Seq[(File, String)],
streams: TaskStreams): File = {
val stageDir = targetDirectory / "stage"
val mappings = classpathJars ++ resources.map {
case (resource, path) => resource -> s"resources/$path"
}
Stager.stage(GraalVMBaseImage)(streams, stageDir, mappings)
}
}
2 changes: 2 additions & 0 deletions src/main/scala/com/typesafe/sbt/packager/universal/Keys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ trait UniversalKeys {
val topLevelDirectory = SettingKey[Option[String]]("topLevelDirectory", "Top level dir in compressed output file.")
val universalArchiveOptions =
SettingKey[Seq[String]]("universal-archive-options", "Options passed to the tar/zip command. Scope by task")

val containerBuildImage = taskKey[Option[String]]("For plugins that support building artifacts inside a docker container, if this is defined, this image will be used to do the building.")
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import sbt.Keys._
import Archives._
import com.typesafe.sbt.SbtNativePackager
import com.typesafe.sbt.packager.Keys._
import com.typesafe.sbt.packager.docker.DockerPlugin
import com.typesafe.sbt.packager.validation._
import com.typesafe.sbt.packager.{SettingsHelper, Stager}
import sbt.Keys.TaskStreams
Expand Down Expand Up @@ -46,6 +47,14 @@ object UniversalPlugin extends AutoPlugin {
override def projectConfigurations: Seq[Configuration] =
Seq(Universal, UniversalDocs, UniversalSrc)

override lazy val buildSettings: Seq[Setting[_]] = Seq[Setting[_]](
// Since more than just the docker plugin uses the docker command, we define this in the universal plugin
// so that it can be configured once and shared by all plugins without requiring the docker plugin. Also, make it
// a build settings so that it can be overridden once, at the build level.
DockerPlugin.autoImport.dockerExecCommand := Seq("docker"),
containerBuildImage := None
)

/** The basic settings for the various packaging types. */
override lazy val projectSettings: Seq[Setting[_]] = Seq[Setting[_]](
// For now, we provide delegates from dist/stage to universal...
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
enablePlugins(GraalVMNativeImagePlugin)

name := "docker-test"
version := "0.1.0"
graalVMNativeImageGraalVersion := Some("19.0.0")
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % sys.props("project.version"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
object Main {
def main(args: Array[String]): Unit = {
println("Hello Graal")
}
}
3 changes: 3 additions & 0 deletions src/sbt-test/graalvm-native-image/docker-native-image/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Generate the GraalVM native image
> show graalvm-native-image:packageBin
$ exec bash -c 'target/graalvm-native-image/docker-test | grep -q "Hello Graal"'
Loading

0 comments on commit fd40bca

Please sign in to comment.