Scala
sbt
ivy

SBTのversion conflict(s)解消方法まとめ

More than 1 year has passed since last update.

ウェブクルー Advent Calendar 2017 6日目の記事です!
昨日は@asukame さんの「Pythonで数理最適化を行ってみた」でした!

はじめに

SBTでversion conflictが発生した場合の解決方法については、公式のドキュメント(英語)Wiki(英語)などに様々なことが書いてありますが、とりあえずこうすれば解決するよぐらいのテンションで書かれた新しめの日本語記事が無かったので書いてみた、という感じです。
また、私自身ScalaとSBTを本格的に触り始めて2ヶ月経つかたたないかというスキル上の問題や理解不足から誤った説明をしている可能性あります。その場合はQiita上でコメントを頂けるとうれしいです!マサカリお待ちしてます!:bow:

SBTのバージョン

説明に利用しているSBTのバージョンは1.0.3を利用しています。
0.13系かそれより前のバージョンでは、コンフリクト時のメッセージが異なりますので、注意して下さい。
追記:0.13.16では1.0系と同等のメッセージが表示されるようです。

1.0系での表示例
[warn] Found version conflict(s) in library dependencies; some are suspected to be binary incompatible:
[warn]  * com.typesafe.akka:akka-actor_2.12:2.5.6 is selected over 2.4.19
[warn]      +- com.typesafe.akka:akka-slf4j_2.12:2.5.6 ()         (depends on 2.5.6)
[warn]      +- com.typesafe.akka:akka-parsing_2.12:10.0.10        (depends on 2.4.19)
[warn]      +- com.typesafe.akka:akka-stream_2.12:2.4.19 ()       (depends on 2.4.19)
[warn] Run 'evicted' to see detailed eviction warnings
0.13系での表示例
[warn] There may be incompatibilities among your library dependencies.
[warn] Here are some of the libraries that were evicted:
[warn]  * com.google.code.findbugs:jsr305:2.0.1 -> 3.0.0
[warn] Run 'evicted' to see detailed eviction warnings

引用元 - Release 1.0.0 · sbt/sbt Eviction warning presentation

build.sbt

今後の説明では、次のようなbuild.sbtがあるときに表示される警告メッセージで説明を進めていきます。

build.sbt
import Dependencies._

lazy val root = (project in file(".")).
  settings(
    inThisBuild(List(
      organization := "net.nokok",
      scalaVersion := "2.12.4",
      version      := "0.1.0-SNAPSHOT"
    )),
    name := "deps",
    libraryDependencies += scalaTest % Test
  )

libraryDependencies += "com.typesafe.akka" %% "akka-http-core" % "10.0.10"
libraryDependencies += "com.typesafe.akka" % "akka-slf4j_2.12" % "2.5.6"
 //コンフリクトさせるために追加している依存

Akkaを選んだ理由は特にありませんw

警告メッセージの読み方

上のようなbuild.sbtでupdateタスクを実行すると、次のような警告メッセージが表示されます。

警告例
sbt:deps> update
[info] Updating {file:(...)nokok/sbt-depconflict/}root...
[info] Done updating.
[warn] Found version conflict(s) in library dependencies; some are suspected to be binary incompatible:
[warn]  * com.typesafe.akka:akka-actor_2.12:2.5.6 is selected over 2.4.19
[warn]      +- com.typesafe.akka:akka-slf4j_2.12:2.5.6 ()         (depends on 2.5.6)
[warn]      +- com.typesafe.akka:akka-parsing_2.12:10.0.10        (depends on 2.4.19)
[warn]      +- com.typesafe.akka:akka-stream_2.12:2.4.19 ()       (depends on 2.4.19)
[warn] Run 'evicted' to see detailed eviction warnings
[success] Total time: 1 s, completed 2017/12/01 14:03:36
sbt:deps>

このメッセージの場合、次のような意味合いになります。

  • ライブラリの依存関係にバイナリ互換性が無い可能性がある
  • com.typesafe.akka:akka-actor が2.4.19と2.5.6で衝突し、2.5.6が選択された
  • com.typesafe.akka:akka-slf4jakka-actorの2.5.6に依存している
  • com.typesafe.akka:akka-parsingcom.typesafe.akka:akka-streamakka-actorの2.4.19に依存している。

どう解決していくか?

1. 依存の整理を検討する

ライブラリバージョンのアップグレードもしくはダウングレード、利用していない余計な依存を除外することにより、コンフリクトしない組み合わせが利用できないかを最初に検討しましょう。

例えば、Play-slickのplay-slick All Releasesのようにサポートしているバージョンの組み合わせを記載していたり、何らかのIssueが上がっていたりするかもしれません。身も蓋もありませんが、このような情報を確認して、コンフリクトを起こさないバージョンで組み合わせる方法が一番確実な解決方法で、実現できれば根本解決です。

今回の例にあるような依存関係の場合、(衝突させるために追加した)akka-slf4jへの依存がそもそも不要なので、依存を消します。

build.sbt
  import Dependencies._

  lazy val root = (project in file(".")).
    settings(
      inThisBuild(List(
        organization := "net.nokok",
        scalaVersion := "2.12.4",
        version      := "0.1.0-SNAPSHOT"
      )),
      name := "deps",
      libraryDependencies += scalaTest % Test
    )

  libraryDependencies += "com.typesafe.akka" %% "akka-http-core" % "10.0.10"
- libraryDependencies += "com.typesafe.akka" % "akka-slf4j_2.12" % "2.5.6"

SBT上でreloadしてから再度updateを実行すると...

sbt:deps> reload
[info] Loading project definition from (...)\nokok\sbt-depconflict\project
[info] Loading settings from build.sbt ...
[info] Set current project to deps (in build file:(...)nokok/sbt-depconflict/)
sbt:deps> update
[info] Updating {file:(...)nokok/sbt-depconflict/}root...
[info] Done updating.
[success] Total time: 1 s, completed 2017/12/01 16:07:53
sbt:deps>

衝突が解消され、警告が表示されなくなりました!解決です。

2. dependencyOverrides でバージョンを固定する

1. 依存の整理を検討する」ではライブラリの依存を見直すことにより衝突を解決させました。根本解決にはつながりますが、実際のアプリケーションではこのように簡単にアップ/ダウングレードできないという状況も考えられると思います。また、ライブラリの依存関係的にそもそも無理、みたいな状況もありそうですね。

そこで、build.sbtに dependencyOverrides を記述することによって依存情報を上書き、特定のライブラリのバージョンを固定することで、警告をが出来ます。

build.sbt
  import Dependencies._

  lazy val root = (project in file(".")).
    settings(
      inThisBuild(List(
        organization := "net.nokok",
        scalaVersion := "2.12.4",
        version      := "0.1.0-SNAPSHOT"
      )),
      name := "deps",
      libraryDependencies += scalaTest % Test
    )

  libraryDependencies += "com.typesafe.akka" %% "akka-http-core" % "10.0.10"
  libraryDependencies += "com.typesafe.akka" % "akka-slf4j_2.12" % "2.5.6"

+ dependencyOverrides += "com.typesafe.akka" %% "akka-actor" % "2.4.19"

このような記述を追加することで、衝突しているAkka Actorのバージョンを明示的に2.4.19に固定することができ、警告メッセージが非表示となります。(もちろん2.5.6に固定することも、別のバージョンにすることもできます)

sbt:deps> reload
[info] Loading project definition from (...)\nokok\sbt-depconflict\project
[info] Loading settings from build.sbt ...
[info] Set current project to deps (in build file:(...)nokok/sbt-depconflict/)
sbt:deps> update
[info] Updating {file:(...)nokok/sbt-depconflict/}root...
[info] Done updating.
[success] Total time: 1 s, completed 2017/12/01 17:29:58
sbt:deps>

警告が出なくなりました

3. excludes で特定の依存関係を除外する

excludes を記述することによって、特定のライブラリの依存関係を除外することができます。

build.sbt
import Dependencies._

 lazy val root = (project in file(".")).
   settings(
     inThisBuild(List(
       organization := "net.nokok",
       scalaVersion := "2.12.4",
       version      := "0.1.0-SNAPSHOT"
     )),
     name := "deps",
     libraryDependencies += scalaTest % Test
   )

 libraryDependencies += "com.typesafe.akka" %% "akka-http-core" % "10.0.10"
-libraryDependencies += "com.typesafe.akka" % "akka-slf4j_2.12" % "2.5.6"
+libraryDependencies += "com.typesafe.akka" % "akka-slf4j_2.12" % "2.5.6" exclude("com.typesafe.akka", "akka-actor_2.12")

こちらも同様に、警告メッセージが表示されなくなります。

sbt:deps> reload
[info] Loading project definition from (...)\nokok\sbt-depconflict\project
[info] Loading settings from build.sbt ...
[info] Set current project to deps (in build file:(...)nokok/sbt-depconflict/)
sbt:deps> update
[info] Updating {file:(...)nokok/sbt-depconflict/}root...
[info] Done updating.
[success] Total time: 1 s, completed 2017/12/01 17:29:58
sbt:deps>

excludesの他に、複数指定できる excludeAllも用意されています。


dependencyOverridesexcludes の違い

今回の例では、どの解決策を選択しても akka-actorは2.4.19が選択されて衝突が解消されます。
そこで気になるのはこのdependencyOverridesexcludesとでは、どのような違いがあるか?というところになると思いますが、dependencyOverridesexcludesでは、依存の追加のされ方が異なります。

これは、 SBTでshow updateを実行することで簡単に確認することができます。

  1. dependencyOverridesを指定したときの com.typesafe.akka:akka-actor_2.12 の情報
...
[info]  com.typesafe.akka:akka-actor_2.12
[info]          - 2.4.19
[info]                  status: release
[info]                  publicationDate: 2017-06-12T21:44:09+09:00
[info]                  resolver: sbt-chain
            ...
[info]                  callers: com.typesafe.akka:akka-stream_2.12:2.4.19 (), com.typesafe.akka:akka-slf4j_2.12:2.5.6 (), com.typesafe.akka:akka-parsing_2.12:10.0.10
[info]
...
  1. excludesを指定したときの com.typesafe.akka:akka-actor_2.12 の情報
...
[info]  com.typesafe.akka:akka-actor_2.12
[info]          - 2.4.19
[info]                  status: release
[info]                  publicationDate: 2017-06-12T21:44:09+09:00
[info]                  resolver: sbt-chain
            ...
[info]                  callers: com.typesafe.akka:akka-stream_2.12:2.4.19 (), com.typesafe.akka:akka-parsing_2.12:10.0.10
...

callers のところが違っているのがわかるでしょうか?
dependencyOverridesは単純にバージョンを上書きをするのに対して
excludesakka-slf4j_2.12 があたかもakka-actorに依存していなかったかのように振る舞います。

まとめ

SBTでversion conflict(s)が出た場合、

  • ライブラリ側がサポートしているバージョンを探してみて、それに合わせてみましょう
  • dependencyOverridesまたはexclude等で依存を制御してみましょう

小ネタ

この記事をまとめるにあたってSBTに関して個人的に「へ~」となった点についていくつか載せます。

依存の衝突が発生したら例外を投げて落としたい

build.sbtにこのような記述を追加することで、衝突発生時に例外をスローさせることが出来ます。

build.sbt
conflictManager := ConflictManager.strict

...

sbt:deps> reload
[info] Loading project definition from (...)\nokok\sbt-depconflict\project
[info] Loading settings from build.sbt ...
[info] Set current project to deps (in build file:(...)nokok/sbt-depconflict/)
sbt:deps> update
[info] Updating {file:(...)nokok/sbt-depconflict/}root...
[error] com.typesafe.akka#akka-actor_2.12;2.5.6 (needed by [com.typesafe.akka#akka-slf4j_2.12;2.5.6]) conflicts with com.typesafe.akka#akka-actor_2.12;2.4.19 (needed by [com.typesafe.akka#akka-parsing_2.12;10.0.10, com.typesafe.akka#akka-stream_2.12;2.4.19])
[error] org.apache.ivy.plugins.conflict.StrictConflictException: com.typesafe.akka#akka-actor_2.12;2.5.6 (needed by [com.typesafe.akka#akka-slf4j_2.12;2.5.6]) conflicts with com.typesafe.akka#akka-actor_2.12;2.4.19 (needed by [com.typesafe.akka#akka-parsing_2.12;10.0.10, com.typesafe.akka#akka-stream_2.12;2.4.19])
[error]         at org.apache.ivy.plugins.conflict.StrictConflictManager.resolveConflicts(StrictConflictManager.java:45)
...

これを利用することで、例えばCI等を走らせているときに「衝突していたらCIを落としたい!」というのが実現できると思います。
このConflictManagerには strict だけでなくいくつか別のものを指定できます。

指定する値 挙動 備考
all (衝突が発生したとしても)何もしません -
latestTime 最後に定義した最新リビジョンのみを選択します 高コスト
latestVersion 最新のリビジョンのみを選択します デフォルト
latestCompatible 衝突したものは最新のバージョンを選択しつつ、互換性のある組み合わせを見つけようとします 互換性のある組み合わせが無い場合は例外をスローします
strict 衝突が発生した場合は例外をスローします -

マルチプロジェクトのときは?

このように記述すれば、プロジェクトごとに依存を分けることができるので、衝突している依存がプロジェクトをまたがない場合はこの方法も有効だと思います。


説明用のプロジェクト

今回は scala/scala-seed テンプレートを用いて意図的に依存ライブラリのコンフリクトを発生させることで説明を行いました。

ソースコード GitHub nokok/sbt-depconflict

解決方法に対応づく形で、各フォルダにプロジェクトが保存されています。

参考資料

sbt Reference Manual — Library Management - Scala SBT
librarymanagement/EvictionWarning.scala at 1.0.x · sbt/librarymanagement
librarymanagement/ConflictWarning.scala at 1.0.x · sbt/librarymanagement
librarymanagement/EvictionWarningSpec.scala at 1.0.x · sbt/librarymanagement
conflict-managers | Apache Ivy™


以上となります!上にも記載しましたが誤りがあったらコメントをいただけるとうれしいです!

良きSBTライフを!

明日の担当は@wchikarusato さんです!よろしくお願いします!