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 + } +} diff --git a/main/actions/src/main/scala/sbt/CacheIvy.scala b/main/actions/src/main/scala/sbt/CacheIvy.scala index 3b74251d4..ace282948 100644 --- a/main/actions/src/main/scala/sbt/CacheIvy.scala +++ b/main/actions/src/main/scala/sbt/CacheIvy.scala @@ -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 diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 12dbb1387..97518bca0 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -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 = diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 9d420ad46..8ff0aefe1 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -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) diff --git a/notes/0.13.6.md b/notes/0.13.6.md index 7e635ad31..1ecaac563 100644 --- a/notes/0.13.6.md +++ b/notes/0.13.6.md @@ -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