Merge pull request #1467 from sbt/wip/eviction-warning

Eviction warnings (Fixes #1200)
This commit is contained in:
Josh Suereth 2014-07-29 11:08:42 -04:00
commit 57aaaf7a60
14 changed files with 1063 additions and 38 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

@ -0,0 +1,200 @@
package sbt
import collection.mutable
import Configurations.Compile
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 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(module: IvySbt#Module, options: EvictionWarningOptions, report: UpdateReport, log: Logger): EvictionWarning = {
val evictions = buildEvictions(options, report)
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.configStrings 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 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
}
}

View File

@ -4,35 +4,124 @@
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 = Option(revId.getBranch)
val (status, publicationDate, resolver, artifactResolver) = dep.isLoaded match {
case true =>
(Option(dep.getDescriptor.getStatus),
Some(new ju.Date(dep.getPublication)),
Option(dep.getModuleRevision.getResolver.getName),
Option(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 => Option(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, 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)
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 +139,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,10 +39,22 @@ 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]) {
override def toString = "\t" + configuration + ":\n" + modules.mkString + evicted.map("\t\t(EVICTED) " + _ + "\n").mkString
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)
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.
@ -50,25 +64,131 @@ 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)
}
/**
* 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,
val name: String,
val modules: Seq[ModuleReport]) {
override def toString: String = {
val details = modules map { _.detailReport }
s"\t$organization:$name\n${details.mkString}\n"
}
}
/**
* 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, Option[String])],
val callers: Seq[Caller]) {
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 =
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, 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)
}
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 +221,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 +243,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 +253,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

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

View File

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

View File

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

View File

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

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, Option[String])], 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)

View File

@ -1100,8 +1100,27 @@ object Classpaths {
ivyModule := { val is = ivySbt.value; new is.Module(moduleSettings.value) },
transitiveUpdate <<= transitiveUpdateTask,
updateCacheName := "update_cache" + (if (crossPaths.value) s"_${scalaBinaryVersion.value}" else ""),
evictionWarningOptions in update := EvictionWarningOptions.default,
update <<= updateTask tag (Tags.Update, Tags.Network),
update := { val report = update.value; ConflictWarning(conflictWarning.value, report, streams.value.log); report },
update := {
import ShowLines._
val report = update.value
val log = streams.value.log
ConflictWarning(conflictWarning.value, report, log)
val ewo = (evictionWarningOptions in update).value
val ew = EvictionWarning(ivyModule.value, ewo, report, log)
ew.lines foreach { log.warn(_) }
report
},
evictionWarningOptions in evicted := EvictionWarningOptions.full,
evicted := {
import ShowLines._
val report = (updateTask tag (Tags.Update, Tags.Network)).value
val log = streams.value.log
val ew = EvictionWarning(ivyModule.value, (evictionWarningOptions in evicted).value, report, log)
ew.lines foreach { log.warn(_) }
ew
},
classifiersModule in updateClassifiers := {
import language.implicitConversions
implicit val key = (m: ModuleID) => (m.organization, m.name, m.revision)

View File

@ -247,6 +247,8 @@ object Keys {
val ivyModule = TaskKey[IvySbt#Module]("ivy-module", "Provides the sbt interface to a configured Ivy module.", CTask)
val updateCacheName = TaskKey[String]("updateCacheName", "Defines the directory name used to store the update cache files (inside the streams cacheDirectory).", DTask)
val update = TaskKey[UpdateReport]("update", "Resolves and optionally retrieves dependencies, producing a report.", ATask)
val evicted = TaskKey[EvictionWarning]("evicted", "Display detailed eviction warnings.", CTask)
val evictionWarningOptions = SettingKey[EvictionWarningOptions]("eviction-warning-options", "Options on eviction warnings after resolving managed dependencies.", DSetting)
val transitiveUpdate = TaskKey[Seq[UpdateReport]]("transitive-update", "UpdateReports for the internal dependencies of this project.", DTask)
val updateClassifiers = TaskKey[UpdateReport]("update-classifiers", "Resolves and optionally retrieves classified artifacts, such as javadocs and sources, for dependency definitions, transitively.", BPlusTask, update)
val transitiveClassifiers = SettingKey[Seq[String]]("transitive-classifiers", "List of classifiers used for transitively obtaining extra artifacts for sbt or declared dependencies.", BSetting)

View File

@ -1,6 +1,25 @@
[413]: https://github.com/sbt/sbt/issues/413
[1200]: https://github.com/sbt/sbt/issues/1200
[1454]: https://github.com/sbt/sbt/pull/1454
### Eviction warnings
sbt 0.13.6 displays eviction warnings when it resolves your project's managed dependencies via `update` task.
Currently the eviction warnings are categorized into three layers: `scalaVersion` eviction, direct evictions, and transitive evictions.
By default eviction warning on `update` task will display only `scalaVersion` evictin and direct evictions.
`scalaVersion` eviction warns you when `scalaVersion` is no longer effecitive. This happens when one of your dependency depends on a newer release of scala-library than your `scalaVersion`.
Direct evctions are evictions related to your direct dependencies. Warnings are displayed only when API incompatibility is suspected. For Java libraries, Semantic Versioning is used for guessing, and for Scala libraries Second Segment versioning (second segment bump makes API incompatible) is used.
To display all eviction warnings with caller information, run `evicted` task.
[warn] There may be incompatibilities among your library dependencies.
[warn] Here are some of the libraries that were evicted:
[warn] * 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)
[#1200][1200].
### Consolidated resolution
sbt 0.13.6 adds a new setting key called `updateOptions`, which can be used to enable consolidated resolution for `update` task.

View File

@ -125,7 +125,7 @@ object Sbt extends Build {
/* **** Intermediate-level Modules **** */
// Apache Ivy integration
lazy val ivySub = baseProject(file("ivy"), "Ivy") dependsOn (interfaceSub, launchInterfaceSub, crossSub, logSub % "compile;test->test", ioSub % "compile;test->test", launchSub % "test->test") settings (ivy, jsch, testExclusive)
lazy val ivySub = baseProject(file("ivy"), "Ivy") dependsOn (interfaceSub, launchInterfaceSub, crossSub, logSub % "compile;test->test", ioSub % "compile;test->test", launchSub % "test->test", collectionSub) settings (ivy, jsch, testExclusive)
// Runner for uniform test interface
lazy val testingSub = baseProject(file("testing"), "Testing") dependsOn (ioSub, classpathSub, logSub, launchInterfaceSub, testAgentSub) settings (testInterface)
// Testing agent for running tests in a separate process.

View File

@ -0,0 +1,15 @@
package sbt
trait ShowLines[A] {
def showLines(a: A): Seq[String]
}
object ShowLines {
def apply[A](f: A => Seq[String]): ShowLines[A] =
new ShowLines[A] {
def showLines(a: A): Seq[String] = f(a)
}
implicit class ShowLinesOp[A: ShowLines](a: A) {
def lines: Seq[String] = implicitly[ShowLines[A]].showLines(a)
}
}