CLOVER🍀

That was when it all began.

続・Infinispan Tree APIで、sbt+Scalaでビルドができない件について

6/24追記)
Mavenでの動作確認と、もう少し結論を加えました

昨日、sbtでもGradleでも、ScalaでInfinispanのTree APIでビルドに失敗し、しかもscalacコマンドではビルドに成功するという意味不明な状態を見ることになりました。

前回のエントリ
http://d.hatena.ne.jp/Kazuhira/20130618/1371566210

で、そこでいったん諦めていたのですが、nekopさんに昨日のエントリを拾われ

もともとリプライいただいた方に対して、「sbtのバグっぽい」というツイートまでしていただきました…。

ちょっと見ていない間に、なんということでしょう。

それで

このツイートを見て、ちょっと疑問があったのでもう少し試してみることにしました。で、最終的に、Scalaコンパイラの問題という結論になりました。

それから、Infinispan Tree APIを使ってsbt+Scalaでコンパイルを通すことができるようになりました。

結論だけまとめておくと、
Scalaから使うライブラリが、optionalな依存関係のアノテーションに依存している場合は、依存関係に明示的に追加しましょう
ということで。

Javaには関係ない話題です。

sbtで依存関係の管理を手動にしてみる

昨日、コケていたクラスが
scala/tools/nsc/symtab/classfile/ClassfileParser.scala
だったことから、あんまりsbtだけの問題がしていませんでした。

たぶんこれ、scalacでクラスファイルをパースする時に必要なクラスですよね…?sbtやGradleのAntタスクだけに登場するとも思えなかったので。

まあ、昨日はそこで諦めたわけですが(笑)。

なので、ここはsbtでライブラリの依存関係を使わずに、手動管理してみることにしました。

build.sbt

scalaVersion := "2.10.2"

build.sbt、これだけ。

あと、昨日scalacを試す時に使ったJARファイルを、libディレクトリに放り込みます。

$ ll lib/
合計 5604
drwxrwxr-x 2 xxxxx xxxxx    4096 Jun 19 21:37 ./
drwxrwxr-x 6 xxxxx xxxxx    4096 Jun 19 21:01 ../
-rw-r--r-- 1 xxxxx xxxxx 2661154 Jun 17 18:16 infinispan-core-5.3.0.CR2.jar
-rw-r--r-- 1 xxxxx xxxxx   67332 Jun 17 18:19 infinispan-tree.jar
-rw-rw-r-- 1 xxxxx xxxxx   60796 Mar 25 12:23 jboss-logging-3.1.1.GA.jar
-rw-rw-r-- 1 xxxxx xxxxx  229949 Mar 25 12:23 jboss-marshalling-1.3.15.GA.jar
-rw-rw-r-- 1 xxxxx xxxxx   82089 Mar 25 12:23 jboss-marshalling-river-1.3.15.GA.jar
-rw-rw-r-- 1 xxxxx xxxxx   11209 Mar 25 12:23 jboss-transaction-api_1.1_spec-1.0.0.Final.jar
-rw-rw-r-- 1 xxxxx xxxxx    2254 Mar 25 12:22 jcip-annotations-1.0.jar
-rw-r--r-- 1 xxxxx xxxxx 2079265 Jun  7 10:06 jgroups-3.3.1.Final.jar
-rw-rw-r-- 1 xxxxx xxxxx  481535 Mar 25 12:22 log4j-1.2.16.jar
-rw-rw-r-- 1 xxxxx xxxxx   36001 Mar 25 12:23 staxmapper-1.1.0.Final.jar

Scalaコードは、昨日とまったく同じです。
src/main/scala/InfinispanTreeExample.scala

import org.infinispan.manager.DefaultCacheManager
import org.infinispan.tree.{TreeCacheFactory, TreeCache}

object InfinispanTreeExample {
  def main(args: Array[String]): Unit = {
    val manager = new DefaultCacheManager
    val cache = manager.getCache[String, String]()

    val treeCache: TreeCache[String, String] = new TreeCacheFactory().createTreeCache(cache)
  }
}

では、sbtを起動してコンパイル。

$ sbt
[info] Set current project to default-32a9bb (in build file:/xxxxx/)
> compile
[info] Updating {file:/xxxxx/}default-32a9bb...
[info] Resolving org.scala-lang#scala-library;2.10.2 ...
[info] Done updating.
[info] Compiling 1 Scala source to /xxxxx/target/scala-2.10/classes...
[success] Total time: 8 s, completed 2013/06/19 21:39:19

…予想はしていましたが、コンパイルが通りました。

というわけで、やっぱりsbtだけの問題じゃなさそうですね。

もう1度、やったことを振り返る

昨日、scalacでコンパイルするために
http://www.jboss.org/infinispan/downloads
からInfinispan 5.3.0.CR2のzipファイルをダウンロードして、

infinispan-5.3.0.CR2-all/modules/tree/runtime-classpath.txt

に書かれていたJARファイルにクラスパスを通したわけです。

ちなみに、中身はこんな感じです。
*見やすいように、Perlワンライナーで改行を入れるようにしています

$ perl -wp -e 's!:!\n!g' infinispan-5.3.0.CR2-all/modules/tree/runtime-classpath.txt 
$ISPN_HOME/lib/infinispan-core-5.3.0.CR2.jar
$ISPN_HOME/lib/log4j-1.2.16.jar
$ISPN_HOME/lib/jboss-logging-3.1.1.GA.jar
$ISPN_HOME/lib/jgroups-3.3.1.Final.jar
$ISPN_HOME/lib/jboss-transaction-api_1.1_spec-1.0.0.Final.jar
$ISPN_HOME/lib/jcip-annotations-1.0.jar
$ISPN_HOME/lib/staxmapper-1.1.0.Final.jar
$ISPN_HOME/lib/jboss-marshalling-1.3.15.GA.jar
$ISPN_HOME/lib/jboss-marshalling-river-1.3.15.GA.jar

ここで見てるライブラリに、差があるんじゃないかなーと考えました。

sbtでライブラリ管理しているプロジェクトの、依存関係を見てみる

なので、もともとsbtの機能で依存関係を解決してビルドしようとしていたプロジェクトで、ライブラリの依存関係を表示してみることにしました。

sbt-dependency-graphプラグインを使います。

sbt-dependency-graph
https://github.com/jrudolph/sbt-dependency-graph

設定。
project/plugins.sbt

addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.7.3")

build.sbtも合わせて修正します。

scalaVersion := "2.10.2"

resolvers += "JBoss Public Maven Repository Group" at "http://repository.jboss.org/nexus/content/groups/public-jboss/"

libraryDependencies += "org.infinispan" % "infinispan-tree" % "5.3.0.CR2"

net.virtualvoid.sbt.graph.Plugin.graphSettings

で、「dependency-tree」で依存関係を表示します。

$ sbt
[info] Loading project definition from /xxxxx/project
[info] Set current project to default-ea72c1 (in build file:/xxxxx/)
> dependency-tree
[info] default:default-ea72c1_2.10:0.1-SNAPSHOT [S]
[info]   +-org.infinispan:infinispan-tree:5.3.0.CR2
[info]     +-org.infinispan:infinispan-core:5.3.0.CR2
[info]       +-org.jboss.logging:jboss-logging:3.1.1.GA
[info]       +-org.jboss.marshalling:jboss-marshalling-river:1.3.15.GA
[info]       | +-org.jboss.marshalling:jboss-marshalling:1.3.15.GA
[info]       | 
[info]       +-org.jboss.marshalling:jboss-marshalling:1.3.15.GA
[info]       +-org.jboss.spec.javax.transaction:jboss-transaction-api_1.1_spec:1.0.0.Final
[info]       +-org.jboss:staxmapper:1.1.0.Final
[info]       +-org.jgroups:jgroups:3.3.1.Final
[info]       
[success] Total time: 0 s, completed 2013/06/19 21:48:44

「org.jboss.marshalling:jboss-marshalling:1.3.15.GA」は2回出てきているので、まとめると全部で8個のライブラリが表示されています。

で、「runtime-classpath.txt」に書かれていたのは

$ perl -wp -e 's!:!\n!g' infinispan-5.3.0.CR2-all/modules/tree/runtime-classpath.txt 
$ISPN_HOME/lib/infinispan-core-5.3.0.CR2.jar
$ISPN_HOME/lib/log4j-1.2.16.jar
$ISPN_HOME/lib/jboss-logging-3.1.1.GA.jar
$ISPN_HOME/lib/jgroups-3.3.1.Final.jar
$ISPN_HOME/lib/jboss-transaction-api_1.1_spec-1.0.0.Final.jar
$ISPN_HOME/lib/jcip-annotations-1.0.jar
$ISPN_HOME/lib/staxmapper-1.1.0.Final.jar
$ISPN_HOME/lib/jboss-marshalling-1.3.15.GA.jar
$ISPN_HOME/lib/jboss-marshalling-river-1.3.15.GA.jar

9個。まあ、これに加えてinfinispan-tree.jarで10個ですね。

というわけで、2つ多い。

多いのは、この2つ。

$ISPN_HOME/lib/log4j-1.2.16.jar
$ISPN_HOME/lib/jcip-annotations-1.0.jar

じゃあ、jcip-annotationsを明示的に依存関係に加えてみよう

Log4jはたぶん関係ないでしょうから、ものは試しとjcip-annotations(Java Concurrency in Practice)を依存関係に加えてみます。

libraryDependencies ++= Seq(
  "org.infinispan" % "infinispan-tree" % "5.3.0.CR2",
  "net.jcip" % "jcip-annotations" % "1.0"
)

sbtを起動して、コンパイル。

$ sbt
[info] Loading project definition from /xxxxx/project
[info] Set current project to default-ea72c1 (in build file:/xxxxx/)
> compile
[info] Updating {file:/xxxxx/}default-ea72c1...
[info] Resolving net.jcip#jcip-annotations;1.0 ...
[info] Done updating.
[info] Compiling 1 Scala source to /xxxxx/target/scala-2.10/classes...
[success] Total time: 9 s, completed 2013/06/19 21:53:50

コンパイル、通ったー!!

というわけで、jcip-annotationsのJARが依存関係になかったのが、sbt+Scalaでビルドが通らなかった直接の原因みたいです。

jcip-annotationsのpomでの依存関係はoptional

で、Infinispan Tree APIのpomの依存関係をちょっと見てみます。

   <artifactId>infinispan-tree</artifactId>
   <packaging>bundle</packaging>
   <name>Infinispan Tree API</name>
   <description>Infinispan tree API module</description>
   <dependencies>
      <dependency>
         <groupId>${project.groupId}</groupId>
         <artifactId>infinispan-core</artifactId>
      </dependency>

      <dependency>
         <groupId>${project.groupId}</groupId>
         <artifactId>infinispan-core</artifactId>
         <type>test-jar</type>
         <scope>test</scope>
      </dependency>

      <dependency>
         <groupId>${project.groupId}</groupId>
         <artifactId>infinispan-cachestore-jdbm</artifactId>
         <scope>test</scope>
      </dependency>

   </dependencies>

ここには、jcip-annotationsはありません。

infinispan-parent-5.3.0.CR2.pomに、optionalとして登録してありました。

      <dependency>
         <groupId>net.jcip</groupId>
         <artifactId>jcip-annotations</artifactId>
         <version>${version.jcipannotations}</version>
         <optional>true</optional>
      </dependency>

Infinispan Tree APIで、jcip-annotationsを使っているものは、以下の3ファイル。

$ fgrep -nr 'net.jcip' *
org/infinispan/tree/Node.java:25:import net.jcip.annotations.ThreadSafe;
org/infinispan/tree/Fqn.java:26:import net.jcip.annotations.Immutable;
org/infinispan/tree/FqnComparator.java:25:import net.jcip.annotations.Immutable;

Fqnクラスでは、クラス宣言に付与されていました。

@Immutable
public class Fqn implements Comparable<Fqn>, Serializable {

ここで、確認のため一応MavenでもScalaを使ってコンパイルしてみました。
pom.xmlのdependenciesの設定。jcip-annotationsはコメントアウトしています。

  <dependencies>
    <dependency>
      <groupId>org.scala-lang</groupId>
      <artifactId>scala-library</artifactId>
      <version>2.10.2</version>
    </dependency>
    <dependency>
      <groupId>org.infinispan</groupId>
      <artifactId>infinispan-tree</artifactId>
      <version>5.3.0.CR2</version>
    </dependency>
    <!--
    <dependency>
      <groupId>net.jcip</groupId>
      <artifactId>jcip-annotations</artifactId>
      <version>1.0</version>
    </dependency>
    -->
  </dependencies>

コンパイル。

$ mvn compile
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building infinispan-tree-test 0.1.0
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-resources-plugin:2.5:resources (default-resources) @ infinispan-tree-test ---
[debug] execute contextualize
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /xxxxx/src/main/resources
[INFO] 
[INFO] --- maven-compiler-plugin:2.3.2:compile (default-compile) @ infinispan-tree-test ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-scala-plugin:2.15.2:compile (default) @ infinispan-tree-test ---
[INFO] Checking for multiple versions of scala
[INFO] includes = [**/*.scala,**/*.java,]
[INFO] excludes = []
[INFO] /xxxxx/src/main/scala:-1: info: compiling
[INFO] Compiling 1 source files to /xxxxx/target/classes at 1372075938825
[WARNING] warning: Class net.jcip.annotations.Immutable not found - continuing with a stub.
[WARNING] warning: Caught: java.lang.NullPointerException while parsing annotations in /xxxxx/.m2/repository/org/infinispan/infinispan-tree/5.3.0.CR2/infinispan-tree-5.3.0.CR2.jar(org/infinispan/tree/Fqn.class)
[ERROR] error: error while loading Fqn, class file '/xxxxx/.m2/repository/org/infinispan/infinispan-tree/5.3.0.CR2/infinispan-tree-5.3.0.CR2.jar(org/infinispan/tree/Fqn.class)' is broken
[INFO] (class java.lang.RuntimeException/bad constant pool index: 0 at pos: 8277)
[WARNING] two warnings found
[ERROR] one error found
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.857s
[INFO] Finished at: Mon Jun 24 21:12:23 JST 2013
[INFO] Final Memory: 9M/121M
[INFO] ------------------------------------------------------------------------

というわけで、Mavenでもバッチリとコンパイルエラー。もちろんJavaではコンパイルは通りますし、Scalaでもjcip-annotationsを依存関係に追加するとコンパイルが通ります。

つまり、どういうことかというと

  • Mavenやsbt(Ivy)、Gradleはoptionalな依存関係は明示しないとクラスパスには入らない(そりゃそうですよね…)
  • プロジェクトで依存しているライブラリの中に、optionalな依存関係のアノテーションを参照しているものがある
  • プロジェクト内のソースコードで、上記に該当するクラスを直接、またはクラスロードの関係で間接的に利用する
  • 以上の条件を満たした場合、Scalaコンパイラはアノテーションの依存関係を解決しようとするもののクラスパス上に該当のアノテーションが存在しないため、コンパイルエラーになる

ということでしょうか。

で、この状況に遭遇した場合は、
optionalなライブラリへの依存関係を明示的に追加する
ということで回避するしかなさそうです。

同じ依存関係の定義で、この事象はJavaでは発生しません。ScalaコンパイラとJavaコンパイラの挙動の差だと思われます。

なので、JavaとScalaでは同じライブラリを使っているのに、微妙に依存関係の定義が異なるケースがありうるということになります。

まあ、滅多なことでは遭遇しないと思いますが…。

蛇足)
そういうわけで、なんとかsbt+ScalaでもInfinispan Tree APIがビルドできるところまで辿り着きましたが…よくよく昨日のエントリの実行ログを見返すと、

[warn] Class net.jcip.annotations.Immutable not found - continuing with a stub.
[warn] Caught: java.lang.NullPointerException while parsing annotations in /home/xxxxx/.ivy2/cache/org.infinispan/infinispan-tree/bundles/infinispan-tree-5.3.0.CR2.jar(org/infinispan/tree/Fqn.class)
[error] error while loading Fqn, class file '/home/xxxxx/.ivy2/cache/org.infinispan/infinispan-tree/bundles/infinispan-tree-5.3.0.CR2.jar(org/infinispan/tree/Fqn.class)' is broken
[error] (class java.lang.RuntimeException/bad constant pool index: 0 at pos: 8277)

と出ていて、[warn]で

[warn] Class net.jcip.annotations.Immutable not found - continuing with a stub.

と言われていたことを見落としていたのに気付きました…。

ここに気付いていれば…なんということでしょう。

また、xuweiさんにご指摘いただきましたが、このわかりくいScalaのコンパイルエラーはScala 2.10にちょっと問題があるようで、Scala 2.9.2でコンパイルすると

[error] error while loading Node, Missing dependency 'class net.jcip.annotations.ThreadSafe', required by /xxxxx/.ivy2/cache/org.infinispan/infinispan-tree/bundles/infinispan-tree-5.3.0.CR2.jar(org/infinispan/tree/Node.class)
[error] error while loading Fqn, Missing dependency 'class net.jcip.annotations.Immutable', required by /xxxxx/.ivy2/cache/org.infinispan/infinispan-tree/bundles/infinispan-tree-5.3.0.CR2.jar(org/infinispan/tree/Fqn.class)
[error] two errors found
[error] (compile:compile) Compilation failed
[error] Total time: 3 s, completed 2013/06/24 21:35:35

と、前のエラーがなんだったのかというくらいわかりやすい感じに表示されました。

ここまで言ってくれれば、もうちょいわかりやすかったのに…。