diff --git a/modules/build/src/main/scala/scala/build/internal/MainClass.scala b/modules/build/src/main/scala/scala/build/internal/MainClass.scala index 7ea5651ecd..d89f3ccd2a 100644 --- a/modules/build/src/main/scala/scala/build/internal/MainClass.scala +++ b/modules/build/src/main/scala/scala/build/internal/MainClass.scala @@ -24,10 +24,11 @@ object MainClass { * into the implementing class, so that class declares `main` itself and is detected. */ enum MainMethodKind(val requiresJep512: Boolean): - case StaticWithArgs extends MainMethodKind(false) - case InstanceWithArgs extends MainMethodKind(true) - case StaticNoArgs extends MainMethodKind(true) - case InstanceNoArgs extends MainMethodKind(true) + case StaticWithArgs extends MainMethodKind(false) + case NonPublicStaticWithArgs extends MainMethodKind(true) + case InstanceWithArgs extends MainMethodKind(true) + case StaticNoArgs extends MainMethodKind(true) + case InstanceNoArgs extends MainMethodKind(true) /** Whether a JVM of version `jvmVersion` can launch this main method shape. The JEP 512 shapes * need JDK 25 or newer, or JDK 21 or newer with `--enable-preview`. @@ -67,31 +68,34 @@ object MainClass { signature: String, exceptions: Array[String] ): asm.MethodVisitor = { + import MainMethodKind.* val isStatic = (access & asm.Opcodes.ACC_STATIC) != 0 val isPrivate = (access & asm.Opcodes.ACC_PRIVATE) != 0 + val isPublic = (access & asm.Opcodes.ACC_PUBLIC) != 0 if name == "" && descriptor == noArgDescriptor && !isPrivate then hasNonPrivateNoArgCtor = true else if name == "main" && !isPrivate then - (isStatic, descriptor) match { - case (true, `stringArrayDescriptor`) => mainKinds += MainMethodKind.StaticWithArgs - case (false, `stringArrayDescriptor`) => mainKinds += MainMethodKind.InstanceWithArgs - case (true, `noArgDescriptor`) => mainKinds += MainMethodKind.StaticNoArgs - case (false, `noArgDescriptor`) => mainKinds += MainMethodKind.InstanceNoArgs - case _ => () + (isStatic, descriptor, isPublic) match { + case (true, `stringArrayDescriptor`, true) => mainKinds += StaticWithArgs + case (true, `stringArrayDescriptor`, false) => mainKinds += NonPublicStaticWithArgs + case (false, `stringArrayDescriptor`, _) => mainKinds += InstanceWithArgs + case (true, `noArgDescriptor`, _) => mainKinds += StaticNoArgs + case (false, `noArgDescriptor`, _) => mainKinds += InstanceNoArgs + case _ => () } null } def candidateOpt: Option[MainClassCandidate] = { + import MainMethodKind.* val isAbstractOrInterface = (classAccess & asm.Opcodes.ACC_ABSTRACT) != 0 || (classAccess & asm.Opcodes.ACC_INTERFACE) != 0 if isAbstractOrInterface then None else // Instance shapes are only invocable when a non-private zero-arg constructor exists. val invocableKinds = mainKinds.filter { - case MainMethodKind.StaticWithArgs | MainMethodKind.StaticNoArgs => true - case MainMethodKind.InstanceWithArgs | MainMethodKind.InstanceNoArgs => - hasNonPrivateNoArgCtor + case StaticWithArgs | NonPublicStaticWithArgs | StaticNoArgs => true + case InstanceWithArgs | InstanceNoArgs => hasNonPrivateNoArgCtor } MainMethodKind.values.find(invocableKinds.contains) .flatMap(kind => nameOpt.map(MainClassCandidate(_, kind))) diff --git a/modules/build/src/test/scala/scala/build/tests/MainClassTests.scala b/modules/build/src/test/scala/scala/build/tests/MainClassTests.scala index 7e3ca85568..514a92ad39 100644 --- a/modules/build/src/test/scala/scala/build/tests/MainClassTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/MainClassTests.scala @@ -17,6 +17,7 @@ class MainClassTests extends TestUtil.ScalaCliBuildSuite { private val jep512MinJava = scala.build.internal.Constants.jep512MinJavaVersion private val jep512PreviewMinJava = scala.build.internal.Constants.jep512PreviewMinJavaVersion + private val preJep512Java = jep512MinJava - 1 private val defaultJvm = scala.build.internal.Constants.defaultJavaVersion val buildThreads: BuildThreads = BuildThreads.create() @@ -47,6 +48,7 @@ class MainClassTests extends TestUtil.ScalaCliBuildSuite { test("MainMethodKind.isSupportedByJvm covers the JEP 512 version matrix") { val jep512Kinds = Seq( + MainMethodKind.NonPublicStaticWithArgs, MainMethodKind.InstanceWithArgs, MainMethodKind.StaticNoArgs, MainMethodKind.InstanceNoArgs @@ -105,7 +107,7 @@ class MainClassTests extends TestUtil.ScalaCliBuildSuite { expect( findKinds(build.output).toMap == Map( "Classic" -> MainMethodKind.StaticWithArgs, - "ProtectedStatic" -> MainMethodKind.StaticWithArgs, + "ProtectedStatic" -> MainMethodKind.NonPublicStaticWithArgs, "StaticNoArgs" -> MainMethodKind.StaticNoArgs, "InstanceWithArgs" -> MainMethodKind.InstanceWithArgs, "InstanceNoArgs" -> MainMethodKind.InstanceNoArgs, @@ -116,6 +118,31 @@ class MainClassTests extends TestUtil.ScalaCliBuildSuite { } } + test("non-public static main methods with arguments require JEP 512") { + TestInputs( + os.rel / "ProtectedStatic.java" -> + s"""//> using jvm $preJep512Java + |public class ProtectedStatic { + | protected static void main(String[] args) {} + |} + |""".stripMargin, + os.rel / "PackagePrivateStatic.java" -> + """public class PackagePrivateStatic { + | static void main(String[] args) {} + |} + |""".stripMargin + ).withBuild(baseOptions, buildThreads, None, buildTests = false) { (_, _, maybeBuild) => + val build = maybeBuild.orThrow.successfulOpt.get + expect( + findKinds(build.output).toMap == Map( + "ProtectedStatic" -> MainMethodKind.NonPublicStaticWithArgs, + "PackagePrivateStatic" -> MainMethodKind.NonPublicStaticWithArgs + ) + ) + expect(build.foundMainClasses().isEmpty) + } + } + test("ignore main methods that cannot be launched, in Java sources") { TestInputs( os.rel / "PrivateStatic.java" -> diff --git a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala index 957dcd3428..c29df25aaf 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala @@ -1132,7 +1132,7 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers { ) final class Jep512MainUnsupportedForBootstrapError(mainClass: String) extends BuildException( - s"""Main class $mainClass uses a JDK ${scala.build.internal.Constants.jep512MinJavaVersion} instance/no-arg main method (JEP 512), which the bootstrap launcher cannot invoke. + s"""Main class $mainClass uses a main method that requires the JDK ${scala.build.internal.Constants.jep512MinJavaVersion} or newer (JEP 512), which the bootstrap launcher cannot invoke. |Use --assembly or --library instead.""".stripMargin )