Reputation: 508
Suppose a small test project (sbt 0.13.8, full project as gist):
name := "test"
organization := "org.example"
version := "0.1.0-SNAPSHOT"
scalaVersion := "2.11.6"
libraryDependencies ++= Seq (
"com.lowagie" % "itext" % "4.2.1",
"com.github.wookietreiber" %% "scala-chart" % "0.4.2"
)
The dependency tree looks as follows:
dependencyTree
[info] org.example:test_2.11:0.1.0-SNAPSHOT [S]
[info] +-com.github.wookietreiber:scala-chart_2.11:0.4.2 [S]
[info] | +-org.jfree:jfreechart:1.0.17
[info] | | +-org.jfree:jcommon:1.0.21
[info] | | +-xml-apis:xml-apis:1.3.04
[info] | |
[info] | +-org.scala-lang.modules:scala-swing_2.11:1.0.1 [S]
[info] |
[info] +-com.lowagie:itext:4.2.1
[info] +-bouncycastle:bctsp-jdk14:138
[info] | +-org.bouncycastle:bctsp-jdk14:1.38
[info] | +-org.bouncycastle:bcmail-jdk14:1.38
[info] | | +-org.bouncycastle:bcprov-jdk14:1.38
[info] | |
[info] | +-org.bouncycastle:bcprov-jdk14:1.38
[info] |
[info] +-dom4j:dom4j:1.6.1
[info] | +-xml-apis:xml-apis:1.0.b2 (evicted by: 1.3.04)
[info] | +-xml-apis:xml-apis:1.3.04
[info] |
[info] +-jfree:jfreechart:1.0.12
[info] | +-jfree:jcommon:1.0.15
[info] |
[info] +-org.swinglabs:pdf-renderer:1.0.5
[info]
[success] Total time: 0 s, completed Jun 2, 2015 12:16:14 PM
The problem is the dependency jfreechart
which gets pulled in by both scala-chart
as well as by itext
(same goes for jcommon
, but I will focus on jfreechart
). However, the slightly older version that gets pulled in by itext
uses another organization name (jfree
vs. org.jfree
).
Normally, if those would use the same organization, the older one would get evicted (as is done in the example with xml-apis
) and there would be no conflicts as long as there is binary compatibility.
The real conflict now is that both dependencies jfree:jfreechart
and org.jfree:jfreechart
are both on the classpath and provide the same classes. Compilation works fine and sbt does not even complain. Take e.g. the following example App
:
package org.example
import scalax.chart.api._
object Main extends App {
val data = for {
i <- 1 to 5
category = i.toString
date = new org.jfree.data.time.Day(i, 2, 1998)
value = i * 2
} yield category -> Map(date -> value)
val chart = XYAreaChart.stacked(data.toTimeTable)
chart.saveAsPDF("/tmp/my-chart.pdf")
}
This compiles just fine! I don't know how sbt chooses which dependency to use. It all blows up if you try to actually run the thing:
sbt @ test $ compile
[info] Updating {file:/home/wookietreiber/projects/scala/jfree-itext-test/}jfree-itext-test...
[info] Resolving jline#jline;2.12.1 ...
[info] Done updating.
[info] Compiling 1 Scala source to /tmp/sbt/test/scala-2.11/classes...
[success] Total time: 1 s, completed Jun 2, 2015 1:18:36 PM
sbt @ test $ run
[info] Running org.example.Main
[error] (run-main-1) java.lang.NoSuchMethodError: org.jfree.data.time.TimeTableXYDataset.add(Lorg/jfree/data/time/TimePeriod;Ljava/lang/Number;Ljava/lang/Comparable;Z)V
java.lang.NoSuchMethodError: org.jfree.data.time.TimeTableXYDataset.add(Lorg/jfree/data/time/TimePeriod;Ljava/lang/Number;Ljava/lang/Comparable;Z)V
at scalax.chart.module.RichChartingCollections$RichCategorizedTuple2s$$anonfun$toTimeTable$1$$anonfun$apply$4.apply(RichChartingCollections.scala:300)
...
To solve this, I would need to specifically evict / remove the older jfree:jfreechart
and jfree:jcommon
versions from the dependency list / classpath. Is this possible and if so how?
Another issue I discovered is with sbt-assembly
with errors like the following (for each duplicate file):
[error] deduplicate: different file contents found in the following:
[error] /home/wookietreiber/.ivy2/cache/jfree/jfreechart/jars/jfreechart-1.0.12.jar:org/jfree/data/KeyedValues2D.class
[error] /home/wookietreiber/.ivy2/cache/org.jfree/jfreechart/jars/jfreechart-1.0.17.jar:org/jfree/data/KeyedValues2D.class
Upvotes: 4
Views: 2663
Reputation: 861
You need to define mergeStragey in assembly
either in your *.sbt or *.scala project definition to work with the sbt-assembly command. Below is a sample.
mergeStrategy in assembly <<= (mergeStrategy in assembly) { (old) =>
{
case PathList("javax", "servlet", xs @ _*) => MergeStrategy.last
case PathList("javax", "activation", xs @ _*) => MergeStrategy.last
case PathList("org", "apache", xs @ _*) => MergeStrategy.last
case PathList("com", "google", xs @ _*) => MergeStrategy.last
case PathList("com", "esotericsoftware", xs @ _*) => MergeStrategy.last
case PathList("com", "codahale", xs @ _*) => MergeStrategy.last
case PathList("com", "yammer", xs @ _*) => MergeStrategy.last
case "about.html" => MergeStrategy.rename
case "META-INF/ECLIPSEF.RSA" => MergeStrategy.last
case "META-INF/mailcap" => MergeStrategy.last
case "META-INF/mimetypes.default" => MergeStrategy.last
case "plugin.properties" => MergeStrategy.last
case "log4j.properties" => MergeStrategy.last
case x => old(x)
}
}
If you have a clear idea what the transitive jar that causes problem, you can exclude it in your libraryDependencies. For example, I excluded the httpclient jar from my direct dependency on play 2.3.6 which depends a higher version of httpclient which is not what I desired.
"com.typesafe.play" %% "play" % "2.3.6" exclude("org.apache.httpcomponents", "httpclient")
In your case, you can do exclude("jfree", "jfreechart")
to your direct dependency that introduced this old jar.
Upvotes: 3