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.
This commit is contained in:
Eugene Yokota 2014-07-18 04:50:23 -04:00
parent 5b070b9dcc
commit 855e7f176b
4 changed files with 218 additions and 35 deletions

View File

@ -2,6 +2,11 @@ package sbt
import DependencyFilter._ 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) { final case class ConflictWarning(label: String, level: Level.Value, failOnConflict: Boolean) {
@deprecated("`filter` is no longer used", "0.13.0") @deprecated("`filter` is no longer used", "0.13.0")
val filter: ModuleFilter = (_: ModuleID) => false val filter: ModuleFilter = (_: ModuleID) => false

View File

@ -4,35 +4,121 @@
package sbt package sbt
import java.io.File import java.io.File
import java.{ util => ju }
import collection.mutable import collection.mutable
import java.net.URL
import org.apache.ivy.core.{ module, report, resolve } import org.apache.ivy.core.{ module, report, resolve }
import module.descriptor.{ Artifact => IvyArtifact } import module.descriptor.{ Artifact => IvyArtifact, License => IvyLicense }
import module.id.ModuleRevisionId import module.id.{ ModuleRevisionId, ModuleId => IvyModuleId }
import resolve.IvyNode
import report.{ ArtifactDownloadReport, ConfigurationResolveReport, ResolveReport } import report.{ ArtifactDownloadReport, ConfigurationResolveReport, ResolveReport }
import resolve.{ IvyNode, IvyNodeCallers }
import IvyNodeCallers.{ Caller => IvyCaller }
object IvyRetrieve { object IvyRetrieve {
def reports(report: ResolveReport): Seq[ConfigurationResolveReport] = def reports(report: ResolveReport): Seq[ConfigurationResolveReport] =
report.getConfigurations map report.getConfigurationReport report.getConfigurations map report.getConfigurationReport
def moduleReports(confReport: ConfigurationResolveReport): Seq[ModuleReport] = 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 = 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 missing = new mutable.ListBuffer[Artifact]
val resolved = new mutable.ListBuffer[(Artifact, File)] val resolved = new mutable.ListBuffer[(Artifact, File)]
for (r <- artReport) { for (r <- artReport) {
val file = r.getLocalFile val fileOpt = Option(r.getLocalFile)
val art = toArtifact(r.getArtifact) val art = toArtifact(r.getArtifact)
if (file eq null) fileOpt match {
missing += art case Some(file) => resolved += ((art, file))
else case None => missing += art
resolved += ((art, file)) }
} }
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] = def evicted(confReport: ConfigurationResolveReport): Seq[ModuleID] =
confReport.getEvictedNodes.map(node => toModuleID(node.getId)) confReport.getEvictedNodes.map(node => toModuleID(node.getId))
@ -50,7 +136,7 @@ object IvyRetrieve {
def updateStats(report: ResolveReport): UpdateStats = def updateStats(report: ResolveReport): UpdateStats =
new UpdateStats(report.getResolveTime, report.getDownloadTime, report.getDownloadSize, false) new UpdateStats(report.getResolveTime, report.getDownloadTime, report.getDownloadSize, false)
def configurationReport(confReport: ConfigurationResolveReport): ConfigurationReport = 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. * Tries to find Ivy graph path the from node to target.

View File

@ -4,6 +4,8 @@
package sbt package sbt
import java.io.File import java.io.File
import java.net.URL
import java.{ util => ju }
/** /**
* Provides information about dependency resolution. * 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. * Provides information about resolution of a single configuration.
* @param configuration the configuration this report is for. * @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 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 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 = 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. * Provides information about the resolution of a module.
* This information is in the context of a specific configuration. * This information is in the context of a specific configuration.
* @param module the `ModuleID` this report is for. * @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]) { final class ModuleReport(
override def toString = val module: ModuleID,
{ val artifacts: Seq[(Artifact, File)],
val arts = artifacts.map(_.toString) ++ missingArtifacts.map(art => "(MISSING) " + art) val missingArtifacts: Seq[Artifact],
"\t\t" + module + ": " + val status: Option[String],
(if (arts.size <= 1) "" else "\n\t\t\t") + arts.mkString("\n\t\t\t") + "\n" 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 = 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 { object UpdateReport {
implicit def richUpdateReport(report: UpdateReport): RichUpdateReport = new RichUpdateReport(report) 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.*/ /** Constructs a new report that only contains files matching the specified filter.*/
def filter(f: DependencyFilter): UpdateReport = def filter(f: DependencyFilter): UpdateReport =
moduleReportMap { (configuration, modReport) => moduleReportMap { (configuration, modReport) =>
import modReport._ modReport.copy(
val newArtifacts = artifacts filter { case (art, file) => f(configuration, module, art) } artifacts = modReport.artifacts filter { case (art, file) => f(configuration, modReport.module, art) },
val newMissing = missingArtifacts filter { art => f(configuration, module, art) } missingArtifacts = modReport.missingArtifacts filter { art => f(configuration, modReport.module, art) }
new ModuleReport(module, newArtifacts, newMissing) )
} }
def substitute(f: (String, ModuleID, Seq[(Artifact, File)]) => Seq[(Artifact, File)]): UpdateReport = def substitute(f: (String, ModuleID, Seq[(Artifact, File)]) => Seq[(Artifact, File)]): UpdateReport =
moduleReportMap { (configuration, modReport) => moduleReportMap { (configuration, modReport) =>
val newArtifacts = f(configuration, modReport.module, modReport.artifacts) 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)] = def toSeq: Seq[(String, ModuleID, Artifact, File)] =
@ -120,8 +203,9 @@ object UpdateReport {
def addMissing(f: ModuleID => Seq[Artifact]): UpdateReport = def addMissing(f: ModuleID => Seq[Artifact]): UpdateReport =
moduleReportMap { (configuration, modReport) => moduleReportMap { (configuration, modReport) =>
import modReport._ modReport.copy(
new ModuleReport(module, artifacts, (missingArtifacts ++ f(module)).distinct) missingArtifacts = (modReport.missingArtifacts ++ f(modReport.module)).distinct
)
} }
def moduleReportMap(f: (String, ModuleReport) => ModuleReport): UpdateReport = def moduleReportMap(f: (String, ModuleReport) => ModuleReport): UpdateReport =
@ -129,7 +213,7 @@ object UpdateReport {
val newConfigurations = report.configurations.map { confReport => val newConfigurations = report.configurations.map { confReport =>
import confReport._ import confReport._
val newModules = modules map { modReport => f(configuration, modReport) } 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) new UpdateReport(report.cachedDescriptor, newConfigurations, report.stats, report.stamps)
} }

View File

@ -7,6 +7,7 @@ import Predef.{ Map, Set, implicitly } // excludes *both 2.10.x conforms and 2.1
import FileInfo.{ exists, hash } import FileInfo.{ exists, hash }
import java.io.File import java.io.File
import java.{ util => ju }
import java.net.URL import java.net.URL
import Types.{ :+:, idFun } import Types.{ :+:, idFun }
import scala.xml.NodeSeq import scala.xml.NodeSeq
@ -62,16 +63,23 @@ object CacheIvy {
} }
implicit def updateStatsFormat: Format[UpdateStats] = 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) }) 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] = 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[ModuleID])](r => (r.configuration, r.modules, r.evicted), { case (c, m, v) => new ConfigurationReport(c, m, v) }) 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 ff: Format[File]): Format[ModuleReport] = implicit def moduleReportFormat(implicit cf: Format[Seq[Caller]], 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) }) 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] = { 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])]( 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), 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) } { 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] = 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) }) 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) implicit def crossVersionFormat: Format[CrossVersion] = wrap(crossToInt, crossFromInt)