Implements eviction warning stories. #1200

This implements all stories from https://github.com/sbt/sbt/wiki/User-Stories%3A-Conflict-Warning.
When scalaVersion is no longer effective an eviction warning will display.

    Scala version was updated by one of library dependencies:
        * org.scala-lang:scala-library:2.10.2 -> 2.10.3

When there're suspected incompatibility in directly depended Java libraries,
eviction warnings will display.

   There may be incompatibilities among your library dependencies.
   Here are some of the libraries that were evicted:
      * commons-io:commons-io:1.4 -> 2.4

When there's suspected incompatiblity in directly depended Scala libraries,
eviction warnings will display.

  There may be incompatibilities among your library dependencies.
  Here are some of the libraries that were evicted:
      * com.typesafe.akka:akka-actor_2.10:2.1.4 -> 2.3.4

This also adds 'evicted' task, which displays more detailed eviction warnings.
This commit is contained in:
Eugene Yokota 2014-07-27 12:26:12 -04:00
parent 439f8498b6
commit 2b8fa35b8d
3 changed files with 447 additions and 16 deletions

View File

@ -1,25 +1,120 @@
package sbt
import collection.mutable
import Configurations.Compile
final class EvictionWarningOptions(
val configurations: Seq[String],
val level: Level.Value) {
final class EvictionWarningOptions private[sbt] (
val configurations: Seq[Configuration],
val warnScalaVersionEviction: Boolean,
val warnDirectEvictions: Boolean,
val warnTransitiveEvictions: Boolean,
val showCallers: Boolean,
val guessCompatible: Function1[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean]) {
private[sbt] def configStrings = configurations map { _.name }
def withConfigurations(configurations: Seq[Configuration]): EvictionWarningOptions =
copy(configurations = configurations)
def withWarnScalaVersionEviction(warnScalaVersionEviction: Boolean): EvictionWarningOptions =
copy(warnScalaVersionEviction = warnScalaVersionEviction)
def withWarnDirectEvictions(warnDirectEvictions: Boolean): EvictionWarningOptions =
copy(warnDirectEvictions = warnDirectEvictions)
def withWarnTransitiveEvictions(warnTransitiveEvictions: Boolean): EvictionWarningOptions =
copy(warnTransitiveEvictions = warnTransitiveEvictions)
def withShowCallers(showCallers: Boolean): EvictionWarningOptions =
copy(showCallers = showCallers)
def withGuessCompatible(guessCompatible: Function1[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean]): EvictionWarningOptions =
copy(guessCompatible = guessCompatible)
private[sbt] def copy(configurations: Seq[Configuration] = configurations,
warnScalaVersionEviction: Boolean = warnScalaVersionEviction,
warnDirectEvictions: Boolean = warnDirectEvictions,
warnTransitiveEvictions: Boolean = warnTransitiveEvictions,
showCallers: Boolean = showCallers,
guessCompatible: Function1[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean] = guessCompatible): EvictionWarningOptions =
new EvictionWarningOptions(configurations = configurations,
warnScalaVersionEviction = warnScalaVersionEviction,
warnDirectEvictions = warnDirectEvictions,
warnTransitiveEvictions = warnTransitiveEvictions,
showCallers = showCallers,
guessCompatible = guessCompatible)
}
object EvictionWarningOptions {
def apply(): EvictionWarningOptions =
new EvictionWarningOptions(Vector("compile"), Level.Warn)
def default: EvictionWarningOptions =
new EvictionWarningOptions(Vector(Compile), true, true, false, false, defaultGuess)
def full: EvictionWarningOptions =
new EvictionWarningOptions(Vector(Compile), true, true, true, true, defaultGuess)
lazy val defaultGuess: Function1[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean] =
guessSecondSegment orElse guessSemVer orElse guessFalse
lazy val guessSecondSegment: PartialFunction[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean] = {
case (m1, Some(m2), Some(ivyScala)) if m2.name.endsWith("_" + ivyScala.scalaFullVersion) || m2.name.endsWith("_" + ivyScala.scalaBinaryVersion) =>
(m1.revision, m2.revision) match {
case (VersionNumber(ns1, ts1, es1), VersionNumber(ns2, ts2, es2)) =>
VersionNumber.SecondSegment.isCompatible(VersionNumber(ns1, ts1, es1), VersionNumber(ns2, ts2, es2))
case _ => false
}
}
lazy val guessSemVer: PartialFunction[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean] = {
case (m1, Some(m2), _) =>
(m1.revision, m2.revision) match {
case (VersionNumber(ns1, ts1, es1), VersionNumber(ns2, ts2, es2)) =>
VersionNumber.SemVer.isCompatible(VersionNumber(ns1, ts1, es1), VersionNumber(ns2, ts2, es2))
case _ => false
}
}
lazy val guessFalse: PartialFunction[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean] = {
case (_, _, _) => false
}
}
final class EvictionPair private[sbt] (
val organization: String,
val name: String,
val winner: Option[ModuleReport],
val evicteds: Seq[ModuleReport],
val includesDirect: Boolean,
val showCallers: Boolean) {
override def toString: String =
EvictionPair.evictionPairLines.showLines(this).mkString
}
object EvictionPair {
implicit val evictionPairLines: ShowLines[EvictionPair] = ShowLines { a: EvictionPair =>
val revs = a.evicteds map { _.module.revision }
val revsStr = if (revs.size <= 1) revs.mkString else "(" + revs.mkString(", ") + ")"
val winnerRev = (a.winner map { r =>
val callers: String =
if (a.showCallers)
r.callers match {
case Seq() => ""
case cs => (cs map { _.caller.toString }).mkString(" (caller: ", ", ", ")")
}
else ""
r.module.revision + callers
}).headOption map { " -> " + _ } getOrElse ""
Seq(s"\t* ${a.organization}:${a.name}:${revsStr}$winnerRev")
}
}
final class EvictionWarning private[sbt] (
val options: EvictionWarningOptions,
val scalaEvictions: Seq[EvictionPair],
val directEvictions: Seq[EvictionPair],
val transitiveEvictions: Seq[EvictionPair],
val allEvictions: Seq[EvictionPair]) {
def reportedEvictions: Seq[EvictionPair] = scalaEvictions ++ directEvictions ++ transitiveEvictions
}
object EvictionWarning {
def apply(options: EvictionWarningOptions, report: UpdateReport, log: Logger): Unit = {
def apply(module: IvySbt#Module, options: EvictionWarningOptions, report: UpdateReport, log: Logger): EvictionWarning = {
val evictions = buildEvictions(options, report)
processEvictions(evictions, log)
processEvictions(module, options, evictions)
}
private[sbt] def buildEvictions(options: EvictionWarningOptions, report: UpdateReport): Seq[ModuleDetailReport] = {
val buffer: mutable.ListBuffer[ModuleDetailReport] = mutable.ListBuffer()
val confs = report.configurations filter { x => options.configurations contains x.configuration }
val confs = report.configurations filter { x => options.configStrings contains x.configuration }
confs flatMap { confReport =>
confReport.details map { detail =>
if ((detail.modules exists { _.evicted }) &&
@ -31,14 +126,75 @@ object EvictionWarning {
buffer.toList.toVector
}
private[sbt] def processEvictions(evictions: Seq[ModuleDetailReport], log: Logger): Unit = {
if (!evictions.isEmpty) {
log.warn("Some dependencies were evicted:")
evictions foreach { detail =>
val revs = detail.modules filter { _.evicted } map { _.module.revision }
val winner = (detail.modules filterNot { _.evicted } map { _.module.revision }).headOption map { " -> " + _ } getOrElse ""
log.warn(s"\t* ${detail.organization}:${detail.name} (${revs.mkString(", ")})$winner")
}
private[sbt] def isScalaArtifact(module: IvySbt#Module, organization: String, name: String): Boolean =
module.moduleSettings.ivyScala match {
case Some(s) =>
organization == s.scalaOrganization &&
(name == "scala-library") || (name == "scala-compiler")
case _ => false
}
private[sbt] def processEvictions(module: IvySbt#Module, options: EvictionWarningOptions, reports: Seq[ModuleDetailReport]): EvictionWarning = {
val directDependencies = module.moduleSettings match {
case x: InlineConfiguration => x.dependencies
case _ => Seq()
}
val pairs = reports map { detail =>
val evicteds = detail.modules filter { _.evicted }
val winner = (detail.modules filterNot { _.evicted }).headOption
val includesDirect: Boolean =
options.warnDirectEvictions &&
(directDependencies exists { dep =>
(detail.organization == dep.organization) && (detail.name == dep.name)
})
new EvictionPair(detail.organization, detail.name, winner, evicteds, includesDirect, options.showCallers)
}
val scalaEvictions: mutable.ListBuffer[EvictionPair] = mutable.ListBuffer()
val directEvictions: mutable.ListBuffer[EvictionPair] = mutable.ListBuffer()
val transitiveEvictions: mutable.ListBuffer[EvictionPair] = mutable.ListBuffer()
def guessCompatible(p: EvictionPair): Boolean =
p.evicteds forall { r =>
options.guessCompatible(r.module, p.winner map { _.module }, module.moduleSettings.ivyScala)
}
pairs foreach {
case p if isScalaArtifact(module, p.organization, p.name) =>
(module.moduleSettings.ivyScala, p.winner) match {
case (Some(s), Some(winner)) if ((s.scalaFullVersion != winner.module.revision) && options.warnScalaVersionEviction) =>
scalaEvictions += p
case _ =>
}
case p if p.includesDirect =>
if (!guessCompatible(p) && options.warnDirectEvictions) {
directEvictions += p
}
case p =>
if (!guessCompatible(p) && options.warnTransitiveEvictions) {
transitiveEvictions += p
}
}
new EvictionWarning(options, scalaEvictions.toList,
directEvictions.toList, transitiveEvictions.toList, pairs)
}
implicit val evictionWarningLines: ShowLines[EvictionWarning] = ShowLines { a: EvictionWarning =>
import ShowLines._
val out: mutable.ListBuffer[String] = mutable.ListBuffer()
if (!a.scalaEvictions.isEmpty) {
out += "Scala version was updated by one of library dependencies:"
out ++= (a.scalaEvictions flatMap { _.lines })
}
if (!a.directEvictions.isEmpty || !a.transitiveEvictions.isEmpty) {
out += "There may be incompatibilities among your library dependencies."
out += "Here are some of the libraries that were evicted:"
out ++= (a.directEvictions flatMap { _.lines })
out ++= (a.transitiveEvictions flatMap { _.lines })
}
if (!a.allEvictions.isEmpty && !a.reportedEvictions.isEmpty && !a.options.showCallers) {
out += "Run 'evicted' to see detailed eviction warnings"
}
out.toList
}
}

View File

@ -0,0 +1,51 @@
package sbt
import Path._, Configurations._
import java.io.File
import org.specs2._
import cross.CrossVersionUtil
trait BaseIvySpecification extends Specification {
def currentBase: File = new File(".")
def currentTarget: File = currentBase / "target" / "ivyhome"
def defaultModuleId: ModuleID = ModuleID("com.example", "foo", "0.1.0", Some("compile"))
lazy val ivySbt = new IvySbt(mkIvyConfiguration)
lazy val log = Logger.Null
def module(moduleId: ModuleID, deps: Seq[ModuleID], scalaFullVersion: Option[String]): IvySbt#Module = {
val ivyScala = scalaFullVersion map { fv =>
new IvyScala(
scalaFullVersion = fv,
scalaBinaryVersion = CrossVersionUtil.binaryScalaVersion(fv),
configurations = Nil,
checkExplicit = true,
filterImplicit = false,
overrideScalaVersion = false)
}
val moduleSetting: ModuleSettings = InlineConfiguration(
module = moduleId,
moduleInfo = ModuleInfo("foo"),
dependencies = deps,
configurations = Seq(Compile, Test, Runtime),
ivyScala = ivyScala)
new ivySbt.Module(moduleSetting)
}
def mkIvyConfiguration: IvyConfiguration = {
val paths = new IvyPaths(currentBase, Some(currentTarget))
val rs = Seq(DefaultMavenRepository)
val other = Nil
val moduleConfs = Seq(ModuleConfiguration("*", DefaultMavenRepository))
val off = false
val check = Nil
val resCacheDir = currentTarget / "resolution-cache"
val uo = UpdateOptions()
new InlineIvyConfiguration(paths, rs, other, moduleConfs, off, None, check, Some(resCacheDir), uo, log)
}
def ivyUpdate(module: IvySbt#Module) = {
// IO.delete(currentTarget)
val config = new UpdateConfiguration(None, false, UpdateLogging.Full)
IvyActions.update(module, config, log)
}
}

View File

@ -0,0 +1,224 @@
package sbt
import org.specs2._
class EvictionWarningSpec extends BaseIvySpecification {
def is = s2"""
This is a specification to check the eviction warnings
Eviction of scala-library whose scalaVersion should
be detected $scalaVersionWarn1
not be detected if it's diabled $scalaVersionWarn2
print out message about the eviction $scalaVersionWarn3
print out message about the eviction with callers $scalaVersionWarn4
Including two (suspect) binary incompatible Java libraries to
direct dependencies should
be detected as eviction $javaLibWarn1
not be detected if it's disabled $javaLibWarn2
print out message about the eviction $javaLibWarn3
print out message about the eviction with callers $javaLibWarn4
Including two (suspect) binary compatible Java libraries to
direct dependencies should
not be detected as eviction $javaLibNoWarn1
print out message about the eviction $javaLibNoWarn2
Including two (suspect) transitively binary incompatible Java libraries to
direct dependencies should
be not detected as eviction $javaLibTransitiveWarn1
be detected if it's enabled $javaLibTransitiveWarn2
print out message about the eviction if it's enabled $javaLibTransitiveWarn3
Including two (suspect) binary incompatible Scala libraries to
direct dependencies should
be detected as eviction $scalaLibWarn1
print out message about the eviction $scalaLibWarn2
Including two (suspect) binary compatible Scala libraries to
direct dependencies should
not be detected as eviction $scalaLibNoWarn1
print out message about the eviction $scalaLibNoWarn2
Including two (suspect) transitively binary incompatible Scala libraries to
direct dependencies should
be not detected as eviction $scalaLibTransitiveWarn1
be detected if it's enabled $scalaLibTransitiveWarn2
print out message about the eviction if it's enabled $scalaLibTransitiveWarn3
"""
def akkaActor214 = ModuleID("com.typesafe.akka", "akka-actor", "2.1.4", Some("compile")) cross CrossVersion.binary
def akkaActor230 = ModuleID("com.typesafe.akka", "akka-actor", "2.3.0", Some("compile")) cross CrossVersion.binary
def akkaActor234 = ModuleID("com.typesafe.akka", "akka-actor", "2.3.4", Some("compile")) cross CrossVersion.binary
def scala2102 = ModuleID("org.scala-lang", "scala-library", "2.10.2", Some("compile"))
def scala2103 = ModuleID("org.scala-lang", "scala-library", "2.10.3", Some("compile"))
def scala2104 = ModuleID("org.scala-lang", "scala-library", "2.10.4", Some("compile"))
def commonsIo13 = ModuleID("commons-io", "commons-io", "1.3", Some("compile"))
def commonsIo14 = ModuleID("commons-io", "commons-io", "1.4", Some("compile"))
def commonsIo24 = ModuleID("commons-io", "commons-io", "2.4", Some("compile"))
def bnfparser10 = ModuleID("ca.gobits.bnf", "bnfparser", "1.0", Some("compile")) // uses commons-io 2.4
def unfilteredUploads080 = ModuleID("net.databinder", "unfiltered-uploads", "0.8.0", Some("compile")) cross CrossVersion.binary // uses commons-io 1.4
def bananaSesame04 = ModuleID("org.w3", "banana-sesame", "0.4", Some("compile")) cross CrossVersion.binary // uses akka-actor 2.1.4
def akkaRemote234 = ModuleID("com.typesafe.akka", "akka-remote", "2.3.4", Some("compile")) cross CrossVersion.binary // uses akka-actor 2.3.4
def defaultOptions = EvictionWarningOptions.default
import ShowLines._
def scalaVersionDeps = Seq(scala2102, akkaActor230)
def scalaVersionWarn1 = {
val m = module(defaultModuleId, scalaVersionDeps, Some("2.10.2"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).scalaEvictions must have size (1)
}
def scalaVersionWarn2 = {
val m = module(defaultModuleId, scalaVersionDeps, Some("2.10.2"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions.withWarnScalaVersionEviction(false), report, log).scalaEvictions must have size (0)
}
def scalaVersionWarn3 = {
val m = module(defaultModuleId, scalaVersionDeps, Some("2.10.2"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).lines must_==
List("Scala version was updated by one of library dependencies:",
"\t* org.scala-lang:scala-library:2.10.2 -> 2.10.3",
"Run 'evicted' to see detailed eviction warnings")
}
def scalaVersionWarn4 = {
val m = module(defaultModuleId, scalaVersionDeps, Some("2.10.2"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions.withShowCallers(true), report, log).lines must_==
List("Scala version was updated by one of library dependencies:",
"\t* org.scala-lang:scala-library:2.10.2 -> 2.10.3 (caller: com.typesafe.akka:akka-actor_2.10:2.3.0, com.example:foo:0.1.0)")
}
def javaLibDirectDeps = Seq(commonsIo14, commonsIo24)
def javaLibWarn1 = {
val m = module(defaultModuleId, javaLibDirectDeps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (1)
}
def javaLibWarn2 = {
val m = module(defaultModuleId, javaLibDirectDeps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions.withWarnDirectEvictions(false), report, log).reportedEvictions must have size (0)
}
def javaLibWarn3 = {
val m = module(defaultModuleId, javaLibDirectDeps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).lines must_==
List("There may be incompatibilities among your library dependencies.",
"Here are some of the libraries that were evicted:",
"\t* commons-io:commons-io:1.4 -> 2.4",
"Run 'evicted' to see detailed eviction warnings")
}
def javaLibWarn4 = {
val m = module(defaultModuleId, javaLibDirectDeps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions.withShowCallers(true), report, log).lines must_==
List("There may be incompatibilities among your library dependencies.",
"Here are some of the libraries that were evicted:",
"\t* commons-io:commons-io:1.4 -> 2.4 (caller: com.example:foo:0.1.0)")
}
def javaLibNoWarn1 = {
val deps = Seq(commonsIo14, commonsIo13)
val m = module(defaultModuleId, deps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (0)
}
def javaLibNoWarn2 = {
val deps = Seq(commonsIo14, commonsIo13)
val m = module(defaultModuleId, deps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).lines must_== Nil
}
def javaLibTransitiveDeps = Seq(unfilteredUploads080, bnfparser10)
def javaLibTransitiveWarn1 = {
val m = module(defaultModuleId, javaLibTransitiveDeps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (0)
}
def javaLibTransitiveWarn2 = {
val m = module(defaultModuleId, javaLibTransitiveDeps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions.withWarnTransitiveEvictions(true), report, log).reportedEvictions must have size (1)
}
def javaLibTransitiveWarn3 = {
val m = module(defaultModuleId, javaLibTransitiveDeps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions.withWarnTransitiveEvictions(true).withShowCallers(true), report, log).lines must_==
List("There may be incompatibilities among your library dependencies.",
"Here are some of the libraries that were evicted:",
"\t* commons-io:commons-io:1.4 -> 2.4 (caller: ca.gobits.bnf:bnfparser:1.0, net.databinder:unfiltered-uploads_2.10:0.8.0)")
}
def scalaLibWarn1 = {
val deps = Seq(scala2104, akkaActor214, akkaActor234)
val m = module(defaultModuleId, deps, Some("2.10.4"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (1)
}
def scalaLibWarn2 = {
val deps = Seq(scala2104, akkaActor214, akkaActor234)
val m = module(defaultModuleId, deps, Some("2.10.4"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).lines must_==
List("There may be incompatibilities among your library dependencies.",
"Here are some of the libraries that were evicted:",
"\t* com.typesafe.akka:akka-actor_2.10:2.1.4 -> 2.3.4",
"Run 'evicted' to see detailed eviction warnings")
}
def scalaLibNoWarn1 = {
val deps = Seq(scala2104, akkaActor230, akkaActor234)
val m = module(defaultModuleId, deps, Some("2.10.4"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (0)
}
def scalaLibNoWarn2 = {
val deps = Seq(scala2104, akkaActor230, akkaActor234)
val m = module(defaultModuleId, deps, Some("2.10.4"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).lines must_== Nil
}
def scalaLibTransitiveDeps = Seq(scala2104, bananaSesame04, akkaRemote234)
def scalaLibTransitiveWarn1 = {
val m = module(defaultModuleId, scalaLibTransitiveDeps, Some("2.10.4"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (0)
}
def scalaLibTransitiveWarn2 = {
val m = module(defaultModuleId, scalaLibTransitiveDeps, Some("2.10.4"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions.withWarnTransitiveEvictions(true), report, log).reportedEvictions must have size (1)
}
def scalaLibTransitiveWarn3 = {
val m = module(defaultModuleId, scalaLibTransitiveDeps, Some("2.10.4"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions.withWarnTransitiveEvictions(true).withShowCallers(true), report, log).lines must_==
List("There may be incompatibilities among your library dependencies.",
"Here are some of the libraries that were evicted:",
"\t* com.typesafe.akka:akka-actor_2.10:2.1.4 -> 2.3.4 (caller: com.typesafe.akka:akka-remote_2.10:2.3.4, org.w3:banana-sesame_2.10:0.4, org.w3:banana-rdf_2.10:0.4)")
}
}