mirror of https://github.com/sbt/sbt.git
Tests and fixes for component manager and cache interface.
This commit is contained in:
parent
3c68c20030
commit
2acaa0ad25
|
|
@ -9,12 +9,16 @@ import xsbti.Versions
|
|||
* version of Scala.
|
||||
*
|
||||
* The component manager provides services to install and retrieve components to the local repository.
|
||||
* This is used for source jars so that the compilation need not be repeated for other projects on the same
|
||||
* This is used for compiled source jars so that the compilation need not be repeated for other projects on the same
|
||||
* machine.
|
||||
*/
|
||||
class ComponentManager(baseDirectory: File, log: IvyLogger) extends NotNull
|
||||
{
|
||||
/** Get the location where files for component 'id' are stored. This method does not ensure that the component is retrieved from the
|
||||
* local repository. By default, the location returned is is baseDirectory / id.*/
|
||||
def location(id: String): File = new File(baseDirectory, id)
|
||||
/** Get the location where files for component 'id' are stored. If the component has not yet been retrieved from the local repository,
|
||||
* it is retrieved first. */
|
||||
def directory(id: String): File =
|
||||
{
|
||||
val dir = location(id)
|
||||
|
|
@ -22,24 +26,38 @@ class ComponentManager(baseDirectory: File, log: IvyLogger) extends NotNull
|
|||
update(id)
|
||||
dir
|
||||
}
|
||||
// get the contents of the given directory, wrapping a null result in an empty list.
|
||||
private def contents(dir: File): Seq[File] =
|
||||
{
|
||||
val fs = dir.listFiles
|
||||
if(fs == null) Nil else fs
|
||||
}
|
||||
/** Get all of the files for component 'id', throwing an exception if no files exist for the component. */
|
||||
def files(id: String): Iterable[File] =
|
||||
{
|
||||
val fs = contents(directory(id))
|
||||
if(!fs.isEmpty) fs else error("Could not find required component '" + id + "'")
|
||||
if(!fs.isEmpty) fs else invalid("Could not find required component '" + id + "'")
|
||||
}
|
||||
/** 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 {
|
||||
case x :: Nil => x
|
||||
case xs => error("Expected single file for component '" + id + "', found: " + xs.mkString(", "))
|
||||
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)
|
||||
|
||||
/** Retrieve the file for component 'id' from the local repository. */
|
||||
def update(id: String): Unit =
|
||||
IvyActions.basicRetrieveLocal(sbtModuleID("manager"), Seq(sbtModuleID(id)), location(id), log)
|
||||
try { IvyCache.retrieveCachedJar(sbtModuleID(id), location(id), log) }
|
||||
catch { case e: NotInCache => invalid(e) }
|
||||
|
||||
def sbtModuleID(id: String) = ModuleID("org.scala-tools.sbt", id, Versions.Sbt)
|
||||
def cache(id: String): Unit = IvyActions.basicPublishLocal(sbtModuleID(id), Nil, files(id), log)
|
||||
/** 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)
|
||||
}
|
||||
class InvalidComponent(msg: String, cause: Throwable) extends RuntimeException(msg, cause)
|
||||
{
|
||||
def this(msg: String) = this(msg, null)
|
||||
}
|
||||
|
|
@ -207,7 +207,7 @@ private object IvySbt
|
|||
moduleID.check()
|
||||
}
|
||||
/** Converts the given sbt module id into an Ivy ModuleRevisionId.*/
|
||||
private def toID(m: ModuleID) =
|
||||
private[xsbt] def toID(m: ModuleID) =
|
||||
{
|
||||
import m._
|
||||
ModuleRevisionId.newInstance(organization, name, revision)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import org.apache.ivy.{core, plugins, util, Ivy}
|
|||
import core.cache.DefaultRepositoryCacheManager
|
||||
import core.LogOptions
|
||||
import core.deliver.DeliverOptions
|
||||
import core.install.InstallOptions
|
||||
import core.module.descriptor.{DefaultArtifact, DefaultDependencyArtifactDescriptor, MDArtifact}
|
||||
import core.module.descriptor.{DefaultDependencyDescriptor, DefaultModuleDescriptor, DependencyDescriptor, ModuleDescriptor}
|
||||
import core.module.id.{ArtifactId,ModuleId, ModuleRevisionId}
|
||||
|
|
@ -18,28 +19,19 @@ final class UpdateConfiguration(val retrieveDirectory: File, val outputPattern:
|
|||
|
||||
object IvyActions
|
||||
{
|
||||
def basicPublishLocal(moduleID: ModuleID, dependencies: Iterable[ModuleID], artifactFiles: Iterable[File], log: IvyLogger)
|
||||
/** Installs the dependencies of the given 'module' from the resolver named 'from' to the resolver named 'to'.*/
|
||||
def install(module: IvySbt#Module, from: String, to: String)
|
||||
{
|
||||
val artifacts = artifactFiles.map(Artifact.defaultArtifact)
|
||||
val (ivy, local) = basicLocalIvy(log)
|
||||
val module = new ivy.Module(ModuleConfiguration(moduleID, dependencies, artifacts))
|
||||
val srcArtifactPatterns = artifactFiles.map(_.getAbsolutePath)
|
||||
publish(module, local.name, srcArtifactPatterns, None, None)
|
||||
module.withModule { (ivy, md, default) =>
|
||||
for(dependency <- md.getDependencies)
|
||||
{
|
||||
module.logger.info("Installing " + dependency)
|
||||
val options = new InstallOptions
|
||||
options.setValidate(module.moduleConfiguration.validate)
|
||||
options.setTransitive(dependency.isTransitive)
|
||||
ivy.install(dependency.getDependencyRevisionId, from, to, options)
|
||||
}
|
||||
def basicRetrieveLocal(moduleID: ModuleID, dependencies: Iterable[ModuleID], to: File, log: IvyLogger)
|
||||
{
|
||||
val (ivy, local) = basicLocalIvy(log)
|
||||
val module = new ivy.Module(ModuleConfiguration(moduleID, dependencies, Nil))
|
||||
val up = new UpdateConfiguration(to, defaultOutputPattern, false, true)
|
||||
update(module, up)
|
||||
}
|
||||
def defaultOutputPattern = "[artifact]-[revision](-[classifier]).[ext]"
|
||||
private def basicLocalIvy(log: IvyLogger) =
|
||||
{
|
||||
val local = Resolver.defaultLocal
|
||||
val paths = new IvyPaths(new File("."), None)
|
||||
val conf = new IvyConfiguration(paths, Seq(local), log)
|
||||
(new IvySbt(conf), local)
|
||||
}
|
||||
|
||||
/** Clears the Ivy cache, as configured by 'config'. */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
package xsbt
|
||||
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
|
||||
import org.apache.ivy.{core, plugins, util}
|
||||
import core.cache.{ArtifactOrigin, CacheDownloadOptions, DefaultRepositoryCacheManager}
|
||||
import core.module.descriptor.{Artifact => IvyArtifact, DefaultArtifact}
|
||||
import plugins.repository.file.{FileRepository=>IvyFileRepository, FileResource}
|
||||
import plugins.repository.{ArtifactResourceResolver, Resource, ResourceDownloader}
|
||||
import plugins.resolver.util.ResolvedResource
|
||||
import util.FileUtil
|
||||
|
||||
class NotInCache(val id: ModuleID, cause: Throwable)
|
||||
extends RuntimeException(NotInCache(id, cause), cause)
|
||||
{
|
||||
def this(id: ModuleID) = this(id, null)
|
||||
}
|
||||
private object NotInCache
|
||||
{
|
||||
def apply(id: ModuleID, cause: Throwable) =
|
||||
{
|
||||
val postfix = if(cause == null) "" else (": " +cause.toString)
|
||||
"File for " + id + " not in cache" + postfix
|
||||
}
|
||||
}
|
||||
/** Provides methods for working at the level of a single jar file with the default Ivy cache.*/
|
||||
object IvyCache
|
||||
{
|
||||
/** 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)
|
||||
{
|
||||
val artifact = defaultArtifact(moduleID)
|
||||
val resolved = new ResolvedResource(new FileResource(new IvyFileRepository, file), moduleID.revision)
|
||||
withDefaultCache(log) { cache =>
|
||||
val resolver = new ArtifactResourceResolver { def resolve(artifact: IvyArtifact) = resolved }
|
||||
cache.download(artifact, resolver, new FileDownloader, new CacheDownloadOptions)
|
||||
}
|
||||
}
|
||||
/** Clears the cache of the jar for the given ID.*/
|
||||
def clearCachedJar(id: ModuleID, log: IvyLogger)
|
||||
{
|
||||
try { getCachedFile(id, log).delete }
|
||||
catch { case e: Exception => log.debug("Error cleaning cached jar: " + e.toString) }
|
||||
}
|
||||
/** Copies the cached jar for the given ID to the directory 'toDirectory'. If the jar is not in the cache, NotInCache is thrown.*/
|
||||
def retrieveCachedJar(id: ModuleID, toDirectory: File, log: IvyLogger) =
|
||||
{
|
||||
val cachedFile = getCachedFile(id, log)
|
||||
val copyTo = new File(toDirectory, cachedFile.getName)
|
||||
FileUtil.copy(cachedFile, copyTo, null)
|
||||
copyTo
|
||||
}
|
||||
/** Get the location of the cached jar for the given ID in the Ivy cache. If the jar is not in the cache, NotInCache is thrown.*/
|
||||
def getCachedFile(id: ModuleID, log: IvyLogger): File =
|
||||
{
|
||||
val cachedFile =
|
||||
try
|
||||
{
|
||||
withDefaultCache(log) { cache =>
|
||||
val artifact = defaultArtifact(id)
|
||||
cache.getArchiveFileInCache(artifact, unknownOrigin(artifact))
|
||||
}
|
||||
}
|
||||
catch { case e: Exception => throw new NotInCache(id, e) }
|
||||
|
||||
if(cachedFile.exists) cachedFile else throw new NotInCache(id)
|
||||
}
|
||||
/** Calls the given function with the default Ivy cache.*/
|
||||
def withDefaultCache[T](log: IvyLogger)(f: DefaultRepositoryCacheManager => T): T =
|
||||
{
|
||||
val (ivy, local) = basicLocalIvy(log)
|
||||
ivy.withIvy { ivy =>
|
||||
val cache = ivy.getSettings.getDefaultRepositoryCacheManager.asInstanceOf[DefaultRepositoryCacheManager]
|
||||
cache.setUseOrigin(false)
|
||||
f(cache)
|
||||
}
|
||||
}
|
||||
private def unknownOrigin(artifact: IvyArtifact) = ArtifactOrigin.unkwnown(artifact)
|
||||
/** A minimal Ivy setup with only a local resolver and the current directory as the base directory.*/
|
||||
private def basicLocalIvy(log: IvyLogger) =
|
||||
{
|
||||
val local = Resolver.defaultLocal
|
||||
val paths = new IvyPaths(new File("."), None)
|
||||
val conf = new IvyConfiguration(paths, Seq(local), log)
|
||||
(new IvySbt(conf), local)
|
||||
}
|
||||
/** Creates a default jar artifact based on the given ID.*/
|
||||
private def defaultArtifact(moduleID: ModuleID): IvyArtifact =
|
||||
new DefaultArtifact(IvySbt.toID(moduleID), null, moduleID.name, "jar", "jar")
|
||||
}
|
||||
/** Required by Ivy for copying to the cache.*/
|
||||
private class FileDownloader extends ResourceDownloader with NotNull
|
||||
{
|
||||
def download(artifact: IvyArtifact, resource: Resource, dest: File)
|
||||
{
|
||||
if(dest.exists()) dest.delete()
|
||||
val part = new File(dest.getAbsolutePath + ".part")
|
||||
FileUtil.copy(resource.openStream, part, null)
|
||||
if(!part.renameTo(dest))
|
||||
error("Could not move temporary file " + part + " to final location " + dest)
|
||||
}
|
||||
}
|
||||
|
|
@ -311,8 +311,8 @@ object Artifact
|
|||
def apply(name: String, url: URL): Artifact =Artifact(name, extract(url, defaultType), extract(url, defaultExtension), None, Nil, Some(url))
|
||||
val defaultExtension = "jar"
|
||||
val defaultType = "jar"
|
||||
private[this] def extract(url: URL, default: String): String = extract(url.toString, default)
|
||||
private[this] def extract(name: String, default: String): String =
|
||||
def extract(url: URL, default: String): String = extract(url.toString, default)
|
||||
def extract(name: String, default: String): String =
|
||||
{
|
||||
val i = name.lastIndexOf('.')
|
||||
if(i >= 0)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
package xsbt
|
||||
|
||||
import java.io.File
|
||||
import org.specs._
|
||||
import FileUtilities.{createDirectory, delete, touch, withTemporaryDirectory}
|
||||
import org.apache.ivy.util.ChecksumHelper
|
||||
|
||||
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] }
|
||||
}
|
||||
"throw an exception if 'file' is called for an empty component" in {
|
||||
withManager { manager =>
|
||||
createDirectory(manager.location(TestID))
|
||||
( manager.file(TestID) ) must throwA[InvalidComponent]
|
||||
}
|
||||
}
|
||||
"return the file for a single-file component" in {
|
||||
withManager { manager =>
|
||||
createFiles(manager, TestID, "a") match { case Seq(x) =>
|
||||
manager.file(TestID).getAbsoluteFile must beEqualTo(x.getAbsoluteFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"throw an exception if 'file' is called for multi-file component" in {
|
||||
withManager { manager =>
|
||||
createFiles(manager, TestID, "a", "b")
|
||||
( manager.file(TestID) ) must throwA[InvalidComponent]
|
||||
}
|
||||
}
|
||||
"return the files for a multi-file component" in {
|
||||
withManager { manager =>
|
||||
val files = createFiles(manager, TestID, "a", "b")
|
||||
manager.files(TestID) must haveTheSameElementsAs(files)
|
||||
}
|
||||
}
|
||||
"return the files for a single-file component" in {
|
||||
withManager { manager =>
|
||||
val files = createFiles(manager, TestID, "a")
|
||||
manager.files(TestID) must haveTheSameElementsAs(files)
|
||||
}
|
||||
}
|
||||
"throw an exception if 'files' is called for a non-existing component" in {
|
||||
withManager { _.files(TestID) must throwA[InvalidComponent] }
|
||||
}
|
||||
|
||||
"properly cache a file and then retrieve it to an unresolved component" in {
|
||||
withManager { manager =>
|
||||
val file = createFile(manager, TestID, "a")
|
||||
val hash = checksum(file)
|
||||
try
|
||||
{
|
||||
manager.cache(TestID)
|
||||
delete(manager.location(TestID))
|
||||
FileUtilities.listFiles(manager.location(TestID)).toList must haveSize(0)
|
||||
checksum(manager.file(TestID)) must beEqualTo(hash)
|
||||
}
|
||||
finally { manager.clearCache(TestID) }
|
||||
}
|
||||
}
|
||||
|
||||
"not retrieve to a component already resolved" in {
|
||||
withManager { manager =>
|
||||
val file = createFile(manager, TestID, "a")
|
||||
try
|
||||
{
|
||||
manager.cache(TestID)
|
||||
val idDirectory = manager.location(TestID)
|
||||
delete(idDirectory)
|
||||
createDirectory(idDirectory)
|
||||
manager.file(TestID) must throwA[InvalidComponent]
|
||||
}
|
||||
finally { manager.clearCache(TestID) }
|
||||
}
|
||||
}
|
||||
}
|
||||
private def checksum(file: File) = ChecksumHelper.computeAsString(file, "sha1")
|
||||
private def createFile(manager: ComponentManager, id: String, name: String): File = createFiles(manager, id, name).toList.head
|
||||
private def createFiles(manager: ComponentManager, id: String, names: String*): Seq[File] =
|
||||
{
|
||||
val dir = manager.location(id)
|
||||
createDirectory(dir)
|
||||
names.map { name =>
|
||||
val testFile = new File(dir, name)
|
||||
touch(testFile)
|
||||
testFile
|
||||
}
|
||||
}
|
||||
private def withManager[T](f: ComponentManager => T): T =
|
||||
TestIvyLogger( logger => withTemporaryDirectory { temp => f(new ComponentManager(temp, logger)) } )
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package xsbt
|
||||
|
||||
import xsbti.TestLogger
|
||||
|
||||
class TestIvyLogger extends TestLogger with IvyLogger
|
||||
object TestIvyLogger
|
||||
{
|
||||
def apply[T](f: TestIvyLogger => T): T = TestLogger(new TestIvyLogger)(f)
|
||||
}
|
||||
Loading…
Reference in New Issue