From dd008e4dc5e8e384928a7bbacaeedd91fcca4543 Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Tue, 15 Jan 2013 08:21:53 -0500 Subject: [PATCH] Warn and optionally error when multiple cross version suffixes for a module are detected. Fixes #639. --- ivy/src/main/scala/sbt/ConflictWarning.scala | 64 +++++++++++++++++-- ivy/src/main/scala/sbt/IvyActions.scala | 1 - .../cross-conflict/build.sbt | 4 ++ .../cross-conflict/changes/conflict-error.sbt | 1 + .../dependency-management/cross-conflict/test | 5 ++ 5 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 sbt/src/sbt-test/dependency-management/cross-conflict/build.sbt create mode 100644 sbt/src/sbt-test/dependency-management/cross-conflict/changes/conflict-error.sbt create mode 100644 sbt/src/sbt-test/dependency-management/cross-conflict/test diff --git a/ivy/src/main/scala/sbt/ConflictWarning.scala b/ivy/src/main/scala/sbt/ConflictWarning.scala index f918990e1..8e0b07f7d 100644 --- a/ivy/src/main/scala/sbt/ConflictWarning.scala +++ b/ivy/src/main/scala/sbt/ConflictWarning.scala @@ -8,24 +8,80 @@ object ConflictWarning def disable: ConflictWarning = ConflictWarning("", (_: ModuleID) => false, org, Level.Warn, false) private[this] def org = (_: ModuleID).organization + private[this] def idString(org: String, name: String) = s"$org:$name" def default(label: String): ConflictWarning = ConflictWarning(label, moduleFilter(organization = GlobFilter(SbtArtifacts.Organization) | GlobFilter(ScalaArtifacts.Organization)), org, Level.Warn, false) - def strict(label: String): ConflictWarning = ConflictWarning(label, (id: ModuleID) => true, (id: ModuleID) => id.organization + ":" + id.name, Level.Error, true) + def strict(label: String): ConflictWarning = ConflictWarning(label, (id: ModuleID) => true, (id: ModuleID) => idString(id.organization, id.name), Level.Error, true) def apply(config: ConflictWarning, report: UpdateReport, log: Logger) + { + processEvicted(config, report, log) + processCrossVersioned(config, report, log) + } + private[this] def processEvicted(config: ConflictWarning, report: UpdateReport, log: Logger) { val conflicts = IvyActions.groupedConflicts(config.filter, config.group)(report) if(!conflicts.isEmpty) { val prefix = if(config.failOnConflict) "Incompatible" else "Potentially incompatible" - val msg = prefix + " versions of dependencies of " + config.label + ":\n " + val msg = s"$prefix versions of dependencies of ${config.label}:\n " val conflictMsgs = for( (label, versions) <- conflicts ) yield label + ": " + versions.mkString(", ") log.log(config.level, conflictMsgs.mkString(msg, "\n ", "")) + + if(config.failOnConflict) + error("Conflicts in " + conflicts.map(_._1).mkString(", ") ) } - if(config.failOnConflict && !conflicts.isEmpty) - error("Conflicts in " + conflicts.map(_._1).mkString(", ") ) } + + private[this] def processCrossVersioned(config: ConflictWarning, report: UpdateReport, log: Logger) + { + val crossMismatches = crossVersionMismatches(report) + if(!crossMismatches.isEmpty) + { + val pre = "Modules were resolved with conflicting cross-version suffixes:\n " + val conflictMsgs = + for( ((org,rawName), fullNames) <- crossMismatches ) yield + { + val suffixes = fullNames.map(n => "_" + getCrossSuffix(n)).mkString(", ") + s"${idString(org,rawName)} $suffixes" + } + log.log(config.level, conflictMsgs.mkString(pre, "\n ", "")) + if(config.failOnConflict) { + val summary = crossMismatches.map{ case ((org,raw),_) => idString(org,raw)}.mkString(", ") + error("Conflicting cross-version suffixes in: " + summary) + } + } + } + + /** Map from (organization, rawName) to set of multiple full names. */ + def crossVersionMismatches(report: UpdateReport): Map[(String,String), Set[String]] = + { + val mismatches = report.configurations.flatMap { confReport => + groupByRawName(confReport.allModules).mapValues { modules => + val differentFullNames = modules.map(_.name).toSet + if(differentFullNames.size > 1) differentFullNames else Set.empty[String] + } + } + (Map.empty[(String,String),Set[String]] /: mismatches)(merge) + } + private[this] def merge[A,B](m: Map[A, Set[B]], b: (A, Set[B])): Map[A, Set[B]] = + if(b._2.isEmpty) m else + m.updated(b._1, m.getOrElse(b._1, Set.empty) ++ b._2) + + private[this] def groupByRawName(ms: Seq[ModuleID]): Map[(String,String), Seq[ModuleID]] = + ms.groupBy(m => (m.organization, dropCrossSuffix(m.name))) + + private[this] val CrossSuffixPattern = """(.+)_(\d+\.\d+(?:\.\d+)?(?:-.+)?)""".r + private[this] def dropCrossSuffix(s: String): String = s match { + case CrossSuffixPattern(raw, _) => raw + case _ => s + } + private[this] def getCrossSuffix(s: String): String = s match { + case CrossSuffixPattern(_, v) => v + case _ => "" + } + } diff --git a/ivy/src/main/scala/sbt/IvyActions.scala b/ivy/src/main/scala/sbt/IvyActions.scala index 66a00f615..b139d7989 100644 --- a/ivy/src/main/scala/sbt/IvyActions.scala +++ b/ivy/src/main/scala/sbt/IvyActions.scala @@ -157,7 +157,6 @@ object IvyActions def grouped[T](grouping: ModuleID => T)(mods: Seq[ModuleID]): Map[T, Set[String]] = mods groupBy(grouping) mapValues(_.map(_.revision).toSet) - def transitiveScratch(ivySbt: IvySbt, label: String, config: GetClassifiersConfiguration, log: Logger): UpdateReport = { import config.{configuration => c, ivyScala, module => mod} diff --git a/sbt/src/sbt-test/dependency-management/cross-conflict/build.sbt b/sbt/src/sbt-test/dependency-management/cross-conflict/build.sbt new file mode 100644 index 000000000..097d7674c --- /dev/null +++ b/sbt/src/sbt-test/dependency-management/cross-conflict/build.sbt @@ -0,0 +1,4 @@ +libraryDependencies ++= Seq( + "org.scalatest" %% "scalatest" % "1.9.1" % "test", + "org.scalamock" %% "scalamock-scalatest-support" % "3.0" % "test" +) diff --git a/sbt/src/sbt-test/dependency-management/cross-conflict/changes/conflict-error.sbt b/sbt/src/sbt-test/dependency-management/cross-conflict/changes/conflict-error.sbt new file mode 100644 index 000000000..189407454 --- /dev/null +++ b/sbt/src/sbt-test/dependency-management/cross-conflict/changes/conflict-error.sbt @@ -0,0 +1 @@ +conflictWarning ~= { _.copy(failOnConflict = true) } diff --git a/sbt/src/sbt-test/dependency-management/cross-conflict/test b/sbt/src/sbt-test/dependency-management/cross-conflict/test new file mode 100644 index 000000000..6b7a1b69a --- /dev/null +++ b/sbt/src/sbt-test/dependency-management/cross-conflict/test @@ -0,0 +1,5 @@ +> update +$ copy-file changes/conflict-error.sbt conflict-error.sbt + +> reload +-> update