Merge pull request #1491 from sbt/fix/528

Unresolved dependency warning includes source position. Fixes #528
This commit is contained in:
Josh Suereth 2014-08-11 07:51:33 -04:00
commit f7c4898a97
5 changed files with 167 additions and 34 deletions

View File

@ -5,6 +5,7 @@ package sbt
import java.io.File
import scala.xml.{ Node => XNode, NodeSeq }
import collection.mutable
import org.apache.ivy.Ivy
import org.apache.ivy.core.{ IvyPatternHelper, LogOptions }
@ -29,6 +30,14 @@ final case class MakePomConfiguration(file: File, moduleInfo: ModuleInfo, config
final case class GetClassifiersConfiguration(module: GetClassifiersModule, exclude: Map[ModuleID, Set[String]], configuration: UpdateConfiguration, ivyScala: Option[IvyScala])
final case class GetClassifiersModule(id: ModuleID, modules: Seq[ModuleID], configurations: Seq[Configuration], classifiers: Seq[String])
final class UnresolvedWarningConfiguration private[sbt] (
val modulePositions: Map[ModuleID, SourcePosition])
object UnresolvedWarningConfiguration {
def apply(): UnresolvedWarningConfiguration = apply(Map())
def apply(modulePositions: Map[ModuleID, SourcePosition]): UnresolvedWarningConfiguration =
new UnresolvedWarningConfiguration(modulePositions)
}
/**
* Configures logging during an 'update'. `level` determines the amount of other information logged.
* `Full` is the default and logs the most.
@ -130,43 +139,37 @@ object IvyActions {
* Resolves and retrieves dependencies. 'ivyConfig' is used to produce an Ivy file and configuration.
* 'updateConfig' configures the actual resolution and retrieval process.
*/
@deprecated("Use updateEither instead.", "0.13.6")
def update(module: IvySbt#Module, configuration: UpdateConfiguration, log: Logger): UpdateReport =
updateEither(module, configuration, UnresolvedWarningConfiguration(), log) match {
case Right(r) => r
case Left(w) =>
throw w.resolveException
}
/**
* Resolves and retrieves dependencies. 'ivyConfig' is used to produce an Ivy file and configuration.
* 'updateConfig' configures the actual resolution and retrieval process.
*/
def updateEither(module: IvySbt#Module, configuration: UpdateConfiguration,
uwconfig: UnresolvedWarningConfiguration, log: Logger): Either[UnresolvedWarning, UpdateReport] =
module.withModule(log) {
case (ivy, md, default) =>
val (report, err) = resolve(configuration.logging)(ivy, md, default)
err match {
case Some(x) if !configuration.missingOk =>
processUnresolved(x, log)
throw x
Left(UnresolvedWarning(x, uwconfig))
case _ =>
val cachedDescriptor = ivy.getSettings.getResolutionCacheManager.getResolvedIvyFileInCache(md.getModuleRevisionId)
val uReport = IvyRetrieve.updateReport(report, cachedDescriptor)
configuration.retrieve match {
case Some(rConf) => retrieve(ivy, uReport, rConf)
case None => uReport
case Some(rConf) => Right(retrieve(ivy, uReport, rConf))
case None => Right(uReport)
}
}
}
def processUnresolved(err: ResolveException, log: Logger) {
val withExtra = err.failed.filter(!_.extraDependencyAttributes.isEmpty)
if (!withExtra.isEmpty) {
log.warn("\n\tNote: Some unresolved dependencies have extra attributes. Check that these dependencies exist with the requested attributes.")
withExtra foreach { id => log.warn("\t\t" + id) }
log.warn("")
}
err.failed foreach { x =>
val failedPaths = err.failedPaths(x)
if (!failedPaths.isEmpty) {
log.warn("\n\tNote: Unresolved dependencies path:")
val reverseFailedPaths = (failedPaths.toList map { _.toString }).reverse
log.warn("\t\t" + reverseFailedPaths.head)
reverseFailedPaths.tail foreach { id =>
log.warn("\t\t +- " + id)
}
}
}
}
@deprecated("No longer used.", "0.13.6")
def processUnresolved(err: ResolveException, log: Logger): Unit = ()
def groupedConflicts[T](moduleFilter: ModuleFilter, grouping: ModuleID => T)(report: UpdateReport): Map[T, Set[String]] =
report.configurations.flatMap { confReport =>
val evicted = confReport.evicted.filter(moduleFilter)
@ -303,3 +306,59 @@ final class ResolveException(
def this(messages: Seq[String], failed: Seq[ModuleID]) =
this(messages, failed, Map(failed map { m => m -> Nil }: _*))
}
/**
* Represents unresolved dependency warning, which displays reconstructed dependency tree
* along with source position of each node.
*/
final class UnresolvedWarning private[sbt] (
val resolveException: ResolveException,
val failedPaths: Seq[Seq[(ModuleID, Option[SourcePosition])]])
object UnresolvedWarning {
private[sbt] def apply(err: ResolveException, config: UnresolvedWarningConfiguration): UnresolvedWarning = {
def modulePosition(m0: ModuleID): Option[SourcePosition] =
config.modulePositions.find {
case (m, p) =>
(m.organization == m0.organization) &&
(m0.name startsWith m.name) &&
(m.revision == m0.revision)
} map {
case (m, p) => p
}
val failedPaths = err.failed map { x: ModuleID =>
err.failedPaths(x).toList.reverse map { id =>
(id, modulePosition(id))
}
}
apply(err, failedPaths)
}
private[sbt] def apply(err: ResolveException, failedPaths: Seq[Seq[(ModuleID, Option[SourcePosition])]]): UnresolvedWarning =
new UnresolvedWarning(err, failedPaths)
private[sbt] def sourcePosStr(posOpt: Option[SourcePosition]): String =
posOpt match {
case Some(LinePosition(path, start)) => s" ($path#L$start)"
case Some(RangePosition(path, LineRange(start, end))) => s" ($path#L$start-$end)"
case _ => ""
}
implicit val unresolvedWarningLines: ShowLines[UnresolvedWarning] = ShowLines { a =>
val withExtra = a.resolveException.failed.filter(!_.extraDependencyAttributes.isEmpty)
val buffer = mutable.ListBuffer[String]()
if (!withExtra.isEmpty) {
buffer += "\n\tNote: Some unresolved dependencies have extra attributes. Check that these dependencies exist with the requested attributes."
withExtra foreach { id => buffer += "\t\t" + id }
}
if (!a.failedPaths.isEmpty) {
buffer += "\n\tNote: Unresolved dependencies path:"
a.failedPaths foreach { path =>
if (!path.isEmpty) {
val head = path.head
buffer += "\t\t" + head._1.toString + sourcePosStr(head._2)
path.tail foreach {
case (m, pos) =>
buffer += "\t\t +- " + m.toString + sourcePosStr(pos)
}
}
}
}
buffer.toList
}
}

View File

@ -55,6 +55,8 @@ object CacheIvy {
lazy val updateIC: InputCache[IvyConfiguration :+: ModuleSettings :+: UpdateConfiguration :+: HNil] = implicitly
/* def deliverIC: InputCache[IvyConfiguration :+: ModuleSettings :+: DeliverConfiguration :+: HNil] = implicitly
def publishIC: InputCache[IvyConfiguration :+: ModuleSettings :+: PublishConfiguration :+: HNil] = implicitly*/
lazy val moduleIDSeqIC: InputCache[Seq[ModuleID]] = implicitly
lazy val modulePositionMapFormat: Format[Map[ModuleID, SourcePosition]] = implicitly
implicit lazy val updateReportFormat: Format[UpdateReport] =
{
@ -83,7 +85,19 @@ object CacheIvy {
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)
implicit def sourcePositionFormat: Format[SourcePosition] =
wrap[SourcePosition, (Int, String, Int, Int)](
{
case NoPosition => (0, "", 0, 0)
case LinePosition(p, s) => (1, p, s, 0)
case RangePosition(p, LineRange(s, e)) => (2, p, s, e)
},
{
case (0, _, _, _) => NoPosition
case (1, p, s, _) => LinePosition(p, s)
case (2, p, s, e) => RangePosition(p, LineRange(s, e))
}
)
private[this] final val DisabledValue = 0
private[this] final val BinaryValue = 1
private[this] final val FullValue = 2

View File

@ -1101,6 +1101,8 @@ object Classpaths {
transitiveUpdate <<= transitiveUpdateTask,
updateCacheName := "update_cache" + (if (crossPaths.value) s"_${scalaBinaryVersion.value}" else ""),
evictionWarningOptions in update := EvictionWarningOptions.default,
dependencyPositions <<= dependencyPositionsTask,
unresolvedWarningConfiguration in update := UnresolvedWarningConfiguration(dependencyPositions.value),
update <<= updateTask tag (Tags.Update, Tags.Network),
update := {
import ShowLines._
@ -1214,7 +1216,13 @@ object Classpaths {
} tag (Tags.Publish, Tags.Network)
import Cache._
import CacheIvy.{ classpathFormat, /*publishIC,*/ updateIC, updateReportFormat, excludeMap }
import CacheIvy.{
classpathFormat, /*publishIC,*/ updateIC,
updateReportFormat,
excludeMap,
moduleIDSeqIC,
modulePositionMapFormat
}
def withExcludes(out: File, classifiers: Seq[String], lock: xsbti.GlobalLock)(f: Map[ModuleID, Set[String]] => UpdateReport): UpdateReport =
{
@ -1251,19 +1259,33 @@ object Classpaths {
case None => sv => if (scalaProvider.version == sv) scalaProvider.jars else Nil
}
val transform: UpdateReport => UpdateReport = r => substituteScalaFiles(scalaOrganization.value, r)(subScalaJars)
val uwConfig = (unresolvedWarningConfiguration in update).value
val show = Reference.display(thisProjectRef.value)
cachedUpdate(s.cacheDirectory / updateCacheName.value, show, ivyModule.value, updateConfiguration.value, transform, skip = (skip in update).value, force = isRoot, depsUpdated = depsUpdated, log = s.log)
cachedUpdate(s.cacheDirectory / updateCacheName.value, show, ivyModule.value, updateConfiguration.value, transform,
skip = (skip in update).value, force = isRoot, depsUpdated = depsUpdated,
uwConfig = uwConfig, log = s.log)
}
def cachedUpdate(cacheFile: File, label: String, module: IvySbt#Module, config: UpdateConfiguration, transform: UpdateReport => UpdateReport, skip: Boolean, force: Boolean, depsUpdated: Boolean, log: Logger): UpdateReport =
@deprecated("Use cachedUpdate with the variant that takes unresolvedHandler instead.", "0.13.6")
def cachedUpdate(cacheFile: File, label: String, module: IvySbt#Module, config: UpdateConfiguration,
transform: UpdateReport => UpdateReport, skip: Boolean, force: Boolean, depsUpdated: Boolean, log: Logger): UpdateReport =
cachedUpdate(cacheFile, label, module, config, transform, skip, force, depsUpdated,
UnresolvedWarningConfiguration(), log)
def cachedUpdate(cacheFile: File, label: String, module: IvySbt#Module, config: UpdateConfiguration,
transform: UpdateReport => UpdateReport, skip: Boolean, force: Boolean, depsUpdated: Boolean,
uwConfig: UnresolvedWarningConfiguration, log: Logger): UpdateReport =
{
implicit val updateCache = updateIC
type In = IvyConfiguration :+: ModuleSettings :+: UpdateConfiguration :+: HNil
def work = (_: In) match {
case conf :+: settings :+: config :+: HNil =>
log.info("Updating " + label + "...")
val r = IvyActions.update(module, config, log)
val r = IvyActions.updateEither(module, config, uwConfig, log) match {
case Right(ur) => ur
case Left(uw) =>
import ShowLines._
uw.lines foreach { log.warn(_) }
throw uw.resolveException
}
log.info("Done updating.")
transform(r)
}
@ -1303,6 +1325,41 @@ object Classpaths {
}
private[this] def fileUptodate(file: File, stamps: Map[File, Long]): Boolean =
stamps.get(file).forall(_ == file.lastModified)
private[sbt] def dependencyPositionsTask: Initialize[Task[Map[ModuleID, SourcePosition]]] = Def.task {
val projRef = thisProjectRef.value
val st = state.value
val s = streams.value
val cacheFile = s.cacheDirectory / updateCacheName.value
implicit val depSourcePosCache = moduleIDSeqIC
implicit val outFormat = modulePositionMapFormat
def modulePositions: Map[ModuleID, SourcePosition] =
try {
val extracted = (Project extract st)
val sk = (libraryDependencies in (GlobalScope in projRef)).scopedKey
val empty = extracted.structure.data set (sk.scope, sk.key, Nil)
val settings = extracted.structure.settings filter { s: Setting[_] =>
(s.key.key == libraryDependencies.key) &&
(s.key.scope.project == Select(projRef))
}
Map(settings flatMap {
case s: Setting[Seq[ModuleID]] @unchecked =>
s.init.evaluate(empty) map { _ -> s.pos }
}: _*)
} catch {
case _: Throwable => Map()
}
val outCacheFile = cacheFile / "output_dsp"
val f = Tracked.inputChanged(cacheFile / "input_dsp") { (inChanged: Boolean, in: Seq[ModuleID]) =>
val outCache = Tracked.lastOutput[Seq[ModuleID], Map[ModuleID, SourcePosition]](outCacheFile) {
case (_, Some(out)) if !inChanged => out
case _ => modulePositions
}
outCache(in)
}
f(libraryDependencies.value)
}
/*
// can't cache deliver/publish easily since files involved are hidden behind patterns. publish will be difficult to verify target-side anyway
def cachedPublish(cacheFile: File)(g: (IvySbt#Module, PublishConfiguration) => Unit, module: IvySbt#Module, config: PublishConfiguration) => Unit =

View File

@ -243,6 +243,8 @@ object Keys {
val unmanagedBase = SettingKey[File]("unmanaged-base", "The default directory for manually managed libraries.", ASetting)
val updateConfiguration = SettingKey[UpdateConfiguration]("update-configuration", "Configuration for resolving and retrieving managed dependencies.", DSetting)
val updateOptions = SettingKey[UpdateOptions]("update-options", "Options for resolving managed dependencies.", DSetting)
val unresolvedWarningConfiguration = TaskKey[UnresolvedWarningConfiguration]("unresolved-warning-configuration", "Configuration for unresolved dependency warning.", DTask)
val dependencyPositions = TaskKey[Map[ModuleID, SourcePosition]]("dependency-positions", "Source positions where the dependencies are defined.", DTask)
val ivySbt = TaskKey[IvySbt]("ivy-sbt", "Provides the sbt interface to Ivy.", CTask)
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)

View File

@ -1,4 +1,5 @@
[413]: https://github.com/sbt/sbt/issues/413
[528]: https://github.com/sbt/sbt/issues/528
[856]: https://github.com/sbt/sbt/issues/856
[1036]: https://github.com/sbt/sbt/pull/1036
[1059]: https://github.com/sbt/sbt/issues/1059
@ -117,7 +118,7 @@ sbt 0.13.6 now allows `enablePlugins` and `disablePlugins` to be written directl
### Unresolved dependencies error
sbt 0.13.6 will try to reconstruct dependencies tree when it fails to resolve a managed dependency.
This is an approximation, but it should help you figure out where the problematic dependency is coming from:
This is an approximation, but it should help you figure out where the problematic dependency is coming from. When possible sbt will display the source position next to the modules:
[warn] ::::::::::::::::::::::::::::::::::::::::::::::
[warn] :: UNRESOLVED DEPENDENCIES ::
@ -128,10 +129,10 @@ This is an approximation, but it should help you figure out where the problemati
[warn] Note: Unresolved dependencies path:
[warn] foundrylogic.vpp:vpp:2.2.1
[warn] +- org.apache.cayenne:cayenne-tools:3.0.2
[warn] +- org.apache.cayenne.plugins:maven-cayenne-plugin:3.0.2
[warn] +- org.apache.cayenne.plugins:maven-cayenne-plugin:3.0.2 (/foo/some-test/build.sbt#L28)
[warn] +- d:d_2.10:0.1-SNAPSHOT
[#1422][1422]/[#1447][1447] by [@eed3si9n][@eed3si9n]
[#528][528]/[#1422][1422]/[#1447][1447] by [@eed3si9n][@eed3si9n]
### Eviction warnings