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) } diff --git a/main/actions/src/main/scala/sbt/CacheIvy.scala b/main/actions/src/main/scala/sbt/CacheIvy.scala index 3b50cadb9..47cf6b494 100644 --- a/main/actions/src/main/scala/sbt/CacheIvy.scala +++ b/main/actions/src/main/scala/sbt/CacheIvy.scala @@ -7,6 +7,7 @@ import Predef.{ Map, Set, implicitly } // excludes *both 2.10.x conforms and 2.1 import FileInfo.{ exists, hash } import java.io.File +import java.{ util => ju } import java.net.URL import Types.{ :+:, idFun } import scala.xml.NodeSeq @@ -62,16 +63,23 @@ object CacheIvy { } implicit def updateStatsFormat: Format[UpdateStats] = wrap[UpdateStats, (Long, Long, Long)](us => (us.resolveTime, us.downloadTime, us.downloadSize), { case (rt, dt, ds) => new UpdateStats(rt, dt, ds, true) }) - implicit def confReportFormat(implicit m: Format[String], mr: Format[Seq[ModuleReport]], mi: Format[Seq[ModuleID]]): Format[ConfigurationReport] = - wrap[ConfigurationReport, (String, Seq[ModuleReport], Seq[ModuleID])](r => (r.configuration, r.modules, r.evicted), { case (c, m, v) => new ConfigurationReport(c, m, v) }) - implicit def moduleReportFormat(implicit ff: Format[File]): Format[ModuleReport] = - wrap[ModuleReport, (ModuleID, Seq[(Artifact, File)], Seq[Artifact])](m => (m.module, m.artifacts, m.missingArtifacts), { case (m, as, ms) => new ModuleReport(m, as, ms) }) + implicit def confReportFormat(implicit m: Format[String], mr: Format[Seq[ModuleReport]], mdr: Format[Seq[ModuleDetailReport]], mi: Format[Seq[ModuleID]]): Format[ConfigurationReport] = + wrap[ConfigurationReport, (String, Seq[ModuleReport], Seq[ModuleDetailReport], Seq[ModuleID])](r => (r.configuration, r.modules, r.details, r.evicted), { case (c, m, d, v) => new ConfigurationReport(c, m, d, v) }) + implicit def moduleReportFormat(implicit cf: Format[Seq[Caller]], ff: Format[File]): Format[ModuleReport] = { + wrap[ModuleReport, (ModuleID, Seq[(Artifact, File)], Seq[Artifact], Option[String], Option[Long], Option[String], Option[String], Boolean, Option[String], Option[String], Option[String], Option[String], Map[String, String], Option[Boolean], Option[String], Seq[String], Seq[(String, URL)], Seq[Caller])]( + m => (m.module, m.artifacts, m.missingArtifacts, m.status, m.publicationDate map { _.getTime }, m.resolver, m.artifactResolver, m.evicted, m.evictedData, m.evictedReason, m.problem, m.homepage, m.extraAttributes, m.isDefault, m.branch, m.configurations, m.licenses, m.callers), + { case (m, as, ms, s, pd, r, a, e, ed, er, p, h, ea, d, b, cs, ls, ks) => new ModuleReport(m, as, ms, s, pd map { new ju.Date(_) }, r, a, e, ed, er, p, h, ea, d, b, cs, ls, ks) }) + } implicit def artifactFormat(implicit sf: Format[String], uf: Format[Option[URL]]): Format[Artifact] = { wrap[Artifact, (String, String, String, Option[String], Seq[Configuration], Option[URL], Map[String, String])]( a => (a.name, a.`type`, a.extension, a.classifier, a.configurations.toSeq, a.url, a.extraAttributes), { case (n, t, x, c, cs, u, e) => Artifact(n, t, x, c, cs, u, e) } ) } + implicit def moduleDetailReportFormat(implicit sf: Format[String], bf: Format[Boolean], df: Format[Seq[ModuleReport]]): Format[ModuleDetailReport] = + wrap[ModuleDetailReport, (String, String, Seq[ModuleReport])](m => (m.organization, m.name, m.modules), { case (o, n, r) => new ModuleDetailReport(o, n, r) }) + implicit def callerFormat: Format[Caller] = + wrap[Caller, (ModuleID, Seq[String], Map[String, String])](c => (c.caller, c.callerConfigurations, c.callerExtraAttributes), { case (c, cc, ea) => new Caller(c, cc, ea) }) implicit def exclusionRuleFormat(implicit sf: Format[String]): Format[ExclusionRule] = wrap[ExclusionRule, (String, String, String, Seq[String])](e => (e.organization, e.name, e.artifact, e.configurations), { case (o, n, a, cs) => ExclusionRule(o, n, a, cs) }) implicit def crossVersionFormat: Format[CrossVersion] = wrap(crossToInt, crossFromInt)