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._
/**
* 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

View File

@ -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.

View File

@ -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)
}

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 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)