Synchronize caches

This commit is contained in:
Mark Harrah 2009-10-10 22:01:03 -04:00
parent e88214efbd
commit 1d40e5e71d
6 changed files with 75 additions and 29 deletions

View File

@ -20,21 +20,18 @@ class ComponentCompiler(compiler: RawCompiler, manager: ComponentManager)
def apply(id: String): File =
{
val binID = binaryID(id)
try { manager.file(binID) }
catch { case e: InvalidComponent => compileAndInstall(id, binID) }
manager.file(binID)( new IfMissing.Define(true, compileAndInstall(id, binID)) )
}
def clearCache(id: String): Unit = manager.clearCache(binaryID(id))
protected def binaryID(id: String) = id + binSeparator + compiler.scalaInstance.actualVersion
protected def compileAndInstall(id: String, binID: String): File =
protected def compileAndInstall(id: String, binID: String)
{
val srcID = id + srcExtension
withTemporaryDirectory { binaryDirectory =>
val targetJar = new File(binaryDirectory, id + ".jar")
compileSources(manager.files(srcID), targetJar, id)
compileSources(manager.files(srcID)(IfMissing.Fail), targetJar, id)
manager.define(binID, Seq(targetJar))
manager.cache(binID)
}
manager.file(binID)
}
/** Extract sources from source jars, compile them with the xsbti interfaces on the classpath, and package the compiled classes and
* any resources from the source jars into a final jar.*/
@ -45,7 +42,7 @@ class ComponentCompiler(compiler: RawCompiler, manager: ComponentManager)
val extractedSources = (Set[File]() /: sourceJars) { (extracted, sourceJar)=> extracted ++ unzip(sourceJar, dir) }
val (sourceFiles, resources) = extractedSources.partition(_.getName.endsWith(".scala"))
withTemporaryDirectory { outputDirectory =>
val xsbtiJars = manager.files(xsbtiID)
val xsbtiJars = manager.files(xsbtiID)(IfMissing.Fail)
manager.log.info("'" + id + "' not yet compiled for Scala " + compiler.scalaInstance.actualVersion + ". Compiling...")
try { compiler(Set() ++ sourceFiles, Set() ++ xsbtiJars, outputDirectory, Nil, true) }
catch { case e: xsbti.CompileFailed => throw new CompileFailed(e.arguments, "Error compiling sbt component '" + id + "'") }

View File

@ -1,6 +1,7 @@
package xsbt
import java.io.File
import java.io.{File,FileOutputStream}
import ComponentManager.lock
/** A component manager provides access to the pieces of xsbt that are distributed as components.
* There are two types of components. The first type is compiled subproject jars with their dependencies.
@ -14,33 +15,73 @@ import java.io.File
class ComponentManager(provider: xsbti.ComponentProvider, val log: IvyLogger) extends NotNull
{
/** Get all of the files for component 'id', throwing an exception if no files exist for the component. */
def files(id: String): Iterable[File] =
def files(id: String)(ifMissing: IfMissing): Iterable[File] =
{
val existing = provider.component(id)
val fs = if(existing.isEmpty) { update(id); provider.component(id) } else existing
if(!fs.isEmpty) fs else invalid("Could not find required component '" + id + "'")
def fromGlobal =
lockGlobalCache {
try { update(id); getOrElse(createAndCache) }
catch { case e: NotInCache => createAndCache }
}
def getOrElse(orElse: => Iterable[File]) =
{
val existing = provider.component(id)
if(existing.isEmpty) orElse else existing
}
def notFound = invalid("Could not find required component '" + id + "'")
def createAndCache =
ifMissing match {
case IfMissing.Fail => notFound
case d: IfMissing.Define =>
d()
if(d.cache) cache(id)
getOrElse(notFound)
}
lockLocalCache { getOrElse(fromGlobal) }
}
private def lockLocalCache[T](action: => T): T = lock("local cache", provider.lockFile, log) ( action )
private def lockGlobalCache[T](action: => T): T = lock("global cache", IvyCache.lockFile, log)( action )
/** Get the file for component 'id', throwing an exception if no files or multiple files exist for the component. */
def file(id: String): File =
files(id).toList match {
def file(id: String)(ifMissing: IfMissing): File =
files(id)(ifMissing).toList match {
case x :: Nil => x
case xs => invalid("Expected single file for component '" + id + "', found: " + xs.mkString(", "))
}
private def invalid(msg: String) = throw new InvalidComponent(msg)
private def invalid(e: NotInCache) = throw new InvalidComponent(e.getMessage, e)
def define(id: String, files: Iterable[File]) = provider.defineComponent(id, files.toSeq.toArray)
def define(id: String, files: Iterable[File]) = lockLocalCache { provider.defineComponent(id, files.toSeq.toArray) }
/** Retrieve the file for component 'id' from the local repository. */
def update(id: String): Unit =
try { IvyCache.withCachedJar(sbtModuleID(id), log)(jar => define(id, Seq(jar)) ) }
catch { case e: NotInCache => invalid(e) }
private def update(id: String): Unit = IvyCache.withCachedJar(sbtModuleID(id), log)(jar => define(id, Seq(jar)) )
def sbtModuleID(id: String) = ModuleID("org.scala-tools.sbt", id, xsbti.Versions.Sbt)
private def sbtModuleID(id: String) = ModuleID("org.scala-tools.sbt", id, xsbti.Versions.Sbt)
/** Install the files for component 'id' to the local repository. This is usually used after writing files to the directory returned by 'location'. */
def cache(id: String): Unit = IvyCache.cacheJar(sbtModuleID(id), file(id), log)
def clearCache(id: String): Unit = IvyCache.clearCachedJar(sbtModuleID(id), log)
def cache(id: String): Unit = IvyCache.cacheJar(sbtModuleID(id), file(id)(IfMissing.Fail), log)
def clearCache(id: String): Unit = lockGlobalCache { IvyCache.clearCachedJar(sbtModuleID(id), log) }
}
class InvalidComponent(msg: String, cause: Throwable) extends RuntimeException(msg, cause)
{
def this(msg: String) = this(msg, null)
}
sealed trait IfMissing extends NotNull
object IfMissing
{
object Fail extends IfMissing
final class Define(val cache: Boolean, define: => Unit) extends IfMissing { def apply() = define }
}
object ComponentManager
{
def lock[T](label: String, file: File, log: IvyLogger)(action: => T): T =
{
synchronized {
val channel = new FileOutputStream(file).getChannel
try {
val freeLock = channel.tryLock
val lock = if(freeLock eq null) { log.info("Waiting for " + label + " to be available..."); channel.lock } else freeLock
try { action }
finally { lock.release() }
}
finally { channel.close() }
}
}
}

View File

@ -27,6 +27,7 @@ private object NotInCache
/** Provides methods for working at the level of a single jar file with the default Ivy cache.*/
object IvyCache
{
def lockFile = new File(System.getProperty("temp.dir"), "sbt.cache.lock")
/** Caches the given 'file' with the given ID. It may be retrieved or cleared using this ID.*/
def cacheJar(moduleID: ModuleID, file: File, log: IvyLogger)
{

View File

@ -4,47 +4,48 @@ import java.io.File
import org.specs._
import FileUtilities.{createDirectory, delete, touch, withTemporaryDirectory}
import org.apache.ivy.util.ChecksumHelper
import IfMissing.Fail
object ComponentManagerTest extends Specification
{
val TestID = "manager-test"
"Component manager" should {
"throw an exception if 'file' is called for a non-existing component" in {
withManager { _.file(TestID) must throwA[InvalidComponent] }
withManager { _.file(TestID)(Fail) must throwA[InvalidComponent] }
}
"throw an exception if 'file' is called for an empty component" in {
withManager { manager =>
manager.define(TestID, Nil)
( manager.file(TestID) ) must throwA[InvalidComponent]
( manager.file(TestID)(Fail) ) must throwA[InvalidComponent]
}
}
"return the file for a single-file component" in {
withManager { manager =>
val hash = defineFile(manager, TestID, "a")
checksum(manager.file(TestID)) must beEqualTo(hash)
checksum(manager.file(TestID)(Fail)) must beEqualTo(hash)
}
}
"throw an exception if 'file' is called for multi-file component" in {
withManager { manager =>
defineFiles(manager, TestID, "a", "b")
( manager.file(TestID) ) must throwA[InvalidComponent]
( manager.file(TestID)(Fail) ) must throwA[InvalidComponent]
}
}
"return the files for a multi-file component" in {
withManager { manager =>
val hashes = defineFiles(manager, TestID, "a", "b")
checksum(manager.files(TestID)) must haveTheSameElementsAs(hashes)
checksum(manager.files(TestID)(Fail)) must haveTheSameElementsAs(hashes)
}
}
"return the files for a single-file component" in {
withManager { manager =>
val hashes = defineFiles(manager, TestID, "a")
checksum(manager.files(TestID)) must haveTheSameElementsAs(hashes)
checksum(manager.files(TestID)(Fail)) must haveTheSameElementsAs(hashes)
}
}
"throw an exception if 'files' is called for a non-existing component" in {
withManager { _.files(TestID) must throwA[InvalidComponent] }
withManager { _.files(TestID)(Fail) must throwA[InvalidComponent] }
}
"properly cache a file and then retrieve it to an unresolved component" in {
@ -54,7 +55,7 @@ object ComponentManagerTest extends Specification
{
definingManager.cache(TestID)
withManager { usingManager =>
checksum(usingManager.file(TestID)) must beEqualTo(hash)
checksum(usingManager.file(TestID)(Fail)) must beEqualTo(hash)
}
}
finally { definingManager.clearCache(TestID) }

View File

@ -106,4 +106,9 @@ class ComponentProvider(baseDirectory: File) extends xsbti.ComponentProvider
else
Copy(files, location)
}
def lockFile =
{
baseDirectory.mkdirs()
new File(baseDirectory, "sbt.components.lock")
}
}

View File

@ -6,4 +6,5 @@ public interface ComponentProvider
{
public File[] component(String componentID);
public void defineComponent(String componentID, File[] components);
public File lockFile();
}