So I’m building a desktop app in Scala with JavaFX. I need a few things from my build process. At first, I thought the build process would be the easiest part of the application. Boy, was I wrong. I’ll save my griping about sbt for another post. Here I’ll talk about how I actually got it to work.
One of the keys (cough) to using sbt effectively is knowing which “settings” or “tasks” to override. You look in Keys.scala or the relevant source for any plugin you’re using to find the key, then figure out how to change it to what you need. Unfortunately, you really have to understand a bit about how sbt works (i.e. read the docs) to get anything done (on the plus side, there are docs). In my case, I needed:
- To build the application with all the dependencies
- Obfuscate it to discourage piracy
- Stuff it into a single jar with all the dependencies to make deployment less of a pain
- Sign the jar to reduce the scariness of the warnings
ProGuard is the de facto standard for obfuscation in Java-land, and Assembly is a commonly used tool for creating the single jar. We use jarsigner from the JDK to sign the jar.
There are at least two major ProGuard plugins for sbt: sbt/sbt-proguard by Typesafe and xsbt-proguard-plugin. I actually went back and forth between them several times, just trying to get either one to work with the flow described above. Finally I ended up with Typesafe’s plugin, because the other one doesn’t expose the output of ProGuard in a way that is easy to manipulate from sbt. Typesafe’s ProGuard plugin does have a “merge” function that appears to duplicate Assembly, but I couldn’t get it to work (ran into what appeared to be a temp directory naming problem), and I did get Assembly to work.
My final build flow in build.sbt looks like this:
- Normal compilation procedure
- Call Proguard, but using only the compilation output as the “input jar” and everything else (including the Scala runtime) is a “library jar”. I had to jump through some hoops here, because the sbt plugin authors seem to think the normal usecase is to shrink/obfuscate the Scala runtime, perhaps for an Android app. But any code you run through ProGuard as input means more nasty ProGuard configuration/warnings/etc., and I couldn’t find a good standard ProGuard config for the latest Scala (2.10.2).
- Run the output of Proguard plus all of the library dependencies into Assembly
- Run the output of Assembly through jarsigner
- Fist pump
In case anyone might find it useful, I’m including some of my build.sbt file. As a bonus, it also includes the trick to connect from your Ecliplse or other IDE debugger.
name := "myApp"
version := "1.0"
scalaVersion := "2.10.2"
libraryDependencies ++= Seq(
"commons-io" % "commons-io" % "2.4" // ...
)
// Uncomment to see warnings
//scalacOptions ++= Seq("-unchecked", "-deprecation")
// Force sbt to run the application in a separate JVM (needed for JavaFX)
fork := true
// Uncomment to run with debugger: connect to port 5005 from your IDE
//javaOptions in run += "-Xdebug"
//javaOptions in run += "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"
//
// Proguard
//
proguardSettings
ProguardKeys.inputs in Proguard <<= exportedProducts in Compile map { _.files }
// Application-specific Proguard config
// dontusemixedcaseclassnames: workaround because Windows files are case-insensitive
ProguardKeys.options in Proguard += """
-dontusemixedcaseclassnames
-keepclassmembers class * { ** MODULE$; }
""" // Add ProGuard config for your application
//
// Assembly
//
assemblySettings
AssemblyKeys.jarName := "myApp.jar" // final output jar name
// Include the obfuscated jar from proguard, but exclude the original unobfuscated files
// Notice the dependency on ProguardKeys.proguard. This is to make sure it actually runs Proguard first;
// otherwise you can get an IOException. You would think ProguardKeys.outputs would be sufficient, but no.
fullClasspath in AssemblyKeys.assembly <<= (fullClasspath in AssemblyKeys.assembly, exportedProducts in Compile,
ProguardKeys.outputs in Proguard, ProguardKeys.proguard in Proguard) map {
(cp, unobfuscated, obfuscated, p) =>
((cp filter { !unobfuscated.contains(_) }).files ++ obfuscated).classpath
}
// If you have duplicate errors when Assembly does the merge, you need to tell it how to resolve them, for example:
//AssemblyKeys.mergeStrategy in AssemblyKeys.assembly <<= (AssemblyKeys.mergeStrategy in AssemblyKeys.assembly) { (old) =>
// {
// case PathList("org", "xmlpull", xs @ _*) => MergeStrategy.first
// case x => old(x)
// }
//}
//
// Jarsigner
//
// Here we redefine the "package" task to suck in the Assembly jar and sign it.
Keys.`package` in Compile <<= (Keys.`package` in Compile, AssemblyKeys.assembly, sourceDirectory, crossTarget) map {
(p, a, s, c) =>
val command = "jarsigner -storepass myKeystorePassword -keystore "" + (s / "main/deploy/keystore.jks") + "" " +
""" + (c / a.getName) + "" myKeystoreAlias"
println(command) // just for fun
command !; // I love that ! executes the command in the shell
a // return the Assembly jar, which is now signed
}
/////////////////////
// Result: run "package" command to generate and sign the "single jar" file called jarName
/////////////////////
And as it says, running “package” from the sbt shell now does all the ProGuard-Assembly-Jarsigner magic. Whew!