Jar Jar Abrams
Jar Jar Abrams は、Java ライブラリをシェーディングするユーティリティである Jar Jar Links の実験的 Scala 拡張だ。
ライブラリ作者にとって他のライブラリは諸刃の剣だ。他のライブラリを使うことは作業の二重化を避け、他のライブラリを使いたくないというのはダブルスタンダードと言われかねない。しかし、その一方で、ライブラリを追加する度にそれはユーザ側にすると間接的依存性が追加されたことになり、衝突が起こる可能性も上がることになる。これは単一のプログラム内において 1つのバージョンのライブラリしか持てないことにもよる。
このような衝突はプログラムがランタイムやフレームワーク上で実行される場合によく起こる。sbt プラグインがその例だ。Spark もそう。1つの緩和策として間接的ライブラリを自分のパッケージの中にシェーディングするという方法がある。2004年に herbyderby (Chris Nokleberg) さんは Jar Jar Links というライブラリを再パッケージ化するツールを作った。
2015年に Wu Xiang さんが Jar Jar Links を使ったシェーディングのサポートを sbt-assembly に追加した。これは前向きな一歩だったが、課題も残っていた。問題の 1つは Scala コンパイラは ScalaSignature 情報を *.class
ファイルに埋め込むが、Jar Jar がそのことを知らないことだ。2020年になって Simacan社の Jeroen ter Voorde さんが ScalaSignature の変換を sbt-assembly#393 にて実装した。sbt 以外でもこれは役に立つかもしれないので、独立したライブラリに抜き出した。これが Jar Jar Abrams だ。
core API
コアには shadeDirectory
関数を実装する Shader
オブジェクトがある。
package com.eed3si9n.jarjarabrams
object Shader {
def shadeDirectory(
rules: Seq[ShadeRule],
dir: Path,
mappings: Seq[(Path, String)],
verbose: Boolean
): Unit = ...
}
この関数は、dir
が JAR ファイルを展開したディレクトリであることを期待する。
sbt-jarjar-abrams
用例のデモとして、1つのライブラリづつシェーディングする sbt プラグインを作った。
以下を project/plugins.sbt
に追加する:
addSbtPlugin("com.eed3si9n.jarjarabrams" % "sbt-jarjar-abrams" % "0.1.0")
build.sbt
はこのようになる:
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / organization := "com.example"
ThisBuild / scalaVersion := "2.12.11"
lazy val shadedJawn = project
.enablePlugins(JarjarAbramsPlugin)
.settings(
name := "shaded-jawn",
jarjarLibraryDependency := "org.typelevel" %% "jawn-parser" % "1.0.0",
jarjarShadeRules += ShadeRuleBuilder.moveUnder("org.typelevel", "shaded")
)
lazy val use = project
.dependsOn(shadedJawn)
jawn-parser は shaded
パッケージ以下にシェーディングされた。REPL を使って確認できる:
sbt:jarjar> use/console
[info] Starting scala interpreter...
Welcome to Scala 2.12.11 (OpenJDK 64-Bit Server VM, Java 1.8.0_232).
Type in expressions for evaluation. Or try :help.
scala> shaded.org.typelevel.jawn.Facade
res0: shaded.org.typelevel.jawn.Facade.type = shaded.org.typelevel.jawn.Facade$@131cedd
元の依存性グラフを真似することで複数のシェーディングライブラリを積み上げることも可能だ:
ThisBuild / organization := "com.example"
ThisBuild / scalaVersion := "2.12.11"
lazy val shadedJawn = project
.enablePlugins(JarjarAbramsPlugin)
.settings(
name := "shaded-jawn",
jarjarLibraryDependency := "org.typelevel" %% "jawn-parser" % "1.0.0",
jarjarShadeRules += ShadeRuleBuilder.moveUnder("org.typelevel", "shaded")
)
lazy val shadedJawnAst = project
.enablePlugins(JarjarAbramsPlugin)
.dependsOn(shadedJawn)
.settings(
name := "shaded-jawn-ast",
jarjarLibraryDependency := "org.typelevel" %% "jawn-ast" % "1.0.0",
jarjarShadeRules += ShadeRuleBuilder.moveUnder("org.typelevel", "shaded")
)
lazy val use = project
.dependsOn(shadedJawnAst)
REPL から使ってみる:
sbt:jarjar> use/console
[info] Starting scala interpreter...
Welcome to Scala 2.12.11 (OpenJDK 64-Bit Server VM, Java 1.8.0_232).
Type in expressions for evaluation. Or try :help.
scala> shaded.org.typelevel.jawn.ast.JParser.parseUnsafe("""{ "x": 10 }""")
res0: shaded.org.typelevel.jawn.ast.JValue = {"x":10}
自己責任
もう一度注意しておきたいのは、これは実験的であるということだ。多くのライブラリは config ファイルなどの Jar Jar Abrams が変換しない実行時の振る舞いに依存する。
これを使って sbt の間接的ライブラリをシェーディングで追い出すことで sbt プラグイン作者が自由に別のバージョンを選べるようになるので、うまくいけば良いなと思っている。