sbt-assembly で依存ライブラリを含んだ über-jar を生成する

scala

sbt-assembly は依存ライブラリを含んだ über-jar (fat-jar) を生成するためのプラグイン。

$ cat peojcts/plugins.sbt 
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")

$ sbt assembly
$ java -jar ./target/scala-2.13/sbt-assembly-test-0.1.0-SNAPSHOT.jar
Hello world!

依存ライブラリの依存先も含めて複数の JAR が同じパスのファイルを含み、その中身が異なっている場合、Deduplicate found different file contents エラーになってしまうので、 その際は assemblyMergeStrategy でそれをどのように解決するかを設定する。

$ cat build.sbt
...
libraryDependencies += "software.amazon.awssdk" % "s3" % "2.20.14"

assembly / assemblyMergeStrategy := {
  case PathList("META-INF", xs @ _*) => MergeStrategy.discard
  case x =>
    val oldStrategy = (ThisBuild / assemblyMergeStrategy).value
    oldStrategy(x)
}

数が多く解決が面倒な場合は assemblyExcludedJars で über-jar からは除きクラスパスに配置して実行時にロードさせることもできるが、結局互換性のためにエラーになったりする可能性がある。

assembly / assemblyExcludedJars := {
  (assembly / fullClasspath).value.filter { _.data.getName.contains("testlib") }
}

val outputExcludedJar = taskKey[Seq[File]]("Output jars that is excluded from the uber jar")
outputExcludedJar := {
  val targetDir = (assembly / Keys.target).value
  val assemblyJAR = (assembly / assemblyOutputPath).value
  val libraryJARs = (assembly / update).value matching configurationFilter(Runtime.name)
  val exludedJARs = libraryJARs.filter { jar => (assembly / assemblyExcludedJars).value.map(_.data).contains(jar) }

  IO.createDirectory(targetDir)
  exludedJARs.foreach { jar =>
    IO.copyFile(jar, targetDir / jar.getName)
  }

  Seq(assemblyJAR) ++ (targetDir ** "*.jar").get
}

assembly := assembly.dependsOn(outputExcludedJar).value

また、実行環境に一部のライブラリが存在している場合は、provided を付けることで JAR から除き、 さらに次のように書くことでローカルでの sbt run 時には含めることができる。

$ cat build.sbt
...
libraryDependencies += "software.amazon.awssdk" % "s3" % "2.20.14" % "provided"

Compile / run := Defaults.runTask(Compile / fullClasspath, Compile / run / mainClass, Compile / run / runner).evaluated 

$ sbt run