Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds cross platform architecture compilation for native image #1549

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .github/workflows/validate-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,11 +252,15 @@ jobs:
restore-keys: |
${{ runner.os }}-ivy-
- name: Setup GraalVM environment
uses: olafurpg/setup-scala@v10
uses: graalvm/setup-graalvm@v1
with:
java-version: [email protected]=tgz+https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.0.0.2/graalvm-ce-java11-linux-amd64-22.0.0.2.tar.gz
- name: Install native-image
run: gu install native-image
java-version: 17.0.8
distribution: 'graalvm'
cache: 'sbt'
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Docker Setup Buildx
uses: docker/[email protected]
- name: Validate
run: sbt "^validateGraalVMNativeImage"

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ target/
.ensime*
.bloop/*
.metals/*

.bsp/
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ organization := "com.github.sbt"
homepage := Some(url("https://github.com/sbt/sbt-native-packager"))

Global / onChangedBuildSource := ReloadOnSourceChanges
Global / scalaVersion := "2.12.12"
Global / scalaVersion := "2.12.13"

// crossBuildingSettings
crossSbtVersions := Vector("1.1.6")
crossSbtVersions := Vector("1.9.3")

Compile / scalacOptions ++= Seq("-deprecation")
javacOptions ++= Seq("-source", "1.8", "-target", "1.8")
Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.5.4
sbt.version=1.9.3
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ trait GraalVMNativeImageKeys {

trait GraalVMNativeImageKeysEx extends GraalVMNativeImageKeys {
val graalVMNativeImageCommand = settingKey[String]("GraalVM native-image executable command")

val graalVMNativeImagePlatformArch = settingKey[Option[String]](
"Platform architecture of GraalVM to build with. This only works when building the native image with a container."
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ object GraalVMNativeImagePlugin extends AutoPlugin {

import autoImport._

private val GraalVMBaseImage = "ghcr.io/graalvm/graalvm-ce"
private val GraalVMBaseImagePath = "ghcr.io/graalvm/"

override def requires: Plugins = JavaAppPackaging

Expand All @@ -37,6 +37,7 @@ object GraalVMNativeImagePlugin extends AutoPlugin {
target in GraalVMNativeImage := target.value / "graalvm-native-image",
graalVMNativeImageOptions := Seq.empty,
graalVMNativeImageGraalVersion := None,
graalVMNativeImagePlatformArch := None,
graalVMNativeImageCommand := (if (scala.util.Properties.isWin) "native-image.cmd" else "native-image"),
resourceDirectory in GraalVMNativeImage := sourceDirectory.value / "graal",
mainClass in GraalVMNativeImage := (mainClass in Compile).value
Expand All @@ -47,9 +48,22 @@ object GraalVMNativeImagePlugin extends AutoPlugin {
includeFilter := "*",
resources := resourceDirectories.value.descendantsExcept(includeFilter.value, excludeFilter.value).get,
UniversalPlugin.autoImport.containerBuildImage := Def.taskDyn {
val splitPackageVersion = "(.*):(.*)".r
graalVMNativeImageGraalVersion.value match {
case Some(tag) => generateContainerBuildImage(s"$GraalVMBaseImage:$tag")
case None => Def.task(None: Option[String])
case Some(splitPackageVersion(packageName, tag)) =>
packageName match {
case "native-image-community" | "native-image" =>
Def.task(Some(s"$GraalVMBaseImagePath$packageName:$tag"): Option[String])
case "graalvm-community" | "graalvm-ce" =>
generateContainerBuildImage(
s"${GraalVMBaseImagePath}$packageName:$tag",
graalVMNativeImagePlatformArch.value
)
case _ => sys.error("Other ghcr.io/graalvm images are unsupported")
}
case Some(tag) =>
generateContainerBuildImage(s"${GraalVMBaseImagePath}graalvm-ce:$tag", graalVMNativeImagePlatformArch.value)
case None => Def.task(None: Option[String])
}
}.value,
packageBin := {
Expand All @@ -59,6 +73,7 @@ object GraalVMNativeImagePlugin extends AutoPlugin {
val className = mainClass.value.getOrElse(sys.error("Could not find a main class."))
val classpathJars = scriptClasspathOrdering.value
val extraOptions = graalVMNativeImageOptions.value
val platformArch = graalVMNativeImagePlatformArch.value
val streams = Keys.streams.value
val dockerCommand = DockerPlugin.autoImport.dockerExecCommand.value
val graalResourceDirectories = resourceDirectories.value
Expand All @@ -85,6 +100,7 @@ object GraalVMNativeImagePlugin extends AutoPlugin {
className,
classpathJars,
extraOptions,
platformArch,
dockerCommand,
resourceMappings,
image,
Expand Down Expand Up @@ -131,23 +147,29 @@ object GraalVMNativeImagePlugin extends AutoPlugin {
className: String,
classpathJars: Seq[(File, String)],
extraOptions: Seq[String],
platformArch: Option[String],
dockerCommand: Seq[String],
resources: Seq[(File, String)],
image: String,
streams: TaskStreams
): File = {

import sys.process._
stage(targetDirectory, classpathJars, resources, streams)

val graalDestDir = "/opt/graalvm"
val stageDestDir = s"$graalDestDir/stage"
val resourcesDestDir = s"$stageDestDir/resources"
val hostPlatform =
(dockerCommand ++ Seq("system", "info", "--format", "{{.OSType}}/{{.Architecture}}")).!!.trim
.replace("x86_64", "amd64")

val command = dockerCommand ++ Seq(
"run",
"--workdir",
"/opt/graalvm",
"--rm",
"--platform",
platformArch.getOrElse(hostPlatform),
"-v",
s"${targetDirectory.getAbsolutePath}:$graalDestDir",
image,
Expand All @@ -166,24 +188,42 @@ object GraalVMNativeImagePlugin extends AutoPlugin {
* 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
* (containerBuildImage in GraalVMNativeImage) := generateContainerBuildImage("my-docker-hub-username/my-graalvm", Some("arm64")).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 generateContainerBuildImage(
baseImage: String,
platformArch: Option[String] = None
): Def.Initialize[Task[Option[String]]] =
Def.task {
import sys.process._

val dockerCommand = (DockerPlugin.autoImport.dockerExecCommand in GraalVMNativeImage).value
val streams = Keys.streams.value
val hostPlatform =
(dockerCommand ++ Seq("system", "info", "--format", "{{.OSType}}/{{.Architecture}}")).!!.trim
.replace("x86_64", "amd64")
val platformValue = platformArch.getOrElse(hostPlatform)

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) {
val buildContainerExists = (dockerCommand ++ Seq(
"image",
"ls",
"--filter",
s"label=com.typesafe.sbt.packager.graalvmnativeimage.platform=$platformValue",
"--quiet",
imageName
)).!!.trim.nonEmpty
if (!buildContainerExists) {
streams.log.info(s"Generating new GraalVM native-image image based on $baseImage: $imageName")

val dockerContent = Dockerfile(
Expand All @@ -194,9 +234,29 @@ object GraalVMNativeImagePlugin extends AutoPlugin {
ExecCmd("ENTRYPOINT", "native-image")
).makeContent

val command = dockerCommand ++ Seq("build", "-t", imageName, "-")

val ret = sys.process.Process(command) #<
val buildCommand = dockerCommand ++ Seq(
"build",
"--label",
s"com.typesafe.sbt.packager.graalvmnativeimage.platform=$platformValue",
"-t",
imageName,
"-"
)

val buildxCommand = dockerCommand ++ Seq(
"buildx",
"build",
"--platform",
platformValue,
"--load",
"--label",
s"com.typesafe.sbt.packager.graalvmnativeimage.platform=$platformValue",
"-t",
imageName,
"-"
)

val ret = sys.process.Process(platformArch.map(_ => buildxCommand).getOrElse(buildCommand)) #<
new ByteArrayInputStream(dockerContent.getBytes()) !
DockerPlugin.publishLocalLogger(streams.log)

Expand All @@ -219,7 +279,7 @@ object GraalVMNativeImagePlugin extends AutoPlugin {
val mappings = classpathJars ++ resources.map {
case (resource, path) => resource -> s"resources/$path"
}
Stager.stage(GraalVMBaseImage)(streams, stageDir, mappings)
Stager.stage(GraalVMBaseImagePath)(streams, stageDir, mappings)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
enablePlugins(GraalVMNativeImagePlugin)

name := "docker-test"
version := "0.1.0"
graalVMNativeImageOptions := Seq("--no-fallback")
graalVMNativeImageGraalVersion := Some("native-image-community:17.0.8")
graalVMNativeImagePlatformArch := Some("arm64")
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("com.github.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")
}
}
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 'docker run --rm --platform arm64 -v .:/test -w /test ubuntu ./target/graalvm-native-image/docker-test | grep -q "Hello Graal"'
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
enablePlugins(GraalVMNativeImagePlugin)

name := "docker-test"
version := "0.1.0"
graalVMNativeImageOptions := Seq("--no-fallback")
graalVMNativeImageGraalVersion := Some("native-image:22.3.3")
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("com.github.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")
}
}
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"'
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
enablePlugins(GraalVMNativeImagePlugin)

name := "docker-test"
version := "0.1.0"
graalVMNativeImageOptions := Seq("--no-fallback")
graalVMNativeImageGraalVersion := Some("22.3.3")
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("com.github.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")
}
}
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"'
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ enablePlugins(GraalVMNativeImagePlugin)

name := "docker-test"
version := "0.1.0"
graalVMNativeImageGraalVersion := Some("22.0.0.2")
graalVMNativeImageOptions := Seq("--no-fallback")
graalVMNativeImageGraalVersion := Some("graalvm-community:17.0.8")
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
enablePlugins(GraalVMNativeImagePlugin)

name := "simple-test"

version := "0.1.0"
graalVMNativeImageOptions := Seq("--no-fallback")
42 changes: 35 additions & 7 deletions src/sphinx/formats/graalvm-native-image.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ GraalVM Native Image Plugin

GraalVM's ``native-image`` compiles Java programs AOT (ahead-of-time) into native binaries.

https://www.graalvm.org/22.1/reference-manual/native-image/ documents the AOT compilation of GraalVM.
https://www.graalvm.org/latest/reference-manual/native-image/ 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 macOS.
container, you can build Linux native images not only on Linux but also on Windows and macOS and for different architectures
like amd64 or arm64.

Requirements
------------

To build using a local installation of GraalVM, you must have the ``native-image`` utility of GraalVM in your ``PATH``.
To build using a docker container, you must have a working installation of docker.
To build for a different architecture, you must have docker with the buildx plugin and QEMU set up for the target architecture.

``native-image`` quick installation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -65,16 +68,41 @@ By default, a local build will be done, expecting the ``native-image`` command t
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 <https://github.com/graalvm/container/pkgs/container/graalvm-ce/>`_. This setting has no
effect if ``containerBuildImage`` is explicitly set.
Setting this enables using a Docker container to build the native image.
It should be in the format ``<packageName>:<tagName>``. `Supported packages <https://github.com/orgs/graalvm/packages?repo_name=container>`_ include:
* ``graalvm-ce`` - Versions prior to and including 22.3.3. An intermediate image will be created.
* ``native-image`` - Versions prior to and including 22.3.3. The docker image will be used directly.
* ``graalvm-community`` - Versions after and including 17.0.7. An intermediate image will be created.
* ``native-image-community`` - Versions after and including 17.0.7. The docker image will be used directly.

The legacy format of specifying the version number is supported up to 22.3.3

This setting has no effect if ``containerBuildImage`` is explicitly set.

For example:

.. code-block:: scala

graalVMNativeImageGraalVersion := Some("19.1.1") // Legacy GraalVM versions supported up to 22.3.3
graalVMNativeImageGraalVersion := Some("graalvm-ce:19.1.1") // Legacy GraalVM versions supported up to 22.3.3
graalVMNativeImageGraalVersion := Some("native-image:19.1.1") // Uses the legacy native-image image from GraalVM directly
graalVMNativeImageGraalVersion := Some("graalvm-community:17.0.8") // New GraalVM version scheme
graalVMNativeImageGraalVersion := Some("native-image-community:17.0.8") // Uses the native-image image from GraalVM directly

``graalVMNativeImagePlatformArch``
Setting this enables building the native image for a different platform architecture. Requires ``graalVMNativeImageGraalVersion``
or ``containerBuildImage`` to be set. Multiplatform builds are currently not supported. Defaults to the platform of the host.
If ``containerBuildImage`` is specified, ensure that your specified image has the same platform that you are targeting.

Requires Docker buildx plugin with a valid builder and QEMU set up for the target platform architecture.
`See here for more information <https://docs.docker.com/build/building/multi-platform/#building-multi-platform-images>`_.

For example:

.. code-block:: scala

graalVMNativeImageGraalVersion := Some("19.1.1")
graalVMNativeImagePlatformArch := Some("arm64")
graalVMNativeImagePlatformArch := Some("linux/amd64")

``containerBuildImage``

Expand Down
Loading