Add offline mode to `UpdateConfiguration`

The following commit tries to address the well-known issue that sbt
cannot be used in offline mode. In order to enable that use case, this
commit adds support for a flag in update configuration called `offline`
that users can change as they wish (and that sbt will expose via
settings).

It adds tests to check that the resolution uses the caches instead of
trying to resolve from the Internet. Unfortunately, ivy does not expose
a way to know whether a resolution was made from the cache or the
Internet, so the test case invents a metric to check that resolution
indeed happens from cache.

In order to benefit from this 100%, we need to update to ivy 2.4.0 or
cherry-pick a commit because a major issue in `useCacheOnly` has been
fixed: https://issues.apache.org/jira/browse/IVY-1515.

In short, this is good for the dependency lock file too. Since we can
make sure that once we have downloaded and resolved all the dependencies
locally, we do resolve from the cache.
This commit is contained in:
jvican 2017-05-05 17:25:10 +02:00
parent 1a11fd86a3
commit 67d9012a17
No known key found for this signature in database
GPG Key ID: 42DAFA0F112E8050
6 changed files with 80 additions and 6 deletions

View File

@ -634,7 +634,8 @@
{ "name": "retrieve", "type": "sbt.internal.librarymanagement.RetrieveConfiguration?" },
{ "name": "missingOk", "type": "boolean" },
{ "name": "logging", "type": "sbt.librarymanagement.UpdateLogging" },
{ "name": "artifactFilter", "type": "sbt.librarymanagement.ArtifactTypeFilter" }
{ "name": "artifactFilter", "type": "sbt.librarymanagement.ArtifactTypeFilter" },
{ "name": "offline", "type": "boolean" }
]
},
{

View File

@ -467,6 +467,7 @@ object IvyActions {
val resolveId = ResolveOptions.getDefaultResolveId(moduleDescriptor)
resolveOptions.setResolveId(resolveId)
resolveOptions.setArtifactFilter(updateConfiguration.artifactFilter)
resolveOptions.setUseCacheOnly(updateConfiguration.offline)
resolveOptions.setLog(ivyLogLevel(logging))
ResolutionCache.cleanModule(
moduleDescriptor.getModuleRevisionId,
@ -519,6 +520,7 @@ object IvyActions {
val resolveId = ResolveOptions.getDefaultResolveId(descriptor)
resolveOptions.setResolveId(resolveId)
resolveOptions.setArtifactFilter(updateConfiguration.artifactFilter)
resolveOptions.setUseCacheOnly(updateConfiguration.offline)
resolveOptions.setLog(ivyLogLevel(updateConfiguration.logging))
val acceptError = updateConfiguration.missingOk
resolver.customResolve(descriptor, acceptError, logicalClock, resolveOptions, cache, log)

View File

@ -68,7 +68,8 @@ class DefaultLibraryManagement(ivyConfiguration: IvyConfiguration, log: Logger)
Some(retrieveConfiguration),
true,
UpdateLogging.DownloadOnly,
artifactFilter
artifactFilter,
false
)
log.debug(s"Attempting to fetch ${dependenciesNames(module)}. This operation may fail.")

View File

@ -69,18 +69,19 @@ trait BaseIvySpecification extends UnitSpec {
log)
}
def makeUpdateConfiguration: UpdateConfiguration = {
def makeUpdateConfiguration(offline: Boolean): UpdateConfiguration = {
val retrieveConfig =
RetrieveConfiguration(currentManaged, Resolver.defaultRetrievePattern).withSync(false)
UpdateConfiguration(Some(retrieveConfig),
false,
UpdateLogging.Full,
ArtifactTypeFilter.forbid(Set("src", "doc")))
ArtifactTypeFilter.forbid(Set("src", "doc")),
offline)
}
def ivyUpdateEither(module: IvySbt#Module): Either[UnresolvedWarning, UpdateReport] = {
// IO.delete(currentTarget)
val config = makeUpdateConfiguration
val config = makeUpdateConfiguration(false)
IvyActions.updateEither(module,
config,
UnresolvedWarningConfiguration(),

View File

@ -0,0 +1,69 @@
package sbt.librarymanagement
import org.scalatest.Assertion
import sbt.internal.librarymanagement._
import sbt.internal.librarymanagement.impl.DependencyBuilders
class OfflineModeSpec extends BaseIvySpecification with DependencyBuilders {
private final val targetDir = Some(currentDependency)
private final val onlineConf = makeUpdateConfiguration(false)
private final val offlineConf = makeUpdateConfiguration(true)
private final val noClock = LogicalClock.unknown
private final val warningConf = UnresolvedWarningConfiguration()
private final val normalOptions = UpdateOptions()
private final val cachedOptions = UpdateOptions().withCachedResolution(true)
final val scalaCompiler = Vector("org.scala-lang" % "scala-compiler" % "2.12.2" % "compile")
def checkOnlineAndOfflineResolution(updateOptions: UpdateOptions): Assertion = {
cleanIvyCache()
val toResolve = module(defaultModuleId, scalaCompiler, None, updateOptions)
val isCachedResolution = updateOptions.cachedResolution
if (isCachedResolution) cleanCachedResolutionCache(toResolve)
val onlineResolution =
IvyActions.updateEither(toResolve, onlineConf, warningConf, noClock, targetDir, log)
assert(onlineResolution.isRight)
assert(onlineResolution.right.exists(report => report.stats.resolveTime > 0))
// Compute an estimate to ensure that the second resolution does indeed use the cache
val resolutionTime: Long = onlineResolution.right.map(_.stats.resolveTime).getOrElse(0L)
val estimatedCachedTime = resolutionTime * 0.15
val offlineResolution =
IvyActions.updateEither(toResolve, offlineConf, warningConf, noClock, targetDir, log)
assert(offlineResolution.isRight)
if (!isCachedResolution) {
// Only check the estimate for the non cached resolution, otherwise resolution is cached
assert(offlineResolution.right.exists(_.stats.resolveTime <= estimatedCachedTime),
"Offline resolution took more than 15% of normal resolution's running time.")
} else assert(true) // We cannot check offline resolution if it's cached.
}
"Offline update configuration" should "reuse the caches when it's enabled" in {
checkOnlineAndOfflineResolution(normalOptions)
}
it should "work with cached resolution" in {
checkOnlineAndOfflineResolution(cachedOptions)
}
def checkFailingResolution(updateOptions: UpdateOptions): Assertion = {
cleanIvyCache()
val toResolve = module(defaultModuleId, scalaCompiler, None, updateOptions)
if (updateOptions.cachedResolution) cleanCachedResolutionCache(toResolve)
val failedOfflineResolution =
IvyActions.updateEither(toResolve, offlineConf, warningConf, noClock, targetDir, log)
assert(failedOfflineResolution.isLeft)
}
it should "fail when artifacts are missing in the cache" in {
checkFailingResolution(normalOptions)
}
it should "fail when artifacts are missing in the cache for cached resolution" in {
checkFailingResolution(cachedOptions)
}
}

View File

@ -44,7 +44,7 @@ class IvyRepoSpec extends BaseIvySpecification with DependencyBuilders {
val m = makeModuleForDepWithSources
// the "default" configuration used in updateEither.
val c = makeUpdateConfiguration
val c = makeUpdateConfiguration(false)
val ivyScala = m.moduleSettings.ivyScala
val srcTypes = Set("src")