mirror of https://github.com/sbt/sbt.git
Merge pull request #1467 from sbt/wip/eviction-warning
Eviction warnings (Fixes #1200)
This commit is contained in:
commit
57aaaf7a60
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue