Turned sbt launcher into a general Scala application launcher as described in launch.specification

This commit is contained in:
Mark Harrah 2009-09-27 14:39:26 -04:00
parent b6b7fe2f9b
commit abd06a17c5
9 changed files with 108 additions and 113 deletions

View File

@ -1,7 +1,6 @@
package xsbt
import java.io.File
import xsbti.Versions
/** 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.
@ -12,30 +11,13 @@ import xsbti.Versions
* 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
class ComponentManager(provider: xsbti.ComponentProvider, 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)
if(!dir.exists)
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))
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 + "'")
}
/** Get the file for component 'id', throwing an exception if no files or multiple files exist for the component. */
@ -47,12 +29,13 @@ class ComponentManager(baseDirectory: File, log: IvyLogger) extends NotNull
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)
/** Retrieve the file for component 'id' from the local repository. */
def update(id: String): Unit =
try { IvyCache.retrieveCachedJar(sbtModuleID(id), location(id), log) }
try { IvyCache.withCachedJar(sbtModuleID(id), log)(jar => define(id, Seq(jar)) ) }
catch { case e: NotInCache => invalid(e) }
def sbtModuleID(id: String) = ModuleID("org.scala-tools.sbt", id, Versions.Sbt)
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)

View File

@ -91,7 +91,7 @@ private object ConvertResolver
setKeyFilePassword(password)
}
}
private def initializePatterns(resolver: AbstractPatternsBasedResolver, patterns: RepositoryHelpers.Patterns)
private def initializePatterns(resolver: AbstractPatternsBasedResolver, patterns: Patterns)
{
resolver.setM2compatible(patterns.isMavenCompatible)
patterns.ivyPatterns.foreach(resolver.addIvyPattern)

View File

@ -17,8 +17,10 @@ import plugins.repository.url.URLResource
private[xsbt] object CustomXmlParser extends XmlModuleDescriptorParser with NotNull
{
import XmlModuleDescriptorParser.Parser
class CustomParser(settings: IvySettings) extends Parser(CustomXmlParser, settings) with NotNull
class CustomParser(settings: IvySettings, defaultConfig: Option[String]) extends Parser(CustomXmlParser, settings) with NotNull
{
defaultConfig.foreach(x => setDefaultConfMapping("*->default(compile)"))
def setSource(url: URL) =
{
super.setResource(new URLResource(url))
@ -29,7 +31,6 @@ private[xsbt] object CustomXmlParser extends XmlModuleDescriptorParser with NotN
override def setResource(res: Resource) {}
override def setMd(md: DefaultModuleDescriptor) = super.setMd(md)
override def parseDepsConfs(confs: String, dd: DefaultDependencyDescriptor) = super.parseDepsConfs(confs, dd)
override def getDefaultConf = super.getDefaultConf
override def setDefaultConf(conf: String) = super.setDefaultConf(conf)
override def getDefaultConf = defaultConfig.getOrElse(super.getDefaultConf)
}
}

View File

@ -8,6 +8,7 @@ import Artifact.{defaultExtension, defaultType}
import java.io.File
import org.apache.ivy.{core, plugins, util, Ivy}
import core.IvyPatternHelper
import core.cache.DefaultRepositoryCacheManager
import core.module.descriptor.{DefaultArtifact, DefaultDependencyArtifactDescriptor, MDArtifact}
import core.module.descriptor.{DefaultDependencyDescriptor, DefaultModuleDescriptor, ModuleDescriptor}
@ -74,7 +75,7 @@ final class IvySbt(configuration: IvyConfiguration)
finally { ivy.popContext() }
}
final class Module(val moduleConfiguration: ModuleConfiguration) extends NotNull
final class Module(val moduleSettings: ModuleSettings) extends NotNull
{
def logger = configuration.log
def withModule[T](f: (Ivy,DefaultModuleDescriptor,String) => T): T =
@ -83,14 +84,14 @@ final class IvySbt(configuration: IvyConfiguration)
private lazy val (moduleDescriptor: DefaultModuleDescriptor, defaultConfig: String) =
{
val (baseModule, baseConfiguration) =
moduleConfiguration match
moduleSettings match
{
case ic: InlineConfiguration => configureInline(ic)
case ec: EmptyConfiguration => configureEmpty(ec.module)
case pc: PomConfiguration => readPom(pc.file, pc.validate)
case ifc: IvyFileConfiguration => readIvyFile(ifc.file, ifc.validate)
}
moduleConfiguration.ivyScala.foreach(IvyScala.checkModule(baseModule, baseConfiguration))
moduleSettings.ivyScala.foreach(IvyScala.checkModule(baseModule, baseConfiguration))
baseModule.getExtraAttributesNamespaces.asInstanceOf[java.util.Map[String,String]].put("e", "http://ant.apache.org/ivy/extra")
(baseModule, baseConfiguration)
}
@ -105,6 +106,7 @@ final class IvySbt(configuration: IvyConfiguration)
IvySbt.addArtifacts(moduleID, artifacts)
IvySbt.addDependencies(moduleID, dependencies, parser)
IvySbt.setModuleConfigurations(settings, moduleConfigurations)
IvySbt.addMainArtifact(moduleID)
(moduleID, parser.getDefaultConf)
}
@ -126,7 +128,7 @@ final class IvySbt(configuration: IvyConfiguration)
private def readIvyFile(ivyFile: File, validate: Boolean) =
{
val url = toURL(ivyFile)
val parser = new CustomXmlParser.CustomParser(settings)
val parser = new CustomXmlParser.CustomParser(settings, None)
parser.setValidate(validate)
parser.setSource(url)
parser.parse()
@ -168,6 +170,20 @@ private object IvySbt
settings.setDefaultResolver(newDefault.getName)
log.debug("Using repositories:\n" + resolvers.mkString("\n\t"))
}
private def setModuleConfigurations(settings: IvySettings, moduleConfigurations: Seq[ModuleConfiguration])
{
val existing = settings.getResolverNames
for(moduleConf <- moduleConfigurations)
{
import moduleConf._
import IvyPatternHelper._
import PatternMatcher._
if(!existing.contains(resolver.name))
settings.addResolver(ConvertResolver(resolver))
val attributes = javaMap(Map(MODULE_KEY -> name, ORGANISATION_KEY -> organization, REVISION_KEY -> revision))
settings.addModuleConfiguration(attributes, settings.getMatcher(EXACT_OR_REGEXP), resolver.name, null, null, null)
}
}
private def configureCache(settings: IvySettings, dir: Option[File])
{
val cacheDir = dir.getOrElse(settings.getDefaultRepositoryCacheBasedir())
@ -244,9 +260,8 @@ private object IvySbt
/** Parses the given in-memory Ivy file 'xml', using the existing 'moduleID' and specifying the given 'defaultConfiguration'. */
private def parseIvyXML(settings: IvySettings, xml: String, moduleID: DefaultModuleDescriptor, defaultConfiguration: String, validate: Boolean): CustomXmlParser.CustomParser =
{
val parser = new CustomXmlParser.CustomParser(settings)
val parser = new CustomXmlParser.CustomParser(settings, Some(defaultConfiguration))
parser.setMd(moduleID)
parser.setDefaultConf(defaultConfiguration)
parser.setValidate(validate)
parser.setInput(xml.getBytes)
parser.parse()

View File

@ -27,7 +27,7 @@ object IvyActions
{
module.logger.info("Installing " + dependency)
val options = new InstallOptions
options.setValidate(module.moduleConfiguration.validate)
options.setValidate(module.moduleSettings.validate)
options.setTransitive(dependency.isTransitive)
ivy.install(dependency.getDependencyRevisionId, from, to, options)
}
@ -50,10 +50,8 @@ object IvyActions
// todo: correct default configuration for extra dependencies
private def addLateDependencies(ivy: Ivy, module: DefaultModuleDescriptor, defaultConfiguration: String, extraDependencies: Iterable[ModuleID])
{
val parser = new CustomXmlParser.CustomParser(ivy.getSettings)
val parser = new CustomXmlParser.CustomParser(ivy.getSettings, Some(defaultConfiguration))
parser.setMd(module)
val defaultConf = if(defaultConfiguration.contains("->")) defaultConfiguration else (defaultConfiguration + "->default(compile)")
parser.setDefaultConf(defaultConf)
IvySbt.addDependencies(module, extraDependencies, parser)
}
private def getConfigurations(module: ModuleDescriptor, configurations: Option[Iterable[Configuration]]) =

View File

@ -40,19 +40,20 @@ object IvyCache
/** Clears the cache of the jar for the given ID.*/
def clearCachedJar(id: ModuleID, log: IvyLogger)
{
try { getCachedFile(id, log).delete }
try { withCachedJar(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 =
withCachedJar(id, log) { cachedFile =>
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
* TODO: locking.*/
def withCachedJar[T](id: ModuleID, log: IvyLogger)(f: File => T): T =
{
val cachedFile =
try
@ -64,7 +65,7 @@ object IvyCache
}
catch { case e: Exception => throw new NotInCache(id, e) }
if(cachedFile.exists) cachedFile else throw new NotInCache(id)
if(cachedFile.exists) f(cachedFile) else throw new NotInCache(id)
}
/** Calls the given function with the default Ivy cache.*/
def withDefaultCache[T](log: IvyLogger)(f: DefaultRepositoryCacheManager => T): T =
@ -82,7 +83,7 @@ object IvyCache
{
val local = Resolver.defaultLocal
val paths = new IvyPaths(new File("."), None)
val conf = new IvyConfiguration(paths, Seq(local), log)
val conf = new IvyConfiguration(paths, Seq(local), Nil, log)
(new IvySbt(conf), local)
}
/** Creates a default jar artifact based on the given ID.*/

View File

@ -7,19 +7,20 @@ import java.io.File
import scala.xml.NodeSeq
final class IvyPaths(val baseDirectory: File, val cacheDirectory: Option[File]) extends NotNull
final class IvyConfiguration(val paths: IvyPaths, val resolvers: Seq[Resolver], val log: IvyLogger) extends NotNull
final class IvyConfiguration(val paths: IvyPaths, val resolvers: Seq[Resolver],
val moduleConfigurations: Seq[ModuleConfiguration], val log: IvyLogger) extends NotNull
sealed trait ModuleConfiguration extends NotNull
sealed trait ModuleSettings extends NotNull
{
def validate: Boolean
def ivyScala: Option[IvyScala]
}
final class IvyFileConfiguration(val file: File, val ivyScala: Option[IvyScala], val validate: Boolean) extends ModuleConfiguration
final class PomConfiguration(val file: File, val ivyScala: Option[IvyScala], val validate: Boolean) extends ModuleConfiguration
final class IvyFileConfiguration(val file: File, val ivyScala: Option[IvyScala], val validate: Boolean) extends ModuleSettings
final class PomConfiguration(val file: File, val ivyScala: Option[IvyScala], val validate: Boolean) extends ModuleSettings
final class InlineConfiguration(val module: ModuleID, val dependencies: Iterable[ModuleID], val ivyXML: NodeSeq,
val configurations: Iterable[Configuration], val defaultConfiguration: Option[Configuration], val ivyScala: Option[IvyScala],
val artifacts: Iterable[Artifact], val validate: Boolean) extends ModuleConfiguration
final class EmptyConfiguration(val module: ModuleID, val ivyScala: Option[IvyScala], val validate: Boolean) extends ModuleConfiguration
val artifacts: Iterable[Artifact], val validate: Boolean) extends ModuleSettings
final class EmptyConfiguration(val module: ModuleID, val ivyScala: Option[IvyScala], val validate: Boolean) extends ModuleSettings
object InlineConfiguration
{
def apply(module: ModuleID, dependencies: Iterable[ModuleID], artifacts: Iterable[Artifact]) =
@ -37,7 +38,7 @@ object InlineConfiguration
else
explicitConfigurations
}
object ModuleConfiguration
object ModuleSettings
{
def apply(ivyScala: Option[IvyScala], validate: Boolean, module: => ModuleID)(baseDirectory: File, log: IvyLogger) =
{

View File

@ -19,7 +19,7 @@ final case class ModuleID(organization: String, name: String, revision: String,
def from(url: String) = artifacts(Artifact(name, new URL(url)))
def classifier(c: String) = artifacts(Artifact(name, c))
def artifacts(newArtifacts: Artifact*) = ModuleID(organization, name, revision, configurations, isChanging, isTransitive, newArtifacts ++ explicitArtifacts, extraAttributes)
def extra(attributes: (String,String)*) = ModuleID(organization, name, revision, configurations, isChanging, isTransitive, explicitArtifacts, extraAttributes ++ attributes)
def extra(attributes: (String,String)*) = ModuleID(organization, name, revision, configurations, isChanging, isTransitive, explicitArtifacts, extraAttributes ++ ModuleID.checkE(attributes))
}
object ModuleID
{
@ -30,6 +30,10 @@ object ModuleID
ModuleID(organization, name, revision, configurations, isChanging, isTransitive, Nil)
def apply(organization: String, name: String, revision: String, configurations: Option[String], isChanging: Boolean, isTransitive: Boolean, explicitArtifacts: Seq[Artifact]): ModuleID =
ModuleID(organization, name, revision, configurations, isChanging, isTransitive, explicitArtifacts, Map.empty)
def checkE(attributes: Seq[(String, String)]) =
for ( (key, value) <- attributes) yield
if(key.startsWith("e:")) (key, value) else ("e:" + key, value)
}
sealed trait Resolver extends NotNull
{
@ -40,14 +44,20 @@ sealed case class MavenRepository(name: String, root: String) extends Resolver
override def toString = name + ": " + root
}
final class Patterns(val ivyPatterns: Seq[String], val artifactPatterns: Seq[String], val isMavenCompatible: Boolean) extends NotNull
{
private[xsbt] def mavenStyle(): Patterns = Patterns(ivyPatterns, artifactPatterns, true)
private[xsbt] def withIvys(patterns: Seq[String]): Patterns = Patterns(patterns ++ ivyPatterns, artifactPatterns, isMavenCompatible)
private[xsbt] def withArtifacts(patterns: Seq[String]): Patterns = Patterns(ivyPatterns, patterns ++ artifactPatterns, isMavenCompatible)
}
object Patterns
{
def apply(artifactPatterns: String*): Patterns = Patterns(true, artifactPatterns : _*)
def apply(isMavenCompatible: Boolean, artifactPatterns: String*): Patterns = Patterns(Nil, artifactPatterns, isMavenCompatible)
def apply(ivyPatterns: Seq[String], artifactPatterns: Seq[String], isMavenCompatible: Boolean): Patterns = new Patterns(ivyPatterns, artifactPatterns, isMavenCompatible)
}
object RepositoryHelpers
{
final case class Patterns(ivyPatterns: Seq[String], artifactPatterns: Seq[String], isMavenCompatible: Boolean) extends NotNull
{
private[xsbt] def mavenStyle(): Patterns = Patterns(ivyPatterns, artifactPatterns, true)
private[xsbt] def withIvys(patterns: Seq[String]): Patterns = Patterns(patterns ++ ivyPatterns, artifactPatterns, isMavenCompatible)
private[xsbt] def withArtifacts(patterns: Seq[String]): Patterns = Patterns(ivyPatterns, patterns ++ artifactPatterns, isMavenCompatible)
}
final case class SshConnection(authentication: Option[SshAuthentication], hostname: Option[String], port: Option[Int]) extends NotNull
{
def copy(authentication: Option[SshAuthentication]) = SshConnection(authentication, hostname, port)
@ -63,7 +73,7 @@ object RepositoryHelpers
final case class PasswordAuthentication(user: String, password: String) extends SshAuthentication
final case class KeyFileAuthentication(keyfile: File, password: String) extends SshAuthentication
}
import RepositoryHelpers.{Patterns, SshConnection, FileConfiguration}
import RepositoryHelpers.{SshConnection, FileConfiguration}
import RepositoryHelpers.{KeyFileAuthentication, PasswordAuthentication, SshAuthentication}
/** sbt interface to an Ivy repository based on patterns, which is most Ivy repositories.*/
@ -283,12 +293,7 @@ object Configurations
private[xsbt] val DefaultMavenConfiguration = defaultConfiguration(true)
private[xsbt] val DefaultIvyConfiguration = defaultConfiguration(false)
private[xsbt] def DefaultConfiguration(mavenStyle: Boolean) = if(mavenStyle) DefaultMavenConfiguration else DefaultIvyConfiguration
private[xsbt] def defaultConfiguration(mavenStyle: Boolean) =
{
val base = if(mavenStyle) Configurations.Compile else Configurations.Default
config(base.name + "->default(compile)")
}
private[xsbt] def defaultConfiguration(mavenStyle: Boolean) = if(mavenStyle) Configurations.Compile else Configurations.Default
private[xsbt] def removeDuplicates(configs: Iterable[Configuration]) = Set(scala.collection.mutable.Map(configs.map(config => (config.name, config)).toSeq: _*).values.toList: _*)
}
/** Represents an Ivy configuration. */
@ -337,6 +342,12 @@ object Artifact
Artifact(name, extract(name, defaultType), extract(name, defaultExtension), None, Nil, Some(file.toURI.toURL))
}
}
final case class ModuleConfiguration(organization: String, name: String, revision: String, resolver: Resolver) extends NotNull
object ModuleConfiguration
{
def apply(org: String, resolver: Resolver): ModuleConfiguration = apply(org, "*", "*", resolver)
def apply(org: String, name: String, resolver: Resolver): ModuleConfiguration = ModuleConfiguration(org, name, "*", resolver)
}
/*
object Credentials
{

View File

@ -14,34 +14,33 @@ object ComponentManagerTest extends Specification
}
"throw an exception if 'file' is called for an empty component" in {
withManager { manager =>
createDirectory(manager.location(TestID))
manager.define(TestID, Nil)
( 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)
}
val hash = defineFile(manager, TestID, "a")
checksum(manager.file(TestID)) must beEqualTo(hash)
}
}
"throw an exception if 'file' is called for multi-file component" in {
withManager { manager =>
createFiles(manager, TestID, "a", "b")
defineFiles(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)
val hashes = defineFiles(manager, TestID, "a", "b")
checksum(manager.files(TestID)) must haveTheSameElementsAs(hashes)
}
}
"return the files for a single-file component" in {
withManager { manager =>
val files = createFiles(manager, TestID, "a")
manager.files(TestID) must haveTheSameElementsAs(files)
val hashes = defineFiles(manager, TestID, "a")
checksum(manager.files(TestID)) must haveTheSameElementsAs(hashes)
}
}
"throw an exception if 'files' is called for a non-existing component" in {
@ -49,47 +48,33 @@ object ComponentManagerTest extends Specification
}
"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)
withManager { definingManager =>
val hash = defineFile(definingManager, TestID, "a")
try
{
manager.cache(TestID)
delete(manager.location(TestID))
FileUtilities.listFiles(manager.location(TestID)).toList must haveSize(0)
checksum(manager.file(TestID)) must beEqualTo(hash)
definingManager.cache(TestID)
withManager { usingManager =>
checksum(usingManager.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) }
finally { definingManager.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 checksum(files: Iterable[File]): Seq[String] = files.map(checksum).toSeq
private def checksum(file: File): String = if(file.exists) ChecksumHelper.computeAsString(file, "sha1") else ""
private def defineFile(manager: ComponentManager, id: String, name: String): String = createFile(manager, id, name)(checksum)
private def defineFiles(manager: ComponentManager, id: String, names: String*): Seq[String] = createFiles(manager, id, names : _*)(checksum)
private def createFile[T](manager: ComponentManager, id: String, name: String)(f: File => T): T = createFiles(manager, id, name)(files => f(files.toList.head))
private def createFiles[T](manager: ComponentManager, id: String, names: String*)(f: Seq[File] => T): T =
withTemporaryDirectory { dir =>
val files = names.map(name => new File(dir, name) )
files.foreach(writeRandomContent)
manager.define(id, files)
f(files)
}
}
private def writeRandomContent(file: File) = FileUtilities.write(file, randomString)
private def randomString = "asdf"
private def withManager[T](f: ComponentManager => T): T =
TestIvyLogger( logger => withTemporaryDirectory { temp => f(new ComponentManager(temp, logger)) } )
TestIvyLogger( logger => withTemporaryDirectory { temp => f(new ComponentManager(new xsbt.boot.ComponentProvider(temp), logger)) } )
}