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

Dynamically choose GraalVM version #2636

Closed
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -401,14 +401,14 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
destPath

case PackageType.GraalVMNativeImage =>
NativeImage.buildNativeImage(
value(NativeImage.buildNativeImage(
build,
value(mainClass),
destPath,
build.inputs.nativeImageWorkDir,
extraArgs,
logger
)
))
destPath

case nativePackagerType: PackageType.NativePackagerType =>
Expand Down
34 changes: 34 additions & 0 deletions modules/cli/src/main/scala/scala/cli/commands/util/JvmUtils.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package scala.cli.commands
package util

import coursier.jvm.JvmIndexEntry

import java.io.File

import scala.build.EitherCps.{either, value}
Expand All @@ -13,6 +15,7 @@ import scala.build.{Os, Position, Positioned, options as bo}
import scala.cli.commands.shared.{CoursierOptions, SharedJvmOptions, SharedOptions}
import scala.concurrent.ExecutionContextExecutorService
import scala.util.control.NonFatal
import scala.util.matching.Regex
import scala.util.{Failure, Properties, Success, Try}

object JvmUtils {
Expand Down Expand Up @@ -118,4 +121,35 @@ object JvmUtils {
javaCmd <- getJavaCmdVersionOrHigher(javaVersion, options)
} yield javaCmd
}

/** Returns the available JVMs for the given version and name. At least one of the two must be
* specified (otherwise, an empty sequence is returned).
*/
def getMatchingJvms(
buildOptions: bo.BuildOptions,
versionOpt: Option[String],
nameOpt: Option[String] = None
): Seq[JvmIndexEntry] =
for {
jvmCache <- buildOptions.javaHomeManager.cache.toSeq
jvmId <-
nameOpt.fold(versionOpt.toSeq)(n => Seq(s"$n${versionOpt.fold("")(_.prepended('@'))}"))
entry <- jvmCache.entries(jvmId)
.unsafeRun()(buildOptions.finalCache.ec)
.toSeq
.flatten
} yield entry

/** Returns the IDs of the available JVMs that match the specified Id regex. */
def getJvmsById(
buildOptions: bo.BuildOptions,
regex: Regex
): Seq[String] = for {
jvmCache <- buildOptions.javaHomeManager.cache.toSeq
jvmIndexTask <- jvmCache.index.toSeq
jvmIndex = jvmIndexTask.unsafeRun()(buildOptions.finalCache.ec)
index <- jvmIndex.available().toSeq
(id, /*versionIndex*/ _) <- index
if regex.matches(id)
} yield id
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ package scala.cli.errors

import scala.build.errors.BuildException

final class GraalVMNativeImageError()
extends BuildException(s"Error building native image with GraalVM")
final class GraalVMNativeImageError(msg: String = "Error building native image with GraalVM")
extends BuildException(msg)
111 changes: 106 additions & 5 deletions modules/cli/src/main/scala/scala/cli/packaging/NativeImage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ package scala.cli.packaging
import java.io.File

import scala.annotation.tailrec
import scala.build.EitherCps.{either, value}
import scala.build.errors.BuildException
import scala.build.internal.{ManifestJar, Runner}
import scala.build.options.BuildOptions
import scala.build.options.BuildOptions.JavaHomeInfo
import scala.build.{Build, Logger, Positioned}
import scala.cli.commands.util.JvmUtils
import scala.cli.errors.GraalVMNativeImageError
import scala.cli.graal.{BytecodeProcessor, TempCache}
import scala.cli.internal.CachedBinary
import scala.concurrent.ExecutionContext
import scala.util.Properties

object NativeImage {
Expand Down Expand Up @@ -35,6 +41,94 @@ object NativeImage {
nativeImage
}

/** Check whether the JVM used for compilation is compatible with the chosen GraalVM */
private def getCompatibleGraalVmId(
build: Build.Successful,
logger: Logger
): Either[GraalVMNativeImageError, String] = {
def defaultError(details: String) =
new GraalVMNativeImageError("Couldn't fetch a correct GraalVM JVM: " + details)

val options = build.options
val cacheEc = options.finalCache.ec
val Positioned(buildJvmSource, buildJavaHome) = options.javaHome()
val buildJvmVersion = buildJavaHome.version

val forcedGraalJvmVersion =
build.options.notForBloopOptions.packageOptions.nativeImageOptions.graalvmJavaVersion

if (forcedGraalJvmVersion.exists(_ < buildJvmVersion))
Left(GraalVMNativeImageError(
s"""Cannot build a native image with a JVM older than the one used for compilation.
|Specified Versions:
| - compilation JVM: $buildJvmVersion, taken from ${buildJvmSource.mkString(", ")}
| - graalVM JVM specified: ${forcedGraalJvmVersion.get}
|""".stripMargin
))
else {
val availableGraalVMJavaVersions = JvmUtils.getJvmsById(options, "graalvm-java\\d\\d?".r)
.map(_.stripPrefix("graalvm-java").toInt)
.sorted

val maybeFinalJavaVersion: Either[GraalVMNativeImageError, Int] = forcedGraalJvmVersion
.orElse {
def isLtsJava(version: Int): Boolean = Seq(8, 11, 17, 21).contains(version)
availableGraalVMJavaVersions.filter(_ >= buildJvmVersion) match
case Nil => None
case versions if versions.forall(v => !isLtsJava(v)) =>
logger.diagnostic("Using a GraalVM for non-LTS Java version")
Some(versions.min)
// Prefer LTS Java versions
case versions => versions.find(isLtsJava)
}
.toRight(defaultError(
s"""The JVM version used for compiling is too high to find a compatible GraalVM JVM.
| - compilation JVM: $buildJvmVersion, taken from ${buildJvmSource.mkString(", ")}
|""".stripMargin
))

val deducedOrSpecified = forcedGraalJvmVersion.fold("deduced")(_ => "specified")

val graalVmJvmId = for {
finalJavaVersion <- maybeFinalJavaVersion
cache <- build.options.javaHomeManager.cache
.toRight(defaultError("No JVM cache found"))

graalVmIndexEntries <- cache.entries(s"graalvm-java$finalJavaVersion")
.map(_.left.map(defaultError))
.unsafeRun()(cacheEc)

_ <- if graalVmIndexEntries.isEmpty then
Left(defaultError(
s"""A release of GraalVM compatible with the $deducedOrSpecified JVM version could not be found.
| - GraalVM JVM $deducedOrSpecified: $finalJavaVersion
| - compilation JVM: $buildJvmVersion, taken from ${buildJvmSource.mkString(", ")}
|Use '--graalvm-java-version' to force a different JVM version.
|""".stripMargin
))
else Right(())

forcedGraalVersion =
build.options.notForBloopOptions.packageOptions.nativeImageOptions.graalvmVersion
finalIndexEntry <-
forcedGraalVersion.fold(Right(graalVmIndexEntries.maxBy(_.version))) { forcedV =>
graalVmIndexEntries.find(_.version == forcedV)
.toRight(defaultError(
s"""No GraalVM found with version ${forcedGraalVersion.getOrElse("")}
|Available versions for the $deducedOrSpecified JVM $finalJavaVersion:
| - ${graalVmIndexEntries.map(_.version).mkString("\n - ")}
|Use '--graalvm-version' to force a different GraalVM version.
|Use '--graalvm-java-version' to force a different JVM version.
|Or Use '--graalvm-jvm-id' to specify both, e.g. '--graalvm-jvm-id graalvm-java17:22.0.0'.
|""".stripMargin
))
}
} yield finalIndexEntry.id

graalVmJvmId
}
}

private def vcVersions = Seq("2022", "2019", "2017")
private def vcEditions = Seq("Enterprise", "Community", "BuildTools")
lazy val vcvarsCandidates = Option(System.getenv("VCVARSALL")) ++ {
Expand Down Expand Up @@ -172,14 +266,19 @@ object NativeImage {
nativeImageWorkDir: os.Path,
extraOptions: Seq[String],
logger: Logger
): Unit = {

): Either[BuildException, Unit] = either {
os.makeDir.all(nativeImageWorkDir)

val jvmId = build.options.notForBloopOptions.packageOptions.nativeImageOptions.jvmId
val graalVmId =
build.options.notForBloopOptions.packageOptions.nativeImageOptions.graalvmJvmId.getOrElse {
value(getCompatibleGraalVmId(build, logger))
}

logger.message(s"Using GraalVM JVM: $graalVmId")

val options = build.options.copy(
javaOptions = build.options.javaOptions.copy(
jvmIdOpt = Some(Positioned.none(jvmId))
jvmIdOpt = Some(Positioned.none(graalVmId))
)
)

Expand Down Expand Up @@ -240,6 +339,8 @@ object NativeImage {
mainClass
) ++ nativeImageArgs

pprint.err.log(args)

maybeWithShorterGraalvmHome(javaHome.javaHome, logger) { graalVMHome =>

val nativeImageCommand = ensureHasNativeImageCommand(graalVMHome, logger)
Expand Down Expand Up @@ -269,7 +370,7 @@ object NativeImage {
)
}
else
throw new GraalVMNativeImageError
throw new GraalVMNativeImageError()
}
}
finally util.Try(toClean.foreach(os.remove.all))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,19 @@ abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersio
}
}

def packageProc(extraArgs: os.Shellable*) =
os.proc(
TestUtil.cli,
"--power",
"package",
extraOptions,
".",
extraArgs,
"--native-image",
"--",
"--no-fallback"
)

test("native image") {
val message = "Hello from native-image"
val dest = "hello"
Expand All @@ -882,18 +895,7 @@ abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersio
|""".stripMargin
)
inputs.fromRoot { root =>
os.proc(
TestUtil.cli,
"--power",
"package",
extraOptions,
".",
"--native-image",
"-o",
dest,
"--",
"--no-fallback"
).call(
packageProc("-o", dest).call(
cwd = root,
stdin = os.Inherit,
stdout = os.Inherit
Expand All @@ -909,6 +911,120 @@ abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersio
}
}

for {
jvmVersion <- Seq(8, 11, 17, 21)
if !Properties.isWin || jvmVersion != 21 // our Windows GitHub runner can't use graalVmJvm 21
}
test(s"native image with JVM $jvmVersion") {
val message = "Hello from native-image"
val dest = "hello"
val actualDest =
if (Properties.isWin) "hello.exe"
else "hello"
val inputs = TestInputs(
os.rel / "Hello.scala" ->
s"""object Hello {
| def main(args: Array[String]): Unit =
| println("$message")
|}
|""".stripMargin
)

val extraEnv: Map[String, String] = if (jvmVersion == 21)
Map("USE_NATIVE_IMAGE_JAVA_PLATFORM_MODULE_SYSTEM" -> "true")
else Map.empty

inputs.fromRoot { root =>
packageProc("--jvm", jvmVersion, "-o", dest).call(
cwd = root,
stdin = os.Inherit,
stdout = os.Inherit,
env = extraEnv
)

expect(os.isFile(root / actualDest))

val res = os.proc(root / actualDest).call(cwd = root)
val output = res.out.trim()
expect(output == message)
}
}

test(s"native image with --jvm 21 and --graalvm-java-version 17") {
val inputs = TestInputs(
os.rel / "Hello.scala" ->
s"""object Hello {
| def main(args: Array[String]): Unit =
| println("nope")
|}
|""".stripMargin
)

inputs.fromRoot { root =>
val res = packageProc("--jvm", 21, "--graalvm-java-version", 17)
.call(
cwd = root,
stderr = os.Pipe,
check = false
)

expect(res.exitCode == 1)

val errOutput = res.err.text().trim()
.linesIterator
.dropWhile(!_.contains("error"))
.mkString(System.lineSeparator())
.replace("Custom(/usr/libexec/java_home -v)", "CommandLine(--jvm)")

assertNoDiff(
errOutput,
"""[error] Cannot build a native image with a JVM older than the one used for compilation.
|Specified Versions:
| - compilation JVM: 21, taken from CommandLine(--jvm)
| - graalVM JVM specified: 17
|""".stripMargin
)
}
}

test(s"native image with --graalvm-vm-version 99") {
val inputs = TestInputs(
os.rel / "Hello.scala" ->
s"""object Hello {
| def main(args: Array[String]): Unit =
| println("nope")
|}
|""".stripMargin
)

inputs.fromRoot { root =>
val res = packageProc("--jvm", 17, "--graalvm-version", 99)
.call(
cwd = root,
stderr = os.Pipe,
check = false
)

expect(res.exitCode == 1)

val errOutput = res.err.text().trim()
.linesIterator
.dropWhile(!_.contains("error"))
.filterNot(_.startsWith(" - "))
.mkString(System.lineSeparator())
assertNoDiff(
errOutput,
"""[error] Couldn't fetch a correct GraalVM JVM: No GraalVM found with version 99
|Available versions for the deduced JVM 17:
|Use '--graalvm-version' to force a different GraalVM version.
|Use '--graalvm-java-version' to force a different JVM version.
|Or Use '--graalvm-jvm-id' to specify both, e.g. '--graalvm-jvm-id graalvm-java17:22.0.0'.
|""".stripMargin,
clue = res.err.text().trim()
)
}
}

test("correctly list main classes") {
val (scalaFile1, scalaFile2, scriptName) = ("ScalaMainClass1", "ScalaMainClass2", "ScalaScript")
val scriptsDir = "scripts"
Expand Down
Loading
Loading