Merge pull request #4476 from eatkins/classloaders

Add cache of layered classloaders for test and run tasks
This commit is contained in:
Ethan Atkins 2019-01-30 10:13:53 -08:00 committed by GitHub
commit 6e5caa90c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
137 changed files with 1832 additions and 83 deletions

View File

@ -23,7 +23,7 @@ env:
- SBT_CMD="scripted source-dependencies/*1of3"
- SBT_CMD="scripted source-dependencies/*2of3"
- SBT_CMD="scripted source-dependencies/*3of3"
- SBT_CMD="scripted tests/* watch/*"
- SBT_CMD="scripted tests/* watch/* classloader-cache/*"
- SBT_CMD="repoOverrideTest:scripted dependency-management/*"
matrix:
@ -53,7 +53,7 @@ install:
script:
# It doesn't need that much memory because compile and run are forked
- sbt -J-XX:ReservedCodeCacheSize=128m -J-Xmx800M -J-Xms800M -J-server "$SBT_CMD"
- sbt -Dsbt.version=1.2.6 -J-XX:ReservedCodeCacheSize=128m -J-Xmx800M -J-Xms800M -J-server "$SBT_CMD"
before_cache:
- find $HOME/.ivy2 -name "ivydata-*.properties" -delete

View File

@ -233,8 +233,6 @@ $AliasCommand name=
def continuousDetail: String = "Executes the specified command whenever source files change."
def continuousBriefHelp: (String, String) =
(ContinuousExecutePrefix + " <command>", continuousDetail)
def FlushFileTreeRepository: String = "flushFileTreeRepository"
def FlushDetailed: String =
"Resets the global file repository in the event that the repository has become inconsistent " +
"with the file system."
def ClearCaches: String = "clearCaches"
def ClearCachesDetailed: String = "Clears all of sbt's internal caches."
}

View File

@ -8,6 +8,7 @@
package sbt
import java.io.File
import sbt.internal.util.AttributeKey
import sbt.internal.inc.classpath.ClassLoaderCache
import sbt.internal.server.ServerHandler

View File

@ -0,0 +1,122 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
/**
* Represents a ClassLoader layering strategy. By providing an instance of [[ClassLoaderLayeringStrategy]],
* users can configure the strategy that they want to use in various sbt tasks, most importantly
* [[Keys.run]] and [[Keys.test]]. This setting is only relevant if fork := false in the task for
* which we obtain a ClassLoaderLayeringStrategy.
*
* ClassLoaders can be composed of multiple ClassLoaders
* to form a graph for loading a class. The different portions of the graph may be cached and
* reused to minimize both the memory taken up by ClassLoaders (and the classes that they load) and
* the startup time for tasks like test and run. For example, the scala library is large and takes
* a while just to load the classes in predef. The [[Keys.scalaInstance]] task provides access to
* a classloader that can load all of the java bootstrap classes and scala.*. Supposing that we want
* to run code in a jar containing scala code called "foo_2.12.jar" in the base directory and that
* we have a scala instance in scope and suppose further that "foo_2.12.jar" contains a main method
* in the class foo.Main, then we can invoke foo.Main.main like so
*
* {{{
* val fooJarFile = new File("foo_2.12.jar")
* val classLoader = new URLClassLoader(
* Array(fooJarFile.toURI.toURL), scalaInstance.loaderLibraryOnly)
* val main = classLoader.loadClass("foo.Main").getDeclaredMethod("main", classOf[Array[String]])
* main.invoke(null, Array.empty[String])
* }}}
*
* Now suppose that we have an alternative jar "foo_alt_2.12.jar" that also provides foo.Main, then
* we can run that main method:
*
* {{{
* val fooJarFile = new File("foo_alt_2.12.jar")
* val altClassLoader = new URLClassLoader(
* Array(fooAltJarFile.toURI.toURL), scalaInstance.loaderLibraryOnly)
* val altMain = classLoader.loadClass("foo.Main").getDeclaredMethod("main", classOf[Array[String]])
* altMain.invoke(null, Array.empty[String])
* }}}
*
* In the second invocation, the scala library will have already been loaded by the
* scalaInstance.loaderLibraryOnly ClassLoader. This can reduce the startup time by O(500ms) and
* prevents an accumulation of scala related Class objects. Note that these ClassLoaders should
* only be used at a code boundary such that their loaded classes do not leak outside of the
* defining scope. This is because the layered class loaders can create mutually incompatible
* classes. For example, in the example above, suppose that there is a class foo.Bar provided
* by both "foo_2.12.jar" and "foo_2.12.jar" and that both also provide a static method
* "foo.Foo$.bar" that returns an instance of foo.Bar, then the following code will not work:
*
* {{{
* Thread.currentThread.setContextClassLoader(altClassLoader)
* val bar: Object = classLoader.loadClass("foo.Foo$").getDeclaredMethod("bar").invoke(null)
* val barTyped: foo.Bar = bar.asInstanceOf[foo.Bar]
* // throws ClassCastException because the thread context class loader is altClassLoader, but
* // but bar was loaded by classLoader.
* }}}
*
* In general, this should only happen if the user explicitly overrides the thread context
* ClassLoader or uses reflection to manipulate classes loaded by different loaders.
*/
sealed trait ClassLoaderLayeringStrategy
/**
* Provides instances of [[ClassLoaderLayeringStrategy]] that can be used to define the ClassLoader used by
* [[Keys.run]], [[Keys.test]] or any other task that runs java code inside of the sbt jvm.
*/
object ClassLoaderLayeringStrategy {
/**
* Include all of the dependencies in the loader. The base loader will be the Application
* ClassLoader. All classes apart from system classes will be reloaded with each run.
*/
case object Flat extends ClassLoaderLayeringStrategy
/**
* Add a layer for the scala instance class loader.
*/
sealed trait ScalaInstance extends ClassLoaderLayeringStrategy
/**
* This should indicate that we use a two layer ClassLoader where the top layer is the scala
* instance and all of the dependencies and project class paths are included in the search path
* of the second layer.
*/
case object ScalaInstance extends ScalaInstance
/**
* Add a layer on top of the ScalaInstance layer for the runtime jar dependencies.
*/
sealed trait RuntimeDependencies extends ScalaInstance
/**
* Add a layer on top of the ScalaInstance layer for the runtime jar dependencies.
*/
case object RuntimeDependencies extends ScalaInstance with RuntimeDependencies
/**
* Add a layer on top of the ScalaInstance layer for the test jar dependencies.
*/
sealed trait TestDependencies extends ScalaInstance
/**
* Add a layer on top of the ScalaInstance layer for the test jar dependencies.
*/
case object TestDependencies extends ScalaInstance with TestDependencies
/**
* Add the TestDependencies layer on top of the RuntimeDependencies layer on top of the
* ScalaInstance layer. This differs from TestDependencies in that it will not reload the
* runtime classpath. The drawback to using this is that if the test dependencies evict
* classes provided in the runtime layer, then tests can fail.
*/
case object ShareRuntimeDependenciesLayerWithTestDependencies
extends ScalaInstance
with RuntimeDependencies
with TestDependencies
}

View File

@ -139,8 +139,10 @@ object Defaults extends BuildCommon {
)
private[sbt] lazy val globalCore: Seq[Setting[_]] = globalDefaults(
defaultTestTasks(test) ++ defaultTestTasks(testOnly) ++ defaultTestTasks(testQuick) ++ Seq(
excludeFilter :== HiddenFileFilter
) ++ globalIvyCore ++ globalJvmCore
excludeFilter :== HiddenFileFilter,
classLoaderCache := ClassLoaderCache(4),
) ++ TaskRepository
.proxy(GlobalScope / classLoaderCache, ClassLoaderCache(4)) ++ globalIvyCore ++ globalJvmCore
) ++ globalSbtCore
private[sbt] lazy val globalJvmCore: Seq[Setting[_]] =
@ -790,11 +792,7 @@ object Defaults extends BuildCommon {
: Seq[Setting[_]] = testTaskOptions(test) ++ testTaskOptions(testOnly) ++ testTaskOptions(
testQuick
) ++ testDefaults ++ Seq(
testLoader := TestFramework.createTestLoader(
data(fullClasspath.value),
scalaInstance.value,
IO.createUniqueDirectory(taskTemporaryDirectory.value)
),
testLoader := ClassLoaders.testTask.value,
loadedTestFrameworks := {
val loader = testLoader.value
val log = streams.value.log
@ -813,7 +811,8 @@ object Defaults extends BuildCommon {
(testExecution in test).value,
(fullClasspath in test).value,
testForkedParallel.value,
(javaOptions in test).value
(javaOptions in test).value,
(classLoaderLayeringStrategy).value
)
}
).value,
@ -979,7 +978,8 @@ object Defaults extends BuildCommon {
newConfig,
fullClasspath.value,
testForkedParallel.value,
javaOptions.value
javaOptions.value,
classLoaderLayeringStrategy.value
)
val taskName = display.show(resolvedScoped.value)
val trl = testResultLogger.value
@ -1022,7 +1022,8 @@ object Defaults extends BuildCommon {
config,
cp,
forkedParallelExecution = false,
javaOptions = Nil
javaOptions = Nil,
strategy = ClassLoaderLayeringStrategy.TestDependencies,
)
}
@ -1043,7 +1044,8 @@ object Defaults extends BuildCommon {
config,
cp,
forkedParallelExecution,
javaOptions = Nil
javaOptions = Nil,
strategy = ClassLoaderLayeringStrategy.TestDependencies,
)
}
@ -1055,7 +1057,8 @@ object Defaults extends BuildCommon {
config: Tests.Execution,
cp: Classpath,
forkedParallelExecution: Boolean,
javaOptions: Seq[String]
javaOptions: Seq[String],
strategy: ClassLoaderLayeringStrategy,
): Initialize[Task[Tests.Output]] = {
val runners = createTestRunners(frameworks, loader, config)
val groupTasks = groups map {
@ -1083,6 +1086,25 @@ object Defaults extends BuildCommon {
}
val output = Tests.foldTasks(groupTasks, config.parallel)
val result = output map { out =>
out.events.foreach {
case (suite, e) =>
e.throwables
.collectFirst {
case t
if t
.isInstanceOf[NoClassDefFoundError] && strategy != ClassLoaderLayeringStrategy.Flat =>
t
}
.foreach { t =>
s.log.error(
s"Test suite $suite failed with $t. This may be due to the ClassLoaderLayeringStrategy"
+ s" ($strategy) used by your task. This issue may be resolved by changing the"
+ " ClassLoaderLayeringStrategy in your configuration (generally Test or IntegrationTest),"
+ "e.g.:\nTest / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat\n"
+ "See ClassLoaderLayeringStrategy.scala for the full list of options."
)
}
}
val summaries =
runners map {
case (tf, r) =>
@ -1384,37 +1406,11 @@ object Defaults extends BuildCommon {
}
}
@deprecated("This is no longer used internally by sbt.", "1.3.0")
def runnerTask: Setting[Task[ScalaRun]] = runner := runnerInit.value
def runnerInit: Initialize[Task[ScalaRun]] = Def.task {
val tmp = taskTemporaryDirectory.value
val resolvedScope = resolvedScoped.value.scope
val si = scalaInstance.value
val s = streams.value
val opts = forkOptions.value
val options = javaOptions.value
val trap = trapExit.value
if (fork.value) {
s.log.debug(s"javaOptions: $options")
new ForkRun(opts)
} else {
if (options.nonEmpty) {
val mask = ScopeMask(project = false)
val showJavaOptions = Scope.displayMasked(
(javaOptions in resolvedScope).scopedKey.scope,
(javaOptions in resolvedScope).key.label,
mask
)
val showFork = Scope.displayMasked(
(fork in resolvedScope).scopedKey.scope,
(fork in resolvedScope).key.label,
mask
)
s.log.warn(s"$showJavaOptions will be ignored, $showFork is set to false")
}
new Run(si, trap, tmp)
}
}
@deprecated("This is no longer used internally by sbt.", "1.3.0")
def runnerInit: Initialize[Task[ScalaRun]] = ClassLoaders.runner
private def foreachJobTask(
f: (BackgroundJobService, JobHandle) => Unit
@ -1764,7 +1760,11 @@ object Defaults extends BuildCommon {
// 1. runnerSettings is added unscoped via JvmPlugin.
// 2. In addition it's added scoped to run task.
lazy val runnerSettings: Seq[Setting[_]] = Seq(runnerTask, forkOptions := forkOptionsTask.value)
lazy val runnerSettings: Seq[Setting[_]] = {
val unscoped: Seq[Def.Setting[_]] =
Seq(runner := ClassLoaders.runner.value, forkOptions := forkOptionsTask.value)
inConfig(Compile)(unscoped) ++ inConfig(Test)(unscoped)
}
lazy val baseTasks: Seq[Setting[_]] = projectTasks ++ packageBase
@ -1772,16 +1772,32 @@ object Defaults extends BuildCommon {
Classpaths.configSettings ++ configTasks ++ configPaths ++ packageConfig ++
Classpaths.compilerPluginConfig ++ deprecationSettings
private val runtimeLayeringSettings: Seq[Setting[_]] = TaskRepository.proxy(
classLoaderCache,
// We need a cache of size four so that the subset of the runtime dependencies that are used
// by the test task layers may be cached without evicting the runtime classloader layers. The
// cache size should be a multiple of two to support snapshot layers.
ClassLoaderCache(4)
) :+ (classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.RuntimeDependencies)
lazy val compileSettings: Seq[Setting[_]] =
configSettings ++
(mainBgRunMainTask +: mainBgRunTask +: FileManagement.appendBaseSources) ++
Classpaths.addUnmanagedLibrary
Classpaths.addUnmanagedLibrary ++ runtimeLayeringSettings
private val testLayeringSettings: Seq[Setting[_]] = TaskRepository.proxy(
classLoaderCache,
// We need a cache of size two for the test dependency layers (regular and snapshot).
ClassLoaderCache(2)
) :+ (classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.TestDependencies)
lazy val testSettings: Seq[Setting[_]] = configSettings ++ testTasks
lazy val itSettings: Seq[Setting[_]] = inConfig(IntegrationTest)(testSettings)
lazy val itSettings: Seq[Setting[_]] = inConfig(IntegrationTest) {
testSettings :+ (classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.RuntimeDependencies)
}
lazy val defaultConfigs: Seq[Setting[_]] = inConfig(Compile)(compileSettings) ++
inConfig(Test)(testSettings) ++ inConfig(Runtime)(Classpaths.configSettings)
inConfig(Test)(testSettings ++ testLayeringSettings) ++
inConfig(Runtime)(Classpaths.configSettings)
// These are project level settings that MUST be on every project.
lazy val coreDefaultSettings: Seq[Setting[_]] =
@ -3556,17 +3572,19 @@ trait BuildExtra extends BuildCommon with DefExtra {
InputTask.apply(Def.value((s: State) => Def.spaceDelimited()))(f)
Vector(
scoped := (inputTask { result =>
(initScoped(scoped.scopedKey, runnerInit)
.zipWith(Def.task { ((fullClasspath in config).value, streams.value, result.value) })) {
scoped := inputTask { result =>
initScoped(
scoped.scopedKey,
ClassLoaders.runner mapReferenced Project.mapScope(s => s.in(config))
).zipWith(Def.task { ((fullClasspath in config).value, streams.value, result.value) }) {
(rTask, t) =>
(t, rTask) map {
case ((cp, s, args), r) =>
r.run(mainClass, data(cp), baseArguments ++ args, s.log).get
}
}
}).evaluated
) ++ inTask(scoped)(forkOptions := forkOptionsTask.value)
}.evaluated
) ++ inTask(scoped)(forkOptions in config := forkOptionsTask.value)
}
// public API
@ -3578,16 +3596,18 @@ trait BuildExtra extends BuildCommon with DefExtra {
arguments: String*
): Vector[Setting[_]] =
Vector(
scoped := ((initScoped(scoped.scopedKey, runnerInit)
.zipWith(Def.task { ((fullClasspath in config).value, streams.value) })) {
scoped := initScoped(
scoped.scopedKey,
ClassLoaders.runner mapReferenced Project.mapScope(s => s.in(config))
).zipWith(Def.task { ((fullClasspath in config).value, streams.value) }) {
case (rTask, t) =>
(t, rTask) map {
case ((cp, s), r) =>
r.run(mainClass, data(cp), arguments, s.log).get
}
})
}
.value
) ++ inTask(scoped)(forkOptions := forkOptionsTask.value)
) ++ inTask(scoped)(forkOptions in config := forkOptionsTask.value)
def initScoped[T](sk: ScopedKey[_], i: Initialize[T]): Initialize[T] =
initScope(fillTaskAxis(sk.scope, sk.key), i)

View File

@ -264,6 +264,7 @@ object Keys {
val bgRunMain = inputKey[JobHandle]("Start a provided main class as a background job")
val fgRunMain = inputKey[Unit]("Start a provided main class as a foreground job")
val bgCopyClasspath = settingKey[Boolean]("Copies classpath on bgRun to prevent conflict.")
val classLoaderLayeringStrategy = settingKey[ClassLoaderLayeringStrategy]("Creates the classloader layering strategy for the particular configuration.")
// Test Keys
val testLoader = taskKey[ClassLoader]("Provides the class loader used for testing.").withRank(DTask)
@ -460,6 +461,8 @@ object Keys {
val resolvedScoped = Def.resolvedScoped
val pluginData = taskKey[PluginData]("Information from the plugin build needed in the main build definition.").withRank(DTask)
val globalPluginUpdate = taskKey[UpdateReport]("A hook to get the UpdateReport of the global plugin.").withRank(DTask)
val classLoaderCache = taskKey[internal.ClassLoaderCache]("The cache of ClassLoaders to be used for layering in tasks that invoke other java code").withRank(DTask)
private[sbt] val taskRepository = AttributeKey[TaskRepository.Repr]("task-repository", "A repository that can be used to cache arbitrary values for a given task key that can be read or filled during task evaluation.", 10000)
// wrapper to work around SI-2915
private[sbt] final class TaskProgress(val progress: ExecuteProgress[Task])

View File

@ -214,7 +214,7 @@ object BuiltinCommands {
BasicCommands.multi,
act,
continuous,
flushFileTreeRepository
clearCaches
) ++ allBasicCommands
def DefaultBootCommands: Seq[String] =
@ -830,7 +830,7 @@ object BuiltinCommands {
val session = Load.initialSession(structure, eval, s0)
SessionSettings.checkSession(session, s)
registerGlobalFileRepository(Project.setProject(session, structure, s))
registerGlobalCaches(Project.setProject(session, structure, s))
}
def registerCompilerCache(s: State): State = {
@ -848,26 +848,31 @@ object BuiltinCommands {
}
s.put(Keys.stateCompilerCache, cache)
}
def registerGlobalFileRepository(s: State): State = {
private[sbt] def registerGlobalCaches(s: State): State = {
val extracted = Project.extract(s)
try {
val (_, config: FileTreeViewConfig) = extracted.runTask(Keys.fileTreeViewConfig, s)
val view: FileTreeDataView[StampedFile] = config.newDataView()
val newState = s.addExitHook {
def cleanup(): Unit = {
s.get(BasicKeys.globalFileTreeView).foreach(_.close())
s.attributes.remove(BasicKeys.globalFileTreeView)
s.get(Keys.taskRepository).foreach(_.close())
s.attributes.remove(Keys.taskRepository)
()
}
newState.get(BasicKeys.globalFileTreeView).foreach(_.close())
newState.put(BasicKeys.globalFileTreeView, view)
val (_, config: FileTreeViewConfig) = extracted.runTask(Keys.fileTreeViewConfig, s)
val view: FileTreeDataView[StampedFile] = config.newDataView()
val newState = s.addExitHook(cleanup())
cleanup()
newState
.put(BasicKeys.globalFileTreeView, view)
.put(Keys.taskRepository, new TaskRepository.Repr)
} catch {
case NonFatal(_) => s
}
}
def flushFileTreeRepository: Command = {
val help = Help.more(FlushFileTreeRepository, FlushDetailed)
Command.command(FlushFileTreeRepository, help)(registerGlobalFileRepository)
def clearCaches: Command = {
val help = Help.more(ClearCaches, ClearCachesDetailed)
Command.command(ClearCaches, help)(registerGlobalCaches)
}
def shell: Command = Command.command(Shell, Help.more(Shell, ShellDetailed)) { s0 =>

View File

@ -0,0 +1,98 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal
import java.io.File
import java.nio.file.Files
import sbt.internal.util.TypeFunctions.Id
import scala.annotation.tailrec
sealed trait ClassLoaderCache
extends Repository[Id, (Seq[File], ClassLoader, Map[String, String], File), ClassLoader]
object ClassLoaderCache {
private type Resources = Map[String, String]
private sealed trait CachedClassLoader extends ClassLoader {
def close(): Unit
}
private sealed trait StableClassLoader extends CachedClassLoader
private sealed trait SnapshotClassLoader extends CachedClassLoader
def apply(maxSize: Int): ClassLoaderCache =
new ClassLoaderCache {
private final def mktmp(tmp: File): File =
if (maxSize > 0) Files.createTempDirectory("sbt-jni").toFile else tmp
private[this] val lruCache =
LRUCache[(JarClassPath, ClassLoader), (JarClassPath, CachedClassLoader)](
maxSize = maxSize,
onExpire =
(_: (JarClassPath, ClassLoader), v: (JarClassPath, CachedClassLoader)) => close(v._2)
)
override def get(info: (Seq[File], ClassLoader, Resources, File)): ClassLoader =
synchronized {
val (paths, parent, resources, tmp) = info
val key @ (keyJCP, _) = (new JarClassPath(paths), parent)
def addLoader(base: Option[StableClassLoader] = None): CachedClassLoader = {
val baseLoader = base.getOrElse {
if (keyJCP.regularJars.isEmpty) new ClassLoader(parent) with StableClassLoader {
override def close(): Unit = parent match {
case s: StableClassLoader => s.close()
case _ => ()
}
override def toString: String = parent.toString
} else
new LayeredClassLoader(keyJCP.regularJars, parent, resources, mktmp(tmp))
with StableClassLoader
}
val loader: CachedClassLoader =
if (keyJCP.snapshotJars.isEmpty) baseLoader
else
new LayeredClassLoader(keyJCP.snapshotJars, baseLoader, resources, mktmp(tmp))
with SnapshotClassLoader
lruCache.put(key, keyJCP -> loader)
loader
}
lruCache.get(key) match {
case Some((jcp, cl)) if keyJCP.strictEquals(jcp) => cl
case Some((_, cl: SnapshotClassLoader)) =>
cl.close()
cl.getParent match {
case p: StableClassLoader => addLoader(Some(p))
case _ => addLoader()
}
case None => addLoader()
}
}
override def close(): Unit = synchronized(lruCache.close())
override def toString: String = {
import PrettyPrint.indent
val cacheElements = lruCache.entries.map {
case ((jcp, parent), (_, l)) =>
s"(\n${indent(jcp, 4)},\n${indent(parent, 4)}\n) =>\n $l"
}
s"ClassLoaderCache(\n size = $maxSize,\n elements =\n${indent(cacheElements, 4)}\n)"
}
// Close the ClassLoader and all of it's closeable parents.
@tailrec
private def close(loader: CachedClassLoader): Unit = {
loader.close()
loader.getParent match {
case c: CachedClassLoader => close(c)
case _ => ()
}
}
}
def empty(newLoader: (Seq[File], ClassLoader, Resources, File) => ClassLoader): ClassLoaderCache =
new ClassLoaderCache {
override def get(key: (Seq[File], ClassLoader, Resources, File)): ClassLoader =
newLoader.tupled(key)
override def close(): Unit = {}
}
}

View File

@ -0,0 +1,249 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package internal
import java.io.File
import java.net.URLClassLoader
import sbt.Keys._
import sbt.SlashSyntax0._
import sbt.internal.inc.ScalaInstance
import sbt.internal.inc.classpath.{ ClasspathUtilities, DualLoader, NullLoader }
import sbt.internal.util.Attributed
import sbt.internal.util.Attributed.data
import sbt.io.IO
import sbt.librarymanagement.Configurations.Runtime
import PrettyPrint.indent
import scala.annotation.tailrec
import ClassLoaderLayeringStrategy.{ ScalaInstance => ScalaInstanceLayer, _ }
private[sbt] object ClassLoaders {
private[this] lazy val interfaceLoader =
combine(
classOf[sbt.testing.Framework].getClassLoader,
new NullLoader,
toString = "sbt.testing.Framework interface ClassLoader"
)
private[this] lazy val baseLoader = {
@tailrec
def getBase(classLoader: ClassLoader): ClassLoader = classLoader.getParent match {
case null => classLoader
case loader => getBase(loader)
}
getBase(ClassLoaders.getClass.getClassLoader)
}
/*
* Get the class loader for a test task. The configuration could be IntegrationTest or Test.
*/
private[sbt] def testTask: Def.Initialize[Task[ClassLoader]] = Def.task {
val si = scalaInstance.value
val rawCP = data(fullClasspath.value)
val fullCP = if (si.isManagedVersion) rawCP else si.allJars.toSeq ++ rawCP
val exclude = dependencyJars(exportedProducts).value.toSet ++ si.allJars.toSeq
buildLayers(
classLoaderLayeringStrategy.value,
si,
fullCP,
dependencyJars(Runtime / dependencyClasspath).value.filterNot(exclude),
dependencyJars(dependencyClasspath).value.filterNot(exclude).toSet,
interfaceLoader,
(Runtime / classLoaderCache).value,
classLoaderCache.value,
ClasspathUtilities.createClasspathResources(fullCP, si),
IO.createUniqueDirectory(taskTemporaryDirectory.value),
resolvedScoped.value.scope
)
}
private[sbt] def runner: Def.Initialize[Task[ScalaRun]] = Def.taskDyn {
val resolvedScope = resolvedScoped.value.scope
val instance = scalaInstance.value
val s = streams.value
val opts = forkOptions.value
val options = javaOptions.value
if (fork.value) {
s.log.debug(s"javaOptions: $options")
Def.task(new ForkRun(opts))
} else {
Def.task {
if (options.nonEmpty) {
val mask = ScopeMask(project = false)
val showJavaOptions = Scope.displayMasked(
(javaOptions in resolvedScope).scopedKey.scope,
(javaOptions in resolvedScope).key.label,
mask
)
val showFork = Scope.displayMasked(
(fork in resolvedScope).scopedKey.scope,
(fork in resolvedScope).key.label,
mask
)
s.log.warn(s"$showJavaOptions will be ignored, $showFork is set to false")
}
val runtimeCache = (Runtime / classLoaderCache).value
val testCache = classLoaderCache.value
val exclude = dependencyJars(exportedProducts).value.toSet ++ instance.allJars
val newLoader =
(classpath: Seq[File]) => {
buildLayers(
classLoaderLayeringStrategy.value: @sbtUnchecked,
instance,
classpath,
(dependencyJars(Runtime / dependencyClasspath).value: @sbtUnchecked)
.filterNot(exclude),
(dependencyJars(dependencyClasspath).value: @sbtUnchecked).filterNot(exclude).toSet,
baseLoader,
runtimeCache,
testCache,
ClasspathUtilities.createClasspathResources(classpath, instance),
taskTemporaryDirectory.value: @sbtUnchecked,
resolvedScope
)
}
new Run(newLoader, trapExit.value)
}
}
}
/*
* Create a layered classloader. There are up to four layers:
* 1) the scala instance class loader
* 2) the runtime dependencies
* 3) the test dependencies
* 4) the rest of the classpath
* The first two layers may be optionally cached to reduce memory usage and improve
* start up latency. Because there may be mutually incompatible libraries in the runtime
* and test dependencies, it's important to be able to configure which layers are used.
*/
private def buildLayers(
strategy: ClassLoaderLayeringStrategy,
si: ScalaInstance,
fullCP: Seq[File],
rawRuntimeDependencies: Seq[File],
allDependencies: Set[File],
base: ClassLoader,
runtimeCache: ClassLoaderCache,
testCache: ClassLoaderCache,
resources: Map[String, String],
tmp: File,
scope: Scope
): ClassLoader = {
val isTest = scope.config.toOption.map(_.name) == Option("test")
val raw = strategy match {
case Flat => flatLoader(fullCP, base)
case _ =>
val (layerDependencies, layerTestDependencies) = strategy match {
case ShareRuntimeDependenciesLayerWithTestDependencies if isTest => (true, true)
case ScalaInstanceLayer => (false, false)
case RuntimeDependencies => (true, false)
case TestDependencies if isTest => (false, true)
case badStrategy =>
val msg = s"Layering strategy $badStrategy is not valid for the classloader in " +
s"$scope. Valid options are: ClassLoaderLayeringStrategy.{ " +
"Flat, ScalaInstance, RuntimeDependencies }"
throw new IllegalArgumentException(msg)
}
// The raw declarations are to avoid having to make a dynamic task. The
// allDependencies and allTestDependencies create a mutually exclusive list of jar
// dependencies for layers 2 and 3. Note that in the Runtime or Compile configs, it
// should always be the case that allTestDependencies == Nil
val allTestDependencies = if (layerTestDependencies) allDependencies else Set.empty[File]
val allRuntimeDependencies = (if (layerDependencies) rawRuntimeDependencies else Nil).toSet
// layer 2
val runtimeDependencies = allDependencies intersect allRuntimeDependencies
val runtimeLayer =
layer(runtimeDependencies.toSeq, loader(si), runtimeCache, resources, tmp)
// layer 3 (optional if testDependencies are empty)
// The top layer needs to include the interface jar or else the test task cannot be created.
// It needs to be separated from the runtimeLayer or else the runtimeLayer cannot be
// shared between the runtime and test tasks.
val top = combine(base, runtimeLayer)
val testDependencies = allTestDependencies diff runtimeDependencies
val testLayer = layer(testDependencies.toSeq, top, testCache, resources, tmp)
// layer 4
val dynamicClasspath =
fullCP.filterNot(testDependencies ++ runtimeDependencies ++ si.allJars)
if (dynamicClasspath.nonEmpty)
new LayeredClassLoader(dynamicClasspath, testLayer, resources, tmp)
else testLayer
}
ClasspathUtilities.filterByClasspath(fullCP, raw)
}
private def dependencyJars(
key: sbt.TaskKey[Seq[Attributed[File]]]
): Def.Initialize[Task[Seq[File]]] = Def.task(data(key.value).filter(_.getName.endsWith(".jar")))
// Creates a one or two layered classloader for the provided classpaths depending on whether
// or not the classpath contains any snapshots. If it does, the snapshots are placed in a layer
// above the regular jar layer. This allows the snapshot layer to be invalidated without
// invalidating the regular jar layer. If the classpath is empty, it just returns the parent
// loader.
private def layer(
classpath: Seq[File],
parent: ClassLoader,
cache: ClassLoaderCache,
resources: Map[String, String],
tmp: File
): ClassLoader = {
val (snapshots, jars) = classpath.partition(_.toString.contains("-SNAPSHOT"))
val jarLoader = if (jars.isEmpty) parent else cache.get((jars, parent, resources, tmp))
if (snapshots.isEmpty) jarLoader else cache.get((snapshots, jarLoader, resources, tmp))
}
// Code related to combining two classloaders that primarily exists so the test loader correctly
// loads the testing framework using the same classloader as sbt itself.
private val interfaceFilter = (name: String) =>
name.startsWith("org.scalatools.testing.") || name.startsWith("sbt.testing.") || name
.startsWith("java.") || name.startsWith("sun.")
private val notInterfaceFilter = (name: String) => !interfaceFilter(name)
private class WrappedDualLoader(
val parent: ClassLoader,
val child: ClassLoader,
string: => String
) extends ClassLoader(
new DualLoader(parent, interfaceFilter, _ => false, child, notInterfaceFilter, _ => true)
) {
override def equals(o: Any): Boolean = o match {
case that: WrappedDualLoader => this.parent == that.parent && this.child == that.child
case _ => false
}
override def hashCode: Int = (parent.hashCode * 31) ^ child.hashCode
override lazy val toString: String = string
}
private def combine(parent: ClassLoader, child: ClassLoader, toString: String): ClassLoader =
new WrappedDualLoader(parent, child, toString)
private def combine(parent: ClassLoader, child: ClassLoader): ClassLoader =
new WrappedDualLoader(
parent,
child,
s"WrappedDualLoader(\n parent =\n${indent(parent, 4)}"
+ s"\n child =\n${indent(child, 4)}\n)"
)
// helper methods
private def flatLoader(classpath: Seq[File], parent: ClassLoader): ClassLoader =
new URLClassLoader(classpath.map(_.toURI.toURL).toArray, parent)
// This makes the toString method of the ScalaInstance classloader much more readable, but
// it is not strictly necessary.
private def loader(si: ScalaInstance): ClassLoader = new ClassLoader(si.loader) {
override lazy val toString: String =
"ScalaInstanceClassLoader(\n instance = " +
s"${indent(si.toString.split(",").mkString("\n ", ",\n ", "\n"), 4)}\n)"
// Delegate equals to that.equals in case that is itself some kind of wrapped classloader that
// needs to delegate its equals method to the delegated ClassLoader.
override def equals(that: Any): Boolean = if (that != null) that.equals(si.loader) else false
override def hashCode: Int = si.loader.hashCode
}
}

View File

@ -26,7 +26,7 @@ private[sbt] object FileManagement {
val interactive = remaining.contains("shell") || remaining.lastOption.contains("iflast shell")
val scripted = remaining.contains("setUpScripted")
val continuous = remaining.exists(_.startsWith(ContinuousExecutePrefix))
val continuous = remaining.lastOption.exists(_.startsWith(ContinuousExecutePrefix))
if (!scripted && (interactive || continuous)) {
FileTreeViewConfig
.default(watchAntiEntropy.value, pollInterval.value, pollingDirectories.value)

View File

@ -0,0 +1,55 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal
import java.io.File
import sbt.io.IO
private[internal] object JarClassPath {
class Snapshot private[internal] (val file: File, val lastModified: Long)
extends Comparable[JarClassPath.Snapshot] {
private[this] val _hash = (file.hashCode * 31) ^ java.lang.Long.valueOf(lastModified).hashCode()
def this(file: File) = this(file, IO.getModifiedTimeOrZero(file))
override def equals(obj: Any): Boolean = obj match {
case that: JarClassPath.Snapshot =>
this.lastModified == that.lastModified && this.file == that.file
case _ => false
}
override def hashCode: Int = _hash
override def compareTo(that: JarClassPath.Snapshot): Int = this.file.compareTo(that.file)
override def toString: String =
"Snapshot(path = " + file + ", lastModified = " + lastModified + ")"
}
}
private[internal] final class JarClassPath(val jars: Seq[File]) {
private[this] def isSnapshot(file: File): Boolean = file.getName contains "-SNAPSHOT"
private val jarSet = jars.toSet
val (snapshotJars, regularJars) = jars.partition(isSnapshot)
private val snapshots = snapshotJars.map(new JarClassPath.Snapshot(_))
override def equals(obj: Any): Boolean = obj match {
case that: JarClassPath => this.jarSet == that.jarSet
case _ => false
}
// The memoization is because codacy isn't smart enough to identify that
// `override lazy val hashCode: Int = jarSet.hashCode` does actually override hashCode and it
// complains that equals and hashCode were not implemented together.
private[this] lazy val _hashCode: Int = jarSet.hashCode
override def hashCode: Int = _hashCode
override def toString: String =
s"JarClassPath(\n jars =\n ${regularJars.mkString(",\n ")}" +
s", snapshots =\n${snapshots.mkString(",\n ")}\n)"
/*
* This is a stricter equality requirement than equals that we can use for cache invalidation.
*/
private[internal] def strictEquals(that: JarClassPath): Boolean = {
this.equals(that) && !this.snapshots.view.zip(that.snapshots).exists { case (l, r) => l != r }
}
}

View File

@ -0,0 +1,113 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal
import java.util.concurrent.atomic.AtomicInteger
import scala.annotation.tailrec
private[sbt] sealed trait LRUCache[K, V] extends AutoCloseable {
def get(key: K): Option[V]
def entries: Seq[(K, V)]
def maxSize: Int
def put(key: K, value: V): Option[V]
def remove(key: K): Option[V]
def size: Int
}
private[sbt] object LRUCache {
private[this] class impl[K, V](override val maxSize: Int, onExpire: Option[((K, V)) => Unit])
extends LRUCache[K, V] {
private[this] val elementsSortedByAccess: Array[(K, V)] = new Array[(K, V)](maxSize)
private[this] val lastIndex: AtomicInteger = new AtomicInteger(-1)
override def close(): Unit = this.synchronized {
val f = onExpire.getOrElse((_: (K, V)) => Unit)
0 until maxSize foreach { i =>
elementsSortedByAccess(i) match {
case null =>
case el => f(el)
}
elementsSortedByAccess(i) = null
}
lastIndex.set(-1)
}
override def entries: Seq[(K, V)] = this.synchronized {
(0 to lastIndex.get()).map(elementsSortedByAccess)
}
override def get(key: K): Option[V] = this.synchronized {
indexOf(key) match {
case -1 => None
case i => replace(i, key, elementsSortedByAccess(i)._2)
}
}
override def put(key: K, value: V): Option[V] = this.synchronized {
indexOf(key) match {
case -1 =>
append(key, value)
None
case i => replace(i, key, value)
}
}
override def remove(key: K): Option[V] = this.synchronized {
indexOf(key) match {
case -1 => None
case i => remove(i, lastIndex.get, expire = false)
}
}
override def size: Int = lastIndex.get + 1
override def toString: String = {
val values = 0 to lastIndex.get() map { i =>
val (key, value) = elementsSortedByAccess(i)
s"$key -> $value"
}
s"LRUCache(${values mkString ", "})"
}
private def indexOf(key: K): Int =
elementsSortedByAccess.view.take(lastIndex.get() + 1).indexWhere(_._1 == key)
private def replace(index: Int, key: K, value: V): Option[V] = {
val prev = remove(index, lastIndex.get(), expire = false)
append(key, value)
prev
}
private def append(key: K, value: V): Unit = {
while (lastIndex.get() >= maxSize - 1) {
remove(0, lastIndex.get(), expire = true)
}
val index = lastIndex.incrementAndGet()
elementsSortedByAccess(index) = (key, value)
}
private def remove(index: Int, endIndex: Int, expire: Boolean): Option[V] = {
@tailrec
def shift(i: Int): Unit = if (i < endIndex) {
elementsSortedByAccess(i) = elementsSortedByAccess(i + 1)
shift(i + 1)
}
val prev = elementsSortedByAccess(index)
shift(index)
lastIndex.set(endIndex - 1)
if (expire) onExpire.foreach(f => f(prev))
Some(prev._2)
}
}
private def emptyCache[K, V]: LRUCache[K, V] = new LRUCache[K, V] {
override def get(key: K): Option[V] = None
override def entries: Seq[(K, V)] = Nil
override def maxSize: Int = 0
override def put(key: K, value: V): Option[V] = None
override def remove(key: K): Option[V] = None
override def size: Int = 0
override def close(): Unit = {}
override def toString = "EmptyLRUCache"
}
def apply[K, V](maxSize: Int): LRUCache[K, V] =
if (maxSize > 0) new impl(maxSize, None) else emptyCache
def apply[K, V](maxSize: Int, onExpire: (K, V) => Unit): LRUCache[K, V] =
if (maxSize > 0) new impl(maxSize, Some(onExpire.tupled)) else emptyCache
}

View File

@ -0,0 +1,68 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal
import java.io.File
import java.net.URLClassLoader
import java.{ util => jutil }
import scala.collection.JavaConverters._
import sbt.internal.inc.classpath._
import sbt.io.IO
private[sbt] class LayeredClassLoader(
classpath: Seq[File],
parent: ClassLoader,
override protected val resources: Map[String, String],
tempDir: File,
) extends URLClassLoader(classpath.toArray.map(_.toURI.toURL), parent)
with RawResources
with NativeCopyLoader
with AutoCloseable {
private[this] val nativeLibs = new jutil.HashSet[File]().asScala
override protected val config = new NativeCopyConfig(
tempDir,
classpath,
IO.parseClasspath(System.getProperty("java.library.path", ""))
)
override def findLibrary(name: String): String = {
super.findLibrary(name) match {
case null => null
case l =>
nativeLibs += new File(l)
l
}
}
override def close(): Unit = nativeLibs.foreach(NativeLibs.delete)
override def toString: String = s"""LayeredClassLoader(
| classpath =
| ${classpath mkString "\n "}
| parent =
| ${parent.toString.linesIterator.mkString("\n ")}
|)""".stripMargin
}
private[internal] object NativeLibs {
private[this] val nativeLibs = new jutil.HashSet[File].asScala
Runtime.getRuntime.addShutdownHook(new Thread("sbt.internal.native-library-deletion") {
override def run(): Unit = {
nativeLibs.foreach(IO.delete)
IO.deleteIfEmpty(nativeLibs.map(_.getParentFile).toSet)
nativeLibs.clear()
}
})
def addNativeLib(lib: String): Unit = {
nativeLibs.add(new File(lib))
()
}
def delete(file: File): Unit = {
nativeLibs.remove(file)
file.delete()
()
}
}

View File

@ -0,0 +1,15 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal
private[sbt] object PrettyPrint {
private[sbt] def indent(any: Any, level: Int): String = {
val i = " " * level
any.toString.linesIterator.mkString(i, "\n" + i, "")
}
}

View File

@ -0,0 +1,58 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal
import java.util.concurrent.ConcurrentHashMap
import scala.collection.JavaConverters._
/**
* Represents an abstract cache of values, accessible by a key. The interface is deliberately
* minimal to give maximum flexibility to the implementation classes. For example, one can construct
* a cache from a `sbt.io.FileTreeRepository` that automatically registers the paths with the
* cache (but does not clear the cache on close):
* {{{
* val repository = sbt.io.FileTreeRepository.default(_.getPath)
* val fileCache = new Repository[Seq, (Path, Boolean), TypedPath] {
* override def get(key: (Path, Boolean)): Seq[TypedPath] = {
* val (path, recursive) = key
* val depth = if (recursive) Int.MaxValue else 0
* repository.register(path, depth)
* repository.list(path, depth, AllPass)
* }
* override def close(): Unit = {}
* }
* }}}
*
* @tparam M the container type of the cache. This will most commonly be `Option` or `Seq`.
* @tparam K the key type
* @tparam V the value type
*/
trait Repository[M[_], K, V] extends AutoCloseable {
def get(key: K): M[V]
}
private[sbt] final class MutableRepository[K, V] extends Repository[Option, K, V] {
private[this] val map = new ConcurrentHashMap[K, V].asScala
override def get(key: K): Option[V] = map.get(key)
def put(key: K, value: V): Unit = this.synchronized {
map.put(key, value)
()
}
def remove(key: K): Unit = this.synchronized {
map.remove(key)
()
}
override def close(): Unit = this.synchronized {
map.foreach {
case (_, v: AutoCloseable) => v.close()
case _ =>
}
map.clear()
}
}

View File

@ -0,0 +1,38 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal
import sbt.Keys.state
import sbt._
private[sbt] object TaskRepository {
private[sbt] type Repr = MutableRepository[TaskKey[_], Any]
private[sbt] def proxy[T: Manifest](taskKey: TaskKey[T], task: => T): Def.Setting[Task[T]] =
proxy(taskKey, Def.task(task))
private[sbt] def proxy[T: Manifest](
taskKey: TaskKey[T],
task: Def.Initialize[Task[T]]
): Def.Setting[Task[T]] =
taskKey := Def.taskDyn {
val taskRepository = state.value
.get(Keys.taskRepository)
.getOrElse {
val msg = "TaskRepository.proxy called before state was initialized"
throw new IllegalStateException(msg)
}
taskRepository.get(taskKey) match {
case Some(value: T) => Def.task(value)
case _ =>
Def.task {
val value = task.value
taskRepository.put(taskKey, value)
value
}
}
}.value
}

View File

@ -0,0 +1,68 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal
import java.io.File
import java.nio.file.Files
import org.scalatest.{ FlatSpec, Matchers }
import sbt.io.IO
object ClassLoaderCacheTest {
private val initLoader = this.getClass.getClassLoader
implicit class CacheOps(val c: ClassLoaderCache) {
def get(classpath: Seq[File]): ClassLoader =
c.get((classpath, initLoader, Map.empty, new File("/dev/null")))
}
}
class ClassLoaderCacheTest extends FlatSpec with Matchers {
import ClassLoaderCacheTest._
def withCache[R](size: Int)(f: CacheOps => R): R = {
val cache = ClassLoaderCache(size)
try f(new CacheOps(cache))
finally cache.close()
}
"ClassLoaderCache.get" should "make a new loader when full" in withCache(0) { cache =>
val classPath = Seq.empty[File]
val firstLoader = cache.get(classPath)
val secondLoader = cache.get(classPath)
assert(firstLoader != secondLoader)
}
it should "not make a new loader when it already exists" in withCache(1) { cache =>
val classPath = Seq.empty[File]
val firstLoader = cache.get(classPath)
val secondLoader = cache.get(classPath)
assert(firstLoader == secondLoader)
}
it should "evict loaders" in withCache(2) { cache =>
val firstClassPath = Seq.empty[File]
val secondClassPath = new File("foo") :: Nil
val thirdClassPath = new File("foo") :: new File("bar") :: Nil
val firstLoader = cache.get(firstClassPath)
val secondLoader = cache.get(secondClassPath)
val thirdLoader = cache.get(thirdClassPath)
assert(cache.get(thirdClassPath) == thirdLoader)
assert(cache.get(secondClassPath) == secondLoader)
assert(cache.get(firstClassPath) != firstLoader)
assert(cache.get(thirdClassPath) != thirdLoader)
}
"Snapshots" should "be invalidated" in IO.withTemporaryDirectory { dir =>
val snapshotJar = Files.createFile(dir.toPath.resolve("foo-SNAPSHOT.jar")).toFile
val regularJar = Files.createFile(dir.toPath.resolve("regular.jar")).toFile
withCache(1) { cache =>
val jarClassPath = snapshotJar :: regularJar :: Nil
val initLoader = cache.get(jarClassPath)
IO.setModifiedTimeOrFalse(snapshotJar, System.currentTimeMillis + 5000L)
val secondLoader = cache.get(jarClassPath)
assert(initLoader != secondLoader)
assert(initLoader.getParent == secondLoader.getParent)
assert(cache.get(jarClassPath) == secondLoader)
assert(cache.get(jarClassPath) != initLoader)
}
}
}

View File

@ -0,0 +1,78 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package internal
import java.util.concurrent.atomic.AtomicInteger
import org.scalatest.{ FlatSpec, Matchers }
class LRUCacheTest extends FlatSpec with Matchers {
"LRUCache" should "flush entries when full" in {
val cache = LRUCache[Int, Int](2)
cache.put(1, 1)
cache.put(2, 2)
cache.put(3, 3)
assert(cache.get(1).isEmpty)
assert(cache.get(2).contains(2))
assert(cache.get(3).contains(3))
assert(cache.get(2).contains(2))
cache.put(1, 1)
assert(cache.get(3).isEmpty)
assert(cache.get(2).contains(2))
assert(cache.get(1).contains(1))
}
it should "remove entries" in {
val cache = LRUCache[Int, Int](2)
cache.put(1, 1)
cache.put(2, 2)
assert(cache.get(1).contains(1))
assert(cache.get(2).contains(2))
assert(cache.remove(1).getOrElse(-1) == 1)
assert(cache.get(1).isEmpty)
assert(cache.get(2).contains(2))
}
it should "clear entries on close" in {
val cache = LRUCache[Int, Int](2)
cache.put(1, 1)
assert(cache.get(1).contains(1))
cache.close()
assert(cache.get(1).isEmpty)
}
it should "call onExpire in close" in {
val count = new AtomicInteger(0)
val cache =
LRUCache[Int, Int](
maxSize = 3,
onExpire = (_: Int, _: Int) => { count.getAndIncrement(); () }
)
cache.put(1, 1)
cache.put(2, 2)
cache.put(3, 3)
cache.put(4, 4)
assert(count.get == 1)
cache.close()
assert(count.get == 4)
}
it should "apply on remove function" in {
val value = new AtomicInteger(0)
val cache = LRUCache[Int, Int](1, (k: Int, v: Int) => value.set(k + v))
cache.put(1, 3)
cache.put(2, 2)
assert(value.get() == 4)
assert(cache.get(2).contains(2))
}
it should "print sorted entries in toString" in {
val cache = LRUCache[Int, Int](2)
cache.put(2, 2)
cache.put(1, 1)
assert(cache.toString == s"LRUCache(2 -> 2, 1 -> 1)")
}
}

View File

@ -58,7 +58,9 @@ class ForkRun(config: ForkOptions) extends ScalaRun {
private def classpathOption(classpath: Seq[File]) =
"-classpath" :: Path.makeString(classpath) :: Nil
}
class Run(instance: ScalaInstance, trapExit: Boolean, nativeTmp: File) extends ScalaRun {
class Run(newLoader: Seq[File] => ClassLoader, trapExit: Boolean) extends ScalaRun {
def this(instance: ScalaInstance, trapExit: Boolean, nativeTmp: File) =
this((cp: Seq[File]) => ClasspathUtilities.makeLoader(cp, instance, nativeTmp), trapExit)
/** Runs the class 'mainClass' using the given classpath and options using the scala runner.*/
def run(mainClass: String, classpath: Seq[File], options: Seq[String], log: Logger): Try[Unit] = {
@ -87,7 +89,7 @@ class Run(instance: ScalaInstance, trapExit: Boolean, nativeTmp: File) extends S
log: Logger
): Unit = {
log.debug(" Classpath:\n\t" + classpath.mkString("\n\t"))
val loader = ClasspathUtilities.makeLoader(classpath, instance, nativeTmp)
val loader = newLoader(classpath)
val main = getMainMethod(mainClassName, loader)
invokeMain(loader, main, options)
}

View File

@ -0,0 +1,9 @@
val akkaTest = (project in file(".")).settings(
name := "akka-test",
scalaVersion := "2.12.8",
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor" % "2.5.16",
"com.lihaoyi" %% "utest" % "0.6.6" % "test"
),
testFrameworks := Seq(new TestFramework("utest.runner.Framework"))
)

View File

@ -0,0 +1,15 @@
package sbt.scripted
import akka.actor.ActorSystem
import scala.concurrent.Await
import scala.concurrent.duration._
object AkkaTest {
def main(args: Array[String]): Unit = {
val now = System.nanoTime
val system = ActorSystem("akka")
Await.result(system.terminate(), 5.seconds)
val elapsed = System.nanoTime - now
println(s"Run took ${elapsed / 1.0e6} ms")
}
}

View File

@ -0,0 +1,12 @@
package sbt.scripted
import utest._
object AkkaPerfTest extends TestSuite {
val tests: Tests = Tests {
'run - {
AkkaTest.main(Array.empty[String])
1 ==> 1
}
}
}

View File

@ -0,0 +1,7 @@
> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.ShareRuntimeDependenciesLayerWithTestDependencies
> run
> test
> test

View File

@ -0,0 +1,38 @@
import java.nio.file._
import scala.collection.JavaConverters._
val copyTestResources = inputKey[Unit]("Copy the native libraries to the base directory")
val appendToLibraryPath = taskKey[Unit]("Append the base directory to the java.library.path system property")
val dropLibraryPath = taskKey[Unit]("Drop the last path from the java.library.path system property")
val wrappedRun = taskKey[Unit]("Run with modified java.library.path")
val wrappedTest = taskKey[Unit]("Test with modified java.library.path")
def wrap(task: InputKey[Unit]): Def.Initialize[Task[Unit]] =
Def.sequential(appendToLibraryPath, task.toTask(""), dropLibraryPath)
val root = (project in file(".")).settings(
scalaVersion := "2.12.8",
javacOptions ++= Seq("-source", "1.8", "-target", "1.8", "-h",
sourceDirectory.value.toPath.resolve("main/native/include").toString),
libraryDependencies += "com.lihaoyi" %% "utest" % "0.6.6" % "test",
testFrameworks := Seq(new TestFramework("utest.runner.Framework")),
copyTestResources := {
val key = Def.spaceDelimited().parsed.head
val base = baseDirectory.value.toPath
val resources = (baseDirectory.value / "src" / "main" / "resources" / key).toPath
Files.walk(resources).iterator.asScala.foreach { p =>
Files.copy(p, base.resolve(p.getFileName), StandardCopyOption.REPLACE_EXISTING)
}
},
appendToLibraryPath := {
val cp = System.getProperty("java.library.path", "").split(":")
val newCp = if (cp.contains(".")) cp else cp :+ "."
System.setProperty("java.library.path", newCp.mkString(":"))
},
dropLibraryPath := {
val cp = System.getProperty("java.library.path", "").split(":").dropRight(1)
System.setProperty("java.library.path", cp.mkString(":"))
},
wrappedRun := wrap(Runtime / run).value,
wrappedTest := wrap(Test / testOnly).value
)

View File

@ -0,0 +1,15 @@
package sbt
import java.nio.file._
import utest._
object JniLibraryTest extends TestSuite {
val tests = Tests {
'load - {
'native - {
System.loadLibrary("sbt-jni-library-test0")
new JniLibrary().getIntegerValue ==> 2
}
}
}
}

View File

@ -0,0 +1,5 @@
package sbt;
final class JniLibrary {
public native int getIntegerValue();
}

View File

@ -0,0 +1,61 @@
TARGET_DIR := ../../../target
BUILD_DIR := $(TARGET_DIR)/build
NATIVE_DIR := native/$(shell uname -sm | cut -d ' ' -f2 | tr '[:upper:]' '[:lower:]')
LIB_DIR := ../resources/$(NATIVE_DIR)
LIB_NAME := sbt-jni-library-test0
POSIX_LIB_NAME := lib$(LIB_NAME)
SOURCE := sbt_JniLibrary
CC := clang
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
JNI_INCLUDE := -I$(shell mdfind -name jni.h | grep jdk1.8 | tail -n 1 | xargs dirname)\
-I$(shell mdfind -name jni_md.h | grep jdk1.8 | tail -n 1 | xargs dirname)
LD := /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
OBJS := $(BUILD_DIR)/x86_64/darwin/$(SOURCE).o
LIBS := $(TARGET_DIR)/x86_64/$(POSIX_LIB_NAME).dylib
endif
ifeq ($(UNAME_S), Linux)
BASE_INCLUDE := $(shell locate jni.h | tail -n 1 | xargs dirname)
JNI_INCLUDE := -I$(BASE_INCLUDE) -I$(BASE_INCLUDE)/linux
OBJS := $(BUILD_DIR)/x86_64/linux/$(SOURCE).o
LIBS := $(TARGET_DIR)/x86_64/$(POSIX_LIB_NAME).so
endif
LINUX_CCFLAGS := -fPIC
LINUX_LDFLAGS := -fPIC -shared
CCFLAGS := -I./../include $(JNI_INCLUDE) -Wno-unused-command-line-argument -std=c++11 -O3
all: $(LIBS)
.PHONY: clean all
$(BUILD_DIR)/x86_64/linux/$(SOURCE).o: $(SOURCE).cc
mkdir -p $(BUILD_DIR)/x86_64/linux; \
$(CC) -c $< $(CCFLAGS) $(JNI_INCLUDE) -fPIC -o $@
$(BUILD_DIR)/x86_64/darwin/$(SOURCE).o: $(SOURCE).cc
mkdir -p $(BUILD_DIR)/x86_64/darwin; \
$(CC) -c $< $(CCFLAGS) -framework Carbon -o $@
$(TARGET_DIR)/x86_64/$(POSIX_LIB_NAME).dylib: $(BUILD_DIR)/x86_64/darwin/$(SOURCE).o
mkdir -p $(TARGET_DIR)/x86_64; \
$(LD) -dynamiclib -framework Carbon $(CCFLAGS) -Wl,-headerpad_max_install_names -install_name @rpath/$(POSIX_LIB_NAME) \
$(BUILD_DIR)/x86_64/darwin/$(SOURCE).o \
-o $@ ; \
mkdir -p ../../resources/1; \
cp $(TARGET_DIR)/x86_64/$(POSIX_LIB_NAME).dylib ../../resources/1
$(TARGET_DIR)/x86_64/$(POSIX_LIB_NAME).so: $(BUILD_DIR)/x86_64/linux/$(SOURCE).o
mkdir -p $(TARGET_DIR)/x86_64; \
$(CC) -shared $< $(CCFLAGS) -Wl,-headerpad_max_install_names -o $@; \
mkdir -p ../../resources/1; \
cp $(TARGET_DIR)/x86_64/$(POSIX_LIB_NAME).so ../../resources/1;
clean:
rm -rf $(TARGET_DIR)/build $(TARGET_DIR)/$(NATIVE)

View File

@ -0,0 +1,15 @@
#include <jni.h>
#include "sbt_JniLibrary.h"
extern "C" {
/*
* Class: sbt_JniLibrary
* Method: getIntegerValue
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_sbt_JniLibrary_getIntegerValue
(JNIEnv *env, jobject obj) {
return 1;
}
}

View File

@ -0,0 +1,61 @@
TARGET_DIR := ../../../target
BUILD_DIR := $(TARGET_DIR)/build
NATIVE_DIR := native/$(shell uname -sm | cut -d ' ' -f2 | tr '[:upper:]' '[:lower:]')
LIB_DIR := ../resources/$(NATIVE_DIR)
LIB_NAME := sbt-jni-library-test0
POSIX_LIB_NAME := lib$(LIB_NAME)
SOURCE := sbt_JniLibrary
CC := clang
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
JNI_INCLUDE := -I$(shell mdfind -name jni.h | grep jdk1.8 | tail -n 1 | xargs dirname)\
-I$(shell mdfind -name jni_md.h | grep jdk1.8 | tail -n 1 | xargs dirname)
LD := /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
OBJS := $(BUILD_DIR)/x86_64/darwin/$(SOURCE).o
LIBS := $(TARGET_DIR)/x86_64/$(POSIX_LIB_NAME).dylib
endif
ifeq ($(UNAME_S), Linux)
BASE_INCLUDE := $(shell locate jni.h | tail -n 1 | xargs dirname)
JNI_INCLUDE := -I$(BASE_INCLUDE) -I$(BASE_INCLUDE)/linux
OBJS := $(BUILD_DIR)/x86_64/linux/$(SOURCE).o
LIBS := $(TARGET_DIR)/x86_64/$(POSIX_LIB_NAME).so
endif
LINUX_CCFLAGS := -fPIC
LINUX_LDFLAGS := -fPIC -shared
CCFLAGS := -I./../include $(JNI_INCLUDE) -Wno-unused-command-line-argument -std=c++11 -O3
all: $(LIBS)
.PHONY: clean all
$(BUILD_DIR)/x86_64/linux/$(SOURCE).o: $(SOURCE).cc
mkdir -p $(BUILD_DIR)/x86_64/linux; \
$(CC) -c $< $(CCFLAGS) $(JNI_INCLUDE) -fPIC -o $@
$(BUILD_DIR)/x86_64/darwin/$(SOURCE).o: $(SOURCE).cc
mkdir -p $(BUILD_DIR)/x86_64/darwin; \
$(CC) -c $< $(CCFLAGS) -framework Carbon -o $@
$(TARGET_DIR)/x86_64/$(POSIX_LIB_NAME).dylib: $(BUILD_DIR)/x86_64/darwin/$(SOURCE).o
mkdir -p $(TARGET_DIR)/x86_64; \
$(LD) -dynamiclib -framework Carbon $(CCFLAGS) -Wl,-headerpad_max_install_names -install_name @rpath/$(POSIX_LIB_NAME) \
$(BUILD_DIR)/x86_64/darwin/$(SOURCE).o \
-o $@ ; \
mkdir -p ../../resources/2; \
cp $(TARGET_DIR)/x86_64/$(POSIX_LIB_NAME).dylib ../../resources/2
$(TARGET_DIR)/x86_64/$(POSIX_LIB_NAME).so: $(BUILD_DIR)/x86_64/linux/$(SOURCE).o
mkdir -p $(TARGET_DIR)/x86_64; \
$(CC) -shared $< $(CCFLAGS) -Wl,-headerpad_max_install_names -o $@; \
mkdir -p ../../resources/2; \
cp $(TARGET_DIR)/x86_64/$(POSIX_LIB_NAME).so ../../resources/2;
clean:
rm -rf $(TARGET_DIR)/build $(TARGET_DIR)/$(NATIVE)

View File

@ -0,0 +1,15 @@
#include <jni.h>
#include "sbt_JniLibrary.h"
extern "C" {
/*
* Class: sbt_JniLibrary
* Method: getIntegerValue
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_sbt_JniLibrary_getIntegerValue
(JNIEnv *env, jobject obj) {
return 2;
}
}

View File

@ -0,0 +1,21 @@
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class sbt_JniLibrary */
#ifndef _Included_sbt_JniLibrary
#define _Included_sbt_JniLibrary
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: sbt_JniLibrary
* Method: getIntegerValue
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_sbt_JniLibrary_getIntegerValue
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif

View File

@ -0,0 +1,12 @@
package sbt
import java.nio.file._
import scala.collection.JavaConverters._
object TestMain {
def main(args: Array[String]): Unit = {
val libraryPath = System.getProperty("java.library.path")
System.loadLibrary("sbt-jni-library-test0")
println(s"Native value is ${new JniLibrary().getIntegerValue}")
}
}

View File

@ -0,0 +1,15 @@
package sbt
import java.nio.file._
import utest._
object JniLibraryTest extends TestSuite {
val tests = Tests {
'load - {
'native - {
System.loadLibrary("sbt-jni-library-test0")
new JniLibrary().getIntegerValue ==> 1
}
}
}
}

View File

@ -0,0 +1,6 @@
> copyTestResources 1
> wrappedRun
> wrappedTest
> copyTestResources 2
$ copy-file changes/JniLibraryTest.scala src/test/scala/sbt/JniLibraryTest.scala
> wrappedTest

View File

@ -0,0 +1,9 @@
val snapshot = (project in file(".")).settings(
name := "mismatched-libraries",
scalaVersion := "2.12.8",
libraryDependencies ++= Seq("com.lihaoyi" %% "utest" % "0.6.6" % "test"),
testFrameworks := Seq(TestFramework("utest.runner.Framework")),
resolvers += "Local Maven" at file("libraries/ivy").toURI.toURL.toString,
libraryDependencies += "sbt" % "transitive-lib" % "0.1.0",
libraryDependencies += "sbt" % "foo-lib" % "0.2.0" % "test",
)

View File

@ -0,0 +1,11 @@
name := "foo-lib"
organization := "sbt"
publishTo := Some(Resolver.file("test-resolver", file("..").getCanonicalFile / "ivy"))
version := "0.1.0"
crossPaths := false
autoScalaLibrary := false

View File

@ -0,0 +1,7 @@
package sbt.foo;
public class Foo {
static public int x() {
return 1;
}
}

View File

@ -0,0 +1,11 @@
name := "foo-lib"
organization := "sbt"
publishTo := Some(Resolver.file("test-resolver", file("..").getCanonicalFile / "ivy"))
version := "0.2.0"
crossPaths := false
autoScalaLibrary := false

View File

@ -0,0 +1,10 @@
package sbt.foo;
public class Foo {
static public int x() {
return 2;
}
static public int y() {
return 3;
}
}

View File

@ -0,0 +1 @@
ca2193d86495f120496370cf2e75355f2b10b2a3

View File

@ -0,0 +1 @@
86ce6f4d12e075e8ef4ba80b671783673a8a0443

View File

@ -0,0 +1 @@
4cd195abad8639ba6df345c125068058

View File

@ -0,0 +1 @@
e18f4c7a4b52d288733f123008d98985610da4f3

View File

@ -0,0 +1,13 @@
<?xml version='1.0' encoding='UTF-8'?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>sbt</groupId>
<artifactId>foo-lib</artifactId>
<packaging>jar</packaging>
<description>foo-lib</description>
<version>0.1.0</version>
<name>foo-lib</name>
<organization>
<name>sbt</name>
</organization>
</project>

View File

@ -0,0 +1 @@
22ebca3d8b32a4c0cb40341501b798e2

View File

@ -0,0 +1 @@
575258d8c9fecff7e8beefee2637c2928328e0dc

View File

@ -0,0 +1 @@
cc1a6b548cb9efd6e43c7fb5fa44728fc3f50d45

View File

@ -0,0 +1 @@
af0ac149e4c810884bb921bb3826e495faa5854b

View File

@ -0,0 +1 @@
fcadc4b2d6965cec0f5e5efb0a59226d

View File

@ -0,0 +1 @@
585422ad5196b83f83bbca644e62a39a2e4171e0

View File

@ -0,0 +1,13 @@
<?xml version='1.0' encoding='UTF-8'?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>sbt</groupId>
<artifactId>foo-lib</artifactId>
<packaging>jar</packaging>
<description>foo-lib</description>
<version>0.2.0</version>
<name>foo-lib</name>
<organization>
<name>sbt</name>
</organization>
</project>

View File

@ -0,0 +1 @@
790acd1d77316ff2c0310bb88d01dc72

View File

@ -0,0 +1 @@
94eec0571d936004eb5682b8b12268d1bcb2be40

View File

@ -0,0 +1 @@
f0fcc50b65e83f42210bbd3f366e2cc439177aa8

View File

@ -0,0 +1,20 @@
<?xml version='1.0' encoding='UTF-8'?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>sbt</groupId>
<artifactId>transitive-lib</artifactId>
<packaging>jar</packaging>
<description>transitive-lib</description>
<version>0.1.0</version>
<name>transitive-lib</name>
<organization>
<name>sbt</name>
</organization>
<dependencies>
<dependency>
<groupId>sbt</groupId>
<artifactId>foo-lib</artifactId>
<version>0.1.0</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1 @@
4cb84c1daf7152544065849db0c1ce2d8d47c334

View File

@ -0,0 +1,15 @@
name := "transitive-lib"
organization := "sbt"
resolvers += "Local Maven" at file("../ivy").getCanonicalFile.toURI.toURL.toString
publishTo := Some(Resolver.file("test-resolver", file("..").getCanonicalFile / "ivy"))
version := "0.1.0"
libraryDependencies += "sbt" % "foo-lib" % "0.1.0"
crossPaths := false
autoScalaLibrary := false

View File

@ -0,0 +1,7 @@
package sbt.transitive;
public class Transitive {
public static int x() {
return sbt.foo.Foo.x();
}
}

View File

@ -0,0 +1,7 @@
package sbt
object TestMain {
def main(args: Array[String]) {
println(transitive.Transitive.x)
}
}

View File

@ -0,0 +1,11 @@
package sbt
import utest._
object MismatchedLibrariesTest extends TestSuite {
val tests: Tests = Tests {
'check - {
assert(foo.Foo.y == 3)
}
}
}

View File

@ -0,0 +1,15 @@
> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.ShareRuntimeDependenciesLayerWithTestDependencies
> run
# This fails because the runtime layer includes an old version of the foo-lib library that doesn't
# have the sbt.foo.Foo.y method defined.
> test
> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.TestDependencies
> run
> test
> test

View File

@ -0,0 +1,6 @@
val layeringStrategyTest = (project in file(".")).settings(
name := "layering-strategy-test",
scalaVersion := "2.12.8",
organization := "sbt",
libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.5.16",
)

View File

@ -0,0 +1,15 @@
package sbt.scripted
import akka.actor.ActorSystem
import scala.concurrent.Await
import scala.concurrent.duration._
object AkkaTest {
def main(args: Array[String]): Unit = {
val now = System.nanoTime
val system = ActorSystem("akka")
Await.result(system.terminate(), 5.seconds)
val elapsed = System.nanoTime - now
println(s"Run took ${elapsed / 1.0e6} ms")
}
}

View File

@ -0,0 +1,15 @@
package sbt.scripted
import akka.actor.ActorSystem
import scala.concurrent.Await
import scala.concurrent.duration._
object TestAkkaTest {
def main(args: Array[String]): Unit = {
val now = System.nanoTime
val system = ActorSystem("akka")
Await.result(system.terminate(), 5.seconds)
val elapsed = System.nanoTime - now
println(s"Test run took ${elapsed / 1.0e6} ms")
}
}

View File

@ -0,0 +1,37 @@
> run
> set Compile / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat
> run
> set Compile / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.ScalaInstance
> run
> set Compile / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.RuntimeDependencies
> run
> set Compile / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.TestDependencies
-> run
> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat
> Test / runMain sbt.scripted.TestAkkaTest
> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.ScalaInstance
> Test / runMain sbt.scripted.TestAkkaTest
> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.RuntimeDependencies
> Test / runMain sbt.scripted.TestAkkaTest
> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.ShareRuntimeDependenciesLayerWithTestDependencies
> Test / runMain sbt.scripted.TestAkkaTest
> set Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.TestDependencies
> Test / runMain sbt.scripted.TestAkkaTest

View File

@ -0,0 +1,3 @@
val test = (project in file(".")).settings(
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.4" % "test"
)

View File

@ -0,0 +1,9 @@
package sbt
import org.scalatest.{ FlatSpec, Matchers }
class ScalatestTest extends FlatSpec with Matchers {
"scalatest" should "fail" in {
1 shouldBe 2
}
}

View File

@ -0,0 +1,9 @@
package sbt
import org.scalatest.{ FlatSpec, Matchers }
class ScalatestTest extends FlatSpec with Matchers {
"scalatest" should "work" in {
1 shouldBe 1
}
}

View File

@ -0,0 +1,5 @@
> test
$ copy-file changes/ScalatestTest.scala src/test/scala/sbt/ScalatestTest.scala
-> test

View File

@ -0,0 +1,24 @@
import java.nio.file.Files
import java.nio.file.attribute.FileTime
import scala.collection.JavaConverters._
val rewriteIvy = inputKey[Unit]("Rewrite ivy directory")
val snapshot = (project in file(".")).settings(
name := "akka-test",
scalaVersion := "2.12.8",
libraryDependencies ++= Seq(
"com.lihaoyi" %% "utest" % "0.6.6" % "test"
),
testFrameworks += TestFramework("utest.runner.Framework"),
resolvers += "Local Maven" at file("ivy").toURI.toURL.toString,
libraryDependencies += "sbt" %% "foo-lib" % "0.1.0-SNAPSHOT",
rewriteIvy := {
val dir = Def.spaceDelimited().parsed.head
sbt.IO.delete(file("ivy"))
sbt.IO.copyDirectory(file(s"libraries/library-$dir/ivy"), file("ivy"))
Files.walk(file("ivy").getCanonicalFile.toPath).iterator.asScala.foreach { f =>
Files.setLastModifiedTime(f, FileTime.fromMillis(System.currentTimeMillis + 3000))
}
}
)

View File

@ -0,0 +1,5 @@
name := "foo-lib"
organization := "sbt"
publishTo := Some(Resolver.file("test-resolver", file("").getCanonicalFile / "ivy"))

Some files were not shown because too many files have changed in this diff Show More