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.
This commit is contained in:
Eugene Yokota 2025-09-28 01:04:07 -04:00
parent 50d5204359
commit 2e7d3fdf93
9 changed files with 71 additions and 31 deletions

View File

@ -21,8 +21,8 @@ jobs:
distribution: temurin distribution: temurin
jobtype: 2 jobtype: 2
- os: ubuntu-latest - os: ubuntu-latest
java: 21 java: 25
distribution: temurin distribution: zulu
jobtype: 3 jobtype: 3
- os: ubuntu-latest - os: ubuntu-latest
java: 21 java: 21

View File

@ -14,7 +14,7 @@ import java.util.Locale
import scala.reflect.macros.blackbox import scala.reflect.macros.blackbox
import scala.language.experimental.macros import scala.language.experimental.macros
import scala.language.reflectiveCalls import scala.language.reflectiveCalls
import scala.util.control.NonFatal import scala.util.Properties
object Util { object Util {
def makeList[T](size: Int, value: T): List[T] = List.fill(size)(value) 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] = def reduceIntents[A1, A2](intents: PartialFunction[A1, A2]*): PartialFunction[A1, A2] =
intents.toList.reduceLeft(_ orElse _) intents.toList.reduceLeft(_ orElse _)
lazy val majorJavaVersion: Int = lazy val isJava19Plus: Boolean = Properties.isJavaAtLeast("19")
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
}
private type GetId = { private type GetId = {
def getId: Long 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() * https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html#threadId()
*/ */
def threadId: Long = def threadId: Long =
if (majorJavaVersion < 19) { if (!isJava19Plus) {
(Thread.currentThread(): AnyRef) match { (Thread.currentThread(): AnyRef) match {
case g: GetId @unchecked => g.getId case g: GetId @unchecked => g.getId
} }

View File

@ -15,7 +15,7 @@ object Dependencies {
private val ioVersion = nightlyVersion.getOrElse("1.10.5") private val ioVersion = nightlyVersion.getOrElse("1.10.5")
private val lmVersion = private val lmVersion =
sys.props.get("sbt.build.lm.version").orElse(nightlyVersion).getOrElse("1.11.5") 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 private val sbtIO = "org.scala-sbt" %% "io" % ioVersion

View File

@ -19,7 +19,7 @@ import sbt.util.Logger
import scala.sys.process.Process import scala.sys.process.Process
import scala.util.control.NonFatal import scala.util.control.NonFatal
import scala.util.{ Failure, Success, Try } import scala.util.{ Failure, Properties, Success, Try }
sealed trait ScalaRun { sealed trait ScalaRun {
def run(mainClass: String, classpath: Seq[File], options: Seq[String], log: Logger): Try[Unit] 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 = def execute(): Unit =
try { try {
log.debug(" Classpath:\n\t" + classpath.mkString("\n\t")) log.debug(" Classpath:\n\t" + classpath.mkString("\n\t"))
val main = getMainMethod(mainClass, loader) val main = detectMainMethod(mainClass, loader)
invokeMain(loader, main, options) invokeMain(loader, main, options)
} catch { } catch {
case e: java.lang.reflect.InvocationTargetException => case e: java.lang.reflect.InvocationTargetException =>
@ -125,14 +125,22 @@ class Run(private[sbt] val newLoader: Seq[File] => ClassLoader, trapExit: Boolea
} }
private def invokeMain( private def invokeMain(
loader: ClassLoader, loader: ClassLoader,
main: Method, main: DetectedMain,
options: Seq[String] options: Seq[String]
): Unit = { ): Unit = {
val currentThread = Thread.currentThread val currentThread = Thread.currentThread
val oldLoader = Thread.currentThread.getContextClassLoader val oldLoader = Thread.currentThread.getContextClassLoader
currentThread.setContextClassLoader(loader) currentThread.setContextClassLoader(loader)
try { 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 { } catch {
case t: Throwable => case t: Throwable =>
t.getCause match { t.getCause match {
@ -148,19 +156,39 @@ class Run(private[sbt] val newLoader: Seq[File] => ClassLoader, trapExit: Boolea
currentThread.setContextClassLoader(oldLoader) 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 mainClass = Class.forName(mainClassName, true, loader)
val method = mainClass.getMethod("main", classOf[Array[String]]) if (Run.isJava25Plus) {
// jvm allows the actual main class to be non-public and to run a method in the non-public class, val method = try {
// we need to make it accessible mainClass.getMethod("main", classOf[Array[String]])
method.setAccessible(true) } catch {
val modifiers = method.getModifiers case _: NoSuchMethodException => mainClass.getMethod("main")
if (!isPublic(modifiers)) }
throw new NoSuchMethodException(mainClassName + ".main is not public") method.setAccessible(true)
if (!isStatic(modifiers)) val modifiers = method.getModifiers
throw new NoSuchMethodException(mainClassName + ".main is not static") DetectedMain(mainClass, method, isStatic(modifiers), method.getParameterCount())
method } 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.*/ /** 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 s"""nonzero exit code returned from $label: $exitCode""".stripMargin
) )
) )
private[sbt] lazy val isJava25Plus: Boolean = Properties.isJavaAtLeast("25")
} }

View File

@ -0,0 +1,7 @@
package example
class A {
def main(): Unit = {
println("hi")
}
}

View File

@ -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 ()
}

View File

@ -0,0 +1,2 @@
# > run
> check