From d7bdc01c6f932321e6e583563d9d000b10534984 Mon Sep 17 00:00:00 2001 From: James Roper Date: Wed, 7 Aug 2019 20:45:11 +1000 Subject: [PATCH] Support building Graal native images in docker Closes #1250 This provides support for building Graal native images in a docker container. --- .../sbt/packager/MappingsHelper.scala | 7 + .../sbt/packager/MappingsHelper.scala | 8 +- .../sbt/packager/docker/DockerPlugin.scala | 1 - .../GraalVMNativeImageBuilder.scala | 36 ++++ .../GraalVMNativeImageKeys.scala | 8 +- .../GraalVMNativeImagePlugin.scala | 186 +++++++++++++++--- .../packager/universal/UniversalPlugin.scala | 8 + .../docker-native-image/build.sbt | 5 + .../docker-native-image/project/plugins.sbt | 1 + .../src/main/scala/Main.scala | 5 + .../docker-native-image/test | 3 + src/sphinx/formats/graalvm-native-image.rst | 64 +++++- 12 files changed, 303 insertions(+), 29 deletions(-) create mode 100644 src/main/scala/com/typesafe/sbt/packager/graalvm-native-image/GraalVMNativeImageBuilder.scala create mode 100644 src/sbt-test/graalvm-native-image/docker-native-image/build.sbt create mode 100644 src/sbt-test/graalvm-native-image/docker-native-image/project/plugins.sbt create mode 100644 src/sbt-test/graalvm-native-image/docker-native-image/src/main/scala/Main.scala create mode 100644 src/sbt-test/graalvm-native-image/docker-native-image/test diff --git a/src/main/scala-sbt-0.13/com/typesafe/sbt/packager/MappingsHelper.scala b/src/main/scala-sbt-0.13/com/typesafe/sbt/packager/MappingsHelper.scala index 0db096bbf..769c21c49 100644 --- a/src/main/scala-sbt-0.13/com/typesafe/sbt/packager/MappingsHelper.scala +++ b/src/main/scala-sbt-0.13/com/typesafe/sbt/packager/MappingsHelper.scala @@ -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) + } + } diff --git a/src/main/scala-sbt-1.0/com/typesafe/sbt/packager/MappingsHelper.scala b/src/main/scala-sbt-1.0/com/typesafe/sbt/packager/MappingsHelper.scala index 8d34c3e5b..3c10dbd60 100644 --- a/src/main/scala-sbt-1.0/com/typesafe/sbt/packager/MappingsHelper.scala +++ b/src/main/scala-sbt-1.0/com/typesafe/sbt/packager/MappingsHelper.scala @@ -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 */ @@ -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) + } } diff --git a/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala index 4526fe97e..6cd07485b 100644 --- a/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/docker/DockerPlugin.scala @@ -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), diff --git a/src/main/scala/com/typesafe/sbt/packager/graalvm-native-image/GraalVMNativeImageBuilder.scala b/src/main/scala/com/typesafe/sbt/packager/graalvm-native-image/GraalVMNativeImageBuilder.scala new file mode 100644 index 000000000..cc79bc900 --- /dev/null +++ b/src/main/scala/com/typesafe/sbt/packager/graalvm-native-image/GraalVMNativeImageBuilder.scala @@ -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 +} diff --git a/src/main/scala/com/typesafe/sbt/packager/graalvm-native-image/GraalVMNativeImageKeys.scala b/src/main/scala/com/typesafe/sbt/packager/graalvm-native-image/GraalVMNativeImageKeys.scala index 64bf48346..5b088af90 100644 --- a/src/main/scala/com/typesafe/sbt/packager/graalvm-native-image/GraalVMNativeImageKeys.scala +++ b/src/main/scala/com/typesafe/sbt/packager/graalvm-native-image/GraalVMNativeImageKeys.scala @@ -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." + ) } diff --git a/src/main/scala/com/typesafe/sbt/packager/graalvm-native-image/GraalVMNativeImagePlugin.scala b/src/main/scala/com/typesafe/sbt/packager/graalvm-native-image/GraalVMNativeImagePlugin.scala index d8fc06c41..40b0595b7 100644 --- a/src/main/scala/com/typesafe/sbt/packager/graalvm-native-image/GraalVMNativeImagePlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/graalvm-native-image/GraalVMNativeImagePlugin.scala @@ -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. @@ -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) + } } diff --git a/src/main/scala/com/typesafe/sbt/packager/universal/UniversalPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/universal/UniversalPlugin.scala index 455dc3f2d..d250bdffa 100644 --- a/src/main/scala/com/typesafe/sbt/packager/universal/UniversalPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/universal/UniversalPlugin.scala @@ -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 @@ -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... diff --git a/src/sbt-test/graalvm-native-image/docker-native-image/build.sbt b/src/sbt-test/graalvm-native-image/docker-native-image/build.sbt new file mode 100644 index 000000000..ee4ef3c72 --- /dev/null +++ b/src/sbt-test/graalvm-native-image/docker-native-image/build.sbt @@ -0,0 +1,5 @@ +enablePlugins(GraalVMNativeImagePlugin) + +name := "docker-test" +version := "0.1.0" +graalVMNativeImageGraalVersion := Some("19.0.0") \ No newline at end of file diff --git a/src/sbt-test/graalvm-native-image/docker-native-image/project/plugins.sbt b/src/sbt-test/graalvm-native-image/docker-native-image/project/plugins.sbt new file mode 100644 index 000000000..b53de154c --- /dev/null +++ b/src/sbt-test/graalvm-native-image/docker-native-image/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % sys.props("project.version")) diff --git a/src/sbt-test/graalvm-native-image/docker-native-image/src/main/scala/Main.scala b/src/sbt-test/graalvm-native-image/docker-native-image/src/main/scala/Main.scala new file mode 100644 index 000000000..43c2459f3 --- /dev/null +++ b/src/sbt-test/graalvm-native-image/docker-native-image/src/main/scala/Main.scala @@ -0,0 +1,5 @@ +object Main { + def main(args: Array[String]): Unit = { + println("Hello Graal") + } +} diff --git a/src/sbt-test/graalvm-native-image/docker-native-image/test b/src/sbt-test/graalvm-native-image/docker-native-image/test new file mode 100644 index 000000000..3d6230bcd --- /dev/null +++ b/src/sbt-test/graalvm-native-image/docker-native-image/test @@ -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"' diff --git a/src/sphinx/formats/graalvm-native-image.rst b/src/sphinx/formats/graalvm-native-image.rst index c1230abce..4a154c5b5 100644 --- a/src/sphinx/formats/graalvm-native-image.rst +++ b/src/sphinx/formats/graalvm-native-image.rst @@ -7,13 +7,18 @@ GraalVM's ``native-image`` compiles Java programs AOT (ahead-of-time) into nativ https://www.graalvm.org/docs/reference-manual/aot-compilation/ documents the AOT compilation of GraalVM. +The plugin supports both using a local installation of the GraalVM ``native-image`` utility, or building inside a +Docker container. If you intend to run the native image on Linux, then building inside a Docker container is +recommended since GraalVM native images can only be built for the platform they are built on. By building in a Docker +container, you can build Linux native images not just on Linux but also on Windows and OSX. + Requirements ------------ -You must have ``native-image`` of GraalVM in your ``PATH``. +To build using a local installation of GraalVM, you must have the ``native-image`` utility of GraalVM in your ``PATH``. -Quick installation -~~~~~~~~~~~~~~~~~~ +``native-image`` quick installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To get started quickly, eg make ``native-image`` available in your ``PATH``, you may reuse the script that is used for sbt-native-packager's continuous integration. @@ -42,12 +47,65 @@ Required Settings Settings -------- +Docker Image Build Settings +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, a local build will be done, expecting the ``native-image`` command to be on your ``PATH``. This can be +customized using the following settings. + + ``graalVMNativeImageGraalVersion`` + Setting this enables generating a Docker container to build the native image, and then building it in that container. + It must correspond to a valid version of the + `Oracle GraalVM Community Edition Docker image `_. This setting has no + effect if ``graalVMNativeImageBuilder`` is explicitly set. + + For example: + + .. code-block:: scala + + graalVMNativeImageGraalVersion := Some("19.1.1") + + ``graalVMNativeImageBuilder`` + + Select a builder for the native image. To explicitly configure a local native image build using the + ``native-image`` command, use: + + .. code-block:: scala + + import com.typesafe.sbt.packager.graalvmnativeimage.GraalVMNativeImageBuilder + + graalVMNativeImageBuilder := GraalVMNativeImageBuilder.Local("native-image") + + To generate a docker container to build the image based on a particular base image, use: + + .. code-block:: scala + + import com.typesafe.sbt.packager.graalvmnativeimage.GraalVMNativeImageBuilder + + graalVMNativeImageBuilder := GraalVMNativeImageBuilder.GeneratedDocker("oracle/graalvm-ce:19.1.1") + + If you have published a native image builder image that you wish to use, you can use that by configuring: + + .. code-block:: scala + + import com.typesafe.sbt.packager.graalvmnativeimage.GraalVMNativeImageBuilder + + graalVMNativeImageBuilder := GraalVMNativeImageBuilder.PrepackagedDocker("my-docker-username/graalvm-ce-native-image:19.1.1") + Publishing Settings ~~~~~~~~~~~~~~~~~~~ ``graalVMNativeImageOptions`` Extra options that will be passed to the ``native-image`` command. By default, this includes the name of the main class. +GraalVM Resources +----------------- + +If you are building the image in a docker container, and you have any resources that need to be available to the +``native-image`` command, such as files passed to ``-H:ResourceConfigurationFiles`` or +``-H:ReflectionConfigurationFiles``, you can place these in your projects ``src/graal`` directory. Any files in there +will be made available to the ``native-image`` docker container under the path ``/opt/graalvm/stage/resources``. + Tasks ----- The GraalVM Native Image plugin provides the following commands: