この記事では Scala で Jar ライブラリやネイティブライブラリを作成する方法について解説します。
背景
Java(javac) は後方互換性なバイナリを生成するため、広く使われることが予想されるライブラリは、LTS で一番低いバージョンでビルドしておくと、多くの方が利用できるでしょう。しかしながら、Scala 2.x は(semberの)マイナーバージョン間で非互換なバイナリを生成するため、各マイナーバージョンでライブラリを作成する必要があります。sbt には、複数の Scala のバージョンをビルドする機能が備わっています。
Jar ライブラリを作る
プロジェクトに crossScalaVersions
を与えることで、複数の Scala バージョンをビルドさせることができます。
ThisBuild / name := "my-library"
lazy val scala3 = "3.1.1"
lazy val scala213 = "2.13.8"
lazy val scala212 = "2.12.15"
lazy val scala211 = "2.11.12"
lazy val supportedScalaVersions = List(scala3, scala213, scala212, scala211)
lazy val root = (project in file("."))
.settings(
scalaVersion := scala213,
// クロスビルド設定
crossScalaVersions := supportedScalaVersions,
// maven リポジトリ用
organization := "org.github.kijuky",
publishTo := {
if (isSnapshot.value) {
Some("snapshots" at ...) // snapshot は自由に配置できるように。
} else {
sys.env.get("PUBLISH_URL").map("release".at) // release は CI で定義された環境変数(PUBLISH_URL) から配置場所を決定。
}
}
)
この crossScalaVersions
が与えられているとき、コマンドの先頭に +
を与えると、 scalaVersion
をそのリストのバージョンにして foreach してくれます。便利ですね。
$ sbt +compile
これで Scala 3.x, Scala 2.13.x, Scala 2.12.x, Scala 2.11.x のコンパイルが行われます。
テストも、
$ sbt +test
maven リポジトリへの公開も、同じようにできます。
$ sbt +publish # PUBLISH_URL が必要
フォルダ構成
Scala のバージョンが異なる場合、ソースコードの書き方も変わってきたりします。例えば、Scala 2.12 から Scala 2.13 では、Java のコレクションライブラリから Scala のコレクションライブラリに変換するパッケージが変わっていたりします。Scala 3 に至ってはオプショナルブレースによってかなり表記が変わります。つまり、Scala はバージョン間でソースコード互換性が保たれていないのです。
このような場合、sbt では scala のバージョンの違いでフォルダを切り替えることができます。
root
├ project
│ ├ build.properties
│ └ plugins.sbt
├ src
│ ├ main
│ │ ├ scala # 全ての scala バージョンでコンパイルされる
│ │ ├ scala-2.11 # scala 2.11.x の時だけコンパイルされる
│ │ ├ scala-2.12 # scala 2.12.x の時だけコンパイルされる
│ │ ├ scala-2.13 # scala 2.13.x の時だけコンパイルされる
│ │ └ scala-3 # scala 3.x の時だけコンパイルされる
│ └ test # test でも同様のフォルダ構成を利用できる
│ ├ scala
│ ├ scala-2.11
│ ├ scala-2.12
│ ├ scala-2.13
│ └ scala-3
└ build.sbt
実際にこの構成をしてみたところ、 scala (バージョンなし)フォルダは利用しない方が良いでしょう。Scala 2.x と Scala 3.x の書き方が違いすぎるため、別フォルダに分けて各バージョンで適切な書き方をしておいた方が見通しが良いです。また、Scala 2.12 と Scala 2.13 でも、多少ソースコードの互換性が失われているので、これらも別々のフォルダに分けておいた方が良いです。
一方、Scala 2.12 と Scala 2.11 ではそこまで変化が少ないため、 Scala 2.11 -> Scala 2.12 でシンボリックリンクを作成しておいても良いでしょう。
異なる Scala バージョンで異なる設定を適用させる
build.sbt では Scala を書けるので、現在の適用バージョンを調べて match 式で分岐を書くことができます。例えば scalacOptions を Scala 2.x と Scala 3.x で書き換える場合:
scalacOptions ++=
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((3, _)) => // 3系
Seq("-new-syntax", "-rewrite")
case _ => // 2系
Seq("-deprecation", "-unchecked", "-Xlint", "-Xfatal-warnings")
}
ネイティブライブラリを作る
GraalVM を使うことで、Jar ライブラリ以外にもネイティブで動くライブラリを作ることができます。ちなみに、こちらで紹介したパーサー・コンビネーターを使ったライブラリくらいはネイティブ化できたりします(しました)。
addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.1")
/* (さっきの記述から追記) */
lazy val cli = (project in file("cli"))
.enablePlugins(NativeImagePlugin)
.dependsOn(root) // メインのライブラリを使う、という設定
.settings(
scalaVersion := scala213,
// ライブラリ設定。ゆるく CLI 書くなら、適当な cli ライブラリを導入すると便利。
libraryDependencies ++= Seq(
"com.github.losizm" %% "little-cli" % "0.8.0", // scala 2.13.x で使えるバージョン
"commons-cli" % "commons-cli" % "1.5.0"
),
// native イメージ生成用
nativeImageOptions ++= Seq("-H:+AllowIncompleteClasspath"), // これがないとエラーになる
nativeImageOutput := target.value / "native-image" / (ThisBuild / name).value // そのままだとプロジェクト名("cli")になっちゃうので、プロダクト名(`ThisBuild / name`)にする。
)
/* TODO: ほんとはいい感じの CLI を書く */
object Main {
def main(args: Array[String]): Unit =
println("Hello world")
}
cli プロジェクトに main 関数を持つクラスを用意しておきます。下記でネイティブイメージが作成できます。
$ sbt cli/nativeImage
...出力は省略...
$ cli/target/native-image/my-project
Hello world
簡単ですね。GitLab CI とかだと、 hseeberger/scala-sbt:graalvm-ce-21.3.0-java17_1.6.2_2.13.8 みたいな、GraalVM を内包する docker イメージをベースにビルドすると、gcc とかを用意する必要がないので便利です。
native:
image: hseeberger/scala-sbt:graalvm-ce-21.3.0-java17_1.6.2_2.13.8
script:
- sbt cli/nativeImage
artifacts:
paths:
- cli/target/native-image
(Windows だとどうするんだろう...)
まとめ
Scala プロジェクトから各種ライブラリを作る方法を紹介しました。 crossScalaVersions
を与えることで、複数の Scala バージョンのビルド/パブリッシュを行うことができます。 sbt-native-image
プラグインを使うことで、ネイティブライブラリを作成することができます。Scala バージョン間のバイナリ互換性がなくても、この sbt のサポートによってかなりハードルが低くなっています。また、ネイティブライブラリの作成ができることで、JVMに依存しない環境(クラウドのラムダだったり、非JVM系のWebサーバーだったり)でも、Scala の資産を活用することができます。
素晴らしいですね!みなさんも Scala 書きましょう!