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コンパイラの挙動の差だと思われます。

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

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

蛇足)
そういうわけで、なんとか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

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

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

scala/scalac/fscなどのコマンドのオプションを、Scalaコンパイラのソースから確認する

先ほどまで、とあるライブラリを使ったコードのコンパイルがうまくいかなくて、Scalaコンパイラのコードとかを眺めていたわけですが、その時にbuild.gradleに

compileScala {
  compileScala.scalaCompileOptions.additionalParameters= ['-Ydebug']
}

みたいなデバッグオプションを付けてコンパイルしていました。

このオプションをどうやって見つけたかですが、Scalaコンパイラのソースから見つけました。

コケていた「scala/tools/nsc/symtab/classfile/ClassfileParser.scala」に

// Code from scala/tools/nsc/symtab/classfile/ClassfileParser.scala...
  private def handleError(e: Exception) = {
    if (settings.debug.value) e.printStackTrace()
    throw new IOException(s"class file '${in.file}' is broken\n(${e.getClass}/${e.getMessage})")
  }

みたいなことが書いてあったので、なんとかして

    if (settings.debug.value) e.printStackTrace()

を有効にしてスタックトレースを出したいというところからきています。

で、実際これを有効にするためのオプションが定義してあったクラスを含めて、scala/scalac/fscなどの各種コマンドで使えるオプションを確認するには、以下のトレイトやクラスを見るとよさそうです。

scala.tools.nsc.settings.Warnings
scala.tools.nsc.settings.StandardScalaSettings
scala.tools.nsc.settings.ScalaSettings
scala.tools.nsc.settings.FscSettings
scala.tools.nsc.GenericRunnerSettings

…一応、トレイトの順番とかは考えて並べてあります。

例えば、今回使った「-Ydebug」オプションですが、scala.tools.nsc.settings.ScalaSettingsに定義してありました。

  val debug           = BooleanSetting    ("-Ydebug", "Increase the quantity of debugging output.")

「-X」なオプションとかも、定義してありました。

  val Xprint        = PhasesSetting     ("-Xprint", "Print out program after")
  val writeICode    = PhasesSetting     ("-Xprint-icode", "Log internal icode to *.icode files after", "icode")
  val Xprintpos     = BooleanSetting    ("-Xprint-pos", "Print tree positions, as offsets.")
  val printtypes    = BooleanSetting    ("-Xprint-types", "Print tree types (debugging option).")

オーソドックスなオプションは、StandardScalaSettingsを見ればいいみたいです。

trait StandardScalaSettings {
  self: AbsScalaSettings =>

  /** Path related settings.
   */
  val bootclasspath =     PathSetting ("-bootclasspath", "Override location of bootstrap class files.", Defaults.scalaBootClassPath)
  val classpath:          PathSetting // is mutated directly in various places (thus inspiring this very effort)
  val d:                OutputSetting // depends on mutable OutputDirs class
  val extdirs =           PathSetting ("-extdirs", "Override location of installed extensions.", Defaults.scalaExtDirs)
  val javabootclasspath = PathSetting ("-javabootclasspath", "Override java boot classpath.", Defaults.javaBootClassPath)
  val javaextdirs =       PathSetting ("-javaextdirs", "Override java extdirs classpath.", Defaults.javaExtDirs)
  val sourcepath =        PathSetting ("-sourcepath", "Specify location(s) of source files.", "") // Defaults.scalaSourcePath

  /** Other settings.
   */
  val dependencyfile =  StringSetting ("-dependencyfile", "file", "Set dependency tracking file.", ".scala_dependencies")
  val deprecation =    BooleanSetting ("-deprecation", "Emit warning and location for usages of deprecated APIs.")
  val encoding =        StringSetting ("-encoding", "encoding", "Specify character encoding used by source files.", Properties.sourceEncoding)
  val explaintypes =   BooleanSetting ("-explaintypes", "Explain type errors in more detail.")
  val feature =        BooleanSetting ("-feature", "Emit warning and location for usages of features that should be imported explicitly.")
  val g =               ChoiceSetting ("-g", "level", "Set level of generated debugging info.", List("none", "source", "line", "vars", "notailcalls"), "vars")
  val help =           BooleanSetting ("-help", "Print a synopsis of standard options")
  val make =            ChoiceSetting ("-make", "policy", "Recompilation detection policy", List("all", "changed", "immediate", "transitive", "transitivenocp"), "all")
                        . withDeprecationMessage ("this option is unmaintained.  Use sbt or an IDE for selective recompilation.")
  val nowarn =         BooleanSetting ("-nowarn", "Generate no warnings.")
  val optimise:        BooleanSetting // depends on post hook which mutates other settings
  val print =          BooleanSetting ("-print", "Print program with Scala-specific features removed.")
  val target =          ChoiceSetting ("-target", "target", "Target platform for object files. All JVM 1.5 targets are deprecated.",
                                       List("jvm-1.5", "jvm-1.5-fjbg", "jvm-1.5-asm", "jvm-1.6", "jvm-1.7", "msil"),
                                       "jvm-1.6")
  val unchecked =      BooleanSetting ("-unchecked", "Enable additional warnings where generated code depends on assumptions.")
  val uniqid =         BooleanSetting ("-uniqid", "Uniquely tag all identifiers in debugging output.")
  val usejavacp =      BooleanSetting ("-usejavacp", "Utilize the java.class.path in classpath resolution.")
  val verbose =        BooleanSetting ("-verbose", "Output messages about what the compiler is doing.")
  val version =        BooleanSetting ("-version", "Print product version and exit.")

  /** These are @<file> and -Dkey=val style settings, which don't
   *  nicely map to identifiers.
   */
  val argfiles: BooleanSetting  // exists only to echo help message, should be done differently
}

ScalaSettingsは、StandardScalaSettingsとWarningsをミックスインしたトレイト、FscSettingsとGenericRunnerSettingsは上位でScalaSettingsをミックスインしているSettingsクラスを継承しているクラスになります。

だいたい、コマンドとの対応がつきそうな感じです。

scalacコマンドに特化したものがありませんが、scalaコマンドの説明で

PARAMETERS


Any scalac option. See scalac(1).

とあるので、scalaコマンドと定義がそれなりに被ってるってことですね。

scalaコマンドのオプションは、ScalaSettingsのオプションにGenericRunnerSettingsで定義したオプションを加えたものだと思います。そして、scalacコマンドで使えるのはScalaSettingsで定義したところまで、ということでしょうね。