sbt マルチプロジェクトビルド sbt-assemblyでそれぞれのjar生成


既に存在する多くのロジックをコピペせずに、サブプロジェクトから既存のロジックをimportしてサブプロジェクトもjarとして生成したいと思ったので試した。


## プロジェクト1つの状態でsbt-assemblyを使ってjar生成

プロジェクト1つの時代の元のディレクトリ構成(雰囲気)

```
app/src/
   /project/
   /target/
   /build.sbt
```

元のbuild.sbt

```scala
lazy val root = (project in file(".")).
  settings(
    name := "project1",
    version := "1.0",
    scalaVersion := "2.12.8",
    libraryDependencies ++= dervy,
    testTasks in Test := {},
    test in assembly := {}
  )

lazy val testTasks = taskKey[Unit]("test tasks.")


lazy val dervy = {
  Seq(
    使うライブラリ達
  )
}

parallelExecution in Test := false

assemblyMergeStrategy in assembly := {
  case PathList(ps @ _*) if ps.last endsWith ".html" => MergeStrategy.first
  case PathList("org", "slf4j", xs @ _*)             => MergeStrategy.first
  case PathList(ps @ _*) if ps.last endsWith ".properties" => MergeStrategy.first
  case PathList(ps @ _*) if ps.last endsWith ".xml" => MergeStrategy.first
  case PathList(ps @ _*) if ps.last endsWith ".types" => MergeStrategy.first
  case PathList(ps @ _*) if ps.last endsWith ".class" => MergeStrategy.first
  case PathList(ps @ _*) if ps.last endsWith ".txt" => MergeStrategy.first
  case "application.conf"                            => MergeStrategy.concat
  case "unwanted.txt"                                => MergeStrategy.discard
  case "BUILD"                                       => MergeStrategy.discard
  case PathList("META-INF", xs @ _*)                 => MergeStrategy.discard
  case x =>
    val oldStrategy = (assemblyMergeStrategy in assembly).value
    oldStrategy(x)
}
```

この状態で`sbt assembly`(addSbtPluginは設定済みの状態)を実行すると`app/target/scala-2.12/project1-assembly-1.0.jar`が生成される。

これまでがプロジェクト1つの場合

## マルチプロジェクトでsbt-assemblyを使ってそれぞれのjar生成

マルチプロジェクトのディレクトリ構成(雰囲気)

```
app/src/
   /project/
   /target/
   /sub-project/ <- 後にbuild.sbtにプロジェクト追加すると生成されるディレクトリ
   /build.sbt
```

サブプロジェクトを追加したbuild.sbt
変更点としてはassemblySettingsをまとめてそれぞれのプロジェクトに設定しているのと、subProjectを作成している点。
subProjectにはdependsOnでproject1のクラス等をimport出来るようにしている。
※各プロジェクトの設定はcommonSettingsを設定した方が良いがやってない。
[マルチプロジェクト・ビルド 共通のセッティング](https://www.scala-sbt.org/1.x/docs/ja/Multi-Project.html#%E5%85%B1%E9%80%9A%E3%81%AE%E3%82%BB%E3%83%83%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0)

```scala
lazy val root = (project in file(".")).
  settings(
    name := "project1",
    version := "1.0",
    scalaVersion := "2.12.8",
    libraryDependencies ++= dervy,
    testTasks in Test := {},
    assemblySettings, // 追加
    test in assembly := {}
  )

lazy val subProject = (project in file("sub-project")).
  dependsOn(root).
  settings(
    name := "sub-project",
    version := "1.0",
    scalaVersion := "2.12.8",
    libraryDependencies ++= dervy,
    testTasks in Test := {},
    assemblySettings, // subProjectでも設定
    test in assembly := {}
  )

lazy val testTasks = taskKey[Unit]("test tasks.")


lazy val dervy = {
  Seq(
    使うライブラリ達
  )
}

parallelExecution in Test := false

// 設定をassemblySettingsにまとめる
lazy val assemblySettings = Seq(
  assemblyMergeStrategy in assembly := {
    case PathList(ps @ _*) if ps.last endsWith ".html" => MergeStrategy.first
    case PathList("org", "slf4j", xs @ _*)             => MergeStrategy.first
    case PathList(ps @ _*) if ps.last endsWith ".properties" => MergeStrategy.first
    case PathList(ps @ _*) if ps.last endsWith ".xml" => MergeStrategy.first
    case PathList(ps @ _*) if ps.last endsWith ".types" => MergeStrategy.first
    case PathList(ps @ _*) if ps.last endsWith ".class" => MergeStrategy.first
    case PathList(ps @ _*) if ps.last endsWith ".txt" => MergeStrategy.first
    case "application.conf"                            => MergeStrategy.concat
    case "unwanted.txt"                                => MergeStrategy.discard
    case "BUILD"                                       => MergeStrategy.discard
    case PathList("META-INF", xs @ _*)                 => MergeStrategy.discard
    case x =>
      val oldStrategy = (assemblyMergeStrategy in assembly).value
      oldStrategy(x)
  }
)
```

このようなbuild.sbtになっている状態で`sbt`を起動して`projects`コマンドと入力すると、`root`と`subProject`の2つが表示されるようになる

subProjectに切り替える場合`project subProject`と入力すると切り替わる
この時点で`sub-project`ディレクトリが生成されていると思うので`src/main/scala/com/github/TakashiOshikawa/SubMain.scala`を作成して元々存在するロジックをsubProjectから利用してみる。



```scala
// ※パッケージ名は好きなように
package com.github.TakashiOshikawa

// ここでproject1側のクラスをimport出来ることを確認
import com.github.TakashiOshikawa.services.SomeService

object SubProjectMain {

  def main(args: Array[String]) = {
    println("sub project main !!")
    println(SomeService.run(1))
  }
}
```

これで`sbt subProject/compile`や`sbt subProject/run`で実行できることを確認したらsbt-assemblyでjar生成していく


project1のコマンド `sbt assembly`
sub projectのコマンド `sbt subProject/assembly`

それぞれのjarは以下のように生成された
`app/target/scala-2.12/project1-assembly-1.0.jar`
`app/sub-project/target/scala-2.12/sub-project-assembly-1.0.jar`


### まとめ
マルチプロジェクトの記事はそこそこあるが、サブプロジェクトのjarをsbt-assemblyで生成するのはあまり見かけなかったので少し手こずった。
そもそもプロジェクトごとにjar生成なんて殆どやることはなくて、単にプロジェクト分割して各レイヤーの依存方向を強制するために使うのが普通なのかどうなのか…



### 参照
https://github.com/sbt/sbt-assembly
https://www.scala-sbt.org/1.x/docs/ja/Multi-Project.html#%E5%85%B1%E9%80%9A%E3%81%AE%E3%82%BB%E3%83%83%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0
https://www.qoosky.io/techs/1ec18db8bc
https://pbassiner.github.io/blog/defining_multi-project_builds_with_sbt.html
https://stackoverflow.com/questions/37588407/how-to-make-multi-project-fat-jar-with-sbt-assembly