From 8fd40e56dffba2878efdd90ca73f3ec28defcb40 Mon Sep 17 00:00:00 2001 From: jvican Date: Sun, 5 Mar 2017 19:05:34 +0100 Subject: [PATCH] Ease `updateTask` and `cachedUpdate` maintenance This commit does some changes to the implementation with the purpose of making this code more readable. I find that this rewrite was necessary as I was implementing the dependency lock file. --- main/src/main/scala/sbt/Defaults.scala | 290 ++++++++++++++----------- 1 file changed, 159 insertions(+), 131 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 962771d84..d906b244b 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -2051,75 +2051,89 @@ object Classpaths { ) } - def updateTask: Initialize[Task[UpdateReport]] = Def.task { - val depsUpdated = transitiveUpdate.value.exists(!_.stats.cached) - val isRoot = executionRoots.value contains resolvedScoped.value - val forceUpdate = forceUpdatePeriod.value - val s = streams.value - val fullUpdateOutput = s.cacheDirectory / "out" - val forceUpdateByTime = forceUpdate match { - case None => false - case Some(period) => - val elapsedDuration = - new FiniteDuration(System.currentTimeMillis() - fullUpdateOutput.lastModified(), - TimeUnit.MILLISECONDS) - fullUpdateOutput.exists() && elapsedDuration > period - } - val scalaProvider = appConfiguration.value.provider.scalaProvider - - // Only substitute unmanaged jars for managed jars when the major.minor parts of the versions the same for: - // the resolved Scala version and the scalaHome version: compatible (weakly- no qualifier checked) - // the resolved Scala version and the declared scalaVersion: assume the user intended scalaHome to override anything with scalaVersion - def subUnmanaged(subVersion: String, jars: Seq[File]) = - (sv: String) => - (partialVersion(sv), partialVersion(subVersion), partialVersion(scalaVersion.value)) match { - case (Some(res), Some(sh), _) if res == sh => jars - case (Some(res), _, Some(decl)) if res == decl => jars - case _ => Nil + /** + * Substitute unmanaged jars for managed jars when the major.minor parts of + * the version are the same for: + * 1. The Scala version and the `scalaHome` (unmanaged) version are equal. + * 2. The Scala version and the `declared` (managed) version are equal. + * + * Equality is weak, that is, no version qualifier is checked. + */ + private def unmanagedJarsTask(scalaVersion: String, unmanagedVersion: String, jars: Seq[File]) = { + (subVersion0: String) => + val scalaV = partialVersion(scalaVersion) + val managedV = partialVersion(subVersion0) + val unmanagedV = partialVersion(unmanagedVersion) + (managedV, unmanagedV, scalaV) match { + case (Some(mv), Some(uv), _) if mv == uv => jars + case (Some(mv), _, Some(sv)) if mv == sv => jars + case _ => Nil + } + } + + def updateTask: Initialize[Task[UpdateReport]] = Def.task { + val s = streams.value + val cacheDirectory = streams.value.cacheDirectory + + val isRoot = executionRoots.value contains resolvedScoped.value + val shouldForce = isRoot || { + forceUpdatePeriod.value match { + case None => false + case Some(period) => + val fullUpdateOutput = cacheDirectory / "out" + val now = System.currentTimeMillis + val diff = now - fullUpdateOutput.lastModified() + val elapsedDuration = new FiniteDuration(diff, TimeUnit.MILLISECONDS) + fullUpdateOutput.exists() && elapsedDuration > period } - val subScalaJars: String => Seq[File] = Defaults.unmanagedScalaInstanceOnly.value match { - case Some(si) => subUnmanaged(si.version, si.allJars) - 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) - val st = state.value - val logicalClock = LogicalClock(st.hashCode) - val depDir = dependencyCacheDirectory.value - val uc0 = updateConfiguration.value - val ms = publishMavenStyle.value - val cw = compatibilityWarningOptions.value - // Normally, log would capture log messages at all levels. - // Ivy logs are treated specially using sbt.UpdateConfiguration.logging. - // This code bumps up the sbt.UpdateConfiguration.logging to Full when logLevel is Debug. - import UpdateLogging.{ Full, DownloadOnly, Default } - val uc = (logLevel in update).?.value orElse st.get(logLevel.key) match { - case Some(Level.Debug) if uc0.logging == Default => uc0.withLogging(Full) - case Some(x) if uc0.logging == Default => uc0.withLogging(DownloadOnly) - case _ => uc0 + + val providedScalaJars: String => Seq[File] = { + val scalaProvider = appConfiguration.value.provider.scalaProvider + Defaults.unmanagedScalaInstanceOnly.value match { + case Some(instance) => + unmanagedJarsTask(scalaVersion.value, instance.version, instance.allJars) + case None => + (subVersion: String) => + if (scalaProvider.version == subVersion) scalaProvider.jars else Nil + } } - val ewo = - if (executionRoots.value exists { _.key == evicted.key }) EvictionWarningOptions.empty + + val state0 = state.value + val updateConf = { + // Log captures log messages at all levels, except ivy logs. + // Use full level when debug is enabled so that ivy logs are shown. + import UpdateLogging.{ Full, DownloadOnly, Default } + val conf = updateConfiguration.value + val maybeUpdateLevel = (logLevel in update).?.value + maybeUpdateLevel.orElse(state0.get(logLevel.key)) match { + case Some(Level.Debug) if conf.logging == Default => conf.withLogging(logging = Full) + case Some(_) if conf.logging == Default => conf.withLogging(logging = DownloadOnly) + case _ => conf + } + } + + val evictionOptions = { + if (executionRoots.value.exists(_.key == evicted.key)) + EvictionWarningOptions.empty else (evictionWarningOptions in update).value + } + cachedUpdate( - s.cacheStoreFactory sub updateCacheName.value, - show, + s.cacheStoreFactory.sub(updateCacheName.value), + Reference.display(thisProjectRef.value), ivyModule.value, - uc, - transform, + updateConf, + substituteScalaFiles(scalaOrganization.value, _)(providedScalaJars), skip = (skip in update).value, - force = isRoot || forceUpdateByTime, - depsUpdated = depsUpdated, - uwConfig = uwConfig, - logicalClock = logicalClock, - depDir = Some(depDir), - ewo = ewo, - mavenStyle = ms, - compatWarning = cw, + force = shouldForce, + depsUpdated = transitiveUpdate.value.exists(!_.stats.cached), + uwConfig = (unresolvedWarningConfiguration in update).value, + logicalClock = LogicalClock(state0.hashCode), + depDir = Some(dependencyCacheDirectory.value), + ewo = evictionOptions, + mavenStyle = publishMavenStyle.value, + compatWarning = compatibilityWarningOptions.value, log = s.log ) } @@ -2230,87 +2244,101 @@ object Classpaths { } } - private[sbt] def cachedUpdate(cacheStoreFactory: CacheStoreFactory, - label: String, - module: IvySbt#Module, - config: UpdateConfiguration, - transform: UpdateReport => UpdateReport, - skip: Boolean, - force: Boolean, - depsUpdated: Boolean, - uwConfig: UnresolvedWarningConfiguration, - logicalClock: LogicalClock, - depDir: Option[File], - ewo: EvictionWarningOptions, - mavenStyle: Boolean, - compatWarning: CompatibilityWarningOptions, - log: Logger): UpdateReport = { - type In = IvyConfiguration :+: ModuleSettings :+: UpdateConfiguration :+: HNil + private type UpdateInputs = IvyConfiguration :+: ModuleSettings :+: UpdateConfiguration :+: HNil - def work = (_: In) match { - case conf :+: settings :+: config :+: HNil => - import ShowLines._ - log.info("Updating " + label + "...") - val r = - IvyActions.updateEither(module, config, uwConfig, logicalClock, depDir, log) match { - case Right(ur) => ur - case Left(uw) => - uw.lines foreach { log.warn(_) } - throw uw.resolveException - } - log.info("Done updating.") - val result = transform(r) - val ew = EvictionWarning(module, ewo, result, log) - ew.lines foreach { log.warn(_) } - ew.infoAllTheThings foreach { log.info(_) } - CompatibilityWarning.run(compatWarning, module, mavenStyle, log) - result + private[sbt] def cachedUpdate( + cacheStoreFactory: CacheStoreFactory, + label: String, + module: IvySbt#Module, + updateConfig: UpdateConfiguration, + transform: UpdateReport => UpdateReport, + skip: Boolean, + force: Boolean, + depsUpdated: Boolean, + uwConfig: UnresolvedWarningConfiguration, + logicalClock: LogicalClock, + depDir: Option[File], + ewo: EvictionWarningOptions, + mavenStyle: Boolean, + compatWarning: CompatibilityWarningOptions, + log: Logger + ): UpdateReport = { + + /* Resolve the module settings from the inputs. */ + def resolve(inputs: UpdateInputs): UpdateReport = { + import ShowLines._ + + log.info(s"Updating $label...") + val reportOrUnresolved: Either[UnresolvedWarning, UpdateReport] = + IvyActions.updateEither(module, updateConfig, uwConfig, logicalClock, depDir, log) + + val report = reportOrUnresolved match { + case Right(report0) => report0 + case Left(unresolvedWarning) => + unresolvedWarning.lines.foreach(log.warn(_)) + throw unresolvedWarning.resolveException + } + log.info("Done updating.") + val finalReport = transform(report) + + // Warn of any eviction and compatibility warnings + val ew = EvictionWarning(module, ewo, finalReport, log) + ew.lines foreach { log.warn(_) } + ew.infoAllTheThings foreach { log.info(_) } + CompatibilityWarning.run(compatWarning, module, mavenStyle, log) + + finalReport } - def uptodate(inChanged: Boolean, out: UpdateReport): Boolean = - !force && - !depsUpdated && - !inChanged && - out.allFiles.forall(f => fileUptodate(f, out.stamps)) && - fileUptodate(out.cachedDescriptor, out.stamps) - val outStore = cacheStoreFactory make "output" - def skipWork: In => UpdateReport = { + /* Check if a update report is still up to date or we must resolve again. */ + def upToDate(inChanged: Boolean, out: UpdateReport): Boolean = { + !force && + !depsUpdated && + !inChanged && + out.allFiles.forall(f => fileUptodate(f, out.stamps)) && + fileUptodate(out.cachedDescriptor, out.stamps) + } + + /* Skip resolve if last output exists, otherwise error. */ + def skipResolve(cache: CacheStore): UpdateInputs => UpdateReport = { import LibraryManagementCodec._ - Tracked.lastOutput[In, UpdateReport](outStore) { + Tracked.lastOutput[UpdateInputs, UpdateReport](cache) { case (_, Some(out)) => out case _ => sys.error("Skipping update requested, but update has not previously run successfully.") } } - def doWorkInternal = { (inChanged: Boolean, in: In) => - import LibraryManagementCodec._ - val outCache = Tracked.lastOutput[In, UpdateReport](outStore) { - case (_, Some(out)) if uptodate(inChanged, out) => out - case _ => work(in) + + def doResolve(cache: CacheStore): UpdateInputs => UpdateReport = { + val doCachedResolve = { (inChanged: Boolean, updateInputs: UpdateInputs) => + import LibraryManagementCodec._ + val cachedResolve = Tracked.lastOutput[UpdateInputs, UpdateReport](cache) { + case (_, Some(out)) if upToDate(inChanged, out) => out + case _ => resolve(updateInputs) + } + import scala.util.control.Exception.catching + catching(classOf[NullPointerException], classOf[OutOfMemoryError]) + .withApply { t => + val resolvedAgain = resolve(updateInputs) + val culprit = t.getClass.getSimpleName + log.warn(s"Update task caching failed due to $culprit.") + log.warn("Report the following output to sbt:") + resolvedAgain.toString.lines foreach { log.warn(_) } + log.trace(t) + resolvedAgain + } + .apply(cachedResolve(updateInputs)) } - try { - outCache(in) - } catch { - case e: NullPointerException => - val r = work(in) - log.warn("Update task has failed to cache the report due to null.") - log.warn("Report the following output to sbt:") - r.toString.lines foreach { log.warn(_) } - log.trace(e) - r - case e: OutOfMemoryError => - val r = work(in) - log.warn("Update task has failed to cache the report due to OutOfMemoryError.") - log.trace(e) - r - } - } - def doWork: In => UpdateReport = { import AltLibraryManagementCodec._ - Tracked.inputChanged(cacheStoreFactory make "inputs")(doWorkInternal) + Tracked.inputChanged(cacheStoreFactory.make("inputs"))(doCachedResolve) } - val f = if (skip && !force) skipWork else doWork - f(module.owner.configuration :+: module.moduleSettings :+: config :+: HNil) + + // Get the handler to use and feed it in the inputs + val ivyConfig = module.owner.configuration + val settings = module.moduleSettings + val outStore = cacheStoreFactory.make("output") + val handler = if (skip && !force) skipResolve(outStore) else doResolve(outStore) + handler(ivyConfig :+: settings :+: updateConfig :+: HNil) } private[this] def fileUptodate(file: File, stamps: Map[File, Long]): Boolean =