diff --git a/ivy/src/main/scala/sbt/IvyActions.scala b/ivy/src/main/scala/sbt/IvyActions.scala index 646293b0e..988809fb0 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: Seq[(ModuleID, SourcePosition)]) +object UnresolvedWarningConfiguration { + def apply(): UnresolvedWarningConfiguration = apply(Seq()) + def apply(modulePositions: Seq[(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. @@ -132,23 +141,24 @@ object IvyActions { */ @deprecated("Use updateEither instead.", "0.13.6") def update(module: IvySbt#Module, configuration: UpdateConfiguration, log: Logger): UpdateReport = - updateEither(module, configuration, log) match { + updateEither(module, configuration, UnresolvedWarningConfiguration(), log) match { case Right(r) => r - case Left(e) => throw e + 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, log: Logger): Either[ResolveException, UpdateReport] = + 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) - Left(x) + Left(UnresolvedWarning(x, uwconfig)) case _ => val cachedDescriptor = ivy.getSettings.getResolutionCacheManager.getResolvedIvyFileInCache(md.getModuleRevisionId) val uReport = IvyRetrieve.updateReport(report, cachedDescriptor) @@ -296,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 + } +} diff --git a/main/actions/src/main/scala/sbt/CacheIvy.scala b/main/actions/src/main/scala/sbt/CacheIvy.scala index 3b74251d4..912d590ad 100644 --- a/main/actions/src/main/scala/sbt/CacheIvy.scala +++ b/main/actions/src/main/scala/sbt/CacheIvy.scala @@ -55,6 +55,7 @@ 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 implicit lazy val updateReportFormat: Format[UpdateReport] = { @@ -83,6 +84,21 @@ 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)) + } + ) + implicit def unresolvedWarningConfigurationFormat: Format[UnresolvedWarningConfiguration] = + wrap[UnresolvedWarningConfiguration, (Seq[(ModuleID, SourcePosition)])](c => (c.modulePositions), { case ps => UnresolvedWarningConfiguration(ps) }) private[this] final val DisabledValue = 0 private[this] final val BinaryValue = 1 diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 3cf0d25dd..97b4ef4b1 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -1101,6 +1101,7 @@ object Classpaths { transitiveUpdate <<= transitiveUpdateTask, updateCacheName := "update_cache" + (if (crossPaths.value) s"_${scalaBinaryVersion.value}" else ""), evictionWarningOptions in update := EvictionWarningOptions.default, + unresolvedWarningConfiguration in update <<= unresolvedWarningConfigurationTask, update <<= updateTask tag (Tags.Update, Tags.Network), update := { import ShowLines._ @@ -1214,7 +1215,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, + unresolvedWarningConfigurationFormat + } def withExcludes(out: File, classifiers: Seq[String], lock: xsbti.GlobalLock)(f: Map[ModuleID, Set[String]] => UpdateReport): UpdateReport = { @@ -1251,38 +1258,32 @@ 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, - unresolvedHandler = { r => - import ShowLines._ - UnresolvedDependencyWarning(r, Some(state.value)).lines foreach { s.log.warn(_) } - }, - log = s.log) + uwConfig = uwConfig, log = s.log) } @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, - { r => - import ShowLines._ - UnresolvedDependencyWarning(r, None).lines foreach { log.warn(_) } - }, log) + UnresolvedWarningConfiguration(), log) def cachedUpdate(cacheFile: File, label: String, module: IvySbt#Module, config: UpdateConfiguration, transform: UpdateReport => UpdateReport, skip: Boolean, force: Boolean, depsUpdated: Boolean, - unresolvedHandler: ResolveException => Unit, log: Logger): UpdateReport = + 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.updateEither(module, config, log) match { + val r = IvyActions.updateEither(module, config, uwConfig, log) match { case Right(ur) => ur - case Left(re) => - unresolvedHandler(re) - throw re + case Left(uw) => + import ShowLines._ + uw.lines foreach { log.warn(_) } + throw uw.resolveException } log.info("Done updating.") transform(r) @@ -1313,6 +1314,39 @@ object Classpaths { } private[this] def fileUptodate(file: File, stamps: Map[File, Long]): Boolean = stamps.get(file).forall(_ == file.lastModified) + private[sbt] def unresolvedWarningConfigurationTask: Initialize[Task[UnresolvedWarningConfiguration]] = Def.task { + val projRef = thisProjectRef.value + val st = state.value + val s = streams.value + val cacheFile = s.cacheDirectory / updateCacheName.value + implicit val uwConfigCache = moduleIDSeqIC + def modulePositions: Seq[(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)) + } + settings flatMap { + case s: Setting[Seq[ModuleID]] @unchecked => + s.init.evaluate(empty) map { _ -> s.pos } + } + } catch { + case _: Throwable => Seq() + } + val outCacheFile = cacheFile / "output_uwc" + val f = Tracked.inputChanged(cacheFile / "input_uwc") { (inChanged: Boolean, in: Seq[ModuleID]) => + val outCache = Tracked.lastOutput[Seq[ModuleID], UnresolvedWarningConfiguration](outCacheFile) { + case (_, Some(out)) if !inChanged => out + case _ => UnresolvedWarningConfiguration(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 = diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 9d420ad46..99036100b 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -243,6 +243,7 @@ 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 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) diff --git a/main/src/main/scala/sbt/UnresolvedDependencyWarning.scala b/main/src/main/scala/sbt/UnresolvedDependencyWarning.scala deleted file mode 100644 index f37ec8bbe..000000000 --- a/main/src/main/scala/sbt/UnresolvedDependencyWarning.scala +++ /dev/null @@ -1,81 +0,0 @@ -package sbt - -import java.io.File -import collection.mutable -import Def.Setting -import Scope.GlobalScope - -private[sbt] final class UnresolvedDependencyWarning( - val withExtra: Seq[ModuleID], - val failedPaths: Seq[Seq[(ModuleID, Option[String])]]) - -private[sbt] object UnresolvedDependencyWarning { - // This method used to live under IvyActions.scala, but it's moved here because it's now aware of State. - private[sbt] def apply(err: ResolveException, stateOpt: Option[State]): UnresolvedDependencyWarning = { - // This is a mapping between modules and original position, in which the module was introduced. - lazy val modulePositions: Seq[(ModuleID, SourcePosition)] = - try { - stateOpt map { state => - val extracted = (Project extract state) - val sk = (Keys.libraryDependencies in (GlobalScope in extracted.currentRef)).scopedKey - val empty = extracted.structure.data set (sk.scope, sk.key, Nil) - val settings = extracted.structure.settings filter { s: Setting[_] => - (s.key.key == Keys.libraryDependencies.key) && - (s.key.scope.project == Select(extracted.currentRef)) - } - settings flatMap { - case s: Setting[Seq[ModuleID]] @unchecked => - s.init.evaluate(empty) map { _ -> s.pos } - } - } getOrElse Seq() - } catch { - case _: Throwable => Seq() - } - def modulePosition(m0: ModuleID): Option[String] = - modulePositions.find { - case (m, p) => - (m.organization == m0.organization) && - (m0.name startsWith m.name) && - (m.revision == m0.revision) - } flatMap { - case (m, LinePosition(path, start)) => - Some(s" ($path#L$start)") - case (m, RangePosition(path, LineRange(start, end))) => - Some(s" ($path#L$start-$end)") - case _ => None - } - val withExtra = err.failed.filter(!_.extraDependencyAttributes.isEmpty) - val failedPaths = err.failed map { x: ModuleID => - err.failedPaths(x).toList.reverse map { id => - (id, modulePosition(id)) - } - } - UnresolvedDependencyWarning(withExtra, failedPaths) - } - - def apply(withExtra: Seq[ModuleID], - failedPaths: Seq[Seq[(ModuleID, Option[String])]]): UnresolvedDependencyWarning = - new UnresolvedDependencyWarning(withExtra, failedPaths) - - implicit val unresolvedDependencyWarningLines: ShowLines[UnresolvedDependencyWarning] = ShowLines { a => - val buffer = mutable.ListBuffer[String]() - if (!a.withExtra.isEmpty) { - buffer += "\n\tNote: Some unresolved dependencies have extra attributes. Check that these dependencies exist with the requested attributes." - a.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 + head._2.getOrElse("") - path.tail foreach { - case (m, pos) => - buffer += "\t\t +- " + m.toString + pos.getOrElse("") - } - } - } - } - buffer.toList - } -}