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 7, 2019
1 parent 4f3ac34 commit d7bdc01
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 29 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.typesafe.sbt
package packager
package graalvmnativeimage

/**
* The builder for a GraalVM native image.
*/
sealed trait GraalVMNativeImageBuilder

object GraalVMNativeImageBuilder {

/**
* Base trait for builders that build the native image in docker
*/
trait Docker extends GraalVMNativeImageBuilder

private val GraalVMNativeImageCommand = "native-image"

/**
* Build locally using the given command.
*/
final case class Local(command: String = GraalVMNativeImageCommand) extends GraalVMNativeImageBuilder

/**
* Build using a prepackaged Graal `native-image` container. The container must execute the Graal `native-image`
* command with the arguments supplied to the container.
*/
final case class PrepackagedDocker(image: String) extends Docker

/**
* Build using a generated Graal `native-image` container. This will build a Graal native-image container if not
* already present in the local docker registry based on the supplied base image, which must provide a Graal
* installation.
*/
final case class GeneratedDocker(baseImage: String = GraalVMNativeImagePlugin.GraalVMBaseImage) extends Docker
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,11 @@ import sbt._
*/
trait GraalVMNativeImageKeys {
val graalVMNativeImageOptions =
SettingKey[Seq[String]]("graalvm-native-image-options", "GraalVM native-image options")
settingKey[Seq[String]]("GraalVM native-image options")

val graalVMNativeImageBuilder = settingKey[GraalVMNativeImageBuilder]("Builder for the GraalVM native image")

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
@@ -1,14 +1,13 @@
package com.typesafe.sbt.packager.graalvmnativeimage

import sbt._
import sbt.Keys._
import java.nio.charset.Charset
import sbt.Keys.{mainClass, name, _}

import com.typesafe.sbt.packager.SettingsHelper
import com.typesafe.sbt.packager.{MappingsHelper, Stager}
import com.typesafe.sbt.packager.Keys._
import com.typesafe.sbt.packager.linux._
import com.typesafe.sbt.packager.Compat._
import com.typesafe.sbt.packager.validation._
import com.typesafe.sbt.packager.archetypes.JavaAppPackaging
import com.typesafe.sbt.packager.docker.{Cmd, DockerPlugin, Dockerfile, ExecCmd}

/**
* Plugin to compile ahead-of-time native executables.
Expand All @@ -24,33 +23,174 @@ object GraalVMNativeImagePlugin extends AutoPlugin {
val GraalVMNativeImage: Configuration = config("graalvm-native-image")
}

private val GraalVMNativeImageCommand = "native-image"

import autoImport._

private[graalvmnativeimage] val GraalVMBaseImage = "oracle/graalvm-ce"

override def requires = JavaAppPackaging

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

override lazy val projectSettings = Seq(
target in GraalVMNativeImage := target.value / "graalvm-native-image",
graalVMNativeImageOptions := Seq.empty,
packageBin in GraalVMNativeImage := {
val targetDirectory = (target in GraalVMNativeImage).value
targetDirectory.mkdirs()
val binaryName = name.value
val command = {
val nativeImageArguments = {
val className = (mainClass in Compile).value.getOrElse(sys.error("Could not find a main class."))
val classpathJars = Seq((packageBin in Compile).value) ++ (dependencyClasspath in Compile).value.map(_.data)
val classpath = classpathJars.mkString(":")
val extraOptions = graalVMNativeImageOptions.value
Seq("--class-path", classpath, s"-H:Name=$binaryName") ++ extraOptions ++ Seq(className)
}
Seq(GraalVMNativeImageCommand) ++ nativeImageArguments
graalVMNativeImageGraalVersion := None,
graalVMNativeImageBuilder := {
graalVMNativeImageGraalVersion.value match {
case Some(tag) => GraalVMNativeImageBuilder.GeneratedDocker(s"$GraalVMBaseImage:$tag")
case None => GraalVMNativeImageBuilder.Local()
}
sys.process.Process(command, targetDirectory) ! streams.value.log match {
case 0 => targetDirectory / binaryName
case x => sys.error(s"Failed to run $GraalVMNativeImageCommand, exit status: " + x)
},
mainClass in GraalVMNativeImage := (mainClass in Compile).value,
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(),
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

graalVMNativeImageBuilder.value match {
case GraalVMNativeImageBuilder.Local(command) =>
buildLocal(
targetDirectory,
binaryName,
className,
classpathJars.map(_._1),
extraOptions,
command,
streams.log
)

case dockerBuilder: GraalVMNativeImageBuilder.Docker =>
val image = dockerBuilder match {
case GraalVMNativeImageBuilder.PrepackagedDocker(image) => image
case GraalVMNativeImageBuilder.GeneratedDocker(baseImage) =>
generateDockerImage(baseImage, dockerCommand, streams, targetDirectory)
}

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],
nativeImageCommand: 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)
}
}

private def generateDockerImage(baseImage: String,
dockerCommand: Seq[String],
streams: TaskStreams,
target: File): String = {
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")
}

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)
}
}
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,13 @@ 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")
)

/** 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 d7bdc01

Please sign in to comment.