From 5e6f0c4ff19a04fbf437fa0b99e6f537aa7682cf Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 18 Jul 2014 04:50:23 -0400 Subject: [PATCH 1/7] Expand ModuleReport into the full Ivy resolution report. #1200 Currently sbt's update task generates UpdateReport from Ivy's resolution report. For each configuration there's ConfigurationReport, which contains just enough information on the resolved module/revision/artifact. Speaking of module, in Ivy module means organization and name, and organization, name, and version is called module revision. In sbt, module revision is called Module. This is relevant because to talk about evictions, we need a terminology for organization and name combo. In any case ConfigurationReport is expanded to have `details` field, which contains Seq[ModuleDetailReport], which represents organization and name combo plus all the modules just like Ivy's resolution report XML. Furthermore, ModuleReport is expanded to include licenses, eviction, callers, etc. --- ivy/src/main/scala/sbt/ConflictWarning.scala | 5 + ivy/src/main/scala/sbt/IvyRetrieve.scala | 108 ++++++++++++++-- ivy/src/main/scala/sbt/UpdateReport.scala | 124 ++++++++++++++++--- 3 files changed, 206 insertions(+), 31 deletions(-) diff --git a/ivy/src/main/scala/sbt/ConflictWarning.scala b/ivy/src/main/scala/sbt/ConflictWarning.scala index 7c5c9a918..6e6b8a37d 100644 --- a/ivy/src/main/scala/sbt/ConflictWarning.scala +++ b/ivy/src/main/scala/sbt/ConflictWarning.scala @@ -2,6 +2,11 @@ package sbt import DependencyFilter._ +/** + * Provide warnings for cross version conflicts. + * A library foo_2.10 and foo_2.11 can potentially be both included on the + * library dependency graph by mistake, but it won't be caught by eviction. + */ final case class ConflictWarning(label: String, level: Level.Value, failOnConflict: Boolean) { @deprecated("`filter` is no longer used", "0.13.0") val filter: ModuleFilter = (_: ModuleID) => false diff --git a/ivy/src/main/scala/sbt/IvyRetrieve.scala b/ivy/src/main/scala/sbt/IvyRetrieve.scala index e863d6723..5998f2a22 100644 --- a/ivy/src/main/scala/sbt/IvyRetrieve.scala +++ b/ivy/src/main/scala/sbt/IvyRetrieve.scala @@ -4,35 +4,121 @@ package sbt import java.io.File +import java.{ util => ju } import collection.mutable +import java.net.URL import org.apache.ivy.core.{ module, report, resolve } -import module.descriptor.{ Artifact => IvyArtifact } -import module.id.ModuleRevisionId -import resolve.IvyNode +import module.descriptor.{ Artifact => IvyArtifact, License => IvyLicense } +import module.id.{ ModuleRevisionId, ModuleId => IvyModuleId } import report.{ ArtifactDownloadReport, ConfigurationResolveReport, ResolveReport } +import resolve.{ IvyNode, IvyNodeCallers } +import IvyNodeCallers.{ Caller => IvyCaller } object IvyRetrieve { def reports(report: ResolveReport): Seq[ConfigurationResolveReport] = report.getConfigurations map report.getConfigurationReport def moduleReports(confReport: ConfigurationResolveReport): Seq[ModuleReport] = - for (revId <- confReport.getModuleRevisionIds.toArray collect { case revId: ModuleRevisionId => revId }) yield artifactReports(toModuleID(revId), confReport getDownloadReports revId) + for { + revId <- confReport.getModuleRevisionIds.toArray.toVector collect { case revId: ModuleRevisionId => revId } + } yield moduleRevisionDetail(confReport, confReport.getDependency(revId)) + @deprecated("Internal only. No longer in use.", "0.13.6") def artifactReports(mid: ModuleID, artReport: Seq[ArtifactDownloadReport]): ModuleReport = + { + val (resolved, missing) = artifacts(mid, artReport) + ModuleReport(mid, resolved, missing) + } + + private[sbt] def artifacts(mid: ModuleID, artReport: Seq[ArtifactDownloadReport]): (Seq[(Artifact, File)], Seq[Artifact]) = { val missing = new mutable.ListBuffer[Artifact] val resolved = new mutable.ListBuffer[(Artifact, File)] for (r <- artReport) { - val file = r.getLocalFile + val fileOpt = Option(r.getLocalFile) val art = toArtifact(r.getArtifact) - if (file eq null) - missing += art - else - resolved += ((art, file)) + fileOpt match { + case Some(file) => resolved += ((art, file)) + case None => missing += art + } } - new ModuleReport(mid, resolved.toSeq, missing.toSeq) + (resolved.toSeq, missing.toSeq) } + // We need this because current module report used as part of UpdateReport/ConfigurationReport contains + // only the revolved modules. + // Sometimes the entire module can be excluded via rules etc. + private[sbt] def details(confReport: ConfigurationResolveReport): Seq[ModuleDetailReport] = { + val dependencies = confReport.getModuleRevisionIds.toArray.toVector collect { case revId: ModuleRevisionId => revId } + val moduleIds = confReport.getModuleIds.toArray.toVector collect { case mId: IvyModuleId => mId } + def moduleDetail(mid: IvyModuleId): ModuleDetailReport = { + val deps = confReport.getNodes(mid).toArray.toVector collect { case node: IvyNode => node } + new ModuleDetailReport(mid.getOrganisation, mid.getName, deps map { moduleRevisionDetail(confReport, _) }) + } + moduleIds map { moduleDetail } + } + + private[sbt] def moduleRevisionDetail(confReport: ConfigurationResolveReport, dep: IvyNode): ModuleReport = { + def toExtraAttributes(ea: ju.Map[_, _]): Map[String, String] = + Map(ea.entrySet.toArray collect { + case entry: ju.Map.Entry[_, _] => (entry.getKey.toString, entry.getValue.toString) + }: _*) + def toCaller(caller: IvyCaller): Caller = { + val m = toModuleID(caller.getModuleRevisionId) + val callerConfigurations = caller.getCallerConfigurations.toArray.toVector + val extraAttributes = toExtraAttributes(caller.getDependencyDescriptor.getExtraAttributes) + new Caller(m, callerConfigurations, extraAttributes) + } + val revId = dep.getResolvedId + val moduleId = toModuleID(revId) + val branch = Some(revId.getBranch) + val (status, publicationDate, resolver, artifactResolver) = dep.isLoaded match { + case true => + (Some(dep.getDescriptor.getStatus), + Some(new ju.Date(dep.getPublication)), + Some(dep.getModuleRevision.getResolver.getName), + Some(dep.getModuleRevision.getArtifactResolver.getName)) + case _ => (None, None, None, None) + } + val (evicted, evictedData, evictedReason) = dep.isEvicted(confReport.getConfiguration) match { + case true => + val ed = dep.getEvictedData(confReport.getConfiguration) + (true, + Some(Option(ed.getConflictManager) map { _.toString } getOrElse { "transitive" }), + Option(ed.getDetail)) + case _ => (false, None, None) + } + val problem = dep.hasProblem match { + case true => Some(dep.getProblem.getMessage) + case _ => None + } + val mdOpt = for { + mr <- Option(dep.getModuleRevision) + md <- Option(mr.getDescriptor) + } yield md + val homepage = mdOpt match { + case Some(md) => + Option(md.getHomePage) + case _ => None + } + val extraAttributes: Map[String, String] = toExtraAttributes(mdOpt match { + case Some(md) => md.getExtraAttributes + case _ => dep.getResolvedId.getExtraAttributes + }) + val isDefault = Option(dep.getDescriptor) map { _.isDefault } + val configurations = dep.getConfigurations(confReport.getConfiguration).toArray.toList + val licenses: Seq[(String, URL)] = mdOpt match { + case Some(md) => md.getLicenses.toArray.toVector collect { case lic: IvyLicense => (lic.getName, new URL(lic.getUrl)) } + case _ => Nil + } + val callers = dep.getCallers(confReport.getConfiguration).toArray.toVector map { toCaller } + val (resolved, missing) = artifacts(moduleId, confReport getDownloadReports revId) + + new ModuleReport(moduleId, resolved, missing, status, publicationDate, resolver, artifactResolver, + evicted, evictedData, evictedReason, problem, homepage, extraAttributes, isDefault, branch, + configurations, licenses, callers) + } + def evicted(confReport: ConfigurationResolveReport): Seq[ModuleID] = confReport.getEvictedNodes.map(node => toModuleID(node.getId)) @@ -50,7 +136,7 @@ object IvyRetrieve { def updateStats(report: ResolveReport): UpdateStats = new UpdateStats(report.getResolveTime, report.getDownloadTime, report.getDownloadSize, false) def configurationReport(confReport: ConfigurationResolveReport): ConfigurationReport = - new ConfigurationReport(confReport.getConfiguration, moduleReports(confReport), evicted(confReport)) + new ConfigurationReport(confReport.getConfiguration, moduleReports(confReport), details(confReport), evicted(confReport)) /** * Tries to find Ivy graph path the from node to target. diff --git a/ivy/src/main/scala/sbt/UpdateReport.scala b/ivy/src/main/scala/sbt/UpdateReport.scala index 71d58390b..39ad7357b 100644 --- a/ivy/src/main/scala/sbt/UpdateReport.scala +++ b/ivy/src/main/scala/sbt/UpdateReport.scala @@ -4,6 +4,8 @@ package sbt import java.io.File +import java.net.URL +import java.{ util => ju } /** * Provides information about dependency resolution. @@ -37,9 +39,18 @@ final class UpdateReport(val cachedDescriptor: File, val configurations: Seq[Con /** * Provides information about resolution of a single configuration. * @param configuration the configuration this report is for. - * @param modules a seqeuence containing one report for each module resolved for this configuration. + * @param modules a sequence containing one report for each module resolved for this configuration. + * @param details a sequence containing one report for each org/name, which may or may not be part of the final resolution. + * @param evicted a sequence of evicted modules */ -final class ConfigurationReport(val configuration: String, val modules: Seq[ModuleReport], val evicted: Seq[ModuleID]) { +final class ConfigurationReport( + val configuration: String, + val modules: Seq[ModuleReport], + val details: Seq[ModuleDetailReport], + val evicted: Seq[ModuleID]) { + // def this(configuration: String, modules: Seq[ModuleReport], evicted: Seq[ModuleID]) = + // this(configuration, modules, Nil, evicted) + override def toString = "\t" + configuration + ":\n" + modules.mkString + evicted.map("\t\t(EVICTED) " + _ + "\n").mkString /** @@ -50,25 +61,94 @@ final class ConfigurationReport(val configuration: String, val modules: Seq[Modu private[this] def addConfiguration(mod: ModuleID): ModuleID = if (mod.configurations.isEmpty) mod.copy(configurations = Some(configuration)) else mod def retrieve(f: (String, ModuleID, Artifact, File) => File): ConfigurationReport = - new ConfigurationReport(configuration, modules map { _.retrieve((mid, art, file) => f(configuration, mid, art, file)) }, evicted) + new ConfigurationReport(configuration, modules map { _.retrieve((mid, art, file) => f(configuration, mid, art, file)) }, details, evicted) +} + +/** + * In sbt's terminology, "module" consists of organization, name, and version. + * In Ivy's, "module" means just organization and name, and the one including version numbers + * are called revisions. + */ +final class ModuleDetailReport( + val organization: String, + val name: String, + val modules: Seq[ModuleReport]) { + override def toString: String = + { s"$organization:$name" } } /** * Provides information about the resolution of a module. * This information is in the context of a specific configuration. * @param module the `ModuleID` this report is for. - * @param artifacts the resolved artifacts for this module, paired with the File the artifact was retrieved to. This File may be in the + * @param artifacts the resolved artifacts for this module, paired with the File the artifact was retrieved to. + * @param missingArtifacts the missing artifacts for this module. */ -final class ModuleReport(val module: ModuleID, val artifacts: Seq[(Artifact, File)], val missingArtifacts: Seq[Artifact]) { - override def toString = - { - val arts = artifacts.map(_.toString) ++ missingArtifacts.map(art => "(MISSING) " + art) - "\t\t" + module + ": " + - (if (arts.size <= 1) "" else "\n\t\t\t") + arts.mkString("\n\t\t\t") + "\n" - } +final class ModuleReport( + val module: ModuleID, + val artifacts: Seq[(Artifact, File)], + val missingArtifacts: Seq[Artifact], + val status: Option[String], + val publicationDate: Option[ju.Date], + val resolver: Option[String], + val artifactResolver: Option[String], + val evicted: Boolean, + val evictedData: Option[String], + val evictedReason: Option[String], + val problem: Option[String], + val homepage: Option[String], + val extraAttributes: Map[String, String], + val isDefault: Option[Boolean], + val branch: Option[String], + val configurations: Seq[String], + val licenses: Seq[(String, URL)], + val callers: Seq[Caller]) { + + override def toString = { + val arts = artifacts.map(_.toString) ++ missingArtifacts.map(art => "(MISSING) " + art) + s"\t\t$module: " + + (if (arts.size <= 1) "" else "\n\t\t\t") + arts.mkString("\n\t\t\t") + "\n" + } def retrieve(f: (ModuleID, Artifact, File) => File): ModuleReport = - new ModuleReport(module, artifacts.map { case (art, file) => (art, f(module, art, file)) }, missingArtifacts) + copy(artifacts = artifacts.map { case (art, file) => (art, f(module, art, file)) }) + + private[sbt] def copy( + module: ModuleID = module, + artifacts: Seq[(Artifact, File)] = artifacts, + missingArtifacts: Seq[Artifact] = missingArtifacts, + status: Option[String] = status, + publicationDate: Option[ju.Date] = publicationDate, + resolver: Option[String] = resolver, + artifactResolver: Option[String] = artifactResolver, + evicted: Boolean = evicted, + evictedData: Option[String] = evictedData, + evictedReason: Option[String] = evictedReason, + problem: Option[String] = problem, + homepage: Option[String] = homepage, + extraAttributes: Map[String, String] = extraAttributes, + isDefault: Option[Boolean] = isDefault, + branch: Option[String] = branch, + configurations: Seq[String] = configurations, + licenses: Seq[(String, URL)] = licenses, + callers: Seq[Caller] = callers): ModuleReport = + new ModuleReport(module, artifacts, missingArtifacts, status, publicationDate, resolver, artifactResolver, + evicted, evictedData, evictedReason, problem, homepage, extraAttributes, isDefault, branch, configurations, licenses, callers) } + +object ModuleReport { + def apply(module: ModuleID, artifacts: Seq[(Artifact, File)], missingArtifacts: Seq[Artifact]): ModuleReport = + new ModuleReport(module, artifacts, missingArtifacts, None, None, None, None, + false, None, None, None, None, Map(), None, None, Nil, Nil, Nil) +} + +final class Caller( + val caller: ModuleID, + val callerConfigurations: Seq[String], + val callerExtraAttributes: Map[String, String]) { + override def toString: String = + s"$caller" +} + object UpdateReport { implicit def richUpdateReport(report: UpdateReport): RichUpdateReport = new RichUpdateReport(report) @@ -101,15 +181,18 @@ object UpdateReport { /** Constructs a new report that only contains files matching the specified filter.*/ def filter(f: DependencyFilter): UpdateReport = moduleReportMap { (configuration, modReport) => - import modReport._ - val newArtifacts = artifacts filter { case (art, file) => f(configuration, module, art) } - val newMissing = missingArtifacts filter { art => f(configuration, module, art) } - new ModuleReport(module, newArtifacts, newMissing) + modReport.copy( + artifacts = modReport.artifacts filter { case (art, file) => f(configuration, modReport.module, art) }, + missingArtifacts = modReport.missingArtifacts filter { art => f(configuration, modReport.module, art) } + ) } def substitute(f: (String, ModuleID, Seq[(Artifact, File)]) => Seq[(Artifact, File)]): UpdateReport = moduleReportMap { (configuration, modReport) => val newArtifacts = f(configuration, modReport.module, modReport.artifacts) - new ModuleReport(modReport.module, newArtifacts, Nil) + modReport.copy( + artifacts = f(configuration, modReport.module, modReport.artifacts), + missingArtifacts = Nil + ) } def toSeq: Seq[(String, ModuleID, Artifact, File)] = @@ -120,8 +203,9 @@ object UpdateReport { def addMissing(f: ModuleID => Seq[Artifact]): UpdateReport = moduleReportMap { (configuration, modReport) => - import modReport._ - new ModuleReport(module, artifacts, (missingArtifacts ++ f(module)).distinct) + modReport.copy( + missingArtifacts = (modReport.missingArtifacts ++ f(modReport.module)).distinct + ) } def moduleReportMap(f: (String, ModuleReport) => ModuleReport): UpdateReport = @@ -129,7 +213,7 @@ object UpdateReport { val newConfigurations = report.configurations.map { confReport => import confReport._ val newModules = modules map { modReport => f(configuration, modReport) } - new ConfigurationReport(configuration, newModules, evicted) + new ConfigurationReport(configuration, newModules, details, evicted) } new UpdateReport(report.cachedDescriptor, newConfigurations, report.stats, report.stamps) } From 05b97b400744b0a7c1b309ea5e3121485c26f7ea Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 18 Jul 2014 06:17:37 -0400 Subject: [PATCH 2/7] Implemented general eviction warning. #1200 a> update [info] Updating {file:/foo/}a... [info] Resolving org.fusesource.jansi#jansi;1.4 ... [info] Done updating. [warn] Some dependencies were evicted: [warn] * org.scala-lang:scala-library (2.10.1) -> 2.10.3 --- ivy/src/main/scala/sbt/EvictionWarning.scala | 44 ++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 ivy/src/main/scala/sbt/EvictionWarning.scala diff --git a/ivy/src/main/scala/sbt/EvictionWarning.scala b/ivy/src/main/scala/sbt/EvictionWarning.scala new file mode 100644 index 000000000..4d3baec98 --- /dev/null +++ b/ivy/src/main/scala/sbt/EvictionWarning.scala @@ -0,0 +1,44 @@ +package sbt + +import collection.mutable + +final class EvictionWarningOptions( + val configurations: Seq[String], + val level: Level.Value) { +} +object EvictionWarningOptions { + def apply(): EvictionWarningOptions = + new EvictionWarningOptions(Vector("compile"), Level.Warn) +} + +object EvictionWarning { + def apply(options: EvictionWarningOptions, report: UpdateReport, log: Logger): Unit = { + val evictions = buildEvictions(options, report) + processEvictions(evictions, log) + } + + 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 } + confs flatMap { confReport => + confReport.details map { detail => + if ((detail.modules exists { _.evicted }) && + !(buffer exists { x => (x.organization == detail.organization) && (x.name == detail.name) })) { + buffer += detail + } + } + } + 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") + } + } + } +} From 71e7e12b9945fe532c14b3a899c17ecbda9be8e6 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 18 Jul 2014 16:27:11 -0400 Subject: [PATCH 3/7] Fixing NullPointerError caused by reading String from Ivy directly --- ivy/src/main/scala/sbt/IvyRetrieve.scala | 10 ++--- ivy/src/main/scala/sbt/UpdateReport.scala | 47 +++++++++++++++++++---- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/ivy/src/main/scala/sbt/IvyRetrieve.scala b/ivy/src/main/scala/sbt/IvyRetrieve.scala index 5998f2a22..e9dba98ee 100644 --- a/ivy/src/main/scala/sbt/IvyRetrieve.scala +++ b/ivy/src/main/scala/sbt/IvyRetrieve.scala @@ -71,13 +71,13 @@ object IvyRetrieve { } val revId = dep.getResolvedId val moduleId = toModuleID(revId) - val branch = Some(revId.getBranch) + val branch = Option(revId.getBranch) val (status, publicationDate, resolver, artifactResolver) = dep.isLoaded match { case true => - (Some(dep.getDescriptor.getStatus), + (Option(dep.getDescriptor.getStatus), Some(new ju.Date(dep.getPublication)), - Some(dep.getModuleRevision.getResolver.getName), - Some(dep.getModuleRevision.getArtifactResolver.getName)) + Option(dep.getModuleRevision.getResolver.getName), + Option(dep.getModuleRevision.getArtifactResolver.getName)) case _ => (None, None, None, None) } val (evicted, evictedData, evictedReason) = dep.isEvicted(confReport.getConfiguration) match { @@ -89,7 +89,7 @@ object IvyRetrieve { case _ => (false, None, None) } val problem = dep.hasProblem match { - case true => Some(dep.getProblem.getMessage) + case true => Option(dep.getProblem.getMessage) case _ => None } val mdOpt = for { diff --git a/ivy/src/main/scala/sbt/UpdateReport.scala b/ivy/src/main/scala/sbt/UpdateReport.scala index 39ad7357b..f8c2804d5 100644 --- a/ivy/src/main/scala/sbt/UpdateReport.scala +++ b/ivy/src/main/scala/sbt/UpdateReport.scala @@ -48,10 +48,12 @@ final class ConfigurationReport( val modules: Seq[ModuleReport], val details: Seq[ModuleDetailReport], val evicted: Seq[ModuleID]) { - // def this(configuration: String, modules: Seq[ModuleReport], evicted: Seq[ModuleID]) = - // this(configuration, modules, Nil, evicted) + def this(configuration: String, modules: Seq[ModuleReport], evicted: Seq[ModuleID]) = + this(configuration, modules, Nil, evicted) - override def toString = "\t" + configuration + ":\n" + modules.mkString + evicted.map("\t\t(EVICTED) " + _ + "\n").mkString + override def toString = s"\t$configuration:\n" + + (if (details.isEmpty) modules.mkString + evicted.map("\t\t(EVICTED) " + _ + "\n").mkString + else details.mkString) /** * All resolved modules for this configuration. @@ -73,8 +75,10 @@ final class ModuleDetailReport( val organization: String, val name: String, val modules: Seq[ModuleReport]) { - override def toString: String = - { s"$organization:$name" } + override def toString: String = { + val details = modules map { _.detailReport } + s"\t$organization:$name\n${details.mkString}\n" + } } /** @@ -104,11 +108,40 @@ final class ModuleReport( val licenses: Seq[(String, URL)], val callers: Seq[Caller]) { - override def toString = { - val arts = artifacts.map(_.toString) ++ missingArtifacts.map(art => "(MISSING) " + art) + private[this] lazy val arts: Seq[String] = artifacts.map(_.toString) ++ missingArtifacts.map(art => "(MISSING) " + art) + override def toString: String = { s"\t\t$module: " + (if (arts.size <= 1) "" else "\n\t\t\t") + arts.mkString("\n\t\t\t") + "\n" } + private[sbt] def detailReport: String = + s"\t\t- ${module.revision}\n" + + (if (arts.size <= 1) "" else arts.mkString("\t\t\t", "\n\t\t\t", "\n")) + + reportStr("status", status) + + reportStr("publicationDate", publicationDate map { _.toString }) + + reportStr("resolver", resolver) + + reportStr("artifactResolver", artifactResolver) + + reportStr("evicted", Some(evicted.toString)) + + reportStr("evictedData", evictedData) + + reportStr("evictedReason", evictedReason) + + reportStr("problem", problem) + + reportStr("homepage", homepage) + + reportStr("textraAttributes", + if (extraAttributes.isEmpty) None + else { Some(extraAttributes.toString) }) + + reportStr("isDefault", isDefault map { _.toString }) + + reportStr("branch", branch) + + reportStr("configurations", + if (configurations.isEmpty) None + else { Some(configurations.mkString(", ")) }) + + reportStr("licenses", + if (licenses.isEmpty) None + else { Some(licenses.mkString(", ")) }) + + reportStr("callers", + if (callers.isEmpty) None + else { Some(callers.mkString(", ")) }) + private[sbt] def reportStr(key: String, value: Option[String]): String = + value map { x => s"\t\t\t$key: $x\n" } getOrElse "" + def retrieve(f: (ModuleID, Artifact, File) => File): ModuleReport = copy(artifacts = artifacts.map { case (art, file) => (art, f(module, art, file)) }) From 3fe8f41a69ecae8e39a4f04f11cccd8db29a9a78 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 27 Jul 2014 12:01:11 -0400 Subject: [PATCH 4/7] Fixing more NullPointerError by wrapping license URL with Option --- ivy/src/main/scala/sbt/IvyRetrieve.scala | 9 ++++++--- ivy/src/main/scala/sbt/UpdateReport.scala | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ivy/src/main/scala/sbt/IvyRetrieve.scala b/ivy/src/main/scala/sbt/IvyRetrieve.scala index e9dba98ee..b3bf04563 100644 --- a/ivy/src/main/scala/sbt/IvyRetrieve.scala +++ b/ivy/src/main/scala/sbt/IvyRetrieve.scala @@ -107,9 +107,12 @@ object IvyRetrieve { }) val isDefault = Option(dep.getDescriptor) map { _.isDefault } val configurations = dep.getConfigurations(confReport.getConfiguration).toArray.toList - val licenses: Seq[(String, URL)] = mdOpt match { - case Some(md) => md.getLicenses.toArray.toVector collect { case lic: IvyLicense => (lic.getName, new URL(lic.getUrl)) } - case _ => Nil + val licenses: Seq[(String, Option[String])] = mdOpt match { + case Some(md) => md.getLicenses.toArray.toVector collect { + case lic: IvyLicense => + (lic.getName, Option(lic.getUrl)) + } + case _ => Nil } val callers = dep.getCallers(confReport.getConfiguration).toArray.toVector map { toCaller } val (resolved, missing) = artifacts(moduleId, confReport getDownloadReports revId) diff --git a/ivy/src/main/scala/sbt/UpdateReport.scala b/ivy/src/main/scala/sbt/UpdateReport.scala index f8c2804d5..41ea4fd80 100644 --- a/ivy/src/main/scala/sbt/UpdateReport.scala +++ b/ivy/src/main/scala/sbt/UpdateReport.scala @@ -105,7 +105,7 @@ final class ModuleReport( val isDefault: Option[Boolean], val branch: Option[String], val configurations: Seq[String], - val licenses: Seq[(String, URL)], + val licenses: Seq[(String, Option[String])], val callers: Seq[Caller]) { private[this] lazy val arts: Seq[String] = artifacts.map(_.toString) ++ missingArtifacts.map(art => "(MISSING) " + art) @@ -162,7 +162,7 @@ final class ModuleReport( isDefault: Option[Boolean] = isDefault, branch: Option[String] = branch, configurations: Seq[String] = configurations, - licenses: Seq[(String, URL)] = licenses, + licenses: Seq[(String, Option[String])] = licenses, callers: Seq[Caller] = callers): ModuleReport = new ModuleReport(module, artifacts, missingArtifacts, status, publicationDate, resolver, artifactResolver, evicted, evictedData, evictedReason, problem, homepage, extraAttributes, isDefault, branch, configurations, licenses, callers) From 439f8498b67abc21e1e2adad9c0550e57d65e74c Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 27 Jul 2014 12:14:30 -0400 Subject: [PATCH 5/7] pseudo-case class VersionNumber VersionNumber is a pseudo-case class that represents any form of version number. The unapply extractor can parse String into three sequences that makes up VersionNumber. VersionNumberCompatibility trait uses two VersionNumber instances to evaluate binary compatibility between them. Two implementations SemVer and SecondSegment are provided. --- ivy/src/main/scala/sbt/VersionNumber.scala | 147 +++++++++++++++++++++ ivy/src/test/scala/VersionNumberSpec.scala | 122 +++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 ivy/src/main/scala/sbt/VersionNumber.scala create mode 100644 ivy/src/test/scala/VersionNumberSpec.scala diff --git a/ivy/src/main/scala/sbt/VersionNumber.scala b/ivy/src/main/scala/sbt/VersionNumber.scala new file mode 100644 index 000000000..7db8c8fef --- /dev/null +++ b/ivy/src/main/scala/sbt/VersionNumber.scala @@ -0,0 +1,147 @@ +package sbt + +final class VersionNumber private[sbt] ( + val numbers: Seq[Long], + val tags: Seq[String], + val extras: Seq[String]) { + def _1: Option[Long] = get(0) + def _2: Option[Long] = get(1) + def _3: Option[Long] = get(2) + def _4: Option[Long] = get(3) + def get(idx: Int): Option[Long] = + if (size <= idx) None + else Some(numbers(idx)) + def size: Int = numbers.size + + private[this] val versionStr: String = + numbers.mkString(".") + + (tags match { + case Seq() => "" + case ts => "-" + ts.mkString("-") + }) + + extras.mkString("") + override def toString: String = versionStr + override def hashCode: Int = + numbers.hashCode * 41 * 41 + + tags.hashCode * 41 + + extras.hashCode + override def equals(o: Any): Boolean = + o match { + case v: VersionNumber => (this.numbers == v.numbers) && (this.tags == v.tags) && (this.extras == v.extras) + case _ => false + } +} + +object VersionNumber { + /** + * @param numbers numbers delimited by a dot. + * @param tags string prefixed by a dash. + * @param any other strings at the end. + */ + def apply(numbers: Seq[Long], tags: Seq[String], extras: Seq[String]): VersionNumber = + new VersionNumber(numbers, tags, extras) + def apply(v: String): VersionNumber = + unapply(v) match { + case Some((ns, ts, es)) => VersionNumber(ns, ts, es) + case _ => sys.error(s"Invalid version number: $v") + } + + def unapply(v: VersionNumber): Option[(Seq[Long], Seq[String], Seq[String])] = + Some((v.numbers, v.tags, v.extras)) + + def unapply(v: String): Option[(Seq[Long], Seq[String], Seq[String])] = { + def splitDot(s: String): Vector[Long] = + Option(s) match { + case Some(x) => x.split('.').toVector.filterNot(_ == "").map(_.toLong) + case _ => Vector() + } + def splitDash(s: String): Vector[String] = + Option(s) match { + case Some(x) => x.split('-').toVector.filterNot(_ == "") + case _ => Vector() + } + def splitPlus(s: String): Vector[String] = + Option(s) match { + case Some(x) => x.split('+').toVector.filterNot(_ == "").map("+" + _) + case _ => Vector() + } + val TaggedVersion = """(\d{1,14})([\.\d{1,14}]*)((?:-\w+)*)((?:\+.+)*)""".r + val NonSpaceString = """(\S+)""".r + v match { + case TaggedVersion(m, ns, ts, es) => Some((Vector(m.toLong) ++ splitDot(ns), splitDash(ts), splitPlus(es))) + case "" => None + case NonSpaceString(s) => Some((Vector(), Vector(), Vector(s))) + case _ => None + } + } + + /** + * Strict. Checks everythig. + */ + object Strict extends VersionNumberCompatibility { + def name: String = "Strict" + def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean = v1 == v2 + } + + /** + * Semantic versioning. See http://semver.org/spec/v2.0.0.html + */ + object SemVer extends VersionNumberCompatibility { + def name: String = "Semantic Versioning" + def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean = + doIsCompat(v1, v2) || doIsCompat(v2, v1) + private[this] def doIsCompat(v1: VersionNumber, v2: VersionNumber): Boolean = + (v1, v2) match { + case (v1, v2) if (v1.size >= 2) && (v2.size >= 2) => // A normal version number MUST take the form X.Y.Z + (v1._1.get, v1._2.get, v1._3.getOrElse(0), v1.tags, v2._1.get, v2._2.get, v2._3.getOrElse(0), v2.tags) match { + case (0L, _, _, _, 0L, _, _, _) => + // Major version zero (0.y.z) is for initial development. Anything may change at any time. The public API should not be considered stable. + equalsIgnoreExtra(v1, v2) + case (_, 0, 0, ts1, _, 0, 0, ts2) if (!ts1.isEmpty) || (!ts2.isEmpty) => + // A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers + equalsIgnoreExtra(v1, v2) + case (x1, _, _, _, x2, _, _, _) => + // Patch version Z (x.y.Z | x > 0) MUST be incremented if only backwards compatible bug fixes are introduced. + // Minor version Y (x.Y.z | x > 0) MUST be incremented if new, backwards compatible functionality is introduced + x1 == x2 + case _ => equalsIgnoreExtra(v1, v2) + } + case _ => false + } + // Build metadata SHOULD be ignored when determining version precedence. + private[this] def equalsIgnoreExtra(v1: VersionNumber, v2: VersionNumber): Boolean = + (v1.numbers == v2.numbers) && (v1.tags == v2.tags) + } + + /* A variant of SemVar that seems to be common among the Scala libraries. + * The second segment (y in x.y.z) increments breaks the binary compatibility even when x > 0. + * Also API comatibility is expected even when the first segment is zero. + */ + object SecondSegment extends VersionNumberCompatibility { + def name: String = "Second Segment Variant" + def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean = + doIsCompat(v1, v2) || doIsCompat(v2, v1) + private[this] def doIsCompat(v1: VersionNumber, v2: VersionNumber): Boolean = + (v1, v2) match { + case (v1, v2) if (v1.size >= 3) && (v2.size >= 3) => // A normal version number MUST take the form X.Y.Z + (v1._1.get, v1._2.get, v1._3.get, v1.tags, v2._1.get, v2._2.get, v2._3.get, v2.tags) match { + case (x1, y1, 0, ts1, x2, y2, 0, ts2) if (!ts1.isEmpty) || (!ts2.isEmpty) => + // A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers + equalsIgnoreExtra(v1, v2) + case (x1, y1, _, _, x2, y2, _, _) => + // Patch version Z (x.y.Z | x > 0) MUST be incremented if only backwards compatible changes are introduced. + (x1 == x2) && (y1 == y2) + case _ => equalsIgnoreExtra(v1, v2) + } + case _ => false + } + // Build metadata SHOULD be ignored when determining version precedence. + private[this] def equalsIgnoreExtra(v1: VersionNumber, v2: VersionNumber): Boolean = + (v1.numbers == v2.numbers) && (v1.tags == v2.tags) + } +} + +trait VersionNumberCompatibility { + def name: String + def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean +} diff --git a/ivy/src/test/scala/VersionNumberSpec.scala b/ivy/src/test/scala/VersionNumberSpec.scala new file mode 100644 index 000000000..7b836050f --- /dev/null +++ b/ivy/src/test/scala/VersionNumberSpec.scala @@ -0,0 +1,122 @@ +package sbt + +import org.specs2._ + +class VersionNumberSpec extends Specification { + def is = s2""" + + This is a specification to check the version number parsing. + + 1 should + ${beParsedAs("1", Seq(1), Seq(), Seq())} + ${breakDownTo("1", Some(1))} + + 1.0 should + ${beParsedAs("1.0", Seq(1, 0), Seq(), Seq())} + ${breakDownTo("1.0", Some(1), Some(0))} + + 1.0.0 should + ${beParsedAs("1.0.0", Seq(1, 0, 0), Seq(), Seq())} + ${breakDownTo("1.0.0", Some(1), Some(0), Some(0))} + + ${beSemVerCompatWith("1.0.0", "1.0.1")} + ${beSemVerCompatWith("1.0.0", "1.1.1")} + ${notBeSemVerCompatWith("1.0.0", "2.0.0")} + ${notBeSemVerCompatWith("1.0.0", "1.0.0-M1")} + + ${beSecSegCompatWith("1.0.0", "1.0.1")} + ${notBeSecSegCompatWith("1.0.0", "1.1.1")} + ${notBeSecSegCompatWith("1.0.0", "2.0.0")} + ${notBeSecSegCompatWith("1.0.0", "1.0.0-M1")} + + 1.0.0.0 should + ${beParsedAs("1.0.0.0", Seq(1, 0, 0, 0), Seq(), Seq())} + ${breakDownTo("1.0.0.0", Some(1), Some(0), Some(0), Some(0))} + + 0.12.0 should + ${beParsedAs("0.12.0", Seq(0, 12, 0), Seq(), Seq())} + ${breakDownTo("0.12.0", Some(0), Some(12), Some(0))} + + ${notBeSemVerCompatWith("0.12.0", "0.12.0-RC1")} + ${notBeSemVerCompatWith("0.12.0", "0.12.1")} + ${notBeSemVerCompatWith("0.12.0", "0.12.1-M1")} + + ${notBeSecSegCompatWith("0.12.0", "0.12.0-RC1")} + ${beSecSegCompatWith("0.12.0", "0.12.1")} + ${beSecSegCompatWith("0.12.0", "0.12.1-M1")} + + 0.1.0-SNAPSHOT should + ${beParsedAs("0.1.0-SNAPSHOT", Seq(0, 1, 0), Seq("SNAPSHOT"), Seq())} + + ${beSemVerCompatWith("0.1.0-SNAPSHOT", "0.1.0-SNAPSHOT")} + ${notBeSemVerCompatWith("0.1.0-SNAPSHOT", "0.1.0")} + ${beSemVerCompatWith("0.1.0-SNAPSHOT", "0.1.0-SNAPSHOT+001")} + + ${beSecSegCompatWith("0.1.0-SNAPSHOT", "0.1.0-SNAPSHOT")} + ${notBeSecSegCompatWith("0.1.0-SNAPSHOT", "0.1.0")} + ${beSecSegCompatWith("0.1.0-SNAPSHOT", "0.1.0-SNAPSHOT+001")} + + 0.1.0-M1 should + ${beParsedAs("0.1.0-M1", Seq(0, 1, 0), Seq("M1"), Seq())} + + 0.1.0-RC1 should + ${beParsedAs("0.1.0-RC1", Seq(0, 1, 0), Seq("RC1"), Seq())} + + 0.1.0-MSERVER-1 should + ${beParsedAs("0.1.0-MSERVER-1", Seq(0, 1, 0), Seq("MSERVER", "1"), Seq())} + + 2.10.4-20140115-000117-b3a-sources should + ${beParsedAs("2.10.4-20140115-000117-b3a-sources", Seq(2, 10, 4), Seq("20140115", "000117", "b3a", "sources"), Seq())} + + ${beSemVerCompatWith("2.10.4-20140115-000117-b3a-sources", "2.0.0")} + + ${notBeSecSegCompatWith("2.10.4-20140115-000117-b3a-sources", "2.0.0")} + + 20140115000117-b3a-sources should + ${beParsedAs("20140115000117-b3a-sources", Seq(20140115000117L), Seq("b3a", "sources"), Seq())} + + 1.0.0-alpha+001+002 should + ${beParsedAs("1.0.0-alpha+001+002", Seq(1, 0, 0), Seq("alpha"), Seq("+001", "+002"))} + + non.space.!?string should + ${beParsedAs("non.space.!?string", Seq(), Seq(), Seq("non.space.!?string"))} + + space !?string should + ${beParsedAsError("space !?string")} + + blank string should + ${beParsedAsError("")} + """ + + def beParsedAs(s: String, ns: Seq[Long], ts: Seq[String], es: Seq[String]) = + s match { + case VersionNumber(ns1, ts1, es1) if (ns1 == ns && ts1 == ts && es1 == es) => + (VersionNumber(ns, ts, es).toString must_== s) and + (VersionNumber(ns, ts, es) == VersionNumber(ns, ts, es)) + case VersionNumber(ns1, ts1, es1) => + sys.error(s"$ns1, $ts1, $es1") + } + def breakDownTo(s: String, major: Option[Long], minor: Option[Long] = None, + patch: Option[Long] = None, buildNumber: Option[Long] = None) = + s match { + case VersionNumber(ns, ts, es) => + val v = VersionNumber(ns, ts, es) + (v._1 must_== major) and + (v._2 must_== minor) and + (v._3 must_== patch) and + (v._4 must_== buildNumber) + } + def beParsedAsError(s: String) = + s match { + case VersionNumber(ns1, ts1, es1) => failure + case _ => success + } + def beSemVerCompatWith(v1: String, v2: String) = + VersionNumber.SemVer.isCompatible(VersionNumber(v1), VersionNumber(v2)) must_== true + def notBeSemVerCompatWith(v1: String, v2: String) = + VersionNumber.SemVer.isCompatible(VersionNumber(v1), VersionNumber(v2)) must_== false + def beSecSegCompatWith(v1: String, v2: String) = + VersionNumber.SecondSegment.isCompatible(VersionNumber(v1), VersionNumber(v2)) must_== true + def notBeSecSegCompatWith(v1: String, v2: String) = + VersionNumber.SecondSegment.isCompatible(VersionNumber(v1), VersionNumber(v2)) must_== false +} From 2b8fa35b8da57f1ff9d3439c37881a7e07d06964 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 27 Jul 2014 12:26:12 -0400 Subject: [PATCH 6/7] 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. --- ivy/src/main/scala/sbt/EvictionWarning.scala | 188 +++++++++++++-- ivy/src/test/scala/BaseIvySpecification.scala | 51 ++++ ivy/src/test/scala/EvictionWarningSpec.scala | 224 ++++++++++++++++++ 3 files changed, 447 insertions(+), 16 deletions(-) create mode 100644 ivy/src/test/scala/BaseIvySpecification.scala create mode 100644 ivy/src/test/scala/EvictionWarningSpec.scala diff --git a/ivy/src/main/scala/sbt/EvictionWarning.scala b/ivy/src/main/scala/sbt/EvictionWarning.scala index 4d3baec98..bacebdee3 100644 --- a/ivy/src/main/scala/sbt/EvictionWarning.scala +++ b/ivy/src/main/scala/sbt/EvictionWarning.scala @@ -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 } } diff --git a/ivy/src/test/scala/BaseIvySpecification.scala b/ivy/src/test/scala/BaseIvySpecification.scala new file mode 100644 index 000000000..e754903fe --- /dev/null +++ b/ivy/src/test/scala/BaseIvySpecification.scala @@ -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) + } +} diff --git a/ivy/src/test/scala/EvictionWarningSpec.scala b/ivy/src/test/scala/EvictionWarningSpec.scala new file mode 100644 index 000000000..171d60bd5 --- /dev/null +++ b/ivy/src/test/scala/EvictionWarningSpec.scala @@ -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)") + } +} From f4cae232bb6016a5945433988be69c1673c4ba51 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Mon, 28 Jul 2014 13:25:54 -0400 Subject: [PATCH 7/7] Improve doc on ModuleDetailReport per @jsuereth --- ivy/src/main/scala/sbt/UpdateReport.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ivy/src/main/scala/sbt/UpdateReport.scala b/ivy/src/main/scala/sbt/UpdateReport.scala index 41ea4fd80..f26f3fc10 100644 --- a/ivy/src/main/scala/sbt/UpdateReport.scala +++ b/ivy/src/main/scala/sbt/UpdateReport.scala @@ -47,6 +47,7 @@ final class ConfigurationReport( val configuration: String, val modules: Seq[ModuleReport], val details: Seq[ModuleDetailReport], + @deprecated("Use details instead to get better eviction info.", "0.13.6") val evicted: Seq[ModuleID]) { def this(configuration: String, modules: Seq[ModuleReport], evicted: Seq[ModuleID]) = this(configuration, modules, Nil, evicted) @@ -67,9 +68,15 @@ final class ConfigurationReport( } /** + * ModuleDetailReport represents an organization+name entry in Ivy resolution report. * In sbt's terminology, "module" consists of organization, name, and version. * In Ivy's, "module" means just organization and name, and the one including version numbers * are called revisions. + * + * A sequence of ModuleDetailReport called details is newly added to ConfigurationReport, replacing evicted. + * (Note old evicted was just a seq of ModuleIDs). + * ModuleDetailReport groups the ModuleReport of both winners and evicted reports by their organization and name, + * which can be used to calculate detailed evction warning etc. */ final class ModuleDetailReport( val organization: String,