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 5 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
8 changes: 4 additions & 4 deletions .github/workflows/validate-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,11 +252,11 @@ jobs:
restore-keys: |
${{ runner.os }}-ivy-
- name: Setup GraalVM environment
uses: olafurpg/setup-scala@v10
uses: graalvm/setup-graalvm@v1
with:
java-version: graalvm@22.0.0=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: Validate
run: sbt "^validateGraalVMNativeImage"

Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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")
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 @@ -14,6 +14,10 @@ trait GraalVMNativeImageKeys {
val graalVMNativeImageGraalVersion = settingKey[Option[String]](
"Version of GraalVM to build with. Setting this has the effect of generating a container build image to build the native image with this version of GraalVM."
)

val graalVMNativeImagePlatformArch = settingKey[Option[String]](
"Platform architecture of GraalVM to build with. This only works when building the native image with a container."
)
}

trait GraalVMNativeImageKeysEx extends GraalVMNativeImageKeys {
Expand Down
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,20 @@ 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 "graalvm-community" => Def.task(Some(s"$GraalVMBaseImagePath$packageName:$tag"): Option[String])
case _ =>
generateContainerBuildImage(
s"${GraalVMBaseImagePath}graalvm-ce:$tag",
graalVMNativeImagePlatformArch.value
)
}
case Some(tag) =>
generateContainerBuildImage(s"${GraalVMBaseImagePath}graalvm-ce:$tag", graalVMNativeImagePlatformArch.value)
case None => Def.task(None: Option[String])
}
}.value,
packageBin := {
Expand All @@ -59,6 +71,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 +98,7 @@ object GraalVMNativeImagePlugin extends AutoPlugin {
className,
classpathJars,
extraOptions,
platformArch,
dockerCommand,
resourceMappings,
image,
Expand Down Expand Up @@ -131,26 +145,32 @@ 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

val command = dockerCommand ++ Seq(
"run",
"--workdir",
"/opt/graalvm",
"--rm",
"--platform",
platformArch.getOrElse(hostPlatform),
"-v",
s"${targetDirectory.getAbsolutePath}:$graalDestDir",
image,
"native-image",
"-cp",
(resourcesDestDir +: classpathJars.map(jar => s"$stageDestDir/" + jar._2)).mkString(":"),
s"-H:Name=$binaryName"
Expand All @@ -166,35 +186,62 @@ 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
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(
Cmd("FROM", baseImage),
Cmd("WORKDIR", "/opt/graalvm"),
ExecCmd("RUN", "gu", "install", "native-image"),
ExecCmd("RUN", "sh", "-c", "ln -s /opt/graalvm-ce-*/bin/native-image /usr/local/bin/native-image"),
ExecCmd("ENTRYPOINT", "native-image")
ExecCmd("CMD", "native-image")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will never get the difference in my head. Why is this change from entrypoint to cmd needed?

Copy link
Contributor Author

@kgston kgston Jul 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the difference between ENTRYPOINT and CMD is that the former predetermines the executable that will run in the docker container while the latter just sets it as the default, but allows you to override it at runtime.

I gave it a bit more thought and looked at the available options again and I found a way to return to using ENTRYPOINT instead.

Originally, the reason why I used CMD is because in the latest GraalVM images that Oracle provides, bundles the native-image executable out of the box by default, so there is no longer any real need to create a custom docker image that installs the native-image executable. Instead, we can just directly use the docker image provided by Oracle. However, in order to keep the docker run command consistent between both the Oracle provided image and the legacy native-packager generated image, I changed the legacy image to use CMD instead of ENTRYPOINT.

But on more research, it seems like there are 2 versions of the image that we can use. graalvm-community is a generic version that uses CMD and allows you to choose your executable, and a native-image-community specialized one that uses ENTRYPOINT. I tweaked the code such that we can support both images while keeping the previous style of using ENTRYPOINT as the entry to the docker image.

).makeContent

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

val ret = sys.process.Process(command) #<
new ByteArrayInputStream(dockerContent.getBytes()) !
Expand All @@ -219,7 +266,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("graalvm-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("graalvm-ce: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")
16 changes: 15 additions & 1 deletion src/sphinx/formats/graalvm-native-image.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,21 @@ customized using the following settings.

.. code-block:: scala

graalVMNativeImageGraalVersion := Some("19.1.1")
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("graalvm-community:17.0.8") // New GraalVM version scheme

``graalVMNativeImagePlatformArch``
Setting this enables building the native image on a different platform architecture. Requires ``graalVMNativeImageGraalVersion``
to be set. Multiplatform builds is 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.

For example:

.. code-block:: scala

graalVMNativeImagePlatformArch := Some("arm64")
graalVMNativeImagePlatformArch := Some("amd64")

``containerBuildImage``

Expand Down