sbt runs IMain and play makes errors

2019-08-28 05:47发布

问题:

I've build a litte object, which can interpret scala code on the fly and catches a value out of it.

object Interpreter {
  import scala.tools.nsc._
  import scala.tools.nsc.interpreter._

  class Dummy

  val settings = new Settings
  settings.usejavacp.value = false
  settings.embeddedDefaults[Dummy]  // to make imain useable with sbt.

  val imain = new IMain(settings)

  def run(code: String, returnId: String) = {
    this.imain.beQuietDuring{
      this.imain.interpret(code)
    }
    val ret = this.imain.valueOfTerm(returnId)
    this.imain.reset()
    ret
  }
}

object Main {
  def main(args: Array[String]) {
    println(Interpreter.run("val x = 1", "x"))
  }
}

In a pure sbt environment or called by the scala interpreter this code works fine! But if I run this in a simple play (version 2.2.2) application, it gets a null pointer at val ret = this.imain.valueOfTerm(returnId).

play uses also a modified sbt, therefor it should probably work. What does play do that this code doesn't work anymore? Any ideas how to get this code to work in play?

Note

That's the used build.sbt:

name := "Test"

version := "1.0"

scalaVersion := "2.10.3"

libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value

Alternatively I tried this implementation, but it doesen't solve the problem either:

object Interpreter2 {
  import scala.tools.nsc._
  import scala.tools.nsc.interpreter._
  import play.api._
  import play.api.Play.current 

  val settings: Settings = {
    lazy val urls = java.lang.Thread.currentThread.getContextClassLoader match {
      case cl: java.net.URLClassLoader => cl.getURLs.toList
      case _ => sys.error("classloader is not a URLClassLoader")
    }

    lazy val classpath = urls map {_.toString}

    val tmp = new Settings()
    tmp.bootclasspath.value = classpath.distinct mkString java.io.File.pathSeparator
    tmp
  }

  val imain = new IMain(settings)

  def run(code: String, returnId: String) = {
    this.imain.beQuietDuring {
      this.imain.interpret(code)
    }
    val ret = this.imain.valueOfTerm(returnId)
    this.imain.reset()
    ret
  }
}

Useful links I found to make this second implementation:

  • scala.tools.nsc.IMain within Play 2.1

  • How to set up classpath for the Scala interpreter in a managed environment?

  • https://groups.google.com/forum/#!topic/scala-user/wV86VwnKaVk

  • https://github.com/gourlaysama/play-repl-example/blob/master/app/REPL.scala#L18

  • https://gist.github.com/mslinn/7205854

回答1:

After spending a few hours on this issue myself, here is a solution that I came up with. It works both inside SBT and outside. It is also expected to work in a variety of managed environments (like OSGi):

private def getClasspathUrls(classLoader: ClassLoader, acc: List[URL]): List[URL] = {
    classLoader match {
      case null                        => acc
      case cl: java.net.URLClassLoader => getClasspathUrls(classLoader.getParent, acc ++ cl.getURLs.toList)
      case c                           => LOGGER.error("classloader is not a URLClassLoader and will be skipped. ClassLoader type that was skipped is " + c.getClass)
                                          getClasspathUrls(classLoader.getParent, acc)
    }
}
val classpathUrls = getClasspathUrls(this.getClass.getClassLoader, List())
val classpathElements = classpathUrls map {url => url.toURI.getPath}
val classpath = classpathElements mkString java.io.File.pathSeparator
val settings = new Settings
settings.bootclasspath.value = classpath
val imain = new IMain(settings)
// use imain to interpret code. It should be able to access all your application classes as well as dependent libraries. 


回答2:

It's because play uses the "fork in run" feature from sbt. This feature starts a new JVM and this causes that this failure appears:

[info] Failed to initialize compiler: object scala.runtime in compiler mirror not found.
[info] ** Note that as of 2.8 scala does not assume use of the java classpath.
[info] ** For the old behavior pass -usejavacp to scala, or if using a Settings
[info] ** object programatically, settings.usejavacp.value = true.

See: http://www.scala-sbt.org/release/docs/Detailed-Topics/Forking