Mateusz Kubuszok
Mateusz Kubuszok

Reputation: 27595

Finding Scala libraries location from within Scala program

I'm trying to make one Scala program spawn another Scala program. I managed to obtain java executable from System.getProperty("java.home"), I've obtained some path from System.getProperty("java.class.path") (sbt-launcher.jar location), and with ClassLoader I've got project/target/scala-2.11/classes directory.

However, I am still unable to run it. JVM complain that it is unable to find Scala library's classes:

Exception in thread "main" java.lang.NoClassDefFoundError: scala/concurrent/ExecutionContext
  at java.lang.Class.getDeclaredMethods0(Native Method)
  at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
  at java.lang.Class.privateGetMethodRecursive(Class.java:3048)
  at java.lang.Class.getMethod0(Class.java:3018)
  at java.lang.Class.getMethod(Class.java:1784)
  at sun.launcher.LauncherHelper.validateMainClass(LauncherHelper.java:544)
    at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:526)
Caused by: java.lang.ClassNotFoundException: scala.concurrent.ExecutionContext
  at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
  at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
  at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
  at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 7 more

I am looking for a way to add those files to classpath, but I want it to be portable. I do not look for solutions like hardcoding scala location on local computer nor do I want to use other environment variables and parameters than the one already existing. I also don't want to rely on SBT or Activators presence in user's environment.

Since the parent JVM process can use them their location has to be stored somewhere and I'll be thankful for help with finding out that location.

Upvotes: 0

Views: 1005

Answers (1)

Mateusz Kubuszok
Mateusz Kubuszok

Reputation: 27595

To successfully spawn one Scala App from another I had to fix several issues with my code:

1. correct main class:

object ChildApp extends App {
  println("success")
}

to make sure that ChildApp is runnable by Java it has to be an object. Scala has no concept of static but object methods would (and main will) be compiled into static method.

2. correct class name:

While ChildApp.getClass.getName returns ChildApp$, it refers to an object (so that we could pass otherwise static-method-only class around). Java expects $ in command line - in other works I had to remove tailing $ before passing it into the process builder.

3. complete class path

I haven't found all used JARs within System.getPropertiy("java.class.path"):

val pcp = System getPropertiy "java.class.path" split File.pathSeparator // sbt-launcher.jar only

I haven't found them in SystemClassLoader either:

val scp = ClassLoader.getSystemClassLoader.asInstanceOf[URLClassLoader].getURLs.map(_.toString) // same as above

I did found compiled files from my project using Class' resources:

// format like jar:file:/(your-project/compiled.jar)!some/package/ChildApp.class
lazy val jarClassPathPattern  = "jar:(file:)?([^!]+)!.+".r
// format like file:/(your-project/compiled/some/package/ChildApp).class
lazy val fileClassPathPattern = "file:(.+).class".r

val jcp = jarClassPathPattern.findFirstMatchIn(pathToClass) map { matcher =>
  val jarDir = Paths get (matcher group 2) getParent()
  s"${jarDir}/*"
} toSet

val fcp = fileClassPathPattern.findFirstMatchIn(pathToClass) map { matcher =>
  val suffix   = "/" + clazz.getName
  val fullPath = matcher group 1
  fullPath substring (0, fullPath.length - suffix.length)
} toList

Finally I found where all those dependencies where stored:

// use App class' ClassLoader instead of system one
val lcp = ChildApp.getClass.getClassLoader.asInstanceOf[URLClassLoader].getURLs.map(_.toString)

4. bonus - JVM params and java location

val jvmArgs   = ManagementFactory.getRuntimeMXBean.getInputArguments.toList

lazy val javaHome = System getProperty "java.home"

lazy val java = Seq(
  Paths.get(javaHome, "bin", "java"),
  Paths.get(javaHome, "bin", "java.exe")
) filter (Files exists _) head

Then you have everything you need for ProcessBuilder / Process:

val executable = java.toString
val arguments  = jvmArgs ++ List("-cp", classPath, mainName) ++ mainClassArguments

PS. I checked several times - those additional JARs aren't passed using neither CLASSPATH environment variable nor with -cp parameter (sbt-launcher.jar's MANIFEST file did't had anything as well). So anyone knowing how they are passed and why my solution actually works, please explain.

Upvotes: 2

Related Questions