diff --git a/launch/interface/src/main/java/xsbti/AppProvider.java b/launch/interface/src/main/java/xsbti/AppProvider.java index c6eb3d33f..36b692b83 100644 --- a/launch/interface/src/main/java/xsbti/AppProvider.java +++ b/launch/interface/src/main/java/xsbti/AppProvider.java @@ -8,13 +8,27 @@ public interface AppProvider public ScalaProvider scalaProvider(); /** The ID of the application that will be created by 'newMain' or 'mainClass'.*/ public ApplicationID id(); - - public ClassLoader loader(); - /** Loads the class for the entry point for the application given by 'id'. This method will return the same class - * every invocation. That is, the ClassLoader is not recreated each call.*/ + /** The classloader used to load this application. */ + public ClassLoader loader(); + /** Loads the class for the entry point for the application given by 'id'. + * This method will return the same class every invocation. + * That is, the ClassLoader is not recreated each call. + * @deprecated("use entryPoint instead") + * + * Note: This will throw an exception if the launched application does not extend AppMain. + */ + @Deprecated public Class mainClass(); + /** Loads the class for the entry point for the application given by 'id'. + * This method will return the same class every invocation. + * That is, the ClassLoader is not recreated each call. + */ + public Class entryPoint(); /** Creates a new instance of the entry point of the application given by 'id'. - * It is guaranteed that newMain().getClass() == mainClass()*/ + * It is NOT guaranteed that newMain().getClass() == mainClass(). + * The sbt launcher can wrap generic static main methods. In this case, there will be a wrapper class, + * and you must use the `entryPoint` method. + */ public AppMain newMain(); /** The classpath from which the main class is loaded, excluding Scala jars.*/ diff --git a/launch/src/main/scala/xsbt/boot/Launch.scala b/launch/src/main/scala/xsbt/boot/Launch.scala index 0359960e0..169523efe 100644 --- a/launch/src/main/scala/xsbt/boot/Launch.scala +++ b/launch/src/main/scala/xsbt/boot/Launch.scala @@ -238,12 +238,20 @@ class Launch private[xsbt](val bootDirectory: File, val lockBoot: Boolean, val i val id = appID def mainClasspath = app.fullClasspath lazy val loader = app.createLoader(scalaProvider.loader) - lazy val mainClass: Class[T] forSome { type T <: xsbti.AppMain } = + lazy val entryPoint: Class[T] forSome { type T } = { val c = Class.forName(id.mainClass, true, loader) - c.asSubclass(classOf[xsbti.AppMain]) + if(classOf[xsbti.AppMain].isAssignableFrom(c)) c + else if(PlainApplication.isPlainApplication(c)) c + else sys.error(s"Class: ${c} is not an instance of xsbti.AppMain nor does it have one of these static methods:\n"+ + " * void main(String[] args)\n * int main(String[] args)\n * xsbti.Exit main(String[] args)") + } + // Deprecated API. Remove when we can. + def mainClass: Class[T] forSome { type T <: xsbti.AppMain } = entryPoint.asSubclass(classOf[xsbti.AppMain]) + def newMain(): xsbti.AppMain = { + if(PlainApplication.isPlainApplication(entryPoint)) PlainApplication(entryPoint) + else mainClass.newInstance } - def newMain(): xsbti.AppMain = mainClass.newInstance lazy val components = componentProvider(appHome) } diff --git a/launch/src/main/scala/xsbt/boot/PlainApplication.scala b/launch/src/main/scala/xsbt/boot/PlainApplication.scala new file mode 100644 index 000000000..65e2bd56b --- /dev/null +++ b/launch/src/main/scala/xsbt/boot/PlainApplication.scala @@ -0,0 +1,48 @@ +package xsbt +package boot + +/** A wrapper around 'raw' static methods to meet the sbt application interface. */ +class PlainApplication private (mainMethod: java.lang.reflect.Method) extends xsbti.AppMain { + override def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = { + // TODO - Figure out if the main method returns an Int... + val IntClass = classOf[Int] + val ExitClass = classOf[xsbti.Exit] + // It seems we may need to wrap exceptions here... + try mainMethod.getReturnType match { + case ExitClass => + mainMethod.invoke(null, configuration.arguments).asInstanceOf[xsbti.Exit] + case IntClass => + PlainApplication.Exit(mainMethod.invoke(null, configuration.arguments).asInstanceOf[Int]) + case _ => + // Here we still invoke, but return 0 if sucessful (no exceptions). + mainMethod.invoke(null, configuration.arguments) + PlainApplication.Exit(0) + } catch { + // This is only thrown if the underlying reflective call throws. + // Let's expose the underlying error. + case e: java.lang.reflect.InvocationTargetException if e.getCause != null => + throw e.getCause + } + + } +} +/** An object that lets us detect compatible "plain" applications and launch them reflectively. */ +object PlainApplication { + def isPlainApplication(clazz: Class[_]): Boolean = findMainMethod(clazz).isDefined + def apply(clazz: Class[_]): xsbti.AppMain = + findMainMethod(clazz) match { + case Some(method) => new PlainApplication(method) + case None => sys.error("Class: " + clazz + " does not have a main method!") + } + private def findMainMethod(clazz: Class[_]): Option[java.lang.reflect.Method] = + try { + val method = + clazz.getMethod("main", classOf[Array[String]]) + if(java.lang.reflect.Modifier.isStatic(method.getModifiers)) Some(method) + else None + } catch { + case n: NoSuchMethodException => None + } + + case class Exit(code: Int) extends xsbti.Exit +} diff --git a/launch/src/test/scala/ScalaProviderTest.scala b/launch/src/test/scala/ScalaProviderTest.scala index a53d6c32a..e9af552db 100644 --- a/launch/src/test/scala/ScalaProviderTest.scala +++ b/launch/src/test/scala/ScalaProviderTest.scala @@ -23,12 +23,21 @@ object ScalaProviderTest extends Specification checkLoad(List("test"), "xsbt.boot.test.ArgumentTest").asInstanceOf[Exit].code must equalTo(0) checkLoad(List(), "xsbt.boot.test.ArgumentTest") must throwA[RuntimeException] } + "Successfully load an plain application from local repository and run it with correct arguments" in { + checkLoad(List("test"), "xsbt.boot.test.PlainArgumentTest").asInstanceOf[Exit].code must equalTo(0) + checkLoad(List(), "xsbt.boot.test.PlainArgumentTest") must throwA[RuntimeException] + } + "Successfully load an plain application with int return from local repository and run it with correct arguments" in { + checkLoad(List("test"), "xsbt.boot.test.PlainArgumentTestWithReturn").asInstanceOf[Exit].code must equalTo(0) + checkLoad(List(), "xsbt.boot.test.PlainArgumentTestWithReturn").asInstanceOf[Exit].code must equalTo(1) + } "Successfully load an application from local repository and run it with correct sbt version" in { checkLoad(List(AppVersion), "xsbt.boot.test.AppVersionTest").asInstanceOf[Exit].code must equalTo(0) } "Add extra resources to the classpath" in { checkLoad(testResources, "xsbt.boot.test.ExtraTest", createExtra).asInstanceOf[Exit].code must equalTo(0) } + } def checkLoad(arguments: List[String], mainClassName: String): MainResult = diff --git a/launch/test-sample/src/main/scala/xsbt/boot/test/Apps.scala b/launch/test-sample/src/main/scala/xsbt/boot/test/Apps.scala index 7fb99891c..217e7b1ef 100644 --- a/launch/test-sample/src/main/scala/xsbt/boot/test/Apps.scala +++ b/launch/test-sample/src/main/scala/xsbt/boot/test/Apps.scala @@ -32,4 +32,14 @@ class ExtraTest extends xsbti.AppMain } new Exit(0) } +} +object PlainArgumentTestWithReturn { + def main(args: Array[String]): Int = + if(args.length == 0) 1 + else 0 +} +object PlainArgumentTest { + def main(args: Array[String]): Unit = + if(args.length == 0) throw new MainException("Arguments were empty") + else () } \ No newline at end of file