diff --git a/compile/ComponentCompiler.scala b/compile/ComponentCompiler.scala index 980042158..5af5e0db4 100644 --- a/compile/ComponentCompiler.scala +++ b/compile/ComponentCompiler.scala @@ -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 + "'") } diff --git a/ivy/ComponentManager.scala b/ivy/ComponentManager.scala index e420ffe22..483a0bb31 100644 --- a/ivy/ComponentManager.scala +++ b/ivy/ComponentManager.scala @@ -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() } + } + } } \ No newline at end of file diff --git a/ivy/IvyCache.scala b/ivy/IvyCache.scala index 9e9c4d9e4..74c27e3e8 100644 --- a/ivy/IvyCache.scala +++ b/ivy/IvyCache.scala @@ -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) { diff --git a/ivy/src/test/scala/ComponentManagerTest.scala b/ivy/src/test/scala/ComponentManagerTest.scala index 6928ab98a..90613ce1e 100644 --- a/ivy/src/test/scala/ComponentManagerTest.scala +++ b/ivy/src/test/scala/ComponentManagerTest.scala @@ -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) } diff --git a/launch/Launch.scala b/launch/Launch.scala index 8da3ed4fa..b71f41a92 100644 --- a/launch/Launch.scala +++ b/launch/Launch.scala @@ -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") + } } \ No newline at end of file diff --git a/launch/interface/src/main/java/xsbti/ComponentProvider.java b/launch/interface/src/main/java/xsbti/ComponentProvider.java index 25d26b803..cac3f9a39 100644 --- a/launch/interface/src/main/java/xsbti/ComponentProvider.java +++ b/launch/interface/src/main/java/xsbti/ComponentProvider.java @@ -6,4 +6,5 @@ public interface ComponentProvider { public File[] component(String componentID); public void defineComponent(String componentID, File[] components); + public File lockFile(); } \ No newline at end of file