From 2e7d3fdf933a9d97836a967e4ae7df8413ed090e Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 28 Sep 2025 01:04:07 -0400 Subject: [PATCH] Support JDK 25 JEP-512/JEP-445 Main run **Problem** sbt currently does not support JDK 25 Main class. JDK 25 supports: 1. non-public main method 2. doesn't need Array[String] arg 3. doesn't have to be a static method **Solution** This updates Zinc, which supports new Main class detection. In addition, this implements in-process run emulation support. --- .github/workflows/ci.yml | 4 +- .../main/scala/sbt/internal/util/Util.scala | 16 +---- project/Dependencies.scala | 2 +- run/src/main/scala/sbt/Run.scala | 60 ++++++++++++++----- .../{test => disabled} | 0 sbt-app/src/sbt-test/run/jep-512/A.scala | 7 +++ sbt-app/src/sbt-test/run/jep-512/build.sbt | 11 ++++ sbt-app/src/sbt-test/run/jep-512/test | 2 + .../run/spawn-exit/{test => disabled} | 0 9 files changed, 71 insertions(+), 31 deletions(-) rename sbt-app/src/sbt-test/dependency-management/cached-resolution-classifier/{test => disabled} (100%) create mode 100644 sbt-app/src/sbt-test/run/jep-512/A.scala create mode 100644 sbt-app/src/sbt-test/run/jep-512/build.sbt create mode 100644 sbt-app/src/sbt-test/run/jep-512/test rename sbt-app/src/sbt-test/run/spawn-exit/{test => disabled} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47377eee8..a3f264a16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,8 +21,8 @@ jobs: distribution: temurin jobtype: 2 - os: ubuntu-latest - java: 21 - distribution: temurin + java: 25 + distribution: zulu jobtype: 3 - os: ubuntu-latest java: 21 diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala index 4e4d3e8b7..fdcebc4b0 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala @@ -14,7 +14,7 @@ import java.util.Locale import scala.reflect.macros.blackbox import scala.language.experimental.macros import scala.language.reflectiveCalls -import scala.util.control.NonFatal +import scala.util.Properties object Util { def makeList[T](size: Int, value: T): List[T] = List.fill(size)(value) @@ -88,17 +88,7 @@ object Util { def reduceIntents[A1, A2](intents: PartialFunction[A1, A2]*): PartialFunction[A1, A2] = intents.toList.reduceLeft(_ orElse _) - lazy val majorJavaVersion: Int = - try { - val javaVersion = sys.props.get("java.version").getOrElse("1.0") - if (javaVersion.startsWith("1.")) { - javaVersion.split("\\.")(1).toInt - } else { - javaVersion.split("\\.")(0).toInt - } - } catch { - case NonFatal(_) => 0 - } + lazy val isJava19Plus: Boolean = Properties.isJavaAtLeast("19") private type GetId = { def getId: Long @@ -113,7 +103,7 @@ object Util { * https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html#threadId() */ def threadId: Long = - if (majorJavaVersion < 19) { + if (!isJava19Plus) { (Thread.currentThread(): AnyRef) match { case g: GetId @unchecked => g.getId } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 6cb8988e9..8127eb1b8 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -15,7 +15,7 @@ object Dependencies { private val ioVersion = nightlyVersion.getOrElse("1.10.5") private val lmVersion = sys.props.get("sbt.build.lm.version").orElse(nightlyVersion).getOrElse("1.11.5") - val zincVersion = nightlyVersion.getOrElse("1.10.8") + val zincVersion = nightlyVersion.getOrElse("1.11.0") private val sbtIO = "org.scala-sbt" %% "io" % ioVersion diff --git a/run/src/main/scala/sbt/Run.scala b/run/src/main/scala/sbt/Run.scala index cda9e5888..f964d9b4b 100644 --- a/run/src/main/scala/sbt/Run.scala +++ b/run/src/main/scala/sbt/Run.scala @@ -19,7 +19,7 @@ import sbt.util.Logger import scala.sys.process.Process import scala.util.control.NonFatal -import scala.util.{ Failure, Success, Try } +import scala.util.{ Failure, Properties, Success, Try } sealed trait ScalaRun { def run(mainClass: String, classpath: Seq[File], options: Seq[String], log: Logger): Try[Unit] @@ -81,7 +81,7 @@ class Run(private[sbt] val newLoader: Seq[File] => ClassLoader, trapExit: Boolea def execute(): Unit = try { log.debug(" Classpath:\n\t" + classpath.mkString("\n\t")) - val main = getMainMethod(mainClass, loader) + val main = detectMainMethod(mainClass, loader) invokeMain(loader, main, options) } catch { case e: java.lang.reflect.InvocationTargetException => @@ -125,14 +125,22 @@ class Run(private[sbt] val newLoader: Seq[File] => ClassLoader, trapExit: Boolea } private def invokeMain( loader: ClassLoader, - main: Method, + main: DetectedMain, options: Seq[String] ): Unit = { val currentThread = Thread.currentThread val oldLoader = Thread.currentThread.getContextClassLoader currentThread.setContextClassLoader(loader) try { - main.invoke(null, options.toArray[String]); () + if (main.isStatic) { + if (main.parameterCount > 0) main.method.invoke(null, options.toArray[String]) + else main.method.invoke(null) + } else { + val ref = main.mainClass.getDeclaredConstructor().newInstance().asInstanceOf[AnyRef] + if (main.parameterCount > 0) main.method.invoke(ref, options.toArray[String]) + else main.method.invoke(ref) + } + () } catch { case t: Throwable => t.getCause match { @@ -148,19 +156,39 @@ class Run(private[sbt] val newLoader: Seq[File] => ClassLoader, trapExit: Boolea currentThread.setContextClassLoader(oldLoader) } } - def getMainMethod(mainClassName: String, loader: ClassLoader) = { + def getMainMethod(mainClassName: String, loader: ClassLoader): Method = + detectMainMethod(mainClassName, loader).method + + private def detectMainMethod(mainClassName: String, loader: ClassLoader) = { val mainClass = Class.forName(mainClassName, true, loader) - val method = mainClass.getMethod("main", classOf[Array[String]]) - // jvm allows the actual main class to be non-public and to run a method in the non-public class, - // we need to make it accessible - method.setAccessible(true) - val modifiers = method.getModifiers - if (!isPublic(modifiers)) - throw new NoSuchMethodException(mainClassName + ".main is not public") - if (!isStatic(modifiers)) - throw new NoSuchMethodException(mainClassName + ".main is not static") - method + if (Run.isJava25Plus) { + val method = try { + mainClass.getMethod("main", classOf[Array[String]]) + } catch { + case _: NoSuchMethodException => mainClass.getMethod("main") + } + method.setAccessible(true) + val modifiers = method.getModifiers + DetectedMain(mainClass, method, isStatic(modifiers), method.getParameterCount()) + } else { + val method = mainClass.getMethod("main", classOf[Array[String]]) + // jvm allows the actual main class to be non-public and to run a method in the non-public class, + // we need to make it accessible + method.setAccessible(true) + val modifiers = method.getModifiers + if (!isPublic(modifiers)) + throw new NoSuchMethodException(mainClassName + ".main is not public") + if (!isStatic(modifiers)) + throw new NoSuchMethodException(mainClassName + ".main is not static") + DetectedMain(mainClass, method, isStatic = true, method.getParameterCount()) + } } + private case class DetectedMain( + mainClass: Class[?], + method: Method, + isStatic: Boolean, + parameterCount: Int + ) } /** This module is an interface to starting the scala interpreter or runner.*/ @@ -195,4 +223,6 @@ object Run { s"""nonzero exit code returned from $label: $exitCode""".stripMargin ) ) + + private[sbt] lazy val isJava25Plus: Boolean = Properties.isJavaAtLeast("25") } diff --git a/sbt-app/src/sbt-test/dependency-management/cached-resolution-classifier/test b/sbt-app/src/sbt-test/dependency-management/cached-resolution-classifier/disabled similarity index 100% rename from sbt-app/src/sbt-test/dependency-management/cached-resolution-classifier/test rename to sbt-app/src/sbt-test/dependency-management/cached-resolution-classifier/disabled diff --git a/sbt-app/src/sbt-test/run/jep-512/A.scala b/sbt-app/src/sbt-test/run/jep-512/A.scala new file mode 100644 index 000000000..daceacfa1 --- /dev/null +++ b/sbt-app/src/sbt-test/run/jep-512/A.scala @@ -0,0 +1,7 @@ +package example + +class A { + def main(): Unit = { + println("hi") + } +} diff --git a/sbt-app/src/sbt-test/run/jep-512/build.sbt b/sbt-app/src/sbt-test/run/jep-512/build.sbt new file mode 100644 index 000000000..e46b37039 --- /dev/null +++ b/sbt-app/src/sbt-test/run/jep-512/build.sbt @@ -0,0 +1,11 @@ +// 2.12.x uses Zinc's compiler bridge +ThisBuild / scalaVersion := "2.12.20" + +@transient +lazy val check = taskKey[Unit]("") + +check := { + if (scala.util.Properties.isJavaAtLeast("25")) + (Compile / run).toTask(" ").value + else () +} diff --git a/sbt-app/src/sbt-test/run/jep-512/test b/sbt-app/src/sbt-test/run/jep-512/test new file mode 100644 index 000000000..c28fe4985 --- /dev/null +++ b/sbt-app/src/sbt-test/run/jep-512/test @@ -0,0 +1,2 @@ +# > run +> check diff --git a/sbt-app/src/sbt-test/run/spawn-exit/test b/sbt-app/src/sbt-test/run/spawn-exit/disabled similarity index 100% rename from sbt-app/src/sbt-test/run/spawn-exit/test rename to sbt-app/src/sbt-test/run/spawn-exit/disabled