Skip to content
Merged
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
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- Special Docker-friendly features for container deployment
- Before pushing changes, ensure formatting code with `./sbt scalafmtAll`
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

Expand Down
1 change: 1 addition & 0 deletions src/main/scala/xerial/sbt/pack/LaunchScript.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ object LaunchScript {
PROG_VERSION: String,
PROG_REVISION: String,
JVM_OPTS: String = "",
JVM_VERSION_OPTS: Map[Int, String] = Map.empty,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The current implementation for version-specific JVM options uses an exact version match. This can be inconvenient if you want to set options for a Java version and all subsequent versions (e.g., Java 11+), as implied by the 24+ comment in the README. A more flexible approach would be to apply the options for the highest configured version that is less than or equal to the detected Java version.

To implement this, I suggest changing JVM_VERSION_OPTS from a Map[Int, String] to a sorted Seq[(Int, String)]. This will allow the launch scripts to iterate through the versions and find the best match. I'll provide related suggestions in other files to complete this change.

Suggested change
JVM_VERSION_OPTS: Map[Int, String] = Map.empty,
JVM_VERSION_OPTS: Seq[(Int, String)] = Seq.empty,

EXTRA_CLASSPATH: String,
MAC_ICON_FILE: String = "icon-mac.png",
ENV_VARS: String = ""
Expand Down
16 changes: 14 additions & 2 deletions src/main/scala/xerial/sbt/pack/PackPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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](
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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)
)
Expand Down
33 changes: 33 additions & 0 deletions src/main/twirl/xerial/sbt/pack/launch.scala.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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=":"
Expand Down
25 changes: 25 additions & 0 deletions src/main/twirl/xerial/sbt/pack/launch_bat.scala.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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%
Expand Down
20 changes: 20 additions & 0 deletions src/sbt-test/sbt-pack/jvm-version-opts/build.sbt
Original file line number Diff line number Diff line change
@@ -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")
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.10.11
7 changes: 7 additions & 0 deletions src/sbt-test/sbt-pack/jvm-version-opts/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -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)
}
86 changes: 86 additions & 0 deletions src/sbt-test/sbt-pack/jvm-version-opts/src/main/scala/Main.scala
Original file line number Diff line number Diff line change
@@ -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!")
}
}
6 changes: 6 additions & 0 deletions src/sbt-test/sbt-pack/jvm-version-opts/test
Original file line number Diff line number Diff line change
@@ -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
Loading