0

After creating a useful application in Scala with dependencies, how do I deploy (create a binary) for it?

I would like to know the most idiomatic way which hopefully is the simplest way.

For me that would be the usual sbt compile, then look for the main class:

./target/scala-2.12/classes/scala_pandoc/Main.class

Then execute it:

$ CLASSPATH="$CLASSPATH:./target/scala-2.12/classes/" scala scala_pandoc.Main --unwrap-explain
Picked up _JAVA_OPTIONS: -Xms256m -Xmx300m
java.lang.ClassNotFoundException: ujson.Value
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at scala_pandoc.Main$.main(Main.scala:51)
    at scala_pandoc.Main.main(Main.scala)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at scala.reflect.internal.util.ScalaClassLoader.$anonfun$run$2(ScalaClassLoader.scala:106)
    at scala.reflect.internal.util.ScalaClassLoader.asContext(ScalaClassLoader.scala:41)
    at scala.reflect.internal.util.ScalaClassLoader.asContext$(ScalaClassLoader.scala:37)
    at scala.reflect.internal.util.ScalaClassLoader$URLClassLoader.asContext(ScalaClassLoader.scala:132)
    at scala.reflect.internal.util.ScalaClassLoader.run(ScalaClassLoader.scala:106)
    at scala.reflect.internal.util.ScalaClassLoader.run$(ScalaClassLoader.scala:98)
    at scala.reflect.internal.util.ScalaClassLoader$URLClassLoader.run(ScalaClassLoader.scala:132)
    at scala.tools.nsc.CommonRunner.run(ObjectRunner.scala:28)
    at scala.tools.nsc.CommonRunner.run$(ObjectRunner.scala:27)
    at scala.tools.nsc.ObjectRunner$.run(ObjectRunner.scala:45)
    at scala.tools.nsc.CommonRunner.runAndCatch(ObjectRunner.scala:35)
    at scala.tools.nsc.CommonRunner.runAndCatch$(ObjectRunner.scala:34)
    at scala.tools.nsc.ObjectRunner$.runAndCatch(ObjectRunner.scala:45)
    at scala.tools.nsc.MainGenericRunner.runTarget$1(MainGenericRunner.scala:73)
    at scala.tools.nsc.MainGenericRunner.run$1(MainGenericRunner.scala:92)
    at scala.tools.nsc.MainGenericRunner.process(MainGenericRunner.scala:103)
    at scala.tools.nsc.MainGenericRunner$.main(MainGenericRunner.scala:108)
    at scala.tools.nsc.MainGenericRunner.main(MainGenericRunner.scala)

But as we can see it somehow does not find the dependencies. When I compile the project a bunch of files are downloaded/created at ~/.sbt/ and ~/.ivy2 but neither adding those (or all sub folders to CLASSPATH) solves the issue.

The forementioned procedure works for projects without external dependencies.

Workaround:

Use https://github.com/sbt/sbt-assembly which is great (creates an executable .jar) which I can run with java -jar myjar.jar but feels hackish/non official/fragile and besides, it also puts more dependencies on my project.


  • build.sbt:

    lazy val scalatest = "org.scalatest" %% "scalatest" % "3.0.5"
    lazy val ujson = "com.lihaoyi" %% "ujson" % "0.7.1"
    
    name := "scala_pandoc"
    
    organization := "org.fmv1992"
    
    licenses += "GPLv2" -> url("https://www.gnu.org/licenses/gpl-2.0.html")
    
    lazy val commonSettings = Seq(
        version := "0.0.1-SNAPSHOT",
        scalaVersion := "2.12.8",
        pollInterval := scala.concurrent.duration.FiniteDuration(50L, "ms"),
        maxErrors := 10,
    
        // This final part makes test artifacts being only importable by the test files
        // libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5" % Test,
        //                                                                   ↑↑↑↑↑
        // Removed on commit 'cd9d482' to enable 'trait ScalaInitiativesTest' define
        // 'namedTest'.
        libraryDependencies ++= Seq(scalatest, ujson),
    
        scalacOptions ++= Seq("-feature", "-deprecation", "-Xfatal-warnings")
        )
    
    lazy val root = (project in file(".")).settings(commonSettings).settings(assemblyJarName in assembly := "scala_pandoc.jar")
    
  • project/build.properties:

    sbt.version=1.2.8
    
  • project/plugins.sbt:

    addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")
    

Related question: Deploy Scala binaries without dependencies

fmv1992
  • 322
  • 1
  • 4
  • 14

1 Answers1

2

sbt-assembly is not hacky - it is maintained by one of sbt creators (Eugene Yokota) and lives in official sbt organization, so it is the official way of deploying Scala JARs in sbt.

Well, one of several official ways. You can take a look at sbt-native-packager. The thing is: there are so many possible targets of sbt build that authors decided that even building an uberjar should not be a special snowflake and it should be done via a plugin.

So just use sbt-assembly and don't feel guilty about it. That is the idiomatic way .

Mateusz Kubuszok
  • 24,995
  • 4
  • 42
  • 64