diff --git a/ivy/src/main/scala/sbt/IvyActions.scala b/ivy/src/main/scala/sbt/IvyActions.scala index 4960c9a2d..4d69ae2c7 100644 --- a/ivy/src/main/scala/sbt/IvyActions.scala +++ b/ivy/src/main/scala/sbt/IvyActions.scala @@ -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 + } +}