diff --git a/CLAUDE.md b/CLAUDE.md index db5c7ebc..181ac303 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,4 +89,5 @@ sbt-pack is an SBT plugin for creating distributable Scala packages. It bundles - Version conflict resolution uses custom `VersionString` comparison - Supports multi-module projects with different main classes - Handles both application and library packaging scenarios -- Special Docker-friendly features for container deployment \ No newline at end of file +- Special Docker-friendly features for container deployment +- Before pushing changes, ensure formatting code with `./sbt scalafmtAll` \ No newline at end of file diff --git a/README.md b/README.md index affdf0da..370f0af5 100755 --- a/README.md +++ b/README.md @@ -76,6 +76,24 @@ packMain := Map("hello" -> "myprog.Hello") // [Optional] JVM options of scripts (program name -> Seq(JVM option, ...)) packJvmOpts := Map("hello" -> Seq("-Xmx512m")) +// [Optional] Java version-specific JVM options (since 0.22) +// The launch scripts will detect the Java version and apply appropriate options +// Options are applied as version ranges: +// - Java 8-10 will get the options for version 8 +// - Java 11-16 will get the options for version 11 +// - Java 17-20 will get the options for version 17 +// - Java 21-23 will get the options for version 21 +// - Java 24+ will get the options for version 24 +packJvmVersionSpecificOpts := Map( + "hello" -> Map( + 8 -> Seq("-XX:MaxPermSize=256m"), // Applied for Java [8,11) + 11 -> Seq("-XX:+UseJVMCICompiler"), // Applied for Java [11,17) + 17 -> Seq("-XX:+UseZGC"), // Applied for Java [17,21) + 21 -> Seq("-XX:+UseZGC", "-XX:+ZGenerational"), // Applied for Java [21,24) + 24 -> Seq("--sun-misc-unsafe-memory-access=allow") // Applied for Java [24,∞) + ) +) + // [Optional] Extra class paths to look when launching a program. You can use ${PROG_HOME} to specify the base directory packExtraClasspath := Map("hello" -> Seq("${PROG_HOME}/etc")) diff --git a/src/main/scala/xerial/sbt/pack/LaunchScript.scala b/src/main/scala/xerial/sbt/pack/LaunchScript.scala index 7b00dfc5..6a59d88a 100644 --- a/src/main/scala/xerial/sbt/pack/LaunchScript.scala +++ b/src/main/scala/xerial/sbt/pack/LaunchScript.scala @@ -21,6 +21,7 @@ object LaunchScript { PROG_VERSION: String, PROG_REVISION: String, JVM_OPTS: String = "", + JVM_VERSION_OPTS: Map[Int, String] = Map.empty, EXTRA_CLASSPATH: String, MAC_ICON_FILE: String = "icon-mac.png", ENV_VARS: String = "" diff --git a/src/main/scala/xerial/sbt/pack/PackPlugin.scala b/src/main/scala/xerial/sbt/pack/PackPlugin.scala index 6358b5a8..e22ebd81 100755 --- a/src/main/scala/xerial/sbt/pack/PackPlugin.scala +++ b/src/main/scala/xerial/sbt/pack/PackPlugin.scala @@ -77,7 +77,10 @@ object PackPlugin extends AutoPlugin with PackArchive { val packAllUnmanagedJars = taskKey[Seq[(Classpath, ProjectRef)]]("all unmanaged jar files") val packModuleEntries = taskKey[Seq[ModuleEntry]]("modules that will be packed") val packJvmOpts = settingKey[Map[String, Seq[String]]]("pack-jvm-opts") - val packExtraClasspath = settingKey[Map[String, Seq[String]]]("pack-extra-classpath") + val packJvmVersionSpecificOpts = settingKey[Map[String, Map[Int, Seq[String]]]]( + "Java version specific JVM options. Map[progName, Map[javaVersion, Seq[options]]]. Options are applied as ranges: if versions 8, 11, and 17 are specified, then Java [8,11) uses options for 8, [11,17) uses options for 11, and [17,∞) uses options for 17" + ) + val packExtraClasspath = settingKey[Map[String, Seq[String]]]("pack-extra-classpath") val packExpandedClasspath = settingKey[Boolean]("Expands the wildcard classpath in launch scripts to point at specific libraries") val packJarNameConvention = settingKey[String]( @@ -140,6 +143,7 @@ object PackPlugin extends AutoPlugin with PackArchive { packMacIconFile := "icon-mac.png", packResourceDir := Map(baseDirectory.value / "src/pack" -> ""), packJvmOpts := Map.empty, + packJvmVersionSpecificOpts := Map.empty, packExtraClasspath := Map.empty, packExpandedClasspath := false, packJarNameConvention := "default", @@ -393,12 +397,17 @@ object PackPlugin extends AutoPlugin with PackArchive { None } + val versionSpecificOpts = packJvmVersionSpecificOpts.value + .getOrElse(name, Map.empty) + .map { case (version, opts) => version -> opts.map("\"%s\"".format(_)).mkString(" ") } + val scriptOpts = LaunchScript.Opts( PROG_NAME = name, PROG_VERSION = progVersion, PROG_REVISION = gitRevision, MAIN_CLASS = mainClass, JVM_OPTS = packJvmOpts.value.getOrElse(name, Nil).map("\"%s\"".format(_)).mkString(" "), + JVM_VERSION_OPTS = versionSpecificOpts, EXTRA_CLASSPATH = extraClasspath(pathSeparator), MAC_ICON_FILE = macIconFile, ENV_VARS = @@ -428,7 +437,10 @@ object PackPlugin extends AutoPlugin with PackArchive { PROG_VERSION = progVersion, PROG_REVISION = gitRevision, MAIN_CLASS = mainClass, - JVM_OPTS = replaceProgHome(scriptOpts.JVM_OPTS), + JVM_OPTS = replaceProgHome(scriptOpts.JVM_OPTS).replace("\"", ""), // Remove quotes for Windows batch script + JVM_VERSION_OPTS = scriptOpts.JVM_VERSION_OPTS.map { case (v, opts) => + v -> replaceProgHome(opts).replace("\"", "") // Remove quotes for Windows batch script + }, EXTRA_CLASSPATH = replaceProgHome(extraPath), MAC_ICON_FILE = replaceProgHome(macIconFile) ) diff --git a/src/main/twirl/xerial/sbt/pack/launch.scala.txt b/src/main/twirl/xerial/sbt/pack/launch.scala.txt index c86321dc..23aaf6a7 100644 --- a/src/main/twirl/xerial/sbt/pack/launch.scala.txt +++ b/src/main/twirl/xerial/sbt/pack/launch.scala.txt @@ -104,6 +104,39 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." 1>&2 fi +# Detect Java version for version-specific options +getJavaVersion() { + java_cmd="$1" + str=$("$java_cmd" -version 2>&1 | grep -E -e '(java|openjdk) version' | head -n 1 | awk '{ print $3 }' | tr -d '"') + + # java -version on java8 says 1.8.x + # but on 9 and 10 it's 9.x.y and 10.x.y. + case "$str" in + 1.*) + echo "$str" | cut -d'.' -f2 + ;; + *) + echo "$str" | cut -d'.' -f1 | cut -d'-' -f1 + ;; + esac +} + +JAVA_VERSION=$(getJavaVersion "$JAVACMD") + +# Add version-specific JVM options using range matching +# Options are applied based on version ranges: if versions 21 and 24 are specified, +# then [21,24) gets opts for 21, [24,∞) gets opts for 24 +@if(opts.JVM_VERSION_OPTS.nonEmpty) { +@for(version <- opts.JVM_VERSION_OPTS.keys.toList.sorted) { +if [ "$JAVA_VERSION" -ge @version ]; then + VERSION_OPTS="@opts.JVM_VERSION_OPTS(version)" +fi +} +if [ -n "$VERSION_OPTS" ]; then + JAVA_OPTS="$JAVA_OPTS $VERSION_OPTS" +fi +} + CLASSPATH_SUFFIX="" # Path separator used in EXTRA_CLASSPATH PSEP=":" diff --git a/src/main/twirl/xerial/sbt/pack/launch_bat.scala.txt b/src/main/twirl/xerial/sbt/pack/launch_bat.scala.txt index 4f660d42..50dd59e6 100644 --- a/src/main/twirl/xerial/sbt/pack/launch_bat.scala.txt +++ b/src/main/twirl/xerial/sbt/pack/launch_bat.scala.txt @@ -88,6 +88,31 @@ goto Win9xApp SET PROG_HOME=%~dp0.. SET PSEP=; +@@REM Detect Java version for version-specific options +for /f tokens^=2-5^ delims^=.-_+^" %%j in ('%JAVA_EXE% -version 2^>^&1') do ( + set "JAVA_VERSION_FULL=%%j.%%k.%%l.%%m" + if "%%j" == "1" ( + set "JAVA_VERSION=%%k" + ) else ( + set "JAVA_VERSION=%%j" + ) +) + +@@REM Add version-specific JVM options using range matching +@@REM Options are applied based on version ranges: if versions 21 and 24 are specified, +@@REM then [21,24) gets opts for 21, [24,infinity) gets opts for 24 +@if(opts.JVM_VERSION_OPTS.nonEmpty) { +set "VERSION_OPTS=" +@for(version <- opts.JVM_VERSION_OPTS.keys.toList.sorted) { +if %JAVA_VERSION% GEQ @version ( + set "VERSION_OPTS=@opts.JVM_VERSION_OPTS(version)" +) +} +if defined VERSION_OPTS ( + set "JAVA_OPTS=%JAVA_OPTS% %VERSION_OPTS%" +) +} + @@REM Start Java program :runm2 SET CMDLINE=%JAVA_EXE% @(opts.JVM_OPTS) %JAVA_OPTS% -cp @if(expandedClasspath.isEmpty){"@(opts.EXTRA_CLASSPATH)%PROG_HOME%\lib\*;" } else {"@(opts.EXTRA_CLASSPATH)@(expandedClasspath.get);" } -Dprog.home="%PROG_HOME%" -Dprog.version="@(opts.PROG_VERSION)" -Dprog.revision="@(opts.PROG_REVISION)" @(opts.MAIN_CLASS) %CMD_LINE_ARGS% diff --git a/src/sbt-test/sbt-pack/jvm-version-opts/build.sbt b/src/sbt-test/sbt-pack/jvm-version-opts/build.sbt new file mode 100644 index 00000000..c58062fc --- /dev/null +++ b/src/sbt-test/sbt-pack/jvm-version-opts/build.sbt @@ -0,0 +1,20 @@ +enablePlugins(PackPlugin) +name := "jvm-version-opts-test" + +scalaVersion := "2.13.16" +crossPaths := false + +packMain := Map("test" -> "Main") +packJvmOpts := Map("test" -> Seq("-Xms256m", "-Xmx512m")) + +// Configure different JVM options for different Java versions +// These are applied as ranges: [8,11), [11,17), [17,21), [21,24), [24,∞) +packJvmVersionSpecificOpts := Map( + "test" -> Map( + 8 -> Seq("-XX:MaxPermSize=256m"), + 11 -> Seq("-XX:+UnlockExperimentalVMOptions", "-XX:+UseJVMCICompiler"), + 17 -> Seq("-XX:+UseZGC"), + 21 -> Seq("-XX:+UseZGC", "-XX:+ZGenerational"), + 24 -> Seq("-XX:+UseG1GC", "-XX:G1HeapRegionSize=32m", "--sun-misc-unsafe-memory-access=allow") + ) +) \ No newline at end of file diff --git a/src/sbt-test/sbt-pack/jvm-version-opts/project/build.properties b/src/sbt-test/sbt-pack/jvm-version-opts/project/build.properties new file mode 100644 index 00000000..cc68b53f --- /dev/null +++ b/src/sbt-test/sbt-pack/jvm-version-opts/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.10.11 diff --git a/src/sbt-test/sbt-pack/jvm-version-opts/project/plugins.sbt b/src/sbt-test/sbt-pack/jvm-version-opts/project/plugins.sbt new file mode 100644 index 00000000..58a6863f --- /dev/null +++ b/src/sbt-test/sbt-pack/jvm-version-opts/project/plugins.sbt @@ -0,0 +1,7 @@ +{ + val pluginVersion = System.getProperty("plugin.version") + if (pluginVersion == null) + throw new RuntimeException("""|The system property 'plugin.version' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) + else addSbtPlugin("org.xerial.sbt" % "sbt-pack" % pluginVersion) +} \ No newline at end of file diff --git a/src/sbt-test/sbt-pack/jvm-version-opts/src/main/scala/Main.scala b/src/sbt-test/sbt-pack/jvm-version-opts/src/main/scala/Main.scala new file mode 100644 index 00000000..d019e8c2 --- /dev/null +++ b/src/sbt-test/sbt-pack/jvm-version-opts/src/main/scala/Main.scala @@ -0,0 +1,86 @@ +object Main { + def main(args: Array[String]): Unit = { + // Print Java version and JVM options for testing + val javaVersion = System.getProperty("java.version") + println(s"Java version: $javaVersion") + + // Extract major version number + val majorVersion = if (javaVersion.startsWith("1.")) { + javaVersion.split("\\.")(1).toInt + } else { + javaVersion.split("\\.")(0).split("-")(0).toInt + } + println(s"Major version: $majorVersion") + + val runtimeMxBean = java.lang.management.ManagementFactory.getRuntimeMXBean + val jvmArgs = runtimeMxBean.getInputArguments + + println("JVM Arguments:") + import scala.jdk.CollectionConverters._ + val argsList = jvmArgs.asScala.toList + argsList.foreach { arg => + println(s" $arg") + } + + // Validate that we have the base JVM options + val hasXms = argsList.exists(_.contains("-Xms256m")) + val hasXmx = argsList.exists(_.contains("-Xmx512m")) + + println("\nValidation:") + println(s" Has -Xms256m: $hasXms") + println(s" Has -Xmx512m: $hasXmx") + + // Validate version-specific options based on current Java version + // Using range-based logic: options are applied from the highest version <= current version + val expectedVersionOptions = majorVersion match { + case _ if majorVersion >= 8 && majorVersion < 11 => + // Java [8,11) should have MaxPermSize + val hasMaxPermSize = argsList.exists(_.contains("MaxPermSize")) + println(s" Has MaxPermSize (Java [8,11)): $hasMaxPermSize") + hasMaxPermSize + case _ if majorVersion >= 11 && majorVersion < 17 => + // Java [11,17) should have UseJVMCICompiler + val hasJVMCI = argsList.exists(_.contains("UseJVMCICompiler")) + println(s" Has UseJVMCICompiler (Java [11,17)): $hasJVMCI") + hasJVMCI + case _ if majorVersion >= 17 && majorVersion < 21 => + // Java [17,21) should have UseZGC + val hasZGC = argsList.exists(_.contains("UseZGC")) + println(s" Has UseZGC (Java [17,21)): $hasZGC") + hasZGC + case _ if majorVersion >= 21 && majorVersion < 24 => + // Java [21,24) should have UseZGC and ZGenerational + val hasZGC = argsList.exists(_.contains("UseZGC")) + val hasGenZGC = argsList.exists(_.contains("ZGenerational")) + println(s" Has UseZGC (Java [21,24)): $hasZGC") + println(s" Has ZGenerational (Java [21,24)): $hasGenZGC") + hasZGC && hasGenZGC + case _ if majorVersion >= 24 => + // Java [24,∞) should have UseG1GC, G1HeapRegionSize, and sun-misc-unsafe-memory-access + val hasG1GC = argsList.exists(_.contains("UseG1GC")) + val hasG1HeapRegion = argsList.exists(_.contains("G1HeapRegionSize")) + val hasUnsafeMemoryAccess = argsList.exists(_.contains("sun-misc-unsafe-memory-access=allow")) + println(s" Has UseG1GC (Java [24,∞)): $hasG1GC") + println(s" Has G1HeapRegionSize (Java [24,∞)): $hasG1HeapRegion") + println(s" Has --sun-misc-unsafe-memory-access=allow (Java [24,∞)): $hasUnsafeMemoryAccess") + hasG1GC && hasG1HeapRegion && hasUnsafeMemoryAccess + case _ => + // For versions before 8 or when no options are defined + println(s" No version-specific options for Java $majorVersion") + true + } + + // Exit with error if validation fails + if (!hasXms || !hasXmx) { + println("\nERROR: Base JVM options not found!") + System.exit(1) + } + + if (!expectedVersionOptions) { + println(s"\nERROR: Expected version-specific JVM options for Java $majorVersion not found!") + System.exit(1) + } + + println("\nAll validations passed!") + } +} \ No newline at end of file diff --git a/src/sbt-test/sbt-pack/jvm-version-opts/test b/src/sbt-test/sbt-pack/jvm-version-opts/test new file mode 100644 index 00000000..1167499e --- /dev/null +++ b/src/sbt-test/sbt-pack/jvm-version-opts/test @@ -0,0 +1,6 @@ +> 'set version := "0.1"' +> pack +$ exists target/pack/bin/test +$ exists target/pack/bin/test.bat +# Run the test program to verify it works (this also validates that version detection doesn't break the script) +$ exec sh ./target/pack/bin/test